@sleep2agi/agent-network-dashboard 0.6.3-preview.49 → 0.6.3-preview.50
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_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +97 -97
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +5 -5
- package/.next/server/app/_not-found.rsc +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +5 -5
- 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 +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- 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 +5 -5
- 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 +8 -8
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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 +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +5 -5
- 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 +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- 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.js.nft.json +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +5 -5
- 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 +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- 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.js.nft.json +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +5 -5
- 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 +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- 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/[alias]/page.js.nft.json +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +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 +5 -5
- 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 +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- 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.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +5 -5
- package/.next/server/app/providers.rsc +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +5 -5
- 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 +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- 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/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +5 -5
- package/.next/server/app/servers.rsc +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +5 -5
- 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 +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +5 -5
- 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 +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- 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 +5 -5
- 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 +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- 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.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +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 +5 -5
- 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 +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- 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]__05z_qq6._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__05z_qq6._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0e7vp44._.js → dash-stageb-wt_app_045dcq5._.js} +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_045dcq5._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0g~a.7i._.js → dash-stageb-wt_app_05u4_h~._.js} +8 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js.map +1 -0
- package/.next/server/chunks/ssr/{dash-stageb-wt_app_0qpn85e._.js → dash-stageb-wt_app_0nuizh~._.js} +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0nuizh~._.js.map +1 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js +5 -5
- package/.next/server/chunks/ssr/dash-stageb-wt_app_components_0vzsmx1._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +3 -0
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +5 -5
- 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/02.5_nifak.uu.js +1 -0
- package/.next/static/chunks/03hzj129k~odb.js +1 -0
- package/.next/static/chunks/06q7-vhy~1-y3.js +7 -0
- package/.next/static/chunks/0_4ljrbp.stxy.js +1 -0
- package/.next/static/chunks/0cey4i.y0a8~j.js +1 -0
- package/.next/static/chunks/{0i8qbq4v47v3r.js → 0ep78sq90x_yc.js} +1 -1
- package/.next/static/chunks/{0_xyyy_x0k42j.js → 0hm52b0y~k0~z.js} +1 -1
- package/.next/static/chunks/0kncmrm2a_q43.js +1 -0
- package/.next/static/chunks/0mhaf6lrw5nn..css +1 -0
- package/.next/static/chunks/{12jbb28xy2hw..js → 0qf8xjihw3zi..js} +1 -1
- package/.next/static/chunks/0xyu92zdvvghl.js +7 -0
- package/.next/static/chunks/0zj_navpyhltx.js +2 -0
- package/.next/static/chunks/10px7ehmo7eq0.js +1 -0
- package/.next/static/chunks/{0tv-.l-_mw4ye.js → 11zz15q37~bz5.js} +1 -1
- package/.next/static/chunks/14y4x3de1lbug.js +1 -0
- package/.next/static/chunks/15ka68xi.w-i-.js +1 -0
- package/.next/static/chunks/17qkc714xy04..js +1 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +2 -2
- package/app/components/NodeList.tsx +40 -4
- package/app/components/Sidebar.tsx +180 -29
- package/app/components/TaskChatPanel.tsx +1 -1
- package/app/components/TaskDetail.tsx +407 -0
- package/app/components/TaskList.tsx +117 -0
- package/app/nodes/NodesView.tsx +28 -3
- package/app/tasks/TasksView.tsx +353 -0
- package/app/tasks/[id]/page.tsx +23 -176
- package/app/tasks/page.tsx +10 -453
- package/package.json +1 -1
- package/tests/e2e-sidebar-default.spec.ts +141 -0
- package/tests/e2e-sidebar-icon-rail.spec.ts +261 -0
- package/tests/e2e-stage-b-chat-pane.spec.ts +286 -0
- package/tests/e2e-stage-tasks-two-column.spec.ts +353 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0b-b6yh._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0fs-j~c._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0ma5-5u._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0~hwpo-._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_095q0~q._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0e7vp44._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0g~a.7i._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qpn85e._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js +0 -9
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0tppjr_._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0y764mt._.js.map +0 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js +0 -3
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_page_tsx_0.fhtup._.js.map +0 -1
- package/.next/static/chunks/027yjbjm9afuk.js +0 -1
- package/.next/static/chunks/07wf61~wof.l~.js +0 -1
- package/.next/static/chunks/0ev252k-tf_aa.css +0 -1
- package/.next/static/chunks/0j-jjs3w89dei.js +0 -1
- package/.next/static/chunks/0ko39v-gezo7d.js +0 -1
- package/.next/static/chunks/0l8vf4q2t5x4d.js +0 -7
- package/.next/static/chunks/0xcazs6t8~l4q.js +0 -2
- package/.next/static/chunks/10sr651~7vm42.js +0 -7
- package/.next/static/chunks/13_i5b0qmko7z.js +0 -1
- package/.next/static/chunks/140suz13o9va6.js +0 -1
- package/.next/static/chunks/144cf7ds3_9~b.js +0 -1
- package/.next/static/chunks/17c4wlsbmmgk9.js +0 -1
- package/.next/static/chunks/17cfly-04z1j0.js +0 -1
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_buildManifest.js +0 -0
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{HIKVEkFShWE481BxQDjwH → vc_aJHtYwefsVcl2_f14Z}/_ssgManifest.js +0 -0
|
@@ -8,6 +8,35 @@ import useSWR from 'swr';
|
|
|
8
8
|
import { useNetworkId } from '../lib/network-context';
|
|
9
9
|
import { isOnline as presenceIsOnline, presenceStatus } from '../lib/presence';
|
|
10
10
|
import { useHealth } from '../lib/hooks';
|
|
11
|
+
import { useChatUnread, badgeLabel } from '../lib/chat-unread';
|
|
12
|
+
|
|
13
|
+
// Sidebar has THREE render modes (通信龙 07-31 brief, dashboard 类飞书):
|
|
14
|
+
// 'expanded' — w-52, icons + labels. Original default; unchanged.
|
|
15
|
+
// 'collapsed' — w-16, icons only, native `title=` tooltips. Existing
|
|
16
|
+
// pre-icon-rail form; kept intact for users on it.
|
|
17
|
+
// 'icon-rail' — w-14, Feishu-style narrow icon-only rail with
|
|
18
|
+
// UNREAD BADGES on nav items that can carry them, and
|
|
19
|
+
// tighter spacing. Distinguishes from 'collapsed' by
|
|
20
|
+
// the badges + width — collapsed has neither.
|
|
21
|
+
//
|
|
22
|
+
// Cycle order (single toggle button, bottom-left arrow): expanded →
|
|
23
|
+
// collapsed → icon-rail → expanded. localStorage key `anet-sidebar-mode`
|
|
24
|
+
// persists the choice across reloads. Migration: pre-existing users
|
|
25
|
+
// have no key set → default to 'expanded' (backward compat: nobody who
|
|
26
|
+
// hadn't touched it gets a surprise on next visit).
|
|
27
|
+
//
|
|
28
|
+
// The `collapsed` bool that existing markup consumes is derived
|
|
29
|
+
// (`mode !== 'expanded'`) — so all pre-icon-rail styling paths keep
|
|
30
|
+
// working exactly as before, with icon-rail adding only its extras
|
|
31
|
+
// (narrower width class + badges + a data attribute for tests).
|
|
32
|
+
type SidebarMode = 'expanded' | 'collapsed' | 'icon-rail';
|
|
33
|
+
const SIDEBAR_MODES: readonly SidebarMode[] = ['expanded', 'collapsed', 'icon-rail'] as const;
|
|
34
|
+
const SIDEBAR_MODE_STORAGE = 'anet-sidebar-mode';
|
|
35
|
+
const SIDEBAR_MODE_LABEL: Record<SidebarMode, string> = {
|
|
36
|
+
expanded: '展开',
|
|
37
|
+
collapsed: '折叠',
|
|
38
|
+
'icon-rail': '图标',
|
|
39
|
+
};
|
|
11
40
|
|
|
12
41
|
const networkFetcher = (url: string) => fetch(url).then(r => r.ok ? r.json() : { networks: [] });
|
|
13
42
|
|
|
@@ -38,7 +67,45 @@ const ROLE_ICON: Record<string, string> = { owner: '⭐', admin: '🔧', member:
|
|
|
38
67
|
|
|
39
68
|
export function Sidebar() {
|
|
40
69
|
const pathname = usePathname();
|
|
41
|
-
|
|
70
|
+
// Default = 'icon-rail' per Vincent 07-31 "飞书同版体验": nobody clicks
|
|
71
|
+
// a toggle to make their sidebar narrow — the narrow form IS the
|
|
72
|
+
// experience. Existing user preferences (whoever has already picked
|
|
73
|
+
// expanded or collapsed via the cycle button) are preserved via the
|
|
74
|
+
// useEffect below, which is the ONLY thing standing between "sane
|
|
75
|
+
// new-user default" and "silently overwriting an existing choice" —
|
|
76
|
+
// the second failure is the one that reads to users as "the app
|
|
77
|
+
// forgot my setting" and it has no error surface, so it needs its
|
|
78
|
+
// own witnessed-red assertion (see e2e-sidebar-default.spec.ts).
|
|
79
|
+
const [mode, setMode] = useState<SidebarMode>('icon-rail');
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
try {
|
|
82
|
+
const v = localStorage.getItem(SIDEBAR_MODE_STORAGE);
|
|
83
|
+
if (v && (SIDEBAR_MODES as readonly string[]).includes(v)) setMode(v as SidebarMode);
|
|
84
|
+
} catch {}
|
|
85
|
+
}, []);
|
|
86
|
+
const cycleMode = () => {
|
|
87
|
+
setMode(current => {
|
|
88
|
+
const idx = SIDEBAR_MODES.indexOf(current);
|
|
89
|
+
const next = SIDEBAR_MODES[(idx + 1) % SIDEBAR_MODES.length];
|
|
90
|
+
try { localStorage.setItem(SIDEBAR_MODE_STORAGE, next); } catch {}
|
|
91
|
+
return next;
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
// Derived: existing markup uses `collapsed` (any non-expanded state
|
|
95
|
+
// hides text + narrows). icon-rail behaves like collapsed by default
|
|
96
|
+
// but adds badges + narrower width class further down.
|
|
97
|
+
const collapsed = mode !== 'expanded';
|
|
98
|
+
const iconRail = mode === 'icon-rail';
|
|
99
|
+
// Only pulled in icon-rail mode so the SWR fetch cost (which happens
|
|
100
|
+
// anyway on other pages via useChatUnread) isn't duplicated for
|
|
101
|
+
// no visual gain when badges aren't drawn.
|
|
102
|
+
const { totalUnread } = useChatUnread();
|
|
103
|
+
// Which nav entries carry a badge in icon-rail mode. Kept as an
|
|
104
|
+
// exact map (not a fuzzy lookup) so a future nav-item rename can't
|
|
105
|
+
// silently orphan a badge onto the wrong route.
|
|
106
|
+
const NAV_BADGE: Partial<Record<string, number>> = {
|
|
107
|
+
'/nodes': totalUnread,
|
|
108
|
+
};
|
|
42
109
|
// Theme switcher (Vincent 07-20 "主题还不能切换吗" — the Settings entry was
|
|
43
110
|
// too buried; a sidebar control is findable from every page). Cycle order
|
|
44
111
|
// matches Settings: 深色(默认) → 浅色 → Slack.
|
|
@@ -86,26 +153,95 @@ export function Sidebar() {
|
|
|
86
153
|
};
|
|
87
154
|
|
|
88
155
|
const nav = (
|
|
89
|
-
<nav
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
<nav
|
|
157
|
+
className={`flex flex-col ${iconRail ? 'gap-2 px-0 py-2 items-center' : 'gap-1 px-2 py-4'}`}
|
|
158
|
+
data-sidebar-nav
|
|
159
|
+
>
|
|
160
|
+
{NAV_ITEMS.map(item => {
|
|
161
|
+
const badge = iconRail ? (NAV_BADGE[item.href] ?? 0) : 0;
|
|
162
|
+
const active = isActive(item.href);
|
|
163
|
+
if (iconRail) {
|
|
164
|
+
// SPEC v1 (设计负责人, 07-31, /home/vansin/dashboard-ui-ref/SPEC.md §3):
|
|
165
|
+
// - Container: 44×44 tap area (icon 24 + 10px padding all sides)
|
|
166
|
+
// - Icon SVG: 24×24, centered
|
|
167
|
+
// - Default icon color: #64748B; hover: #E2E8F0 + bg rgba(255,255,255,0.04)
|
|
168
|
+
// - Selected: icon #7DD3FC + bg rgba(125,211,252,0.12) rounded 8px 40×40 + 3px left bar length 20px
|
|
169
|
+
// - Gap between items: 8px (nav's gap-2)
|
|
170
|
+
// - No border between rail and next column (contrast via bg brightness)
|
|
171
|
+
return (
|
|
172
|
+
<Link
|
|
173
|
+
key={item.href}
|
|
174
|
+
href={item.href}
|
|
175
|
+
prefetch={false}
|
|
176
|
+
onClick={() => setMobileOpen(false)}
|
|
177
|
+
title={item.label}
|
|
178
|
+
aria-label={item.label}
|
|
179
|
+
data-nav-item
|
|
180
|
+
data-nav-href={item.href}
|
|
181
|
+
data-nav-active={active ? 'true' : undefined}
|
|
182
|
+
data-badge={badge > 0 ? String(badge) : undefined}
|
|
183
|
+
className="relative flex items-center justify-center w-11 h-11 group anet-nav-item-rail"
|
|
184
|
+
>
|
|
185
|
+
{/* 3px left indicator bar for selected — 20px length, 2px
|
|
186
|
+
round, matches spec. Positioned absolute so it hangs
|
|
187
|
+
off the container without moving the icon. */}
|
|
188
|
+
{active && (
|
|
189
|
+
<span
|
|
190
|
+
aria-hidden
|
|
191
|
+
data-nav-active-bar
|
|
192
|
+
className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-[2px] bg-[#7DD3FC]"
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
{/* 40×40 icon background square (selected only) — inner
|
|
196
|
+
visual, centered inside the 44×44 tap container. */}
|
|
197
|
+
<span
|
|
198
|
+
className={`flex items-center justify-center w-10 h-10 rounded-lg transition-colors ${
|
|
199
|
+
active
|
|
200
|
+
? 'bg-[rgba(125,211,252,0.12)] text-[#7DD3FC]'
|
|
201
|
+
: 'text-[#64748B] hover:text-[#E2E8F0] hover:bg-[rgba(255,255,255,0.04)]'
|
|
202
|
+
}`}
|
|
203
|
+
>
|
|
204
|
+
<svg className="w-6 h-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
205
|
+
<path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
|
|
206
|
+
</svg>
|
|
207
|
+
</span>
|
|
208
|
+
{badge > 0 && (
|
|
209
|
+
<span
|
|
210
|
+
aria-label={`${item.label} — ${badgeLabel(badge)} unread`}
|
|
211
|
+
data-nav-badge
|
|
212
|
+
className="absolute top-0 right-0 min-w-[16px] h-4 px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-4 text-center pointer-events-none"
|
|
213
|
+
>
|
|
214
|
+
{badgeLabel(badge)}
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
</Link>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
// Non-icon-rail (expanded / collapsed) — unchanged legacy behavior.
|
|
221
|
+
return (
|
|
222
|
+
<Link
|
|
223
|
+
key={item.href}
|
|
224
|
+
href={item.href}
|
|
225
|
+
prefetch={false}
|
|
226
|
+
onClick={() => setMobileOpen(false)}
|
|
227
|
+
title={collapsed ? item.label : undefined}
|
|
228
|
+
aria-label={item.label}
|
|
229
|
+
data-nav-item
|
|
230
|
+
data-nav-href={item.href}
|
|
231
|
+
data-nav-active={active ? 'true' : undefined}
|
|
232
|
+
className={`relative flex items-center gap-3 rounded-lg text-sm transition-colors active:bg-[#232327] ${
|
|
233
|
+
active
|
|
234
|
+
? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/20 anet-nav-active'
|
|
235
|
+
: 'text-gray-400 hover:text-gray-200 hover:bg-[#1c1c1f]'
|
|
236
|
+
} ${collapsed ? 'justify-center px-0 py-3' : 'px-3 py-3'}`}
|
|
237
|
+
>
|
|
238
|
+
<svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
239
|
+
<path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
|
|
240
|
+
</svg>
|
|
241
|
+
{!collapsed && <span>{item.label}</span>}
|
|
242
|
+
</Link>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
109
245
|
</nav>
|
|
110
246
|
);
|
|
111
247
|
|
|
@@ -142,10 +278,14 @@ export function Sidebar() {
|
|
|
142
278
|
in light themes so the sidebar reads as its own card.
|
|
143
279
|
Round 47: explicit ease-out curve + slight shadow so the drawer
|
|
144
280
|
edge "leaves a trail" as it slides in. */}
|
|
145
|
-
<aside
|
|
146
|
-
|
|
281
|
+
<aside
|
|
282
|
+
data-anet-sidebar="true"
|
|
283
|
+
data-sidebar-mode={mode}
|
|
284
|
+
data-total-unread={totalUnread}
|
|
285
|
+
className={`
|
|
286
|
+
fixed top-0 left-0 h-full z-40
|
|
147
287
|
transition-transform duration-200 ease-out
|
|
148
|
-
${
|
|
288
|
+
${mode === 'expanded' ? 'w-52 bg-[#111113] border-r border-[#26262b]' : mode === 'icon-rail' ? 'w-14 bg-[#0A0C14]' : 'w-16 bg-[#111113] border-r border-[#26262b]'}
|
|
149
289
|
${mobileOpen ? 'translate-x-0 shadow-2xl shadow-black/40 lg:shadow-none' : '-translate-x-full'}
|
|
150
290
|
lg:translate-x-0 lg:static lg:shrink-0 lg:shadow-none
|
|
151
291
|
`}>
|
|
@@ -252,12 +392,23 @@ export function Sidebar() {
|
|
|
252
392
|
</button>
|
|
253
393
|
<div className={`flex items-center ${collapsed ? 'flex-col px-0 py-2' : 'px-3 py-2 justify-end'}`}>
|
|
254
394
|
<button
|
|
255
|
-
onClick={
|
|
256
|
-
title={
|
|
257
|
-
|
|
258
|
-
|
|
395
|
+
onClick={cycleMode}
|
|
396
|
+
title={`当前:${SIDEBAR_MODE_LABEL[mode]} — 点击切换(展开 → 折叠 → 图标)`}
|
|
397
|
+
data-testid="sidebar-mode-cycle"
|
|
398
|
+
className="hidden lg:flex p-1.5 rounded text-gray-600 hover:text-gray-400 transition-colors items-center gap-1"
|
|
399
|
+
aria-label={`Cycle sidebar mode (current: ${mode})`}
|
|
259
400
|
>
|
|
260
|
-
|
|
401
|
+
{/* Chevron rotates to hint at direction — expanded →
|
|
402
|
+
points left (will collapse); collapsed → left too
|
|
403
|
+
(will narrow further); icon-rail → right (will
|
|
404
|
+
expand back to full width, wrapping the cycle). */}
|
|
405
|
+
<svg
|
|
406
|
+
className={`w-4 h-4 transition-transform ${mode === 'icon-rail' ? 'rotate-180' : ''}`}
|
|
407
|
+
fill="none"
|
|
408
|
+
viewBox="0 0 24 24"
|
|
409
|
+
stroke="currentColor"
|
|
410
|
+
strokeWidth={2}
|
|
411
|
+
>
|
|
261
412
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
262
413
|
</svg>
|
|
263
414
|
</button>
|
|
@@ -1260,7 +1260,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
1260
1260
|
mobile and the per-pair grouping (line 540) from space-y-2
|
|
1261
1261
|
to space-y-1.5 so messages read denser without losing the
|
|
1262
1262
|
speaker-turn rhythm. Desktop unchanged at sm: and up. */}
|
|
1263
|
-
<div ref={scrollRef} onScroll={onMessagesScroll} className="flex-1 overflow-y-auto px-3 py-3 sm:px-4 sm:py-4 space-y-2 sm:space-y-4">
|
|
1263
|
+
<div ref={scrollRef} onScroll={onMessagesScroll} data-testid="chat-messages-scroll" className="flex-1 overflow-y-auto px-3 py-3 sm:px-4 sm:py-4 space-y-2 sm:space-y-4">
|
|
1264
1264
|
{searchOpen ? (
|
|
1265
1265
|
<div className="sticky top-0 z-20 -mx-1 flex items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--bg-elevated)]/95 px-2 py-1.5 shadow-lg shadow-black/30 backdrop-blur">
|
|
1266
1266
|
<input
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import useSWR from 'swr';
|
|
5
|
+
import { AliasAvatar } from './AliasAvatar';
|
|
6
|
+
import { STATUS_CHIP_CLASS as STATUS_CHIP } from '../lib/status';
|
|
7
|
+
|
|
8
|
+
// Detail pane for the /tasks two-column layout.
|
|
9
|
+
//
|
|
10
|
+
// Feature-parity target (通信龙 07-31 parity 硬门, migrating from the
|
|
11
|
+
// old full-page /tasks/[id]/page.tsx): five blocks, each with its own
|
|
12
|
+
// positive assertion in tests + real data visible (not just "the div
|
|
13
|
+
// is there"):
|
|
14
|
+
// 1. Timeline card — steps: created / delivered / started / completed
|
|
15
|
+
// 2. Info card — task_id / from / to / priority / status / expires
|
|
16
|
+
// 3. Content
|
|
17
|
+
// 4. Result
|
|
18
|
+
// 5. Events feed — /api/hub/task-events?limit=50 (10s poll)
|
|
19
|
+
//
|
|
20
|
+
// Poll cadence — the old full-page useSWR was 5s (task) + 10s (events).
|
|
21
|
+
// Do NOT widen either interval: "page's there but data's stale" is worse
|
|
22
|
+
// than "page's not there" because it looks fine to anyone glancing at it.
|
|
23
|
+
// The intervals are part of parity — tests should assert both refetch.
|
|
24
|
+
|
|
25
|
+
interface TaskDetailData {
|
|
26
|
+
task_id: string;
|
|
27
|
+
network_id?: string;
|
|
28
|
+
from_name: string;
|
|
29
|
+
to_name: string;
|
|
30
|
+
status: string;
|
|
31
|
+
priority: string;
|
|
32
|
+
content: string;
|
|
33
|
+
result: string;
|
|
34
|
+
created_at: string;
|
|
35
|
+
updated_at: string;
|
|
36
|
+
delivered_at: string;
|
|
37
|
+
started_at: string;
|
|
38
|
+
completed_at: string;
|
|
39
|
+
expires_at: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TaskEvent {
|
|
43
|
+
id: number;
|
|
44
|
+
event_type: string;
|
|
45
|
+
from_status: string;
|
|
46
|
+
to_status: string;
|
|
47
|
+
detail: string;
|
|
48
|
+
created_at: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 🔴 Poll cadences are declared as module-scope constants so the useSWR
|
|
52
|
+
// refreshInterval and the DOM `data-poll-*-ms` attribute are *literally*
|
|
53
|
+
// the same number. Two separate literals meant the attribute-based test
|
|
54
|
+
// assertion could go green even if someone widened refreshInterval —
|
|
55
|
+
// the assertion was watching the string, not the fact.
|
|
56
|
+
// (通信龙 07-31 catch: "断言的是声明, 不是事实.")
|
|
57
|
+
// Widen either interval only alongside a paired update to the test
|
|
58
|
+
// asserting cadence, and re-run witnessed-red by bumping one of these
|
|
59
|
+
// to a wrong value.
|
|
60
|
+
const POLL_TASK_MS = 5000;
|
|
61
|
+
const POLL_EVENTS_MS = 10000;
|
|
62
|
+
|
|
63
|
+
function timeAgo(dateStr: string): string {
|
|
64
|
+
if (!dateStr) return '--';
|
|
65
|
+
const diff = Date.now() - new Date(dateStr.replace(' ', 'T') + 'Z').getTime();
|
|
66
|
+
const s = Math.floor(diff / 1000);
|
|
67
|
+
if (s < 60) return `${s}s ago`;
|
|
68
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
69
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
70
|
+
return `${Math.floor(s / 86400)}d ago`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function priorityClass(priority: string) {
|
|
74
|
+
if (priority === 'high') return 'text-red-400';
|
|
75
|
+
if (priority === 'low') return 'text-gray-600';
|
|
76
|
+
return 'text-gray-400';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fetcher = async (url: string) => {
|
|
80
|
+
const res = await fetch(url);
|
|
81
|
+
if (res.status === 401) {
|
|
82
|
+
window.location.assign('/login');
|
|
83
|
+
throw new Error('unauthorized');
|
|
84
|
+
}
|
|
85
|
+
return res.json();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function TaskDetail({
|
|
89
|
+
taskId,
|
|
90
|
+
onClose,
|
|
91
|
+
onRetried,
|
|
92
|
+
}: {
|
|
93
|
+
taskId: string;
|
|
94
|
+
/** X in the header — desktop returns pane to empty, mobile dismisses overlay.
|
|
95
|
+
* Either way the URL drops back to /tasks (parent handles). Preserves the
|
|
96
|
+
* 返回入口 the old full-page detail had via its "← Tasks" link. */
|
|
97
|
+
onClose?: () => void;
|
|
98
|
+
/** After a successful retry so the parent list can refetch. */
|
|
99
|
+
onRetried?: () => void;
|
|
100
|
+
}) {
|
|
101
|
+
// 🔴 Poll intervals are part of parity: 5s task, 10s events. Do not widen
|
|
102
|
+
// without updating the corresponding cadence assertion.
|
|
103
|
+
const { data: taskData, isLoading, error, mutate: refetchTask } = useSWR<{
|
|
104
|
+
tasks: TaskDetailData[];
|
|
105
|
+
}>(
|
|
106
|
+
`/api/hub/tasks?task_id=${encodeURIComponent(taskId)}`,
|
|
107
|
+
fetcher,
|
|
108
|
+
{ refreshInterval: POLL_TASK_MS },
|
|
109
|
+
);
|
|
110
|
+
const { data: eventsData } = useSWR<{ events: TaskEvent[] }>(
|
|
111
|
+
`/api/hub/task-events?task_id=${encodeURIComponent(taskId)}&limit=50`,
|
|
112
|
+
fetcher,
|
|
113
|
+
{ refreshInterval: POLL_EVENTS_MS },
|
|
114
|
+
);
|
|
115
|
+
const [retrying, setRetrying] = useState(false);
|
|
116
|
+
|
|
117
|
+
const task = taskData?.tasks?.[0];
|
|
118
|
+
const events = eventsData?.events || [];
|
|
119
|
+
|
|
120
|
+
const doRetry = async () => {
|
|
121
|
+
if (!task) return;
|
|
122
|
+
setRetrying(true);
|
|
123
|
+
try {
|
|
124
|
+
await fetch('/api/hub/send', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
alias: task.to_name,
|
|
129
|
+
task: task.content,
|
|
130
|
+
priority: task.priority,
|
|
131
|
+
...(task.network_id ? { network_id: task.network_id } : {}),
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
refetchTask();
|
|
135
|
+
onRetried?.();
|
|
136
|
+
} finally {
|
|
137
|
+
setRetrying(false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const timelineSteps: { label: string; time: string; color: string }[] = task
|
|
142
|
+
? [
|
|
143
|
+
{ label: 'Created', time: task.created_at, color: 'text-gray-400' },
|
|
144
|
+
{ label: 'Delivered', time: task.delivered_at, color: 'text-blue-400' },
|
|
145
|
+
{ label: 'Started', time: task.started_at, color: 'text-green-400' },
|
|
146
|
+
{ label: 'Completed', time: task.completed_at, color: 'text-purple-400' },
|
|
147
|
+
]
|
|
148
|
+
: [];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
data-testid="task-detail-pane"
|
|
153
|
+
data-task-id={taskId}
|
|
154
|
+
data-poll-task-ms={String(POLL_TASK_MS)}
|
|
155
|
+
data-poll-events-ms={String(POLL_EVENTS_MS)}
|
|
156
|
+
className="flex flex-col h-full max-h-screen overflow-hidden"
|
|
157
|
+
>
|
|
158
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[#1c1c1f]">
|
|
159
|
+
<div className="min-w-0 flex-1 flex items-center gap-2">
|
|
160
|
+
<div className="text-xs text-gray-500 truncate" title={taskId}>
|
|
161
|
+
<span className="text-gray-600">Task ID:</span>{' '}
|
|
162
|
+
<span className="font-mono">{taskId.slice(0, 12)}…</span>
|
|
163
|
+
</div>
|
|
164
|
+
{task && (
|
|
165
|
+
<span
|
|
166
|
+
data-testid="task-detail-status"
|
|
167
|
+
className={`text-xs px-2 py-0.5 rounded-md border ${
|
|
168
|
+
STATUS_CHIP[task.status] ||
|
|
169
|
+
'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
{task.status}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
{onClose && (
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={onClose}
|
|
180
|
+
aria-label="Close detail (返回 tasks 列表)"
|
|
181
|
+
data-testid="task-detail-close"
|
|
182
|
+
className="shrink-0 rounded-md border border-[#26262b] px-2 py-1 text-xs text-gray-400 hover:text-white hover:border-[#3a3a41]"
|
|
183
|
+
>
|
|
184
|
+
←
|
|
185
|
+
</button>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
|
|
190
|
+
{isLoading && !task && (
|
|
191
|
+
<div className="animate-pulse space-y-3">
|
|
192
|
+
<div className="h-6 w-1/3 bg-gray-800/40 rounded" />
|
|
193
|
+
<div className="h-24 bg-gray-800/40 rounded" />
|
|
194
|
+
<div className="h-24 bg-gray-800/40 rounded" />
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
{error && (
|
|
198
|
+
<div className="bg-red-900/20 border border-red-800/40 text-red-300 px-3 py-2 rounded text-sm">
|
|
199
|
+
{error instanceof Error ? error.message : 'fetch failed'}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
{!isLoading && !error && !task && (
|
|
203
|
+
<div className="text-gray-400 text-sm">Task not found</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{task && (
|
|
207
|
+
<>
|
|
208
|
+
{/* Block 1: Timeline card (parity #1) */}
|
|
209
|
+
<section
|
|
210
|
+
data-testid="task-detail-timeline"
|
|
211
|
+
data-block="timeline"
|
|
212
|
+
className="bg-[#161618] border border-[#26262b] rounded-xl p-4"
|
|
213
|
+
>
|
|
214
|
+
<h2 className="text-sm font-semibold text-gray-300 mb-4">Timeline</h2>
|
|
215
|
+
<div className="relative">
|
|
216
|
+
{timelineSteps.map((step, i) => {
|
|
217
|
+
const done = !!step.time;
|
|
218
|
+
const isLast = i === timelineSteps.length - 1;
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
key={step.label}
|
|
222
|
+
data-testid={`timeline-step-${step.label.toLowerCase()}`}
|
|
223
|
+
data-timeline-step={step.label.toLowerCase()}
|
|
224
|
+
data-timeline-done={done ? 'true' : 'false'}
|
|
225
|
+
className="flex gap-3 pb-4 last:pb-0"
|
|
226
|
+
>
|
|
227
|
+
<div className="flex flex-col items-center">
|
|
228
|
+
<div
|
|
229
|
+
className={`w-3 h-3 rounded-full border-2 shrink-0 ${
|
|
230
|
+
done ? `${step.color} border-current` : 'border-gray-700 bg-transparent'
|
|
231
|
+
}`}
|
|
232
|
+
/>
|
|
233
|
+
{!isLast && (
|
|
234
|
+
<div
|
|
235
|
+
className={`w-0.5 flex-1 mt-1 ${
|
|
236
|
+
done && timelineSteps[i + 1]?.time
|
|
237
|
+
? 'bg-cyan-500/40'
|
|
238
|
+
: 'bg-gray-800'
|
|
239
|
+
}`}
|
|
240
|
+
/>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
<div className="min-w-0 -mt-0.5">
|
|
244
|
+
<div className={`text-sm font-medium ${done ? 'text-gray-200' : 'text-gray-600'}`}>
|
|
245
|
+
{step.label}
|
|
246
|
+
</div>
|
|
247
|
+
<div
|
|
248
|
+
className="text-xs text-gray-500"
|
|
249
|
+
title={step.time || undefined}
|
|
250
|
+
>
|
|
251
|
+
{step.time ? `${timeAgo(step.time)} — ${step.time}` : 'Pending'}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</div>
|
|
258
|
+
</section>
|
|
259
|
+
|
|
260
|
+
{/* Block 2: Info card (parity #2) */}
|
|
261
|
+
<section
|
|
262
|
+
data-testid="task-detail-info"
|
|
263
|
+
data-block="info"
|
|
264
|
+
className="bg-[#161618] border border-[#26262b] rounded-xl p-4"
|
|
265
|
+
>
|
|
266
|
+
<h2 className="text-sm font-semibold text-gray-300 mb-3">Info</h2>
|
|
267
|
+
<div className="space-y-2 text-xs">
|
|
268
|
+
{[
|
|
269
|
+
['Task ID', task.task_id],
|
|
270
|
+
['From', task.from_name],
|
|
271
|
+
['To', task.to_name],
|
|
272
|
+
['Priority', task.priority],
|
|
273
|
+
['Status', task.status],
|
|
274
|
+
['Expires', task.expires_at],
|
|
275
|
+
].map(([label, val]) => (
|
|
276
|
+
<div key={label as string} className="flex justify-between gap-4">
|
|
277
|
+
<span className="text-gray-500 shrink-0">{label}</span>
|
|
278
|
+
<span
|
|
279
|
+
data-info-field={String(label).toLowerCase().replace(/\s+/g, '-')}
|
|
280
|
+
className="text-gray-300 truncate max-w-[240px] text-right"
|
|
281
|
+
title={String(val || '')}
|
|
282
|
+
>
|
|
283
|
+
{String(val || '--')}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
<div
|
|
289
|
+
className="mt-4 pt-3 border-t border-[#1c1c1f] flex items-center gap-2 text-sm text-gray-300"
|
|
290
|
+
data-testid="task-detail-participants"
|
|
291
|
+
>
|
|
292
|
+
{task.from_name && <AliasAvatar alias={task.from_name} size={20} />}
|
|
293
|
+
<span className="truncate">{task.from_name || '--'}</span>
|
|
294
|
+
<span className="text-gray-600 shrink-0">→</span>
|
|
295
|
+
{task.to_name && <AliasAvatar alias={task.to_name} size={20} />}
|
|
296
|
+
<span className="truncate">{task.to_name || '--'}</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="mt-2 text-[10px]">
|
|
299
|
+
<span
|
|
300
|
+
className={priorityClass(task.priority)}
|
|
301
|
+
data-testid="task-detail-priority"
|
|
302
|
+
>
|
|
303
|
+
Priority: {task.priority || 'normal'}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
</section>
|
|
307
|
+
|
|
308
|
+
{/* Block 3: Content (parity #3) */}
|
|
309
|
+
<section
|
|
310
|
+
data-testid="task-detail-content"
|
|
311
|
+
data-block="content"
|
|
312
|
+
className="bg-[#161618] border border-[#26262b] rounded-xl p-4"
|
|
313
|
+
>
|
|
314
|
+
<h2 className="text-sm font-semibold text-gray-300 mb-2">Content</h2>
|
|
315
|
+
<pre
|
|
316
|
+
data-testid="task-detail-content-body"
|
|
317
|
+
className="text-xs text-gray-200 whitespace-pre-wrap break-words bg-[#0e0e10] rounded-lg px-3 py-2 border border-[#1c1c1f] max-h-64 overflow-y-auto font-sans"
|
|
318
|
+
>
|
|
319
|
+
{task.content || '--'}
|
|
320
|
+
</pre>
|
|
321
|
+
</section>
|
|
322
|
+
|
|
323
|
+
{/* Block 4: Result (parity #4) */}
|
|
324
|
+
{task.result && (
|
|
325
|
+
<section
|
|
326
|
+
data-testid="task-detail-result"
|
|
327
|
+
data-block="result"
|
|
328
|
+
className="bg-[#161618] border border-[#26262b] rounded-xl p-4"
|
|
329
|
+
>
|
|
330
|
+
<h2 className="text-sm font-semibold text-gray-300 mb-2">Result</h2>
|
|
331
|
+
<pre
|
|
332
|
+
data-testid="task-detail-result-body"
|
|
333
|
+
className="text-xs text-gray-200 whitespace-pre-wrap break-words bg-[#0e0e10] rounded-lg px-3 py-2 border border-[#1c1c1f] max-h-64 overflow-y-auto font-sans"
|
|
334
|
+
>
|
|
335
|
+
{task.result}
|
|
336
|
+
</pre>
|
|
337
|
+
</section>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{/* Block 5: Events feed (parity #5) — 10s poll, always rendered
|
|
341
|
+
even when empty so "no events" state is visible; a silently
|
|
342
|
+
missing block would hide the difference between "feed
|
|
343
|
+
gone" and "task has no events yet". */}
|
|
344
|
+
<section
|
|
345
|
+
data-testid="task-detail-events"
|
|
346
|
+
data-block="events"
|
|
347
|
+
data-events-count={events.length}
|
|
348
|
+
className="bg-[#161618] border border-[#26262b] rounded-xl p-4"
|
|
349
|
+
>
|
|
350
|
+
<h2 className="text-sm font-semibold text-gray-300 mb-3">
|
|
351
|
+
Events {events.length > 0 && <span className="text-gray-500">({events.length})</span>}
|
|
352
|
+
</h2>
|
|
353
|
+
{events.length === 0 ? (
|
|
354
|
+
<div className="text-xs text-gray-600 italic">No events yet.</div>
|
|
355
|
+
) : (
|
|
356
|
+
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
357
|
+
{events.map((e, i) => (
|
|
358
|
+
<div
|
|
359
|
+
key={e.id ?? i}
|
|
360
|
+
data-testid="task-detail-event-row"
|
|
361
|
+
className="flex items-center gap-2 text-xs"
|
|
362
|
+
>
|
|
363
|
+
<span
|
|
364
|
+
aria-hidden
|
|
365
|
+
className={`w-1.5 h-1.5 rounded-full shrink-0 ${
|
|
366
|
+
e.to_status === 'running'
|
|
367
|
+
? 'bg-green-400'
|
|
368
|
+
: e.to_status === 'replied'
|
|
369
|
+
? 'bg-purple-400'
|
|
370
|
+
: e.to_status === 'failed'
|
|
371
|
+
? 'bg-red-400'
|
|
372
|
+
: 'bg-blue-400'
|
|
373
|
+
}`}
|
|
374
|
+
/>
|
|
375
|
+
<span className="text-gray-400">{e.event_type}</span>
|
|
376
|
+
{e.from_status && (
|
|
377
|
+
<span className="text-gray-600">
|
|
378
|
+
{e.from_status} → {e.to_status}
|
|
379
|
+
</span>
|
|
380
|
+
)}
|
|
381
|
+
<span className="text-gray-600 ml-auto shrink-0" title={e.created_at}>
|
|
382
|
+
{timeAgo(e.created_at)}
|
|
383
|
+
</span>
|
|
384
|
+
</div>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</section>
|
|
389
|
+
|
|
390
|
+
{(task.status === 'failed' || task.status === 'expired') && (
|
|
391
|
+
<div>
|
|
392
|
+
<button
|
|
393
|
+
onClick={doRetry}
|
|
394
|
+
disabled={retrying}
|
|
395
|
+
data-testid="task-detail-retry"
|
|
396
|
+
className="text-xs text-orange-400 hover:text-orange-300 px-3 py-1.5 rounded-md border border-orange-500/30 hover:bg-orange-500/10 disabled:opacity-50 disabled:cursor-wait"
|
|
397
|
+
>
|
|
398
|
+
{retrying ? 'Retrying…' : 'Retry task'}
|
|
399
|
+
</button>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
</>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|