--- phase: 25-file-system plan: "03" subsystem: chat-files-ui tags: [file-preview, chat-ui, file-upload, server-query] dependency_graph: requires: ["25-01", "25-02"] provides: ["ChatFilePreview", "ChatFileCard", "file-render-in-message", "attach-files-on-send"] affects: ["ChatMessage", "ChatPanel", "server/chat.ts"] tech_stack: added: [] patterns: - ChatFilePreview delegates to ChatFileCard for all non-image types - inArray batch query for files after listMessages, merged in-memory - completedFileIds captured before clearCompleted, parallel PATCH for each fileId key_files: created: - ui/src/components/ChatFileCard.tsx - ui/src/components/ChatFilePreview.tsx modified: - ui/src/components/ChatMessage.tsx - ui/src/components/ChatMessageList.tsx - ui/src/components/ChatPanel.tsx - ui/src/api/chat.ts - server/src/services/chat.ts decisions: - "ChatFilePreview shows inline image with max-h-[300px] + ChatFileCard below for download; non-image types render ChatFileCard only" - "listMessages fetches chatFiles with inArray(messageId) as second query; merged in-memory (no join, no schema change)" - "completedFileIds captured before clearCompleted in handleSend to avoid race with state update" - "addMessage returns files: [] for consistency with listMessages shape" metrics: duration: "3 minutes" completed_date: "2026-04-01" tasks: 2 files_modified: 7 --- # Phase 25 Plan 03: File Preview and Full Flow Wiring Summary **One-liner:** ChatFilePreview (inline images) and ChatFileCard (download cards) wired end-to-end: upload -> attach to message -> render previews with server-side file loading in listMessages. ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | Create ChatFilePreview and ChatFileCard | 6053f30e | ChatFilePreview.tsx, ChatFileCard.tsx | | 2 | Wire files into ChatMessage, ChatPanel, server | 64ec8588 | ChatMessage.tsx, ChatMessageList.tsx, ChatPanel.tsx, chat.ts (api), chat.ts (server) | ## What Was Built ### ChatFileCard (`ui/src/components/ChatFileCard.tsx`) - Compact file attachment card with lucide icon per category (ImageIcon, FileCode, FileText, File) - Shows original filename (truncated), human-readable size (B/KB/MB via `formatFileSize`) - Download anchor with `download` attribute and `target="_blank"` for one-click download - Theme-aware: `bg-muted border-border rounded-lg` — works across Catppuccin Mocha, Tokyo Night, Catppuccin Latte ### ChatFilePreview (`ui/src/components/ChatFilePreview.tsx`) - Images: inline `` with `loading="lazy"`, `max-h-[300px] rounded-lg object-contain`, wrapped in clickable link for full-size - Images also render ChatFileCard below for download button - All other categories (code, document, other): render ChatFileCard only - No complexity of fetching file content for syntax highlighting — code files show as downloadable cards ### Server: listMessages with files (`server/src/services/chat.ts`) - After fetching messages, collects all message IDs - Second query: `SELECT * FROM chat_files WHERE message_id IN (...)` ordered by createdAt asc - Groups by messageId into a Map, attaches `files` array to each message - Imports: `inArray` from drizzle-orm, `chatFiles` from @paperclipai/db - `addMessage` returns `{ ...message, files: [] }` for shape consistency ### ChatMessage files rendering (`ui/src/components/ChatMessage.tsx`) - Added `files?: ChatFile[]` prop - Renders `
` with ChatFilePreview for each file - Applied to both user message bubble and assistant message sections ### ChatPanel upload orchestration (`ui/src/components/ChatPanel.tsx`) - Calls `useChatFileUpload(activeConversationId)` for pendingFiles, addFile, removeFile, clearCompleted, completedFileIds - Passes `pendingFiles`, `onRemoveFile`, `onFilesPicked` to ChatInput - `handleSend`: captures `completedFileIds` before clearing, calls `chatApi.attachFilesToMessage` after postMessage, then `clearCompleted()`, then invalidates messages query ### chatApi.attachFilesToMessage (`ui/src/api/chat.ts`) - `Promise.all(fileIds.map(id => api.patch('/files/:id', { messageId })))` — parallel PATCH calls ## Deviations from Plan None — plan executed exactly as written. ## Known Stubs None — all file data flows from server to UI. File previews render real content from `/api/files/:id/content`. ## Self-Check: PASSED