@sleep2agi/agent-network-dashboard 0.4.5-preview.9 → 0.4.5-preview.91
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/.mcp.json +11 -0
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +48 -48
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-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/page/next-font-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +4 -4
- package/.next/server/app/_not-found.rsc +12 -12
- package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page/next-font-manifest.json +1 -1
- package/.next/server/app/admin/page.js.nft.json +1 -1
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +4 -4
- package/.next/server/app/admin.rsc +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
- package/.next/server/app/admin.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +4 -4
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +7 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/login/page/next-font-manifest.json +1 -1
- package/.next/server/app/login/page.js.nft.json +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 +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +7 -7
- package/.next/server/app/login.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page/next-font-manifest.json +1 -1
- package/.next/server/app/logs/page.js.nft.json +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +4 -4
- package/.next/server/app/logs.rsc +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/logs.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page/next-font-manifest.json +1 -1
- package/.next/server/app/messages/page.js.nft.json +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +4 -4
- package/.next/server/app/messages.rsc +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
- package/.next/server/app/messages.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page/next-font-manifest.json +1 -1
- package/.next/server/app/node/page.js.nft.json +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +4 -4
- package/.next/server/app/node.rsc +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +7 -7
- package/.next/server/app/node.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/page/next-font-manifest.json +1 -1
- package/.next/server/app/nodes/page.js.nft.json +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +4 -4
- package/.next/server/app/nodes.rsc +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
- package/.next/server/app/nodes.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page/next-font-manifest.json +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs/page/next-font-manifest.json +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +4 -4
- package/.next/server/app/server-logs.rsc +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page/next-font-manifest.json +1 -1
- package/.next/server/app/settings/networks/page.js.nft.json +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +4 -4
- package/.next/server/app/settings/networks.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page/next-font-manifest.json +1 -1
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page/next-font-manifest.json +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +4 -4
- package/.next/server/app/settings/tokens.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +4 -4
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
- package/.next/server/app/settings.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page/next-font-manifest.json +1 -1
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page/next-font-manifest.json +1 -1
- package/.next/server/app/tasks/page.js.nft.json +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +4 -4
- package/.next/server/app/tasks.rsc +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
- package/.next/server/app/tasks.segments/_tree.segment.rsc +3 -3
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jl4ukn._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0n46s~k._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0w2ay2h._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
- package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
- package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
- package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
- package/.next/server/chunks/ssr/app_01y-~mp._.js +3 -0
- package/.next/server/chunks/ssr/app_01y-~mp._.js.map +1 -0
- package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
- package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
- package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0aenxef._.js +3 -0
- package/.next/server/chunks/ssr/app_0aenxef._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0aum_g5._.js +3 -0
- package/.next/server/chunks/ssr/app_0aum_g5._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0emvkn2._.js +3 -0
- package/.next/server/chunks/ssr/app_0emvkn2._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0esuljp._.js +3 -0
- package/.next/server/chunks/ssr/app_0esuljp._.js.map +1 -0
- package/.next/server/chunks/ssr/app_0izi28j._.js +1 -1
- package/.next/server/chunks/ssr/app_0izi28j._.js.map +1 -1
- package/.next/server/chunks/ssr/app_components_08ys5y3._.js +5 -5
- package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
- package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
- package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
- package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
- package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/next-font-manifest.js +1 -1
- package/.next/server/next-font-manifest.json +14 -14
- package/.next/server/pages/404.html +4 -4
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/0-hlcuoemhe7w.js +7 -0
- package/.next/static/chunks/02_zpo7taex9o.js +7 -0
- package/.next/static/chunks/03b6jkic-1r9u.css +2 -0
- package/.next/static/chunks/06fliypy0.f0r.js +1 -0
- package/.next/static/chunks/06n3d~fbbtagk.js +1 -0
- package/.next/static/chunks/07eba1lz2u3p4.js +1 -0
- package/.next/static/chunks/0av2~0.96~d~g.js +1 -0
- package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
- package/.next/static/chunks/0g9aof0ob_fcj.js +1 -0
- package/.next/static/chunks/0gtbwcilfu6b7.js +1 -0
- package/.next/static/chunks/0s8jo6jypzkx5.js +1 -0
- package/.next/static/chunks/0ubqqhhpsaez4.js +1 -0
- package/.next/static/chunks/0vmfu_gz9tp4f.js +1 -0
- package/.next/static/chunks/0~qw9kxksr1v2.js +1 -0
- package/.next/static/chunks/12gprw3l0kvvf.js +1 -0
- package/.next/static/chunks/12mjtyh-j74~k.js +1 -0
- package/.next/static/chunks/12yuo353giomx.js +1 -0
- package/.next/static/media/4fa387ec64143e14-s.0.qu-9752pffj.woff2 +0 -0
- package/.next/static/media/5ce348bf30bf5439-s.0ee55_hj9qcer.woff2 +0 -0
- package/.next/static/media/6306c77e7c8268e4-s.0mao5jbfbduzp.woff2 +0 -0
- package/.next/static/media/797e433ab948586e-s.p.09zddjkbdep5a.woff2 +0 -0
- package/.next/static/media/7d817b4c03b0c5f1-s.0uzt.a6d44yda.woff2 +0 -0
- package/.next/static/media/bbc41e54d2fcbd21-s.0mvwgmnhv29no.woff2 +0 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/admin/page.tsx +97 -27
- package/app/components/AgentCard.tsx +19 -5
- package/app/components/BroadcastBar.tsx +22 -4
- package/app/components/DispatchPanel.tsx +9 -5
- package/app/components/EmptyState.tsx +127 -12
- package/app/components/HealthBanner.tsx +11 -2
- package/app/components/HelpOverlay.tsx +50 -10
- package/app/components/InboxPanel.tsx +7 -4
- package/app/components/Sidebar.tsx +81 -44
- package/app/components/StatsBar.tsx +59 -36
- package/app/components/TaskChatPanel.tsx +8 -10
- package/app/components/TaskDrawer.tsx +72 -28
- package/app/components/TopoGraph.tsx +527 -41
- package/app/components/utils.ts +13 -0
- package/app/globals.css +90 -0
- package/app/lib/status.ts +85 -0
- package/app/lib/version.ts +7 -0
- package/app/login/page.tsx +40 -7
- package/app/logs/page.tsx +70 -21
- package/app/messages/page.tsx +34 -9
- package/app/node/page.tsx +52 -27
- package/app/nodes/page.tsx +45 -21
- package/app/page.tsx +103 -32
- package/app/server-logs/page.tsx +104 -28
- package/app/settings/networks/page.tsx +40 -16
- package/app/settings/page.tsx +39 -11
- package/app/tasks/[id]/page.tsx +2 -13
- package/app/tasks/page.tsx +66 -73
- package/package.json +1 -1
- package/public/intern_avatar.png +0 -0
- package/scripts/empty-state-test.mjs +41 -0
- package/scripts/fold-only.mjs +47 -0
- package/scripts/intern-alias-test.mjs +46 -0
- package/scripts/loop-review.mjs +270 -0
- package/scripts/network-derive-shot.mjs +16 -0
- package/scripts/network-derive-test.mjs +28 -0
- package/scripts/topo-brand-shot.mjs +61 -0
- package/scripts/topo-cluster-test.mjs +41 -0
- package/scripts/topo-controls-shot.mjs +24 -0
- package/scripts/topo-group-test.mjs +37 -0
- package/scripts/topo-inspect.mjs +56 -0
- package/scripts/topo-interaction-test.mjs +80 -0
- package/scripts/topo-mock-large.mjs +58 -0
- package/scripts/topo-r104-test.mjs +54 -0
- package/scripts/topo-shot.mjs +31 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w_u7t._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__06w_u7t._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0ehu0jb._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0ehu0jb._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
- package/.next/server/chunks/ssr/_13_v7-3._.js +0 -3
- package/.next/server/chunks/ssr/_13_v7-3._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
- package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0ap_oiq._.js +0 -3
- package/.next/server/chunks/ssr/app_0ap_oiq._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0dk5wao._.js +0 -3
- package/.next/server/chunks/ssr/app_0dk5wao._.js.map +0 -1
- package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
- package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
- package/.next/server/chunks/ssr/app_10b5blc._.js +0 -9
- package/.next/server/chunks/ssr/app_10b5blc._.js.map +0 -1
- package/.next/server/chunks/ssr/app_11vl967._.js +0 -3
- package/.next/server/chunks/ssr/app_11vl967._.js.map +0 -1
- package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +0 -3
- package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +0 -1
- package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
- package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
- package/.next/static/chunks/0-.hjh--kcdiy.js +0 -1
- package/.next/static/chunks/02afyxgpty3uf.js +0 -1
- package/.next/static/chunks/02flrewxlf_m_.js +0 -1
- package/.next/static/chunks/051gslfjka1kj.js +0 -1
- package/.next/static/chunks/07oobuohrpigy.js +0 -1
- package/.next/static/chunks/09al3qm76q7nd.js +0 -1
- package/.next/static/chunks/0e7c40p~wq665.js +0 -1
- package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
- package/.next/static/chunks/0h.ajsk_uxigl.js +0 -1
- package/.next/static/chunks/0msa~bub38ytf.css +0 -2
- package/.next/static/chunks/0ogeals5ufhho.js +0 -1
- package/.next/static/chunks/10ir.g.n~hre0.js +0 -7
- package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
- package/.next/static/chunks/12dcb86elsl-e.js +0 -7
- package/.next/static/chunks/13.dst7liziwo.js +0 -1
- package/.next/static/chunks/132e__998_q9r.js +0 -1
- package/.next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 +0 -0
- package/.next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 +0 -0
- package/.next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 +0 -0
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/next.svg +0 -1
- package/public/vercel.svg +0 -1
- package/public/window.svg +0 -1
- /package/.next/static/{stSZtBF9uGYc3L399m0h1 → u-HIIfy-O0UVQ02APphkX}/_buildManifest.js +0 -0
- /package/.next/static/{stSZtBF9uGYc3L399m0h1 → u-HIIfy-O0UVQ02APphkX}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{stSZtBF9uGYc3L399m0h1 → u-HIIfy-O0UVQ02APphkX}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Session } from './types';
|
|
5
|
+
import { aliasAvatarColors, aliasInitial } from './AliasAvatar';
|
|
5
6
|
|
|
6
7
|
interface MessageFlow {
|
|
7
8
|
from_alias: string;
|
|
@@ -31,7 +32,19 @@ interface FlowLink {
|
|
|
31
32
|
const cx = 500;
|
|
32
33
|
const cy = 330;
|
|
33
34
|
const onlineRadius = 220;
|
|
34
|
-
|
|
35
|
+
// Round 97 (issue #50): tier into two rings when N > 8; round 98
|
|
36
|
+
// (issue #61): tier into three rings when N > 14. At N=22 (Vincent's
|
|
37
|
+
// network) two rings still leave 11 nodes per ring → inner chord of
|
|
38
|
+
// 88px can't fit a 100px label; three rings give ~⌈N/3⌉ per ring so
|
|
39
|
+
// every tier has enough arc room.
|
|
40
|
+
const onlineTierThreshold = 8;
|
|
41
|
+
const onlineTripleThreshold = 14;
|
|
42
|
+
const onlineInnerRadius = 175;
|
|
43
|
+
const onlineOuterRadius = 260;
|
|
44
|
+
const onlineTripleInnerR = 145;
|
|
45
|
+
const onlineTripleMidR = 215;
|
|
46
|
+
const onlineTripleOuterR = 285;
|
|
47
|
+
const offlineRadius = 325;
|
|
35
48
|
|
|
36
49
|
/** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset
|
|
37
50
|
* the whole ring so two stacked rings don't align radially (round 25: the
|
|
@@ -184,6 +197,64 @@ function useTheme(): 'light' | 'dark' {
|
|
|
184
197
|
return theme;
|
|
185
198
|
}
|
|
186
199
|
|
|
200
|
+
/** Round 100 (issue #79): brand showcase mode. Activate via `?brand=intern`
|
|
201
|
+
* on any dashboard page — TopoGraph nodes show the 书小生 mascot instead
|
|
202
|
+
* of alias initials. Stored in localStorage so the flag persists across
|
|
203
|
+
* navigation. Clears with `?brand=none` or `?brand=` (empty). */
|
|
204
|
+
function useBrand(): string | null {
|
|
205
|
+
const [brand, setBrand] = useState<string | null>(null);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
try {
|
|
208
|
+
const url = new URL(window.location.href);
|
|
209
|
+
const param = url.searchParams.get('brand');
|
|
210
|
+
if (param !== null) {
|
|
211
|
+
if (param === '' || param === 'none') {
|
|
212
|
+
localStorage.removeItem('anet-brand');
|
|
213
|
+
setBrand(null);
|
|
214
|
+
} else {
|
|
215
|
+
localStorage.setItem('anet-brand', param);
|
|
216
|
+
setBrand(param);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
setBrand(localStorage.getItem('anet-brand'));
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
}, []);
|
|
223
|
+
return brand;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Round 106 (issue #83): cluster agents by shared alias prefix so a team
|
|
227
|
+
* reads as one unit in the topology. Adjacent (sorted) aliases that share
|
|
228
|
+
* a ≥2-char prefix join the same group; the group key is the prefix common
|
|
229
|
+
* to every member. Singletons map to their own alias (no hue shift). The
|
|
230
|
+
* group key feeds aliasAvatarColors() so e.g. all 通信* nodes get one hue,
|
|
231
|
+
* all 研究员* another — the "同色相 tint" clustering option from #83. */
|
|
232
|
+
function commonPrefix(a: string, b: string): string {
|
|
233
|
+
let i = 0;
|
|
234
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
235
|
+
return a.slice(0, i);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function computeGroupKeys(aliases: string[]): Record<string, string> {
|
|
239
|
+
const sorted = [...aliases].sort((a, b) => a.localeCompare(b));
|
|
240
|
+
const keys: Record<string, string> = {};
|
|
241
|
+
let i = 0;
|
|
242
|
+
while (i < sorted.length) {
|
|
243
|
+
let j = i;
|
|
244
|
+
let prefix = sorted[i];
|
|
245
|
+
while (j + 1 < sorted.length) {
|
|
246
|
+
const lcp = commonPrefix(prefix, sorted[j + 1]);
|
|
247
|
+
if (lcp.length < 2) break;
|
|
248
|
+
prefix = lcp;
|
|
249
|
+
j++;
|
|
250
|
+
}
|
|
251
|
+
const key = j > i ? prefix : sorted[i];
|
|
252
|
+
for (let k = i; k <= j; k++) keys[sorted[k]] = key;
|
|
253
|
+
i = j + 1;
|
|
254
|
+
}
|
|
255
|
+
return keys;
|
|
256
|
+
}
|
|
257
|
+
|
|
187
258
|
function buildFlowLinks(messages: MessageFlow[], positions: Record<string, Point>) {
|
|
188
259
|
const links = new Map<string, FlowLink>();
|
|
189
260
|
|
|
@@ -215,6 +286,8 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
215
286
|
const theme = useTheme();
|
|
216
287
|
const isLight = theme === 'light';
|
|
217
288
|
const pal = isLight ? LIGHT_PALETTE : DARK_PALETTE;
|
|
289
|
+
const brand = useBrand();
|
|
290
|
+
const isIntern = brand === 'intern';
|
|
218
291
|
const [messages, setMessages] = useState<MessageFlow[]>([]);
|
|
219
292
|
|
|
220
293
|
useEffect(() => {
|
|
@@ -241,24 +314,73 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
241
314
|
nodePositions,
|
|
242
315
|
flowLinks,
|
|
243
316
|
activeAliases,
|
|
317
|
+
groupKeys,
|
|
244
318
|
} = useMemo(() => {
|
|
245
319
|
const sseCount = (s: { alias: string; network_id?: string }) =>
|
|
246
320
|
(s.network_id ? sseSessions[`${s.network_id}:${s.alias}`] : undefined) ?? sseSessions[s.alias];
|
|
247
|
-
|
|
248
|
-
|
|
321
|
+
// Round 106 (issue #83): sort by alias so same-prefix agents
|
|
322
|
+
// (通信龙 / 通信牛 / 通信工程马 …, or 研究员1号 / 研究员2号 …) end up
|
|
323
|
+
// adjacent in the array — the tier layout below assigns angles by
|
|
324
|
+
// index, so contiguous-in-array becomes contiguous-in-ring, i.e.
|
|
325
|
+
// each team visually clusters. localeCompare keeps CJK ordering sane.
|
|
326
|
+
const byAlias = (a: Session, b: Session) => a.alias.localeCompare(b.alias);
|
|
327
|
+
const online = sessions.filter(s => s.status !== 'offline' || sseCount(s)).sort(byAlias);
|
|
328
|
+
const offline = sessions.filter(s => s.status === 'offline' && !sseCount(s)).sort(byAlias);
|
|
249
329
|
const positions: Record<string, Point> = {};
|
|
250
330
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
331
|
+
// Round 97 (issue #50) + 98 (issue #61): three layout modes by N.
|
|
332
|
+
// N ≤ 8 → single ring (r=220)
|
|
333
|
+
// 8 < N ≤ 14 → two rings (inner r=175, outer r=260, half-step rot)
|
|
334
|
+
// N > 14 → three rings (r=145/215/285, ⌈N/3⌉ per ring)
|
|
335
|
+
// Each ring's spread is 1.78π so its node count drives chord length:
|
|
336
|
+
// labels are 100px wide so each ring needs ≥110px chord per node.
|
|
337
|
+
const tripleTier = online.length > onlineTripleThreshold;
|
|
338
|
+
const dualTier = !tripleTier && online.length > onlineTierThreshold;
|
|
339
|
+
let outerOnlineCount = online.length;
|
|
340
|
+
if (tripleTier) {
|
|
341
|
+
const per = Math.ceil(online.length / 3);
|
|
342
|
+
const r1 = online.slice(0, per);
|
|
343
|
+
const r2 = online.slice(per, 2 * per);
|
|
344
|
+
const r3 = online.slice(2 * per);
|
|
345
|
+
r1.forEach((s, index) => {
|
|
346
|
+
positions[s.alias] = polarPoint(index, Math.max(r1.length, 1), onlineTripleInnerR);
|
|
347
|
+
});
|
|
348
|
+
const r1Spread = r1.length <= 2 ? Math.PI : Math.PI * 1.78;
|
|
349
|
+
const r1Step = r1.length > 1 ? r1Spread / (r1.length - 1) : 0;
|
|
350
|
+
r2.forEach((s, index) => {
|
|
351
|
+
positions[s.alias] = polarPoint(index, Math.max(r2.length, 1), onlineTripleMidR, r1Step / 2);
|
|
352
|
+
});
|
|
353
|
+
r3.forEach((s, index) => {
|
|
354
|
+
positions[s.alias] = polarPoint(index, Math.max(r3.length, 1), onlineTripleOuterR);
|
|
355
|
+
});
|
|
356
|
+
outerOnlineCount = r3.length;
|
|
357
|
+
} else if (dualTier) {
|
|
358
|
+
const innerCount = Math.ceil(online.length / 2);
|
|
359
|
+
const outerCount = online.length - innerCount;
|
|
360
|
+
const innerNodes = online.slice(0, innerCount);
|
|
361
|
+
const outerNodes = online.slice(innerCount);
|
|
362
|
+
innerNodes.forEach((s, index) => {
|
|
363
|
+
positions[s.alias] = polarPoint(index, Math.max(innerCount, 1), onlineInnerRadius);
|
|
364
|
+
});
|
|
365
|
+
const innerSpread = innerCount <= 2 ? Math.PI : Math.PI * 1.78;
|
|
366
|
+
const innerStep = innerCount > 1 ? innerSpread / (innerCount - 1) : 0;
|
|
367
|
+
outerNodes.forEach((s, index) => {
|
|
368
|
+
positions[s.alias] = polarPoint(index, Math.max(outerCount, 1), onlineOuterRadius, innerStep / 2);
|
|
369
|
+
});
|
|
370
|
+
outerOnlineCount = outerCount;
|
|
371
|
+
} else {
|
|
372
|
+
online.forEach((s, index) => {
|
|
373
|
+
positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
254
376
|
|
|
255
|
-
// Offset the offline ring radially by half the online step so
|
|
256
|
-
// bubbles sit in the angular gaps between online bubbles instead
|
|
257
|
-
// stacking directly behind them. Also push the outer ring further when
|
|
377
|
+
// Offset the offline ring radially by half the outermost online step so
|
|
378
|
+
// offline bubbles sit in the angular gaps between online bubbles instead
|
|
379
|
+
// of stacking directly behind them. Also push the outer ring further when
|
|
258
380
|
// there are many offline nodes so labels don't crowd the legend.
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const offlineRotation =
|
|
381
|
+
const outerSpreadBase = outerOnlineCount <= 2 ? Math.PI : Math.PI * 1.78;
|
|
382
|
+
const outerStep = outerOnlineCount > 1 ? outerSpreadBase / (outerOnlineCount - 1) : 0;
|
|
383
|
+
const offlineRotation = outerOnlineCount > 0 ? outerStep / 2 : 0;
|
|
262
384
|
const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;
|
|
263
385
|
|
|
264
386
|
offline.forEach((s, index) => {
|
|
@@ -272,17 +394,151 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
272
394
|
active.add(link.to);
|
|
273
395
|
});
|
|
274
396
|
|
|
397
|
+
// Round 106 (issue #83): group key per alias → shared hue per team.
|
|
398
|
+
const groupKeys = computeGroupKeys([...online, ...offline].map(s => s.alias));
|
|
399
|
+
|
|
275
400
|
return {
|
|
276
401
|
onlineNodes: online,
|
|
277
402
|
offlineNodes: offline,
|
|
278
403
|
nodePositions: positions,
|
|
279
404
|
flowLinks: links,
|
|
280
405
|
activeAliases: active,
|
|
406
|
+
groupKeys,
|
|
281
407
|
};
|
|
282
408
|
}, [messages, sessions, sseSessions]);
|
|
283
409
|
|
|
284
410
|
const workingCount = onlineNodes.filter(s => s.status === 'working').length;
|
|
285
411
|
|
|
412
|
+
// --- Round 103 (issue #81): fullscreen + zoom + pan interaction layer ---
|
|
413
|
+
// DIY native (no d3 / svg-pan-zoom): wrap the topology content in a single
|
|
414
|
+
// <g transform> and drive it with wheel + pointer-drag. The panel <rect>
|
|
415
|
+
// backdrop stays fixed so panning never reveals empty canvas. View state
|
|
416
|
+
// {zoom,x,y} persists to localStorage (same sticky pattern as brand flag).
|
|
417
|
+
const VIEWBOX_W = 1000;
|
|
418
|
+
const VIEWBOX_H = 680;
|
|
419
|
+
const ZOOM_MIN = 0.5;
|
|
420
|
+
const ZOOM_MAX = 4;
|
|
421
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
422
|
+
const svgRef = useRef<SVGSVGElement>(null);
|
|
423
|
+
const [view, setView] = useState({ zoom: 1, x: 0, y: 0 });
|
|
424
|
+
const viewRef = useRef(view);
|
|
425
|
+
const dragRef = useRef({ active: false, startX: 0, startY: 0, baseX: 0, baseY: 0 });
|
|
426
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
427
|
+
|
|
428
|
+
useEffect(() => { viewRef.current = view; }, [view]);
|
|
429
|
+
|
|
430
|
+
// restore persisted view once on mount
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
try {
|
|
433
|
+
const raw = localStorage.getItem('anet-topo-view');
|
|
434
|
+
if (raw) {
|
|
435
|
+
const v = JSON.parse(raw);
|
|
436
|
+
if (typeof v?.zoom === 'number') {
|
|
437
|
+
setView({
|
|
438
|
+
zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, v.zoom)),
|
|
439
|
+
x: typeof v.x === 'number' ? v.x : 0,
|
|
440
|
+
y: typeof v.y === 'number' ? v.y : 0,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch {}
|
|
445
|
+
}, []);
|
|
446
|
+
|
|
447
|
+
// persist view
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
try { localStorage.setItem('anet-topo-view', JSON.stringify(view)); } catch {}
|
|
450
|
+
}, [view]);
|
|
451
|
+
|
|
452
|
+
// track fullscreen state (button label + Esc-exit sync)
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
const onFsChange = () => setIsFullscreen(document.fullscreenElement === containerRef.current);
|
|
455
|
+
document.addEventListener('fullscreenchange', onFsChange);
|
|
456
|
+
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
|
457
|
+
}, []);
|
|
458
|
+
|
|
459
|
+
// wheel zoom — native non-passive listener so preventDefault() actually
|
|
460
|
+
// stops the page from scrolling. Uses functional setState so the listener
|
|
461
|
+
// never goes stale and can attach once.
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
const svg = svgRef.current;
|
|
464
|
+
if (!svg) return;
|
|
465
|
+
const onWheel = (e: WheelEvent) => {
|
|
466
|
+
e.preventDefault();
|
|
467
|
+
const rect = svg.getBoundingClientRect();
|
|
468
|
+
const mx = ((e.clientX - rect.left) / rect.width) * VIEWBOX_W;
|
|
469
|
+
const my = ((e.clientY - rect.top) / rect.height) * VIEWBOX_H;
|
|
470
|
+
setView(prev => {
|
|
471
|
+
// Round 104 (issue #81 follow-up): Vincent 实测 zoom 太灵敏. The
|
|
472
|
+
// old factor was a fixed 1.15x per wheel *event* — fine for a
|
|
473
|
+
// mouse notch but a trackpad fires dozens of events per gesture
|
|
474
|
+
// so it compounded into huge jumps. Scale the factor by deltaY
|
|
475
|
+
// magnitude with a small coefficient, then clamp the per-event
|
|
476
|
+
// change so no single tick moves more than ~8%.
|
|
477
|
+
const factor = Math.min(1.08, Math.max(0.926, Math.exp(-e.deltaY * 0.0006)));
|
|
478
|
+
const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));
|
|
479
|
+
const ratio = nz / prev.zoom;
|
|
480
|
+
// keep the point under the cursor stationary
|
|
481
|
+
return { zoom: nz, x: mx - (mx - prev.x) * ratio, y: my - (my - prev.y) * ratio };
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
svg.addEventListener('wheel', onWheel, { passive: false });
|
|
485
|
+
return () => svg.removeEventListener('wheel', onWheel);
|
|
486
|
+
}, []);
|
|
487
|
+
|
|
488
|
+
const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
489
|
+
if (e.button !== 0) return;
|
|
490
|
+
(e.currentTarget as Element).setPointerCapture?.(e.pointerId);
|
|
491
|
+
dragRef.current = {
|
|
492
|
+
active: true,
|
|
493
|
+
startX: e.clientX,
|
|
494
|
+
startY: e.clientY,
|
|
495
|
+
baseX: viewRef.current.x,
|
|
496
|
+
baseY: viewRef.current.y,
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
500
|
+
const d = dragRef.current;
|
|
501
|
+
if (!d.active) return;
|
|
502
|
+
const svg = svgRef.current;
|
|
503
|
+
if (!svg) return;
|
|
504
|
+
const rect = svg.getBoundingClientRect();
|
|
505
|
+
const dx = ((e.clientX - d.startX) / rect.width) * VIEWBOX_W;
|
|
506
|
+
const dy = ((e.clientY - d.startY) / rect.height) * VIEWBOX_H;
|
|
507
|
+
setView(prev => ({ ...prev, x: d.baseX + dx, y: d.baseY + dy }));
|
|
508
|
+
};
|
|
509
|
+
const onPointerUp = (e: React.PointerEvent<SVGSVGElement>) => {
|
|
510
|
+
if (!dragRef.current.active) return;
|
|
511
|
+
dragRef.current.active = false;
|
|
512
|
+
try {
|
|
513
|
+
(e.currentTarget as Element).releasePointerCapture?.(e.pointerId);
|
|
514
|
+
} catch {}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// zoom buttons — zoom around the canvas center
|
|
518
|
+
const zoomBy = (factor: number) => {
|
|
519
|
+
setView(prev => {
|
|
520
|
+
const nz = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, prev.zoom * factor));
|
|
521
|
+
const ratio = nz / prev.zoom;
|
|
522
|
+
const cx0 = VIEWBOX_W / 2;
|
|
523
|
+
const cy0 = VIEWBOX_H / 2;
|
|
524
|
+
return { zoom: nz, x: cx0 - (cx0 - prev.x) * ratio, y: cy0 - (cy0 - prev.y) * ratio };
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
const resetView = () => setView({ zoom: 1, x: 0, y: 0 });
|
|
528
|
+
|
|
529
|
+
const toggleFullscreen = () => {
|
|
530
|
+
const el = containerRef.current;
|
|
531
|
+
if (!el) return;
|
|
532
|
+
if (document.fullscreenElement) {
|
|
533
|
+
document.exitFullscreen?.();
|
|
534
|
+
} else {
|
|
535
|
+
const req =
|
|
536
|
+
el.requestFullscreen ||
|
|
537
|
+
(el as unknown as { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen;
|
|
538
|
+
req?.call(el);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
286
542
|
return (
|
|
287
543
|
<section className="w-full max-w-6xl mx-auto mb-8">
|
|
288
544
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between mb-3 px-1">
|
|
@@ -304,12 +560,23 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
304
560
|
</div>
|
|
305
561
|
|
|
306
562
|
<div
|
|
307
|
-
|
|
563
|
+
ref={containerRef}
|
|
564
|
+
className={`relative overflow-hidden rounded-lg border shadow-2xl ${isLight ? 'shadow-zinc-900/5' : 'shadow-cyan-950/30'} ${isFullscreen ? 'flex items-center justify-center' : ''}`}
|
|
308
565
|
style={{ background: pal.containerBg, borderColor: pal.containerBorder }}
|
|
309
566
|
>
|
|
310
567
|
<div className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${pal.topRailGradient}`} />
|
|
311
568
|
|
|
312
|
-
<svg
|
|
569
|
+
<svg
|
|
570
|
+
ref={svgRef}
|
|
571
|
+
viewBox="0 0 1000 680"
|
|
572
|
+
className="w-full h-auto block"
|
|
573
|
+
preserveAspectRatio="xMidYMid meet"
|
|
574
|
+
onPointerDown={onPointerDown}
|
|
575
|
+
onPointerMove={onPointerMove}
|
|
576
|
+
onPointerUp={onPointerUp}
|
|
577
|
+
onPointerLeave={onPointerUp}
|
|
578
|
+
style={{ cursor: 'grab', touchAction: 'none' }}
|
|
579
|
+
>
|
|
313
580
|
<defs>
|
|
314
581
|
<linearGradient id="topo-panel" x1="0" x2="1" y1="0" y2="1">
|
|
315
582
|
<stop offset="0%" stopColor={pal.panelStops[0]} />
|
|
@@ -333,15 +600,90 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
333
600
|
<marker id="topo-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
334
601
|
<path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
|
|
335
602
|
</marker>
|
|
603
|
+
{/* Round 45: radial radar sweep gradient — bright at center
|
|
604
|
+
(where it meets the hub) fading to leading edge. */}
|
|
605
|
+
<radialGradient id="topo-sweep" cx="0%" cy="50%" r="100%">
|
|
606
|
+
<stop offset="0%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.18 : 0.32} />
|
|
607
|
+
<stop offset="70%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.10 : 0.18} />
|
|
608
|
+
<stop offset="100%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity="0" />
|
|
609
|
+
</radialGradient>
|
|
336
610
|
</defs>
|
|
337
611
|
|
|
612
|
+
{/* panel backdrop stays fixed — panning never reveals empty canvas */}
|
|
338
613
|
<rect width="1000" height="680" fill="url(#topo-panel)" />
|
|
614
|
+
|
|
615
|
+
{/* Round 103 (issue #81): everything inside this <g> zooms + pans
|
|
616
|
+
together. transform order = translate then scale. */}
|
|
617
|
+
<g transform={`translate(${view.x} ${view.y}) scale(${view.zoom})`}>
|
|
339
618
|
<circle cx={cx} cy={cy} r="330" fill="url(#topo-radar)" />
|
|
340
619
|
|
|
620
|
+
{/* Round 45: subtle star field — 24 deterministic dots scattered
|
|
621
|
+
across the canvas give the radar bg some depth. Skipped on
|
|
622
|
+
light theme so the white surface stays clean. */}
|
|
623
|
+
{!isLight && (
|
|
624
|
+
<g opacity="0.5">
|
|
625
|
+
{Array.from({ length: 28 }).map((_, i) => {
|
|
626
|
+
// Deterministic pseudo-random scatter so positions are
|
|
627
|
+
// stable between renders (no JS hydration mismatch).
|
|
628
|
+
const seed = i * 9301 + 49297;
|
|
629
|
+
const x = ((seed * 13) % 1000);
|
|
630
|
+
const y = ((seed * 7) % 680);
|
|
631
|
+
const r = (i % 3 === 0) ? 1.2 : 0.7;
|
|
632
|
+
return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} />;
|
|
633
|
+
})}
|
|
634
|
+
</g>
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{/* Round 45: rotating radar sweep — a 40° wedge with a soft
|
|
638
|
+
leading-edge gradient. Slow 6s rotation reads as a radar
|
|
639
|
+
scan without being noisy. Inline transform-origin on the
|
|
640
|
+
<g> wrapper ensures Chrome / Firefox rotate around (cx,cy)
|
|
641
|
+
instead of the SVG viewBox corner. */}
|
|
642
|
+
<g
|
|
643
|
+
style={{
|
|
644
|
+
transformOrigin: `${cx}px ${cy}px`,
|
|
645
|
+
transformBox: 'view-box',
|
|
646
|
+
}}
|
|
647
|
+
className="anet-topo-sweep"
|
|
648
|
+
opacity={isLight ? 0.7 : 1}
|
|
649
|
+
>
|
|
650
|
+
<path
|
|
651
|
+
d={`M ${cx} ${cy} L ${cx + 330} ${cy} A 330 330 0 0 0 ${cx + 330 * Math.cos(-Math.PI / 4.5)} ${cy + 330 * Math.sin(-Math.PI / 4.5)} Z`}
|
|
652
|
+
fill="url(#topo-sweep)"
|
|
653
|
+
/>
|
|
654
|
+
</g>
|
|
655
|
+
|
|
341
656
|
{/* radar rings */}
|
|
342
657
|
{[90, 170, 250, 330].map(radius => (
|
|
343
658
|
<circle key={radius} cx={cx} cy={cy} r={radius} fill="none" stroke={pal.ringStroke} strokeWidth="1" opacity={isLight ? 0.6 : 0.35} />
|
|
344
659
|
))}
|
|
660
|
+
|
|
661
|
+
{/* Round 50: 4 small particles slowly orbiting the outer ring
|
|
662
|
+
(r=330). Each starts at a different angle (offset 0/0.25/0.5/0.75
|
|
663
|
+
of the cycle) so they're evenly spaced. 16s per revolution is
|
|
664
|
+
slow enough to feel ambient, not noisy. Skipped on light theme
|
|
665
|
+
so the white surface stays clean. */}
|
|
666
|
+
{!isLight && [0, 0.25, 0.5, 0.75].map((phase, i) => (
|
|
667
|
+
<g key={`orbit-${i}`}>
|
|
668
|
+
<circle
|
|
669
|
+
cx={cx + 330} cy={cy}
|
|
670
|
+
r={i === 0 ? 2.8 : 2.2}
|
|
671
|
+
fill="#22d3ee"
|
|
672
|
+
opacity={0.9}
|
|
673
|
+
filter="url(#topo-glow)"
|
|
674
|
+
>
|
|
675
|
+
<animateTransform
|
|
676
|
+
attributeName="transform"
|
|
677
|
+
type="rotate"
|
|
678
|
+
from={`${phase * 360} ${cx} ${cy}`}
|
|
679
|
+
to={`${phase * 360 + 360} ${cx} ${cy}`}
|
|
680
|
+
dur="16s"
|
|
681
|
+
repeatCount="indefinite"
|
|
682
|
+
/>
|
|
683
|
+
</circle>
|
|
684
|
+
</g>
|
|
685
|
+
))}
|
|
686
|
+
|
|
345
687
|
{[0, 30, 60, 90, 120, 150].map(angle => (
|
|
346
688
|
<line
|
|
347
689
|
key={angle}
|
|
@@ -355,21 +697,27 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
355
697
|
/>
|
|
356
698
|
))}
|
|
357
699
|
|
|
358
|
-
{/* hub links
|
|
359
|
-
|
|
700
|
+
{/* hub links — round 46: idle spokes now have animated
|
|
701
|
+
stroke-dashoffset so dashes flow outward from the hub
|
|
702
|
+
("command relay" feel). Active spokes carrying live message
|
|
703
|
+
flow stay as solid bright strokes. */}
|
|
704
|
+
{onlineNodes.map((session, idx) => {
|
|
360
705
|
const pos = nodePositions[session.alias];
|
|
361
706
|
if (!pos) return null;
|
|
362
707
|
const path = curvePath({ x: cx, y: cy }, pos, 0);
|
|
708
|
+
const isActiveSpoke = activeAliases.has(session.alias);
|
|
363
709
|
|
|
364
710
|
return (
|
|
365
711
|
<path
|
|
366
712
|
key={`hub-${session.alias}`}
|
|
367
713
|
d={path}
|
|
368
714
|
fill="none"
|
|
369
|
-
stroke={
|
|
370
|
-
strokeWidth={
|
|
371
|
-
strokeDasharray={
|
|
372
|
-
opacity={
|
|
715
|
+
stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
|
|
716
|
+
strokeWidth={isActiveSpoke ? 2 : 1}
|
|
717
|
+
strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
|
|
718
|
+
opacity={isActiveSpoke ? 0.7 : 0.45}
|
|
719
|
+
className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
|
|
720
|
+
style={isActiveSpoke ? undefined : { animationDelay: `${-(idx * 0.25)}s` }}
|
|
373
721
|
/>
|
|
374
722
|
);
|
|
375
723
|
})}
|
|
@@ -412,27 +760,32 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
412
760
|
);
|
|
413
761
|
})}
|
|
414
762
|
|
|
415
|
-
{/* center hub —
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
logo or label. Closes agent-network#5. */}
|
|
763
|
+
{/* center hub — round 39: enlarged from r=6 (12px) to r=10 (20px)
|
|
764
|
+
core so the "control plane" reads as the network's anchor,
|
|
765
|
+
not a stray particle. Static halo ring at r=18 grounds it
|
|
766
|
+
visually; two outward pulses keep the "signal source" idea
|
|
767
|
+
from r17. Light theme gets a paler core to avoid hot-spotting
|
|
768
|
+
the bg. */}
|
|
422
769
|
<g>
|
|
770
|
+
{/* static grounding halo — sits underneath, low opacity */}
|
|
771
|
+
<circle
|
|
772
|
+
cx={cx} cy={cy} r="18"
|
|
773
|
+
fill={isLight ? '#d1fae5' : '#10b981'}
|
|
774
|
+
opacity={isLight ? 0.35 : 0.10}
|
|
775
|
+
/>
|
|
423
776
|
{/* outer pulse 1 */}
|
|
424
|
-
<circle cx={cx} cy={cy} r="
|
|
425
|
-
<animate attributeName="r" values="
|
|
777
|
+
<circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5">
|
|
778
|
+
<animate attributeName="r" values="10;38;10" dur="2.4s" repeatCount="indefinite" />
|
|
426
779
|
<animate attributeName="opacity" values="0.55;0;0.55" dur="2.4s" repeatCount="indefinite" />
|
|
427
780
|
</circle>
|
|
428
781
|
{/* outer pulse 2 (delayed 1.2s) */}
|
|
429
|
-
<circle cx={cx} cy={cy} r="
|
|
430
|
-
<animate attributeName="r" values="
|
|
782
|
+
<circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
|
|
783
|
+
<animate attributeName="r" values="10;38;10" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
|
|
431
784
|
<animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
|
|
432
785
|
</circle>
|
|
433
|
-
{/* core
|
|
434
|
-
<circle cx={cx} cy={cy} r="
|
|
435
|
-
<circle cx={cx} cy={cy} r="
|
|
786
|
+
{/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
|
|
787
|
+
<circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
|
|
788
|
+
<circle cx={cx} cy={cy} r="5" fill="#d1fae5" opacity="0.9" />
|
|
436
789
|
</g>
|
|
437
790
|
|
|
438
791
|
{/* agent nodes */}
|
|
@@ -454,7 +807,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
454
807
|
<animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
|
|
455
808
|
</circle>
|
|
456
809
|
)}
|
|
457
|
-
<circle cx={pos.x} cy={pos.y} r={radius + 8} fill={status.halo} opacity={isOnline ? (isLight ? 0.85 : 0.
|
|
810
|
+
<circle cx={pos.x} cy={pos.y} r={radius + 8} fill={status.halo} opacity={isOnline ? (isLight ? 0.85 : 0.65) : (isLight ? 0.4 : 0.25)} />
|
|
458
811
|
<circle
|
|
459
812
|
cx={pos.x}
|
|
460
813
|
cy={pos.y}
|
|
@@ -465,17 +818,83 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
465
818
|
strokeDasharray={isOnline ? 'none' : '5 5'}
|
|
466
819
|
filter={isOnline && !isLight ? 'url(#topo-glow)' : undefined}
|
|
467
820
|
/>
|
|
468
|
-
|
|
821
|
+
{/* Round 99 (issue #79): node "avatar" — hue-hashed
|
|
822
|
+
initials circle inside the status ring. Round 100:
|
|
823
|
+
when brand=intern, swap the initials for the 书小生
|
|
824
|
+
mascot image (asset from 群星马 task 51dd0d1d). The
|
|
825
|
+
image already has a transparent bg so it sits cleanly
|
|
826
|
+
on the node's dark/light fill; the outer status ring
|
|
827
|
+
still carries working/idle/etc. */}
|
|
828
|
+
{(() => {
|
|
829
|
+
const ar = isOnline ? 14 : 10;
|
|
830
|
+
// Round 108 (issue #79 reopened): show the 书小生 avatar
|
|
831
|
+
// coin for any agent whose alias marks it as an Intern /
|
|
832
|
+
// 书生 runtime — not only under the global ?brand=intern
|
|
833
|
+
// flag. Vincent 4565: a fleet of 书生N号 nodes should SHOW
|
|
834
|
+
// 书生, not a generic "书" initial. ?brand=intern still
|
|
835
|
+
// forces the coin on every node (full brand showcase).
|
|
836
|
+
const isInternNode = isIntern || /书生|书小生|intern/i.test(session.alias);
|
|
837
|
+
if (isInternNode) {
|
|
838
|
+
// Round 102: self-contained "avatar coin" — 书小生 figure
|
|
839
|
+
// keyed off its white background and composited onto a
|
|
840
|
+
// cream circular backplate. The cream coin gives the
|
|
841
|
+
// dark-haired figure consistent contrast on BOTH the
|
|
842
|
+
// dark cyber theme and light theme. Render at the node
|
|
843
|
+
// diameter so the coin fills inside the status ring.
|
|
844
|
+
const size = radius * 2;
|
|
845
|
+
return (
|
|
846
|
+
<image
|
|
847
|
+
href="/intern_avatar.png"
|
|
848
|
+
x={pos.x - size / 2}
|
|
849
|
+
y={pos.y - size / 2}
|
|
850
|
+
width={size}
|
|
851
|
+
height={size}
|
|
852
|
+
preserveAspectRatio="xMidYMid meet"
|
|
853
|
+
/>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
// Round 106 (issue #83): hue keyed to the prefix group,
|
|
857
|
+
// not the full alias — every 通信* node shares one color,
|
|
858
|
+
// every 研究员* another, so teams cluster visually even
|
|
859
|
+
// when the tier layout spreads them across rings.
|
|
860
|
+
const c = aliasAvatarColors(groupKeys[session.alias] || session.alias);
|
|
861
|
+
const fs = isOnline ? 14 : 10;
|
|
862
|
+
return (
|
|
863
|
+
<>
|
|
864
|
+
<circle cx={pos.x} cy={pos.y} r={ar} fill={c.bg} stroke={c.ring} strokeWidth="1" />
|
|
865
|
+
<text
|
|
866
|
+
x={pos.x}
|
|
867
|
+
y={pos.y}
|
|
868
|
+
dy="0.34em"
|
|
869
|
+
textAnchor="middle"
|
|
870
|
+
fill={c.text}
|
|
871
|
+
fontSize={fs}
|
|
872
|
+
fontFamily="monospace"
|
|
873
|
+
fontWeight="700"
|
|
874
|
+
>
|
|
875
|
+
{aliasInitial(session.alias)}
|
|
876
|
+
</text>
|
|
877
|
+
</>
|
|
878
|
+
);
|
|
879
|
+
})()}
|
|
469
880
|
{session.status === 'working' && (
|
|
470
|
-
|
|
881
|
+
// working pulse — small dot just inside the status ring,
|
|
882
|
+
// above the avatar text. Was a 18px horizontal bar at
|
|
883
|
+
// y+11 which now collides with the avatar; the dot reads
|
|
884
|
+
// as the same "active" cue without occluding the initial.
|
|
885
|
+
<circle cx={pos.x} cy={pos.y - (isOnline ? 18 : 13)} r="2.5" fill={pal.flowParticle}>
|
|
471
886
|
<animate attributeName="opacity" values="1;0.25;1" dur="1.1s" repeatCount="indefinite" />
|
|
472
|
-
</
|
|
887
|
+
</circle>
|
|
473
888
|
)}
|
|
474
889
|
|
|
890
|
+
{/* Round 98 (issue #61): label rect 124px → 100px so a
|
|
891
|
+
3-ring layout (inner r=145, 8 nodes) still has room for
|
|
892
|
+
the label without overlapping a neighbour. Truncate
|
|
893
|
+
tightened from 14 → 12 chars to match. */}
|
|
475
894
|
<g transform={`translate(${pos.x}, ${pos.y + radius + 22})`}>
|
|
476
|
-
<rect x="-
|
|
895
|
+
<rect x="-50" y="-14" width="100" height="42" rx="6" fill={pal.labelBox.fill} stroke={pal.labelBox.stroke} opacity={isLight ? 1 : 0.94} />
|
|
477
896
|
<text x="0" y="1" textAnchor="middle" fill={status.text} fontSize="12" fontFamily="monospace" fontWeight="700">
|
|
478
|
-
{truncate(session.alias,
|
|
897
|
+
{truncate(session.alias, 12)}
|
|
479
898
|
</text>
|
|
480
899
|
<text x="0" y="17" textAnchor="middle" fill={status.primary} fontSize="9" fontFamily="monospace">
|
|
481
900
|
{status.label}{isOnline && sseCountFor != null ? ` sse:${sseCountFor}` : ''}
|
|
@@ -508,7 +927,74 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
|
|
|
508
927
|
<text x="34" y="82" fill={pal.legendText} fontSize="11" fontFamily="monospace">offline / no SSE</text>
|
|
509
928
|
<path d="M150,78 Q176,52 210,78" fill="none" stroke={pal.flowEdge} strokeWidth="3" markerEnd="url(#topo-arrow)" />
|
|
510
929
|
</g>
|
|
930
|
+
</g>
|
|
511
931
|
</svg>
|
|
932
|
+
|
|
933
|
+
{/* Round 103 (issue #81): zoom / pan / fullscreen controls — HTML
|
|
934
|
+
overlay so they stay fixed while the SVG content transforms.
|
|
935
|
+
Round 104: Vincent 实测 — the reset action used to be hidden
|
|
936
|
+
behind the "%" label (looked like an indicator, not a button).
|
|
937
|
+
Split into a plain % readout + an explicit reset button with
|
|
938
|
+
its own icon + tooltip. */}
|
|
939
|
+
<div className="absolute bottom-3 right-3 flex items-center gap-1.5 text-xs select-none">
|
|
940
|
+
<div
|
|
941
|
+
className="flex items-center rounded-md border overflow-hidden"
|
|
942
|
+
style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder }}
|
|
943
|
+
>
|
|
944
|
+
<button
|
|
945
|
+
onClick={() => zoomBy(1 / 1.2)}
|
|
946
|
+
className="px-2 py-1 hover:bg-white/5 transition-colors"
|
|
947
|
+
style={{ color: pal.legendText }}
|
|
948
|
+
aria-label="Zoom out"
|
|
949
|
+
>
|
|
950
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden><path d="M5 12h14" /></svg>
|
|
951
|
+
</button>
|
|
952
|
+
<span
|
|
953
|
+
className="px-2 py-1 tabular-nums border-x text-center"
|
|
954
|
+
style={{ color: pal.legendText, borderColor: pal.containerBorder, minWidth: 46 }}
|
|
955
|
+
title="Current zoom level"
|
|
956
|
+
>
|
|
957
|
+
{Math.round(view.zoom * 100)}%
|
|
958
|
+
</span>
|
|
959
|
+
<button
|
|
960
|
+
onClick={() => zoomBy(1.2)}
|
|
961
|
+
className="px-2 py-1 hover:bg-white/5 transition-colors"
|
|
962
|
+
style={{ color: pal.legendText }}
|
|
963
|
+
aria-label="Zoom in"
|
|
964
|
+
>
|
|
965
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden><path d="M12 5v14M5 12h14" /></svg>
|
|
966
|
+
</button>
|
|
967
|
+
</div>
|
|
968
|
+
<button
|
|
969
|
+
onClick={resetView}
|
|
970
|
+
className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
|
|
971
|
+
style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
|
|
972
|
+
aria-label="Reset view"
|
|
973
|
+
title="Reset zoom + pan (or double-click the canvas)"
|
|
974
|
+
>
|
|
975
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
976
|
+
<path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-6.4 2.6L3 8" />
|
|
977
|
+
<path d="M3 3v5h5" />
|
|
978
|
+
</svg>
|
|
979
|
+
</button>
|
|
980
|
+
<button
|
|
981
|
+
onClick={toggleFullscreen}
|
|
982
|
+
className="p-1.5 rounded-md border hover:bg-white/5 transition-colors"
|
|
983
|
+
style={{ background: pal.legendBox.fill, borderColor: pal.containerBorder, color: pal.legendText }}
|
|
984
|
+
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
985
|
+
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
986
|
+
>
|
|
987
|
+
{isFullscreen ? (
|
|
988
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
989
|
+
<path d="M8 3v4a1 1 0 0 1-1 1H3M21 8h-4a1 1 0 0 1-1-1V3M3 16h4a1 1 0 0 1 1 1v4M16 21v-4a1 1 0 0 1 1-1h4" />
|
|
990
|
+
</svg>
|
|
991
|
+
) : (
|
|
992
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
993
|
+
<path d="M3 8V5a2 2 0 0 1 2-2h3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M21 16v3a2 2 0 0 1-2 2h-3" />
|
|
994
|
+
</svg>
|
|
995
|
+
)}
|
|
996
|
+
</button>
|
|
997
|
+
</div>
|
|
512
998
|
</div>
|
|
513
999
|
</section>
|
|
514
1000
|
);
|