Spaces:
Running
Running
| import { useState, useRef, useEffect } from "react"; | |
| import { SendHorizontal, Square } from "lucide-react"; | |
| import type { VoiceState } from "../../../hooks/useVoiceSession"; | |
| import VoiceMicButton from "./VoiceMicButton"; | |
| interface ChatInputProps { | |
| onSend: (text: string) => void; | |
| onStop?: () => void; | |
| isLoading: boolean; | |
| disabled?: boolean; | |
| voiceState: VoiceState; | |
| onVoiceToggle: () => void; | |
| } | |
| export default function ChatInput({ | |
| onSend, | |
| onStop, | |
| isLoading, | |
| disabled, | |
| voiceState, | |
| onVoiceToggle, | |
| }: ChatInputProps) { | |
| const [value, setValue] = useState(""); | |
| const textareaRef = useRef<HTMLTextAreaElement>(null); | |
| const isVoiceActive = voiceState !== "IDLE" && voiceState !== "ERROR"; | |
| useEffect(() => { | |
| const el = textareaRef.current; | |
| if (!el) return; | |
| el.style.height = "auto"; | |
| el.style.height = Math.min(el.scrollHeight, 50) + "px"; | |
| }, [value]); | |
| const handleSend = () => { | |
| const trimmed = value.trim(); | |
| if (!trimmed || isLoading || disabled || isVoiceActive) return; | |
| onSend(trimmed); | |
| setValue(""); | |
| }; | |
| const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleSend(); | |
| } | |
| }; | |
| const canSend = value.trim().length > 0 && !disabled && !isVoiceActive; | |
| return ( | |
| <div | |
| className={`bg-white border rounded-2xl shadow-sm px-4 py-3 transition-all ${ | |
| disabled || isVoiceActive | |
| ? "opacity-80 border-neutral-200" | |
| : "border-neutral-200 focus-within:border-brand-green focus-within:shadow-md focus-within:shadow-brand-green/10" | |
| }`} | |
| > | |
| <div className="flex items-end gap-3"> | |
| <VoiceMicButton | |
| voiceState={voiceState} | |
| onToggle={onVoiceToggle} | |
| disabled={isLoading} | |
| /> | |
| <textarea | |
| ref={textareaRef} | |
| value={value} | |
| onChange={(e) => setValue(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder={ | |
| isVoiceActive | |
| ? "Voice session active — speak to interact" | |
| : "Ask me anything… (Enter to send, Shift+Enter for newline)" | |
| } | |
| disabled={isLoading || disabled || isVoiceActive} | |
| rows={1} | |
| className="flex-1 resize-none bg-transparent text-sm text-neutral-900 leading-6 placeholder:text-neutral-400 outline-none overflow-y-auto" | |
| style={{ minHeight: "24px", maxHeight: "50px" }} | |
| /> | |
| {isLoading ? ( | |
| <button | |
| onClick={onStop} | |
| className="w-9 h-9 flex-shrink-0 rounded-xl bg-gradient-to-br from-brand-green-light to-brand-green text-white shadow-md shadow-brand-green/25 hover:brightness-105 flex items-center justify-center transition-all" | |
| aria-label="Stop" | |
| > | |
| <Square className="h-3.5 w-3.5" /> | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={handleSend} | |
| disabled={!canSend} | |
| className={`w-9 h-9 flex-shrink-0 rounded-xl flex items-center justify-center transition-all ${ | |
| canSend | |
| ? "bg-gradient-to-br from-brand-green-light to-brand-green text-white shadow-md shadow-brand-green/25 hover:brightness-105" | |
| : "bg-neutral-100 text-neutral-300 cursor-not-allowed" | |
| }`} | |
| aria-label="Send" | |
| > | |
| <SendHorizontal className="h-4 w-4" /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |