diff --git a/brain-ui/src/App.tsx b/brain-ui/src/App.tsx index 0aa2387..4f88eb6 100644 --- a/brain-ui/src/App.tsx +++ b/brain-ui/src/App.tsx @@ -45,7 +45,15 @@ const NAV_ITEMS: NavItem[] = [ function AppInner() { const { addToast } = useToast() - const [activeView, setActiveView] = useState('workflows') + // Detect URL path for direct routing (/ui/docs β†’ docs view) + const initialView = (): ActiveView => { + const path = window.location.pathname + if (path.includes('/docs')) return 'docs' + if (path.includes('/cosmos')) return 'cosmos' + if (path.includes('/workspace')) return 'workspace' + return 'workflows' + } + const [activeView, setActiveView] = useState(initialView) const [pendingGate, setPendingGate] = useState(null) const [gateDrawer, setGateDrawer] = useState<{ open: boolean; workflowId: string | null; stepId: string | null }>({ open: false, @@ -55,6 +63,14 @@ function AppInner() { const [logsProject, setLogsProject] = useState(null) const [paletteOpen, setPaletteOpen] = useState(false) + // Sync URL with active view + const handleViewChange = (view: ActiveView) => { + setActiveView(view) + const base = import.meta.env.BASE_URL || '/ui/' + const slug = view === 'workflows' ? '' : view + window.history.replaceState(null, '', `${base}${slug}`) + } + const { workflows, wsStatus } = useWorkflows() useWebSocket(addToast) const storeWorkflows = useBrainStore((s) => s.workflows) @@ -132,7 +148,7 @@ function AppInner() {
)}
diff --git a/brain-ui/src/components/AgentDashboard.tsx b/brain-ui/src/components/AgentDashboard.tsx new file mode 100644 index 0000000..7e76a4b --- /dev/null +++ b/brain-ui/src/components/AgentDashboard.tsx @@ -0,0 +1,240 @@ +import { useState, useEffect } from 'react' + +const API_BASE = import.meta.env.VITE_BRAIN_API ?? '' + +interface Agent { + id: string + label: string + tier: string + export: boolean + status: string + triggers: string[] + scope: string + description: string +} + +const TIER_COLORS: Record = { + free: { emoji: '🟒', color: '#4ade80', bg: 'rgba(34,197,94,0.1)' }, + featured: { emoji: 'πŸ”΅', color: '#60a5fa', bg: 'rgba(59,130,246,0.1)' }, + pro: { emoji: '🟠', color: '#fbbf24', bg: 'rgba(245,158,11,0.1)' }, + full: { emoji: '🟣', color: '#c084fc', bg: 'rgba(168,85,247,0.1)' }, + owner: { emoji: '🟣', color: '#c084fc', bg: 'rgba(168,85,247,0.1)' }, +} + +// Groupes mΓ©tier pour organiser les agents +const AGENT_GROUPS: Record = { + 'code': { + label: 'Code & Qualite', + agents: ['code-review', 'security', 'testing', 'refacto', 'optimizer-backend', 'optimizer-db', 'optimizer-frontend', 'frontend-stack'], + }, + 'infra': { + label: 'Infra & Deploy', + agents: ['vps', 'ci-cd', 'monitoring', 'pm2', 'mail', 'migration'], + }, + 'brain': { + label: 'Brain & Systeme', + agents: ['scribe', 'todo-scribe', 'metabolism-scribe', 'wiki-scribe', 'coach', 'coach-boot', 'coach-scribe', 'capital-scribe', 'toolkit-scribe', 'helloWorld', 'session-orchestrator', 'secrets-guardian', 'brain-guardian', 'key-guardian', 'pre-flight', 'feature-gate', 'brain-hypervisor', 'kernel-orchestrator'], + }, + 'explore': { + label: 'Exploration', + agents: ['debug', 'brainstorm', 'mentor', 'orchestrator', 'interprete', 'aside', 'recruiter', 'agent-review', 'time-anchor', 'pattern-scribe'], + }, +} + +function getAgentGroup(agentId: string): string { + for (const [group, data] of Object.entries(AGENT_GROUPS)) { + if (data.agents.includes(agentId)) return group + } + return 'other' +} + +export function AgentCatalog() { + const [agents, setAgents] = useState([]) + const [error, setError] = useState(null) + const [filter, setFilter] = useState('all') + + useEffect(() => { + fetch(`${API_BASE}/agents`) + .then(r => r.json()) + .then(data => setAgents(Array.isArray(data) ? data : [])) + .catch(e => setError(e.message)) + }, []) + + if (error) return
Erreur: {error}
+ if (!agents.length) return
Chargement...
+ + // Grouper les agents + const grouped: Record = {} + for (const agent of agents) { + const group = getAgentGroup(agent.id) + if (!grouped[group]) grouped[group] = [] + grouped[group].push(agent) + } + + // Stats + const tierCounts = agents.reduce>((acc, a) => { + acc[a.tier] = (acc[a.tier] || 0) + 1 + return acc + }, {}) + + const filteredAgents = filter === 'all' ? agents : agents.filter(a => { + if (filter === 'code' || filter === 'infra' || filter === 'brain' || filter === 'explore') { + return getAgentGroup(a.id) === filter + } + return a.tier === filter + }) + + const filteredGrouped: Record = {} + for (const agent of filteredAgents) { + const group = getAgentGroup(agent.id) + if (!filteredGrouped[group]) filteredGrouped[group] = [] + filteredGrouped[group].push(agent) + } + + return ( +
+

Catalogue des agents

+

+ {agents.length} agents disponibles β€” donnees live depuis brain-engine +

+ + {/* Stats bar */} +
+ setFilter('all')} + /> + {Object.entries(TIER_COLORS).filter(([t]) => tierCounts[t]).map(([tier, colors]) => ( + setFilter(filter === tier ? 'all' : tier)} + /> + ))} +
+ + {/* Group filters */} +
+ {Object.entries(AGENT_GROUPS).map(([key, data]) => ( + + ))} +
+ + {/* Agent cards by group */} + {Object.entries(AGENT_GROUPS) + .filter(([key]) => filteredGrouped[key]?.length) + .map(([key, data]) => ( +
+

{data.label}

+
+ {filteredGrouped[key].map(agent => ( + + ))} +
+
+ )) + } + + {/* Uncategorized */} + {filteredGrouped['other']?.length > 0 && ( +
+

Autres

+
+ {filteredGrouped['other'].map(agent => ( + + ))} +
+
+ )} +
+ ) +} + +function AgentCard({ agent }: { agent: Agent }) { + const tier = TIER_COLORS[agent.tier] || TIER_COLORS['free'] + return ( +
+
+
+ {agent.id} + + {tier.emoji} {agent.tier} + +
+ {agent.description && ( +

+ {agent.description} +

+ )} +
+ {agent.triggers.length > 0 && ( +
+ {agent.triggers.slice(0, 3).join(', ')} +
+ )} +
+ ) +} + +function StatBadge({ label, count, active, color, onClick }: { + label: string; count: number; active: boolean; color: string; onClick: () => void +}) { + return ( + + ) +} diff --git a/brain-ui/src/components/DocsView.tsx b/brain-ui/src/components/DocsView.tsx index 4f1c60c..5d5b159 100644 --- a/brain-ui/src/components/DocsView.tsx +++ b/brain-ui/src/components/DocsView.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, ReactNode } from 'react' import ReactMarkdown, { Components } from 'react-markdown' import { TierComparatif, TierSingle } from './TierDashboard' +import { AgentCatalog } from './AgentDashboard' interface DocFile { name: string @@ -210,6 +211,9 @@ export default function DocsView() { const tierName = activeDoc.replace('vue-', '') return
} + if (liveMode && activeDoc === 'agents') { + return
+ } // Mode standard β€” markdown return (