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>
152 lines
3.3 KiB
TypeScript
152 lines
3.3 KiB
TypeScript
'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 (
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-2 text-slate-500',
|
|
className
|
|
)}
|
|
>
|
|
<Loader2 className={cn(sizeConfig.icon, 'animate-spin')} />
|
|
<span className={sizeConfig.text}>{message}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Skeleton variant - pulsing placeholder blocks
|
|
if (type === 'skeleton') {
|
|
return (
|
|
<div className={cn('space-y-2', className)}>
|
|
<div
|
|
className={cn(
|
|
sizeConfig.skeleton,
|
|
'bg-slate-200 rounded animate-pulse w-3/4'
|
|
)}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
sizeConfig.skeleton,
|
|
'bg-slate-200 rounded animate-pulse w-1/2'
|
|
)}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
sizeConfig.skeleton,
|
|
'bg-slate-200 rounded animate-pulse w-2/3'
|
|
)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Pulse variant - animated dots
|
|
return (
|
|
<div className={cn('flex items-center gap-1', className)}>
|
|
<div
|
|
className={cn(
|
|
sizeConfig.dot,
|
|
'bg-blue-500 rounded-full animate-pulse'
|
|
)}
|
|
style={{ animationDelay: '0ms' }}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
sizeConfig.dot,
|
|
'bg-blue-500 rounded-full animate-pulse'
|
|
)}
|
|
style={{ animationDelay: '150ms' }}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
sizeConfig.dot,
|
|
'bg-blue-500 rounded-full animate-pulse'
|
|
)}
|
|
style={{ animationDelay: '300ms' }}
|
|
/>
|
|
{message && (
|
|
<span className={cn(sizeConfig.text, 'text-slate-500 ml-2')}>
|
|
{message}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 (
|
|
<Loader2
|
|
className={cn(sizeConfig.icon, 'animate-spin text-current', className)}
|
|
/>
|
|
);
|
|
}
|