import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import type { ProposalKind } from '../utils/recordProposals'; /** * What a saved visit note offers to put on the patient's record. * * SPENCER, 2026-09-04: *"this needs to be able to update anything in a patient's clinical record since * this IS the ui for the practitioner to do that."* So the list this fetches is a door into the record, * not a convenience, and every kind that has a form can appear in it. * * THE ROUTE LIVES ON THE BASE API, not the clinical one - it reads no FHIR resource. It is handed the * text the clinician just saved and returns suggestions; every write that follows goes out through the * ordinary resource route for that kind. * * A PROVIDER WITH A TOLERANT ACCESSOR, and that shape is load-bearing rather than convention. The note * editor's own rule, learned on the scribe: *"an add-on must never be able to take down note writing."* * Reaching for `useHttpService` directly from the editor would make a surface without an auth provider * throw where it used to render - which is exactly what happened, and what the editor's existing tests * caught. Absent the provider there are simply no proposals. */ const RECORD_PROPOSAL_API_URL = environment.fhirBaseApi; export interface ProposedRecordItem { kind: ProposalKind; /** What the clinician reads first - "Lisinopril 10 mg". */ title: string; /** What the note supplied, keyed by the form path the value belongs at. */ prefilled: Record; } export interface RecordProposalResult { proposals: ProposedRecordItem[]; /** Entries naming a kind with no form. A quality signal, not something to show. */ droppedUnknownKind: number; /** Proposals past the server's cap. Counted rather than silently discarded. */ droppedOverCap: number; /** The note could not be read, so the empty list is unknown rather than "nothing found". */ failed?: boolean; } /** * Why the note is being read. The route reads a note after a save ('save') only for a practice with the * AI Notes add-on; Check the note ('check') is the on-demand door the deck offers without it. */ export type NoteReadOrigin = 'check' | 'save'; export interface RecordProposalApi { proposeFromNote: (text: string, origin?: NoteReadOrigin) => Promise; } const NOTHING: RecordProposalResult = { proposals: [], droppedUnknownKind: 0, droppedOverCap: 0 }; const RecordProposalContext = createContext(undefined); /** * The accessor, tolerant of an absent provider. * * A DEGRADATION RATHER THAN A BREAK, and the direction matters: the alternative is a missing provider * taking down the surface a clinician writes notes on, to add a list of suggestions to it. */ export const useOptionalRecordProposalService = (): RecordProposalApi | undefined => useContext(RecordProposalContext); export const RecordProposalProvider = ({ children }: { children: ReactNode }) => { const { post } = useHttpService(); /** * Read the note and return what it offers. * * Never throws: a failed request answers `failed`, stated beside the saved note. */ const proposeFromNote = useCallback( async (text: string, origin: NoteReadOrigin = 'check'): Promise => { if (!text.trim()) return NOTHING; try { return await post( `${RECORD_PROPOSAL_API_URL}/record-proposals`, { text, origin }, ); } catch (error) { console.error('Could not read the note for things to add to the record', error); return { ...NOTHING, failed: true }; } }, [post], ); const api = useMemo(() => ({ proposeFromNote }), [proposeFromNote]); return ( {children} ); };