@data-club/ai-hub 0.0.1

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 (344) hide show
  1. package/dist/auth/passwordPolicy.d.ts +55 -0
  2. package/dist/auth/passwordPolicy.d.ts.map +1 -0
  3. package/dist/auth/passwordPolicy.js +63 -0
  4. package/dist/auth/passwordPolicy.js.map +1 -0
  5. package/dist/auth/texts.d.ts +815 -0
  6. package/dist/auth/texts.d.ts.map +1 -0
  7. package/dist/auth/texts.js +429 -0
  8. package/dist/auth/texts.js.map +1 -0
  9. package/dist/client/AIHubClient.d.ts +1058 -0
  10. package/dist/client/AIHubClient.d.ts.map +1 -0
  11. package/dist/client/AIHubClient.js +950 -0
  12. package/dist/client/AIHubClient.js.map +1 -0
  13. package/dist/client/index.d.ts +11 -0
  14. package/dist/client/index.d.ts.map +1 -0
  15. package/dist/client/index.js +9 -0
  16. package/dist/client/index.js.map +1 -0
  17. package/dist/client/sseParser.d.ts +41 -0
  18. package/dist/client/sseParser.d.ts.map +1 -0
  19. package/dist/client/sseParser.js +153 -0
  20. package/dist/client/sseParser.js.map +1 -0
  21. package/dist/client/tokenStorage.d.ts +56 -0
  22. package/dist/client/tokenStorage.d.ts.map +1 -0
  23. package/dist/client/tokenStorage.js +116 -0
  24. package/dist/client/tokenStorage.js.map +1 -0
  25. package/dist/components/AgentsPicker/index.d.ts +70 -0
  26. package/dist/components/AgentsPicker/index.d.ts.map +1 -0
  27. package/dist/components/AgentsPicker/index.js +63 -0
  28. package/dist/components/AgentsPicker/index.js.map +1 -0
  29. package/dist/components/AuthGate/index.d.ts +79 -0
  30. package/dist/components/AuthGate/index.d.ts.map +1 -0
  31. package/dist/components/AuthGate/index.js +61 -0
  32. package/dist/components/AuthGate/index.js.map +1 -0
  33. package/dist/components/CacheBadge/index.d.ts +88 -0
  34. package/dist/components/CacheBadge/index.d.ts.map +1 -0
  35. package/dist/components/CacheBadge/index.js +127 -0
  36. package/dist/components/CacheBadge/index.js.map +1 -0
  37. package/dist/components/ChangePassword/index.d.ts +109 -0
  38. package/dist/components/ChangePassword/index.d.ts.map +1 -0
  39. package/dist/components/ChangePassword/index.js +142 -0
  40. package/dist/components/ChangePassword/index.js.map +1 -0
  41. package/dist/components/ChatShell/ShellHeader.d.ts +44 -0
  42. package/dist/components/ChatShell/ShellHeader.d.ts.map +1 -0
  43. package/dist/components/ChatShell/ShellHeader.js +14 -0
  44. package/dist/components/ChatShell/ShellHeader.js.map +1 -0
  45. package/dist/components/ChatShell/UserMenu.d.ts +48 -0
  46. package/dist/components/ChatShell/UserMenu.d.ts.map +1 -0
  47. package/dist/components/ChatShell/UserMenu.js +86 -0
  48. package/dist/components/ChatShell/UserMenu.js.map +1 -0
  49. package/dist/components/ChatShell/index.d.ts +98 -0
  50. package/dist/components/ChatShell/index.d.ts.map +1 -0
  51. package/dist/components/ChatShell/index.js +124 -0
  52. package/dist/components/ChatShell/index.js.map +1 -0
  53. package/dist/components/DataClubAIHub/index.d.ts +132 -0
  54. package/dist/components/DataClubAIHub/index.d.ts.map +1 -0
  55. package/dist/components/DataClubAIHub/index.js +124 -0
  56. package/dist/components/DataClubAIHub/index.js.map +1 -0
  57. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts +89 -0
  58. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.d.ts.map +1 -0
  59. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js +199 -0
  60. package/dist/components/DataClubAIHubAdmin/AgentEditorModal.js.map +1 -0
  61. package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts +33 -0
  62. package/dist/components/DataClubAIHubAdmin/AgentsPage.d.ts.map +1 -0
  63. package/dist/components/DataClubAIHubAdmin/AgentsPage.js +137 -0
  64. package/dist/components/DataClubAIHubAdmin/AgentsPage.js.map +1 -0
  65. package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts +50 -0
  66. package/dist/components/DataClubAIHubAdmin/CreateUserModal.d.ts.map +1 -0
  67. package/dist/components/DataClubAIHubAdmin/CreateUserModal.js +93 -0
  68. package/dist/components/DataClubAIHubAdmin/CreateUserModal.js.map +1 -0
  69. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts +49 -0
  70. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.d.ts.map +1 -0
  71. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js +94 -0
  72. package/dist/components/DataClubAIHubAdmin/PasswordRevealModal.js.map +1 -0
  73. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts +37 -0
  74. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.d.ts.map +1 -0
  75. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js +44 -0
  76. package/dist/components/DataClubAIHubAdmin/ResetPasswordModal.js.map +1 -0
  77. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts +56 -0
  78. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.d.ts.map +1 -0
  79. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js +158 -0
  80. package/dist/components/DataClubAIHubAdmin/RotatingMessageEditorModal.js.map +1 -0
  81. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts +29 -0
  82. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.d.ts.map +1 -0
  83. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js +183 -0
  84. package/dist/components/DataClubAIHubAdmin/RotatingMessagesPage.js.map +1 -0
  85. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts +38 -0
  86. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.d.ts.map +1 -0
  87. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js +294 -0
  88. package/dist/components/DataClubAIHubAdmin/SessionsOverviewPage.js.map +1 -0
  89. package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts +36 -0
  90. package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -0
  91. package/dist/components/DataClubAIHubAdmin/SettingsPage.js +373 -0
  92. package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -0
  93. package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts +47 -0
  94. package/dist/components/DataClubAIHubAdmin/UsersPage.d.ts.map +1 -0
  95. package/dist/components/DataClubAIHubAdmin/UsersPage.js +233 -0
  96. package/dist/components/DataClubAIHubAdmin/UsersPage.js.map +1 -0
  97. package/dist/components/DataClubAIHubAdmin/index.d.ts +87 -0
  98. package/dist/components/DataClubAIHubAdmin/index.d.ts.map +1 -0
  99. package/dist/components/DataClubAIHubAdmin/index.js +137 -0
  100. package/dist/components/DataClubAIHubAdmin/index.js.map +1 -0
  101. package/dist/components/DataClubAIHubLogin/index.d.ts +113 -0
  102. package/dist/components/DataClubAIHubLogin/index.d.ts.map +1 -0
  103. package/dist/components/DataClubAIHubLogin/index.js +123 -0
  104. package/dist/components/DataClubAIHubLogin/index.js.map +1 -0
  105. package/dist/components/EffortControl/index.d.ts +57 -0
  106. package/dist/components/EffortControl/index.d.ts.map +1 -0
  107. package/dist/components/EffortControl/index.js +84 -0
  108. package/dist/components/EffortControl/index.js.map +1 -0
  109. package/dist/components/Layout/index.d.ts +63 -0
  110. package/dist/components/Layout/index.d.ts.map +1 -0
  111. package/dist/components/Layout/index.js +45 -0
  112. package/dist/components/Layout/index.js.map +1 -0
  113. package/dist/components/MainColumn/index.d.ts +69 -0
  114. package/dist/components/MainColumn/index.d.ts.map +1 -0
  115. package/dist/components/MainColumn/index.js +47 -0
  116. package/dist/components/MainColumn/index.js.map +1 -0
  117. package/dist/components/MarkdownRenderer/index.d.ts +66 -0
  118. package/dist/components/MarkdownRenderer/index.d.ts.map +1 -0
  119. package/dist/components/MarkdownRenderer/index.js +153 -0
  120. package/dist/components/MarkdownRenderer/index.js.map +1 -0
  121. package/dist/components/MessageInput/index.d.ts +85 -0
  122. package/dist/components/MessageInput/index.d.ts.map +1 -0
  123. package/dist/components/MessageInput/index.js +139 -0
  124. package/dist/components/MessageInput/index.js.map +1 -0
  125. package/dist/components/MessageList/index.d.ts +156 -0
  126. package/dist/components/MessageList/index.d.ts.map +1 -0
  127. package/dist/components/MessageList/index.js +352 -0
  128. package/dist/components/MessageList/index.js.map +1 -0
  129. package/dist/components/ReasoningSelector/index.d.ts +77 -0
  130. package/dist/components/ReasoningSelector/index.d.ts.map +1 -0
  131. package/dist/components/ReasoningSelector/index.js +156 -0
  132. package/dist/components/ReasoningSelector/index.js.map +1 -0
  133. package/dist/components/ReasoningView/index.d.ts +94 -0
  134. package/dist/components/ReasoningView/index.d.ts.map +1 -0
  135. package/dist/components/ReasoningView/index.js +83 -0
  136. package/dist/components/ReasoningView/index.js.map +1 -0
  137. package/dist/components/ScrollToPresentButton/index.d.ts +54 -0
  138. package/dist/components/ScrollToPresentButton/index.d.ts.map +1 -0
  139. package/dist/components/ScrollToPresentButton/index.js +44 -0
  140. package/dist/components/ScrollToPresentButton/index.js.map +1 -0
  141. package/dist/components/SessionsPanel/SessionTitleModal.d.ts +79 -0
  142. package/dist/components/SessionsPanel/SessionTitleModal.d.ts.map +1 -0
  143. package/dist/components/SessionsPanel/SessionTitleModal.js +102 -0
  144. package/dist/components/SessionsPanel/SessionTitleModal.js.map +1 -0
  145. package/dist/components/SessionsPanel/index.d.ts +100 -0
  146. package/dist/components/SessionsPanel/index.d.ts.map +1 -0
  147. package/dist/components/SessionsPanel/index.js +176 -0
  148. package/dist/components/SessionsPanel/index.js.map +1 -0
  149. package/dist/components/SystemNotice/index.d.ts +66 -0
  150. package/dist/components/SystemNotice/index.d.ts.map +1 -0
  151. package/dist/components/SystemNotice/index.js +81 -0
  152. package/dist/components/SystemNotice/index.js.map +1 -0
  153. package/dist/components/ThemeSwitcher/index.d.ts +66 -0
  154. package/dist/components/ThemeSwitcher/index.d.ts.map +1 -0
  155. package/dist/components/ThemeSwitcher/index.js +73 -0
  156. package/dist/components/ThemeSwitcher/index.js.map +1 -0
  157. package/dist/components/ToolCallView/formatContent.d.ts +36 -0
  158. package/dist/components/ToolCallView/formatContent.d.ts.map +1 -0
  159. package/dist/components/ToolCallView/formatContent.js +53 -0
  160. package/dist/components/ToolCallView/formatContent.js.map +1 -0
  161. package/dist/components/ToolCallView/index.d.ts +120 -0
  162. package/dist/components/ToolCallView/index.d.ts.map +1 -0
  163. package/dist/components/ToolCallView/index.js +104 -0
  164. package/dist/components/ToolCallView/index.js.map +1 -0
  165. package/dist/components/TpmBanner/index.d.ts +53 -0
  166. package/dist/components/TpmBanner/index.d.ts.map +1 -0
  167. package/dist/components/TpmBanner/index.js +116 -0
  168. package/dist/components/TpmBanner/index.js.map +1 -0
  169. package/dist/components/TurnStatusIndicator/index.d.ts +77 -0
  170. package/dist/components/TurnStatusIndicator/index.d.ts.map +1 -0
  171. package/dist/components/TurnStatusIndicator/index.js +122 -0
  172. package/dist/components/TurnStatusIndicator/index.js.map +1 -0
  173. package/dist/components/internal/PasswordInput.d.ts +62 -0
  174. package/dist/components/internal/PasswordInput.d.ts.map +1 -0
  175. package/dist/components/internal/PasswordInput.js +42 -0
  176. package/dist/components/internal/PasswordInput.js.map +1 -0
  177. package/dist/context/AIHubContext.d.ts +220 -0
  178. package/dist/context/AIHubContext.d.ts.map +1 -0
  179. package/dist/context/AIHubContext.js +19 -0
  180. package/dist/context/AIHubContext.js.map +1 -0
  181. package/dist/context/AIHubProvider.d.ts +75 -0
  182. package/dist/context/AIHubProvider.d.ts.map +1 -0
  183. package/dist/context/AIHubProvider.js +954 -0
  184. package/dist/context/AIHubProvider.js.map +1 -0
  185. package/dist/context/index.d.ts +9 -0
  186. package/dist/context/index.d.ts.map +1 -0
  187. package/dist/context/index.js +9 -0
  188. package/dist/context/index.js.map +1 -0
  189. package/dist/context/theme.d.ts +43 -0
  190. package/dist/context/theme.d.ts.map +1 -0
  191. package/dist/context/theme.js +70 -0
  192. package/dist/context/theme.js.map +1 -0
  193. package/dist/context/useAIHub.d.ts +104 -0
  194. package/dist/context/useAIHub.d.ts.map +1 -0
  195. package/dist/context/useAIHub.js +63 -0
  196. package/dist/context/useAIHub.js.map +1 -0
  197. package/dist/context/useAIHubAuth.d.ts +44 -0
  198. package/dist/context/useAIHubAuth.d.ts.map +1 -0
  199. package/dist/context/useAIHubAuth.js +35 -0
  200. package/dist/context/useAIHubAuth.js.map +1 -0
  201. package/dist/index.d.ts +76 -0
  202. package/dist/index.d.ts.map +1 -0
  203. package/dist/index.js +57 -0
  204. package/dist/index.js.map +1 -0
  205. package/dist/primitives/index.d.ts +99 -0
  206. package/dist/primitives/index.d.ts.map +1 -0
  207. package/dist/primitives/index.js +97 -0
  208. package/dist/primitives/index.js.map +1 -0
  209. package/dist/state/affordances.d.ts +182 -0
  210. package/dist/state/affordances.d.ts.map +1 -0
  211. package/dist/state/affordances.js +189 -0
  212. package/dist/state/affordances.js.map +1 -0
  213. package/dist/state/stateReducer.d.ts +72 -0
  214. package/dist/state/stateReducer.d.ts.map +1 -0
  215. package/dist/state/stateReducer.js +122 -0
  216. package/dist/state/stateReducer.js.map +1 -0
  217. package/dist/stories/EffortControl.stories.d.ts +14 -0
  218. package/dist/stories/EffortControl.stories.d.ts.map +1 -0
  219. package/dist/stories/EffortControl.stories.js +43 -0
  220. package/dist/stories/EffortControl.stories.js.map +1 -0
  221. package/dist/stories/MarkdownRenderer.stories.d.ts +25 -0
  222. package/dist/stories/MarkdownRenderer.stories.d.ts.map +1 -0
  223. package/dist/stories/MarkdownRenderer.stories.js +178 -0
  224. package/dist/stories/MarkdownRenderer.stories.js.map +1 -0
  225. package/dist/stories/MessageBubble.stories.d.ts +21 -0
  226. package/dist/stories/MessageBubble.stories.d.ts.map +1 -0
  227. package/dist/stories/MessageBubble.stories.js +59 -0
  228. package/dist/stories/MessageBubble.stories.js.map +1 -0
  229. package/dist/stories/ReasoningView.stories.d.ts +18 -0
  230. package/dist/stories/ReasoningView.stories.d.ts.map +1 -0
  231. package/dist/stories/ReasoningView.stories.js +62 -0
  232. package/dist/stories/ReasoningView.stories.js.map +1 -0
  233. package/dist/stories/ScrollToPresentButton.stories.d.ts +18 -0
  234. package/dist/stories/ScrollToPresentButton.stories.d.ts.map +1 -0
  235. package/dist/stories/ScrollToPresentButton.stories.js +34 -0
  236. package/dist/stories/ScrollToPresentButton.stories.js.map +1 -0
  237. package/dist/stories/SystemNotice.stories.d.ts +17 -0
  238. package/dist/stories/SystemNotice.stories.d.ts.map +1 -0
  239. package/dist/stories/SystemNotice.stories.js +42 -0
  240. package/dist/stories/SystemNotice.stories.js.map +1 -0
  241. package/dist/stories/ThemeSwitcher.stories.d.ts +15 -0
  242. package/dist/stories/ThemeSwitcher.stories.d.ts.map +1 -0
  243. package/dist/stories/ThemeSwitcher.stories.js +35 -0
  244. package/dist/stories/ThemeSwitcher.stories.js.map +1 -0
  245. package/dist/stories/ToolCallView.stories.d.ts +20 -0
  246. package/dist/stories/ToolCallView.stories.d.ts.map +1 -0
  247. package/dist/stories/ToolCallView.stories.js +65 -0
  248. package/dist/stories/ToolCallView.stories.js.map +1 -0
  249. package/dist/stories/TpmBanner.stories.d.ts +24 -0
  250. package/dist/stories/TpmBanner.stories.d.ts.map +1 -0
  251. package/dist/stories/TpmBanner.stories.js +63 -0
  252. package/dist/stories/TpmBanner.stories.js.map +1 -0
  253. package/dist/stories/TurnStatusIndicator.stories.d.ts +25 -0
  254. package/dist/stories/TurnStatusIndicator.stories.d.ts.map +1 -0
  255. package/dist/stories/TurnStatusIndicator.stories.js +56 -0
  256. package/dist/stories/TurnStatusIndicator.stories.js.map +1 -0
  257. package/dist/stories/_mockProvider.d.ts +44 -0
  258. package/dist/stories/_mockProvider.d.ts.map +1 -0
  259. package/dist/stories/_mockProvider.js +84 -0
  260. package/dist/stories/_mockProvider.js.map +1 -0
  261. package/dist/types.d.ts +787 -0
  262. package/dist/types.d.ts.map +1 -0
  263. package/dist/types.js +19 -0
  264. package/dist/types.js.map +1 -0
  265. package/dist/webcomponent/defineWebComponent.d.ts +106 -0
  266. package/dist/webcomponent/defineWebComponent.d.ts.map +1 -0
  267. package/dist/webcomponent/defineWebComponent.js +126 -0
  268. package/dist/webcomponent/defineWebComponent.js.map +1 -0
  269. package/dist/webcomponent/index.d.ts +7 -0
  270. package/dist/webcomponent/index.d.ts.map +1 -0
  271. package/dist/webcomponent/index.js +7 -0
  272. package/dist/webcomponent/index.js.map +1 -0
  273. package/package.json +63 -0
  274. package/src/__lint-canary__/step1-lint-canary.ts.disabled +28 -0
  275. package/src/auth/passwordPolicy.ts +75 -0
  276. package/src/auth/texts.ts +1294 -0
  277. package/src/client/AIHubClient.ts +1718 -0
  278. package/src/client/index.ts +30 -0
  279. package/src/client/sseParser.ts +168 -0
  280. package/src/client/tokenStorage.ts +131 -0
  281. package/src/components/AgentsPicker/index.tsx +147 -0
  282. package/src/components/AuthGate/index.tsx +132 -0
  283. package/src/components/CacheBadge/index.tsx +245 -0
  284. package/src/components/ChangePassword/index.tsx +375 -0
  285. package/src/components/ChatShell/ShellHeader.tsx +80 -0
  286. package/src/components/ChatShell/UserMenu.tsx +196 -0
  287. package/src/components/ChatShell/index.tsx +307 -0
  288. package/src/components/DataClubAIHub/index.tsx +300 -0
  289. package/src/components/DataClubAIHubAdmin/AgentEditorModal.tsx +505 -0
  290. package/src/components/DataClubAIHubAdmin/AgentsPage.tsx +342 -0
  291. package/src/components/DataClubAIHubAdmin/CreateUserModal.tsx +214 -0
  292. package/src/components/DataClubAIHubAdmin/PasswordRevealModal.tsx +204 -0
  293. package/src/components/DataClubAIHubAdmin/ResetPasswordModal.tsx +122 -0
  294. package/src/components/DataClubAIHubAdmin/RotatingMessageEditorModal.tsx +408 -0
  295. package/src/components/DataClubAIHubAdmin/RotatingMessagesPage.tsx +373 -0
  296. package/src/components/DataClubAIHubAdmin/SessionsOverviewPage.tsx +639 -0
  297. package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +979 -0
  298. package/src/components/DataClubAIHubAdmin/UsersPage.tsx +495 -0
  299. package/src/components/DataClubAIHubAdmin/index.tsx +277 -0
  300. package/src/components/DataClubAIHubLogin/index.tsx +282 -0
  301. package/src/components/EffortControl/index.tsx +138 -0
  302. package/src/components/Layout/index.tsx +92 -0
  303. package/src/components/MainColumn/index.tsx +131 -0
  304. package/src/components/MarkdownRenderer/index.tsx +256 -0
  305. package/src/components/MessageInput/index.tsx +283 -0
  306. package/src/components/MessageList/index.tsx +753 -0
  307. package/src/components/ReasoningSelector/index.tsx +268 -0
  308. package/src/components/ReasoningView/index.tsx +183 -0
  309. package/src/components/ScrollToPresentButton/index.tsx +85 -0
  310. package/src/components/SessionsPanel/SessionTitleModal.tsx +256 -0
  311. package/src/components/SessionsPanel/index.tsx +406 -0
  312. package/src/components/SystemNotice/index.tsx +151 -0
  313. package/src/components/ThemeSwitcher/index.tsx +136 -0
  314. package/src/components/ToolCallView/formatContent.ts +50 -0
  315. package/src/components/ToolCallView/index.tsx +274 -0
  316. package/src/components/TpmBanner/index.tsx +172 -0
  317. package/src/components/TurnStatusIndicator/index.tsx +203 -0
  318. package/src/components/internal/PasswordInput.tsx +145 -0
  319. package/src/context/AIHubContext.ts +249 -0
  320. package/src/context/AIHubProvider.tsx +1084 -0
  321. package/src/context/index.ts +9 -0
  322. package/src/context/theme.ts +76 -0
  323. package/src/context/useAIHub.ts +151 -0
  324. package/src/context/useAIHubAuth.ts +65 -0
  325. package/src/index.ts +275 -0
  326. package/src/primitives/index.ts +101 -0
  327. package/src/state/affordances.ts +321 -0
  328. package/src/state/stateReducer.ts +131 -0
  329. package/src/stories/EffortControl.stories.tsx +63 -0
  330. package/src/stories/MarkdownRenderer.stories.tsx +201 -0
  331. package/src/stories/MessageBubble.stories.tsx +101 -0
  332. package/src/stories/ReasoningView.stories.tsx +81 -0
  333. package/src/stories/ScrollToPresentButton.stories.tsx +52 -0
  334. package/src/stories/SystemNotice.stories.tsx +59 -0
  335. package/src/stories/ThemeSwitcher.stories.tsx +55 -0
  336. package/src/stories/ToolCallView.stories.tsx +95 -0
  337. package/src/stories/TpmBanner.stories.tsx +93 -0
  338. package/src/stories/TurnStatusIndicator.stories.tsx +86 -0
  339. package/src/stories/_mockProvider.tsx +116 -0
  340. package/src/styles/CLASSES.md +356 -0
  341. package/src/styles/default.css +2937 -0
  342. package/src/types.ts +867 -0
  343. package/src/webcomponent/defineWebComponent.tsx +193 -0
  344. package/src/webcomponent/index.ts +11 -0
@@ -0,0 +1,979 @@
1
+ /**
2
+ * `<AdminSettingsPage />` — the super_admin-only Settings sub-page mounted
3
+ * inside `<DataClubAIHubAdmin />` ([08-admin-settings.md]).
4
+ *
5
+ * Five independent sections, each with its own Save button — per the spec,
6
+ * "Don't make the user save all five sections together":
7
+ *
8
+ * 1. **Letta connection** — base URL, ADE URL, API key (masked + replaceable).
9
+ * 2. **Theme** — default theme + switcher-enabled checkbox.
10
+ * 3. **UI feature toggles** — the §8.4 toggle blob.
11
+ * 4. **Signing key** — rotate with `ROTATE`-typing friction. Rotating
12
+ * invalidates every JWT, including the calling super-admin's.
13
+ * 5. **Metrics scrape token** — rotate with the same friction. The new token
14
+ * is shown ONCE via a reveal modal; subsequent settings reads no longer
15
+ * expose it. Does NOT invalidate the calling JWT.
16
+ *
17
+ * Each section owns its own dirty-state and save handler; sibling sections
18
+ * never share state. This means a half-typed Letta URL won't be pushed when
19
+ * the admin clicks `Save toggles` in a different section.
20
+ *
21
+ * Visible to `super_admin` only — the parent shell hides the nav item for
22
+ * other roles and the server returns 403 for any other caller.
23
+ *
24
+ * @see SPECIFICATIONS.md §6.3.4 / §8.3 / §8.4 / §6.7 / §6.10
25
+ * @see ui-sketches/08-admin-settings.md
26
+ */
27
+ import { useEffect, useState, type ReactNode } from "react";
28
+
29
+ import type { AIHubAdminTexts } from "../../auth/texts.js";
30
+ import type { AdminSettingsView } from "../../client/AIHubClient.js";
31
+ import { useAIHub } from "../../context/useAIHub.js";
32
+ import { useAIHubAuth } from "../../context/useAIHubAuth.js";
33
+ import type { AIHubFeatureToggles } from "../../types.js";
34
+
35
+ /** All feature-toggle keys per §8.4. */
36
+ const ALL_TOGGLE_KEYS: ReadonlyArray<keyof AIHubFeatureToggles> = [
37
+ "sessions_panel_enabled",
38
+ "sessions_panel_resizable",
39
+ "sessions_panel_collapsible",
40
+ "create_session_button_enabled",
41
+ "user_input_enabled",
42
+ "stop_button_enabled",
43
+ "tool_calls_visible",
44
+ "reasoning_visible",
45
+ "system_messages_visible",
46
+ "tpm_notice_visible",
47
+ "effort_control_visible",
48
+ "scroll_to_present_visible",
49
+ "turn_status_visible",
50
+ ];
51
+
52
+ /** Toggles grouped by the §8.4 visual grouping (purely organizational). */
53
+ const TOGGLE_GROUPS: ReadonlyArray<{
54
+ groupLabelKey: keyof AIHubAdminTexts;
55
+ keys: ReadonlyArray<keyof AIHubFeatureToggles>;
56
+ }> = [
57
+ {
58
+ groupLabelKey: "settingsTogglesGroupSessions",
59
+ keys: [
60
+ "sessions_panel_enabled",
61
+ "sessions_panel_resizable",
62
+ "sessions_panel_collapsible",
63
+ "create_session_button_enabled",
64
+ ],
65
+ },
66
+ {
67
+ groupLabelKey: "settingsTogglesGroupInput",
68
+ keys: ["user_input_enabled", "effort_control_visible"],
69
+ },
70
+ {
71
+ groupLabelKey: "settingsTogglesGroupAffordances",
72
+ keys: [
73
+ "stop_button_enabled",
74
+ "tool_calls_visible",
75
+ "reasoning_visible",
76
+ "system_messages_visible",
77
+ "tpm_notice_visible",
78
+ "scroll_to_present_visible",
79
+ "turn_status_visible",
80
+ ],
81
+ },
82
+ ];
83
+
84
+ /** Toggle defaults — every toggle is on by default per §8.4. */
85
+ function defaultToggleValue(): boolean {
86
+ return true;
87
+ }
88
+
89
+ /** Format ISO date as `YYYY-MM-DD (N days ago)`. */
90
+ function formatRotationStamp(iso: string | null): string | null {
91
+ if (iso === null) return null;
92
+ const ts = Date.parse(iso);
93
+ if (Number.isNaN(ts)) return iso;
94
+ const dateOnly = new Date(ts).toISOString().slice(0, 10);
95
+ const diffDays = Math.floor((Date.now() - ts) / (24 * 60 * 60 * 1000));
96
+ if (diffDays <= 0) return `${dateOnly} (today)`;
97
+ if (diffDays === 1) return `${dateOnly} (1 day ago)`;
98
+ return `${dateOnly} (${diffDays} days ago)`;
99
+ }
100
+
101
+ /** Props for `<AdminSettingsPage />`. */
102
+ export interface AdminSettingsPageProps {
103
+ /** Resolved admin texts. */
104
+ texts: AIHubAdminTexts;
105
+ }
106
+
107
+ /** Renders the Settings sub-page. */
108
+ export function AdminSettingsPage(props: AdminSettingsPageProps): ReactNode {
109
+ const { client } = useAIHub();
110
+
111
+ const [settings, setSettings] = useState<AdminSettingsView | null>(null);
112
+ const [loadError, setLoadError] = useState<string | null>(null);
113
+
114
+ useEffect(() => {
115
+ let cancelled = false;
116
+ void (async () => {
117
+ try {
118
+ const result = await client.getAdminSettings();
119
+ if (!cancelled) {
120
+ setSettings(result);
121
+ setLoadError(null);
122
+ }
123
+ } catch (err) {
124
+ if (!cancelled) {
125
+ setLoadError(
126
+ err instanceof Error && err.message.length > 0
127
+ ? err.message
128
+ : props.texts.settingsGenericError,
129
+ );
130
+ }
131
+ }
132
+ })();
133
+ return () => {
134
+ cancelled = true;
135
+ };
136
+ }, [client, props.texts.settingsGenericError]);
137
+
138
+ function handleSectionSaved(next: AdminSettingsView): void {
139
+ setSettings(next);
140
+ }
141
+
142
+ return (
143
+ <div
144
+ className="data-club-ai-hub-admin__settings-page"
145
+ data-testid="admin-settings-page"
146
+ >
147
+ <header className="data-club-ai-hub-admin__page-header">
148
+ <h1
149
+ className="data-club-ai-hub-admin__page-heading"
150
+ data-testid="admin-settings-heading"
151
+ >
152
+ {props.texts.settingsHeading}
153
+ </h1>
154
+ </header>
155
+ {loadError !== null ? (
156
+ <div
157
+ className="data-club-ai-hub-admin__error"
158
+ role="alert"
159
+ data-testid="admin-settings-error"
160
+ >
161
+ {loadError}
162
+ </div>
163
+ ) : null}
164
+ {settings === null && loadError === null ? (
165
+ <p
166
+ className="data-club-ai-hub-admin__loading"
167
+ data-testid="admin-settings-loading"
168
+ >
169
+ {props.texts.settingsLoading}
170
+ </p>
171
+ ) : null}
172
+ {settings !== null ? (
173
+ <>
174
+ <LettaConnectionSection
175
+ settings={settings}
176
+ texts={props.texts}
177
+ onSaved={handleSectionSaved}
178
+ />
179
+ <ThemeSection
180
+ settings={settings}
181
+ texts={props.texts}
182
+ onSaved={handleSectionSaved}
183
+ />
184
+ <FeatureTogglesSection
185
+ settings={settings}
186
+ texts={props.texts}
187
+ onSaved={(toggles) =>
188
+ handleSectionSaved({ ...settings, feature_toggles: toggles })
189
+ }
190
+ />
191
+ <SigningKeySection
192
+ settings={settings}
193
+ texts={props.texts}
194
+ onRotated={(rotatedAt) =>
195
+ handleSectionSaved({
196
+ ...settings,
197
+ signing_key_rotated_at: rotatedAt,
198
+ })
199
+ }
200
+ />
201
+ <MetricsTokenSection
202
+ settings={settings}
203
+ texts={props.texts}
204
+ onRotated={(rotatedAt) =>
205
+ handleSectionSaved({
206
+ ...settings,
207
+ metrics_scrape_token_rotated_at: rotatedAt,
208
+ })
209
+ }
210
+ />
211
+ </>
212
+ ) : null}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ /* ────────────────────────────────────────────────────────────────────────── */
218
+ /* Section: Letta connection */
219
+ /* ────────────────────────────────────────────────────────────────────────── */
220
+
221
+ interface SectionProps {
222
+ settings: AdminSettingsView;
223
+ texts: AIHubAdminTexts;
224
+ }
225
+
226
+ interface LettaConnectionSectionProps extends SectionProps {
227
+ onSaved: (next: AdminSettingsView) => void;
228
+ }
229
+
230
+ function LettaConnectionSection(props: LettaConnectionSectionProps): ReactNode {
231
+ const { client } = useAIHub();
232
+ const [baseUrl, setBaseUrl] = useState(props.settings.letta_base_url ?? "");
233
+ const [adeUrl, setAdeUrl] = useState(props.settings.letta_ade_url ?? "");
234
+ // Empty string = leave-as-is; non-empty = replace.
235
+ const [apiKey, setApiKey] = useState("");
236
+ const [saving, setSaving] = useState(false);
237
+ const [flash, setFlash] = useState<{ tone: "success" | "error"; text: string } | null>(
238
+ null,
239
+ );
240
+
241
+ async function handleSave(event: React.FormEvent): Promise<void> {
242
+ event.preventDefault();
243
+ setSaving(true);
244
+ setFlash(null);
245
+ try {
246
+ const body: Parameters<typeof client.patchAdminSettings>[0] = {};
247
+ // Compare against current to avoid stomping the existing value with the
248
+ // identical one. The server treats null + value identically, but the
249
+ // schema forbids `null + ""` so we coerce explicitly.
250
+ if (baseUrl !== (props.settings.letta_base_url ?? "")) {
251
+ body.lettaBaseUrl = baseUrl === "" ? null : baseUrl;
252
+ }
253
+ if (adeUrl !== (props.settings.letta_ade_url ?? "")) {
254
+ body.lettaAdeUrl = adeUrl === "" ? null : adeUrl;
255
+ }
256
+ if (apiKey !== "") body.lettaApiKey = apiKey;
257
+ const next = await client.patchAdminSettings(body);
258
+ props.onSaved(next);
259
+ setApiKey("");
260
+ setFlash({ tone: "success", text: props.texts.settingsSaveSuccessFlash });
261
+ } catch (err) {
262
+ setFlash({
263
+ tone: "error",
264
+ text:
265
+ err instanceof Error && err.message.length > 0
266
+ ? err.message
267
+ : props.texts.settingsSaveGenericError,
268
+ });
269
+ } finally {
270
+ setSaving(false);
271
+ }
272
+ }
273
+
274
+ // Clears the DB column (sends `letta_api_key: ""`), which makes the
275
+ // server fall back to env.LETTA_API_KEY at runtime per the
276
+ // resolveApiKey precedence in letta/client.ts. Same code path as the
277
+ // base/ADE URL nulling — only it targets the API-key column.
278
+ async function handleClearApiKeyOverride(): Promise<void> {
279
+ setSaving(true);
280
+ setFlash(null);
281
+ try {
282
+ const next = await client.patchAdminSettings({ lettaApiKey: "" });
283
+ props.onSaved(next);
284
+ setFlash({ tone: "success", text: props.texts.settingsSaveSuccessFlash });
285
+ } catch (err) {
286
+ setFlash({
287
+ tone: "error",
288
+ text:
289
+ err instanceof Error && err.message.length > 0
290
+ ? err.message
291
+ : props.texts.settingsSaveGenericError,
292
+ });
293
+ } finally {
294
+ setSaving(false);
295
+ }
296
+ }
297
+
298
+ return (
299
+ <section
300
+ className="data-club-ai-hub-admin__settings-section"
301
+ data-testid="admin-settings-letta-section"
302
+ >
303
+ <h2 className="data-club-ai-hub-admin__settings-section-heading">
304
+ {props.texts.settingsLettaSectionHeading}
305
+ </h2>
306
+ <form onSubmit={(event) => void handleSave(event)} noValidate>
307
+ <label className="data-club-ai-hub-admin__form-field">
308
+ <span className="data-club-ai-hub-admin__form-label">
309
+ {props.texts.settingsLettaBaseUrlLabel}
310
+ </span>
311
+ <input
312
+ type="url"
313
+ className="data-club-ai-hub-admin__form-input"
314
+ data-testid="admin-settings-letta-base-url"
315
+ value={baseUrl}
316
+ onChange={(event) => setBaseUrl(event.target.value)}
317
+ disabled={saving}
318
+ />
319
+ </label>
320
+ <label className="data-club-ai-hub-admin__form-field">
321
+ <span className="data-club-ai-hub-admin__form-label">
322
+ {props.texts.settingsLettaAdeUrlLabel}
323
+ </span>
324
+ <input
325
+ type="url"
326
+ className="data-club-ai-hub-admin__form-input"
327
+ data-testid="admin-settings-letta-ade-url"
328
+ value={adeUrl}
329
+ onChange={(event) => setAdeUrl(event.target.value)}
330
+ disabled={saving}
331
+ />
332
+ <span className="data-club-ai-hub-admin__form-helper">
333
+ {props.texts.settingsLettaAdeUrlHelper}
334
+ </span>
335
+ </label>
336
+ <label className="data-club-ai-hub-admin__form-field">
337
+ <span className="data-club-ai-hub-admin__form-label">
338
+ {props.texts.settingsLettaApiKeyLabel}
339
+ </span>
340
+ <input
341
+ type="password"
342
+ className="data-club-ai-hub-admin__form-input"
343
+ data-testid="admin-settings-letta-api-key"
344
+ value={apiKey}
345
+ placeholder={
346
+ props.settings.letta_api_key_source === "db"
347
+ ? props.texts.settingsLettaApiKeyPlaceholder
348
+ : ""
349
+ }
350
+ onChange={(event) => setApiKey(event.target.value)}
351
+ disabled={saving}
352
+ autoComplete="new-password"
353
+ />
354
+ <span
355
+ className="data-club-ai-hub-admin__form-helper"
356
+ data-testid="admin-settings-letta-api-key-helper"
357
+ data-source={props.settings.letta_api_key_source}
358
+ >
359
+ {props.settings.letta_api_key_source === "db"
360
+ ? props.texts.settingsLettaApiKeyDbHelper
361
+ : props.settings.letta_api_key_source === "env"
362
+ ? props.texts.settingsLettaApiKeyEnvHelper
363
+ : props.texts.settingsLettaApiKeyNoneHelper}
364
+ </span>
365
+ {props.settings.letta_api_key_source === "db" ? (
366
+ <button
367
+ type="button"
368
+ className="data-club-ai-hub-admin__button"
369
+ data-testid="admin-settings-letta-api-key-clear"
370
+ onClick={() => void handleClearApiKeyOverride()}
371
+ disabled={saving}
372
+ >
373
+ {props.texts.settingsLettaApiKeyClearOverrideButton}
374
+ </button>
375
+ ) : null}
376
+ </label>
377
+ {flash !== null ? (
378
+ <div
379
+ className={
380
+ flash.tone === "success"
381
+ ? "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--success"
382
+ : "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--error"
383
+ }
384
+ data-testid="admin-settings-letta-flash"
385
+ role={flash.tone === "error" ? "alert" : "status"}
386
+ >
387
+ {flash.text}
388
+ </div>
389
+ ) : null}
390
+ <button
391
+ type="submit"
392
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--primary"
393
+ data-testid="admin-settings-letta-save"
394
+ disabled={saving}
395
+ >
396
+ {saving
397
+ ? props.texts.settingsSaveSavingButton
398
+ : props.texts.settingsSaveLettaButton}
399
+ </button>
400
+ </form>
401
+ </section>
402
+ );
403
+ }
404
+
405
+ /* ────────────────────────────────────────────────────────────────────────── */
406
+ /* Section: Theme */
407
+ /* ────────────────────────────────────────────────────────────────────────── */
408
+
409
+ interface ThemeSectionProps extends SectionProps {
410
+ onSaved: (next: AdminSettingsView) => void;
411
+ }
412
+
413
+ function ThemeSection(props: ThemeSectionProps): ReactNode {
414
+ const { client } = useAIHub();
415
+ const [themeDefault, setThemeDefault] = useState<"light" | "dark">(
416
+ props.settings.theme.default,
417
+ );
418
+ const [switcherEnabled, setSwitcherEnabled] = useState(
419
+ props.settings.theme.switcher_enabled,
420
+ );
421
+ const [saving, setSaving] = useState(false);
422
+ const [flash, setFlash] = useState<{ tone: "success" | "error"; text: string } | null>(
423
+ null,
424
+ );
425
+
426
+ async function handleSave(event: React.FormEvent): Promise<void> {
427
+ event.preventDefault();
428
+ setSaving(true);
429
+ setFlash(null);
430
+ try {
431
+ const next = await client.patchAdminSettings({
432
+ theme: { default: themeDefault, switcherEnabled },
433
+ });
434
+ props.onSaved(next);
435
+ setFlash({ tone: "success", text: props.texts.settingsSaveSuccessFlash });
436
+ } catch (err) {
437
+ setFlash({
438
+ tone: "error",
439
+ text:
440
+ err instanceof Error && err.message.length > 0
441
+ ? err.message
442
+ : props.texts.settingsSaveGenericError,
443
+ });
444
+ } finally {
445
+ setSaving(false);
446
+ }
447
+ }
448
+
449
+ return (
450
+ <section
451
+ className="data-club-ai-hub-admin__settings-section"
452
+ data-testid="admin-settings-theme-section"
453
+ >
454
+ <h2 className="data-club-ai-hub-admin__settings-section-heading">
455
+ {props.texts.settingsThemeSectionHeading}
456
+ </h2>
457
+ <form onSubmit={(event) => void handleSave(event)} noValidate>
458
+ <fieldset className="data-club-ai-hub-admin__radio-group">
459
+ <legend className="data-club-ai-hub-admin__form-label">
460
+ {props.texts.settingsThemeDefaultLabel}
461
+ </legend>
462
+ <label className="data-club-ai-hub-admin__radio">
463
+ <input
464
+ type="radio"
465
+ name="theme-default"
466
+ value="light"
467
+ data-testid="admin-settings-theme-default-light"
468
+ checked={themeDefault === "light"}
469
+ onChange={() => setThemeDefault("light")}
470
+ disabled={saving}
471
+ />
472
+ <span>{props.texts.settingsThemeDefaultLight}</span>
473
+ </label>
474
+ <label className="data-club-ai-hub-admin__radio">
475
+ <input
476
+ type="radio"
477
+ name="theme-default"
478
+ value="dark"
479
+ data-testid="admin-settings-theme-default-dark"
480
+ checked={themeDefault === "dark"}
481
+ onChange={() => setThemeDefault("dark")}
482
+ disabled={saving}
483
+ />
484
+ <span>{props.texts.settingsThemeDefaultDark}</span>
485
+ </label>
486
+ </fieldset>
487
+ <label className="data-club-ai-hub-admin__checkbox">
488
+ <input
489
+ type="checkbox"
490
+ data-testid="admin-settings-theme-switcher-enabled"
491
+ checked={switcherEnabled}
492
+ onChange={(event) => setSwitcherEnabled(event.target.checked)}
493
+ disabled={saving}
494
+ />
495
+ <span>{props.texts.settingsThemeSwitcherEnabledLabel}</span>
496
+ </label>
497
+ <p className="data-club-ai-hub-admin__form-helper">
498
+ {props.texts.settingsThemeSwitcherEnabledHelper}
499
+ </p>
500
+ {flash !== null ? (
501
+ <div
502
+ className={
503
+ flash.tone === "success"
504
+ ? "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--success"
505
+ : "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--error"
506
+ }
507
+ data-testid="admin-settings-theme-flash"
508
+ role={flash.tone === "error" ? "alert" : "status"}
509
+ >
510
+ {flash.text}
511
+ </div>
512
+ ) : null}
513
+ <button
514
+ type="submit"
515
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--primary"
516
+ data-testid="admin-settings-theme-save"
517
+ disabled={saving}
518
+ >
519
+ {saving
520
+ ? props.texts.settingsSaveSavingButton
521
+ : props.texts.settingsSaveThemeButton}
522
+ </button>
523
+ </form>
524
+ </section>
525
+ );
526
+ }
527
+
528
+ /* ────────────────────────────────────────────────────────────────────────── */
529
+ /* Section: Feature toggles */
530
+ /* ────────────────────────────────────────────────────────────────────────── */
531
+
532
+ interface FeatureTogglesSectionProps extends SectionProps {
533
+ onSaved: (next: AIHubFeatureToggles) => void;
534
+ }
535
+
536
+ function FeatureTogglesSection(props: FeatureTogglesSectionProps): ReactNode {
537
+ const { client } = useAIHub();
538
+ // The server's blob may omit keys (meaning "fall through to defaults"). The
539
+ // UI shows the effective value for the user — fill in defaults for any
540
+ // missing key. Save only sends the keys the user actually has toggled (in
541
+ // our case all of them, since the blob always carries all 13 once saved).
542
+ const [values, setValues] = useState<Record<string, boolean>>(() => {
543
+ const initial: Record<string, boolean> = {};
544
+ for (const key of ALL_TOGGLE_KEYS) {
545
+ const fromServer = props.settings.feature_toggles[key];
546
+ initial[key] = typeof fromServer === "boolean" ? fromServer : defaultToggleValue();
547
+ }
548
+ return initial;
549
+ });
550
+ const [saving, setSaving] = useState(false);
551
+ const [flash, setFlash] = useState<{ tone: "success" | "error"; text: string } | null>(
552
+ null,
553
+ );
554
+
555
+ function toggle(key: string, value: boolean): void {
556
+ setValues((prev) => ({ ...prev, [key]: value }));
557
+ }
558
+
559
+ async function handleSave(event: React.FormEvent): Promise<void> {
560
+ event.preventDefault();
561
+ setSaving(true);
562
+ setFlash(null);
563
+ try {
564
+ const body: AIHubFeatureToggles = {};
565
+ for (const key of ALL_TOGGLE_KEYS) {
566
+ (body as Record<string, boolean>)[key] = values[key]!;
567
+ }
568
+ const next = await client.patchAdminFeatureToggles(body);
569
+ props.onSaved(next);
570
+ setFlash({ tone: "success", text: props.texts.settingsSaveSuccessFlash });
571
+ } catch (err) {
572
+ setFlash({
573
+ tone: "error",
574
+ text:
575
+ err instanceof Error && err.message.length > 0
576
+ ? err.message
577
+ : props.texts.settingsSaveGenericError,
578
+ });
579
+ } finally {
580
+ setSaving(false);
581
+ }
582
+ }
583
+
584
+ return (
585
+ <section
586
+ className="data-club-ai-hub-admin__settings-section"
587
+ data-testid="admin-settings-toggles-section"
588
+ >
589
+ <h2 className="data-club-ai-hub-admin__settings-section-heading">
590
+ {props.texts.settingsTogglesSectionHeading}
591
+ </h2>
592
+ <form onSubmit={(event) => void handleSave(event)} noValidate>
593
+ {TOGGLE_GROUPS.map((group) => (
594
+ <fieldset
595
+ key={String(group.groupLabelKey)}
596
+ className="data-club-ai-hub-admin__toggle-group"
597
+ >
598
+ <legend className="data-club-ai-hub-admin__form-label">
599
+ {props.texts[group.groupLabelKey] as string}
600
+ </legend>
601
+ {group.keys.map((key) => (
602
+ <label key={key} className="data-club-ai-hub-admin__checkbox">
603
+ <input
604
+ type="checkbox"
605
+ data-testid={`admin-settings-toggle-${key}`}
606
+ checked={Boolean(values[key])}
607
+ onChange={(event) => toggle(key, event.target.checked)}
608
+ disabled={saving}
609
+ />
610
+ <code className="data-club-ai-hub-admin__mono">{key}</code>
611
+ </label>
612
+ ))}
613
+ </fieldset>
614
+ ))}
615
+ {flash !== null ? (
616
+ <div
617
+ className={
618
+ flash.tone === "success"
619
+ ? "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--success"
620
+ : "data-club-ai-hub-admin__flash data-club-ai-hub-admin__flash--error"
621
+ }
622
+ data-testid="admin-settings-toggles-flash"
623
+ role={flash.tone === "error" ? "alert" : "status"}
624
+ >
625
+ {flash.text}
626
+ </div>
627
+ ) : null}
628
+ <button
629
+ type="submit"
630
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--primary"
631
+ data-testid="admin-settings-toggles-save"
632
+ disabled={saving}
633
+ >
634
+ {saving
635
+ ? props.texts.settingsSaveSavingButton
636
+ : props.texts.settingsTogglesSaveButton}
637
+ </button>
638
+ </form>
639
+ </section>
640
+ );
641
+ }
642
+
643
+ /* ────────────────────────────────────────────────────────────────────────── */
644
+ /* Section: Signing key */
645
+ /* ────────────────────────────────────────────────────────────────────────── */
646
+
647
+ interface SigningKeySectionProps extends SectionProps {
648
+ onRotated: (rotatedAt: string) => void;
649
+ }
650
+
651
+ function SigningKeySection(props: SigningKeySectionProps): ReactNode {
652
+ const { client } = useAIHub();
653
+ const auth = useAIHubAuth();
654
+ const [showModal, setShowModal] = useState(false);
655
+ const lastRotated = formatRotationStamp(props.settings.signing_key_rotated_at);
656
+
657
+ async function handleRotateConfirmed(): Promise<void> {
658
+ await client.rotateAdminSigningKey();
659
+ // Rotating invalidates every JWT, including this caller's. The provider
660
+ // will see the 401 on the next request and route back to login; we
661
+ // accelerate that by calling logout right here so the UI doesn't sit on
662
+ // a stale token. Update the local card first for the milliseconds the
663
+ // modal is still on screen.
664
+ props.onRotated(new Date().toISOString());
665
+ setShowModal(false);
666
+ auth.logout();
667
+ }
668
+
669
+ return (
670
+ <section
671
+ className="data-club-ai-hub-admin__settings-section"
672
+ data-testid="admin-settings-signing-key-section"
673
+ >
674
+ <h2 className="data-club-ai-hub-admin__settings-section-heading">
675
+ {props.texts.settingsSigningKeySectionHeading}
676
+ </h2>
677
+ <p className="data-club-ai-hub-admin__form-helper">
678
+ {lastRotated === null
679
+ ? props.texts.settingsSigningKeyNeverRotated
680
+ : props.texts.settingsSigningKeyLastRotated.replace("{date}", lastRotated)}
681
+ </p>
682
+ <p
683
+ className="data-club-ai-hub-admin__warning"
684
+ data-testid="admin-settings-signing-key-warning"
685
+ >
686
+ {props.texts.settingsSigningKeyWarning}
687
+ </p>
688
+ <button
689
+ type="button"
690
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--danger"
691
+ data-testid="admin-settings-rotate-signing-key"
692
+ onClick={() => setShowModal(true)}
693
+ >
694
+ {props.texts.settingsRotateSigningKeyButton}
695
+ </button>
696
+ {showModal ? (
697
+ <RotateConfirmModal
698
+ title={props.texts.settingsRotateSigningKeyModalTitle}
699
+ body={props.texts.settingsRotateSigningKeyModalBody}
700
+ texts={props.texts}
701
+ testIdPrefix="admin-settings-signing-key-modal"
702
+ onConfirm={handleRotateConfirmed}
703
+ onCancel={() => setShowModal(false)}
704
+ />
705
+ ) : null}
706
+ </section>
707
+ );
708
+ }
709
+
710
+ /* ────────────────────────────────────────────────────────────────────────── */
711
+ /* Section: Metrics scrape token */
712
+ /* ────────────────────────────────────────────────────────────────────────── */
713
+
714
+ interface MetricsTokenSectionProps extends SectionProps {
715
+ onRotated: (rotatedAt: string) => void;
716
+ }
717
+
718
+ function MetricsTokenSection(props: MetricsTokenSectionProps): ReactNode {
719
+ const { client } = useAIHub();
720
+ const [showConfirm, setShowConfirm] = useState(false);
721
+ const [revealedToken, setRevealedToken] = useState<string | null>(null);
722
+ const lastRotated = formatRotationStamp(
723
+ props.settings.metrics_scrape_token_rotated_at,
724
+ );
725
+
726
+ async function handleRotateConfirmed(): Promise<void> {
727
+ const { token } = await client.rotateAdminMetricsToken();
728
+ props.onRotated(new Date().toISOString());
729
+ setShowConfirm(false);
730
+ setRevealedToken(token);
731
+ }
732
+
733
+ return (
734
+ <section
735
+ className="data-club-ai-hub-admin__settings-section"
736
+ data-testid="admin-settings-metrics-token-section"
737
+ >
738
+ <h2 className="data-club-ai-hub-admin__settings-section-heading">
739
+ {props.texts.settingsMetricsSectionHeading}
740
+ </h2>
741
+ <p className="data-club-ai-hub-admin__form-helper">
742
+ {lastRotated === null
743
+ ? props.texts.settingsMetricsNeverRotated
744
+ : props.texts.settingsMetricsLastRotated.replace("{date}", lastRotated)}
745
+ </p>
746
+ <p
747
+ className="data-club-ai-hub-admin__warning"
748
+ data-testid="admin-settings-metrics-warning"
749
+ >
750
+ {props.texts.settingsMetricsWarning}
751
+ </p>
752
+ <button
753
+ type="button"
754
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--danger"
755
+ data-testid="admin-settings-rotate-metrics-token"
756
+ onClick={() => setShowConfirm(true)}
757
+ >
758
+ {props.texts.settingsRotateMetricsButton}
759
+ </button>
760
+ {showConfirm ? (
761
+ <RotateConfirmModal
762
+ title={props.texts.settingsRotateMetricsModalTitle}
763
+ body={props.texts.settingsRotateMetricsModalBody}
764
+ texts={props.texts}
765
+ testIdPrefix="admin-settings-metrics-token-modal"
766
+ onConfirm={handleRotateConfirmed}
767
+ onCancel={() => setShowConfirm(false)}
768
+ />
769
+ ) : null}
770
+ {revealedToken !== null ? (
771
+ <MetricsTokenRevealModal
772
+ token={revealedToken}
773
+ texts={props.texts}
774
+ onClose={() => setRevealedToken(null)}
775
+ />
776
+ ) : null}
777
+ </section>
778
+ );
779
+ }
780
+
781
+ /* ────────────────────────────────────────────────────────────────────────── */
782
+ /* Modal: rotate-confirm with ROTATE-typing friction */
783
+ /* ────────────────────────────────────────────────────────────────────────── */
784
+
785
+ interface RotateConfirmModalProps {
786
+ title: string;
787
+ body: string;
788
+ texts: AIHubAdminTexts;
789
+ testIdPrefix: string;
790
+ onConfirm: () => Promise<void>;
791
+ onCancel: () => void;
792
+ }
793
+
794
+ function RotateConfirmModal(props: RotateConfirmModalProps): ReactNode {
795
+ const [typed, setTyped] = useState("");
796
+ const [submitting, setSubmitting] = useState(false);
797
+ const [error, setError] = useState<string | null>(null);
798
+ const canConfirm = typed === props.texts.settingsRotateConfirmKeyword;
799
+
800
+ async function handleConfirm(): Promise<void> {
801
+ if (!canConfirm) return;
802
+ setSubmitting(true);
803
+ setError(null);
804
+ try {
805
+ await props.onConfirm();
806
+ } catch (err) {
807
+ setError(
808
+ err instanceof Error && err.message.length > 0
809
+ ? err.message
810
+ : props.texts.settingsRotateGenericError,
811
+ );
812
+ setSubmitting(false);
813
+ }
814
+ }
815
+
816
+ return (
817
+ <div
818
+ className="data-club-ai-hub-admin__modal-backdrop"
819
+ role="presentation"
820
+ onMouseDown={(event) => {
821
+ if (event.target === event.currentTarget && !submitting) props.onCancel();
822
+ }}
823
+ >
824
+ <div
825
+ className="data-club-ai-hub-admin__modal"
826
+ role="dialog"
827
+ aria-modal="true"
828
+ data-testid={props.testIdPrefix}
829
+ >
830
+ <h2 className="data-club-ai-hub-admin__modal-title">{props.title}</h2>
831
+ <p
832
+ className="data-club-ai-hub-admin__modal-body"
833
+ data-testid={`${props.testIdPrefix}-body`}
834
+ >
835
+ {props.body}
836
+ </p>
837
+ <label className="data-club-ai-hub-admin__form-field">
838
+ <span className="data-club-ai-hub-admin__form-label">
839
+ {props.texts.settingsRotateConfirmInputLabel}
840
+ </span>
841
+ <input
842
+ type="text"
843
+ className="data-club-ai-hub-admin__form-input"
844
+ data-testid={`${props.testIdPrefix}-input`}
845
+ value={typed}
846
+ placeholder={props.texts.settingsRotateConfirmPlaceholder}
847
+ onChange={(event) => setTyped(event.target.value)}
848
+ disabled={submitting}
849
+ autoFocus
850
+ />
851
+ </label>
852
+ {error !== null ? (
853
+ <div
854
+ className="data-club-ai-hub-admin__error"
855
+ role="alert"
856
+ data-testid={`${props.testIdPrefix}-error`}
857
+ >
858
+ {error}
859
+ </div>
860
+ ) : null}
861
+ <div className="data-club-ai-hub-admin__modal-actions">
862
+ <button
863
+ type="button"
864
+ className="data-club-ai-hub-admin__button"
865
+ data-testid={`${props.testIdPrefix}-cancel`}
866
+ onClick={props.onCancel}
867
+ disabled={submitting}
868
+ >
869
+ {props.texts.agentModalCancelButton}
870
+ </button>
871
+ <button
872
+ type="button"
873
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--danger"
874
+ data-testid={`${props.testIdPrefix}-confirm`}
875
+ onClick={() => void handleConfirm()}
876
+ disabled={!canConfirm || submitting}
877
+ >
878
+ {submitting
879
+ ? props.texts.settingsRotateConfirmingButton
880
+ : props.texts.settingsRotateConfirmButton}
881
+ </button>
882
+ </div>
883
+ </div>
884
+ </div>
885
+ );
886
+ }
887
+
888
+ /* ────────────────────────────────────────────────────────────────────────── */
889
+ /* Modal: metrics-token reveal (one-time) */
890
+ /* ────────────────────────────────────────────────────────────────────────── */
891
+
892
+ interface MetricsTokenRevealModalProps {
893
+ token: string;
894
+ texts: AIHubAdminTexts;
895
+ onClose: () => void;
896
+ }
897
+
898
+ function MetricsTokenRevealModal(props: MetricsTokenRevealModalProps): ReactNode {
899
+ const [copied, setCopied] = useState(false);
900
+
901
+ async function handleCopy(): Promise<void> {
902
+ try {
903
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
904
+ await navigator.clipboard.writeText(props.token);
905
+ }
906
+ } catch {
907
+ /* fall through — flash still fires to signal the click */
908
+ }
909
+ setCopied(true);
910
+ setTimeout(() => setCopied(false), 1800);
911
+ }
912
+
913
+ return (
914
+ <div
915
+ className="data-club-ai-hub-admin__modal-backdrop"
916
+ role="presentation"
917
+ >
918
+ <div
919
+ className="data-club-ai-hub-admin__modal"
920
+ role="dialog"
921
+ aria-modal="true"
922
+ data-testid="admin-settings-metrics-reveal-modal"
923
+ >
924
+ <h2 className="data-club-ai-hub-admin__modal-title">
925
+ {props.texts.settingsMetricsRevealTitle}
926
+ </h2>
927
+ <p
928
+ className="data-club-ai-hub-admin__warning"
929
+ data-testid="admin-settings-metrics-reveal-warning"
930
+ >
931
+ {props.texts.settingsMetricsRevealWarning}
932
+ </p>
933
+ <label className="data-club-ai-hub-admin__form-field">
934
+ <span className="data-club-ai-hub-admin__form-label">
935
+ {props.texts.settingsMetricsRevealTokenLabel}
936
+ </span>
937
+ <div className="data-club-ai-hub-admin__reveal-row">
938
+ <input
939
+ type="text"
940
+ className="data-club-ai-hub-admin__form-input data-club-ai-hub-admin__mono"
941
+ data-testid="admin-settings-metrics-reveal-token"
942
+ readOnly
943
+ value={props.token}
944
+ />
945
+ <button
946
+ type="button"
947
+ className="data-club-ai-hub-admin__button"
948
+ data-testid="admin-settings-metrics-reveal-copy"
949
+ aria-label={props.texts.passwordRevealCopyAriaLabel}
950
+ onClick={() => void handleCopy()}
951
+ >
952
+ 📋
953
+ </button>
954
+ </div>
955
+ {copied ? (
956
+ <span
957
+ className="data-club-ai-hub-admin__copied-flash"
958
+ data-testid="admin-settings-metrics-reveal-copied"
959
+ role="status"
960
+ >
961
+ {props.texts.passwordRevealCopiedFlash}
962
+ </span>
963
+ ) : null}
964
+ </label>
965
+ <div className="data-club-ai-hub-admin__modal-actions">
966
+ <button
967
+ type="button"
968
+ className="data-club-ai-hub-admin__button data-club-ai-hub-admin__button--primary"
969
+ data-testid="admin-settings-metrics-reveal-done"
970
+ onClick={props.onClose}
971
+ >
972
+ {props.texts.passwordRevealDoneButton}
973
+ </button>
974
+ </div>
975
+ </div>
976
+ </div>
977
+ );
978
+ }
979
+