UNPKG

insight-aid

Version:

Insight-aid: lightweight floating help + AI chatbot plugin

268 lines (226 loc) 8.62 kB
window.insightAid = function(config) { const cfg = config; if (!cfg.apiBase || !cfg.appId || !cfg.selectors) return; const helpData = {}; let fetched = false; const iconUrl = cfg.iconUrl || `${basePath}insight-logo.png`; function getHelpId(el) { const id = el.getAttribute("id") || el.getAttribute("name") || el.innerText?.slice(0, 20); return btoa(`${cfg.appId}-${id}`).replace(/=/g, ""); } async function fetchHelpData() { if (fetched) return; try { const res = await fetch(`${cfg.apiBase}?appId=${cfg.appId}`); const json = await res.json(); json.forEach(item => { helpData[item.insight_aid_id] = { text: item.insight_aid_text, metadata: item.metadata || {} }; }); fetched = true; } catch (err) { console.error("HelpAgent fetch error", err); } } async function saveHelp(helpId, text, metadata) { return fetch(cfg.apiBase, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ app_id: cfg.appId, insight_aid_id: helpId, insight_aid_text: text, metadata: metadata }) }); } function showInlineEditor(container, editable, helpId) { const existing = container.querySelector(".help-inline-editor"); if (existing) { existing.remove(); return; } const helpEntry = helpData[helpId] || { text: "", metadata: {} }; const wrapper = document.createElement("div"); wrapper.className = "help-inline-editor"; const msgDiv = document.createElement("div"); msgDiv.className = "help-msg"; if (editable) { const toolbar = document.createElement("div"); toolbar.className = "toolbar"; const editorDiv = document.createElement("div"); editorDiv.contentEditable = true; editorDiv.className = "editor"; editorDiv.innerHTML = helpEntry.text || ""; setTimeout(() => editorDiv.focus(), 100); const makeLink = () => { const sel = window.getSelection(); if (!sel.rangeCount) return; const range = sel.getRangeAt(0); const selectedText = range.toString(); if (!selectedText) return; const a = document.createElement("a"); a.href = "#"; a.textContent = selectedText; range.deleteContents(); range.insertNode(a); }; const buttons = [ { label: "B", cmd: "bold" }, { label: "I", cmd: "italic" }, { label: "U", cmd: "underline" }, { label: "🔗", cmd: "link" } ]; buttons.forEach(btn => { const button = document.createElement("button"); button.textContent = btn.label; button.onclick = () => { if (btn.cmd === "link") { makeLink(); } else { document.execCommand(btn.cmd); } }; toolbar.appendChild(button); }); const btnContainer = document.createElement("div"); btnContainer.className = "btn-container"; const saveBtn = document.createElement("button"); saveBtn.textContent = "Save"; saveBtn.className = "btn-save"; const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancel"; cancelBtn.className = "btn-cancel"; cancelBtn.onclick = () => wrapper.remove(); btnContainer.appendChild(cancelBtn); btnContainer.appendChild(saveBtn); saveBtn.onclick = async () => { const newText = editorDiv.innerHTML.trim(); if (!newText || newText === "<br>" || newText === "&nbsp;") { msgDiv.textContent = "Help text cannot be empty."; return; } const result = extractAllTextContent(container); const metadata = { extractedText: result.texts.join(" "), urlPath: result.urlPath }; await saveHelp(helpId, newText, JSON.stringify(metadata)); helpData[helpId] = { text: newText, metadata }; msgDiv.style.color = "green"; msgDiv.textContent = "Saved!"; setTimeout(() => wrapper.remove(), 2000); }; wrapper.appendChild(toolbar); wrapper.appendChild(editorDiv); wrapper.appendChild(btnContainer); wrapper.appendChild(msgDiv); } else { const textDiv = document.createElement("div"); textDiv.className = "text-content"; textDiv.innerHTML = helpEntry.text || "No help available."; const closeBtn = document.createElement("button"); closeBtn.innerHTML = "&times;"; closeBtn.className = "btn-close"; closeBtn.onclick = () => wrapper.remove(); wrapper.appendChild(closeBtn); wrapper.appendChild(textDiv); } container.appendChild(wrapper); } function extractAllTextContent(el) { const texts = new Set(); let urlPath = ""; if (!el) return { texts: [], urlPath: "" }; el.querySelectorAll("*").forEach(node => { const style = window.getComputedStyle(node); const isHidden = style.display === "none" || style.visibility === "hidden" || node.offsetParent === null; if (isHidden) return; const tag = node.tagName.toLowerCase(); if (tag === "option" || node.closest("select") || node.closest("table")) return; if (tag === "label" && node.innerText.trim()) texts.add(node.innerText.trim()); if ((tag === "input" || tag === "textarea") && node.placeholder) texts.add(node.placeholder.trim()); if (node.title) texts.add(node.title.trim()); if (tag === "button" || tag === "legend") { const text = node.textContent.trim(); if (text) texts.add(text); } const onlyText = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE; if (onlyText) { const text = node.textContent.trim(); if (text && text.length < 200) texts.add(text); } }); const currentURL = window.location.pathname; urlPath = currentURL.replace(/^.*\/(Account\/transfer)(\/.*)?(\?.*)?$/, "$1"); return { texts: Array.from(texts), urlPath: urlPath || currentURL }; } function attachHelpIcon(el, helpId) { if (el.dataset.helpAttached) return; el.dataset.helpAttached = "true"; const role = cfg.role || "prod"; if (role === "prod" && !(helpData[helpId]?.text || "").trim()) return; const wrapperDiv = document.createElement("div"); wrapperDiv.style.padding = "20px"; wrapperDiv.style.position = "relative"; const icon = document.createElement("img"); icon.src = iconUrl; icon.className = "help-agent-icon"; icon.style.cursor = "pointer"; icon.onclick = (e) => { e.stopPropagation(); showInlineEditor(el, role === "dev", helpId); }; wrapperDiv.appendChild(icon); el.appendChild(wrapperDiv); } function isNestedUnderSameSelector(el) { let parent = el.parentElement; while (parent) { if (cfg.selectors.some(sel => parent.matches(sel)) && parent.querySelector(".help-agent-icon")) { return true; } parent = parent.parentElement; } return false; } function scanAndAttach() { cfg.selectors.forEach(sel => { document.querySelectorAll(sel).forEach(el => { if (el.dataset.helpAttached) return; if (isNestedUnderSameSelector(el)) return; const helpId = getHelpId(el); attachHelpIcon(el, helpId); }); }); } window.addEventListener("DOMContentLoaded", async () => { await fetchHelpData(); scanAndAttach(); }); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; cfg.selectors.forEach(sel => { if (node.matches(sel) && !node.dataset.helpAttached && !isNestedUnderSameSelector(node)) { const helpId = getHelpId(node); attachHelpIcon(node, helpId); } node.querySelectorAll(sel).forEach(child => { if (!child.dataset.helpAttached && !isNestedUnderSameSelector(child)) { const helpId = getHelpId(child); attachHelpIcon(child, helpId); } }); }); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); };