feat: add file upload and preview support (images and documents)

- Support image (JPEG/PNG/GIF/WebP, max 5MB) and document uploads
- Images sent as multimodal content blocks to Claude SDK
- Documents saved to temp dir for Claude's Read tool
- Emerald capsule badges in input for attached files
- Drag-and-drop, paste, and file picker support via PromptInput
- Chat display: image thumbnails with lightbox, file cards
- Lightbox uses Radix Dialog with prev/next navigation
- File metadata embedded in message content as HTML comments
- Compatible with existing slash command badge system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-08 21:35:45 +08:00
1 parent 4ac1f8cea3
commit 164f730da1
11 files changed
+562 -22

No files matched your search

+15 -3
View File
@@ -1,15 +1,15 @@
import { NextRequest } from 'next/server';
import { streamClaude } from '@/lib/claude-client';
import { addMessage, getSession, updateSessionTitle, updateSdkSessionId, getSetting } from '@/lib/db';
import type { SendMessageRequest, SSEEvent, TokenUsage, MessageContentBlock } from '@/types';
import type { SendMessageRequest, SSEEvent, TokenUsage, MessageContentBlock, FileAttachment } from '@/types';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function POST(request: NextRequest) {
try {
const body: SendMessageRequest = await request.json();
const { session_id, content, model, mode } = body;
const body: SendMessageRequest & { files?: FileAttachment[] } = await request.json();
const { session_id, content, model, mode, files } = body;
if (!session_id || !content) {
return new Response(JSON.stringify({ error: 'session_id and content are required' }), {
@@ -63,6 +63,17 @@ export async function POST(request: NextRequest) {
abortController.abort();
});
// Convert file attachments to the format expected by streamClaude
const fileAttachments: FileAttachment[] | undefined = files && files.length > 0
? files.map((f, i) => ({
id: f.id || `file-${Date.now()}-${i}`,
name: f.name,
type: f.type,
size: f.size,
data: f.data,
}))
: undefined;
// Stream Claude response, using SDK session ID for resume if available
const stream = streamClaude({
prompt: content,
@@ -73,6 +84,7 @@ export async function POST(request: NextRequest) {
workingDirectory: session.working_directory || undefined,
abortController,
permissionMode,
files: fileAttachments,
});
// Tee the stream: one for client, one for collecting the response