import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { Box, Flex, VStack, HStack, Text, IconButton, Button, Input, useDisclosure, Drawer, DrawerOverlay, DrawerContent, DrawerHeader, DrawerBody, DrawerCloseButton, Divider, Tag, Code, Badge, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Tabs, TabList, TabPanels, Tab, TabPanel, Tooltip, Collapse, useToast, } from '@chakra-ui/react'; import { LayoutDashboard, Wallet, Settings, Terminal as TerminalIcon, ChevronRight, Eye, Search, BarChart3, Bot, ChevronDown, ChevronUp } from 'lucide-react'; import { MetricHeader } from './components/MetricHeader'; import { AgentGraph } from './components/AgentGraph'; import { PortfolioViewer } from './components/PortfolioViewer'; import { useAgentStream, AgentEvent } from './hooks/useAgentStream'; import axios from 'axios'; const API_BASE = 'http://127.0.0.1:8088/api'; // ─── Run type definitions with required parameters ──────────────────── type RunType = 'scan' | 'pipeline' | 'portfolio' | 'auto'; interface RunParams { date: string; ticker: string; portfolio_id: string; } const RUN_TYPE_LABELS: Record = { scan: 'Scan', pipeline: 'Pipeline', portfolio: 'Portfolio', auto: 'Auto', }; /** Which params each run type needs. */ const REQUIRED_PARAMS: Record = { scan: ['date'], pipeline: ['ticker', 'date'], portfolio: ['date', 'portfolio_id'], auto: ['date', 'ticker'], }; /** Return the colour token for a given event type. */ const eventColor = (type: AgentEvent['type']): string => { switch (type) { case 'tool': return 'purple.400'; case 'tool_result': return 'purple.300'; case 'result': return 'green.400'; case 'log': return 'yellow.300'; default: return 'cyan.400'; } }; /** Return a short label badge for the event type. */ const eventLabel = (type: AgentEvent['type']): string => { switch (type) { case 'thought': return '💭'; case 'tool': return '🔧'; case 'tool_result': return '✅🔧'; case 'result': return '✅'; case 'log': return 'ℹ️'; default: return '●'; } }; /** 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 'tool_result': return `Tool done: ${evt.message.replace(/^✓ Tool result: /, '').split(' | ')[0]}`; case 'result': return 'Completed'; case 'log': return evt.message; default: return evt.type; } }; // ─── Full Event Detail Modal ───────────────────────────────────────── const EventDetailModal: React.FC<{ event: AgentEvent | null; isOpen: boolean; onClose: () => void }> = ({ event, isOpen, onClose }) => { if (!event) return null; return ( {event.type.toUpperCase()} {event.agent} {event.timestamp} {event.prompt && Prompt / Request} {(event.response || (event.type === 'result' && event.message)) && Response} Summary {event.metrics && Metrics} {event.prompt && ( {event.prompt} )} {(event.response || (event.type === 'result' && event.message)) && ( {event.response || event.message} )} {event.message} {event.metrics && ( {event.metrics.model && event.metrics.model !== 'unknown' && ( Model:{event.metrics.model} )} {event.metrics.tokens_in != null && event.metrics.tokens_in > 0 && ( Tokens In:{event.metrics.tokens_in} )} {event.metrics.tokens_out != null && event.metrics.tokens_out > 0 && ( Tokens Out:{event.metrics.tokens_out} )} {event.metrics.latency_ms != null && event.metrics.latency_ms > 0 && ( Latency:{event.metrics.latency_ms}ms )} {event.node_id && ( Node ID:{event.node_id} )} )} ); }; // ─── Detail card for a single event in the drawer ───────────────────── const EventDetail: React.FC<{ event: AgentEvent; onOpenModal?: (evt: AgentEvent) => void }> = ({ event, onOpenModal }) => ( {event.type.toUpperCase()} {event.agent} {event.timestamp} {onOpenModal && ( )} {event.metrics?.model && event.metrics.model !== 'unknown' && ( Model {event.metrics.model} )} {event.metrics && (event.metrics.tokens_in != null || event.metrics.latency_ms != null) && ( Metrics {event.metrics.tokens_in != null && event.metrics.tokens_in > 0 && ( Tokens: {event.metrics.tokens_in} in / {event.metrics.tokens_out} out )} {event.metrics.latency_ms != null && event.metrics.latency_ms > 0 && ( Latency: {event.metrics.latency_ms}ms )} )} {/* Show prompt if available */} {event.prompt && ( Request / Prompt {event.prompt.length > 1000 ? event.prompt.substring(0, 1000) + '…' : event.prompt} )} {/* Show response if available (result events) */} {event.response && ( Response {event.response.length > 1000 ? event.response.substring(0, 1000) + '…' : event.response} )} {/* Fallback: show message if no prompt/response */} {!event.prompt && !event.response && ( Message {event.message} )} {event.node_id && ( Node ID {event.node_id} )} ); // ─── Detail drawer showing all events for a given graph node ────────── const NodeEventsDetail: React.FC<{ nodeId: string; events: AgentEvent[]; onOpenModal: (evt: AgentEvent) => void }> = ({ nodeId, events, onOpenModal }) => { const nodeEvents = useMemo( () => events.filter((e) => e.node_id === nodeId), [events, nodeId], ); if (nodeEvents.length === 0) { return No events recorded for this node yet.; } return ( {nodeEvents.map((evt) => ( ))} ); }; // ─── Sidebar page type ──────────────────────────────────────────────── type Page = 'dashboard' | 'portfolio'; export const Dashboard: React.FC = () => { const [activePage, setActivePage] = useState('dashboard'); const [activeRunId, setActiveRunId] = useState(null); const [activeRunType, setActiveRunType] = useState(null); const [isTriggering, setIsTriggering] = useState(false); const { events, status, clearEvents } = useAgentStream(activeRunId); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); // Event detail modal state const { isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose } = useDisclosure(); const [modalEvent, setModalEvent] = useState(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(null); const [selectedNodeId, setSelectedNodeId] = useState(null); // Parameter inputs const [showParams, setShowParams] = useState(false); const [params, setParams] = useState({ date: new Date().toISOString().split('T')[0], ticker: 'AAPL', portfolio_id: 'main_portfolio', }); // Auto-scroll the terminal to the bottom as new events arrive const terminalEndRef = useRef(null); useEffect(() => { terminalEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [events.length]); // Clear activeRunType when run completes useEffect(() => { if (status === 'completed' || status === 'error') { setActiveRunType(null); } }, [status]); const isRunning = isTriggering || status === 'streaming' || status === 'connecting'; const startRun = async (type: RunType) => { if (isRunning) return; // Validate required params const required = REQUIRED_PARAMS[type]; const missing = required.filter((k) => !params[k]?.trim()); if (missing.length > 0) { toast({ title: `Missing required fields for ${RUN_TYPE_LABELS[type]}`, description: `Please fill in: ${missing.join(', ')}`, status: 'warning', duration: 3000, isClosable: true, position: 'top', }); setShowParams(true); return; } setIsTriggering(true); setActiveRunType(type); try { clearEvents(); const res = await axios.post(`${API_BASE}/run/${type}`, { portfolio_id: params.portfolio_id, date: params.date, ticker: params.ticker, }); setActiveRunId(res.data.run_id); } catch (err) { console.error("Failed to start run:", err); setActiveRunType(null); } finally { setIsTriggering(false); } }; /** Open the full-screen event detail modal */ const openModal = useCallback((evt: AgentEvent) => { setModalEvent(evt); onModalOpen(); }, [onModalOpen]); /** 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 ( {/* Sidebar */} A } variant="ghost" color={activePage === 'dashboard' ? 'cyan.400' : 'whiteAlpha.600'} bg={activePage === 'dashboard' ? 'whiteAlpha.100' : 'transparent'} _hover={{ bg: "whiteAlpha.100" }} onClick={() => setActivePage('dashboard')} /> } variant="ghost" color={activePage === 'portfolio' ? 'cyan.400' : 'whiteAlpha.600'} bg={activePage === 'portfolio' ? 'whiteAlpha.100' : 'transparent'} _hover={{ bg: "whiteAlpha.100" }} onClick={() => setActivePage('portfolio')} /> } variant="ghost" color="whiteAlpha.600" _hover={{ bg: "whiteAlpha.100" }} /> {/* ─── Portfolio Page ────────────────────────────────────────── */} {activePage === 'portfolio' && ( )} {/* ─── Dashboard Page ────────────────────────────────────────── */} {activePage === 'dashboard' && ( {/* Top Metric Header */} {/* Dashboard Body */} {/* Left Side: Graph Area */} {/* Floating Control Panel */} {/* Run buttons row */} {(['scan', 'pipeline', 'portfolio', 'auto'] as RunType[]).map((type) => { const isThisRunning = isRunning && activeRunType === type; const isOtherRunning = isRunning && activeRunType !== type; const icons: Record = { scan: , pipeline: , portfolio: , auto: , }; const colors: Record = { scan: 'cyan', pipeline: 'blue', portfolio: 'purple', auto: 'green', }; return ( ); })} {status.toUpperCase()} : } size="xs" variant="ghost" color="whiteAlpha.600" onClick={() => setShowParams(!showParams)} /> {/* Collapsible parameter inputs */} Date: setParams((p) => ({ ...p, date: e.target.value }))} /> Ticker: setParams((p) => ({ ...p, ticker: e.target.value.toUpperCase() }))} /> Portfolio: setParams((p) => ({ ...p, portfolio_id: e.target.value }))} /> Required: Scan → date · Pipeline → ticker, date · Portfolio → date, portfolio · Auto → date, ticker {/* Right Side: Live Terminal */} Live Terminal {events.length} events {events.map((evt) => ( openEventDetail(evt)} transition="background 0.15s" > [{evt.timestamp}] {eventLabel(evt.type)} {evt.agent} {eventSummary(evt)} ))} {events.length === 0 && ( Awaiting agent activation... )}
)} {/* Unified Inspector Drawer (single event or all node events) */} {drawerTitle} {drawerMode === 'event' && selectedEvent && ( )} {drawerMode === 'node' && selectedNodeId && ( )} {/* Full event detail modal */} ); };