/** * Vanna chat embed bootstrap — fonte única do JS de wiring exigido pelo * : theme pierce (adoptedStyleSheets), tradutor PT-BR * (MutationObserver), markdown processor pros balões, fonts, e load do * bundle. * * Servido pelo Vanna server em /vanna-embed-bootstrap.js. Consumido por: * - static/embed-demo.html (smoke-test page, mesmo origin) * - clubpetro-frontend/.../vannaChatLoader.ts (React app, cross-origin) * * API pública: * window.VannaEmbed.ensureLoaded({ baseUrl, extraCss? }) -> Promise * - baseUrl: "" pra mesma origem (demo); URL completa do Vanna server * quando embedado em outra origem (React app). * - extraCss: CSS opcional appended ao theme sheet (ex.: override do * avatar logo quando o SVG mora na origem da app, não na do Vanna). * - Idempotente: chamadas subsequentes retornam a mesma Promise. */ (function () { if (window.VannaEmbed) return; let loaderPromise = null; const escapeHtml = (s) => s.replace(/&/g, "&").replace(//g, ">"); const mdToHtml = (md) => { if (!md) return ""; let s = escapeHtml(md); s = s.replace(/`([^`]+)`/g, "$1"); s = s.replace(/\*\*\*([^*]+)\*\*\*/g, "$1"); s = s.replace(/\*\*([^*]+)\*\*/g, "$1"); s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1$2"); s = s.replace(/(^|[^\w])_([^_\n]+)_(?!\w)/g, "$1$2"); s = s.replace( /\[([^\]]+)\]\(([^)]+)\)/g, '$1' ); // Headers ANTES das listas. A regex de lista consome o \n final do // último item; rodar headers depois deixaria `### Conclusão` logo // após um bloco de lista precedido só de `` (sem \n) e a regex // de header (que exige ^ ou \n antes do #) falharia. s = s.replace( /(^|\n)(#{1,6})\s+([^\n]+)/g, (_, lead, hashes, text) => `${lead}${text.trim()}` ); s = s.replace(/(?:^|\n)((?:[-*]\s+.+(?:\n|$))+)/g, (_, block) => { const items = block .trim() .split(/\n/) .map((l) => l.replace(/^[-*]\s+/, "").trim()) .map((t) => `
  • ${t}
  • `) .join(""); return `\n\n`; }); s = s.replace(/\n/g, "
    "); s = s.replace(/
    \s*(<\/?(?:ul|li|h[1-6])>)/g, "$1"); s = s.replace(/(<\/?(?:ul|li|h[1-6])>)\s*
    /g, "$1"); return s; }; const EXACT = { "Search...": "Buscar...", "Export": "Baixar", "📥 Export": "📥 Baixar", "Export to CSV": "Baixar para CSV", }; const REGEX = [ [/^(\d+)\s+rows?$/i, (_, n) => `${n} ${n === "1" ? "linha" : "linhas"}`], [/^(\d+)\s+columns?$/i, (_, n) => `${n} ${n === "1" ? "coluna" : "colunas"}`], [ /^Showing\s+(\d+)\s+of\s+(\d+)\s+rows?$/i, (_, a, b) => `Mostrando ${a} de ${b} ${b === "1" ? "linha" : "linhas"}`, ], ]; const translate = (s) => { if (typeof s !== "string") return s; const t = s.trim(); if (!t) return s; if (EXACT[t]) return s.replace(t, EXACT[t]); for (const [re, fn] of REGEX) { const m = t.match(re); if (m) return s.replace(t, fn(...m)); } return s; }; const setupThemePierce = (baseUrl, extraCss) => { if (window.__vannaThemeSheet) return; const themeSheet = new CSSStyleSheet(); window.__vannaThemeSheet = themeSheet; const desc = Object.getOwnPropertyDescriptor( ShadowRoot.prototype, "adoptedStyleSheets" ); if (desc && desc.set) { Object.defineProperty(ShadowRoot.prototype, "adoptedStyleSheets", { configurable: true, enumerable: true, get() { return desc.get.call(this); }, set(value) { const without = (value || []).filter((s) => s !== themeSheet); desc.set.call(this, [...without, themeSheet]); }, }); } const merge = (css) => (extraCss ? css + "\n" + extraCss : css); fetch(baseUrl + "/vanna-theme.css?t=" + Date.now()) .then((r) => r.text()) .then((css) => themeSheet.replaceSync(merge(css))) .catch(() => { if (extraCss) themeSheet.replaceSync(extraCss); }); }; const setupTranslator = () => { if (window.__vannaTranslatorArmed) return; window.__vannaTranslatorArmed = true; const isInsideMessageContent = (n) => !!n && (n.classList?.contains("message-content") || n.closest?.(".message-content")); const processMessageContent = (el) => { const text = el.textContent || ""; if (el.dataset.bare === text) return; el.innerHTML = mdToHtml(text); el.dataset.bare = el.textContent || ""; }; const walkAndTranslate = (root) => { const tw = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); let node; while ((node = tw.nextNode())) { const next = translate(node.nodeValue); if (next !== node.nodeValue) node.nodeValue = next; } root.querySelectorAll?.("[placeholder], [title]")?.forEach((el) => { for (const attr of ["placeholder", "title"]) { const v = el.getAttribute(attr); if (v == null) continue; const next = translate(v); if (next !== v) el.setAttribute(attr, next); } }); root .querySelectorAll?.(".message-content") ?.forEach(processMessageContent); }; const observers = new WeakSet(); const attachObserver = (root) => { if (observers.has(root)) return; observers.add(root); walkAndTranslate(root); new MutationObserver((muts) => { for (const m of muts) { const target = m.target; const inMd = isInsideMessageContent(target); if (m.type === "characterData") { if (inMd) { const host = target.parentElement?.closest?.(".message-content"); if (host) processMessageContent(host); } else { const next = translate(target.nodeValue); if (next !== target.nodeValue) target.nodeValue = next; } } else if (m.type === "attributes") { const v = target.getAttribute(m.attributeName); const next = translate(v); if (next !== v) target.setAttribute(m.attributeName, next); } else { m.addedNodes.forEach((n) => { if (n.nodeType === 3) { if (inMd) { const host = target.closest?.(".message-content"); if (host) processMessageContent(host); } else { const next = translate(n.nodeValue); if (next !== n.nodeValue) n.nodeValue = next; } } else if (n.nodeType === 1) { walkAndTranslate(n); if (n.shadowRoot) attachObserver(n.shadowRoot); } }); } } }).observe(root, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["placeholder", "title"], }); }; const origAttach = Element.prototype.attachShadow; Element.prototype.attachShadow = function (init) { const root = origAttach.call(this, init); queueMicrotask(() => attachObserver(root)); return root; }; }; // Wrap window.ResizeObserver pra capturar errors NA ORIGEM. Plotly tem // um RO interno (vanna-components.js:113878) que dispara `_t [as relayout]` // (kt at 22888) num node desmontado durante minimize/maximize do chat, // e o crash propaga como `TypeError: Cannot read properties of undefined // (reading 'width' / 'height')`. Listeners em window.error registrados // depois do webpack-dev-server (caso CRA) não conseguem barrar o overlay // porque o handler do dev server roda antes em capture phase. Try/catch // dentro da própria callback garante que o error nunca escapa pra window. // Só engole o erro específico (filename + message bate); qualquer outro // erro continua propagando normal. const wrapResizeObserver = () => { if (window.__vannaResizeObserverWrapped) return; window.__vannaResizeObserverWrapped = true; const Original = window.ResizeObserver; if (!Original) return; const isPlotlyCrash = (err) => { if (!err) return false; const msg = (err && err.message) || String(err); const stack = (err && err.stack) || ""; const widthHeight = msg.indexOf("reading 'width'") !== -1 || msg.indexOf("reading 'height'") !== -1 || msg.indexOf("ResizeObserver loop") !== -1; const fromBundle = stack.indexOf("vanna-components") !== -1; return widthHeight && fromBundle; }; const Wrapped = function (callback) { const safeCallback = function (entries, observer) { try { callback.call(this, entries, observer); } catch (err) { if (!isPlotlyCrash(err)) throw err; } }; return new Original(safeCallback); }; Wrapped.prototype = Original.prototype; window.ResizeObserver = Wrapped; }; const loadFontsOnce = () => { if (window.__vannaFontsLoaded) return; window.__vannaFontsLoaded = true; const links = [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous", }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap", }, ]; for (const cfg of links) { const link = document.createElement("link"); Object.entries(cfg).forEach(([k, v]) => link.setAttribute(k, v)); document.head.appendChild(link); } }; const injectBundle = (baseUrl) => new Promise((resolve, reject) => { const sel = 'script[data-vanna-bundle="' + baseUrl + '"]'; const existing = document.querySelector(sel); if (existing) { if (existing.__loaded) return resolve(); existing.addEventListener("load", () => resolve()); existing.addEventListener("error", () => reject(new Error("vanna bundle failed to load")) ); return; } const script = document.createElement("script"); script.type = "module"; script.src = baseUrl + "/static/vanna-components.js"; script.dataset.vannaBundle = baseUrl; script.onload = () => { script.__loaded = true; resolve(); }; script.onerror = () => reject(new Error("vanna bundle failed to load")); document.head.appendChild(script); }); window.VannaEmbed = { ensureLoaded(config) { if (loaderPromise) return loaderPromise; const baseUrl = (config && config.baseUrl) || ""; const extraCss = config && config.extraCss; loaderPromise = (async () => { wrapResizeObserver(); setupThemePierce(baseUrl, extraCss); setupTranslator(); loadFontsOnce(); await injectBundle(baseUrl); await customElements.whenDefined("vanna-chat"); })(); return loaderPromise; }, }; })();