diff --git a/components/cortex/command-center/command-input.tsx b/components/cortex/command-center/command-input.tsx deleted file mode 100644 index 5691bed..0000000 --- a/components/cortex/command-center/command-input.tsx +++ /dev/null @@ -1,375 +0,0 @@ -'use client'; - -/** - * Command Input - * - * Bottom input bar for text and voice commands. - * Height: 64px (h-16) - * - * Features: - * - Text input with dynamic placeholder - * - Focus state with ring - * - Send button (appears when input has value) - * - Voice input with Deepgram streaming - * - ⌘K shortcut hint - */ - -import { forwardRef, useState, useEffect, useRef } from 'react'; -import { useCortexStore } from '@/stores/cortex-store'; -import { useCortexVoice } from '@/lib/cortex/use-cortex-voice'; -import type { BlockType } from '@/lib/cortex/types'; -import { Mic, MicOff, Send, Loader2 } from 'lucide-react'; -import { useToast } from '@/hooks/use-toast'; -import { safeFetch, getErrorInfo } from '@/lib/cortex/error-handler'; -import { routeIntentToArtifact } from '@/lib/cortex/action-parser'; -import { usePatientSelection } from '@/lib/cortex/hooks/use-patient-selection'; -import type { PatientSearchResult } from '@/lib/cortex/hooks/use-patient-search'; -import { PatientMentionDropdown } from './patient-mention-dropdown'; - -export const CommandInput = forwardRef(function CommandInput(_, ref) { - const { - inputValue, - setInputValue, - clearInput, - activePatient, - activeBlock, - isVoiceActive, - openBlock, - openArtifact, - addRecentAction, - } = useCortexStore(); - const { toast } = useToast(); - - const { - isRecording, - isConnecting, - isConnected, - error: voiceError, - startRecording, - stopRecording, - analyserNode, - isBrowserSupported, - } = useCortexVoice(); - - const [isProcessing, setIsProcessing] = useState(false); - const waveformRef = useRef(null); - const animationRef = useRef(null); - - // @mention state (E2.S1) - const [mentionState, setMentionState] = useState<{ - query: string; - startIndex: number; - } | null>(null); - - // Patient selection hook (E2.S3) - const { selectPatient } = usePatientSelection({ - showSuccessToast: false, // Don't show toast for @mention selection - }); - - const hasValue = inputValue.trim().length > 0; - - // @mention detection (E2.S1) - const detectMention = (value: string) => { - const lastAtIndex = value.lastIndexOf('@'); - if (lastAtIndex !== -1) { - const afterAt = value.slice(lastAtIndex + 1); - // Active if: no space after @, and at least 1 char - if (!afterAt.includes(' ') && afterAt.length > 0) { - setMentionState({ query: afterAt, startIndex: lastAtIndex }); - return; - } - } - setMentionState(null); - }; - - // Handle input change with @mention detection - const handleInputChange = (value: string) => { - setInputValue(value); - detectMention(value); - }; - - // Handle @mention selection (E2.S3) - const handleMentionSelect = (patient: PatientSearchResult) => { - if (!mentionState) return; - - // 1. Replace @query with @name in input - const before = inputValue.slice(0, mentionState.startIndex); - const after = inputValue.slice(mentionState.startIndex + mentionState.query.length + 1); - const newValue = `${before}@${patient.name} ${after}`.trim(); - setInputValue(newValue); - - // 2. Set activePatient via selection hook - selectPatient(patient); - - // 3. Close dropdown - setMentionState(null); - }; - - // Waveform visualization - useEffect(() => { - if (!analyserNode || !waveformRef.current || !isRecording) { - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - animationRef.current = null; - } - return; - } - - const canvas = waveformRef.current; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - const bufferLength = analyserNode.frequencyBinCount; - const dataArray = new Uint8Array(bufferLength); - - const draw = () => { - if (!isRecording) return; - - animationRef.current = requestAnimationFrame(draw); - analyserNode.getByteFrequencyData(dataArray); - - ctx.fillStyle = 'rgb(248, 250, 252)'; // slate-50 - ctx.fillRect(0, 0, canvas.width, canvas.height); - - const barWidth = (canvas.width / bufferLength) * 2.5; - let x = 0; - - for (let i = 0; i < bufferLength; i++) { - const barHeight = (dataArray[i] / 255) * canvas.height; - - // Gradient from blue to red based on amplitude - const hue = 220 - (dataArray[i] / 255) * 40; - ctx.fillStyle = `hsl(${hue}, 70%, 60%)`; - - ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); - x += barWidth + 1; - } - }; - - draw(); - - return () => { - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - } - }; - }, [analyserNode, isRecording]); - - // Dynamic placeholder based on context - const getPlaceholder = () => { - if (isRecording) return 'Luisteren...'; - if (isConnecting) return 'Verbinden met spraakherkenning...'; - if (activePatient) { - return `Actie voor ${activePatient.name_given[0]}... (bijv. "notitie medicatie")`; - } - return 'Typ of spreek je intentie... (bijv. "notitie jan medicatie")'; - }; - - const handleSubmit = async (e?: React.FormEvent) => { - e?.preventDefault(); - if (!hasValue || isProcessing) return; - - // Stop recording if active - if (isRecording) { - stopRecording(); - } - - const inputText = inputValue.trim(); - setIsProcessing(true); - - try { - // Call intent classification API - const response = await safeFetch( - '/api/intent/classify', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ input: inputText }), - }, - { operation: 'Intent classificeren' } - ); - - const result = await response.json(); - const { intent, confidence, entities } = result; - - // Route intent to artifact using Epic 5.S1 routing logic - const artifactConfig = routeIntentToArtifact(intent, entities, confidence); - - if (artifactConfig) { - // Open artifact with routing configuration - openArtifact({ - type: artifactConfig.type, - title: artifactConfig.title, - prefill: artifactConfig.prefill, - }); - - // Add to recent actions - addRecentAction({ - intent, - label: inputText.slice(0, 50), // Truncate for display - patientName: entities.patientName, - }); - - clearInput(); - } else { - // Low confidence or missing required entities - show FallbackPicker - openBlock('fallback', { content: inputText }); - clearInput(); - } - } catch (error) { - console.error('Error processing intent:', error); - const statusCode = (error as any)?.statusCode; - const errorInfo = getErrorInfo(error, { - operation: 'Intent classificeren', - statusCode, - }); - - // Show error toast - toast({ - variant: 'destructive', - title: errorInfo.title, - description: errorInfo.description, - }); - - // On error, show FallbackPicker so user can choose - // This ensures the user's input is not lost - openBlock('fallback', { content: inputText }); - clearInput(); - } finally { - setIsProcessing(false); - } - }; - - const handleVoiceToggle = () => { - if (isRecording) { - stopRecording(); - } else { - startRecording(); - } - }; - - // Keyboard shortcut: Cmd/Ctrl+Enter to submit - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Cmd/Ctrl+Enter: submit command - if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { - e.preventDefault(); - handleSubmit(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [handleSubmit]); - - const isDisabled = isProcessing || activeBlock !== null; - - return ( - - ); -}); diff --git a/components/cortex/command-center/index.ts b/components/cortex/command-center/index.ts index d114e9d..b1dc6ba 100644 --- a/components/cortex/command-center/index.ts +++ b/components/cortex/command-center/index.ts @@ -3,7 +3,6 @@ */ export { CommandCenter } from './command-center'; -export { CommandInput } from './command-input'; export { ContextBar } from './context-bar'; export { CanvasArea } from './canvas-area'; export { RecentStrip } from './recent-strip';