From ac8ef73bee377f2fc354abebffb1736767ca5d89 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Mon, 5 May 2025 12:38:09 -0700 Subject: [PATCH] Support image/file attachments (#80) --- src/components/chat/AttachmentsList.tsx | 62 ++++++ src/components/chat/ChatInput.tsx | 77 ++++++- src/components/chat/DragDropOverlay.tsx | 18 ++ src/components/chat/HomeChatInput.tsx | 89 +++++++- src/hooks/useAttachments.ts | 58 ++++++ src/hooks/useStreamChat.ts | 8 +- src/ipc/handlers/chat_stream_handlers.ts | 247 ++++++++++++++++++++++- src/ipc/ipc_client.ts | 71 +++++-- src/ipc/ipc_types.ts | 5 + src/pages/home.tsx | 19 +- 10 files changed, 620 insertions(+), 34 deletions(-) create mode 100644 src/components/chat/AttachmentsList.tsx create mode 100644 src/components/chat/DragDropOverlay.tsx create mode 100644 src/hooks/useAttachments.ts diff --git a/src/components/chat/AttachmentsList.tsx b/src/components/chat/AttachmentsList.tsx new file mode 100644 index 0000000..2cb269b --- /dev/null +++ b/src/components/chat/AttachmentsList.tsx @@ -0,0 +1,62 @@ +import { FileText, X } from "lucide-react"; +import { useEffect } from "react"; + +interface AttachmentsListProps { + attachments: File[]; + onRemove: (index: number) => void; +} + +export function AttachmentsList({ + attachments, + onRemove, +}: AttachmentsListProps) { + if (attachments.length === 0) return null; + + return ( +
+ {attachments.map((file, index) => ( +
+ {file.type.startsWith("image/") ? ( +
+ {file.name} + URL.revokeObjectURL((e.target as HTMLImageElement).src) + } + onError={(e) => + URL.revokeObjectURL((e.target as HTMLImageElement).src) + } + /> +
+ {file.name} + URL.revokeObjectURL((e.target as HTMLImageElement).src) + } + /> +
+
+ ) : ( + + )} + {file.name} + +
+ ))} +
+ ); +} diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 8b349d4..a45b83c 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -16,6 +16,7 @@ import { ChevronsUpDown, ChevronsDownUp, BarChart2, + Paperclip, } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -54,6 +55,9 @@ import { } from "../ui/tooltip"; import { useNavigate } from "@tanstack/react-router"; import { useVersions } from "@/hooks/useVersions"; +import { useAttachments } from "@/hooks/useAttachments"; +import { AttachmentsList } from "./AttachmentsList"; +import { DragDropOverlay } from "./DragDropOverlay"; const showTokenBarAtom = atom(false); @@ -73,6 +77,20 @@ export function ChatInput({ chatId }: { chatId?: number }) { const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom); + // Use the attachments hook + const { + attachments, + fileInputRef, + isDraggingOver, + handleAttachmentClick, + handleFileChange, + removeAttachment, + handleDragOver, + handleDragLeave, + handleDrop, + clearAttachments, + } = useAttachments(); + // Use the hook to fetch the proposal const { proposalResult, @@ -118,13 +136,25 @@ export function ChatInput({ chatId }: { chatId?: number }) { }; const handleSubmit = async () => { - if (!inputValue.trim() || isStreaming || !chatId) { + if ( + (!inputValue.trim() && attachments.length === 0) || + isStreaming || + !chatId + ) { return; } const currentInput = inputValue; setInputValue(""); - await streamMessage({ prompt: currentInput, chatId }); + + // Send message with attachments and clear them after sending + await streamMessage({ + prompt: currentInput, + chatId, + attachments, + redo: false, + }); + clearAttachments(); posthog.capture("chat:submit"); }; @@ -236,7 +266,14 @@ export function ChatInput({ chatId }: { chatId?: number }) { )}
-
+
{/* Only render ChatInputActions if proposal is loaded */} {proposal && proposalResult?.chatId === chatId && ( )} + + {/* Use the AttachmentsList component */} + + + {/* Use the DragDropOverlay component */} + +