---
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
