feat(cortex): Epic 3 - UI Components complete

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 <noreply@anthropic.com>
This commit is contained in:
colinislit
2026-01-01 12:02:15 +01:00
parent b36df63d03
commit 42d64761ec
7 changed files with 980 additions and 22 deletions

View File

@@ -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<HTMLDivElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(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 (
<div className="h-full flex flex-col bg-white">
{/* Chat messages area - scrollable */}
@@ -100,6 +160,42 @@ export function ChatPanel() {
{chatMessages.map((message) => (
<ChatMessage key={message.id} message={message} showTimestamp />
))}
{/* V2: Processing indicator while AI is thinking */}
{isStreaming && (
<div className="self-start">
<ProcessingIndicator message="Even nadenken..." />
</div>
)}
{/* V2: Multi-intent action chain */}
<AnimatePresence mode="wait">
{showActionChain && (
<ActionChainCard
key={activeChain.id}
chain={activeChain}
onConfirmAction={handleConfirmAction}
onSkipAction={handleSkipAction}
onRetryAction={handleRetryAction}
onDismissChain={handleDismissChain}
/>
)}
</AnimatePresence>
{/* V2: Clarification card for ambiguous input */}
<AnimatePresence mode="wait">
{pendingClarification && (
<ClarificationCard
key="clarification"
question={pendingClarification.question}
options={pendingClarification.options}
originalInput={pendingClarification.originalInput}
onSelectOption={handleSelectClarification}
onDismiss={handleDismissClarification}
/>
)}
</AnimatePresence>
{/* Invisible element to scroll to */}
<div ref={messagesEndRef} />
</div>