karthikmulugu08 commited on
Commit
e7bacb5
·
verified ·
1 Parent(s): b1c60e4

Fix: suppress JSON text when table renders, escape HTML in trace, fix source chips

Browse files
Files changed (1) hide show
  1. frontend/index.html +78 -108
frontend/index.html CHANGED
@@ -16,7 +16,6 @@
16
  return client;
17
  }
18
 
19
- // ── Utilities ──────────────────────────────────────────────────────────────
20
  function setStatus(msg, type = "info") {
21
  const el = document.getElementById("status");
22
  el.innerHTML = msg;
@@ -51,8 +50,12 @@
51
  });
52
  }
53
 
 
 
 
54
  function markBold(t) {
55
- return (t || "").replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
 
56
  }
57
 
58
  function renderTable(data) {
@@ -78,46 +81,47 @@
78
  const body = document.getElementById("safety-body");
79
 
80
  if (report.error) {
81
- body.innerHTML = `<p class="safety-error">Analysis error: ${report.error}</p>`;
82
  panel.className = "safety-panel review";
 
 
83
  panel.style.display = "block";
84
  return;
85
  }
86
 
87
  const statusMap = {
88
- SAFE: { cls: "safe", icon: "✅", label: "All Clear" },
89
- REVIEW_NEEDED: { cls: "review", icon: "⚠️", label: "Review Needed" },
90
- URGENT: { cls: "danger", icon: "🚨", label: "Urgent — Action Required" },
91
  };
92
  const s = statusMap[report.overall_safety] || statusMap.REVIEW_NEEDED;
93
 
94
  const alertsHtml = (report.alerts || []).map(a => {
95
  const cls = a.level === "DANGER" ? "alert-danger"
96
  : a.level === "WARNING" ? "alert-warn" : "alert-info";
97
- const icon = a.level === "DANGER" ? "🚨" : a.level === "WARNING" ? "⚠️" : "ℹ️";
 
98
  return `<div class="safety-alert ${cls}">
99
- <div class="alert-title">${icon} ${a.title}</div>
100
  <div class="alert-detail">${a.detail}</div>
101
- <div class="alert-rec"><strong>Recommendation:</strong> ${a.recommendation}</div>
102
  </div>`;
103
  }).join("");
104
 
105
  const medsHtml = report.medications?.length
106
- ? `<details class="extracted-details"><summary>💊 ${report.medications.length} Medication(s) extracted</summary>
107
  ${renderTable(report.medications)}</details>` : "";
108
  const allergyHtml = report.allergies?.length
109
- ? `<details class="extracted-details"><summary>⚠️ ${report.allergies.length} Allerg(y/ies) extracted</summary>
110
  ${renderTable(report.allergies)}</details>` : "";
111
 
112
  body.innerHTML = alertsHtml + medsHtml + allergyHtml
113
- || `<p class="no-alerts">No safety concerns found in the current documents.</p>`;
114
 
115
  panel.className = `safety-panel ${s.cls}`;
116
- document.getElementById("safety-status-icon").textContent = s.icon;
117
  document.getElementById("safety-status-label").textContent = s.label;
118
  panel.style.display = "block";
119
-
120
- // Scroll to panel
121
  panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
122
  }
123
 
@@ -133,12 +137,11 @@
133
  const li = document.createElement("li");
134
  li.className = "trace-step entering";
135
  li.innerHTML = `
136
- <span class="step-icon">${step.icon || "•"}</span>
137
  <div class="step-body">
138
  <span class="step-name">${step.step}</span>
139
  <span class="step-detail">${markBold(step.detail)}</span>
140
- ${step.sub_queries ? `<ul class="sub-qs">${step.sub_queries.map(q=>`<li>"${q}"</li>`).join("")}</ul>` : ""}
141
- ${step.sources ? `<div class="step-srcs">${step.sources.map(s=>`<span class="src-chip">${s}</span>`).join("")}</div>` : ""}
142
  <span class="step-ms">${step.ms}ms</span>
143
  </div>`;
144
  ol.appendChild(li);
@@ -148,12 +151,12 @@
148
  function setExcerpts(chunks) {
149
  document.getElementById("excerpts-list").innerHTML = (chunks || []).map(c =>
150
  `<div class="excerpt-card">
151
- <div class="excerpt-src">📄 ${c.source}</div>
152
- <div class="excerpt-txt">${c.excerpt}…</div>
153
  </div>`).join("");
154
  }
155
 
156
- // ── Document loading ───────────────────────────────────────────────────────
157
  async function runSafetyCheck() {
158
  document.getElementById("safety-panel").style.display = "none";
159
  document.getElementById("safety-loading").style.display = "flex";
@@ -163,7 +166,7 @@
163
  const report = JSON.parse(res.data[0]);
164
  showSafetyPanel(report);
165
  } catch(e) {
166
- showSafetyPanel({ error: e.message });
167
  } finally {
168
  document.getElementById("safety-loading").style.display = "none";
169
  }
@@ -176,24 +179,22 @@
176
  document.querySelectorAll(".tpl-btn").forEach(b => b.disabled = false);
177
  document.getElementById("docs-ready").style.display = "flex";
178
  document.getElementById("doc-names").textContent = names.join(" · ");
179
- // Auto-run safety check
180
  runSafetyCheck();
181
  }
182
 
183
  document.getElementById("load-samples").addEventListener("click", async () => {
184
- setStatus("Loading demo documents…", "info");
185
  try {
186
  const c = await gc();
187
  const res = await c.predict("/load_samples", {});
188
  const d = JSON.parse(res.data[0]);
189
  if (d.status === "ok") {
190
- setStatus(`✅ ${d.files} document(s) · ${d.chunks} chunks ready`, "success");
191
  onDocsLoaded(d.names || ["demo docs"]);
192
- } else setStatus("❌ " + d.message, "error");
193
- } catch(e) { setStatus("❌ " + e.message, "error"); }
194
  });
195
 
196
- // Drag & drop / file picker
197
  const dropzone = document.getElementById("dropzone");
198
  const fileInput = document.getElementById("file-input");
199
  dropzone.addEventListener("click", () => fileInput.click());
@@ -204,7 +205,7 @@
204
 
205
  async function handleFiles(files) {
206
  if (!files.length) return;
207
- setStatus(`Indexing ${files.length} file(s)…`, "info");
208
  const items = [];
209
  for (const f of files) {
210
  const buf = await f.arrayBuffer();
@@ -216,10 +217,10 @@
216
  const res = await c.predict("/ingest", { files_json: JSON.stringify(items) });
217
  const d = JSON.parse(res.data[0]);
218
  if (d.status === "ok") {
219
- setStatus(`✅ ${d.files} file(s) · ${d.chunks} chunks`, "success");
220
  onDocsLoaded(d.names || items.map(i => i.name));
221
- } else setStatus("❌ " + (d.message || "Error"), "error");
222
- } catch(e) { setStatus("❌ " + e.message, "error"); }
223
  }
224
 
225
  // ── Ask ────────────────────────────────────────────────────────────────────
@@ -231,9 +232,8 @@
231
  clearTrace();
232
 
233
  appendBubble("user", question.replace(/</g,"&lt;"));
234
- const thinkId = "think-" + Date.now();
235
- const thinkEl = appendBubble("bot",
236
- '<span class="dots"><span></span><span></span><span></span></span>', thinkId);
237
 
238
  let finalAnswer = "", structuredData = null, chunks = [];
239
 
@@ -242,46 +242,34 @@
242
  for await (const msg of c.submit("/ask_stream", { question })) {
243
  if (!msg.data) continue;
244
  const ev = JSON.parse(msg.data[0]);
245
- if (ev.type === "trace_step") addTraceStep(ev.step);
246
- else if (ev.type === "answer") {
247
- finalAnswer = ev.answer;
248
- structuredData = ev.structured_data;
249
- chunks = ev.chunks || [];
250
- } else if (ev.type === "done" && ev.trace_url) {
251
  const lnk = document.getElementById("trace-url-link");
252
  lnk.href = ev.trace_url; lnk.style.display = "inline-flex";
253
- } else if (ev.type === "error") finalAnswer = "⚠️ " + ev.message;
 
254
  }
255
  } catch(_) {
256
- // Non-streaming fallback
257
  try {
258
  const c2 = await gc();
259
  const res = await c2.predict("/ask", { question });
260
  const d = JSON.parse(res.data[0]);
261
- if (d.error) { finalAnswer = "⚠️ " + d.error; }
262
- else {
263
- finalAnswer = d.answer || "";
264
- structuredData = d.structured_data;
265
- chunks = d.chunks || [];
266
- (d.trace || []).forEach(addTraceStep);
267
- if (d.trace_url) {
268
- const lnk = document.getElementById("trace-url-link");
269
- lnk.href = d.trace_url; lnk.style.display = "inline-flex";
270
- }
271
- }
272
- } catch(e2) { finalAnswer = "❌ " + e2.message; }
273
  }
274
 
275
- // Render answer
276
  const wrap = document.getElementById(thinkId);
277
  if (wrap) {
278
  const b = wrap.querySelector(".bubble");
279
  const tbl = structuredData ? renderTable(structuredData) : "";
 
 
280
  b.innerHTML = `<div class="answer-txt"></div>${tbl}`;
281
- await typewrite(b.querySelector(".answer-txt"), finalAnswer);
282
  }
283
  setExcerpts(chunks);
284
-
285
  inp.disabled = false;
286
  document.getElementById("ask-btn").disabled = false;
287
  inp.focus();
@@ -294,14 +282,11 @@
294
  });
295
 
296
  document.querySelectorAll(".tpl-btn").forEach(btn =>
297
- btn.addEventListener("click", () => {
298
- document.getElementById("ask-input").value = btn.dataset.q;
299
- ask(btn.dataset.q);
300
- }));
301
 
302
  document.getElementById("clear-btn").addEventListener("click", () => {
303
  document.getElementById("chat").innerHTML =
304
- `<div class="welcome"><div class="welcome-icon">🩺</div>
305
  <p>Load patient documents, then ask any question.<br/>
306
  <em>Try: "Do any medications interact with the patient's allergies?"</em></p></div>`;
307
  clearTrace();
@@ -310,16 +295,15 @@
310
  document.getElementById("theme-btn").addEventListener("click", () => {
311
  document.documentElement.classList.toggle("dark");
312
  document.getElementById("theme-btn").textContent =
313
- document.documentElement.classList.contains("dark") ? "☀️" : "🌙";
314
  });
315
  </script>
316
  </head>
317
  <body>
318
- <!-- HEADER -->
319
  <header>
320
  <div class="hdr">
321
  <div class="logo">
322
- <span class="logo-icon">🩺</span>
323
  <div>
324
  <h1>ClinIQ</h1>
325
  <p class="tagline">On-device clinical document assistant · Qwen2.5-3B · llama.cpp · Modal</p>
@@ -327,33 +311,31 @@
327
  </div>
328
  <div class="hdr-right">
329
  <div class="badges">
330
- <span class="badge b-purple">🐜 Tiny Titan ≤4B</span>
331
- <span class="badge b-teal">🦙 llama.cpp</span>
332
- <span class="badge b-blue">⚡ Modal A10G</span>
333
- <span class="badge b-gold">🏆 Build Small 2026</span>
334
  </div>
335
- <button id="theme-btn" class="icon-btn">🌙</button>
336
  </div>
337
  </div>
338
  </header>
339
 
340
  <div class="privacy-bar">
341
- 🔒 <strong>Privacy guarantee:</strong> Documents are indexed locally. Only relevant excerpts — never full records — reach the GPU.
342
  </div>
343
 
344
  <main>
345
- <!-- ── LEFT SIDEBAR ── -->
346
  <aside class="sidebar">
347
 
348
- <!-- Upload -->
349
  <div class="card">
350
- <h2>📂 Documents</h2>
351
  <div id="dropzone" class="dropzone">
352
- <span style="font-size:1.8rem">📄</span>
353
  <span class="drop-txt">Drop PDFs / TXT here<br/><small>or click to browse</small></span>
354
  <input type="file" id="file-input" multiple accept=".pdf,.txt" hidden/>
355
  </div>
356
- <button id="load-samples" class="btn secondary full">⚡ Load Demo Patient Docs</button>
357
  <div id="status" class="status info">Upload documents or load demo docs to begin.</div>
358
  <div id="docs-ready" class="docs-ok" style="display:none">
359
  <span class="pulse-dot"></span>
@@ -361,78 +343,66 @@
361
  </div>
362
  </div>
363
 
364
- <!-- 🚨 Safety Panel — THE hero feature -->
365
  <div class="card safety-card-wrap">
366
  <div class="safety-card-header">
367
- <h2>🚨 Drug Safety Check</h2>
368
- <span class="safety-auto-badge">Auto-runs on upload</span>
369
  </div>
370
- <p class="card-hint">Proactive drug-allergy & interaction alerts — no question needed.</p>
371
 
372
  <div id="safety-loading" class="safety-loading" style="display:none">
373
  <div class="spinner"></div>
374
- <span>Qwen2.5-3B analysing medications &amp; allergies…</span>
375
  </div>
376
 
377
  <div id="safety-panel" class="safety-panel" style="display:none">
378
  <div class="safety-status-bar">
379
- <span id="safety-status-icon">✅</span>
380
  <span id="safety-status-label">All Clear</span>
381
  </div>
382
  <div id="safety-body"></div>
383
  </div>
384
  </div>
385
 
386
- <!-- Smart Templates -->
387
  <div class="card">
388
- <h2>🧩 Smart Templates</h2>
389
  <div class="templates">
390
- <button class="tpl-btn" disabled data-q="List all medications with doses and frequency">💊 Medication List</button>
391
- <button class="tpl-btn" disabled data-q="List all allergies and reactions">⚠️ Allergy Summary</button>
392
- <button class="tpl-btn" disabled data-q="What are all the diagnoses?">🏥 Diagnoses</button>
393
- <button class="tpl-btn" disabled data-q="What follow-up appointments are scheduled?">📅 Follow-up Plan</button>
394
- <button class="tpl-btn" disabled data-q="Do any medications interact with the patient's allergies or other medications?">💊⚠️ Drug Interactions</button>
395
- <button class="tpl-btn" disabled data-q="Compare the medications listed in each document — did anything change?">🔄 Compare Across Visits</button>
396
  </div>
397
  </div>
398
 
399
  </aside>
400
 
401
- <!-- ── RIGHT: CHAT + TRACE ── -->
402
  <div class="right-col">
403
 
404
- <!-- Chat -->
405
  <div class="card chat-card">
406
  <div class="chat-hdr">
407
- <h2>🗨️ Clinical Q&amp;A</h2>
408
- <button id="clear-btn" class="icon-btn sm">🗑️</button>
409
  </div>
410
  <div id="chat" class="chat">
411
  <div class="welcome">
412
- <div class="welcome-icon">🩺</div>
413
  <p>Load patient documents, then ask any question.<br/>
414
  <em>Try: "Do any medications interact with the patient's allergies?"</em></p>
415
  </div>
416
  </div>
417
  <div class="input-row">
418
  <textarea id="ask-input" rows="2" disabled
419
- placeholder="Ask a clinical question… (Enter to send)"></textarea>
420
- <button id="ask-btn" class="btn primary ask-btn" disabled>
421
- Ask
422
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
423
- <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
424
- </svg>
425
- </button>
426
  </div>
427
  </div>
428
 
429
- <!-- Agent Trace -->
430
  <div class="card trace-card">
431
  <div class="trace-hdr">
432
- <h2>🔬 Agent Trace</h2>
433
- <a id="trace-url-link" href="#" target="_blank" class="share-btn" style="display:none">
434
- 📤 View on HF Hub
435
- </a>
436
  </div>
437
  <ol id="trace-steps" class="trace-steps"></ol>
438
  <h3>Retrieved Excerpts</h3>
 
16
  return client;
17
  }
18
 
 
19
  function setStatus(msg, type = "info") {
20
  const el = document.getElementById("status");
21
  el.innerHTML = msg;
 
50
  });
51
  }
52
 
53
+ function esc(t) {
54
+ return String(t||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
55
+ }
56
  function markBold(t) {
57
+ // Escape HTML first, then apply bold markdown
58
+ return esc(t).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
59
  }
60
 
61
  function renderTable(data) {
 
81
  const body = document.getElementById("safety-body");
82
 
83
  if (report.error) {
84
+ body.innerHTML = `<p class="safety-note">${report.error}</p>`;
85
  panel.className = "safety-panel review";
86
+ document.getElementById("safety-status-icon").textContent = "—";
87
+ document.getElementById("safety-status-label").textContent = "Not available";
88
  panel.style.display = "block";
89
  return;
90
  }
91
 
92
  const statusMap = {
93
+ SAFE: { cls: "safe", label: "All Clear" },
94
+ REVIEW_NEEDED: { cls: "review", label: "Review Needed" },
95
+ URGENT: { cls: "danger", label: "Urgent — Action Required" },
96
  };
97
  const s = statusMap[report.overall_safety] || statusMap.REVIEW_NEEDED;
98
 
99
  const alertsHtml = (report.alerts || []).map(a => {
100
  const cls = a.level === "DANGER" ? "alert-danger"
101
  : a.level === "WARNING" ? "alert-warn" : "alert-info";
102
+ const tag = a.level === "DANGER" ? "DANGER"
103
+ : a.level === "WARNING" ? "WARNING" : "INFO";
104
  return `<div class="safety-alert ${cls}">
105
+ <div class="alert-title"><span class="alert-tag">${tag}</span> ${a.title}</div>
106
  <div class="alert-detail">${a.detail}</div>
107
+ <div class="alert-rec"><strong>Action:</strong> ${a.recommendation}</div>
108
  </div>`;
109
  }).join("");
110
 
111
  const medsHtml = report.medications?.length
112
+ ? `<details class="extracted-details"><summary>${report.medications.length} Medication(s) extracted</summary>
113
  ${renderTable(report.medications)}</details>` : "";
114
  const allergyHtml = report.allergies?.length
115
+ ? `<details class="extracted-details"><summary>${report.allergies.length} Allergy/Allergies extracted</summary>
116
  ${renderTable(report.allergies)}</details>` : "";
117
 
118
  body.innerHTML = alertsHtml + medsHtml + allergyHtml
119
+ || `<p class="no-alerts">No safety concerns found.</p>`;
120
 
121
  panel.className = `safety-panel ${s.cls}`;
122
+ document.getElementById("safety-status-icon").textContent = "";
123
  document.getElementById("safety-status-label").textContent = s.label;
124
  panel.style.display = "block";
 
 
125
  panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
126
  }
127
 
 
137
  const li = document.createElement("li");
138
  li.className = "trace-step entering";
139
  li.innerHTML = `
 
140
  <div class="step-body">
141
  <span class="step-name">${step.step}</span>
142
  <span class="step-detail">${markBold(step.detail)}</span>
143
+ ${step.sub_queries ? `<ul class="sub-qs">${step.sub_queries.map(q=>`<li>"${esc(q)}"</li>`).join("")}</ul>` : ""}
144
+ ${step.sources ? `<div class="step-srcs">${step.sources.map(s=>`<span class="src-chip">${esc(s)}</span>`).join("")}</div>` : ""}
145
  <span class="step-ms">${step.ms}ms</span>
146
  </div>`;
147
  ol.appendChild(li);
 
151
  function setExcerpts(chunks) {
152
  document.getElementById("excerpts-list").innerHTML = (chunks || []).map(c =>
153
  `<div class="excerpt-card">
154
+ <div class="excerpt-src">${c.source}</div>
155
+ <div class="excerpt-txt">${c.excerpt}...</div>
156
  </div>`).join("");
157
  }
158
 
159
+ // ── Doc loading ────────────────────────────────────────────────────────────
160
  async function runSafetyCheck() {
161
  document.getElementById("safety-panel").style.display = "none";
162
  document.getElementById("safety-loading").style.display = "flex";
 
166
  const report = JSON.parse(res.data[0]);
167
  showSafetyPanel(report);
168
  } catch(e) {
169
+ showSafetyPanel({ error: "Safety check unavailable: " + e.message });
170
  } finally {
171
  document.getElementById("safety-loading").style.display = "none";
172
  }
 
179
  document.querySelectorAll(".tpl-btn").forEach(b => b.disabled = false);
180
  document.getElementById("docs-ready").style.display = "flex";
181
  document.getElementById("doc-names").textContent = names.join(" · ");
 
182
  runSafetyCheck();
183
  }
184
 
185
  document.getElementById("load-samples").addEventListener("click", async () => {
186
+ setStatus("Loading demo documents...", "info");
187
  try {
188
  const c = await gc();
189
  const res = await c.predict("/load_samples", {});
190
  const d = JSON.parse(res.data[0]);
191
  if (d.status === "ok") {
192
+ setStatus(d.files + " document(s) loaded — " + d.chunks + " chunks ready", "success");
193
  onDocsLoaded(d.names || ["demo docs"]);
194
+ } else setStatus("Error: " + d.message, "error");
195
+ } catch(e) { setStatus("Error: " + e.message, "error"); }
196
  });
197
 
 
198
  const dropzone = document.getElementById("dropzone");
199
  const fileInput = document.getElementById("file-input");
200
  dropzone.addEventListener("click", () => fileInput.click());
 
205
 
206
  async function handleFiles(files) {
207
  if (!files.length) return;
208
+ setStatus("Indexing " + files.length + " file(s)...", "info");
209
  const items = [];
210
  for (const f of files) {
211
  const buf = await f.arrayBuffer();
 
217
  const res = await c.predict("/ingest", { files_json: JSON.stringify(items) });
218
  const d = JSON.parse(res.data[0]);
219
  if (d.status === "ok") {
220
+ setStatus(d.files + " file(s) indexed — " + d.chunks + " chunks", "success");
221
  onDocsLoaded(d.names || items.map(i => i.name));
222
+ } else setStatus("Error: " + (d.message || "Unknown error"), "error");
223
+ } catch(e) { setStatus("Error: " + e.message, "error"); }
224
  }
225
 
226
  // ── Ask ────────────────────────────────────────────────────────────────────
 
232
  clearTrace();
233
 
234
  appendBubble("user", question.replace(/</g,"&lt;"));
235
+ const thinkId = "think-" + Date.now();
236
+ appendBubble("bot", '<span class="dots"><span></span><span></span><span></span></span>', thinkId);
 
237
 
238
  let finalAnswer = "", structuredData = null, chunks = [];
239
 
 
242
  for await (const msg of c.submit("/ask_stream", { question })) {
243
  if (!msg.data) continue;
244
  const ev = JSON.parse(msg.data[0]);
245
+ if (ev.type === "trace_step") addTraceStep(ev.step);
246
+ else if (ev.type === "answer") { finalAnswer = ev.answer; structuredData = ev.structured_data; chunks = ev.chunks || []; }
247
+ else if (ev.type === "done" && ev.trace_url) {
 
 
 
248
  const lnk = document.getElementById("trace-url-link");
249
  lnk.href = ev.trace_url; lnk.style.display = "inline-flex";
250
+ }
251
+ else if (ev.type === "error") finalAnswer = ev.message;
252
  }
253
  } catch(_) {
 
254
  try {
255
  const c2 = await gc();
256
  const res = await c2.predict("/ask", { question });
257
  const d = JSON.parse(res.data[0]);
258
+ if (d.error) { finalAnswer = d.error; }
259
+ else { finalAnswer = d.answer || ""; structuredData = d.structured_data; chunks = d.chunks || []; (d.trace||[]).forEach(addTraceStep); }
260
+ } catch(e2) { finalAnswer = e2.message; }
 
 
 
 
 
 
 
 
 
261
  }
262
 
 
263
  const wrap = document.getElementById(thinkId);
264
  if (wrap) {
265
  const b = wrap.querySelector(".bubble");
266
  const tbl = structuredData ? renderTable(structuredData) : "";
267
+ // If structured data rendered a table, suppress the raw JSON text answer
268
+ const displayText = (tbl && structuredData) ? "" : finalAnswer;
269
  b.innerHTML = `<div class="answer-txt"></div>${tbl}`;
270
+ if (displayText) await typewrite(b.querySelector(".answer-txt"), displayText);
271
  }
272
  setExcerpts(chunks);
 
273
  inp.disabled = false;
274
  document.getElementById("ask-btn").disabled = false;
275
  inp.focus();
 
282
  });
283
 
284
  document.querySelectorAll(".tpl-btn").forEach(btn =>
285
+ btn.addEventListener("click", () => { document.getElementById("ask-input").value = btn.dataset.q; ask(btn.dataset.q); }));
 
 
 
286
 
287
  document.getElementById("clear-btn").addEventListener("click", () => {
288
  document.getElementById("chat").innerHTML =
289
+ `<div class="welcome"><div class="welcome-logo">&#x1FA7A;</div>
290
  <p>Load patient documents, then ask any question.<br/>
291
  <em>Try: "Do any medications interact with the patient's allergies?"</em></p></div>`;
292
  clearTrace();
 
295
  document.getElementById("theme-btn").addEventListener("click", () => {
296
  document.documentElement.classList.toggle("dark");
297
  document.getElementById("theme-btn").textContent =
298
+ document.documentElement.classList.contains("dark") ? "Light" : "Dark";
299
  });
300
  </script>
301
  </head>
302
  <body>
 
303
  <header>
304
  <div class="hdr">
305
  <div class="logo">
306
+ <span class="logo-icon">&#x1FA7A;</span>
307
  <div>
308
  <h1>ClinIQ</h1>
309
  <p class="tagline">On-device clinical document assistant · Qwen2.5-3B · llama.cpp · Modal</p>
 
311
  </div>
312
  <div class="hdr-right">
313
  <div class="badges">
314
+ <span class="badge b-purple">Tiny Titan 3B</span>
315
+ <span class="badge b-teal">llama.cpp</span>
316
+ <span class="badge b-blue">Modal A10G</span>
317
+ <span class="badge b-gold">Build Small 2026</span>
318
  </div>
319
+ <button id="theme-btn" class="icon-btn">Dark</button>
320
  </div>
321
  </div>
322
  </header>
323
 
324
  <div class="privacy-bar">
325
+ Privacy guarantee: Documents are indexed locally. Only relevant excerpts — never full records — reach the GPU.
326
  </div>
327
 
328
  <main>
 
329
  <aside class="sidebar">
330
 
 
331
  <div class="card">
332
+ <h2>Documents</h2>
333
  <div id="dropzone" class="dropzone">
334
+ <span class="drop-icon">&#x1F4C4;</span>
335
  <span class="drop-txt">Drop PDFs / TXT here<br/><small>or click to browse</small></span>
336
  <input type="file" id="file-input" multiple accept=".pdf,.txt" hidden/>
337
  </div>
338
+ <button id="load-samples" class="btn secondary full">Load Demo Patient Docs</button>
339
  <div id="status" class="status info">Upload documents or load demo docs to begin.</div>
340
  <div id="docs-ready" class="docs-ok" style="display:none">
341
  <span class="pulse-dot"></span>
 
343
  </div>
344
  </div>
345
 
 
346
  <div class="card safety-card-wrap">
347
  <div class="safety-card-header">
348
+ <h2>Drug Safety Check</h2>
349
+ <span class="safety-auto-badge">Auto on upload</span>
350
  </div>
351
+ <p class="card-hint">Proactive drug-allergy and interaction alerts — no question needed.</p>
352
 
353
  <div id="safety-loading" class="safety-loading" style="display:none">
354
  <div class="spinner"></div>
355
+ <span>Analysing medications and allergies...</span>
356
  </div>
357
 
358
  <div id="safety-panel" class="safety-panel" style="display:none">
359
  <div class="safety-status-bar">
360
+ <span id="safety-status-icon"></span>
361
  <span id="safety-status-label">All Clear</span>
362
  </div>
363
  <div id="safety-body"></div>
364
  </div>
365
  </div>
366
 
 
367
  <div class="card">
368
+ <h2>Smart Templates</h2>
369
  <div class="templates">
370
+ <button class="tpl-btn" disabled data-q="List all medications with doses and frequency">Medication List</button>
371
+ <button class="tpl-btn" disabled data-q="List all allergies and reactions">Allergy Summary</button>
372
+ <button class="tpl-btn" disabled data-q="What are all the diagnoses?">Diagnoses</button>
373
+ <button class="tpl-btn" disabled data-q="What follow-up appointments are scheduled?">Follow-up Plan</button>
374
+ <button class="tpl-btn" disabled data-q="Do any medications interact with the patient's allergies or other medications?">Drug Interactions</button>
375
+ <button class="tpl-btn" disabled data-q="Compare the medications listed in each document — did anything change?">Compare Across Visits</button>
376
  </div>
377
  </div>
378
 
379
  </aside>
380
 
 
381
  <div class="right-col">
382
 
 
383
  <div class="card chat-card">
384
  <div class="chat-hdr">
385
+ <h2>Clinical Q&amp;A</h2>
386
+ <button id="clear-btn" class="icon-btn sm">Clear</button>
387
  </div>
388
  <div id="chat" class="chat">
389
  <div class="welcome">
390
+ <div class="welcome-logo">&#x1FA7A;</div>
391
  <p>Load patient documents, then ask any question.<br/>
392
  <em>Try: "Do any medications interact with the patient's allergies?"</em></p>
393
  </div>
394
  </div>
395
  <div class="input-row">
396
  <textarea id="ask-input" rows="2" disabled
397
+ placeholder="Ask a clinical question... (Enter to send)"></textarea>
398
+ <button id="ask-btn" class="btn primary ask-btn" disabled>Ask</button>
 
 
 
 
 
399
  </div>
400
  </div>
401
 
 
402
  <div class="card trace-card">
403
  <div class="trace-hdr">
404
+ <h2>Agent Trace</h2>
405
+ <a id="trace-url-link" href="#" target="_blank" class="share-btn" style="display:none">View on HF Hub</a>
 
 
406
  </div>
407
  <ol id="trace-steps" class="trace-steps"></ol>
408
  <h3>Retrieved Excerpts</h3>