feat(cortex): no-show casus — intent flow, brief-rescript en document artifact
Cortex handelt een no-show af vanuit één chatcommando: afspraak annuleren (declarabiliteits-nudge), en de openstaande concept- huisartsbrief wordt via LLM herschreven en ter review aangeboden in een document artifact (human-in-the-loop, PATCH dispatch zet status op verzendklaar). - API-routes: context, rescript, cancel, dispatch - NoShowDocumentBlock: review/edit UI met origineel-vergelijk - Mock-data voor concept huisartsbrief - PRD, FO, bouwplan en epics in docs/intent/noshow-case/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
689
docs/intent/noshow-case/bouwplan-cortex-casus-noshow.md
Normal file
689
docs/intent/noshow-case/bouwplan-cortex-casus-noshow.md
Normal file
@@ -0,0 +1,689 @@
|
||||
# Bouwplan — Cortex Casus: No Show Afhandeling
|
||||
|
||||
**Projectnaam:** Cortex — Casus 2 (No Show)
|
||||
**Versie:** v1.0
|
||||
**Datum:** 2026-03-29
|
||||
**Auteur:** Colin Lit / Antigravity
|
||||
|
||||
---
|
||||
|
||||
## 1. Doel en Context
|
||||
|
||||
**Doel:** Een werkende demonstratie bouwen van de no-show afhandelings-flow: van één getypte zin ("patiënt niet verschenen") tot een AI-herschreven, geaccordeerde huisartsbrief — zonder dat de regiebehandelaar een enkel scherm verlaat.
|
||||
|
||||
**Context:** De V2 Cortex architectuur (three-layer: Reflex → Orchestrator → Nudge) is volledig operationeel. De chat pipeline, artifact store en nudge infrastructure zijn live. Dit bouwplan bouwt de no-show casus **bovenop** die bestaande foundation.
|
||||
|
||||
**Kernprincipe:**
|
||||
> "Eén zin van de zorgverlener vervangt vijf administratieve handelingen."
|
||||
|
||||
**Beoogd resultaat:** Een demonstreerbare end-to-end flow die:
|
||||
- `register_no_show` intent herkent (Reflex + Orchestrator)
|
||||
- Declarabiliteitsregel proactief signaleert (Nudge stap 1)
|
||||
- Na 1-klik annulering de conceptbrief opspoort (Nudge stap 2)
|
||||
- De brief via LLM herschrijft en toont in Artifact (Document Block)
|
||||
- Accordering en `ready_for_dispatch` status afhandelt
|
||||
|
||||
**Referenties:**
|
||||
- PRD: `docs/intent/noshow-case/prd-cortex-casus-noshow.md`
|
||||
- FO: `docs/intent/noshow-case/fo-cortex-casus-noshow.md`
|
||||
- Bestaand bouwplan V2: `docs/intent/bouwplan-cortex-v2.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. Dev Quick Start
|
||||
|
||||
### Codebase oriëntatie
|
||||
|
||||
```
|
||||
lib/cortex/
|
||||
├── types.ts # ✅ UITBREIDEN — register_no_show intent + BlockType
|
||||
├── reflex-classifier.ts # ✅ UITBREIDEN — no-show patronen toevoegen
|
||||
├── nudge.ts # ✅ UITBREIDEN — 2 nieuwe rules toevoegen
|
||||
└── noshow-handler.ts # 🆕 NIEUW in E3 — multi-step state machine
|
||||
|
||||
stores/
|
||||
└── cortex-store.ts # ✅ UITBREIDEN — noShowFlow state in E3
|
||||
|
||||
components/cortex/
|
||||
├── blocks/
|
||||
│ └── noshow-document-block.tsx # 🆕 NIEUW in E4 — document artifact component
|
||||
└── chat/
|
||||
└── nudge-chat-message.tsx # ✅ HERGEBRUIK — werkt al met accept/dismiss
|
||||
|
||||
app/api/cortex/
|
||||
└── noshow/
|
||||
├── cancel/route.ts # 🆕 NIEUW in E3 — annuleer met cancelled_no_show
|
||||
├── context/route.ts # 🆕 NIEUW in E3 — concept brief ophalen
|
||||
└── rescript/route.ts # 🆕 NIEUW in E3 — LLM brief herschrijven
|
||||
```
|
||||
|
||||
### Werkwijze per story
|
||||
1. Lees story + done criteria
|
||||
2. Check bestaande code (zie "Bestaande code" sectie per epic)
|
||||
3. Implementeer — kleinste werkende stap
|
||||
4. Run `pnpm lint` en `pnpm build`
|
||||
5. Commit: `feat(cortex): NS.E{n}.S{n} — <beschrijving>`
|
||||
|
||||
### Conventies
|
||||
- TypeScript strict mode — geen `any`
|
||||
- Nederlandse gebruikersteksten, Engelse code/comments
|
||||
- Zod voor alle API validatie
|
||||
- Mock data voor prototype — geen echte Zorgmail/declaratie koppeling
|
||||
- Graceful degradation: als LLM faalt, toon originele brief + foutmelding
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecturele Keuzes
|
||||
|
||||
### 3.1 Multi-step flow: hoe?
|
||||
|
||||
De no-show flow heeft 5 sequentiële stappen waarbij elke stap afhangt van de vorige keuze. De huidige nudge engine is **single-shot** (trigger → 1 nudge → klaar).
|
||||
|
||||
**Gekozen aanpak: Lightweight state machine in de store**
|
||||
|
||||
```
|
||||
noShowFlowState: {
|
||||
step: 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done'
|
||||
appointmentId: string | null
|
||||
documentId: string | null
|
||||
originalBriefContent: string | null
|
||||
}
|
||||
```
|
||||
|
||||
Bij elke nudge-accept checkt de `chat-panel` welke `step` actief is en dispatcht de juiste vervolgactie. Dit hergebruikt de bestaande nudge-accept flow zonder een compleet nieuwe state machine framework nodig te hebben.
|
||||
|
||||
**Niet gekozen:** Een aparte state machine library (XState) — te zwaar voor prototype scope.
|
||||
|
||||
### 3.2 Document Block: Rich Text Editor?
|
||||
|
||||
De FO noemt een `rich-text-editor.tsx`, maar die bestaat niet in de codebase. Voor prototype scope is een gewone `<Textarea>` voldoende. De "kwaliteit" van de brief zit in de LLM output, niet in de editor.
|
||||
|
||||
**Besluit:** `<Textarea>` hergebruiken (zoals in `dagnotitie-block.tsx`), focus op Akkoord-knop en status-update.
|
||||
|
||||
### 3.3 Mock data strategie
|
||||
|
||||
Twee mock objecten toevoegen in `lib/cortex/mock-data/noshow.ts`:
|
||||
- Een declarabele afspraak voor de actieve patiënt
|
||||
- Een concept huisartsbrief gekoppeld aan die patiënt
|
||||
|
||||
De API routes checken eerst op mock data voordat ze Supabase bevragen — zodat de demo altijd werkt.
|
||||
|
||||
---
|
||||
|
||||
## 4. Epics & Stories Overzicht
|
||||
|
||||
| Epic ID | Titel | Doel | Stories | Geschatte tijd |
|
||||
|---------|-------|------|---------|----------------|
|
||||
| **NS.E1** | Intent Foundation | Type + classifier | 3 | ~1u |
|
||||
| **NS.E2** | Nudge Rules | 2 rules + chained flow | 3 | ~2u |
|
||||
| **NS.E3** | No-Show API | Cancel + brief-context + LLM rescript | 4 | ~3u |
|
||||
| **NS.E4** | Document Artifact | Block component + Akkoord-knop | 3 | ~2u |
|
||||
| **NS.E5** | Integration & Demo | End-to-end flow + polish | 3 | ~2u |
|
||||
|
||||
**Totaal:** 16 stories, ~10u bouwtijd
|
||||
|
||||
---
|
||||
|
||||
## 5. Epics & Stories (Gedetailleerd)
|
||||
|
||||
---
|
||||
|
||||
### NS.E1 — Intent Foundation
|
||||
|
||||
**Doel:** `register_no_show` herkenbaar maken voor het volledige classificatie systeem.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `lib/cortex/types.ts` — `CortexIntent` union type + `BLOCK_CONFIGS`
|
||||
- `lib/cortex/reflex-classifier.ts` — patroon hoe andere intents worden gematcht
|
||||
|
||||
---
|
||||
|
||||
#### NS.E1.S1 — CortexIntent type uitbreiden
|
||||
|
||||
**Taak:** `register_no_show` toevoegen aan de type definities.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/types.ts`:
|
||||
- Voeg `'register_no_show'` toe aan `CortexIntent` union
|
||||
- Voeg `'register_no_show'` toe aan `BLOCK_CONFIGS` met `{ type: 'register_no_show', title: 'No Show Registratie', size: 'md', icon: 'UserX' }`
|
||||
- `BlockType` wordt automatisch meegenomen (afgeleid van `CortexIntent`)
|
||||
|
||||
**Done criteria:**
|
||||
- `pnpm build` slaagt zonder type errors
|
||||
- `register_no_show` is een geldige `CortexIntent` en `BlockType`
|
||||
|
||||
---
|
||||
|
||||
#### NS.E1.S2 — Reflex Classifier patronen
|
||||
|
||||
**Taak:** Snelle (< 20ms) lokale patroonherkenning voor no-show input.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/reflex-classifier.ts` — voeg patronen toe aan de intent-map:
|
||||
```
|
||||
'register_no_show': [
|
||||
/no.?show/i,
|
||||
/niet verschenen/i,
|
||||
/niet gekomen/i,
|
||||
/afwezig.*afspraak/i,
|
||||
/no show/i,
|
||||
/komt niet op/i,
|
||||
]
|
||||
```
|
||||
- Confidence: `0.85` bij directe match (hoog — weinig ambiguïteit verwacht)
|
||||
|
||||
**Done criteria:**
|
||||
- `classifyWithReflex("patiënt is niet verschenen")` geeft `{ intent: 'register_no_show', confidence: 0.85 }`
|
||||
- `classifyWithReflex("no show vandaag")` geeft hetzelfde resultaat
|
||||
- Input zonder patroon escaleert correct naar AI
|
||||
|
||||
---
|
||||
|
||||
#### NS.E1.S3 — Orchestrator system prompt uitbreiden
|
||||
|
||||
**Taak:** Orchestrator (Layer 2) informeren over de nieuwe intent.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/orchestrator.ts` of het `buildSystemPrompt` in `app/api/cortex/chat/route.ts`:
|
||||
- Voeg `register_no_show` toe aan de intent-beschrijvingen:
|
||||
```
|
||||
register_no_show: Gebruik wanneer de zorgverlener aangeeft dat een patiënt
|
||||
niet op de afspraak is verschenen. Signaalwoorden: "no show", "niet verschenen",
|
||||
"niet gekomen", "afwezig bij afspraak".
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- System prompt bevat `register_no_show` met correcte beschrijving
|
||||
- AI classificeert "cliënt was er niet" als `register_no_show` (handmatig te testen via chat)
|
||||
|
||||
---
|
||||
|
||||
### NS.E2 — Nudge Rules & Chained Flow
|
||||
|
||||
**Doel:** Twee proactieve nudges aanmaken die sequentieel volgen op de no-show registratie.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `lib/cortex/nudge.ts` — `ProtocolRule` interface + `PROTOCOL_RULES` array + `evaluateNudge()`
|
||||
- `components/cortex/chat/nudge-chat-message.tsx` — hoe accept/dismiss werkt
|
||||
- `components/cortex/chat/chat-panel.tsx` — hoe `evaluateNudge` wordt aangeroepen na action completion
|
||||
- `stores/cortex-store.ts` — `noShowFlowState` is hier **nog niet** — toevoegen in S1
|
||||
|
||||
---
|
||||
|
||||
#### NS.E2.S1 — No-show flow state aan store toevoegen
|
||||
|
||||
**Taak:** Minimale state machine voor de 5-staps flow in de Zustand store.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `stores/cortex-store.ts`:
|
||||
```typescript
|
||||
// Voeg toe aan state interface:
|
||||
noShowFlow: {
|
||||
step: 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done';
|
||||
appointmentId: string | null;
|
||||
documentId: string | null;
|
||||
originalContent: string | null;
|
||||
};
|
||||
|
||||
// Actions:
|
||||
setNoShowStep: (step: NoShowStep) => void;
|
||||
setNoShowContext: (ctx: Partial<NoShowFlowContext>) => void;
|
||||
resetNoShowFlow: () => void;
|
||||
```
|
||||
- Initiële waarde: `{ step: 'idle', appointmentId: null, documentId: null, originalContent: null }`
|
||||
|
||||
**Done criteria:**
|
||||
- Store compileert zonder errors
|
||||
- `useCorteStore(s => s.noShowFlow)` geeft initiële state terug
|
||||
|
||||
---
|
||||
|
||||
#### NS.E2.S2 — Nudge Rule 1: Declarabiliteitscheck
|
||||
|
||||
**Taak:** Na `register_no_show` intent → nudge die waarschuwt over declarabele afspraak.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `lib/cortex/nudge.ts` — voeg toe aan `PROTOCOL_RULES`:
|
||||
```typescript
|
||||
{
|
||||
id: 'noshow-declarabel-check',
|
||||
trigger: {
|
||||
intent: 'register_no_show',
|
||||
},
|
||||
priority: 'high',
|
||||
suggestion: {
|
||||
intent: 'cancel_appointment',
|
||||
message: 'Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als \'No Show\'?',
|
||||
prefillEntities: {},
|
||||
},
|
||||
expiresAfterMs: 5 * 60 * 1000,
|
||||
}
|
||||
```
|
||||
- **Let op:** `priority: 'high'` — deze nudge verschijnt altijd als eerste
|
||||
|
||||
**Done criteria:**
|
||||
- `evaluateNudge({ intent: 'register_no_show', ... })` geeft nudge met id `noshow-declarabel-check` terug
|
||||
- Nudge toont correct in chat UI met `[Ja, annuleer]` accept-knop
|
||||
|
||||
---
|
||||
|
||||
#### NS.E2.S3 — Nudge Rule 2: Concept Brief Alert + Chained Trigger
|
||||
|
||||
**Taak:** Na acceptatie van Nudge 1 → check op concept brief → Nudge 2 tonen.
|
||||
|
||||
**Aanpassingen:**
|
||||
|
||||
**Stap A — Nudge Rule 2 definiëren** in `lib/cortex/nudge.ts`:
|
||||
```typescript
|
||||
{
|
||||
id: 'noshow-brief-check',
|
||||
trigger: {
|
||||
intent: 'cancel_appointment',
|
||||
conditions: [{ field: 'noShowContext', operator: 'exists', value: true }],
|
||||
},
|
||||
priority: 'high',
|
||||
suggestion: {
|
||||
intent: 'register_no_show', // hergebruik als signaal voor brief-stap
|
||||
message: 'Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?',
|
||||
prefillEntities: {},
|
||||
},
|
||||
expiresAfterMs: 5 * 60 * 1000,
|
||||
}
|
||||
```
|
||||
|
||||
**Stap B — Chained accept logica** in `components/cortex/chat/chat-panel.tsx`:
|
||||
- Bij `acceptSuggestion` van `noshow-declarabel-check`:
|
||||
1. `setNoShowStep('waiting_cancel')`
|
||||
2. Roep `/api/cortex/noshow/cancel` aan
|
||||
3. Roep `/api/cortex/noshow/context` aan (check op conceptbrief)
|
||||
4. Als brief gevonden: `setNoShowContext({ documentId, originalContent })` + `evaluateNudge` voor rule 2
|
||||
5. `setNoShowStep('waiting_brief')`
|
||||
- Bij `acceptSuggestion` van `noshow-brief-check`:
|
||||
1. Roep `/api/cortex/noshow/rescript` aan
|
||||
2. `openArtifact({ type: 'register_no_show', ... })` met herschreven inhoud
|
||||
3. `setNoShowStep('brief_open')`
|
||||
|
||||
**Done criteria:**
|
||||
- Klikken op `[Ja, annuleer]` triggert de cancel API call + toont Nudge 2
|
||||
- Klikken op `[Ja, pas brief aan]` triggert rescript API + opent artifact
|
||||
- `noShowFlow.step` werkt correct door alle stappen
|
||||
|
||||
---
|
||||
|
||||
### NS.E3 — No-Show API Routes
|
||||
|
||||
**Doel:** Drie backend routes bouwen die de no-show flow ondersteunen.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `app/api/cortex/agenda/cancel/route.ts` — bestaand cancel patroon
|
||||
- `app/api/cortex/chat/route.ts` — Anthropic streaming patroon
|
||||
- `lib/auth/server.ts` — `createClient()` voor authenticated requests
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S1 — Mock data aanmaken
|
||||
|
||||
**Taak:** Statische mock data voor declarabele afspraak en concept huisartsbrief.
|
||||
|
||||
**Nieuw bestand:** `lib/cortex/mock-data/noshow.ts`
|
||||
```typescript
|
||||
export const MOCK_NO_SHOW_APPOINTMENT = {
|
||||
id: 'mock-appt-noshow-001',
|
||||
patientId: 'demo-patient-001', // matcht actieve demo patiënt
|
||||
date: new Date().toISOString(),
|
||||
type: 'intake_consult',
|
||||
is_billable: true,
|
||||
status: 'scheduled',
|
||||
title: 'Intake Consult',
|
||||
};
|
||||
|
||||
export const MOCK_CONCEPT_BRIEF = {
|
||||
id: 'mock-brief-001',
|
||||
patientId: 'demo-patient-001',
|
||||
type: 'huisartsbrief',
|
||||
status: 'concept',
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
content: `Geachte collega,
|
||||
|
||||
Hierbij informeer ik u over de intake van uw patiënt die bij ons in zorg is gekomen.
|
||||
Tijdens de intake is uitgebreid de hulpvraag besproken en zijn de volgende aandachtspunten
|
||||
naar voren gekomen: [...]
|
||||
|
||||
Het vervolgtraject bestaat uit wekelijkse gesprekken gericht op [...]
|
||||
|
||||
Met vriendelijke groet,
|
||||
[Behandelaar]`,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- Bestand bestaat en exporteert beide mock objecten
|
||||
- TypeScript compileert zonder errors
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S2 — Cancel Route (`/api/cortex/noshow/cancel`)
|
||||
|
||||
**Taak:** Afspraak annuleren met `cancelled_no_show` status.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/cancel/route.ts`
|
||||
|
||||
**Request body (Zod):**
|
||||
```typescript
|
||||
const CancelNoShowSchema = z.object({
|
||||
appointmentId: z.string(),
|
||||
patientId: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{ success: true, appointmentId: string, newStatus: 'cancelled_no_show' }
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
1. Auth check via `createClient()`
|
||||
2. Zod validatie
|
||||
3. Check op mock data (`MOCK_NO_SHOW_APPOINTMENT.id`) — als match, return mock success
|
||||
4. Anders: Supabase update op `encounters` tabel, `status = 'cancelled_no_show'`
|
||||
5. Return success response
|
||||
|
||||
**Done criteria:**
|
||||
- `POST /api/cortex/noshow/cancel` met mock appointment ID geeft `{ success: true }` terug
|
||||
- Route geeft correcte 400/401 bij ongeldige input/auth
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S3 — Context Route (`/api/cortex/noshow/context`)
|
||||
|
||||
**Taak:** Openstaande concept documenten ophalen voor actieve patiënt.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/context/route.ts`
|
||||
|
||||
**Query params:** `?patientId=<id>`
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
hasConceptBrief: boolean;
|
||||
document?: {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
1. Auth check
|
||||
2. `patientId` param ophalen
|
||||
3. Check op mock data — als `patientId` matcht `demo-patient-001`, return mock brief
|
||||
4. Anders: Supabase query op `reports` tabel, `type = 'huisartsbrief'` + `status = 'concept'` + `patient_id = patientId` + `deleted_at IS NULL`
|
||||
|
||||
**Done criteria:**
|
||||
- `GET /api/cortex/noshow/context?patientId=demo-patient-001` geeft mock brief terug
|
||||
- `hasConceptBrief: false` bij onbekende patiënt
|
||||
|
||||
---
|
||||
|
||||
#### NS.E3.S4 — Rescript Route (`/api/cortex/noshow/rescript`)
|
||||
|
||||
**Taak:** LLM herschrijft de huisartsbrief met de no-show professioneel verwerkt.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/rescript/route.ts`
|
||||
|
||||
**Request body (Zod):**
|
||||
```typescript
|
||||
const RescriptSchema = z.object({
|
||||
documentId: z.string(),
|
||||
originalContent: z.string(),
|
||||
patientId: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{ rescriptedContent: string; documentId: string; }
|
||||
```
|
||||
|
||||
**System prompt voor LLM:**
|
||||
```
|
||||
Je bent een klinisch schrijver in een GGZ instelling. Je taak is een bestaande conceptbrief
|
||||
aanpassen zodat de no-show van de patiënt professioneel en contextbewust verwerkt is.
|
||||
|
||||
Regels:
|
||||
- Integreer de no-show in de lopende tekst, voeg het NIET achteraan toe
|
||||
- Gebruik formele GGZ-taal ("de patiënt is niet verschenen op het geplande consult")
|
||||
- Bewaar alle andere informatie in de brief
|
||||
- Voeg een zin toe over herschikken van het vervolggesprek
|
||||
- Reageer ALLEEN met de herschreven brieftekst, geen uitleg
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- Route roept Anthropic API aan met correct system prompt
|
||||
- Herschreven brief is contextbewust (no-show verwerkt, niet achteraan geplakt)
|
||||
- Route faalt graceful (geeft originele content terug + foutmelding bij API error)
|
||||
|
||||
---
|
||||
|
||||
### NS.E4 — Document Artifact Block
|
||||
|
||||
**Doel:** Een nieuw block component dat de herschreven brief weergeeft met een Akkoord-knop.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `components/cortex/blocks/dagnotitie-block.tsx` — patroon voor block met textarea
|
||||
- `components/cortex/shared/block-footer.tsx` — `primaryAction` / `secondaryAction` props
|
||||
- `components/cortex/artifacts/artifact-container.tsx` — `renderArtifactBlock()` switch
|
||||
|
||||
---
|
||||
|
||||
#### NS.E4.S1 — NoShowDocumentBlock component
|
||||
|
||||
**Taak:** Block component voor het weergeven en bewerken van de herschreven brief.
|
||||
|
||||
**Nieuw bestand:** `components/cortex/blocks/noshow-document-block.tsx`
|
||||
|
||||
**Props interface:**
|
||||
```typescript
|
||||
interface NoShowDocumentBlockProps {
|
||||
prefill: {
|
||||
documentId: string;
|
||||
content: string;
|
||||
title: string;
|
||||
originalContent?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**UI Structuur:**
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 📄 Huisartsbrief — aangepast door Cortex │
|
||||
│ ─────────────────────────────────────────── │
|
||||
│ [Textarea met herschreven inhoud] │
|
||||
│ (editeerbaar, minimaal 12 regels hoog) │
|
||||
│ │
|
||||
│ ─────────────────────────────────────────── │
|
||||
│ [Origineel bekijken ↕] [Akkoord & Verzendklaar →] │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
- State: `content` (editeerbaar), `isSubmitting`, `isDone`
|
||||
- "Origineel bekijken" toggle: toont `originalContent` in grijs eronder (collapsible)
|
||||
- "Akkoord & Verzendklaar" knop:
|
||||
1. `setIsSubmitting(true)`
|
||||
2. `PATCH /api/cortex/noshow/dispatch` (S2) met `{ documentId, finalContent: content }`
|
||||
3. Bij success: `setIsDone(true)`, `setNoShowStep('done')`, toast "Brief verzendklaar"
|
||||
4. Bij fout: toast foutmelding, `setIsSubmitting(false)`
|
||||
|
||||
**Done criteria:**
|
||||
- Component rendert herschreven inhoud in editeerbare textarea
|
||||
- "Akkoord" knop triggert dispatch API call
|
||||
- Na success toont component bevestiging (geen navigatie nodig)
|
||||
|
||||
---
|
||||
|
||||
#### NS.E4.S2 — Dispatch Route (`/api/cortex/noshow/dispatch`)
|
||||
|
||||
**Taak:** Document status naar `ready_for_dispatch` zetten na accordering.
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/dispatch/route.ts`
|
||||
|
||||
**Request body (Zod):**
|
||||
```typescript
|
||||
const DispatchSchema = z.object({
|
||||
documentId: z.string(),
|
||||
finalContent: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Logica:**
|
||||
1. Auth check
|
||||
2. Als `documentId` matcht mock ID → return success (mock)
|
||||
3. Anders: Supabase update op `reports`, `status = 'ready_for_dispatch'`, `content = finalContent`
|
||||
|
||||
**Done criteria:**
|
||||
- `PATCH` met mock document ID geeft `{ success: true, status: 'ready_for_dispatch' }` terug
|
||||
|
||||
---
|
||||
|
||||
#### NS.E4.S3 — ArtifactContainer register_no_show toevoegen
|
||||
|
||||
**Taak:** `renderArtifactBlock()` switch uitbreiden met het nieuwe block.
|
||||
|
||||
**Aanpassingen:**
|
||||
- `components/cortex/artifacts/artifact-container.tsx`:
|
||||
```typescript
|
||||
case 'register_no_show':
|
||||
return (
|
||||
<NoShowDocumentBlock
|
||||
prefill={artifact.prefill as NoShowDocumentPrefill}
|
||||
/>
|
||||
);
|
||||
```
|
||||
- Import toevoegen voor `NoShowDocumentBlock`
|
||||
|
||||
**Done criteria:**
|
||||
- `openArtifact({ type: 'register_no_show', prefill: { documentId, content, title } })` opent het juiste block
|
||||
- Geen TypeScript errors in artifact-container
|
||||
|
||||
---
|
||||
|
||||
### NS.E5 — Integration & Demo Polish
|
||||
|
||||
**Doel:** De volledige flow end-to-end werkend maken en demo-klaar polijsten.
|
||||
|
||||
**Bestaande code om te lezen voor je begint:**
|
||||
- `components/cortex/chat/chat-panel.tsx` — de centrale orchestrator voor de hele flow
|
||||
- `docs/intent/noshow-case/fo-cortex-casus-noshow.md` — de exacte stap-voor-stap flow
|
||||
|
||||
---
|
||||
|
||||
#### NS.E5.S1 — Chat Panel integratie
|
||||
|
||||
**Taak:** `chat-panel.tsx` uitbreiden met de no-show flow orchestratie.
|
||||
|
||||
**Aanpassingen in `chat-panel.tsx`:**
|
||||
|
||||
```typescript
|
||||
// Na handleConfirmAction / na evaluateNudge call:
|
||||
const handleNudgeAccept = async (suggestion: NudgeSuggestion) => {
|
||||
const { noShowFlow, setNoShowStep, setNoShowContext } = useCorteStore.getState();
|
||||
|
||||
if (suggestion.id === 'noshow-declarabel-check') {
|
||||
setNoShowStep('waiting_cancel');
|
||||
// Cancel afspraak
|
||||
await fetch('/api/cortex/noshow/cancel', { method: 'POST', body: ... });
|
||||
// Check concept brief
|
||||
const ctx = await fetch(`/api/cortex/noshow/context?patientId=${activePatient?.id}`).then(r => r.json());
|
||||
if (ctx.hasConceptBrief) {
|
||||
setNoShowContext({ documentId: ctx.document.id, originalContent: ctx.document.content });
|
||||
// Trigger nudge 2 als chat message
|
||||
addChatMessage({ type: 'nudge', content: BRIEF_NUDGE_MESSAGE, nudge: NOSHOW_BRIEF_NUDGE });
|
||||
setNoShowStep('waiting_brief');
|
||||
} else {
|
||||
setNoShowStep('done');
|
||||
addChatMessage({ type: 'assistant', content: 'Afspraak geannuleerd als No Show. Er zijn geen openstaande conceptbrieven gevonden.' });
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestion.id === 'noshow-brief-check') {
|
||||
setNoShowStep('brief_open');
|
||||
const { documentId, originalContent } = noShowFlow;
|
||||
const result = await fetch('/api/cortex/noshow/rescript', { method: 'POST', body: ... }).then(r => r.json());
|
||||
openArtifact({
|
||||
type: 'register_no_show',
|
||||
title: 'Huisartsbrief — aangepast',
|
||||
prefill: { documentId, content: result.rescriptedContent, title: 'Huisartsbrief', originalContent },
|
||||
});
|
||||
addChatMessage({ type: 'assistant', content: 'Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent.' });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Done criteria:**
|
||||
- Volledige 5-staps flow is doorloopbaar in de browser
|
||||
- Elke stap produceert de juiste chat message en UI state
|
||||
|
||||
---
|
||||
|
||||
#### NS.E5.S2 — Loading states en foutafhandeling
|
||||
|
||||
**Taak:** Zorgdragen dat de flow nooit "vastloopt" voor de gebruiker.
|
||||
|
||||
**Aanpassingen:**
|
||||
- Voeg `isNoShowProcessing: boolean` toe aan store
|
||||
- `chat-panel.tsx`: toon `<ProcessingIndicator>` tijdens API calls tussen stappen
|
||||
- Bij API fout: voeg error chat message toe met Nederlandse foutmelding
|
||||
- `resetNoShowFlow()` aanroepen bij component unmount (cleanup)
|
||||
|
||||
**Done criteria:**
|
||||
- Loading indicator zichtbaar tijdens elke API call
|
||||
- Bij netwerk fout verschijnt: "Er is iets misgegaan. Probeer het opnieuw."
|
||||
- Flow kan opnieuw gestart worden na een fout
|
||||
|
||||
---
|
||||
|
||||
#### NS.E5.S3 — Demo script validatie
|
||||
|
||||
**Taak:** De flow handmatig doorlopen en valideren aan de succescriteria uit het PRD.
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Input "patiënt niet verschenen" → `register_no_show` herkend (Reflex, < 20ms)
|
||||
- [ ] Input "cliënt was er niet vandaag" → Orchestrator herkent correct
|
||||
- [ ] Nudge 1 verschijnt met juiste declarabiliteits-waarschuwing
|
||||
- [ ] Klik `[Ja, annuleer]` → Cancel API succesvol → Nudge 2 verschijnt
|
||||
- [ ] Klik `[Ja, pas brief aan]` → Rescript API → Artifact opent met aangepaste brief
|
||||
- [ ] Textarea is editeerbaar — aanpassing blijft behouden
|
||||
- [ ] Klik `[Akkoord & Verzendklaar]` → Dispatch API → bevestiging in chat
|
||||
- [ ] Volledige flow duurt < 30 seconden (excl. LLM latency)
|
||||
- [ ] Geen schermwissels nodig gedurende de hele flow
|
||||
- [ ] `pnpm build` slaagt zonder errors
|
||||
|
||||
**Done criteria:** Alle checkboxen afgevinkt = casus NS volledig gereed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Risico's en Mitigatie
|
||||
|
||||
| Risico | Kans | Impact | Mitigatie |
|
||||
|--------|------|--------|-----------|
|
||||
| LLM rescript kwaliteit onvoldoende | Middel | Hoog | System prompt itereren; originele brief altijd tonen als fallback |
|
||||
| Multi-step flow breekt bij snelle klikken | Laag | Middel | `isNoShowProcessing` boolean blokkeert dubbele submits |
|
||||
| Mock data matcht niet met actieve demo patiënt | Middel | Hoog | `patientId` hardcoderen in mock als `demo-patient-001` — zelfde als in bestaande demo setup |
|
||||
| `ArtifactContainer` switch mist nieuwe BlockType | Laag | Hoog | TypeScript exhaustive check dwingt dit af bij `pnpm build` |
|
||||
| Nudge 2 triggert niet als brief niet bestaat | Laag | Laag | Flow eindigt graceful met chat message "geen brief gevonden" |
|
||||
|
||||
---
|
||||
|
||||
## 7. Niet in Scope (Prototype)
|
||||
|
||||
- Echte declaratie-integratie (ZPM/HiX koppeling)
|
||||
- Zorgmail verzending van de brief
|
||||
- Automatisch inplannen van vervangende afspraak
|
||||
- Meerdere conceptbrieven per patiënt (multi-document select)
|
||||
- Audit log van de no-show registratie
|
||||
- Undo/rollback van de annulering
|
||||
296
docs/intent/noshow-case/epics/NS-E1-intent-foundation.md
Normal file
296
docs/intent/noshow-case/epics/NS-E1-intent-foundation.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# NS.E1 — Intent Foundation
|
||||
|
||||
**Casus:** Cortex No Show Afhandeling
|
||||
**Epic doel:** `register_no_show` herkenbaar maken door het volledige classificatiesysteem — types, reflex patterns, en orchestrator prompt.
|
||||
**Geschatte tijd:** ~1.5 uur
|
||||
**Afhankelijkheden:** Geen (dit is de basis voor alle andere epics)
|
||||
|
||||
---
|
||||
|
||||
## Context & waarschuwingen
|
||||
|
||||
### Dubbele BlockType definitie — kritiek
|
||||
|
||||
Er zijn **twee** `BlockType` definities in de codebase die allebei bijgewerkt moeten worden:
|
||||
|
||||
| Bestand | Definitie |
|
||||
|---|---|
|
||||
| `lib/cortex/types.ts` | `Exclude<CortexIntent, 'unknown' \| 'intake_navigeer'> \| 'patient-dashboard'` |
|
||||
| `stores/cortex-store.ts` | `Exclude<CortexIntent, 'unknown'> \| 'fallback' \| 'patient-dashboard'` |
|
||||
|
||||
De store-versie is breder (includeert `intake_navigeer` en `fallback`). Door `register_no_show` toe te voegen aan `CortexIntent` wordt het automatisch onderdeel van beide `BlockType` afgeleidingen — maar alleen als je ook `BLOCK_CONFIGS` bijwerkt.
|
||||
|
||||
### `INTENT_PATTERNS` is strikt getypeerd
|
||||
|
||||
`reflex-classifier.ts` regel 27:
|
||||
```typescript
|
||||
const INTENT_PATTERNS: Record<Exclude<CortexIntent, 'unknown'>, PatternConfig[]> = { ... }
|
||||
```
|
||||
|
||||
Dit betekent: als je `register_no_show` toevoegt aan `CortexIntent`, **eist TypeScript** dat je ook een entry toevoegt aan `INTENT_PATTERNS`. De build faalt anders. Dit is bewust — het voorkomt vergeten patronen.
|
||||
|
||||
---
|
||||
|
||||
## NS.E1.S1 — CortexIntent type + BLOCK_CONFIGS uitbreiden
|
||||
|
||||
**Bestand:** `lib/cortex/types.ts`
|
||||
|
||||
### Wijziging 1 — CortexIntent union
|
||||
|
||||
Voeg `'register_no_show'` toe na de intake-intents:
|
||||
|
||||
```typescript
|
||||
export type CortexIntent =
|
||||
| 'dagnotitie'
|
||||
| 'zoeken'
|
||||
| 'overdracht'
|
||||
| 'agenda_query'
|
||||
| 'create_appointment'
|
||||
| 'cancel_appointment'
|
||||
| 'reschedule_appointment'
|
||||
| 'intake_status'
|
||||
| 'intake_navigeer'
|
||||
| 'risico_query'
|
||||
| 'diagnose_query'
|
||||
// No Show casus
|
||||
| 'register_no_show'
|
||||
| 'unknown';
|
||||
```
|
||||
|
||||
### Wijziging 2 — BLOCK_CONFIGS
|
||||
|
||||
Voeg een entry toe in `BLOCK_CONFIGS` (het is een `Record<BlockType, BlockConfig>` — alle BlockTypes moeten erin):
|
||||
|
||||
```typescript
|
||||
register_no_show: {
|
||||
type: 'register_no_show',
|
||||
title: 'No Show Registratie',
|
||||
size: 'md',
|
||||
icon: 'UserX',
|
||||
},
|
||||
```
|
||||
|
||||
Locatie: onderaan de `BLOCK_CONFIGS` definitie, na `diagnose_query`.
|
||||
|
||||
### Done criteria
|
||||
- `pnpm build` slaagt — geen TypeScript errors
|
||||
- `register_no_show` is een geldige waarde voor `CortexIntent`
|
||||
- `BLOCK_CONFIGS['register_no_show']` bestaat
|
||||
|
||||
---
|
||||
|
||||
## NS.E1.S2 — Reflex Classifier patronen toevoegen
|
||||
|
||||
**Bestand:** `lib/cortex/reflex-classifier.ts`
|
||||
|
||||
### Context
|
||||
|
||||
`INTENT_PATTERNS` is getypeerd als `Record<Exclude<CortexIntent, 'unknown'>, PatternConfig[]>`. Na toevoeging van `register_no_show` aan de union **moet** je hier ook een entry toevoegen — anders faalt de build met:
|
||||
|
||||
```
|
||||
Type '{ dagnotitie: ...; ... }' is missing the following properties
|
||||
from type 'Record<...>': register_no_show
|
||||
```
|
||||
|
||||
### Wijziging — patronen toevoegen
|
||||
|
||||
Voeg toe aan `INTENT_PATTERNS`, na het `diagnose_query` blok:
|
||||
|
||||
```typescript
|
||||
// =========================================================================
|
||||
// No Show casus
|
||||
// =========================================================================
|
||||
register_no_show: [
|
||||
// Exacte no-show varianten
|
||||
{ pattern: /no.?show/i, weight: 1.0 },
|
||||
{ pattern: /no show/i, weight: 1.0 },
|
||||
|
||||
// "Niet verschenen" varianten
|
||||
{ pattern: /niet\s+verschenen/i, weight: 0.95 },
|
||||
{ pattern: /niet\s+gekomen/i, weight: 0.95 },
|
||||
{ pattern: /niet\s+op\s+komen\s+dagen/i, weight: 0.95 },
|
||||
|
||||
// "Afwezig" + context
|
||||
{ pattern: /afwezig\s+(bij|voor)\s+(de\s+)?afspraak/i, weight: 0.9 },
|
||||
{ pattern: /pati[eë]nt\s+afwezig/i, weight: 0.85 },
|
||||
|
||||
// Werkwoordvormen
|
||||
{ pattern: /komt?\s+niet\s+(op|naar)/i, weight: 0.85 },
|
||||
{ pattern: /is\s+er\s+niet\s+(geweest)?/i, weight: 0.7 },
|
||||
],
|
||||
```
|
||||
|
||||
**Toelichting weights:**
|
||||
- `1.0` — directe "no show" termen, geen ambiguïteit
|
||||
- `0.95` — sterke klinische uitdrukkingen
|
||||
- `0.85–0.9` — contextuele varianten
|
||||
- `0.7` — vaag ("is er niet geweest") — escaleer naar AI bij twijfel
|
||||
|
||||
### Escalatie check
|
||||
|
||||
Controleer of de bestaande escalatiepatronen niet per ongeluk triggeren op no-show input:
|
||||
- `"patiënt niet verschenen"` — bevat geen conjuncties → geen multi_intent
|
||||
- `"patiënt is er niet"` — bevat geen voornaamwoorden die escaleren → OK
|
||||
- `"patiënt niet verschenen morgen"` — bevat `morgen` → triggert `relative_time` escalatie → correct (AI lost dit op)
|
||||
|
||||
### Done criteria
|
||||
- `classifyWithReflex("patiënt is niet verschenen")` geeft `{ intent: 'register_no_show', confidence: 0.95, shouldEscalateToAI: false }`
|
||||
- `classifyWithReflex("no show vandaag")` geeft `{ intent: 'register_no_show', confidence: 1.0, shouldEscalateToAI: false }`
|
||||
- `classifyWithReflex("patiënt niet verschenen morgen")` geeft `shouldEscalateToAI: true, escalationReason: 'relative_time'`
|
||||
- `pnpm build` slaagt — TypeScript tevreden met de nieuwe entry
|
||||
|
||||
---
|
||||
|
||||
## NS.E1.S3 — Orchestrator system prompt uitbreiden
|
||||
|
||||
**Bestand:** `app/api/cortex/chat/route.ts`
|
||||
|
||||
### Context
|
||||
|
||||
De chat API bouwt een system prompt via `buildSystemPrompt(context)`. Die prompt bevat een opsomming van alle intents met beschrijvingen. De Orchestrator (Layer 2) én de chat AI (Layer 3) gebruiken allebei deze prompt.
|
||||
|
||||
Zoek de sectie in de prompt waar intents worden opgesomd — het zal er zo uitzien:
|
||||
```
|
||||
- dagnotitie: Gebruik wanneer...
|
||||
- zoeken: Gebruik wanneer...
|
||||
```
|
||||
|
||||
### Wijziging — intent beschrijving toevoegen
|
||||
|
||||
Voeg toe aan de intent-opsomming in `buildSystemPrompt`:
|
||||
|
||||
```
|
||||
- register_no_show: Gebruik wanneer de zorgverlener aangeeft dat een patiënt
|
||||
niet op de geplande afspraak is verschenen. Signaalwoorden: "no show",
|
||||
"niet verschenen", "niet gekomen", "afwezig bij afspraak", "komt niet op".
|
||||
Entiteiten: geen specifieke entiteiten nodig — de actieve patiënt en
|
||||
huidige context worden gebruikt.
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- System prompt in de chat API bevat `register_no_show` met beschrijving
|
||||
- Handmatige test: typ "cliënt was er niet vandaag" in de chat → AI classificeert als `register_no_show` (zichtbaar in console via `[ChatPanel] Action detected:`)
|
||||
- Typ "patiënt heeft de afspraak gemist" → zelfde resultaat
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## NS.E1.S4 — `action-parser.ts` uitbreiden
|
||||
|
||||
**Bestand:** `lib/cortex/action-parser.ts`
|
||||
|
||||
### Context — waarom dit hier hoort
|
||||
|
||||
`action-parser.ts` bevat vier hardcoded enums en switches die de volledige lijst van geldige intents bevatten. Ze zijn **niet** afgeleid van `CortexIntent` in `types.ts` — ze zijn handmatig gesynchroniseerd. Dit is het meest kritieke gat: als `register_no_show` hier ontbreekt, faalt de Zod validatie silently op de AI response en wordt `parsed.action` altijd `null`. De nudge triggert dan **nooit** — ook niet na alle andere fixes.
|
||||
|
||||
### Wijziging 1 — `ActionSchema.intent` enum (regel ~16)
|
||||
|
||||
Voeg `'register_no_show'` toe aan de `z.enum([...])` lijst:
|
||||
|
||||
```typescript
|
||||
intent: z.enum([
|
||||
'dagnotitie',
|
||||
'zoeken',
|
||||
'overdracht',
|
||||
'agenda_query',
|
||||
'create_appointment',
|
||||
'cancel_appointment',
|
||||
'reschedule_appointment',
|
||||
'intake_status',
|
||||
'intake_navigeer',
|
||||
'risico_query',
|
||||
'diagnose_query',
|
||||
// No Show casus
|
||||
'register_no_show',
|
||||
'unknown',
|
||||
]),
|
||||
```
|
||||
|
||||
### Wijziging 2 — `ActionSchema.artifact.type` enum (regel ~77)
|
||||
|
||||
Voeg `'register_no_show'` toe aan de artifact type enum:
|
||||
|
||||
```typescript
|
||||
type: z.enum([
|
||||
'dagnotitie',
|
||||
'zoeken',
|
||||
'overdracht',
|
||||
'agenda_query',
|
||||
'create_appointment',
|
||||
'cancel_appointment',
|
||||
'reschedule_appointment',
|
||||
'intake_status',
|
||||
'risico_query',
|
||||
'diagnose_query',
|
||||
'fallback',
|
||||
'patient-dashboard',
|
||||
// No Show casus
|
||||
'register_no_show',
|
||||
]),
|
||||
```
|
||||
|
||||
### Wijziging 3 — `routeIntentToArtifact` switch
|
||||
|
||||
De switch heeft een `default: return null`. Voeg een expliciete case toe **vóór** de `default`, na het `intake_navigeer` blok:
|
||||
|
||||
```typescript
|
||||
case 'register_no_show':
|
||||
// Artifact wordt geopend via de no-show handler in chat-panel.tsx,
|
||||
// niet via de generieke routing. Geef null terug zodat de handler
|
||||
// de controle houdt.
|
||||
return null;
|
||||
```
|
||||
|
||||
**Toelichting:** We returnen bewust `null` — het artifact voor no-show wordt door `handleNoShowRescriptStep` geopend met volledige prefill data (documentId, content, originalContent) die pas beschikbaar is ná de rescript API call. De generieke router heeft die data niet.
|
||||
|
||||
### Wijziging 4 — `getDefaultConfirmationMessage` switch
|
||||
|
||||
Voeg toe na het `overdracht` case, vóór de `default`:
|
||||
|
||||
```typescript
|
||||
case 'register_no_show':
|
||||
return 'Ik registreer de no show en controleer de agenda op declarabiliteit.';
|
||||
```
|
||||
|
||||
### Wijziging 5 — `getAcceptButtonText` in `nudge-chat-message.tsx`
|
||||
|
||||
**Bestand:** `components/cortex/chat/nudge-chat-message.tsx`
|
||||
|
||||
De `getAcceptButtonText` functie bepaalt de knoptekst op basis van `suggestion.suggestion.intent`:
|
||||
- Nudge 1 heeft `intent: 'cancel_appointment'` → toont al correct **"Ja, annuleren"**
|
||||
- Nudge 2 heeft `intent: 'register_no_show'` → valt op `default: 'Ja, uitvoeren'`
|
||||
|
||||
Voeg toe in de switch van `getAcceptButtonText`:
|
||||
|
||||
```typescript
|
||||
case 'register_no_show':
|
||||
return 'Ja, pas brief aan';
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- `ActionSchema.safeParse({ type: 'action', intent: 'register_no_show', entities: {}, confidence: 0.9 })` geeft `success: true`
|
||||
- `routeIntentToArtifact('register_no_show', {}, 0.9)` geeft `null` terug (geen artifact via generieke router)
|
||||
- `getDefaultConfirmationMessage('register_no_show', {})` geeft correcte Nederlandse zin
|
||||
- Nudge 2 accept-knop toont "Ja, pas brief aan"
|
||||
- `pnpm build` slaagt
|
||||
|
||||
---
|
||||
|
||||
## Validatie na NS.E1 (alle stories)
|
||||
|
||||
Run na voltooiing van alle vier stories:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
Verwacht: geen errors. Als er TypeScript errors zijn over `INTENT_PATTERNS` of `BLOCK_CONFIGS`, controleer dan of beide `Record<>` types volledig zijn bijgewerkt.
|
||||
|
||||
**Snelle handmatige smoke test:**
|
||||
1. Open de app (`pnpm dev`)
|
||||
2. Navigeer naar het Cortex dashboard
|
||||
3. Typ: `"patiënt niet verschenen"`
|
||||
4. Verwacht: AI response met intent `register_no_show` zichtbaar in browser console via `[ChatPanel] Action detected: { intent: 'register_no_show', ... }`
|
||||
5. Verwacht: nudge bubble verschijnt (NS.E2 vereist — maar de action parse moet nu wel slagen)
|
||||
374
docs/intent/noshow-case/epics/NS-E2-nudge-rules.md
Normal file
374
docs/intent/noshow-case/epics/NS-E2-nudge-rules.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# NS.E2 — Nudge Rules & Chained Flow
|
||||
|
||||
**Casus:** Cortex No Show Afhandeling
|
||||
**Epic doel:** Twee proactieve nudges bouwen die sequentieel volgen op de no-show registratie, plus de store-uitbreiding die de multi-step state bijhoudt.
|
||||
**Geschatte tijd:** ~2 uur
|
||||
**Afhankelijkheden:** NS.E1 volledig klaar (CortexIntent type moet bestaan)
|
||||
|
||||
---
|
||||
|
||||
## Context & waarschuwingen
|
||||
|
||||
### Nudge evaluatie wordt NIET aangeroepen vanuit de hoofd-chat flow
|
||||
|
||||
Dit is het grootste architecturele gat. `evaluateNudge()` wordt momenteel **alleen** aangeroepen vanuit `handleConfirmAction` in `chat-panel.tsx` — dat is de V2 chain flow (feature-flagged). De hoofd-chat SSE flow (`onDone` callback) roept dit **niet** aan.
|
||||
|
||||
Gevolg: als een gebruiker "patiënt niet verschenen" typt en de AI antwoordt met `register_no_show`, triggert er **geen nudge** via de protocol rules engine.
|
||||
|
||||
**Oplossing in dit epic:** We voegen nudge-evaluatie toe aan de `onDone` callback van de hoofd-chat flow, maar alleen voor de `register_no_show` intent. Dit is bewust scopebegrensd — geen generieke uitbreiding van de flow.
|
||||
|
||||
### NudgeSuggestion ID bevat timestamp
|
||||
|
||||
Nudge IDs worden gegenereerd als `nudge-${rule.id}-${Date.now()}`. We kunnen dus **niet** matchen op het volledige ID in `handleAcceptNudge`. Match op `suggestion.trigger.intent` of de `suggestion.suggestion.rationale` (= `rule.name`).
|
||||
|
||||
**Gekozen aanpak:** `suggestion.trigger.intent` gebruiken als discriminator.
|
||||
- Nudge 1: `trigger.intent === 'register_no_show'`
|
||||
- Nudge 2: handmatig geconstrueerd (niet via `evaluateNudge`) — zie NS.E2.S3
|
||||
|
||||
### ProtocolRule interface vereisten
|
||||
|
||||
De `ProtocolRule` interface in `nudge.ts` heeft **verplichte** velden die het bouwplan oorspronkelijk wegliet:
|
||||
|
||||
```typescript
|
||||
interface ProtocolRule {
|
||||
id: string;
|
||||
name: string; // ← verplicht
|
||||
trigger: {
|
||||
intent: CortexIntent;
|
||||
conditions: ProtocolCondition[]; // ← verplicht, mag [] zijn
|
||||
};
|
||||
suggestion: {
|
||||
intent: CortexIntent;
|
||||
message: string;
|
||||
prefillEntities: (source: ExtractedEntities) => Partial<ExtractedEntities>; // ← FUNCTIE, niet object
|
||||
};
|
||||
protocol?: ProtocolMetadata;
|
||||
priority: NudgePriority;
|
||||
enabled: boolean; // ← verplicht
|
||||
expiresAfterMs: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NS.E2.S1 — No-show flow state aan store toevoegen
|
||||
|
||||
**Bestand:** `stores/cortex-store.ts`
|
||||
|
||||
### Context
|
||||
|
||||
De no-show flow heeft 5 sequentiële stappen. We slaan de state op in de Zustand store zodat alle componenten er toegang toe hebben.
|
||||
|
||||
### Wijziging 1 — Types toevoegen (boven `CortexStore` interface)
|
||||
|
||||
```typescript
|
||||
// No-show flow state machine
|
||||
export type NoShowStep = 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done';
|
||||
|
||||
export interface NoShowFlowState {
|
||||
step: NoShowStep;
|
||||
appointmentId: string | null;
|
||||
documentId: string | null;
|
||||
originalContent: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Wijziging 2 — State toevoegen aan `CortexStore` interface
|
||||
|
||||
Voeg toe na `pendingClarification`:
|
||||
```typescript
|
||||
// No-show flow state
|
||||
noShowFlow: NoShowFlowState;
|
||||
```
|
||||
|
||||
Voeg actions toe na bestaande clarification actions:
|
||||
```typescript
|
||||
// No-show flow actions
|
||||
setNoShowStep: (step: NoShowStep) => void;
|
||||
setNoShowContext: (ctx: Partial<Omit<NoShowFlowState, 'step'>>) => void;
|
||||
resetNoShowFlow: () => void;
|
||||
```
|
||||
|
||||
### Wijziging 3 — Initiële waarde in `initialState`
|
||||
|
||||
```typescript
|
||||
noShowFlow: {
|
||||
step: 'idle',
|
||||
appointmentId: null,
|
||||
documentId: null,
|
||||
originalContent: null,
|
||||
} as NoShowFlowState,
|
||||
```
|
||||
|
||||
### Wijziging 4 — Actions implementeren (in `create()` body)
|
||||
|
||||
```typescript
|
||||
setNoShowStep: (step) =>
|
||||
set(
|
||||
(state) => ({ noShowFlow: { ...state.noShowFlow, step } }),
|
||||
false,
|
||||
'setNoShowStep'
|
||||
),
|
||||
|
||||
setNoShowContext: (ctx) =>
|
||||
set(
|
||||
(state) => ({ noShowFlow: { ...state.noShowFlow, ...ctx } }),
|
||||
false,
|
||||
'setNoShowContext'
|
||||
),
|
||||
|
||||
resetNoShowFlow: () =>
|
||||
set(
|
||||
{
|
||||
noShowFlow: {
|
||||
step: 'idle',
|
||||
appointmentId: null,
|
||||
documentId: null,
|
||||
originalContent: null,
|
||||
},
|
||||
},
|
||||
false,
|
||||
'resetNoShowFlow'
|
||||
),
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- `pnpm build` slaagt
|
||||
- `useCortexStore(s => s.noShowFlow)` geeft `{ step: 'idle', ... }` terug
|
||||
- `useCortexStore(s => s.setNoShowStep)('waiting_cancel')` werkt zonder error
|
||||
|
||||
---
|
||||
|
||||
## NS.E2.S2 — Nudge Rule 1: Declarabiliteitscheck
|
||||
|
||||
**Bestand:** `lib/cortex/nudge.ts`
|
||||
|
||||
### Wijziging — rule toevoegen aan `PROTOCOL_RULES`
|
||||
|
||||
Voeg toe **vóór** de bestaande wondzorg-regel (hogere priority verdient eerste positie):
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'noshow-declarabel-check',
|
||||
name: 'No Show declarabiliteitscheck',
|
||||
trigger: {
|
||||
intent: 'register_no_show',
|
||||
conditions: [], // Geen aanvullende condities — altijd triggeren bij no-show
|
||||
},
|
||||
suggestion: {
|
||||
intent: 'cancel_appointment',
|
||||
message: 'Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als \'No Show\'?',
|
||||
prefillEntities: (_source) => ({}), // Geen prefill nodig — context wordt via API opgehaald
|
||||
},
|
||||
priority: 'high',
|
||||
enabled: true,
|
||||
expiresAfterMs: DEFAULT_EXPIRY_MS,
|
||||
},
|
||||
```
|
||||
|
||||
**Let op:** Er is geen `protocol` metadata voor deze rule — dat is optioneel en ontbreekt hier bewust (het gaat om een inkoopverplichting, geen klinisch protocol).
|
||||
|
||||
### Done criteria
|
||||
- `evaluateNudge({ intent: 'register_no_show', actionId: 'test', entities: {}, content: '' })` geeft een array terug met één nudge
|
||||
- De nudge heeft `priority: 'high'` en `suggestion.intent: 'cancel_appointment'`
|
||||
- De message bevat "declarabel"
|
||||
|
||||
---
|
||||
|
||||
## NS.E2.S3 — Hoofd-chat flow uitbreiden met nudge trigger
|
||||
|
||||
**Bestand:** `components/cortex/chat/chat-panel.tsx`
|
||||
|
||||
### Context
|
||||
|
||||
De nudge voor no-show moet triggeren ná de AI-response, niet ná een chain action. Dat betekent: uitbreiding van de `onDone` callback in de `ChatInput` `onSend` handler.
|
||||
|
||||
Momenteel (vereenvoudigd):
|
||||
```typescript
|
||||
onDone: () => {
|
||||
setStreaming(false);
|
||||
const parsed = parseActionFromResponse(accumulatedContent);
|
||||
if (parsed.action) {
|
||||
updateLastMessage(...);
|
||||
setPendingAction(parsed.action);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stap 1 — Store actions ophalen
|
||||
|
||||
Voeg bovenaan `ChatPanel()` toe:
|
||||
```typescript
|
||||
const noShowFlow = useCortexStore((s) => s.noShowFlow);
|
||||
const setNoShowStep = useCortexStore((s) => s.setNoShowStep);
|
||||
const setNoShowContext = useCortexStore((s) => s.setNoShowContext);
|
||||
```
|
||||
|
||||
### Stap 2 — Nudge trigger in `onDone`
|
||||
|
||||
Voeg toe in de `onDone` callback, **ná** het bestaande `setPendingAction` blok:
|
||||
|
||||
```typescript
|
||||
// No-show flow: trigger nudge na register_no_show intent
|
||||
if (parsed.action?.intent === 'register_no_show' && isFeatureEnabled('CORTEX_NUDGE')) {
|
||||
const suggestions = evaluateNudge({
|
||||
intent: 'register_no_show',
|
||||
actionId: crypto.randomUUID(),
|
||||
entities: parsed.action.entities,
|
||||
content: message, // originele user input
|
||||
});
|
||||
|
||||
suggestions.forEach((suggestion) => {
|
||||
addChatMessage({
|
||||
type: 'nudge',
|
||||
content: suggestion.suggestion.message,
|
||||
nudge: suggestion,
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Stap 3 — `handleAcceptNudge` uitbreiden met no-show logica
|
||||
|
||||
De bestaande `handleAcceptNudge` doet: `acceptSuggestion` → `routeIntentToArtifact` → `openArtifact`. We voegen een **vroege exit** toe voor no-show nudges die een aparte flow hebben.
|
||||
|
||||
Voeg toe aan het begin van `handleAcceptNudge`, vóór de bestaande `routeIntentToArtifact` aanroep:
|
||||
|
||||
```typescript
|
||||
const handleAcceptNudge = useCallback(async (
|
||||
suggestionId: string,
|
||||
suggestion: ChatMessageType['nudge']
|
||||
) => {
|
||||
acceptSuggestion(suggestionId);
|
||||
|
||||
if (!suggestion) return;
|
||||
|
||||
// --- No-show flow: stap 2 → stap 3 ---
|
||||
if (suggestion.trigger.intent === 'register_no_show') {
|
||||
await handleNoShowCancelStep(suggestion);
|
||||
return; // Vroege exit — geen generieke artifact routing
|
||||
}
|
||||
|
||||
// Bestaande generieke flow (voor alle andere nudges)
|
||||
const artifact = routeIntentToArtifact(
|
||||
suggestion.suggestion.intent,
|
||||
suggestion.suggestion.entities,
|
||||
0.9
|
||||
);
|
||||
if (artifact) {
|
||||
openArtifact({ type: artifact.type, prefill: artifact.prefill, title: artifact.title });
|
||||
}
|
||||
}, [acceptSuggestion, openArtifact, setNoShowStep, setNoShowContext, addChatMessage, activePatient]);
|
||||
```
|
||||
|
||||
**Let op:** `handleAcceptNudge` is nu `async`. Controleer of de prop-definitie in `NudgeChatMessage` dit ondersteunt — zo niet, pas de prop type aan.
|
||||
|
||||
### Stap 4 — `handleNoShowCancelStep` functie
|
||||
|
||||
Voeg toe als aparte `useCallback` in `ChatPanel`:
|
||||
|
||||
```typescript
|
||||
const handleNoShowCancelStep = useCallback(async (suggestion: NudgeSuggestion) => {
|
||||
setNoShowStep('waiting_cancel');
|
||||
|
||||
// Voeg verwerking chat message toe
|
||||
addChatMessage({ type: 'assistant', content: 'Bezig met annuleren...' });
|
||||
|
||||
try {
|
||||
// Annuleer de afspraak
|
||||
const cancelRes = await fetch('/api/cortex/noshow/cancel', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
appointmentId: 'mock-appt-noshow-001', // mock voor nu
|
||||
patientId: activePatient?.id ?? 'demo-patient-001',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!cancelRes.ok) throw new Error('Cancel mislukt');
|
||||
|
||||
// Check op concept brief
|
||||
const patientId = activePatient?.id ?? 'demo-patient-001';
|
||||
const ctxRes = await fetch(`/api/cortex/noshow/context?patientId=${patientId}`);
|
||||
const ctx = await ctxRes.json();
|
||||
|
||||
if (ctx.hasConceptBrief) {
|
||||
setNoShowContext({
|
||||
documentId: ctx.document.id,
|
||||
originalContent: ctx.document.content,
|
||||
});
|
||||
setNoShowStep('waiting_brief');
|
||||
|
||||
// Construeer nudge 2 handmatig (niet via evaluateNudge)
|
||||
const briefNudge: NudgeSuggestion = {
|
||||
id: `nudge-noshow-brief-${Date.now()}`,
|
||||
trigger: {
|
||||
actionId: 'noshow-cancel-done',
|
||||
intent: 'cancel_appointment',
|
||||
entities: {},
|
||||
},
|
||||
suggestion: {
|
||||
intent: 'register_no_show', // gebruikt als signaal voor brief-stap
|
||||
entities: {},
|
||||
message: 'Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?',
|
||||
rationale: 'noshow-brief-check', // gebruikt als discriminator in handleAcceptNudge stap 5
|
||||
},
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
addChatMessage({ type: 'nudge', content: briefNudge.suggestion.message, nudge: briefNudge });
|
||||
} else {
|
||||
setNoShowStep('done');
|
||||
addChatMessage({
|
||||
type: 'assistant',
|
||||
content: 'Afspraak geannuleerd als No Show. Er zijn geen openstaande conceptbrieven gevonden.',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
setNoShowStep('idle');
|
||||
addChatMessage({
|
||||
type: 'error',
|
||||
content: 'Er is iets misgegaan bij het annuleren. Probeer het opnieuw.',
|
||||
});
|
||||
}
|
||||
}, [activePatient, setNoShowStep, setNoShowContext, addChatMessage]);
|
||||
```
|
||||
|
||||
### Stap 5 — `handleAcceptNudge` uitbreiden voor brief-stap
|
||||
|
||||
In de bestaande `handleAcceptNudge`, voeg een tweede no-show check toe vóór de generieke flow:
|
||||
|
||||
```typescript
|
||||
// --- No-show flow: stap 4 → brief openen ---
|
||||
if (suggestion.suggestion.rationale === 'noshow-brief-check') {
|
||||
await handleNoShowRescriptStep();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Voeg `handleNoShowRescriptStep` toe als aparte `useCallback` (zie NS.E3.S4 voor de rescript API, die hier aangeroepen wordt). De volledige implementatie staat in NS.E5.S1.
|
||||
|
||||
### Done criteria
|
||||
- Typen "patiënt niet verschenen" → AI antwoordt → nudge 1 verschijnt in chat
|
||||
- Klikken `[Ja]` op nudge 1 → cancel API aangeroepen → nudge 2 verschijnt
|
||||
- Klikken `[Ja]` op nudge 2 → rescript API aangeroepen → artifact opent (NS.E4)
|
||||
- Klikken `[Nee]` op nudge 1 → `dismissSuggestion` → nudge verdwijnt, geen verdere actie
|
||||
- `noShowFlow.step` doorloopt correct: `idle` → `waiting_cancel` → `waiting_brief` → `brief_open`
|
||||
|
||||
---
|
||||
|
||||
## Validatie na NS.E2
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
**Visuele check:**
|
||||
1. Typ `"patiënt niet verschenen"` in de chat
|
||||
2. Wacht op AI response
|
||||
3. Verwacht: nudge bubble met "Ik zie een declarabel consult..." en `[Ja, annuleer]` / `[Nee]` knoppen
|
||||
4. Browser console toont: `[ChatPanel] Nudge suggestions (chat-based): 1`
|
||||
452
docs/intent/noshow-case/epics/NS-E3-api-routes.md
Normal file
452
docs/intent/noshow-case/epics/NS-E3-api-routes.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# NS.E3 — No-Show API Routes
|
||||
|
||||
**Casus:** Cortex No Show Afhandeling
|
||||
**Epic doel:** Vier backend routes bouwen die de no-show flow ondersteunen — annuleren, context ophalen, brief herschrijven, en accordering.
|
||||
**Geschatte tijd:** ~3 uur
|
||||
**Afhankelijkheden:** NS.E1 (types), NS.E2.S1 (store types voor type safety in aanroepen)
|
||||
|
||||
---
|
||||
|
||||
## Context & aanpak
|
||||
|
||||
Alle routes volgen het bestaande patroon in `app/api/cortex/`:
|
||||
- Auth check via `createClient()` uit `lib/auth/server.ts`
|
||||
- Zod validatie op request body / query params
|
||||
- Nederlandse foutmeldingen in responses
|
||||
- Mock-first: routes checken eerst op bekende mock IDs, dan echte DB
|
||||
|
||||
**Mock strategie:** De demo moet altijd werken zonder echte database-inhoud. Elke route checkt of de input overeenkomt met mock data (vaste IDs/patientIds). Als dat zo is, return een mock response. Zo is de happy path altijd demonstreerbaar.
|
||||
|
||||
---
|
||||
|
||||
## NS.E3.S1 — Mock data aanmaken
|
||||
|
||||
**Nieuw bestand:** `lib/cortex/mock-data/noshow.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Mock data voor de No Show demo flow.
|
||||
* Gebruikt in alle /api/cortex/noshow/* routes voor de happy-path demo.
|
||||
*/
|
||||
|
||||
export const MOCK_NO_SHOW_PATIENT_ID = 'demo-patient-001';
|
||||
|
||||
export const MOCK_NO_SHOW_APPOINTMENT = {
|
||||
id: 'mock-appt-noshow-001',
|
||||
patientId: MOCK_NO_SHOW_PATIENT_ID,
|
||||
date: new Date().toISOString(),
|
||||
type: 'intake_consult',
|
||||
is_billable: true,
|
||||
status: 'scheduled',
|
||||
title: 'Intake Consult',
|
||||
duration_minutes: 60,
|
||||
} as const;
|
||||
|
||||
export const MOCK_CONCEPT_BRIEF = {
|
||||
id: 'mock-brief-noshow-001',
|
||||
patientId: MOCK_NO_SHOW_PATIENT_ID,
|
||||
type: 'huisartsbrief',
|
||||
status: 'concept',
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
content: `Geachte collega,
|
||||
|
||||
Hierbij informeer ik u over de intake van uw patiënt die bij ons in behandeling is gekomen voor ambulante GGZ-zorg.
|
||||
|
||||
Tijdens de intake is uitvoerig stilgestaan bij de hulpvraag. De patiënt heeft aangegeven al langere tijd last te hebben van stemmingsgerelateerde klachten, waarbij slaapproblemen en concentratieproblemen op de voorgrond staan. Er is sprake van een beperkt sociaal netwerk en recente stresserende levensgebeurtenissen.
|
||||
|
||||
Op basis van het gesprek en de afgenomen vragenlijsten lijkt er sprake te zijn van een depressieve stoornis, mogelijk in samenhang met een aanpassingsstoornis. Een nadere diagnostische verdieping is aangewezen.
|
||||
|
||||
Het voorgestelde vervolgtraject bestaat uit wekelijkse individuele gesprekken gericht op stabilisatie, psycho-educatie en het in kaart brengen van de klachten. Ik stel voor om na zes sessies de voortgang te evalueren en u dan nader te informeren.
|
||||
|
||||
Mocht u vragen hebben of aanvullende informatie willen delen, neemt u dan gerust contact op.
|
||||
|
||||
Met vriendelijke groet,
|
||||
|
||||
[Behandelaar naam]
|
||||
GGZ Instelling`,
|
||||
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 dagen geleden
|
||||
} as const;
|
||||
|
||||
/** Helper: check of een patientId de mock demo patient is */
|
||||
export function isMockPatient(patientId: string): boolean {
|
||||
return patientId === MOCK_NO_SHOW_PATIENT_ID;
|
||||
}
|
||||
|
||||
/** Helper: check of een appointmentId het mock appointment is */
|
||||
export function isMockAppointment(appointmentId: string): boolean {
|
||||
return appointmentId === MOCK_NO_SHOW_APPOINTMENT.id;
|
||||
}
|
||||
|
||||
/** Helper: check of een documentId het mock document is */
|
||||
export function isMockDocument(documentId: string): boolean {
|
||||
return documentId === MOCK_CONCEPT_BRIEF.id;
|
||||
}
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- Bestand bestaat en compileert zonder errors
|
||||
- Exports zijn beschikbaar voor import in API routes
|
||||
|
||||
---
|
||||
|
||||
## NS.E3.S2 — Cancel Route
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/cancel/route.ts`
|
||||
|
||||
**Doel:** Annuleer een afspraak en registreer het als no-show.
|
||||
|
||||
```typescript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { isMockAppointment } from '@/lib/cortex/mock-data/noshow';
|
||||
|
||||
const CancelNoShowSchema = z.object({
|
||||
appointmentId: z.string().min(1, 'appointmentId is verplicht'),
|
||||
patientId: z.string().min(1, 'patientId is verplicht'),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Auth check
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Validatie
|
||||
let body: z.infer<typeof CancelNoShowSchema>;
|
||||
try {
|
||||
body = CancelNoShowSchema.parse(await request.json());
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: 'Ongeldige invoer' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { appointmentId, patientId } = body;
|
||||
|
||||
// Mock path — demo flow
|
||||
if (isMockAppointment(appointmentId)) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
appointmentId,
|
||||
newStatus: 'cancelled_no_show',
|
||||
message: 'Afspraak geregistreerd als no show (demo)',
|
||||
});
|
||||
}
|
||||
|
||||
// Productie path — Supabase update
|
||||
// Pas aan naar de juiste tabel in jouw schema (encounters of appointments)
|
||||
const { error } = await supabase
|
||||
.from('encounters')
|
||||
.update({ status: 'cancelled_no_show' })
|
||||
.eq('id', appointmentId)
|
||||
.eq('patient_id', patientId);
|
||||
|
||||
if (error) {
|
||||
console.error('[noshow/cancel] DB error:', error);
|
||||
return NextResponse.json({ error: 'Annuleren mislukt' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
appointmentId,
|
||||
newStatus: 'cancelled_no_show',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- `POST /api/cortex/noshow/cancel` met `{ appointmentId: 'mock-appt-noshow-001', patientId: 'demo-patient-001' }` geeft `{ success: true, newStatus: 'cancelled_no_show' }` terug
|
||||
- Geeft `401` zonder auth
|
||||
- Geeft `400` bij ontbrekende velden
|
||||
|
||||
---
|
||||
|
||||
## NS.E3.S3 — Context Route
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/context/route.ts`
|
||||
|
||||
**Doel:** Check of er openstaande conceptbrieven zijn voor de actieve patiënt.
|
||||
|
||||
```typescript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { isMockPatient, MOCK_CONCEPT_BRIEF } from '@/lib/cortex/mock-data/noshow';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Auth check
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Query param
|
||||
const { searchParams } = new URL(request.url);
|
||||
const patientId = searchParams.get('patientId');
|
||||
|
||||
if (!patientId) {
|
||||
return NextResponse.json({ error: 'patientId is verplicht' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Mock path — demo flow
|
||||
if (isMockPatient(patientId)) {
|
||||
return NextResponse.json({
|
||||
hasConceptBrief: true,
|
||||
document: {
|
||||
id: MOCK_CONCEPT_BRIEF.id,
|
||||
title: MOCK_CONCEPT_BRIEF.title,
|
||||
content: MOCK_CONCEPT_BRIEF.content,
|
||||
type: MOCK_CONCEPT_BRIEF.type,
|
||||
createdAt: MOCK_CONCEPT_BRIEF.createdAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Productie path — Supabase query
|
||||
// Zoek eerste concept huisartsbrief voor deze patiënt
|
||||
const { data, error } = await supabase
|
||||
.from('reports')
|
||||
.select('id, title, structured_data, type, created_at')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'huisartsbrief')
|
||||
.eq('status', 'concept')
|
||||
.is('deleted_at', null)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return NextResponse.json({ hasConceptBrief: false });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
hasConceptBrief: true,
|
||||
document: {
|
||||
id: data.id,
|
||||
title: data.title || 'Huisartsbrief',
|
||||
content: data.structured_data?.content ?? '',
|
||||
type: data.type,
|
||||
createdAt: data.created_at,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- `GET /api/cortex/noshow/context?patientId=demo-patient-001` geeft `{ hasConceptBrief: true, document: { ... } }` terug
|
||||
- `GET /api/cortex/noshow/context?patientId=onbekend-id` geeft `{ hasConceptBrief: false }` terug
|
||||
- Geeft `400` zonder `patientId` param
|
||||
|
||||
---
|
||||
|
||||
## NS.E3.S4 — Rescript Route
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/rescript/route.ts`
|
||||
|
||||
**Doel:** Roep de LLM aan om de huisartsbrief professioneel te herschrijven met de no-show verwerkt.
|
||||
|
||||
```typescript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { isMockDocument } from '@/lib/cortex/mock-data/noshow';
|
||||
|
||||
const RescriptSchema = z.object({
|
||||
documentId: z.string().min(1),
|
||||
originalContent: z.string().min(1, 'Originele inhoud is verplicht'),
|
||||
patientId: z.string().min(1),
|
||||
});
|
||||
|
||||
const RESCRIPT_SYSTEM_PROMPT = `Je bent een klinisch schrijver in een GGZ instelling.
|
||||
Je taak is een bestaande conceptbrief aanpassen zodat de no-show van de patiënt
|
||||
professioneel en contextbewust is verwerkt.
|
||||
|
||||
Regels:
|
||||
- Integreer de no-show in de lopende tekst — voeg het NIET als losse zin achteraan toe
|
||||
- Gebruik formele GGZ-briefstijl: "de patiënt is helaas niet verschenen op het geplande consult"
|
||||
- Bewaar alle bestaande informatie in de brief volledig
|
||||
- Voeg een zin toe over het verzetten van het vervolgcontact
|
||||
- De brief moet leesbaar en coherent blijven
|
||||
- Reageer UITSLUITEND met de herschreven brieftekst — geen uitleg, geen inleiding`;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Auth check
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Validatie
|
||||
let body: z.infer<typeof RescriptSchema>;
|
||||
try {
|
||||
body = RescriptSchema.parse(await request.json());
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Ongeldige invoer' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { documentId, originalContent } = body;
|
||||
|
||||
// LLM aanroepen (zowel voor mock als productie — we willen altijd echte AI output)
|
||||
try {
|
||||
const client = new Anthropic();
|
||||
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 1500,
|
||||
system: RESCRIPT_SYSTEM_PROMPT,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Herschrijf de volgende conceptbrief zodat de no-show van vandaag professioneel is verwerkt:\n\n${originalContent}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rescriptedContent =
|
||||
response.content[0].type === 'text'
|
||||
? response.content[0].text
|
||||
: originalContent; // Fallback naar origineel
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
documentId,
|
||||
rescriptedContent,
|
||||
originalContent, // Meesturen zodat de UI "origineel bekijken" kan tonen
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[noshow/rescript] LLM error:', error);
|
||||
|
||||
// Graceful degradation: geef originele inhoud terug met waarschuwing
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
documentId,
|
||||
rescriptedContent: originalContent, // Origineel als fallback
|
||||
originalContent,
|
||||
warning: 'AI herschrijving mislukt — originele tekst wordt getoond',
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Noot over model:** We gebruiken `claude-haiku-4-5-20251001` (snel + goedkoop) voor de rescript taak. Als de kwaliteit onvoldoende is, schakel over naar `claude-sonnet-4-6`.
|
||||
|
||||
**Noot over fallback:** De route geeft altijd `200` terug — ook bij LLM fout. Het `success: false` veld + `warning` laat de UI zien dat de AI niet werkte, maar de brief is alsnog bewerkbaar (met originele inhoud).
|
||||
|
||||
### Done criteria
|
||||
- `POST /api/cortex/noshow/rescript` met mock document content geeft herschreven tekst terug
|
||||
- De herschreven tekst is langer dan de originele (no-show is toegevoegd)
|
||||
- Bij Anthropic API fout: `{ success: false, rescriptedContent: <origineel>, warning: '...' }`
|
||||
- Response tijd < 10 seconden (Haiku is snel)
|
||||
|
||||
---
|
||||
|
||||
## NS.E3.S5 — Dispatch Route
|
||||
|
||||
**Nieuw bestand:** `app/api/cortex/noshow/dispatch/route.ts`
|
||||
|
||||
**Doel:** Document status naar `ready_for_dispatch` zetten en definitieve inhoud opslaan.
|
||||
|
||||
```typescript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { isMockDocument } from '@/lib/cortex/mock-data/noshow';
|
||||
|
||||
const DispatchSchema = z.object({
|
||||
documentId: z.string().min(1),
|
||||
finalContent: z.string().min(1, 'Definitieve inhoud is verplicht'),
|
||||
});
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
// Auth check
|
||||
const supabase = await createClient();
|
||||
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
||||
if (authError || !user) {
|
||||
return NextResponse.json({ error: 'Niet geautoriseerd' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Validatie
|
||||
let body: z.infer<typeof DispatchSchema>;
|
||||
try {
|
||||
body = DispatchSchema.parse(await request.json());
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Ongeldige invoer' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { documentId, finalContent } = body;
|
||||
|
||||
// Mock path — demo flow
|
||||
if (isMockDocument(documentId)) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
documentId,
|
||||
newStatus: 'ready_for_dispatch',
|
||||
message: 'Brief verzendklaar gemaakt (demo)',
|
||||
});
|
||||
}
|
||||
|
||||
// Productie path — Supabase update
|
||||
const { error } = await supabase
|
||||
.from('reports')
|
||||
.update({
|
||||
status: 'ready_for_dispatch',
|
||||
// Sla de definitieve inhoud op — pas het veld aan naar het juiste kolom in je schema
|
||||
structured_data: { content: finalContent },
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', documentId)
|
||||
.eq('user_id', user.id); // Extra veiligheidscheck — alleen eigen documenten
|
||||
|
||||
if (error) {
|
||||
console.error('[noshow/dispatch] DB error:', error);
|
||||
return NextResponse.json({ error: 'Opslaan mislukt' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
documentId,
|
||||
newStatus: 'ready_for_dispatch',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- `PATCH /api/cortex/noshow/dispatch` met `{ documentId: 'mock-brief-noshow-001', finalContent: '...' }` geeft `{ success: true, newStatus: 'ready_for_dispatch' }` terug
|
||||
- Geeft `401` zonder auth
|
||||
- Geeft `400` bij lege `finalContent`
|
||||
|
||||
---
|
||||
|
||||
## Validatie na NS.E3
|
||||
|
||||
Test alle routes met de browser devtools of een API client:
|
||||
|
||||
```bash
|
||||
# Controleer of de routes bestaan
|
||||
pnpm build
|
||||
```
|
||||
|
||||
**Snelle curl tests (vervang <TOKEN> met een geldig Supabase session token):**
|
||||
|
||||
```bash
|
||||
# Cancel
|
||||
curl -X POST http://localhost:3000/api/cortex/noshow/cancel \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: <supabase-session-cookie>" \
|
||||
-d '{"appointmentId":"mock-appt-noshow-001","patientId":"demo-patient-001"}'
|
||||
|
||||
# Context
|
||||
curl "http://localhost:3000/api/cortex/noshow/context?patientId=demo-patient-001" \
|
||||
-H "Cookie: <supabase-session-cookie>"
|
||||
|
||||
# Dispatch
|
||||
curl -X PATCH http://localhost:3000/api/cortex/noshow/dispatch \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: <supabase-session-cookie>" \
|
||||
-d '{"documentId":"mock-brief-noshow-001","finalContent":"test inhoud"}'
|
||||
```
|
||||
|
||||
Verwacht: alle drie geven `{ success: true }` terug.
|
||||
336
docs/intent/noshow-case/epics/NS-E4-document-artifact.md
Normal file
336
docs/intent/noshow-case/epics/NS-E4-document-artifact.md
Normal file
@@ -0,0 +1,336 @@
|
||||
# NS.E4 — Document Artifact Block
|
||||
|
||||
**Casus:** Cortex No Show Afhandeling
|
||||
**Epic doel:** Een block component bouwen dat de herschreven huisartsbrief toont in het artifact paneel, inclusief accordering.
|
||||
**Geschatte tijd:** ~2 uur
|
||||
**Afhankelijkheden:** NS.E1 (BlockType), NS.E3 (dispatch API)
|
||||
|
||||
---
|
||||
|
||||
## Context & waarschuwingen
|
||||
|
||||
### Bestaande `renderArtifactBlock` heeft een `default` case
|
||||
|
||||
`artifact-container.tsx` heeft op regel 209:
|
||||
```typescript
|
||||
default:
|
||||
return (
|
||||
<div className="p-4 text-slate-500">
|
||||
Onbekend artifact type: {artifact.type}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
TypeScript heeft dit als `default` case, niet als een exhaustive check. Dat betekent: als we `register_no_show` vergeten toe te voegen aan de switch, valt de app niet over — maar toont wel "Onbekend artifact type". Visueel zichtbaar, maar geen compile-time fout.
|
||||
|
||||
### `getArtifactTitle` heeft ook een switch
|
||||
|
||||
De `getArtifactTitle` functie onderaan `artifact-container.tsx` heeft een eigen switch over `BlockType` met een `default: return 'Artifact'`. Ook hier: geen compile-time fout bij ontbrekende case, maar een generieke titel.
|
||||
|
||||
### Geen rich text editor in codebase
|
||||
|
||||
Er bestaat geen TipTap, Quill of Slate in de codebase. `dagnotitie-block.tsx` gebruikt een `<Textarea>` van shadcn/ui. Wij doen hetzelfde — dat is voldoende voor het prototype.
|
||||
|
||||
### BlockPrefillData is een open interface
|
||||
|
||||
`BlockPrefillData extends ChatEntities` — dat is een open interface. We kunnen er extra velden aan toevoegen voor no-show specifieke data (`documentId`, `originalContent`) zonder de store te wijzigen. De cast `as NoShowDocumentPrefill` in de container zorgt voor type safety binnen het block.
|
||||
|
||||
---
|
||||
|
||||
## NS.E4.S1 — NoShowDocumentBlock component
|
||||
|
||||
**Nieuw bestand:** `components/cortex/blocks/noshow-document-block.tsx`
|
||||
|
||||
### Props interface
|
||||
|
||||
```typescript
|
||||
interface NoShowDocumentPrefill {
|
||||
documentId: string;
|
||||
content: string; // Herschreven inhoud van LLM
|
||||
title: string;
|
||||
originalContent?: string; // Originele inhoud voor "bekijk origineel"
|
||||
rescriptWarning?: string; // Tonen als LLM fout had
|
||||
}
|
||||
```
|
||||
|
||||
### Component implementatie
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { FileText, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCortexStore } from '@/stores/cortex-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface NoShowDocumentPrefill {
|
||||
documentId: string;
|
||||
content: string;
|
||||
title: string;
|
||||
originalContent?: string;
|
||||
rescriptWarning?: string;
|
||||
}
|
||||
|
||||
interface NoShowDocumentBlockProps {
|
||||
prefill: NoShowDocumentPrefill;
|
||||
}
|
||||
|
||||
export function NoShowDocumentBlock({ prefill }: NoShowDocumentBlockProps) {
|
||||
const [content, setContent] = useState(prefill.content);
|
||||
const [showOriginal, setShowOriginal] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
|
||||
const addChatMessage = useCortexStore((s) => s.addChatMessage);
|
||||
const setNoShowStep = useCortexStore((s) => s.setNoShowStep);
|
||||
|
||||
const handleDispatch = async () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/cortex/noshow/dispatch', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentId: prefill.documentId,
|
||||
finalContent: content,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Dispatch mislukt');
|
||||
|
||||
setIsDone(true);
|
||||
setNoShowStep('done');
|
||||
addChatMessage({
|
||||
type: 'assistant',
|
||||
content: 'Brief is verzendklaar gemaakt. De no-show afhandeling is compleet.',
|
||||
});
|
||||
} catch {
|
||||
addChatMessage({
|
||||
type: 'error',
|
||||
content: 'Opslaan mislukt. Probeer het opnieuw.',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<FileText className="w-5 h-5 text-slate-600" />
|
||||
<h2 className="text-lg font-semibold text-slate-800">
|
||||
{prefill.title || 'Huisartsbrief'}
|
||||
</h2>
|
||||
<span className="ml-auto text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded font-medium">
|
||||
Aangepast door Cortex
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* LLM waarschuwing (bij fallback naar origineel) */}
|
||||
{prefill.rescriptWarning && (
|
||||
<div className="mb-3 p-3 bg-amber-50 border border-amber-200 rounded text-sm text-amber-800">
|
||||
⚠️ {prefill.rescriptWarning}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bewerkbare tekstinhoud */}
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
disabled={isDone}
|
||||
className={cn(
|
||||
'min-h-[320px] font-mono text-sm resize-y',
|
||||
isDone && 'opacity-60 cursor-not-allowed'
|
||||
)}
|
||||
placeholder="Brief inhoud..."
|
||||
/>
|
||||
|
||||
{/* Origineel bekijken — collapsible */}
|
||||
{prefill.originalContent && (
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOriginal(!showOriginal)}
|
||||
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-700 transition-colors"
|
||||
>
|
||||
{showOriginal ? (
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
)}
|
||||
{showOriginal ? 'Origineel verbergen' : 'Originele brief bekijken'}
|
||||
</button>
|
||||
|
||||
{showOriginal && (
|
||||
<div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-500 font-mono whitespace-pre-wrap">
|
||||
{prefill.originalContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer met Akkoord knop */}
|
||||
<div className="mt-4 flex items-center justify-between border-t border-slate-200 pt-4">
|
||||
<span className="text-xs text-slate-400">
|
||||
{isDone ? '✓ Brief is verzendklaar' : 'Controleer en pas aan indien nodig'}
|
||||
</span>
|
||||
|
||||
{isDone ? (
|
||||
<span className="text-sm font-medium text-green-600 flex items-center gap-1">
|
||||
✓ Verzendklaar
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleDispatch}
|
||||
disabled={isSubmitting || content.trim().length === 0}
|
||||
className="bg-slate-800 hover:bg-slate-700 text-white"
|
||||
>
|
||||
{isSubmitting ? 'Opslaan...' : 'Akkoord & Verzendklaar →'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- Component rendert de herschreven inhoud in een bewerkbare textarea
|
||||
- "Originele brief bekijken" toggle werkt (collapsible)
|
||||
- Textarea is editeerbaar — aanpassingen blijven behouden
|
||||
- "Akkoord & Verzendklaar" knop roept dispatch API aan
|
||||
- Na succesvolle dispatch: knop verdwijnt, "✓ Verzendklaar" verschijnt, chat ontvangt bevestiging
|
||||
- Bij leeg content: knop is disabled
|
||||
- Bij API fout: error message in chat, knop hergebruikbaar
|
||||
|
||||
---
|
||||
|
||||
## NS.E4.S2 — ArtifactContainer uitbreiden
|
||||
|
||||
**Bestand:** `components/cortex/artifacts/artifact-container.tsx`
|
||||
|
||||
### Wijziging 1 — Import toevoegen
|
||||
|
||||
Voeg toe na de bestaande block imports:
|
||||
```typescript
|
||||
import { NoShowDocumentBlock } from '../blocks/noshow-document-block';
|
||||
```
|
||||
|
||||
### Wijziging 2 — Case toevoegen in `renderArtifactBlock`
|
||||
|
||||
Voeg toe in de switch, vóór de `default` case:
|
||||
|
||||
```typescript
|
||||
case 'register_no_show': {
|
||||
const nsPrefill = artifact.prefill as {
|
||||
documentId: string;
|
||||
content: string;
|
||||
title: string;
|
||||
originalContent?: string;
|
||||
rescriptWarning?: string;
|
||||
};
|
||||
return (
|
||||
<NoShowDocumentBlock
|
||||
key={artifact.id}
|
||||
prefill={nsPrefill}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Wijziging 3 — Case toevoegen in `getArtifactTitle`
|
||||
|
||||
Voeg toe in de switch, vóór de `default` case:
|
||||
|
||||
```typescript
|
||||
case 'register_no_show':
|
||||
return prefill?.title
|
||||
? `Brief — ${prefill.title}`
|
||||
: 'Huisartsbrief';
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- `openArtifact({ type: 'register_no_show', title: 'Huisartsbrief n.a.v. intake', prefill: { documentId: '...', content: '...', title: '...' } })` opent het `NoShowDocumentBlock`
|
||||
- Tab titel in multi-artifact view toont "Brief — Huisartsbrief n.a.v. intake"
|
||||
- Geen TypeScript errors in `artifact-container.tsx`
|
||||
- Geen "Onbekend artifact type" fallback zichtbaar
|
||||
|
||||
---
|
||||
|
||||
## NS.E4.S3 — `openArtifact` aanroep in `handleNoShowRescriptStep`
|
||||
|
||||
**Bestand:** `components/cortex/chat/chat-panel.tsx`
|
||||
|
||||
Dit is de koppeling tussen de rescript API (NS.E3.S4) en het artifact (NS.E4.S1).
|
||||
|
||||
Voeg `handleNoShowRescriptStep` toe als `useCallback` in `ChatPanel`:
|
||||
|
||||
```typescript
|
||||
const handleNoShowRescriptStep = useCallback(async () => {
|
||||
const { noShowFlow } = useCortexStore.getState();
|
||||
setNoShowStep('brief_open');
|
||||
|
||||
addChatMessage({ type: 'assistant', content: 'Huisartsbrief aanpassen...' });
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/cortex/noshow/rescript', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentId: noShowFlow.documentId ?? 'mock-brief-noshow-001',
|
||||
originalContent: noShowFlow.originalContent ?? '',
|
||||
patientId: activePatient?.id ?? 'demo-patient-001',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
openArtifact({
|
||||
type: 'register_no_show',
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
prefill: {
|
||||
documentId: result.documentId,
|
||||
content: result.rescriptedContent,
|
||||
title: 'Huisartsbrief n.a.v. intake',
|
||||
originalContent: result.originalContent,
|
||||
rescriptWarning: result.warning, // undefined als LLM succesvol was
|
||||
},
|
||||
});
|
||||
|
||||
addChatMessage({
|
||||
type: 'assistant',
|
||||
content: 'Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent.',
|
||||
});
|
||||
} catch {
|
||||
setNoShowStep('waiting_brief'); // Terug naar vorige stap
|
||||
addChatMessage({
|
||||
type: 'error',
|
||||
content: 'Herschrijven mislukt. Probeer het opnieuw.',
|
||||
});
|
||||
}
|
||||
}, [activePatient, setNoShowStep, addChatMessage, openArtifact]);
|
||||
```
|
||||
|
||||
**Let op:** `useCortexStore.getState()` wordt hier gebruikt (niet de hook) omdat we binnen een `useCallback` zitten en de state op het moment van uitvoering nodig hebben, niet de state van de laatste render.
|
||||
|
||||
### Done criteria
|
||||
- Klikken op `[Ja, pas brief aan]` → rescript API aangeroepen → artifact paneel opent met `NoShowDocumentBlock`
|
||||
- Brief inhoud is de AI-herschreven versie (of origineel bij LLM fout)
|
||||
- Als `rescriptWarning` aanwezig is: gele waarschuwingsbalk zichtbaar in het block
|
||||
|
||||
---
|
||||
|
||||
## Validatie na NS.E4
|
||||
|
||||
**Visuele test:**
|
||||
1. Doorloop de flow tot en met nudge 2 acceptatie
|
||||
2. Verwacht: artifact paneel schuift open met `NoShowDocumentBlock`
|
||||
3. Inhoud is de AI-herschreven brief
|
||||
4. Textarea is editeerbaar
|
||||
5. Klik `[Akkoord & Verzendklaar]`
|
||||
6. Verwacht: "✓ Verzendklaar" in het block + bevestiging in chat
|
||||
7. `pnpm build` — geen errors
|
||||
299
docs/intent/noshow-case/epics/NS-E5-integration.md
Normal file
299
docs/intent/noshow-case/epics/NS-E5-integration.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# NS.E5 — Integration & Demo Polish
|
||||
|
||||
**Casus:** Cortex No Show Afhandeling
|
||||
**Epic doel:** De volledige end-to-end flow werkend krijgen, edge cases afvangen, en de demo valideren aan de succescriteria uit het PRD.
|
||||
**Geschatte tijd:** ~2 uur
|
||||
**Afhankelijkheden:** NS.E1 t/m NS.E4 volledig afgerond
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Na NS.E1–NS.E4 zijn alle individuele onderdelen gebouwd:
|
||||
- Intent wordt herkend (E1)
|
||||
- Nudges triggeren en lopen sequentieel (E2)
|
||||
- API routes werken (E3)
|
||||
- Artifact component toont de brief (E4)
|
||||
|
||||
Dit epic zorgt dat alles correct samenwerkt, fouten netjes worden afgevangen, en de demo vloeiend loopt.
|
||||
|
||||
---
|
||||
|
||||
## NS.E5.S1 — Volledige `chat-panel.tsx` integratie valideren
|
||||
|
||||
**Bestand:** `components/cortex/chat/chat-panel.tsx`
|
||||
|
||||
### Checklist: zijn alle functies aanwezig en correct verbonden?
|
||||
|
||||
Na NS.E2 en NS.E4 moeten de volgende functies bestaan in `ChatPanel`:
|
||||
|
||||
| Functie | Aangemaakt in | Doet |
|
||||
|---|---|---|
|
||||
| `handleNoShowCancelStep` | NS.E2.S3 | Cancel API → context check → nudge 2 of done |
|
||||
| `handleNoShowRescriptStep` | NS.E4.S3 | Rescript API → openArtifact |
|
||||
| `handleAcceptNudge` (uitgebreid) | NS.E2.S3 | Brancht op `trigger.intent` en `suggestion.rationale` |
|
||||
|
||||
### Verbinding `handleAcceptNudge` → `handleNoShowRescriptStep`
|
||||
|
||||
Controleer dat de stap-5 check correct is geïmplementeerd:
|
||||
|
||||
```typescript
|
||||
const handleAcceptNudge = useCallback(async (
|
||||
suggestionId: string,
|
||||
suggestion: ChatMessageType['nudge']
|
||||
) => {
|
||||
acceptSuggestion(suggestionId);
|
||||
if (!suggestion) return;
|
||||
|
||||
// Stap 2→3: declarabiliteitscheck geaccepteerd → annuleer + check brief
|
||||
if (suggestion.trigger.intent === 'register_no_show') {
|
||||
await handleNoShowCancelStep(suggestion);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stap 4→5: brief-check geaccepteerd → herschrijf brief
|
||||
if (suggestion.suggestion.rationale === 'noshow-brief-check') {
|
||||
await handleNoShowRescriptStep();
|
||||
return;
|
||||
}
|
||||
|
||||
// Generieke flow voor alle andere nudges
|
||||
const artifact = routeIntentToArtifact(
|
||||
suggestion.suggestion.intent,
|
||||
suggestion.suggestion.entities,
|
||||
0.9
|
||||
);
|
||||
if (artifact) {
|
||||
openArtifact({ type: artifact.type, prefill: artifact.prefill, title: artifact.title });
|
||||
}
|
||||
}, [
|
||||
acceptSuggestion,
|
||||
openArtifact,
|
||||
handleNoShowCancelStep,
|
||||
handleNoShowRescriptStep,
|
||||
]);
|
||||
```
|
||||
|
||||
**Belangrijk:** `handleNoShowCancelStep` en `handleNoShowRescriptStep` moeten in de `useCallback` dependency array staan. Als ze er niet in staan, kan je stale closures krijgen.
|
||||
|
||||
### `onSend` handler: no-show nudge trigger
|
||||
|
||||
Controleer dat de `onDone` callback de no-show nudge triggert. Dit blok moet aanwezig zijn in de `onDone`:
|
||||
|
||||
```typescript
|
||||
// No-show nudge trigger na register_no_show classificatie
|
||||
if (parsed.action?.intent === 'register_no_show' && isFeatureEnabled('CORTEX_NUDGE')) {
|
||||
const suggestions = evaluateNudge({
|
||||
intent: 'register_no_show',
|
||||
actionId: crypto.randomUUID(),
|
||||
entities: parsed.action.entities,
|
||||
content: message,
|
||||
});
|
||||
|
||||
suggestions.forEach((suggestion) => {
|
||||
addChatMessage({
|
||||
type: 'nudge',
|
||||
content: suggestion.suggestion.message,
|
||||
nudge: suggestion,
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- Volledige flow doorloopbaar zonder errors in de browser console
|
||||
- `[ChatPanel]` log berichten verschijnen op de juiste momenten
|
||||
- Geen `TypeError` of `undefined is not a function` errors
|
||||
|
||||
---
|
||||
|
||||
## NS.E5.S2 — Loading states en dubbele submits voorkomen
|
||||
|
||||
**Bestand:** `components/cortex/chat/chat-panel.tsx` + `stores/cortex-store.ts`
|
||||
|
||||
### Probleem
|
||||
|
||||
Tussen de API calls (cancel → context → rescript) zit latentie. Als de gebruiker snel klikt, kunnen er dubbele calls ontstaan. Ook moet de UI duidelijk maken dat er iets "bezig" is.
|
||||
|
||||
### Oplossing A — `isNoShowProcessing` flag in store
|
||||
|
||||
Voeg toe aan store (in `CortexStore` interface, `initialState`, en actions):
|
||||
|
||||
```typescript
|
||||
// In interface:
|
||||
isNoShowProcessing: boolean;
|
||||
|
||||
// In initialState:
|
||||
isNoShowProcessing: false,
|
||||
|
||||
// Actions:
|
||||
setNoShowProcessing: (processing: boolean) => void;
|
||||
```
|
||||
|
||||
Implementatie:
|
||||
```typescript
|
||||
setNoShowProcessing: (processing) =>
|
||||
set({ isNoShowProcessing: processing }, false, 'setNoShowProcessing'),
|
||||
```
|
||||
|
||||
### Oplossing B — Gebruik in `handleNoShowCancelStep` en `handleNoShowRescriptStep`
|
||||
|
||||
Wrap elke async functie:
|
||||
|
||||
```typescript
|
||||
const handleNoShowCancelStep = useCallback(async (...) => {
|
||||
const { isNoShowProcessing } = useCortexStore.getState();
|
||||
if (isNoShowProcessing) return; // Dubbele klik preventie
|
||||
|
||||
setNoShowProcessing(true);
|
||||
try {
|
||||
// ... bestaande logica
|
||||
} finally {
|
||||
setNoShowProcessing(false);
|
||||
}
|
||||
}, [..., setNoShowProcessing]);
|
||||
```
|
||||
|
||||
### Oplossing C — Nudge knop disablen tijdens processing
|
||||
|
||||
In `NudgeChatMessage` is de accept/dismiss knop al aanwezig. We moeten de accept knop disablen tijdens `isNoShowProcessing`:
|
||||
|
||||
**Bestand:** `components/cortex/chat/nudge-chat-message.tsx`
|
||||
|
||||
Voeg toe:
|
||||
```typescript
|
||||
const isNoShowProcessing = useCortexStore((s) => s.isNoShowProcessing);
|
||||
```
|
||||
|
||||
Voeg `disabled={isNoShowProcessing}` toe aan de accept-knop als de nudge een no-show nudge is (`suggestion.trigger.intent === 'register_no_show'` of `suggestion.suggestion.rationale === 'noshow-brief-check'`).
|
||||
|
||||
### Done criteria
|
||||
- Snel twee keer klikken op `[Ja, annuleer]` triggert de API maar één keer
|
||||
- Knop is visueel disabled tijdens verwerking
|
||||
- Na voltooiing is de knop weer actief (relevant voor de "Nee" knop)
|
||||
|
||||
---
|
||||
|
||||
## NS.E5.S3 — Cleanup bij unmount
|
||||
|
||||
**Bestand:** `components/cortex/chat/chat-panel.tsx`
|
||||
|
||||
Als de gebruiker weg navigeert tijdens een lopende no-show flow, moet de state worden opgeruimd.
|
||||
|
||||
Voeg toe in `ChatPanel`:
|
||||
|
||||
```typescript
|
||||
const resetNoShowFlow = useCortexStore((s) => s.resetNoShowFlow);
|
||||
|
||||
// Cleanup bij unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Reset no-show flow als component unmount tijdens actieve flow
|
||||
const { noShowFlow } = useCortexStore.getState();
|
||||
if (noShowFlow.step !== 'idle' && noShowFlow.step !== 'done') {
|
||||
resetNoShowFlow();
|
||||
}
|
||||
};
|
||||
}, [resetNoShowFlow]);
|
||||
```
|
||||
|
||||
### Done criteria
|
||||
- Navigeren weg van het dashboard tijdens een actieve no-show flow reset de state
|
||||
- Bij terugkeren: flow begint opnieuw (geen "stuck" state)
|
||||
|
||||
---
|
||||
|
||||
## NS.E5.S4 — Demo Validatie Checklist
|
||||
|
||||
Doorloop de volledige flow handmatig en vink elk punt af:
|
||||
|
||||
### Classificatie
|
||||
- [ ] Input `"patiënt is niet verschenen"` → `register_no_show` intent herkend (zichtbaar in console)
|
||||
- [ ] Input `"no show vandaag"` → `register_no_show` intent herkend
|
||||
- [ ] Input `"cliënt was er niet"` → AI classificeert als `register_no_show`
|
||||
- [ ] Input `"patiënt niet verschenen volgende week"` → escaleert naar AI (relative_time)
|
||||
|
||||
### Nudge 1 — Declarabiliteitscheck
|
||||
- [ ] Na no-show input: nudge bubble verschijnt met declarabiliteits-waarschuwing
|
||||
- [ ] Nudge heeft prioriteit badge `high` (rode kleur)
|
||||
- [ ] `[Nee]` klik: nudge verdwijnt, geen verdere actie, flow eindigt
|
||||
- [ ] `[Ja, annuleer]` klik: loading state actief (knop disabled)
|
||||
|
||||
### Stap 3 — Annulering
|
||||
- [ ] Cancel API aangeroepen: `POST /api/cortex/noshow/cancel` → 200 response
|
||||
- [ ] Context API aangeroepen: `GET /api/cortex/noshow/context` → `hasConceptBrief: true`
|
||||
- [ ] Chat toont "Bezig met annuleren..." tijdens verwerking
|
||||
- [ ] Na voltooiing: nudge 2 verschijnt met brief-vraag
|
||||
|
||||
### Nudge 2 — Brief check
|
||||
- [ ] Nudge 2 toont correcte message over conceptbrief
|
||||
- [ ] `[Nee]` klik: nudge verdwijnt, chat bevestigt annulering
|
||||
- [ ] `[Ja, pas brief aan]` klik: rescript API aangeroepen
|
||||
|
||||
### Stap 4 — Rescript
|
||||
- [ ] Rescript API aangeroepen: `POST /api/cortex/noshow/rescript` → herschreven inhoud
|
||||
- [ ] Artifact paneel opent met `NoShowDocumentBlock`
|
||||
- [ ] Chat toont "Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent."
|
||||
- [ ] Herschreven brief is NIET alleen de originele tekst met "no show" erachter geplakt
|
||||
|
||||
### Stap 5 — Accordering
|
||||
- [ ] Textarea is bewerkbaar — gebruiker kan aanpassingen maken
|
||||
- [ ] "Originele brief bekijken" toggle werkt
|
||||
- [ ] `[Akkoord & Verzendklaar]` roept dispatch API aan
|
||||
- [ ] Na dispatch: knop verandert naar "✓ Verzendklaar"
|
||||
- [ ] Chat ontvangt bevestigingsbericht
|
||||
|
||||
### Technische criteria
|
||||
- [ ] Volledige flow doorloopbaar in < 30 seconden (excl. LLM latency ~3-5s)
|
||||
- [ ] Geen schermwissels nodig gedurende de hele flow
|
||||
- [ ] `pnpm build` slaagt zonder errors
|
||||
- [ ] `pnpm lint` slaagt zonder errors
|
||||
- [ ] Geen rode console errors in de browser
|
||||
|
||||
### Edge cases
|
||||
- [ ] Offline/netwerk fout tijdens cancel → error message in chat, flow herstelbaar
|
||||
- [ ] LLM fout in rescript → originele brief getoond + gele waarschuwingsbalk
|
||||
- [ ] Dubbele klik op `[Ja, annuleer]` → slechts één API call
|
||||
|
||||
---
|
||||
|
||||
## NS.E5.S5 — Commit strategie
|
||||
|
||||
Commitvolgorde die logisch werkt voor code review:
|
||||
|
||||
```
|
||||
feat(cortex): NS.E1 — register_no_show intent type + reflex patterns
|
||||
feat(cortex): NS.E2.S1 — no-show flow state in cortex store
|
||||
feat(cortex): NS.E2.S2 — nudge rule: declarabiliteitscheck
|
||||
feat(cortex): NS.E2.S3 — chat panel: no-show nudge trigger + handlers
|
||||
feat(cortex): NS.E3 — no-show API routes (cancel, context, rescript, dispatch)
|
||||
feat(cortex): NS.E4.S1 — NoShowDocumentBlock component
|
||||
feat(cortex): NS.E4.S2 — artifact-container: register_no_show case
|
||||
feat(cortex): NS.E4.S3 — chat panel: rescript step + artifact koppeling
|
||||
feat(cortex): NS.E5 — loading states, cleanup, demo polish
|
||||
```
|
||||
|
||||
Elke commit moet `pnpm build` laten slagen.
|
||||
|
||||
---
|
||||
|
||||
## Overzicht gewijzigde bestanden (totaal)
|
||||
|
||||
| Bestand | Wijziging |
|
||||
|---|---|
|
||||
| `lib/cortex/types.ts` | `register_no_show` in union + BLOCK_CONFIGS |
|
||||
| `lib/cortex/reflex-classifier.ts` | Patronen voor register_no_show |
|
||||
| `app/api/cortex/chat/route.ts` | System prompt uitgebreid |
|
||||
| `lib/cortex/nudge.ts` | Nudge rule 1 toegevoegd |
|
||||
| `stores/cortex-store.ts` | NoShowFlowState + actions + isNoShowProcessing |
|
||||
| `lib/cortex/mock-data/noshow.ts` | **Nieuw** |
|
||||
| `app/api/cortex/noshow/cancel/route.ts` | **Nieuw** |
|
||||
| `app/api/cortex/noshow/context/route.ts` | **Nieuw** |
|
||||
| `app/api/cortex/noshow/rescript/route.ts` | **Nieuw** |
|
||||
| `app/api/cortex/noshow/dispatch/route.ts` | **Nieuw** |
|
||||
| `components/cortex/blocks/noshow-document-block.tsx` | **Nieuw** |
|
||||
| `components/cortex/artifacts/artifact-container.tsx` | Case + import + getArtifactTitle |
|
||||
| `components/cortex/chat/chat-panel.tsx` | Nudge trigger + handlers + cleanup |
|
||||
| `components/cortex/chat/nudge-chat-message.tsx` | isNoShowProcessing check |
|
||||
|
||||
**Totaal: 14 bestanden** — 5 nieuw, 9 aangepast
|
||||
68
docs/intent/noshow-case/fo-cortex-casus-noshow.md
Normal file
68
docs/intent/noshow-case/fo-cortex-casus-noshow.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Functioneel Ontwerp (FO) - Cortex Use Case: No Show Afhandeling
|
||||
|
||||
**Project:** Cortex Intent System (Mini-EPD Prototype)
|
||||
**Focus:** Casus 2 (Journaliseren Consult - No Show)
|
||||
**Datum:** Vandaag
|
||||
**Link:** [PRD - Casus No Show](./prd-cortex-casus-noshow.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Gedetailleerde User Flow (Stap-voor-stap)
|
||||
|
||||
De interactie verloopt volledig conversationeel via het Cortex Chat venster en de stateful UI (Artifacts).
|
||||
|
||||
* **Stap 1: Registratie**
|
||||
* **User:** Opent (of is in) het dossier van de patiënt en typt in de inputbalk: *"Patiënt is niet verschenen - no show."*
|
||||
* **Systeem:** De NLP checkt intents en ziet `register_no_show`.
|
||||
* **Stap 2: Context Analyse & Waarschuwing (Nudge)**
|
||||
* **Systeem:** Achterhaalt de actieve afspraak. Rule engine check: is `is_billable` waar? Ja.
|
||||
* **Cortex AI Bericht:** *"Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als 'No Show'?"*
|
||||
* **UI Component:** Toont knoppen (Nudge Suggestions) eronder: `[Ja, annuleer]` | `[Nee, laat staan]`
|
||||
* **Stap 3: Activering Brief Check**
|
||||
* **User:** Klikt op `[Ja, annuleer]`.
|
||||
* **Systeem:** Verstuurt signaal naar backend. Wijzigt afspraak status naar 'cancelled_no_show'. Systeem zoekt naar openstaande taken voor deze patiënt en vindt Concept Huisartsbrief.
|
||||
* **Cortex AI Bericht:** *"Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?"*
|
||||
* **UI Component:** Toont knoppen: `[Ja, pas brief aan]` | `[Nee]`
|
||||
* **Stap 4: AI Generatie & Beoordeling (Artifact)**
|
||||
* **User:** Klikt op `[Ja, pas brief aan]`.
|
||||
* **Systeem:** Roept LLM API aan in de backend met de originele tekst + "Integreer dat de afspraak vandaag gepauzeerd is wegens no show."
|
||||
* **Cortex AI Bericht:** *"Huisartsbrief is aangepast. Kijk of je hiermee akkoord bent."*
|
||||
* **UI Component:** Triggert `openArtifact` en het rechtervenster met de bewerkte brief schuift in beeld.
|
||||
* **Stap 5: Accordering**
|
||||
* **User:** Leest de brief in het Artifact venster, eventuele handmatige typ-aanpassingen, en klikt op `[Akkoord & Verzendklaar]`.
|
||||
* **Systeem:** Slaat document/state op (status naar `ready_for_dispatch`). Toont kleine succesmelding in chat.
|
||||
|
||||
---
|
||||
|
||||
## 2. Componenten Matrix (Hergebruik vs. Nieuw)
|
||||
|
||||
Hier is in kaart gebracht welke React/Next.js store- en UI componenten we nodig hebben en wat hun huidige status is in de codebase.
|
||||
|
||||
### 2.1 UI Componenten (Frontend)
|
||||
|
||||
| Component | Status | Toelichting | Locatie verwachting |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Cortex Chat Input** | ♻️ Hergebruik | De bestaande inputbalk / command center waar de "no show" in getypt wordt. Omdat intent-herkenning centraal op de tekst werkt, hebben we geen nieuwe velden nodig. | `components/cortex/chat/...` of `command-center/...` |
|
||||
| **Cortex Chat Message** | ♻️ Hergebruik | De 'bubbels' waarin Cortex reageert. Deze renderen al responses. | `components/cortex/chat/...` |
|
||||
| **Nudge Suggestion Card** | 🛠️ Aanpassen | Deze tonen de UI knoppen (zoals `[Ja, annuleren]`). We moeten in de component bevestigen dat we via een klik een onzichtbaar of command-response kunnen terug sturen naar de orchestrator voor Stap 3. | `components/cortex/chat/nudge-suggestion.tsx` (indien apart) |
|
||||
| **Document Artifact Window**| ♻️ Hergebruik | We hebben de Artifact functionaliteit open staan in de store (`openArtifacts`). In dat artefact paneel kunnen we document content afbeelden. | `components/cortex/artifacts/...` |
|
||||
| **Rich Text Editor** | ♻️ Hergebruik | Binnenin het Artifact tonen we de Huisartsbrief via de editor component, zodat de arts nog kan bewerken. | `components/cortex/rich-text-editor.tsx` |
|
||||
| **Artifact Action Bar** | ✨ Nieuw | Een component (of extra 'footer') aan het Artifact component, met een knop `[Akkoord & Verzendklaar]`. Nu is het vaak readonly. | `components/cortex/artifacts/` of als los element. |
|
||||
|
||||
### 2.2 Logica & Backend Componenten
|
||||
|
||||
| Component / Module | Status | Toelichting | Huidige locatie |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Intent Classifier** | 🛠️ Aanpassen | Toevoegen van het `register_no_show` label in de mapping. | `lib/cortex/intent-labels.ts` & `lib/cortex/types.ts` |
|
||||
| **Nudge Protocol Rules** | 🛠️ Aanpassen | Hier integreren we The "Declarabele Check" en "Brief Alert". We programmeren een regel af in `nudge.ts` die luistert naar de no_show. | `lib/cortex/nudge.ts` |
|
||||
| **Cortex Store** | ♻️ Hergebruik | Functies zoals `addSuggestion` en `openArtifact` zijn klaar voor gebruik! Hier is geen redesign van state voor nodig. | `stores/cortex-store.ts` |
|
||||
| **No-Show Orchestrator/API** | ✨ Nieuw | Specifieke API call en/of uitbreiding in AI logica die de database context opzoekt, LLM (via Langchain) aanstuurt en de brief herschrijft. | `app/api/cortex/...` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementatie Fasering (FO -> Bouw)
|
||||
|
||||
1. **Interface Prep:** `register_no_show` in alle TypeScript Types & configuraties toevoegen. De Nudge Rule definiëren (*Fase 1 is statisch zonder database checks om UI op te pikken*).
|
||||
2. **Acties (Nudges):** Knoppen reageren: we zorgen dat het klikken op de Chat Suggestie door praat met de store, zodat het volgende venster / bericht ('Zal ik conceptbrief openen?') getriggerd kan worden.
|
||||
3. **Artifact Opening:** `rich-text-editor.tsx` integreren in een Artifact layout, openen doormiddel van `openArtifact`. Knoppen 'Akkoord' toevoegen (console logs voorlopig).
|
||||
4. **Backend (API & DB):** Bouw van server-side checks. Echt data mocken en een route voor de LLM brief-rescript uitschrijven.
|
||||
54
docs/intent/noshow-case/prd-cortex-casus-noshow.md
Normal file
54
docs/intent/noshow-case/prd-cortex-casus-noshow.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Product Requirements Document (PRD) - Cortex Use Case: No Show Afhandeling
|
||||
|
||||
**Project:** Cortex Intent System (Mini-EPD Prototype)
|
||||
**Focus:** Casus 2 (Journaliseren Consult - No Show)
|
||||
**Datum:** Vandaag
|
||||
**Auteur:** Colin Lit / Antigravity
|
||||
|
||||
---
|
||||
|
||||
## 1. Context & Doelstelling
|
||||
|
||||
**Probleem:**
|
||||
In het huidige EPD-systeem vergt een relatief simpele gebeurtenis zoals een "No Show" (patiënt verschijnt niet op afspraak) veel handmatige, administratieve handelingen voor de regiebehandelaar (RB). De RB moet notities maken, de agenda handmatig updaten (vaak belangrijk in verband met inkoopvoorwaarden en declarabiliteit), een nieuwe afspraak inplannen, en eventueel openstaande correspondentie (zoals een huisartsbrief over de intake) aanpassen.
|
||||
|
||||
**Doelstelling:**
|
||||
Cortex (in de hoedanigheid van AI-assistent of co-piloot) neemt de orkestratie van deze taken over op basis van één simpele verslaglegging ("cliënt is niet verschenen"). Cortex bewaakt de bedrijfsregels (declarabiliteit) en neemt proactief openstaande taken (brieven) mee in de flow.
|
||||
|
||||
---
|
||||
|
||||
## 2. Gebruikers en Behoeften
|
||||
|
||||
**Primaire Gebruiker:** Regiebehandelaar (RB) / Psycholoog / Arts
|
||||
**Behoefte:** Focus op de zorg, niet op het navigeren door schermen. Verlaging van cognitieve belasting. Voorkomen van administratieve fouten (zoals onterecht declareren van een no-show).
|
||||
|
||||
## 3. User Stories
|
||||
|
||||
1. **Intentie Herkenning:** Als RB wil ik in mijn journaalveld (of tegen Cortex) kunnen typen/inspreken dat de cliënt een "no show" was, zodat Cortex direct mijn intentie herkent zonder dat ik specifieke knoppen hoef te zoeken.
|
||||
2. **Business Rule Waarschuwing (Nudge):** Als RB wil ik proactief door Cortex erop gewezen worden wanneer de gemiste afspraak nog als 'declarabel' in de agenda staat, zodat ik kan voldoen aan inkoopvoorwaarden.
|
||||
3. **1-Klik Actie:** Als RB wil ik met één goedkeuringsklik in de chat de afspraak annuleren als 'no show' en direct het inplannen van een nieuwe afspraak initiëren.
|
||||
4. **Contextueel Meedenken (Brieven):** Als RB wil ik dat Cortex weet welke openstaande documenten er liggen (bijv. een concept huisartsbrief) en mij aanbiedt deze automatisch de updaten met de informatie dat de patiënt niet is komen opdagen, zodat ik niet dezelfde informatie op twee plekken hoef in te typen.
|
||||
5. **Review en Verzending:** Als RB wil ik de door AI aangepaste huisartsbrief direct kunnen lezen in een duidelijke weergave (Artifact) en met één klik accorderen en verzendklaar maken.
|
||||
|
||||
---
|
||||
|
||||
## 4. Scope & Functionaliteiten
|
||||
|
||||
**In Scope (Prototype MVP):**
|
||||
- **Nieuwe Intentie:** Herkenning van `register_no_show`.
|
||||
- **Nudges Engine Integratie:** Het afvuren van een UI-nudge suggestion als de afspraak declarabel is.
|
||||
- **Context Lookup:** Checken of er mock-data (concept huisartsbrief) bestaat voor de actieve patiënt.
|
||||
- **Generatieve AI:** Een LLM (zoals in `app/api/cortex/chat`) gebruiken om een alinea over de "no show" correct te integreren in de bestaande tekst van de huisartsbrief.
|
||||
- **Artifact View:** Het openen van de geüpdatete brief in de `Artifact` UI van de Cortex store.
|
||||
|
||||
**Out of Scope:**
|
||||
- Volledige kalender-integratie voor het verzetten van de afspraak (we mocken het inplannen van de *nieuwe* afspraak of gebruiken een simpele toast dat deze 'in de kalender staat').
|
||||
- Verzenden van de brief via echte Zorgmail-integratie (we updaten puur de status naar `ready_for_dispatch`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Succescriteria
|
||||
|
||||
- **Flow Completion Time:** De gehele flow van "No Show" typen tot "Brief Verzonden" moet binnen 30 seconden kunnen worden doorlopen en gedemonstreerd.
|
||||
- **Zero-Navigation:** De regiebehandelaar hoeft het actieve patiënt-dashboard of de journaal-view niet te verlaten; alle interactie verloopt via Cortex Chat en de dynamische pop-up (Artifacts).
|
||||
- **Kwaliteit Brief:** De door de AI herschreven brief moet grammaticaal en contextueel logisch zijn (bijv. niet zomaar "Patiënt was een no show" aan het einde plakken, maar verweven op een professionele manier in relatie tot het vervolgtraject).
|
||||
Reference in New Issue
Block a user