fix(cortex): update date handling in API and UI components

- Adjusted date handling in the agenda API to ensure optional parameters for start, end, and label are correctly processed.
- Enhanced chat input and action parser to support optional date labels for relative dates.
- Updated agenda components to handle date ranges and loading states more effectively.
- Improved error handling and loading indicators in the agenda block for better user experience.

This commit ensures consistency in date handling across the application, aligning with the new requirements for relative date inputs.
This commit is contained in:
colinislit
2026-01-02 09:21:37 +01:00
parent 011815072b
commit 52e07aaf80
20 changed files with 2092 additions and 65 deletions

View File

@@ -42,10 +42,11 @@ export async function GET(request: NextRequest) {
} }
// Parse and validate query parameters // Parse and validate query parameters
// Note: searchParams.get() returns null if not present, but Zod expects undefined
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const start = searchParams.get('start'); const start = searchParams.get('start') ?? undefined;
const end = searchParams.get('end'); const end = searchParams.get('end') ?? undefined;
const label = searchParams.get('label'); const label = searchParams.get('label') ?? undefined;
const validation = QuerySchema.safeParse({ start, end, label }); const validation = QuerySchema.safeParse({ start, end, label });
if (!validation.success) { if (!validation.success) {

View File

@@ -110,6 +110,27 @@ Je helpt zorgmedewerkers (verpleegkundigen, psychiaters, behandelaren) met docum
- Kort en to-the-point (geen lange uitleg) - Kort en to-the-point (geen lange uitleg)
- Empatisch voor werkdruk zorgmedewerkers - Empatisch voor werkdruk zorgmedewerkers
## BELANGRIJK: Datum/Tijd Handling
**Stuur ALLEEN labels, GEEN datums!** De client berekent de exacte datum zelf.
Toegestane labels:
- "vandaag", "morgen", "overmorgen"
- "deze week", "volgende week"
- "maandag", "dinsdag", etc. (weekdagen)
Voorbeeld datetime entity:
\`\`\`json
{
"datetime": {
"label": "morgen",
"time": "14:00"
}
}
\`\`\`
**NOOIT** een "date" veld met een ISO-datum string sturen. De client bepaalt de datum op basis van het label.
## Wat je DOET ## Wat je DOET
### 1. Intents herkennen ### 1. Intents herkennen
@@ -138,12 +159,12 @@ Je herkent de volgende gebruikersintenties en voert acties uit:
- **agenda_query** — Afspraken opvragen - **agenda_query** — Afspraken opvragen
- Triggers: "afspraken vandaag", "agenda morgen", "wat is mijn volgende afspraak", "afspraken deze week" - Triggers: "afspraken vandaag", "agenda morgen", "wat is mijn volgende afspraak", "afspraken deze week"
- Entities: dateRange (vandaag/morgen/deze week/volgende week) - Entities: dateRange (alleen label: "vandaag"/"morgen"/"deze week"/"volgende week" — GEEN datums!)
- Actie: Toon lijst van afspraken in AgendaBlock - Actie: Toon lijst van afspraken in AgendaBlock
- **create_appointment** — Nieuwe afspraak maken - **create_appointment** — Nieuwe afspraak maken
- Triggers: "maak afspraak [patient]", "plan intake [patient]", "afspraak maken met [patient] [datum] [tijd]" - Triggers: "maak afspraak [patient]", "plan intake [patient]", "afspraak maken met [patient] [datum] [tijd]"
- Entities: patientName (naam), datetime (datum + tijd), appointmentType (intake/behandeling/follow-up/telefonisch/huisbezoek/online/crisis), location (praktijk/online/thuis) - Entities: patientName (naam), datetime (label + tijd), appointmentType (intake/behandeling/follow-up/telefonisch/huisbezoek/online/crisis), location (praktijk/online/thuis)
- Required: patientName OF patientId, datetime - Required: patientName OF patientId, datetime
- Optional: appointmentType (default: behandeling), location (default: praktijk) - Optional: appointmentType (default: behandeling), location (default: praktijk)
- Actie: Open create form met pre-fill - Actie: Open create form met pre-fill
@@ -346,8 +367,6 @@ Je hebt toegang tot de volgende context:
"intent": "agenda_query", "intent": "agenda_query",
"entities": { "entities": {
"dateRange": { "dateRange": {
"start": "2025-12-27",
"end": "2025-12-27",
"label": "vandaag" "label": "vandaag"
} }
}, },
@@ -356,8 +375,6 @@ Je hebt toegang tot de volgende context:
"type": "agenda_query", "type": "agenda_query",
"prefill": { "prefill": {
"dateRange": { "dateRange": {
"start": "2025-12-27",
"end": "2025-12-27",
"label": "vandaag" "label": "vandaag"
} }
} }
@@ -380,7 +397,7 @@ Je hebt toegang tot de volgende context:
"entities": { "entities": {
"patientName": "Jan", "patientName": "Jan",
"datetime": { "datetime": {
"date": "2025-12-28", "label": "morgen",
"time": "14:00" "time": "14:00"
}, },
"appointmentType": "behandeling", "appointmentType": "behandeling",
@@ -392,7 +409,7 @@ Je hebt toegang tot de volgende context:
"prefill": { "prefill": {
"patientName": "Jan", "patientName": "Jan",
"datetime": { "datetime": {
"date": "2025-12-28", "label": "morgen",
"time": "14:00" "time": "14:00"
}, },
"appointmentType": "behandeling", "appointmentType": "behandeling",
@@ -430,7 +447,6 @@ Je hebt toegang tot de volgende context:
"time": "14:00" "time": "14:00"
}, },
"newDatetime": { "newDatetime": {
"date": "2025-12-27",
"time": "15:00" "time": "15:00"
} }
}, },
@@ -443,7 +459,6 @@ Je hebt toegang tot de volgende context:
"time": "14:00" "time": "14:00"
}, },
"newDatetime": { "newDatetime": {
"date": "2025-12-27",
"time": "15:00" "time": "15:00"
} }
} }

View File

@@ -107,28 +107,47 @@ interface CreateEncounterParams {
export async function createEncounter(params: CreateEncounterParams) { export async function createEncounter(params: CreateEncounterParams) {
const supabase = await createClient(); const supabase = await createClient();
console.log('[createEncounter] Starting with params:', {
patientId: params.patientId,
periodStart: params.periodStart,
periodEnd: params.periodEnd,
typeCode: params.typeCode,
});
const insertData = {
patient_id: params.patientId,
practitioner_id: params.practitionerId || null,
period_start: params.periodStart,
period_end: params.periodEnd,
type_code: params.typeCode,
type_display: params.typeDisplay,
class_code: params.classCode,
class_display: params.classDisplay,
notes: params.notes,
status: 'planned',
};
console.log('[createEncounter] Insert data:', insertData);
const { data, error } = await supabase const { data, error } = await supabase
.from('encounters') .from('encounters')
.insert({ .insert(insertData)
patient_id: params.patientId,
practitioner_id: params.practitionerId || null,
period_start: params.periodStart,
period_end: params.periodEnd,
type_code: params.typeCode,
type_display: params.typeDisplay,
class_code: params.classCode,
class_display: params.classDisplay,
notes: params.notes,
status: 'planned',
})
.select() .select()
.single(); .single();
console.log('[createEncounter] Result:', { data, error });
if (error) { if (error) {
console.error('Error creating encounter:', error); console.error('[createEncounter] Error:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
if (!data) {
console.error('[createEncounter] No data returned after insert');
return { success: false, error: 'Geen data teruggegeven na insert' };
}
console.log('[createEncounter] Success! Created encounter:', data.id);
revalidatePath('/epd/agenda'); revalidatePath('/epd/agenda');
return { success: true, data }; return { success: true, data };
} }

View File

@@ -20,6 +20,7 @@ interface AgendaCalendarProps {
events: CalendarEvent[]; events: CalendarEvent[];
initialView?: CalendarView; initialView?: CalendarView;
currentView?: CalendarView; currentView?: CalendarView;
currentDate?: Date;
onEventClick?: (event: CalendarEvent) => void; onEventClick?: (event: CalendarEvent) => void;
onDateSelect?: (start: Date, end: Date) => void; onDateSelect?: (start: Date, end: Date) => void;
onEventDrop?: (eventId: string, newStart: Date, newEnd: Date | null) => void; onEventDrop?: (eventId: string, newStart: Date, newEnd: Date | null) => void;
@@ -31,6 +32,7 @@ export function AgendaCalendar({
events, events,
initialView = 'timeGridWeek', initialView = 'timeGridWeek',
currentView, currentView,
currentDate,
onEventClick, onEventClick,
onDateSelect, onDateSelect,
onEventDrop, onEventDrop,
@@ -47,7 +49,6 @@ export function AgendaCalendar({
if (api.view.type !== currentView) { if (api.view.type !== currentView) {
isChangingViewRef.current = true; isChangingViewRef.current = true;
api.changeView(currentView); api.changeView(currentView);
// Reset flag after a short delay to allow the view change to complete
setTimeout(() => { setTimeout(() => {
isChangingViewRef.current = false; isChangingViewRef.current = false;
}, 100); }, 100);
@@ -55,6 +56,20 @@ export function AgendaCalendar({
} }
}, [currentView]); }, [currentView]);
// Sync date when currentDate prop changes
useEffect(() => {
if (currentDate && internalRef.current) {
const api = internalRef.current.getApi();
if (api.getDate().toDateString() !== currentDate.toDateString()) {
isChangingViewRef.current = true;
api.gotoDate(currentDate);
setTimeout(() => {
isChangingViewRef.current = false;
}, 100);
}
}
}, [currentDate]);
const handleEventClick = useCallback((info: EventClickArg) => { const handleEventClick = useCallback((info: EventClickArg) => {
if (onEventClick) { if (onEventClick) {
const event = info.event; const event = info.event;

View File

@@ -118,7 +118,9 @@ export function AgendaView({ initialEvents, initialDate, highlightEncounterId }:
// Handle date range change from calendar // Handle date range change from calendar
const handleDateChange = useCallback((start: Date, end: Date) => { const handleDateChange = useCallback((start: Date, end: Date) => {
setCurrentDate(start); setCurrentDate((prev) =>
prev.toDateString() === start.toDateString() ? prev : start
);
fetchEvents(start, end); fetchEvents(start, end);
}, [fetchEvents]); }, [fetchEvents]);
@@ -262,6 +264,7 @@ export function AgendaView({ initialEvents, initialDate, highlightEncounterId }:
events={events} events={events}
initialView={currentView} initialView={currentView}
currentView={currentView} currentView={currentView}
currentDate={currentDate}
onEventClick={handleEventClick} onEventClick={handleEventClick}
onDateSelect={handleDateSelect} onDateSelect={handleDateSelect}
onEventDrop={handleEventDrop} onEventDrop={handleEventDrop}

View File

@@ -19,6 +19,7 @@ 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 { APPOINTMENT_TYPES, type AppointmentTypeCode, type LocationClassCode } from '@/app/epd/agenda/types';
import type { Artifact, BlockType } from '@/stores/cortex-store'; import type { Artifact, BlockType } from '@/stores/cortex-store';
import { parseRelativeDate, isDateRange } from '@/lib/cortex/date-time-parser';
interface ArtifactContainerProps { interface ArtifactContainerProps {
artifacts: Artifact[]; artifacts: Artifact[];
@@ -55,13 +56,13 @@ function coerceDate(value: unknown): Date | undefined {
} }
function coerceDateRange(raw: any): AgendaBlockProps['dateRange'] | undefined { function coerceDateRange(raw: any): AgendaBlockProps['dateRange'] | undefined {
const start = coerceDate(raw?.start); const label = typeof raw?.label === 'string' ? raw.label : undefined;
const end = coerceDate(raw?.end); if (!label) return undefined;
if (!start || !end) return undefined;
return { return {
start, start: coerceDate(raw?.start),
end, end: coerceDate(raw?.end),
label: typeof raw?.label === 'string' ? raw.label : 'custom', label,
}; };
} }
@@ -78,6 +79,21 @@ function resolveLocation(value: unknown): LocationClassCode | undefined {
return undefined; return undefined;
} }
/**
* Convert date label to Date using central date-time-parser (DRY)
*/
function resolveDateFromLabel(label?: string): Date | null {
if (!label) return null;
const parsed = parseRelativeDate(label);
if (!parsed) return null;
// DateRange → return start date
if (isDateRange(parsed)) return parsed.start;
return parsed;
}
function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlockProps['prefillData'] { function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlockProps['prefillData'] {
if (!prefill || typeof prefill !== 'object') return undefined; if (!prefill || typeof prefill !== 'object') return undefined;
@@ -90,9 +106,15 @@ function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlo
} }
: undefined); : undefined);
// Try to resolve date from label first (more reliable than AI-generated dates)
const dateLabel = prefill?.datetime?.label || prefill?.dateRange?.label;
const resolvedDate = resolveDateFromLabel(dateLabel);
const datetimeDate = const datetimeDate =
resolvedDate || // Prefer calculated date from label
coerceDate(prefill?.datetime?.date) || coerceDate(prefill?.datetime?.date) ||
(prefill?.datetime?.time ? new Date() : undefined); (prefill?.datetime?.time ? new Date() : undefined);
const datetime = datetimeDate const datetime = datetimeDate
? { ? {
date: datetimeDate, date: datetimeDate,
@@ -103,7 +125,12 @@ function buildAgendaPrefill(prefill: Record<string, any> | undefined): AgendaBlo
const appointmentType = resolveAppointmentType(prefill?.appointmentType || prefill?.type); const appointmentType = resolveAppointmentType(prefill?.appointmentType || prefill?.type);
const location = resolveLocation(prefill?.location); const location = resolveLocation(prefill?.location);
// Also resolve newDatetime from label
const newDateLabel = prefill?.newDatetime?.label;
const resolvedNewDate = resolveDateFromLabel(newDateLabel);
const newDatetimeDate = const newDatetimeDate =
resolvedNewDate || // Prefer calculated date from label
coerceDate(prefill?.newDatetime?.date) || coerceDate(prefill?.newDatetime?.date) ||
(prefill?.newDatetime?.time ? new Date() : undefined); (prefill?.newDatetime?.time ? new Date() : undefined);
const newDatetime = newDatetimeDate const newDatetime = newDatetimeDate

View File

@@ -1,16 +1,18 @@
'use client'; 'use client';
import React from 'react'; import React, { useState, useEffect } from 'react';
import { 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';
import { AgendaRescheduleForm } from './agenda-reschedule-form'; import { AgendaRescheduleForm } from './agenda-reschedule-form';
import { AgendaErrorState } from './agenda-error-state';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Loader2 } from 'lucide-react';
export interface AgendaBlockProps { export interface AgendaBlockProps {
mode: 'list' | 'create' | 'cancel' | 'reschedule'; mode: 'list' | 'create' | 'cancel' | 'reschedule';
appointments?: CalendarEvent[]; appointments?: CalendarEvent[];
dateRange?: { start: Date; end: Date; label: string }; dateRange?: { start?: Date; end?: Date; label: string };
prefillData?: { prefillData?: {
patient?: { id: string; name: string }; patient?: { id: string; name: string };
datetime?: { date: Date; time: string }; datetime?: { date: Date; time: string };
@@ -26,15 +28,92 @@ export interface AgendaBlockProps {
export function AgendaBlock({ export function AgendaBlock({
mode, mode,
appointments, appointments: initialAppointments,
dateRange, dateRange: initialDateRange,
prefillData, prefillData,
disambiguationOptions, disambiguationOptions,
onClose, onClose,
}: AgendaBlockProps) { }: AgendaBlockProps) {
// State for fetched appointments (only used in list mode)
const [appointments, setAppointments] = useState<CalendarEvent[] | undefined>(initialAppointments);
const [dateRange, setDateRange] = useState<{ start: Date; end: Date; label: string } | undefined>(initialDateRange);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [refetchKey, setRefetchKey] = useState(0);
// Fetch appointments when in list mode and no appointments are provided
// Server-side bepaalt de datum (consistent met EPD agenda)
useEffect(() => {
if (mode !== 'list') return;
if (initialAppointments && initialAppointments.length > 0) return;
const fetchAppointments = async () => {
setIsLoading(true);
setError(null);
try {
// Build query params - server bepaalt de datum
const params = new URLSearchParams();
// Stuur alleen label naar server, server berekent de datums
if (initialDateRange?.label) {
params.set('label', initialDateRange.label);
}
// Als geen label en geen expliciete datums, server defaults naar vandaag
const response = await fetch(`/api/cortex/agenda?${params}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Kon afspraken niet laden');
}
const data = await response.json();
setAppointments(data.appointments || []);
// Update dateRange met server-side berekende waarden
if (data.dateRange) {
setDateRange({
start: new Date(data.dateRange.start),
end: new Date(data.dateRange.end),
label: data.dateRange.label,
});
}
} catch (err) {
console.error('Error fetching appointments:', err);
setError(err instanceof Error ? err.message : 'Er ging iets mis');
} finally {
setIsLoading(false);
}
};
fetchAppointments();
}, [mode, initialAppointments, initialDateRange, refetchKey]);
const renderContent = () => { const renderContent = () => {
switch (mode) { switch (mode) {
case 'list': case 'list':
if (isLoading) {
return (
<div key="loading" className="flex flex-col items-center justify-center h-full text-slate-500">
<Loader2 className="h-8 w-8 animate-spin text-teal-600 mb-4" />
<p className="text-sm">Afspraken laden...</p>
</div>
);
}
if (error) {
return (
<AgendaErrorState
key="error"
error={error}
context="query"
onRetry={() => {
setError(null);
setRefetchKey((k) => k + 1); // Trigger refetch
}}
/>
);
}
return ( return (
<AgendaListView <AgendaListView
key="list" key="list"

View File

@@ -17,7 +17,7 @@ import {
interface AgendaListViewProps { interface AgendaListViewProps {
appointments?: CalendarEvent[]; appointments?: CalendarEvent[];
dateRange?: { start: Date; end: Date; label: string }; dateRange?: { start?: Date; end?: Date; label: string };
onClose?: () => void; onClose?: () => void;
onCancelAppointment?: (encounter: CalendarEvent) => void; onCancelAppointment?: (encounter: CalendarEvent) => void;
onViewDetails?: (encounter: CalendarEvent) => void; onViewDetails?: (encounter: CalendarEvent) => void;
@@ -48,7 +48,7 @@ export function AgendaListView({
const formatDateLabel = () => { const formatDateLabel = () => {
if (dateRange?.label) { if (dateRange?.label) {
if (dateRange.label === 'vandaag' || dateRange.label === 'morgen') { if ((dateRange.label === 'vandaag' || dateRange.label === 'morgen') && dateRange.start) {
const dateStr = format(dateRange.start, 'd MMMM', { locale: nl }); const dateStr = format(dateRange.start, 'd MMMM', { locale: nl });
return `Afspraken ${dateRange.label} - ${dateStr}`; return `Afspraken ${dateRange.label} - ${dateStr}`;
} }

View File

@@ -0,0 +1,180 @@
'use client';
/**
* Chat Empty State Component
*
* Interactive grid of cards shown when the chat has no messages.
* Each card represents a main capability category.
*/
import { motion } from 'framer-motion';
import { FileText, Calendar, Search, ClipboardList } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ChatEmptyStateProps {
/** Callback when a quick action card is clicked */
onSelectAction: (text: string) => void;
/** Optional active patient name */
activePatientName?: string;
}
const quickActions = [
{
id: 'notitie',
icon: FileText,
title: 'Dagnotitie maken',
description: 'Registreer observaties, medicatie of incidenten',
example: 'Notitie [naam] medicatie gegeven',
color: 'bg-blue-50 text-blue-600 border-blue-200',
hoverColor: 'hover:bg-blue-100 hover:border-blue-300',
},
{
id: 'agenda',
icon: Calendar,
title: 'Agenda bekijken',
description: 'Bekijk of plan afspraken',
example: 'Agenda vandaag',
color: 'bg-emerald-50 text-emerald-600 border-emerald-200',
hoverColor: 'hover:bg-emerald-100 hover:border-emerald-300',
},
{
id: 'zoeken',
icon: Search,
title: 'Patiënt zoeken',
description: 'Zoek in dossiers en patiëntgegevens',
example: 'Zoek [naam]',
color: 'bg-violet-50 text-violet-600 border-violet-200',
hoverColor: 'hover:bg-violet-100 hover:border-violet-300',
},
{
id: 'overdracht',
icon: ClipboardList,
title: 'Overdracht maken',
description: 'Genereer een samenvatting voor de volgende dienst',
example: 'Overdracht',
color: 'bg-amber-50 text-amber-600 border-amber-200',
hoverColor: 'hover:bg-amber-100 hover:border-amber-300',
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
},
},
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.3,
ease: 'easeOut',
},
},
};
export function ChatEmptyState({ onSelectAction, activePatientName }: ChatEmptyStateProps) {
const handleCardClick = (example: string) => {
// Replace [naam] with patient name if available
const text = activePatientName
? example.replace(/\[naam\]/g, activePatientName)
: example;
onSelectAction(text);
};
return (
<div className="flex-1 flex flex-col items-center justify-center p-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="text-center mb-8"
>
<div className="text-4xl mb-3">💬</div>
<h2 className="text-xl font-semibold text-slate-800 mb-2">
Welkom bij Cortex Assistent
</h2>
<p className="text-sm text-slate-500 max-w-md">
Typ of spreek wat je wilt doen. Klik op een kaart om snel te starten.
</p>
</motion.div>
{/* Action cards grid */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-1 sm:grid-cols-2 gap-3 w-full max-w-lg"
>
{quickActions.map((action) => {
const Icon = action.icon;
const displayExample = activePatientName
? action.example.replace(/\[naam\]/g, activePatientName)
: action.example;
return (
<motion.button
key={action.id}
variants={cardVariants}
onClick={() => handleCardClick(action.example)}
className={cn(
'group relative text-left p-4 rounded-xl border-2',
'transition-all duration-200',
'active:scale-[0.98]',
action.color,
action.hoverColor
)}
>
{/* Icon */}
<div className="flex items-start gap-3">
<div
className={cn(
'p-2 rounded-lg bg-white/60',
'group-hover:bg-white group-hover:shadow-sm',
'transition-all duration-200'
)}
>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-slate-800 text-sm mb-0.5">
{action.title}
</h3>
<p className="text-xs text-slate-500 mb-2">
{action.description}
</p>
<p className="text-[11px] text-slate-400 italic truncate">
&ldquo;{displayExample}&rdquo;
</p>
</div>
</div>
</motion.button>
);
})}
</motion.div>
{/* Keyboard hint */}
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-xs text-slate-400 mt-6"
>
Tip: Gebruik{' '}
<kbd className="px-1.5 py-0.5 bg-slate-100 border border-slate-300 rounded text-xs">
K
</kbd>{' '}
om direct te beginnen met typen
</motion.p>
</div>
);
}

View File

@@ -23,6 +23,7 @@ interface ChatInputProps {
export interface ChatInputHandle { export interface ChatInputHandle {
focus: () => void; focus: () => void;
clear: () => void; clear: () => void;
setValue: (value: string) => void;
} }
export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function ChatInput({ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function ChatInput({
@@ -34,7 +35,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const addChatMessage = useCortexStore((s) => s.addChatMessage); const addChatMessage = useCortexStore((s) => s.addChatMessage);
// Expose focus and clear methods to parent // Expose focus, clear, and setValue methods to parent
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
focus: () => { focus: () => {
textareaRef.current?.focus(); textareaRef.current?.focus();
@@ -45,6 +46,21 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = 'auto';
} }
}, },
setValue: (value: string) => {
setInputValue(value);
// Auto-resize after setting value
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
// Use setTimeout to ensure the value is set before measuring
setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}, 0);
}
// Focus the input after setting value
textareaRef.current?.focus();
},
})); }));
// Handle input change and auto-resize // Handle input change and auto-resize

View File

@@ -15,12 +15,14 @@ import { ArrowDown } from 'lucide-react';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { ChatMessage } from './chat-message'; import { ChatMessage } from './chat-message';
import { ChatInput, ChatInputHandle } from './chat-input'; import { ChatInput, ChatInputHandle } from './chat-input';
import { ChatSuggestions } from './chat-suggestions';
import { ChatEmptyState } from './chat-empty-state';
import { ActionChainCard } from './action-chain-card'; import { ActionChainCard } from './action-chain-card';
import { ClarificationCard } from './clarification-card'; import { ClarificationCard } from './clarification-card';
import { ProcessingIndicator } from './processing-indicator'; import { ProcessingIndicator } from './processing-indicator';
import { useCortexStore } from '@/stores/cortex-store'; import { useCortexStore } from '@/stores/cortex-store';
import { sendChatMessage } from '@/lib/cortex/chat-api'; import { sendChatMessage } from '@/lib/cortex/chat-api';
import { parseActionFromResponse, shouldOpenArtifact, routeIntentToArtifact } from '@/lib/cortex/action-parser'; import { parseActionFromResponse, shouldOpenArtifact, routeIntentToArtifact, getDefaultConfirmationMessage } from '@/lib/cortex/action-parser';
import { evaluateNudge } from '@/lib/cortex/nudge'; import { evaluateNudge } from '@/lib/cortex/nudge';
import { isFeatureEnabled } from '@/lib/config/feature-flags'; import { isFeatureEnabled } from '@/lib/config/feature-flags';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -61,8 +63,17 @@ export function ChatPanel() {
const [isScrolledUp, setIsScrolledUp] = useState(false); const [isScrolledUp, setIsScrolledUp] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false);
// Intent Helper state: minimize suggestions after first message
const [isSuggestionsMinimized, setIsSuggestionsMinimized] = useState(false);
const hasAutoMinimizedRef = useRef(false);
const hasMessages = chatMessages.length > 0; const hasMessages = chatMessages.length > 0;
// Get active patient name for suggestion placeholders
const activePatientName = activePatient
? `${activePatient.name_given?.[0] || ''} ${activePatient.name_family || ''}`.trim()
: undefined;
// Scroll to bottom function // Scroll to bottom function
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => { const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
messagesEndRef.current?.scrollIntoView({ behavior }); messagesEndRef.current?.scrollIntoView({ behavior });
@@ -95,6 +106,19 @@ export function ChatPanel() {
scrollToBottom('auto'); scrollToBottom('auto');
}, [scrollToBottom]); }, [scrollToBottom]);
// Auto-minimize suggestions after first message is sent (only once)
useEffect(() => {
if (hasMessages && !hasAutoMinimizedRef.current) {
hasAutoMinimizedRef.current = true;
setIsSuggestionsMinimized(true);
}
}, [hasMessages]);
// Handle suggestion selection - fill input with selected text
const handleSelectSuggestion = useCallback((text: string) => {
chatInputRef.current?.setValue(text);
}, []);
// Global keyboard shortcuts // Global keyboard shortcuts
useEffect(() => { useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => { const handleGlobalKeyDown = (e: KeyboardEvent) => {
@@ -280,23 +304,10 @@ export function ChatPanel() {
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center h-full"> <ChatEmptyState
<div className="max-w-md text-center text-slate-500"> onSelectAction={handleSelectSuggestion}
<div className="text-4xl mb-4">💬</div> activePatientName={activePatientName}
<h3 className="text-lg font-medium text-slate-700 mb-2"> />
Welkom bij Cortex Assistent
</h3>
<p className="text-sm mb-4">
Typ of spreek wat je wilt doen...
</p>
<div className="text-left text-sm space-y-1 bg-slate-50 rounded-lg p-4">
<p className="font-medium text-slate-700 mb-2">Voorbeelden:</p>
<p> &ldquo;Notitie voor Jan: medicatie gegeven&rdquo;</p>
<p> &ldquo;Zoek Marie van den Berg&rdquo;</p>
<p> &ldquo;Maak overdracht voor deze dienst&rdquo;</p>
</div>
</div>
</div>
)} )}
{/* Scroll to bottom button */} {/* Scroll to bottom button */}
@@ -319,6 +330,14 @@ export function ChatPanel() {
)} )}
</div> </div>
{/* Suggestion strip above input */}
<ChatSuggestions
isMinimized={isSuggestionsMinimized}
onToggleMinimize={() => setIsSuggestionsMinimized(!isSuggestionsMinimized)}
onSelectSuggestion={handleSelectSuggestion}
activePatientName={activePatientName}
/>
{/* Chat input */} {/* Chat input */}
<ChatInput <ChatInput
ref={chatInputRef} ref={chatInputRef}
@@ -361,8 +380,12 @@ export function ChatPanel() {
if (parsed.action) { if (parsed.action) {
console.log('[ChatPanel] Action detected:', parsed.action); console.log('[ChatPanel] Action detected:', parsed.action);
// Update last message with cleaned text content and action // If textContent is empty but we have an action, generate a default confirmation message
updateLastMessage(parsed.textContent, parsed.action); const displayContent = parsed.textContent.trim() ||
getDefaultConfirmationMessage(parsed.action.intent, parsed.action.entities);
// Update last message with text content (or default) and action
updateLastMessage(displayContent, parsed.action);
// Store action in pendingAction for artifact opening (E3.S6) // Store action in pendingAction for artifact opening (E3.S6)
if (shouldOpenArtifact(parsed.action.confidence)) { if (shouldOpenArtifact(parsed.action.confidence)) {

View File

@@ -0,0 +1,160 @@
'use client';
/**
* Chat Suggestions Component
*
* A categorized suggestion strip above the chat input.
* Shows tabs for categories and clickable chips with example sentences.
*
* Features:
* - Minimizable after first message
* - Click to fill input (not send)
* - Contextual patient name replacement
*/
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronDown, ChevronUp, Lightbulb } from 'lucide-react';
import { cn } from '@/lib/utils';
import { SUGGESTION_CATEGORIES, replacePlaceholder } from '@/lib/cortex/suggestions';
interface ChatSuggestionsProps {
/** Whether the strip should be minimized */
isMinimized: boolean;
/** Callback when minimized state changes */
onToggleMinimize: () => void;
/** Callback when a suggestion is clicked */
onSelectSuggestion: (text: string) => void;
/** Optional active patient name to replace [naam] placeholders */
activePatientName?: string;
}
export function ChatSuggestions({
isMinimized,
onToggleMinimize,
onSelectSuggestion,
activePatientName,
}: ChatSuggestionsProps) {
const [activeCategory, setActiveCategory] = useState(SUGGESTION_CATEGORIES[0].id);
const currentCategory = SUGGESTION_CATEGORIES.find((c) => c.id === activeCategory);
const handleChipClick = (text: string, hasPlaceholder?: boolean) => {
// Replace placeholder with patient name if available, otherwise keep placeholder
const finalText = hasPlaceholder
? replacePlaceholder(text, activePatientName)
: text;
onSelectSuggestion(finalText);
};
return (
<div className="border-t border-slate-100 bg-gradient-to-b from-slate-50/80 to-white">
{/* Minimized state - just a button */}
<AnimatePresence mode="wait">
{isMinimized ? (
<motion.button
key="minimized"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
onClick={onToggleMinimize}
className={cn(
'w-full flex items-center justify-center gap-2 py-2',
'text-xs text-slate-500 hover:text-slate-700',
'hover:bg-slate-50 transition-colors'
)}
>
<Lightbulb className="w-3.5 h-3.5" />
<span>Toon suggesties</span>
<ChevronUp className="w-3.5 h-3.5" />
</motion.button>
) : (
<motion.div
key="expanded"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
{/* Header with minimize button */}
<div className="flex items-center justify-between px-4 pt-2 pb-1">
<div className="flex items-center gap-1.5 text-xs text-slate-500">
<Lightbulb className="w-3.5 h-3.5" />
<span>Wat kan ik vragen?</span>
</div>
<button
onClick={onToggleMinimize}
className={cn(
'text-slate-400 hover:text-slate-600',
'p-1 rounded hover:bg-slate-100 transition-colors'
)}
aria-label="Minimaliseer suggesties"
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
{/* Category tabs */}
<div className="flex gap-1 px-3 pb-2 overflow-x-auto">
{SUGGESTION_CATEGORIES.map((category) => (
<button
key={category.id}
onClick={() => setActiveCategory(category.id)}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-full',
'text-xs font-medium whitespace-nowrap',
'transition-all duration-200',
activeCategory === category.id
? 'bg-brand-100 text-brand-700 shadow-sm'
: 'bg-white text-slate-600 hover:bg-slate-100 border border-slate-200'
)}
>
<span>{category.icon}</span>
<span>{category.label}</span>
</button>
))}
</div>
{/* Example chips */}
<div className="px-3 pb-3">
<motion.div
key={activeCategory}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15 }}
className="flex flex-wrap gap-2"
>
{currentCategory?.examples.map((example, index) => (
<button
key={index}
onClick={() => handleChipClick(example.text, example.hasPatientPlaceholder)}
className={cn(
'text-[11px] leading-relaxed',
'bg-white border border-slate-200 rounded-full',
'px-3 py-1.5',
'text-slate-700',
'hover:bg-brand-50 hover:border-brand-200 hover:text-brand-700',
'active:scale-[0.98]',
'transition-all duration-150',
'shadow-sm hover:shadow'
)}
>
&ldquo;{example.hasPatientPlaceholder && activePatientName
? replacePlaceholder(example.text, activePatientName)
: example.text}&rdquo;
</button>
))}
</motion.div>
{currentCategory && (
<p className="text-[10px] text-slate-400 mt-2 px-1">
{currentCategory.description}
</p>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,717 @@
# 📊 Analyse: Intents & Nudges — Cortex V2
**Datum:** 01-01-2026
**Status:** Analyse voor MVP uitbreiding
**Auteur:** Colin Lit (met AI-assistentie)
---
## 1. Samenvatting
Dit document bevat een uitgebreide analyse van:
- De huidige geïmplementeerde intents en nudges in Cortex V2
- De EPD-functionaliteit die beschikbaar is voor nudge triggers
- Aanbevelingen voor nieuwe nudges gebaseerd op zorgprotocollen
- **Nieuw:** Analyse van query/overzicht intents voor rapportages, behandelplannen en risico's
**Conclusies:**
1. De huidige 7 intents dekken basis acties, maar missen **query/overzicht** functionaliteit
2. Er worden 7 nieuwe nudges aanbevolen om de "proactieve AI collega" beter te demonstreren
3. Er worden 4 nieuwe query intents aanbevolen voor overzichten (rapportages, behandelplan, risico's, dashboard)
---
## 2. Huidige Implementatie
### 2.1 Intents (7 + unknown)
| Intent | Nederlands Label | UI Block | Categorie | Status |
|--------|-----------------|----------|-----------|--------|
| `dagnotitie` | Notitie | ✅ `dagnotitie-block.tsx` | Actie | Volledig |
| `zoeken` | Patiënt zoeken | ✅ `zoeken-block.tsx` | Query | Volledig |
| `overdracht` | Overdracht | ✅ `overdracht-block.tsx` | Query | Volledig |
| `agenda_query` | Agenda | ⚠️ Via artifact | Query | Basis |
| `create_appointment` | Nieuwe afspraak | ⚠️ Via artifact | Actie | Basis |
| `cancel_appointment` | Afspraak annuleren | ⚠️ Via artifact | Actie | Basis |
| `reschedule_appointment` | Afspraak verzetten | ⚠️ Via artifact | Actie | Basis |
| `unknown` | Fallback | — | — | N.v.t. |
**Locatie:** `lib/cortex/types.ts`
### 2.2 Overzicht Mogelijkheden (Query Intents)
#### ✅ Wat WEL via spraak bereikbaar is
| Intent | Input Voorbeeld | Output |
|--------|-----------------|--------|
| `agenda_query` | "Agenda vandaag", "Afspraken deze week" | `AgendaListView` - lijst van afspraken met periode filter |
| `overdracht` | "Overdracht", "Samenvatting" | `OverdrachtBlock` - AI-samenvatting per patiënt met aandachtspunten |
| `zoeken` | "Zoek Jan", "Wie is Marie" | `ZoekenBlock` - patiënt zoekresultaten |
De **OverdrachtBlock** is krachtig:
- Genereert AI-samenvatting per patiënt
- Toont aandachtspunten met bronverwijzingen
- Toont actiepunten
- Filterbaar per periode (1d, 3d, 7d, 14d)
- Filterbaar per rol (verpleegkundige/psychiater)
#### ❌ Wat NIET via spraak bereikbaar is
| Gewenste Input | Huidige Status | Bestaande UI |
|----------------|----------------|--------------|
| "Toon rapportages van Jan" | ❌ Geen intent | `app/epd/patients/[id]/rapportage/` bestaat |
| "Wat is het behandelplan van Jan?" | ❌ Geen intent | `components/behandelplan/` bestaat |
| "Wat zijn de risico's van Jan?" | ❌ Geen intent | `RisksBlock` in verpleegrapportage bestaat |
| "Overzicht Jan" / "Dashboard Jan" | ❌ Geen intent | ✅ `PatientDashboardBlock` bestaat maar niet bereikbaar!
### 2.3 Nudges (2 protocol rules)
| ID | Naam | Trigger | Suggestie | Priority |
|----|------|---------|-----------|----------|
| `wondzorg-controle` | Wondcontrole na verzorging | `dagnotitie` + content bevat "wond" | "Wondcontrole inplannen over 3 dagen?" → `create_appointment` | medium |
| `medicatie-controle` | Medicatie controle na wijziging | `dagnotitie` + content bevat "medicatie" EN "gewijzigd" | "Medicatie evaluatie inplannen over 1 week?" → `create_appointment` | medium |
**Locatie:** `lib/cortex/nudge.ts`
---
## 3. EPD Functionaliteit Analyse
### 3.1 Beschikbare Modules
| Module | Pad | Relevante Data voor Nudges |
|--------|-----|---------------------------|
| **Rapportage** | `app/epd/patients/[id]/rapportage/` | Report types, categories |
| **Risico Taxatie** | `intakes/[intakeId]/risk/` | Risk types, risk levels |
| **Verpleegrapportage** | `app/epd/verpleegrapportage/` | Vitals, AI samenvatting |
| **Agenda** | `app/epd/agenda/` | Appointment types |
| **Intake** | `intakes/[intakeId]/` | Anamnese, diagnose, kindcheck |
| **Behandelplan** | `patients/[id]/behandelplan/` | Leefgebieden, doelen |
| **Screening** | `patients/[id]/screening/` | Hulpvraag, besluit |
### 3.2 Report Types (voor nudge triggers)
```typescript
// lib/types/report.ts
export const REPORT_TYPES = [
'voortgang',
'observatie',
'incident', // → Trigger voor MIC-melding
'medicatie', // → Trigger voor evaluatie
'contact',
'crisis', // → Trigger voor team informeren
'intake',
'behandeladvies',
'vrije_notitie',
'verpleegkundig',
] as const;
```
### 3.3 Verpleegkundige Categorieën
```typescript
// lib/types/report.ts
export const VERPLEEGKUNDIG_CATEGORIES = [
'medicatie', // → Trigger voor medicatie nudges
'adl', // → Trigger voor zorgplan nudges
'gedrag', // → Trigger voor risico nudges
'incident', // → Trigger voor MIC nudges
'observatie',
] as const;
```
### 3.4 Risk Types (in EPD)
```typescript
// app/epd/patients/[id]/intakes/[intakeId]/risk/components/risk-manager.tsx
const riskTypeOptions = [
{ value: 'suicidaliteit', label: 'Suïcidaliteit' },
{ value: 'agressie', label: 'Agressie' },
{ value: 'zelfverwaarlozing', label: 'Zelfverwaarlozing' },
{ value: 'middelenmisbruik', label: 'Middelenmisbruik' },
{ value: 'verward_gedrag', label: 'Verward gedrag' },
{ value: 'overig', label: 'Overig' },
];
// app/epd/verpleegrapportage/components/blocks/risks-block.tsx (verpleegkunde)
const riskTypes = {
valrisico: 'Valrisico',
decubitus: 'Decubitus',
ondervoeding: 'Ondervoeding',
delier: 'Delier',
infectie: 'Infectie',
suiciderisico: 'Suïciderisico',
agressie: 'Agressie',
weglopen: 'Weglopen',
};
```
### 3.5 Appointment Types
```typescript
// lib/cortex/types.ts
appointmentType?: 'intake' | 'behandeling' | 'follow-up' | 'telefonisch' |
'huisbezoek' | 'online' | 'crisis' | 'overig';
```
---
## 4. Aanbevelingen: Nieuwe Nudges
### 4.1 Overzicht Aanbevolen Nudges
| # | ID | Trigger Intent | Conditions | Suggestie | Priority |
|---|-----|---------------|------------|-----------|----------|
| 1 | `suicidaliteit-veiligheidsplan` | dagnotitie | content matches `suïcida\|zelfmoord\|zelfbeschadig` | Veiligheidsplan actualiseren | 🔴 high |
| 2 | `crisis-team-informeren` | dagnotitie | content matches `crisis\|noodgeval\|acuut` | Crisisteam informeren | 🔴 high |
| 3 | `incident-mic-melding` | dagnotitie | category = `incident` | MIC-melding invullen | 🔴 high |
| 4 | `gedrag-risico-update` | dagnotitie | category = `gedrag` + content matches `agressie\|dreigend\|verward` | Risicotaxatie bijwerken | 🟡 medium |
| 5 | `adl-zorgplan-update` | dagnotitie | category = `adl` + content matches `hulp nodig\|verslechter` | Zorgplan bijwerken | 🟡 medium |
| 6 | `intake-behandelplan` | create_appointment | appointmentType = `intake` | Behandelplan opstellen | 🔵 low |
| 7 | `handover-mark` | dagnotitie | content matches `let op\|belangrijk\|doorgeven` | Opnemen in overdracht | 🔵 low |
### 4.2 Gedetailleerde Specificaties
#### 🔴 HIGH Priority — Safety & Compliance
**1. suicidaliteit-veiligheidsplan**
```typescript
{
id: 'suicidaliteit-veiligheidsplan',
name: 'Veiligheidsplan bij suïcidaliteit',
trigger: {
intent: 'dagnotitie',
conditions: [
{
field: 'content',
operator: 'matches',
value: 'suïcida|zelfmoord|zelfbeschadig|automutil'
},
],
},
suggestion: {
intent: 'dagnotitie',
message: '⚠️ Veiligheidsplan actualiseren en risicotaxatie bijwerken?',
prefillEntities: (source) => ({
patientName: source.patientName,
category: 'observatie',
}),
},
priority: 'high',
enabled: true,
expiresAfterMs: 10 * 60 * 1000, // 10 minuten - urgent
}
```
**Rationale:** Vereist door GGZ-richtlijnen. Bij suïcidaliteit moet altijd het veiligheidsplan geëvalueerd worden.
---
**2. crisis-team-informeren**
```typescript
{
id: 'crisis-team-informeren',
name: 'Team informeren bij crisis',
trigger: {
intent: 'dagnotitie',
conditions: [
{
field: 'content',
operator: 'matches',
value: 'crisis|noodgeval|acuut|spoed'
},
],
},
suggestion: {
intent: 'dagnotitie',
message: '🚨 Crisisteam en dienstdoende psychiater informeren?',
prefillEntities: (source) => ({
patientName: source.patientName,
category: 'incident',
}),
},
priority: 'high',
enabled: true,
expiresAfterMs: 10 * 60 * 1000,
}
```
**Rationale:** Crisis situaties vereisen multidisciplinaire afstemming.
---
**3. incident-mic-melding**
```typescript
{
id: 'incident-mic-melding',
name: 'MIC-melding na incident',
trigger: {
intent: 'dagnotitie',
conditions: [
{ field: 'category', operator: 'equals', value: 'incident' },
],
},
suggestion: {
intent: 'dagnotitie',
message: '⚠️ MIC-melding invullen voor dit incident?',
prefillEntities: (source) => ({
patientName: source.patientName,
category: 'incident',
}),
},
priority: 'high',
enabled: true,
expiresAfterMs: 15 * 60 * 1000,
}
```
**Rationale:** MIC (Melding Incidenten Cliënten) is wettelijk verplicht bij incidenten in de zorg.
---
#### 🟡 MEDIUM Priority — Protocol & Follow-up
**4. gedrag-risico-update**
```typescript
{
id: 'gedrag-risico-update',
name: 'Risicotaxatie bij gedragsverandering',
trigger: {
intent: 'dagnotitie',
conditions: [
{ field: 'category', operator: 'equals', value: 'gedrag' },
{
field: 'content',
operator: 'matches',
value: 'agressie|agressief|dreigend|onrustig|agitatie|verward'
},
],
},
suggestion: {
intent: 'dagnotitie',
message: '📋 Risicotaxatie bijwerken voor dit gedrag?',
prefillEntities: (source) => ({
patientName: source.patientName,
category: 'observatie',
}),
},
priority: 'medium',
enabled: true,
expiresAfterMs: 5 * 60 * 1000,
}
```
**Rationale:** Gedragsverandering kan indicatie zijn voor veranderd risicoprofiel.
---
**5. adl-zorgplan-update**
```typescript
{
id: 'adl-zorgplan-update',
name: 'Zorgplan update bij ADL wijziging',
trigger: {
intent: 'dagnotitie',
conditions: [
{ field: 'category', operator: 'equals', value: 'adl' },
{
field: 'content',
operator: 'matches',
value: 'hulp nodig|niet meer zelfstandig|verslechter|achteruit'
},
],
},
suggestion: {
intent: 'dagnotitie',
message: '🏠 Zorgplan/behandelplan bijwerken voor gewijzigde ADL?',
prefillEntities: (source) => ({
patientName: source.patientName,
category: 'observatie',
}),
},
priority: 'medium',
enabled: true,
expiresAfterMs: 5 * 60 * 1000,
}
```
**Rationale:** ADL-veranderingen moeten leiden tot zorgplan-aanpassingen.
---
#### 🔵 LOW Priority — Administratief & Workflow
**6. intake-behandelplan**
```typescript
{
id: 'intake-behandelplan',
name: 'Behandelplan na intake',
trigger: {
intent: 'create_appointment',
conditions: [
{ field: 'appointmentType', operator: 'equals', value: 'intake' },
],
},
suggestion: {
intent: 'dagnotitie',
message: '📝 Behandelplan opstellen na de intake?',
prefillEntities: (source) => ({
patientName: source.patientName,
category: 'observatie',
}),
},
priority: 'low',
enabled: true,
expiresAfterMs: 30 * 60 * 1000, // 30 minuten
}
```
**Rationale:** Na een intake hoort een behandelplan opgesteld te worden.
---
**7. handover-mark**
```typescript
{
id: 'handover-mark',
name: 'Opnemen in overdracht',
trigger: {
intent: 'dagnotitie',
conditions: [
{
field: 'content',
operator: 'matches',
value: 'let op|belangrijk|doorgeven|overdracht|collega|volgende dienst'
},
],
},
suggestion: {
intent: 'dagnotitie',
message: '📋 Deze notitie opnemen in de dienst-overdracht?',
prefillEntities: (source) => ({
patientName: source.patientName,
}),
},
priority: 'low',
enabled: true,
expiresAfterMs: 5 * 60 * 1000,
}
```
**Rationale:** Belangrijke informatie moet in de overdracht terechtkomen.
---
## 5. Totaaloverzicht na Implementatie
### 5.1 Alle Nudges (9 totaal)
| # | ID | Trigger | Priority | Expiry | Status |
|---|-----|---------|----------|--------|--------|
| 1 | `wondzorg-controle` | dagnotitie + "wond" | 🟡 medium | 5 min | ✅ Bestaand |
| 2 | `medicatie-controle` | dagnotitie + "medicatie" + "gewijzigd" | 🟡 medium | 5 min | ✅ Bestaand |
| 3 | `suicidaliteit-veiligheidsplan` | dagnotitie + suïcide keywords | 🔴 high | 10 min | 🆕 Nieuw |
| 4 | `crisis-team-informeren` | dagnotitie + crisis keywords | 🔴 high | 10 min | 🆕 Nieuw |
| 5 | `incident-mic-melding` | dagnotitie + category=incident | 🔴 high | 15 min | 🆕 Nieuw |
| 6 | `gedrag-risico-update` | dagnotitie + category=gedrag + agressie | 🟡 medium | 5 min | 🆕 Nieuw |
| 7 | `adl-zorgplan-update` | dagnotitie + category=adl + verslechtering | 🟡 medium | 5 min | 🆕 Nieuw |
| 8 | `intake-behandelplan` | create_appointment + type=intake | 🔵 low | 30 min | 🆕 Nieuw |
| 9 | `handover-mark` | dagnotitie + "belangrijk" keywords | 🔵 low | 5 min | 🆕 Nieuw |
### 5.2 Verdeling per Priority
- **🔴 HIGH (3):** Safety-gerelateerd, vereisen directe actie
- **🟡 MEDIUM (4):** Protocol-gerelateerd, binnen dienst afhandelen
- **🔵 LOW (2):** Administratief, handig maar niet urgent
### 5.3 Verdeling per Trigger Intent
- **dagnotitie (8):** Meeste nudges triggeren op notities
- **create_appointment (1):** Na intake afspraak
---
## 6. Demo Impact
### 6.1 Uitgebreide Demo Scene 4
Met de nieuwe nudges kan de demo uitgebreid worden:
| Stap | Input | Verwachte Nudge | Priority |
|------|-------|-----------------|----------|
| 4a | "Wond verzorgd, ziet er goed uit" | Wondcontrole inplannen | 🟡 medium |
| 4b | "Patient was vandaag erg agressief" | Risicotaxatie bijwerken | 🟡 medium |
| 4c | "Incident: patient gevallen in gang" | MIC-melding invullen | 🔴 high |
| 4d | "Crisis: acute suïcidaliteit gemeld" | Veiligheidsplan + team | 🔴 high |
### 6.2 Demo Script Aanpassing
```markdown
### Scene 4: Proactiviteit (uitgebreid - 2.5 min)
**Stap 1:** "Wond verzorgd, ziet er goed uit"
→ NudgeToast (medium/oranje): "Wondcontrole inplannen over 3 dagen?"
**Highlight:** "Systeem kent zorgprotocollen"
**Stap 2:** "Patient was vandaag agressief naar medepatiënt"
→ NudgeToast (medium/oranje): "Risicotaxatie bijwerken?"
**Highlight:** "Gedrag wordt gekoppeld aan risico-assessment"
**Stap 3:** "Crisis: patient spreekt over suïcide"
→ NudgeToast (high/rood): "⚠️ Veiligheidsplan actualiseren?"
**Highlight:** "Hoge prioriteit suggesties vallen direct op"
```
### 6.3 Potentiële Demo met Query Intents
Als de query intents geïmplementeerd worden:
```markdown
### Scene 5: Informatie Ophalen (1.5 min)
**Stap 1:** "Overzicht Jan"
→ PatientDashboardBlock met basisgegevens, intakes, afspraken
**Highlight:** "Direct patiëntinformatie zonder klikken"
**Stap 2:** "Wat zijn de risico's?"
→ Risico overzicht met levels en maatregelen
**Highlight:** "Contextueel - weet dat we over Jan praten"
**Stap 3:** "Toon rapportages van deze week"
→ Gefilterde rapportage lijst
**Highlight:** "Natuurlijke tijdsaanduiding werkt"
```
---
## 7. Technische Implementatie
### 7.1 Benodigde Wijzigingen
| Bestand | Actie |
|---------|-------|
| `lib/cortex/nudge.ts` | 7 nieuwe `ProtocolRule` entries toevoegen |
| `components/cortex/command-center/nudge-toast.tsx` | Styling voor high priority (rood) verifiëren |
| `docs/intent/demo-script-cortex-v2.md` | Demo script uitbreiden met nieuwe scenes |
### 7.2 Backward Compatibility
Alle nieuwe nudges zijn **additief** - bestaande functionaliteit blijft werken. De feature flag `CORTEX_NUDGE` moet `true` zijn om nudges te tonen.
---
## 8. Aanbevelingen: Nieuwe Query Intents
### 8.1 Overzicht Aanbevolen Query Intents
| # | Intent | Input Voorbeelden | Output | Prioriteit |
|---|--------|-------------------|--------|------------|
| 1 | `patient_overview` | "Overzicht Jan", "Dashboard Marie" | `PatientDashboardBlock` (bestaat al!) | 🔴 Hoog |
| 2 | `rapportage_query` | "Toon rapportages van Jan", "Notities deze week" | Rapportage lijst met filters | 🟡 Middel |
| 3 | `behandelplan_query` | "Wat is het behandelplan?", "Toon doelen" | Behandelplan overzicht | 🟡 Middel |
| 4 | `risico_query` | "Wat zijn de risico's?", "Risicotaxatie" | Risico overzicht | 🟡 Middel |
### 8.2 Gedetailleerde Specificaties Query Intents
#### 🔴 HIGH Priority — `patient_overview`
**Waarom hoog?** De UI (`PatientDashboardBlock`) bestaat al, alleen de intent routing ontbreekt.
**Input voorbeelden:**
- "Overzicht Jan"
- "Dashboard Marie"
- "Patiëntoverzicht"
**Pattern matching (Reflex):**
```typescript
patient_overview: [
{ pattern: /^overzicht\s+\w+/i, weight: 1.0 },
{ pattern: /^dashboard\s+\w+/i, weight: 1.0 },
{ pattern: /^pati[eë]nt(en)?overzicht\b/i, weight: 0.9 },
{ pattern: /^toon\s+(alles|info)\s+(van\s+)?\w+/i, weight: 0.85 },
]
```
**Entities:**
```typescript
{
patientName: string;
}
```
**Output:** `PatientDashboardBlock` - toont:
- Basisgegevens (naam, geboortedatum, BSN)
- Recente intakes
- Agenda afspraken
- Actief behandelplan (doelen/interventies count)
---
#### 🟡 MEDIUM Priority — `rapportage_query`
**Input voorbeelden:**
- "Toon rapportages van Jan"
- "Wat is er gerapporteerd deze week?"
- "Rapportages vandaag"
- "Notities van Marie"
**Pattern matching (Reflex):**
```typescript
rapportage_query: [
{ pattern: /^(toon\s+)?rapportages?\b/i, weight: 1.0 },
{ pattern: /^wat\s+is\s+er\s+gerapporteerd\b/i, weight: 0.95 },
{ pattern: /^overzicht\s+notities\b/i, weight: 0.9 },
{ pattern: /^rapportages?\s+(van\s+)?\w+/i, weight: 0.9 },
{ pattern: /^notities\s+(van\s+)?\w+/i, weight: 0.85 },
]
```
**Entities:**
```typescript
{
patientName?: string;
dateRange?: { start: Date; end: Date; label: string };
category?: VerpleegkundigCategory; // optioneel filter
}
```
**Benodigde UI:** `RapportageQueryBlock` (nieuw) - lijst van rapportages met:
- Periode filter (vandaag, week, maand)
- Categorie filter (medicatie, adl, gedrag, etc.)
- Sorteer opties (datum, categorie)
---
#### 🟡 MEDIUM Priority — `behandelplan_query`
**Input voorbeelden:**
- "Wat is het behandelplan van Jan?"
- "Toon doelen van Marie"
- "Behandelplan"
- "Welke interventies heeft Jan?"
**Pattern matching (Reflex):**
```typescript
behandelplan_query: [
{ pattern: /^(toon\s+)?behandelplan\b/i, weight: 1.0 },
{ pattern: /^wat\s+(zijn|is)\s+(het\s+)?behandelplan\b/i, weight: 0.95 },
{ pattern: /^(toon\s+)?doelen\b/i, weight: 0.9 },
{ pattern: /^(toon\s+)?interventies\b/i, weight: 0.9 },
{ pattern: /^behandelplan\s+(van\s+)?\w+/i, weight: 0.9 },
]
```
**Entities:**
```typescript
{
patientName?: string;
queryFocus?: 'doelen' | 'interventies' | 'leefgebieden' | 'alles';
}
```
**Benodigde UI:** `BehandelplanQueryBlock` (nieuw) of hergebruik `BehandelplanView`:
- Hulpvraag weergave
- Leefgebieden scores
- Doelen lijst
- Interventies lijst
---
#### 🟡 MEDIUM Priority — `risico_query`
**Input voorbeelden:**
- "Wat zijn de risico's van Jan?"
- "Toon risicotaxatie"
- "Risico's"
- "Veiligheidsplan"
**Pattern matching (Reflex):**
```typescript
risico_query: [
{ pattern: /^(toon\s+)?risico['']?s?\b/i, weight: 1.0 },
{ pattern: /^wat\s+zijn\s+(de\s+)?risico['']?s?\b/i, weight: 0.95 },
{ pattern: /^risicotaxatie\b/i, weight: 0.95 },
{ pattern: /^veiligheidsplan\b/i, weight: 0.8 },
{ pattern: /^risico['']?s?\s+(van\s+)?\w+/i, weight: 0.9 },
]
```
**Entities:**
```typescript
{
patientName?: string;
riskType?: 'suicidaliteit' | 'agressie' | 'valrisico' | 'all';
}
```
**Benodigde UI:** Hergebruik `RisksBlock` uit verpleegrapportage:
- Risico's gesorteerd op niveau (zeer_hoog → laag)
- Met rationale en maatregelen
- High risk count badge
---
### 8.3 Implementatie Impact Query Intents
| Intent | Types Wijziging | Patterns Wijziging | UI Wijziging | Complexiteit |
|--------|----------------|-------------------|--------------|--------------|
| `patient_overview` | ✅ Toevoegen | ✅ Toevoegen | ❌ Bestaat al | **Laag** |
| `rapportage_query` | ✅ Toevoegen | ✅ Toevoegen | 🆕 Nieuw block | **Middel** |
| `behandelplan_query` | ✅ Toevoegen | ✅ Toevoegen | ⚠️ Wrap bestaand | **Middel** |
| `risico_query` | ✅ Toevoegen | ✅ Toevoegen | ⚠️ Wrap bestaand | **Middel** |
---
## 9. Toekomstige Uitbreidingen (Post-MVP)
### 9.1 Potentiële Nieuwe Actie Intents
| Intent | Use Case | Prioriteit |
|--------|----------|------------|
| `medicatie_toediening` | "Geef Jan zijn medicatie" | Medium |
| `vitals_registratie` | "Bloeddruk Jan 120/80" | Medium |
| `taak_aanmaken` | "Herinnering morgen 10:00" | Low |
| `bericht_sturen` | "Mail naar huisarts" | Low |
### 9.2 Potentiële Nieuwe Nudges
| ID | Trigger | Suggestie |
|----|---------|-----------|
| `vitals-afwijkend` | Vitals met H/L interpretatie | Arts informeren |
| `lithium-lab` | Medicatie notitie + "lithium" | Lab aanvragen |
| `overdracht-einde-dienst` | Einde dienst nadert | Overdracht schrijven |
| `kindcheck-reminder` | Na intake met kinderen | Kindcheck uitvoeren |
---
## 10. Referenties
- Bouwplan: `docs/intent/bouwplan-cortex-v2.md`
- Architecture: `docs/intent/architecture-cortex-v2.md`
- MVP User Stories: `docs/intent/mvp-userstories-intent-system.md`
- Huidige nudge implementatie: `lib/cortex/nudge.ts`
- Intent types: `lib/cortex/types.ts`
- Reflex classifier: `lib/cortex/reflex-classifier.ts`
- Report types: `lib/types/report.ts`
- Risk types: `app/epd/patients/[id]/intakes/[intakeId]/risk/components/risk-manager.tsx`
- PatientDashboardBlock: `components/cortex/blocks/patient-dashboard-block.tsx`
- OverdrachtBlock: `components/cortex/blocks/overdracht-block.tsx`
- BehandelplanView: `components/behandelplan/behandelplan-view.tsx`
- RisksBlock: `app/epd/verpleegrapportage/components/blocks/risks-block.tsx`
---
## Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-01-2026 | Colin Lit | Initiële analyse document |
| v1.1 | 01-01-2026 | Colin Lit | Query intents analyse toegevoegd (rapportage, behandelplan, risico, dashboard) |

View File

@@ -0,0 +1,86 @@
# Intent-Driven Design for Healthcare: A Framework Gap Analysis
The search for an established framework combining language-first interface design, multi-intent recognition, context-aware execution, and proactive suggestions for professional healthcare software reveals a significant gap. **No comprehensive, published methodology exists that matches all specified criteria**, though several emerging frameworks address individual components. This gap represents both a validation of the proposed approach's novelty and an opportunity for framework development.
## The closest existing framework: IBM's Natural Conversation Framework
The **IBM Natural Conversation Framework (NCF)**, published by ACM Books in 2019, represents the most academically rigorous methodology for language-first interface design. Developed by Robert J. Moore and Raphael Arar at IBM Research-Almaden, NCF is grounded in **Conversation Analysis** from social sciences and offers over 100 reusable conversational UX patterns organized into 15 activity modules.
NCF explicitly treats language as the primary interface paradigm rather than an add-on, replacing traditional wireframes with sample dialogs and sequence metrics. Its four core components—an interaction model of "expandable sequences," a six-slot content format, a pattern language, and navigation methods—provide systematic tools for designing conversational experiences. The framework handles context through sequential understanding across turns, distinguishes between local and global intents, and includes sophisticated repair mechanisms for error recovery.
However, NCF was designed for general enterprise applications and lacks healthcare-specific adaptations. It does not address clinical workflows, medical terminology handling, or the proactive suggestion patterns ("nudges") that distinguish modern agentic systems. While healthcare chapters exist in the broader NCF literature, these focus on chatbots rather than primary clinical interfaces.
## Emerging agentic AI design patterns fill part of the gap
Microsoft's **Agent UX Design Principles**, published in April 2025, introduces concepts that closely align with the proposed framework. The principle of "nudging more than notifying" explicitly describes systems that "proactively start chats, create artifacts, and dynamically generate cues" rather than delivering static notifications. Microsoft's temporal framework distinguishes between agents "reflecting on history" (using memory/context), operating in the present through nudges, and "adapting and evolving" for future interactions.
The **Shape of AI** pattern library offers the most comprehensive UX taxonomy for agentic interfaces, organizing patterns into categories including:
- **Wayfinders**: Suggestions, nudges, and follow-up patterns that help users navigate capabilities
- **Prompt Actions**: Transform, expand, summarize, and chained action patterns for multi-step execution
- **Governors**: Action plans, verification, and "stream of thought" patterns for human oversight
- **Trust Builders**: Citations, footprints, and consent patterns for transparency
Google Cloud's **Agentic AI Design Patterns** provides architectural templates including sequential, parallel, coordinator, and hierarchical task decomposition patterns—useful for implementing multi-intent recognition and complex clinical workflows.
## Healthcare NLI remains focused on documentation, not primary interfaces
The dominant paradigm in healthcare natural language interfaces is **ambient clinical intelligence**—systems like Nuance DAX Copilot and AWS HealthScribe that capture clinician-patient conversations and generate documentation. These operate as sophisticated assistants augmenting traditional EHR interfaces rather than replacing them. **70% of clinicians** using ambient AI report reduced burnout, validating the speech-first approach, but these systems address documentation burden rather than reimagining the entire interaction model.
**Oracle Health EHR**, which received regulatory approval in November 2025 for ambulatory use, represents the first major departure from this pattern. Designed explicitly as a "voice-first intelligent solution," Oracle's approach enables clinicians to navigate the entire EHR through spoken commands: "Show me the patient's latest MRI results" rather than multi-menu navigation. The system synthesizes data from multiple sources and delivers contextual insights conversationally. However, as a commercial product launched recently, published design methodology documentation remains limited.
Academic research identified several healthcare-specific design principles including the **TURF framework** for EHR usability and the **Three-Phase User-Centered XAI-CDSS Framework** for clinical decision support. A critical finding from Frontiers in Computer Science's systematic review of 127 papers: clinicians prefer documentation methods aligned with workflows, and many resist AI-CDSS when they perceive no need for data support—emphasizing that successful NLI design must integrate seamlessly with existing clinical mental models.
## The proposed CAPTURE-INTERPRET-GOAL-EXECUTE-NUDGE pattern appears novel
Mapping existing frameworks against the proposed pattern reveals partial coverage but no complete alignment:
| Framework | CAPTURE | INTERPRET | GOAL | EXECUTE | NUDGE |
|-----------|---------|-----------|------|---------|-------|
| IBM NCF | Opening patterns | Intent recognition, repair | Activity modules | Sequence closers | Next-topic patterns (limited) |
| Microsoft Agent UX | Accessible entry | Context awareness | User-defined goals | Multi-modal actions | **Explicit "nudging" principle** |
| Shape of AI | Wayfinders | Tuners, filters | Prompt actions | Chained actions | Suggestions, nudges |
| Ambient AI (DAX) | Speech capture | NLU parsing | Documentation focus | Note generation | None |
The **NUDGE component**—proactive, context-aware suggestions that anticipate clinician needs—represents the least developed aspect in existing frameworks. Microsoft's principle comes closest, describing agents that "dynamically generate cues" based on context, but no healthcare-specific implementation methodology exists. This proactive layer distinguishes intent-driven interfaces from reactive voice command systems.
## Dutch healthcare context reveals significant implementation gaps
The Netherlands has **no published frameworks** for conversational or intent-driven EPD design. Current Dutch healthcare AI focuses primarily on speech-to-text documentation automation through vendors like Wellcom Health and HealthTalk.ai, addressing "registratielast" (registration burden) rather than reimagining interface paradigms.
Key Dutch resources include **MedRoBERTa.nl**, the first domain-specific language model for Dutch Electronic Health Records developed at VU Amsterdam. Pre-trained on 13GB of text from nearly 10 million hospital notes, MedRoBERTa.nl addresses unique Dutch medical language characteristics—shorter sentences, omitted functional words, specialized vocabulary differing from standard Dutch. This provides a foundation for Dutch clinical NLU but lacks accompanying interface design methodology.
**Nictiz**, the national standards organization, maintains **Zorginformatiebouwstenen (ZIBs)**—healthcare information building blocks providing standardized definitions for medical concepts linked to SNOMED CT, LOINC, and ICD. The "Eenheid van Taal" (Unity of Language) principle establishes semantic foundations that could support intent-driven interfaces, but no guidance exists for conversational design patterns.
**Platform AiGGz** provides GGZ-specific AI guidance, and the AI Kompas voor de Geestelijke Gezondheidszorg offers implementation frameworks. However, author Nicky Hekster notes "veel versnippering" (much fragmentation)—applications don't scale and coordination is lacking. Mental healthcare presents additional design challenges: clinicians prefer narrative documentation to capture uncertainty and nuance, workarounds are common when systems don't fit psychiatric workflows, and the therapeutic relationship creates unique considerations for voice interfaces.
## Recommended framework synthesis approach
Given the absence of a single comprehensive methodology, constructing a framework requires synthesizing elements from multiple sources:
**For design methodology (replacing wireframes):** Adapt IBM NCF's sample dialog and sequence metric approaches. NCF's 15 activity modules and 70+ patterns provide systematic tools for mapping user utterances to system behaviors. Design artifacts should include intent-system maps, dialog flow documentation, and conversational activity patterns rather than traditional screen wireframes.
**For multi-intent recognition and context awareness:** Apply Google Cloud's agentic patterns for handling complex, multi-step clinical workflows. The coordinator and hierarchical task decomposition patterns address scenarios where clinicians express compound intents ("Check the medication interactions and schedule a follow-up if the renal function allows it").
**For proactive suggestions (nudges):** Extend Microsoft's "nudging more than notifying" principle with the Shape of AI's wayfinder patterns. Design for context-triggered suggestions that surface relevant clinical information before the clinician explicitly requests it—medication alerts based on documented symptoms, care pathway recommendations based on diagnosis patterns.
**For healthcare-specific adaptation:** Incorporate the user-centered XAI-CDSS framework's emphasis on workflow alignment, the ambient AI literature's integration patterns, and mental health-specific considerations from the GGZ literature regarding narrative flexibility and therapeutic context.
**For Dutch implementation:** Build on MedRoBERTa.nl for language understanding, align with ZIB semantic standards for clinical concept mapping, and address the "registratielast" concern by demonstrating documentation efficiency gains alongside the interface paradigm shift.
## Key terminology note: the field lacks standard vocabulary
The search revealed no established terminology for this design approach. "Intent-driven design," "language-first interface design," "utterance-driven design," and "conversational design for enterprise" return general conversational AI resources rather than specific methodologies. "Agentic interface design" yields results focused on technical architecture rather than UX methodology. The closest established terms are:
- **Conversational UX Design** (IBM NCF's preferred terminology)
- **Voice-First Design** (common but typically consumer-focused)
- **Agent UX** (Microsoft's emerging terminology)
- **Natural Language Interface Design** (academic, broader scope)
The absence of standardized terminology reinforces the novelty of the proposed approach and suggests opportunity for establishing definitional frameworks alongside the design methodology.
## Conclusion
The research confirms that while individual components of intent-driven healthcare interface design exist across multiple frameworks, **no comprehensive methodology combines language-first primary interfaces, multi-intent clinical workflow recognition, context-aware action execution, and proactive clinical suggestions**. IBM's Natural Conversation Framework provides the strongest methodological foundation for conversational design, Microsoft's Agent UX principles introduce the critical "nudging" concept, and Oracle's voice-first EHR demonstrates commercial viability of primary voice interfaces in clinical settings.
For Dutch GGZ EPD development, the opportunity exists to synthesize these elements into a novel framework. The combination of MedRoBERTa.nl for Dutch clinical NLU, ZIB semantic standards for concept mapping, and adapted NCF patterns for conversational structure could form the basis for a CAPTURE→INTERPRET→GOAL→EXECUTE→NUDGE methodology specifically designed for mental healthcare workflows. Given the documented fragmentation in Dutch healthcare AI initiatives and the absence of published frameworks, such a methodology would represent a genuine contribution to the field.

View File

@@ -0,0 +1,523 @@
# 📝 Dataset: GGZ Zinnen voor Cortex V2
**Datum:** 01-01-2026
**Status:** Training & Test Dataset
**Auteur:** Colin Lit (met AI-assistentie)
---
## 1. Inleiding
Dit document bevat een uitgebreide dataset van GGZ-specifieke zinnen die gebruikt kunnen worden voor:
- Training van het intent classificatiesysteem
- Testing van de Reflex Arc (Layer 1)
- Evaluatie van de AI Orchestrator (Layer 2)
- Validatie van Nudge triggers (Layer 3)
### 1.1 Structuur per Zin
```typescript
{
input: string; // De invoerzin
expectedIntent: CortexIntent | CortexIntent[]; // Verwachte intent(s)
expectedEntities?: object; // Verwachte entities
shouldEscalate?: boolean; // Of Layer 2 nodig is
shouldTriggerNudge?: string; // Welke nudge verwacht wordt
priority?: 'high' | 'medium' | 'low'; // GGZ urgentie
context?: string; // GGZ context/scenario
}
```
---
## 2. Single Intent Zinnen
### 2.1 Dagnotitie — Medicatie
| Input | Expected Intent | Entities | Nudge Trigger | Context |
|-------|-----------------|----------|---------------|---------|
| "Notitie Jan medicatie ingenomen" | `dagnotitie` | `{patientName: "Jan", category: "medicatie"}` | — | Routine medicatie registratie |
| "Jan heeft zijn antipsychotica geweigerd" | `dagnotitie` | `{patientName: "Jan", category: "medicatie", content: "antipsychotica geweigerd"}` | — | Medicatie weigering |
| "Medicatie Jan gewijzigd naar Olanzapine 10mg" | `dagnotitie` | `{patientName: "Jan", category: "medicatie"}` | `medicatie-controle` | Medicatie aanpassing |
| "Marie gestart met Lithium vandaag" | `dagnotitie` | `{patientName: "Marie", category: "medicatie"}` | `medicatie-controle` | Nieuwe medicatie start |
| "Piet klaagt over bijwerkingen quetiapine" | `dagnotitie` | `{patientName: "Piet", category: "medicatie"}` | — | Bijwerkingen |
| "Depakine spiegel afgenomen bij Sophie" | `dagnotitie` | `{patientName: "Sophie", category: "medicatie"}` | — | Lab controle |
| "PRN Oxazepam 10mg gegeven aan Kees om 14:00" | `dagnotitie` | `{patientName: "Kees", category: "medicatie"}` | — | PRN medicatie |
| "Medicatie voor Anna klaargelegd voor de nacht" | `dagnotitie` | `{patientName: "Anna", category: "medicatie"}` | — | Medicatie voorbereiding |
| "Jan weigert al 3 dagen zijn medicatie" | `dagnotitie` | `{patientName: "Jan", category: "medicatie", severity: "high"}` | `gedrag-risico-update` | Langdurige weigering |
| "Clozapine spiegel te laag, dosering verhoogd" | `dagnotitie` | `{category: "medicatie"}` | `medicatie-controle` | Dosisaanpassing |
### 2.2 Dagnotitie — Gedrag & Observatie
| Input | Expected Intent | Entities | Nudge Trigger | Context |
|-------|-----------------|----------|---------------|---------|
| "Jan is vandaag rustig en coöperatief" | `dagnotitie` | `{patientName: "Jan", category: "observatie"}` | — | Positieve observatie |
| "Marie vertoont toenemende onrust" | `dagnotitie` | `{patientName: "Marie", category: "gedrag"}` | `gedrag-risico-update` | Gedragsverandering |
| "Piet was agressief naar medepatiënt" | `dagnotitie` | `{patientName: "Piet", category: "gedrag"}` | `gedrag-risico-update` | Agressie incident |
| "Sophie hoort stemmen sinds gisteren" | `dagnotitie` | `{patientName: "Sophie", category: "observatie"}` | — | Psychotische symptomen |
| "Kees is geagiteerd en loopt veel te ijsberen" | `dagnotitie` | `{patientName: "Kees", category: "gedrag"}` | `gedrag-risico-update` | Agitatie |
| "Anna heeft slecht geslapen afgelopen nacht" | `dagnotitie` | `{patientName: "Anna", category: "observatie"}` | — | Slaapprobleem |
| "Jan trekt zich terug en communiceert minimaal" | `dagnotitie` | `{patientName: "Jan", category: "gedrag"}` | — | Sociaal terugtrekken |
| "Marie is verward en gedesoriënteerd" | `dagnotitie` | `{patientName: "Marie", category: "observatie"}` | `gedrag-risico-update` | Verwardheid |
| "Piet heeft goed gegeten vandaag" | `dagnotitie` | `{patientName: "Piet", category: "adl"}` | — | ADL positief |
| "Sophie weigert te eten sinds 2 dagen" | `dagnotitie` | `{patientName: "Sophie", category: "adl"}` | `adl-zorgplan-update` | ADL verslechtering |
### 2.3 Dagnotitie — Crisis & Veiligheid (HIGH PRIORITY)
| Input | Expected Intent | Entities | Nudge Trigger | Priority | Context |
|-------|-----------------|----------|---------------|----------|---------|
| "Jan spreekt over suïcide" | `dagnotitie` | `{patientName: "Jan", category: "observatie", severity: "high"}` | `suicidaliteit-veiligheidsplan` | 🔴 HIGH | Suïcidaliteit signaal |
| "Marie heeft zelfbeschadiging gepleegd" | `dagnotitie` | `{patientName: "Marie", category: "incident", severity: "high"}` | `suicidaliteit-veiligheidsplan` | 🔴 HIGH | Automutilatie |
| "Crisis: Piet dreigt met geweld" | `dagnotitie` | `{patientName: "Piet", category: "incident", severity: "high"}` | `crisis-team-informeren` | 🔴 HIGH | Geweld dreiging |
| "Sophie probeert weg te lopen" | `dagnotitie` | `{patientName: "Sophie", category: "incident"}` | `incident-mic-melding` | 🔴 HIGH | Weglopen |
| "Acuut: Kees is psychotisch en onhandelbaar" | `dagnotitie` | `{patientName: "Kees", category: "incident", severity: "high"}` | `crisis-team-informeren` | 🔴 HIGH | Acute psychose |
| "Jan heeft doodswensen geuit vanavond" | `dagnotitie` | `{patientName: "Jan", severity: "high"}` | `suicidaliteit-veiligheidsplan` | 🔴 HIGH | Suïcidaliteit |
| "Separatie ingezet bij Marie om 15:00" | `dagnotitie` | `{patientName: "Marie", category: "incident"}` | `incident-mic-melding` | 🔴 HIGH | Separatie |
| "Dwangmedicatie toegediend bij Piet" | `dagnotitie` | `{patientName: "Piet", category: "medicatie", severity: "high"}` | `incident-mic-melding` | 🔴 HIGH | Dwangmedicatie |
| "Jan gevallen in gang, geen letsel" | `dagnotitie` | `{patientName: "Jan", category: "incident"}` | `incident-mic-melding` | 🟡 MEDIUM | Valincident |
| "Fixatie toegepast na agressie incident" | `dagnotitie` | `{category: "incident", severity: "high"}` | `incident-mic-melding` | 🔴 HIGH | Fixatie |
### 2.4 Dagnotitie — ADL & Zorg
| Input | Expected Intent | Entities | Nudge Trigger | Context |
|-------|-----------------|----------|---------------|---------|
| "Jan geholpen met douchen" | `dagnotitie` | `{patientName: "Jan", category: "adl"}` | — | ADL ondersteuning |
| "Marie weigert persoonlijke verzorging" | `dagnotitie` | `{patientName: "Marie", category: "adl"}` | `adl-zorgplan-update` | ADL weigering |
| "Wond verzorgd bij Piet, ziet er goed uit" | `dagnotitie` | `{patientName: "Piet", category: "adl"}` | `wondzorg-controle` | Wondzorg |
| "Sophie heeft hulp nodig bij aankleden" | `dagnotitie` | `{patientName: "Sophie", category: "adl"}` | `adl-zorgplan-update` | Toenemende hulpbehoefte |
| "Decubitus plek gecontroleerd, graad 2" | `dagnotitie` | `{category: "adl"}` | `wondzorg-controle` | Decubitus |
| "Kees kan niet meer zelfstandig naar toilet" | `dagnotitie` | `{patientName: "Kees", category: "adl"}` | `adl-zorgplan-update` | ADL achteruitgang |
| "Anna heeft goed ontbeten vandaag" | `dagnotitie` | `{patientName: "Anna", category: "adl"}` | — | Voeding positief |
| "Jan is 3 kilo afgevallen deze week" | `dagnotitie` | `{patientName: "Jan", category: "observatie"}` | — | Gewichtsverlies |
### 2.5 Zoeken
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Zoek Jan" | `zoeken` | `{query: "Jan"}` | Simpel zoeken |
| "Wie is mevrouw De Vries?" | `zoeken` | `{query: "De Vries"}` | Formeel zoeken |
| "Dossier Marie" | `zoeken` | `{query: "Marie"}` | Dossier opvragen |
| "Vind patiënt Pietersen" | `zoeken` | `{query: "Pietersen"}` | Achternaam zoeken |
| "Zoek BSN 123456789" | `zoeken` | `{query: "123456789"}` | BSN zoeken |
| "Patiënt met kamer 12" | `zoeken` | `{query: "kamer 12"}` | Locatie zoeken |
| "Wie ligt er op afdeling 3?" | `zoeken` | `{query: "afdeling 3"}` | Afdeling zoeken |
### 2.6 Agenda Query
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Agenda vandaag" | `agenda_query` | `{dateRange: {label: "vandaag"}}` | Dag overzicht |
| "Wat heb ik morgen?" | `agenda_query` | `{dateRange: {label: "morgen"}}` | Morgen |
| "Afspraken deze week" | `agenda_query` | `{dateRange: {label: "deze week"}}` | Week overzicht |
| "Hoeveel intakes heb ik nog?" | `agenda_query` | `{appointmentType: "intake"}` | Type filter |
| "Wanneer is mijn volgende afspraak met Jan?" | `agenda_query` | `{patientName: "Jan"}` | Patiënt filter |
| "Alle huisbezoeken komende week" | `agenda_query` | `{appointmentType: "huisbezoek", dateRange: {label: "volgende week"}}` | Type + periode |
| "Crisisdienst schema" | `agenda_query` | `{appointmentType: "crisis"}` | Crisis planning |
### 2.7 Overdracht
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Overdracht" | `overdracht` | `{}` | Standaard overdracht |
| "Dienst afronden" | `overdracht` | `{}` | Einde dienst |
| "Samenvatting voor collega" | `overdracht` | `{}` | Collega overdracht |
| "Wat is er gebeurd vandaag?" | `overdracht` | `{dateRange: {label: "vandaag"}}` | Dag samenvatting |
| "Overdracht avonddienst" | `overdracht` | `{shift: "avond"}` | Avond overdracht |
| "Aandachtspunten voor de nacht" | `overdracht` | `{shift: "nacht"}` | Nacht overdracht |
### 2.8 Afspraak Maken
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Plan intake Jan morgen 14:00" | `create_appointment` | `{patientName: "Jan", appointmentType: "intake", datetime: {...}}` | Intake plannen |
| "Maak behandelafspraak Marie vrijdag" | `create_appointment` | `{patientName: "Marie", appointmentType: "behandeling"}` | Behandeling |
| "Huisbezoek bij Piet volgende week" | `create_appointment` | `{patientName: "Piet", appointmentType: "huisbezoek"}` | Huisbezoek |
| "Online consult Sophie woensdag 10:00" | `create_appointment` | `{patientName: "Sophie", appointmentType: "online", datetime: {...}}` | Online |
| "Plan evaluatie medicatie voor Kees" | `create_appointment` | `{patientName: "Kees", appointmentType: "follow-up"}` | Follow-up |
| "Crisisafspraak vandaag voor Anna" | `create_appointment` | `{patientName: "Anna", appointmentType: "crisis", dateRange: {label: "vandaag"}}` | Crisis |
| "Bel-afspraak Jan overmorgen" | `create_appointment` | `{patientName: "Jan", appointmentType: "telefonisch"}` | Telefonisch |
### 2.9 Afspraak Annuleren
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Annuleer afspraak Jan" | `cancel_appointment` | `{patientName: "Jan"}` | Simpel annuleren |
| "Zeg Marie van morgen af" | `cancel_appointment` | `{patientName: "Marie", datetime: {...}}` | Specifieke dag |
| "Cancel intake Piet" | `cancel_appointment` | `{patientName: "Piet", appointmentType: "intake"}` | Type specifiek |
| "Afspraak van 14:00 afzeggen" | `cancel_appointment` | `{datetime: {time: "14:00"}}` | Tijd specifiek |
### 2.10 Afspraak Verzetten
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Verzet Jan naar volgende week" | `reschedule_appointment` | `{patientName: "Jan", newDatetime: {...}}` | Verzetten |
| "Verplaats de intake naar vrijdag" | `reschedule_appointment` | `{appointmentType: "intake", newDatetime: {...}}` | Type specifiek |
| "Marie's afspraak 1 uur later" | `reschedule_appointment` | `{patientName: "Marie", newDatetime: {...}}` | Relatief |
---
## 3. Query Intents (Nieuw in V2)
### 3.1 Patient Overview
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Overzicht Jan" | `patient_overview` | `{patientName: "Jan"}` | Basis overzicht |
| "Dashboard Marie" | `patient_overview` | `{patientName: "Marie"}` | Dashboard |
| "Toon alles van Piet" | `patient_overview` | `{patientName: "Piet"}` | Volledig overzicht |
| "Patiëntoverzicht Sophie" | `patient_overview` | `{patientName: "Sophie"}` | Formeel |
| "Wie is Jan en wat speelt er?" | `patient_overview` | `{patientName: "Jan"}` | Context vraag |
| "Geef me info over Kees" | `patient_overview` | `{patientName: "Kees"}` | Informeel |
### 3.2 Rapportage Query
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Toon rapportages Jan" | `rapportage_query` | `{patientName: "Jan"}` | Alle rapportages |
| "Wat is er gerapporteerd deze week?" | `rapportage_query` | `{dateRange: {label: "deze week"}}` | Periode filter |
| "Medicatie notities Marie afgelopen maand" | `rapportage_query` | `{patientName: "Marie", category: "medicatie", dateRange: {...}}` | Categorie filter |
| "Incidenten van Piet" | `rapportage_query` | `{patientName: "Piet", category: "incident"}` | Incident filter |
| "Alle gedragsobservaties vandaag" | `rapportage_query` | `{category: "gedrag", dateRange: {label: "vandaag"}}` | Type + periode |
| "Voortgangsrapportages Sophie" | `rapportage_query` | `{patientName: "Sophie", reportType: "voortgang"}` | Report type |
| "Laatste 5 notities van Jan" | `rapportage_query` | `{patientName: "Jan", limit: 5}` | Limiet |
### 3.3 Behandelplan Query
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Wat is het behandelplan van Jan?" | `behandelplan_query` | `{patientName: "Jan"}` | Volledig plan |
| "Toon doelen van Marie" | `behandelplan_query` | `{patientName: "Marie", queryFocus: "doelen"}` | Doelen focus |
| "Behandelplan" | `behandelplan_query` | `{}` | Context-afhankelijk |
| "Welke interventies heeft Piet?" | `behandelplan_query` | `{patientName: "Piet", queryFocus: "interventies"}` | Interventies |
| "Leefgebieden scores Sophie" | `behandelplan_query` | `{patientName: "Sophie", queryFocus: "leefgebieden"}` | Leefgebieden |
| "Hulpvraag van Kees" | `behandelplan_query` | `{patientName: "Kees", queryFocus: "hulpvraag"}` | Hulpvraag |
### 3.4 Risico Query
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Wat zijn de risico's van Jan?" | `risico_query` | `{patientName: "Jan"}` | Alle risico's |
| "Toon risicotaxatie Marie" | `risico_query` | `{patientName: "Marie"}` | Risicotaxatie |
| "Suïciderisico van Piet" | `risico_query` | `{patientName: "Piet", riskType: "suicidaliteit"}` | Specifiek risico |
| "Risico's" | `risico_query` | `{}` | Context-afhankelijk |
| "Veiligheidsplan Sophie" | `risico_query` | `{patientName: "Sophie", riskType: "suicidaliteit"}` | Veiligheidsplan |
| "Agressierisico van Kees" | `risico_query` | `{patientName: "Kees", riskType: "agressie"}` | Specifiek |
| "Wie heeft hoog risico?" | `risico_query` | `{riskLevel: "hoog"}` | Niveau filter |
---
## 4. Multi-Intent Zinnen (Layer 2 Required)
### 4.1 Notitie + Afspraak
| Input | Expected Intents | Entities | Context |
|-------|------------------|----------|---------|
| "Zeg Jan af en maak notitie dat hij ziek is" | `[cancel_appointment, dagnotitie]` | `[{patientName: "Jan"}, {patientName: "Jan", content: "ziek"}]` | Annulering + reden |
| "Plan follow-up Marie en noteer dat medicatie werkt" | `[create_appointment, dagnotitie]` | `[{patientName: "Marie", appointmentType: "follow-up"}, {patientName: "Marie", category: "medicatie"}]` | Afspraak + notitie |
| "Intake gedaan bij Piet, plan evaluatie over 2 weken" | `[dagnotitie, create_appointment]` | `[{patientName: "Piet", category: "observatie"}, {patientName: "Piet", appointmentType: "follow-up"}]` | Notitie + follow-up |
### 4.2 Notitie + Notitie
| Input | Expected Intents | Entities | Context |
|-------|------------------|----------|---------|
| "Jan medicatie gegeven en hij is rustig vandaag" | `[dagnotitie, dagnotitie]` | `[{patientName: "Jan", category: "medicatie"}, {patientName: "Jan", category: "observatie"}]` | Meerdere categorieën |
| "Marie heeft gegeten en is gedoucht met hulp" | `[dagnotitie, dagnotitie]` | `[{patientName: "Marie", category: "adl"}, {patientName: "Marie", category: "adl"}]` | Meerdere ADL |
### 4.3 Query + Actie
| Input | Expected Intents | Entities | Context |
|-------|------------------|----------|---------|
| "Toon agenda en plan intake Jan morgen" | `[agenda_query, create_appointment]` | `[{dateRange: {...}}, {patientName: "Jan", appointmentType: "intake"}]` | Overzicht + actie |
| "Check risico's van Marie en maak notitie" | `[risico_query, dagnotitie]` | `[{patientName: "Marie"}, {patientName: "Marie"}]` | Query + notitie |
### 4.4 GGZ-Specifieke Combinaties
| Input | Expected Intents | Entities | Priority | Context |
|-------|------------------|----------|----------|---------|
| "Crisis bij Jan, separeer en informeer psychiater" | `[dagnotitie, dagnotitie]` | `[{patientName: "Jan", category: "incident"}, {patientName: "Jan", category: "incident"}]` | 🔴 HIGH | Crisis protocol |
| "Dwangmedicatie bij Marie, MIC invullen" | `[dagnotitie, dagnotitie]` | `[{patientName: "Marie", category: "medicatie"}, {patientName: "Marie", category: "incident"}]` | 🔴 HIGH | Dwang + melding |
| "Plan ontslaggesprek Piet en maak overdracht" | `[create_appointment, overdracht]` | `[{patientName: "Piet"}, {}]` | 🟡 MEDIUM | Ontslag planning |
---
## 5. Context-Afhankelijke Zinnen (Pronoun Resolution)
### 5.1 Pronoun "Hij/Zij/Hem/Haar"
| Input | Context | Expected Resolution | Expected Intent |
|-------|---------|---------------------|-----------------|
| "Maak notitie voor hem" | activePatient: Jan | patientName: "Jan", patientResolution: "pronoun" | `dagnotitie` |
| "Zij is vandaag rustig" | activePatient: Marie | patientName: "Marie", patientResolution: "pronoun" | `dagnotitie` |
| "Geef hem zijn medicatie" | activePatient: Piet | patientName: "Piet", patientResolution: "pronoun" | `dagnotitie` |
| "Plan afspraak voor haar" | activePatient: Sophie | patientName: "Sophie", patientResolution: "pronoun" | `create_appointment` |
| "Wat zijn zijn risico's?" | activePatient: Kees | patientName: "Kees", patientResolution: "pronoun" | `risico_query` |
### 5.2 Demonstratief "Die/Deze/Dezelfde"
| Input | Context | Expected Resolution | Expected Intent |
|-------|---------|---------------------|-----------------|
| "Verzet die afspraak naar morgen" | recentIntent: cancel_appointment voor Jan | patientName: "Jan" | `reschedule_appointment` |
| "Annuleer deze intake" | currentView: appointment detail Jan | patientName: "Jan", appointmentType: "intake" | `cancel_appointment` |
| "Notitie voor dezelfde patiënt" | lastPatient: Marie | patientName: "Marie" | `dagnotitie` |
### 5.3 Impliciete Context
| Input | Context | Expected Resolution | Expected Intent |
|-------|---------|---------------------|-----------------|
| "Medicatie gegeven" | activePatient: Jan | patientName: "Jan", category: "medicatie" | `dagnotitie` |
| "Is rustig vandaag" | activePatient: Marie | patientName: "Marie", category: "observatie" | `dagnotitie` |
| "Behandelplan updaten" | activePatient: Piet | patientName: "Piet" | `behandelplan_query` |
---
## 6. Ambigue Zinnen (Clarification Required)
### 6.1 Intent Ambigu
| Input | Clarification Question | Options | Context |
|-------|------------------------|---------|---------|
| "Jan wondzorg" | "Wil je een notitie maken over wondzorg of een afspraak plannen?" | ["Notitie maken", "Afspraak plannen"] | Onduidelijk actie type |
| "Medicatie Marie" | "Wil je medicatie registreren of de medicatielijst bekijken?" | ["Medicatie notitie", "Medicatielijst"] | Notitie vs query |
| "Plan Jan" | "Wat wil je plannen voor Jan?" | ["Intake", "Behandelafspraak", "Follow-up", "Huisbezoek"] | Type ontbreekt |
| "Overdracht Sophie" | "Wil je de overdracht bekijken of een notitie maken voor overdracht?" | ["Overdracht bekijken", "Notitie voor overdracht"] | Bekijken vs maken |
### 6.2 Patiënt Ambigu
| Input | Clarification Question | Options | Context |
|-------|------------------------|---------|---------|
| "Notitie voor Jan" | "Welke Jan bedoel je?" | ["Jan de Vries (kamer 12)", "Jan Pietersen (kamer 8)"] | Meerdere Jans |
| "Afspraak Marie" | "Bedoel je Marie Bakker of Marie Smit?" | ["Marie Bakker", "Marie Smit"] | Meerdere Maries |
### 6.3 Tijd Ambigu
| Input | Clarification Question | Options | Context |
|-------|------------------------|---------|---------|
| "Plan intake snel" | "Wanneer wil je de intake plannen?" | ["Vandaag", "Morgen", "Deze week"] | Vage tijd |
| "Afspraak binnenkort" | "Welke dag heeft je voorkeur?" | ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag"] | Vage tijd |
---
## 7. Nudge-Triggerende Zinnen
### 7.1 HIGH Priority Nudges
| Input | Expected Nudge | Nudge Message | Context |
|-------|----------------|---------------|---------|
| "Jan spreekt over suïcide" | `suicidaliteit-veiligheidsplan` | "⚠️ Veiligheidsplan actualiseren en risicotaxatie bijwerken?" | Suïcidaliteit signaal |
| "Marie heeft zichzelf gesneden" | `suicidaliteit-veiligheidsplan` | "⚠️ Veiligheidsplan actualiseren en risicotaxatie bijwerken?" | Automutilatie |
| "Crisis bij Piet, acuut suïcidaal" | `crisis-team-informeren` | "🚨 Crisisteam en dienstdoende psychiater informeren?" | Acute crisis |
| "Incident: Sophie gevallen met letsel" | `incident-mic-melding` | "⚠️ MIC-melding invullen voor dit incident?" | Val met letsel |
| "Noodgeval: Kees psychotisch en agressief" | `crisis-team-informeren` | "🚨 Crisisteam en dienstdoende psychiater informeren?" | Acute psychose |
| "Fixatie toegepast bij Anna" | `incident-mic-melding` | "⚠️ MIC-melding invullen voor dit incident?" | Vrijheidsbeperkende maatregel |
### 7.2 MEDIUM Priority Nudges
| Input | Expected Nudge | Nudge Message | Context |
|-------|----------------|---------------|---------|
| "Jan was agressief naar verpleegkundige" | `gedrag-risico-update` | "📋 Risicotaxatie bijwerken voor dit gedrag?" | Agressie |
| "Marie vertoont verward gedrag" | `gedrag-risico-update` | "📋 Risicotaxatie bijwerken voor dit gedrag?" | Verwardheid |
| "Piet kan niet meer zelfstandig lopen" | `adl-zorgplan-update` | "🏠 Zorgplan/behandelplan bijwerken voor gewijzigde ADL?" | ADL verslechtering |
| "Sophie weigert persoonlijke verzorging" | `adl-zorgplan-update` | "🏠 Zorgplan/behandelplan bijwerken voor gewijzigde ADL?" | ADL weigering |
| "Wond verzorgd, ziet er goed uit" | `wondzorg-controle` | "Wondcontrole inplannen over 3 dagen?" | Wondzorg |
| "Medicatie gewijzigd naar hogere dosis" | `medicatie-controle` | "Medicatie evaluatie inplannen over 1 week?" | Medicatie wijziging |
### 7.3 LOW Priority Nudges
| Input | Expected Nudge | Nudge Message | Context |
|-------|----------------|---------------|---------|
| "Intake afspraak gepland" | `intake-behandelplan` | "📝 Behandelplan opstellen na de intake?" | Post-intake |
| "Let op: Jan moet extra gecontroleerd worden" | `handover-mark` | "📋 Deze notitie opnemen in de dienst-overdracht?" | Overdracht markering |
| "Belangrijk voor collega: Sophie heeft onrust" | `handover-mark` | "📋 Deze notitie opnemen in de dienst-overdracht?" | Overdracht markering |
---
## 8. Domein-Specifieke Terminologie
### 8.1 GGZ Diagnosen & Symptomen
| Term | Variaties | Context |
|------|-----------|---------|
| Psychose | psychotisch, psychotische symptomen, hallucinaties | Psychiatrisch |
| Depressie | depressief, depressieve, somber, neerslachtig | Stemmingsstoornis |
| Manie | manisch, manische, ontremd | Bipolair |
| Angst | angstig, angststoornis, paniekerig | Angststoornis |
| Persoonlijkheidsstoornis | borderline, antisociaal, vermijdend | Persoonlijkheid |
| Schizofrenie | schizofreen, wanen, stemmen horen | Psychotisch |
| ADHD | hyperactief, ongeconcentreerd, impulsief | Ontwikkeling |
| Autisme | autistisch, prikkelgevoelig, sociale interactie | Ontwikkeling |
| Verslaving | middelengebruik, alcohol, drugs, afhankelijk | Verslaving |
| Dementie | dement, geheugenproblemen, vergeetachtig | Cognitief |
### 8.2 GGZ Interventies
| Term | Variaties | Context |
|------|-----------|---------|
| Separatie | separeren, afgezonderd, isolatie | Dwangmaatregel |
| Fixatie | gefixeerd, bed vastgelegd | Dwangmaatregel |
| Dwangmedicatie | noodmedicatie, gedwongen medicatie | Dwangmaatregel |
| ECT | electroshock, elektroconvulsietherapie | Behandeling |
| CGT | cognitieve gedragstherapie | Therapie |
| EMDR | traumaverwerking | Therapie |
| Groepstherapie | groepsgesprek, groepsbehandeling | Therapie |
### 8.3 GGZ Rollen
| Term | Variaties | Context |
|------|-----------|---------|
| Psychiater | arts, dienstdoende arts | Medisch |
| Verpleegkundige | verpleging, vpk | Zorg |
| Psycholoog | GZ-psycholoog, klinisch psycholoog | Behandeling |
| Sociaal werker | maatschappelijk werker, SW | Begeleiding |
| Ervaringsdeskundige | ED, peer support | Ondersteuning |
| Diëtist | voedingsdeskundige | Zorg |
| Vaktherapeut | beeldend therapeut, PMT | Therapie |
---
## 9. Edge Cases & Speciale Scenarios
### 9.1 Lege of Minimale Input
| Input | Expected Behavior | Handling |
|-------|-------------------|----------|
| "" | Error | "Geen invoer ontvangen" |
| " " | Error | "Geen invoer ontvangen" |
| "hallo" | `unknown` + clarification | "Waarmee kan ik je helpen?" |
| "ja" | Context-afhankelijk | Check pending confirmation |
| "nee" | Context-afhankelijk | Check pending dismissal |
### 9.2 Spelfouten & Variaties
| Input (met fout) | Correcte Interpretatie | Expected Intent |
|------------------|------------------------|-----------------|
| "Medicatei gegeven" | "Medicatie gegeven" | `dagnotitie` |
| "Aganda vandaag" | "Agenda vandaag" | `agenda_query` |
| "Suïcide gedachten" | "Suïcidale gedachten" | `dagnotitie` + nudge |
| "Notiteie Jan" | "Notitie Jan" | `dagnotitie` |
| "Psyhcose" | "Psychose" | `dagnotitie` |
### 9.3 Informele Taal
| Input | Expected Intent | Entities | Context |
|-------|-----------------|----------|---------|
| "Jansen is weer aan het flippen" | `dagnotitie` | `{patientName: "Jansen", category: "gedrag"}` | Informeel gedrag |
| "Die vent wil niet eten" | `dagnotitie` | `{category: "adl"}` | Context-afhankelijk |
| "Pil gegeven om 8 uur" | `dagnotitie` | `{category: "medicatie"}` | Informele medicatie |
| "Mevrouw is weer helemaal de weg kwijt" | `dagnotitie` | `{category: "observatie"}` | Verwardheid |
### 9.4 Gecombineerde Urgentie
| Input | Priority | Expected Behavior |
|-------|----------|-------------------|
| "Crisis bij Jan, suïcidaal en agressief" | 🔴 HIGHEST | Dubbele nudge: `suicidaliteit-veiligheidsplan` + `crisis-team-informeren` |
| "Incident: fixatie na suïcidepoging" | 🔴 HIGHEST | Dubbele nudge: `incident-mic-melding` + `suicidaliteit-veiligheidsplan` |
---
## 10. Testset Statistieken
### 10.1 Verdeling per Intent
| Intent | Aantal Zinnen | Percentage |
|--------|---------------|------------|
| `dagnotitie` | ~85 | 45% |
| `zoeken` | ~10 | 5% |
| `agenda_query` | ~10 | 5% |
| `overdracht` | ~10 | 5% |
| `create_appointment` | ~15 | 8% |
| `cancel_appointment` | ~8 | 4% |
| `reschedule_appointment` | ~6 | 3% |
| `patient_overview` | ~10 | 5% |
| `rapportage_query` | ~12 | 6% |
| `behandelplan_query` | ~10 | 5% |
| `risico_query` | ~10 | 5% |
| Multi-intent | ~15 | 8% |
| **Totaal** | **~190** | **100%** |
### 10.2 Verdeling per Complexity
| Complexity | Aantal | Layer | Beschrijving |
|------------|--------|-------|--------------|
| Simple | ~120 | Layer 1 (Reflex) | Hoge confidence, single intent |
| Context-dependent | ~30 | Layer 2 (AI) | Pronoun resolution nodig |
| Multi-intent | ~20 | Layer 2 (AI) | Meerdere acties |
| Ambiguous | ~20 | Layer 2 (AI) | Clarification nodig |
### 10.3 Verdeling per Priority
| Priority | Aantal | Nudge Required |
|----------|--------|----------------|
| 🔴 HIGH | ~25 | Ja (safety-related) |
| 🟡 MEDIUM | ~50 | Mogelijk |
| 🔵 LOW | ~115 | Nee |
---
## 11. Implementatie Aanbevelingen
### 11.1 Training Data Format
```json
{
"version": "1.0",
"language": "nl",
"domain": "ggz",
"sentences": [
{
"id": "ggz-001",
"input": "Jan spreekt over suïcide",
"expected": {
"intent": "dagnotitie",
"entities": {
"patientName": "Jan",
"category": "observatie",
"severity": "high"
},
"nudge": "suicidaliteit-veiligheidsplan"
},
"complexity": "simple",
"priority": "high",
"tags": ["crisis", "safety", "suicidality"]
}
]
}
```
### 11.2 Evaluatie Metrics
| Metric | Beschrijving | Target |
|--------|--------------|--------|
| Intent Accuracy | Correct intent geclassificeerd | ≥95% |
| Entity Extraction | Correcte entities geëxtraheerd | ≥90% |
| Nudge Precision | Correcte nudge getriggerd | ≥90% |
| Escalation Rate | % dat naar Layer 2 gaat | 20-30% |
| Clarification Rate | % dat verduidelijking vraagt | <10% |
| Response Time L1 | Reflex Arc latency | <20ms |
| Response Time L2 | AI Orchestrator latency | <500ms |
### 11.3 Continuous Improvement
1. **Log alle classificaties** met intent, confidence, entities
2. **Track nudge acceptance rate** per rule
3. **Monitor escalation reasons** voor pattern verbeteringen
4. **Verzamel user corrections** voor fine-tuning
5. **A/B test** nieuwe patterns voordat ze live gaan
---
## Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 01-01-2026 | Colin Lit | Initieel dataset document |
---
## Referenties
- [Analyse Intents & Nudges](./analyse-intents-nudges.md)
- [Architecture Cortex V2](./architecture-cortex-v2.md)
- [Bouwplan Cortex V2](../bouwplan-cortex-v2.md)
- Intent types: `lib/cortex/types.ts`
- Report types: `lib/types/report.ts`

View File

@@ -31,15 +31,16 @@ const ActionSchema = z.object({
query: z.string().optional(), // For zoeken intent query: z.string().optional(), // For zoeken intent
dateRange: z dateRange: z
.object({ .object({
start: z.string(), start: z.string().optional(),
end: z.string(), end: z.string().optional(),
label: z.string(), label: z.string(),
}) })
.optional(), .optional(),
datetime: z datetime: z
.object({ .object({
date: z.string(), date: z.string().optional(),
time: z.string(), time: z.string().optional(),
label: z.string().optional(), // Voor relatieve datums: vandaag, morgen, etc.
}) })
.optional(), .optional(),
appointmentType: z.string().optional(), appointmentType: z.string().optional(),
@@ -61,6 +62,7 @@ const ActionSchema = z.object({
.object({ .object({
date: z.string().optional(), date: z.string().optional(),
time: z.string().optional(), time: z.string().optional(),
label: z.string().optional(), // Voor relatieve datums
}) })
.optional(), .optional(),
}), }),
@@ -197,6 +199,56 @@ export function getConfidenceLabel(confidence: number): string {
return 'Zeer onzeker'; return 'Zeer onzeker';
} }
/**
* Generate a default confirmation message for an intent when AI response has no text
*/
export function getDefaultConfirmationMessage(intent: CortexIntent, entities: Record<string, any>): string {
const dateLabel = entities?.dateRange?.label || entities?.datetime?.label;
const patientName = entities?.patientName;
switch (intent) {
case 'agenda_query':
if (dateLabel) {
return `Ik toon je de afspraken voor ${dateLabel}.`;
}
return 'Ik toon je de agenda.';
case 'create_appointment':
if (patientName && dateLabel) {
return `Ik open het afspraakformulier voor ${patientName} op ${dateLabel}.`;
}
if (patientName) {
return `Ik open het afspraakformulier voor ${patientName}.`;
}
return 'Ik open het afspraakformulier.';
case 'cancel_appointment':
return 'Ik help je met het annuleren van een afspraak.';
case 'reschedule_appointment':
return 'Ik help je met het verzetten van een afspraak.';
case 'dagnotitie':
if (patientName) {
return `Ik open de dagnotitie voor ${patientName}.`;
}
return 'Ik open het dagnotitie formulier.';
case 'zoeken':
const query = entities?.query || entities?.patientName;
if (query) {
return `Ik zoek naar "${query}".`;
}
return 'Ik open de zoekfunctie.';
case 'overdracht':
return 'Ik bereid de overdracht voor.';
default:
return 'Ik help je verder.';
}
}
/** /**
* Validate that artifact type matches intent * Validate that artifact type matches intent
*/ */

109
lib/cortex/suggestions.ts Normal file
View File

@@ -0,0 +1,109 @@
/**
* Cortex Suggestion Data
*
* Defines categories and example sentences for the Intent Helper UI.
* Based on implemented intents in lib/cortex/types.ts
*/
import type { CortexIntent } from './types';
export interface SuggestionCategory {
id: string;
label: string;
icon: string;
description: string;
examples: SuggestionExample[];
}
export interface SuggestionExample {
text: string;
intent: CortexIntent;
/** Placeholder marker for patient name */
hasPatientPlaceholder?: boolean;
}
/**
* Suggestion categories with examples
* Based on the 7 implemented intents:
* - dagnotitie
* - zoeken
* - overdracht
* - agenda_query
* - create_appointment
* - cancel_appointment
* - reschedule_appointment
*/
export const SUGGESTION_CATEGORIES: SuggestionCategory[] = [
{
id: 'notities',
label: 'Notities',
icon: '📝',
description: 'Dagnotities en rapportages maken',
examples: [
{ text: 'Notitie [naam] medicatie gegeven', intent: 'dagnotitie', hasPatientPlaceholder: true },
{ text: '[naam] is rustig vandaag', intent: 'dagnotitie', hasPatientPlaceholder: true },
{ text: 'Incident: patient gevallen', intent: 'dagnotitie' },
{ text: 'ADL: hulp bij douchen gegeven', intent: 'dagnotitie' },
],
},
{
id: 'agenda',
label: 'Agenda',
icon: '📅',
description: 'Afspraken bekijken en beheren',
examples: [
{ text: 'Agenda vandaag', intent: 'agenda_query' },
{ text: 'Plan intake [naam] morgen', intent: 'create_appointment', hasPatientPlaceholder: true },
{ text: 'Annuleer afspraak [naam]', intent: 'cancel_appointment', hasPatientPlaceholder: true },
{ text: 'Verzet afspraak naar volgende week', intent: 'reschedule_appointment' },
],
},
{
id: 'zoeken',
label: 'Zoeken',
icon: '🔍',
description: 'Patiënten en dossiers zoeken',
examples: [
{ text: 'Zoek [naam]', intent: 'zoeken', hasPatientPlaceholder: true },
{ text: 'Wie is mevrouw de Vries?', intent: 'zoeken' },
{ text: 'Dossier Jan Pietersen', intent: 'zoeken' },
{ text: 'Patiënt kamer 12', intent: 'zoeken' },
],
},
{
id: 'overdracht',
label: 'Overdracht',
icon: '📋',
description: 'Dienstoverdrachten en samenvattingen',
examples: [
{ text: 'Overdracht', intent: 'overdracht' },
{ text: 'Wat is er gebeurd vandaag?', intent: 'overdracht' },
{ text: 'Samenvatting voor collega', intent: 'overdracht' },
{ text: 'Aandachtspunten voor de nacht', intent: 'overdracht' },
],
},
];
/**
* Get a flat list of all examples for quick access
*/
export function getAllExamples(): SuggestionExample[] {
return SUGGESTION_CATEGORIES.flatMap((category) => category.examples);
}
/**
* Get examples for a specific category
*/
export function getExamplesByCategory(categoryId: string): SuggestionExample[] {
const category = SUGGESTION_CATEGORIES.find((c) => c.id === categoryId);
return category?.examples ?? [];
}
/**
* Replace placeholder [naam] with actual patient name
*/
export function replacePlaceholder(text: string, patientName?: string): string {
if (!patientName) return text;
return text.replace(/\[naam\]/g, patientName);
}

View File

@@ -43,6 +43,7 @@ export interface ChatEntities {
datetime?: { datetime?: {
date?: string; date?: string;
time?: string; time?: string;
label?: string; // Voor relatieve datums: vandaag, morgen, etc.
}; };
appointmentType?: string; appointmentType?: string;
location?: string; location?: string;
@@ -50,6 +51,7 @@ export interface ChatEntities {
newDatetime?: { newDatetime?: {
date?: string; date?: string;
time?: string; time?: string;
label?: string; // Voor relatieve datums
}; };
} }