Files
Khalid Abdi 929bec8f31 feat: AI-added records save with placeholders + "Added by AI" provenance
Stop blocking AI imports/proposals on missing non-critical fields. Records
the chat agent drafts now save with safe placeholders, auto-generated file
numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a
clinician can review/edit them later.

Backend:
- add `source` (manual|ai) column to patients/appointments/prescriptions
  (migration 0014) + canonical types, services, validation schemas
- relax patient/appointment validation: empty file number allowed, demographic
  + type/provider/initials fall back to placeholders (initials derived from name)
- patients.generateFileNumber() auto-assigns an MRN when one is missing
- proposeAppointment accepts a name when no file number resolves; AI commits +
  /api/ai/import stamp source="ai"

Frontend:
- `source` on Appointment/Patient/Prescription types; AI commits send source="ai"
- reusable <AiBadge> shown on the Patients table/detail and prescriptions list

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:27:17 +03:00

85 lines
2.3 KiB
TypeScript

"use client";
import { SpeechInput } from "@/components/ai-elements/speech-input";
import { useCallback, useState } from "react";
/**
* Fallback handler for browsers that don't support Web Speech API (Firefox, Safari).
* This function receives recorded audio and should send it to a transcription service.
* Example uses OpenAI Whisper API - replace with your preferred service.
*/
const handleAudioRecorded = async (audioBlob: Blob): Promise<string> => {
const formData = new FormData();
formData.append("file", audioBlob, "audio.webm");
formData.append("model", "whisper-1");
const response = await fetch(
"https://api.openai.com/v1/audio/transcriptions",
{
body: formData,
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
},
method: "POST",
}
);
if (!response.ok) {
throw new Error("Transcription failed");
}
const data = await response.json();
return data.text;
};
const Example = () => {
const [transcript, setTranscript] = useState("");
const handleTranscriptionChange = useCallback((text: string) => {
setTranscript((prev) => {
const newText = prev ? `${prev} ${text}` : text;
return newText;
});
}, []);
const handleClear = useCallback(() => {
setTranscript("");
}, []);
return (
<div className="flex size-full flex-col items-center justify-center gap-4">
<div className="flex gap-2">
<SpeechInput
onAudioRecorded={handleAudioRecorded}
onTranscriptionChange={handleTranscriptionChange}
size="icon"
variant="outline"
/>
{transcript && (
<button
className="text-muted-foreground text-sm underline hover:text-foreground"
onClick={handleClear}
type="button"
>
Clear
</button>
)}
</div>
{transcript ? (
<div className="max-w-md rounded-lg border bg-card p-4 text-sm">
<p className="text-muted-foreground">
<strong>Transcript:</strong>
</p>
<p className="mt-2">{transcript}</p>
</div>
) : (
<p className="text-muted-foreground text-sm">
Click the microphone to start speaking
</p>
)}
</div>
);
};
export default Example;