From 42d64761ec318a947b45951c98b6d8bc0ac1d3ed Mon Sep 17 00:00:00 2001 From: colinislit Date: Thu, 1 Jan 2026 12:02:15 +0100 Subject: [PATCH] feat(cortex): Epic 3 - UI Components complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Epic 3 delivers the visual layer for multi-intent flows and clarification: E3.S1 - ActionChainCard (3 SP) - Container for multi-intent chain display - Header with AI/Local source badge and action count - Original user input context - Stacked ActionItems with Framer Motion animations - Collapsible AI reasoning section - Status-dependent styling (pending/executing/completed/partial/failed) E3.S2 - ActionItem (2 SP) - 6 status icons (pending, confirming, executing, success, failed, skipped) - Dutch intent labels via shared intent-labels.ts - Confidence badge with color coding (green ≥90%, amber ≥70%, red <70%) - Entity summary formatting - Confirmation buttons for confirming status - Retry button for failed + recoverable actions E3.S3 - ClarificationCard (2 SP) - Amber-themed styling for question context - Header with HelpCircle icon - Original user input display - Responsive options grid (2 cols desktop, 1 col mobile) - Dismiss button and cancel link - Framer Motion entry/exit animations E3.S4 - ProcessingIndicator (1 SP) - 3 variants: spinner, skeleton, pulse - 3 sizes: sm, md, lg - Configurable message - InlineSpinner export for buttons Integration in ChatPanel (v4.0): - V2 store hooks for activeChain, pendingClarification - Event handlers for confirm/skip/retry/dismiss - Conditional rendering with AnimatePresence New files: - lib/cortex/intent-labels.ts - components/cortex/chat/action-chain-card.tsx - components/cortex/chat/action-item.tsx - components/cortex/chat/clarification-card.tsx - components/cortex/chat/processing-indicator.tsx Modified: - components/cortex/chat/chat-panel.tsx (v3.0 → v4.0) - docs/intent/bouwplan-cortex-v2.md (v1.2 → v1.3) Progress: 35/48 SP (73%) - E0, E1, E2, E3 complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- components/cortex/chat/action-chain-card.tsx | 229 ++++++++++++++++++ components/cortex/chat/action-item.tsx | 166 +++++++++++++ components/cortex/chat/chat-panel.tsx | 102 +++++++- components/cortex/chat/clarification-card.tsx | 133 ++++++++++ .../cortex/chat/processing-indicator.tsx | 151 ++++++++++++ docs/intent/bouwplan-cortex-v2.md | 39 +-- lib/cortex/intent-labels.ts | 182 ++++++++++++++ 7 files changed, 980 insertions(+), 22 deletions(-) create mode 100644 components/cortex/chat/action-chain-card.tsx create mode 100644 components/cortex/chat/action-item.tsx create mode 100644 components/cortex/chat/clarification-card.tsx create mode 100644 components/cortex/chat/processing-indicator.tsx create mode 100644 lib/cortex/intent-labels.ts diff --git a/components/cortex/chat/action-chain-card.tsx b/components/cortex/chat/action-chain-card.tsx new file mode 100644 index 0000000..81f6133 --- /dev/null +++ b/components/cortex/chat/action-chain-card.tsx @@ -0,0 +1,229 @@ +'use client'; + +/** + * ActionChainCard Component + * + * Container for displaying multi-intent action chains with: + * - Header showing source (AI/local) and action count + * - Original user input + * - Stacked ActionItem components for each action + * - Collapsible AI reasoning section + * - Dismiss button + * + * Epic: E3 (UI Components) + * Story: E3.S1 (ActionChainCard component) + */ + +import { useState } from 'react'; +import { Sparkles, ChevronDown, ChevronUp, X, Cpu } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import type { IntentChain } from '@/lib/cortex/types'; +import { ActionItem } from './action-item'; + +interface ActionChainCardProps { + chain: IntentChain; + onConfirmAction: (actionId: string) => void; + onSkipAction: (actionId: string) => void; + onRetryAction: (actionId: string) => void; + onDismissChain: () => void; +} + +/** + * Animation variants for the card 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 }, + }, +}; + +/** + * Stagger animation for action items + */ +const listVariants = { + animate: { + transition: { + staggerChildren: 0.05, + }, + }, +}; + +const itemVariants = { + initial: { opacity: 0, x: -10 }, + animate: { + opacity: 1, + x: 0, + transition: { duration: 0.15 }, + }, +}; + +export function ActionChainCard({ + chain, + onConfirmAction, + onSkipAction, + onRetryAction, + onDismissChain, +}: ActionChainCardProps) { + const [showReasoning, setShowReasoning] = useState(false); + + const actionCount = chain.actions.length; + const completedCount = chain.actions.filter( + (a) => a.status === 'success' || a.status === 'skipped' + ).length; + const isFromAI = chain.meta.source === 'ai'; + const hasReasoning = !!chain.meta.aiReasoning; + + // Determine overall chain state for styling + const getChainStatusStyle = () => { + switch (chain.status) { + case 'executing': + return 'border-blue-300 bg-blue-50/50'; + case 'completed': + return 'border-green-300 bg-green-50/50'; + case 'partial': + return 'border-amber-300 bg-amber-50/50'; + case 'failed': + return 'border-red-300 bg-red-50/50'; + default: + return 'border-slate-200 bg-white'; + } + }; + + return ( + + {/* Header */} +
+
+ {/* Source badge */} + {isFromAI ? ( +
+ + AI +
+ ) : ( +
+ + Lokaal +
+ )} + + {/* Action count */} + + {actionCount} {actionCount === 1 ? 'actie' : 'acties'} gedetecteerd + + + {/* Progress indicator */} + {chain.status === 'executing' && ( + + ({completedCount}/{actionCount} voltooid) + + )} +
+ + {/* Dismiss button */} + +
+ + {/* Original input */} +
+

+ “{chain.originalInput}” +

+
+ + {/* Action items */} + + + {chain.actions.map((action, index) => ( + + onConfirmAction(action.id)} + onSkip={() => onSkipAction(action.id)} + onRetry={() => onRetryAction(action.id)} + /> + + ))} + + + + {/* AI Reasoning (collapsible) */} + {hasReasoning && ( +
+ + + + {showReasoning && ( + +
+ {chain.meta.aiReasoning} +
+
+ )} +
+
+ )} + + {/* Processing time footer */} +
+

+ Verwerkt in {chain.meta.processingTimeMs}ms +

+
+
+ ); +} diff --git a/components/cortex/chat/action-item.tsx b/components/cortex/chat/action-item.tsx new file mode 100644 index 0000000..457c99c --- /dev/null +++ b/components/cortex/chat/action-item.tsx @@ -0,0 +1,166 @@ +'use client'; + +/** + * ActionItem Component + * + * Displays a single action in a multi-intent chain with: + * - Sequence number badge + * - Status icon (pending, confirming, executing, success, failed, skipped) + * - Intent label (Dutch) + * - Confidence badge with color coding + * - Entity summary + * - Confirmation/retry buttons + * + * Epic: E3 (UI Components) + * Story: E3.S2 (ActionItem sub-component) + */ + +import { + Circle, + AlertCircle, + Loader2, + Check, + X, + RotateCcw, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import type { IntentAction, IntentActionStatus } from '@/lib/cortex/types'; +import { + INTENT_LABELS, + STATUS_STYLES, + getConfidenceStyle, + formatEntitySummary, +} from '@/lib/cortex/intent-labels'; + +interface ActionItemProps { + action: IntentAction; + sequence: number; + totalActions: number; + onConfirm: () => void; + onSkip: () => void; + onRetry: () => void; +} + +/** + * Status icons mapped to each action status + */ +const STATUS_ICONS: Record = { + pending: , + confirming: , + executing: , + success: , + failed: , + skipped: , +}; + +export function ActionItem({ + action, + sequence, + totalActions, + onConfirm, + onSkip, + onRetry, +}: ActionItemProps) { + const statusStyle = STATUS_STYLES[action.status]; + const confidenceStyle = getConfidenceStyle(action.confidence); + const intentLabel = INTENT_LABELS[action.intent] || action.intent; + const entitySummary = formatEntitySummary(action.intent, action.entities); + + const isConfirming = action.status === 'confirming'; + const isFailed = action.status === 'failed'; + const isCompleted = action.status === 'success' || action.status === 'skipped'; + + return ( +
+ {/* Main row: sequence, icon, label, confidence */} +
+ {/* Sequence badge */} +
+ {sequence} +
+ + {/* Status icon */} +
+ {STATUS_ICONS[action.status]} +
+ + {/* Intent label */} +
+ + {intentLabel} + +
+ + {/* Confidence badge */} +
+ {Math.round(action.confidence * 100)}% +
+
+ + {/* Entity summary */} +
{entitySummary}
+ + {/* Confirmation message and buttons */} + {isConfirming && ( +
+ {action.confirmationMessage && ( +

{action.confirmationMessage}

+ )} +
+ + +
+
+ )} + + {/* Error state with retry option */} + {isFailed && action.error && ( +
+

{action.error.message}

+ {action.error.recoverable && ( + + )} +
+ )} + + {/* Completion time (optional, for completed actions) */} + {isCompleted && action.completedAt && ( +
+ {action.status === 'success' ? 'Voltooid' : 'Overgeslagen'} +
+ )} +
+ ); +} diff --git a/components/cortex/chat/chat-panel.tsx b/components/cortex/chat/chat-panel.tsx index 1f523cd..dfbe211 100644 --- a/components/cortex/chat/chat-panel.tsx +++ b/components/cortex/chat/chat-panel.tsx @@ -1,24 +1,30 @@ 'use client'; /** - * Chat Panel (v3.0) + * Chat Panel (v4.0) * * Chat interface met scrollable message list, auto-scroll, en scroll-lock detection. + * V2 integratie: ActionChainCard, ClarificationCard, ProcessingIndicator * - * Epic: E2 (Chat Panel & Messages) - * Story: E2.S3 (ChatPanel component - scrolling) + * Epic: E2 (Chat Panel & Messages), E3 (UI Components) + * Story: E2.S3 (ChatPanel component - scrolling), E3 Integration */ import { useRef, useEffect, useState, useCallback } from 'react'; import { ArrowDown } from 'lucide-react'; +import { AnimatePresence } from 'framer-motion'; import { ChatMessage } from './chat-message'; import { ChatInput, ChatInputHandle } from './chat-input'; +import { ActionChainCard } from './action-chain-card'; +import { ClarificationCard } from './clarification-card'; +import { ProcessingIndicator } from './processing-indicator'; import { useCortexStore } from '@/stores/cortex-store'; import { sendChatMessage } from '@/lib/cortex/chat-api'; import { parseActionFromResponse, shouldOpenArtifact } from '@/lib/cortex/action-parser'; import { cn } from '@/lib/utils'; export function ChatPanel() { + // Chat state const chatMessages = useCortexStore((s) => s.chatMessages); const addChatMessage = useCortexStore((s) => s.addChatMessage); const updateLastMessage = useCortexStore((s) => s.updateLastMessage); @@ -28,6 +34,16 @@ export function ChatPanel() { const activePatient = useCortexStore((s) => s.activePatient); const shift = useCortexStore((s) => s.shift); + // V2 Chain state + const activeChain = useCortexStore((s) => s.activeChain); + const updateActionStatus = useCortexStore((s) => s.updateActionStatus); + const completeChain = useCortexStore((s) => s.completeChain); + + // V2 Clarification state + const pendingClarification = useCortexStore((s) => s.pendingClarification); + const setPendingClarification = useCortexStore((s) => s.setPendingClarification); + const resolveClarification = useCortexStore((s) => s.resolveClarification); + // Refs for scrolling const scrollContainerRef = useRef(null); const messagesEndRef = useRef(null); @@ -87,6 +103,50 @@ export function ChatPanel() { return () => window.removeEventListener('keydown', handleGlobalKeyDown); }, []); + // V2 Chain action handlers + const handleConfirmAction = useCallback((actionId: string) => { + console.log('[ChatPanel] Confirming action:', actionId); + updateActionStatus(actionId, 'executing'); + // TODO: E5.S2 - Execute the actual action via API + // For now, simulate success after a short delay + setTimeout(() => { + updateActionStatus(actionId, 'success'); + }, 500); + }, [updateActionStatus]); + + const handleSkipAction = useCallback((actionId: string) => { + console.log('[ChatPanel] Skipping action:', actionId); + updateActionStatus(actionId, 'skipped'); + }, [updateActionStatus]); + + const handleRetryAction = useCallback((actionId: string) => { + console.log('[ChatPanel] Retrying action:', actionId); + updateActionStatus(actionId, 'pending'); + // Re-trigger confirmation flow + setTimeout(() => { + updateActionStatus(actionId, 'confirming'); + }, 100); + }, [updateActionStatus]); + + const handleDismissChain = useCallback(() => { + console.log('[ChatPanel] Dismissing chain'); + completeChain(); + }, [completeChain]); + + const handleSelectClarification = useCallback((option: string) => { + console.log('[ChatPanel] Clarification selected:', option); + resolveClarification(option); + // TODO: E5 - Re-process with selected option + }, [resolveClarification]); + + const handleDismissClarification = useCallback(() => { + console.log('[ChatPanel] Clarification dismissed'); + setPendingClarification(null); + }, [setPendingClarification]); + + // Check if we should show multi-intent UI + const showActionChain = activeChain && activeChain.actions.length > 1; + return (
{/* Chat messages area - scrollable */} @@ -100,6 +160,42 @@ export function ChatPanel() { {chatMessages.map((message) => ( ))} + + {/* V2: Processing indicator while AI is thinking */} + {isStreaming && ( +
+ +
+ )} + + {/* V2: Multi-intent action chain */} + + {showActionChain && ( + + )} + + + {/* V2: Clarification card for ambiguous input */} + + {pendingClarification && ( + + )} + + {/* Invisible element to scroll to */}
diff --git a/components/cortex/chat/clarification-card.tsx b/components/cortex/chat/clarification-card.tsx new file mode 100644 index 0000000..a6b4092 --- /dev/null +++ b/components/cortex/chat/clarification-card.tsx @@ -0,0 +1,133 @@ +'use client'; + +/** + * ClarificationCard Component + * + * Displays clarification questions from the AI when user input is ambiguous. + * Shows: + * - Question text + * - Multiple choice options as buttons + * - Original input for context + * - Dismiss option + * + * Epic: E3 (UI Components) + * Story: E3.S3 (ClarificationCard component) + */ + +import { HelpCircle, X } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface ClarificationCardProps { + question: string; + options: string[]; + originalInput: string; + onSelectOption: (option: string) => void; + onDismiss: () => void; +} + +/** + * Animation variants for the card + */ +const cardVariants = { + initial: { opacity: 0, y: 10, scale: 0.98 }, + animate: { + opacity: 1, + y: 0, + scale: 1, + transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] as const }, + }, + exit: { + opacity: 0, + y: -5, + scale: 0.98, + transition: { duration: 0.15 }, + }, +}; + +export function ClarificationCard({ + question, + options, + originalInput, + onSelectOption, + onDismiss, +}: ClarificationCardProps) { + return ( + + {/* Header */} +
+
+
+ +
+ + Verduidelijking nodig + +
+ + {/* Dismiss button */} + +
+ + {/* Content */} +
+ {/* Original input context */} +
+ Je zei:{' '} + “{originalInput}” +
+ + {/* Question */} +

{question}

+ + {/* Options grid */} +
+ {options.map((option, index) => ( + + ))} +
+ + {/* Cancel link */} +
+ +
+
+
+ ); +} diff --git a/components/cortex/chat/processing-indicator.tsx b/components/cortex/chat/processing-indicator.tsx new file mode 100644 index 0000000..624f8b2 --- /dev/null +++ b/components/cortex/chat/processing-indicator.tsx @@ -0,0 +1,151 @@ +'use client'; + +/** + * ProcessingIndicator Component + * + * Shows loading state during AI operations with multiple variants: + * - spinner: Rotating loader icon with text (default) + * - skeleton: Pulsing placeholder blocks + * - pulse: Animated dots + * + * Epic: E3 (UI Components) + * Story: E3.S4 (Processing indicator) + */ + +import { Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface ProcessingIndicatorProps { + /** Loading message to display (default: "Even nadenken...") */ + message?: string; + /** Visual style variant */ + type?: 'spinner' | 'skeleton' | 'pulse'; + /** Size of the indicator */ + size?: 'sm' | 'md' | 'lg'; + /** Additional CSS classes */ + className?: string; +} + +/** + * Size configurations for each variant + */ +const SIZE_CONFIG = { + sm: { + icon: 'w-3 h-3', + text: 'text-xs', + dot: 'w-1.5 h-1.5', + skeleton: 'h-3', + }, + md: { + icon: 'w-4 h-4', + text: 'text-sm', + dot: 'w-2 h-2', + skeleton: 'h-4', + }, + lg: { + icon: 'w-5 h-5', + text: 'text-base', + dot: 'w-2.5 h-2.5', + skeleton: 'h-5', + }, +}; + +export function ProcessingIndicator({ + message = 'Even nadenken...', + type = 'spinner', + size = 'md', + className, +}: ProcessingIndicatorProps) { + const sizeConfig = SIZE_CONFIG[size]; + + // Spinner variant - rotating icon with text + if (type === 'spinner') { + return ( +
+ + {message} +
+ ); + } + + // Skeleton variant - pulsing placeholder blocks + if (type === 'skeleton') { + return ( +
+
+
+
+
+ ); + } + + // Pulse variant - animated dots + return ( +
+
+
+
+ {message && ( + + {message} + + )} +
+ ); +} + +/** + * Inline spinner for use in buttons or compact spaces + */ +export function InlineSpinner({ + size = 'sm', + className, +}: { + size?: 'sm' | 'md' | 'lg'; + className?: string; +}) { + const sizeConfig = SIZE_CONFIG[size]; + return ( + + ); +} diff --git a/docs/intent/bouwplan-cortex-v2.md b/docs/intent/bouwplan-cortex-v2.md index d181f67..ca287a0 100644 --- a/docs/intent/bouwplan-cortex-v2.md +++ b/docs/intent/bouwplan-cortex-v2.md @@ -1,7 +1,7 @@ # Bouwplan — Cortex Intent System V2 **Projectnaam:** Cortex V2 - Agentic Intent Architecture -**Versie:** v1.2 +**Versie:** v1.3 **Datum:** 01-01-2026 **Auteur:** Colin Lit @@ -134,7 +134,7 @@ lib/config/ | **E0** | Foundation & Context | Types, API, feature flags | ✅ Done | 5 | 8 SP | | **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 | ⏳ To Do | 4 | 8 SP | +| **E3** | UI Components | ActionChainCard, ClarificationCard | ✅ Done | 4 | 8 SP | | **E4** | Nudge MVP (Layer 3) | Proactieve suggesties | ⏳ To Do | 3 | 5 SP | | **E5** | Integration & Polish | End-to-end flow, testing | ⏳ To Do | 4 | 8 SP | @@ -679,10 +679,10 @@ try { | Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | |----------|--------------|---------------------|--------|------------------|----| -| E3.S1 | **ActionChainCard** component | Stacked cards met sequence, status, confidence | ⏳ | E0.S1 | 3 | -| E3.S2 | **ActionItem** sub-component | Status icons, confirmation buttons, error states | ⏳ | E3.S1 | 2 | -| E3.S3 | **ClarificationCard** component | Vraag + keuze-knoppen bij ambigue input | ⏳ | — | 2 | -| E3.S4 | **Processing indicator** | Spinner/skeleton bij AI-acties | ⏳ | — | 1 | +| E3.S1 | **ActionChainCard** component | Stacked cards met sequence, status, confidence | ✅ | E0.S1 | 3 | +| E3.S2 | **ActionItem** sub-component | Status icons, confirmation buttons, error states | ✅ | E3.S1 | 2 | +| E3.S3 | **ClarificationCard** component | Vraag + keuze-knoppen bij ambigue input | ✅ | — | 2 | +| E3.S4 | **Processing indicator** | Spinner/skeleton bij AI-acties | ✅ | — | 1 | **Deliverable:** Multi-intent flows visueel weergegeven @@ -736,9 +736,9 @@ interface ActionChainCardProps { ``` *Done criteria:* -- [ ] Toont alle acties in chain -- [ ] Header toont aantal acties -- [ ] AI reasoning collapsible (details/summary) +- [x] Toont alle acties in chain +- [x] Header toont aantal acties +- [x] AI reasoning collapsible (details/summary) --- @@ -777,10 +777,10 @@ const INTENT_LABELS = { ``` *Done criteria:* -- [ ] Status icon correct per status -- [ ] Confidence badge met juiste kleur -- [ ] Confirmation buttons bij `confirming` status -- [ ] Retry button bij `failed` status +- [x] Status icon correct per status +- [x] Confidence badge met juiste kleur +- [x] Confirmation buttons bij `confirming` status +- [x] Retry button bij `failed` status --- @@ -808,9 +808,9 @@ interface ClarificationCardProps { ``` *Done criteria:* -- [ ] Vraag duidelijk zichtbaar -- [ ] Knoppen voor elke optie -- [ ] Click triggert `onSelect` callback +- [x] Vraag duidelijk zichtbaar +- [x] Knoppen voor elke optie +- [x] Click triggert `onSelect` callback --- @@ -836,9 +836,9 @@ interface ClarificationCardProps { ``` *Done criteria:* -- [ ] Indicator zichtbaar tijdens AI call -- [ ] Verdwijnt zodra response binnen is -- [ ] Geen "frozen" UI gevoel +- [x] Indicator zichtbaar tijdens AI call +- [x] Verdwijnt zodra response binnen is +- [x] Geen "frozen" UI gevoel --- @@ -1398,3 +1398,4 @@ De MVP User Stories uit `mvp-userstories-intent-system.md` zijn als volgt verdee | v1.0 | 30-12-2025 | Colin Lit | Initiële versie op basis van PRD, FO, TO, Architecture docs | | 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 | diff --git a/lib/cortex/intent-labels.ts b/lib/cortex/intent-labels.ts new file mode 100644 index 0000000..88b38cd --- /dev/null +++ b/lib/cortex/intent-labels.ts @@ -0,0 +1,182 @@ +/** + * Intent Labels & Status Styles + * + * Shared Dutch labels and styling for Cortex V2 UI components. + * Used by ActionChainCard, ActionItem, and ClarificationCard. + * + * Epic: E3 (UI Components) + */ + +import type { CortexIntent, IntentActionStatus, NudgePriority, ExtractedEntities } from './types'; + +/** + * Dutch labels for intent types + */ +export const INTENT_LABELS: Record = { + dagnotitie: 'Dagnotitie', + zoeken: 'Patiënt zoeken', + overdracht: 'Overdracht', + agenda_query: 'Agenda bekijken', + create_appointment: 'Afspraak maken', + cancel_appointment: 'Afspraak annuleren', + reschedule_appointment: 'Afspraak verzetten', + unknown: 'Onbekend', +}; + +/** + * Status styling for IntentAction items + */ +export const STATUS_STYLES: Record< + IntentActionStatus, + { bg: string; border: string; text: string; icon: string } +> = { + pending: { + bg: 'bg-slate-50', + border: 'border-slate-200', + text: 'text-slate-700', + icon: 'text-slate-300', + }, + confirming: { + bg: 'bg-amber-50', + border: 'border-amber-200', + text: 'text-amber-700', + icon: 'text-amber-500', + }, + executing: { + bg: 'bg-blue-50', + border: 'border-blue-200', + text: 'text-blue-700', + icon: 'text-blue-500', + }, + success: { + bg: 'bg-green-50', + border: 'border-green-200', + text: 'text-green-700', + icon: 'text-green-500', + }, + failed: { + bg: 'bg-red-50', + border: 'border-red-200', + text: 'text-red-700', + icon: 'text-red-500', + }, + skipped: { + bg: 'bg-slate-50', + border: 'border-slate-200', + text: 'text-slate-400', + icon: 'text-slate-400', + }, +}; + +/** + * Get confidence badge styling based on confidence score + * >= 0.9: green (high confidence) + * >= 0.7: amber (medium confidence) + * < 0.7: red (low confidence) + */ +export function getConfidenceStyle(confidence: number): { + bg: string; + text: string; + label: string; +} { + if (confidence >= 0.9) { + return { + bg: 'bg-green-50', + text: 'text-green-700', + label: 'Hoog', + }; + } + if (confidence >= 0.7) { + return { + bg: 'bg-amber-50', + text: 'text-amber-700', + label: 'Gemiddeld', + }; + } + return { + bg: 'bg-red-50', + text: 'text-red-700', + label: 'Laag', + }; +} + +/** + * Priority styling for NudgeSuggestion toasts + */ +export const PRIORITY_STYLES: Record< + NudgePriority, + { bg: string; border: string; text: string } +> = { + low: { + bg: 'bg-blue-50', + border: 'border-blue-200', + text: 'text-blue-700', + }, + medium: { + bg: 'bg-amber-50', + border: 'border-amber-200', + text: 'text-amber-700', + }, + high: { + bg: 'bg-red-50', + border: 'border-red-200', + text: 'text-red-700', + }, +}; + +/** + * Format entity summary for display in ActionItem + */ +export function formatEntitySummary( + intent: CortexIntent, + entities: ExtractedEntities +): string { + const parts: string[] = []; + + // Patient name + if (entities.patientName) { + parts.push(`Patiënt: ${entities.patientName}`); + } + + // Content/query + if (entities.content && typeof entities.content === 'string') { + const truncated = + entities.content.length > 50 + ? `${entities.content.slice(0, 50)}...` + : entities.content; + parts.push(truncated); + } + + if (entities.query && typeof entities.query === 'string') { + parts.push(`Zoekterm: "${entities.query}"`); + } + + // Date/time + if (entities.datetime) { + const dt = entities.datetime; + // datetime.date is a Date object, format it + const dateStr = dt.date ? dt.date.toLocaleDateString('nl-NL') : null; + if (dateStr && dt.time) { + parts.push(`${dateStr} om ${dt.time}`); + } else if (dateStr) { + parts.push(dateStr); + } else if (dt.time) { + parts.push(`om ${dt.time}`); + } + } + + // Category for dagnotitie + if (intent === 'dagnotitie' && entities.category) { + const categoryLabels: Record = { + medicatie: 'Medicatie', + adl: 'ADL', + gedrag: 'Gedrag', + incident: 'Incident', + observatie: 'Observatie', + }; + const label = categoryLabels[entities.category as string] || entities.category; + parts.push(`Categorie: ${label}`); + } + + return parts.join(' • ') || 'Geen details'; +}