PageSourceSearch

https://reputationsystems.ai/chat/chatHome.js

js reputationsystems.ai collected 2026-10-05 12:31:23 UTC 7,565 bytes, 134 lines download raw bytes

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.