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:
parent
06e913f1ba
commit
cf2df83c38
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue