// Empty string = relative URLs (Docker/HF Spaces: Nginx routes /api/* to FastAPI) // "http://localhost:8000" = direct for local dev without Docker const BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; async function req(path: string, options?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { headers: { "Content-Type": "application/json" }, ...options, }); if (!res.ok) { const err = await res.text(); throw new Error(`API ${res.status}: ${err}`); } return res.json(); } // ── Patients ────────────────────────────────────────────────────────────────── export const getPatients = () => req<{ patients: any[]; total: number }>("/api/v1/patients"); export const getPatient = (id: string) => req(`/api/v1/patients/${id}`); export const getPatientFhir = (id: string) => req(`/api/v1/patients/${id}/fhir`); // ── Trials ──────────────────────────────────────────────────────────────────── export const searchTrials = (condition: string, phase?: string, pageSize = 20) => { const params = new URLSearchParams({ condition, page_size: String(pageSize) }); if (phase) params.set("phase", phase); return req<{ trials: any[]; total: number; condition: string }>(`/api/v1/trials/search?${params}`); }; export const getTrial = (nctId: string) => req(`/api/v1/trials/${nctId}`); export const getTrialEligiblePatients = (nctId: string) => req(`/api/v1/trials/${nctId}/eligible-patients`); export const getTrialIntelligence = (nctId: string) => req(`/api/v1/trials/${nctId}/intelligence`); // ── Matching ────────────────────────────────────────────────────────────────── export const matchPatientToTrials = (patientId: string, condition?: string, topN = 5) => { const params = new URLSearchParams({ top_n: String(topN) }); if (condition) params.set("condition", condition); return req<{ patient_id: string; matches: any[]; total: number }>(`/api/v1/patients/${patientId}/match-trials?${params}`); }; export const screenPatient = (patientId: string, nctId: string) => req(`/api/v1/patients/${patientId}/screen/${nctId}`, { method: "POST" }); // ── A2A Workflow ────────────────────────────────────────────────────────────── export const runWorkflow = (patientId: string, nctId?: string, condition?: string) => req("/api/v1/workflow/run", { method: "POST", body: JSON.stringify({ patient_id: patientId, nct_id: nctId, condition }), }); export const getWorkflowStatus = (workflowId: string) => req(`/api/v1/workflow/${workflowId}/status`); export const listWorkflows = () => req<{ workflows: any[] }>("/api/v1/workflows"); // ── Recruitment ─────────────────────────────────────────────────────────────── export const getKanbanBoard = () => req>("/api/v1/recruitment/board"); export const getRecruitmentRecords = () => req<{ records: any[] }>("/api/v1/recruitment/records"); export const createRecruitmentRecord = (data: { patient_id: string; nct_id: string; trial_title: string; match_score: number }) => req("/api/v1/recruitment/records", { method: "POST", body: JSON.stringify(data) }); export const updateRecordStatus = (recordId: string, status: string) => req(`/api/v1/recruitment/records/${recordId}/status`, { method: "PATCH", body: JSON.stringify({ status }) }); export const generateOutreach = (data: { patient_id: string; nct_id: string; trial_title: string; channel: string }) => req("/api/v1/recruitment/outreach", { method: "POST", body: JSON.stringify(data) }); // ── Analytics ───────────────────────────────────────────────────────────────── export const getKPIs = () => req("/api/v1/analytics/kpi"); export const getEnrollmentFunnel = (trialId?: string) => { const params = trialId ? `?trial_id=${trialId}` : ""; return req<{ funnel: any[] }>(`/api/v1/analytics/funnel${params}`); }; export const getSitePerformance = () => req<{ sites: any[] }>("/api/v1/analytics/sites"); export const getDemographics = (trialId?: string) => { const params = trialId ? `?trial_id=${trialId}` : ""; return req(`/api/v1/analytics/demographics${params}`); }; export const getTimeline = (days = 30) => req<{ timeline: any[] }>(`/api/v1/analytics/timeline?days=${days}`); export const getMapData = () => req<{ sites: any[]; patient_clusters: any[] }>("/api/v1/map/data"); export const getGraphStats = () => req("/api/v1/graph/stats"); export const getGraphPatients = (condition?: string, limit = 200) => { const params = new URLSearchParams({ limit: String(limit) }); if (condition) params.set("condition", condition); return req<{ patients: any[]; total: number }>(`/api/v1/graph/patients?${params}`); }; // ── Clinical Intake ─────────────────────────────────────────────────────────── export interface IntakeLabs { hemoglobin?: number; // g/dL wbc?: number; // ×10⁹/L anc?: number; // ×10⁹/L platelets?: number; // ×10⁹/L creatinine?: number; // μmol/L egfr?: number; // mL/min/1.73m² bilirubin?: number; // μmol/L alt?: number; // U/L ast?: number; // U/L albumin?: number; // g/dL } export interface IntakePayload { condition: string; age?: number; sex?: string; ecog?: number; stage?: string; biomarkers?: string[]; labs?: IntakeLabs; prior_chemo?: boolean; prior_radiation?: boolean; prior_surgery?: boolean; medications?: string[]; save_to_graph?: boolean; } export const submitIntake = (data: IntakePayload) => req<{ condition: string; matches: any[]; total: number; patient_id?: string }>( "/api/v1/intake/match", { method: "POST", body: JSON.stringify(data) } ); export const getBiomarkerRegistry = () => req<{ biomarkers: { id: string; label: string }[] }>("/api/v1/intake/biomarkers"); // ── Graph RAG ───────────────────────────────────────────────────────────────── export const graphQuery = (question: string) => req<{ response: string }>("/api/v1/graph/query", { method: "POST", body: JSON.stringify({ question }) }); // ── Streaming A2A Workflow ──────────────────────────────────────────────────── export const startWorkflow = (patientId: string, nctId?: string, condition?: string) => req<{ workflow_id: string; status: string; stream_url: string }>("/api/v1/workflow/start", { method: "POST", body: JSON.stringify({ patient_id: patientId, nct_id: nctId, condition }), }); export const streamWorkflow = (workflowId: string, onEvent: (evt: any) => void, onDone: () => void) => { const url = `${BASE}/api/v1/workflow/${workflowId}/stream`; const es = new EventSource(url); es.onmessage = (e) => { if (e.data === "[DONE]") { es.close(); onDone(); return; } try { onEvent(JSON.parse(e.data)); } catch {} }; es.onerror = () => { es.close(); onDone(); }; return () => es.close(); }; // ── Consent & Scheduling ───────────────────────────────────────────────────── export const getConsents = (patientId?: string) => { const params = patientId ? `?patient_id=${patientId}` : ""; return req<{ consents: any[] }>(`/api/v1/consent${params}`); }; export const getConsentStats = () => req("/api/v1/consent/stats"); export const updateConsentStatus = (consentId: string, status: string, notes?: string) => req(`/api/v1/consent/${consentId}/status`, { method: "PATCH", body: JSON.stringify({ status, notes }), }); export const getAppointments = (patientId?: string) => { const params = patientId ? `?patient_id=${patientId}` : ""; return req<{ appointments: any[] }>(`/api/v1/appointments${params}`); }; export const confirmAppointment = (apptId: string) => req(`/api/v1/appointments/${apptId}/confirm`, { method: "PATCH" }); // ── Health ──────────────────────────────────────────────────────────────────── export const getHealth = () => req("/health");