diff --git a/components/swift/command-center/recent-strip.tsx b/components/swift/command-center/recent-strip.tsx index b352aa3..585b91d 100644 --- a/components/swift/command-center/recent-strip.tsx +++ b/components/swift/command-center/recent-strip.tsx @@ -8,12 +8,16 @@ */ 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 = { 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' }, 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' }, }; diff --git a/docs/swift/bouwplan-swift-agenda-planning.md b/docs/swift/bouwplan-swift-agenda-planning.md index cd5d3e4..5ab2816 100644 --- a/docs/swift/bouwplan-swift-agenda-planning.md +++ b/docs/swift/bouwplan-swift-agenda-planning.md @@ -17,11 +17,13 @@ Toelichting: dit bouwt voort op het Swift conversatie‑model en hergebruikt de ### 2.1 Technische Stack - Frontend: Next.js App Router + React + TypeScript -- Styling: Tailwind CSS -- State: Zustand (Swift store) -- Backend: Next.js route handlers + Supabase (encounters) -- AI/Intent: Local regex + Claude Haiku fallback -- Auth: Supabase Auth (server-side guard) +- Styling: Tailwind CSS + shadcn/ui components +- State: Zustand (Swift store) + React Query for data caching +- Backend: Next.js route handlers + Supabase (encounters table) +- AI/Intent: Two-tier system (Local regex <50ms + Claude Haiku fallback ~400ms) +- 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 - 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. ### 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. - SOC: intent parsing, data fetching en UI gescheiden. - 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 | Epic ID | Titel | Doel | Status | Stories | Opmerkingen | |---------|-------|------|--------|---------|-------------| | 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 | | 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 | @@ -76,13 +96,95 @@ Epic doel: agenda intent types en entities toevoegen aan Swift. | 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.S2 | Local intent patterns | Regex patterns voor agenda intents in `intent-classifier.ts` | To Do | E1.S1 | 3 | -| E1.S3 | AI fallback prompt | Prompt in `intent-classifier-ai.ts` uitgebreid met agenda intents | To Do | E1.S1 | 3 | -| E1.S4 | Entities schema | `ExtractedEntities` uitgebreid met date/time/identifier | To Do | E1.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` | Done | 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 | 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). +- 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.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 @@ -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.S4 | Patient match API | Fuzzy patiënt matching + disambiguation lijst | To Do | E1.S4 | 2 | -Technical notes: -- Reuse `app/epd/agenda/actions.ts` voor database logic. -- Auth guard via `createClient()` en `supabase.auth.getUser()`. +**Technical notes:** + +**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.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: `` +- 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: `` +- 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: `` +- 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: `` +- 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 @@ -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.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 @@ -154,30 +465,129 @@ Test types: | Test Type | Scope | Tools | Verantwoordelijke | |-----------|-------|-------|-------------------| | 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 | -Manual test checklist (MVP): -- "afspraken vandaag" → AgendaBlock list view -- "maak afspraak Jan morgen 14:00" → create form met prefill -- "annuleer afspraak Jan" → disambiguation indien nodig -- "verzet 14:00 naar 15:00" → reschedule form -- Link naar `/epd/agenda` werkt -- Geen auth → redirect naar `/login` +### Manual Test Checklist (uit FO, 20 scenarios): + +| # | Scenario | Expected Result | Priority | Status | +|---|----------|-----------------|----------|--------| +| 1 | "afspraken vandaag" | AgendaBlock list view, toont vandaag's afspraken | P1 | ⬜ | +| 2 | "wat is volgende afspraak" | Chat response met eerstvolgende, AgendaBlock toont details | P1 | ⬜ | +| 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 -Doel: korte demo van agenda planning via Swift. +## 6. User Stories (uit FO) -Flow: -1) "afspraken vandaag" → lijst -2) "maak afspraak met Jan morgen 14:00" → create form → submit -3) "annuleer afspraak Jan" → confirm +### Primaire User Stories (P1 - MVP): + +| ID | Als | Wil ik | Zodat | Acceptatie | +|----|-----|--------|-------|------------| +| 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 | |--------|------|--------|-----------|-------| | 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: - Welke intents vaak misclassificeren? - Hoe snel users afspraken kunnen plannen? @@ -195,27 +656,102 @@ Te documenteren na oplevering: --- -## 9. Referenties -- FO: `docs/swift/fo-swift-agenda-planning.md` -- Swift FO v3: `docs/swift/fo-swift-medical-scribe-v3.md` -- Agenda module: `app/epd/agenda` -- Swift chat: `app/api/swift/chat/route.ts` -- Intent classifier: `lib/swift/intent-classifier.ts` +## 12. Referenties & Documentatie + +### Primaire Documenten: +- **FO Agenda Afspraken**: `docs/swift/fo-agenda-afspraken.md` - Gebruikersgericht FO +- **FO Agenda Planning**: `docs/swift/fo-swift-agenda-planning.md` - Technisch FO met detailed specs +- **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 | |------|-----------| -| Epic | Grote feature of fase | -| Story | Kleine uitvoerbare taak | -| MVP | Minimum Viable Product | -| FO | Functioneel Ontwerp | -| SSE | Server-Sent Events | +| **Epic** | Grote feature of fase, bevat meerdere stories | +| **Story** | Kleine uitvoerbare taak (user story) | +| **SP** | Story Points - schatting van complexiteit | +| **MVP** | Minimum Viable Product | +| **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 | |--------|-------|--------|-----------| -| 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) diff --git a/lib/swift/action-parser.ts b/lib/swift/action-parser.ts index 97ed29f..d5f85b6 100644 --- a/lib/swift/action-parser.ts +++ b/lib/swift/action-parser.ts @@ -20,6 +20,9 @@ const ActionSchema = z.object({ category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(), content: z.string().optional(), 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), artifact: z diff --git a/lib/swift/intent-classifier-ai.ts b/lib/swift/intent-classifier-ai.ts index 2859622..5c50c5f 100644 --- a/lib/swift/intent-classifier-ai.ts +++ b/lib/swift/intent-classifier-ai.ts @@ -11,12 +11,25 @@ import type { VerpleegkundigCategory } from '@/lib/types/report'; // Zod schema for AI response validation 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), entities: z.object({ patientName: z.string().optional(), category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(), content: z.string().optional(), + query: z.string().optional(), + date: z.string().optional(), + time: z.string().optional(), + identifier: z.string().optional(), }).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 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: - patientName: de naam van de patiënt (indien genoemd) @@ -55,14 +80,41 @@ Voor dagnotitie, extraheer ook: Voor zoeken, extraheer: - 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: { - "intent": "dagnotitie" | "zoeken" | "overdracht" | "unknown", + "intent": "dagnotitie" | "zoeken" | "overdracht" | "agenda_query" | "create_appointment" | "cancel_appointment" | "reschedule_appointment" | "unknown", "confidence": 0.0-1.0, "entities": { "patientName": "naam" (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) }`; @@ -138,6 +190,10 @@ export async function classifyIntentWithAI(input: string): Promise, PatternConfig[]> { pattern: /^begin\s+dienst\b/i, weight: 0.9 }, { 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) diff --git a/lib/swift/types.ts b/lib/swift/types.ts index 83c63b1..1fbf186 100644 --- a/lib/swift/types.ts +++ b/lib/swift/types.ts @@ -7,7 +7,15 @@ import type { VerpleegkundigCategory } from '@/lib/types/report'; // 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 | 'patient-dashboard'; @@ -29,6 +37,9 @@ export interface ExtractedEntities { category?: VerpleegkundigCategory; content?: string; query?: string; + date?: string; + time?: string; + identifier?: string; } // Block sizes @@ -62,6 +73,30 @@ export const BLOCK_CONFIGS: Record = { size: 'lg', 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': { type: 'patient-dashboard', title: 'Patiëntoverzicht', diff --git a/stores/swift-store.ts b/stores/swift-store.ts index 9c82e25..bf72631 100644 --- a/stores/swift-store.ts +++ b/stores/swift-store.ts @@ -13,6 +13,10 @@ export type SwiftIntent = | 'dagnotitie' | 'zoeken' | 'overdracht' + | 'agenda_query' + | 'create_appointment' + | 'cancel_appointment' + | 'reschedule_appointment' | 'unknown'; export type BlockType = Exclude | 'fallback' | 'patient-dashboard'; @@ -45,6 +49,9 @@ export interface ExtractedEntities { category?: VerpleegkundigCategory; content?: string; query?: string; + date?: string; + time?: string; + identifier?: string; } // Block prefill data