Files
UHD_Media_Tool/apps/web/app/components/agent/tool-call-card-io.tsx
T

88 lines
3.7 KiB
TypeScript

import { Braces } from "lucide-react";
import { useState } from "react";
import { useCapabilityTools } from "~/api/mcp.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { JsonView } from "~/components/console/json-view.tsx";
import { KvList } from "~/components/console/kv-list.tsx";
import { Button } from "~/components/ui/button.tsx";
import { formatDateTime } from "~/lib/utils.ts";
import type { ToolCallView } from "./use-agent-stream.ts";
// 会话页工具卡片的"查看 schema 与本次输入输出"抽屉。
// 本次输入输出取事件流里的摘要:服务端已脱敏(凭证键一律遮盖),超长部分截断;schema 取能力清单(GET /capabilities/tools)。
/** 摘要尽量按 JSON 美化;截断后的摘要不是合法 JSON,原样显示 */
export function parseSummary(text: string | null): { value: unknown; truncated: boolean } {
if (text === null) return { value: null, truncated: false };
const truncated = /…\(\d+ more\)$/.test(text);
if (truncated) return { value: text, truncated };
try {
return { value: JSON.parse(text), truncated };
} catch {
return { value: text, truncated };
}
}
export function ToolCallIo({ call }: { call: ToolCallView }) {
const [open, setOpen] = useState(false);
return (
<>
<Button size="sm" variant="ghost" className="justify-self-start" onClick={() => setOpen(true)}>
<Braces />
查看 schema 与输入输出
</Button>
{open ? <ToolCallIoSheet call={call} onClose={() => setOpen(false)} /> : null}
</>
);
}
function ToolCallIoSheet({ call, onClose }: { call: ToolCallView; onClose: () => void }) {
const tools = useCapabilityTools();
const tool = tools.data?.items.find((t) => t.id === call.capabilityId) ?? null;
const input = parseSummary(call.inputSummary);
const output = parseSummary(call.outputSummary);
const failed = call.status === "failed" || call.status === "denied";
return (
<DetailSheet
open
onOpenChange={(o) => !o && onClose()}
title={tool?.title ?? call.capabilityId}
description={call.capabilityId}
width="lg"
>
<KvList
items={[
{ label: "调用 ID", value: call.toolCallId, mono: true, wide: true },
{ label: "开始", value: formatDateTime(call.startedAt) },
{ label: "结束", value: call.endedAt ? formatDateTime(call.endedAt) : "—" },
]}
/>
<DetailSection title="本次输入(已脱敏)">
<JsonView value={input.value} maxHeight="18rem" />
</DetailSection>
<DetailSection title={failed ? "本次错误" : "本次输出(已脱敏)"}>
<JsonView value={output.value ?? call.reason} maxHeight="18rem" tone={failed ? "destructive" : "neutral"} />
</DetailSection>
{input.truncated || output.truncated ? (
<p className="text-caption text-muted-foreground">只保存前 500 个字符,其余已截断。</p>
) : null}
<DetailSection title="输入 schema">
{tools.isPending ? (
<LoadingState rows={2} />
) : tools.error ? (
<ErrorState title="读取能力清单失败" error={tools.error} onRetry={() => tools.refetch()} plain />
) : tool ? (
<JsonView value={tool.inputJsonSchema} maxHeight="20rem" />
) : (
<p className="text-caption text-muted-foreground">这个能力未注册,没有 schema。</p>
)}
</DetailSection>
{tool ? (
<JsonView value={tool.outputJsonSchema} maxHeight="20rem" defaultOpen={false} label="输出 schema" />
) : null}
</DetailSheet>
);
}