| import { getToken } from "./auth"; |
| import type { |
| CaseDetail, |
| CaseReviewData, |
| ConceptRegistry, |
| CoverageResponse, |
| DashboardSummary, |
| DataDictionary, |
| DictionaryOption, |
| Escalation, |
| ExportResult, |
| FieldChatRequest, |
| FieldChatResponse, |
| FieldDecisionContext, |
| FieldSpec, |
| IngestionJob, |
| IngestionRunRequest, |
| IngestionRunResult, |
| MappingChatRequest, |
| MappingChatResponse, |
| MappingConfirmRequest, |
| MappingReview, |
| Project, |
| ProjectList, |
| StagingEditionOption, |
| TextSourceOption, |
| TraceResponse, |
| VisualEvidence, |
| WorklistRow, |
| } from "./types"; |
|
|
| |
| |
| |
| const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000"; |
|
|
| |
| |
| |
| const API_ROOT = `${API_BASE}/api`; |
|
|
| export class UnauthorizedError extends Error { |
| constructor() { |
| super("Your session has expired or you are signed out. Log in again to run extraction or save changes."); |
| this.name = "UnauthorizedError"; |
| } |
| } |
|
|
| |
| |
| export interface LoginResult { |
| token: string; |
| username: string; |
| display_name: string; |
| role: string; |
| holds_licence: boolean; |
| } |
|
|
| |
| |
| |
| export async function login(username: string, password: string): Promise<LoginResult> { |
| const resp = await fetch(`${API_ROOT}/login`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ username, password }), |
| }); |
| if (resp.status === 401) throw new Error("Invalid username or password."); |
| if (resp.status === 409) throw new Error("This deployment runs without accounts; no login is needed."); |
| if (!resp.ok) throw new Error(`Login failed (${resp.status}).`); |
| return resp.json() as Promise<LoginResult>; |
| } |
|
|
| async function request<T>(path: string, init?: RequestInit): Promise<T> { |
| |
| const headers = new Headers(init?.headers); |
| const token = getToken(); |
| if (token) headers.set("Authorization", `Bearer ${token}`); |
|
|
| const resp = await fetch(`${API_ROOT}${path}`, { ...init, headers }); |
| if (resp.status === 401) { |
| throw new UnauthorizedError(); |
| } |
| if (!resp.ok) { |
| const body = await resp.text(); |
| throw new Error(`${init?.method ?? "GET"} ${path} failed (${resp.status}): ${body}`); |
| } |
| return resp.json() as Promise<T>; |
| } |
|
|
| |
| |
| export function fetchFields(): Promise<FieldSpec[]> { |
| return request<FieldSpec[]>("/fields"); |
| } |
|
|
| |
| |
| export function fetchConcepts(): Promise<ConceptRegistry> { |
| return request<ConceptRegistry>("/concepts"); |
| } |
|
|
| |
| |
| |
| |
| export function fetchMapping(dictionary?: string): Promise<MappingReview> { |
| const qs = dictionary ? `?dictionary=${encodeURIComponent(dictionary)}` : ""; |
| return request<MappingReview>(`/mapping${qs}`); |
| } |
|
|
| |
| |
| |
| export function fetchDictionaries(): Promise<DictionaryOption[]> { |
| return request<DictionaryOption[]>("/dictionaries"); |
| } |
|
|
| |
| |
| |
| |
| |
| export function confirmMappingEdge(req: MappingConfirmRequest): Promise<MappingReview> { |
| return request<MappingReview>("/mapping/confirm", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(req), |
| }); |
| } |
|
|
| |
| |
| |
| export function askMappingEdge(req: MappingChatRequest): Promise<MappingChatResponse> { |
| return request<MappingChatResponse>("/mapping/edge/chat", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(req), |
| }); |
| } |
|
|
| |
| |
| export function fetchProjects(): Promise<ProjectList> { |
| return request<ProjectList>("/projects"); |
| } |
|
|
| |
| |
| export function createProject(name: string): Promise<Project> { |
| return request<Project>("/projects", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ name }), |
| }); |
| } |
|
|
| export interface WorklistFilters { |
| status?: string; |
| fieldName?: string; |
| fieldStatus?: string; |
| maxConfidence?: number; |
| |
| project?: string; |
| } |
|
|
| export function fetchWorklist(filters: WorklistFilters = {}): Promise<WorklistRow[]> { |
| const params = new URLSearchParams(); |
| if (filters.status) params.set("status", filters.status); |
| if (filters.fieldName) params.set("field_name", filters.fieldName); |
| if (filters.fieldStatus) params.set("field_status", filters.fieldStatus); |
| if (filters.maxConfidence !== undefined) { |
| params.set("max_confidence", String(filters.maxConfidence)); |
| } |
| if (filters.project) params.set("project", filters.project); |
| const qs = params.toString(); |
| return request<WorklistRow[]>(`/cases${qs ? `?${qs}` : ""}`); |
| } |
|
|
| export function fetchCase(caseBarcode: string): Promise<CaseDetail> { |
| return request<CaseDetail>(`/cases/${encodeURIComponent(caseBarcode)}`); |
| } |
|
|
| |
| |
| export function fetchTextSources(): Promise<TextSourceOption[]> { |
| return request<TextSourceOption[]>("/ingestion/sources"); |
| } |
|
|
| |
| |
| export function fetchBuiltinDictionary(): Promise<DataDictionary> { |
| return request<DataDictionary>("/ingestion/dictionary"); |
| } |
|
|
| |
| |
| export function fetchIngestionJobs(): Promise<IngestionJob[]> { |
| return request<IngestionJob[]>("/ingestion/jobs"); |
| } |
|
|
| |
| |
| |
| |
| export function triggerIngestion(req?: IngestionRunRequest): Promise<IngestionRunResult> { |
| return request<IngestionRunResult>("/ingestion/run", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(req ?? { source: { kind: "prepared_corpus" } }), |
| }); |
| } |
|
|
| |
| |
| export function fetchCaseReview(caseBarcode: string): Promise<CaseReviewData> { |
| return request<CaseReviewData>(`/cases/${encodeURIComponent(caseBarcode)}/review`); |
| } |
|
|
| |
| |
| export function confirmField( |
| caseBarcode: string, |
| fieldName: string, |
| decision?: FieldDecisionContext, |
| ): Promise<CaseDetail> { |
| return request<CaseDetail>( |
| `/cases/${encodeURIComponent(caseBarcode)}/fields/${encodeURIComponent(fieldName)}/confirm`, |
| { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(decision ?? {}), |
| }, |
| ); |
| } |
|
|
| |
| export function escalateField( |
| caseBarcode: string, |
| fieldName: string, |
| decision?: FieldDecisionContext, |
| ): Promise<CaseDetail> { |
| return request<CaseDetail>( |
| `/cases/${encodeURIComponent(caseBarcode)}/fields/${encodeURIComponent(fieldName)}/escalate`, |
| { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(decision ?? {}), |
| }, |
| ); |
| } |
|
|
| |
| |
| export function fetchTrace(limit = 100): Promise<TraceResponse> { |
| return request<TraceResponse>(`/trace?limit=${limit}`); |
| } |
|
|
| |
| |
| export function fetchCoverage(project?: string): Promise<CoverageResponse> { |
| const qs = project ? `?project=${encodeURIComponent(project)}` : ""; |
| return request<CoverageResponse>(`/coverage${qs}`); |
| } |
|
|
| export function fetchEscalations(project?: string): Promise<{ escalations: Escalation[] }> { |
| const qs = project ? `?project=${encodeURIComponent(project)}` : ""; |
| return request<{ escalations: Escalation[] }>(`/escalations${qs}`); |
| } |
|
|
| |
| |
| export interface LongitudinalRecord { |
| record_id: string; |
| record_type: string; |
| record_index: number; |
| days_to_followup: number | null; |
| days_to_treatment: number | null; |
| days_to_recurrence: number | null; |
| therapy_type: string | null; |
| recurrence_type: string | null; |
| } |
|
|
| |
| export function fetchLongitudinalRecords( |
| caseBarcode: string, |
| ): Promise<{ records: LongitudinalRecord[] }> { |
| return request<{ records: LongitudinalRecord[] }>( |
| `/longitudinal/records/${encodeURIComponent(caseBarcode)}`, |
| ); |
| } |
|
|
| |
| export interface LongitudinalJoin { |
| record_id: string; |
| disposition: string; |
| confirmed_by: string | null; |
| role: string | null; |
| record_type: string; |
| } |
|
|
| |
| export function fetchLongitudinalJoins( |
| caseBarcode: string, |
| ): Promise<{ joins: LongitudinalJoin[] }> { |
| return request<{ joins: LongitudinalJoin[] }>( |
| `/longitudinal/joins/${encodeURIComponent(caseBarcode)}`, |
| ); |
| } |
|
|
| |
| |
| |
| export function confirmLinkage( |
| caseBarcode: string, |
| ): Promise<{ confirmed: number; rejected: number; total_records: number; role: string | null }> { |
| return request(`/longitudinal/confirm-linkage/${encodeURIComponent(caseBarcode)}`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({}), |
| }); |
| } |
|
|
| |
| |
| export function flagLongitudinalRecord( |
| caseBarcode: string, |
| recordId: string, |
| ): Promise<{ disposition: string }> { |
| return request(`/longitudinal/join/${encodeURIComponent(caseBarcode)}`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ record_id: recordId, disposition: "rejected" }), |
| }); |
| } |
|
|
| |
| |
| export interface LongitudinalCohortSummary { |
| cases: number; |
| recurrence: { observed: number; none: number; unknown: number }; |
| survival: { dead: number; alive: number; unknown: number }; |
| treatment: { chemotherapy: number; radiation: number; hormone: number }; |
| median_follow_up_days: number | null; |
| median_overall_survival_days: number | null; |
| follow_up_days: number[]; |
| } |
|
|
| |
| export function fetchLongitudinalCohort(project?: string): Promise<LongitudinalCohortSummary> { |
| const qs = project ? `?project=${encodeURIComponent(project)}` : ""; |
| return request<LongitudinalCohortSummary>(`/longitudinal/cohort${qs}`); |
| } |
|
|
| |
| |
| |
| export function demoReset( |
| caseBarcode?: string, |
| seedEscalation = false, |
| fieldName?: string, |
| clearLongitudinal = false, |
| ): Promise<{ reset: boolean }> { |
| const params = new URLSearchParams(); |
| if (caseBarcode) params.set("case_barcode", caseBarcode); |
| if (seedEscalation) params.set("seed_escalation", "true"); |
| if (fieldName) params.set("field_name", fieldName); |
| if (clearLongitudinal) params.set("clear_longitudinal", "true"); |
| const qs = params.toString(); |
| return request<{ reset: boolean }>(`/demo/reset${qs ? `?${qs}` : ""}`, { method: "POST" }); |
| } |
|
|
| |
| |
| export function editField( |
| caseBarcode: string, |
| fieldName: string, |
| value: unknown, |
| decision?: FieldDecisionContext, |
| ): Promise<CaseDetail> { |
| return request<CaseDetail>( |
| `/cases/${encodeURIComponent(caseBarcode)}/fields/${encodeURIComponent(fieldName)}/edit`, |
| { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ value, decision }), |
| }, |
| ); |
| } |
|
|
| |
| |
| export function askFieldChat( |
| caseBarcode: string, |
| fieldName: string, |
| req: FieldChatRequest, |
| ): Promise<FieldChatResponse> { |
| return request<FieldChatResponse>( |
| `/cases/${encodeURIComponent(caseBarcode)}/fields/${encodeURIComponent(fieldName)}/chat`, |
| { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(req), |
| }, |
| ); |
| } |
|
|
| |
| |
| export function processCase(caseBarcode: string): Promise<CaseDetail> { |
| return request<CaseDetail>( |
| `/cases/${encodeURIComponent(caseBarcode)}/process`, |
| { method: "POST" }, |
| ); |
| } |
|
|
| |
| |
| |
| |
| export function fetchVisualEvidence( |
| caseBarcode: string, |
| fieldName: string, |
| ): Promise<VisualEvidence> { |
| return request<VisualEvidence>( |
| `/cases/${encodeURIComponent(caseBarcode)}/fields/${encodeURIComponent(fieldName)}/visual_evidence`, |
| ); |
| } |
|
|
| |
| |
| export function pageImageUrl(caseBarcode: string, pageNumber: number): string { |
| return `${API_ROOT}/cases/${encodeURIComponent(caseBarcode)}/pages/${pageNumber}`; |
| } |
|
|
| export function fetchDashboard(project?: string): Promise<DashboardSummary> { |
| const qs = project ? `?project=${encodeURIComponent(project)}` : ""; |
| return request<DashboardSummary>(`/dashboard${qs}`); |
| } |
|
|
| |
| |
| export function fetchExport(stagingEdition?: string, project?: string): Promise<ExportResult> { |
| const params = new URLSearchParams(); |
| if (stagingEdition) params.set("staging_edition", stagingEdition); |
| if (project) params.set("project", project); |
| const qs = params.toString(); |
| return request<ExportResult>(`/export${qs ? `?${qs}` : ""}`); |
| } |
|
|
| |
| export function fetchStagingEditions(): Promise<StagingEditionOption[]> { |
| return request<StagingEditionOption[]>("/staging_editions"); |
| } |
|
|