125 lines
4.4 KiB
TypeScript
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"
|
|
/>
|
|
);
|
|
}
|