Files
atc-agents/ui/src/hooks/useCommandCenter.ts
T
mo a11621b21f Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
2026-06-25 00:28:23 +00:00

332 lines
11 KiB
TypeScript

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,
}
}