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:
@@ -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