mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-10 09:04:24 +01:00
feat: system information view
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Trans } from "@lingui/react";
|
||||
import { ChevronDown, Lightbulb, Settings } from "lucide-react";
|
||||
import { ChevronDown, Lightbulb, Wrench } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import type { FC } from "react";
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
oneDark,
|
||||
oneLight,
|
||||
} from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import z from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
@@ -25,12 +26,28 @@ import {
|
||||
} from "@/components/ui/collapsible";
|
||||
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
|
||||
import type { AssistantMessageContent } from "@/lib/conversation-schema/message/AssistantMessageSchema";
|
||||
import { Button } from "../../../../../../../components/ui/button";
|
||||
import type { SidechainConversation } from "../../../../../../../lib/conversation-schema";
|
||||
import { MarkdownContent } from "../../../../../../components/MarkdownContent";
|
||||
import { SidechainConversationModal } from "../conversationModal/SidechainConversationModal";
|
||||
|
||||
const taskToolInputSchema = z.object({
|
||||
prompt: z.string(),
|
||||
});
|
||||
|
||||
export const AssistantConversationContent: FC<{
|
||||
content: AssistantMessageContent;
|
||||
getToolResult: (toolUseId: string) => ToolResultContent | undefined;
|
||||
}> = ({ content, getToolResult }) => {
|
||||
getSidechainConversationByPrompt: (
|
||||
prompt: string,
|
||||
) => SidechainConversation | undefined;
|
||||
getSidechainConversations: (rootUuid: string) => SidechainConversation[];
|
||||
}> = ({
|
||||
content,
|
||||
getToolResult,
|
||||
getSidechainConversationByPrompt,
|
||||
getSidechainConversations,
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const syntaxTheme = resolvedTheme === "dark" ? oneDark : oneLight;
|
||||
if (content.type === "text") {
|
||||
@@ -71,11 +88,48 @@ export const AssistantConversationContent: FC<{
|
||||
if (content.type === "tool_use") {
|
||||
const toolResult = getToolResult(content.id);
|
||||
|
||||
const taskModal = (() => {
|
||||
const taskInput =
|
||||
content.name === "Task"
|
||||
? taskToolInputSchema.safeParse(content.input)
|
||||
: undefined;
|
||||
|
||||
if (taskInput === undefined || taskInput.success === false) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversation = getSidechainConversationByPrompt(
|
||||
taskInput.data.prompt,
|
||||
);
|
||||
|
||||
if (conversation === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidechainConversationModal
|
||||
conversation={conversation}
|
||||
sidechainConversations={getSidechainConversations(
|
||||
conversation.uuid,
|
||||
).map((original) => ({
|
||||
...original,
|
||||
isSidechain: false,
|
||||
}))}
|
||||
getToolResult={getToolResult}
|
||||
trigger={
|
||||
<Button variant="outline" size="sm">
|
||||
View Log
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<Card className="border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20 gap-2 py-3 mb-2">
|
||||
<CardHeader className="py-0 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<Wrench className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<CardTitle className="text-sm font-medium">
|
||||
<Trans id="assistant.tool_use" message="Tool Use" />
|
||||
</CardTitle>
|
||||
@@ -159,6 +213,7 @@ export const AssistantConversationContent: FC<{
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
{taskModal}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
import type { Conversation } from "@/lib/conversation-schema";
|
||||
import type {
|
||||
Conversation,
|
||||
SidechainConversation,
|
||||
} from "@/lib/conversation-schema";
|
||||
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
|
||||
import { SidechainConversationModal } from "../conversationModal/SidechainConversationModal";
|
||||
import { AssistantConversationContent } from "./AssistantConversationContent";
|
||||
@@ -13,11 +16,15 @@ export const ConversationItem: FC<{
|
||||
conversation: Conversation;
|
||||
getToolResult: (toolUseId: string) => ToolResultContent | undefined;
|
||||
isRootSidechain: (conversation: Conversation) => boolean;
|
||||
getSidechainConversations: (rootUuid: string) => Conversation[];
|
||||
getSidechainConversationByPrompt: (
|
||||
prompt: string,
|
||||
) => SidechainConversation | undefined;
|
||||
getSidechainConversations: (rootUuid: string) => SidechainConversation[];
|
||||
}> = ({
|
||||
conversation,
|
||||
getToolResult,
|
||||
isRootSidechain,
|
||||
getSidechainConversationByPrompt,
|
||||
getSidechainConversations,
|
||||
}) => {
|
||||
if (conversation.type === "summary") {
|
||||
@@ -54,13 +61,10 @@ export const ConversationItem: FC<{
|
||||
conversation={conversation}
|
||||
sidechainConversations={getSidechainConversations(
|
||||
conversation.uuid,
|
||||
).map((original) => {
|
||||
if (original.type === "summary") return original;
|
||||
return {
|
||||
...original,
|
||||
isSidechain: false,
|
||||
};
|
||||
})}
|
||||
).map((original) => ({
|
||||
...original,
|
||||
isSidechain: false,
|
||||
}))}
|
||||
getToolResult={getToolResult}
|
||||
/>
|
||||
);
|
||||
@@ -99,6 +103,10 @@ export const ConversationItem: FC<{
|
||||
<AssistantConversationContent
|
||||
content={content}
|
||||
getToolResult={getToolResult}
|
||||
getSidechainConversationByPrompt={
|
||||
getSidechainConversationByPrompt
|
||||
}
|
||||
getSidechainConversations={getSidechainConversations}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -126,8 +126,11 @@ export const ConversationList: FC<ConversationListProps> = ({
|
||||
conversations.filter((conversation) => conversation.type !== "x-error"),
|
||||
[conversations],
|
||||
);
|
||||
const { isRootSidechain, getSidechainConversations } =
|
||||
useSidechain(validConversations);
|
||||
const {
|
||||
isRootSidechain,
|
||||
getSidechainConversations,
|
||||
getSidechainConversationByPrompt,
|
||||
} = useSidechain(validConversations);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
@@ -148,6 +151,7 @@ export const ConversationList: FC<ConversationListProps> = ({
|
||||
getToolResult={getToolResult}
|
||||
isRootSidechain={isRootSidechain}
|
||||
getSidechainConversations={getSidechainConversations}
|
||||
getSidechainConversationByPrompt={getSidechainConversationByPrompt}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ import type {
|
||||
SidechainConversation,
|
||||
} from "@/lib/conversation-schema";
|
||||
import type { ToolResultContent } from "@/lib/conversation-schema/content/ToolResultContentSchema";
|
||||
import { extractFirstUserText } from "../../../../../../../server/core/session/functions/extractFirstUserText";
|
||||
import { ConversationList } from "../conversationList/ConversationList";
|
||||
|
||||
type SidechainConversationModalProps = {
|
||||
conversation: SidechainConversation;
|
||||
sidechainConversations: Conversation[];
|
||||
trigger?: React.ReactNode;
|
||||
getToolResult: (toolUseId: string) => ToolResultContent | undefined;
|
||||
};
|
||||
|
||||
@@ -38,29 +40,12 @@ const sidechainTitle = (conversations: Conversation[]): string => {
|
||||
return defaultTitle;
|
||||
}
|
||||
|
||||
if (firstConversation.type !== "user") {
|
||||
return defaultTitle;
|
||||
}
|
||||
|
||||
const textContent =
|
||||
typeof firstConversation.message.content === "string"
|
||||
? firstConversation.message.content
|
||||
: (() => {
|
||||
const firstContent = firstConversation.message.content.at(0);
|
||||
if (firstContent === undefined) return null;
|
||||
|
||||
if (typeof firstContent === "string") return firstContent;
|
||||
if (firstContent.type === "text") return firstContent.text;
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
return textContent ?? defaultTitle;
|
||||
return extractFirstUserText(firstConversation) ?? defaultTitle;
|
||||
};
|
||||
|
||||
export const SidechainConversationModal: FC<
|
||||
SidechainConversationModalProps
|
||||
> = ({ conversation, sidechainConversations, getToolResult }) => {
|
||||
> = ({ conversation, sidechainConversations, trigger, getToolResult }) => {
|
||||
const title = sidechainTitle(sidechainConversations);
|
||||
|
||||
const rootUuid = conversation.uuid;
|
||||
@@ -68,19 +53,21 @@ export const SidechainConversationModal: FC<
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mb-3 items-center justify-start"
|
||||
data-testid="sidechain-task-button"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Eye className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="overflow-hidden text-ellipsis">
|
||||
View Task: {title}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
{trigger ?? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full mb-3 items-center justify-start"
|
||||
data-testid="sidechain-task-button"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Eye className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="overflow-hidden text-ellipsis">
|
||||
View Task: {title}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="w-[95vw] md:w-[90vw] max-h-[80vh] overflow-hidden flex flex-col px-2 md:px-8"
|
||||
|
||||
@@ -15,6 +15,19 @@ export const useSidechain = (conversations: Conversation[]) => {
|
||||
);
|
||||
}, [sidechainConversations]);
|
||||
|
||||
const conversationPromptMap = useMemo(() => {
|
||||
return new Map<string, SidechainConversation>(
|
||||
sidechainConversations
|
||||
.filter((conv) => conv.type === "user")
|
||||
.filter(
|
||||
(conv) =>
|
||||
conv.parentUuid === null &&
|
||||
typeof conv.message.content === "string",
|
||||
)
|
||||
.map((conv) => [conv.message.content as string, conv] as const),
|
||||
);
|
||||
}, [sidechainConversations]);
|
||||
|
||||
const getRootConversationRecursive = useCallback(
|
||||
(conversation: SidechainConversation): SidechainConversation => {
|
||||
if (conversation.parentUuid === null) {
|
||||
@@ -72,8 +85,16 @@ export const useSidechain = (conversations: Conversation[]) => {
|
||||
[sidechainConversationGroups],
|
||||
);
|
||||
|
||||
const getSidechainConversationByPrompt = useCallback(
|
||||
(prompt: string) => {
|
||||
return conversationPromptMap.get(prompt);
|
||||
},
|
||||
[conversationPromptMap],
|
||||
);
|
||||
|
||||
return {
|
||||
isRootSidechain,
|
||||
getSidechainConversations,
|
||||
getSidechainConversationByPrompt,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user