@sleep2agi/agent-network-dashboard 0.4.5-preview.3 → 0.4.5-preview.30

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.
Files changed (247) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +49 -49
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page.js.nft.json +1 -1
  23. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/admin.html +2 -2
  25. package/.next/server/app/admin.rsc +14 -14
  26. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  27. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  28. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  29. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  30. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  31. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  32. package/.next/server/app/index.html +2 -2
  33. package/.next/server/app/index.rsc +14 -14
  34. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  35. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  36. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  38. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/login.html +2 -2
  41. package/.next/server/app/login.rsc +14 -14
  42. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  43. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  44. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  45. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  47. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  48. package/.next/server/app/logs/page.js.nft.json +1 -1
  49. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  50. package/.next/server/app/logs.html +2 -2
  51. package/.next/server/app/logs.rsc +14 -14
  52. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  53. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  54. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  55. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  56. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  57. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  58. package/.next/server/app/messages/page.js.nft.json +1 -1
  59. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/messages.html +2 -2
  61. package/.next/server/app/messages.rsc +14 -14
  62. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  63. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  65. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  66. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  67. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  68. package/.next/server/app/node/page.js.nft.json +1 -1
  69. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/node.html +2 -2
  71. package/.next/server/app/node.rsc +14 -14
  72. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  73. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  74. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  75. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  76. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  77. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  78. package/.next/server/app/nodes/page.js.nft.json +1 -1
  79. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  80. package/.next/server/app/nodes.html +2 -2
  81. package/.next/server/app/nodes.rsc +14 -14
  82. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  83. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  84. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  85. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  86. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  87. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  88. package/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  91. package/.next/server/app/server-logs.html +2 -2
  92. package/.next/server/app/server-logs.rsc +14 -14
  93. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  94. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  95. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  96. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  97. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  98. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  99. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  100. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  101. package/.next/server/app/settings/networks.html +2 -2
  102. package/.next/server/app/settings/networks.rsc +14 -14
  103. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  104. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  105. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  106. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  107. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  108. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  109. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  110. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  111. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  112. package/.next/server/app/settings/tokens.html +2 -2
  113. package/.next/server/app/settings/tokens.rsc +14 -14
  114. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  115. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  116. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  117. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  118. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  119. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  120. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  121. package/.next/server/app/settings.html +2 -2
  122. package/.next/server/app/settings.rsc +14 -14
  123. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  124. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  125. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  126. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  127. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  128. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  129. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/tasks/page.js.nft.json +1 -1
  131. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  132. package/.next/server/app/tasks.html +2 -2
  133. package/.next/server/app/tasks.rsc +14 -14
  134. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  135. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  136. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  137. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  138. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  139. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  140. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js +3 -0
  141. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js.map +1 -0
  142. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js +3 -0
  143. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js.map +1 -0
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js +3 -0
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js.map +1 -0
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  147. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  148. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  149. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  150. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  151. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  152. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  153. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  154. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  155. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  156. package/.next/server/chunks/ssr/app_0dk5wao._.js +1 -1
  157. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +1 -1
  158. package/.next/server/chunks/ssr/app_0tncr1_._.js +3 -0
  159. package/.next/server/chunks/ssr/app_0tncr1_._.js.map +1 -0
  160. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +2 -2
  161. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  162. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js +3 -0
  163. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js.map +1 -0
  164. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +1 -1
  165. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +1 -1
  166. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  167. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  168. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  169. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  170. package/.next/server/middleware-build-manifest.js +3 -3
  171. package/.next/server/pages/404.html +2 -2
  172. package/.next/server/pages/500.html +1 -1
  173. package/.next/server/server-reference-manifest.js +1 -1
  174. package/.next/server/server-reference-manifest.json +1 -1
  175. package/.next/static/chunks/00-ce-5_uu76b.js +1 -0
  176. package/.next/static/chunks/05q0hb5.1ust6.js +7 -0
  177. package/.next/static/chunks/06x7w8nwggx~~.js +1 -0
  178. package/.next/static/chunks/0943n0t.9c-o2.js +1 -0
  179. package/.next/static/chunks/0awwnfcasumt~.css +2 -0
  180. package/.next/static/chunks/0b.hnp2_fa824.js +1 -0
  181. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  182. package/.next/static/chunks/0csmku~3y472t.js +1 -0
  183. package/.next/static/chunks/0hqhvq6f83m38.js +1 -0
  184. package/.next/static/chunks/0l3gf0ickq445.js +1 -0
  185. package/.next/static/chunks/0qnpc7rbxn12u.js +1 -0
  186. package/.next/static/chunks/121k.shryv1j_.js +1 -0
  187. package/.next/static/chunks/14r_.uh5d2r_e.js +1 -0
  188. package/.next/static/chunks/15nn5pyy3l5zo.js +7 -0
  189. package/.next/static/chunks/17uwnuk~0zoo3.js +1 -0
  190. package/.next/trace +2 -2
  191. package/.next/trace-build +1 -1
  192. package/app/admin/page.tsx +77 -25
  193. package/app/components/AgentCard.tsx +19 -5
  194. package/app/components/AliasAvatar.tsx +51 -0
  195. package/app/components/CommandPalette.tsx +53 -20
  196. package/app/components/HelpOverlay.tsx +50 -10
  197. package/app/components/Sidebar.tsx +40 -35
  198. package/app/components/TaskChatPanel.tsx +8 -10
  199. package/app/components/TaskDrawer.tsx +72 -28
  200. package/app/components/TopoGraph.tsx +91 -24
  201. package/app/globals.css +129 -0
  202. package/app/login/page.tsx +37 -7
  203. package/app/logs/page.tsx +72 -21
  204. package/app/messages/page.tsx +85 -51
  205. package/app/node/page.tsx +32 -15
  206. package/app/nodes/page.tsx +21 -23
  207. package/app/page.tsx +66 -13
  208. package/app/server-logs/page.tsx +37 -3
  209. package/app/settings/networks/page.tsx +37 -13
  210. package/app/settings/page.tsx +33 -8
  211. package/app/tasks/page.tsx +44 -24
  212. package/package.json +1 -1
  213. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +0 -3
  214. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +0 -1
  215. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  216. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  217. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js +0 -3
  218. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js.map +0 -1
  219. package/.next/server/chunks/ssr/_0d-fuox._.js +0 -3
  220. package/.next/server/chunks/ssr/_0d-fuox._.js.map +0 -1
  221. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  222. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  223. package/.next/server/chunks/ssr/app_0ap_oiq._.js +0 -3
  224. package/.next/server/chunks/ssr/app_0ap_oiq._.js.map +0 -1
  225. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  226. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  227. package/.next/server/chunks/ssr/app_0ff~kjj._.js +0 -9
  228. package/.next/server/chunks/ssr/app_0ff~kjj._.js.map +0 -1
  229. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  230. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  231. package/.next/static/chunks/051gslfjka1kj.js +0 -1
  232. package/.next/static/chunks/06t_vzei8s9_h.js +0 -1
  233. package/.next/static/chunks/07oobuohrpigy.js +0 -1
  234. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  235. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  236. package/.next/static/chunks/0khpyxpdrp~ty.js +0 -1
  237. package/.next/static/chunks/0lo_g.q1fm-hr.js +0 -7
  238. package/.next/static/chunks/0m328uz90rp81.js +0 -1
  239. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  240. package/.next/static/chunks/0qgc758gz3on4.js +0 -7
  241. package/.next/static/chunks/1031v6nw33ud3.js +0 -1
  242. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  243. package/.next/static/chunks/11l7hps85s2k7.css +0 -2
  244. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  245. /package/.next/static/{N_JUC5rLQduweE3JrL3yd → 4KoU2jLT2aIRN3WuiMQyh}/_buildManifest.js +0 -0
  246. /package/.next/static/{N_JUC5rLQduweE3JrL3yd → 4KoU2jLT2aIRN3WuiMQyh}/_clientMiddlewareManifest.js +0 -0
  247. /package/.next/static/{N_JUC5rLQduweE3JrL3yd → 4KoU2jLT2aIRN3WuiMQyh}/_ssgManifest.js +0 -0
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useState } from 'react';
4
4
  import { timeAgo } from './utils';
5
5
  import Link from 'next/link';
6
+ import { AliasAvatar } from './AliasAvatar';
6
7
 
7
8
  interface TaskDetail {
8
9
  task_id: string;
@@ -58,12 +59,20 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
58
59
  })();
59
60
  }, [taskId]);
60
61
 
62
+ /** Timeline steps — same 4 hops every task goes through. `time` is set
63
+ * when the hop has happened; missing time means pending. Round 36 adds
64
+ * `key` for the "current step" highlight + the `done` flag for layout. */
61
65
  const timeline = task ? [
62
- { label: 'Created', time: task.created_at, color: 'bg-gray-400' },
63
- { label: 'Delivered', time: task.delivered_at, color: 'bg-blue-400' },
64
- { label: 'Started', time: task.started_at, color: 'bg-green-400' },
65
- { label: 'Completed', time: task.completed_at, color: 'bg-purple-400' },
66
- ] : [];
66
+ { key: 'created', label: 'Created', time: task.created_at, color: 'bg-gray-400' },
67
+ { key: 'delivered', label: 'Delivered', time: task.delivered_at, color: 'bg-blue-400' },
68
+ { key: 'started', label: 'Started', time: task.started_at, color: 'bg-green-400' },
69
+ { key: 'completed', label: 'Completed', time: task.completed_at, color: 'bg-purple-400'},
70
+ ].map(s => ({ ...s, done: !!s.time })) : [];
71
+
72
+ /** Index of the highest-completed step. The step *after* this index is
73
+ * the "current" one (in-progress). When all are done, currentIdx = -1. */
74
+ const currentStepIdx = timeline.findIndex(s => !s.done);
75
+ const isActive = task && task.status !== 'completed' && task.status !== 'failed' && task.status !== 'expired' && task.status !== 'cancelled';
67
76
 
68
77
  const duration = task?.started_at && task?.completed_at
69
78
  ? Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 1000)
@@ -71,7 +80,7 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
71
80
 
72
81
  return (
73
82
  <>
74
- <div className="fixed inset-0 bg-black/30 z-40" onClick={onClose} />
83
+ <div className="fixed inset-0 bg-black/30 z-40 anet-fade-in" onClick={onClose} />
75
84
  <div className="fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 overflow-y-auto animate-slide-in">
76
85
  {/* Header */}
77
86
  <div className="flex items-center justify-between px-5 py-4 border-b border-[#2a2a4a] bg-[#0d0d1a] sticky top-0">
@@ -105,29 +114,68 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
105
114
  )}
106
115
  </div>
107
116
 
108
- {/* From → To */}
109
- <div className="flex items-center gap-2 text-sm">
110
- <span className="text-blue-400 font-medium">{task.from_name || '--'}</span>
111
- <span className="text-gray-600">→</span>
112
- <span className="text-cyan-400 font-medium">{task.to_name || '--'}</span>
117
+ {/* From → To — round 41: use AliasAvatar so from/to colours
118
+ match the rest of the app instead of the legacy blue/cyan
119
+ hardcode. */}
120
+ <div className="flex items-center gap-2 text-sm flex-wrap">
121
+ {task.from_name && <AliasAvatar alias={task.from_name} size={18} />}
122
+ <span className="text-gray-200 font-medium">{task.from_name || '--'}</span>
123
+ <span className="text-gray-600">&rarr;</span>
124
+ {task.to_name && <AliasAvatar alias={task.to_name} size={18} />}
125
+ <span className="text-gray-200 font-medium">{task.to_name || '--'}</span>
113
126
  </div>
114
127
 
115
- {/* Timeline */}
128
+ {/* Timeline — round 36 polish: relative timestamps, current-step
129
+ pulse if task is still in flight, full ISO in title=. */}
116
130
  <div className="bg-[#111128] border border-[#2a2a4a] rounded-xl p-4">
117
- <div className="text-xs text-gray-500 uppercase mb-3">Timeline</div>
131
+ <div className="flex items-center justify-between mb-3">
132
+ <div className="text-xs text-gray-500 uppercase tracking-wide">Timeline</div>
133
+ {duration !== null && (
134
+ <div className="text-[10px] text-gray-600">
135
+ {duration < 60 ? `${duration}s` : `${Math.floor(duration/60)}m ${duration%60}s`} runtime
136
+ </div>
137
+ )}
138
+ </div>
118
139
  <div className="space-y-3">
119
- {timeline.map((step, i) => (
120
- <div key={step.label} className="flex items-center gap-3">
121
- <div className="flex flex-col items-center">
122
- <div className={`w-3 h-3 rounded-full ${step.time ? step.color : 'bg-gray-700'}`} />
123
- {i < timeline.length - 1 && <div className={`w-0.5 h-4 mt-1 ${step.time && timeline[i+1]?.time ? 'bg-gray-600' : 'bg-gray-800'}`} />}
140
+ {timeline.map((step, i) => {
141
+ const isCurrent = isActive && i === currentStepIdx;
142
+ const isNextDone = !!timeline[i+1]?.done;
143
+ return (
144
+ <div key={step.key} className="flex items-start gap-3">
145
+ <div className="flex flex-col items-center pt-0.5">
146
+ <span
147
+ className={`relative w-3 h-3 rounded-full shrink-0 ${
148
+ step.done ? step.color : 'bg-gray-700'
149
+ }`}
150
+ >
151
+ {isCurrent && (
152
+ <span
153
+ aria-hidden
154
+ className="absolute -inset-1 rounded-full border border-current opacity-60 anet-current-step-pulse"
155
+ style={{ borderColor: 'currentColor' }}
156
+ />
157
+ )}
158
+ </span>
159
+ {i < timeline.length - 1 && (
160
+ <div className={`w-0.5 h-5 mt-1 ${
161
+ step.done && isNextDone ? 'bg-gray-500' : step.done ? 'bg-gradient-to-b from-gray-500 to-gray-800' : 'bg-gray-800'
162
+ }`} />
163
+ )}
164
+ </div>
165
+ <div className="flex-1 min-w-0">
166
+ <div className={`text-xs font-medium flex items-center gap-2 ${
167
+ step.done ? 'text-gray-200' : isCurrent ? 'text-cyan-300' : 'text-gray-600'
168
+ }`}>
169
+ <span>{step.label}</span>
170
+ {isCurrent && <span className="text-[9px] uppercase tracking-wide text-cyan-400">in&nbsp;progress</span>}
171
+ </div>
172
+ <div className="text-[10px] text-gray-500" title={step.time || undefined}>
173
+ {step.time ? timeAgo(step.time) : isCurrent ? '—' : 'Pending'}
174
+ </div>
175
+ </div>
124
176
  </div>
125
- <div className="flex-1">
126
- <div className={`text-xs font-medium ${step.time ? 'text-gray-300' : 'text-gray-600'}`}>{step.label}</div>
127
- <div className="text-[10px] text-gray-500">{step.time || 'Pending'}</div>
128
- </div>
129
- </div>
130
- ))}
177
+ );
178
+ })}
131
179
  </div>
132
180
  </div>
133
181
 
@@ -172,10 +220,6 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
172
220
  </div>
173
221
  )}
174
222
  </div>
175
- <style jsx global>{`
176
- @keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
177
- .animate-slide-in { animation: slide-in 0.2s ease-out; }
178
- `}</style>
179
223
  </>
180
224
  );
181
225
  }
@@ -33,10 +33,14 @@ const cy = 330;
33
33
  const onlineRadius = 220;
34
34
  const offlineRadius = 315;
35
35
 
36
- function polarPoint(index: number, total: number, radius: number) {
36
+ /** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset
37
+ * the whole ring so two stacked rings don't align radially (round 25: the
38
+ * offline ring gets a half-step rotation so its nodes sit in the gaps
39
+ * between online ones). */
40
+ function polarPoint(index: number, total: number, radius: number, rotateBy = 0) {
37
41
  const spread = total <= 2 ? Math.PI : Math.PI * 1.78;
38
42
  const start = -Math.PI / 2 - spread / 2;
39
- const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1);
43
+ const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1) + rotateBy;
40
44
  return {
41
45
  x: cx + radius * Math.cos(angle),
42
46
  y: cy + radius * Math.sin(angle),
@@ -248,8 +252,17 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
248
252
  positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);
249
253
  });
250
254
 
255
+ // Offset the offline ring radially by half the online step so offline
256
+ // bubbles sit in the angular gaps between online bubbles instead of
257
+ // stacking directly behind them. Also push the outer ring further when
258
+ // there are many offline nodes so labels don't crowd the legend.
259
+ const spread = online.length <= 2 ? Math.PI : Math.PI * 1.78;
260
+ const onlineStep = online.length > 1 ? spread / (online.length - 1) : 0;
261
+ const offlineRotation = online.length > 0 ? onlineStep / 2 : 0;
262
+ const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;
263
+
251
264
  offline.forEach((s, index) => {
252
- positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineRadius);
265
+ positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineR, offlineRotation);
253
266
  });
254
267
 
255
268
  const links = buildFlowLinks(messages, positions);
@@ -320,11 +333,54 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
320
333
  <marker id="topo-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
321
334
  <path d="M 0 0 L 10 5 L 0 10 z" fill={pal.arrowFill} />
322
335
  </marker>
336
+ {/* Round 45: radial radar sweep gradient — bright at center
337
+ (where it meets the hub) fading to leading edge. */}
338
+ <radialGradient id="topo-sweep" cx="0%" cy="50%" r="100%">
339
+ <stop offset="0%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.18 : 0.32} />
340
+ <stop offset="70%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity={isLight ? 0.10 : 0.18} />
341
+ <stop offset="100%" stopColor={isLight ? '#0d9488' : '#22d3ee'} stopOpacity="0" />
342
+ </radialGradient>
323
343
  </defs>
324
344
 
325
345
  <rect width="1000" height="680" fill="url(#topo-panel)" />
326
346
  <circle cx={cx} cy={cy} r="330" fill="url(#topo-radar)" />
327
347
 
348
+ {/* Round 45: subtle star field — 24 deterministic dots scattered
349
+ across the canvas give the radar bg some depth. Skipped on
350
+ light theme so the white surface stays clean. */}
351
+ {!isLight && (
352
+ <g opacity="0.5">
353
+ {Array.from({ length: 28 }).map((_, i) => {
354
+ // Deterministic pseudo-random scatter so positions are
355
+ // stable between renders (no JS hydration mismatch).
356
+ const seed = i * 9301 + 49297;
357
+ const x = ((seed * 13) % 1000);
358
+ const y = ((seed * 7) % 680);
359
+ const r = (i % 3 === 0) ? 1.2 : 0.7;
360
+ return <circle key={i} cx={x} cy={y} r={r} fill="#a5b4fc" opacity={0.35 + (i % 4) * 0.05} />;
361
+ })}
362
+ </g>
363
+ )}
364
+
365
+ {/* Round 45: rotating radar sweep — a 40° wedge with a soft
366
+ leading-edge gradient. Slow 6s rotation reads as a radar
367
+ scan without being noisy. Inline transform-origin on the
368
+ <g> wrapper ensures Chrome / Firefox rotate around (cx,cy)
369
+ instead of the SVG viewBox corner. */}
370
+ <g
371
+ style={{
372
+ transformOrigin: `${cx}px ${cy}px`,
373
+ transformBox: 'view-box',
374
+ }}
375
+ className="anet-topo-sweep"
376
+ opacity={isLight ? 0.7 : 1}
377
+ >
378
+ <path
379
+ 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`}
380
+ fill="url(#topo-sweep)"
381
+ />
382
+ </g>
383
+
328
384
  {/* radar rings */}
329
385
  {[90, 170, 250, 330].map(radius => (
330
386
  <circle key={radius} cx={cx} cy={cy} r={radius} fill="none" stroke={pal.ringStroke} strokeWidth="1" opacity={isLight ? 0.6 : 0.35} />
@@ -342,21 +398,27 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
342
398
  />
343
399
  ))}
344
400
 
345
- {/* hub links */}
346
- {onlineNodes.map(session => {
401
+ {/* hub links — round 46: idle spokes now have animated
402
+ stroke-dashoffset so dashes flow outward from the hub
403
+ ("command relay" feel). Active spokes carrying live message
404
+ flow stay as solid bright strokes. */}
405
+ {onlineNodes.map((session, idx) => {
347
406
  const pos = nodePositions[session.alias];
348
407
  if (!pos) return null;
349
408
  const path = curvePath({ x: cx, y: cy }, pos, 0);
409
+ const isActiveSpoke = activeAliases.has(session.alias);
350
410
 
351
411
  return (
352
412
  <path
353
413
  key={`hub-${session.alias}`}
354
414
  d={path}
355
415
  fill="none"
356
- stroke={activeAliases.has(session.alias) ? pal.spokeStroke.active : pal.spokeStroke.idle}
357
- strokeWidth={activeAliases.has(session.alias) ? 2 : 1}
358
- strokeDasharray={activeAliases.has(session.alias) ? 'none' : '8 10'}
359
- opacity={activeAliases.has(session.alias) ? 0.65 : 0.35}
416
+ stroke={isActiveSpoke ? pal.spokeStroke.active : pal.spokeStroke.idle}
417
+ strokeWidth={isActiveSpoke ? 2 : 1}
418
+ strokeDasharray={isActiveSpoke ? 'none' : '6 14'}
419
+ opacity={isActiveSpoke ? 0.7 : 0.45}
420
+ className={isActiveSpoke ? undefined : 'anet-topo-spoke-flow'}
421
+ style={isActiveSpoke ? undefined : { animationDelay: `${-(idx * 0.25)}s` }}
360
422
  />
361
423
  );
362
424
  })}
@@ -399,27 +461,32 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
399
461
  );
400
462
  })}
401
463
 
402
- {/* center hub — 24px pulse signal source.
403
- Previous design (56px filled circle + 42px animated ring +
404
- `+` glyph + "DASHBOARD" / "command relay" labels) over-emphasized
405
- the hub and competed with the agent nodes for visual weight.
406
- The new design is a calm 12px emerald source with two outward
407
- pulse rings — reads as "signal origin" without a placeholder
408
- logo or label. Closes agent-network#5. */}
464
+ {/* center hub — round 39: enlarged from r=6 (12px) to r=10 (20px)
465
+ core so the "control plane" reads as the network's anchor,
466
+ not a stray particle. Static halo ring at r=18 grounds it
467
+ visually; two outward pulses keep the "signal source" idea
468
+ from r17. Light theme gets a paler core to avoid hot-spotting
469
+ the bg. */}
409
470
  <g>
471
+ {/* static grounding halo — sits underneath, low opacity */}
472
+ <circle
473
+ cx={cx} cy={cy} r="18"
474
+ fill={isLight ? '#d1fae5' : '#10b981'}
475
+ opacity={isLight ? 0.35 : 0.10}
476
+ />
410
477
  {/* outer pulse 1 */}
411
- <circle cx={cx} cy={cy} r="6" fill="none" stroke="#10b981" strokeWidth="1.5">
412
- <animate attributeName="r" values="6;28;6" dur="2.4s" repeatCount="indefinite" />
478
+ <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5">
479
+ <animate attributeName="r" values="10;38;10" dur="2.4s" repeatCount="indefinite" />
413
480
  <animate attributeName="opacity" values="0.55;0;0.55" dur="2.4s" repeatCount="indefinite" />
414
481
  </circle>
415
482
  {/* outer pulse 2 (delayed 1.2s) */}
416
- <circle cx={cx} cy={cy} r="6" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
417
- <animate attributeName="r" values="6;28;6" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
483
+ <circle cx={cx} cy={cy} r="10" fill="none" stroke="#10b981" strokeWidth="1.5" opacity="0">
484
+ <animate attributeName="r" values="10;38;10" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
418
485
  <animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" begin="1.2s" repeatCount="indefinite" />
419
486
  </circle>
420
- {/* core dot 12px diameter (r=6), emerald with inner highlight */}
421
- <circle cx={cx} cy={cy} r="6" fill="#10b981" />
422
- <circle cx={cx} cy={cy} r="3" fill="#d1fae5" opacity="0.9" />
487
+ {/* core — 20px diameter, larger inner highlight reads as a "lit lamp" */}
488
+ <circle cx={cx} cy={cy} r="10" fill={isLight ? '#059669' : '#10b981'} />
489
+ <circle cx={cx} cy={cy} r="5" fill="#d1fae5" opacity="0.9" />
423
490
  </g>
424
491
 
425
492
  {/* agent nodes */}
@@ -441,7 +508,7 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
441
508
  <animate attributeName="opacity" values={isLight ? '0.12;0.02;0.12' : '0.18;0.04;0.18'} dur="2.4s" repeatCount="indefinite" />
442
509
  </circle>
443
510
  )}
444
- <circle cx={pos.x} cy={pos.y} r={radius + 8} fill={status.halo} opacity={isOnline ? (isLight ? 0.85 : 0.55) : (isLight ? 0.4 : 0.25)} />
511
+ <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)} />
445
512
  <circle
446
513
  cx={pos.x}
447
514
  cy={pos.y}
package/app/globals.css CHANGED
@@ -708,6 +708,45 @@ body {
708
708
  background-color: rgba(0, 158, 126, 0.08) !important;
709
709
  }
710
710
 
711
+ /* Round 20 — matched-char highlight inside cmdk titles/hints. Inherits
712
+ line color (`color`) instead of forcing one — keeps text readable on
713
+ both inactive (gray) and active (cyan-300) rows. <mark> default is a
714
+ garish yellow block, which we override. */
715
+ .anet-cmdk-mark {
716
+ background: transparent;
717
+ color: inherit;
718
+ font-weight: 600;
719
+ /* Subtle underline so the mark stays visible even on the active row
720
+ where the background tint matches the accent color. */
721
+ text-decoration: underline;
722
+ text-decoration-color: currentColor;
723
+ text-decoration-thickness: 1px;
724
+ text-underline-offset: 2px;
725
+ text-decoration-skip-ink: none;
726
+ }
727
+ /* Cyber: accent the matched glyph color so it pops on the dark bg. */
728
+ [data-theme="cyber"] .anet-cmdk-mark,
729
+ :root:not([data-theme]) .anet-cmdk-mark {
730
+ color: #22d3ee;
731
+ }
732
+ /* Light/mint: use the brand teal — undertone matches the active row. */
733
+ [data-theme="light"] .anet-cmdk-mark,
734
+ [data-theme="mint"] .anet-cmdk-mark {
735
+ color: #0d9488;
736
+ }
737
+ [data-theme="sunset"] .anet-cmdk-mark {
738
+ color: #ea580c;
739
+ }
740
+ /* On the active (cyan-tinted) row, the title is already cyan-300 — use
741
+ white-with-underline so the highlight reads as emphasis, not redundancy. */
742
+ .anet-cmdk-row.bg-cyan-500\/10 .anet-cmdk-mark {
743
+ color: #f0fdff;
744
+ }
745
+ [data-theme="light"] .anet-cmdk-row.bg-cyan-500\/10 .anet-cmdk-mark,
746
+ [data-theme="mint"] .anet-cmdk-row.bg-cyan-500\/10 .anet-cmdk-mark {
747
+ color: #064e3b;
748
+ }
749
+
711
750
  /* Help overlay — same white-card-with-shadow treatment as cmdk on light */
712
751
  [data-theme="light"] .anet-help > div:not([aria-hidden]),
713
752
  [data-theme="mint"] .anet-help > div:not([aria-hidden]) {
@@ -772,3 +811,93 @@ body {
772
811
  color: #92400e !important;
773
812
  border-color: rgba(217, 119, 6, 0.28) !important;
774
813
  }
814
+
815
+ /* Round 30 — global drawer entrance keyframes so TaskDrawer, TaskChatPanel,
816
+ and any future right-side panel share one source. Previously the
817
+ keyframe lived inside TaskChatPanel's <style jsx global>; if that
818
+ component never mounted, the keyframe was missing for TaskDrawer. */
819
+ @keyframes anet-slide-in {
820
+ from { transform: translateX(100%); }
821
+ to { transform: translateX(0); }
822
+ }
823
+ .animate-slide-in { animation: anet-slide-in 0.2s ease-out; }
824
+
825
+ @keyframes anet-fade-in {
826
+ from { opacity: 0; }
827
+ to { opacity: 1; }
828
+ }
829
+ .anet-fade-in { animation: anet-fade-in 0.15s ease-out; }
830
+
831
+ /* Round 36 — current-step ring pulse on TaskDrawer timeline. Halo gently
832
+ breathes around the active step's dot so users can spot "task is here". */
833
+ @keyframes anet-current-step-pulse-kf {
834
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
835
+ 50% { transform: scale(1.25); opacity: 0.0; }
836
+ }
837
+ .anet-current-step-pulse {
838
+ animation: anet-current-step-pulse-kf 1.6s ease-in-out infinite;
839
+ }
840
+
841
+ /* Round 45 — TopoGraph rotating radar sweep wedge. 6s slow rotation
842
+ gives the topology canvas a real "radar scan" feel without distracting
843
+ from the agent nodes. */
844
+ @keyframes anet-topo-sweep-kf {
845
+ 0% { transform: rotate(0deg); }
846
+ 100% { transform: rotate(360deg); }
847
+ }
848
+ .anet-topo-sweep {
849
+ animation: anet-topo-sweep-kf 6s linear infinite;
850
+ }
851
+
852
+ /* Round 46 — animated dashed spokes flowing from hub outward. Each spoke
853
+ gets a small animationDelay (set inline) so the dashes don't all flow
854
+ in sync — reads as "command relay" pulse rather than a metronome. */
855
+ @keyframes anet-topo-spoke-flow-kf {
856
+ from { stroke-dashoffset: 0; }
857
+ to { stroke-dashoffset: -40; }
858
+ }
859
+ .anet-topo-spoke-flow {
860
+ animation: anet-topo-spoke-flow-kf 2.4s linear infinite;
861
+ }
862
+
863
+ /* ─────────────────────────────────────────────────────────────────────
864
+ Round 29 — prefers-reduced-motion a11y sweep
865
+
866
+ Rounds 19~27 accumulated a lot of small animations: Tasks row 0fr↔1fr
867
+ expand, chevron rotate, hover bg shifts, Cmd+K popover, TaskChatPanel
868
+ slide-in, brand/skeleton pulses already had their own guards.
869
+
870
+ For users who set system "reduce motion": neutralise everything in one
871
+ pass. Don't kill state changes outright — only their durations and
872
+ keyframes. Hover and focus colours still apply, just instantly.
873
+ ───────────────────────────────────────────────────────────────────── */
874
+ @media (prefers-reduced-motion: reduce) {
875
+ *,
876
+ *::before,
877
+ *::after {
878
+ animation-duration: 0.001ms !important;
879
+ animation-iteration-count: 1 !important;
880
+ transition-duration: 0.001ms !important;
881
+ scroll-behavior: auto !important;
882
+ }
883
+ /* Skeleton pulse already has its own guard; harden in case the bar
884
+ class is reused without anet-skeleton-pulse. */
885
+ .anet-skeleton-bar {
886
+ animation: none !important;
887
+ }
888
+ /* Brand pulse hidden — green/red dot still indicates state via color. */
889
+ .anet-brand-pulse {
890
+ animation: none !important;
891
+ }
892
+ /* Drawer entrance kills — keyframes promoted to globals in r30. */
893
+ .animate-slide-in,
894
+ .anet-fade-in,
895
+ .anet-current-step-pulse,
896
+ .anet-topo-sweep,
897
+ .anet-topo-spoke-flow {
898
+ animation: none !important;
899
+ }
900
+ /* Tasks row grid 0fr↔1fr keep working (it's the toggle mechanism),
901
+ but the smooth transition just snaps. Already handled via the
902
+ blanket transition-duration override above. */
903
+ }
@@ -6,6 +6,7 @@ export default function LoginPage() {
6
6
  const [mode, setMode] = useState<'login' | 'register'>('login');
7
7
  const [username, setUsername] = useState('');
8
8
  const [password, setPassword] = useState('');
9
+ const [showPassword, setShowPassword] = useState(false);
9
10
  const [error, setError] = useState('');
10
11
  const [pending, setPending] = useState(false);
11
12
 
@@ -68,7 +69,9 @@ export default function LoginPage() {
68
69
  </div>
69
70
  <h1 className="text-[22px] sm:text-2xl font-semibold text-white tracking-tight">Agent Network</h1>
70
71
  <p className="text-[13px] text-gray-500 mt-1.5 leading-snug">
71
- One control plane for your AI agents · <span className="text-gray-600">Tasks · Mesh · Messages</span>
72
+ {mode === 'login'
73
+ ? <>Welcome back · <span className="text-gray-600">Tasks · Mesh · Messages</span></>
74
+ : <>Create your account · <span className="text-gray-600">Free preview · no credit card</span></>}
72
75
  </p>
73
76
  </div>
74
77
 
@@ -93,17 +96,44 @@ export default function LoginPage() {
93
96
  <label htmlFor="password" className="block text-xs text-gray-500 mb-2 uppercase tracking-wider">
94
97
  Password
95
98
  </label>
96
- <input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)}
97
- placeholder="Enter password"
98
- className="w-full bg-[#0a0a15] border border-[#2a2a4a] rounded-lg px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 focus:outline-none transition-all" />
99
+ <div className="relative">
100
+ <input id="password" type={showPassword ? 'text' : 'password'} value={password} onChange={e => setPassword(e.target.value)}
101
+ placeholder="Enter password"
102
+ className="w-full bg-[#0a0a15] border border-[#2a2a4a] rounded-lg px-4 pr-11 py-3 text-sm text-white placeholder-gray-600 focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20 focus:outline-none transition-all" />
103
+ <button
104
+ type="button"
105
+ onClick={() => setShowPassword(s => !s)}
106
+ tabIndex={-1}
107
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
108
+ className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-600 hover:text-gray-300 transition-colors"
109
+ >
110
+ {showPassword ? (
111
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
112
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
113
+ <line x1="1" y1="1" x2="23" y2="23" />
114
+ </svg>
115
+ ) : (
116
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
117
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
118
+ <circle cx="12" cy="12" r="3" />
119
+ </svg>
120
+ )}
121
+ </button>
122
+ </div>
99
123
 
100
124
  {error && (
101
125
  <div className="mt-3 text-sm text-red-300 bg-red-500/5 border border-red-500/10 rounded-lg px-3 py-2">{error}</div>
102
126
  )}
103
127
 
104
- <button type="submit" disabled={pending || !password}
105
- className="mt-5 w-full px-4 py-3 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-gray-800 disabled:to-gray-800 disabled:text-gray-600 text-white text-sm font-medium rounded-lg transition-all shadow-lg shadow-cyan-500/10 hover:shadow-cyan-500/20 disabled:shadow-none">
106
- {pending ? (mode === 'login' ? 'Signing in...' : 'Registering...') : (mode === 'login' ? 'Sign in' : 'Create account')}
128
+ <button type="submit" disabled={pending || !password || !username.trim()}
129
+ className="mt-5 w-full flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:from-gray-800 disabled:to-gray-800 disabled:text-gray-600 text-white text-sm font-medium rounded-lg transition-all shadow-lg shadow-cyan-500/10 hover:shadow-cyan-500/20 disabled:shadow-none">
130
+ {pending && (
131
+ <svg aria-hidden className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
132
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" opacity="0.25" />
133
+ <path d="M4 12a8 8 0 018-8" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
134
+ </svg>
135
+ )}
136
+ <span>{pending ? (mode === 'login' ? 'Signing in…' : 'Registering…') : (mode === 'login' ? 'Sign in' : 'Create account')}</span>
107
137
  </button>
108
138
  </form>
109
139