mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-04 14:14:22 +01:00
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:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user