chore: remove dead code command-input.tsx (376 lines)
Component was exported but never imported/used anywhere. v3.0 architecture uses chat-input.tsx instead. Voice features (useCortexVoice hook) remain available for future use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<HTMLInputElement>(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<HTMLCanvasElement>(null);
|
|
||||||
const animationRef = useRef<number | null>(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 (
|
|
||||||
<footer className="h-16 border-t border-slate-200 flex items-center px-4 shrink-0 bg-white">
|
|
||||||
<form onSubmit={handleSubmit} className="flex-1 flex items-center gap-2">
|
|
||||||
{/* Input wrapper with optional waveform and @mention dropdown */}
|
|
||||||
<div className="relative flex-1">
|
|
||||||
{/* @mention dropdown (E2.S2) */}
|
|
||||||
{mentionState && (
|
|
||||||
<PatientMentionDropdown
|
|
||||||
query={mentionState.query}
|
|
||||||
onSelect={handleMentionSelect}
|
|
||||||
onClose={() => setMentionState(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Waveform canvas (shown when recording) */}
|
|
||||||
{isRecording && (
|
|
||||||
<canvas
|
|
||||||
ref={waveformRef}
|
|
||||||
width={200}
|
|
||||||
height={40}
|
|
||||||
className="absolute left-2 top-1/2 -translate-y-1/2 rounded opacity-60"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => handleInputChange(e.target.value)}
|
|
||||||
placeholder={getPlaceholder()}
|
|
||||||
disabled={isDisabled}
|
|
||||||
className={`w-full bg-white border rounded-xl py-3 text-slate-900 placeholder:text-slate-400
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
|
||||||
transition-all duration-200
|
|
||||||
${isRecording ? 'pl-[220px] pr-12 border-red-500/50' : 'pl-4 pr-12 border-slate-300'}`}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Status indicators */}
|
|
||||||
{!hasValue && !isRecording && !isConnecting && (
|
|
||||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-600 pointer-events-none hidden sm:block">
|
|
||||||
⌘K
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isConnecting && (
|
|
||||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-amber-500 flex items-center gap-1">
|
|
||||||
<Loader2 size={12} className="animate-spin" />
|
|
||||||
Verbinden
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{voiceError && (
|
|
||||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-red-400 max-w-[150px] truncate">
|
|
||||||
{voiceError}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Send button - appears when has value */}
|
|
||||||
{hasValue && (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isProcessing}
|
|
||||||
className="p-3 rounded-xl bg-blue-600 hover:bg-blue-500 text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
title="Verstuur (Enter)"
|
|
||||||
>
|
|
||||||
{isProcessing ? (
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Send size={20} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Voice button */}
|
|
||||||
{isBrowserSupported ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleVoiceToggle}
|
|
||||||
disabled={isDisabled || isConnecting}
|
|
||||||
className={`p-3 rounded-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
||||||
isRecording
|
|
||||||
? 'bg-red-600 hover:bg-red-500 text-white ring-4 ring-red-600/30'
|
|
||||||
: 'bg-slate-100 hover:bg-slate-200 text-slate-600'
|
|
||||||
}`}
|
|
||||||
title={isRecording ? 'Stop opname' : 'Start voice input'}
|
|
||||||
>
|
|
||||||
{isConnecting ? (
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
) : isRecording ? (
|
|
||||||
<MicOff size={20} />
|
|
||||||
) : (
|
|
||||||
<Mic size={20} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled
|
|
||||||
className="p-3 rounded-xl bg-slate-100 text-slate-400 cursor-not-allowed"
|
|
||||||
title="Spraakherkenning niet ondersteund in deze browser"
|
|
||||||
>
|
|
||||||
<MicOff size={20} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { CommandCenter } from './command-center';
|
export { CommandCenter } from './command-center';
|
||||||
export { CommandInput } from './command-input';
|
|
||||||
export { ContextBar } from './context-bar';
|
export { ContextBar } from './context-bar';
|
||||||
export { CanvasArea } from './canvas-area';
|
export { CanvasArea } from './canvas-area';
|
||||||
export { RecentStrip } from './recent-strip';
|
export { RecentStrip } from './recent-strip';
|
||||||
|
|||||||
Reference in New Issue
Block a user