# 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; // ← 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>) => 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`