Files
Alfred/src/web/features/chat/parts/ToolPart.tsx

125 lines
4.4 KiB
TypeScript

import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from "@ant-design/icons";
import { Collapse, Flex, Typography } from "antd";
import { HighlightBlock } from "./HighlightBlock";
import type { PartProps } from "./types";
interface ToolPartData {
errorText?: string;
input?: unknown;
output?: unknown;
toolCallId?: string;
toolMetadata?: Record<string, unknown>;
toolName?: string;
type?: string;
}
function getToolState(part: ToolPartData) {
if ("errorText" in part && part.errorText) return "output-error" as const;
if ("output" in part) return "output-available" as const;
if ("input" in part) return "input-available" as const;
return "input-streaming" as const;
}
function getInputLang(value: unknown): string {
return typeof value === "object" && value !== null ? "json" : "text";
}
function getOutputLang(value: unknown): string {
return typeof value === "object" && value !== null ? "json" : "text";
}
function formatContent(value: unknown): string {
if (typeof value === "object" && value !== null) return JSON.stringify(value, null, 2);
if (typeof value === "string") return value;
return String(value);
}
export function ToolPart({ part }: PartProps) {
const toolPart = part as unknown as ToolPartData;
const state = getToolState(toolPart);
const rawToolName = toolPart.toolName ?? (toolPart.type ?? "unknown").replace(/^tool-/, "");
const toolName =
typeof toolPart.toolMetadata?.["displayName"] === "string" ? toolPart.toolMetadata["displayName"] : rawToolName;
const isStreaming = state === "input-streaming" || state === "input-available";
const formattedInput = toolPart.input != null ? formatContent(toolPart.input) : "";
const inputLang = toolPart.input != null ? getInputLang(toolPart.input) : "text";
const hasOutput = "output" in toolPart && toolPart.output != null;
const formattedOutput = hasOutput ? formatContent(toolPart.output) : "";
const outputLang = hasOutput ? getOutputLang(toolPart.output) : "text";
if (state === "output-error") {
return (
<Collapse
ghost
items={[
{
children: (
<div className="tool-call-section">
<Typography.Text type="danger"></Typography.Text>
<HighlightBlock code={toolPart.errorText!} isStreaming={false} lang="text" />
</div>
),
key: toolPart.toolCallId ?? toolName,
label: (
<Flex align="center" component="span" gap={4}>
<CloseCircleFilled className="icon-error" />
<Typography.Text type="danger">{toolName} </Typography.Text>
</Flex>
),
},
]}
size="small"
/>
);
}
return (
<Collapse
defaultActiveKey={isStreaming ? [toolPart.toolCallId ?? toolName] : undefined}
ghost
items={[
{
children: (
<Flex gap={8} vertical>
{toolPart.input != null && (
<div className="tool-call-section">
<Typography.Text type="secondary"></Typography.Text>
<HighlightBlock code={formattedInput} isStreaming={isStreaming} lang={inputLang} />
</div>
)}
{hasOutput && (
<div className="tool-call-section">
<Typography.Text type="secondary"></Typography.Text>
<HighlightBlock code={formattedOutput} isStreaming={isStreaming} lang={outputLang} />
</div>
)}
{!toolPart.input && !("output" in toolPart) && (
<Typography.Text type="secondary">...</Typography.Text>
)}
</Flex>
),
key: toolPart.toolCallId ?? toolName,
label: isStreaming ? (
<Flex align="center" component="span" gap={4}>
<LoadingOutlined className="icon-primary" />
<Typography.Text type="secondary">
{state === "input-streaming" ? "生成参数" : `调用 ${toolName}`}
</Typography.Text>
</Flex>
) : (
<Flex align="center" component="span" gap={4}>
<CheckCircleFilled className="icon-success" />
<Typography.Text type="secondary">{toolName}</Typography.Text>
</Flex>
),
},
]}
size="small"
/>
);
}