mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-28 02:34:21 +01:00
feat: File upload(plain text, pdf, image) #34
* support file upload * preview pdf
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.23",
|
||||
"@anthropic-ai/claude-code": "^2.0.24",
|
||||
"@anthropic-ai/sdk": "^0.67.0",
|
||||
"@effect/platform": "^0.92.1",
|
||||
"@effect/platform-node": "^0.98.4",
|
||||
"@hono/node-server": "^1.19.5",
|
||||
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@anthropic-ai/claude-code':
|
||||
specifier: ^2.0.24
|
||||
version: 2.0.24
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.67.0
|
||||
version: 0.67.0(zod@4.1.12)
|
||||
'@effect/platform':
|
||||
specifier: ^0.92.1
|
||||
version: 0.92.1(effect@3.18.4)
|
||||
@@ -231,6 +234,15 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@anthropic-ai/sdk@0.67.0':
|
||||
resolution: {integrity: sha512-Buxbf6jYJ+pPtfCgXe1pcFtZmdXPrbdqhBjiscFt9irS1G0hCsmR/fPA+DwKTk4GPjqeNnnCYNecXH6uVZ4G/A==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -3210,6 +3222,10 @@ packages:
|
||||
resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
@@ -4280,6 +4296,9 @@ packages:
|
||||
trough@2.2.0:
|
||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -4611,6 +4630,12 @@ snapshots:
|
||||
'@img/sharp-linux-x64': 0.33.5
|
||||
'@img/sharp-win32-x64': 0.33.5
|
||||
|
||||
'@anthropic-ai/sdk@0.67.0(zod@4.1.12)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
optionalDependencies:
|
||||
zod: 4.1.12
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
@@ -7544,6 +7569,11 @@ snapshots:
|
||||
|
||||
json-parse-even-better-errors@4.0.0: {}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
ts-algebra: 2.0.0
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
@@ -8863,6 +8893,8 @@ snapshots:
|
||||
|
||||
trough@2.2.0: {}
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.20.6:
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Trans, useLingui } from "@lingui/react";
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
LoaderIcon,
|
||||
PaperclipIcon,
|
||||
SendIcon,
|
||||
SparklesIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { type FC, useCallback, useId, useRef, useState } from "react";
|
||||
import { Button } from "../../../../../components/ui/button";
|
||||
@@ -11,11 +13,18 @@ import { Textarea } from "../../../../../components/ui/textarea";
|
||||
import { useConfig } from "../../../../hooks/useConfig";
|
||||
import type { CommandCompletionRef } from "./CommandCompletion";
|
||||
import type { FileCompletionRef } from "./FileCompletion";
|
||||
import type { DocumentBlock, ImageBlock } from "./fileUtils";
|
||||
import { InlineCompletion } from "./InlineCompletion";
|
||||
|
||||
export interface MessageInput {
|
||||
text: string;
|
||||
images?: ImageBlock[];
|
||||
documents?: DocumentBlock[];
|
||||
}
|
||||
|
||||
export interface ChatInputProps {
|
||||
projectId: string;
|
||||
onSubmit: (message: string) => Promise<void>;
|
||||
onSubmit: (input: MessageInput) => Promise<void>;
|
||||
isPending: boolean;
|
||||
error?: Error | null;
|
||||
placeholder: string;
|
||||
@@ -40,6 +49,9 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
}) => {
|
||||
const { i18n } = useLingui();
|
||||
const [message, setMessage] = useState("");
|
||||
const [attachedFiles, setAttachedFiles] = useState<
|
||||
Array<{ file: File; id: string }>
|
||||
>([]);
|
||||
const [cursorPosition, setCursorPosition] = useState<{
|
||||
relative: { top: number; left: number };
|
||||
absolute: { top: number; left: number };
|
||||
@@ -47,15 +59,63 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const commandCompletionRef = useRef<CommandCompletionRef>(null);
|
||||
const fileCompletionRef = useRef<FileCompletionRef>(null);
|
||||
const helpId = useId();
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!message.trim()) return;
|
||||
await onSubmit(message.trim());
|
||||
if (!message.trim() && attachedFiles.length === 0) return;
|
||||
|
||||
const { processFile } = await import("./fileUtils");
|
||||
|
||||
const images: ImageBlock[] = [];
|
||||
const documents: DocumentBlock[] = [];
|
||||
let additionalText = "";
|
||||
|
||||
for (const { file } of attachedFiles) {
|
||||
const result = await processFile(file);
|
||||
|
||||
if (result.type === "text") {
|
||||
additionalText += `\n\nFile: ${file.name}\n${result.content}`;
|
||||
} else if (result.type === "image") {
|
||||
images.push(result.block);
|
||||
} else if (result.type === "document") {
|
||||
documents.push(result.block);
|
||||
}
|
||||
}
|
||||
|
||||
const finalText = message.trim() + additionalText;
|
||||
|
||||
await onSubmit({
|
||||
text: finalText,
|
||||
images: images.length > 0 ? images : undefined,
|
||||
documents: documents.length > 0 ? documents : undefined,
|
||||
});
|
||||
|
||||
setMessage("");
|
||||
setAttachedFiles([]);
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const newFiles = Array.from(files).map((file) => ({
|
||||
file,
|
||||
id: `${file.name}-${Date.now()}-${Math.random()}`,
|
||||
}));
|
||||
|
||||
setAttachedFiles((prev) => [...prev, ...newFiles]);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (id: string) => {
|
||||
setAttachedFiles((prev) => prev.filter((f) => f.id !== id));
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -159,7 +219,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleFileSelect = (filePath: string) => {
|
||||
const handleFilePathSelect = (filePath: string) => {
|
||||
setMessage(filePath);
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
@@ -216,8 +276,50 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className="px-5 py-3 flex flex-wrap gap-2 border-t border-border/40">
|
||||
{attachedFiles.map(({ file, id }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted rounded-lg text-sm"
|
||||
>
|
||||
<span className="truncate max-w-[200px]">{file.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(id)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
disabled={isPending}
|
||||
>
|
||||
<XIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 px-5 py-3 bg-muted/30 border-t border-border/40">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/png,image/jpeg,image/gif,image/webp,application/pdf,text/plain"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isPending || disabled}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<PaperclipIcon className="w-4 h-4" />
|
||||
<span className="text-xs">
|
||||
<Trans id="chat.attach_file" message="Attach" />
|
||||
</span>
|
||||
</Button>
|
||||
<span
|
||||
className="text-xs font-medium text-muted-foreground/80"
|
||||
id={helpId}
|
||||
@@ -238,7 +340,11 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!message.trim() || isPending || disabled}
|
||||
disabled={
|
||||
(!message.trim() && attachedFiles.length === 0) ||
|
||||
isPending ||
|
||||
disabled
|
||||
}
|
||||
size={buttonSize}
|
||||
className="gap-2 transition-all duration-200 hover:shadow-md hover:scale-105 active:scale-95 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 disabled:from-muted disabled:to-muted"
|
||||
>
|
||||
@@ -267,7 +373,7 @@ export const ChatInput: FC<ChatInputProps> = ({
|
||||
commandCompletionRef={commandCompletionRef}
|
||||
fileCompletionRef={fileCompletionRef}
|
||||
handleCommandSelect={handleCommandSelect}
|
||||
handleFileSelect={handleFileSelect}
|
||||
handleFileSelect={handleFilePathSelect}
|
||||
cursorPosition={cursorPosition}
|
||||
/>
|
||||
</div>
|
||||
|
||||
148
src/app/projects/[projectId]/components/chatForm/fileUtils.ts
Normal file
148
src/app/projects/[projectId]/components/chatForm/fileUtils.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* File utilities for file upload and encoding
|
||||
*/
|
||||
|
||||
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
|
||||
*/
|
||||
export const determineFileType = (mimeType: string): FileType => {
|
||||
if (mimeType.startsWith("image/")) {
|
||||
return "image";
|
||||
}
|
||||
if (mimeType === "application/pdf") {
|
||||
return "pdf";
|
||||
}
|
||||
return "text";
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if MIME type is supported
|
||||
*/
|
||||
export const isSupportedMimeType = (mimeType: string): boolean => {
|
||||
const supportedImageTypes = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
];
|
||||
const supportedDocumentTypes = ["application/pdf"];
|
||||
const supportedTextTypes = ["text/plain"];
|
||||
|
||||
return (
|
||||
supportedImageTypes.includes(mimeType) ||
|
||||
supportedDocumentTypes.includes(mimeType) ||
|
||||
supportedTextTypes.includes(mimeType)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert File to base64 encoded string (without data URL prefix)
|
||||
*/
|
||||
export const fileToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
if (typeof result === "string") {
|
||||
// Remove data URL prefix (e.g., "data:image/png;base64,")
|
||||
const base64 = result.split(",")[1];
|
||||
resolve(base64 ?? "");
|
||||
} else {
|
||||
reject(new Error("Failed to read file as base64"));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read file"));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert File to plain text
|
||||
*/
|
||||
export const fileToText = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
if (typeof result === "string") {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error("Failed to read file as text"));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read file"));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a file and return appropriate block structure
|
||||
*/
|
||||
export const processFile = async (
|
||||
file: File,
|
||||
): Promise<
|
||||
| { type: "text"; content: string }
|
||||
| { type: "image"; block: ImageBlock }
|
||||
| { type: "document"; block: DocumentBlock }
|
||||
> => {
|
||||
const fileType = determineFileType(file.type);
|
||||
|
||||
if (fileType === "text") {
|
||||
const content = await fileToText(file);
|
||||
return { type: "text", content };
|
||||
}
|
||||
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
if (fileType === "image") {
|
||||
const mediaType = file.type as ImageBlock["source"]["media_type"];
|
||||
return {
|
||||
type: "image",
|
||||
block: {
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mediaType,
|
||||
data: base64Data,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// PDF
|
||||
return {
|
||||
type: "document",
|
||||
block: {
|
||||
type: "document",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "application/pdf",
|
||||
data: base64Data,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export type { ChatInputProps } from "./ChatInput";
|
||||
export type { ChatInputProps, MessageInput } from "./ChatInput";
|
||||
export { ChatInput } from "./ChatInput";
|
||||
export type { CommandCompletionRef } from "./CommandCompletion";
|
||||
export { CommandCompletion } from "./CommandCompletion";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { honoClient } from "../../../../../lib/api/client";
|
||||
import type { MessageInput } from "./ChatInput";
|
||||
|
||||
export const useCreateSessionProcessMutation = (
|
||||
projectId: string,
|
||||
@@ -10,7 +11,7 @@ export const useCreateSessionProcessMutation = (
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (options: {
|
||||
message: string;
|
||||
input: MessageInput;
|
||||
baseSessionId?: string;
|
||||
}) => {
|
||||
const response = await honoClient.api.cc["session-processes"].$post(
|
||||
@@ -18,7 +19,7 @@ export const useCreateSessionProcessMutation = (
|
||||
json: {
|
||||
projectId,
|
||||
baseSessionId: options.baseSessionId,
|
||||
message: options.message,
|
||||
input: options.input,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -53,7 +54,7 @@ export const useContinueSessionProcessMutation = (
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: async (options: {
|
||||
message: string;
|
||||
input: MessageInput;
|
||||
sessionProcessId: string;
|
||||
}) => {
|
||||
const response = await honoClient.api.cc["session-processes"][
|
||||
@@ -64,7 +65,7 @@ export const useContinueSessionProcessMutation = (
|
||||
json: {
|
||||
projectId: projectId,
|
||||
baseSessionId: baseSessionId,
|
||||
continueMessage: options.message,
|
||||
input: options.input,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Trans, useLingui } from "@lingui/react";
|
||||
import type { FC } from "react";
|
||||
import { useConfig } from "../../../../hooks/useConfig";
|
||||
import { ChatInput, useCreateSessionProcessMutation } from "../chatForm";
|
||||
import {
|
||||
ChatInput,
|
||||
type MessageInput,
|
||||
useCreateSessionProcessMutation,
|
||||
} from "../chatForm";
|
||||
|
||||
export const NewChat: FC<{
|
||||
projectId: string;
|
||||
@@ -14,8 +18,8 @@ export const NewChat: FC<{
|
||||
);
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
await createSessionProcess.mutateAsync({ message });
|
||||
const handleSubmit = async (input: MessageInput) => {
|
||||
await createSessionProcess.mutateAsync({ input });
|
||||
};
|
||||
|
||||
const getPlaceholder = () => {
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { Trans } from "@lingui/react";
|
||||
import { AlertCircle, Image as ImageIcon } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
FileText,
|
||||
Image as ImageIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import type { UserMessageContent } from "@/lib/conversation-schema/message/UserMessageSchema";
|
||||
import { UserTextContent } from "./UserTextContent";
|
||||
|
||||
@@ -28,38 +37,39 @@ export const UserConversationContent: FC<{
|
||||
if (content.source.type === "base64") {
|
||||
return (
|
||||
<Card
|
||||
className="border-purple-200 bg-purple-50/50 dark:border-purple-800 dark:bg-purple-950/20"
|
||||
className="border-purple-200 bg-purple-50/50 dark:border-purple-800 dark:bg-purple-950/20 mb-2 p-0 overflow-hidden"
|
||||
id={id}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
<Trans id="user.content.image" message="Image" />
|
||||
</CardTitle>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-purple-300 text-purple-700 dark:border-purple-700 dark:text-purple-300"
|
||||
>
|
||||
{content.source.media_type}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
<Trans
|
||||
id="user.content.image.description"
|
||||
message="User uploaded image content"
|
||||
/>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-lg border overflow-hidden bg-background">
|
||||
<img
|
||||
src={`data:${content.source.media_type};base64,${content.source.data}`}
|
||||
alt="User uploaded content"
|
||||
className="max-w-full h-auto max-h-96 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="cursor-pointer hover:bg-purple-100/50 dark:hover:bg-purple-900/20 transition-colors px-3 py-1.5 group">
|
||||
<div className="flex items-center gap-2">
|
||||
<ImageIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm font-medium">
|
||||
<Trans id="user.content.image" message="Image" />
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-purple-300 text-purple-700 dark:border-purple-700 dark:text-purple-300"
|
||||
>
|
||||
{content.source.media_type}
|
||||
</Badge>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180 ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="py-3 px-4 border-t border-purple-200 dark:border-purple-800">
|
||||
<div className="rounded-lg border overflow-hidden bg-background">
|
||||
<img
|
||||
src={`data:${content.source.media_type};base64,${content.source.data}`}
|
||||
alt="User uploaded content"
|
||||
className="max-w-full h-auto max-h-96 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -93,6 +103,124 @@ export const UserConversationContent: FC<{
|
||||
);
|
||||
}
|
||||
|
||||
if (content.type === "document") {
|
||||
if (content.source.type === "base64") {
|
||||
// PDFの場合
|
||||
if (content.source.media_type === "application/pdf") {
|
||||
return (
|
||||
<Card
|
||||
className="border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20 mb-2 p-0 overflow-hidden"
|
||||
id={id}
|
||||
>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="cursor-pointer hover:bg-blue-100/50 dark:hover:bg-blue-900/20 transition-colors px-3 py-1.5 group">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.document.pdf"
|
||||
message="PDF Document"
|
||||
/>
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-blue-300 text-blue-700 dark:border-blue-700 dark:text-blue-300"
|
||||
>
|
||||
{content.source.media_type}
|
||||
</Badge>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180 ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="py-3 px-4 border-t border-blue-200 dark:border-blue-800">
|
||||
<div className="rounded-lg border overflow-hidden bg-background">
|
||||
<embed
|
||||
src={`data:${content.source.media_type};base64,${content.source.data}`}
|
||||
type="application/pdf"
|
||||
className="w-full h-[600px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (content.source.type === "text") {
|
||||
// テキストファイルの場合
|
||||
return (
|
||||
<Card
|
||||
className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20 mb-2 p-0 overflow-hidden"
|
||||
id={id}
|
||||
>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="cursor-pointer hover:bg-green-100/50 dark:hover:bg-green-900/20 transition-colors px-3 py-1.5 group">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.document.text"
|
||||
message="Text Document"
|
||||
/>
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-green-300 text-green-700 dark:border-green-700 dark:text-green-300"
|
||||
>
|
||||
{content.source.media_type}
|
||||
</Badge>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180 ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="py-3 px-4 border-t border-green-200 dark:border-green-800">
|
||||
<div className="rounded-lg border overflow-hidden bg-background">
|
||||
<pre className="p-4 text-sm overflow-auto max-h-96">
|
||||
{content.source.data}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20"
|
||||
id={id}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
<Trans
|
||||
id="user.content.unsupported_document"
|
||||
message="Unsupported Document"
|
||||
/>
|
||||
</CardTitle>
|
||||
<Badge variant="destructive">
|
||||
<Trans id="common.error" message="Error" />
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
<Trans
|
||||
id="user.content.unsupported_document.description"
|
||||
message="Document type not supported for display"
|
||||
/>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.type === "tool_result") {
|
||||
// ツール結果は Assistant の呼び出し側に添えるので
|
||||
return null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FC } from "react";
|
||||
import { useConfig } from "../../../../../../hooks/useConfig";
|
||||
import {
|
||||
ChatInput,
|
||||
type MessageInput,
|
||||
useContinueSessionProcessMutation,
|
||||
} from "../../../../components/chatForm";
|
||||
|
||||
@@ -18,8 +19,8 @@ export const ContinueChat: FC<{
|
||||
);
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
await continueSessionProcess.mutateAsync({ message, sessionProcessId });
|
||||
const handleSubmit = async (input: MessageInput) => {
|
||||
await continueSessionProcess.mutateAsync({ input, sessionProcessId });
|
||||
};
|
||||
|
||||
const getPlaceholder = () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FC } from "react";
|
||||
import { useConfig } from "../../../../../../hooks/useConfig";
|
||||
import {
|
||||
ChatInput,
|
||||
type MessageInput,
|
||||
useCreateSessionProcessMutation,
|
||||
} from "../../../../components/chatForm";
|
||||
|
||||
@@ -14,9 +15,9 @@ export const ResumeChat: FC<{
|
||||
const createSessionProcess = useCreateSessionProcessMutation(projectId);
|
||||
const { config } = useConfig();
|
||||
|
||||
const handleSubmit = async (message: string) => {
|
||||
const handleSubmit = async (input: MessageInput) => {
|
||||
await createSessionProcess.mutateAsync({
|
||||
message,
|
||||
input,
|
||||
baseSessionId: sessionId,
|
||||
});
|
||||
};
|
||||
|
||||
17
src/lib/conversation-schema/content/DocumentContentSchema.ts
Normal file
17
src/lib/conversation-schema/content/DocumentContentSchema.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const DocumentContentSchema = z.object({
|
||||
type: z.literal("document"),
|
||||
source: z.union([
|
||||
z.object({
|
||||
media_type: z.literal("text/plain"),
|
||||
type: z.literal("text"),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
media_type: z.enum(["application/pdf"]),
|
||||
type: z.literal("base64"),
|
||||
data: z.string(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
@@ -5,6 +5,6 @@ export const ImageContentSchema = z.object({
|
||||
source: z.object({
|
||||
type: z.literal("base64"),
|
||||
data: z.string(),
|
||||
media_type: z.enum(["image/png"]),
|
||||
media_type: z.enum(["image/png", "image/jpeg", "image/gif", "image/webp"]),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { DocumentContentSchema } from "../content/DocumentContentSchema";
|
||||
import { ImageContentSchema } from "../content/ImageContentSchema";
|
||||
import { TextContentSchema } from "../content/TextContentSchema";
|
||||
import { ToolResultContentSchema } from "../content/ToolResultContentSchema";
|
||||
@@ -8,6 +9,7 @@ const UserMessageContentSchema = z.union([
|
||||
TextContentSchema,
|
||||
ToolResultContentSchema,
|
||||
ImageContentSchema,
|
||||
DocumentContentSchema,
|
||||
]);
|
||||
|
||||
export type UserMessageContent = z.infer<typeof UserMessageContentSchema>;
|
||||
|
||||
@@ -772,7 +772,7 @@
|
||||
"origin": [["src/components/scheduler/CronExpressionBuilder.tsx", 183]]
|
||||
},
|
||||
"user.content.image": {
|
||||
"message": "Image",
|
||||
"message": "Attached Image",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
@@ -781,7 +781,7 @@
|
||||
39
|
||||
]
|
||||
],
|
||||
"translation": "Image"
|
||||
"translation": "Attached Image"
|
||||
},
|
||||
"assistant.tool.input_parameters": {
|
||||
"message": "Input Parameters",
|
||||
@@ -1569,7 +1569,7 @@
|
||||
"translation": "Unsupported Media"
|
||||
},
|
||||
"user.content.image.description": {
|
||||
"message": "User uploaded image content",
|
||||
"message": "Image attached by user",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
@@ -1578,7 +1578,55 @@
|
||||
49
|
||||
]
|
||||
],
|
||||
"translation": "User uploaded image content"
|
||||
"translation": "Image attached by user"
|
||||
},
|
||||
"user.content.document.pdf": {
|
||||
"message": "PDF Document",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
121
|
||||
]
|
||||
],
|
||||
"translation": "PDF Document"
|
||||
},
|
||||
"user.content.document.text": {
|
||||
"message": "Text Document",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
163
|
||||
]
|
||||
],
|
||||
"translation": "Text Document"
|
||||
},
|
||||
"user.content.unsupported_document": {
|
||||
"message": "Unsupported Document",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
200
|
||||
]
|
||||
],
|
||||
"translation": "Unsupported Document"
|
||||
},
|
||||
"user.content.unsupported_document.description": {
|
||||
"message": "Document type not supported for display",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
210
|
||||
]
|
||||
],
|
||||
"translation": "Document type not supported for display"
|
||||
},
|
||||
"system_info.feature.agent_sdk.description": {
|
||||
"message": "Uses Claude Agent SDK instead of Claude Code SDK (v1.0.125+)",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -772,7 +772,7 @@
|
||||
"origin": [["src/components/scheduler/CronExpressionBuilder.tsx", 183]]
|
||||
},
|
||||
"user.content.image": {
|
||||
"message": "Image",
|
||||
"message": "Attached Image",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
@@ -781,7 +781,7 @@
|
||||
39
|
||||
]
|
||||
],
|
||||
"translation": "画像"
|
||||
"translation": "添付画像"
|
||||
},
|
||||
"assistant.tool.input_parameters": {
|
||||
"message": "Input Parameters",
|
||||
@@ -1569,7 +1569,7 @@
|
||||
"translation": "サポートされていないメディア"
|
||||
},
|
||||
"user.content.image.description": {
|
||||
"message": "User uploaded image content",
|
||||
"message": "Image attached by user",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
@@ -1578,7 +1578,55 @@
|
||||
49
|
||||
]
|
||||
],
|
||||
"translation": "ユーザーがアップロードした画像コンテンツ"
|
||||
"translation": "ユーザーが添付した画像"
|
||||
},
|
||||
"user.content.document.pdf": {
|
||||
"message": "PDF Document",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
121
|
||||
]
|
||||
],
|
||||
"translation": "PDF文書"
|
||||
},
|
||||
"user.content.document.text": {
|
||||
"message": "Text Document",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
163
|
||||
]
|
||||
],
|
||||
"translation": "テキスト文書"
|
||||
},
|
||||
"user.content.unsupported_document": {
|
||||
"message": "Unsupported Document",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
200
|
||||
]
|
||||
],
|
||||
"translation": "サポートされていない文書"
|
||||
},
|
||||
"user.content.unsupported_document.description": {
|
||||
"message": "Document type not supported for display",
|
||||
"placeholders": {},
|
||||
"comments": [],
|
||||
"origin": [
|
||||
[
|
||||
"src/app/projects/[projectId]/sessions/[sessionId]/components/conversationList/UserConversationContent.tsx",
|
||||
210
|
||||
]
|
||||
],
|
||||
"translation": "文書タイプは表示がサポートされていません"
|
||||
},
|
||||
"system_info.feature.agent_sdk.description": {
|
||||
"message": "Uses Claude Agent SDK instead of Claude Code SDK (v1.0.125+)",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,8 +2,18 @@ import type {
|
||||
SDKMessage,
|
||||
SDKUserMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import type {
|
||||
DocumentBlockParam,
|
||||
ImageBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources";
|
||||
import { controllablePromise } from "../../../../lib/controllablePromise";
|
||||
|
||||
export type UserMessageInput = {
|
||||
text: string;
|
||||
images?: readonly ImageBlockParam[];
|
||||
documents?: readonly DocumentBlockParam[];
|
||||
};
|
||||
|
||||
export type OnMessage = (message: SDKMessage) => void | Promise<void>;
|
||||
|
||||
export type MessageGenerator = () => AsyncGenerator<
|
||||
@@ -14,37 +24,61 @@ export type MessageGenerator = () => AsyncGenerator<
|
||||
|
||||
export const createMessageGenerator = (): {
|
||||
generateMessages: MessageGenerator;
|
||||
setNextMessage: (message: string) => void;
|
||||
setNextMessage: (input: UserMessageInput) => void;
|
||||
setHooks: (hooks: {
|
||||
onNextMessageSet?: (message: string) => void | Promise<void>;
|
||||
onNewUserMessageResolved?: (message: string) => void | Promise<void>;
|
||||
onNextMessageSet?: (input: UserMessageInput) => void | Promise<void>;
|
||||
onNewUserMessageResolved?: (
|
||||
input: UserMessageInput,
|
||||
) => void | Promise<void>;
|
||||
}) => void;
|
||||
} => {
|
||||
let sendMessagePromise = controllablePromise<string>();
|
||||
let sendMessagePromise = controllablePromise<UserMessageInput>();
|
||||
let registeredHooks: {
|
||||
onNextMessageSet: ((message: string) => void | Promise<void>)[];
|
||||
onNewUserMessageResolved: ((message: string) => void | Promise<void>)[];
|
||||
onNextMessageSet: ((input: UserMessageInput) => void | Promise<void>)[];
|
||||
onNewUserMessageResolved: ((
|
||||
input: UserMessageInput,
|
||||
) => void | Promise<void>)[];
|
||||
} = {
|
||||
onNextMessageSet: [],
|
||||
onNewUserMessageResolved: [],
|
||||
};
|
||||
|
||||
const createMessage = (message: string): SDKUserMessage => {
|
||||
const createMessage = (input: UserMessageInput): SDKUserMessage => {
|
||||
const { images = [], documents = [] } = input;
|
||||
|
||||
if (images.length === 0 && documents.length === 0) {
|
||||
return {
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: input.text,
|
||||
},
|
||||
parent_tool_use_id: null,
|
||||
} satisfies Omit<SDKUserMessage, "session_id"> as SDKUserMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "user",
|
||||
message: {
|
||||
role: "user",
|
||||
content: message,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: input.text,
|
||||
},
|
||||
...images,
|
||||
...documents,
|
||||
],
|
||||
},
|
||||
} as SDKUserMessage;
|
||||
};
|
||||
|
||||
async function* generateMessages(): ReturnType<MessageGenerator> {
|
||||
sendMessagePromise = controllablePromise<string>();
|
||||
sendMessagePromise = controllablePromise<UserMessageInput>();
|
||||
|
||||
while (true) {
|
||||
const message = await sendMessagePromise.promise;
|
||||
sendMessagePromise = controllablePromise<string>();
|
||||
sendMessagePromise = controllablePromise<UserMessageInput>();
|
||||
void Promise.allSettled(
|
||||
registeredHooks.onNewUserMessageResolved.map((hook) => hook(message)),
|
||||
);
|
||||
@@ -53,16 +87,18 @@ export const createMessageGenerator = (): {
|
||||
}
|
||||
}
|
||||
|
||||
const setNextMessage = (message: string) => {
|
||||
sendMessagePromise.resolve(message);
|
||||
const setNextMessage = (input: UserMessageInput) => {
|
||||
sendMessagePromise.resolve(input);
|
||||
void Promise.allSettled(
|
||||
registeredHooks.onNextMessageSet.map((hook) => hook(message)),
|
||||
registeredHooks.onNextMessageSet.map((hook) => hook(input)),
|
||||
);
|
||||
};
|
||||
|
||||
const setHooks = (hooks: {
|
||||
onNextMessageSet?: (message: string) => void | Promise<void>;
|
||||
onNewUserMessageResolved?: (message: string) => void | Promise<void>;
|
||||
onNextMessageSet?: (input: UserMessageInput) => void | Promise<void>;
|
||||
onNewUserMessageResolved?: (
|
||||
input: UserMessageInput,
|
||||
) => void | Promise<void>;
|
||||
}) => {
|
||||
registeredHooks = {
|
||||
onNextMessageSet: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Effect } from "effect";
|
||||
import type { UserEntry } from "../../../../lib/conversation-schema/entry/UserEntrySchema";
|
||||
import type { UserMessageInput } from "../functions/createMessageGenerator";
|
||||
import type { InitMessageContext } from "../types";
|
||||
import * as ClaudeCode from "./ClaudeCode";
|
||||
import type * as CCTask from "./ClaudeCodeTask";
|
||||
@@ -10,7 +11,7 @@ export type CCSessionProcessDef = {
|
||||
projectId: string;
|
||||
cwd: string;
|
||||
abortController: AbortController;
|
||||
setNextMessage: (message: string) => void;
|
||||
setNextMessage: (input: UserMessageInput) => void;
|
||||
};
|
||||
|
||||
type CCSessionProcessStateBase = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ControllerResponse } from "../../../lib/effect/toEffectResponse";
|
||||
import type { InferEffect } from "../../../lib/effect/types";
|
||||
import { UserConfigService } from "../../platform/services/UserConfigService";
|
||||
import { ProjectRepository } from "../../project/infrastructure/ProjectRepository";
|
||||
import type { UserMessageInput } from "../functions/createMessageGenerator";
|
||||
import { ClaudeCodeLifeCycleService } from "../services/ClaudeCodeLifeCycleService";
|
||||
|
||||
const LayerImpl = Effect.gen(function* () {
|
||||
@@ -33,11 +34,11 @@ const LayerImpl = Effect.gen(function* () {
|
||||
|
||||
const createSessionProcess = (options: {
|
||||
projectId: string;
|
||||
message: string;
|
||||
input: UserMessageInput;
|
||||
baseSessionId?: string | undefined;
|
||||
}) =>
|
||||
Effect.gen(function* () {
|
||||
const { projectId, message, baseSessionId } = options;
|
||||
const { projectId, input, baseSessionId } = options;
|
||||
|
||||
const { project } = yield* projectRepository.getProject(projectId);
|
||||
const userConfig = yield* userConfigService.getUserConfig();
|
||||
@@ -56,7 +57,7 @@ const LayerImpl = Effect.gen(function* () {
|
||||
sessionId: baseSessionId,
|
||||
},
|
||||
userConfig,
|
||||
message,
|
||||
input,
|
||||
});
|
||||
|
||||
const { sessionId } = yield* result.yieldSessionInitialized();
|
||||
@@ -75,13 +76,12 @@ const LayerImpl = Effect.gen(function* () {
|
||||
|
||||
const continueSessionProcess = (options: {
|
||||
projectId: string;
|
||||
continueMessage: string;
|
||||
input: UserMessageInput;
|
||||
baseSessionId: string;
|
||||
sessionProcessId: string;
|
||||
}) =>
|
||||
Effect.gen(function* () {
|
||||
const { projectId, continueMessage, baseSessionId, sessionProcessId } =
|
||||
options;
|
||||
const { projectId, input, baseSessionId, sessionProcessId } = options;
|
||||
|
||||
const { project } = yield* projectRepository.getProject(projectId);
|
||||
|
||||
@@ -94,7 +94,7 @@ const LayerImpl = Effect.gen(function* () {
|
||||
|
||||
const result = yield* claudeCodeLifeCycleService.continueTask({
|
||||
sessionProcessId,
|
||||
message: continueMessage,
|
||||
input,
|
||||
baseSessionId,
|
||||
});
|
||||
|
||||
|
||||
36
src/server/core/claude-code/schema.ts
Normal file
36
src/server/core/claude-code/schema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Schema for image block parameter
|
||||
*/
|
||||
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"]),
|
||||
data: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for user message input with optional images and documents
|
||||
*/
|
||||
export const userMessageInputSchema = z.object({
|
||||
text: z.string().min(1),
|
||||
images: z.array(imageBlockSchema).optional(),
|
||||
documents: z.array(documentBlockSchema).optional(),
|
||||
});
|
||||
|
||||
export type UserMessageInputSchema = z.infer<typeof userMessageInputSchema>;
|
||||
@@ -14,7 +14,10 @@ import type { EnvService } from "../../platform/services/EnvService";
|
||||
import { SessionRepository } from "../../session/infrastructure/SessionRepository";
|
||||
import { VirtualConversationDatabase } from "../../session/infrastructure/VirtualConversationDatabase";
|
||||
import type { SessionMetaService } from "../../session/services/SessionMetaService";
|
||||
import { createMessageGenerator } from "../functions/createMessageGenerator";
|
||||
import {
|
||||
createMessageGenerator,
|
||||
type UserMessageInput,
|
||||
} from "../functions/createMessageGenerator";
|
||||
import * as CCSessionProcess from "../models/CCSessionProcess";
|
||||
import * as ClaudeCode from "../models/ClaudeCode";
|
||||
import { ClaudeCodePermissionService } from "./ClaudeCodePermissionService";
|
||||
@@ -46,9 +49,9 @@ const LayerImpl = Effect.gen(function* () {
|
||||
const continueTask = (options: {
|
||||
sessionProcessId: string;
|
||||
baseSessionId: string;
|
||||
message: string;
|
||||
input: UserMessageInput;
|
||||
}) => {
|
||||
const { sessionProcessId, baseSessionId, message } = options;
|
||||
const { sessionProcessId, baseSessionId, input } = options;
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const { sessionProcess, task } =
|
||||
@@ -65,7 +68,7 @@ const LayerImpl = Effect.gen(function* () {
|
||||
const virtualConversation =
|
||||
yield* CCSessionProcess.createVirtualConversation(sessionProcess, {
|
||||
sessionId: baseSessionId,
|
||||
userMessage: message,
|
||||
userMessage: input.text,
|
||||
});
|
||||
|
||||
yield* virtualConversationDatabase.createVirtualConversation(
|
||||
@@ -74,7 +77,7 @@ const LayerImpl = Effect.gen(function* () {
|
||||
[virtualConversation],
|
||||
);
|
||||
|
||||
sessionProcess.def.setNextMessage(message);
|
||||
sessionProcess.def.setNextMessage(input);
|
||||
return {
|
||||
sessionProcess,
|
||||
task,
|
||||
@@ -89,9 +92,9 @@ const LayerImpl = Effect.gen(function* () {
|
||||
projectId: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
message: string;
|
||||
input: UserMessageInput;
|
||||
}) => {
|
||||
const { baseSession, message, userConfig } = options;
|
||||
const { baseSession, input, userConfig } = options;
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const {
|
||||
@@ -131,11 +134,11 @@ const LayerImpl = Effect.gen(function* () {
|
||||
}>();
|
||||
|
||||
setMessageGeneratorHooks({
|
||||
onNewUserMessageResolved: async (message) => {
|
||||
onNewUserMessageResolved: async (input) => {
|
||||
Effect.runFork(
|
||||
sessionProcessService.toNotInitializedState({
|
||||
sessionProcessId: sessionProcess.def.sessionProcessId,
|
||||
rawUserMessage: message,
|
||||
rawUserMessage: input.text,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -276,7 +279,7 @@ const LayerImpl = Effect.gen(function* () {
|
||||
}),
|
||||
);
|
||||
|
||||
setNextMessage(message);
|
||||
setNextMessage(input);
|
||||
|
||||
try {
|
||||
for await (const message of messageIter) {
|
||||
|
||||
@@ -138,7 +138,9 @@ const LayerImpl = Effect.gen(function* () {
|
||||
sessionId: undefined,
|
||||
},
|
||||
userConfig,
|
||||
message: "/init",
|
||||
input: {
|
||||
text: "/init",
|
||||
},
|
||||
});
|
||||
|
||||
const { sessionId } = yield* result.yieldSessionFileCreated();
|
||||
|
||||
@@ -27,7 +27,9 @@ export const executeJob = (job: SchedulerJob) =>
|
||||
sessionId: message.baseSessionId ?? undefined,
|
||||
},
|
||||
userConfig,
|
||||
message: message.content,
|
||||
input: {
|
||||
text: message.content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import packageJson from "../../../package.json" with { type: "json" };
|
||||
import { ClaudeCodeController } from "../core/claude-code/presentation/ClaudeCodeController";
|
||||
import { ClaudeCodePermissionController } from "../core/claude-code/presentation/ClaudeCodePermissionController";
|
||||
import { ClaudeCodeSessionProcessController } from "../core/claude-code/presentation/ClaudeCodeSessionProcessController";
|
||||
import { userMessageInputSchema } from "../core/claude-code/schema";
|
||||
import { ClaudeCodeLifeCycleService } from "../core/claude-code/services/ClaudeCodeLifeCycleService";
|
||||
import { TypeSafeSSE } from "../core/events/functions/typeSafeSSE";
|
||||
import { SSEController } from "../core/events/presentation/SSEController";
|
||||
@@ -356,7 +357,7 @@ export const routes = (app: HonoAppType) =>
|
||||
"json",
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
message: z.string(),
|
||||
input: userMessageInputSchema,
|
||||
baseSessionId: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
@@ -378,7 +379,7 @@ export const routes = (app: HonoAppType) =>
|
||||
"json",
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
continueMessage: z.string(),
|
||||
input: userMessageInputSchema,
|
||||
baseSessionId: z.string(),
|
||||
}),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user