({ input, setInput, onSubmit, status, stop, files, setFiles, disabled = false, placeholder = 'Send a message...', canSendOverride, bare = false, className, codeMode = false, codeModeSettings })
| 44 | } |
| 45 | |
| 46 | export function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, disabled = false, placeholder = 'Send a message...', canSendOverride, bare = false, className, codeMode = false, codeModeSettings }) { |
| 47 | const textareaRef = useRef(null); |
| 48 | const fileInputRef = useRef(null); |
| 49 | const [isDragging, setIsDragging] = useState(false); |
| 50 | const [modeDropdownOpen, setModeDropdownOpen] = useState(false); |
| 51 | const [agentPickerOpen, setAgentPickerOpen] = useState(false); |
| 52 | const [partialText, setPartialText] = useState(''); |
| 53 | const [secretsOpen, setSecretsOpen] = useState(false); |
| 54 | const dropdownRef = useRef(null); |
| 55 | const agentPickerRef = useRef(null); |
| 56 | const isStreaming = status === 'streaming' || status === 'submitted'; |
| 57 | const volumeRef = useRef(0); |
| 58 | |
| 59 | const { voiceAvailable, isConnecting, isRecording, startRecording, stopRecording } = useVoiceInput({ |
| 60 | getToken: getVoiceTokenFetch, |
| 61 | onVolumeChange: (rms) => { volumeRef.current = rms; }, |
| 62 | onTranscript: (text) => { |
| 63 | setInput((prev) => { |
| 64 | const needsSpace = prev && !prev.endsWith(' '); |
| 65 | return prev + (needsSpace ? ' ' : '') + text; |
| 66 | }); |
| 67 | }, |
| 68 | onPartialTranscript: (text) => setPartialText(text), |
| 69 | onError: (err) => console.error('[voice]', err), |
| 70 | }); |
| 71 | |
| 72 | // Auto-resize textarea |
| 73 | const adjustHeight = useCallback(() => { |
| 74 | const textarea = textareaRef.current; |
| 75 | if (!textarea) return; |
| 76 | textarea.style.height = 'auto'; |
| 77 | textarea.style.height = `${textarea.scrollHeight}px`; |
| 78 | textarea.scrollTop = textarea.scrollHeight; |
| 79 | }, []); |
| 80 | |
| 81 | useEffect(() => { |
| 82 | adjustHeight(); |
| 83 | }, [input, adjustHeight]); |
| 84 | |
| 85 | // Focus textarea on mount |
| 86 | useEffect(() => { |
| 87 | textareaRef.current?.focus(); |
| 88 | }, []); |
| 89 | |
| 90 | // Refocus textarea when streaming ends so the user can immediately type the next message |
| 91 | const wasStreaming = useRef(isStreaming); |
| 92 | useEffect(() => { |
| 93 | if (wasStreaming.current && !isStreaming) { |
| 94 | textareaRef.current?.focus(); |
| 95 | } |
| 96 | wasStreaming.current = isStreaming; |
| 97 | }, [isStreaming]); |
| 98 | |
| 99 | // Close dropdown on outside click |
| 100 | useEffect(() => { |
| 101 | if (!modeDropdownOpen) return; |
| 102 | const handleClickOutside = (e) => { |
| 103 | if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { |
nothing calls this directly
no test coverage detected