a11621b21f
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
332 lines
11 KiB
TypeScript
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,
|
|
}
|
|
}
|