feat(cortex): Epic 4 - Nudge MVP (Layer 3) complete
Proactive suggestions after successful actions - proof of concept. E4.S1 - Protocol Rules (lib/cortex/nudge.ts) - ProtocolRule and ProtocolCondition types - Condition operators: equals, contains, matches, exists - 2 hardcoded rules: wondzorg-controle, medicatie-controle E4.S2 - evaluateNudge Function - evaluateNudge(input) returns NudgeSuggestion[] - checkCondition() helper for rule matching - Priority sorting (high → medium → low) E4.S3 - NudgeToast Component - Countdown progress bar with auto-dismiss (5 min) - Priority-based styling (low/medium/high) - Accept and dismiss buttons - Framer Motion animations E4.S4 - Integration - NudgeToast rendered in CommandCenter (fixed position) - Trigger added in DagnotatieBlock after successful save - Accept opens corresponding artifact with prefilled data Demo flow: 1. Create dagnotitie with "wond" in content 2. Save → NudgeToast appears 3. Click "Ja, inplannen" → Appointment artifact opens 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Loader2, Search, User, RefreshCw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { safeFetch, getErrorInfo, retryFetch } from '@/lib/cortex/error-handler';
|
||||
import { evaluateNudge } from '@/lib/cortex/nudge';
|
||||
|
||||
interface DagnotitieBlockProps {
|
||||
prefill?: BlockPrefillData;
|
||||
@@ -39,7 +40,7 @@ interface Patient {
|
||||
|
||||
export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
||||
const config = BLOCK_CONFIGS.dagnotitie;
|
||||
const { closeBlock } = useCortexStore();
|
||||
const { closeBlock, addSuggestion } = useCortexStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
// Form state
|
||||
@@ -221,6 +222,23 @@ export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
||||
description: `Notitie voor ${patientName} is opgeslagen`,
|
||||
});
|
||||
|
||||
// E4: Evaluate nudge after successful save
|
||||
const nudges = evaluateNudge({
|
||||
intent: 'dagnotitie',
|
||||
actionId: data.id || `dagnotitie-${Date.now()}`,
|
||||
entities: {
|
||||
patientName,
|
||||
category,
|
||||
},
|
||||
content: content.trim(),
|
||||
});
|
||||
|
||||
// Add nudge suggestions to store
|
||||
nudges.forEach((nudge) => {
|
||||
addSuggestion(nudge);
|
||||
console.log('[DagnotatieBlock] Nudge triggered:', nudge.suggestion.message);
|
||||
});
|
||||
|
||||
// Close block after short delay
|
||||
setTimeout(() => {
|
||||
closeBlock();
|
||||
@@ -241,7 +259,7 @@ export function DagnotatieBlock({ prefill }: DagnotitieBlockProps) {
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [patientId, content, category, includeInHandover, patientName, toast, closeBlock]);
|
||||
}, [patientId, content, category, includeInHandover, patientName, toast, closeBlock, addSuggestion]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -17,16 +17,28 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useCortexStore } from '@/stores/cortex-store';
|
||||
import { ContextBar } from './context-bar';
|
||||
import { OfflineBanner } from './offline-banner';
|
||||
import { NudgeToast } from './nudge-toast';
|
||||
import { ChatPanel } from '../chat/chat-panel';
|
||||
import { ArtifactArea } from '../artifacts/artifact-area';
|
||||
import { getArtifactTitle } from '../artifacts/artifact-container';
|
||||
import { routeIntentToArtifact } from '@/lib/cortex/action-parser';
|
||||
|
||||
export function CommandCenter() {
|
||||
const { closeAllArtifacts, openArtifacts, openArtifact, pendingAction, setPendingAction } = useCortexStore();
|
||||
const {
|
||||
closeAllArtifacts,
|
||||
openArtifacts,
|
||||
openArtifact,
|
||||
pendingAction,
|
||||
setPendingAction,
|
||||
// Nudge state (E4)
|
||||
suggestions,
|
||||
acceptSuggestion,
|
||||
dismissSuggestion,
|
||||
} = useCortexStore();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Global keyboard shortcuts
|
||||
@@ -118,6 +130,43 @@ export function CommandCenter() {
|
||||
<ArtifactArea />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nudge Toast - E4 (fixed position, bottom-left above chat) */}
|
||||
<AnimatePresence mode="wait">
|
||||
{suggestions.length > 0 && (
|
||||
<div className="fixed bottom-20 left-4 right-4 lg:left-4 lg:right-auto lg:w-[38%] z-50">
|
||||
<NudgeToast
|
||||
key={suggestions[0].id}
|
||||
suggestion={suggestions[0]}
|
||||
onAccept={(id) => {
|
||||
const suggestion = suggestions.find((s) => s.id === id);
|
||||
if (suggestion) {
|
||||
// Route the suggested intent to an artifact
|
||||
const artifact = routeIntentToArtifact(
|
||||
suggestion.suggestion.intent,
|
||||
suggestion.suggestion.entities,
|
||||
1.0 // High confidence since user explicitly accepted
|
||||
);
|
||||
|
||||
if (artifact) {
|
||||
console.log('[CommandCenter] Opening artifact from nudge:', artifact.type);
|
||||
openArtifact({
|
||||
type: artifact.type,
|
||||
prefill: artifact.prefill,
|
||||
title: artifact.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
acceptSuggestion(id);
|
||||
}}
|
||||
onDismiss={(id) => {
|
||||
dismissSuggestion(id);
|
||||
console.log('[CommandCenter] Nudge dismissed:', id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
186
components/cortex/command-center/nudge-toast.tsx
Normal file
186
components/cortex/command-center/nudge-toast.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* NudgeToast Component
|
||||
*
|
||||
* Proactive suggestion toast that appears after successful actions.
|
||||
* Features:
|
||||
* - Countdown progress bar with auto-dismiss
|
||||
* - Priority-based styling (low/medium/high)
|
||||
* - Accept and dismiss actions
|
||||
* - Framer Motion animations
|
||||
*
|
||||
* Epic: E4 (Nudge MVP)
|
||||
* Story: E4.S3 (NudgeToast component)
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Lightbulb, X } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { NudgeSuggestion } from '@/lib/cortex/types';
|
||||
|
||||
interface NudgeToastProps {
|
||||
suggestion: NudgeSuggestion;
|
||||
onAccept: (suggestionId: string) => void;
|
||||
onDismiss: (suggestionId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation variants for the toast container
|
||||
*/
|
||||
const containerVariants = {
|
||||
initial: { opacity: 0, y: 20, scale: 0.95 },
|
||||
animate: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] as const },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: -10,
|
||||
scale: 0.98,
|
||||
transition: { duration: 0.15 },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Priority-based styling for the toast
|
||||
*/
|
||||
const PRIORITY_STYLES = {
|
||||
low: {
|
||||
container: 'bg-slate-50 border-slate-200',
|
||||
text: 'text-slate-700',
|
||||
icon: 'text-slate-500',
|
||||
progress: 'bg-slate-300',
|
||||
},
|
||||
medium: {
|
||||
container: 'bg-amber-50 border-amber-200',
|
||||
text: 'text-amber-800',
|
||||
icon: 'text-amber-600',
|
||||
progress: 'bg-amber-400',
|
||||
},
|
||||
high: {
|
||||
container: 'bg-red-50 border-red-200',
|
||||
text: 'text-red-800',
|
||||
icon: 'text-red-600',
|
||||
progress: 'bg-red-400',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get button text based on suggested intent
|
||||
*/
|
||||
function getAcceptButtonText(intent: string): string {
|
||||
switch (intent) {
|
||||
case 'create_appointment':
|
||||
return 'Ja, inplannen';
|
||||
case 'dagnotitie':
|
||||
return 'Ja, notitie maken';
|
||||
case 'cancel_appointment':
|
||||
return 'Ja, annuleren';
|
||||
default:
|
||||
return 'Ja, uitvoeren';
|
||||
}
|
||||
}
|
||||
|
||||
export function NudgeToast({ suggestion, onAccept, onDismiss }: NudgeToastProps) {
|
||||
const [progress, setProgress] = useState(100);
|
||||
const styles = PRIORITY_STYLES[suggestion.priority];
|
||||
|
||||
// Memoize dismiss handler to avoid effect re-runs
|
||||
const handleDismiss = useCallback(() => {
|
||||
onDismiss(suggestion.id);
|
||||
}, [onDismiss, suggestion.id]);
|
||||
|
||||
// Auto-dismiss countdown effect
|
||||
useEffect(() => {
|
||||
if (!suggestion.expiresAt) return;
|
||||
|
||||
const expiresAt = new Date(suggestion.expiresAt).getTime();
|
||||
const createdAt = new Date(suggestion.createdAt).getTime();
|
||||
const total = expiresAt - createdAt;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const remaining = expiresAt - now;
|
||||
|
||||
if (remaining <= 0) {
|
||||
handleDismiss();
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress((remaining / total) * 100);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [suggestion.expiresAt, suggestion.createdAt, handleDismiss]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
className={cn(
|
||||
'rounded-lg border p-4 shadow-sm',
|
||||
styles.container
|
||||
)}
|
||||
>
|
||||
{/* Header: Icon + Message + Dismiss */}
|
||||
<div className="flex items-start gap-3">
|
||||
<Lightbulb className={cn('h-5 w-5 flex-shrink-0 mt-0.5', styles.icon)} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={cn('font-medium', styles.text)}>
|
||||
{suggestion.suggestion.message}
|
||||
</p>
|
||||
<p className={cn('text-sm mt-1 opacity-75', styles.text)}>
|
||||
{suggestion.suggestion.rationale}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className={cn(
|
||||
'flex-shrink-0 rounded-md p-1 hover:bg-black/5 transition-colors',
|
||||
styles.text
|
||||
)}
|
||||
aria-label="Sluiten"
|
||||
>
|
||||
<X className="h-4 w-4 opacity-50 hover:opacity-100" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onAccept(suggestion.id)}
|
||||
className="bg-teal-600 hover:bg-teal-700 text-white"
|
||||
>
|
||||
{getAcceptButtonText(suggestion.suggestion.intent)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleDismiss}
|
||||
className={styles.text}
|
||||
>
|
||||
Later
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Progress bar countdown */}
|
||||
<div className="mt-3 h-1 bg-white/50 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className={cn('h-full rounded-full', styles.progress)}
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 1, ease: 'linear' }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Bouwplan — Cortex Intent System V2
|
||||
|
||||
**Projectnaam:** Cortex V2 - Agentic Intent Architecture
|
||||
**Versie:** v1.3
|
||||
**Versie:** v1.4
|
||||
**Datum:** 01-01-2026
|
||||
**Auteur:** Colin Lit
|
||||
|
||||
@@ -135,7 +135,7 @@ lib/config/
|
||||
| **E1** | Reflex Arc (Layer 1) | Snelle lokale classificatie | ✅ Done | 4 | 6 SP |
|
||||
| **E2** | Intent Orchestrator (Layer 2) | AI-gedreven multi-intent | ✅ Done | 6 | 13 SP |
|
||||
| **E3** | UI Components | ActionChainCard, ClarificationCard | ✅ Done | 4 | 8 SP |
|
||||
| **E4** | Nudge MVP (Layer 3) | Proactieve suggesties | ⏳ To Do | 3 | 5 SP |
|
||||
| **E4** | Nudge MVP (Layer 3) | Proactieve suggesties | ✅ Done | 3 | 5 SP |
|
||||
| **E5** | Integration & Polish | End-to-end flow, testing | ⏳ To Do | 4 | 8 SP |
|
||||
|
||||
**Totaal MVP: 26 stories, 48 Story Points**
|
||||
@@ -848,9 +848,9 @@ interface ClarificationCardProps {
|
||||
|
||||
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|
||||
|----------|--------------|---------------------|--------|------------------|----|
|
||||
| E4.S1 | **Protocol rules** definiëren | Wondzorg-controle regel (hardcoded) | ⏳ | E0.S1 | 1 |
|
||||
| E4.S2 | **evaluateNudge** functie | Check protocol rules na actie completion | ⏳ | E4.S1 | 2 |
|
||||
| E4.S3 | **NudgeToast** component | Toast met countdown timer, accept/dismiss | ⏳ | E0.S4 | 2 |
|
||||
| E4.S1 | **Protocol rules** definiëren | Wondzorg-controle regel (hardcoded) | ✅ | E0.S1 | 1 |
|
||||
| E4.S2 | **evaluateNudge** functie | Check protocol rules na actie completion | ✅ | E4.S1 | 2 |
|
||||
| E4.S3 | **NudgeToast** component | Toast met countdown timer, accept/dismiss | ✅ | E0.S4 | 2 |
|
||||
|
||||
**Demo Case:**
|
||||
```
|
||||
@@ -910,8 +910,8 @@ export const PROTOCOL_RULES: ProtocolRule[] = [
|
||||
- `exists` - field is not null/empty
|
||||
|
||||
*Done criteria:*
|
||||
- [ ] ProtocolRule type gedefinieerd
|
||||
- [ ] Minimaal 1 werkende regel (wondzorg)
|
||||
- [x] ProtocolRule type gedefinieerd
|
||||
- [x] Minimaal 1 werkende regel (wondzorg)
|
||||
|
||||
---
|
||||
|
||||
@@ -944,9 +944,9 @@ function checkCondition(condition: ProtocolCondition, entities: ExtractedEntitie
|
||||
```
|
||||
|
||||
*Done criteria:*
|
||||
- [ ] Notitie met "wond" → NudgeSuggestion returned
|
||||
- [ ] Notitie zonder "wond" → empty array
|
||||
- [ ] Suggesties gesorteerd op priority
|
||||
- [x] Notitie met "wond" → NudgeSuggestion returned
|
||||
- [x] Notitie zonder "wond" → empty array
|
||||
- [x] Suggesties gesorteerd op priority
|
||||
|
||||
---
|
||||
|
||||
@@ -986,10 +986,10 @@ useEffect(() => {
|
||||
```
|
||||
|
||||
*Done criteria:*
|
||||
- [ ] Toast verschijnt na matching actie
|
||||
- [ ] Progress bar animeert
|
||||
- [ ] Accept triggert nieuwe actie
|
||||
- [ ] Dismiss verwijdert toast
|
||||
- [x] Toast verschijnt na matching actie
|
||||
- [x] Progress bar animeert
|
||||
- [x] Accept triggert nieuwe actie
|
||||
- [x] Dismiss verwijdert toast
|
||||
|
||||
---
|
||||
|
||||
@@ -1399,3 +1399,4 @@ De MVP User Stories uit `mvp-userstories-intent-system.md` zijn als volgt verdee
|
||||
| v1.1 | 31-12-2025 | Colin Lit | Dev-instructies per epic toegevoegd, file mappings, done criteria |
|
||||
| v1.2 | 01-01-2026 | Colin Lit | Epic 2 (Intent Orchestrator) compleet - alle 6 stories afgerond |
|
||||
| v1.3 | 01-01-2026 | Colin Lit | Epic 3 (UI Components) compleet - ActionChainCard, ActionItem, ClarificationCard, ProcessingIndicator |
|
||||
| v1.4 | 01-01-2026 | Colin Lit | Epic 4 (Nudge MVP) compleet - ProtocolRules, evaluateNudge, NudgeToast, DagnotatieBlock integratie |
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './reflex-classifier';
|
||||
export * from './entity-extractor';
|
||||
export * from './date-time-parser';
|
||||
export * from './logger';
|
||||
export * from './nudge';
|
||||
|
||||
234
lib/cortex/nudge.ts
Normal file
234
lib/cortex/nudge.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Cortex Layer 3: Nudge System
|
||||
*
|
||||
* Proactive suggestions after successful actions based on protocol rules.
|
||||
* MVP implementation with hardcoded rules for demonstration.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CortexIntent,
|
||||
ExtractedEntities,
|
||||
NudgePriority,
|
||||
NudgeSuggestion,
|
||||
} from './types';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Protocol Rule Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Condition operator for matching */
|
||||
export type ConditionOperator = 'equals' | 'contains' | 'matches' | 'exists';
|
||||
|
||||
/** Single condition to check against entities or content */
|
||||
export interface ProtocolCondition {
|
||||
/** Field to check - 'content' for the original input text, or entity field */
|
||||
field: keyof ExtractedEntities | 'content';
|
||||
/** Comparison operator */
|
||||
operator: ConditionOperator;
|
||||
/** Value to compare against (not used for 'exists' operator) */
|
||||
value: string;
|
||||
}
|
||||
|
||||
/** Protocol rule that triggers a nudge suggestion */
|
||||
export interface ProtocolRule {
|
||||
/** Unique identifier for the rule */
|
||||
id: string;
|
||||
/** Human-readable name for the rule */
|
||||
name: string;
|
||||
/** Trigger conditions */
|
||||
trigger: {
|
||||
/** Intent that must match */
|
||||
intent: CortexIntent;
|
||||
/** Additional conditions that must all be met */
|
||||
conditions: ProtocolCondition[];
|
||||
};
|
||||
/** Suggestion to show when rule matches */
|
||||
suggestion: {
|
||||
/** Suggested follow-up intent */
|
||||
intent: CortexIntent;
|
||||
/** User-facing message */
|
||||
message: string;
|
||||
/** Function to prefill entities from source action */
|
||||
prefillEntities: (source: ExtractedEntities) => Partial<ExtractedEntities>;
|
||||
};
|
||||
/** Priority for sorting multiple suggestions */
|
||||
priority: NudgePriority;
|
||||
/** Whether this rule is active */
|
||||
enabled: boolean;
|
||||
/** Time in ms before suggestion expires (default 5 minutes) */
|
||||
expiresAfterMs: number;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Condition Checker (Internal)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if a single condition is met
|
||||
*/
|
||||
function checkCondition(
|
||||
condition: ProtocolCondition,
|
||||
entities: ExtractedEntities,
|
||||
content?: string
|
||||
): boolean {
|
||||
// Get the value to check
|
||||
const value = condition.field === 'content'
|
||||
? content
|
||||
: entities[condition.field as keyof ExtractedEntities];
|
||||
|
||||
// Handle null/undefined values
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return condition.operator === 'exists' ? false : false;
|
||||
}
|
||||
|
||||
const stringValue = String(value);
|
||||
|
||||
switch (condition.operator) {
|
||||
case 'equals':
|
||||
return stringValue.toLowerCase() === condition.value.toLowerCase();
|
||||
case 'contains':
|
||||
return stringValue.toLowerCase().includes(condition.value.toLowerCase());
|
||||
case 'matches':
|
||||
try {
|
||||
return new RegExp(condition.value, 'i').test(stringValue);
|
||||
} catch {
|
||||
// Invalid regex, return false
|
||||
return false;
|
||||
}
|
||||
case 'exists':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Nudge Evaluation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Input for nudge evaluation */
|
||||
export interface NudgeEvaluationInput {
|
||||
/** The completed action's intent */
|
||||
intent: CortexIntent;
|
||||
/** The action's ID */
|
||||
actionId: string;
|
||||
/** Extracted entities from the action */
|
||||
entities: ExtractedEntities;
|
||||
/** Original user input text (for content matching) */
|
||||
content?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate completed action against protocol rules and generate suggestions
|
||||
*
|
||||
* @param input - The completed action details
|
||||
* @returns Array of NudgeSuggestions sorted by priority (high first)
|
||||
*/
|
||||
export function evaluateNudge(input: NudgeEvaluationInput): NudgeSuggestion[] {
|
||||
const suggestions: NudgeSuggestion[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (const rule of PROTOCOL_RULES) {
|
||||
// Skip disabled rules
|
||||
if (!rule.enabled) continue;
|
||||
|
||||
// Check intent match
|
||||
if (rule.trigger.intent !== input.intent) continue;
|
||||
|
||||
// Check all conditions (AND logic)
|
||||
const allConditionsMet = rule.trigger.conditions.every(
|
||||
(cond) => checkCondition(cond, input.entities, input.content)
|
||||
);
|
||||
|
||||
if (allConditionsMet) {
|
||||
suggestions.push({
|
||||
id: `nudge-${rule.id}-${Date.now()}`,
|
||||
trigger: {
|
||||
actionId: input.actionId,
|
||||
intent: input.intent,
|
||||
entities: input.entities,
|
||||
},
|
||||
suggestion: {
|
||||
intent: rule.suggestion.intent,
|
||||
entities: rule.suggestion.prefillEntities(input.entities),
|
||||
message: rule.suggestion.message,
|
||||
rationale: rule.name,
|
||||
},
|
||||
status: 'pending',
|
||||
priority: rule.priority,
|
||||
expiresAt: new Date(now.getTime() + rule.expiresAfterMs),
|
||||
createdAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority (high first)
|
||||
const priorityOrder: Record<NudgePriority, number> = {
|
||||
high: 0,
|
||||
medium: 1,
|
||||
low: 2,
|
||||
};
|
||||
|
||||
return suggestions.sort(
|
||||
(a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MVP Protocol Rules (Hardcoded)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Default expiry time: 5 minutes */
|
||||
const DEFAULT_EXPIRY_MS = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* MVP Protocol Rules
|
||||
*
|
||||
* For the prototype, we hardcode a few demonstration rules.
|
||||
* In production, these would come from a database or configuration.
|
||||
*/
|
||||
export const PROTOCOL_RULES: ProtocolRule[] = [
|
||||
{
|
||||
id: 'wondzorg-controle',
|
||||
name: 'Wondcontrole na verzorging',
|
||||
trigger: {
|
||||
intent: 'dagnotitie',
|
||||
conditions: [
|
||||
{ field: 'content', operator: 'contains', value: 'wond' },
|
||||
],
|
||||
},
|
||||
suggestion: {
|
||||
intent: 'create_appointment',
|
||||
message: 'Wondcontrole inplannen over 3 dagen?',
|
||||
prefillEntities: (source) => ({
|
||||
patientName: source.patientName,
|
||||
appointmentType: 'follow-up',
|
||||
}),
|
||||
},
|
||||
priority: 'medium',
|
||||
enabled: true,
|
||||
expiresAfterMs: DEFAULT_EXPIRY_MS,
|
||||
},
|
||||
{
|
||||
id: 'medicatie-controle',
|
||||
name: 'Medicatie controle na wijziging',
|
||||
trigger: {
|
||||
intent: 'dagnotitie',
|
||||
conditions: [
|
||||
{ field: 'content', operator: 'contains', value: 'medicatie' },
|
||||
{ field: 'content', operator: 'contains', value: 'gewijzigd' },
|
||||
],
|
||||
},
|
||||
suggestion: {
|
||||
intent: 'create_appointment',
|
||||
message: 'Medicatie evaluatie inplannen over 1 week?',
|
||||
prefillEntities: (source) => ({
|
||||
patientName: source.patientName,
|
||||
appointmentType: 'follow-up',
|
||||
}),
|
||||
},
|
||||
priority: 'medium',
|
||||
enabled: true,
|
||||
expiresAfterMs: DEFAULT_EXPIRY_MS,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user