From 4159985933a43dac39afb4d85ff8177a32561c20 Mon Sep 17 00:00:00 2001 From: Vorflux AI Date: Mon, 1 Jun 2026 14:24:33 +0000 Subject: [PATCH] fix: wire handleKeyDown, use shared chip vars, disableAttachments on home, hide copy on attachment-only --- .../components/chat/home-chat-composer.tsx | 1 + apps/web/components/chat/input/index.tsx | 170 ++++++++++-------- .../components/chat/message/user-message.tsx | 4 +- 3 files changed, 101 insertions(+), 74 deletions(-) diff --git a/apps/web/components/chat/home-chat-composer.tsx b/apps/web/components/chat/home-chat-composer.tsx index 2f7cda2d..a6ac71b5 100644 --- a/apps/web/components/chat/home-chat-composer.tsx +++ b/apps/web/components/chat/home-chat-composer.tsx @@ -48,6 +48,7 @@ export function HomeChatComposer({ onKeyDown={handleKeyDown} isResponding={false} showStatusStrip={false} + disableAttachments stackedToolbar={ <> (null) const fileInputRef = useRef(null) + const attachmentsRef = useRef(attachments) + attachmentsRef.current = attachments + const handleFileChange = useCallback( async (e: React.ChangeEvent) => { - const files = Array.from(e.target.files ?? []) + const incoming = Array.from(e.target.files ?? []) e.target.value = "" + const current = attachmentsRef.current + const slots = MAX_FILE_COUNT - current.length + if (slots <= 0) return + const candidates = incoming + .slice(0, slots) + .filter((f) => f.size <= MAX_FILE_SIZE_MB * 1024 * 1024) + // Enforce total budget (rough estimate; data URL is ~1.37x raw size) + const budgetBytes = MAX_TOTAL_SIZE_MB * 1024 * 1024 + let consumed = current.reduce((sum, att) => { + // Estimate original size from data URL length + return sum + Math.round(att.url.length * 0.75) + }, 0) + const accepted: File[] = [] + for (const f of candidates) { + if (consumed + f.size <= budgetBytes) { + accepted.push(f) + consumed += f.size + } + } + if (accepted.length === 0) return const parts = await Promise.all( - files - .filter((f) => f.size <= MAX_FILE_SIZE_MB * 1024 * 1024) - .map( - (file) => - new Promise((resolve) => { - const reader = new FileReader() - reader.onload = () => - resolve({ - type: "file" as const, - mediaType: file.type, - filename: file.name, - url: reader.result as string, - }) - reader.readAsDataURL(file) - }), - ), + accepted.map( + (file) => + new Promise((resolve) => { + const reader = new FileReader() + reader.onload = () => + resolve({ + type: "file", + mediaType: file.type, + filename: file.name, + url: reader.result as string, + }) + reader.readAsDataURL(file) + }), + ), ) setAttachments((prev) => [...prev, ...parts]) }, [], ) + const removeAttachment = useCallback( + (i: number) => setAttachments((prev) => prev.filter((_, idx) => idx !== i)), + [], + ) + const handleSend = useCallback(() => { onSend(attachments.length > 0 ? attachments : undefined) setAttachments([]) }, [onSend, attachments]) + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + if (!isResponding && (value.trim() || attachments.length > 0)) { + handleSend() + } + return + } + externalOnKeyDown?.(e) + }, + [externalOnKeyDown, handleSend, isResponding, value, attachments.length], + ) + useEffect(() => { if (!showStatusStrip && isExpanded) { setIsExpanded(false) @@ -99,16 +144,28 @@ export default function ChatInput({ setIsMultiline(textarea.scrollHeight > 52) } + const attachmentChips = attachments.length > 0 && ( +
+ {attachments.map((att, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: stable list + removeAttachment(i)} /> + ))} +
+ ) + + const paperclipButton = !disableAttachments && ( + + ) + return ( - <> - + {showStatusStrip ? ( <>
- {attachments.length > 0 && ( -
- {attachments.map((att, i) => ( - setAttachments((prev) => prev.filter((_, idx) => idx !== i))} - /> - ))} -
- )} + {attachmentChips}