feat(cortex): Epic 0 - Foundation & Context voor V2 architectuur
Implementeert de basis voor de Cortex V2 three-layer architecture. E0.S1 - CortexContext Types (lib/cortex/types.ts): - EscalationReason type voor Reflex → Orchestrator - LocalClassificationResult met ambiguity detection - CortexContext voor AI classificatie context - IntentChain en IntentAction voor multi-intent flows - NudgeSuggestion voor proactieve suggesties - CONFIDENCE_THRESHOLD (0.7) en AMBIGUITY_THRESHOLD (0.1) - getCurrentShift() utility (DRY - herbruikbaar) E0.S2 - Context API (app/api/cortex/context/route.ts): - GET endpoint voor huidige context - Haalt agenda vandaag op uit database - Retourneert CortexContext object E0.S3 - Feature Flags (lib/config/feature-flags.ts): - CORTEX_V2_ENABLED, CORTEX_MULTI_INTENT - CORTEX_NUDGE, CORTEX_LOGGING - isFeatureEnabled() helper - Dev mode: default true E0.S4 - CortexStore V2 (stores/cortex-store.ts): - Types geïmporteerd uit lib/cortex (DRY) - ChatEntities type voor AI responses (string-based) - V2 state: context, activeChain, chainHistory, suggestions - V2 actions: chain management, nudge management, clarification E0.S5 - Classification Logging (lib/cortex/logger.ts): - PII sanitization (BSN, tel, namen, email) - logClassification(), logEscalation(), logNudge() - logPerformance() met threshold checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
111
app/api/cortex/context/route.ts
Normal file
111
app/api/cortex/context/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/auth/server';
|
||||
import { getCurrentShift } from '@/lib/cortex/types';
|
||||
import type { CortexContext } from '@/lib/cortex/types';
|
||||
|
||||
/**
|
||||
* Cortex Context API
|
||||
*
|
||||
* GET /api/cortex/context
|
||||
*
|
||||
* Returns the current context for AI classification.
|
||||
* Includes: shift, agenda today, recent intents.
|
||||
* Note: activePatient is set client-side via store.
|
||||
*/
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Auth check
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
error: authError,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (authError || !user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Niet geautoriseerd. Log opnieuw in.' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get practitioner info
|
||||
const { data: practitioner } = await supabase
|
||||
.from('practitioners')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
|
||||
// Get today's date range
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
// Fetch today's appointments
|
||||
const { data: appointments } = await supabase
|
||||
.from('encounters')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
period_start,
|
||||
type_display,
|
||||
patient:patients(id, name_given, name_prefix, name_family)
|
||||
`
|
||||
)
|
||||
.eq('practitioner_id', practitioner?.id ?? '')
|
||||
.gte('period_start', today.toISOString())
|
||||
.lt('period_start', tomorrow.toISOString())
|
||||
.order('period_start');
|
||||
|
||||
// Build context object
|
||||
const context: CortexContext = {
|
||||
activePatient: null, // Set by client via store
|
||||
currentView: 'dashboard', // Default, overridden by client
|
||||
shift: getCurrentShift(),
|
||||
currentTime: new Date(),
|
||||
agendaToday: (appointments || []).map((apt) => {
|
||||
// Handle the nested patient relation
|
||||
const patient = apt.patient as {
|
||||
id: string;
|
||||
name_given: string[];
|
||||
name_prefix: string | null;
|
||||
name_family: string;
|
||||
} | null;
|
||||
|
||||
// Construct full name: "Voornaam [tussenvoegsel] Achternaam"
|
||||
const fullName = patient
|
||||
? [
|
||||
patient.name_given?.[0],
|
||||
patient.name_prefix,
|
||||
patient.name_family,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
: 'Onbekend';
|
||||
|
||||
return {
|
||||
time: new Date(apt.period_start).toLocaleTimeString('nl-NL', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
patientName: fullName,
|
||||
patientId: patient?.id || '',
|
||||
type: apt.type_display || 'afspraak',
|
||||
};
|
||||
}),
|
||||
recentIntents: [], // Populated by client from store history
|
||||
};
|
||||
|
||||
return NextResponse.json({ context }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[Cortex Context API] Error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'Er ging iets mis bij het ophalen van context. Probeer het opnieuw.',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/config/feature-flags.ts
Normal file
46
lib/config/feature-flags.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Feature Flags for Cortex V2
|
||||
*
|
||||
* Controls gradual rollout of new features.
|
||||
* In development, all flags default to true for testing.
|
||||
*/
|
||||
|
||||
/** Feature flag definitions */
|
||||
export const FEATURE_FLAGS = {
|
||||
/** Enable Cortex V2 architecture (Reflex + Orchestrator) */
|
||||
CORTEX_V2_ENABLED: process.env.NEXT_PUBLIC_CORTEX_V2 === 'true',
|
||||
|
||||
/** Enable multi-intent detection and ActionChainCard */
|
||||
CORTEX_MULTI_INTENT: process.env.NEXT_PUBLIC_CORTEX_MULTI_INTENT === 'true',
|
||||
|
||||
/** Enable proactive nudge suggestions */
|
||||
CORTEX_NUDGE: process.env.NEXT_PUBLIC_CORTEX_NUDGE === 'true',
|
||||
|
||||
/** Enable classification logging (dev only) */
|
||||
CORTEX_LOGGING: process.env.NEXT_PUBLIC_CORTEX_LOGGING === 'true',
|
||||
} as const;
|
||||
|
||||
/** Type for feature flag keys */
|
||||
export type FeatureFlagKey = keyof typeof FEATURE_FLAGS;
|
||||
|
||||
/**
|
||||
* Check if a feature flag is enabled
|
||||
*/
|
||||
export function isFeatureEnabled(flag: FeatureFlagKey): boolean {
|
||||
// In development, default to true if env var not set
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const envValue = process.env[`NEXT_PUBLIC_${flag}`];
|
||||
// Only return false if explicitly set to 'false'
|
||||
return envValue !== 'false';
|
||||
}
|
||||
return FEATURE_FLAGS[flag];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled features (useful for debugging)
|
||||
*/
|
||||
export function getEnabledFeatures(): FeatureFlagKey[] {
|
||||
return (Object.keys(FEATURE_FLAGS) as FeatureFlagKey[]).filter(
|
||||
(flag) => isFeatureEnabled(flag)
|
||||
);
|
||||
}
|
||||
@@ -8,3 +8,4 @@ export * from './intent-classifier';
|
||||
export * from './intent-classifier-ai';
|
||||
export * from './entity-extractor';
|
||||
export * from './date-time-parser';
|
||||
export * from './logger';
|
||||
|
||||
160
lib/cortex/logger.ts
Normal file
160
lib/cortex/logger.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Cortex Classification Logger
|
||||
*
|
||||
* Logging utility with PII sanitization for classification results.
|
||||
* In development: full logging for debugging.
|
||||
* In production: sanitized logging (no names, BSN, phone numbers).
|
||||
*/
|
||||
|
||||
import { isFeatureEnabled } from '@/lib/config/feature-flags';
|
||||
import type { LocalClassificationResult, IntentChain } from './types';
|
||||
|
||||
// PII patterns to sanitize in production
|
||||
const PII_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
|
||||
// BSN (9 digits)
|
||||
{ pattern: /\b\d{9}\b/g, replacement: '[BSN]' },
|
||||
// Dutch phone numbers
|
||||
{ pattern: /\b0[1-9]\d{8}\b/g, replacement: '[TEL]' },
|
||||
{ pattern: /\b06[-\s]?\d{8}\b/g, replacement: '[TEL]' },
|
||||
{ pattern: /\+31[-\s]?\d{9}\b/g, replacement: '[TEL]' },
|
||||
// Dates (dd-mm-yyyy format)
|
||||
{ pattern: /\b\d{2}[-/]\d{2}[-/]\d{4}\b/g, replacement: '[DATUM]' },
|
||||
// Email addresses
|
||||
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: '[EMAIL]' },
|
||||
// Common Dutch first names (case insensitive)
|
||||
{
|
||||
pattern:
|
||||
/\b(Jan|Piet|Klaas|Marie|Anna|Lisa|Eva|Emma|Sophie|Thomas|Lucas|Daan|Sem|Liam|Noah|Julia|Sara|Lotte|Henk|Willem|Pieter|Johan|Marieke|Sandra|Linda|Monique|Peter|Hans|Jeroen|Bart|Mark|Erik|Rob|Kees|Cor|Arie|Gerrit|Hendrik|Johannes)\b/gi,
|
||||
replacement: '[NAAM]',
|
||||
},
|
||||
// Common Dutch family name prefixes + word after
|
||||
{
|
||||
pattern: /\b(van|de|den|der|het|ter|ten)\s+[A-Z][a-z]+\b/gi,
|
||||
replacement: '[NAAM]',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Sanitize input string by removing PII
|
||||
* In development: returns original input
|
||||
* In production: replaces PII with placeholders
|
||||
*/
|
||||
export function sanitizeForLogging(input: string): string {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return input;
|
||||
}
|
||||
|
||||
let sanitized = input;
|
||||
for (const { pattern, replacement } of PII_PATTERNS) {
|
||||
sanitized = sanitized.replace(pattern, replacement);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a classification result (Reflex or Orchestrator)
|
||||
* Only logs if CORTEX_LOGGING feature flag is enabled
|
||||
*/
|
||||
export function logClassification(
|
||||
input: string,
|
||||
result: LocalClassificationResult | IntentChain
|
||||
): void {
|
||||
if (!isFeatureEnabled('CORTEX_LOGGING')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedInput = sanitizeForLogging(input);
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if ('shouldEscalateToAI' in result) {
|
||||
// LocalClassificationResult (Reflex Arc)
|
||||
const logData = {
|
||||
input: sanitizedInput,
|
||||
intent: result.intent,
|
||||
confidence: result.confidence.toFixed(2),
|
||||
escalate: result.shouldEscalateToAI,
|
||||
reason: result.escalationReason || null,
|
||||
timeMs: result.processingTimeMs,
|
||||
};
|
||||
|
||||
if (result.shouldEscalateToAI) {
|
||||
console.log(`[Cortex:Reflex→AI] ${timestamp}`, logData);
|
||||
} else {
|
||||
console.log(`[Cortex:Reflex] ${timestamp}`, logData);
|
||||
}
|
||||
} else {
|
||||
// IntentChain (Orchestrator)
|
||||
console.log(`[Cortex:Orchestrator] ${timestamp}`, {
|
||||
input: sanitizedInput,
|
||||
actionCount: result.actions.length,
|
||||
intents: result.actions.map((a) => a.intent),
|
||||
source: result.meta.source,
|
||||
timeMs: result.meta.processingTimeMs,
|
||||
reasoning: result.meta.aiReasoning
|
||||
? sanitizeForLogging(result.meta.aiReasoning).slice(0, 100)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an escalation event (Reflex → Orchestrator)
|
||||
*/
|
||||
export function logEscalation(
|
||||
input: string,
|
||||
reason: LocalClassificationResult['escalationReason'],
|
||||
reflexResult: LocalClassificationResult
|
||||
): void {
|
||||
if (!isFeatureEnabled('CORTEX_LOGGING')) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Cortex:Escalation] ${new Date().toISOString()}`, {
|
||||
input: sanitizeForLogging(input),
|
||||
reason,
|
||||
reflexIntent: reflexResult.intent,
|
||||
reflexConfidence: reflexResult.confidence.toFixed(2),
|
||||
secondBest: reflexResult.secondBestIntent || null,
|
||||
secondBestConfidence: reflexResult.secondBestConfidence?.toFixed(2) || null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a nudge suggestion event
|
||||
*/
|
||||
export function logNudge(
|
||||
triggeredBy: string,
|
||||
suggestionMessage: string,
|
||||
accepted: boolean
|
||||
): void {
|
||||
if (!isFeatureEnabled('CORTEX_LOGGING')) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Cortex:Nudge] ${new Date().toISOString()}`, {
|
||||
trigger: triggeredBy,
|
||||
suggestion: sanitizeForLogging(suggestionMessage),
|
||||
accepted,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log performance metrics
|
||||
*/
|
||||
export function logPerformance(
|
||||
layer: 'reflex' | 'orchestrator',
|
||||
durationMs: number,
|
||||
success: boolean
|
||||
): void {
|
||||
if (!isFeatureEnabled('CORTEX_LOGGING')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emoji = success ? '✓' : '✗';
|
||||
const threshold = layer === 'reflex' ? 20 : 800;
|
||||
const status = durationMs <= threshold ? 'OK' : 'SLOW';
|
||||
|
||||
console.log(
|
||||
`[Cortex:Perf] ${layer} ${emoji} ${durationMs}ms [${status}]`
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,15 @@ export type BlockType = Exclude<CortexIntent, 'unknown'> | 'patient-dashboard';
|
||||
// Shift types
|
||||
export type ShiftType = 'nacht' | 'ochtend' | 'middag' | 'avond';
|
||||
|
||||
/** Calculate current shift based on time of day */
|
||||
export function getCurrentShift(): ShiftType {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 0 && hour < 7) return 'nacht';
|
||||
if (hour >= 7 && hour < 12) return 'ochtend';
|
||||
if (hour >= 12 && hour < 17) return 'middag';
|
||||
return 'avond';
|
||||
}
|
||||
|
||||
// Intent classification result
|
||||
export interface IntentClassificationResult {
|
||||
intent: CortexIntent;
|
||||
@@ -145,3 +154,161 @@ export interface RecentAction {
|
||||
timestamp: Date;
|
||||
patientName?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cortex V2 Types — Three-Layer Architecture
|
||||
// =============================================================================
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Layer 1: Reflex Arc Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Reasons why Reflex Arc escalates to Orchestrator */
|
||||
export type EscalationReason =
|
||||
| 'low_confidence'
|
||||
| 'ambiguous'
|
||||
| 'multi_intent_detected'
|
||||
| 'needs_context'
|
||||
| 'relative_time';
|
||||
|
||||
/** Local classification result from Reflex Arc */
|
||||
export interface LocalClassificationResult {
|
||||
intent: CortexIntent;
|
||||
confidence: number;
|
||||
/** Second best match for ambiguity detection */
|
||||
secondBestIntent?: CortexIntent;
|
||||
secondBestConfidence?: number;
|
||||
matchedPattern?: string;
|
||||
processingTimeMs: number;
|
||||
shouldEscalateToAI: boolean;
|
||||
escalationReason?: EscalationReason;
|
||||
}
|
||||
|
||||
/** Confidence threshold for high-confidence local classification */
|
||||
export const CONFIDENCE_THRESHOLD = 0.7;
|
||||
|
||||
/** Threshold for ambiguity detection (delta between top-2 scores) */
|
||||
export const AMBIGUITY_THRESHOLD = 0.1;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Layer 2: Orchestrator Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Context provided to AI for intelligent classification */
|
||||
export interface CortexContext {
|
||||
activePatient: {
|
||||
id: string;
|
||||
name: string;
|
||||
recentNotes?: string[];
|
||||
upcomingAppointments?: { date: Date; type: string }[];
|
||||
} | null;
|
||||
currentView: 'dashboard' | 'patient-detail' | 'agenda' | 'reports' | 'chat';
|
||||
shift: ShiftType;
|
||||
currentTime: Date;
|
||||
agendaToday: {
|
||||
time: string;
|
||||
patientName: string;
|
||||
patientId: string;
|
||||
type: string;
|
||||
}[];
|
||||
recentIntents: {
|
||||
intent: CortexIntent;
|
||||
patientName?: string;
|
||||
timestamp: Date;
|
||||
}[];
|
||||
userPreferences?: {
|
||||
confirmationLevel: 'always' | 'destructive' | 'never';
|
||||
frequentIntents: CortexIntent[];
|
||||
};
|
||||
}
|
||||
|
||||
/** Action status in an intent chain */
|
||||
export type IntentActionStatus =
|
||||
| 'pending'
|
||||
| 'confirming'
|
||||
| 'executing'
|
||||
| 'success'
|
||||
| 'failed'
|
||||
| 'skipped';
|
||||
|
||||
/** Single action in a multi-intent chain */
|
||||
export interface IntentAction {
|
||||
id: string;
|
||||
sequence: number;
|
||||
intent: CortexIntent;
|
||||
confidence: number;
|
||||
entities: ExtractedEntities;
|
||||
status: IntentActionStatus;
|
||||
requiresConfirmation: boolean;
|
||||
confirmationMessage?: string;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
recoverable: boolean;
|
||||
};
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
/** Chain status */
|
||||
export type IntentChainStatus =
|
||||
| 'pending'
|
||||
| 'executing'
|
||||
| 'completed'
|
||||
| 'partial'
|
||||
| 'failed';
|
||||
|
||||
/** Multi-intent container from single user input */
|
||||
export interface IntentChain {
|
||||
id: string;
|
||||
originalInput: string;
|
||||
createdAt: Date;
|
||||
actions: IntentAction[];
|
||||
status: IntentChainStatus;
|
||||
meta: {
|
||||
source: 'local' | 'ai';
|
||||
processingTimeMs: number;
|
||||
aiReasoning?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Layer 3: Nudge Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Nudge suggestion priority */
|
||||
export type NudgePriority = 'low' | 'medium' | 'high';
|
||||
|
||||
/** Nudge suggestion status */
|
||||
export type NudgeStatus = 'pending' | 'accepted' | 'dismissed' | 'expired';
|
||||
|
||||
/** Proactive suggestion after action completion */
|
||||
export interface NudgeSuggestion {
|
||||
id: string;
|
||||
trigger: {
|
||||
actionId: string;
|
||||
intent: CortexIntent;
|
||||
entities: ExtractedEntities;
|
||||
};
|
||||
suggestion: {
|
||||
intent: CortexIntent;
|
||||
entities: Partial<ExtractedEntities>;
|
||||
message: string;
|
||||
rationale: string;
|
||||
};
|
||||
status: NudgeStatus;
|
||||
priority: NudgePriority;
|
||||
expiresAt?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Clarification Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Clarification request when AI needs user input */
|
||||
export interface ClarificationRequest {
|
||||
question: string;
|
||||
options: string[];
|
||||
originalInput: string;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,58 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import type { Database } from '@/lib/supabase/database.types';
|
||||
import type { VerpleegkundigCategory } from '@/lib/types/report';
|
||||
|
||||
// Import shared types from lib/cortex (DRY - single source of truth)
|
||||
import {
|
||||
getCurrentShift,
|
||||
type CortexIntent,
|
||||
type ShiftType,
|
||||
type ExtractedEntities,
|
||||
type CortexContext,
|
||||
type IntentChain,
|
||||
type IntentAction,
|
||||
type NudgeSuggestion,
|
||||
type ClarificationRequest,
|
||||
} from '@/lib/cortex/types';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export type { CortexIntent, ShiftType };
|
||||
|
||||
// Database types
|
||||
export type Patient = Database['public']['Tables']['patients']['Row'];
|
||||
|
||||
// Cortex-specific types
|
||||
export type ShiftType = 'nacht' | 'ochtend' | 'middag' | 'avond';
|
||||
|
||||
export type CortexIntent =
|
||||
| 'dagnotitie'
|
||||
| 'zoeken'
|
||||
| 'overdracht'
|
||||
| 'agenda_query'
|
||||
| 'create_appointment'
|
||||
| 'cancel_appointment'
|
||||
| 'reschedule_appointment'
|
||||
| 'unknown';
|
||||
|
||||
// Store-specific types (not in lib/cortex/types.ts)
|
||||
export type BlockType = Exclude<CortexIntent, 'unknown'> | 'fallback' | 'patient-dashboard';
|
||||
|
||||
// Chat entities - simplified version for AI responses (strings, not Dates)
|
||||
// This differs from ExtractedEntities in lib/cortex/types.ts which uses Date objects
|
||||
// All properties are optional to match Zod schema flexibility
|
||||
export interface ChatEntities {
|
||||
patientName?: string;
|
||||
patientId?: string;
|
||||
category?: 'medicatie' | 'adl' | 'gedrag' | 'incident' | 'observatie';
|
||||
content?: string;
|
||||
query?: string;
|
||||
date?: string;
|
||||
time?: string;
|
||||
dateRange?: {
|
||||
start: string;
|
||||
end: string;
|
||||
label: string;
|
||||
};
|
||||
datetime?: {
|
||||
date?: string;
|
||||
time?: string;
|
||||
};
|
||||
appointmentType?: string;
|
||||
location?: string;
|
||||
identifier?: string;
|
||||
newDatetime?: {
|
||||
date?: string;
|
||||
time?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Chat types (v3.0)
|
||||
export type ChatMessageType = 'user' | 'assistant' | 'system' | 'error';
|
||||
|
||||
@@ -29,12 +61,12 @@ export interface ChatMessage {
|
||||
type: ChatMessageType;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
action?: ChatAction; // Optional action attached to assistant messages
|
||||
action?: ChatAction;
|
||||
}
|
||||
|
||||
export interface ChatAction {
|
||||
intent: CortexIntent;
|
||||
entities: ExtractedEntities;
|
||||
entities: ChatEntities;
|
||||
confidence: number;
|
||||
artifact?: {
|
||||
type: BlockType;
|
||||
@@ -42,20 +74,8 @@ export interface ChatAction {
|
||||
};
|
||||
}
|
||||
|
||||
// Extracted entities from user input
|
||||
export interface ExtractedEntities {
|
||||
patientName?: string;
|
||||
patientId?: string;
|
||||
category?: VerpleegkundigCategory;
|
||||
content?: string;
|
||||
query?: string;
|
||||
date?: string;
|
||||
time?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
// Block prefill data
|
||||
export interface BlockPrefillData extends ExtractedEntities {
|
||||
// Block prefill data - uses ChatEntities (string-based) for UI prefilling
|
||||
export interface BlockPrefillData extends ChatEntities {
|
||||
// Additional prefill data specific to blocks
|
||||
}
|
||||
|
||||
@@ -104,6 +124,19 @@ interface CortexStore {
|
||||
openArtifacts: Artifact[];
|
||||
activeArtifactId: string | null;
|
||||
|
||||
// V2 Context state
|
||||
context: CortexContext | null;
|
||||
|
||||
// V2 Intent Chain state
|
||||
activeChain: IntentChain | null;
|
||||
chainHistory: IntentChain[];
|
||||
|
||||
// V2 Nudge Suggestions state
|
||||
suggestions: NudgeSuggestion[];
|
||||
|
||||
// V2 Clarification state
|
||||
pendingClarification: ClarificationRequest | null;
|
||||
|
||||
// Context actions
|
||||
setActivePatient: (patient: Patient | null) => void;
|
||||
setShift: (shift: ShiftType) => void;
|
||||
@@ -134,19 +167,32 @@ interface CortexStore {
|
||||
setStreaming: (streaming: boolean) => void;
|
||||
setPendingAction: (action: ChatAction | null) => void;
|
||||
|
||||
// V2 Context actions
|
||||
setContext: (context: CortexContext) => void;
|
||||
updateContext: (partial: Partial<CortexContext>) => void;
|
||||
|
||||
// V2 Chain actions
|
||||
startChain: (chain: IntentChain) => void;
|
||||
updateActionStatus: (
|
||||
actionId: string,
|
||||
status: IntentAction['status'],
|
||||
error?: IntentAction['error']
|
||||
) => void;
|
||||
completeChain: () => void;
|
||||
|
||||
// V2 Nudge actions
|
||||
addSuggestion: (suggestion: NudgeSuggestion) => void;
|
||||
acceptSuggestion: (suggestionId: string) => void;
|
||||
dismissSuggestion: (suggestionId: string) => void;
|
||||
|
||||
// V2 Clarification actions
|
||||
setPendingClarification: (clarification: ClarificationRequest | null) => void;
|
||||
resolveClarification: (selectedOption: string) => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// Helper to calculate current shift based on time
|
||||
function getCurrentShift(): ShiftType {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 0 && hour < 7) return 'nacht';
|
||||
if (hour >= 7 && hour < 12) return 'ochtend';
|
||||
if (hour >= 12 && hour < 17) return 'middag';
|
||||
return 'avond';
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState = {
|
||||
activePatient: null,
|
||||
@@ -156,14 +202,20 @@ const initialState = {
|
||||
isBlockLoading: false,
|
||||
inputValue: '',
|
||||
isVoiceActive: false,
|
||||
recentActions: [],
|
||||
recentActions: [] as RecentAction[],
|
||||
// Chat state (v3.0)
|
||||
chatMessages: [],
|
||||
chatMessages: [] as ChatMessage[],
|
||||
isStreaming: false,
|
||||
pendingAction: null,
|
||||
pendingAction: null as ChatAction | null,
|
||||
// Artifact state (E4)
|
||||
openArtifacts: [],
|
||||
activeArtifactId: null,
|
||||
openArtifacts: [] as Artifact[],
|
||||
activeArtifactId: null as string | null,
|
||||
// V2 state
|
||||
context: null as CortexContext | null,
|
||||
activeChain: null as IntentChain | null,
|
||||
chainHistory: [] as IntentChain[],
|
||||
suggestions: [] as NudgeSuggestion[],
|
||||
pendingClarification: null as ClarificationRequest | null,
|
||||
};
|
||||
|
||||
// Create the store
|
||||
@@ -368,6 +420,122 @@ export const useCortexStore = create<CortexStore>()(
|
||||
);
|
||||
},
|
||||
|
||||
// V2 Context actions
|
||||
setContext: (context) => set({ context }, false, 'setContext'),
|
||||
|
||||
updateContext: (partial) =>
|
||||
set(
|
||||
(state) => ({
|
||||
context: state.context ? { ...state.context, ...partial } : null,
|
||||
}),
|
||||
false,
|
||||
'updateContext'
|
||||
),
|
||||
|
||||
// V2 Chain actions
|
||||
startChain: (chain) =>
|
||||
set(
|
||||
{
|
||||
activeChain: chain,
|
||||
},
|
||||
false,
|
||||
'startChain'
|
||||
),
|
||||
|
||||
updateActionStatus: (actionId, status, error) =>
|
||||
set(
|
||||
(state) => {
|
||||
if (!state.activeChain) return state;
|
||||
|
||||
const actions = state.activeChain.actions.map((action) =>
|
||||
action.id === actionId
|
||||
? {
|
||||
...action,
|
||||
status,
|
||||
error,
|
||||
...(status === 'executing' ? { startedAt: new Date() } : {}),
|
||||
...(status === 'success' || status === 'failed'
|
||||
? { completedAt: new Date() }
|
||||
: {}),
|
||||
}
|
||||
: action
|
||||
);
|
||||
|
||||
return {
|
||||
activeChain: {
|
||||
...state.activeChain,
|
||||
actions,
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'updateActionStatus'
|
||||
),
|
||||
|
||||
completeChain: () =>
|
||||
set(
|
||||
(state) => {
|
||||
if (!state.activeChain) return state;
|
||||
|
||||
// Move to history
|
||||
const completedChain: IntentChain = {
|
||||
...state.activeChain,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
return {
|
||||
activeChain: null,
|
||||
chainHistory: [completedChain, ...state.chainHistory].slice(0, 10), // Keep last 10
|
||||
};
|
||||
},
|
||||
false,
|
||||
'completeChain'
|
||||
),
|
||||
|
||||
// V2 Nudge actions
|
||||
addSuggestion: (suggestion) =>
|
||||
set(
|
||||
(state) => ({
|
||||
suggestions: [...state.suggestions, suggestion],
|
||||
}),
|
||||
false,
|
||||
'addSuggestion'
|
||||
),
|
||||
|
||||
acceptSuggestion: (suggestionId) =>
|
||||
set(
|
||||
(state) => ({
|
||||
suggestions: state.suggestions.map((s) =>
|
||||
s.id === suggestionId ? { ...s, status: 'accepted' as const } : s
|
||||
),
|
||||
}),
|
||||
false,
|
||||
'acceptSuggestion'
|
||||
),
|
||||
|
||||
dismissSuggestion: (suggestionId) =>
|
||||
set(
|
||||
(state) => ({
|
||||
suggestions: state.suggestions.filter((s) => s.id !== suggestionId),
|
||||
}),
|
||||
false,
|
||||
'dismissSuggestion'
|
||||
),
|
||||
|
||||
// V2 Clarification actions
|
||||
setPendingClarification: (clarification) =>
|
||||
set({ pendingClarification: clarification }, false, 'setPendingClarification'),
|
||||
|
||||
resolveClarification: (selectedOption) =>
|
||||
set(
|
||||
(state) => {
|
||||
console.log('[Store] Clarification resolved:', selectedOption);
|
||||
return { pendingClarification: null };
|
||||
},
|
||||
false,
|
||||
'resolveClarification'
|
||||
),
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState, false, 'reset'),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user