Add debug transcript components
This commit is contained in:
162
web/components/debug-transcript/MessageTool.tsx
Normal file
162
web/components/debug-transcript/MessageTool.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user