import { Badge, Button, Tabs } from "@cloudflare/kumo"; import { prettyApi } from "../../lib/gradio"; import { ArrowClockwiseIcon, ArrowSquareOutIcon, CaretDownIcon, CaretRightIcon, CheckIcon, CrosshairIcon, EyeIcon, EyeSlashIcon, FunctionIcon, MagnifyingGlassIcon, PlusIcon, PushPinIcon, PushPinSlashIcon, SpinnerIcon, TrashIcon, WarningIcon, LockSimpleIcon, } from "@phosphor-icons/react"; import { useReactFlow } from "@xyflow/react"; import { useMemo, useRef, useState, type DragEvent } from "react"; import { useShallow } from "zustand/react/shallow"; import { MOD } from "../../lib/format"; import { fnToDef } from "../../lib/groups"; import { hardwareLabel, isUsable, stageLabel } from "../../lib/hub"; import { allDefs, defAccent, getDef, TYPE_TOKEN, typeColor } from "../../lib/registry"; import type { CanvasNode, NodeDef, SpaceEntry } from "../../lib/types"; import { TEMPLATES } from "../../lib/workflows"; import { useCanvasStore } from "../../store/useCanvasStore"; import { spaceAction } from "../NodeError"; import { useLibraryStore } from "../../store/useLibraryStore"; import { useContextMenu, type MenuItem } from "../ContextMenu"; /* Adding nodes ----------------------------------------------------------- */ function useAddNode() { const addNode = useCanvasStore((s) => s.addNode); const addStep = useCanvasStore((s) => s.addStep); const view = useCanvasStore((s) => s.view); const snapToGrid = useCanvasStore((s) => s.snapToGrid); const { screenToFlowPosition } = useReactFlow(); return (def: NodeDef) => { if (view === "pipeline") { addStep(def.type); return; } const rect = document.querySelector(".react-flow")?.getBoundingClientRect(); const center = rect ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : { x: window.innerWidth / 2, y: window.innerHeight / 2 }; const p = screenToFlowPosition(center); const jitter = (Math.random() - 0.5) * 80; let pos = { x: p.x - (def.width ?? 260) / 2 + jitter, y: p.y - 60 + jitter }; if (snapToGrid) pos = { x: Math.round(pos.x / 20) * 20, y: Math.round(pos.y / 20) * 20 }; addNode(def.type, pos); }; } function useDragGhost() { const ghostRef = useRef(null); const onDragStart = (e: DragEvent, def: NodeDef) => { e.dataTransfer.setData("application/kumo-node", def.type); e.dataTransfer.effectAllowed = "copy"; const ghost = ghostRef.current; if (ghost) { ghost.textContent = def.title; ghost.style.setProperty("--node-accent", typeColor(defAccent(def))); e.dataTransfer.setDragImage(ghost, 16, 16); } }; return { ghostRef, onDragStart }; } /* Workflow tab ----------------------------------------------------------- */ const STATUS_TEXT: Record = { queued: "Queued", running: "Running", done: "Done", error: "Failed" }; /** * Everything about *this* workflow: a dock of the nodes you reach for most, * the Spaces it depends on (and whether they're awake), and an outline of the * nodes on the canvas so a failing one is one click away. Finding new Spaces * happens in the explorer (⌘K), not here. */ function WorkflowTab() { const nodes = useCanvasStore((s) => s.nodes); const order = useCanvasStore((s) => s.order); const spaces = useCanvasStore((s) => s.spaces); const groupDefs = useCanvasStore(useShallow((s) => s.functions)); const openPalette = useCanvasStore((s) => s.openPalette); const focusNode = useCanvasStore((s) => s.focusNode); const removeSpace = useCanvasStore((s) => s.removeSpace); const refreshSpace = useCanvasStore((s) => s.refreshSpace); const deleteFunction = useCanvasStore((s) => s.deleteFunction); const pinned = useLibraryStore((s) => s.pinned); const hidden = useLibraryStore((s) => s.hidden); const pin = useLibraryStore((s) => s.pin); const unpin = useLibraryStore((s) => s.unpin); const hide = useLibraryStore((s) => s.hide); const unhide = useLibraryStore((s) => s.unhide); const onAdd = useAddNode(); const { ghostRef, onDragStart } = useDragGhost(); const ctx = useContextMenu(); const [openSpaces, setOpenSpaces] = useState>({}); const [showHidden, setShowHidden] = useState(false); const dock = useMemo(() => { const all = [...allDefs(), ...groupDefs.map(fnToDef)]; const byType = new Map(all.map((d) => [d.type, d])); const pins = pinned.map((t) => byType.get(t)).filter((d): d is NodeDef => !!d); const dockable = (d: NodeDef) => d.category === "Inputs" || d.category === "Utilities"; const inputs = all.filter((d) => dockable(d) && !pinned.includes(d.type) && !hidden.includes(d.type)); const groups = all.filter((d) => d.category === "Functions" && !pinned.includes(d.type)); const hiddenInputs = all.filter((d) => dockable(d) && hidden.includes(d.type)); return { pins, inputs, groups, hiddenInputs }; }, [groupDefs, pinned, hidden]); const entries = useMemo(() => Object.values(spaces).sort((a, b) => a.summary.title.localeCompare(b.summary.title)), [spaces]); const usedBy = useMemo(() => { const m = new Map(); for (const n of nodes) { const sid = getDef(n.data.defType).space?.id; if (sid) m.set(sid, (m.get(sid) ?? 0) + 1); } return m; }, [nodes]); const outline = useMemo(() => { const rank = new Map(order.map((id, i) => [id, i])); const sorted = [...nodes].sort((a, b) => (rank.get(a.id) ?? 1e9) - (rank.get(b.id) ?? 1e9)); const children = new Map(); const roots: CanvasNode[] = []; for (const n of sorted) { if (n.parentId) children.set(n.parentId, [...(children.get(n.parentId) ?? []), n]); else roots.push(n); } return { roots, children }; }, [nodes, order]); const defMenu = (e: React.MouseEvent, def: NodeDef) => { const isPinned = pinned.includes(def.type); const items: MenuItem[] = [ { label: "Add to canvas", icon: PlusIcon, onSelect: () => onAdd(def) }, isPinned ? { label: "Remove from dock", icon: PushPinSlashIcon, onSelect: () => unpin(def.type) } : { label: "Pin to dock", icon: PushPinIcon, onSelect: () => pin(def.type) }, ]; if ((def.category === "Inputs" || def.category === "Utilities") && !isPinned) items.push(hidden.includes(def.type) ? { label: "Show in dock", icon: EyeIcon, onSelect: () => unhide(def.type) } : { label: "Hide from dock", icon: EyeSlashIcon, onSelect: () => hide(def.type) }); if (def.space) { items.push( hidden.includes(def.type) ? { label: "Show endpoint", icon: EyeIcon, onSelect: () => unhide(def.type) } : { label: "Hide endpoint", icon: EyeSlashIcon, onSelect: () => hide(def.type) }, { label: "Open Space on Hugging Face", icon: ArrowSquareOutIcon, href: `https://huggingface.co/spaces/${def.space.id}`, separator: true }, ); } if (def.category === "Functions") items.push({ label: "Delete group", icon: TrashIcon, danger: true, separator: true, onSelect: () => deleteFunction(def.type.slice(3)) }); ctx.open(e, items); }; const spaceMenu = (e: React.MouseEvent, sp: SpaceEntry) => { const own = sp.defs; const allHidden = own.length > 0 && own.every((d) => hidden.includes(d.type)); const used = usedBy.get(sp.summary.id) ?? 0; ctx.open(e, [ { label: "Reload endpoints", icon: ArrowClockwiseIcon, onSelect: () => void refreshSpace(sp.summary.id) }, { label: "Open on Hugging Face", icon: ArrowSquareOutIcon, href: `https://huggingface.co/spaces/${sp.summary.id}` }, allHidden ? { label: "Show all endpoints", icon: EyeIcon, separator: true, onSelect: () => own.forEach((d) => unhide(d.type)) } : { label: "Hide all endpoints", icon: EyeSlashIcon, separator: true, onSelect: () => own.forEach((d) => hide(d.type)) }, { label: used ? `Remove Space (${used} node${used === 1 ? "" : "s"} use it)` : "Remove Space", icon: TrashIcon, danger: true, disabled: used > 0, onSelect: () => removeSpace(sp.summary.id) }, ]); }; const Chip = ({ def, pinnedChip, dim }: { def: NodeDef; pinnedChip?: boolean; dim?: boolean }) => { const sp = def.space ? spaces[def.space.id] : undefined; return ( ); }; const NodeRow = ({ n, depth }: { n: CanvasNode; depth: number }) => { const def = getDef(n.data.defType); const sp = def.space ? spaces[def.space.id] : undefined; const kids = outline.children.get(n.id) ?? []; const st = n.data.status; return ( <> {st === "error" && n.data.error && (
focusNode(n.id)}> {n.data.error}
)} {kids.map((k) => ( ))} ); }; return (

Quick add

{dock.pins.map((d) => ( ))} {dock.inputs.map((d) => ( ))} {dock.groups.map((d) => ( ))} {showHidden && dock.hiddenInputs.map((d) => )} {dock.hiddenInputs.length > 0 && ( )}

Click adds to the canvas, or drag one exactly where you want it. Pin anything from the explorer and it lands here.

Spaces in this workflow {entries.length > 0 && {entries.length}}

{entries.length === 0 ? (

None yet. Every Space you add from the explorer shows up here with its status and endpoints.

) : ( entries.map((sp) => { const visible = sp.defs.filter((d) => !hidden.includes(d.type)); const isOpen = openSpaces[sp.summary.id] ?? false; const used = usedBy.get(sp.summary.id) ?? 0; const usable = isUsable(sp.summary.stage); const fix = sp.status === "error" || (sp.status === "ready" && sp.summary.stage !== "RUNNING" && sp.summary.stage !== "UNKNOWN") ? spaceAction(sp.summary.id, sp, sp.error ?? "") : null; const hw = hardwareLabel(sp.summary.hardware); return (
setOpenSpaces((o) => ({ ...o, [sp.summary.id]: !isOpen }))} onKeyDown={(e) => e.key === "Enter" && setOpenSpaces((o) => ({ ...o, [sp.summary.id]: !isOpen }))} onContextMenu={(e) => spaceMenu(e, sp)} > {isOpen ? : } {sp.summary.emoji}
{sp.summary.title} {sp.summary.private && }
{sp.status === "loading" ? ( <> Loading endpoints… ) : sp.status === "error" ? ( {sp.error ?? "Failed to load"} ) : ( <> {stageLabel(sp.summary.stage)} {hw && ( {hw} )} · {used ? `${used} on canvas` : "unused"} )}
{sp.status === "loading" ? null : fix ? ( fix.href ? ( e.stopPropagation()}> {fix.label} ) : ( ) ) : sp.status === "error" || !usable ? (
{isOpen && (
{sp.defs.length === 0 &&

No callable endpoints.

} {sp.defs.map((d) => { const off = hidden.includes(d.type); return (
onDragStart(e, d)} onClick={() => !off && onAdd(d)} onContextMenu={(e) => defMenu(e, d)} role="button" tabIndex={0} onKeyDown={(e) => e.key === "Enter" && !off && onAdd(d)} title={`${d.description}\nClick to add · drag onto the canvas`} > {d.space && sp.defs.length > 1 ? prettyApi(d.space.apiName) : d.title} {Array.from(new Set(d.outputs.map((o) => o.type))).map((t) => ( ))}
); })}
)}
); }) )} {nodes.length > 0 && ( <>

On the canvas {nodes.length}

{outline.roots.map((n) => ( ))}
)}
{ctx.menu}
); } /* Templates tab --------------------------------------------------------- */ function TemplatesTab() { const workflowId = useCanvasStore((s) => s.workflowId); const loadTemplate = useCanvasStore((s) => s.loadTemplate); const { fitView } = useReactFlow(); const load = async (id: string) => { await loadTemplate(id); requestAnimationFrame(() => fitView({ padding: 0.18, duration: 400, maxZoom: 1 })); }; return (
{TEMPLATES.map((t) => { const current = t.id === workflowId; return (
{t.name} {current && Open}

{t.description}

{t.spaces.join(" · ")}
); })}
); } export function NodeLibrary() { const tab = useCanvasStore((s) => s.libraryTab); const setLibraryTab = useCanvasStore((s) => s.setLibraryTab); return (
setLibraryTab(t as "workflow" | "templates")} tabs={[ { value: "workflow", label: "Workflow" }, { value: "templates", label: "Templates" }, ]} />
{tab === "templates" ? : }
); }