feat(cortex): add Intake Blocks MVP for voice/text commands
Adds 4 new intents for intake workflow: - intake_status: shows completion percentage and section checklist - intake_navigeer: navigation to specific intake tabs - risico_query: displays risk assessments with severity indicators - diagnose_query: shows primary/secondary diagnoses New components: - IntakeStatusBlock, RisicoBlock, DiagnoseBlock - Shared block components (BlockLoading, BlockError, BlockEmpty, BlockSection, BlockItem, BlockFooter) - useBlockData and useIntakeContext hooks New API routes: - GET /api/cortex/intake/status - GET /api/cortex/intake/risico - GET /api/cortex/intake/diagnose Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
103
components/cortex/shared/block-footer.tsx
Normal file
103
components/cortex/shared/block-footer.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* BlockFooter Component
|
||||
*
|
||||
* Footer with action buttons for Cortex blocks.
|
||||
* Secondary action on left, primary action on right.
|
||||
*
|
||||
* Epic: E1.S3 - Block Layout
|
||||
*/
|
||||
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
interface ActionConfig {
|
||||
/** Knop label */
|
||||
label: string;
|
||||
/** Optioneel icoon */
|
||||
icon?: LucideIcon;
|
||||
/** Click handler */
|
||||
onClick: () => void;
|
||||
/** Loading state (alleen voor primary) */
|
||||
loading?: boolean;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface BlockFooterProps {
|
||||
/** Linker actie (ghost button) */
|
||||
secondaryAction?: ActionConfig;
|
||||
/** Rechter actie (solid button) */
|
||||
primaryAction?: ActionConfig;
|
||||
/** Extra CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Footer for Cortex blocks with action buttons.
|
||||
* Returns null if no actions provided.
|
||||
*/
|
||||
export function BlockFooter({
|
||||
secondaryAction,
|
||||
primaryAction,
|
||||
className,
|
||||
}: BlockFooterProps) {
|
||||
// Don't render if no actions
|
||||
if (!secondaryAction && !primaryAction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between',
|
||||
'pt-4 mt-4 border-t border-slate-200',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Secondary Action (left) */}
|
||||
{secondaryAction ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={secondaryAction.onClick}
|
||||
disabled={secondaryAction.disabled}
|
||||
>
|
||||
{secondaryAction.icon && (
|
||||
<secondaryAction.icon className="h-4 w-4 mr-1.5" />
|
||||
)}
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
) : (
|
||||
<div /> // Spacer
|
||||
)}
|
||||
|
||||
{/* Primary Action (right) */}
|
||||
{primaryAction && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={primaryAction.onClick}
|
||||
disabled={primaryAction.loading || primaryAction.disabled}
|
||||
>
|
||||
{primaryAction.loading ? (
|
||||
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||
) : primaryAction.icon ? (
|
||||
<primaryAction.icon className="h-4 w-4 mr-1.5" />
|
||||
) : null}
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
128
components/cortex/shared/block-item.tsx
Normal file
128
components/cortex/shared/block-item.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* BlockItem Component
|
||||
*
|
||||
* List item for use within BlockSection.
|
||||
* Supports title, subtitle, badge, and click handling.
|
||||
*
|
||||
* Epic: E1.S2 - Block Layout
|
||||
*/
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type BadgeVariant = 'default' | 'success' | 'warning' | 'danger';
|
||||
|
||||
interface BlockItemProps {
|
||||
/** Hoofdtekst */
|
||||
title: string;
|
||||
/** Subtekst (datum, auteur, etc.) */
|
||||
subtitle?: string;
|
||||
/** Status badge */
|
||||
badge?: {
|
||||
label: string;
|
||||
variant: BadgeVariant;
|
||||
};
|
||||
/** Klik handler (maakt item klikbaar) */
|
||||
onClick?: () => void;
|
||||
/** Extra CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Styling
|
||||
// ============================================================================
|
||||
|
||||
const BADGE_STYLES: Record<BadgeVariant, string> = {
|
||||
default: 'bg-slate-100 text-slate-700 border-slate-200',
|
||||
success: 'bg-green-50 text-green-700 border-green-200',
|
||||
warning: 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
danger: 'bg-red-50 text-red-700 border-red-200',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* List item for Cortex blocks.
|
||||
* Renders as button when clickable, div otherwise.
|
||||
*/
|
||||
export function BlockItem({
|
||||
title,
|
||||
subtitle,
|
||||
badge,
|
||||
onClick,
|
||||
className,
|
||||
}: BlockItemProps) {
|
||||
const isClickable = Boolean(onClick);
|
||||
const Component = isClickable ? 'button' : 'div';
|
||||
|
||||
return (
|
||||
<Component
|
||||
type={isClickable ? 'button' : undefined}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex items-center justify-between p-3 rounded-lg',
|
||||
'bg-slate-50 border border-slate-200',
|
||||
'w-full text-left',
|
||||
isClickable && [
|
||||
'cursor-pointer',
|
||||
'hover:bg-slate-100 hover:border-slate-300',
|
||||
'transition-colors',
|
||||
],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-900 truncate">{title}</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-slate-500 mt-0.5 truncate">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badge */}
|
||||
{badge && (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-3 flex-shrink-0',
|
||||
'px-2 py-0.5 rounded-full',
|
||||
'text-xs font-medium border',
|
||||
BADGE_STYLES[badge.variant]
|
||||
)}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper: Get variant from risk level
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maps risk level string to badge variant.
|
||||
* Used for consistent risk display across blocks.
|
||||
*/
|
||||
export function getRiskBadgeVariant(level: string): BadgeVariant {
|
||||
switch (level.toLowerCase()) {
|
||||
case 'laag':
|
||||
return 'success';
|
||||
case 'gemiddeld':
|
||||
case 'matig':
|
||||
return 'warning';
|
||||
case 'hoog':
|
||||
case 'zeer_hoog':
|
||||
case 'zeer hoog':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
62
components/cortex/shared/block-section.tsx
Normal file
62
components/cortex/shared/block-section.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* BlockSection Component
|
||||
*
|
||||
* Groups related content within a Cortex block with a header.
|
||||
*
|
||||
* Epic: E1.S1 - Block Layout
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BlockSectionProps {
|
||||
/** Icoon component */
|
||||
icon: LucideIcon;
|
||||
/** Tailwind kleur class voor icoon */
|
||||
iconColor?: string;
|
||||
/** Sectie titel */
|
||||
title: string;
|
||||
/** Optionele count badge */
|
||||
count?: number;
|
||||
/** Sectie inhoud */
|
||||
children: ReactNode;
|
||||
/** Extra CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Section wrapper for Cortex blocks.
|
||||
* Shows icon + title header with optional count, wraps children content.
|
||||
*/
|
||||
export function BlockSection({
|
||||
icon: Icon,
|
||||
iconColor = 'text-slate-600',
|
||||
title,
|
||||
count,
|
||||
children,
|
||||
className,
|
||||
}: BlockSectionProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'bg-white rounded-lg border border-slate-200 p-4',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Icon className={cn('h-4 w-4', iconColor)} />
|
||||
<h3 className="text-sm font-medium text-slate-700">{title}</h3>
|
||||
{count !== undefined && (
|
||||
<span className="text-xs text-slate-500">({count})</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
136
components/cortex/shared/block-states.tsx
Normal file
136
components/cortex/shared/block-states.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Block State Components
|
||||
*
|
||||
* Reusable state components for Cortex blocks:
|
||||
* - BlockLoading: Loading spinner with message
|
||||
* - BlockError: Error state with retry option
|
||||
* - BlockEmpty: Empty state with action option
|
||||
*
|
||||
* Epic: E0 - Block States
|
||||
*/
|
||||
|
||||
import { Loader2, AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ============================================================================
|
||||
// BlockLoading (E0.S1)
|
||||
// ============================================================================
|
||||
|
||||
interface BlockLoadingProps {
|
||||
/** Tekst onder de spinner */
|
||||
message?: string;
|
||||
/** Extra CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading state for Cortex blocks.
|
||||
* Shows centered spinner with optional message.
|
||||
*/
|
||||
export function BlockLoading({
|
||||
message = 'Laden...',
|
||||
className,
|
||||
}: BlockLoadingProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center py-12',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mb-2" />
|
||||
<span className="text-sm text-slate-500">{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BlockError (E0.S2)
|
||||
// ============================================================================
|
||||
|
||||
interface BlockErrorProps {
|
||||
/** Foutmelding tekst */
|
||||
message: string;
|
||||
/** Callback voor retry knop (toont knop indien aanwezig) */
|
||||
onRetry?: () => void;
|
||||
/** Extra CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error state for Cortex blocks.
|
||||
* Shows error icon, message, and optional retry button.
|
||||
*/
|
||||
export function BlockError({
|
||||
message,
|
||||
onRetry,
|
||||
className,
|
||||
}: BlockErrorProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center py-8 text-center',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="h-8 w-8 text-red-500 mb-2" />
|
||||
<p className="text-sm text-red-700 mb-3 max-w-xs">{message}</p>
|
||||
{onRetry && (
|
||||
<Button variant="outline" size="sm" onClick={onRetry}>
|
||||
<RefreshCw className="h-4 w-4 mr-1.5" />
|
||||
Opnieuw proberen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BlockEmpty (E0.S3)
|
||||
// ============================================================================
|
||||
|
||||
interface BlockEmptyProps {
|
||||
/** Icoon component */
|
||||
icon: LucideIcon;
|
||||
/** Hoofdboodschap */
|
||||
message: string;
|
||||
/** Optionele actie knop */
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
/** Extra CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty state for Cortex blocks.
|
||||
* Shows icon, message, and optional action button.
|
||||
*/
|
||||
export function BlockEmpty({
|
||||
icon: Icon,
|
||||
message,
|
||||
action,
|
||||
className,
|
||||
}: BlockEmptyProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center py-8 text-center',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icon className="h-8 w-8 text-slate-300 mb-2" />
|
||||
<p className="text-sm text-slate-500 mb-3">{message}</p>
|
||||
{action && (
|
||||
<Button variant="outline" size="sm" onClick={action.onClick}>
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
components/cortex/shared/index.ts
Normal file
21
components/cortex/shared/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Cortex Shared Components
|
||||
*
|
||||
* Reusable components for Cortex blocks.
|
||||
*/
|
||||
|
||||
// Block States (E0)
|
||||
export { BlockLoading, BlockError, BlockEmpty } from './block-states';
|
||||
|
||||
// Block Layout (E1)
|
||||
export { BlockSection } from './block-section';
|
||||
export { BlockItem, getRiskBadgeVariant, type BadgeVariant } from './block-item';
|
||||
export { BlockFooter } from './block-footer';
|
||||
|
||||
// Patient Components (existing)
|
||||
export {
|
||||
PatientListItem,
|
||||
PatientListEmpty,
|
||||
PatientListLoading,
|
||||
} from './patient-list-item';
|
||||
export { LinkedEvidence } from './linked-evidence';
|
||||
Reference in New Issue
Block a user