Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
This commit is contained in:
+159
-199
@@ -1,218 +1,178 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { OpsFloor } from './components/OpsFloor'
|
||||
import { PromptBar } from './components/PromptBar'
|
||||
import type { Agent, AgentAnim, Approval, FeedEntry, StatusData, Zone } from './types'
|
||||
|
||||
const TABS = ['Overview', 'Agents', 'Feed', 'Approvals', 'Audit'] as const
|
||||
type Tab = (typeof TABS)[number]
|
||||
|
||||
const LEVEL_COLOR = { ok: '#22aa44', warn: '#cc7700', down: '#dd3355', unknown: '#8b9cb3' }
|
||||
const LEVEL_BG = { ok: '#e8f8ec', warn: '#fff6e6', down: '#ffeef2', unknown: '#f0f3f8' }
|
||||
|
||||
function wsUrl() {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const host = window.location.host
|
||||
return `${proto}://${host}/api/ws/ops`
|
||||
}
|
||||
import { useRef, useState } from 'react'
|
||||
import { useClock } from './hooks/useClock'
|
||||
import { useCommandCenter } from './hooks/useCommandCenter'
|
||||
import { useLiveMetrics } from './hooks/useLiveMetrics'
|
||||
import { SideNav } from './components/layout/SideNav'
|
||||
import { TopBar } from './components/layout/TopBar'
|
||||
import { AgentFleet } from './components/features/AgentFleet'
|
||||
import { ApprovalInbox } from './components/features/ApprovalInbox'
|
||||
import { ChatDrawer } from './components/features/ChatDrawer'
|
||||
import { GpuMonitor } from './components/features/GpuMonitor'
|
||||
import { InfraQuickAccess } from './components/features/InfraQuickAccess'
|
||||
import { InspectorPanel } from './components/features/InspectorPanel'
|
||||
import { PlatformTopology } from './components/features/PlatformTopology'
|
||||
import { PresentationView } from './components/features/PresentationView'
|
||||
import { DataQualityView } from './components/features/DataQualityView'
|
||||
import { KnowledgeChatView } from './components/features/KnowledgeChatView'
|
||||
import { StorageView } from './components/features/StorageView'
|
||||
import { TerminalDock } from './components/features/TerminalDock'
|
||||
import { resolveInfraNode } from './lib/infraCatalog'
|
||||
import { cn } from './lib/utils'
|
||||
|
||||
export default function App() {
|
||||
const [tab, setTab] = useState<Tab>('Overview')
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
const [zones, setZones] = useState<Zone[]>([])
|
||||
const [status, setStatus] = useState<StatusData | null>(null)
|
||||
const [feed, setFeed] = useState<FeedEntry[]>([])
|
||||
const [approvals, setApprovals] = useState<Approval[]>([])
|
||||
const [chat, setChat] = useState<{ role: 'user' | 'agent'; text: string; agent?: string }[]>([])
|
||||
const [anims, setAnims] = useState<Record<string, AgentAnim>>({})
|
||||
const [busy, setBusy] = useState(false)
|
||||
const clock = useClock()
|
||||
const cc = useCommandCenter()
|
||||
const [gpuChatActive, setGpuChatActive] = useState(false)
|
||||
const gpuBoost = gpuChatActive || cc.mainView === 'knowledge'
|
||||
const { agentLoads, gpuLive } = useLiveMetrics(cc.agents, cc.gpu, cc.anims, gpuBoost)
|
||||
const mainScrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [a, s, f, ap] = await Promise.all([
|
||||
fetch('/api/agents').then((r) => r.json()),
|
||||
fetch('/api/status').then((r) => r.json()),
|
||||
fetch('/api/feed').then((r) => r.json()),
|
||||
fetch('/api/approvals').then((r) => r.json()),
|
||||
])
|
||||
setAgents(a.agents || [])
|
||||
setZones(a.zones || [])
|
||||
setStatus(s)
|
||||
setFeed(f.entries || [])
|
||||
setApprovals(ap.approvals || [])
|
||||
}, [])
|
||||
const isPlatform = cc.mainView === 'platform'
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
const ws = new WebSocket(wsUrl())
|
||||
ws.onmessage = (ev) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (msg.type === 'status') setStatus(msg.data)
|
||||
if (msg.type === 'feed') setFeed((prev) => [msg.entry, ...prev].slice(0, 100))
|
||||
if (msg.type === 'agent_dispatch') {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'walk', zone: msg.zone } }))
|
||||
}
|
||||
if (msg.type === 'agent_fetch') {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'fetch', zone: msg.zone } }))
|
||||
}
|
||||
if (msg.type === 'agent_return') {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'return', zone: msg.zone } }))
|
||||
setTimeout(() => {
|
||||
setAnims((p) => ({ ...p, [msg.agent_id]: { agentId: msg.agent_id, state: 'idle' } }))
|
||||
}, 1200)
|
||||
}
|
||||
if (msg.type === 'prompt_result') {
|
||||
setChat((c) => [...c, { role: 'agent', text: msg.answer, agent: msg.agent_id }])
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
const iv = setInterval(load, 30000)
|
||||
return () => { ws.close(); clearInterval(iv) }
|
||||
}, [load])
|
||||
|
||||
const sendPrompt = async (message: string) => {
|
||||
setBusy(true)
|
||||
setChat((c) => [...c, { role: 'user', text: message }])
|
||||
await fetch('/api/prompt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message }),
|
||||
})
|
||||
const openApprovals = () => {
|
||||
cc.setMainView('approvals')
|
||||
mainScrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const decide = async (id: string, approved: boolean) => {
|
||||
await fetch(`/api/approvals/${id}/decide`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ approved }),
|
||||
})
|
||||
load()
|
||||
}
|
||||
|
||||
const allOk = status && Object.values(status.domains).every((d) => d.level === 'ok')
|
||||
const terminalSubject = cc.terminalSubjectId
|
||||
const terminalLabel = (() => {
|
||||
if (cc.selectedAgent) return cc.selectedAgent.name.split(' ·')[0]
|
||||
const infra = resolveInfraNode(terminalSubject)
|
||||
if (infra) return infra.label
|
||||
if (cc.selectedNode) return cc.selectedNode.label
|
||||
return 'Lab'
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6 max-w-7xl mx-auto font-display flex flex-col gap-4">
|
||||
<header className="glass-strong rounded-2xl px-5 py-4 flex flex-wrap justify-between items-center gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-11 h-11 rounded-xl flex items-center justify-center text-xl font-bold text-white shadow-neon-cyan"
|
||||
style={{ background: 'linear-gradient(135deg, #0099cc, #8844cc)' }}
|
||||
>
|
||||
ATC
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neon-cyan neon-text-cyan tracking-tight">Command Center</h1>
|
||||
<p className="text-xs font-mono text-ink-muted">Agent ops floor · Dell ATC Lab</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{status && (
|
||||
<span className={`text-xs font-mono px-3 py-1.5 rounded-full border ${allOk ? 'bg-green-50 border-neon-green/30 text-neon-green' : 'bg-amber-50 border-neon-amber/30 text-neon-amber'}`}>
|
||||
{allOk ? '● All systems operational' : '● Attention required'}
|
||||
</span>
|
||||
)}
|
||||
{status && (
|
||||
<span className="text-xs font-mono text-ink-faint">
|
||||
Scan {new Date(status.ts).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex h-full flex-col overflow-hidden bg-surface">
|
||||
<TopBar
|
||||
clock={clock}
|
||||
status={cc.status}
|
||||
workload={cc.workload}
|
||||
agents={cc.agents}
|
||||
approvals={cc.approvals}
|
||||
onApprovalsClick={openApprovals}
|
||||
/>
|
||||
|
||||
<OpsFloor agents={agents} zones={zones} animations={anims} />
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<SideNav
|
||||
agents={cc.agents}
|
||||
animations={cc.anims}
|
||||
gpu={cc.gpu}
|
||||
gpuLive={gpuLive}
|
||||
gpuBoost={gpuBoost}
|
||||
selectedAgentId={cc.selectedAgentId}
|
||||
selectedNodeId={cc.selectedNodeId}
|
||||
mainView={cc.mainView}
|
||||
approvalCount={cc.approvals.length}
|
||||
agentsLoading={cc.agentsLoading}
|
||||
onSetMainView={cc.setMainView}
|
||||
onOpenApprovals={openApprovals}
|
||||
onSelectAgent={cc.selectAgent}
|
||||
onSelectZone={cc.selectNode}
|
||||
/>
|
||||
|
||||
<nav className="flex gap-2 flex-wrap">
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-mono border transition-all ${
|
||||
tab === t
|
||||
? 'bg-white border-neon-cyan text-neon-cyan shadow-neon-cyan font-semibold'
|
||||
: 'bg-white/60 border-slate-200 text-ink-muted hover:bg-white hover:border-neon-cyan/40'
|
||||
}`}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 flex-1">
|
||||
<main className="lg:col-span-2 glass-strong rounded-2xl p-5 min-h-[260px]">
|
||||
{tab === 'Overview' && status && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{Object.entries(status.domains).map(([key, d]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="status-card rounded-xl p-4 border"
|
||||
style={{ borderColor: `${LEVEL_COLOR[d.level]}44`, background: LEVEL_BG[d.level] }}
|
||||
>
|
||||
<div className="text-xs font-mono uppercase tracking-wider text-ink-muted">{key}</div>
|
||||
<div className="text-xl font-bold mt-1" style={{ color: LEVEL_COLOR[d.level] }}>{d.label}</div>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<div className="w-2.5 h-2.5 rounded-full animate-pulse" style={{ background: LEVEL_COLOR[d.level], boxShadow: `0 0 8px ${LEVEL_COLOR[d.level]}` }} />
|
||||
<span className="text-xs font-mono uppercase" style={{ color: LEVEL_COLOR[d.level] }}>{d.level}</span>
|
||||
<div ref={mainScrollRef} className="scrollbar-thin flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto bg-surface">
|
||||
<div className={cn('flex min-h-0 flex-1', isPlatform ? '' : 'flex-col')}>
|
||||
<div className={cn('flex min-w-0 flex-1 flex-col gap-2', isPlatform ? 'p-2' : 'min-h-0 p-3')}>
|
||||
{isPlatform && (
|
||||
<>
|
||||
<div className="grid shrink-0 grid-cols-1 gap-2 xl:grid-cols-[1fr_auto]">
|
||||
<AgentFleet
|
||||
agents={cc.agents}
|
||||
animations={cc.anims}
|
||||
selectedId={cc.selectedAgentId}
|
||||
loads={agentLoads}
|
||||
approvalCount={cc.approvals.length}
|
||||
onSelect={cc.selectAgent}
|
||||
onOpenApprovals={openApprovals}
|
||||
/>
|
||||
<GpuMonitor gpu={cc.gpu} live={gpuLive} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Agents' && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{agents.map((a) => (
|
||||
<div key={a.id} className="status-card rounded-xl p-4 border border-slate-200/80">
|
||||
<div className="font-semibold text-lg" style={{ color: a.color }}>{a.name}</div>
|
||||
<div className="text-sm text-ink-muted mt-1">{a.role}</div>
|
||||
<div className="text-xs font-mono text-ink-faint mt-2 px-2 py-1 rounded-md bg-slate-50 inline-block">zone: {a.zone}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Feed' && (
|
||||
<div className="font-mono text-xs space-y-2 max-h-80 overflow-y-auto">
|
||||
{feed.map((e) => (
|
||||
<div key={e.id} className="flex gap-2 py-1.5 border-b border-slate-100 last:border-0">
|
||||
<span className="text-ink-faint shrink-0">{e.ts ? new Date(e.ts).toLocaleTimeString() : ''}</span>
|
||||
<span className="font-semibold shrink-0" style={{ color: agents.find((a) => a.id === e.agent_id)?.color || '#888' }}>{e.agent_id}</span>
|
||||
<span className={e.level === 'warn' ? 'text-neon-amber' : 'text-ink'}>{e.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Approvals' && (
|
||||
<div className="space-y-3">
|
||||
{approvals.length === 0 && <p className="text-ink-muted text-sm">Geen pending approvals.</p>}
|
||||
{approvals.map((a) => (
|
||||
<div key={a.id} className="status-card border border-neon-magenta/25 rounded-xl p-4">
|
||||
<div className="text-sm font-semibold text-neon-magenta">{a.action}</div>
|
||||
<div className="text-xs text-ink-muted mt-1">{a.reason}</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button onClick={() => decide(a.id, true)} className="px-3 py-1.5 rounded-lg bg-green-50 border border-neon-green/40 text-neon-green text-xs font-semibold hover:bg-green-100">Approve</button>
|
||||
<button onClick={() => decide(a.id, false)} className="px-3 py-1.5 rounded-lg bg-red-50 border border-red-300 text-red-600 text-xs font-semibold hover:bg-red-100">Deny</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tab === 'Audit' && (
|
||||
<p className="text-ink-muted text-sm">Audit log — approvals en agent acties (v1 via Feed tab).</p>
|
||||
)}
|
||||
</main>
|
||||
<InfraQuickAccess
|
||||
workload={cc.workload}
|
||||
agents={cc.agents}
|
||||
selectedNodeId={cc.selectedNodeId}
|
||||
busy={cc.nodeBusy}
|
||||
onSelectNode={cc.selectNode}
|
||||
onSelectAgent={cc.selectAgent}
|
||||
onProbe={cc.probeNodeId}
|
||||
onOpenTerminal={cc.openTerminal}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<aside className="glass-strong rounded-2xl p-5 flex flex-col max-h-80 lg:max-h-none">
|
||||
<h3 className="text-sm font-mono font-semibold text-neon-cyan mb-3 tracking-wider">CHAT</h3>
|
||||
<div className="flex-1 overflow-y-auto space-y-3 text-sm font-mono mb-2">
|
||||
{chat.length === 0 && <p className="text-ink-faint text-xs">Stel een vraag — je agent loopt data ophalen.</p>}
|
||||
{chat.map((m, i) => (
|
||||
<div key={i} className={`rounded-lg p-3 ${m.role === 'user' ? 'bg-cyan-50 border border-cyan-100' : 'bg-slate-50 border border-slate-100'}`}>
|
||||
<span className="text-ink-faint text-xs">{m.role === 'user' ? '▶ jij' : `◀ ${m.agent}`}</span>
|
||||
<div className={`mt-1 whitespace-pre-wrap ${m.role === 'user' ? 'text-neon-cyan' : 'text-ink'}`}>{m.text}</div>
|
||||
<div className={isPlatform ? 'min-h-[420px]' : 'min-h-0 flex-1'}>
|
||||
{cc.mainView === 'platform' ? (
|
||||
<PlatformTopology
|
||||
workload={cc.workload}
|
||||
animations={cc.anims}
|
||||
selectedNodeId={cc.selectedNodeId}
|
||||
onNodeClick={cc.selectNode}
|
||||
/>
|
||||
) : cc.mainView === 'presentation' ? (
|
||||
<PresentationView />
|
||||
) : cc.mainView === 'dataquality' ? (
|
||||
<DataQualityView />
|
||||
) : cc.mainView === 'knowledge' ? (
|
||||
<KnowledgeChatView onGpuActivity={setGpuChatActive} />
|
||||
) : cc.mainView === 'storage' ? (
|
||||
<StorageView />
|
||||
) : (
|
||||
<ApprovalInbox agents={cc.agents} livePending={cc.approvals} onDecide={cc.decide} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isPlatform && (
|
||||
<div className="flex w-[340px] shrink-0 flex-col border-l border-border">
|
||||
<InspectorPanel
|
||||
node={cc.selectedNode}
|
||||
nodeDetail={cc.nodeDetail}
|
||||
agent={cc.selectedAgent}
|
||||
agents={cc.agents}
|
||||
workload={cc.workload}
|
||||
gpu={cc.gpu}
|
||||
feed={cc.feed}
|
||||
lines={cc.inspectorLines}
|
||||
busy={cc.nodeBusy}
|
||||
onProbe={cc.probeNode}
|
||||
onAsk={cc.askNode}
|
||||
onSelectAgent={cc.selectAgent}
|
||||
onSendPrompt={cc.sendPrompt}
|
||||
onClear={cc.clearSelection}
|
||||
onOpenTerminal={cc.openTerminal}
|
||||
onProbeNodeId={cc.probeNodeId}
|
||||
/>
|
||||
<TerminalDock
|
||||
subjectId={terminalSubject}
|
||||
subjectLabel={terminalLabel}
|
||||
lines={cc.inspectorLines}
|
||||
busy={cc.nodeBusy || cc.promptBusy}
|
||||
expanded={cc.terminalExpanded}
|
||||
onToggle={() => cc.setTerminalExpanded(!cc.terminalExpanded)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PromptBar onSubmit={sendPrompt} busy={busy} />
|
||||
<ChatDrawer
|
||||
expanded={cc.chatExpanded}
|
||||
onToggle={() => cc.setChatExpanded(!cc.chatExpanded)}
|
||||
chat={cc.chat}
|
||||
feed={cc.feed}
|
||||
agents={cc.agents}
|
||||
approvals={cc.approvals}
|
||||
selectedAgent={cc.selectedAgent}
|
||||
promptBusy={cc.promptBusy}
|
||||
approvalHighlight={cc.approvalHighlight}
|
||||
filterAgentId={cc.selectedAgentId}
|
||||
onSendPrompt={cc.sendPrompt}
|
||||
onDecide={cc.decide}
|
||||
onDismissHighlight={() => cc.setApprovalHighlight(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user