Large World graph, agent platform, brain stack & dashboard rebuild
Frontend - Large World: collapse org/company/team tiers into one expandable React Flow hierarchy (WorldFlow) with per-click expand, persisted node positions, a compact tree sidebar, wrench multi-select delete across levels, and a sized right slide-out (phone/tablet/full) showing an agent summary + drill button. - Agent page: GitHub-style animated contribution grid (VitalsCard), collapsible System Prompt + Personality cards, restructured anatomy cards, bigger avatar with name/title header row, Markdown/JSON-aware rendering, brain registry + history, avatar generate/upload. - User-icon menu (Infrastructure/Brains/Tools/Profile/Credits) + ToolPanel; Master Planner deploy wizard (Specialists/Swarm/Scheduled/Triggered); Team Runs view; reap-progress modal; dashboard is the single live interface. Backend - cm-brain crate (.brain as the agent definition) + brain apply/history. - Hard-purge reap (FK-ordered) + sandbox release + SSE batch-delete. - Swarm self-verifying loop, mode-aware planner, web.search tool, webhooks (migration 0013), org/company/team delete endpoints, scheduler sweeps. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9f266d5806
commit
34f744734b
@@ -0,0 +1,239 @@
|
||||
"use client";
|
||||
|
||||
// Large World — one React Flow stage showing the whole org → company → team →
|
||||
// agent hierarchy. Nodes expand on click (incremental: reveal direct children);
|
||||
// agent leaves open their profile page. Each level is colored distinctly. A
|
||||
// dependency-free tidy top-down tree positions the visible nodes; fitView zooms.
|
||||
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
Handle,
|
||||
MiniMap,
|
||||
Position,
|
||||
useNodesState,
|
||||
type Edge,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
} from "@xyflow/react";
|
||||
|
||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||
|
||||
// Structurally compatible with StructureTree's TreeItem.
|
||||
export interface WorldItem {
|
||||
id: string;
|
||||
level: string; // "org" | "company" | "team" | "claw"
|
||||
label: string;
|
||||
meta?: string;
|
||||
grad?: string;
|
||||
ink?: string;
|
||||
status?: string;
|
||||
children?: WorldItem[];
|
||||
}
|
||||
|
||||
const LEVEL: Record<string, { color: string; ink: string }> = {
|
||||
org: { color: "#c98af0", ink: "#1a0a2a" },
|
||||
company: { color: "#8a9af0", ink: "#0a0e2a" },
|
||||
team: { color: "#6fd0c0", ink: "#06201f" },
|
||||
claw: { color: "#ff8a7a", ink: "#2a0d05" },
|
||||
};
|
||||
const statusColor = (s?: string) => (s === "running" ? "#5ec8d8" : s === "online" ? "#5fd08a" : "#3a3a40");
|
||||
|
||||
interface WorldNodeData {
|
||||
level: string;
|
||||
label: string;
|
||||
sub: string;
|
||||
grad: string;
|
||||
ink: string;
|
||||
status: string;
|
||||
selected: boolean;
|
||||
hasChildren: boolean;
|
||||
expanded: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function WorldNodeImpl({ data }: NodeProps) {
|
||||
const d = data as WorldNodeData;
|
||||
if (d.level === "claw") {
|
||||
return (
|
||||
<div style={{ width: 120, display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
|
||||
<Handle type="target" position={Position.Top} style={{ opacity: 0 }} />
|
||||
<div style={{ position: "relative", width: 48, height: 48 }}>
|
||||
{d.status === "running" ? <div className="cm-halo" style={{ position: "absolute", inset: 0, borderRadius: "50%", background: "rgba(94,200,216,.4)" }} /> : null}
|
||||
{d.selected ? <div style={{ position: "absolute", inset: -6, borderRadius: "50%", border: "2px solid #ff6f61", boxShadow: "0 0 0 4px rgba(255,111,97,.12)" }} /> : null}
|
||||
<div style={{ position: "relative", width: 48, height: 48, borderRadius: "50%", background: d.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 17, fontWeight: 700, color: d.ink, boxShadow: "0 0 26px rgba(0,0,0,.45)" }}>{(d.label || "?").charAt(0).toUpperCase()}</div>
|
||||
<span style={{ position: "absolute", right: -1, bottom: -1, width: 11, height: 11, borderRadius: "50%", background: statusColor(d.status), border: "2px solid #0a0a0c" }} />
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<div style={{ fontSize: 11.5, fontWeight: 600, color: d.selected ? "#fff" : "#dcdce2" }}>{d.label}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 8.5, color: "#6a6a72" }}>{d.sub}</div>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Bottom} style={{ opacity: 0 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const lv = LEVEL[d.level] ?? LEVEL.team;
|
||||
return (
|
||||
<div style={{ width: 172 }}>
|
||||
<Handle type="target" position={Position.Top} style={{ opacity: 0 }} />
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 9, padding: "9px 11px", borderRadius: 11, background: "#101015", border: `1.5px solid ${d.selected ? "#ff6f61" : lv.color + "66"}`, boxShadow: d.selected ? "0 0 0 4px rgba(255,111,97,.12)" : "0 6px 18px rgba(0,0,0,.4)" }}>
|
||||
<span style={{ width: 30, height: 30, flex: "none", borderRadius: 8, background: lv.color, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 13, fontWeight: 800, color: lv.ink }}>{(d.label || "?").charAt(0).toUpperCase()}</span>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 700, color: "#f3f3f5", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.label}</div>
|
||||
<div style={{ fontFamily: mono, fontSize: 8.5, letterSpacing: ".06em", color: lv.color, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.level.toUpperCase()}{d.sub ? ` · ${d.sub}` : ""}</div>
|
||||
</div>
|
||||
{d.hasChildren ? <span style={{ flex: "none", color: "#8a8a92", fontSize: 11 }}>{d.expanded ? "▾" : "▸"}</span> : null}
|
||||
</div>
|
||||
<Handle type="source" position={Position.Bottom} style={{ opacity: 0 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const WorldNode = memo(WorldNodeImpl);
|
||||
const nodeTypes = { world: WorldNode };
|
||||
|
||||
function visibleChildren(item: WorldItem, expanded: Set<string>): WorldItem[] {
|
||||
return expanded.has(item.id) ? (item.children ?? []) : [];
|
||||
}
|
||||
|
||||
function flattenVisible(roots: WorldItem[], expanded: Set<string>): WorldItem[] {
|
||||
const out: WorldItem[] = [];
|
||||
const walk = (item: WorldItem) => { out.push(item); visibleChildren(item, expanded).forEach(walk); };
|
||||
roots.forEach(walk);
|
||||
return out;
|
||||
}
|
||||
|
||||
interface Pos { x: number; y: number }
|
||||
function layoutWorld(roots: WorldItem[], expanded: Set<string>): Map<string, Pos> {
|
||||
const pos = new Map<string, Pos>();
|
||||
const GAPX = 150;
|
||||
const GAPY = 150;
|
||||
let leaf = 0;
|
||||
const place = (item: WorldItem, depth: number): number => {
|
||||
const kids = visibleChildren(item, expanded);
|
||||
let x: number;
|
||||
if (kids.length === 0) { x = leaf * GAPX; leaf++; }
|
||||
else { const xs = kids.map((k) => place(k, depth + 1)); x = (xs[0] + xs[xs.length - 1]) / 2; }
|
||||
pos.set(item.id, { x, y: depth * GAPY });
|
||||
return x;
|
||||
};
|
||||
roots.forEach((r) => place(r, 0));
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Persisted manual positions (localStorage) so a layout the user arranged
|
||||
// survives expand/collapse and navigating away. Keyed by node id.
|
||||
const POS_KEY = "cm.world.pos";
|
||||
function loadSavedPos(): Record<string, Pos> {
|
||||
if (typeof window === "undefined") return {};
|
||||
try { return JSON.parse(window.localStorage.getItem(POS_KEY) || "{}") as Record<string, Pos>; } catch { return {}; }
|
||||
}
|
||||
function persistPos(p: Record<string, Pos>) {
|
||||
try { window.localStorage.setItem(POS_KEY, JSON.stringify(p)); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export function WorldFlow({ roots, expanded, selectedId, onToggleExpand, onSelect }: {
|
||||
roots: WorldItem[];
|
||||
expanded: Set<string>;
|
||||
selectedId: string | null;
|
||||
onToggleExpand: (id: string) => void;
|
||||
onSelect: (id: string) => void;
|
||||
}) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||
const visible = useMemo(() => flattenVisible(roots, expanded), [roots, expanded]);
|
||||
const pos = useMemo(() => layoutWorld(roots, expanded), [roots, expanded]);
|
||||
|
||||
// Manual positions the user dragged, loaded once from localStorage. A node the
|
||||
// user moved keeps its saved position across re-layouts; everything else
|
||||
// auto-arranges into the tidy tree (so newly-revealed children get placed).
|
||||
const [savedPos, setSavedPos] = useState<Record<string, Pos> | null>(null);
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSavedPos(loadSavedPos());
|
||||
}, []);
|
||||
const saved = savedPos ?? {};
|
||||
|
||||
// Rebuild when the visible set / selection changes (or saved positions load).
|
||||
// Surviving nodes keep their measurements so they don't flash back to hidden.
|
||||
const layoutKey = `${visible.map((v) => v.id).join(",")}|${selectedId}|${savedPos ? "L" : "U"}`;
|
||||
const [seen, setSeen] = useState("");
|
||||
if (seen !== layoutKey) {
|
||||
setSeen(layoutKey);
|
||||
setNodes((prev) => {
|
||||
const prevById = new Map(prev.map((n) => [n.id, n]));
|
||||
return visible.map((it) => {
|
||||
const old = prevById.get(it.id);
|
||||
const kids = it.children ?? [];
|
||||
return {
|
||||
id: it.id,
|
||||
type: "world",
|
||||
position: saved[it.id] ?? pos.get(it.id) ?? { x: 0, y: 0 },
|
||||
data: {
|
||||
level: it.level,
|
||||
label: it.label,
|
||||
sub: it.meta ?? "",
|
||||
grad: it.grad ?? LEVEL.claw.color,
|
||||
ink: it.ink ?? "#fff",
|
||||
status: it.status ?? "idle",
|
||||
selected: it.id === selectedId,
|
||||
hasChildren: kids.length > 0,
|
||||
expanded: expanded.has(it.id),
|
||||
} satisfies WorldNodeData,
|
||||
...(old?.measured ? { measured: old.measured, width: old.width, height: old.height } : {}),
|
||||
} as Node;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const edges: Edge[] = useMemo(() => {
|
||||
const es: Edge[] = [];
|
||||
visible.forEach((p) => {
|
||||
visibleChildren(p, expanded).forEach((c) => {
|
||||
const live = p.id === selectedId || c.id === selectedId;
|
||||
es.push({ id: `${p.id}->${c.id}`, source: p.id, target: c.id, animated: true, style: { stroke: live ? "rgba(255,111,97,.6)" : "rgba(94,200,216,.4)", strokeWidth: live ? 2 : 1.3 } });
|
||||
});
|
||||
});
|
||||
return es;
|
||||
}, [visible, expanded, selectedId]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "absolute", inset: 0 }}>
|
||||
<ReactFlow
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onNodeDragStop={(_, node, dragged) => {
|
||||
const moved = dragged && dragged.length ? dragged : [node];
|
||||
const next = { ...(savedPos ?? {}) };
|
||||
moved.forEach((n) => { if (n) next[n.id] = { x: n.position.x, y: n.position.y }; });
|
||||
setSavedPos(next);
|
||||
persistPos(next);
|
||||
}}
|
||||
onNodeClick={(_, n) => {
|
||||
const item = visible.find((v) => v.id === n.id);
|
||||
if (!item) return;
|
||||
// Agents: just select (opens the summary panel). Non-agents: select + expand.
|
||||
onSelect(item.id);
|
||||
if (item.level !== "claw" && (item.children?.length ?? 0) > 0) onToggleExpand(item.id);
|
||||
}}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.24 }}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
nodesConnectable={false}
|
||||
elementsSelectable={false}
|
||||
minZoom={0.3}
|
||||
maxZoom={1.6}
|
||||
colorMode="dark"
|
||||
>
|
||||
<Background color="#1c1c22" gap={26} />
|
||||
<Controls showInteractive={false} />
|
||||
<MiniMap pannable zoomable nodeColor={(n) => LEVEL[(n.data as WorldNodeData)?.level]?.color ?? "#ff6f61"} maskColor="rgba(8,8,10,.6)" style={{ background: "#0b0b0e" }} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user