feat: implement AgentOS frontend and live backend integration

- scaffold Vite + React + TypeScript frontend with Chakra UI and React Flow
- implement AgentGraph, MetricHeader, and Dashboard components
- connect FastAPI to live LangGraph events via astream_events
- implement real-time event mapping for 'scan' and 'pipeline'
- refactor run storage for shared access between REST and WebSockets
This commit is contained in:
Ahmet Guzererler 2026-03-22 22:12:33 +01:00
parent a26c93463a
commit 078d7e2f2a
16 changed files with 649 additions and 114 deletions

View File

@ -2,14 +2,12 @@ from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException
from typing import Dict, Any, List
import uuid
import time
from agent_os.backend.store import runs
from agent_os.backend.dependencies import get_current_user
from agent_os.backend.services.langgraph_engine import LangGraphEngine
router = APIRouter(prefix="/api/run", tags=["runs"])
# In-memory store for demo (should be replaced by Redis/DB for persistence)
runs: Dict[str, Dict[str, Any]] = {}
engine = LangGraphEngine()
@router.post("/scan")
@ -24,7 +22,8 @@ async def trigger_scan(
"type": "scan",
"status": "queued",
"created_at": time.time(),
"user_id": user["user_id"]
"user_id": user["user_id"],
"params": params or {}
}
background_tasks.add_task(engine.run_scan, run_id, params or {})
return {"run_id": run_id, "status": "queued"}
@ -41,7 +40,8 @@ async def trigger_pipeline(
"type": "pipeline",
"status": "queued",
"created_at": time.time(),
"user_id": user["user_id"]
"user_id": user["user_id"],
"params": params or {}
}
background_tasks.add_task(engine.run_pipeline, run_id, params or {})
return {"run_id": run_id, "status": "queued"}
@ -58,7 +58,8 @@ async def trigger_portfolio(
"type": "portfolio",
"status": "queued",
"created_at": time.time(),
"user_id": user["user_id"]
"user_id": user["user_id"],
"params": params or {}
}
background_tasks.add_task(engine.run_portfolio, run_id, params or {})
return {"run_id": run_id, "status": "queued"}
@ -75,7 +76,8 @@ async def trigger_auto(
"type": "auto",
"status": "queued",
"created_at": time.time(),
"user_id": user["user_id"]
"user_id": user["user_id"],
"params": params or {}
}
background_tasks.add_task(engine.run_auto, run_id, params or {})
return {"run_id": run_id, "status": "queued"}

View File

@ -4,106 +4,53 @@ import time
import uuid
from typing import Dict, Any
from agent_os.backend.dependencies import get_current_user
from agent_os.backend.store import runs
from agent_os.backend.services.langgraph_engine import LangGraphEngine
router = APIRouter(prefix="/ws", tags=["websocket"])
engine = LangGraphEngine()
@router.websocket("/stream/{run_id}")
async def websocket_endpoint(
websocket: WebSocket,
run_id: str,
# user: dict = Depends(get_current_user) # In V2, validate token from query string
):
await websocket.accept()
print(f"WebSocket client connected to run: {run_id}")
if run_id not in runs:
await websocket.send_json({"type": "system", "message": f"Error: Run {run_id} not found."})
await websocket.close()
return
run_info = runs[run_id]
run_type = run_info["type"]
params = run_info.get("params", {})
try:
# For now, we use a mock stream.
# In a real implementation, this would subscribe to an event queue or a database stream
# that's being populated by the BackgroundTask running the LangGraph.
stream_gen = None
if run_type == "scan":
stream_gen = engine.run_scan(run_id, params)
elif run_type == "pipeline":
stream_gen = engine.run_pipeline(run_id, params)
# Add other types as they are implemented in LangGraphEngine
mock_events = [
{
"id": "node_1",
"node_id": "analyst_node",
"parent_node_id": "start",
"type": "thought",
"agent": "ANALYST",
"message": "Evaluating market data...",
"metrics": {
"model": "gpt-4-turbo",
"tokens_in": 120,
"tokens_out": 45,
"latency_ms": 450
}
},
{
"id": "node_2",
"node_id": "tool_node",
"parent_node_id": "analyst_node",
"type": "tool",
"agent": "ANALYST",
"message": "> Tool Call: get_news_sentiment",
"metrics": {
"latency_ms": 800
}
},
{
"id": "node_3",
"node_id": "research_node",
"parent_node_id": "analyst_node",
"type": "thought",
"agent": "RESEARCHER",
"message": "Synthesizing industry trends...",
"metrics": {
"model": "claude-3-opus",
"tokens_in": 800,
"tokens_out": 300,
"latency_ms": 2200
}
},
{
"id": "node_4",
"node_id": "trader_node",
"parent_node_id": "research_node",
"type": "result",
"agent": "TRADER",
"message": "Action determined: BUY VLO",
"details": {
"model_used": "gpt-4-turbo",
"latency_ms": 1200,
"input_tokens": 450,
"output_tokens": 120,
"raw_json_response": '{"action": "buy", "ticker": "VLO"}'
},
"metrics": {
"model": "gpt-4-turbo",
"tokens_in": 450,
"tokens_out": 120,
"latency_ms": 1200
}
}
]
for evt in mock_events:
payload = {
"id": evt["id"],
"node_id": evt["node_id"],
"parent_node_id": evt["parent_node_id"],
"timestamp": time.strftime("%H:%M:%S"),
"agent": evt["agent"],
"tier": "mid" if evt["agent"] == "ANALYST" else "deep",
"type": evt["type"],
"message": evt["message"],
"details": evt.get("details"),
"metrics": evt.get("metrics")
}
await websocket.send_json(payload)
await asyncio.sleep(2) # Simulating execution delay
if stream_gen:
async for payload in stream_gen:
# Add timestamp if not present
if "timestamp" not in payload:
payload["timestamp"] = time.strftime("%H:%M:%S")
await websocket.send_json(payload)
else:
await websocket.send_json({"type": "system", "message": f"Error: Run type {run_type} streaming not yet implemented."})
await websocket.send_json({"type": "system", "message": "Run completed."})
except WebSocketDisconnect:
print(f"WebSocket client disconnected from run {run_id}")
except Exception as e:
import traceback
traceback.print_exc()
await websocket.send_json({"type": "system", "message": f"Error: {str(e)}"})
await websocket.close()

View File

@ -1,35 +1,135 @@
import asyncio
import time
from typing import Dict, Any
from typing import Dict, Any, AsyncGenerator
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.graph.scanner_graph import ScannerGraph
from tradingagents.default_config import DEFAULT_CONFIG
class LangGraphEngine:
"""Orchestrates LangGraph pipeline executions for the AgentOS API."""
"""Orchestrates LangGraph pipeline executions and streams events."""
def __init__(self):
# This is where you would import and setup your LangGraph workflows
# e.g., from tradingagents.graph.setup import setup_trading_graph
pass
self.config = DEFAULT_CONFIG.copy()
# In-memory store to keep track of running tasks if needed
self.active_runs = {}
async def run_scan(self, run_id: str, params: Dict[str, Any]):
print(f"Engine: Starting SCAN {run_id} with params {params}")
# Placeholder for actual scanner graph execution
await asyncio.sleep(15)
print(f"Engine: SCAN {run_id} completed")
async def run_scan(self, run_id: str, params: Dict[str, Any]) -> AsyncGenerator[Dict[str, Any], None]:
"""Run the 3-phase macro scanner and stream events."""
date = params.get("date", time.strftime("%Y-%m-%d"))
# Initialize ScannerGraph
# Note: ScannerGraph in TradingAgents seems to take date and config
scanner = ScannerGraph(date=date, config=self.config)
print(f"Engine: Starting SCAN {run_id} for date {date}")
# Initial state for scanner
# Based on tradingagents/graph/scanner_graph.py
initial_state = {
"date": date,
"geopolitical_report": "",
"market_movers_report": "",
"sector_report": "",
"industry_deep_dive_report": "",
"macro_synthesis_report": "",
"top_10_watchlist": []
}
async def run_pipeline(self, run_id: str, params: Dict[str, Any]):
print(f"Engine: Starting PIPELINE {run_id} with params {params}")
# Placeholder for actual analysis pipeline execution
await asyncio.sleep(20)
print(f"Engine: PIPELINE {run_id} completed")
async for event in scanner.graph.astream_events(initial_state, version="v2"):
mapped_event = self._map_langgraph_event(event)
if mapped_event:
yield mapped_event
async def run_portfolio(self, run_id: str, params: Dict[str, Any]):
print(f"Engine: Starting PORTFOLIO rebalance {run_id} with params {params}")
# Placeholder for actual portfolio manager graph execution
await asyncio.sleep(10)
print(f"Engine: PORTFOLIO {run_id} completed")
async def run_pipeline(self, run_id: str, params: Dict[str, Any]) -> AsyncGenerator[Dict[str, Any], None]:
"""Run per-ticker analysis pipeline and stream events."""
ticker = params.get("ticker", "AAPL")
date = params.get("date", time.strftime("%Y-%m-%d"))
analysts = params.get("analysts", ["market", "news", "fundamentals"])
print(f"Engine: Starting PIPELINE {run_id} for {ticker} on {date}")
# Initialize TradingAgentsGraph
graph_wrapper = TradingAgentsGraph(
selected_analysts=analysts,
config=self.config,
debug=True
)
initial_state = graph_wrapper.propagator.create_initial_state(ticker, date)
# We don't use propagator.get_graph_args() here because we want to stream events directly
async for event in graph_wrapper.graph.astream_events(initial_state, version="v2"):
mapped_event = self._map_langgraph_event(event)
if mapped_event:
yield mapped_event
async def run_auto(self, run_id: str, params: Dict[str, Any]):
print(f"Engine: Starting AUTO {run_id} with params {params}")
# Placeholder for full automated trading cycle
await asyncio.sleep(30)
print(f"Engine: AUTO {run_id} completed")
def _map_langgraph_event(self, event: Dict[str, Any]) -> Dict[str, Any] | None:
"""Map LangGraph v2 events to AgentOS frontend contract."""
kind = event["event"]
name = event["name"]
tags = event.get("tags", [])
# Try to extract node name from tags or metadata
node_name = name
for tag in tags:
if tag.startswith("graph:node:"):
node_name = tag.split(":", 2)[-1]
# Filter for relevant events
if kind == "on_chat_model_start":
return {
"id": event["run_id"],
"node_id": node_name,
"parent_node_id": "start", # Simplified for now
"type": "thought",
"agent": node_name.upper(),
"message": f"Thinking...",
"metrics": {
"model": event["data"].get("invocation_params", {}).get("model_name", "unknown"),
}
}
elif kind == "on_tool_start":
return {
"id": event["run_id"],
"node_id": f"tool_{name}",
"parent_node_id": node_name,
"type": "tool",
"agent": node_name.upper(),
"message": f"> Tool Call: {name}",
"metrics": {}
}
elif kind == "on_chat_model_end":
output = event["data"].get("output")
usage = {}
model = "unknown"
if hasattr(output, "usage_metadata") and output.usage_metadata:
usage = output.usage_metadata
if hasattr(output, "response_metadata") and output.response_metadata:
model = output.response_metadata.get("model_name", "unknown")
return {
"id": f"{event['run_id']}_end",
"node_id": node_name,
"type": "result",
"agent": node_name.upper(),
"message": "Action determined.",
"metrics": {
"model": model,
"tokens_in": usage.get("input_tokens", 0),
"tokens_out": usage.get("output_tokens", 0),
# "latency_ms": ... # calculated in frontend or here if we track start
}
}
return None
# Sync versions for BackgroundTasks (if we still want to use them)
async def run_scan_background(self, run_id: str, params: Dict[str, Any]):
async for _ in self.run_scan(run_id, params):
pass
async def run_pipeline_background(self, run_id: str, params: Dict[str, Any]):
async for _ in self.run_pipeline(run_id, params):
pass

View File

@ -0,0 +1,4 @@
from typing import Dict, Any
# In-memory store for demo (should be replaced by Redis/DB for persistence)
runs: Dict[str, Dict[str, Any]] = {}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AgentOS | Observability Command Center</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,38 @@
{
"name": "agent-os-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"axios": "^1.6.2",
"framer-motion": "^10.16.5",
"lucide-react": "^0.294.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"reactflow": "^11.10.1"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,13 @@
import { ChakraProvider } from '@chakra-ui/react';
import theme from './theme';
import { Dashboard } from './Dashboard';
function App() {
return (
<ChakraProvider theme={theme}>
<Dashboard />
</ChakraProvider>
);
}
export default App;

View File

@ -0,0 +1,139 @@
import React, { useState } from 'react';
import {
Box,
Flex,
VStack,
HStack,
Text,
IconButton,
Button,
useDisclosure,
Drawer,
DrawerOverlay,
DrawerContent,
DrawerHeader,
DrawerBody,
Divider,
Tag,
} from '@chakra-ui/react';
import { LayoutDashboard, Wallet, Settings, Play, Terminal as TerminalIcon, ChevronRight } from 'lucide-react';
import { MetricHeader } from './components/MetricHeader';
import { AgentGraph } from './components/AgentGraph';
import { useAgentStream } from './hooks/useAgentStream';
import axios from 'axios';
const API_BASE = 'http://localhost:8000/api';
export const Dashboard: React.FC = () => {
const [activeRunId, setActiveRunId] = useState<string | null>(null);
const { events, status, clearEvents } = useAgentStream(activeRunId);
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedNode, setSelectedNode] = useState<any>(null);
const startRun = async (type: string) => {
try {
clearEvents();
const res = await axios.post(`${API_BASE}/run/${type}`);
setActiveRunId(res.data.run_id);
} catch (err) {
console.error("Failed to start run:", err);
}
};
return (
<Flex h="100vh" bg="slate.950" color="white" overflow="hidden">
{/* Sidebar */}
<VStack w="64px" bg="slate.900" borderRight="1px solid" borderColor="whiteAlpha.100" py={4} spacing={6}>
<Box mb={4}><Text fontWeight="black" color="cyan.400" fontSize="xl">A</Text></Box>
<IconButton aria-label="Dashboard" icon={<LayoutDashboard size={20} />} variant="ghost" color="cyan.400" _hover={{ bg: "whiteAlpha.100" }} />
<IconButton aria-label="Portfolio" icon={<Wallet size={20} />} variant="ghost" color="whiteAlpha.600" _hover={{ bg: "whiteAlpha.100" }} />
<IconButton aria-label="Settings" icon={<Settings size={20} />} variant="ghost" color="whiteAlpha.600" _hover={{ bg: "whiteAlpha.100" }} />
</VStack>
{/* Main Content */}
<Flex flex="1" direction="column">
{/* Top Metric Header */}
<MetricHeader />
{/* Dashboard Body */}
<Flex flex="1" overflow="hidden">
{/* Left Side: Graph Area */}
<Box flex="1" position="relative" borderRight="1px solid" borderColor="whiteAlpha.100">
<AgentGraph events={events} />
{/* 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}>
<Button
size="sm"
leftIcon={<Play size={14} />}
colorScheme="cyan"
variant="solid"
onClick={() => startRun('scan')}
isLoading={status === 'connecting' || status === 'streaming'}
>
Start Market Scan
</Button>
<Divider orientation="vertical" h="20px" />
<Tag size="sm" colorScheme={status === 'streaming' ? 'green' : 'gray'}>
{status.toUpperCase()}
</Tag>
</HStack>
</Box>
{/* Right Side: Live Terminal */}
<VStack w="400px" bg="blackAlpha.400" align="stretch" spacing={0}>
<Flex p={3} bg="whiteAlpha.50" align="center" gap={2} borderBottom="1px solid" borderColor="whiteAlpha.100">
<TerminalIcon size={16} color="#4fd1c5" />
<Text fontSize="xs" fontWeight="bold" textTransform="uppercase" letterSpacing="wider">Live Terminal</Text>
</Flex>
<Box flex="1" overflowY="auto" p={4} sx={{
'&::-webkit-scrollbar': { width: '4px' },
'&::-webkit-scrollbar-track': { background: 'transparent' },
'&::-webkit-scrollbar-thumb': { background: 'whiteAlpha.300' }
}}>
{events.map((evt, i) => (
<Box key={evt.id} mb={3} fontSize="xs" fontFamily="mono">
<Flex gap={2}>
<Text color="whiteAlpha.400" minW="60px">[{evt.timestamp}]</Text>
<Text color={evt.type === 'tool' ? 'purple.400' : evt.type === 'result' ? 'amber.400' : 'cyan.400'} fontWeight="bold">
{evt.agent}
</Text>
<ChevronRight size={12} style={{ marginTop: 2 }} />
<Text color="whiteAlpha.800">{evt.message}</Text>
</Flex>
{evt.metrics && (
<HStack spacing={4} mt={1} ml="70px" color="whiteAlpha.400" fontSize="10px">
<Text>tokens: {evt.metrics.tokens_in}/{evt.metrics.tokens_out}</Text>
<Text>time: {evt.metrics.latency_ms}ms</Text>
</HStack>
)}
</Box>
))}
{events.length === 0 && (
<Flex h="100%" align="center" justify="center" direction="column" gap={4} opacity={0.3}>
<TerminalIcon size={48} />
<Text fontSize="sm">Awaiting agent activation...</Text>
</Flex>
)}
</Box>
</VStack>
</Flex>
</Flex>
{/* Node Inspector Drawer */}
<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">
<DrawerHeader borderBottomWidth="1px" borderColor="whiteAlpha.100">
Node Inspector: {selectedNode?.agent}
</DrawerHeader>
<DrawerBody>
{/* Inspector content would go here */}
<Text>Detailed metrics and raw JSON responses for the selected node.</Text>
</DrawerBody>
</DrawerContent>
</Drawer>
</Flex>
);
};

View File

@ -0,0 +1,158 @@
import React, { useMemo, useEffect } from 'react';
import ReactFlow, {
Background,
Controls,
Node,
Edge,
Handle,
Position,
NodeProps,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { Box, Text, Flex, Icon, Tooltip, Badge } from '@chakra-ui/react';
import { Cpu, Tool, Database, TrendingUp, Clock } from 'lucide-react';
import { AgentEvent } from '../hooks/useAgentStream';
// --- Custom Agent Node Component ---
const AgentNode = ({ data }: NodeProps) => {
const getIcon = (agent: string) => {
switch (agent.toUpperCase()) {
case 'ANALYST': return Cpu;
case 'RESEARCHER': return Database;
case 'TRADER': return TrendingUp;
default: return Tool;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'running': return 'cyan.400';
case 'completed': return 'green.400';
case 'error': return 'red.400';
default: return 'whiteAlpha.500';
}
};
return (
<Box
bg="slate.900"
border="1px solid"
borderColor={getStatusColor(data.status)}
p={3}
borderRadius="lg"
minW="180px"
boxShadow="0 0 15px rgba(0,0,0,0.5)"
>
<Handle type="target" position={Position.Top} />
<Flex direction="column" gap={2}>
<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>
</Flex>
<Box height="1px" bg="whiteAlpha.200" width="100%" />
<Flex justify="space-between" align="center">
<Flex align="center" gap={1}>
<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 && (
<Badge variant="outline" fontSize="2xs" colorScheme="blue">{data.metrics.model}</Badge>
)}
</Flex>
{data.status === 'running' && (
<Box width="100%" height="2px" bg="cyan.400" borderRadius="full" overflow="hidden">
<Box
as="div"
width="40%"
height="100%"
bg="white"
sx={{
animation: "shimmer 2s infinite linear",
"@keyframes shimmer": {
"0%": { transform: "translateX(-100%)" },
"100%": { transform: "translateX(300%)" }
}
}}
/>
</Box>
)}
</Flex>
<Handle type="source" position={Position.Bottom} />
</Box>
);
};
const nodeTypes = {
agentNode: AgentNode,
};
interface AgentGraphProps {
events: AgentEvent[];
}
export const AgentGraph: React.FC<AgentGraphProps> = ({ events }) => {
const { nodes, edges } = useMemo(() => {
const graphNodes: Node[] = [];
const graphEdges: Edge[] = [];
const seenNodes = new Set<string>();
events.forEach((evt) => {
if (!evt.node_id) return;
if (!seenNodes.has(evt.node_id)) {
graphNodes.push({
id: evt.node_id,
type: 'agentNode',
position: { x: 250, y: graphNodes.length * 150 + 50 }, // Simple vertical layout
data: {
agent: evt.agent,
status: evt.type === 'result' ? 'completed' : 'running',
metrics: evt.metrics
},
});
seenNodes.add(evt.node_id);
if (evt.parent_node_id && evt.parent_node_id !== 'start') {
graphEdges.push({
id: `e-${evt.parent_node_id}-${evt.node_id}`,
source: evt.parent_node_id,
target: evt.node_id,
animated: true,
style: { stroke: '#4fd1c5' },
});
}
} else {
// Update existing node status and metrics
const idx = graphNodes.findIndex(n => n.id === evt.node_id);
if (idx !== -1) {
graphNodes[idx].data = {
...graphNodes[idx].data,
status: evt.type === 'result' ? 'completed' : 'running',
metrics: evt.metrics || graphNodes[idx].data.metrics
};
}
}
});
return { nodes: graphNodes, edges: graphEdges };
}, [events]);
return (
<Box height="100%" width="100%" bg="slate.950">
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
>
<Background color="#333" gap={16} />
<Controls />
</ReactFlow>
</Box>
);
};

View File

@ -0,0 +1,45 @@
import React from 'react';
import { Box, Flex, Text, Stat, StatLabel, StatNumber, StatHelpText, StatArrow, Badge, Icon } from '@chakra-ui/react';
import { Activity, ShieldAlert, TrendingUp } from 'lucide-react';
export const MetricHeader: React.FC = () => {
return (
<Flex bg="slate.900" borderBottom="1px solid" borderColor="whiteAlpha.200" p={4} gap={6} align="center" width="100%">
{/* Metric 1: Sharpe Ratio */}
<Box flex="1" bg="whiteAlpha.50" p={3} borderRadius="md" border="1px solid" borderColor="whiteAlpha.100">
<Flex align="center" gap={2} mb={1}>
<Icon as={TrendingUp} color="green.400" boxSize={4} />
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" textTransform="uppercase">Sharpe Ratio (30d)</Text>
</Flex>
<Flex align="baseline" gap={2}>
<Text fontSize="2xl" fontWeight="black" color="white">2.42</Text>
<Badge colorScheme="green" variant="subtle" fontSize="2xs">High Efficiency</Badge>
</Flex>
</Box>
{/* Metric 2: Market Regime */}
<Box flex="1" bg="whiteAlpha.50" p={3} borderRadius="md" border="1px solid" borderColor="whiteAlpha.100">
<Flex align="center" gap={2} mb={1}>
<Icon as={Activity} color="cyan.400" boxSize={4} />
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" textTransform="uppercase">Market Regime</Text>
</Flex>
<Flex align="baseline" gap={2}>
<Text fontSize="2xl" fontWeight="black" color="cyan.400">BULL</Text>
<Text fontSize="xs" color="whiteAlpha.500">Beta: 1.15</Text>
</Flex>
</Box>
{/* Metric 3: Risk / Drawdown */}
<Box flex="1" bg="whiteAlpha.50" p={3} borderRadius="md" border="1px solid" borderColor="whiteAlpha.100">
<Flex align="center" gap={2} mb={1}>
<Icon as={ShieldAlert} color="red.400" boxSize={4} />
<Text fontSize="xs" fontWeight="bold" color="whiteAlpha.600" textTransform="uppercase">Risk / Drawdown</Text>
</Flex>
<Flex align="baseline" gap={2}>
<Text fontSize="2xl" fontWeight="black" color="red.400">-2.4%</Text>
<Text fontSize="xs" color="whiteAlpha.500">VaR (1d): $4.2k</Text>
</Flex>
</Box>
</Flex>
);
};

View File

@ -0,0 +1,14 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: dark;
}
body {
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@ -0,0 +1,35 @@
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
const config: ThemeConfig = {
initialColorMode: 'dark',
useSystemColorMode: false,
};
const theme = extendTheme({
config,
colors: {
slate: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
},
styles: {
global: {
body: {
bg: 'slate.950',
color: 'white',
},
},
},
});
export default theme;

View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}