/* ============================================================================ _shared/ai-assist.jsx — Corporate AI Assist (floating panel) ---------------------------------------------------------------------------- On every officer station + admin dashboard, this mounts a floating button (bottom-right, emerald) that opens a side panel for AI drafting. Context is read from `window.__AI_CONTEXT` (set per-page or per-drill). If unset, falls back to {station_slug, route} from the URL. Backend: POST /api/v1/ai/chat → nginx proxy → 192.168.1.53:8082 (the Jetson AI Router with Llama-3.2 ours-first + xAI cross-check). Every AI use mints `ai_assist_used` HCC via window.CHWorkCredit.fire. ============================================================================ */ (function () { "use strict"; if (window.__chAIAssistMounted) return; window.__chAIAssistMounted = true; function getToken() { const m = ("; " + document.cookie).match(/; ch_corp_token=([^;]+)/); return m ? decodeURIComponent(m[1]) : null; } function getContext() { return Object.assign({ station_slug: window.__STATION || null, route: location.pathname, }, window.__AI_CONTEXT || {}); } function buildSystemPrompt(intent, ctx) { const lines = []; if (ctx.item_type) lines.push("Item type: " + ctx.item_type); if (ctx.item_id) lines.push("Item ID: " + ctx.item_id); if (ctx.item_summary) lines.push("Item summary: " + ctx.item_summary); if (ctx.extra) lines.push("Notes: " + ctx.extra); return lines.join("\n"); } const INTENTS = []; /* ---- styles ------------------------------------------------------------- */ const style = document.createElement("style"); style.textContent = ` #ch-ai-fab { position: fixed; bottom: 18px; right: 18px; z-index: 9990; width: 52px; height: 52px; border-radius: 50%; background: #117A4D; color: #FAF9F6; border: 0; cursor: pointer; box-shadow: 0 10px 30px rgba(17,122,77,.35); display: grid; place-items: center; font: 600 18px/1 "Fraunces", Georgia, serif; transition: transform 150ms ease; } #ch-ai-fab:hover { transform: scale(1.08); } #ch-ai-fab .pulse { position: absolute; inset: 0; border-radius: 50%; box-shadow: 0 0 0 0 rgba(17,122,77,.6); animation: chAiPulse 2.4s ease-out infinite; } @keyframes chAiPulse { 0% { box-shadow: 0 0 0 0 rgba(17,122,77,.55); } 80% { box-shadow: 0 0 0 22px rgba(17,122,77,0); } 100% { box-shadow: 0 0 0 0 rgba(17,122,77,0); } } #ch-ai-panel { position: fixed; right: 0; top: 0; bottom: 0; width: min(560px, 96vw); z-index: 9991; background: #FAF9F6; border-left: 1px solid rgba(26,26,23,.10); box-shadow: -20px 0 40px rgba(0,0,0,.18); display: none; flex-direction: column; font-family: "Fraunces", Georgia, serif; color: #1A1A17; } #ch-ai-panel.open { display: flex; } #ch-ai-panel header { padding: 22px 26px 14px; border-bottom: 1px solid rgba(26,26,23,.08); } #ch-ai-panel header .eb { font: 500 10px "Geist Mono",monospace; letter-spacing:.22em; text-transform:uppercase; color:#2C5F87; } #ch-ai-panel header h2 { font: italic 500 24px/1.15 "Fraunces",Georgia,serif; margin:6px 0 4px; } #ch-ai-panel header .sub { font: 500 11px "Geist Mono",monospace; color:#777570; letter-spacing:.08em; } #ch-ai-panel .closeX { position: absolute; top: 14px; right: 18px; background: none; border: 0; font: 600 18px/1 "Geist Mono",monospace; color:#777570; cursor: pointer; } #ch-ai-panel .ctx { margin: 12px 26px; padding: 10px 14px; border:1px solid rgba(26,26,23,.10); border-radius: 8px; background: #F4F1EA; font: 500 11px/1.55 "Geist Mono",monospace; color:#4A4A45; } #ch-ai-panel .intents { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 26px 12px; } #ch-ai-panel .intents button { padding: 6px 12px; border-radius: 999px; border: 1px solid rgba(26,26,23,.12); background: #FFFFFF; color: #1A1A17; font: 600 10px "Geist Mono",monospace; letter-spacing:.12em; text-transform: uppercase; cursor: pointer; } #ch-ai-panel .intents button.active { background:#117A4D; color:#FAF9F6; border-color:#117A4D; } #ch-ai-panel .promptArea { padding: 0 26px; } #ch-ai-panel textarea { width: 100%; border: 1px solid rgba(26,26,23,.12); border-radius: 10px; padding: 12px 14px; font: 14px/1.5 "Fraunces",Georgia,serif; resize: vertical; background: #FFFFFF; color: #1A1A17; } #ch-ai-panel .row { display:flex; gap:8px; margin-top: 10px; padding: 0 26px; } #ch-ai-panel .row button { padding: 9px 14px; border-radius: 999px; border: 1px solid rgba(26,26,23,.12); background: #FFFFFF; color: #1A1A17; font: 600 11px "Geist Mono",monospace; letter-spacing:.12em; text-transform: uppercase; cursor: pointer; } #ch-ai-panel .row button.primary { background:#117A4D; color:#FAF9F6; border-color:#117A4D; } #ch-ai-panel .row button:disabled { opacity:.5; cursor:wait; } #ch-ai-panel .draftWrap { flex: 1; overflow: auto; padding: 14px 26px 26px; } #ch-ai-panel .draftWrap label { display:block; font: 600 11px "Geist Mono",monospace; letter-spacing:.18em; text-transform:uppercase; color:#4A4A45; margin: 8px 0 6px; } #ch-ai-panel .meta { display:flex; gap:14px; font: 500 10px "Geist Mono",monospace; color:#777570; letter-spacing:.10em; text-transform:uppercase; margin-top: 8px; } `; document.head.appendChild(style); /* ---- DOM ---------------------------------------------------------------- */ const fab = document.createElement("button"); fab.id = "ch-ai-fab"; fab.title = "AI Assist · draft a response with the corporate AI"; fab.innerHTML = ``; const panel = document.createElement("div"); panel.id = "ch-ai-panel"; panel.innerHTML = `
AI Assist · ours-first · xAI cross-check

Draft something for the work in front of you.

Llama edge → reviewed before sending.
Reading page context…
`; document.body.appendChild(fab); document.body.appendChild(panel); /* ---- render intents ------------------------------------------------------ */ let activeIntent = INTENTS[0]; const intentsEl = panel.querySelector("#ch-ai-intents"); INTENTS.forEach(i => { const b = document.createElement("button"); b.type = "button"; b.textContent = i.label; b.dataset.id = i.id; if (i.id === activeIntent.id) b.classList.add("active"); b.addEventListener("click", () => { activeIntent = i; intentsEl.querySelectorAll("button").forEach(x => x.classList.toggle("active", x.dataset.id === i.id)); }); intentsEl.appendChild(b); }); /* ---- open / close ------------------------------------------------------- */ function refreshContext() { const ctx = getContext(); const ctxEl = panel.querySelector("#ch-ai-ctx"); const lines = []; if (ctx.item_type) lines.push("ITEM TYPE · " + ctx.item_type); if (ctx.item_id) lines.push("ITEM ID · " + (ctx.item_id || "").slice(0,8)); if (ctx.item_summary) lines.push("SUMMARY · " + ctx.item_summary); if (!lines.length) lines.push("No item selected — the AI will write generically for the current page."); ctxEl.textContent = lines.join(" · "); } function openPanel() { refreshContext(); panel.classList.add("open"); panel.querySelector("#ch-ai-prompt").focus(); } function closePanel() { panel.classList.remove("open"); } fab.addEventListener("click", openPanel); panel.querySelector(".closeX").addEventListener("click", closePanel); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePanel(); }); /* ---- compose ------------------------------------------------------------ */ panel.querySelector("#ch-ai-go").addEventListener("click", async () => { const TOK = getToken(); if (!TOK) { alert("Sign in to use AI Assist."); return; } const ctx = getContext(); const userPrompt = panel.querySelector("#ch-ai-prompt").value.trim(); const draftEl = panel.querySelector("#ch-ai-draft"); const metaEl = panel.querySelector("#ch-ai-meta"); const goBtn = panel.querySelector("#ch-ai-go"); goBtn.disabled = true; goBtn.textContent = "Routing…"; draftEl.value = "Routing through ours-first → xAI cross-check…"; metaEl.textContent = ""; // Compose the query string the Jetson AI Router expects. // /api/v1/ai/route accepts { query, intent } and returns { response, source, used, latency_ms, from_cache, ... } const systemBlock = buildSystemPrompt(activeIntent.label, ctx); const query = systemBlock + "\n\nTask: " + activeIntent.promptHint + (userPrompt ? "\n\nExtra instructions: " + userPrompt : ""); const t0 = performance.now(); try { const r = activeIntent.askMode ? await fetch("/api/v1/admin/copilot/ask", { method: "POST", headers: { "Authorization": "Bearer " + TOK, "Content-Type": "application/json" }, body: JSON.stringify({ question: userPrompt || activeIntent.promptHint, context_summary: (ctx.item_summary || "") + " · page=" + (ctx.route||""), context_data: { station_slug: ctx.station_slug, item_type: ctx.item_type, item_id: ctx.item_id }, route: ctx.route, }), }) : await fetch("/api/v1/ai/route", { method: "POST", headers: { "Authorization": "Bearer " + TOK, "Content-Type": "application/json" }, body: JSON.stringify({ query: query, intent: activeIntent.id }), }); if (!r.ok) { const t = await r.text(); throw new Error("HTTP " + r.status + ": " + t.slice(0, 200)); } const j = await r.json(); const content = j.answer || j.response || j.content || JSON.stringify(j, null, 2); const t1 = performance.now(); draftEl.value = content; metaEl.innerHTML = "SOURCE · " + (j.source || j.used || "?") + "" + "CONF · " + (j.ours_meta && j.ours_meta.confidence != null ? (j.ours_meta.confidence * 100).toFixed(0) + "%" : "—") + "" + "CACHE · " + (j.from_cache ? "HIT" : "miss") + "" + "RAG · " + (j.rag_hits || 0) + "" + "LATENCY · " + Math.round(t1 - t0) + "ms"; // Mint HCC for the assist use if (window.CHWorkCredit) { window.CHWorkCredit.fire("ai_assist_used", { item_type: ctx.item_type || "dashboard", item_id: ctx.item_id || null, }, "AI Assist used"); } } catch (e) { draftEl.value = "✗ AI Assist failed: " + e.message; metaEl.textContent = ""; } goBtn.disabled = false; goBtn.textContent = "Compose →"; }); panel.querySelector("#ch-ai-clear").addEventListener("click", () => { panel.querySelector("#ch-ai-prompt").value = ""; panel.querySelector("#ch-ai-draft").value = ""; panel.querySelector("#ch-ai-meta").textContent = ""; }); panel.querySelector("#ch-ai-copy").addEventListener("click", () => { const text = panel.querySelector("#ch-ai-draft").value; if (!text) return; navigator.clipboard.writeText(text); const btn = panel.querySelector("#ch-ai-copy"); const o = btn.textContent; btn.textContent = "✓ Copied"; setTimeout(() => btn.textContent = o, 1500); }); panel.querySelector("#ch-ai-send-mail").addEventListener("click", async () => { const text = panel.querySelector("#ch-ai-draft").value; if (!text || text.length < 5) { alert("Nothing to send yet — compose a draft first."); return; } const to = prompt("Send to (email):"); if (!to) return; const subject = prompt("Subject:", "From the corp command center"); if (!subject) return; const TOK = getToken(); try { const r = await fetch("/api/v1/comms/mail/send", { method: "POST", headers: { "Authorization": "Bearer " + TOK, "Content-Type": "application/json" }, body: JSON.stringify({ to: to, subject: subject, body: text }), }); if (!r.ok) throw new Error("HTTP " + r.status); if (window.CHWorkCredit) { const ctx = getContext(); window.CHWorkCredit.fire("email_sent", { item_type: ctx.item_type, item_id: ctx.item_id }, "Email sent"); } alert("Sent."); } catch (e) { alert("Send failed: " + e.message); } }); panel.querySelector("#ch-ai-save-note").addEventListener("click", () => { if (window.CHWorkCredit) { const ctx = getContext(); window.CHWorkCredit.fire("email_drafted", { item_type: ctx.item_type, item_id: ctx.item_id }, "Draft saved"); } const btn = panel.querySelector("#ch-ai-save-note"); const o = btn.textContent; btn.textContent = "✓ Saved (local)"; setTimeout(() => btn.textContent = o, 1500); // v1: local-only — persistence to notes table is a later wave }); panel.querySelector("#ch-ai-toggle-ctx").addEventListener("click", () => { const ctx = getContext(); const txt = prompt("Edit AI context (JSON):", JSON.stringify(ctx, null, 2)); if (!txt) return; try { window.__AI_CONTEXT = JSON.parse(txt); refreshContext(); } catch (e) { alert("Invalid JSON: " + e.message); } }); })();