@sleep2agi/agent-network-dashboard 0.5.3-preview.260 → 0.5.3-preview.261
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +5 -5
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/admin.html +1 -1
- package/.next/server/app/admin.rsc +1 -1
- package/.next/server/app/admin.segments/_full.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +2 -2
- package/.next/server/app/login.segments/_full.segment.rsc +2 -2
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +1 -1
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs.html +1 -1
- package/.next/server/app/logs.rsc +1 -1
- package/.next/server/app/logs.segments/_full.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages.html +1 -1
- package/.next/server/app/messages.rsc +1 -1
- package/.next/server/app/messages.segments/_full.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node.html +1 -1
- package/.next/server/app/node.rsc +1 -1
- package/.next/server/app/node.segments/_full.segment.rsc +1 -1
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +1 -1
- package/.next/server/app/node.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes.html +1 -1
- package/.next/server/app/nodes.rsc +1 -1
- package/.next/server/app/nodes.segments/_full.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +1 -1
- package/.next/server/app/server-logs.rsc +1 -1
- package/.next/server/app/server-logs.segments/_full.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/settings/networks.html +1 -1
- package/.next/server/app/settings/networks.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +1 -1
- package/.next/server/app/settings/tokens.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +2 -2
- package/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks.html +1 -1
- package/.next/server/app/tasks.rsc +1 -1
- package/.next/server/app/tasks.segments/_full.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0sv~g.o._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js +3 -3
- package/.next/server/chunks/ssr/agent-network-dashboard_09kk21a._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_01jhlxz._.js.map +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js +1 -1
- package/.next/server/chunks/ssr/agent-network-dashboard_app_09d29my._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/{0p142v5va508~.js → 0-vlr8~b0f-cx.js} +1 -1
- package/.next/static/chunks/{11sahbo6ikg8g.js → 08.pwokcpknmp.js} +1 -1
- package/.next/static/chunks/{0u8v68gl7g6j1.js → 0nd-y~i5proep.js} +1 -1
- package/.next/static/chunks/0ztakmtfxkgya.js +4 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/TopoGraph.tsx +446 -22
- package/app/lib/vendorIdentity.ts +74 -56
- package/package.json +4 -4
- package/public/vendors/claude.svg +7 -8
- package/public/vendors/minimax.svg +8 -9
- package/public/vendors/openai.svg +8 -10
- package/scripts/topo-overlap-test.mjs +22 -8
- package/scripts/topo-tree-diag.mjs +95 -0
- package/.next/static/chunks/083elibefsefi.js +0 -4
- /package/.next/static/{wz1T-LhLDalz691PpN3E7 → CLsgsYpGMRUiIcJJK3xkR}/_buildManifest.js +0 -0
- /package/.next/static/{wz1T-LhLDalz691PpN3E7 → CLsgsYpGMRUiIcJJK3xkR}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{wz1T-LhLDalz691PpN3E7 → CLsgsYpGMRUiIcJJK3xkR}/_ssgManifest.js +0 -0
|
@@ -85,6 +85,23 @@ interface Point {
|
|
|
85
85
|
y: number;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** #170 tree-view MVP — payload for the org-chart layout. `lines` are the
|
|
89
|
+
* parent→child elbow connectors (raw endpoints; the render branch draws
|
|
90
|
+
* the right-angle path). `synthLabels` are placeholder chips for the
|
|
91
|
+
* synthetic nodes (指挥中心 root / 未分组 bucket) that have no backing
|
|
92
|
+
* session. Empty in ring/grid layouts. */
|
|
93
|
+
interface TreeConnectorLine {
|
|
94
|
+
x1: number;
|
|
95
|
+
y1: number;
|
|
96
|
+
x2: number;
|
|
97
|
+
y2: number;
|
|
98
|
+
}
|
|
99
|
+
interface TreeLayout {
|
|
100
|
+
lines: TreeConnectorLine[];
|
|
101
|
+
synthLabels: { x: number; y: number; label: string }[];
|
|
102
|
+
synthRoot: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
interface FlowLink {
|
|
89
106
|
key: string;
|
|
90
107
|
from: string;
|
|
@@ -607,11 +624,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
607
624
|
// grid arranges nodes in an N×M grid (better for 30+ nodes). Persisted to
|
|
608
625
|
// localStorage like the zoom/pan view state. Declared above nodePositions
|
|
609
626
|
// since that useMemo branches on it.
|
|
610
|
-
const [layout, setLayout] = useState<'ring' | 'grid'>('ring');
|
|
627
|
+
const [layout, setLayout] = useState<'ring' | 'grid' | 'tree'>('ring');
|
|
611
628
|
useEffect(() => {
|
|
612
629
|
try {
|
|
613
630
|
const saved = localStorage.getItem('anet-topo-layout');
|
|
614
|
-
if (saved === 'grid' || saved === 'ring') {
|
|
631
|
+
if (saved === 'grid' || saved === 'ring' || saved === 'tree') {
|
|
615
632
|
setLayout(saved);
|
|
616
633
|
} else if (sessions.length >= 20) {
|
|
617
634
|
// v0.10.0 Hero 3 Wave 1 §3.D — auto-grid for dense fleets.
|
|
@@ -650,11 +667,26 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
650
667
|
// R168's smoothView arming but on a different visual axis
|
|
651
668
|
// (opacity, not transform).
|
|
652
669
|
const [layoutSwitching, setLayoutSwitching] = useState(false);
|
|
670
|
+
// #170 tree-view MVP — layout is now 3-way (ring | grid | tree).
|
|
671
|
+
// selectLayout sets a specific mode (used by the chrome segmented
|
|
672
|
+
// control's 3 buttons); toggleLayout cycles ring → grid → tree → ring
|
|
673
|
+
// (used by the `l` keyboard shortcut). Both arm the R170 crossfade-
|
|
674
|
+
// blink so the node teleport between layouts reads as a soft swap.
|
|
675
|
+
const LAYOUT_CYCLE: ('ring' | 'grid' | 'tree')[] = ['ring', 'grid', 'tree'];
|
|
676
|
+
const selectLayout = (next: 'ring' | 'grid' | 'tree') => {
|
|
677
|
+
setLayoutSwitching(true);
|
|
678
|
+
setTimeout(() => setLayoutSwitching(false), 400);
|
|
679
|
+
setLayout(prev => {
|
|
680
|
+
if (prev === next) return prev;
|
|
681
|
+
try { localStorage.setItem('anet-topo-layout', next); } catch {}
|
|
682
|
+
return next;
|
|
683
|
+
});
|
|
684
|
+
};
|
|
653
685
|
const toggleLayout = () => {
|
|
654
686
|
setLayoutSwitching(true);
|
|
655
687
|
setTimeout(() => setLayoutSwitching(false), 400);
|
|
656
688
|
setLayout(prev => {
|
|
657
|
-
const next = prev
|
|
689
|
+
const next = LAYOUT_CYCLE[(LAYOUT_CYCLE.indexOf(prev) + 1) % LAYOUT_CYCLE.length];
|
|
658
690
|
try { localStorage.setItem('anet-topo-layout', next); } catch {}
|
|
659
691
|
return next;
|
|
660
692
|
});
|
|
@@ -718,6 +750,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
718
750
|
groupKeys,
|
|
719
751
|
groupBoxes,
|
|
720
752
|
gridContentBottom,
|
|
753
|
+
treeConnectors,
|
|
754
|
+
treeContentBox,
|
|
721
755
|
} = useMemo(() => {
|
|
722
756
|
const sseCount = (s: { alias: string; network_id?: string }) =>
|
|
723
757
|
(s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
|
|
@@ -922,6 +956,269 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
922
956
|
groupKeys,
|
|
923
957
|
groupBoxes,
|
|
924
958
|
gridContentBottom,
|
|
959
|
+
treeConnectors: { lines: [], synthLabels: [], synthRoot: false } as TreeLayout,
|
|
960
|
+
treeContentBox: null as { x: number; y: number; w: number; h: number } | null,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (layout === 'tree') {
|
|
965
|
+
// #170 tree-view MVP — classic top-down org chart, derived purely
|
|
966
|
+
// from alias + runtime (zero-config; org.json override is the
|
|
967
|
+
// RFC-017 formal version, out of scope here).
|
|
968
|
+
//
|
|
969
|
+
// layer 0 总指挥 ── synthetic root if none / multiple
|
|
970
|
+
// layer 1 副指挥 ── report to 总指挥
|
|
971
|
+
// layer 2 team leads ── one per computeGroups() team
|
|
972
|
+
// + a 未分组 bucket for orphans
|
|
973
|
+
// layer 3 deputy + members hanging under their team lead
|
|
974
|
+
//
|
|
975
|
+
// The classifier per team: LEAD = runtime claude-code-cli OR alias
|
|
976
|
+
// contains 负责人; DEPUTY = runtime codex-sdk (or runtime string
|
|
977
|
+
// containing "codex"); everyone else = members.
|
|
978
|
+
const all = [...online, ...offline];
|
|
979
|
+
const groupKeys = computeGroups(all);
|
|
980
|
+
const isCommander = (a: string) => a.includes('总指挥');
|
|
981
|
+
const isDeputyCmdr = (a: string) => a.includes('副指挥');
|
|
982
|
+
const runtimeOf = (s: Session) => (s.runtime || '').toLowerCase();
|
|
983
|
+
const isCodexRt = (s: Session) => runtimeOf(s).includes('codex');
|
|
984
|
+
const isCliRt = (s: Session) => runtimeOf(s) === 'claude-code-cli';
|
|
985
|
+
|
|
986
|
+
// commanders are roots, never team members
|
|
987
|
+
const commanders = all.filter(s => isCommander(s.alias));
|
|
988
|
+
const deputyCommanders = all.filter(s => isDeputyCmdr(s.alias) && !isCommander(s.alias));
|
|
989
|
+
const rankAliases = new Set([...commanders, ...deputyCommanders].map(s => s.alias));
|
|
990
|
+
|
|
991
|
+
// teams = computeGroups components, minus the commander aliases.
|
|
992
|
+
// A "real" team is a group key shared by ≥2 non-commander members.
|
|
993
|
+
const teamMembers: Record<string, Session[]> = {};
|
|
994
|
+
const orphans: Session[] = [];
|
|
995
|
+
for (const s of all) {
|
|
996
|
+
if (rankAliases.has(s.alias)) continue;
|
|
997
|
+
const gk = groupKeys[s.alias] ?? s.alias;
|
|
998
|
+
(teamMembers[gk] ||= []).push(s);
|
|
999
|
+
}
|
|
1000
|
+
const realTeams: { key: string; members: Session[] }[] = [];
|
|
1001
|
+
for (const [key, members] of Object.entries(teamMembers)) {
|
|
1002
|
+
if (members.length >= 2) realTeams.push({ key, members });
|
|
1003
|
+
else orphans.push(...members);
|
|
1004
|
+
}
|
|
1005
|
+
realTeams.sort((a, b) => a.key.localeCompare(b.key));
|
|
1006
|
+
|
|
1007
|
+
// within a team: pick the LEAD (claude-code-cli runtime, or 负责人
|
|
1008
|
+
// in the alias forces it), then DEPUTY (codex runtime), rest = members.
|
|
1009
|
+
type TeamTree = { key: string; lead: Session; deputy: Session | null; members: Session[] };
|
|
1010
|
+
const pickLead = (members: Session[]): Session => {
|
|
1011
|
+
const forced = members.find(s => s.alias.includes('负责人'));
|
|
1012
|
+
if (forced) return forced;
|
|
1013
|
+
const cli = members.find(s => isCliRt(s));
|
|
1014
|
+
if (cli) return cli;
|
|
1015
|
+
return [...members].sort((a, b) => a.alias.localeCompare(b.alias))[0];
|
|
1016
|
+
};
|
|
1017
|
+
const teamTrees: TeamTree[] = realTeams.map(({ key, members }) => {
|
|
1018
|
+
const lead = pickLead(members);
|
|
1019
|
+
const rest = members.filter(s => s.alias !== lead.alias);
|
|
1020
|
+
const deputy = rest.find(s => isCodexRt(s)) ?? null;
|
|
1021
|
+
const others = rest
|
|
1022
|
+
.filter(s => !deputy || s.alias !== deputy.alias)
|
|
1023
|
+
.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
1024
|
+
return { key, lead, deputy, members: others };
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// ---- box-based org layout (Vincent #170 feedback) ----------------
|
|
1028
|
+
// Each team is a rectangular BOX (reusing the grid-mode cluster-box
|
|
1029
|
+
// visual via groupBoxes); its members pack into a compact ~√n grid
|
|
1030
|
+
// INSIDE the box rather than one wide row — an 8-person team is a
|
|
1031
|
+
// ~3×3 block, not a 1×8 strip. Boxes sit side-by-side at layer 2;
|
|
1032
|
+
// 副指挥 (layer 1) centre over their contiguous box-run; the root
|
|
1033
|
+
// (layer 0) centres over the 副指挥. Keeps the whole chart compact
|
|
1034
|
+
// and screenshot-friendly even at dozens of nodes.
|
|
1035
|
+
const nodeR = Math.round(26 * nodeScale);
|
|
1036
|
+
const cellW = Math.max(108, 2 * nodeR + 56); // member grid cell — fits a label card
|
|
1037
|
+
const cellH = Math.max(96, 2 * nodeR + 48); // + label drop room below the node
|
|
1038
|
+
const BOX_PAD = 16; // box inner padding
|
|
1039
|
+
const BOX_LABEL = 26; // box top label band
|
|
1040
|
+
const BOX_GAP = 48; // gap between sibling boxes
|
|
1041
|
+
// iter5 (Vincent UX — tree presentation): the two header layers
|
|
1042
|
+
// (总指挥 / 副指挥) hold single small nodes; the iter-2 ROW=116 gap
|
|
1043
|
+
// gave them ~362px of pure header before the first team box. HEAD_ROW
|
|
1044
|
+
// tightens those two layer gaps — still clears node radius + label
|
|
1045
|
+
// drop + connector elbow (needs >72px; 88 leaves a safe margin) but
|
|
1046
|
+
// trims header bloat so the box-layout auto-fit lands at a larger
|
|
1047
|
+
// zoom. The team-box rows below keep their own spacing untouched.
|
|
1048
|
+
const HEAD_ROW = Math.max(88, nodeR + 62); // header layer gap (iter5)
|
|
1049
|
+
const TOP = 130; // y of layer-0 root (clears corner panels)
|
|
1050
|
+
const LEFT = 70;
|
|
1051
|
+
|
|
1052
|
+
// a "box unit" = a team (or the 未分组 bucket) drawn as a grid box.
|
|
1053
|
+
type BoxUnit = {
|
|
1054
|
+
key: string; isOrphan: boolean;
|
|
1055
|
+
members: Session[]; // grid order: lead, deputy, …
|
|
1056
|
+
cols: number; rows: number; w: number; h: number;
|
|
1057
|
+
x: number; y: number; // top-left, filled below
|
|
1058
|
+
};
|
|
1059
|
+
const mkBox = (key: string, isOrphan: boolean, members: Session[]): BoxUnit => {
|
|
1060
|
+
const n = Math.max(1, members.length);
|
|
1061
|
+
const cols = Math.ceil(Math.sqrt(n));
|
|
1062
|
+
const rows = Math.ceil(n / cols);
|
|
1063
|
+
return {
|
|
1064
|
+
key, isOrphan, members, cols, rows,
|
|
1065
|
+
w: cols * cellW + 2 * BOX_PAD,
|
|
1066
|
+
h: rows * cellH + 2 * BOX_PAD + BOX_LABEL,
|
|
1067
|
+
x: 0, y: 0,
|
|
1068
|
+
};
|
|
1069
|
+
};
|
|
1070
|
+
const boxes: BoxUnit[] = teamTrees.map(tt =>
|
|
1071
|
+
mkBox(tt.key, false, [tt.lead, ...(tt.deputy ? [tt.deputy] : []), ...tt.members]));
|
|
1072
|
+
// orphan bucket — every underivable agent in its own 未分组 box so
|
|
1073
|
+
// none are ever dropped from the chart.
|
|
1074
|
+
if (orphans.length > 0) {
|
|
1075
|
+
boxes.push(mkBox('未分组', true,
|
|
1076
|
+
[...orphans].sort((a, b) => a.alias.localeCompare(b.alias))));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// lay boxes into a 2D wrapped grid (Vincent iter 3: team boxes need
|
|
1080
|
+
// not sit in one long row — wrap into rows so the chart stays
|
|
1081
|
+
// compact / screenshot-friendly even with many teams).
|
|
1082
|
+
const layer2Y = TOP + 2 * HEAD_ROW;
|
|
1083
|
+
const BOX_ROW_GAP = 40; // vertical gap between box rows
|
|
1084
|
+
const ROW_W_TARGET = 1240; // wrap once a row would exceed this
|
|
1085
|
+
let rowX = LEFT, rowY = layer2Y, rowMaxH = 0, rowCount = 0;
|
|
1086
|
+
let lastRowBottom = layer2Y;
|
|
1087
|
+
for (const b of boxes) {
|
|
1088
|
+
if (rowCount > 0 && rowX + b.w > LEFT + ROW_W_TARGET) {
|
|
1089
|
+
rowY += rowMaxH + BOX_ROW_GAP; // wrap to next box row
|
|
1090
|
+
rowX = LEFT; rowMaxH = 0; rowCount = 0;
|
|
1091
|
+
}
|
|
1092
|
+
b.x = rowX; b.y = rowY;
|
|
1093
|
+
rowX += b.w + BOX_GAP;
|
|
1094
|
+
rowMaxH = Math.max(rowMaxH, b.h);
|
|
1095
|
+
rowCount++;
|
|
1096
|
+
lastRowBottom = rowY + rowMaxH;
|
|
1097
|
+
}
|
|
1098
|
+
// member positions — compact grid inside each box.
|
|
1099
|
+
for (const b of boxes) {
|
|
1100
|
+
b.members.forEach((s, i) => {
|
|
1101
|
+
const col = i % b.cols;
|
|
1102
|
+
const row = Math.floor(i / b.cols);
|
|
1103
|
+
positions[s.alias] = {
|
|
1104
|
+
x: b.x + BOX_PAD + (col + 0.5) * cellW,
|
|
1105
|
+
y: b.y + BOX_LABEL + BOX_PAD + (row + 0.5) * cellH,
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
const boxCentreX = (b: BoxUnit) => b.x + b.w / 2;
|
|
1110
|
+
|
|
1111
|
+
// layer 1: 副指挥, each centred over a contiguous run of boxes.
|
|
1112
|
+
const layer1Y = TOP + HEAD_ROW;
|
|
1113
|
+
const depNodes: { alias: string; x: number; y: number }[] = [];
|
|
1114
|
+
const depPer = deputyCommanders.length > 0
|
|
1115
|
+
? Math.ceil(Math.max(boxes.length, 1) / deputyCommanders.length)
|
|
1116
|
+
: 0;
|
|
1117
|
+
deputyCommanders.forEach((s, di) => {
|
|
1118
|
+
const run = boxes.slice(di * depPer, di * depPer + depPer);
|
|
1119
|
+
const xs = run.length ? run.map(boxCentreX)
|
|
1120
|
+
: [LEFT + di * (cellW + BOX_GAP) + cellW / 2];
|
|
1121
|
+
const x = (Math.min(...xs) + Math.max(...xs)) / 2;
|
|
1122
|
+
depNodes.push({ alias: s.alias, x, y: layer1Y });
|
|
1123
|
+
positions[s.alias] = { x, y: layer1Y };
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
// layer 0: root — single 总指挥, else a synthetic 指挥中心.
|
|
1127
|
+
const anchorXs = depNodes.length > 0 ? depNodes.map(d => d.x)
|
|
1128
|
+
: boxes.length > 0 ? boxes.map(boxCentreX)
|
|
1129
|
+
: [LEFT + cellW / 2];
|
|
1130
|
+
const rootX = (Math.min(...anchorXs) + Math.max(...anchorXs)) / 2;
|
|
1131
|
+
let synthRoot = false;
|
|
1132
|
+
if (commanders.length === 1) {
|
|
1133
|
+
positions[commanders[0].alias] = { x: rootX, y: TOP };
|
|
1134
|
+
} else {
|
|
1135
|
+
synthRoot = true;
|
|
1136
|
+
// any 总指挥 (0, or >1) spread along layer 0 beside the synth anchor
|
|
1137
|
+
commanders.forEach((c, ci) => {
|
|
1138
|
+
positions[c.alias] = {
|
|
1139
|
+
x: rootX + (ci - (commanders.length - 1) / 2) * (cellW + 28),
|
|
1140
|
+
y: TOP,
|
|
1141
|
+
};
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// elbow connectors: root → 副指挥 → team-box top edge (render draws
|
|
1146
|
+
// the right-angle path behind the nodes).
|
|
1147
|
+
const connectors: TreeConnectorLine[] = [];
|
|
1148
|
+
for (const d of depNodes) {
|
|
1149
|
+
connectors.push({ x1: rootX, y1: TOP, x2: d.x, y2: d.y });
|
|
1150
|
+
}
|
|
1151
|
+
boxes.forEach((b, bi) => {
|
|
1152
|
+
let px = rootX, py = TOP;
|
|
1153
|
+
if (depNodes.length > 0) {
|
|
1154
|
+
const di = Math.min(depNodes.length - 1, Math.floor(bi / Math.max(depPer, 1)));
|
|
1155
|
+
px = depNodes[di].x; py = depNodes[di].y;
|
|
1156
|
+
}
|
|
1157
|
+
connectors.push({ x1: px, y1: py, x2: boxCentreX(b), y2: b.y });
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// synthetic root label (only when there is no single 总指挥)
|
|
1161
|
+
const synthBoxes = synthRoot ? [{ x: rootX, y: TOP, label: '指挥中心' }] : [];
|
|
1162
|
+
|
|
1163
|
+
// team boxes surfaced as groupBoxes — reuses the grid-mode cluster
|
|
1164
|
+
// box render + per-box working/idle/offline pip strip.
|
|
1165
|
+
const teamGroupBoxes = boxes.map(b => {
|
|
1166
|
+
let w = 0, i = 0, o = 0;
|
|
1167
|
+
for (const s of b.members) {
|
|
1168
|
+
const isOn = s.status !== 'offline' || !!sseCount(s);
|
|
1169
|
+
if (s.status === 'working') w++;
|
|
1170
|
+
else if (isOn) i++;
|
|
1171
|
+
else o++;
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
key: b.key, isOrphan: b.isOrphan, count: b.members.length,
|
|
1175
|
+
statuses: { working: w, idle: i, offline: o },
|
|
1176
|
+
x: b.x, y: b.y, w: b.w, h: b.h,
|
|
1177
|
+
};
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// tree mode is a clean org chart (Vincent iter 4): message flow-links
|
|
1181
|
+
// between nodes in different team boxes tangle as long curves across
|
|
1182
|
+
// the hierarchy and bury the structure. Suppress them in tree — the
|
|
1183
|
+
// right-angle report connectors (treeConnectors) carry the org lines.
|
|
1184
|
+
|
|
1185
|
+
// content bottom for auto-fit zoom — bottom of the LAST box row.
|
|
1186
|
+
// Boxes wrap into multiple rows (iter 3), so this must track the
|
|
1187
|
+
// wrapped lastRowBottom, not layer2Y + tallest-box.
|
|
1188
|
+
const treeBottom = boxes.length ? lastRowBottom : TOP + HEAD_ROW;
|
|
1189
|
+
const gridContentBottom = treeBottom + 48;
|
|
1190
|
+
|
|
1191
|
+
// iter5 (Vincent UX): full content bounding box over every team box
|
|
1192
|
+
// + every node. The auto-fit effect uses this to scale the org chart
|
|
1193
|
+
// to fit BOTH viewBox dimensions AND centre it. Pre-iter5 auto-fit
|
|
1194
|
+
// only fit height and left-anchored the chart (LEFT=70), so the tree
|
|
1195
|
+
// hugged the top-left corner with dead canvas down the right side.
|
|
1196
|
+
const treeBX: number[] = [], treeBY: number[] = [];
|
|
1197
|
+
for (const b of boxes) { treeBX.push(b.x, b.x + b.w); treeBY.push(b.y, b.y + b.h); }
|
|
1198
|
+
for (const a of Object.keys(positions)) { treeBX.push(positions[a].x); treeBY.push(positions[a].y); }
|
|
1199
|
+
// TREE_M pads the raw box/node extent so node radii + label drops
|
|
1200
|
+
// (root/副指挥 sit above the boxes) never clip at the fitted edge.
|
|
1201
|
+
const TREE_M = 52;
|
|
1202
|
+
const treeContentBox: { x: number; y: number; w: number; h: number } | null = treeBX.length
|
|
1203
|
+
? {
|
|
1204
|
+
x: Math.min(...treeBX) - TREE_M,
|
|
1205
|
+
y: Math.min(...treeBY) - TREE_M,
|
|
1206
|
+
w: Math.max(...treeBX) - Math.min(...treeBX) + 2 * TREE_M,
|
|
1207
|
+
h: Math.max(...treeBY) - Math.min(...treeBY) + 2 * TREE_M,
|
|
1208
|
+
}
|
|
1209
|
+
: null;
|
|
1210
|
+
|
|
1211
|
+
return {
|
|
1212
|
+
onlineNodes: online,
|
|
1213
|
+
offlineNodes: offline,
|
|
1214
|
+
nodePositions: positions,
|
|
1215
|
+
flowLinks: [],
|
|
1216
|
+
activeAliases: new Set<string>(),
|
|
1217
|
+
groupKeys,
|
|
1218
|
+
groupBoxes: teamGroupBoxes,
|
|
1219
|
+
gridContentBottom,
|
|
1220
|
+
treeConnectors: { lines: connectors, synthLabels: synthBoxes, synthRoot } as TreeLayout,
|
|
1221
|
+
treeContentBox,
|
|
925
1222
|
};
|
|
926
1223
|
}
|
|
927
1224
|
|
|
@@ -1006,6 +1303,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1006
1303
|
groupBoxes: [] as { key: string; isOrphan?: boolean; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
|
|
1007
1304
|
// ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
|
|
1008
1305
|
gridContentBottom: 0,
|
|
1306
|
+
treeConnectors: { lines: [], synthLabels: [], synthRoot: false } as TreeLayout,
|
|
1307
|
+
treeContentBox: null as { x: number; y: number; w: number; h: number } | null,
|
|
1009
1308
|
};
|
|
1010
1309
|
}, [messages, sessions, sseSessions, layout, nodeScale]);
|
|
1011
1310
|
|
|
@@ -1026,7 +1325,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1026
1325
|
const vendorDist = useMemo(() => {
|
|
1027
1326
|
const tally = new Map<string, { initial: string; count: number; color: string }>();
|
|
1028
1327
|
for (const s of [...onlineNodes, ...offlineNodes]) {
|
|
1029
|
-
const v = vendorForModel(s.model);
|
|
1328
|
+
const v = vendorForModel(s.model, s.runtime);
|
|
1030
1329
|
const key = v.id === 'unknown' ? '?' : v.initial;
|
|
1031
1330
|
const cur = tally.get(key);
|
|
1032
1331
|
if (cur) cur.count++;
|
|
@@ -1175,7 +1474,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1175
1474
|
// a single-axis paint lift. Post-R674 inline filter applies the
|
|
1176
1475
|
// same 2+4 stride at pal.legendAccent tint as R667/R668 chrome-
|
|
1177
1476
|
// control siblings.
|
|
1178
|
-
const [hoveredLayout, setHoveredLayout] = useState<'ring' | 'grid' | null>(null);
|
|
1477
|
+
const [hoveredLayout, setHoveredLayout] = useState<'ring' | 'grid' | 'tree' | null>(null);
|
|
1179
1478
|
// R675 — hover state for the chrome nodeSize S/M/L segmented trio.
|
|
1180
1479
|
// Value-typed (single state covers all three buttons) drives the
|
|
1181
1480
|
// multi-layer halo filter completing the chrome strip's nodeSize
|
|
@@ -1328,7 +1627,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1328
1627
|
// {zoom,x,y} persists to localStorage (same sticky pattern as brand flag).
|
|
1329
1628
|
const VIEWBOX_W = 1000;
|
|
1330
1629
|
const VIEWBOX_H = 680;
|
|
1331
|
-
|
|
1630
|
+
// tree mode can render a tall multi-row org chart — it needs a lower
|
|
1631
|
+
// zoom-out floor than ring/grid so the whole tree can fit (Vincent iter
|
|
1632
|
+
// 3: "缩放最多 50%" couldn't shrink the tree enough).
|
|
1633
|
+
const ZOOM_MIN = layout === 'tree' ? 0.3 : 0.5;
|
|
1332
1634
|
const ZOOM_MAX = 4;
|
|
1333
1635
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
1334
1636
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
@@ -1368,7 +1670,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1368
1670
|
// keeps its own R184 rotation animation (different gesture, semantic).
|
|
1369
1671
|
// Node-size S/M/L keep their R171 layoutSwitching crossfade (already
|
|
1370
1672
|
// gestural). Type union grows but the helper signature stays one-arg.
|
|
1371
|
-
type ChromePop = 'zoom-in' | 'zoom-out' | 'layout-ring' | 'layout-grid' | 'fullscreen' | 'size-S' | 'size-M' | 'size-L';
|
|
1673
|
+
type ChromePop = 'zoom-in' | 'zoom-out' | 'layout-ring' | 'layout-grid' | 'layout-tree' | 'fullscreen' | 'size-S' | 'size-M' | 'size-L';
|
|
1372
1674
|
const [chromePopping, setChromePopping] = useState<ChromePop | null>(null);
|
|
1373
1675
|
const popChrome = (which: ChromePop) => {
|
|
1374
1676
|
setChromePopping(which);
|
|
@@ -1442,7 +1744,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1442
1744
|
autoFitDoneRef.current = true;
|
|
1443
1745
|
return;
|
|
1444
1746
|
}
|
|
1445
|
-
if (
|
|
1747
|
+
if (sessions.length === 0) return;
|
|
1748
|
+
// iter5 (Vincent UX): tree auto-fit scales the org chart to fit BOTH
|
|
1749
|
+
// viewBox dimensions and CENTRES it. Pre-iter5 the shared grid path
|
|
1750
|
+
// only fit height and left-anchored the chart, so the tree hugged the
|
|
1751
|
+
// top-left corner with dead canvas down the right side.
|
|
1752
|
+
if (layout === 'tree') {
|
|
1753
|
+
if (!treeContentBox) return;
|
|
1754
|
+
const b = treeContentBox;
|
|
1755
|
+
if (b.w <= 0 || b.h <= 0) { autoFitDoneRef.current = true; return; }
|
|
1756
|
+
const pad = 24;
|
|
1757
|
+
const z = Math.max(ZOOM_MIN, Math.min(
|
|
1758
|
+
1, (VIEWBOX_W - 2 * pad) / b.w, (VIEWBOX_H - 2 * pad) / b.h));
|
|
1759
|
+
setView({
|
|
1760
|
+
zoom: z,
|
|
1761
|
+
x: VIEWBOX_W / 2 - (b.x + b.w / 2) * z,
|
|
1762
|
+
y: VIEWBOX_H / 2 - (b.y + b.h / 2) * z,
|
|
1763
|
+
});
|
|
1764
|
+
autoFitDoneRef.current = true;
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (layout !== 'grid' || !gridContentBottom) return;
|
|
1446
1768
|
if (gridContentBottom <= VIEWBOX_H) {
|
|
1447
1769
|
autoFitDoneRef.current = true; // no overflow → no fit needed
|
|
1448
1770
|
return;
|
|
@@ -1450,7 +1772,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1450
1772
|
const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
|
|
1451
1773
|
setView({ zoom: fitZoom, x: 0, y: 0 });
|
|
1452
1774
|
autoFitDoneRef.current = true;
|
|
1453
|
-
}, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);
|
|
1775
|
+
}, [layout, sessions.length, gridContentBottom, treeContentBox, hadPersistedViewOnMount]);
|
|
1454
1776
|
|
|
1455
1777
|
// persist view
|
|
1456
1778
|
useEffect(() => {
|
|
@@ -1594,13 +1916,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1594
1916
|
// when the operator invokes fit-to-content via the hub click (R52),
|
|
1595
1917
|
// chrome button, `f` key, or palette command.
|
|
1596
1918
|
const fitView = useCallback(() => {
|
|
1919
|
+
setSmoothView(true);
|
|
1920
|
+
setTimeout(() => setSmoothView(false), 350);
|
|
1921
|
+
// iter5: tree fits-and-centres via its content bbox (sister to the
|
|
1922
|
+
// auto-fit effect); grid/ring keep the height-only fit-to-content.
|
|
1923
|
+
if (layout === 'tree' && treeContentBox && treeContentBox.w > 0 && treeContentBox.h > 0) {
|
|
1924
|
+
const b = treeContentBox;
|
|
1925
|
+
const pad = 24;
|
|
1926
|
+
const z = Math.max(ZOOM_MIN, Math.min(
|
|
1927
|
+
1, (VIEWBOX_W - 2 * pad) / b.w, (VIEWBOX_H - 2 * pad) / b.h));
|
|
1928
|
+
setView({
|
|
1929
|
+
zoom: z,
|
|
1930
|
+
x: VIEWBOX_W / 2 - (b.x + b.w / 2) * z,
|
|
1931
|
+
y: VIEWBOX_H / 2 - (b.y + b.h / 2) * z,
|
|
1932
|
+
});
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1597
1935
|
const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H
|
|
1598
1936
|
? 1
|
|
1599
1937
|
: Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
|
|
1600
|
-
setSmoothView(true);
|
|
1601
|
-
setTimeout(() => setSmoothView(false), 350);
|
|
1602
1938
|
setView({ zoom, x: 0, y: 0 });
|
|
1603
|
-
}, [gridContentBottom]);
|
|
1939
|
+
}, [gridContentBottom, layout, treeContentBox]);
|
|
1604
1940
|
|
|
1605
1941
|
// R74: listen for layout + view palette commands. Sister to R69's
|
|
1606
1942
|
// pin listener — palette dispatches a CustomEvent, the reducer here
|
|
@@ -2394,11 +2730,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
2394
2730
|
data-topo-chrome-wrapper-halo-family="layout"
|
|
2395
2731
|
>
|
|
2396
2732
|
<button
|
|
2397
|
-
onClick={() => { popChrome('layout-ring');
|
|
2733
|
+
onClick={() => { popChrome('layout-ring'); selectLayout('ring'); }}
|
|
2398
2734
|
onMouseEnter={() => setHoveredLayout('ring')}
|
|
2399
2735
|
onMouseLeave={() => setHoveredLayout((prev) => prev === 'ring' ? null : prev)}
|
|
2400
2736
|
aria-pressed={layout === 'ring'}
|
|
2401
|
-
title="Ring layout (l to
|
|
2737
|
+
title="Ring layout (l to cycle)"
|
|
2402
2738
|
data-topo-chrome-layout="ring"
|
|
2403
2739
|
data-topo-chrome-layout-active={layout === 'ring' ? 'true' : 'false'}
|
|
2404
2740
|
data-topo-chrome-layout-ring-popping={chromePopping === 'layout-ring' ? 'true' : 'false'}
|
|
@@ -2533,11 +2869,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
2533
2869
|
Ring
|
|
2534
2870
|
</button>
|
|
2535
2871
|
<button
|
|
2536
|
-
onClick={() => { popChrome('layout-grid');
|
|
2872
|
+
onClick={() => { popChrome('layout-grid'); selectLayout('grid'); }}
|
|
2537
2873
|
onMouseEnter={() => setHoveredLayout('grid')}
|
|
2538
2874
|
onMouseLeave={() => setHoveredLayout((prev) => prev === 'grid' ? null : prev)}
|
|
2539
2875
|
aria-pressed={layout === 'grid'}
|
|
2540
|
-
title="Grid layout (l to
|
|
2876
|
+
title="Grid layout (l to cycle)"
|
|
2541
2877
|
data-topo-chrome-layout="grid"
|
|
2542
2878
|
data-topo-chrome-layout-active={layout === 'grid' ? 'true' : 'false'}
|
|
2543
2879
|
data-topo-chrome-layout-grid-popping={chromePopping === 'layout-grid' ? 'true' : 'false'}
|
|
@@ -2585,6 +2921,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
2585
2921
|
>
|
|
2586
2922
|
Grid
|
|
2587
2923
|
</button>
|
|
2924
|
+
{/* #170 tree-view MVP — third segment in the layout control.
|
|
2925
|
+
Org-chart view: 总指挥/副指挥 root → teams → members.
|
|
2926
|
+
Mirrors the Grid button's border-l divider + active/
|
|
2927
|
+
inactive paint vocabulary; the heavier R-series polish
|
|
2928
|
+
(halo layers, hover-fw preview attrs) is deferred to the
|
|
2929
|
+
RFC-017 formal version. */}
|
|
2930
|
+
<button
|
|
2931
|
+
onClick={() => { popChrome('layout-tree'); selectLayout('tree'); }}
|
|
2932
|
+
onMouseEnter={() => setHoveredLayout('tree')}
|
|
2933
|
+
onMouseLeave={() => setHoveredLayout((prev) => prev === 'tree' ? null : prev)}
|
|
2934
|
+
aria-pressed={layout === 'tree'}
|
|
2935
|
+
title="Tree layout — org chart (l to cycle)"
|
|
2936
|
+
data-topo-chrome-layout="tree"
|
|
2937
|
+
data-topo-chrome-layout-active={layout === 'tree' ? 'true' : 'false'}
|
|
2938
|
+
data-topo-chrome-layout-tree-popping={chromePopping === 'layout-tree' ? 'true' : 'false'}
|
|
2939
|
+
className={`px-2.5 py-1 border-l focus:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60 focus-visible:ring-inset hover:tracking-wide hover:brightness-[1.15] active:scale-95 transform-gpu ${layout === 'tree' ? 'bg-cyan-500/15 text-cyan-300 font-medium hover:bg-cyan-500/20 hover:text-cyan-200 active:bg-cyan-500/25' : 'text-gray-400 hover:text-cyan-300 hover:bg-cyan-500/5 active:bg-cyan-500/15 hover:font-medium'} ${chromePopping === 'layout-tree' ? ' anet-chrome-pop' : ''}`}
|
|
2940
|
+
style={{
|
|
2941
|
+
borderColor: pal.containerBorder,
|
|
2942
|
+
transition: 'background-color 150ms ease, color 150ms ease, border-color 200ms ease-out, letter-spacing 200ms ease-out, transform 150ms ease-out, font-weight 150ms ease, filter 150ms ease',
|
|
2943
|
+
filter: hoveredLayout === 'tree' ? `drop-shadow(0 0 2px ${pal.legendAccent}80) drop-shadow(0 0 4px ${pal.legendAccent}40) brightness(1.15)` : undefined,
|
|
2944
|
+
}}
|
|
2945
|
+
>
|
|
2946
|
+
Tree
|
|
2947
|
+
</button>
|
|
2588
2948
|
</div>
|
|
2589
2949
|
{/* R79: working + online count chips become hover affordances —
|
|
2590
2950
|
extends R77's chip-hover pattern to status counts. Hover the
|
|
@@ -3506,7 +3866,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
3506
3866
|
// unknowns folded to '?').
|
|
3507
3867
|
const matchAliases = [...onlineNodes, ...offlineNodes]
|
|
3508
3868
|
.filter(s => {
|
|
3509
|
-
const v = vendorForModel(s.model);
|
|
3869
|
+
const v = vendorForModel(s.model, s.runtime);
|
|
3510
3870
|
return (v.id === 'unknown' ? '?' : v.initial) === pinnedVendor;
|
|
3511
3871
|
})
|
|
3512
3872
|
.map(s => s.alias);
|
|
@@ -3776,7 +4136,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
3776
4136
|
if (gk !== pinnedGroup) return false;
|
|
3777
4137
|
}
|
|
3778
4138
|
if (pinnedVendor) {
|
|
3779
|
-
const v = vendorForModel(s.model);
|
|
4139
|
+
const v = vendorForModel(s.model, s.runtime);
|
|
3780
4140
|
const initial = v.id === 'unknown' ? '?' : v.initial;
|
|
3781
4141
|
if (initial !== pinnedVendor) return false;
|
|
3782
4142
|
}
|
|
@@ -3974,7 +4334,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
3974
4334
|
// UI shows "A:3" should hover-explain which 3.
|
|
3975
4335
|
const aliases = [...onlineNodes, ...offlineNodes]
|
|
3976
4336
|
.filter(s => {
|
|
3977
|
-
const vid = vendorForModel(s.model);
|
|
4337
|
+
const vid = vendorForModel(s.model, s.runtime);
|
|
3978
4338
|
return (vid.id === 'unknown' ? '?' : vid.initial) === v.initial;
|
|
3979
4339
|
})
|
|
3980
4340
|
.map(s => s.alias);
|
|
@@ -6400,6 +6760,70 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
6400
6760
|
});
|
|
6401
6761
|
})()}
|
|
6402
6762
|
|
|
6763
|
+
{/* #170 tree-view MVP — org-chart elbow connectors. Tree layout
|
|
6764
|
+
only (treeConnectors.lines is empty in ring/grid). Rendered
|
|
6765
|
+
here, behind the nodes + labels, as right-angle polylines:
|
|
6766
|
+
vertical drop from the parent, horizontal at the mid-y, then
|
|
6767
|
+
vertical into the child. pointer-events off so they never
|
|
6768
|
+
intercept a node click. The synthetic-node chips (指挥中心
|
|
6769
|
+
root / 未分组 bucket) render here too — they have no backing
|
|
6770
|
+
session so the node-map below skips them. */}
|
|
6771
|
+
{layout === 'tree' && treeConnectors.lines.map((ln, i) => {
|
|
6772
|
+
const midY = (ln.y1 + ln.y2) / 2;
|
|
6773
|
+
// drop from just below the parent ring to just above the child
|
|
6774
|
+
// ring — keeps the connector clear of the avatars themselves.
|
|
6775
|
+
const startY = ln.y1;
|
|
6776
|
+
const endY = ln.y2;
|
|
6777
|
+
const d = `M ${ln.x1} ${startY} L ${ln.x1} ${midY} L ${ln.x2} ${midY} L ${ln.x2} ${endY}`;
|
|
6778
|
+
return (
|
|
6779
|
+
<path
|
|
6780
|
+
key={`tree-conn-${i}`}
|
|
6781
|
+
d={d}
|
|
6782
|
+
fill="none"
|
|
6783
|
+
stroke={pal.containerBorder}
|
|
6784
|
+
strokeWidth={1.5}
|
|
6785
|
+
strokeLinejoin="round"
|
|
6786
|
+
strokeLinecap="round"
|
|
6787
|
+
opacity={0.55}
|
|
6788
|
+
style={{ pointerEvents: 'none' }}
|
|
6789
|
+
/>
|
|
6790
|
+
);
|
|
6791
|
+
})}
|
|
6792
|
+
{layout === 'tree' && treeConnectors.synthLabels.map((s, i) => {
|
|
6793
|
+
// synthetic node chip: a small pill with a centred label, drawn
|
|
6794
|
+
// at the synthetic node's slot. No status ring → never counted
|
|
6795
|
+
// by the node↔node overlap test; sized to clear neighbours.
|
|
6796
|
+
const chipW = Math.max(58, s.label.length * 13 + 20);
|
|
6797
|
+
const chipH = 26;
|
|
6798
|
+
return (
|
|
6799
|
+
<g key={`tree-synth-${i}`} style={{ pointerEvents: 'none' }} data-topo-tree-synth={s.label}>
|
|
6800
|
+
<rect
|
|
6801
|
+
x={s.x - chipW / 2}
|
|
6802
|
+
y={s.y - chipH / 2}
|
|
6803
|
+
width={chipW}
|
|
6804
|
+
height={chipH}
|
|
6805
|
+
rx={13}
|
|
6806
|
+
fill={pal.containerBg}
|
|
6807
|
+
stroke={pal.containerBorder}
|
|
6808
|
+
strokeWidth={1.25}
|
|
6809
|
+
strokeDasharray="3 3"
|
|
6810
|
+
opacity={0.92}
|
|
6811
|
+
/>
|
|
6812
|
+
<text
|
|
6813
|
+
x={s.x}
|
|
6814
|
+
y={s.y}
|
|
6815
|
+
textAnchor="middle"
|
|
6816
|
+
dominantBaseline="central"
|
|
6817
|
+
fontSize={12}
|
|
6818
|
+
fontWeight={600}
|
|
6819
|
+
fill={pal.legendAccent}
|
|
6820
|
+
>
|
|
6821
|
+
{s.label}
|
|
6822
|
+
</text>
|
|
6823
|
+
</g>
|
|
6824
|
+
);
|
|
6825
|
+
})}
|
|
6826
|
+
|
|
6403
6827
|
{/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout
|
|
6404
6828
|
only — groupBoxes is empty in ring mode. Rendered behind the
|
|
6405
6829
|
flow links + nodes; pointer-events off so they never intercept
|
|
@@ -10105,7 +10529,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
10105
10529
|
opacity: hoveredEdgeEndpoints && !hoveredEdgeEndpoints.has(session.alias) && chatAlias !== session.alias
|
|
10106
10530
|
? 0.28
|
|
10107
10531
|
: activeVendor && chatAlias !== session.alias && (() => {
|
|
10108
|
-
const v = vendorForModel(session.model);
|
|
10532
|
+
const v = vendorForModel(session.model, session.runtime);
|
|
10109
10533
|
const initial = v.id === 'unknown' ? '?' : v.initial;
|
|
10110
10534
|
return initial !== activeVendor;
|
|
10111
10535
|
})()
|
|
@@ -11370,7 +11794,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
11370
11794
|
{(() => {
|
|
11371
11795
|
const ar = Math.round((isOnline ? 14 : 10) * nodeScale);
|
|
11372
11796
|
const size = radius * 2;
|
|
11373
|
-
const vendor = vendorForModel(session.model);
|
|
11797
|
+
const vendor = vendorForModel(session.model, session.runtime);
|
|
11374
11798
|
const internByAlias = /书生|书小生|intern/i.test(session.alias);
|
|
11375
11799
|
|
|
11376
11800
|
if (isIntern || internByAlias || vendor.logo) {
|
|
@@ -12624,7 +13048,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
12624
13048
|
gate as showFullLabel; dense fleets already have
|
|
12625
13049
|
too much per-node chrome competing. */}
|
|
12626
13050
|
{!reducedMotion && hoveredAlias === session.alias && !denseLayout && (() => {
|
|
12627
|
-
const v = vendorForModel(session.model);
|
|
13051
|
+
const v = vendorForModel(session.model, session.runtime);
|
|
12628
13052
|
const rt = runtimeIdentity(session.runtime);
|
|
12629
13053
|
const flipLeft = pos.x > VIEWBOX_W * 0.65;
|
|
12630
13054
|
const detailW = 192;
|