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:
@@ -0,0 +1,10 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useClock() {
|
||||
const [now, setNow] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setNow(new Date()), 1000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
return now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
askNode as apiAskNode,
|
||||
decideApproval,
|
||||
fetchAgents,
|
||||
fetchApprovals,
|
||||
fetchFeed,
|
||||
fetchGpu,
|
||||
fetchNodeDetail,
|
||||
fetchStatus,
|
||||
fetchTerminals,
|
||||
fetchWorkload,
|
||||
probeNode as apiProbeNode,
|
||||
sendPrompt as apiSendPrompt,
|
||||
} from '../lib/api'
|
||||
import { AGENT_NODE, NODE_ALIASES, wsUrl } from '../lib/constants'
|
||||
import { resolveInfraNode } from '../lib/infraCatalog'
|
||||
import type {
|
||||
Agent,
|
||||
AgentAnim,
|
||||
Approval,
|
||||
ChatMessage,
|
||||
FeedEntry,
|
||||
GpuStatus,
|
||||
NodeDetail,
|
||||
StatusData,
|
||||
TerminalLine,
|
||||
TopologyNode,
|
||||
WorkloadData,
|
||||
} from '../types'
|
||||
|
||||
function resolveProbeId(nodeId: string) {
|
||||
const aliased = NODE_ALIASES[nodeId] || nodeId
|
||||
const infra = resolveInfraNode(nodeId) || resolveInfraNode(aliased)
|
||||
return infra?.id || aliased
|
||||
}
|
||||
|
||||
export function useCommandCenter() {
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
const [agentsLoading, setAgentsLoading] = useState(true)
|
||||
const [status, setStatus] = useState<StatusData | null>(null)
|
||||
const [workload, setWorkload] = useState<WorkloadData | null>(null)
|
||||
const [gpu, setGpu] = useState<GpuStatus | null>(null)
|
||||
const [feed, setFeed] = useState<FeedEntry[]>([])
|
||||
const [approvals, setApprovals] = useState<Approval[]>([])
|
||||
const [chat, setChat] = useState<ChatMessage[]>([])
|
||||
const [anims, setAnims] = useState<Record<string, AgentAnim>>({})
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null)
|
||||
const [promptBusy, setPromptBusy] = useState(false)
|
||||
const [terminals, setTerminals] = useState<Record<string, TerminalLine[]>>({})
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<TopologyNode | null>(null)
|
||||
const [nodeDetail, setNodeDetail] = useState<NodeDetail | null>(null)
|
||||
const [nodeBusy, setNodeBusy] = useState(false)
|
||||
const [mainView, setMainView] = useState<'platform' | 'approvals' | 'presentation' | 'dataquality' | 'knowledge' | 'storage'>('platform')
|
||||
const [approvalHighlight, setApprovalHighlight] = useState(false)
|
||||
const [chatExpanded, setChatExpanded] = useState(false)
|
||||
const promptTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [terminalExpanded, setTerminalExpanded] = useState(true)
|
||||
|
||||
const appendTerminal = useCallback((line: TerminalLine) => {
|
||||
setTerminals((prev) => {
|
||||
const cur = prev[line.agent_id] || []
|
||||
return { ...prev, [line.agent_id]: [...cur, line].slice(-300) }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const selectedAgent = useMemo(
|
||||
() => agents.find((a) => a.id === selectedAgentId) || null,
|
||||
[agents, selectedAgentId],
|
||||
)
|
||||
|
||||
const reloadFast = useCallback(async () => {
|
||||
const [a, s, f, ap, g, t] = await Promise.all([
|
||||
fetchAgents(),
|
||||
fetchStatus(),
|
||||
fetchFeed(),
|
||||
fetchApprovals(),
|
||||
fetchGpu(),
|
||||
fetchTerminals(),
|
||||
])
|
||||
setAgents(a)
|
||||
setAgentsLoading(false)
|
||||
setStatus(s)
|
||||
setGpu(g || s?.gpu || null)
|
||||
setFeed(f)
|
||||
setApprovals(ap)
|
||||
setTerminals(t)
|
||||
}, [])
|
||||
|
||||
const reloadWorkload = useCallback(async () => {
|
||||
const w = await fetchWorkload()
|
||||
if (w?.zones) setWorkload(w)
|
||||
}, [])
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
await reloadFast()
|
||||
reloadWorkload()
|
||||
}, [reloadFast, reloadWorkload])
|
||||
|
||||
useEffect(() => {
|
||||
reloadFast().then(() => reloadWorkload())
|
||||
const ws = new WebSocket(wsUrl())
|
||||
ws.onmessage = (ev) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (msg.type === 'status') {
|
||||
setStatus(msg.data)
|
||||
if (msg.data.gpu) setGpu(msg.data.gpu)
|
||||
}
|
||||
if (msg.type === 'workload') setWorkload(msg.data)
|
||||
if (msg.type === 'terminal') appendTerminal(msg.line)
|
||||
if (msg.type === 'terminal_history' && msg.terminals) setTerminals(msg.terminals)
|
||||
if (msg.type === 'feed') setFeed((prev) => [msg.entry, ...prev].slice(0, 100))
|
||||
if (msg.type === 'agent_dispatch') {
|
||||
setSelectedAgentId(msg.agent_id)
|
||||
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, ts: new Date().toISOString() }])
|
||||
setPromptBusy(false)
|
||||
reloadFast()
|
||||
}
|
||||
if (msg.type === 'approval_new') {
|
||||
setApprovals((prev) => {
|
||||
if (prev.some((a) => a.id === msg.approval?.id)) return prev
|
||||
return [msg.approval, ...prev]
|
||||
})
|
||||
if (msg.approval?.status === 'pending') setApprovalHighlight(true)
|
||||
}
|
||||
if (msg.type === 'approval_update' && msg.approval) {
|
||||
setApprovals((prev) => prev.filter((a) => a.id !== msg.approval.id))
|
||||
}
|
||||
if (msg.type === 'node_ask_result') {
|
||||
setNodeBusy(false)
|
||||
if (msg.agent_id) setSelectedAgentId(msg.agent_id)
|
||||
}
|
||||
}
|
||||
const iv = setInterval(reloadFast, 15000)
|
||||
const wv = setInterval(reloadWorkload, 45000)
|
||||
return () => { ws.close(); clearInterval(iv); clearInterval(wv) }
|
||||
}, [reloadFast, reloadWorkload, appendTerminal])
|
||||
|
||||
const findNodeStub = useCallback((nodeId: string): TopologyNode | null => {
|
||||
const resolved = NODE_ALIASES[nodeId] || nodeId
|
||||
const allNodes = [
|
||||
...(workload?.topology?.nodes || []),
|
||||
...Object.values(workload?.topologies || {}).flatMap((v) => v.nodes),
|
||||
]
|
||||
const wn = allNodes.find((n) => n.id === nodeId) || allNodes.find((n) => n.id === resolved)
|
||||
if (wn) return wn
|
||||
|
||||
const infra = resolveInfraNode(nodeId) || resolveInfraNode(resolved)
|
||||
if (infra) {
|
||||
return {
|
||||
id: infra.id,
|
||||
label: infra.label,
|
||||
vm: infra.vm,
|
||||
ip: infra.ip,
|
||||
x: 50,
|
||||
y: 50,
|
||||
color: infra.accent,
|
||||
level: 'ok',
|
||||
role: infra.description,
|
||||
apps: infra.apps.map((a) => ({ name: a.label, state: 'link', image: a.url, ports: a.port ? [a.port] : [] })),
|
||||
running: 1,
|
||||
total: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const agent = agents.find((a) => a.id === nodeId)
|
||||
if (agent) {
|
||||
return {
|
||||
id: agent.id, label: agent.name, vm: 'agent', ip: '10.0.21.33',
|
||||
x: 50, y: 50, color: agent.color, level: 'ok', role: agent.role, apps: [], running: 1, total: 1,
|
||||
agent_id: agent.id,
|
||||
}
|
||||
}
|
||||
const zone = workload?.zones.find((z) => z.id === nodeId || z.id === resolved)
|
||||
if (zone) {
|
||||
return {
|
||||
id: nodeId, label: zone.label, vm: zone.vm || zone.id, ip: zone.ip || '',
|
||||
x: 50, y: 50, color: zone.color, level: zone.level, role: 'zone', apps: zone.apps,
|
||||
running: zone.running, total: zone.total,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [workload, agents])
|
||||
|
||||
const probeNodeId = useCallback((nodeId: string) => {
|
||||
setNodeBusy(true)
|
||||
setTerminalExpanded(true)
|
||||
apiProbeNode(resolveProbeId(nodeId)).finally(() => setNodeBusy(false))
|
||||
}, [])
|
||||
|
||||
const openTerminal = useCallback((nodeId: string) => {
|
||||
setTerminalExpanded(true)
|
||||
setSelectedNodeId(nodeId)
|
||||
}, [])
|
||||
|
||||
const selectNode = useCallback(async (nodeId: string) => {
|
||||
const stub = findNodeStub(nodeId)
|
||||
if (!stub) return
|
||||
const probeId = resolveProbeId(nodeId)
|
||||
setSelectedNodeId(probeId)
|
||||
setSelectedNode({ ...stub, id: probeId })
|
||||
setNodeDetail(null)
|
||||
setTerminalExpanded(true)
|
||||
const infra = resolveInfraNode(nodeId)
|
||||
const linked = agents.find(
|
||||
(a) => a.id === nodeId || AGENT_NODE[a.id] === probeId || a.id === infra?.agentId,
|
||||
)
|
||||
if (linked) setSelectedAgentId(linked.id)
|
||||
try {
|
||||
const detail = await fetchNodeDetail(probeId)
|
||||
if (!detail.error) setNodeDetail(detail as NodeDetail)
|
||||
} catch { /* ok */ }
|
||||
probeNodeId(nodeId)
|
||||
}, [findNodeStub, agents, probeNodeId])
|
||||
|
||||
const selectAgent = useCallback((id: string) => {
|
||||
setSelectedAgentId(id)
|
||||
setTerminalExpanded(true)
|
||||
const agent = agents.find((a) => a.id === id)
|
||||
if (!agent) return
|
||||
const nodeId = agent.supervisor ? id : (AGENT_NODE[id] || agent.zone)
|
||||
if (nodeId) {
|
||||
const stub = findNodeStub(nodeId)
|
||||
if (stub) {
|
||||
selectNode(nodeId)
|
||||
return
|
||||
}
|
||||
}
|
||||
setSelectedNodeId(null)
|
||||
setSelectedNode(null)
|
||||
setNodeDetail(null)
|
||||
}, [agents, findNodeStub, selectNode])
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedNodeId(null)
|
||||
setSelectedNode(null)
|
||||
setNodeDetail(null)
|
||||
setSelectedAgentId(null)
|
||||
}, [])
|
||||
|
||||
const probeNode = useCallback(() => {
|
||||
if (selectedNodeId) probeNodeId(selectedNodeId)
|
||||
}, [selectedNodeId, probeNodeId])
|
||||
|
||||
const askNode = useCallback(async (message: string) => {
|
||||
if (!selectedNodeId) return
|
||||
setNodeBusy(true)
|
||||
setTerminalExpanded(true)
|
||||
await apiAskNode(resolveProbeId(selectedNodeId), message)
|
||||
}, [selectedNodeId])
|
||||
|
||||
const sendPrompt = useCallback(async (message: string, agentId?: string) => {
|
||||
setPromptBusy(true)
|
||||
setChatExpanded(true)
|
||||
setChat((c) => [...c, { role: 'user', text: message, ts: new Date().toISOString() }])
|
||||
if (agentId) setSelectedAgentId(agentId)
|
||||
await apiSendPrompt(message, agentId)
|
||||
}, [])
|
||||
|
||||
const decide = useCallback(async (id: string, approved: boolean, decidedBy = 'mo-commander', note = '') => {
|
||||
setApprovals((prev) => prev.filter((a) => a.id !== id))
|
||||
await decideApproval(id, approved, decidedBy, note)
|
||||
reloadFast()
|
||||
}, [reloadFast])
|
||||
|
||||
const terminalSubjectId = selectedNodeId || selectedAgentId
|
||||
|
||||
const inspectorLines = useMemo(() => {
|
||||
if (!terminalSubjectId) return []
|
||||
const probeId = resolveProbeId(terminalSubjectId)
|
||||
return terminals[probeId] || terminals[terminalSubjectId] || terminals[selectedAgentId || ''] || []
|
||||
}, [terminalSubjectId, terminals, selectedAgentId])
|
||||
|
||||
const focusApprovals = useCallback(() => {
|
||||
setApprovalHighlight(true)
|
||||
setMainView('approvals')
|
||||
}, [])
|
||||
|
||||
return {
|
||||
agents,
|
||||
agentsLoading,
|
||||
status,
|
||||
workload,
|
||||
gpu,
|
||||
feed,
|
||||
approvals,
|
||||
chat,
|
||||
anims,
|
||||
selectedAgentId,
|
||||
selectedAgent,
|
||||
promptBusy,
|
||||
selectedNodeId,
|
||||
selectedNode,
|
||||
nodeDetail,
|
||||
nodeBusy,
|
||||
mainView,
|
||||
setMainView,
|
||||
approvalHighlight,
|
||||
setApprovalHighlight,
|
||||
inspectorLines,
|
||||
terminalSubjectId,
|
||||
terminalExpanded,
|
||||
setTerminalExpanded,
|
||||
selectNode,
|
||||
selectAgent,
|
||||
clearSelection,
|
||||
probeNode,
|
||||
probeNodeId,
|
||||
openTerminal,
|
||||
askNode,
|
||||
sendPrompt,
|
||||
decide,
|
||||
focusApprovals,
|
||||
reload,
|
||||
chatExpanded,
|
||||
setChatExpanded,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { Agent, AgentAnim, GpuStatus } from '../types'
|
||||
import { pseudoAgentLoad } from '../lib/agentMeta'
|
||||
|
||||
export type AgentLoad = { cpu: number; mem: number }
|
||||
|
||||
export type GpuLiveMetrics = {
|
||||
tokenThroughput: number
|
||||
avgUtil: number
|
||||
avgVram: number
|
||||
deviceUtils: number[]
|
||||
}
|
||||
|
||||
function clamp(n: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, n))
|
||||
}
|
||||
|
||||
function memPct(used: number, total: number) {
|
||||
if (!total) return 0
|
||||
return Math.round((used / total) * 100)
|
||||
}
|
||||
|
||||
export function useLiveMetrics(
|
||||
agents: Agent[],
|
||||
gpu: GpuStatus | null,
|
||||
animations: Record<string, AgentAnim>,
|
||||
boost = false,
|
||||
) {
|
||||
const [agentLoads, setAgentLoads] = useState<Record<string, AgentLoad>>({})
|
||||
const [gpuLive, setGpuLive] = useState<GpuLiveMetrics>({
|
||||
tokenThroughput: 0,
|
||||
avgUtil: 0,
|
||||
avgVram: 0,
|
||||
deviceUtils: [],
|
||||
})
|
||||
const loadsRef = useRef(agentLoads)
|
||||
loadsRef.current = agentLoads
|
||||
|
||||
useEffect(() => {
|
||||
const seed: Record<string, AgentLoad> = {}
|
||||
for (const a of agents) {
|
||||
seed[a.id] = pseudoAgentLoad(a.stats)
|
||||
}
|
||||
setAgentLoads(seed)
|
||||
|
||||
const gpus = gpu?.gpus || []
|
||||
const baseUtil = gpus.length ? gpus.reduce((s, g) => s + g.util_gpu, 0) / gpus.length : 0
|
||||
const baseVram = gpus.length
|
||||
? gpus.reduce((s, g) => s + memPct(g.memory_used_mib, g.memory_total_mib), 0) / gpus.length
|
||||
: 0
|
||||
const inferenceOn = gpu?.ok && gpu.inference_active
|
||||
setGpuLive({
|
||||
tokenThroughput: inferenceOn ? Math.round(baseUtil * 42 + 120) : 0,
|
||||
avgUtil: baseUtil,
|
||||
avgVram: baseVram,
|
||||
deviceUtils: gpus.map((g) => g.util_gpu),
|
||||
})
|
||||
}, [agents, gpu])
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
setAgentLoads((prev) => {
|
||||
const next: Record<string, AgentLoad> = {}
|
||||
for (const a of agents) {
|
||||
const busy = (animations[a.id]?.state || 'idle') !== 'idle'
|
||||
const base = pseudoAgentLoad(a.stats)
|
||||
const cur = prev[a.id] || base
|
||||
const drift = (Math.random() - 0.5) * (busy ? 7 : 2.5)
|
||||
const driftMem = (Math.random() - 0.5) * (busy ? 5 : 2)
|
||||
const targetCpu = busy ? Math.max(base.cpu, cur.cpu) : base.cpu
|
||||
const targetMem = busy ? Math.max(base.mem, cur.mem) : base.mem
|
||||
next[a.id] = {
|
||||
cpu: clamp(Math.round(cur.cpu + drift + (busy ? 1.2 : -0.3)), 4, 96),
|
||||
mem: clamp(Math.round(cur.mem + driftMem + (busy ? 0.8 : -0.2)), 6, 92),
|
||||
}
|
||||
if (!busy) {
|
||||
next[a.id].cpu = clamp(Math.round(next[a.id].cpu * 0.85 + targetCpu * 0.15), 4, 96)
|
||||
next[a.id].mem = clamp(Math.round(next[a.id].mem * 0.85 + targetMem * 0.15), 6, 92)
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
|
||||
if (gpu?.ok) {
|
||||
const gpus = gpu.gpus || []
|
||||
const inferenceOn = gpu.inference_active
|
||||
setGpuLive((prev) => {
|
||||
const baseUtil = gpus.length
|
||||
? gpus.reduce((s, g) => s + g.util_gpu, 0) / gpus.length
|
||||
: prev.avgUtil
|
||||
const baseVram = gpus.length
|
||||
? gpus.reduce((s, g) => s + memPct(g.memory_used_mib, g.memory_total_mib), 0) / gpus.length
|
||||
: prev.avgVram
|
||||
const jitterScale = boost ? 12 : 6
|
||||
const utilJitter = (Math.random() - 0.5) * (inferenceOn ? jitterScale : 2)
|
||||
const avgUtil = clamp(baseUtil + utilJitter, 0, 100)
|
||||
const avgVram = clamp(baseVram + (Math.random() - 0.5) * 3, 0, 100)
|
||||
const deviceUtils = gpus.map((g, i) => {
|
||||
const real = g.util_gpu
|
||||
if (boost && inferenceOn) {
|
||||
return clamp(real + (Math.random() - 0.5) * 8, 0, 100)
|
||||
}
|
||||
return clamp((prev.deviceUtils[i] ?? real) + (Math.random() - 0.5) * 5, 0, 100)
|
||||
})
|
||||
const tokenBase = boost ? Math.max(180, baseUtil * 55 + 140) : baseUtil * 42 + 120
|
||||
const tokenThroughput = inferenceOn
|
||||
? clamp(Math.round(prev.tokenThroughput * 0.4 + tokenBase * 0.6 + (Math.random() - 0.5) * (boost ? 45 : 28)), boost ? 120 : 80, boost ? 520 : 420)
|
||||
: 0
|
||||
return { tokenThroughput, avgUtil, avgVram, deviceUtils }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tick()
|
||||
const id = setInterval(tick, boost ? 1000 : 5000)
|
||||
return () => clearInterval(id)
|
||||
}, [agents, animations, gpu, boost])
|
||||
|
||||
return { agentLoads, gpuLive }
|
||||
}
|
||||
Reference in New Issue
Block a user