insight-aid
Version:
Insight-aid: lightweight floating help + AI chatbot plugin
268 lines (226 loc) • 8.62 kB
JavaScript
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 === " ") {
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 = "×";
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
});
};