feat(cortex): update chat/nudge flow, deepgram streaming and agenda form
Refines chat panel, nudge messages and artifact rendering, reworks deepgram token/streaming handling, and adds test tooling deps (playwright, cypress) to package.json. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,19 @@
|
||||
* Story: E2.S4 (ChatInput component)
|
||||
*/
|
||||
|
||||
import { useState, useRef, KeyboardEvent, ChangeEvent, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Send, Mic } from 'lucide-react';
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
KeyboardEvent,
|
||||
ChangeEvent,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { Send, Mic, Square, Loader2 } from 'lucide-react';
|
||||
import { useCortexStore } from '@/stores/cortex-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCortexVoice } from '@/lib/cortex/use-cortex-voice';
|
||||
import { usePatientSelection } from '@/lib/cortex/hooks/use-patient-selection';
|
||||
import type { PatientSearchResult } from '@/lib/cortex/hooks/use-patient-search';
|
||||
import { PatientMentionDropdown } from '../command-center/patient-mention-dropdown';
|
||||
@@ -34,9 +43,19 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
onSend,
|
||||
disabled = false,
|
||||
}, ref) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const addChatMessage = useCortexStore((s) => s.addChatMessage);
|
||||
const inputValue = useCortexStore((s) => s.inputValue);
|
||||
const setInputValue = useCortexStore((s) => s.setInputValue);
|
||||
const clearInput = useCortexStore((s) => s.clearInput);
|
||||
const {
|
||||
isRecording,
|
||||
isConnecting,
|
||||
error: voiceError,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
isBrowserSupported,
|
||||
} = useCortexVoice();
|
||||
|
||||
// @mention state (E2)
|
||||
const [mentionState, setMentionState] = useState<{
|
||||
@@ -49,13 +68,19 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
showSuccessToast: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!textareaRef.current) return;
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||
}, [inputValue]);
|
||||
|
||||
// Expose focus, clear, and setValue methods to parent
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
textareaRef.current?.focus();
|
||||
},
|
||||
clear: () => {
|
||||
setInputValue('');
|
||||
clearInput();
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
@@ -135,11 +160,15 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
content: trimmedValue,
|
||||
});
|
||||
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
// Call optional onSend callback
|
||||
onSend?.(trimmedValue);
|
||||
|
||||
// Clear input
|
||||
setInputValue('');
|
||||
clearInput();
|
||||
|
||||
// Reset textarea height
|
||||
if (textareaRef.current) {
|
||||
@@ -165,7 +194,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
setMentionState(null);
|
||||
return;
|
||||
}
|
||||
setInputValue('');
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
}
|
||||
clearInput();
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
}
|
||||
@@ -174,6 +206,19 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
// Shift+Enter for new line (default behavior, no need to handle)
|
||||
};
|
||||
|
||||
const handleVoiceClick = async () => {
|
||||
if (disabled || isConnecting || !isBrowserSupported) return;
|
||||
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
textareaRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
await startRecording();
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-200 p-4 bg-white">
|
||||
<div className="relative flex items-end gap-2">
|
||||
@@ -208,20 +253,36 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
style={{ minHeight: '48px' }}
|
||||
/>
|
||||
|
||||
{/* Voice input button (placeholder for now) */}
|
||||
{/* Voice input button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleVoiceClick}
|
||||
className={cn(
|
||||
'absolute right-12 bottom-3',
|
||||
'text-slate-400 hover:text-slate-600',
|
||||
'transition-colors p-1.5 rounded-md hover:bg-slate-100',
|
||||
disabled && 'opacity-50 cursor-not-allowed'
|
||||
isRecording && 'text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100',
|
||||
isConnecting && 'text-amber-600 bg-amber-50',
|
||||
(disabled || !isBrowserSupported) && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
disabled={disabled}
|
||||
aria-label="Spraak invoer"
|
||||
title="Spraak invoer (komt in E5.S3)"
|
||||
disabled={disabled || isConnecting || !isBrowserSupported}
|
||||
aria-label={isRecording ? 'Stop spraakopname' : 'Start spraakopname'}
|
||||
aria-pressed={isRecording}
|
||||
title={
|
||||
!isBrowserSupported
|
||||
? 'Spraakopname wordt niet ondersteund in deze browser'
|
||||
: isRecording
|
||||
? 'Stop opname'
|
||||
: 'Spreek je Cortex opdracht in'
|
||||
}
|
||||
>
|
||||
<Mic className="w-5 h-5" />
|
||||
{isConnecting ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : isRecording ? (
|
||||
<Square className="w-5 h-5" />
|
||||
) : (
|
||||
<Mic className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Send button */}
|
||||
@@ -258,6 +319,16 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(function Ch
|
||||
</kbd>{' '}
|
||||
versturen
|
||||
</p>
|
||||
{isRecording && (
|
||||
<p className="mt-2 text-xs text-red-600">
|
||||
Opname actief. Spreek je opdracht in en druk daarna op Enter.
|
||||
</p>
|
||||
)}
|
||||
{voiceError && (
|
||||
<p className="mt-2 text-xs text-red-600">
|
||||
{voiceError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user