/* ════════════════════════════════════════════════════════════════ ParcelPilot Intelligence — Application Controller ════════════════════════════════════════════════════════════════ */ 'use strict'; /* ─── State ─── */ let pendingAction = null; let evalScenarios = []; let currentUser = {}; let matrixLoaded = false; /* ─── Boot ─── */ document.addEventListener('DOMContentLoaded', () => { initContextSwitcher(); initTabs(); initComposer(); initSubtabs(); prefetchScenarios(); initExportLog(); // Pre-fetch proactive insights so badge shows immediately loadProactiveInsights(); }); /* ════════════════════════════════════════ CONTEXT SWITCHER ════════════════════════════════════════ */ function initContextSwitcher() { const modeEl = document.getElementById('context-mode-select'); const accountEl = document.getElementById('account-select'); const roleEl = document.getElementById('role-select'); const update = () => { currentUser = buildContext(); updateContextIndicator(); // Re-fetch proactive when context changes (if on that tab) if (document.getElementById('tab-proactive').classList.contains('active')) { loadProactiveInsights(); } }; modeEl.addEventListener('change', update); accountEl.addEventListener('change', update); roleEl.addEventListener('change', update); update(); } function buildContext() { const mode = document.getElementById('context-mode-select').value; const accountId = document.getElementById('account-select').value; const role = document.getElementById('role-select').value; const is_internal = mode === 'internal'; const accountNames = { 'ACCT-001': 'Northstar Logistics', 'ACCT-002': 'LumenWorks', 'ACCT-003': 'Beacon Retail', 'ACCT-004': 'Axis Labs', }; return { account_id: accountId, account_name: accountNames[accountId] || accountId, is_internal, role: is_internal ? role : 'customer', user_id: is_internal ? 'USR-STAFF-99' : `USR-${accountId}`, }; } function updateContextIndicator() { const el = document.getElementById('active-context-indicator'); if (!el) return; const ctx = currentUser; if (ctx.is_internal) { el.textContent = `Internal · ${ctx.role.replace(/_/g, ' ')}`; el.style.color = '#22d3ee'; } else { el.textContent = `${ctx.account_name} · Customer`; el.style.color = '#34d399'; } } /* ════════════════════════════════════════ TAB ROUTING ════════════════════════════════════════ */ function initTabs() { document.querySelectorAll('.navtab[data-tab]').forEach(btn => { btn.addEventListener('click', () => activateTab(btn.dataset.tab)); }); } function activateTab(tabId) { document.querySelectorAll('.navtab').forEach(b => b.classList.remove('active')); document.querySelectorAll('.stage-pane').forEach(p => p.classList.remove('active')); const btn = document.querySelector(`.navtab[data-tab="${tabId}"]`); const pane = document.getElementById(tabId); if (btn) btn.classList.add('active'); if (pane) pane.classList.add('active'); if (tabId === 'tab-proactive') loadProactiveInsights(); if (tabId === 'tab-matrix') loadContractMatrix(); if (tabId === 'tab-data') loadExplorer('subtab-documents'); } /* ════════════════════════════════════════ EVALUATOR PRESETS ════════════════════════════════════════ */ async function prefetchScenarios() { try { const res = await fetch('/api/evaluator/scenarios'); evalScenarios = await res.json(); } catch (e) { console.warn('Could not load scenarios:', e); } } function onPresetSelectChange(sel) { const id = sel.value; if (!id) return; sel.value = ''; runScenario(id); } function runScenario(id) { const s = evalScenarios.find(x => x.id === id); if (!s) return; document.getElementById('context-mode-select').value = s.is_internal ? 'internal' : 'customer'; document.getElementById('account-select').value = s.account_id; document.getElementById('role-select').value = s.role; document.getElementById('context-mode-select').dispatchEvent(new Event('change')); activateTab('tab-chat'); document.getElementById('chat-input').value = s.prompt; sendMessage(); } /* ════════════════════════════════════════ CHAT COMPOSER ════════════════════════════════════════ */ function initComposer() { const input = document.getElementById('chat-input'); const sendBtn = document.getElementById('send-btn'); sendBtn.addEventListener('click', sendMessage); input.addEventListener('keydown', e => { const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const modKey = isMac ? e.metaKey : e.ctrlKey; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } else if (e.key === 'Enter' && modKey) { e.preventDefault(); sendMessage(); } }); input.addEventListener('input', () => autoResizeTextarea(input)); } function autoResizeTextarea(el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 140) + 'px'; } async function sendMessage() { const input = document.getElementById('chat-input'); const text = input.value.trim(); if (!text) return; input.value = ''; input.style.height = 'auto'; appendMsg('user', text); const loadingId = appendTyping(); try { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: text, ...currentUser }), }); const data = await res.json(); removeMsg(loadingId); if (!res.ok) { appendMsg('assistant', `Error ${res.status}: ${data.detail || 'Request failed.'}`); return; } // Update latency/confidence display if (data.metrics) { const ms = data.metrics.total_duration_ms; const pct = Math.round(data.metrics.confidence_score * 100); document.getElementById('metrics-summary-text').textContent = `${ms}ms · ${pct}% confidence`; } appendMsg('assistant', data.answer, data.citations, data.widget_data); updateEvidencePanel(data.citations, data.conflict_matrix, data.trace_steps); if (data.pending_action) { pendingAction = data.pending_action; openActionModal(data.pending_action); } } catch (err) { removeMsg(loadingId); appendMsg('assistant', 'Network error — could not reach the ParcelPilot backend. Is the server running?'); console.error(err); } } /* ════════════════════════════════════════ MESSAGE RENDERING ════════════════════════════════════════ */ function appendMsg(role, text, citations = [], widgetData = null) { const area = document.getElementById('chat-messages'); const row = document.createElement('div'); row.className = `msg-row msg-${role}`; const avatar = document.createElement('div'); avatar.className = 'msg-avatar'; avatar.setAttribute('aria-hidden', 'true'); avatar.innerHTML = role === 'user' ? `` : ``; const bubble = document.createElement('div'); bubble.className = 'msg-bubble'; if (role === 'assistant') { const nameEl = document.createElement('p'); nameEl.className = 'msg-name'; nameEl.textContent = 'ParcelPilot Intelligence'; bubble.appendChild(nameEl); } const textNode = document.createElement('div'); textNode.innerHTML = renderMarkdown(text); bubble.appendChild(textNode); // Widget card if (widgetData && widgetData.type !== 'action_pending') { const wc = renderWidgetCard(widgetData); if (wc) bubble.appendChild(wc); } // Citations if (citations && citations.length > 0) { const citeBlock = document.createElement('div'); citeBlock.className = 'chat-citations'; const label = document.createElement('div'); label.className = 'chat-citations-label'; label.textContent = 'Source Citations'; citeBlock.appendChild(label); citations.forEach(c => { const cr = document.createElement('div'); cr.className = 'chat-citation-row'; cr.innerHTML = `${escHtml(c.source)} (${escHtml(c.authority_level)})`; citeBlock.appendChild(cr); }); bubble.appendChild(citeBlock); } row.appendChild(avatar); row.appendChild(bubble); area.appendChild(row); area.scrollTop = area.scrollHeight; } function renderWidgetCard(data) { if (!data) return null; const card = document.createElement('div'); card.className = 'widget-card'; if (data.type === 'order_cancellation_widget') { card.innerHTML = `
`; } else if (data.type === 'service_credit_widget') { const delayVal = data.delay_hours !== null && data.delay_hours !== undefined ? `${data.delay_hours}h` : 'N/A'; card.innerHTML = ` `; } return card.innerHTML ? card : null; } function appendTyping() { const id = `typing-${Date.now()}`; const area = document.getElementById('chat-messages'); const row = document.createElement('div'); row.id = id; row.className = 'msg-row msg-assistant msg-typing'; row.innerHTML = `$1`)
.replace(/^### (.+)$/gm, ''); return `
${t}
`; } function escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function shortDocName(name) { if (!name) return '—'; return name .replace(/^\d+_/, '') .replace(/\.pdf$/i, '') .replace(/_/g, ' ') .trim(); } /* ════════════════════════════════════════ EXPORT LOG ════════════════════════════════════════ */ function initExportLog() { const btn = document.getElementById('export-log-btn'); if (btn) btn.addEventListener('click', exportAuditLog); } function exportAuditLog() { const dateStr = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const filename = `ParcelPilot_Audit_Log_${currentUser.account_id}_${dateStr}.txt`; let content = `=================================================================\n`; content += `PARCELPILOT AI OPERATIONS - AUDIT LOG\n`; content += `Generated: ${new Date().toISOString()}\n`; content += `Context: ${currentUser.account_name} (${currentUser.account_id}) | Role: ${currentUser.role}\n`; content += `=================================================================\n\n`; const messages = document.querySelectorAll('.msg-row'); if (messages.length === 0) { content += `No conversation history available in current session.\n`; } else { messages.forEach(msg => { const isUser = msg.classList.contains('msg-user'); const author = isUser ? currentUser.account_name : 'ParcelPilot AI'; // Get main text let text = ''; if (isUser) { text = msg.querySelector('.msg-bubble').textContent.trim(); } else { // Strip out citations text and widget text for cleaner log const bubble = msg.cloneNode(true); const widget = bubble.querySelector('.widget-card'); const citations = bubble.querySelector('.chat-citations'); if (widget) widget.remove(); if (citations) citations.remove(); text = bubble.querySelector('.msg-bubble').textContent.replace('ParcelPilot Intelligence', '').trim(); } content += `[${author}]\n${text}\n\n`; }); } const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }