import { useEffect, useRef, useState } from 'react'; import { ArrowUp, Check, ChevronDown, FileText, Headphones, Image, Loader2, Mic, Paperclip, Plus, Square, Trash2 } from 'lucide-react'; import { formatBytes, permissionLabel, PERMISSION_OPTIONS, REASONING_OPTIONS, reasoningLabel, shortModelName } from '../app-core-utils.js'; import { useVoiceInputRecorder } from './useVoiceInputRecorder.js'; export function Composer({ input, setInput, onSubmit, running, onAbort, models, selectedModel, onSelectModel, selectedReasoningEffort, onSelectReasoningEffort, permissionMode, onSelectPermission, attachments, onUploadFiles, onRemoveAttachment, onRateLimit, uploading, onVoiceSubmit, onOpenVoiceDialog, voiceDialogActive, disabled, disabledReason, actionDisabledReasons = {}, backgroundInert = false }) { const textareaRef = useRef(null); const imageInputRef = useRef(null); const fileInputRef = useRef(null); const [openMenu, setOpenMenu] = useState(null); const hasInput = input.trim().length > 0 || attachments.length > 0; const modelList = models?.length ? models : [{ value: selectedModel || 'gpt-5.5', label: selectedModel || 'gpt-5.5' }]; const selectedModelLabel = modelList.find((model) => model.value === selectedModel)?.label || selectedModel || 'gpt-5.5'; const { voiceState, voiceError, voiceRecording, voiceTranscribing, voiceSending, toggleVoiceInput } = useVoiceInputRecorder({ onVoiceSubmit, onRateLimit, onBeforeStart: () => setOpenMenu(null) }); const uploadDisabledReason = actionDisabledReasons.upload || disabledReason; const sendDisabledReason = actionDisabledReasons.send || disabledReason; const voiceDisabledReason = actionDisabledReasons.voice || disabledReason; const voiceDialogDisabledReason = actionDisabledReasons.voiceDialog || disabledReason; const inertProps = backgroundInert ? { inert: '' } : {}; const stopOnly = running && !hasInput; const sendButtonDisabled = uploading || (stopOnly ? false : Boolean(sendDisabledReason) || !hasInput); const actionNotice = [ sendDisabledReason && `发送:${sendDisabledReason}`, uploadDisabledReason && `上传:${uploadDisabledReason}`, voiceDisabledReason && `语音:${voiceDisabledReason}`, voiceDialogDisabledReason && `对话:${voiceDialogDisabledReason}` ].filter(Boolean)[0] || ''; useEffect(() => { const textarea = textareaRef.current; if (!textarea) { return; } textarea.style.height = '0px'; textarea.style.height = `${Math.min(textarea.scrollHeight, 132)}px`; }, [input]); const submit = (event) => { event.preventDefault(); submitMessage(); }; const submitMessage = () => { if (running && !hasInput) { onAbort(); return; } if (sendButtonDisabled) { return; } onSubmit(); setOpenMenu(null); }; const handleKeyDown = (event) => { const composing = event.isComposing || event.nativeEvent?.isComposing; const modified = event.shiftKey || event.altKey || event.metaKey || event.ctrlKey; if (event.key !== 'Enter' || modified || composing) { return; } event.preventDefault(); submitMessage(); }; function toggleMenu(name) { setOpenMenu((current) => (current === name ? null : name)); } function handleFiles(event, kind) { const files = Array.from(event.target.files || []); if (files.length) { onUploadFiles(files, kind); } event.target.value = ''; setOpenMenu(null); } return (
); }