diff --git a/backend/src/version.ts b/backend/src/version.ts index 4beee82..9411128 100644 --- a/backend/src/version.ts +++ b/backend/src/version.ts @@ -1 +1 @@ -export const API_VERSION = '2026-06-12'; +export const API_VERSION = '2026-07-01'; diff --git a/frontend/src/api/async-job.ts b/frontend/src/api/async-job.ts new file mode 100644 index 0000000..cd432a1 --- /dev/null +++ b/frontend/src/api/async-job.ts @@ -0,0 +1,44 @@ +export type AsyncJobStatus = "ONGOING" | "SUCCEEDED" | "FAILED"; + +export interface AsyncJob { + id: string; + status: AsyncJobStatus; + payload?: TPayload; +} + +const DEFAULT_POLL_INTERVAL_MS = 10 * 1_000; +const DEFAULT_TIMEOUT_MS = 5 * 60 * 1_000; + +export async function pollUntilSucceeded( + fetchJob: () => Promise>, + options: { intervalMs?: number; timeoutMs?: number } = {}, +): Promise { + const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS; + const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const deadline = Date.now() + timeoutMs; + + while (true) { + await sleep(intervalMs); + + if (Date.now() >= deadline) { + throw new Error(`Did not get results within ${timeoutMs}ms. Aborting polling.`); + } + + const job = await fetchJob(); + + if (job.status === "SUCCEEDED") { + if (job.payload === undefined) { + throw new Error("Async job succeeded but returned no payload."); + } + return job.payload; + } + + if (job.status === "FAILED") { + throw new Error(`Async job ${job.id} failed.`); + } + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/frontend/src/api/dictate.ts b/frontend/src/api/dictate.ts index c186348..e7da19a 100644 --- a/frontend/src/api/dictate.ts +++ b/frontend/src/api/dictate.ts @@ -16,9 +16,16 @@ export interface AudioChunkAck { ack_id: number; } +export interface AsyncCorrection { + type: "ASYNC_CORRECTION"; + suffix: string; + replacement: string; +} + export type DictateServerMessage = | DictatedText | AudioChunkAck + | AsyncCorrection | { type: string }; export const DICTATE_ENCODING = "PCM_S16LE" as const; @@ -51,6 +58,8 @@ export function buildDictateConfig(locale: DictationLocale, noteText: string) { selection_start: noteText.length, selection_length: 0, }, + // Opts into server-side refinements of already-streamed text via ASYNC_CORRECTION. + enable_async_corrections: true, }; } diff --git a/frontend/src/api/normalize.ts b/frontend/src/api/normalize.ts index a3862b8..7c3e99a 100644 --- a/frontend/src/api/normalize.ts +++ b/frontend/src/api/normalize.ts @@ -1,3 +1,4 @@ +import { type AsyncJob, pollUntilSucceeded } from "./async-job.js"; import { nablaFetch } from "../transport/client.js"; import type { ClinicalNote } from "./note.js"; @@ -18,11 +19,18 @@ export interface NormalizedData { export async function generateNormalizedData( note: ClinicalNote, ): Promise { - const response = await nablaFetch("/v1/core/user/generate-normalized-data", { - method: "POST", - body: JSON.stringify({ - note, - }), - }); - return response.json() as Promise; + const submitResponse = await nablaFetch( + "/v1/core/user/generate-normalized-data-async", + { + method: "POST", + body: JSON.stringify({ note }), + }, + ); + const { id } = (await submitResponse.json()) as AsyncJob; + + return pollUntilSucceeded(() => + nablaFetch(`/v1/core/user/generate-normalized-data-async/${id}`).then( + (response) => response.json() as Promise>, + ), + ); } diff --git a/frontend/src/api/version.ts b/frontend/src/api/version.ts index 91561a8..28331d1 100644 --- a/frontend/src/api/version.ts +++ b/frontend/src/api/version.ts @@ -1 +1 @@ -export const API_VERSION = "2026-06-12"; +export const API_VERSION = "2026-07-01"; diff --git a/frontend/src/pages/full-encounter-demo/work-on-note.render.ts b/frontend/src/pages/full-encounter-demo/work-on-note.render.ts index cf6450e..4e27cc6 100644 --- a/frontend/src/pages/full-encounter-demo/work-on-note.render.ts +++ b/frontend/src/pages/full-encounter-demo/work-on-note.render.ts @@ -66,7 +66,7 @@ export function markup(): string {

Extract ICD-10 / LOINC codes in FHIR format from the note.