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 ? '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 = `
`; 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 += `