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:
colinislit
2026-02-03 23:21:18 +01:00
parent 05836c5c6a
commit b095b1e492
22 changed files with 2415 additions and 15 deletions

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

View 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';
}
}

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

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

View 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';