import { MODEL_ID, compareRankings, findRedFlags, normalizeInput, rankRows, reorderSymptoms, runsToCsv, } from "./core.js"; const worker = new Worker("./src/worker.js", { type: "module" }); const runs = []; const pending = new Map(); let requestCounter = 0; let loadInfo = null; const form = document.querySelector("#analysis-form"); const input = document.querySelector("#symptoms"); const status = document.querySelector("#status"); const results = document.querySelector("#results"); const compareButton = document.querySelector("#compare"); const downloadButton = document.querySelector("#download"); const redFlags = document.querySelector("#red-flags"); const runtime = document.querySelector("#runtime"); function setStatus(message, kind = "") { status.textContent = message; status.dataset.kind = kind; } function formatBytes(bytes) { if (!bytes) return "size unavailable"; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } function renderRows(rows) { results.innerHTML = rows .map( (row) => `
  • ${row.rank} ${row.label} ${(row.score * 100).toFixed(1)}
  • `, ) .join(""); } function showRedFlags(text) { const flags = findRedFlags(text); redFlags.hidden = flags.length === 0; redFlags.textContent = flags.length ? `Safety phrase detected (${flags.join(", ")}). This experiment cannot triage emergencies. Seek qualified medical help.` : ""; } function analyze(text, variant) { return new Promise((resolve, reject) => { const requestId = ++requestCounter; pending.set(requestId, { resolve, reject, text, variant }); worker.postMessage({ type: "analyze", requestId, text }); }); } worker.addEventListener("message", (event) => { const message = event.data; if (message.type === "loading") { runtime.textContent = `${MODEL_ID} · ${message.runtime.device} · ${message.runtime.dtype} · ${formatBytes(message.totalBytes)}`; setStatus("Downloading and loading the model. The first run may take a minute."); return; } if (message.type === "progress") { setStatus(`Loading model: ${Math.round(message.progress)}%`); return; } if (message.type === "ready") { loadInfo = message; runtime.textContent = `${MODEL_ID} · ${message.runtime.device} · ${message.runtime.dtype} · loaded in ${(message.loadMs / 1000).toFixed(1)} s`; return; } const request = pending.get(message.requestId); if (!request) return; pending.delete(message.requestId); if (message.type === "error") { request.reject(new Error(message.message)); return; } const rows = rankRows(message.result); const run = { timestamp: new Date().toISOString(), input: request.text, variant: request.variant, modelId: MODEL_ID, device: message.runtime.device, dtype: message.runtime.dtype, inferenceMs: message.inferenceMs, loadMs: loadInfo?.loadMs ?? null, results: rows, }; runs.push(run); downloadButton.disabled = false; request.resolve(run); }); form.addEventListener("submit", async (event) => { event.preventDefault(); const text = normalizeInput(input.value); if (!text) return; showRedFlags(text); form.querySelector("button").disabled = true; compareButton.disabled = true; setStatus("Analyzing locally…"); try { const run = await analyze(text, "original"); renderRows(run.results); setStatus( `Complete in ${(run.inferenceMs / 1000).toFixed(1)} s. Scores are model signals, not medical probabilities.`, "success", ); compareButton.disabled = false; } catch (error) { setStatus(`Could not run the model: ${error.message}`, "error"); } finally { form.querySelector("button").disabled = false; } }); compareButton.addEventListener("click", async () => { const originalText = normalizeInput(input.value); const reorderedText = reorderSymptoms(originalText); if (!reorderedText || reorderedText === originalText) { setStatus("Enter at least two comma-, period-, semicolon-, or line-separated symptoms."); return; } compareButton.disabled = true; setStatus(`Testing reordered input: ${reorderedText}`); try { const original = [...runs].reverse().find( (run) => run.variant === "original" && run.input === originalText, ) ?? (await analyze(originalText, "original")); const reordered = await analyze(reorderedText, "reordered"); const comparison = compareRankings(original.results, reordered.results); const changed = comparison.filter((row) => row.shift !== 0).length; renderRows(reordered.results); setStatus( `Reordered test complete: ${changed} of the original top-five labels changed rank. Download the run log to inspect both versions.`, "success", ); } catch (error) { setStatus(`Could not run the comparison: ${error.message}`, "error"); } finally { compareButton.disabled = false; } }); downloadButton.addEventListener("click", () => { const blob = new Blob([runsToCsv(runs)], { type: "text/csv" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `symptom-pattern-runs-${new Date().toISOString().slice(0, 10)}.csv`; link.click(); URL.revokeObjectURL(url); });