nexus/.planning/phases/25-file-system/25-03-SUMMARY.md

91 lines
4.3 KiB
Markdown

---
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 `<img>` 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 `<div className="mt-2 flex flex-col gap-2">` 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