feat(cortex): update chat/nudge flow, deepgram streaming and agenda form

Refines chat panel, nudge messages and artifact rendering, reworks
deepgram token/streaming handling, and adds test tooling deps
(playwright, cypress) to package.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2026-07-09 22:37:46 +02:00
parent 6c6dae5eb7
commit cdeea793bd
20 changed files with 1634 additions and 283 deletions

View File

@@ -20,6 +20,8 @@ import { Badge } from '@/components/ui/badge';
import { toast } from '@/hooks/use-toast';
import { ToastAction } from '@/components/ui/toast';
import { createEncounter } from '@/app/epd/agenda/actions';
import { useCortexStore } from '@/stores/cortex-store';
import { formatPatientName as formatPatientNameFromDb } from '@/lib/fhir/patient-mapper';
import {
APPOINTMENT_TYPES,
LOCATION_CLASSES,
@@ -47,7 +49,12 @@ interface PatientResult {
birthDate?: string;
}
function normalizePatientName(name: string) {
return name.toLowerCase().trim().replace(/\s+/g, ' ');
}
export function AgendaCreateForm({ prefillData, onClose }: AgendaCreateFormProps) {
const activePatient = useCortexStore((s) => s.activePatient);
// Form State
const [patientId, setPatientId] = useState<string>(prefillData?.patient?.id || '');
const [patientName, setPatientName] = useState<string>(prefillData?.patient?.name || '');
@@ -69,6 +76,7 @@ export function AgendaCreateForm({ prefillData, onClose }: AgendaCreateFormProps
const [isSearching, setIsSearching] = useState(false);
const [showResults, setShowResults] = useState(false);
const searchRef = useRef<HTMLDivElement>(null);
const autoResolvedPatientRef = useRef<string | null>(null);
// Initialize search query if patient is prefilled but we want to allow editing
useEffect(() => {
@@ -77,6 +85,69 @@ export function AgendaCreateForm({ prefillData, onClose }: AgendaCreateFormProps
}
}, [prefillData]);
// If Cortex only extracted a name, resolve it once so the appointment form
// can submit without forcing the user to retype and select the same client.
useEffect(() => {
const prefilledPatient = prefillData?.patient;
if (!prefilledPatient?.name || prefilledPatient.id || patientId) return;
const normalizedPrefill = normalizePatientName(prefilledPatient.name);
if (autoResolvedPatientRef.current === normalizedPrefill) return;
autoResolvedPatientRef.current = normalizedPrefill;
if (activePatient) {
const activePatientName = formatPatientNameFromDb(activePatient);
if (normalizePatientName(activePatientName) === normalizedPrefill) {
setPatientId(activePatient.id);
setPatientName(activePatientName);
setSearchQuery(activePatientName);
setShowResults(false);
return;
}
}
let cancelled = false;
async function resolvePrefilledPatient() {
setIsSearching(true);
try {
const res = await fetch(`/api/cortex/patients/search?q=${encodeURIComponent(prefilledPatient.name)}`);
if (!res.ok || cancelled) return;
const data = await res.json();
const patients = (data.patients || []) as PatientResult[];
const exactMatch = patients.find(
(patient) => normalizePatientName(patient.name) === normalizedPrefill
);
const match = exactMatch || (patients.length === 1 ? patients[0] : null);
if (match) {
setPatientId(match.id);
setPatientName(match.name);
setSearchQuery(match.name);
setSearchResults([]);
setShowResults(false);
} else {
setSearchResults(patients);
setShowResults(patients.length > 0);
}
} catch (err) {
console.error('Failed to resolve prefilled patient', err);
} finally {
if (!cancelled) {
setIsSearching(false);
}
}
}
resolvePrefilledPatient();
return () => {
cancelled = true;
};
}, [prefillData, patientId, activePatient]);
// Handle outside click to close search results
useEffect(() => {
function handleClickOutside(event: MouseEvent) {