@sleep2agi/agent-network-dashboard 0.5.3-preview.260 → 0.5.3-preview.262
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/{0u8v68gl7g6j1.js → 01~4zvq_o_k32.js} +1 -1
- package/.next/static/chunks/{0p142v5va508~.js → 070._s.6bq1dx.js} +1 -1
- package/.next/static/chunks/0vuc69a~-fb7u.js +4 -0
- package/.next/static/chunks/{11sahbo6ikg8g.js → 0~~5hvnaek6hz.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/TopoGraph.tsx +396 -22
- package/app/lib/vendorIdentity.ts +74 -56
- package/package.json +1 -1
- 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 → uxzbzXUe6BEA30oLCqiD_}/_buildManifest.js +0 -0
- /package/.next/static/{wz1T-LhLDalz691PpN3E7 → uxzbzXUe6BEA30oLCqiD_}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{wz1T-LhLDalz691PpN3E7 → uxzbzXUe6BEA30oLCqiD_}/_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,219 @@ 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
|
+
// ---- swimlane org layout (Vincent dispatch: "一行一个团队,成员
|
|
1028
|
+
// 不换行,最左边放总指挥和副指挥") ------------------------------------
|
|
1029
|
+
// Each team is ONE horizontal lane — a wide box whose members sit in
|
|
1030
|
+
// a single NON-WRAPPING row. Lanes stack top-to-bottom (one team per
|
|
1031
|
+
// row). The command layer (总指挥 + 副指挥) is pinned in its own box
|
|
1032
|
+
// down the left edge. Reverses the iter1–3 2D box-grid; the iter5
|
|
1033
|
+
// treeContentBox auto-fit still scales + centres the whole chart.
|
|
1034
|
+
const nodeR = Math.round(26 * nodeScale);
|
|
1035
|
+
const cellW = Math.max(118, 2 * nodeR + 66); // member slot — node + dense label
|
|
1036
|
+
const BOX_LABEL = 26; // box top label band
|
|
1037
|
+
const LANE_BODY = Math.max(82, 2 * nodeR + 36);// member-row band height
|
|
1038
|
+
const LANE_H = BOX_LABEL + LANE_BODY; // full lane box height
|
|
1039
|
+
const LANE_GAP = 16; // vertical gap between lanes
|
|
1040
|
+
const BOX_PAD = 14; // lane inner side padding
|
|
1041
|
+
const CMD_W = Math.max(150, 2 * nodeR + 100); // command column width
|
|
1042
|
+
const CMD_GAP = 44; // gap: command column → lanes
|
|
1043
|
+
const CMD_SLOT = Math.max(116, 2 * nodeR + 64);// command node vertical pitch
|
|
1044
|
+
const TOP = 92;
|
|
1045
|
+
const LEFT = 44;
|
|
1046
|
+
const laneX0 = LEFT + CMD_W + CMD_GAP; // x where team lanes start
|
|
1047
|
+
|
|
1048
|
+
// ordered lanes: one per team (teamTrees is already alias-sorted) +
|
|
1049
|
+
// a trailing 未分组 lane collecting every orphan so none are dropped.
|
|
1050
|
+
type Lane = { key: string; isOrphan: boolean; members: Session[] };
|
|
1051
|
+
const lanes: Lane[] = teamTrees.map(tt => ({
|
|
1052
|
+
key: tt.key, isOrphan: false,
|
|
1053
|
+
members: [tt.lead, ...(tt.deputy ? [tt.deputy] : []), ...tt.members],
|
|
1054
|
+
}));
|
|
1055
|
+
if (orphans.length > 0) {
|
|
1056
|
+
lanes.push({
|
|
1057
|
+
key: '未分组', isOrphan: true,
|
|
1058
|
+
members: [...orphans].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// each lane = a wide box; its members in a single row inside it.
|
|
1063
|
+
const laneStep = LANE_H + LANE_GAP;
|
|
1064
|
+
const laneBoxes = lanes.map((lane, i) => {
|
|
1065
|
+
const y = TOP + i * laneStep;
|
|
1066
|
+
const w = 2 * BOX_PAD + Math.max(1, lane.members.length) * cellW;
|
|
1067
|
+
lane.members.forEach((s, j) => {
|
|
1068
|
+
positions[s.alias] = {
|
|
1069
|
+
x: laneX0 + BOX_PAD + (j + 0.5) * cellW,
|
|
1070
|
+
y: y + BOX_LABEL + LANE_BODY / 2,
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
1073
|
+
return { lane, x: laneX0, y, w, h: LANE_H };
|
|
1074
|
+
});
|
|
1075
|
+
const laneBlockH = lanes.length ? lanes.length * laneStep - LANE_GAP : LANE_H;
|
|
1076
|
+
const laneBlockBottom = TOP + laneBlockH;
|
|
1077
|
+
|
|
1078
|
+
// command column — 总指挥 + 副指挥 stacked in a compact box pinned to
|
|
1079
|
+
// the far left, vertically centred against the lane block.
|
|
1080
|
+
const cmdMembers: Session[] = [...commanders, ...deputyCommanders];
|
|
1081
|
+
const cmdX = LEFT + CMD_W / 2;
|
|
1082
|
+
const cmdH = cmdMembers.length > 0 ? BOX_LABEL + cmdMembers.length * CMD_SLOT : 0;
|
|
1083
|
+
const cmdBox = cmdMembers.length > 0
|
|
1084
|
+
? { x: LEFT, y: TOP + Math.max(0, (laneBlockH - cmdH) / 2), w: CMD_W, h: cmdH }
|
|
1085
|
+
: null;
|
|
1086
|
+
if (cmdBox) {
|
|
1087
|
+
cmdMembers.forEach((s, i) => {
|
|
1088
|
+
positions[s.alias] = {
|
|
1089
|
+
x: cmdX,
|
|
1090
|
+
y: cmdBox.y + BOX_LABEL + (i + 0.5) * CMD_SLOT,
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// org link: a light connector from the command column to each lane.
|
|
1096
|
+
const connectors: TreeConnectorLine[] = [];
|
|
1097
|
+
if (cmdBox) {
|
|
1098
|
+
const cmdRight = cmdBox.x + cmdBox.w;
|
|
1099
|
+
const cmdMidY = cmdBox.y + cmdBox.h / 2;
|
|
1100
|
+
for (const lb of laneBoxes) {
|
|
1101
|
+
connectors.push({ x1: cmdRight, y1: cmdMidY, x2: lb.x, y2: lb.y + LANE_H / 2 });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// swimlane has no synthetic root chip — the command box (labelled
|
|
1106
|
+
// 指挥中心 when there is no single 总指挥) is the command layer.
|
|
1107
|
+
const synthRoot = false;
|
|
1108
|
+
const synthBoxes: { x: number; y: number; label: string }[] = [];
|
|
1109
|
+
|
|
1110
|
+
// lanes + command column surfaced as groupBoxes — reuses the cluster
|
|
1111
|
+
// box render + per-box working/idle/offline pip strip.
|
|
1112
|
+
const tally = (members: Session[]) => {
|
|
1113
|
+
let w = 0, i = 0, o = 0;
|
|
1114
|
+
for (const s of members) {
|
|
1115
|
+
const isOn = s.status !== 'offline' || !!sseCount(s);
|
|
1116
|
+
if (s.status === 'working') w++;
|
|
1117
|
+
else if (isOn) i++;
|
|
1118
|
+
else o++;
|
|
1119
|
+
}
|
|
1120
|
+
return { working: w, idle: i, offline: o };
|
|
1121
|
+
};
|
|
1122
|
+
const teamGroupBoxes = [
|
|
1123
|
+
...(cmdBox ? [{
|
|
1124
|
+
key: commanders.length === 1 ? commanders[0].alias : '指挥中心',
|
|
1125
|
+
isOrphan: false, count: cmdMembers.length,
|
|
1126
|
+
statuses: tally(cmdMembers),
|
|
1127
|
+
x: cmdBox.x, y: cmdBox.y, w: cmdBox.w, h: cmdBox.h,
|
|
1128
|
+
}] : []),
|
|
1129
|
+
...laneBoxes.map(lb => ({
|
|
1130
|
+
key: lb.lane.key, isOrphan: lb.lane.isOrphan,
|
|
1131
|
+
count: lb.lane.members.length,
|
|
1132
|
+
statuses: tally(lb.lane.members),
|
|
1133
|
+
x: lb.x, y: lb.y, w: lb.w, h: lb.h,
|
|
1134
|
+
})),
|
|
1135
|
+
];
|
|
1136
|
+
|
|
1137
|
+
// content bottom for auto-fit — bottom of the lowest box.
|
|
1138
|
+
const treeBottom = Math.max(
|
|
1139
|
+
laneBlockBottom,
|
|
1140
|
+
cmdBox ? cmdBox.y + cmdBox.h : TOP + LANE_H,
|
|
1141
|
+
);
|
|
1142
|
+
const gridContentBottom = treeBottom + 48;
|
|
1143
|
+
|
|
1144
|
+
// iter5 (Vincent UX): full content bounding box → the auto-fit effect
|
|
1145
|
+
// scales the chart to fit BOTH viewBox dimensions AND centres it.
|
|
1146
|
+
const treeBX: number[] = [], treeBY: number[] = [];
|
|
1147
|
+
for (const gb of teamGroupBoxes) { treeBX.push(gb.x, gb.x + gb.w); treeBY.push(gb.y, gb.y + gb.h); }
|
|
1148
|
+
for (const a of Object.keys(positions)) { treeBX.push(positions[a].x); treeBY.push(positions[a].y); }
|
|
1149
|
+
// TREE_M pads the raw box/node extent so node radii + label drops
|
|
1150
|
+
// never clip at the fitted edge.
|
|
1151
|
+
const TREE_M = 52;
|
|
1152
|
+
const treeContentBox: { x: number; y: number; w: number; h: number } | null = treeBX.length
|
|
1153
|
+
? {
|
|
1154
|
+
x: Math.min(...treeBX) - TREE_M,
|
|
1155
|
+
y: Math.min(...treeBY) - TREE_M,
|
|
1156
|
+
w: Math.max(...treeBX) - Math.min(...treeBX) + 2 * TREE_M,
|
|
1157
|
+
h: Math.max(...treeBY) - Math.min(...treeBY) + 2 * TREE_M,
|
|
1158
|
+
}
|
|
1159
|
+
: null;
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
onlineNodes: online,
|
|
1163
|
+
offlineNodes: offline,
|
|
1164
|
+
nodePositions: positions,
|
|
1165
|
+
flowLinks: [],
|
|
1166
|
+
activeAliases: new Set<string>(),
|
|
1167
|
+
groupKeys,
|
|
1168
|
+
groupBoxes: teamGroupBoxes,
|
|
1169
|
+
gridContentBottom,
|
|
1170
|
+
treeConnectors: { lines: connectors, synthLabels: synthBoxes, synthRoot } as TreeLayout,
|
|
1171
|
+
treeContentBox,
|
|
925
1172
|
};
|
|
926
1173
|
}
|
|
927
1174
|
|
|
@@ -1006,6 +1253,8 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1006
1253
|
groupBoxes: [] as { key: string; isOrphan?: boolean; count: number; statuses: { working: number; idle: number; offline: number }; x: number; y: number; w: number; h: number }[],
|
|
1007
1254
|
// ring fits within VIEWBOX_H by construction (offlineRadius=325 + centre at y=330)
|
|
1008
1255
|
gridContentBottom: 0,
|
|
1256
|
+
treeConnectors: { lines: [], synthLabels: [], synthRoot: false } as TreeLayout,
|
|
1257
|
+
treeContentBox: null as { x: number; y: number; w: number; h: number } | null,
|
|
1009
1258
|
};
|
|
1010
1259
|
}, [messages, sessions, sseSessions, layout, nodeScale]);
|
|
1011
1260
|
|
|
@@ -1026,7 +1275,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1026
1275
|
const vendorDist = useMemo(() => {
|
|
1027
1276
|
const tally = new Map<string, { initial: string; count: number; color: string }>();
|
|
1028
1277
|
for (const s of [...onlineNodes, ...offlineNodes]) {
|
|
1029
|
-
const v = vendorForModel(s.model);
|
|
1278
|
+
const v = vendorForModel(s.model, s.runtime);
|
|
1030
1279
|
const key = v.id === 'unknown' ? '?' : v.initial;
|
|
1031
1280
|
const cur = tally.get(key);
|
|
1032
1281
|
if (cur) cur.count++;
|
|
@@ -1175,7 +1424,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1175
1424
|
// a single-axis paint lift. Post-R674 inline filter applies the
|
|
1176
1425
|
// same 2+4 stride at pal.legendAccent tint as R667/R668 chrome-
|
|
1177
1426
|
// control siblings.
|
|
1178
|
-
const [hoveredLayout, setHoveredLayout] = useState<'ring' | 'grid' | null>(null);
|
|
1427
|
+
const [hoveredLayout, setHoveredLayout] = useState<'ring' | 'grid' | 'tree' | null>(null);
|
|
1179
1428
|
// R675 — hover state for the chrome nodeSize S/M/L segmented trio.
|
|
1180
1429
|
// Value-typed (single state covers all three buttons) drives the
|
|
1181
1430
|
// multi-layer halo filter completing the chrome strip's nodeSize
|
|
@@ -1328,7 +1577,10 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1328
1577
|
// {zoom,x,y} persists to localStorage (same sticky pattern as brand flag).
|
|
1329
1578
|
const VIEWBOX_W = 1000;
|
|
1330
1579
|
const VIEWBOX_H = 680;
|
|
1331
|
-
|
|
1580
|
+
// tree mode can render a tall multi-row org chart — it needs a lower
|
|
1581
|
+
// zoom-out floor than ring/grid so the whole tree can fit (Vincent iter
|
|
1582
|
+
// 3: "缩放最多 50%" couldn't shrink the tree enough).
|
|
1583
|
+
const ZOOM_MIN = layout === 'tree' ? 0.3 : 0.5;
|
|
1332
1584
|
const ZOOM_MAX = 4;
|
|
1333
1585
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
1334
1586
|
const svgRef = useRef<SVGSVGElement>(null);
|
|
@@ -1368,7 +1620,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1368
1620
|
// keeps its own R184 rotation animation (different gesture, semantic).
|
|
1369
1621
|
// Node-size S/M/L keep their R171 layoutSwitching crossfade (already
|
|
1370
1622
|
// 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';
|
|
1623
|
+
type ChromePop = 'zoom-in' | 'zoom-out' | 'layout-ring' | 'layout-grid' | 'layout-tree' | 'fullscreen' | 'size-S' | 'size-M' | 'size-L';
|
|
1372
1624
|
const [chromePopping, setChromePopping] = useState<ChromePop | null>(null);
|
|
1373
1625
|
const popChrome = (which: ChromePop) => {
|
|
1374
1626
|
setChromePopping(which);
|
|
@@ -1442,7 +1694,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1442
1694
|
autoFitDoneRef.current = true;
|
|
1443
1695
|
return;
|
|
1444
1696
|
}
|
|
1445
|
-
if (
|
|
1697
|
+
if (sessions.length === 0) return;
|
|
1698
|
+
// iter5 (Vincent UX): tree auto-fit scales the org chart to fit BOTH
|
|
1699
|
+
// viewBox dimensions and CENTRES it. Pre-iter5 the shared grid path
|
|
1700
|
+
// only fit height and left-anchored the chart, so the tree hugged the
|
|
1701
|
+
// top-left corner with dead canvas down the right side.
|
|
1702
|
+
if (layout === 'tree') {
|
|
1703
|
+
if (!treeContentBox) return;
|
|
1704
|
+
const b = treeContentBox;
|
|
1705
|
+
if (b.w <= 0 || b.h <= 0) { autoFitDoneRef.current = true; return; }
|
|
1706
|
+
const pad = 24;
|
|
1707
|
+
const z = Math.max(ZOOM_MIN, Math.min(
|
|
1708
|
+
1, (VIEWBOX_W - 2 * pad) / b.w, (VIEWBOX_H - 2 * pad) / b.h));
|
|
1709
|
+
setView({
|
|
1710
|
+
zoom: z,
|
|
1711
|
+
x: VIEWBOX_W / 2 - (b.x + b.w / 2) * z,
|
|
1712
|
+
y: VIEWBOX_H / 2 - (b.y + b.h / 2) * z,
|
|
1713
|
+
});
|
|
1714
|
+
autoFitDoneRef.current = true;
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (layout !== 'grid' || !gridContentBottom) return;
|
|
1446
1718
|
if (gridContentBottom <= VIEWBOX_H) {
|
|
1447
1719
|
autoFitDoneRef.current = true; // no overflow → no fit needed
|
|
1448
1720
|
return;
|
|
@@ -1450,7 +1722,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1450
1722
|
const fitZoom = Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
|
|
1451
1723
|
setView({ zoom: fitZoom, x: 0, y: 0 });
|
|
1452
1724
|
autoFitDoneRef.current = true;
|
|
1453
|
-
}, [layout, sessions.length, gridContentBottom, hadPersistedViewOnMount]);
|
|
1725
|
+
}, [layout, sessions.length, gridContentBottom, treeContentBox, hadPersistedViewOnMount]);
|
|
1454
1726
|
|
|
1455
1727
|
// persist view
|
|
1456
1728
|
useEffect(() => {
|
|
@@ -1594,13 +1866,27 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
1594
1866
|
// when the operator invokes fit-to-content via the hub click (R52),
|
|
1595
1867
|
// chrome button, `f` key, or palette command.
|
|
1596
1868
|
const fitView = useCallback(() => {
|
|
1869
|
+
setSmoothView(true);
|
|
1870
|
+
setTimeout(() => setSmoothView(false), 350);
|
|
1871
|
+
// iter5: tree fits-and-centres via its content bbox (sister to the
|
|
1872
|
+
// auto-fit effect); grid/ring keep the height-only fit-to-content.
|
|
1873
|
+
if (layout === 'tree' && treeContentBox && treeContentBox.w > 0 && treeContentBox.h > 0) {
|
|
1874
|
+
const b = treeContentBox;
|
|
1875
|
+
const pad = 24;
|
|
1876
|
+
const z = Math.max(ZOOM_MIN, Math.min(
|
|
1877
|
+
1, (VIEWBOX_W - 2 * pad) / b.w, (VIEWBOX_H - 2 * pad) / b.h));
|
|
1878
|
+
setView({
|
|
1879
|
+
zoom: z,
|
|
1880
|
+
x: VIEWBOX_W / 2 - (b.x + b.w / 2) * z,
|
|
1881
|
+
y: VIEWBOX_H / 2 - (b.y + b.h / 2) * z,
|
|
1882
|
+
});
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1597
1885
|
const zoom = !gridContentBottom || gridContentBottom <= VIEWBOX_H
|
|
1598
1886
|
? 1
|
|
1599
1887
|
: Math.max(ZOOM_MIN, Math.min(1, VIEWBOX_H / gridContentBottom));
|
|
1600
|
-
setSmoothView(true);
|
|
1601
|
-
setTimeout(() => setSmoothView(false), 350);
|
|
1602
1888
|
setView({ zoom, x: 0, y: 0 });
|
|
1603
|
-
}, [gridContentBottom]);
|
|
1889
|
+
}, [gridContentBottom, layout, treeContentBox]);
|
|
1604
1890
|
|
|
1605
1891
|
// R74: listen for layout + view palette commands. Sister to R69's
|
|
1606
1892
|
// pin listener — palette dispatches a CustomEvent, the reducer here
|
|
@@ -2394,11 +2680,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
2394
2680
|
data-topo-chrome-wrapper-halo-family="layout"
|
|
2395
2681
|
>
|
|
2396
2682
|
<button
|
|
2397
|
-
onClick={() => { popChrome('layout-ring');
|
|
2683
|
+
onClick={() => { popChrome('layout-ring'); selectLayout('ring'); }}
|
|
2398
2684
|
onMouseEnter={() => setHoveredLayout('ring')}
|
|
2399
2685
|
onMouseLeave={() => setHoveredLayout((prev) => prev === 'ring' ? null : prev)}
|
|
2400
2686
|
aria-pressed={layout === 'ring'}
|
|
2401
|
-
title="Ring layout (l to
|
|
2687
|
+
title="Ring layout (l to cycle)"
|
|
2402
2688
|
data-topo-chrome-layout="ring"
|
|
2403
2689
|
data-topo-chrome-layout-active={layout === 'ring' ? 'true' : 'false'}
|
|
2404
2690
|
data-topo-chrome-layout-ring-popping={chromePopping === 'layout-ring' ? 'true' : 'false'}
|
|
@@ -2533,11 +2819,11 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
2533
2819
|
Ring
|
|
2534
2820
|
</button>
|
|
2535
2821
|
<button
|
|
2536
|
-
onClick={() => { popChrome('layout-grid');
|
|
2822
|
+
onClick={() => { popChrome('layout-grid'); selectLayout('grid'); }}
|
|
2537
2823
|
onMouseEnter={() => setHoveredLayout('grid')}
|
|
2538
2824
|
onMouseLeave={() => setHoveredLayout((prev) => prev === 'grid' ? null : prev)}
|
|
2539
2825
|
aria-pressed={layout === 'grid'}
|
|
2540
|
-
title="Grid layout (l to
|
|
2826
|
+
title="Grid layout (l to cycle)"
|
|
2541
2827
|
data-topo-chrome-layout="grid"
|
|
2542
2828
|
data-topo-chrome-layout-active={layout === 'grid' ? 'true' : 'false'}
|
|
2543
2829
|
data-topo-chrome-layout-grid-popping={chromePopping === 'layout-grid' ? 'true' : 'false'}
|
|
@@ -2585,6 +2871,30 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
2585
2871
|
>
|
|
2586
2872
|
Grid
|
|
2587
2873
|
</button>
|
|
2874
|
+
{/* #170 tree-view MVP — third segment in the layout control.
|
|
2875
|
+
Org-chart view: 总指挥/副指挥 root → teams → members.
|
|
2876
|
+
Mirrors the Grid button's border-l divider + active/
|
|
2877
|
+
inactive paint vocabulary; the heavier R-series polish
|
|
2878
|
+
(halo layers, hover-fw preview attrs) is deferred to the
|
|
2879
|
+
RFC-017 formal version. */}
|
|
2880
|
+
<button
|
|
2881
|
+
onClick={() => { popChrome('layout-tree'); selectLayout('tree'); }}
|
|
2882
|
+
onMouseEnter={() => setHoveredLayout('tree')}
|
|
2883
|
+
onMouseLeave={() => setHoveredLayout((prev) => prev === 'tree' ? null : prev)}
|
|
2884
|
+
aria-pressed={layout === 'tree'}
|
|
2885
|
+
title="Tree layout — org chart (l to cycle)"
|
|
2886
|
+
data-topo-chrome-layout="tree"
|
|
2887
|
+
data-topo-chrome-layout-active={layout === 'tree' ? 'true' : 'false'}
|
|
2888
|
+
data-topo-chrome-layout-tree-popping={chromePopping === 'layout-tree' ? 'true' : 'false'}
|
|
2889
|
+
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' : ''}`}
|
|
2890
|
+
style={{
|
|
2891
|
+
borderColor: pal.containerBorder,
|
|
2892
|
+
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',
|
|
2893
|
+
filter: hoveredLayout === 'tree' ? `drop-shadow(0 0 2px ${pal.legendAccent}80) drop-shadow(0 0 4px ${pal.legendAccent}40) brightness(1.15)` : undefined,
|
|
2894
|
+
}}
|
|
2895
|
+
>
|
|
2896
|
+
Tree
|
|
2897
|
+
</button>
|
|
2588
2898
|
</div>
|
|
2589
2899
|
{/* R79: working + online count chips become hover affordances —
|
|
2590
2900
|
extends R77's chip-hover pattern to status counts. Hover the
|
|
@@ -3506,7 +3816,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
3506
3816
|
// unknowns folded to '?').
|
|
3507
3817
|
const matchAliases = [...onlineNodes, ...offlineNodes]
|
|
3508
3818
|
.filter(s => {
|
|
3509
|
-
const v = vendorForModel(s.model);
|
|
3819
|
+
const v = vendorForModel(s.model, s.runtime);
|
|
3510
3820
|
return (v.id === 'unknown' ? '?' : v.initial) === pinnedVendor;
|
|
3511
3821
|
})
|
|
3512
3822
|
.map(s => s.alias);
|
|
@@ -3776,7 +4086,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
3776
4086
|
if (gk !== pinnedGroup) return false;
|
|
3777
4087
|
}
|
|
3778
4088
|
if (pinnedVendor) {
|
|
3779
|
-
const v = vendorForModel(s.model);
|
|
4089
|
+
const v = vendorForModel(s.model, s.runtime);
|
|
3780
4090
|
const initial = v.id === 'unknown' ? '?' : v.initial;
|
|
3781
4091
|
if (initial !== pinnedVendor) return false;
|
|
3782
4092
|
}
|
|
@@ -3974,7 +4284,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
3974
4284
|
// UI shows "A:3" should hover-explain which 3.
|
|
3975
4285
|
const aliases = [...onlineNodes, ...offlineNodes]
|
|
3976
4286
|
.filter(s => {
|
|
3977
|
-
const vid = vendorForModel(s.model);
|
|
4287
|
+
const vid = vendorForModel(s.model, s.runtime);
|
|
3978
4288
|
return (vid.id === 'unknown' ? '?' : vid.initial) === v.initial;
|
|
3979
4289
|
})
|
|
3980
4290
|
.map(s => s.alias);
|
|
@@ -6400,6 +6710,70 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
6400
6710
|
});
|
|
6401
6711
|
})()}
|
|
6402
6712
|
|
|
6713
|
+
{/* #170 tree-view MVP — org-chart elbow connectors. Tree layout
|
|
6714
|
+
only (treeConnectors.lines is empty in ring/grid). Rendered
|
|
6715
|
+
here, behind the nodes + labels, as right-angle polylines:
|
|
6716
|
+
vertical drop from the parent, horizontal at the mid-y, then
|
|
6717
|
+
vertical into the child. pointer-events off so they never
|
|
6718
|
+
intercept a node click. The synthetic-node chips (指挥中心
|
|
6719
|
+
root / 未分组 bucket) render here too — they have no backing
|
|
6720
|
+
session so the node-map below skips them. */}
|
|
6721
|
+
{layout === 'tree' && treeConnectors.lines.map((ln, i) => {
|
|
6722
|
+
const midY = (ln.y1 + ln.y2) / 2;
|
|
6723
|
+
// drop from just below the parent ring to just above the child
|
|
6724
|
+
// ring — keeps the connector clear of the avatars themselves.
|
|
6725
|
+
const startY = ln.y1;
|
|
6726
|
+
const endY = ln.y2;
|
|
6727
|
+
const d = `M ${ln.x1} ${startY} L ${ln.x1} ${midY} L ${ln.x2} ${midY} L ${ln.x2} ${endY}`;
|
|
6728
|
+
return (
|
|
6729
|
+
<path
|
|
6730
|
+
key={`tree-conn-${i}`}
|
|
6731
|
+
d={d}
|
|
6732
|
+
fill="none"
|
|
6733
|
+
stroke={pal.containerBorder}
|
|
6734
|
+
strokeWidth={1.5}
|
|
6735
|
+
strokeLinejoin="round"
|
|
6736
|
+
strokeLinecap="round"
|
|
6737
|
+
opacity={0.55}
|
|
6738
|
+
style={{ pointerEvents: 'none' }}
|
|
6739
|
+
/>
|
|
6740
|
+
);
|
|
6741
|
+
})}
|
|
6742
|
+
{layout === 'tree' && treeConnectors.synthLabels.map((s, i) => {
|
|
6743
|
+
// synthetic node chip: a small pill with a centred label, drawn
|
|
6744
|
+
// at the synthetic node's slot. No status ring → never counted
|
|
6745
|
+
// by the node↔node overlap test; sized to clear neighbours.
|
|
6746
|
+
const chipW = Math.max(58, s.label.length * 13 + 20);
|
|
6747
|
+
const chipH = 26;
|
|
6748
|
+
return (
|
|
6749
|
+
<g key={`tree-synth-${i}`} style={{ pointerEvents: 'none' }} data-topo-tree-synth={s.label}>
|
|
6750
|
+
<rect
|
|
6751
|
+
x={s.x - chipW / 2}
|
|
6752
|
+
y={s.y - chipH / 2}
|
|
6753
|
+
width={chipW}
|
|
6754
|
+
height={chipH}
|
|
6755
|
+
rx={13}
|
|
6756
|
+
fill={pal.containerBg}
|
|
6757
|
+
stroke={pal.containerBorder}
|
|
6758
|
+
strokeWidth={1.25}
|
|
6759
|
+
strokeDasharray="3 3"
|
|
6760
|
+
opacity={0.92}
|
|
6761
|
+
/>
|
|
6762
|
+
<text
|
|
6763
|
+
x={s.x}
|
|
6764
|
+
y={s.y}
|
|
6765
|
+
textAnchor="middle"
|
|
6766
|
+
dominantBaseline="central"
|
|
6767
|
+
fontSize={12}
|
|
6768
|
+
fontWeight={600}
|
|
6769
|
+
fill={pal.legendAccent}
|
|
6770
|
+
>
|
|
6771
|
+
{s.label}
|
|
6772
|
+
</text>
|
|
6773
|
+
</g>
|
|
6774
|
+
);
|
|
6775
|
+
})}
|
|
6776
|
+
|
|
6403
6777
|
{/* #111: prefix-group boundary boxes (Vincent 4722). Grid layout
|
|
6404
6778
|
only — groupBoxes is empty in ring mode. Rendered behind the
|
|
6405
6779
|
flow links + nodes; pointer-events off so they never intercept
|
|
@@ -10105,7 +10479,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
10105
10479
|
opacity: hoveredEdgeEndpoints && !hoveredEdgeEndpoints.has(session.alias) && chatAlias !== session.alias
|
|
10106
10480
|
? 0.28
|
|
10107
10481
|
: activeVendor && chatAlias !== session.alias && (() => {
|
|
10108
|
-
const v = vendorForModel(session.model);
|
|
10482
|
+
const v = vendorForModel(session.model, session.runtime);
|
|
10109
10483
|
const initial = v.id === 'unknown' ? '?' : v.initial;
|
|
10110
10484
|
return initial !== activeVendor;
|
|
10111
10485
|
})()
|
|
@@ -11370,7 +11744,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
11370
11744
|
{(() => {
|
|
11371
11745
|
const ar = Math.round((isOnline ? 14 : 10) * nodeScale);
|
|
11372
11746
|
const size = radius * 2;
|
|
11373
|
-
const vendor = vendorForModel(session.model);
|
|
11747
|
+
const vendor = vendorForModel(session.model, session.runtime);
|
|
11374
11748
|
const internByAlias = /书生|书小生|intern/i.test(session.alias);
|
|
11375
11749
|
|
|
11376
11750
|
if (isIntern || internByAlias || vendor.logo) {
|
|
@@ -12624,7 +12998,7 @@ export function TopoGraph({ sessions, sseSessions, renameSignal }: TopoGraphProp
|
|
|
12624
12998
|
gate as showFullLabel; dense fleets already have
|
|
12625
12999
|
too much per-node chrome competing. */}
|
|
12626
13000
|
{!reducedMotion && hoveredAlias === session.alias && !denseLayout && (() => {
|
|
12627
|
-
const v = vendorForModel(session.model);
|
|
13001
|
+
const v = vendorForModel(session.model, session.runtime);
|
|
12628
13002
|
const rt = runtimeIdentity(session.runtime);
|
|
12629
13003
|
const flipLeft = pos.x > VIEWBOX_W * 0.65;
|
|
12630
13004
|
const detailW = 192;
|