From e17b58b4818ed72542b88aef62c5a946c79d707f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8D=E3=82=80=E3=81=9D=E3=82=93?= Date: Sun, 2 Nov 2025 00:00:10 +0900 Subject: [PATCH] feat: Support markdown and source code file display (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support markdown and source code file display This commit extends file upload support beyond plain text to include: - Markdown files (.md, .markdown) with full rendering - Source code files with syntax highlighting (JS, TS, Python, Go, Rust, etc.) - Proper media type detection and display type selection Changes: - Updated DocumentContentSchema to accept various text-based media types - Created file-type-detector utility to map media types to display strategies - Enhanced UserConversationContent to render markdown with MarkdownContent component - Added syntax highlighting for code files using react-syntax-highlighter - Added comprehensive tests for file type detection Fixes #39 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * chore: fix implementation * Revert "feat: support markdown and source code file display" This reverts commit 5409a02c61c04b78a968bfe7a0c56a36a3db787b. --------- Co-authored-by: Claude --- .../components/chatForm/ChatInput.tsx | 37 +++++++++++------- .../components/chatForm/fileUtils.ts | 39 +++++++------------ src/server/core/claude-code/schema.ts | 32 ++++++++++++--- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx b/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx index 0f14654..7fd9b4d 100644 --- a/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx +++ b/src/app/projects/[projectId]/components/chatForm/ChatInput.tsx @@ -21,16 +21,20 @@ import { } from "../../../../../components/ui/select"; import { Textarea } from "../../../../../components/ui/textarea"; import { useCreateSchedulerJob } from "../../../../../hooks/useScheduler"; +import type { + DocumentBlockParam, + ImageBlockParam, +} from "../../../../../server/core/claude-code/schema"; import { useConfig } from "../../../../hooks/useConfig"; import type { CommandCompletionRef } from "./CommandCompletion"; import type { FileCompletionRef } from "./FileCompletion"; -import type { DocumentBlock, ImageBlock } from "./fileUtils"; +import { processFile } from "./fileUtils"; import { InlineCompletion } from "./InlineCompletion"; export interface MessageInput { text: string; - images?: ImageBlock[]; - documents?: DocumentBlock[]; + images?: ImageBlockParam[]; + documents?: DocumentBlockParam[]; } export interface ChatInputProps { @@ -97,17 +101,25 @@ export const ChatInput: FC = ({ const handleSubmit = async () => { if (!message.trim() && attachedFiles.length === 0) return; - const { processFile } = await import("./fileUtils"); - - const images: ImageBlock[] = []; - const documents: DocumentBlock[] = []; - let additionalText = ""; + const images: ImageBlockParam[] = []; + const documents: DocumentBlockParam[] = []; for (const { file } of attachedFiles) { const result = await processFile(file); + if (result === null) { + continue; + } + if (result.type === "text") { - additionalText += `\n\nFile: ${file.name}\n${result.content}`; + documents.push({ + type: "document", + source: { + type: "text", + media_type: "text/plain", + data: result.content, + }, + }); } else if (result.type === "image") { images.push(result.block); } else if (result.type === "document") { @@ -115,8 +127,6 @@ export const ChatInput: FC = ({ } } - const finalText = message.trim() + additionalText; - if (enableScheduledSend && sendMode === "scheduled") { // Create a scheduler job for scheduled send const match = scheduledTime.match( @@ -140,7 +150,7 @@ export const ChatInput: FC = ({ reservedExecutionTime: localDate.toISOString(), }, message: { - content: finalText, + content: message, projectId, baseSessionId, }, @@ -176,7 +186,7 @@ export const ChatInput: FC = ({ } else { // Immediate send await onSubmit({ - text: finalText, + text: message, images: images.length > 0 ? images : undefined, documents: documents.length > 0 ? documents : undefined, }); @@ -391,7 +401,6 @@ export const ChatInput: FC = ({ ref={fileInputRef} type="file" multiple - accept="image/png,image/jpeg,image/gif,image/webp,application/pdf,text/plain" onChange={handleFileSelect} className="hidden" /> diff --git a/src/app/projects/[projectId]/components/chatForm/fileUtils.ts b/src/app/projects/[projectId]/components/chatForm/fileUtils.ts index 7d6df50..5086ed4 100644 --- a/src/app/projects/[projectId]/components/chatForm/fileUtils.ts +++ b/src/app/projects/[projectId]/components/chatForm/fileUtils.ts @@ -1,27 +1,11 @@ -/** - * File utilities for file upload and encoding - */ +import { + type DocumentBlockParam, + type ImageBlockParam, + mediaTypeSchema, +} from "../../../../../server/core/claude-code/schema"; export type FileType = "text" | "image" | "pdf"; -export type ImageBlock = { - type: "image"; - source: { - type: "base64"; - media_type: "image/png" | "image/jpeg" | "image/gif" | "image/webp"; - data: string; - }; -}; - -export type DocumentBlock = { - type: "document"; - source: { - type: "base64"; - media_type: "application/pdf"; - data: string; - }; -}; - /** * Determine file type based on MIME type */ @@ -106,8 +90,9 @@ export const processFile = async ( file: File, ): Promise< | { type: "text"; content: string } - | { type: "image"; block: ImageBlock } - | { type: "document"; block: DocumentBlock } + | { type: "image"; block: ImageBlockParam } + | { type: "document"; block: DocumentBlockParam } + | null > => { const fileType = determineFileType(file.type); @@ -119,14 +104,18 @@ export const processFile = async ( const base64Data = await fileToBase64(file); if (fileType === "image") { - const mediaType = file.type as ImageBlock["source"]["media_type"]; + const mediaType = mediaTypeSchema.safeParse(file.type); + if (!mediaType.success) { + return null; + } + return { type: "image", block: { type: "image", source: { type: "base64", - media_type: mediaType, + media_type: mediaType.data, data: base64Data, }, }, diff --git a/src/server/core/claude-code/schema.ts b/src/server/core/claude-code/schema.ts index 0f096ac..ea61b30 100644 --- a/src/server/core/claude-code/schema.ts +++ b/src/server/core/claude-code/schema.ts @@ -1,5 +1,14 @@ import { z } from "zod"; +export const mediaTypeSchema = z.enum([ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", +]); + +export type MediaType = z.infer; + /** * Schema for image block parameter */ @@ -7,23 +16,34 @@ const imageBlockSchema = z.object({ type: z.literal("image"), source: z.object({ type: z.literal("base64"), - media_type: z.enum(["image/png", "image/jpeg", "image/gif", "image/webp"]), + media_type: mediaTypeSchema, data: z.string(), }), }); +export type ImageBlockParam = z.infer; + /** * Schema for document block parameter */ const documentBlockSchema = z.object({ type: z.literal("document"), - source: z.object({ - type: z.literal("base64"), - media_type: z.enum(["application/pdf"]), - data: z.string(), - }), + source: z.union([ + z.object({ + type: z.literal("text"), + media_type: z.enum(["text/plain"]), + data: z.string(), + }), + z.object({ + type: z.literal("base64"), + media_type: z.enum(["application/pdf"]), + data: z.string(), + }), + ]), }); +export type DocumentBlockParam = z.infer; + /** * Schema for user message input with optional images and documents */