Files
2026-03-13 07:11:48 +08:00

163 lines
5.5 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { ChevronDown, ChevronRight, Wrench } from 'lucide-react';
import { Badge, Button } from '@/components/UI';
import { cn } from '@/lib/utils';
import type { DebugTranscriptToolRow } from './types';
const shouldAutoExpand = (status: DebugTranscriptToolRow['status']) =>
status === 'pending' || status === 'error' || status === 'timeout';
const formatStructuredValue = (value: unknown) => {
if (value === undefined || value === null) return '';
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) return '';
try {
return JSON.stringify(JSON.parse(trimmed), null, 2);
} catch {
return value;
}
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
};
const getStatusBadge = (status: DebugTranscriptToolRow['status']) => {
if (status === 'success') {
return { label: 'Success', variant: 'success' as const, className: '' };
}
if (status === 'pending') {
return { label: 'Pending', variant: 'warning' as const, className: '' };
}
if (status === 'timeout') {
return {
label: 'Timeout',
variant: 'outline' as const,
className: 'border-orange-400/40 bg-orange-500/10 text-orange-200',
};
}
return {
label: 'Error',
variant: 'outline' as const,
className: 'border-rose-400/40 bg-rose-500/10 text-rose-200',
};
};
const Section: React.FC<{
label: string;
value: unknown;
defaultOpen?: boolean;
}> = ({ label, value, defaultOpen = true }) => {
const formattedValue = useMemo(() => formatStructuredValue(value), [value]);
if (!formattedValue) return null;
return (
<details
open={defaultOpen}
className="rounded-md border border-white/10 bg-black/20"
>
<summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground">
{label}
</summary>
<pre className="overflow-x-auto whitespace-pre-wrap break-all border-t border-white/10 px-3 py-3 text-[11px] leading-5 text-foreground/90">
{formattedValue}
</pre>
</details>
);
};
const MessageTool: React.FC<{
row: DebugTranscriptToolRow;
nested?: boolean;
}> = ({ row, nested = false }) => {
const [isExpanded, setIsExpanded] = useState(() => shouldAutoExpand(row.status));
useEffect(() => {
setIsExpanded(shouldAutoExpand(row.status));
}, [row.status]);
const statusBadge = getStatusBadge(row.status);
return (
<div className={cn(nested ? 'w-full' : 'flex justify-start')}>
<div
className={cn(
'w-full max-w-full border border-amber-400/30 bg-amber-500/10 p-3 text-amber-50',
nested ? 'rounded-md' : 'rounded-lg'
)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-md border border-amber-300/30 bg-black/20 text-amber-200">
<Wrench className="h-4 w-4" />
</div>
<div className="min-w-0">
<div className="truncate text-sm font-medium text-foreground">
{row.toolDisplayName || row.toolName}
</div>
<div className="truncate text-[11px] text-amber-100/70">
{row.toolName}
</div>
</div>
</div>
<Badge variant={statusBadge.variant} className={statusBadge.className}>
{statusBadge.label}
</Badge>
<Badge variant="outline" className="border-white/15 bg-black/10 text-[10px] uppercase">
{row.executor}
</Badge>
{row.source && (
<Badge variant="outline" className="border-white/15 bg-black/10 text-[10px] uppercase">
{row.source}
</Badge>
)}
</div>
<div className="mt-2 text-[11px] text-amber-100/70">
tool_call_id: <span className="font-mono">{row.toolCallId}</span>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2 text-amber-100 hover:bg-white/10 hover:text-foreground"
onClick={() => setIsExpanded((value) => !value)}
aria-expanded={isExpanded}
>
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
</div>
<div
className={cn(
'grid transition-all',
isExpanded ? 'mt-3 grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
)}
>
<div className="min-h-0 overflow-hidden">
<div className="space-y-2 pt-1">
<Section label="Arguments" value={row.args} defaultOpen={row.status === 'pending'} />
<Section label="Result" value={row.result} />
<Section label="Error" value={row.error} defaultOpen />
<Section label="Raw call" value={row.rawCall} defaultOpen={false} />
<Section label="Raw result" value={row.rawResult} defaultOpen={false} />
</div>
</div>
</div>
</div>
</div>
);
};
export default React.memo(MessageTool);