feat: clickable terminal events, node inspector drawer, stop animation on complete, vite 8

1. Terminal: remove inline prompts/full text; show short summary per event;
   click any event to open detail drawer with full request/response/model/metrics
2. Fix node "thinking" animation: shimmer only when status=running;
   on_chat_model_end (result) transitions node to completed, animation stops
3. Link nodes to events: clicking a graph node opens the drawer showing
   all events for that node (prompts, tool calls, results)
4. Upgrade Vite 5→8.0.1, @vitejs/plugin-react→5.2.0;
   update tsconfig moduleResolution to "bundler" for Vite 8 compat

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>
Agent-Logs-Url: https://github.com/aguzererler/TradingAgents/sessions/93c31c35-9509-4254-96fd-6f47aad07927
This commit is contained in:
copilot-swe-agent[bot] 2026-03-23 07:03:48 +00:00
parent 06e913f1ba
commit cf2df83c38
6 changed files with 730 additions and 758 deletions

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitejs/plugin-react": "^5.2.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
@ -33,6 +33,6 @@
"postcss": "^8.4.47",
"tailwindcss": "^3.4.0",
"typescript": "^5.6.0",
"vite": "^5.4.0"
"vite": "^8.0.1"
}
}

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import {
Box,
Flex,
@ -13,10 +13,13 @@ import {
DrawerContent,
DrawerHeader,
DrawerBody,
DrawerCloseButton,
Divider,
Tag,
Code,
Badge,
} from '@chakra-ui/react';
import { LayoutDashboard, Wallet, Settings, Play, Terminal as TerminalIcon, ChevronRight } from 'lucide-react';
import { LayoutDashboard, Wallet, Settings, Play, Terminal as TerminalIcon, ChevronRight, Eye } from 'lucide-react';
import { MetricHeader } from './components/MetricHeader';
import { AgentGraph } from './components/AgentGraph';
import { useAgentStream, AgentEvent } from './hooks/useAgentStream';
@ -45,13 +48,101 @@ const eventLabel = (type: AgentEvent['type']): string => {
}
};
/** Short summary for terminal — no inline prompts, just agent + type. */
const eventSummary = (evt: AgentEvent): string => {
switch (evt.type) {
case 'thought': return `Thinking… (${evt.metrics?.model || 'LLM'})`;
case 'tool': return evt.message.startsWith('✓') ? 'Tool result received' : `Tool call: ${evt.message.replace(/^▶ Tool: /, '').split(' | ')[0]}`;
case 'result': return 'Completed';
case 'log': return evt.message;
default: return evt.type;
}
};
// ─── Detail drawer for a single event ─────────────────────────────────
const EventDetail: React.FC<{ event: AgentEvent }> = ({ event }) => (
<VStack align="stretch" spacing={4}>
<HStack>
<Badge colorScheme="cyan">{event.type.toUpperCase()}</Badge>
<Badge variant="outline">{event.agent}</Badge>
<Text fontSize="xs" color="whiteAlpha.400">{event.timestamp}</Text>
</HStack>
{event.metrics?.model && event.metrics.model !== 'unknown' && (
<Box>
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" mb={1}>Model</Text>
<Code colorScheme="blue" fontSize="sm">{event.metrics.model}</Code>
</Box>
)}
{event.metrics && (event.metrics.tokens_in != null || event.metrics.latency_ms != null) && (
<Box>
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" mb={1}>Metrics</Text>
<HStack spacing={4} fontSize="sm">
{event.metrics.tokens_in != null && (
<Text>Tokens: <Code>{event.metrics.tokens_in}</Code> in / <Code>{event.metrics.tokens_out}</Code> out</Text>
)}
{event.metrics.latency_ms != null && event.metrics.latency_ms > 0 && (
<Text>Latency: <Code>{event.metrics.latency_ms}ms</Code></Text>
)}
</HStack>
</Box>
)}
<Box>
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" mb={1}>
{event.type === 'thought' ? 'Request / Prompt' : event.type === 'result' ? 'Response' : 'Message'}
</Text>
<Box bg="blackAlpha.500" p={3} borderRadius="md" border="1px solid" borderColor="whiteAlpha.100" maxH="300px" overflowY="auto">
<Text fontSize="xs" fontFamily="mono" whiteSpace="pre-wrap" wordBreak="break-word" color="whiteAlpha.900">
{event.message}
</Text>
</Box>
</Box>
{event.node_id && (
<Box>
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" mb={1}>Node ID</Text>
<Code fontSize="xs">{event.node_id}</Code>
</Box>
)}
</VStack>
);
// ─── Detail drawer showing all events for a given graph node ──────────
const NodeEventsDetail: React.FC<{ nodeId: string; events: AgentEvent[] }> = ({ nodeId, events }) => {
const nodeEvents = useMemo(
() => events.filter((e) => e.node_id === nodeId),
[events, nodeId],
);
if (nodeEvents.length === 0) {
return <Text color="whiteAlpha.500" fontSize="sm">No events recorded for this node yet.</Text>;
}
return (
<VStack align="stretch" spacing={4}>
{nodeEvents.map((evt) => (
<Box key={evt.id} bg="whiteAlpha.50" p={3} borderRadius="md" border="1px solid" borderColor="whiteAlpha.100">
<EventDetail event={evt} />
</Box>
))}
</VStack>
);
};
export const Dashboard: React.FC = () => {
const [activeRunId, setActiveRunId] = useState<string | null>(null);
const [isTriggering, setIsTriggering] = useState(false);
const [portfolioId, setPortfolioId] = useState<string>("main_portfolio");
const { events, status, clearEvents } = useAgentStream(activeRunId);
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedNode, setSelectedNode] = useState<any>(null);
// What's shown in the drawer: either a single event or all events for a node
const [drawerMode, setDrawerMode] = useState<'event' | 'node'>('event');
const [selectedEvent, setSelectedEvent] = useState<AgentEvent | null>(null);
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
// Auto-scroll the terminal to the bottom as new events arrive
const terminalEndRef = useRef<HTMLDivElement>(null);
@ -77,6 +168,27 @@ export const Dashboard: React.FC = () => {
}
};
/** Open the drawer for a single event (terminal click). */
const openEventDetail = useCallback((evt: AgentEvent) => {
setDrawerMode('event');
setSelectedEvent(evt);
setSelectedNodeId(null);
onOpen();
}, [onOpen]);
/** Open the drawer showing all events for a graph node (node click). */
const openNodeDetail = useCallback((nodeId: string) => {
setDrawerMode('node');
setSelectedNodeId(nodeId);
setSelectedEvent(null);
onOpen();
}, [onOpen]);
// Derive a readable drawer title
const drawerTitle = drawerMode === 'event'
? `Event: ${selectedEvent?.agent ?? ''}${selectedEvent?.type ?? ''}`
: `Node: ${selectedNodeId ?? ''}`;
return (
<Flex h="100vh" bg="slate.950" color="white" overflow="hidden">
{/* Sidebar */}
@ -96,7 +208,7 @@ export const Dashboard: React.FC = () => {
<Flex flex="1" overflow="hidden">
{/* Left Side: Graph Area */}
<Box flex="1" position="relative" borderRight="1px solid" borderColor="whiteAlpha.100">
<AgentGraph events={events} />
<AgentGraph events={events} onNodeClick={openNodeDetail} />
{/* Floating Control Panel */}
<HStack position="absolute" top={4} left={4} bg="blackAlpha.800" p={2} borderRadius="lg" backdropFilter="blur(10px)" border="1px solid" borderColor="whiteAlpha.200" spacing={3}>
@ -131,23 +243,29 @@ export const Dashboard: React.FC = () => {
'&::-webkit-scrollbar-thumb': { background: 'whiteAlpha.300' }
}}>
{events.map((evt) => (
<Box key={evt.id} mb={3} fontSize="xs" fontFamily="mono">
<Flex gap={2} align="flex-start">
<Text color="whiteAlpha.400" minW="60px" flexShrink={0}>[{evt.timestamp}]</Text>
<Box
key={evt.id}
mb={2}
fontSize="xs"
fontFamily="mono"
px={2}
py={1}
borderRadius="md"
cursor="pointer"
_hover={{ bg: 'whiteAlpha.100' }}
onClick={() => openEventDetail(evt)}
transition="background 0.15s"
>
<Flex gap={2} align="center">
<Text color="whiteAlpha.400" minW="52px" flexShrink={0}>[{evt.timestamp}]</Text>
<Text flexShrink={0}>{eventLabel(evt.type)}</Text>
<Text color={eventColor(evt.type)} fontWeight="bold" flexShrink={0}>
{evt.agent}
</Text>
<ChevronRight size={12} style={{ marginTop: 2, flexShrink: 0 }} />
<Text color="whiteAlpha.800" wordBreak="break-word">{evt.message}</Text>
<ChevronRight size={10} style={{ flexShrink: 0, opacity: 0.4 }} />
<Text color="whiteAlpha.700" isTruncated>{eventSummary(evt)}</Text>
<Eye size={12} style={{ flexShrink: 0, opacity: 0.3, marginLeft: 'auto' }} />
</Flex>
{evt.metrics && (evt.metrics.tokens_in != null || evt.metrics.latency_ms != null) && (
<HStack spacing={4} mt={1} ml="70px" color="whiteAlpha.400" fontSize="10px">
{evt.metrics.tokens_in != null && <Text>tokens: {evt.metrics.tokens_in}/{evt.metrics.tokens_out}</Text>}
{evt.metrics.latency_ms != null && evt.metrics.latency_ms > 0 && <Text>time: {evt.metrics.latency_ms}ms</Text>}
{evt.metrics.model && evt.metrics.model !== 'unknown' && <Text>model: {evt.metrics.model}</Text>}
</HStack>
)}
</Box>
))}
{events.length === 0 && (
@ -162,16 +280,21 @@ export const Dashboard: React.FC = () => {
</Flex>
</Flex>
{/* Node Inspector Drawer */}
{/* Unified Inspector Drawer (single event or all node events) */}
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="md">
<DrawerOverlay backdropFilter="blur(4px)" />
<DrawerContent bg="slate.900" color="white" borderLeft="1px solid" borderColor="whiteAlpha.200">
<DrawerCloseButton />
<DrawerHeader borderBottomWidth="1px" borderColor="whiteAlpha.100">
Node Inspector: {selectedNode?.agent}
{drawerTitle}
</DrawerHeader>
<DrawerBody>
{/* Inspector content would go here */}
<Text>Detailed metrics and raw JSON responses for the selected node.</Text>
<DrawerBody py={4}>
{drawerMode === 'event' && selectedEvent && (
<EventDetail event={selectedEvent} />
)}
{drawerMode === 'node' && selectedNodeId && (
<NodeEventsDetail nodeId={selectedNodeId} events={events} />
)}
</DrawerBody>
</DrawerContent>
</Drawer>

View File

@ -44,6 +44,8 @@ const AgentNode = ({ data }: NodeProps) => {
borderRadius="lg"
minW="180px"
boxShadow="0 0 15px rgba(0,0,0,0.5)"
cursor="pointer"
_hover={{ borderColor: 'cyan.300', boxShadow: '0 0 20px rgba(79,209,197,0.3)' }}
>
<Handle type="target" position={Position.Top} />
@ -51,6 +53,9 @@ const AgentNode = ({ data }: NodeProps) => {
<Flex align="center" gap={2}>
<Icon as={getIcon(data.agent)} color={getStatusColor(data.status)} boxSize={4} />
<Text fontSize="sm" fontWeight="bold" color="white">{data.agent}</Text>
{data.status === 'completed' && (
<Badge colorScheme="green" fontSize="2xs" ml="auto">Done</Badge>
)}
</Flex>
<Box height="1px" bg="whiteAlpha.200" width="100%" />
@ -60,7 +65,7 @@ const AgentNode = ({ data }: NodeProps) => {
<Icon as={Clock} boxSize={3} color="whiteAlpha.500" />
<Text fontSize="2xs" color="whiteAlpha.600">{data.metrics?.latency_ms || 0}ms</Text>
</Flex>
{data.metrics?.model && (
{data.metrics?.model && data.metrics.model !== 'unknown' && (
<Badge variant="outline" fontSize="2xs" colorScheme="blue">{data.metrics.model}</Badge>
)}
</Flex>
@ -95,9 +100,10 @@ const nodeTypes = {
interface AgentGraphProps {
events: AgentEvent[];
onNodeClick?: (nodeId: string) => void;
}
export const AgentGraph: React.FC<AgentGraphProps> = ({ events }) => {
export const AgentGraph: React.FC<AgentGraphProps> = ({ events, onNodeClick }) => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Track which node_ids we have already added so we never duplicate
@ -193,6 +199,10 @@ export const AgentGraph: React.FC<AgentGraphProps> = ({ events }) => {
}
}, [events.length, setNodes, setEdges]);
const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
onNodeClick?.(node.id);
}, [onNodeClick]);
return (
<Box height="100%" width="100%" bg="slate.950">
<ReactFlow
@ -200,6 +210,7 @@ export const AgentGraph: React.FC<AgentGraphProps> = ({ events }) => {
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={handleNodeClick}
nodeTypes={nodeTypes}
fitView
>

View File

@ -10,7 +10,7 @@
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]