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", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^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", "autoprefixer": "^10.4.20",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
@ -33,6 +33,6 @@
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.6.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 { import {
Box, Box,
Flex, Flex,
@ -13,10 +13,13 @@ import {
DrawerContent, DrawerContent,
DrawerHeader, DrawerHeader,
DrawerBody, DrawerBody,
DrawerCloseButton,
Divider, Divider,
Tag, Tag,
Code,
Badge,
} from '@chakra-ui/react'; } 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 { MetricHeader } from './components/MetricHeader';
import { AgentGraph } from './components/AgentGraph'; import { AgentGraph } from './components/AgentGraph';
import { useAgentStream, AgentEvent } from './hooks/useAgentStream'; 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 = () => { export const Dashboard: React.FC = () => {
const [activeRunId, setActiveRunId] = useState<string | null>(null); const [activeRunId, setActiveRunId] = useState<string | null>(null);
const [isTriggering, setIsTriggering] = useState(false); const [isTriggering, setIsTriggering] = useState(false);
const [portfolioId, setPortfolioId] = useState<string>("main_portfolio"); const [portfolioId, setPortfolioId] = useState<string>("main_portfolio");
const { events, status, clearEvents } = useAgentStream(activeRunId); const { events, status, clearEvents } = useAgentStream(activeRunId);
const { isOpen, onOpen, onClose } = useDisclosure(); 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 // Auto-scroll the terminal to the bottom as new events arrive
const terminalEndRef = useRef<HTMLDivElement>(null); 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 ( return (
<Flex h="100vh" bg="slate.950" color="white" overflow="hidden"> <Flex h="100vh" bg="slate.950" color="white" overflow="hidden">
{/* Sidebar */} {/* Sidebar */}
@ -96,7 +208,7 @@ export const Dashboard: React.FC = () => {
<Flex flex="1" overflow="hidden"> <Flex flex="1" overflow="hidden">
{/* Left Side: Graph Area */} {/* Left Side: Graph Area */}
<Box flex="1" position="relative" borderRight="1px solid" borderColor="whiteAlpha.100"> <Box flex="1" position="relative" borderRight="1px solid" borderColor="whiteAlpha.100">
<AgentGraph events={events} /> <AgentGraph events={events} onNodeClick={openNodeDetail} />
{/* Floating Control Panel */} {/* 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}> <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' } '&::-webkit-scrollbar-thumb': { background: 'whiteAlpha.300' }
}}> }}>
{events.map((evt) => ( {events.map((evt) => (
<Box key={evt.id} mb={3} fontSize="xs" fontFamily="mono"> <Box
<Flex gap={2} align="flex-start"> key={evt.id}
<Text color="whiteAlpha.400" minW="60px" flexShrink={0}>[{evt.timestamp}]</Text> 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 flexShrink={0}>{eventLabel(evt.type)}</Text>
<Text color={eventColor(evt.type)} fontWeight="bold" flexShrink={0}> <Text color={eventColor(evt.type)} fontWeight="bold" flexShrink={0}>
{evt.agent} {evt.agent}
</Text> </Text>
<ChevronRight size={12} style={{ marginTop: 2, flexShrink: 0 }} /> <ChevronRight size={10} style={{ flexShrink: 0, opacity: 0.4 }} />
<Text color="whiteAlpha.800" wordBreak="break-word">{evt.message}</Text> <Text color="whiteAlpha.700" isTruncated>{eventSummary(evt)}</Text>
<Eye size={12} style={{ flexShrink: 0, opacity: 0.3, marginLeft: 'auto' }} />
</Flex> </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> </Box>
))} ))}
{events.length === 0 && ( {events.length === 0 && (
@ -162,16 +280,21 @@ export const Dashboard: React.FC = () => {
</Flex> </Flex>
</Flex> </Flex>
{/* Node Inspector Drawer */} {/* Unified Inspector Drawer (single event or all node events) */}
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="md"> <Drawer isOpen={isOpen} placement="right" onClose={onClose} size="md">
<DrawerOverlay backdropFilter="blur(4px)" /> <DrawerOverlay backdropFilter="blur(4px)" />
<DrawerContent bg="slate.900" color="white" borderLeft="1px solid" borderColor="whiteAlpha.200"> <DrawerContent bg="slate.900" color="white" borderLeft="1px solid" borderColor="whiteAlpha.200">
<DrawerCloseButton />
<DrawerHeader borderBottomWidth="1px" borderColor="whiteAlpha.100"> <DrawerHeader borderBottomWidth="1px" borderColor="whiteAlpha.100">
Node Inspector: {selectedNode?.agent} {drawerTitle}
</DrawerHeader> </DrawerHeader>
<DrawerBody> <DrawerBody py={4}>
{/* Inspector content would go here */} {drawerMode === 'event' && selectedEvent && (
<Text>Detailed metrics and raw JSON responses for the selected node.</Text> <EventDetail event={selectedEvent} />
)}
{drawerMode === 'node' && selectedNodeId && (
<NodeEventsDetail nodeId={selectedNodeId} events={events} />
)}
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

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

View File

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

View File

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