Files
triqura-ecd/components/cortex/chat/processing-indicator.tsx
colinislit 42d64761ec 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>
2026-01-01 12:02:15 +01:00

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)}
/>
);
}