163 lines
5.5 KiB
TypeScript
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);
|