feat(ui): playground/A2AMetrics to shadcn

- antd Tooltip/Button + full @ant-design/icons set (CheckCircle/Clock/
  Loading/ExclamationCircle/Copy/Down/Right/Link/FileText/Robot) \u2192
  shadcn Tooltip/Button + lucide AlertCircle/Bot/CheckCircle2/
  ChevronDown/ChevronRight/Clock/Copy/FileText/Link/Loader2.
- Status pill palette (emerald/blue/red/muted) + categorical TTFT
  green / latency blue accents. File added to .eslintrc overrides.
- Inline code blocks and details panel use bg-muted/bg-background
  semantic tokens.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 16:25:01 +00:00
parent 9b86cd428f
commit c235ee3a7f
No known key found for this signature in database
2 changed files with 132 additions and 95 deletions

View file

@ -62,6 +62,7 @@
"src/components/prompts/prompt_table.tsx",
"src/components/common_components/ModelAliasManager.tsx",
"src/components/mcp_tools/mcp_server_columns.tsx",
"src/components/playground/chat_ui/A2AMetrics.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,17 +1,24 @@
import React, { useState } from "react";
import { Tooltip, Button } from "antd";
import { Button } from "@/components/ui/button";
import {
CheckCircleOutlined,
ClockCircleOutlined,
LoadingOutlined,
ExclamationCircleOutlined,
CopyOutlined,
DownOutlined,
RightOutlined,
LinkOutlined,
FileTextOutlined,
RobotOutlined,
} from "@ant-design/icons";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
AlertCircle,
Bot,
CheckCircle2,
ChevronDown,
ChevronRight,
Clock,
Copy,
FileText,
Link as LinkIcon,
Loader2,
} from "lucide-react";
export interface A2ATaskMetadata {
taskId?: string;
@ -21,6 +28,7 @@ export interface A2ATaskMetadata {
timestamp?: string;
message?: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: Record<string, any>;
}
@ -33,30 +41,30 @@ interface A2AMetricsProps {
const getStatusIcon = (state?: string) => {
switch (state) {
case "completed":
return <CheckCircleOutlined className="text-green-500" />;
return <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />;
case "working":
case "submitted":
return <LoadingOutlined className="text-blue-500" />;
return <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500" />;
case "failed":
case "canceled":
return <ExclamationCircleOutlined className="text-red-500" />;
return <AlertCircle className="h-3.5 w-3.5 text-red-500" />;
default:
return <ClockCircleOutlined className="text-gray-500" />;
return <Clock className="h-3.5 w-3.5 text-muted-foreground" />;
}
};
const getStatusColor = (state?: string) => {
switch (state) {
case "completed":
return "bg-green-100 text-green-700";
return "bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-300";
case "working":
case "submitted":
return "bg-blue-100 text-blue-700";
return "bg-blue-100 text-blue-700 dark:bg-blue-950/30 dark:text-blue-300";
case "failed":
case "canceled":
return "bg-red-100 text-red-700";
return "bg-red-100 text-red-700 dark:bg-red-950/30 dark:text-red-300";
default:
return "bg-gray-100 text-gray-700";
return "bg-muted text-muted-foreground";
}
};
@ -79,7 +87,11 @@ const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken, totalLatency }) => {
const A2AMetrics: React.FC<A2AMetricsProps> = ({
a2aMetadata,
timeToFirstToken,
totalLatency,
}) => {
const [showDetails, setShowDetails] = useState(false);
if (!a2aMetadata && !timeToFirstToken && !totalLatency) return null;
@ -88,115 +100,138 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
const formattedTime = formatTimestamp(status?.timestamp);
return (
<div className="a2a-metrics mt-3 pt-2 border-t border-gray-200 text-xs">
{/* A2A Metadata Header */}
<div className="flex items-center mb-2 text-gray-600">
<RobotOutlined className="mr-1.5 text-blue-500" />
<span className="font-medium text-gray-700">A2A Metadata</span>
<div className="a2a-metrics mt-3 pt-2 border-t border-border text-xs">
<div className="flex items-center mb-2 text-muted-foreground">
<Bot className="h-3.5 w-3.5 mr-1.5 text-blue-500" />
<span className="font-medium text-foreground">A2A Metadata</span>
</div>
{/* Main metrics row */}
<div className="flex flex-wrap items-center gap-2 text-gray-500 ml-4">
{/* Status badge */}
<div className="flex flex-wrap items-center gap-2 text-muted-foreground ml-4">
{status?.state && (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${getStatusColor(status.state)}`}>
<span
className={cn(
"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium gap-1",
getStatusColor(status.state),
)}
>
{getStatusIcon(status.state)}
<span className="ml-1 capitalize">{status.state}</span>
<span className="capitalize">{status.state}</span>
</span>
)}
{/* Timestamp */}
{formattedTime && (
<Tooltip title={status?.timestamp}>
<span className="flex items-center">
<ClockCircleOutlined className="mr-1" />
{formattedTime}
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center">
<Clock className="h-3.5 w-3.5 mr-1" />
{formattedTime}
</span>
</TooltipTrigger>
<TooltipContent>{status?.timestamp}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Latency */}
{totalLatency !== undefined && (
<Tooltip title="Total latency">
<span className="flex items-center text-blue-600">
<ClockCircleOutlined className="mr-1" />
{(totalLatency / 1000).toFixed(2)}s
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center text-blue-600">
<Clock className="h-3.5 w-3.5 mr-1" />
{(totalLatency / 1000).toFixed(2)}s
</span>
</TooltipTrigger>
<TooltipContent>Total latency</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Time to first token */}
{timeToFirstToken !== undefined && (
<Tooltip title="Time to first token">
<span className="flex items-center text-green-600">
TTFT: {(timeToFirstToken / 1000).toFixed(2)}s
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center text-emerald-600">
TTFT: {(timeToFirstToken / 1000).toFixed(2)}s
</span>
</TooltipTrigger>
<TooltipContent>Time to first token</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{/* IDs row */}
<div className="flex flex-wrap items-center gap-3 text-gray-500 ml-4 mt-1.5">
{/* Task ID */}
<div className="flex flex-wrap items-center gap-3 text-muted-foreground ml-4 mt-1.5">
{taskId && (
<Tooltip title={`Click to copy: ${taskId}`}>
<span
className="flex items-center cursor-pointer hover:text-gray-700"
onClick={() => copyToClipboard(taskId)}
>
<FileTextOutlined className="mr-1" />
Task: {truncateId(taskId)}
<CopyOutlined className="ml-1 text-gray-400 hover:text-gray-600" />
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className="flex items-center cursor-pointer hover:text-foreground"
onClick={() => copyToClipboard(taskId)}
>
<FileText className="h-3.5 w-3.5 mr-1" />
Task: {truncateId(taskId)}
<Copy className="h-3 w-3 ml-1 text-muted-foreground hover:text-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Click to copy: {taskId}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Context/Session ID */}
{contextId && (
<Tooltip title={`Click to copy: ${contextId}`}>
<span
className="flex items-center cursor-pointer hover:text-gray-700"
onClick={() => copyToClipboard(contextId)}
>
<LinkOutlined className="mr-1" />
Session: {truncateId(contextId)}
<CopyOutlined className="ml-1 text-gray-400 hover:text-gray-600" />
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className="flex items-center cursor-pointer hover:text-foreground"
onClick={() => copyToClipboard(contextId)}
>
<LinkIcon className="h-3.5 w-3.5 mr-1" />
Session: {truncateId(contextId)}
<Copy className="h-3 w-3 ml-1 text-muted-foreground hover:text-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Click to copy: {contextId}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Details toggle */}
{(metadata || status?.message) && (
<Button
type="text"
size="small"
variant="ghost"
size="sm"
className="text-xs text-blue-500 hover:text-blue-700 p-0 h-auto"
onClick={() => setShowDetails(!showDetails)}
>
{showDetails ? <DownOutlined /> : <RightOutlined />}
{showDetails ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<span className="ml-1">Details</span>
</Button>
)}
</div>
{/* Expandable details panel */}
{showDetails && (
<div className="mt-2 ml-4 p-3 bg-gray-50 rounded-md text-gray-600 border border-gray-200">
{/* Status message */}
<div className="mt-2 ml-4 p-3 bg-muted rounded-md text-muted-foreground border border-border">
{status?.message && (
<div className="mb-2">
<span className="font-medium text-gray-700">Status Message:</span>
<span className="font-medium text-foreground">Status Message:</span>
<span className="ml-2">{status.message}</span>
</div>
)}
{/* Full IDs */}
{taskId && (
<div className="mb-1.5 flex items-center">
<span className="font-medium text-gray-700 w-24">Task ID:</span>
<code className="ml-2 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-mono">{taskId}</code>
<CopyOutlined
className="ml-2 cursor-pointer text-gray-400 hover:text-blue-500"
<span className="font-medium text-foreground w-24">Task ID:</span>
<code className="ml-2 px-2 py-1 bg-background border border-border rounded text-xs font-mono">
{taskId}
</code>
<Copy
className="ml-2 h-3.5 w-3.5 cursor-pointer text-muted-foreground hover:text-primary"
onClick={() => copyToClipboard(taskId)}
/>
</div>
@ -204,20 +239,21 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{contextId && (
<div className="mb-1.5 flex items-center">
<span className="font-medium text-gray-700 w-24">Session ID:</span>
<code className="ml-2 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-mono">{contextId}</code>
<CopyOutlined
className="ml-2 cursor-pointer text-gray-400 hover:text-blue-500"
<span className="font-medium text-foreground w-24">Session ID:</span>
<code className="ml-2 px-2 py-1 bg-background border border-border rounded text-xs font-mono">
{contextId}
</code>
<Copy
className="ml-2 h-3.5 w-3.5 cursor-pointer text-muted-foreground hover:text-primary"
onClick={() => copyToClipboard(contextId)}
/>
</div>
)}
{/* Metadata fields */}
{metadata && Object.keys(metadata).length > 0 && (
<div className="mt-3">
<span className="font-medium text-gray-700">Custom Metadata:</span>
<pre className="mt-1.5 p-2 bg-white border border-gray-200 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
<span className="font-medium text-foreground">Custom Metadata:</span>
<pre className="mt-1.5 p-2 bg-background border border-border rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(metadata, null, 2)}
</pre>
</div>