"use client";
import { useState, useEffect, useRef } from "react";
import { startWorkflow, streamWorkflow, screenPatient, getGraphPatients } from "@/lib/api";
import { ClipboardCheck, CheckCircle, XCircle, AlertCircle, Loader2, ChevronRight, GitBranch } from "lucide-react";
import { clsx } from "clsx";
const FALLBACK_PATIENTS = ["P001", "P002", "P003", "P004", "P005"];
const QUICK_TRIALS = [
{ id: "NCT04889131", label: "NCT04889131 – HER2+ Breast Cancer" },
{ id: "NCT05123456", label: "NCT05123456 – Immunotherapy Combo" },
{ id: "NCT05456789", label: "NCT05456789 – BRCA2 Prostate" },
{ id: "NCT06112233", label: "NCT06112233 – EGFR NSCLC" },
{ id: "NCT05334455", label: "NCT05334455 – MSI-H Colorectal" },
];
const WORKFLOW_STATES = [
"PENDING", "INGESTING", "PARSING_PROTOCOL", "MATCHING", "SCORING", "RECRUITING", "COMPLETED",
];
function StateTracker({ events }: { events: any[] }) {
const stateSet = new Set(events.map((e: any) => e.state));
return (
{WORKFLOW_STATES.map((state, i) => {
const done = stateSet.has(state);
const isCurrent = events[events.length - 1]?.state === state;
return (
{state}
{i < WORKFLOW_STATES.length - 1 && }
);
})}
);
}
function ScoreBar({ score }: { score: number }) {
const pct = Math.round(score * 100);
const color = pct >= 80 ? "bg-emerald-500" : pct >= 60 ? "bg-amber-500" : "bg-red-500";
return (
= 80 ? "text-emerald-600" : pct >= 60 ? "text-amber-600" : "text-red-600")}>
{pct}%
);
}
function CriterionRow({ criterion, met, triggered, confidence, note }: any) {
const pass = met === true || triggered === false;
const fail = met === false || triggered === true;
return (
{pass ?
: fail ?
:
}
{criterion}
{note &&
{note}
}
{confidence && (
{confidence}
)}
);
}
function MatchPath({ path }: { path: any[] }) {
if (!path?.length) return null;
return (
Graph Match Path
{path.map((node: any, i: number) => (
{node.from?.split(":")[1] ?? node.from}
—[{node.rel}]→
{node.to?.split(":")[1] ?? node.to}
{node.note && ({node.note})}
))}
);
}
export default function ScreeningPage() {
const [patientId, setPatientId] = useState("P001");
const [nctId, setNctId] = useState(QUICK_TRIALS[0].id);
const [customNct, setCustomNct] = useState("");
const [useCustom, setUseCustom] = useState(false);
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState<"screen" | "workflow">("screen");
const [result, setResult] = useState(null);
const [workflowResult, setWorkflowResult] = useState(null);
const [streamEvents, setStreamEvents] = useState([]);
const [streamDone, setStreamDone] = useState(false);
const [error, setError] = useState("");
const [graphPatients, setGraphPatients] = useState([]);
const cleanupRef = useRef<(() => void) | null>(null);
useEffect(() => {
getGraphPatients(undefined, 500).then((d) => setGraphPatients(d.patients)).catch(() => {});
return () => { cleanupRef.current?.(); };
}, []);
const effectiveNct = useCustom ? customNct : nctId;
const handleScreen = async () => {
if (!patientId.trim()) { setError("Patient ID is required"); return; }
if (!effectiveNct.trim()) { setError("NCT ID is required"); return; }
setLoading(true);
setError("");
setResult(null);
try {
const data = await screenPatient(patientId.trim(), effectiveNct.trim());
setResult(data);
} catch (e: any) {
setError(e.message);
}
setLoading(false);
};
const handleWorkflow = async () => {
if (!patientId.trim()) { setError("Patient ID is required"); return; }
cleanupRef.current?.();
setLoading(true);
setError("");
setWorkflowResult(null);
setStreamEvents([]);
setStreamDone(false);
try {
const { workflow_id } = await startWorkflow(patientId.trim());
const stop = streamWorkflow(
workflow_id,
(evt) => setStreamEvents((prev) => [...prev, evt]),
() => { setStreamDone(true); setLoading(false); },
);
cleanupRef.current = stop;
} catch (e: any) {
setError(e.message);
setLoading(false);
}
};
return (
Patient Screening
AI-powered eligibility assessment using FHIR R4 patient data and A2A orchestration
{/* Mode tabs */}
{(["screen", "workflow"] as const).map((m) => (
))}
{error && (
{error}
)}
{/* Single screen result */}
{result && (
Eligibility Score
{result.eligible ? "ELIGIBLE" : "NOT ELIGIBLE"}
{result.summary && (
{result.summary}
)}
{result.risk_flags?.length > 0 && (
{result.risk_flags.map((f: string, i: number) => (
{f}
))}
)}
{result.inclusion_results?.length > 0 && (
Inclusion Criteria
{result.inclusion_results.map((c: any, i: number) => )}
)}
{result.exclusion_results?.length > 0 && (
Exclusion Criteria
{result.exclusion_results.map((c: any, i: number) => )}
)}
{result.match_path?.length > 0 && (
Graph Explainability
)}
)}
{/* Streaming A2A workflow */}
{(streamEvents.length > 0 || loading) && mode === "workflow" && (
A2A Pipeline — Live
{!streamDone && }
{streamDone && Complete}
{streamEvents.map((evt: any, i: number) => (
{evt.state}
{evt.message}
{evt.data?.eligible_count !== undefined && (
{evt.data.eligible_count} eligible
)}
))}
{/* Final summary when complete */}
{streamDone && (() => {
const final = streamEvents[streamEvents.length - 1];
if (!final || !["COMPLETED", "FAILED"].includes(final.state)) return null;
return (
{final.state === "COMPLETED" ? "Pipeline Complete" : "Pipeline Failed"}
{final.state === "COMPLETED" && (
{final.eligible_trials ?? 0}
Eligible Trials
{final.total_evaluated ?? 0}
Evaluated
{final.recruitment_records ?? 0}
Outreach Generated
)}
{final.error && (
{final.error}
)}
);
})()}
)}
);
}