Spaces:
Sleeping
Sleeping
Fix: suppress JSON text when table renders, escape HTML in trace, fix source chips
Browse files- 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 |
-
|
|
|
|
| 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-
|
| 82 |
panel.className = "safety-panel review";
|
|
|
|
|
|
|
| 83 |
panel.style.display = "block";
|
| 84 |
return;
|
| 85 |
}
|
| 86 |
|
| 87 |
const statusMap = {
|
| 88 |
-
SAFE: { cls: "safe",
|
| 89 |
-
REVIEW_NEEDED: { cls: "review",
|
| 90 |
-
URGENT: { cls: "danger",
|
| 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
|
|
|
|
| 98 |
return `<div class="safety-alert ${cls}">
|
| 99 |
-
<div class="alert-title">${
|
| 100 |
<div class="alert-detail">${a.detail}</div>
|
| 101 |
-
<div class="alert-rec"><strong>
|
| 102 |
</div>`;
|
| 103 |
}).join("");
|
| 104 |
|
| 105 |
const medsHtml = report.medications?.length
|
| 106 |
-
? `<details class="extracted-details"><summary>
|
| 107 |
${renderTable(report.medications)}</details>` : "";
|
| 108 |
const allergyHtml = report.allergies?.length
|
| 109 |
-
? `<details class="extracted-details"><summary>
|
| 110 |
${renderTable(report.allergies)}</details>` : "";
|
| 111 |
|
| 112 |
body.innerHTML = alertsHtml + medsHtml + allergyHtml
|
| 113 |
-
|| `<p class="no-alerts">No safety concerns found
|
| 114 |
|
| 115 |
panel.className = `safety-panel ${s.cls}`;
|
| 116 |
-
document.getElementById("safety-status-icon").textContent =
|
| 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">
|
| 152 |
-
<div class="excerpt-txt">${c.excerpt}
|
| 153 |
</div>`).join("");
|
| 154 |
}
|
| 155 |
|
| 156 |
-
// ──
|
| 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
|
| 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(
|
| 191 |
onDocsLoaded(d.names || ["demo docs"]);
|
| 192 |
-
} else setStatus("
|
| 193 |
-
} catch(e) { setStatus("
|
| 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(
|
| 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(
|
| 220 |
onDocsLoaded(d.names || items.map(i => i.name));
|
| 221 |
-
} else setStatus("
|
| 222 |
-
} catch(e) { setStatus("
|
| 223 |
}
|
| 224 |
|
| 225 |
// ── Ask ────────────────────────────────────────────────────────────────────
|
|
@@ -231,9 +232,8 @@
|
|
| 231 |
clearTrace();
|
| 232 |
|
| 233 |
appendBubble("user", question.replace(/</g,"<"));
|
| 234 |
-
const thinkId
|
| 235 |
-
|
| 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")
|
| 246 |
-
else if (ev.type === "answer") {
|
| 247 |
-
|
| 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 |
-
}
|
|
|
|
| 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 =
|
| 262 |
-
else {
|
| 263 |
-
|
| 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"),
|
| 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-
|
| 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">
|
| 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">
|
| 331 |
-
<span class="badge b-teal">
|
| 332 |
-
<span class="badge b-blue">
|
| 333 |
-
<span class="badge b-gold">
|
| 334 |
</div>
|
| 335 |
-
<button id="theme-btn" class="icon-btn">
|
| 336 |
</div>
|
| 337 |
</div>
|
| 338 |
</header>
|
| 339 |
|
| 340 |
<div class="privacy-bar">
|
| 341 |
-
|
| 342 |
</div>
|
| 343 |
|
| 344 |
<main>
|
| 345 |
-
<!-- ── LEFT SIDEBAR ── -->
|
| 346 |
<aside class="sidebar">
|
| 347 |
|
| 348 |
-
<!-- Upload -->
|
| 349 |
<div class="card">
|
| 350 |
-
<h2>
|
| 351 |
<div id="dropzone" class="dropzone">
|
| 352 |
-
<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">
|
| 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>
|
| 368 |
-
<span class="safety-auto-badge">Auto
|
| 369 |
</div>
|
| 370 |
-
<p class="card-hint">Proactive drug-allergy
|
| 371 |
|
| 372 |
<div id="safety-loading" class="safety-loading" style="display:none">
|
| 373 |
<div class="spinner"></div>
|
| 374 |
-
<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">
|
| 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>
|
| 389 |
<div class="templates">
|
| 390 |
-
<button class="tpl-btn" disabled data-q="List all medications with doses and frequency">
|
| 391 |
-
<button class="tpl-btn" disabled data-q="List all allergies and reactions">
|
| 392 |
-
<button class="tpl-btn" disabled data-q="What are all the diagnoses?">
|
| 393 |
-
<button class="tpl-btn" disabled data-q="What follow-up appointments are scheduled?">
|
| 394 |
-
<button class="tpl-btn" disabled data-q="Do any medications interact with the patient's allergies or other medications?">
|
| 395 |
-
<button class="tpl-btn" disabled data-q="Compare the medications listed in each document — did anything change?">
|
| 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>
|
| 408 |
-
<button id="clear-btn" class="icon-btn sm">
|
| 409 |
</div>
|
| 410 |
<div id="chat" class="chat">
|
| 411 |
<div class="welcome">
|
| 412 |
-
<div class="welcome-
|
| 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
|
| 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>
|
| 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,"&").replace(/</g,"<").replace(/>/g,">");
|
| 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,"<"));
|
| 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">🩺</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">🩺</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">📄</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&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">🩺</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>
|