below-threshold's picture
UX: welcome message with example questions, specific failure details in verdict
f7a25db
const API = ''; // same origin
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,
};
// ── Boot ──────────────────────────────────────────────────────────────────
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();
});
}
// ── Switchers ─────────────────────────────────────────────────────────────
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();
}
// ── Send ──────────────────────────────────────────────────────────────────
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);
}
}
// ── Messages ──────────────────────────────────────────────────────────────
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;
}
// ── Eval panel ────────────────────────────────────────────────────────────
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) {
// pii_leakage: 1.0 = pass, anything else = fail (binary)
if (metric === 'pii_leakage') return score === 1.0 ? 'pass' : 'fail';
if (score >= 0.75) return 'pass';
if (score >= 0.45) return 'warn';
return 'fail';
}
// ── Helpers ───────────────────────────────────────────────────────────────
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
boot();