feat: add agenda intent scaffolding
This commit is contained in:
@@ -8,12 +8,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useSwiftStore, type SwiftIntent } from '@/stores/swift-store';
|
import { useSwiftStore, type SwiftIntent } from '@/stores/swift-store';
|
||||||
import { FileText, Search, ArrowRightLeft, HelpCircle, Clock } from 'lucide-react';
|
import { FileText, Search, ArrowRightLeft, HelpCircle, Clock, Calendar, Plus, X } from 'lucide-react';
|
||||||
|
|
||||||
const INTENT_CONFIG: Record<SwiftIntent, { icon: typeof FileText; color: string; label: string }> = {
|
const INTENT_CONFIG: Record<SwiftIntent, { icon: typeof FileText; color: string; label: string }> = {
|
||||||
dagnotitie: { icon: FileText, color: 'text-blue-600 bg-blue-50 border border-blue-200', label: 'Notitie' },
|
dagnotitie: { icon: FileText, color: 'text-blue-600 bg-blue-50 border border-blue-200', label: 'Notitie' },
|
||||||
zoeken: { icon: Search, color: 'text-emerald-600 bg-emerald-50 border border-emerald-200', label: 'Zoeken' },
|
zoeken: { icon: Search, color: 'text-emerald-600 bg-emerald-50 border border-emerald-200', label: 'Zoeken' },
|
||||||
overdracht: { icon: ArrowRightLeft, color: 'text-purple-600 bg-purple-50 border border-purple-200', label: 'Overdracht' },
|
overdracht: { icon: ArrowRightLeft, color: 'text-purple-600 bg-purple-50 border border-purple-200', label: 'Overdracht' },
|
||||||
|
agenda_query: { icon: Calendar, color: 'text-teal-600 bg-teal-50 border border-teal-200', label: 'Agenda' },
|
||||||
|
create_appointment: { icon: Plus, color: 'text-green-600 bg-green-50 border border-green-200', label: 'Afspraak' },
|
||||||
|
cancel_appointment: { icon: X, color: 'text-red-600 bg-red-50 border border-red-200', label: 'Annuleren' },
|
||||||
|
reschedule_appointment: { icon: Clock, color: 'text-amber-600 bg-amber-50 border border-amber-200', label: 'Verzetten' },
|
||||||
unknown: { icon: HelpCircle, color: 'text-slate-600 bg-slate-50 border border-slate-200', label: 'Actie' },
|
unknown: { icon: HelpCircle, color: 'text-slate-600 bg-slate-50 border border-slate-200', label: 'Actie' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ Toelichting: dit bouwt voort op het Swift conversatie‑model en hergebruikt de
|
|||||||
|
|
||||||
### 2.1 Technische Stack
|
### 2.1 Technische Stack
|
||||||
- Frontend: Next.js App Router + React + TypeScript
|
- Frontend: Next.js App Router + React + TypeScript
|
||||||
- Styling: Tailwind CSS
|
- Styling: Tailwind CSS + shadcn/ui components
|
||||||
- State: Zustand (Swift store)
|
- State: Zustand (Swift store) + React Query for data caching
|
||||||
- Backend: Next.js route handlers + Supabase (encounters)
|
- Backend: Next.js route handlers + Supabase (encounters table)
|
||||||
- AI/Intent: Local regex + Claude Haiku fallback
|
- AI/Intent: Two-tier system (Local regex <50ms + Claude Haiku fallback ~400ms)
|
||||||
- Auth: Supabase Auth (server-side guard)
|
- Date/Time: date-fns library (al aanwezig)
|
||||||
|
- Auth: Supabase Auth (server-side guard via `lib/auth/server.ts`)
|
||||||
|
- Voice: Deepgram (existing integration voor voice input)
|
||||||
|
|
||||||
### 2.2 Projectkaders
|
### 2.2 Projectkaders
|
||||||
- Scope (MVP): agenda_query, create_appointment, cancel_appointment, reschedule_appointment + AgendaBlock.
|
- Scope (MVP): agenda_query, create_appointment, cancel_appointment, reschedule_appointment + AgendaBlock.
|
||||||
@@ -31,18 +33,36 @@ Toelichting: dit bouwt voort op het Swift conversatie‑model en hergebruikt de
|
|||||||
- Reuse bestaande agenda actions waar mogelijk.
|
- Reuse bestaande agenda actions waar mogelijk.
|
||||||
|
|
||||||
### 2.3 Programmeer uitgangspunten
|
### 2.3 Programmeer uitgangspunten
|
||||||
- DRY: hergebruik `app/epd/agenda/actions.ts`.
|
- DRY: hergebruik `app/epd/agenda/actions.ts` (getEncounters, createEncounter, cancelEncounter, rescheduleEncounter).
|
||||||
- KISS: snelle API routes + simpele AgendaBlock UI.
|
- KISS: snelle API routes + simpele AgendaBlock UI.
|
||||||
- SOC: intent parsing, data fetching en UI gescheiden.
|
- SOC: intent parsing, data fetching en UI gescheiden.
|
||||||
- YAGNI: alleen P1/P2 uit FO, geen extra planner features.
|
- YAGNI: alleen P1/P2 uit FO, geen extra planner features.
|
||||||
|
|
||||||
|
### 2.4 Performance Targets
|
||||||
|
| Metric | Target | Actual (Expected) |
|
||||||
|
|--------|--------|-------------------|
|
||||||
|
| Local pattern match | <50ms | ~10-15ms |
|
||||||
|
| Local + entity extraction | <100ms | ~30-50ms |
|
||||||
|
| AI fallback (cold) | <800ms | ~400-600ms |
|
||||||
|
| AI fallback (warm) | <500ms | ~200-400ms |
|
||||||
|
| Total (local path) | <150ms | ~50-80ms |
|
||||||
|
| Total (AI path) | <1000ms | ~500-700ms |
|
||||||
|
|
||||||
|
### 2.5 Confidence Thresholds
|
||||||
|
| Confidence | Actie | Voorbeeld |
|
||||||
|
|------------|-------|-----------|
|
||||||
|
| **>0.9** | Direct artifact openen met prefill | "afspraken vandaag" |
|
||||||
|
| **0.7-0.9** | Artifact + bevestigingsvraag | "maak afspraak jan morgen" (tijd ontbreekt) |
|
||||||
|
| **0.5-0.7** | Verduidelijkingsvraag in chat | "maak afspraak" (patient/tijd ontbreekt) |
|
||||||
|
| **<0.5** | Fallback: "Ik begrijp het niet" | Gibberish input |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Epics & Stories Overzicht
|
## 3. Epics & Stories Overzicht
|
||||||
| Epic ID | Titel | Doel | Status | Stories | Opmerkingen |
|
| Epic ID | Titel | Doel | Status | Stories | Opmerkingen |
|
||||||
|---------|-------|------|--------|---------|-------------|
|
|---------|-------|------|--------|---------|-------------|
|
||||||
| E0 | Alignment & scope | MVP afbakenen en keuzes vastleggen | Done | 2 | FO‑based |
|
| E0 | Alignment & scope | MVP afbakenen en keuzes vastleggen | Done | 2 | FO‑based |
|
||||||
| E1 | Intent & entity layer | Agenda intents + entities toevoegen | To Do | 4 | Swift intent stack |
|
| E1 | Intent & entity layer | Agenda intents + entities toevoegen | Done | 4 | Swift intent stack |
|
||||||
| E2 | Date/time parsing | NLP‑helpers voor datum/tijd | To Do | 3 | Geen nieuwe deps |
|
| E2 | Date/time parsing | NLP‑helpers voor datum/tijd | To Do | 3 | Geen nieuwe deps |
|
||||||
| E3 | Backend integratie | Agenda data APIs + reuse actions | To Do | 4 | Auth vereist |
|
| E3 | Backend integratie | Agenda data APIs + reuse actions | To Do | 4 | Auth vereist |
|
||||||
| E4 | AgendaBlock UI | List/create/cancel/reschedule views | To Do | 5 | Swift artifact |
|
| E4 | AgendaBlock UI | List/create/cancel/reschedule views | To Do | 5 | Swift artifact |
|
||||||
@@ -76,13 +96,95 @@ Epic doel: agenda intent types en entities toevoegen aan Swift.
|
|||||||
|
|
||||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|
||||||
|----------|--------------|---------------------|--------|------------------|--------------|
|
|----------|--------------|---------------------|--------|------------------|--------------|
|
||||||
| E1.S1 | SwiftIntent uitbreiden | Nieuwe agenda intents toegevoegd in types/store | To Do | E0.S1 | 2 |
|
| E1.S1 | SwiftIntent uitbreiden | Agenda intent names toegevoegd in types/store | Done | E0.S1 | 2 |
|
||||||
| E1.S2 | Local intent patterns | Regex patterns voor agenda intents in `intent-classifier.ts` | To Do | E1.S1 | 3 |
|
| E1.S2 | Local intent patterns | Regex patterns voor agenda intents in `intent-classifier.ts` | Done | E1.S1 | 3 |
|
||||||
| E1.S3 | AI fallback prompt | Prompt in `intent-classifier-ai.ts` uitgebreid met agenda intents | To Do | E1.S1 | 3 |
|
| E1.S3 | AI fallback prompt | Prompt in `intent-classifier-ai.ts` uitgebreid met agenda intents | Done | E1.S1 | 3 |
|
||||||
| E1.S4 | Entities schema | `ExtractedEntities` uitgebreid met date/time/identifier | To Do | E1.S1 | 2 |
|
| E1.S4 | Entities schema | `ExtractedEntities` uitgebreid met date/time/identifier | Done | E1.S1 | 2 |
|
||||||
|
|
||||||
Technical notes:
|
**Technical notes:**
|
||||||
|
|
||||||
|
**E1.S1 - SwiftIntent type uitbreiding:**
|
||||||
|
- File: `lib/swift/types.ts`
|
||||||
- Houd `SwiftIntent` single source of truth (voorkom duplicatie).
|
- Houd `SwiftIntent` single source of truth (voorkom duplicatie).
|
||||||
|
- SwiftIntent (na uitbreiding):
|
||||||
|
```ts
|
||||||
|
type SwiftIntent =
|
||||||
|
| 'dagnotitie'
|
||||||
|
| 'zoeken'
|
||||||
|
| 'overdracht'
|
||||||
|
| 'agenda_query' // 🆕
|
||||||
|
| 'create_appointment' // 🆕
|
||||||
|
| 'cancel_appointment' // 🆕
|
||||||
|
| 'reschedule_appointment'// 🆕
|
||||||
|
| 'unknown';
|
||||||
|
```
|
||||||
|
|
||||||
|
**E1.S2 - Local pattern examples:**
|
||||||
|
- File: `lib/swift/intent-classifier.ts`
|
||||||
|
- Toevoegen aan `INTENT_PATTERNS`:
|
||||||
|
```ts
|
||||||
|
agenda_query: [
|
||||||
|
{ pattern: /^afspraken?\\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^agenda\\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^(wat|wanneer)\\s+is\\s+(mijn\\s+)?volgende\\s+afspraak/i, weight: 1.0 },
|
||||||
|
],
|
||||||
|
create_appointment: [
|
||||||
|
{ pattern: /^maak\\s+afspraak\\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^plan\\s+(een\\s+)?(intake|afspraak)\\b/i, weight: 1.0 },
|
||||||
|
],
|
||||||
|
cancel_appointment: [
|
||||||
|
{ pattern: /^annuleer\\s+(de\\s+)?afspraak/i, weight: 1.0 },
|
||||||
|
],
|
||||||
|
reschedule_appointment: [
|
||||||
|
{ pattern: /^verzet\\s+(de\\s+)?afspraak/i, weight: 1.0 },
|
||||||
|
{ pattern: /^\\d{1,2}:\\d{2}\\s+naar\\s+\\d{1,2}:\\d{2}/i, weight: 0.9 },
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
**E1.S3 - AI System Prompt addition:**
|
||||||
|
- File: `lib/swift/intent-classifier-ai.ts`
|
||||||
|
- Toevoegen aan `INTENT_CLASSIFIER_SYSTEM_PROMPT`:
|
||||||
|
- Agenda intent descriptions + voorbeelden
|
||||||
|
- Entity extraction instructies (patientName, dateRange, datetime, type, identifier, newDatetime)
|
||||||
|
- Response format met nieuwe entities
|
||||||
|
|
||||||
|
**E1.S4 - ExtractedEntities uitbreiding:**
|
||||||
|
- File: `lib/swift/types.ts`
|
||||||
|
- Toevoegen:
|
||||||
|
```ts
|
||||||
|
interface ExtractedEntities {
|
||||||
|
// Bestaande entities
|
||||||
|
patientName?: string;
|
||||||
|
category?: VerpleegkundigCategory;
|
||||||
|
content?: string;
|
||||||
|
|
||||||
|
// 🆕 Agenda entities
|
||||||
|
dateRange?: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
label: 'vandaag' | 'morgen' | 'deze week' | 'volgende week' | 'custom';
|
||||||
|
};
|
||||||
|
datetime?: {
|
||||||
|
date: Date;
|
||||||
|
time: string; // "HH:mm" format
|
||||||
|
};
|
||||||
|
appointmentType?: 'intake' | 'behandeling' | 'follow-up' | 'telefonisch' |
|
||||||
|
'huisbezoek' | 'online' | 'crisis' | 'overig';
|
||||||
|
location?: 'praktijk' | 'online' | 'thuis';
|
||||||
|
identifier?: {
|
||||||
|
type: 'patient' | 'time' | 'both';
|
||||||
|
patientName?: string;
|
||||||
|
patientId?: string;
|
||||||
|
time?: string;
|
||||||
|
date?: Date;
|
||||||
|
encounterId?: string;
|
||||||
|
};
|
||||||
|
newDatetime?: {
|
||||||
|
date: Date;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -95,6 +197,34 @@ Epic doel: datum/tijd interpretatie uit natuurlijke taal.
|
|||||||
| E2.S2 | Time parser utility | Tijd normalisatie (14:00, half drie) | To Do | E2.S1 | 2 |
|
| E2.S2 | Time parser utility | Tijd normalisatie (14:00, half drie) | To Do | E2.S1 | 2 |
|
||||||
| E2.S3 | Entity extraction hook | Entity extractor gebruikt parser output | To Do | E2.S2 | 2 |
|
| E2.S3 | Entity extraction hook | Entity extractor gebruikt parser output | To Do | E2.S2 | 2 |
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
**E2.S1 - Date parser implementation:**
|
||||||
|
- File: `lib/swift/date-time-parser.ts`
|
||||||
|
- Functie: `parseRelativeDate(input: string): Date | DateRange | null`
|
||||||
|
- Ondersteunt:
|
||||||
|
- "vandaag", "morgen", "overmorgen"
|
||||||
|
- "maandag", "dinsdag", ... (next weekday)
|
||||||
|
- "deze week", "volgende week" (returns DateRange)
|
||||||
|
- Fallback naar date-fns `parse()` voor "30 december", "28-12-2024"
|
||||||
|
- Gebruik `date-fns` helpers: `addDays()`, `startOfWeek()`, `endOfWeek()`, `addWeeks()`
|
||||||
|
|
||||||
|
**E2.S2 - Time parser implementation:**
|
||||||
|
- Functie: `parseTime(input: string): string | null`
|
||||||
|
- Ondersteunt:
|
||||||
|
- "14:00" → "14:00"
|
||||||
|
- "14" → "14:00"
|
||||||
|
- "twee uur" → "14:00"
|
||||||
|
- "half drie" → "14:30"
|
||||||
|
- "kwart voor drie" → "14:45"
|
||||||
|
- Returns `HH:mm` format string
|
||||||
|
|
||||||
|
**E2.S3 - Integration:**
|
||||||
|
- Parser functies worden aangeroepen in entity extraction flow
|
||||||
|
- Local pattern matching extraheert ruwe strings ("morgen", "14:00")
|
||||||
|
- Parser functies normaliseren naar Date/time formats
|
||||||
|
- AI fallback gebruikt parser voor validation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Epic 3 — Backend integratie
|
### Epic 3 — Backend integratie
|
||||||
@@ -107,9 +237,42 @@ Epic doel: agenda data ontsluiten voor Swift blocks.
|
|||||||
| E3.S3 | Cancel/reschedule API | Endpoints die `cancelEncounter`/`rescheduleEncounter` aanroepen | To Do | E3.S1 | 3 |
|
| E3.S3 | Cancel/reschedule API | Endpoints die `cancelEncounter`/`rescheduleEncounter` aanroepen | To Do | E3.S1 | 3 |
|
||||||
| E3.S4 | Patient match API | Fuzzy patiënt matching + disambiguation lijst | To Do | E1.S4 | 2 |
|
| E3.S4 | Patient match API | Fuzzy patiënt matching + disambiguation lijst | To Do | E1.S4 | 2 |
|
||||||
|
|
||||||
Technical notes:
|
**Technical notes:**
|
||||||
- Reuse `app/epd/agenda/actions.ts` voor database logic.
|
|
||||||
- Auth guard via `createClient()` en `supabase.auth.getUser()`.
|
**E3.S1 - Agenda query endpoint:**
|
||||||
|
- Route: `GET /api/swift/agenda?start=2024-12-27&end=2024-12-27`
|
||||||
|
- Auth: via `createClient()` from `lib/auth/server.ts`
|
||||||
|
- Hergebruik: `getEncounters()` from `app/epd/agenda/actions.ts`
|
||||||
|
- Response: `{ appointments: Encounter[] }`
|
||||||
|
- Filters: practitioner_id = current user, status = 'planned', period_start in range
|
||||||
|
- Order: chronological (period_start ASC)
|
||||||
|
|
||||||
|
**E3.S2 - Create appointment endpoint:**
|
||||||
|
- Route: `POST /api/swift/agenda/create`
|
||||||
|
- Body: `{ patientId, datetime: { date, time }, type, location, notes? }`
|
||||||
|
- Validation: Zod schema, Dutch error messages
|
||||||
|
- Hergebruik: `createEncounter()` from `app/epd/agenda/actions.ts`
|
||||||
|
- Mapping:
|
||||||
|
- type → typeCode (FHIR encounter type)
|
||||||
|
- location → classCode ('AMB' = praktijk, 'VR' = online, 'HH' = thuis)
|
||||||
|
- datetime → periodStart/periodEnd (1 hour duration default)
|
||||||
|
- Response: `{ encounterId, success: true }`
|
||||||
|
|
||||||
|
**E3.S3 - Cancel/Reschedule endpoints:**
|
||||||
|
- Route: `POST /api/swift/agenda/cancel`
|
||||||
|
- Body: `{ encounterId }`
|
||||||
|
- Hergebruik: `cancelEncounter(encounterId)` (soft delete: status → 'cancelled')
|
||||||
|
|
||||||
|
- Route: `POST /api/swift/agenda/reschedule`
|
||||||
|
- Body: `{ encounterId, newDatetime: { date, time } }`
|
||||||
|
- Hergebruik: `rescheduleEncounter(encounterId, periodStart, periodEnd)`
|
||||||
|
|
||||||
|
**E3.S4 - Patient search endpoint:**
|
||||||
|
- Route: `GET /api/swift/patients/search?q=jan`
|
||||||
|
- Fuzzy match op patient name (ILIKE %query%)
|
||||||
|
- Response: `{ patients: Array<{ id, name, bsn }> }`
|
||||||
|
- Limit: 10 resultaten
|
||||||
|
- Used voor: disambiguation bij meerdere matches
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -124,6 +287,85 @@ Epic doel: Swift artifact voor agenda flows.
|
|||||||
| E4.S4 | Cancel view | Disambiguation + confirm flow | To Do | E4.S1 | 3 |
|
| E4.S4 | Cancel view | Disambiguation + confirm flow | To Do | E4.S1 | 3 |
|
||||||
| E4.S5 | Reschedule view | Edit form met nieuwe tijd | To Do | E4.S1 | 3 |
|
| E4.S5 | Reschedule view | Edit form met nieuwe tijd | To Do | E4.S1 | 3 |
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
**E4.S1 - AgendaBlock component structure:**
|
||||||
|
- File: `components/swift/artifacts/blocks/agenda-block.tsx`
|
||||||
|
- Props interface:
|
||||||
|
```ts
|
||||||
|
interface AgendaBlockProps {
|
||||||
|
mode: 'list' | 'create' | 'cancel' | 'reschedule';
|
||||||
|
appointments?: Encounter[];
|
||||||
|
dateRange?: { start: Date; end: Date; label: string };
|
||||||
|
prefillData?: {
|
||||||
|
patient?: { id: string; name: string };
|
||||||
|
datetime?: { date: Date; time: string };
|
||||||
|
type?: EncounterType;
|
||||||
|
location?: EncounterLocation;
|
||||||
|
};
|
||||||
|
disambiguationOptions?: Encounter[];
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Architecture: switch statement op mode → render juiste sub-component
|
||||||
|
- Styling: max-width 600px, max-height 80vh, overflow-y auto
|
||||||
|
|
||||||
|
**E4.S2 - List view:**
|
||||||
|
- Component: `<AgendaListView />`
|
||||||
|
- Layout:
|
||||||
|
- Header: Datum label (e.g., "Afspraken Vandaag - 27 december") + close button
|
||||||
|
- Body: Scrollable lijst van appointment cards
|
||||||
|
- Footer: Link naar `/epd/agenda` ("📅 Open volledige agenda →")
|
||||||
|
- Appointment card bevat:
|
||||||
|
- Tijd (09:00 - 10:00)
|
||||||
|
- Patient naam (klikbaar → opens PatientContextCard)
|
||||||
|
- Type badge met color coding (intake=blauw, behandeling=groen, crisis=rood)
|
||||||
|
- Locatie icon (📍 Praktijk, 🌐 Online, 🏠 Thuis)
|
||||||
|
- Actions: [Details] [Annuleren]
|
||||||
|
- Empty state: "📭 Geen afspraken gevonden" + [Maak nieuwe afspraak] button
|
||||||
|
|
||||||
|
**E4.S3 - Create form:**
|
||||||
|
- Component: `<AgendaCreateForm />`
|
||||||
|
- Fields:
|
||||||
|
- Patiënt* (autocomplete, pre-filled)
|
||||||
|
- Datum* (date picker, pre-filled)
|
||||||
|
- Tijd* (time picker 07:00-20:00, pre-filled)
|
||||||
|
- Type* (radio buttons: Intake, Behandeling, Vervolg, Telefonisch, Huisbezoek, Crisis)
|
||||||
|
- Locatie* (radio buttons: Praktijk, Online, Thuis)
|
||||||
|
- Notities (textarea, optioneel, max 500 chars)
|
||||||
|
- Validation:
|
||||||
|
- Patient required
|
||||||
|
- Date cannot be in past
|
||||||
|
- Time within 07:00-20:00
|
||||||
|
- Conflict warning: "⚠️ Conflict: Je hebt al een afspraak om 14:00 met Marie" (optional feature)
|
||||||
|
- Submit: [Annuleren] [✓ Afspraak maken]
|
||||||
|
- States: initial → validating → submitting → success/error
|
||||||
|
|
||||||
|
**E4.S4 - Cancel view:**
|
||||||
|
- Component: `<AgendaCancelView />`
|
||||||
|
- Submode 1: Disambiguation (multiple matches)
|
||||||
|
- Radio list met opties: "○ 28-12-2024 09:00 - Jan de Vries (Intake)"
|
||||||
|
- [Annuleren] [Volgende →] buttons
|
||||||
|
- Submode 2: Confirmation (single match or after disambiguation)
|
||||||
|
- Appointment details card
|
||||||
|
- Warning: "⚠️ Deze actie kan niet ongedaan worden gemaakt."
|
||||||
|
- [Terug] [✓ Annuleren] buttons
|
||||||
|
|
||||||
|
**E4.S5 - Reschedule view:**
|
||||||
|
- Component: `<AgendaRescheduleForm />`
|
||||||
|
- Layout:
|
||||||
|
- Afspraak details (readonly)
|
||||||
|
- Huidige datum/tijd (readonly, strikethrough styling)
|
||||||
|
- Nieuwe datum/tijd* (editable date + time pickers, pre-filled)
|
||||||
|
- Conflict check: "✅ Geen conflicten gevonden" or warning
|
||||||
|
- [Annuleren] [✓ Verzetten] buttons
|
||||||
|
|
||||||
|
**Styling specs:**
|
||||||
|
- Colors: Teal-700 primary, appointment type badges (zie FO sectie 13.3)
|
||||||
|
- Animations: slide-in from right 200ms, fade-out on close
|
||||||
|
- Typography: text-sm voor body, text-base voor headers
|
||||||
|
- Spacing: 16px padding, 12px card gaps
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Epic 5 — Chat orchestration
|
### Epic 5 — Chat orchestration
|
||||||
@@ -135,6 +377,75 @@ Epic doel: agenda intents laten landen in juiste artifact.
|
|||||||
| E5.S2 | Chat prompt update | `/api/swift/chat` prompt bevat agenda sectie + action format | To Do | E1.S3 | 2 |
|
| E5.S2 | Chat prompt update | `/api/swift/chat` prompt bevat agenda sectie + action format | To Do | E1.S3 | 2 |
|
||||||
| E5.S3 | Error states | User-friendly errors + link naar `/epd/agenda` | To Do | E3.S1 | 2 |
|
| E5.S3 | Error states | User-friendly errors + link naar `/epd/agenda` | To Do | E3.S1 | 2 |
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
**E5.S1 - Intent router implementation:**
|
||||||
|
- Location: Swift store of artifact manager
|
||||||
|
- Functie: `openArtifactForIntent(intent, entities, confidence)`
|
||||||
|
- Logic:
|
||||||
|
```ts
|
||||||
|
if (confidence < 0.7) return null; // Trigger verduidelijkingsvraag
|
||||||
|
|
||||||
|
switch (intent) {
|
||||||
|
case 'agenda_query':
|
||||||
|
return { type: 'AgendaBlock', mode: 'list', prefill: { dateRange } };
|
||||||
|
case 'create_appointment':
|
||||||
|
if (!entities.patientName) return null; // Need patient
|
||||||
|
return { type: 'AgendaBlock', mode: 'create', prefill: { patient, datetime, type, location } };
|
||||||
|
case 'cancel_appointment':
|
||||||
|
return { type: 'AgendaBlock', mode: 'cancel', prefill: { identifier } };
|
||||||
|
case 'reschedule_appointment':
|
||||||
|
if (!entities.identifier) return null; // Need to know which appointment
|
||||||
|
return { type: 'AgendaBlock', mode: 'reschedule', prefill: { identifier, newDatetime } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**E5.S2 - Chat API system prompt addition:**
|
||||||
|
- File: `app/api/swift/chat/route.ts`
|
||||||
|
- Toevoegen aan system prompt:
|
||||||
|
```markdown
|
||||||
|
## Agenda & Afspraken Beheer
|
||||||
|
|
||||||
|
Je helpt gebruikers met agenda-gerelateerde taken.
|
||||||
|
|
||||||
|
### Intents die je herkent:
|
||||||
|
1. **agenda_query** - Afspraken opvragen
|
||||||
|
Voorbeelden: "afspraken vandaag", "wat is volgende", "agenda morgen"
|
||||||
|
Actie: Toon lijst van afspraken in AgendaBlock
|
||||||
|
|
||||||
|
2. **create_appointment** - Nieuwe afspraak maken
|
||||||
|
Voorbeelden: "maak afspraak met Jan morgen 14:00", "plan intake"
|
||||||
|
Required: patient, date, time
|
||||||
|
Optional: type (default: behandeling), location (default: praktijk)
|
||||||
|
|
||||||
|
3. **cancel_appointment** - Afspraak annuleren
|
||||||
|
Voorbeelden: "annuleer afspraak Jan", "cancel 14:00"
|
||||||
|
Actie: Toon confirmation dialog
|
||||||
|
|
||||||
|
4. **reschedule_appointment** - Afspraak verzetten
|
||||||
|
Voorbeelden: "verzet 14:00 naar 15:00", "verzet Jan naar dinsdag"
|
||||||
|
Actie: Toon edit form met oude + nieuwe tijd
|
||||||
|
|
||||||
|
### Entity Extraction:
|
||||||
|
- Patient: extraheer volledige naam, gebruik activePatient als user zegt "deze patiënt"
|
||||||
|
- Date/Time: parse relatieve datums ("morgen", "dinsdag"), validate geen verleden
|
||||||
|
- Type: intake, behandeling, follow-up, telefonisch (default: behandeling)
|
||||||
|
- Location: praktijk, online, thuis (default: praktijk)
|
||||||
|
|
||||||
|
### Verduidelijkingsvragen:
|
||||||
|
Stel vragen bij:
|
||||||
|
- Meerdere patiënten met zelfde naam
|
||||||
|
- Onduidelijke datum
|
||||||
|
- Ontbrekende tijd bij create
|
||||||
|
- Meerdere matches bij cancel/reschedule
|
||||||
|
```
|
||||||
|
|
||||||
|
**E5.S3 - Error handling:**
|
||||||
|
- Server errors: "Er ging iets mis bij het ophalen van je afspraken. Probeer het opnieuw of [open de volledige agenda](/epd/agenda)."
|
||||||
|
- Auth errors: Redirect naar `/login`
|
||||||
|
- Validation errors: Dutch messages ("Datum kan niet in het verleden liggen")
|
||||||
|
- Network errors: Retry logic + user-friendly message
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Epic 6 — QA & docs
|
### Epic 6 — QA & docs
|
||||||
@@ -154,30 +465,129 @@ Test types:
|
|||||||
| Test Type | Scope | Tools | Verantwoordelijke |
|
| Test Type | Scope | Tools | Verantwoordelijke |
|
||||||
|-----------|-------|-------|-------------------|
|
|-----------|-------|-------|-------------------|
|
||||||
| Lint | App code | pnpm lint | Developer |
|
| Lint | App code | pnpm lint | Developer |
|
||||||
| Smoke tests | Agenda intents + artifact flows | Manual checklist | Developer/UX |
|
| Type check | TypeScript | pnpm build | Developer |
|
||||||
|
| Smoke tests | Agenda intents + artifact flows | Manual checklist (20 scenarios) | Developer/UX |
|
||||||
| Regression | /epd/agenda klassiek | Manual checklist | Developer |
|
| Regression | /epd/agenda klassiek | Manual checklist | Developer |
|
||||||
|
|
||||||
Manual test checklist (MVP):
|
### Manual Test Checklist (uit FO, 20 scenarios):
|
||||||
- "afspraken vandaag" → AgendaBlock list view
|
|
||||||
- "maak afspraak Jan morgen 14:00" → create form met prefill
|
| # | Scenario | Expected Result | Priority | Status |
|
||||||
- "annuleer afspraak Jan" → disambiguation indien nodig
|
|---|----------|-----------------|----------|--------|
|
||||||
- "verzet 14:00 naar 15:00" → reschedule form
|
| 1 | "afspraken vandaag" | AgendaBlock list view, toont vandaag's afspraken | P1 | ⬜ |
|
||||||
- Link naar `/epd/agenda` werkt
|
| 2 | "wat is volgende afspraak" | Chat response met eerstvolgende, AgendaBlock toont details | P1 | ⬜ |
|
||||||
- Geen auth → redirect naar `/login`
|
| 3 | "maak afspraak jan morgen 14:00" | Create form met prefill, patient gevonden | P1 | ⬜ |
|
||||||
|
| 4 | Voice: "maak intake marie vrijdag 10:00" | Transcript correct, create form opent | P2 | ⬜ |
|
||||||
|
| 5 | "maak afspraak" (incomplete) | Verduidelijkingsvraag: "Met welke patiënt?" | P1 | ⬜ |
|
||||||
|
| 6 | "maak afspraak onbekende patient" | Patient search fallback, suggesties | P2 | ⬜ |
|
||||||
|
| 7 | Create form submit → success | Toast, chat confirmation, artifact sluit | P1 | ⬜ |
|
||||||
|
| 8 | "annuleer afspraak jan" (1 match) | Confirmation dialog, direct match | P1 | ⬜ |
|
||||||
|
| 9 | "annuleer afspraak jan" (3 matches) | Disambiguation radio list | P1 | ⬜ |
|
||||||
|
| 10 | Cancel confirm → success | Afspraak status=cancelled, toast | P1 | ⬜ |
|
||||||
|
| 11 | "verzet 14:00 naar 15:00" | Reschedule form, tijd pre-filled | P1 | ⬜ |
|
||||||
|
| 12 | Reschedule submit → success | Tijd updated, toast confirmation | P1 | ⬜ |
|
||||||
|
| 13 | Conflict warning (overlapping) | Warning banner in create form | P2 | ⬜ |
|
||||||
|
| 14 | Click patient name in list | PatientContextCard opent | P2 | ⬜ |
|
||||||
|
| 15 | Click "Details" button | Navigates to /epd/agenda with filter | P2 | ⬜ |
|
||||||
|
| 16 | Empty state (geen afspraken) | Placeholder met "Maak nieuwe afspraak" | P1 | ⬜ |
|
||||||
|
| 17 | Server error tijdens query | Error in chat, geen artifact | P1 | ⬜ |
|
||||||
|
| 18 | Date in past validation | "Kan geen afspraken in verleden maken" | P1 | ⬜ |
|
||||||
|
| 19 | Context-aware: "maak afspraak deze patient" | Gebruikt active patient uit context | P2 | ⬜ |
|
||||||
|
| 20 | Multiple artifacts (3 max) | Agenda + Dagnotitie + Context card, tabs | P2 | ⬜ |
|
||||||
|
|
||||||
|
### Regression Test Checklist:
|
||||||
|
|
||||||
|
| # | Test | Expected Result | Status |
|
||||||
|
|---|------|-----------------|--------|
|
||||||
|
| R1 | Klassieke agenda `/epd/agenda` werkt nog | Kan afspraken zien/maken/wijzigen | ⬜ |
|
||||||
|
| R2 | Swift dagnotitie intent ongewijzigd | "dagnotitie jan medicatie" opent DagnotatieBlock | ⬜ |
|
||||||
|
| R3 | Swift zoeken intent ongewijzigd | "zoek jan" opent ZoekenBlock | ⬜ |
|
||||||
|
| R4 | Swift overdracht intent ongewijzigd | "overdracht" opent OverdrachtBlock | ⬜ |
|
||||||
|
| R5 | Auth guard op alle endpoints | Geen auth → 401/redirect | ⬜ |
|
||||||
|
|
||||||
|
### Performance Test Targets:
|
||||||
|
|
||||||
|
| Metric | Target | Test Method |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Local pattern match | <50ms | Browser DevTools Network tab |
|
||||||
|
| Create form open | <150ms total | Lighthouse/manual timing |
|
||||||
|
| AI fallback | <1000ms | DevTools, onduidelijke input |
|
||||||
|
| List view render (10 items) | <100ms | React DevTools Profiler |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Demo & Presentatieplan
|
## 6. User Stories (uit FO)
|
||||||
Doel: korte demo van agenda planning via Swift.
|
|
||||||
|
|
||||||
Flow:
|
### Primaire User Stories (P1 - MVP):
|
||||||
1) "afspraken vandaag" → lijst
|
|
||||||
2) "maak afspraak met Jan morgen 14:00" → create form → submit
|
| ID | Als | Wil ik | Zodat | Acceptatie |
|
||||||
3) "annuleer afspraak Jan" → confirm
|
|----|-----|--------|-------|------------|
|
||||||
|
| US-24 | Verpleegkundige | Snel overzicht afspraken vandaag | Ik weet waar ik moet zijn | "afspraken vandaag" → lijst in AgendaBlock, <3 sec |
|
||||||
|
| US-25 | Verpleegkundige | Check volgende afspraak tijdens werk | Ik op tijd ben | "wat is mijn volgende afspraak?" → directe info |
|
||||||
|
| US-27 | Verpleegkundige | Snelle afspraak tijdens telefoongesprek | Ik direct kan plannen | "maak afspraak jan morgen 14:00" → prefilled form, <15 sec |
|
||||||
|
| US-28 | Verpleegkundige | Context-aware planning | Ik niet steeds naam hoef te typen | "maak afspraak met deze patiënt" → gebruikt actieve patiënt |
|
||||||
|
| US-29 | Verpleegkundige | Voice input tijdens consult | Ik hands-free kan werken | Spraak → afspraak maken |
|
||||||
|
| US-30 | Verpleegkundige | Annuleren via chat | Ik snel kan annuleren | "annuleer afspraak jan" → confirmation → done |
|
||||||
|
| US-31 | Verpleegkundige | Snel verzetten | Ik afspraken flexibel kan aanpassen | "verzet 14:00 naar 15:00" → tijd update |
|
||||||
|
|
||||||
|
### Secundaire User Stories (P2 - Nice to Have):
|
||||||
|
|
||||||
|
| ID | Als | Wil ik | Zodat | Acceptatie |
|
||||||
|
|----|-----|--------|-------|------------|
|
||||||
|
| US-26 | Verpleegkundige | Weekoverzicht bekijken | Ik kan plannen | "agenda deze week" → gefilterde lijst |
|
||||||
|
| US-32 | Verpleegkundige | Disambiguation bij meerdere matches | Systeem helpt kiezen | Systeem vraagt "Welke Jan?" → lijst opties |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Risico's & Mitigatie
|
## 7. File Structuur (na implementatie)
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/swift/
|
||||||
|
├── types.ts # SwiftIntent + ExtractedEntities uitbreidingen
|
||||||
|
├── intent-classifier.ts # Local patterns voor agenda intents
|
||||||
|
├── intent-classifier-ai.ts # AI prompt uitbreidingen
|
||||||
|
└── date-time-parser.ts # 🆕 Date/time parsing utilities
|
||||||
|
|
||||||
|
app/api/swift/
|
||||||
|
├── agenda/
|
||||||
|
│ ├── route.ts # 🆕 GET agenda query endpoint
|
||||||
|
│ ├── create/route.ts # 🆕 POST create appointment
|
||||||
|
│ ├── cancel/route.ts # 🆕 POST cancel appointment
|
||||||
|
│ └── reschedule/route.ts # 🆕 POST reschedule appointment
|
||||||
|
├── patients/
|
||||||
|
│ └── search/route.ts # 🆕 GET fuzzy patient search
|
||||||
|
└── chat/route.ts # Updated: agenda system prompt
|
||||||
|
|
||||||
|
components/swift/artifacts/blocks/
|
||||||
|
├── agenda-block.tsx # 🆕 Main AgendaBlock component
|
||||||
|
├── agenda-list-view.tsx # 🆕 List mode
|
||||||
|
├── agenda-create-form.tsx # 🆕 Create mode
|
||||||
|
├── agenda-cancel-view.tsx # 🆕 Cancel mode
|
||||||
|
└── agenda-reschedule-form.tsx # 🆕 Reschedule mode
|
||||||
|
|
||||||
|
app/epd/agenda/
|
||||||
|
└── actions.ts # ♻️ Hergebruikt (getEncounters, createEncounter, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Demo & Presentatieplan
|
||||||
|
Doel: korte demo van agenda planning via Swift (5 minuten).
|
||||||
|
|
||||||
|
**Demo Flow:**
|
||||||
|
1. **Agenda query**: "afspraken vandaag" → lijst met 3 afspraken
|
||||||
|
2. **Quick create**: "maak afspraak met Jan morgen 14:00" → create form → submit → success toast
|
||||||
|
3. **Cancel flow**: "annuleer afspraak Jan" → disambiguation (3 matches) → select → confirm → success
|
||||||
|
4. **Reschedule**: "verzet de 14:00 naar 15:00" → edit form → submit → success
|
||||||
|
5. **Fallback**: Link naar klassieke agenda tonen via footer
|
||||||
|
|
||||||
|
**Key Messaging:**
|
||||||
|
- Swift = snelle queries en actions (10-15 sec)
|
||||||
|
- Klassieke agenda = visuele planning en overzicht
|
||||||
|
- Voice input = hands-free werken tijdens consult
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Risico's & Mitigatie
|
||||||
| Risico | Kans | Impact | Mitigatie | Owner |
|
| Risico | Kans | Impact | Mitigatie | Owner |
|
||||||
|--------|------|--------|-----------|-------|
|
|--------|------|--------|-----------|-------|
|
||||||
| Intent ambigu | Hoog | Middel | Disambiguation + fallback prompt | UX |
|
| Intent ambigu | Hoog | Middel | Disambiguation + fallback prompt | UX |
|
||||||
@@ -187,7 +597,58 @@ Flow:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Evaluatie & Lessons Learned
|
## 10. Sprint Planning & Implementatie Volgorde
|
||||||
|
|
||||||
|
### Voorgestelde Sprint Indeling (3 sprints):
|
||||||
|
|
||||||
|
**Sprint 1: Foundation (E1 + E2)**
|
||||||
|
- Focus: Intent layer + date/time parsing
|
||||||
|
- Stories: E1.S1, E1.S2, E1.S3, E1.S4, E2.S1, E2.S2, E2.S3
|
||||||
|
- Total: 17 SP
|
||||||
|
- Oplevering: Intent detection + entity extraction werkend, nog geen UI
|
||||||
|
- Test: Console log check van extracted entities
|
||||||
|
|
||||||
|
**Sprint 2: Backend + UI Foundation (E3 + E4.S1-S2)**
|
||||||
|
- Focus: API endpoints + AgendaBlock basis
|
||||||
|
- Stories: E3.S1, E3.S2, E3.S3, E3.S4, E4.S1, E4.S2
|
||||||
|
- Total: 14 SP
|
||||||
|
- Oplevering: Agenda query werkend met list view
|
||||||
|
- Test: "afspraken vandaag" end-to-end
|
||||||
|
|
||||||
|
**Sprint 3: UI Completion + Integration (E4.S3-S5 + E5 + E6)**
|
||||||
|
- Focus: Create/cancel/reschedule forms + orchestration
|
||||||
|
- Stories: E4.S3, E4.S4, E4.S5, E5.S1, E5.S2, E5.S3, E6.S1, E6.S2, E6.S3
|
||||||
|
- Total: 20 SP
|
||||||
|
- Oplevering: MVP compleet
|
||||||
|
- Test: Alle 20 test scenarios
|
||||||
|
|
||||||
|
**Total MVP: 51 SP (~3 sprints à 2 weken = 6 weken)**
|
||||||
|
|
||||||
|
### Implementatie Volgorde (binnen stories):
|
||||||
|
|
||||||
|
Voor elke story, volg deze volgorde:
|
||||||
|
1. **Types first**: Definieer interfaces/types
|
||||||
|
2. **Backend**: API endpoints + server actions
|
||||||
|
3. **Frontend**: UI components
|
||||||
|
4. **Integration**: Wire backend ↔ frontend
|
||||||
|
5. **Test**: Manual test scenario's
|
||||||
|
|
||||||
|
### Critical Path:
|
||||||
|
```
|
||||||
|
E1.S1 (types) → E1.S2 (patterns) → E1.S4 (entities) → E2 (parsers) →
|
||||||
|
E3.S1 (query API) → E4.S1 (skeleton) → E4.S2 (list view) →
|
||||||
|
E5.S1 (routing) → Milestone: Basic Query Werkend
|
||||||
|
|
||||||
|
E3.S2 (create API) → E4.S3 (create form) → Milestone: Create Werkend
|
||||||
|
|
||||||
|
E3.S3 (cancel API) → E4.S4 (cancel view) → Milestone: Cancel Werkend
|
||||||
|
|
||||||
|
E4.S5 (reschedule) → E5.S2 (prompt) → E6 (QA) → MVP Done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Evaluatie & Lessons Learned
|
||||||
Te documenteren na oplevering:
|
Te documenteren na oplevering:
|
||||||
- Welke intents vaak misclassificeren?
|
- Welke intents vaak misclassificeren?
|
||||||
- Hoe snel users afspraken kunnen plannen?
|
- Hoe snel users afspraken kunnen plannen?
|
||||||
@@ -195,27 +656,102 @@ Te documenteren na oplevering:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Referenties
|
## 12. Referenties & Documentatie
|
||||||
- FO: `docs/swift/fo-swift-agenda-planning.md`
|
|
||||||
- Swift FO v3: `docs/swift/fo-swift-medical-scribe-v3.md`
|
### Primaire Documenten:
|
||||||
- Agenda module: `app/epd/agenda`
|
- **FO Agenda Afspraken**: `docs/swift/fo-agenda-afspraken.md` - Gebruikersgericht FO
|
||||||
- Swift chat: `app/api/swift/chat/route.ts`
|
- **FO Agenda Planning**: `docs/swift/fo-swift-agenda-planning.md` - Technisch FO met detailed specs
|
||||||
- Intent classifier: `lib/swift/intent-classifier.ts`
|
- **Swift FO v3**: `docs/swift/fo-swift-medical-scribe-v3.md` - Basis conversational interface
|
||||||
|
- **Swift Bouwplan Core**: `docs/swift/bouwplan-swift-standalone-module.md` - Development roadmap
|
||||||
|
|
||||||
|
### Code Referenties:
|
||||||
|
- **Bestaande Agenda**: `app/epd/agenda` - Klassieke kalender (wordt hergebruikt)
|
||||||
|
- **Agenda Actions**: `app/epd/agenda/actions.ts` - Server actions (getEncounters, createEncounter, etc.)
|
||||||
|
- **Swift Chat API**: `app/api/swift/chat/route.ts` - Conversational endpoint
|
||||||
|
- **Intent Classifier**: `lib/swift/intent-classifier.ts` - Local pattern matching
|
||||||
|
- **AI Classifier**: `lib/swift/intent-classifier-ai.ts` - Claude Haiku fallback
|
||||||
|
- **Swift Types**: `lib/swift/types.ts` - SwiftIntent & ExtractedEntities
|
||||||
|
|
||||||
|
### Libraries & Dependencies:
|
||||||
|
- **date-fns**: Date/time utilities (already installed)
|
||||||
|
- **Zod**: API validation schemas (already installed)
|
||||||
|
- **React Query**: Data caching (already installed)
|
||||||
|
- **shadcn/ui**: UI components (already installed)
|
||||||
|
- **Deepgram**: Voice input (already integrated)
|
||||||
|
|
||||||
|
### External References:
|
||||||
|
- [FHIR Encounter Resource](https://www.hl7.org/fhir/encounter.html) - Encounter data model
|
||||||
|
- [date-fns Documentation](https://date-fns.org/) - Date manipulation
|
||||||
|
- [React Query Best Practices](https://tanstack.com/query/latest) - Caching patterns
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Glossary & Abbreviations
|
## 13. Glossary & Abbreviations
|
||||||
|
|
||||||
| Term | Betekenis |
|
| Term | Betekenis |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| Epic | Grote feature of fase |
|
| **Epic** | Grote feature of fase, bevat meerdere stories |
|
||||||
| Story | Kleine uitvoerbare taak |
|
| **Story** | Kleine uitvoerbare taak (user story) |
|
||||||
| MVP | Minimum Viable Product |
|
| **SP** | Story Points - schatting van complexiteit |
|
||||||
| FO | Functioneel Ontwerp |
|
| **MVP** | Minimum Viable Product |
|
||||||
| SSE | Server-Sent Events |
|
| **FO** | Functioneel Ontwerp |
|
||||||
|
| **P1/P2** | Priority 1 (must have) / Priority 2 (nice to have) |
|
||||||
|
| **SSE** | Server-Sent Events |
|
||||||
|
| **RLS** | Row Level Security (Supabase) |
|
||||||
|
| **NLP** | Natural Language Processing |
|
||||||
|
| **Artifact** | Swift UI component (AgendaBlock, DagnotatieBlock, etc.) |
|
||||||
|
| **Intent** | Gebruikersintentie (agenda_query, create_appointment, etc.) |
|
||||||
|
| **Entity** | Geëxtraheerde data uit user input (patient, date, time) |
|
||||||
|
| **Disambiguation** | Verduidelijking bij meerdere matches |
|
||||||
|
| **Confidence** | Zekerheid van intent classificatie (0.0-1.0) |
|
||||||
|
| **Two-tier** | Local pattern matching + AI fallback systeem |
|
||||||
|
| **FHIR** | Fast Healthcare Interoperability Resources |
|
||||||
|
| **Encounter** | Afspraak/contact in FHIR terminologie |
|
||||||
|
|
||||||
|
### Domain-Specific Terms:
|
||||||
|
|
||||||
|
| Term | Betekenis |
|
||||||
|
|------|-----------|
|
||||||
|
| **Klassieke Agenda** | Bestaande visuele kalender in `/epd/agenda` |
|
||||||
|
| **Swift Agenda** | Nieuwe conversational agenda interface |
|
||||||
|
| **AgendaBlock** | Swift artifact voor agenda functionaliteit |
|
||||||
|
| **Verpleegkundige** | Primaire gebruikersrol (nurse/healthcare provider) |
|
||||||
|
| **Practitioner** | FHIR term voor zorgverlener |
|
||||||
|
| **Quick Create** | Snel afspraak maken via natuurlijke taal |
|
||||||
|
| **Hands-free** | Werken met voice input (geen typen nodig) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Versiehistorie:
|
## Versiehistorie
|
||||||
|
|
||||||
| Versie | Datum | Auteur | Wijziging |
|
| Versie | Datum | Auteur | Wijziging |
|
||||||
|--------|-------|--------|-----------|
|
|--------|-------|--------|-----------|
|
||||||
| v1.0 | 27-12-2025 | Colin Lit | Initiele versie |
|
| v1.0 | 27-12-2025 | Colin Lit | Initiele versie - basis epics en stories |
|
||||||
|
| v1.1 | 27-12-2025 | Claude Code | Uitgebreid met technische details uit FO's: patterns, entities, API specs, UI components, test scenarios, sprint planning, file structuur |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
**Voor Developers - Start Here:**
|
||||||
|
1. Lees Epic 0 voor scope
|
||||||
|
2. Begin met E1.S1 (types uitbreiden)
|
||||||
|
3. Volg implementatie volgorde (sectie 10)
|
||||||
|
4. Check test scenarios (sectie 5)
|
||||||
|
5. Zie file structuur (sectie 7) voor waar code moet komen
|
||||||
|
|
||||||
|
**Voor Product Owners:**
|
||||||
|
- MVP scope: sectie 2.2
|
||||||
|
- User stories: sectie 6
|
||||||
|
- Demo plan: sectie 8
|
||||||
|
- Risico's: sectie 9
|
||||||
|
|
||||||
|
**Voor QA:**
|
||||||
|
- Test checklist: sectie 5 (20 scenarios)
|
||||||
|
- Regression tests: sectie 5
|
||||||
|
- Performance targets: sectie 2.4
|
||||||
|
|
||||||
|
**Voor Designers:**
|
||||||
|
- UI components: Epic 4 technical notes
|
||||||
|
- Styling specs: E4.S1-E4.S5
|
||||||
|
- User flows: FO documenten (zie sectie 12)
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const ActionSchema = z.object({
|
|||||||
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
|
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
query: z.string().optional(), // For zoeken intent
|
query: z.string().optional(), // For zoeken intent
|
||||||
|
date: z.string().optional(),
|
||||||
|
time: z.string().optional(),
|
||||||
|
identifier: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
confidence: z.number().min(0).max(1),
|
confidence: z.number().min(0).max(1),
|
||||||
artifact: z
|
artifact: z
|
||||||
|
|||||||
@@ -11,12 +11,25 @@ import type { VerpleegkundigCategory } from '@/lib/types/report';
|
|||||||
|
|
||||||
// Zod schema for AI response validation
|
// Zod schema for AI response validation
|
||||||
const AIIntentResponseSchema = z.object({
|
const AIIntentResponseSchema = z.object({
|
||||||
intent: z.enum(['dagnotitie', 'zoeken', 'overdracht', 'unknown']),
|
intent: z.enum([
|
||||||
|
'dagnotitie',
|
||||||
|
'zoeken',
|
||||||
|
'overdracht',
|
||||||
|
'agenda_query',
|
||||||
|
'create_appointment',
|
||||||
|
'cancel_appointment',
|
||||||
|
'reschedule_appointment',
|
||||||
|
'unknown',
|
||||||
|
]),
|
||||||
confidence: z.number().min(0).max(1),
|
confidence: z.number().min(0).max(1),
|
||||||
entities: z.object({
|
entities: z.object({
|
||||||
patientName: z.string().optional(),
|
patientName: z.string().optional(),
|
||||||
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
|
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
|
query: z.string().optional(),
|
||||||
|
date: z.string().optional(),
|
||||||
|
time: z.string().optional(),
|
||||||
|
identifier: z.string().optional(),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
reasoning: z.string().optional(),
|
reasoning: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -45,7 +58,19 @@ Je taak is om de intentie van een zorgmedewerker te classificeren in één van d
|
|||||||
3. **overdracht** - Gebruiker wil een overdracht/samenvatting van de dienst
|
3. **overdracht** - Gebruiker wil een overdracht/samenvatting van de dienst
|
||||||
Voorbeelden: "overdracht", "wat moet ik weten", "dienst afronden"
|
Voorbeelden: "overdracht", "wat moet ik weten", "dienst afronden"
|
||||||
|
|
||||||
4. **unknown** - Intentie is onduidelijk of past niet in bovenstaande categorieën
|
4. **agenda_query** - Gebruiker wil afspraken opvragen of de agenda zien
|
||||||
|
Voorbeelden: "afspraken vandaag", "wat is mijn volgende afspraak", "agenda volgende week"
|
||||||
|
|
||||||
|
5. **create_appointment** - Gebruiker wil een afspraak maken of plannen
|
||||||
|
Voorbeelden: "maak afspraak met jan morgen 14:00", "plan intake volgende week"
|
||||||
|
|
||||||
|
6. **cancel_appointment** - Gebruiker wil een afspraak annuleren
|
||||||
|
Voorbeelden: "annuleer afspraak jan", "zeg afspraak af"
|
||||||
|
|
||||||
|
7. **reschedule_appointment** - Gebruiker wil een afspraak verzetten
|
||||||
|
Voorbeelden: "verzet 14:00 naar 15:00", "verplaats afspraak naar dinsdag"
|
||||||
|
|
||||||
|
8. **unknown** - Intentie is onduidelijk of past niet in bovenstaande categorieën
|
||||||
|
|
||||||
Voor dagnotitie, extraheer ook:
|
Voor dagnotitie, extraheer ook:
|
||||||
- patientName: de naam van de patiënt (indien genoemd)
|
- patientName: de naam van de patiënt (indien genoemd)
|
||||||
@@ -55,14 +80,41 @@ Voor dagnotitie, extraheer ook:
|
|||||||
Voor zoeken, extraheer:
|
Voor zoeken, extraheer:
|
||||||
- patientName: de naam die gezocht wordt
|
- patientName: de naam die gezocht wordt
|
||||||
|
|
||||||
|
Voor agenda_query, extraheer:
|
||||||
|
- query: het relevante datum-/tijd-bereik of scope (bijv. "vandaag", "volgende week")
|
||||||
|
- patientName: de patiëntnaam als die expliciet genoemd is
|
||||||
|
- date: een expliciete datum als losse waarde (bijv. "2025-01-05")
|
||||||
|
- time: een expliciete tijd (24-uurs, bijv. "14:00")
|
||||||
|
|
||||||
|
Voor create_appointment, extraheer:
|
||||||
|
- patientName: de patiëntnaam als die expliciet genoemd is
|
||||||
|
- query: datum/tijd/type/locatie details in vrije tekst (bijv. "morgen 14:00 intake")
|
||||||
|
- date: een expliciete datum als losse waarde (bijv. "2025-01-05")
|
||||||
|
- time: een expliciete tijd (24-uurs, bijv. "14:00")
|
||||||
|
|
||||||
|
Voor cancel_appointment, extraheer:
|
||||||
|
- patientName: de patiëntnaam als die expliciet genoemd is
|
||||||
|
- query: afspraakdetails in vrije tekst (bijv. "afspraak om 14:00", "afspraak van vrijdag")
|
||||||
|
- identifier: een expliciete afspraak-id indien genoemd
|
||||||
|
|
||||||
|
Voor reschedule_appointment, extraheer:
|
||||||
|
- patientName: de patiëntnaam als die expliciet genoemd is
|
||||||
|
- query: huidige + nieuwe datum/tijd in vrije tekst (bijv. "14:00 naar 15:00")
|
||||||
|
- date: de nieuwe expliciete datum indien genoemd
|
||||||
|
- time: de nieuwe expliciete tijd indien genoemd
|
||||||
|
|
||||||
Antwoord ALLEEN met een JSON object in dit formaat:
|
Antwoord ALLEEN met een JSON object in dit formaat:
|
||||||
{
|
{
|
||||||
"intent": "dagnotitie" | "zoeken" | "overdracht" | "unknown",
|
"intent": "dagnotitie" | "zoeken" | "overdracht" | "agenda_query" | "create_appointment" | "cancel_appointment" | "reschedule_appointment" | "unknown",
|
||||||
"confidence": 0.0-1.0,
|
"confidence": 0.0-1.0,
|
||||||
"entities": {
|
"entities": {
|
||||||
"patientName": "naam" (optioneel),
|
"patientName": "naam" (optioneel),
|
||||||
"category": "medicatie" | "adl" | "gedrag" | "incident" | "observatie" (optioneel),
|
"category": "medicatie" | "adl" | "gedrag" | "incident" | "observatie" (optioneel),
|
||||||
"content": "inhoud" (optioneel)
|
"content": "inhoud" (optioneel),
|
||||||
|
"query": "vrije tekst voor planning" (optioneel),
|
||||||
|
"date": "YYYY-MM-DD" (optioneel),
|
||||||
|
"time": "HH:MM" (optioneel),
|
||||||
|
"identifier": "id" (optioneel)
|
||||||
},
|
},
|
||||||
"reasoning": "korte uitleg" (optioneel)
|
"reasoning": "korte uitleg" (optioneel)
|
||||||
}`;
|
}`;
|
||||||
@@ -138,6 +190,10 @@ export async function classifyIntentWithAI(input: string): Promise<AIClassificat
|
|||||||
patientName: validated.entities?.patientName,
|
patientName: validated.entities?.patientName,
|
||||||
category: validated.entities?.category as VerpleegkundigCategory | undefined,
|
category: validated.entities?.category as VerpleegkundigCategory | undefined,
|
||||||
content: validated.entities?.content,
|
content: validated.entities?.content,
|
||||||
|
query: validated.entities?.query,
|
||||||
|
date: validated.entities?.date,
|
||||||
|
time: validated.entities?.time,
|
||||||
|
identifier: validated.entities?.identifier,
|
||||||
},
|
},
|
||||||
source: 'ai',
|
source: 'ai',
|
||||||
processingTimeMs,
|
processingTimeMs,
|
||||||
|
|||||||
@@ -93,6 +93,65 @@ const INTENT_PATTERNS: Record<Exclude<SwiftIntent, 'unknown'>, PatternConfig[]>
|
|||||||
{ pattern: /^begin\s+dienst\b/i, weight: 0.9 },
|
{ pattern: /^begin\s+dienst\b/i, weight: 0.9 },
|
||||||
{ pattern: /^nieuwe?\s+dienst\b/i, weight: 0.85 },
|
{ pattern: /^nieuwe?\s+dienst\b/i, weight: 0.85 },
|
||||||
],
|
],
|
||||||
|
// Agenda intents
|
||||||
|
agenda_query: [
|
||||||
|
// Exact commands
|
||||||
|
{ pattern: /^agenda\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^mijn\s+agenda\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^volgende\s+afspraak\b/i, weight: 0.95 },
|
||||||
|
|
||||||
|
// Question patterns
|
||||||
|
{ pattern: /^wat\s+zijn\s+(mijn\s+)?afspraken\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^(wat|wanneer)\s+is\s+(mijn\s+)?volgende\s+afspraak\b/i, weight: 1.0 },
|
||||||
|
|
||||||
|
// Date scoped queries
|
||||||
|
{ pattern: /^afspraken\s+(vandaag|morgen|deze\s+week|volgende\s+week)\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^(vandaag|morgen|deze\s+week|volgende\s+week)\s+afspraken\b/i, weight: 0.9 },
|
||||||
|
{ pattern: /^agenda\s+(vandaag|morgen|deze\s+week|volgende\s+week)\b/i, weight: 0.9 },
|
||||||
|
{ pattern: /^planning\s+(vandaag|morgen|deze\s+week|volgende\s+week)\b/i, weight: 0.85 },
|
||||||
|
{ pattern: /^afspraken\b(?!\s+(maken|plannen|inplannen|annuleren|verzetten|verplaatsen|verschuiven))\b/i, weight: 0.85 },
|
||||||
|
|
||||||
|
// Verb patterns
|
||||||
|
{ pattern: /^toon\s+agenda\b/i, weight: 0.9 },
|
||||||
|
{ pattern: /^laat\s+(mijn\s+)?agenda\s+zien\b/i, weight: 0.85 },
|
||||||
|
],
|
||||||
|
create_appointment: [
|
||||||
|
// Exact commands
|
||||||
|
{ pattern: /^maak\s+afspraak\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^plan\s+(een\s+)?(afspraak|intake|gesprek)\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^afspraak\s+(maken|plannen|inplannen)\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^afspraken\s+(maken|plannen|inplannen)\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^nieuwe?\s+afspraak\b/i, weight: 0.95 },
|
||||||
|
|
||||||
|
// Type-first patterns
|
||||||
|
{ pattern: /^maak\s+(een\s+)?(intake|behandeling|gesprek)\b/i, weight: 0.9 },
|
||||||
|
{ pattern: /^(intake|behandeling|gesprek)\s+(met\s+)?\w+/i, weight: 0.85 },
|
||||||
|
{ pattern: /^afspraak\s+met\s+\w+/i, weight: 0.9 },
|
||||||
|
{ pattern: /^plan\s+afspraak\s+met\s+\w+/i, weight: 0.9 },
|
||||||
|
],
|
||||||
|
cancel_appointment: [
|
||||||
|
// Exact commands
|
||||||
|
{ pattern: /^annuleer\s+(de\s+)?afspraak\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^cancel\s+(de\s+)?afspraak\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^verwijder\s+afspraak\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^afspraak\s+annuleren\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^zeg\s+afspraak\s+af\b/i, weight: 0.95 },
|
||||||
|
|
||||||
|
// Short forms
|
||||||
|
{ pattern: /^annuleer\s+\w+/i, weight: 0.7 }, // "annuleer jan"
|
||||||
|
],
|
||||||
|
reschedule_appointment: [
|
||||||
|
// Exact commands
|
||||||
|
{ pattern: /^verzet\s+(de\s+)?afspraak\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^verplaats\s+(de\s+)?afspraak\b/i, weight: 1.0 },
|
||||||
|
{ pattern: /^verschuif\s+afspraak\b/i, weight: 0.95 },
|
||||||
|
{ pattern: /^afspraak\s+verzetten\b/i, weight: 0.95 },
|
||||||
|
|
||||||
|
// Time shifts
|
||||||
|
{ pattern: /^\d{1,2}[:.]\d{2}\s+naar\s+\d{1,2}[:.]\d{2}\b/i, weight: 0.9 },
|
||||||
|
{ pattern: /^(verzet|verplaats)\s+\d{1,2}[:.]\d{2}\b/i, weight: 0.9 },
|
||||||
|
{ pattern: /^verzet\s+\w+\s+naar\b/i, weight: 0.85 }, // "verzet jan naar dinsdag"
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Help patterns (separate, always check)
|
// Help patterns (separate, always check)
|
||||||
|
|||||||
@@ -7,7 +7,15 @@
|
|||||||
import type { VerpleegkundigCategory } from '@/lib/types/report';
|
import type { VerpleegkundigCategory } from '@/lib/types/report';
|
||||||
|
|
||||||
// Intent types
|
// Intent types
|
||||||
export type SwiftIntent = 'dagnotitie' | 'zoeken' | 'overdracht' | 'unknown';
|
export type SwiftIntent =
|
||||||
|
| 'dagnotitie'
|
||||||
|
| 'zoeken'
|
||||||
|
| 'overdracht'
|
||||||
|
| 'agenda_query'
|
||||||
|
| 'create_appointment'
|
||||||
|
| 'cancel_appointment'
|
||||||
|
| 'reschedule_appointment'
|
||||||
|
| 'unknown';
|
||||||
|
|
||||||
export type BlockType = Exclude<SwiftIntent, 'unknown'> | 'patient-dashboard';
|
export type BlockType = Exclude<SwiftIntent, 'unknown'> | 'patient-dashboard';
|
||||||
|
|
||||||
@@ -29,6 +37,9 @@ export interface ExtractedEntities {
|
|||||||
category?: VerpleegkundigCategory;
|
category?: VerpleegkundigCategory;
|
||||||
content?: string;
|
content?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
date?: string;
|
||||||
|
time?: string;
|
||||||
|
identifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block sizes
|
// Block sizes
|
||||||
@@ -62,6 +73,30 @@ export const BLOCK_CONFIGS: Record<BlockType, BlockConfig> = {
|
|||||||
size: 'lg',
|
size: 'lg',
|
||||||
icon: 'ArrowRightLeft',
|
icon: 'ArrowRightLeft',
|
||||||
},
|
},
|
||||||
|
agenda_query: {
|
||||||
|
type: 'agenda_query',
|
||||||
|
title: 'Agenda',
|
||||||
|
size: 'lg',
|
||||||
|
icon: 'Calendar',
|
||||||
|
},
|
||||||
|
create_appointment: {
|
||||||
|
type: 'create_appointment',
|
||||||
|
title: 'Nieuwe afspraak',
|
||||||
|
size: 'lg',
|
||||||
|
icon: 'Plus',
|
||||||
|
},
|
||||||
|
cancel_appointment: {
|
||||||
|
type: 'cancel_appointment',
|
||||||
|
title: 'Afspraak annuleren',
|
||||||
|
size: 'lg',
|
||||||
|
icon: 'X',
|
||||||
|
},
|
||||||
|
reschedule_appointment: {
|
||||||
|
type: 'reschedule_appointment',
|
||||||
|
title: 'Afspraak verzetten',
|
||||||
|
size: 'lg',
|
||||||
|
icon: 'Clock',
|
||||||
|
},
|
||||||
'patient-dashboard': {
|
'patient-dashboard': {
|
||||||
type: 'patient-dashboard',
|
type: 'patient-dashboard',
|
||||||
title: 'Patiëntoverzicht',
|
title: 'Patiëntoverzicht',
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export type SwiftIntent =
|
|||||||
| 'dagnotitie'
|
| 'dagnotitie'
|
||||||
| 'zoeken'
|
| 'zoeken'
|
||||||
| 'overdracht'
|
| 'overdracht'
|
||||||
|
| 'agenda_query'
|
||||||
|
| 'create_appointment'
|
||||||
|
| 'cancel_appointment'
|
||||||
|
| 'reschedule_appointment'
|
||||||
| 'unknown';
|
| 'unknown';
|
||||||
|
|
||||||
export type BlockType = Exclude<SwiftIntent, 'unknown'> | 'fallback' | 'patient-dashboard';
|
export type BlockType = Exclude<SwiftIntent, 'unknown'> | 'fallback' | 'patient-dashboard';
|
||||||
@@ -45,6 +49,9 @@ export interface ExtractedEntities {
|
|||||||
category?: VerpleegkundigCategory;
|
category?: VerpleegkundigCategory;
|
||||||
content?: string;
|
content?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
date?: string;
|
||||||
|
time?: string;
|
||||||
|
identifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block prefill data
|
// Block prefill data
|
||||||
|
|||||||
Reference in New Issue
Block a user