feat: wire agenda artifacts
This commit is contained in:
@@ -11,11 +11,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ArtifactTab } from './artifact-tab';
|
import { ArtifactTab } from './artifact-tab';
|
||||||
|
import { AgendaBlock, type AgendaBlockProps } from './blocks/agenda-block';
|
||||||
import { DagnotatieBlock } from '../blocks/dagnotitie-block';
|
import { DagnotatieBlock } from '../blocks/dagnotitie-block';
|
||||||
import { ZoekenBlock } from '../blocks/zoeken-block';
|
import { ZoekenBlock } from '../blocks/zoeken-block';
|
||||||
import { OverdrachtBlock } from '../blocks/overdracht-block';
|
import { OverdrachtBlock } from '../blocks/overdracht-block';
|
||||||
import { PatientDashboardBlock } from '../blocks/patient-dashboard-block';
|
import { PatientDashboardBlock } from '../blocks/patient-dashboard-block';
|
||||||
import { FallbackPicker } from '../blocks/fallback-picker';
|
import { FallbackPicker } from '../blocks/fallback-picker';
|
||||||
|
import { APPOINTMENT_TYPES, type AppointmentTypeCode, type LocationClassCode } from '@/app/epd/agenda/types';
|
||||||
import type { Artifact, BlockType } from '@/stores/swift-store';
|
import type { Artifact, BlockType } from '@/stores/swift-store';
|
||||||
|
|
||||||
interface ArtifactContainerProps {
|
interface ArtifactContainerProps {
|
||||||
@@ -25,10 +27,107 @@ interface ArtifactContainerProps {
|
|||||||
onCloseArtifact: (id: string) => void;
|
onCloseArtifact: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgendaIntent = 'agenda_query' | 'create_appointment' | 'cancel_appointment' | 'reschedule_appointment';
|
||||||
|
type AgendaMode = AgendaBlockProps['mode'];
|
||||||
|
|
||||||
|
const AGENDA_MODE_MAP: Record<AgendaIntent, AgendaMode> = {
|
||||||
|
agenda_query: 'list',
|
||||||
|
create_appointment: 'create',
|
||||||
|
cancel_appointment: 'cancel',
|
||||||
|
reschedule_appointment: 'reschedule',
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOCATION_CODE_MAP: Record<string, LocationClassCode> = {
|
||||||
|
praktijk: 'AMB',
|
||||||
|
online: 'VR',
|
||||||
|
thuis: 'HH',
|
||||||
|
};
|
||||||
|
|
||||||
|
function coerceDate(value: unknown): Date | undefined {
|
||||||
|
if (value instanceof Date) return value;
|
||||||
|
if (typeof value === 'string' || typeof value === 'number') {
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (!Number.isNaN(parsed.getTime())) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function coerceDateRange(raw: any): AgendaBlockProps['dateRange'] | undefined {
|
||||||
|
const start = coerceDate(raw?.start);
|
||||||
|
const end = coerceDate(raw?.end);
|
||||||
|
if (!start || !end) return undefined;
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
label: typeof raw?.label === 'string' ? raw.label : 'custom',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAppointmentType(value: unknown): AppointmentTypeCode | undefined {
|
||||||
|
if (typeof value !== 'string') return undefined;
|
||||||
|
if (value in APPOINTMENT_TYPES) return value as AppointmentTypeCode;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLocation(value: unknown): LocationClassCode | undefined {
|
||||||
|
if (typeof value !== 'string') return undefined;
|
||||||
|
if (value in LOCATION_CODE_MAP) return LOCATION_CODE_MAP[value];
|
||||||
|
if (value === 'AMB' || value === 'VR' || value === 'HH') return value;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlockProps['prefillData'] {
|
||||||
|
if (!prefill || typeof prefill !== 'object') return undefined;
|
||||||
|
|
||||||
|
const patient =
|
||||||
|
prefill.patient ||
|
||||||
|
(prefill.patientName || prefill.patientId
|
||||||
|
? {
|
||||||
|
id: prefill.patientId || '',
|
||||||
|
name: prefill.patientName || '',
|
||||||
|
}
|
||||||
|
: undefined);
|
||||||
|
|
||||||
|
const datetimeDate =
|
||||||
|
coerceDate(prefill?.datetime?.date) ||
|
||||||
|
(prefill?.datetime?.time ? new Date() : undefined);
|
||||||
|
const datetime = datetimeDate
|
||||||
|
? {
|
||||||
|
date: datetimeDate,
|
||||||
|
time: typeof prefill?.datetime?.time === 'string' ? prefill.datetime.time : '',
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const appointmentType = resolveAppointmentType(prefill?.appointmentType || prefill?.type);
|
||||||
|
const location = resolveLocation(prefill?.location);
|
||||||
|
|
||||||
|
const newDatetimeDate =
|
||||||
|
coerceDate(prefill?.newDatetime?.date) ||
|
||||||
|
(prefill?.newDatetime?.time ? new Date() : undefined);
|
||||||
|
const newDatetime = newDatetimeDate
|
||||||
|
? {
|
||||||
|
date: newDatetimeDate,
|
||||||
|
time: typeof prefill?.newDatetime?.time === 'string' ? prefill.newDatetime.time : '',
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
patient,
|
||||||
|
datetime,
|
||||||
|
type: appointmentType,
|
||||||
|
location,
|
||||||
|
notes: typeof prefill?.notes === 'string' ? prefill.notes : undefined,
|
||||||
|
identifier: prefill?.identifier,
|
||||||
|
newDatetime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the appropriate block component based on artifact type
|
* Render the appropriate block component based on artifact type
|
||||||
*/
|
*/
|
||||||
function renderArtifactBlock(artifact: Artifact) {
|
function renderArtifactBlock(artifact: Artifact, onCloseArtifact: (id: string) => void) {
|
||||||
switch (artifact.type) {
|
switch (artifact.type) {
|
||||||
case 'dagnotitie':
|
case 'dagnotitie':
|
||||||
return <DagnotatieBlock key={artifact.id} prefill={artifact.prefill} />;
|
return <DagnotatieBlock key={artifact.id} prefill={artifact.prefill} />;
|
||||||
@@ -36,6 +135,33 @@ function renderArtifactBlock(artifact: Artifact) {
|
|||||||
return <ZoekenBlock key={artifact.id} prefill={artifact.prefill} />;
|
return <ZoekenBlock key={artifact.id} prefill={artifact.prefill} />;
|
||||||
case 'overdracht':
|
case 'overdracht':
|
||||||
return <OverdrachtBlock key={artifact.id} prefill={artifact.prefill} />;
|
return <OverdrachtBlock key={artifact.id} prefill={artifact.prefill} />;
|
||||||
|
case 'agenda_query':
|
||||||
|
case 'create_appointment':
|
||||||
|
case 'cancel_appointment':
|
||||||
|
case 'reschedule_appointment': {
|
||||||
|
const agendaType = artifact.type as AgendaIntent;
|
||||||
|
const rawPrefill = artifact.prefill as Record<string, any>;
|
||||||
|
const dateRange = coerceDateRange(rawPrefill?.dateRange);
|
||||||
|
const prefillData = buildAgendaPrefill(rawPrefill);
|
||||||
|
const appointments = Array.isArray(rawPrefill?.appointments)
|
||||||
|
? rawPrefill.appointments
|
||||||
|
: undefined;
|
||||||
|
const disambiguationOptions = Array.isArray(rawPrefill?.disambiguationOptions)
|
||||||
|
? rawPrefill.disambiguationOptions
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AgendaBlock
|
||||||
|
key={artifact.id}
|
||||||
|
mode={AGENDA_MODE_MAP[agendaType]}
|
||||||
|
appointments={appointments}
|
||||||
|
dateRange={dateRange}
|
||||||
|
prefillData={prefillData}
|
||||||
|
disambiguationOptions={disambiguationOptions}
|
||||||
|
onClose={() => onCloseArtifact(artifact.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'patient-dashboard':
|
case 'patient-dashboard':
|
||||||
return <PatientDashboardBlock key={artifact.id} prefill={artifact.prefill} />;
|
return <PatientDashboardBlock key={artifact.id} prefill={artifact.prefill} />;
|
||||||
case 'fallback':
|
case 'fallback':
|
||||||
@@ -62,6 +188,16 @@ export function getArtifactTitle(type: BlockType, prefill?: any): string {
|
|||||||
return 'Patiënt Zoeken';
|
return 'Patiënt Zoeken';
|
||||||
case 'overdracht':
|
case 'overdracht':
|
||||||
return 'Dienst Overdracht';
|
return 'Dienst Overdracht';
|
||||||
|
case 'agenda_query':
|
||||||
|
return 'Agenda';
|
||||||
|
case 'create_appointment':
|
||||||
|
return prefill?.patientName
|
||||||
|
? `Nieuwe afspraak - ${prefill.patientName}`
|
||||||
|
: 'Nieuwe afspraak';
|
||||||
|
case 'cancel_appointment':
|
||||||
|
return 'Afspraak annuleren';
|
||||||
|
case 'reschedule_appointment':
|
||||||
|
return 'Afspraak verzetten';
|
||||||
case 'fallback':
|
case 'fallback':
|
||||||
return 'Kies een actie';
|
return 'Kies een actie';
|
||||||
case 'patient-dashboard':
|
case 'patient-dashboard':
|
||||||
@@ -120,7 +256,7 @@ export function ArtifactContainer({
|
|||||||
<div className="flex-1 flex items-center justify-center p-6 overflow-y-auto">
|
<div className="flex-1 flex items-center justify-center p-6 overflow-y-auto">
|
||||||
{activeArtifact ? (
|
{activeArtifact ? (
|
||||||
<div key={activeArtifact.id} className="artifact-enter w-full">
|
<div key={activeArtifact.id} className="artifact-enter w-full">
|
||||||
{renderArtifactBlock(activeArtifact)}
|
{renderArtifactBlock(activeArtifact, onCloseArtifact)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-slate-500">
|
<div className="text-slate-500">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Encounter, AppointmentTypeCode, LocationClassCode, CalendarEvent } from '@/app/epd/agenda/types';
|
import { AppointmentTypeCode, LocationClassCode, CalendarEvent } from '@/app/epd/agenda/types';
|
||||||
import { AgendaListView } from './agenda-list-view';
|
import { AgendaListView } from './agenda-list-view';
|
||||||
import { AgendaCreateForm } from './agenda-create-form';
|
import { AgendaCreateForm } from './agenda-create-form';
|
||||||
import { AgendaCancelView } from './agenda-cancel-view';
|
import { AgendaCancelView } from './agenda-cancel-view';
|
||||||
@@ -16,6 +16,8 @@ export interface AgendaBlockProps {
|
|||||||
type?: AppointmentTypeCode;
|
type?: AppointmentTypeCode;
|
||||||
location?: LocationClassCode;
|
location?: LocationClassCode;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
identifier?: { encounterId?: string; encounter?: CalendarEvent };
|
||||||
|
newDatetime?: { date: Date; time: string };
|
||||||
};
|
};
|
||||||
disambiguationOptions?: CalendarEvent[];
|
disambiguationOptions?: CalendarEvent[];
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { OfflineBanner } from './offline-banner';
|
|||||||
import { ChatPanel } from '../chat/chat-panel';
|
import { ChatPanel } from '../chat/chat-panel';
|
||||||
import { ArtifactArea } from '../artifacts/artifact-area';
|
import { ArtifactArea } from '../artifacts/artifact-area';
|
||||||
import { getArtifactTitle } from '../artifacts/artifact-container';
|
import { getArtifactTitle } from '../artifacts/artifact-container';
|
||||||
|
import { routeIntentToArtifact } from '@/lib/swift/action-parser';
|
||||||
|
|
||||||
export function CommandCenter() {
|
export function CommandCenter() {
|
||||||
const { closeAllArtifacts, openArtifacts, openArtifact, pendingAction, setPendingAction } = useSwiftStore();
|
const { closeAllArtifacts, openArtifacts, openArtifact, pendingAction, setPendingAction } = useSwiftStore();
|
||||||
@@ -73,12 +74,28 @@ export function CommandCenter() {
|
|||||||
title,
|
title,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear pending action after processing
|
|
||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routedArtifact = routeIntentToArtifact(
|
||||||
|
pendingAction.intent,
|
||||||
|
pendingAction.entities,
|
||||||
|
pendingAction.confidence
|
||||||
|
);
|
||||||
|
|
||||||
|
if (routedArtifact) {
|
||||||
|
console.log('[CommandCenter] Routing action to artifact:', routedArtifact.type);
|
||||||
|
openArtifact({
|
||||||
|
type: routedArtifact.type,
|
||||||
|
prefill: routedArtifact.prefill,
|
||||||
|
title: routedArtifact.title,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('[CommandCenter] Action has no artifact, skipping');
|
console.log('[CommandCenter] Action has no artifact, skipping');
|
||||||
setPendingAction(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPendingAction(null);
|
||||||
}, [pendingAction, openArtifact, setPendingAction]);
|
}, [pendingAction, openArtifact, setPendingAction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Action Parser for Swift Medical Scribe
|
* Action Parser for Swift Assistent
|
||||||
*
|
*
|
||||||
* Parses JSON action objects from AI responses and validates them.
|
* Parses JSON action objects from AI responses and validates them.
|
||||||
*
|
*
|
||||||
* Epic: E3 (Chat API & Medical Scribe)
|
* Epic: E3 (Chat API & Swift Assistent)
|
||||||
* Story: E3.S4 (Intent detection in response)
|
* Story: E3.S4 (Intent detection in response)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -29,9 +29,40 @@ const ActionSchema = z.object({
|
|||||||
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
|
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
query: z.string().optional(), // For zoeken intent
|
query: z.string().optional(), // For zoeken intent
|
||||||
|
dateRange: z
|
||||||
|
.object({
|
||||||
|
start: z.string(),
|
||||||
|
end: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
datetime: z
|
||||||
|
.object({
|
||||||
|
date: z.string(),
|
||||||
|
time: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
appointmentType: z.string().optional(),
|
||||||
|
location: z.string().optional(),
|
||||||
date: z.string().optional(),
|
date: z.string().optional(),
|
||||||
time: z.string().optional(),
|
time: z.string().optional(),
|
||||||
identifier: z.string().optional(),
|
identifier: z
|
||||||
|
.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
encounterId: z.string().optional(),
|
||||||
|
patientName: z.string().optional(),
|
||||||
|
time: z.string().optional(),
|
||||||
|
date: z.string().optional(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
|
newDatetime: z
|
||||||
|
.object({
|
||||||
|
date: z.string().optional(),
|
||||||
|
time: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
confidence: z.number().min(0).max(1),
|
confidence: z.number().min(0).max(1),
|
||||||
artifact: z
|
artifact: z
|
||||||
|
|||||||
Reference in New Issue
Block a user