| 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> |
| ); |
| } |
|
|