1import { getChatProfile } from './chatProfiles.js'; 2import { installConversationWelcome } from './chatWelcome.js'; 3 4/** Adapt only accessible SalesIQ frames on this website. Returns full teardown. */ 5export function hideProviderAttribution(inner) { 6 const normalize = value => value?.replace(/\s+/g, ' ').trim(); 7 for (const node of inner.querySelectorAll('a, button, [role="link"]')) { 8 if (normalize(node.textContent) !== 'Driven by Zoho SalesIQ') continue; 9 const target = node.closest?.('.siqcw-drivenby-cont') || node; 10 target.hidden = true; 11 target.setAttribute('aria-hidden', 'true'); 12 target.style.setProperty('display', 'none', 'important'); 13 } 14} 15 16export function repairUnresolvedAnswerLinks(inner, profile) { 17 const productSlugs = ['advertising-systems', 'reputation-systems', 'hotel-systems', 'ota-systems', 'image-systems', 'revenue-systems']; 18 for (const node of inner.querySelectorAll('[data-zsqa="msg"]')) { 19 const text = node.textContent || ''; 20 if (!/LINK_\d+/.test(text)) continue; 21 const slug = productSlugs.find(value => text.includes(getChatProfile(value)?.name)); 22 const answerProfile = slug ? getChatProfile(slug, profile.surface) : profile; 23 const destination = new globalThis.URL(answerProfile.pricingUrl); 24 const page = `${answerProfile.name} Pricing page at ${destination.hostname}${destination.pathname}`; 25 node.textContent = text 26 .replace(/\[([^\]]+)\]\(LINK_\d+\)/g, '$1 at ' + destination.hostname + destination.pathname) 27 .replace(/\[LINK_\d+\]/g, page) 28 .replace(/\(LINK_\d+\)/g, 'at ' + destination.hostname + destination.pathname); 29 } 30} 31 32export function setChatHeader(inner, profile, titles) { 33 const title = inner.querySelector('[data-zsqa="header_title"]'); 34 if (!title) return; 35 if (!titles.has(title)) titles.set(title, title.textContent); 36 if (title.textContent !== profile.title) title.textContent = profile.title; 37} 38 39export function installChatHome(doc, css, profile = getChatProfile()) { 40 if (!profile || typeof profile !== "object") profile = getChatProfile(); 41 const win = doc.defaultView; 42 const frames = new Map(); 43 const cleanups = new Map(); 44 // Carry the site's real loaded font into the frame without overriding icon fonts. 45 let fontFaces = ''; 46 for (const sheet of doc.styleSheets) { 47 try { 48 for (const rule of sheet.cssRules) { 49 if (rule.type === win.CSSRule.FONT_FACE_RULE) fontFaces += rule.cssText.replace(/url\(["']?([^"')]+)["']?\)/g, (_, url) => `url("${new win.URL(url, sheet.href || doc.baseURI).href}")`); 50 } 51 } catch { /* Cross-origin stylesheets remain private to their origin. */ } 52 } 53 const attach = frame => { 54 cleanups.get(frame)?.(); 55 cleanups.delete(frame); 56 let inner; 57 try { inner = frame.contentDocument; } catch { return; } 58 if (!inner?.body) return; 59 const style = inner.createElement('style'); 60 style.id = 'ms-chat-style'; 61 style.textContent = fontFaces + css.replaceAll('/media-kit/multisystems-logo.svg', profile.logo) + `\n:root { --ms-chat-font: ${win.getComputedStyle(doc.body).fontFamily}; }`; 62 inner.head.append(style); 63 const titles = new Map(); 64 const apply = () => { 65 repairUnresolvedAnswerLinks(inner, profile); 66 hideProviderAttribution(inner); 67 setChatHeader(inner, profile, titles); 68 installConversationWelcome(inner, profile.logo, profile); 69 const home = inner.querySelector('.callhome-wrap'); 70 const composer = home?.querySelector('.home-option-wrap [data-zsqa="chat"]'); 71 const close = home?.querySelector('.home-header-wrap [aria-label="Minimize"]'); 72 if (!home || !composer || !close || home.querySelector('#ms-chat-home')) return; 73 const section = inner.createElement('section'); 74 section.id = 'ms-chat-home'; 75 section.setAttribute('aria-label', profile.name + ' help'); 76 section.innerHTML = `<header class="ms-chat-heading"> 77 <img src="/media-kit/multisystems-logo.svg" alt="" width="42" height="42"> 78 <div><h2>Multisystems Assistant</h2><p>Explore our products and find help for your business.</p></div> 79 <button type="button" class="ms-chat-close" aria-label="Close chat"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button> 80 </header> 81 <button type="button" class="ms-chat-ask">Ask a question</button> 82 <p class="ms-chat-caption">A good place to start</p> 83 <details><summary>Which product fits my business?</summary><p>Explore Reputation, Hotel, OTA, Image, Advertising and Revenue Systems.</p><a href="/products" target="_top">Explore our products</a></details> 84 <details><summary>How do I get started?</summary><p>Tell our team about your business and the workflows you want to improve.</p><a href="/demo" target="_top">Book a demo</a></details>`; 85 section.querySelector('img').src = profile.logo; 86 section.querySelector('h2').textContent = profile.title; 87 section.querySelector('.ms-chat-heading p').textContent = profile.description; 88 const details = section.querySelectorAll('details'); 89 details[0].querySelector('summary').textContent = profile.prompts[0]; 90 details[0].querySelector('p').textContent = profile.description; 91 details[0].querySelector('a').textContent = 'Explore ' + profile.name; 92 details[0].querySelector('a').href = profile.productUrl; 93 details[1].querySelector('summary').textContent = profile.surface === 'app' ? 'How can I get support?' : 'How do I get started?'; 94 details[1].querySelector('p').textContent = 'Ask a product question here or contact our team.'; 95 details[1].querySelector('a').textContent = 'Contact the team'; 96 details[1].querySelector('a').href = profile.contactUrl; 97 section.querySelector('.ms-chat-close').addEventListener('click', () => home.querySelector('.home-header-wrap [aria-label="Minimize"]')?.click()); 98 section.querySelector('.ms-chat-ask').addEventListener('click', () => home.querySelector('.home-option-wrap [data-zsqa="chat"]')?.click());
99 home.prepend(section); 100 }; 101 const observer = new win.MutationObserver(apply); 102 observer.observe(inner.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); 103 apply(); 104 cleanups.set(frame, () => { 105 observer.disconnect(); 106 style.remove(); 107 for (const [title, text] of titles) { if (title.textContent === profile.title) title.textContent = text; } 108 inner.querySelectorAll('#ms-chat-home, .ms-conversation-welcome').forEach(node => node.remove()); 109 inner.querySelectorAll('.ms-conversation-host').forEach(node => node.classList.remove('ms-conversation-host')); 110 }); 111 }; 112 const find = () => { 113 for (const [frame, load] of frames) { 114 if (!frame.isConnected) { 115 frame.removeEventListener('load', load); 116 cleanups.get(frame)?.(); cleanups.delete(frame); frames.delete(frame); 117 } 118 } 119 const frame = doc.querySelector('#siq_chatwindow'); 120 if (!frame || frames.has(frame)) return; 121 const load = () => attach(frame); 122 frames.set(frame, load); 123 frame.addEventListener('load', load); 124 attach(frame); 125 }; 126 const observer = new win.MutationObserver(find); 127 observer.observe(doc.body, { childList: true, subtree: true }); 128 find(); 129 return () => { 130 observer.disconnect(); 131 for (const [frame, load] of frames) frame.removeEventListener('load', load); 132 for (const cleanup of cleanups.values()) cleanup(); 133 }; 134}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.