import { useEffect, useState } from "react"; type RuntimeMap = { comfyui?: { url: string; client_id?: string; online?: boolean; error?: string }; ai_toolkit?: { toolkit_dir: string; venv: string; training_dir: string; runner: string; status: string }; }; type NodeInfo = { id?: string; label?: string; url?: string; roles?: string[]; online: boolean; error?: string; runtimes?: RuntimeMap; gpu_name?: string; vram_total?: number; vram_free?: number; torch_vram_total?: number; torch_vram_free?: number; queue_running?: number; queue_pending?: number; system?: { comfyui_version?: string; os?: string }; }; function gb(value?: number) { if (!value) return "—"; return `${(value / 1_000_000_000).toFixed(1)} GB`; } function vramPercent(node: NodeInfo) { if (!node.vram_total || node.vram_free == null) return null; return Math.max(0, Math.min(100, ((node.vram_total - node.vram_free) / node.vram_total) * 100)); } function roleClass(role: string) { if (role === "training") return "text-amber-300 border-amber-500/30 bg-amber-500/10"; if (role === "image" || role === "video") return "text-blue-300 border-blue-500/30 bg-blue-500/10"; return "text-gray-400 border-gray-700 bg-gray-900/60"; } export function NodesTab() { const [nodes, setNodes] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function load() { try { setError(null); const response = await fetch("/api/nodes"); if (!response.ok) throw new Error(`/api/nodes returned ${response.status}`); const data = await response.json(); if (!cancelled) setNodes(data.nodes || {}); } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : String(err)); } finally { if (!cancelled) setLoading(false); } } load(); const id = window.setInterval(load, 5000); return () => { cancelled = true; window.clearInterval(id); }; }, []); const entries = Object.entries(nodes); return (

GPU Nodes

Compute workers and runtimes. ComfyUI handles image/video generation; ai-toolkit handles LoRA training on AMD GPUs.

{loading &&

Checking nodes...

} {error &&

{error}

} {!loading && entries.length === 0 && !error && (
No GPU nodes configured.
)} {entries.map(([id, node]) => { const percent = vramPercent(node); const comfy = node.runtimes?.comfyui; const aiToolkit = node.runtimes?.ai_toolkit; return (

{node.label || id}

{comfy?.url &&

ComfyUI: {comfy.url}

}
{node.online ? "Comfy online" : "Comfy offline"}
{node.roles && node.roles.length > 0 && (
{node.roles.map((role) => ( {role} ))}
)}
{comfy && (

ComfyUI

{comfy.online ? "online" : "offline"}

Image/video generation runtime.

{comfy.error &&

{comfy.error}

}
)} {aiToolkit && (

ai-toolkit

training

AMD GPU LoRA training runtime.

{aiToolkit.training_dir}

)}
{node.online && (

{node.gpu_name || "GPU detected"}

ComfyUI {node.system?.comfyui_version || "version unknown"}

VRAM used {gb((node.vram_total || 0) - (node.vram_free || 0))} / {gb(node.vram_total)}

Running

{node.queue_running ?? 0}

Pending

{node.queue_pending ?? 0}

)}
); })}
); }