import { useCallback } from "react"; import { environment } from "../environments/environment.ts"; import { useHttpService } from "./HttpService.tsx"; /** * The in-product help assistant., slices 4 and 5. * * `fhirBaseApi`, NOT `apiUrl` - `/R4/assistant/ask` is declared in `templates/fhir/02-fhir-base.yaml`, * and that template's gateway is the one `environment.fhirBaseApi` resolves to. Already ends in `/R4`; * do not append another. * * THIS COMMENT PREVIOUSLY SAID `apiUrl` AND WAS WRONG, WHICH IS WORTH KEEPING RATHER THAN QUIETLY * CORRECTING. It named the right template and then drew the wrong host from it - `02-fhir-base.yaml` * serves `fhir-base....`, not `api....`. **A confident comment asserting the exact thing it got wrong is * worse than no comment**, because the next reader checks it and moves on. * * HOW IT WAS CAUGHT, and it is the cheap probe worth reusing: POST the path to each candidate host * unauthenticated and read the STATUS, not the body. * * api.staging... 403 <- empty body: API Gateway's "no such resource", before any authorizer * fhir-base.staging... 401 <- the route EXISTS and the authorizer refused an anonymous caller * fhir-clinical.staging... 403 * * **401 means the route is there; a bare 403 means it is not.** In a browser the wrong host surfaces * only as a failed CORS preflight and `net::ERR_FAILED`, which reads like a network or CORS problem and * sends you to the wrong place entirely - exactly what `ImagingFrameService` documents about this trap. */ const API_URL = environment.fhirBaseApi; /** A place in the product the answer points at. The server resolved it; the client only renders it. */ export interface AssistantDestinationLink { id: string; label: string; path: string; /** * Resolved by the server from the record it read. Only ever ids it fetched itself, never anything * the model wrote - it picks a numbered entry and the server turns that into this. */ params?: { visitId?: string }; } export interface AssistantReply { answer: string; /** * `clinical` when the question needed a practitioner, `unknown` when the assistant could not answer. * Worth rendering differently from an ordinary answer: a refusal is a real outcome the reader * should recognise as deliberate, not as the assistant trailing off. */ refusal: "clinical" | "unknown" | null; destinations: AssistantDestinationLink[]; } /** * THE PAGE IS SENT AS A ROUTE AND NOTHING ELSE. The server drops it unless it is route-shaped, so * anything else is discarded there too - but sending a title, a patient name or a search term "for * context" is the client half of the same mistake, and it is the half that is easy to add later. */ export interface AssistantQuestion { question: string; page?: string; } export const useAssistantService = () => { const { post } = useHttpService(); const ask = useCallback( (input: AssistantQuestion): Promise => post(`${API_URL}/assistant/ask`, input), [post], ); return { ask }; };