feat: system information view

This commit is contained in:
d-kimsuon
2025-10-20 03:00:13 +09:00
parent 81a5d31f6e
commit 0047b6b2a2
15 changed files with 881 additions and 180 deletions

View File

@@ -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>
);

View File

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

View File

@@ -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}
/>
);

View File

@@ -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"

View File

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