From cf26022583e33791fb530e14937cb26ddcc4a969 Mon Sep 17 00:00:00 2001 From: colinislit Date: Wed, 31 Dec 2025 12:46:35 +0100 Subject: [PATCH] feat(cortex): Epic 0 - Foundation & Context voor V2 architectuur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/api/cortex/context/route.ts | 111 ++++++++++++++ lib/config/feature-flags.ts | 46 ++++++ lib/cortex/index.ts | 1 + lib/cortex/logger.ts | 160 ++++++++++++++++++++ lib/cortex/types.ts | 167 +++++++++++++++++++++ stores/cortex-store.ts | 256 ++++++++++++++++++++++++++------ 6 files changed, 697 insertions(+), 44 deletions(-) create mode 100644 app/api/cortex/context/route.ts create mode 100644 lib/config/feature-flags.ts create mode 100644 lib/cortex/logger.ts diff --git a/app/api/cortex/context/route.ts b/app/api/cortex/context/route.ts new file mode 100644 index 0000000..af04ba9 --- /dev/null +++ b/app/api/cortex/context/route.ts @@ -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 } + ); + } +} diff --git a/lib/config/feature-flags.ts b/lib/config/feature-flags.ts new file mode 100644 index 0000000..5d9ce43 --- /dev/null +++ b/lib/config/feature-flags.ts @@ -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) + ); +} diff --git a/lib/cortex/index.ts b/lib/cortex/index.ts index e3ce3fc..d4217a7 100644 --- a/lib/cortex/index.ts +++ b/lib/cortex/index.ts @@ -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'; diff --git a/lib/cortex/logger.ts b/lib/cortex/logger.ts new file mode 100644 index 0000000..cfc1be1 --- /dev/null +++ b/lib/cortex/logger.ts @@ -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}]` + ); +} diff --git a/lib/cortex/types.ts b/lib/cortex/types.ts index f62611a..de6bdbc 100644 --- a/lib/cortex/types.ts +++ b/lib/cortex/types.ts @@ -22,6 +22,15 @@ export type BlockType = Exclude | '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; + 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; +} diff --git a/stores/cortex-store.ts b/stores/cortex-store.ts index ca2adae..92f1805 100644 --- a/stores/cortex-store.ts +++ b/stores/cortex-store.ts @@ -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 | '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) => 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()( ); }, + // 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'), }),