"use client" import { useCallback, useState } from "react" import ChatInput from "./input" import ChatModelSelector from "./model-selector" import { useProject } from "@/stores" import { cn } from "@lib/utils" import type { ModelId } from "@/lib/models" import { SpaceSelector } from "@/components/space-selector" import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import { toast } from "sonner" import { chatAttachmentKey, CHAT_ATTACHMENT_ACCEPT, createChatAttachmentDraft, type ChatAttachmentDraft, isAcceptedChatAttachment, } from "./attachments" import { ReasoningSelector } from "./reasoning-selector" import { getDefaultReasoningEffort, type ReasoningEffort } from "@/lib/models" export function HomeChatComposer({ onStartChat, className, }: { onStartChat: ( message: string, model: ModelId, projectId: string, reasoningEffort: ReasoningEffort, attachments?: ChatAttachmentDraft[], ) => void className?: string }) { const [input, setInput] = useState("") const [attachmentDrafts, setAttachmentDrafts] = useState< ChatAttachmentDraft[] >([]) const [selectedModel, setSelectedModel] = useState("grok-4.5") const [reasoningEffort, setReasoningEffort] = useState( getDefaultReasoningEffort("grok-4.5"), ) const { selectedProject } = useProject() const [chatSpaceProjects, setChatSpaceProjects] = useState([ AUTO_CHAT_SPACE_ID, ]) const handleModelChange = useCallback((model: ModelId) => { setSelectedModel(model) setReasoningEffort(getDefaultReasoningEffort(model)) }, []) const send = useCallback(() => { const t = input.trim() if (!t && attachmentDrafts.length === 0) return onStartChat( t, selectedModel, chatSpaceProjects[0] ?? selectedProject, reasoningEffort, attachmentDrafts, ) setInput("") setAttachmentDrafts([]) }, [ attachmentDrafts, chatSpaceProjects, input, onStartChat, reasoningEffort, selectedModel, selectedProject, ]) const handleAddAttachmentFiles = useCallback( (files: FileList | File[]) => { const incoming = Array.from(files) const accepted = incoming.filter(isAcceptedChatAttachment) const rejected = incoming.length - accepted.length if (rejected > 0) { toast.error( rejected === 1 ? "One attachment is not supported or is over 50MB" : `${rejected} attachments are not supported or are over 50MB`, ) } if (accepted.length === 0) return const existingKeys = new Set( attachmentDrafts.map((item) => chatAttachmentKey(item.file)), ) const nextItems: ChatAttachmentDraft[] = [] let duplicateCount = 0 for (const file of accepted) { const key = chatAttachmentKey(file) if (existingKeys.has(key)) { duplicateCount++ continue } existingKeys.add(key) nextItems.push(createChatAttachmentDraft(file)) } if (duplicateCount > 0) { toast.message( duplicateCount === 1 ? "Skipped duplicate attachment" : `Skipped ${duplicateCount} duplicate attachments`, ) } if (nextItems.length === 0) return setAttachmentDrafts((prev) => [...prev, ...nextItems]) }, [attachmentDrafts], ) const handleRemoveAttachment = useCallback((id: string) => { setAttachmentDrafts((prev) => prev.filter((item) => item.id !== id)) }, []) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() send() } } return (
setInput(e.target.value)} onSend={send} onStop={() => {}} onKeyDown={handleKeyDown} isResponding={false} attachments={attachmentDrafts} onAddAttachmentFiles={handleAddAttachmentFiles} onRemoveAttachment={handleRemoveAttachment} canSend={input.trim().length > 0 || attachmentDrafts.length > 0} attachmentAccept={CHAT_ATTACHMENT_ACCEPT} showStatusStrip={false} stackedToolbar={ } toolbarTrailing={ } toolbarEnd={ } />
) }