45752b9a29
New Agents tab in the sidebar (Ops group) for viewing, editing, and
creating AI agent configurations:
Database (migration 026):
- ai_agents table: editable configs for each LLM agent (model, prompts,
temperature, tokens, retries). source='system' for built-in,
source='user' for custom. Seeds 3 system agents (Document Extractor,
Event Classifier, Thesis Rewriter) using WHERE NOT EXISTS to never
overwrite user edits across reinstalls.
- agent_performance_log table: per-invocation metrics (duration,
confidence, retries, tokens, errors) linked to agent config.
API endpoints:
- GET/POST /api/agents — list and create agents
- GET/PUT/DELETE /api/agents/{id} — view, edit, delete (system agents
can be edited but not deleted)
- GET /api/agents/{id}/performance — aggregated metrics (success rate,
avg/p95 latency, confidence, token usage)
- GET /api/agents/{id}/performance/history — hourly time series
Frontend:
- AgentsPage with sidebar list + detail panel
- Agent detail: config display, system prompt viewer, performance
dashboard with metrics cards and time-series chart
- Edit form: all config fields editable including system prompt,
model, temperature, tokens, retries
- Create form: new user-defined agents with auto-slug generation
- System agents show blue badge, user agents show green badge
118 lines
4.4 KiB
TypeScript
118 lines
4.4 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { Link, useRouterState } from '@tanstack/react-router';
|
|
import {
|
|
Home,
|
|
Building2,
|
|
FileText,
|
|
TrendingUp,
|
|
Lightbulb,
|
|
ShoppingCart,
|
|
Wallet,
|
|
ShieldCheck,
|
|
Activity,
|
|
Download,
|
|
Cpu,
|
|
Radar,
|
|
Terminal,
|
|
LayoutDashboard,
|
|
List,
|
|
Globe,
|
|
BarChart3,
|
|
Bot,
|
|
} from 'lucide-react';
|
|
|
|
interface NavItem {
|
|
to: string;
|
|
label: string;
|
|
icon: ReactNode;
|
|
group?: string;
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ to: '/', label: 'Home', icon: <Home size={18} /> },
|
|
{ to: '/companies', label: 'Companies', icon: <Building2 size={18} />, group: 'Data' },
|
|
{ to: '/watchlists', label: 'Watchlists', icon: <List size={18} />, group: 'Data' },
|
|
{ to: '/documents', label: 'Documents', icon: <FileText size={18} />, group: 'Data' },
|
|
{ to: '/trends', label: 'Trends', icon: <TrendingUp size={18} />, group: 'Intelligence' },
|
|
{ to: '/recommendations', label: 'Recommendations', icon: <Lightbulb size={18} />, group: 'Intelligence' },
|
|
{ to: '/macro/events', label: 'Global Events', icon: <Globe size={18} />, group: 'Intelligence' },
|
|
{ to: '/orders', label: 'Orders', icon: <ShoppingCart size={18} />, group: 'Trading' },
|
|
{ to: '/positions', label: 'Positions', icon: <Wallet size={18} />, group: 'Trading' },
|
|
{ to: '/trading', label: 'Trading Controls', icon: <ShieldCheck size={18} />, group: 'Trading' },
|
|
{ to: '/trading/engine', label: 'Trading Engine', icon: <BarChart3 size={18} />, group: 'Trading' },
|
|
{ to: '/ops/pipeline', label: 'Pipeline', icon: <Activity size={18} />, group: 'Ops' },
|
|
{ to: '/ops/ingestion', label: 'Ingestion', icon: <Download size={18} />, group: 'Ops' },
|
|
{ to: '/ops/model', label: 'Model Perf', icon: <Cpu size={18} />, group: 'Ops' },
|
|
{ to: '/agents', label: 'Agents', icon: <Bot size={18} />, group: 'Ops' },
|
|
{ to: '/ops/coverage', label: 'Coverage', icon: <Radar size={18} />, group: 'Ops' },
|
|
{ to: '/analytics/query', label: 'SQL Explorer', icon: <Terminal size={18} />, group: 'Analytics' },
|
|
{ to: '/analytics/dashboards', label: 'Dashboards', icon: <LayoutDashboard size={18} />, group: 'Analytics' },
|
|
];
|
|
|
|
export function AppLayout({ children }: { children: ReactNode }) {
|
|
const routerState = useRouterState();
|
|
const currentPath = routerState.location.pathname;
|
|
|
|
let lastGroup: string | undefined;
|
|
|
|
return (
|
|
<div className="flex h-screen overflow-hidden">
|
|
{/* Sidebar */}
|
|
<nav
|
|
className="w-56 shrink-0 bg-surface-900 border-r border-surface-700 flex flex-col"
|
|
aria-label="Main navigation"
|
|
>
|
|
<div className="px-4 py-4 border-b border-surface-700">
|
|
<span className="text-lg font-bold tracking-tight text-brand-400">
|
|
Stonks Oracle
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto py-2 px-2 space-y-0.5">
|
|
{navItems.map((item) => {
|
|
const showGroup = item.group && item.group !== lastGroup;
|
|
lastGroup = item.group;
|
|
// Check if another nav item is a more specific match (child route)
|
|
const hasMoreSpecificMatch = navItems.some(
|
|
(other) => other.to !== item.to && other.to.startsWith(item.to + '/') && currentPath.startsWith(other.to),
|
|
);
|
|
const active =
|
|
item.to === '/'
|
|
? currentPath === '/'
|
|
: hasMoreSpecificMatch
|
|
? currentPath === item.to
|
|
: currentPath === item.to || currentPath.startsWith(item.to + '/');
|
|
|
|
return (
|
|
<div key={item.to}>
|
|
{showGroup && (
|
|
<div className="px-2 pt-4 pb-1 text-[11px] font-semibold uppercase tracking-wider text-gray-500">
|
|
{item.group}
|
|
</div>
|
|
)}
|
|
<Link
|
|
to={item.to}
|
|
className={`flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-sm transition-colors ${
|
|
active
|
|
? 'bg-brand-600/20 text-brand-300'
|
|
: 'text-gray-400 hover:bg-surface-800 hover:text-gray-200'
|
|
}`}
|
|
aria-current={active ? 'page' : undefined}
|
|
>
|
|
{item.icon}
|
|
{item.label}
|
|
</Link>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Main content */}
|
|
<main className="flex-1 overflow-y-auto bg-surface-950 p-6">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|