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:
151
components/cortex/chat/processing-indicator.tsx
Normal file
151
components/cortex/chat/processing-indicator.tsx
Normal file
@@ -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 (
|
||||
<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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user