TIDE / web /app.js
hiasgnpsadgd's picture
Deploy TIDE Docker backend
33d7314 verified
Raw
History Blame Contribute Delete
11.8 kB
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);