Spaces:
Sleeping
Sleeping
| const form = document.querySelector("#trialForm"); | |
| const statusEl = document.querySelector("#status"); | |
| const reportEl = document.querySelector("#report"); | |
| const emptyState = document.querySelector("#emptyState"); | |
| const headlineGrid = document.querySelector("#headlineGrid"); | |
| const summaryCard = document.querySelector("#summaryCard"); | |
| const actionList = document.querySelector("#actionList"); | |
| const compareTable = document.querySelector("#compareTable"); | |
| const examplesEl = document.querySelector("#examples"); | |
| const moduleList = document.querySelector("#moduleList"); | |
| const provenanceNote = document.querySelector("#provenanceNote"); | |
| const jsonOutput = document.querySelector("#jsonOutput"); | |
| const loadingState = document.querySelector("#loadingState"); | |
| const submitButton = form.querySelector('button[type="submit"]'); | |
| const sampleButton = document.querySelector("#sampleButton"); | |
| const clearButton = document.querySelector("#clearButton"); | |
| let latestReport = null; | |
| let running = false; | |
| const sample = { | |
| domain: "cancer", | |
| study_type: "INTERVENTIONAL", | |
| overall_status: "NOT_YET_RECRUITING", | |
| brief_title: "Remote Monitoring During Oncology Treatment", | |
| official_title: | |
| "A Randomized Phase 2 Trial of Remote Symptom Monitoring During Systemic Oncology Treatment", | |
| brief_summary: | |
| "This study evaluates whether remote symptom monitoring can improve follow-up completeness and early toxicity detection for adults receiving systemic oncology treatment.", | |
| conditions: "Cancer; treatment toxicity; remote monitoring", | |
| sponsor_name: "Example University Cancer Center", | |
| source_class: "OTHER", | |
| responsible_party_type: "SPONSOR", | |
| start_date: "2026-09-01", | |
| primary_completion_date: "2028-09-01", | |
| completion_date: "2029-03-01", | |
| phase: "PHASE2", | |
| primary_purpose: "TREATMENT", | |
| intervention_type: "BEHAVIORAL", | |
| intervention_model: "PARALLEL", | |
| allocation: "RANDOMIZED", | |
| masking: "NONE", | |
| enrollment: 420, | |
| enrollment_type: "ANTICIPATED", | |
| number_of_arms: 2, | |
| number_of_facilities: 12, | |
| number_of_primary_outcomes: 1, | |
| number_of_secondary_outcomes: 3, | |
| primary_outcome_title: "Completed follow-up assessment rate", | |
| primary_outcome_time_frame: "12 months", | |
| gender: "ALL", | |
| minimum_age_years: 18, | |
| maximum_age_years: "", | |
| has_dmc: false, | |
| has_us_facility: true, | |
| healthy_volunteers: false, | |
| ipd_sharing_plan: "UNDECIDED", | |
| criteria: | |
| "Inclusion Criteria: Adults with confirmed cancer diagnosis, planned systemic therapy, access to follow-up communication, and ability to complete patient-reported symptom assessments. Exclusion Criteria: inability to provide consent, concurrent investigational monitoring intervention, uncontrolled psychiatric or medical condition preventing follow-up, or enrollment in a conflicting trial.", | |
| }; | |
| form.addEventListener("submit", async (event) => { | |
| event.preventDefault(); | |
| // Guard: one request at a time. Blocks double-clicks / repeated Enter so we | |
| // never fire a duplicate (paid) LLM call for the same submit. | |
| if (running) return; | |
| running = true; | |
| setRunning(true); | |
| setStatus("Running"); | |
| try { | |
| const response = await fetch("/api/report", { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify(readForm()), | |
| }); | |
| const report = await response.json(); | |
| latestReport = report; | |
| renderReport(report); | |
| setStatus(report.ok ? "Complete" : "Needs Input", !report.ok); | |
| } catch (error) { | |
| latestReport = { ok: false, error: error.message }; | |
| renderReport(latestReport); | |
| setStatus("Error", true); | |
| } finally { | |
| running = false; | |
| setRunning(false); | |
| } | |
| }); | |
| function setRunning(on) { | |
| submitButton.disabled = on; | |
| sampleButton.disabled = on; | |
| clearButton.disabled = on; | |
| submitButton.innerHTML = on | |
| ? '<span class="btn-spinner" aria-hidden="true"></span>Running…' | |
| : "Run Preflight"; | |
| if (on) { | |
| emptyState.classList.add("hidden"); | |
| reportEl.classList.add("hidden"); | |
| loadingState.classList.remove("hidden"); | |
| } else { | |
| loadingState.classList.add("hidden"); | |
| } | |
| } | |
| document.querySelector("#sampleButton").addEventListener("click", () => { | |
| writeForm(sample); | |
| setStatus("Sample Loaded"); | |
| }); | |
| document.querySelector("#clearButton").addEventListener("click", () => { | |
| form.reset(); | |
| hideReport(); | |
| setStatus("Ready"); | |
| }); | |
| document.querySelector("#copyButton").addEventListener("click", async () => { | |
| if (!latestReport) return; | |
| await navigator.clipboard.writeText(JSON.stringify(latestReport, null, 2)); | |
| setStatus("Copied"); | |
| }); | |
| function readForm() { | |
| const data = Object.fromEntries(new FormData(form).entries()); | |
| for (const key of ["has_dmc", "has_us_facility", "healthy_volunteers"]) { | |
| data[key] = form.elements[key].checked; | |
| } | |
| return data; | |
| } | |
| function writeForm(values) { | |
| for (const [key, value] of Object.entries(values)) { | |
| const field = form.elements[key]; | |
| if (!field) continue; | |
| if (field.type === "checkbox") field.checked = Boolean(value); | |
| else field.value = value; | |
| } | |
| } | |
| function renderReport(report) { | |
| emptyState.classList.add("hidden"); | |
| reportEl.classList.remove("hidden"); | |
| jsonOutput.textContent = JSON.stringify(report, null, 2); | |
| if (!report.ok) { | |
| renderErrorState(report); | |
| return; | |
| } | |
| const output = report.final_output; | |
| const evidence = report.historical_evidence; | |
| renderHeadline(output.headline); | |
| renderSummary(output); | |
| renderActions(output.actions); | |
| renderCompare(output.key_numbers); | |
| renderExamples(evidence.examples); | |
| renderModules(output.module_statuses); | |
| provenanceNote.textContent = output.model_status || ""; | |
| } | |
| function renderErrorState(report) { | |
| headlineGrid.innerHTML = ""; | |
| compareTable.innerHTML = ""; | |
| examplesEl.innerHTML = ""; | |
| moduleList.innerHTML = ""; | |
| provenanceNote.textContent = ""; | |
| summaryCard.replaceChildren(paragraph("The protocol entry is incomplete — fix the items below and run again.")); | |
| const errors = | |
| report.trial_feature_profile?.validation?.errors || [report.error || "Report failed."]; | |
| actionList.innerHTML = ""; | |
| for (const err of errors) { | |
| actionList.appendChild(actionItem({ severity: "review", text: err })); | |
| } | |
| } | |
| /* ---------- tier 1 ---------- */ | |
| function renderHeadline(items) { | |
| headlineGrid.innerHTML = ""; | |
| for (const item of items || []) { | |
| const card = document.createElement("div"); | |
| card.className = `stat tone-${item.tone || "neutral"}`; | |
| card.innerHTML = ` | |
| <div class="stat-label"></div> | |
| <div class="stat-value"></div> | |
| <div class="stat-sub"></div> | |
| <span class="chip"></span>`; | |
| card.querySelector(".stat-label").textContent = item.label; | |
| card.querySelector(".stat-value").textContent = item.value ?? "NA"; | |
| card.querySelector(".stat-sub").textContent = item.sub || ""; | |
| card.querySelector(".chip").textContent = item.provenance || ""; | |
| headlineGrid.appendChild(card); | |
| } | |
| } | |
| /* ---------- tier 2 ---------- */ | |
| function renderSummary(output) { | |
| summaryCard.innerHTML = ""; | |
| if (output.takeaway) { | |
| const lead = document.createElement("p"); | |
| lead.className = "takeaway"; | |
| lead.textContent = output.takeaway; | |
| summaryCard.appendChild(lead); | |
| } | |
| for (const line of output.summary || []) { | |
| summaryCard.appendChild(paragraph(line)); | |
| } | |
| if (output.summary_warning) { | |
| const warn = document.createElement("p"); | |
| warn.className = "summary-source"; | |
| warn.textContent = output.summary_warning; | |
| summaryCard.appendChild(warn); | |
| } | |
| } | |
| function renderActions(actions) { | |
| actionList.innerHTML = ""; | |
| for (const action of actions || []) { | |
| actionList.appendChild(actionItem(action)); | |
| } | |
| } | |
| function actionItem(action) { | |
| const li = document.createElement("li"); | |
| li.className = `action sev-${action.severity || "review"}`; | |
| const marker = document.createElement("span"); | |
| marker.className = "marker"; | |
| const text = document.createElement("span"); | |
| text.className = "text"; | |
| text.textContent = action.text; | |
| const tag = document.createElement("span"); | |
| tag.className = "tag"; | |
| tag.textContent = { review: "design", completeness: "fill-in", ok: "ok" }[action.severity] || "note"; | |
| li.append(marker, text, tag); | |
| return li; | |
| } | |
| /* ---------- tier 3 ---------- */ | |
| function renderCompare(numbers) { | |
| const rows = [ | |
| ["Enrollment", numbers.planned_enrollment, numbers.comparator_median_enrollment], | |
| ["Facilities", numbers.planned_facilities, numbers.comparator_median_facilities], | |
| ["Arms", numbers.planned_arms, numbers.comparator_median_arms], | |
| ["Primary outcomes", numbers.planned_primary_outcomes, numbers.comparator_median_primary_outcomes], | |
| ["Secondary outcomes", numbers.planned_secondary_outcomes, numbers.comparator_median_secondary_outcomes], | |
| ]; | |
| let body = ""; | |
| for (const [label, planned, median] of rows) { | |
| body += `<tr><td>${label}</td><td class="num">${fmt(planned)}</td><td class="num">${fmt(median)}</td><td class="num">${ratio(planned, median)}</td></tr>`; | |
| } | |
| compareTable.innerHTML = ` | |
| <thead><tr> | |
| <th>Metric</th><th class="num">Your plan</th><th class="num">Comparator median</th><th class="num">Ratio</th> | |
| </tr></thead> | |
| <tbody>${body}</tbody>`; | |
| } | |
| function renderExamples(examples) { | |
| if (!examples || !examples.length) { | |
| examplesEl.textContent = "No comparator examples returned."; | |
| return; | |
| } | |
| const rows = examples | |
| .map( | |
| (e) => `<tr> | |
| <td>${e.nct_id ?? "NA"}</td> | |
| <td>${e.brief_title ?? "NA"}</td> | |
| <td>${e.phase ?? "NA"}</td> | |
| <td class="num">${fmt(e.enrollment)}</td> | |
| <td class="num">${fmt(e.arms)}</td> | |
| <td class="${e.published ? "pub-yes" : "pub-no"}">${e.published ? "Yes" : "No"}</td> | |
| </tr>` | |
| ) | |
| .join(""); | |
| const table = document.createElement("table"); | |
| table.className = "example-table"; | |
| table.innerHTML = ` | |
| <thead><tr> | |
| <th>NCT ID</th><th>Title</th><th>Phase</th><th class="num">Enrollment</th><th class="num">Arms</th><th>Published</th> | |
| </tr></thead><tbody>${rows}</tbody>`; | |
| const wrap = document.createElement("div"); | |
| wrap.className = "table-scroll"; | |
| wrap.appendChild(table); | |
| examplesEl.replaceChildren(wrap); | |
| } | |
| function renderModules(modules) { | |
| moduleList.innerHTML = ""; | |
| for (const [name, d] of Object.entries(modules || {})) { | |
| const item = document.createElement("div"); | |
| item.className = "module-item"; | |
| const title = document.createElement("strong"); | |
| title.textContent = name.replaceAll("_", " "); | |
| const badge = document.createElement("span"); | |
| badge.className = `sel-badge sel-${d.selection || "primary"}`; | |
| badge.textContent = d.selection || "primary"; | |
| const meta = document.createElement("span"); | |
| meta.className = "meta"; | |
| meta.textContent = `${d.capability || ""} · ${d.language || "?"} · ${d.status || "?"}`; | |
| const desc = document.createElement("p"); | |
| desc.textContent = d.selection === "skipped" && d.reason ? d.reason : d.description || ""; | |
| item.append(title, badge, meta, desc); | |
| moduleList.appendChild(item); | |
| } | |
| } | |
| /* ---------- helpers ---------- */ | |
| function hideReport() { | |
| reportEl.classList.add("hidden"); | |
| emptyState.classList.remove("hidden"); | |
| latestReport = null; | |
| } | |
| function setStatus(text, isError = false) { | |
| statusEl.textContent = text; | |
| statusEl.classList.toggle("error", isError); | |
| } | |
| function paragraph(text) { | |
| const p = document.createElement("p"); | |
| p.textContent = text; | |
| return p; | |
| } | |
| function fmt(value) { | |
| if (value === null || value === undefined || value === "") return "NA"; | |
| return value; | |
| } | |
| function ratio(planned, median) { | |
| const a = Number(planned); | |
| const b = Number(median); | |
| if (!a || !b) return "—"; | |
| const r = a / b; | |
| const cls = r >= 1.5 ? "delta-high" : r <= 0.67 ? "delta-low" : ""; | |
| return `<span class="${cls}">${r.toFixed(2)}×</span>`; | |
| } | |
| writeForm(sample); | |