From dca8906e9fb072e89f6d47bf14c212a4778f6670 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:32:28 +0000 Subject: [PATCH] feat: add file upload button to chat input area - Add file state management (addFiles, removeFile, clearFiles) to InputStore - Add file upload button (Paperclip icon) at bottom-left of input area - Show file preview with name, size, and remove button for attached files - Integrate existing FileUpload component with drag & drop support - Update send button to allow sending when files are attached (even without text) - Clear attached files after sending message Co-Authored-By: David Mu --- .../content/components/footer/footer.tsx | 131 +++++++++++++----- app/web-app/src/stores/input-store.ts | 20 ++- 2 files changed, 117 insertions(+), 34 deletions(-) diff --git a/app/web-app/src/components/chat/components/content/components/footer/footer.tsx b/app/web-app/src/components/chat/components/content/components/footer/footer.tsx index 01e073f..95b59f3 100644 --- a/app/web-app/src/components/chat/components/content/components/footer/footer.tsx +++ b/app/web-app/src/components/chat/components/content/components/footer/footer.tsx @@ -1,5 +1,5 @@ import { observer } from 'mobx-react-lite'; -import { Send } from 'lucide-react'; +import { Paperclip, Send, X } from 'lucide-react'; import { PromptInput, PromptInputAction, @@ -9,9 +9,16 @@ import { import { Button } from '@/components/ui/button.tsx'; import { Loader } from '@/components/ui/loader.tsx'; import { PromptSuggestion } from '@/components/ui/prompt-suggestion.tsx'; +import { FileUpload, FileUploadContent, FileUploadTrigger } from '@/components/ui/file-upload.tsx'; import rootStore from '@/stores/root-store.ts'; import stream from '@/stream/stream.ts'; +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + const Footer = observer(() => { const { inputStore } = rootStore; @@ -21,10 +28,14 @@ const Footer = observer(() => { inputStore.setInput(suggestion); }; + const handleFilesAdded = (files: File[]) => { + inputStore.addFiles(files); + }; + return (
{/* Prompt Suggestions */} - {!inputStore.input && ( + {!inputStore.input && inputStore.files.length === 0 && (
{promptSuggestions.map((suggestion, index) => ( handleSuggestionClick(suggestion)}> @@ -35,37 +46,93 @@ const Footer = observer(() => { )} {/* Prompt Input */} - inputStore.setInput(value)} - onSubmit={() => inputStore.handleSend()} - disabled={stream.loading} - > - - - - +
+ ))} +
+ )} + + + {/* File Upload Button (bottom-left) */} + + + + + + + {/* Send Button (bottom-right) */} + - {stream.loading ? ( - - ) : ( - - )} - - - - + + + + + + {/* Drag & Drop Overlay */} + +
+ +

Drop files here

+
+
+ ); }); diff --git a/app/web-app/src/stores/input-store.ts b/app/web-app/src/stores/input-store.ts index e8ae126..226b368 100644 --- a/app/web-app/src/stores/input-store.ts +++ b/app/web-app/src/stores/input-store.ts @@ -1,18 +1,33 @@ -import { makeAutoObservable } from 'mobx'; +import { makeAutoObservable, observable } from 'mobx'; import { toast } from 'sonner'; import stream from '@/stream/stream.ts'; export class InputStore { input: string = ''; + files = observable.array([]); + constructor() { makeAutoObservable(this); } + setInput(text: string) { this.input = text; } + addFiles(newFiles: File[]) { + this.files.push(...newFiles); + } + + removeFile(index: number) { + this.files.splice(index, 1); + } + + clearFiles() { + this.files.clear(); + } + async handleSend() { - if (!this.input.trim()) { + if (!this.input.trim() && this.files.length === 0) { toast.info('please input your prompt'); return; } @@ -25,5 +40,6 @@ export class InputStore { return; } await stream.task({ input: this.input }); + this.clearFiles(); } }