feat: Support markdown and source code file display (#40)

* 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 <noreply@anthropic.com>

* chore: fix implementation

* Revert "feat: support markdown and source code file display"

This reverts commit 5409a02c61c04b78a968bfe7a0c56a36a3db787b.

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
きむそん
2025-11-02 00:00:10 +09:00
committed by GitHub
parent 1192e146a0
commit e17b58b481
3 changed files with 63 additions and 45 deletions

View File

@@ -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<ChatInputProps> = ({
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<ChatInputProps> = ({
}
}
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<ChatInputProps> = ({
reservedExecutionTime: localDate.toISOString(),
},
message: {
content: finalText,
content: message,
projectId,
baseSessionId,
},
@@ -176,7 +186,7 @@ export const ChatInput: FC<ChatInputProps> = ({
} 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<ChatInputProps> = ({
ref={fileInputRef}
type="file"
multiple
accept="image/png,image/jpeg,image/gif,image/webp,application/pdf,text/plain"
onChange={handleFileSelect}
className="hidden"
/>

View File

@@ -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,
},
},

View File

@@ -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<typeof mediaTypeSchema>;
/**
* 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<typeof imageBlockSchema>;
/**
* 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<typeof documentBlockSchema>;
/**
* Schema for user message input with optional images and documents
*/