/* ════════════════════════════════════════════════════════════════ 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 = `
Order Cancellation Analysis · ${escHtml(data.order_id)} ${escHtml(data.account_name)}
Order Status ${escHtml(data.order_status)}
Time Since Booking ${data.elapsed_minutes} min
SOP v4 Default Fee INR ${data.standard_fee_inr}
Final Fee INR ${data.final_fee_inr}${data.fee_waived ? ' — Waived' : ''}
`; } 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 = `
Service Credit Evaluation · ${escHtml(data.order_id)} ${escHtml(data.account_name)}
Pickup Delay ${delayVal}
Required Threshold > ${data.required_threshold_hours}h
Eligible ${data.eligible ? 'Yes' : 'No — Below Threshold'}
Credit Amount INR ${data.credit_amount_inr}
`; } 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 = `
`; area.appendChild(row); area.scrollTop = area.scrollHeight; return id; } function removeMsg(id) { const el = document.getElementById(id); if (el) el.remove(); } /* ════════════════════════════════════════ EVIDENCE PANEL ════════════════════════════════════════ */ function updateEvidencePanel(citations, conflictMatrix, traceSteps) { // Show anchor card filled state const anchorCard = document.getElementById('anchor-card'); const anchorEmpty = document.getElementById('anchor-empty'); const anchorFilled = document.getElementById('anchor-filled'); if (citations && citations.length > 0) { const top = citations[0]; if (anchorEmpty) anchorEmpty.style.display = 'none'; if (anchorFilled) anchorFilled.style.display = ''; if (anchorCard) anchorCard.classList.remove('anchor-card--empty'); const badgeEl = document.getElementById('anchor-badge'); const filenameEl = document.getElementById('anchor-filename'); const quoteEl = document.getElementById('anchor-quote'); const metaEl = document.getElementById('anchor-meta'); if (badgeEl) badgeEl.textContent = top.authority_level || 'Level 4 · Signed Contract'; if (filenameEl) filenameEl.textContent = shortDocName(top.source); if (quoteEl) quoteEl.textContent = top.relevance || 'Primary governing clause evaluated.'; if (metaEl) metaEl.textContent = `Source: ${top.source}`; } // Precedence matrix const matrixEl = document.getElementById('conflict-matrix-list'); if (matrixEl && conflictMatrix && conflictMatrix.length > 0) { matrixEl.innerHTML = ''; conflictMatrix.forEach(m => { const statusClass = resolveStatusClass(m.status); const statusLabel = formatStatus(m.status); const row = document.createElement('div'); row.className = `matrix-row ${statusClass}`; row.innerHTML = `
${escHtml(shortDocName(m.source_name))} ${escHtml(m.authority_level)}
${escHtml(m.rule_stated)}
${escHtml(statusLabel)}
`; matrixEl.appendChild(row); }); } // Execution trace const traceBody = document.getElementById('dag-body'); const traceCount = document.getElementById('trace-count'); if (traceBody && traceSteps && traceSteps.length > 0) { traceCount.textContent = `${traceSteps.length} steps`; traceBody.innerHTML = ''; traceSteps.forEach(s => { const step = document.createElement('div'); step.className = 'trace-step'; step.innerHTML = `
Step ${s.step_id}: ${escHtml(s.name || '')} ${s.duration_ms ?? '—'}ms
${escHtml(s.details || s.status || '')}
`; traceBody.appendChild(step); }); } } function resolveStatusClass(status = '') { const s = status.toUpperCase(); if (s.includes('OVERRIDING_WINNER') || s.includes('WINNER')) return 'status-winner'; if (s.includes('APPLIED') || s.includes('CONTRACT')) return 'status-applied'; if (s.includes('OVERRIDDEN') || s.includes('REPLACED') || s.includes('DEFAULT')) return 'status-overridden'; if (s.includes('ERROR') || s.includes('DISREGARD')) return 'status-error'; if (s.includes('EXCLUDED') || s.includes('NOT_APPLICABLE')) return 'status-excluded'; return 'status-applied'; } function formatStatus(status = '') { return status .replace(/_/g, ' ') .toLowerCase() .replace(/\b\w/g, c => c.toUpperCase()); } /* ════════════════════════════════════════ ACTION MODAL ════════════════════════════════════════ */ function openActionModal(action) { document.getElementById('modal-action-title').textContent = action.action_title || 'System Action'; document.getElementById('modal-action-details').textContent = JSON.stringify(action.parameters || {}, null, 2); document.getElementById('action-modal').classList.remove('hidden'); } function closeActionModal() { document.getElementById('action-modal').classList.add('hidden'); } async function confirmAction(confirmed) { closeActionModal(); if (!pendingAction) return; const loadingId = appendTyping(); try { const res = await fetch('/api/confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action_id: pendingAction.action_id, confirmed, ...currentUser, }), }); const data = await res.json(); removeMsg(loadingId); appendMsg('assistant', confirmed ? `**Confirmed.** ${data.message || 'Action executed successfully.'}` : 'Action declined — no changes were applied to the system.' ); } catch (err) { removeMsg(loadingId); appendMsg('assistant', 'Failed to record confirmation response.'); } pendingAction = null; } /* ════════════════════════════════════════ PROACTIVE OPS RADAR ════════════════════════════════════════ */ async function loadProactiveInsights() { const ctx = currentUser; const badge = document.getElementById('proactive-alert-count'); if (badge) badge.classList.add('loading'); const url = `/api/proactive/insights?account_id=${ctx.account_id}&is_internal=${ctx.is_internal}&role=${ctx.role}`; try { const res = await fetch(url); const data = await res.json(); if (badge) { badge.classList.remove('loading'); badge.textContent = data.total_alerts || '0'; } if (data.access_restricted) { ['list-sla-breaches','list-security','list-clusters','list-carrier'].forEach(id => { const el = document.getElementById(id); if (el) el.innerHTML = `
${escHtml(data.message || 'Access restricted.')}
`; }); ['count-sla-breaches','count-security','count-clusters','count-carrier'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = '0'; }); return; } // SLA Breaches setStat('count-sla-breaches', data.sla_breaches.length); renderList('list-sla-breaches', data.sla_breaches.map(b => ({ id: b.ticket_id, pillClass:'pill-rose', badgeText: b.overdue_by_minutes > 0 ? `${b.overdue_by_minutes}m overdue` : 'Approaching', secondary: b.severity, subject: b.subject, source: b.rule_source, action: { label: 'Escalate →', scenarioId: 'scenario-4' } }))); // Security setStat('count-security', data.security_alerts.length); renderList('list-security', data.security_alerts.map(s => ({ id: s.ticket_id, pillClass:'pill-amber', badgeText:'Critical', secondary: null, subject: s.description, source: s.recommended_action, }))); // Clusters setStat('count-clusters', data.ticket_clusters.length); renderList('list-clusters', data.ticket_clusters.map(c => ({ id: c.known_issue_id, pillClass:'pill-cyan', badgeText:`${c.affected_tickets.length} tickets`, secondary: null, subject: c.issue_title, source: `Workaround: ${c.workaround}`, }))); // Carrier setStat('count-carrier', data.carrier_delays.length); renderList('list-carrier', data.carrier_delays.map(car => ({ id: car.order_id, pillClass:'pill-violet', badgeText:`${car.delay_hours}h delay`, secondary: null, subject: `${car.carrier} — ${car.issue_summary}`, source: car.recommended_action, }))); } catch (err) { console.error('Proactive insights error:', err); if (badge) badge.classList.remove('loading'); } } function setStat(id, count) { const el = document.getElementById(id); if (el) el.textContent = String(count); } function renderList(listId, items) { const el = document.getElementById(listId); if (!el) return; if (!items || items.length === 0) { el.innerHTML = '
No active issues detected.
'; return; } el.innerHTML = items.map(item => `
${escHtml(item.id)} ${item.secondary ? `${escHtml(item.secondary)}` : ''} ${escHtml(item.badgeText)}
${escHtml(item.subject)}
${escHtml(item.source)}
${item.action ? `` : ''}
`).join(''); } /* ════════════════════════════════════════ CONTRACT MATRIX ════════════════════════════════════════ */ async function loadContractMatrix() { if (matrixLoaded) return; // Only fetch once const grid = document.getElementById('contract-matrix-grid'); grid.innerHTML = '
Loading contracts…
'; try { const res = await fetch('/api/data/compare-contracts'); const data = await res.json(); grid.innerHTML = data.map(item => { const planPill = item.plan === 'Enterprise' ? `${item.plan}` : item.plan === 'Growth' ? `${item.plan}` : `${item.plan}`; const hasContract = item.governing_contract !== 'None (Standard Policy Applies)' && item.governing_contract !== 'None (Standard Enterprise Policy Applies)'; return `
${escHtml(item.account_name)} (${escHtml(item.account_id)})
${planPill} ${hasContract ? `Custom Contract` : ''}
Agreement
${escHtml(item.governing_contract)}
P1 SLA Target
${escHtml(item.p1_sla)}
Cancellation Rule
${escHtml(item.cancellation_rule)}
Service Credit Rule
${escHtml(item.service_credit_rule)}
${escHtml(item.precedence_notes)}
`; }).join(''); matrixLoaded = true; } catch (err) { grid.innerHTML = `
Failed to load contract matrix.
`; } } /* ════════════════════════════════════════ DATA EXPLORER ════════════════════════════════════════ */ function initSubtabs() { document.querySelectorAll('.subtab').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.subtab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); loadExplorer(btn.dataset.subtab); }); }); } async function loadExplorer(subtabId) { const container = document.getElementById('explorer-content'); container.innerHTML = '
Loading…
'; const endpoints = { 'subtab-documents': '/api/data/documents', 'subtab-accounts': '/api/data/accounts', 'subtab-orders': '/api/data/orders', 'subtab-tickets': '/api/data/tickets', }; const endpoint = endpoints[subtabId]; if (!endpoint) return; const ctx = currentUser; const url = `${endpoint}?account_id=${ctx.account_id}&is_internal=${ctx.is_internal}&role=${ctx.role}`; try { const res = await fetch(url); const data = await res.json(); if (!data || data.length === 0) { container.innerHTML = '
No records accessible under current context permissions.
'; return; } const keys = Object.keys(data[0]); const table = document.createElement('table'); table.className = 'data-table'; table.innerHTML = ` ${keys.map(k => `${escHtml(k.replace(/_/g,' '))}`).join('')} ${data.map(row => `${keys.map(k => `${escHtml(String(row[k] ?? '—'))}`).join('')}` ).join('')} `; container.innerHTML = ''; container.appendChild(table); } catch (err) { container.innerHTML = '
Failed to load data.
'; } } /* ════════════════════════════════════════ MARKDOWN & UTILITIES ════════════════════════════════════════ */ function renderMarkdown(text) { if (!text) return ''; // Escape first, then apply markdown let t = text .replace(/&/g, '&') .replace(//g, '>'); t = t .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/`([^`]+)`/g, `$1`) .replace(/^### (.+)$/gm, '

$1

') .replace(/^#### (.+)$/gm, '
$1
') .replace(/^---$/gm, '
') .replace(/^- (.+)$/gm, '
  • $1
  • ') .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') .replace(/\n\n/g, '

    '); 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); }