| const API = ''; |
|
|
| const CLIENT_EXAMPLES = { |
| novamart: ['What happens when a product runs out of stock?', 'How do I add a new supplier?', 'How do I update pricing for an item?'], |
| shelfwise: ['What triggers an out-of-stock alert?', 'How does planogram compliance work?', 'How do I configure a new store?'], |
| clinixone: ['What is prior authorization?', 'How are adverse events reported?', 'What are contraindicated drug combinations?'], |
| pharmalink: ['What is formulary pre-approval?', 'How does benefit tier affect drug coverage?', 'What is a pharmacovigilance alert?'], |
| }; |
|
|
| let state = { |
| domain: null, |
| client: null, |
| domains: {}, |
| loading: false, |
| }; |
|
|
| |
|
|
| async function boot() { |
| const res = await fetch(`${API}/config`); |
| const data = await res.json(); |
| state.domains = data.domains; |
|
|
| const firstDomain = Object.keys(data.domains)[0]; |
| renderDomainSwitcher(); |
| selectDomain(firstDomain); |
|
|
| document.getElementById('send-btn').addEventListener('click', handleSend); |
| document.getElementById('query-input').addEventListener('keydown', e => { |
| if (e.key === 'Enter' && !e.shiftKey) handleSend(); |
| }); |
| } |
|
|
| |
|
|
| function renderDomainSwitcher() { |
| const el = document.getElementById('domain-switcher'); |
| el.innerHTML = Object.keys(state.domains).map(d => ` |
| <button data-domain="${d}" onclick="selectDomain('${d}')">${capitalize(d)}</button> |
| `).join(''); |
| } |
|
|
| function selectDomain(domain) { |
| state.domain = domain; |
| document.querySelectorAll('#domain-switcher button').forEach(b => { |
| b.classList.toggle('active', b.dataset.domain === domain); |
| }); |
|
|
| const clients = state.domains[domain]; |
| const el = document.getElementById('client-switcher'); |
| el.innerHTML = clients.map(c => ` |
| <button data-client="${c.id}" onclick="selectClient('${c.id}')">${c.display}</button> |
| `).join(''); |
|
|
| selectClient(clients[0].id); |
| } |
|
|
| function selectClient(clientId) { |
| state.client = clientId; |
| document.querySelectorAll('#client-switcher button').forEach(b => { |
| b.classList.toggle('active', b.dataset.client === clientId); |
| }); |
| showWelcome(clientId); |
| } |
|
|
| function showWelcome(clientId) { |
| const messages = getMessages(); |
| messages.innerHTML = ''; |
| document.getElementById('eval-body').innerHTML = ` |
| <div class="eval-empty"> |
| <div style="font-size:28px">π</div> |
| <div>Ask a question to see evaluation results</div> |
| </div>`; |
|
|
| const examples = CLIENT_EXAMPLES[clientId] || []; |
| const chips = examples.map(q => ` |
| <button class="example-chip" onclick="sendExample(this)">${escapeHtml(q)}</button> |
| `).join(''); |
|
|
| const el = document.createElement('div'); |
| el.className = 'message bot'; |
| el.innerHTML = ` |
| <div class="bubble">I'm the <strong>${clientId}</strong> assistant. Ask me anything about this client's domain β or try one of these:</div> |
| <div class="example-chips">${chips}</div> |
| <div class="meta">System</div> |
| `; |
| messages.appendChild(el); |
| } |
|
|
| function sendExample(btn) { |
| const input = document.getElementById('query-input'); |
| input.value = btn.textContent; |
| handleSend(); |
| } |
|
|
| |
|
|
| async function handleSend() { |
| const input = document.getElementById('query-input'); |
| const query = input.value.trim(); |
| if (!query || state.loading) return; |
|
|
| input.value = ''; |
| setLoading(true); |
|
|
| appendMessage('user', query); |
| const thinkingEl = appendThinking(); |
|
|
| try { |
| const res = await fetch(`${API}/query`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ query, client: state.client }), |
| }); |
|
|
| if (!res.ok) { |
| const err = await res.json().catch(() => ({ detail: res.statusText })); |
| throw new Error(err.detail || 'Request failed'); |
| } |
|
|
| const data = await res.json(); |
| thinkingEl.remove(); |
| appendBotMessage(data); |
| renderEval(data); |
| } catch (err) { |
| thinkingEl.remove(); |
| appendMessage('bot', `Error: ${err.message}`); |
| } finally { |
| setLoading(false); |
| } |
| } |
|
|
| |
|
|
| function appendMessage(role, text) { |
| const el = document.createElement('div'); |
| el.className = `message ${role}`; |
| el.innerHTML = ` |
| <div class="bubble">${escapeHtml(text)}</div> |
| <div class="meta">${role === 'user' ? 'You' : 'Bot'}</div> |
| `; |
| getMessages().appendChild(el); |
| scrollMessages(); |
| return el; |
| } |
|
|
| function appendBotMessage(data) { |
| const overall = data.evaluation.overall_pass; |
| const verdictClass = overall ? 'pass' : 'fail'; |
| const failedNames = Object.entries(data.evaluation.metrics) |
| .filter(([, m]) => !m.passed) |
| .map(([k]) => METRIC_LABELS[k] || k) |
| .join(', '); |
| const verdictLabel = overall ? 'β All checks passed' : `β Failed: ${failedNames}`; |
| const flagBanner = data.flagged |
| ? `<div class="flagged-banner">β Response flagged β ${failedNames}</div>` |
| : ''; |
|
|
| const el = document.createElement('div'); |
| el.className = 'message bot'; |
| el.innerHTML = ` |
| ${flagBanner} |
| <div class="bubble">${escapeHtml(data.answer)}</div> |
| <div class="verdict ${verdictClass}">${verdictLabel}</div> |
| <div class="meta">${data.client_display}</div> |
| `; |
| getMessages().appendChild(el); |
| scrollMessages(); |
| } |
|
|
| function appendThinking() { |
| const wrap = document.createElement('div'); |
| wrap.className = 'message bot'; |
| wrap.innerHTML = ` |
| <div class="thinking"> |
| <span></span><span></span><span></span> |
| </div> |
| `; |
| getMessages().appendChild(wrap); |
| scrollMessages(); |
| return wrap; |
| } |
|
|
| |
|
|
| const METRIC_LABELS = { |
| pii_leakage: 'PII Leakage', |
| token_budget: 'Token Budget', |
| answer_relevancy: 'Answer Relevancy', |
| faithfulness: 'Faithfulness', |
| chain_terminology: 'Chain Terminology', |
| }; |
|
|
| const METRIC_DESC = { |
| pii_leakage: 'Regex scan β no PII in response', |
| token_budget: 'Response within token ceiling', |
| answer_relevancy: 'Cosine similarity: query β response', |
| faithfulness: 'NLI cross-encoder: grounded in retrieved context?', |
| chain_terminology: 'Deterministic: client-specific terms used', |
| }; |
|
|
| function renderEval(data) { |
| const metrics = data.evaluation.metrics; |
| const sources = data.sources; |
|
|
| const metricCards = Object.entries(metrics).map(([key, m]) => { |
| const cls = scoreClass(m.score, key); |
| const pct = Math.round(m.score * 100); |
| return ` |
| <div class="metric-card ${cls}"> |
| <div class="metric-header"> |
| <span class="metric-name">${METRIC_LABELS[key] || key}</span> |
| <span class="score-badge ${cls}">${pct}%</span> |
| </div> |
| <div class="metric-detail">${escapeHtml(METRIC_DESC[key] || '')}</div> |
| <div class="metric-detail" style="margin-top:4px;color:#6a8aaa">${escapeHtml(m.detail)}</div> |
| <div class="score-bar-wrap"> |
| <div class="score-bar-bg"> |
| <div class="score-bar-fill ${cls}" style="width:${pct}%"></div> |
| </div> |
| </div> |
| </div> |
| `; |
| }).join(''); |
|
|
| const sourceItems = sources.map(s => ` |
| <div class="source-item"> |
| <span class="source-title">${escapeHtml(s.title)}</span> |
| <span class="source-score">${(s.score * 100).toFixed(0)}%</span> |
| </div> |
| `).join(''); |
|
|
| document.getElementById('eval-body').innerHTML = ` |
| <div class="eval-content"> |
| ${metricCards} |
| <div class="sources-section"> |
| <div class="sources-label">Retrieved Sources</div> |
| ${sourceItems || '<div style="font-size:11px;color:#8aabcc">No sources retrieved</div>'} |
| </div> |
| </div> |
| `; |
| } |
|
|
| function scoreClass(score, metric) { |
| |
| if (metric === 'pii_leakage') return score === 1.0 ? 'pass' : 'fail'; |
| if (score >= 0.75) return 'pass'; |
| if (score >= 0.45) return 'warn'; |
| return 'fail'; |
| } |
|
|
| |
|
|
| function setLoading(val) { |
| state.loading = val; |
| document.getElementById('send-btn').disabled = val; |
| document.getElementById('query-input').disabled = val; |
| } |
|
|
| function getMessages() { |
| return document.getElementById('messages'); |
| } |
|
|
| function scrollMessages() { |
| const el = getMessages(); |
| el.scrollTop = el.scrollHeight; |
| } |
|
|
| function capitalize(s) { |
| return s.charAt(0).toUpperCase() + s.slice(1); |
| } |
|
|
| function escapeHtml(str) { |
| return String(str) |
| .replace(/&/g, '&') |
| .replace(/</g, '<') |
| .replace(/>/g, '>') |
| .replace(/"/g, '"'); |
| } |
|
|
| boot(); |
|
|