mirror of
https://github.com/temetro/temetro.git
synced 2026-08-12 11:36:52 +00:00
feat: ambient AI visit scribe (record/paste → reviewed SOAP note)
Record a clinician↔patient visit (or paste a transcript) on the patient
sheet; the backend transcribes it (OpenAI Whisper / Gemini), de-identifies
the transcript + context through Veil, and drafts a structured SOAP note
the clinician reviews and edits before saving — the same write-approval
gate as the chat agent.
Backend: POST /api/scribe/{transcribe,draft,save} (routes/scribe.ts,
services/ai/transcribe.ts), veil.redactText() free-text redactor,
appendEncounter service, audio MIME types on attachments. Gated by
patient:write + the clinic AI policy (reception/disabled-AI excluded).
Frontend: ScribeDialog + lib/scribe.ts, "Record visit" on the patient
detail, gated by clinical access + AI availability. New `scribe` locale
namespace across all five languages. Bumps to v0.4.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,48 @@
|
||||
{
|
||||
"scribe": {
|
||||
"recordVisit": "تسجيل الزيارة",
|
||||
"title": "كاتب الزيارة",
|
||||
"subtitle": "سجّل أو الصق زيارة لـ {{name}}، ثم راجع الملاحظة المُصاغة قبل الحفظ.",
|
||||
"tabs": {
|
||||
"record": "تسجيل",
|
||||
"paste": "لصق النص"
|
||||
},
|
||||
"record": {
|
||||
"start": "بدء التسجيل",
|
||||
"stop": "إيقاف",
|
||||
"ready": "التسجيل جاهز ({{duration}})",
|
||||
"discard": "تجاهل"
|
||||
},
|
||||
"paste": {
|
||||
"placeholder": "الصق نص الزيارة هنا…"
|
||||
},
|
||||
"visitType": {
|
||||
"label": "نوع الزيارة (اختياري)",
|
||||
"placeholder": "مثال: متابعة"
|
||||
},
|
||||
"consent": "يُخزَّن الصوت في ملف المريض. عند استخدام مزوّد ذكاء اصطناعي خارجي، يغادر التسجيل العيادة ليُفرَّغ نصيًّا — لا يستطيع Veil إخفاء الكلام، لذا تُخفى هوية الملاحظة المُصاغة فقط.",
|
||||
"generate": "صياغة الملاحظة",
|
||||
"processing": "جارٍ العمل…",
|
||||
"cancel": "إلغاء",
|
||||
"review": {
|
||||
"type": "نوع الزيارة",
|
||||
"date": "التاريخ",
|
||||
"summary": "الملاحظة",
|
||||
"provider": "الطبيب: {{provider}}",
|
||||
"veil": "أُخفيت الهوية عبر Veil قبل {{provider}}.",
|
||||
"back": "رجوع",
|
||||
"save": "الحفظ في السجل"
|
||||
},
|
||||
"saved": {
|
||||
"title": "تم حفظ ملاحظة الزيارة"
|
||||
},
|
||||
"errors": {
|
||||
"mic": "تعذّر الوصول إلى الميكروفون. تحقّق من أذونات المتصفح أو الصق نصًّا بدلاً من ذلك.",
|
||||
"noRecording": "سجّل الزيارة أولاً، أو انتقل إلى تبويب اللصق.",
|
||||
"empty": "النص فارغ.",
|
||||
"generic": "حدث خطأ ما. يُرجى المحاولة مرة أخرى."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "البريد الإلكتروني",
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
{
|
||||
"scribe": {
|
||||
"recordVisit": "Besuch aufnehmen",
|
||||
"title": "Besuchs-Schreiber",
|
||||
"subtitle": "Nehmen Sie einen Besuch für {{name}} auf oder fügen Sie ihn ein und prüfen Sie die entworfene Notiz vor dem Speichern.",
|
||||
"tabs": {
|
||||
"record": "Aufnehmen",
|
||||
"paste": "Transkript einfügen"
|
||||
},
|
||||
"record": {
|
||||
"start": "Aufnahme starten",
|
||||
"stop": "Stopp",
|
||||
"ready": "Aufnahme bereit ({{duration}})",
|
||||
"discard": "Verwerfen"
|
||||
},
|
||||
"paste": {
|
||||
"placeholder": "Besuchstranskript hier einfügen…"
|
||||
},
|
||||
"visitType": {
|
||||
"label": "Besuchsart (optional)",
|
||||
"placeholder": "z. B. Nachsorge"
|
||||
},
|
||||
"consent": "Die Audioaufnahme wird in der Patientenakte gespeichert. Bei Nutzung eines externen KI-Anbieters verlässt die Aufnahme zur Transkription die Klinik — Veil kann Sprache nicht schwärzen, daher wird nur die entworfene Notiz anonymisiert.",
|
||||
"generate": "Notiz entwerfen",
|
||||
"processing": "In Arbeit…",
|
||||
"cancel": "Abbrechen",
|
||||
"review": {
|
||||
"type": "Besuchsart",
|
||||
"date": "Datum",
|
||||
"summary": "Notiz",
|
||||
"provider": "Behandler: {{provider}}",
|
||||
"veil": "Über Veil anonymisiert vor {{provider}}.",
|
||||
"back": "Zurück",
|
||||
"save": "In Akte speichern"
|
||||
},
|
||||
"saved": {
|
||||
"title": "Besuchsnotiz gespeichert"
|
||||
},
|
||||
"errors": {
|
||||
"mic": "Kein Zugriff auf das Mikrofon. Prüfen Sie die Browser-Berechtigungen oder fügen Sie ein Transkript ein.",
|
||||
"noRecording": "Nehmen Sie zuerst den Besuch auf oder wechseln Sie zum Einfügen-Tab.",
|
||||
"empty": "Das Transkript ist leer.",
|
||||
"generic": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "E-Mail",
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
{
|
||||
"scribe": {
|
||||
"recordVisit": "Record visit",
|
||||
"title": "Visit scribe",
|
||||
"subtitle": "Record or paste a visit for {{name}}, then review the drafted note before saving.",
|
||||
"tabs": {
|
||||
"record": "Record",
|
||||
"paste": "Paste transcript"
|
||||
},
|
||||
"record": {
|
||||
"start": "Start recording",
|
||||
"stop": "Stop",
|
||||
"ready": "Recording ready ({{duration}})",
|
||||
"discard": "Discard"
|
||||
},
|
||||
"paste": {
|
||||
"placeholder": "Paste the visit transcript here…"
|
||||
},
|
||||
"visitType": {
|
||||
"label": "Visit type (optional)",
|
||||
"placeholder": "e.g. Follow-up"
|
||||
},
|
||||
"consent": "The audio is stored on the patient's chart. When you use an external AI provider, the recording leaves the clinic to be transcribed — Veil cannot redact speech, so only the drafted note is de-identified.",
|
||||
"generate": "Draft note",
|
||||
"processing": "Working…",
|
||||
"cancel": "Cancel",
|
||||
"review": {
|
||||
"type": "Visit type",
|
||||
"date": "Date",
|
||||
"summary": "Note",
|
||||
"provider": "Provider: {{provider}}",
|
||||
"veil": "De-identified through Veil before {{provider}}.",
|
||||
"back": "Back",
|
||||
"save": "Save to record"
|
||||
},
|
||||
"saved": {
|
||||
"title": "Visit note saved"
|
||||
},
|
||||
"errors": {
|
||||
"mic": "Couldn't access the microphone. Check browser permissions or paste a transcript instead.",
|
||||
"noRecording": "Record the visit first, or switch to the paste tab.",
|
||||
"empty": "The transcript is empty.",
|
||||
"generic": "Something went wrong. Please try again."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "Email",
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
{
|
||||
"scribe": {
|
||||
"recordVisit": "Enregistrer la visite",
|
||||
"title": "Scribe de visite",
|
||||
"subtitle": "Enregistrez ou collez une visite pour {{name}}, puis vérifiez la note rédigée avant de l'enregistrer.",
|
||||
"tabs": {
|
||||
"record": "Enregistrer",
|
||||
"paste": "Coller la transcription"
|
||||
},
|
||||
"record": {
|
||||
"start": "Démarrer l'enregistrement",
|
||||
"stop": "Arrêter",
|
||||
"ready": "Enregistrement prêt ({{duration}})",
|
||||
"discard": "Supprimer"
|
||||
},
|
||||
"paste": {
|
||||
"placeholder": "Collez la transcription de la visite ici…"
|
||||
},
|
||||
"visitType": {
|
||||
"label": "Type de visite (facultatif)",
|
||||
"placeholder": "ex. Suivi"
|
||||
},
|
||||
"consent": "L'audio est enregistré dans le dossier du patient. Si vous utilisez un fournisseur d'IA externe, l'enregistrement quitte la clinique pour être transcrit — Veil ne peut pas expurger la parole, seule la note rédigée est dépersonnalisée.",
|
||||
"generate": "Rédiger la note",
|
||||
"processing": "En cours…",
|
||||
"cancel": "Annuler",
|
||||
"review": {
|
||||
"type": "Type de visite",
|
||||
"date": "Date",
|
||||
"summary": "Note",
|
||||
"provider": "Praticien : {{provider}}",
|
||||
"veil": "Dépersonnalisé via Veil avant {{provider}}.",
|
||||
"back": "Retour",
|
||||
"save": "Enregistrer dans le dossier"
|
||||
},
|
||||
"saved": {
|
||||
"title": "Note de visite enregistrée"
|
||||
},
|
||||
"errors": {
|
||||
"mic": "Impossible d'accéder au microphone. Vérifiez les autorisations du navigateur ou collez une transcription.",
|
||||
"noRecording": "Enregistrez d'abord la visite, ou passez à l'onglet coller.",
|
||||
"empty": "La transcription est vide.",
|
||||
"generic": "Une erreur s'est produite. Veuillez réessayer."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "E-mail",
|
||||
|
||||
@@ -1,4 +1,48 @@
|
||||
{
|
||||
"scribe": {
|
||||
"recordVisit": "Duub booqasho",
|
||||
"title": "Qoraaga booqashada",
|
||||
"subtitle": "Duub ama ku dhaji booqasho {{name}}, ka dibna dib u eeg qoraalka la sameeyay ka hor inta aadan keydin.",
|
||||
"tabs": {
|
||||
"record": "Duub",
|
||||
"paste": "Ku dhaji qoraalka"
|
||||
},
|
||||
"record": {
|
||||
"start": "Bilow duubista",
|
||||
"stop": "Jooji",
|
||||
"ready": "Duubista diyaar ({{duration}})",
|
||||
"discard": "Tirtir"
|
||||
},
|
||||
"paste": {
|
||||
"placeholder": "Halkan ku dhaji qoraalka booqashada…"
|
||||
},
|
||||
"visitType": {
|
||||
"label": "Nooca booqashada (ikhtiyaari)",
|
||||
"placeholder": "tusaale: Dib-u-eegis"
|
||||
},
|
||||
"consent": "Codka waxaa lagu keydiyaa faylka bukaanka. Marka aad isticmaasho bixiye AI dibadeed, duubistu waxay ka baxdaa rugta si loo qoro — Veil ma tirtiri karo hadalka, sidaas darteed kaliya qoraalka la sameeyay ayaa la qariyaa.",
|
||||
"generate": "Samee qoraalka",
|
||||
"processing": "Waa la shaqaynayaa…",
|
||||
"cancel": "Jooji",
|
||||
"review": {
|
||||
"type": "Nooca booqashada",
|
||||
"date": "Taariikhda",
|
||||
"summary": "Qoraalka",
|
||||
"provider": "Bixiye: {{provider}}",
|
||||
"veil": "Waxaa lagu qariyay Veil ka hor {{provider}}.",
|
||||
"back": "Dib u noqo",
|
||||
"save": "Ku keydi diiwaanka"
|
||||
},
|
||||
"saved": {
|
||||
"title": "Qoraalka booqashada waa la keydiyay"
|
||||
},
|
||||
"errors": {
|
||||
"mic": "Lama gaari karin makarafoonka. Hubi oggolaanshaha browserka ama ku dhaji qoraal.",
|
||||
"noRecording": "Marka hore duub booqashada, ama u wareeg tabka ku-dhajinta.",
|
||||
"empty": "Qoraalku waa madhan yahay.",
|
||||
"generic": "Wax baa qaldamay. Fadlan isku day mar kale."
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "Iimayl",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Client for the ambient AI visit-scribe API. The dialog records (or accepts a
|
||||
// pasted transcript of) a clinician↔patient visit, transcribes it, drafts a
|
||||
// structured encounter note, and — after the clinician reviews it — appends the
|
||||
// note to the patient record (the same write-approval gate as the chat agent).
|
||||
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
import type { Encounter, Patient } from "@/lib/patients";
|
||||
|
||||
export type ScribeVeil = {
|
||||
active: boolean;
|
||||
level: string;
|
||||
classes: string[];
|
||||
provider: string;
|
||||
};
|
||||
|
||||
// Turn a stored audio attachment into a raw transcript (server streams it to the
|
||||
// user's OpenAI/Gemini speech provider). Audio does NOT pass through Veil.
|
||||
export function transcribeRecording(
|
||||
attachmentId: string,
|
||||
): Promise<{ transcript: string }> {
|
||||
return apiFetch<{ transcript: string }>("/api/scribe/transcribe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ attachmentId }),
|
||||
});
|
||||
}
|
||||
|
||||
// Draft an encounter note from a transcript. The transcript + patient context
|
||||
// are de-identified through Veil before any external call; the draft is never
|
||||
// saved automatically.
|
||||
export function draftNote(input: {
|
||||
fileNumber: string;
|
||||
transcript: string;
|
||||
visitType?: string;
|
||||
date?: string;
|
||||
}): Promise<{ draft: Encounter; veil: ScribeVeil }> {
|
||||
return apiFetch<{ draft: Encounter; veil: ScribeVeil }>("/api/scribe/draft", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
// The approval step: append the reviewed note to the patient record.
|
||||
export function saveNote(
|
||||
fileNumber: string,
|
||||
encounter: Encounter,
|
||||
): Promise<Patient> {
|
||||
return apiFetch<Patient>("/api/scribe/save", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fileNumber, encounter }),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user