@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,1084 @@
1
+ /**
2
+ * `<AIHubProvider>` — root of every ai-hub React tree.
3
+ *
4
+ * Owns:
5
+ *
6
+ * - The shared `AIHubClient` instance (consumers call SDK methods through
7
+ * `useAIHub().client`).
8
+ * - The JWT lifecycle: read from `tokenStorage` on mount, attempt `/auth/me`
9
+ * to hydrate `user` + `deployment`; clear on logout; clear and route
10
+ * back to login on any 401 from the server (§6.7 — no silent refresh).
11
+ * - The resolved theme: client-side precedence per §7.6, FOUC-avoided via
12
+ * a cached value in `localStorage` applied on first paint.
13
+ * - Per-message reasoning effort state.
14
+ * - **Step 9 — chat state:** agents catalog, active agent, sessions list,
15
+ * active session, message history, in-flight turn state (driven by the
16
+ * §10 reducer over SSE events from `client.streamChat`), `sendMessage`,
17
+ * `stop`. The state-aware affordance gates (TPM banner, theme switcher,
18
+ * tool-call card, reasoning view) layer on top in Step 10.
19
+ *
20
+ * @see SPECIFICATIONS.md §7.2 — provider props
21
+ * @see SPECIFICATIONS.md §7.5 — state on the client
22
+ * @see SPECIFICATIONS.md §7.6 — theme resolution
23
+ * @see SPECIFICATIONS.md §6.7 — auth model
24
+ */
25
+
26
+ import {
27
+ useCallback,
28
+ useEffect,
29
+ useMemo,
30
+ useRef,
31
+ useState,
32
+ type ReactNode,
33
+ } from "react";
34
+
35
+ import {
36
+ AIHubClient,
37
+ AIHubRequestError,
38
+ type AgentSummary,
39
+ type PatchSessionOptions,
40
+ } from "../client/AIHubClient.js";
41
+ import type { AIHubEffort } from "../client/AIHubClient.js";
42
+ import type { TokenStorageMode, TokenStorage } from "../client/tokenStorage.js";
43
+ import {
44
+ initialAIHubState,
45
+ reduceAIHubState,
46
+ } from "../state/stateReducer.js";
47
+ import type {
48
+ AIHubAuthContext,
49
+ AIHubDeployment,
50
+ AIHubError,
51
+ AIHubFeatureToggles,
52
+ AIHubMessage,
53
+ AIHubRole,
54
+ AIHubState,
55
+ AIHubToolCallRecord,
56
+ AIHubUser,
57
+ Session,
58
+ } from "../types.js";
59
+
60
+ import { aiHubContext, type AIHubContextValue } from "./AIHubContext.js";
61
+ import {
62
+ readCachedTheme,
63
+ resolveTheme,
64
+ writeCachedTheme,
65
+ type ResolvedTheme,
66
+ } from "./theme.js";
67
+
68
+ /**
69
+ * When the post-stream canonicalize refetches messages from the server,
70
+ * we merge the optimistic-streaming `reasoning` + `tool_calls` we
71
+ * captured during the turn into the canonical assistant message that
72
+ * carries the same id. This handles two cases:
73
+ *
74
+ * 1. **Race window.** Letta's `tool_return_message` commit can lag the
75
+ * SSE `tool_call_result` event by a few ms; if our refetch hits in
76
+ * that window the projector sees the tool_call but no return, so
77
+ * the canonical row's `tool_calls` is missing or stale.
78
+ * 2. **Older server build.** Operators upgrading mid-session may still
79
+ * be running a server without the §10.x aggregating projector. The
80
+ * canonical row carries no `reasoning` / `tool_calls` at all.
81
+ *
82
+ * Either way, the optimistic data we already showed during streaming
83
+ * is correct; merging it onto the canonical row keeps it visible.
84
+ *
85
+ * @param canonical - the freshly-refetched list from
86
+ * `client.getSessionMessages`.
87
+ * @param optimistic - the message list we held in state before the
88
+ * refetch (post-streaming accumulation).
89
+ * @returns a new list, same identity / shape as `canonical`, with the
90
+ * missing affordance fields filled in from `optimistic` when present.
91
+ */
92
+ function mergeOptimisticAffordances(
93
+ canonical: ReadonlyArray<AIHubMessage>,
94
+ optimistic: ReadonlyArray<AIHubMessage>,
95
+ ): AIHubMessage[] {
96
+ const optimisticById = new Map<string, AIHubMessage>();
97
+ for (const m of optimistic) {
98
+ if (m.role === "assistant" && (m.reasoning || m.tool_calls?.length || m.usage)) {
99
+ optimisticById.set(m.id, m);
100
+ }
101
+ }
102
+ return canonical.map((c) => {
103
+ if (c.role !== "assistant") return c;
104
+ const o = optimisticById.get(c.id);
105
+ if (!o) return c;
106
+ const next: AIHubMessage = { ...c };
107
+ if (!next.reasoning && o.reasoning) {
108
+ next.reasoning = o.reasoning;
109
+ }
110
+ if ((!next.tool_calls || next.tool_calls.length === 0) && o.tool_calls?.length) {
111
+ next.tool_calls = o.tool_calls;
112
+ }
113
+ // Usage rides through too — if the canonical refetch happens
114
+ // before the server stamped `message_usage` (small race window),
115
+ // the live `usage` event we already handled keeps the badge
116
+ // rendered until the next refresh confirms it.
117
+ if (!next.usage && o.usage) {
118
+ next.usage = o.usage;
119
+ }
120
+ return next;
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Public props on `<AIHubProvider>`.
126
+ *
127
+ * `defaultTheme` is overridden by the deployment's `theme_default` once
128
+ * `/auth/me` resolves (server config wins — same rule as the §8.4 toggles).
129
+ */
130
+ export interface AIHubProviderProps {
131
+ /**
132
+ * Base URL the ai-hub-server routes are mounted at. Typically
133
+ * `/api/ai-hub` in a per-customer wrapper.
134
+ */
135
+ baseUrl: string;
136
+ /** Custom `fetch`; defaults to `globalThis.fetch`. */
137
+ fetch?: typeof fetch;
138
+ /** Token storage mode or custom adapter. Defaults to `"localStorage"`. */
139
+ tokenStorage?: TokenStorageMode | TokenStorage;
140
+ /**
141
+ * Pre-built `AIHubClient`. When set, `baseUrl` / `fetch` / `tokenStorage`
142
+ * are ignored. Useful for tests that need to stub the SDK without going
143
+ * through the provider's lifecycle.
144
+ */
145
+ client?: AIHubClient;
146
+ /**
147
+ * Notification hook for top-level errors. Fired for transport and
148
+ * surfaced stream errors; not fired for normal stream `done` /
149
+ * `cancelled`. The provider does not render its own error UI.
150
+ */
151
+ onError?: (err: AIHubError) => void;
152
+ /**
153
+ * Fallback theme used when no `/auth/me` has resolved yet and the cache
154
+ * is empty. Server config (`theme_default`) wins after hydration.
155
+ */
156
+ defaultTheme?: ResolvedTheme;
157
+ /** The tree under the provider. */
158
+ children: ReactNode;
159
+ }
160
+
161
+ /**
162
+ * The provider component. Single instance per ai-hub mount — multiple
163
+ * mounts on the same page each manage their own auth state (this is the
164
+ * shape `defineWebComponent` ships).
165
+ *
166
+ * @param props - the provider props.
167
+ */
168
+ export function AIHubProvider(props: AIHubProviderProps): ReactNode {
169
+ const {
170
+ baseUrl,
171
+ children,
172
+ client: clientProp,
173
+ defaultTheme = "light",
174
+ fetch: fetchProp,
175
+ onError,
176
+ tokenStorage = "localStorage",
177
+ } = props;
178
+
179
+ // ── client ───────────────────────────────────────────────────────────
180
+ const unauthorizedRef = useRef<(() => void) | null>(null);
181
+ const client = useMemo(() => {
182
+ if (clientProp) return clientProp;
183
+ return new AIHubClient({
184
+ baseUrl,
185
+ ...(fetchProp ? { fetch: fetchProp } : {}),
186
+ tokenStorage,
187
+ onUnauthorized: () => unauthorizedRef.current?.(),
188
+ });
189
+ }, [baseUrl, clientProp, fetchProp, tokenStorage]);
190
+
191
+ // ── auth state ──────────────────────────────────────────────────────
192
+ const [user, setUser] = useState<AIHubUser | null>(null);
193
+ const [deployment, setDeployment] = useState<AIHubDeployment | null>(null);
194
+ // Server feature toggles fetched from public GET /feature-toggles on mount
195
+ // (§8.4). `null` while the request is in flight; thereafter `{}` (empty
196
+ // blob — no overrides) or the typed blob. The provider does not block
197
+ // rendering on this — the §8.4 precedence ("server toggle > React prop >
198
+ // built-in default") falls through to the prop default until the fetch
199
+ // resolves, which keeps the FOUC story honest.
200
+ const [featureToggles, setFeatureToggles] =
201
+ useState<AIHubFeatureToggles | null>(null);
202
+ const [mustChangePassword, setMustChangePassword] = useState(false);
203
+ const [hydrating, setHydrating] = useState(true);
204
+
205
+ // ── effort (§7.4) + theme (§7.6) ────────────────────────────────────
206
+ const [effort, setEffort] = useState<AIHubEffort>("medium");
207
+ const [theme, setThemeState] = useState<ResolvedTheme>(() => {
208
+ return readCachedTheme() ?? defaultTheme;
209
+ });
210
+
211
+ // ── chat state (Step 9) ─────────────────────────────────────────────
212
+ const [agents, setAgents] = useState<AgentSummary[]>([]);
213
+ const [agentsLoading, setAgentsLoading] = useState(false);
214
+ const [activeAgentId, setActiveAgentIdState] = useState<string | null>(null);
215
+ const [sessions, setSessions] = useState<Session[]>([]);
216
+ const [sessionsLoading, setSessionsLoading] = useState(false);
217
+ const [includeArchivedSessions, setIncludeArchivedSessions] = useState(false);
218
+ const [activeSessionId, setActiveSessionIdState] = useState<string | null>(
219
+ null,
220
+ );
221
+ const [messages, setMessages] = useState<AIHubMessage[]>([]);
222
+ const [messagesLoading, setMessagesLoading] = useState(false);
223
+ const [streamState, setStreamState] = useState<AIHubState>(() =>
224
+ initialAIHubState(),
225
+ );
226
+ const [streamingAssistantText, setStreamingAssistantText] = useState<
227
+ string | null
228
+ >(null);
229
+ // Reasoning text accumulating during the current turn, when the user
230
+ // sent a non-`none` effort and the agent emitted `reasoning_delta`
231
+ // events. Cleared on `stream_end` (the canonicalized assistant
232
+ // message will carry the final reasoning).
233
+ const [streamingReasoning, setStreamingReasoning] = useState<string | null>(
234
+ null,
235
+ );
236
+ // Tool calls accumulating during the current turn. Map keyed by
237
+ // `tool_call_id`; one entry per call. Cleared on `stream_end`.
238
+ const [streamingToolCalls, setStreamingToolCalls] = useState<
239
+ ReadonlyArray<AIHubToolCallRecord>
240
+ >([]);
241
+ const [lastTurnError, setLastTurnError] = useState<string | null>(null);
242
+ // True from `stop()` invocation until the matching `stream_end` lands.
243
+ // The state machine's `state.status` stays `running` for the brief
244
+ // server-side cancel window — `isStoppingTurn` is the client-only flag
245
+ // that drives the "Stopping…" button label and keeps the input
246
+ // disabled until the genuine terminal event arrives.
247
+ const [isStoppingTurn, setIsStoppingTurn] = useState(false);
248
+ // Text to push into `<MessageInput>` on the next render. Populated by
249
+ // the post-stream-end branch when a cancel rolled back the user's
250
+ // message (single-step cancel: Letta deletes the entire turn from
251
+ // history — the user's typed text would be lost without this).
252
+ const [pendingDraft, setPendingDraft] = useState<string | null>(null);
253
+
254
+ // The turn currently being streamed: AbortController + cached turn id
255
+ // (read off `stream_start`) so `stop()` can fire the explicit cancel
256
+ // endpoint *and* abort the SSE iterator at the same time. We carry both
257
+ // in a ref so updates don't trigger re-renders mid-stream.
258
+ const streamRef = useRef<{
259
+ controller: AbortController;
260
+ turnId: string | null;
261
+ } | null>(null);
262
+
263
+ // Mirror of `activeSessionId` for use inside long-running async
264
+ // callbacks (the streamChat loop closes over `sessionId` from the
265
+ // moment `sendMessage` was called; we use this ref to detect whether
266
+ // the user has switched sessions in the meantime before applying the
267
+ // post-stream canonicalize).
268
+ const activeSessionIdRef = useRef<string | null>(null);
269
+
270
+ // Mirror of `messages` used by `sendMessage` to snapshot the message
271
+ // count BEFORE the optimistic user-bubble append. After stream_end
272
+ // the post-stream branch compares the canonical message count to this
273
+ // baseline: if `canonical.length <= baseline` the cancelled turn was
274
+ // rolled back entirely (single-step cancel — Letta deletes the user
275
+ // message too), and the user's typed text is pushed into `pendingDraft`
276
+ // so `<MessageInput>` can prefill it. State refs can lag a render
277
+ // behind the value but `sendMessage` reads it synchronously at the top.
278
+ const messagesRef = useRef<AIHubMessage[]>([]);
279
+ messagesRef.current = messages;
280
+
281
+ /**
282
+ * Resets every chat-state slot. Called on logout and on
283
+ * unauthenticated → authenticated transitions so a previous user's
284
+ * artifacts don't leak into the next session.
285
+ */
286
+ const resetChatState = useCallback((): void => {
287
+ streamRef.current?.controller.abort();
288
+ streamRef.current = null;
289
+ setAgents([]);
290
+ setActiveAgentIdState(null);
291
+ setSessions([]);
292
+ setIncludeArchivedSessions(false);
293
+ setActiveSessionIdState(null);
294
+ setMessages([]);
295
+ setStreamState(initialAIHubState());
296
+ setStreamingAssistantText(null);
297
+ setLastTurnError(null);
298
+ }, []);
299
+
300
+ unauthorizedRef.current = () => {
301
+ setUser(null);
302
+ setDeployment(null);
303
+ setMustChangePassword(false);
304
+ resetChatState();
305
+ };
306
+
307
+ // Keep the ref in sync so the post-stream canonicalize sees the
308
+ // current value (it closes over a stale `activeSessionId` otherwise).
309
+ activeSessionIdRef.current = activeSessionId;
310
+
311
+ // ── hydrate on mount ────────────────────────────────────────────────
312
+ useEffect(() => {
313
+ let cancelled = false;
314
+ const token = client.getToken();
315
+ if (!token) {
316
+ setHydrating(false);
317
+ return;
318
+ }
319
+ void (async () => {
320
+ try {
321
+ const context: AIHubAuthContext = await client.me();
322
+ if (cancelled) return;
323
+ setUser(context.user);
324
+ setDeployment(context.deployment);
325
+ setMustChangePassword(false);
326
+ } catch (err) {
327
+ if (cancelled) return;
328
+ if (err instanceof AIHubRequestError && err.code !== "unauthorized") {
329
+ onError?.({
330
+ code: err.code,
331
+ message: err.message,
332
+ ...(err.status !== undefined ? { status: err.status } : {}),
333
+ ...(err.request_id !== undefined ? { request_id: err.request_id } : {}),
334
+ });
335
+ }
336
+ } finally {
337
+ if (!cancelled) setHydrating(false);
338
+ }
339
+ })();
340
+ return () => {
341
+ cancelled = true;
342
+ };
343
+ }, [client, onError]);
344
+
345
+ // ── load deployment-wide feature toggles on mount ───────────────────
346
+ // The endpoint is public (no auth) so we fetch as soon as the provider
347
+ // mounts — even before login lands — so the chat shell can apply the
348
+ // §8.4 precedence rules on first paint. Errors are logged via
349
+ // `onError` but never block rendering (we fall through to React-prop
350
+ // defaults).
351
+ useEffect(() => {
352
+ let cancelled = false;
353
+ void (async () => {
354
+ try {
355
+ const toggles = await client.getFeatureToggles();
356
+ if (cancelled) return;
357
+ setFeatureToggles(toggles);
358
+ } catch (err) {
359
+ if (cancelled) return;
360
+ if (err instanceof AIHubRequestError) {
361
+ onError?.({
362
+ code: err.code,
363
+ message: err.message,
364
+ ...(err.status !== undefined ? { status: err.status } : {}),
365
+ });
366
+ }
367
+ // Fall through with an empty blob so every prop default applies.
368
+ setFeatureToggles({});
369
+ }
370
+ })();
371
+ return () => {
372
+ cancelled = true;
373
+ };
374
+ }, [client, onError]);
375
+
376
+ // ── reconcile resolved theme whenever inputs change ────────────────
377
+ useEffect(() => {
378
+ if (!deployment) return;
379
+ const resolved = resolveTheme(user, deployment, defaultTheme);
380
+ setThemeState(resolved);
381
+ writeCachedTheme(resolved);
382
+ }, [user, deployment, defaultTheme]);
383
+
384
+ // ── load /agents whenever an authenticated user becomes available ───
385
+ const refreshAgents = useCallback(async (): Promise<void> => {
386
+ setAgentsLoading(true);
387
+ try {
388
+ const list = await client.listAgents();
389
+ setAgents(list);
390
+ } catch (err) {
391
+ if (err instanceof AIHubRequestError) {
392
+ onError?.({
393
+ code: err.code,
394
+ message: err.message,
395
+ ...(err.status !== undefined ? { status: err.status } : {}),
396
+ });
397
+ }
398
+ setAgents([]);
399
+ } finally {
400
+ setAgentsLoading(false);
401
+ }
402
+ }, [client, onError]);
403
+
404
+ useEffect(() => {
405
+ if (!user || mustChangePassword) return;
406
+ void refreshAgents();
407
+ }, [user, mustChangePassword, refreshAgents]);
408
+
409
+ // ── load /sessions whenever an agent is opened ──────────────────────
410
+ const refreshSessions = useCallback(async (): Promise<void> => {
411
+ if (!activeAgentId) {
412
+ setSessions([]);
413
+ return;
414
+ }
415
+ setSessionsLoading(true);
416
+ try {
417
+ const list = await client.listSessions({
418
+ agentId: activeAgentId,
419
+ includeArchived: includeArchivedSessions,
420
+ });
421
+ setSessions(list);
422
+ } catch (err) {
423
+ if (err instanceof AIHubRequestError) {
424
+ onError?.({
425
+ code: err.code,
426
+ message: err.message,
427
+ ...(err.status !== undefined ? { status: err.status } : {}),
428
+ });
429
+ }
430
+ setSessions([]);
431
+ } finally {
432
+ setSessionsLoading(false);
433
+ }
434
+ }, [activeAgentId, client, includeArchivedSessions, onError]);
435
+
436
+ useEffect(() => {
437
+ if (!activeAgentId) {
438
+ setSessions([]);
439
+ return;
440
+ }
441
+ void refreshSessions();
442
+ }, [activeAgentId, includeArchivedSessions, refreshSessions]);
443
+
444
+ // ── load /sessions/{id}/messages whenever a session is opened ──────
445
+ //
446
+ // Note: `setActiveSession` / `createSession` already clear `messages`
447
+ // and set `messagesLoading=true` synchronously in the same batch as
448
+ // the id flip, so the previous session's bubbles never flash during
449
+ // this fetch. We don't repeat those resets here — only the fetch +
450
+ // the loading-flag flip on completion are this effect's job.
451
+ useEffect(() => {
452
+ if (!activeSessionId) {
453
+ setMessages([]);
454
+ setMessagesLoading(false);
455
+ return;
456
+ }
457
+ let cancelled = false;
458
+ setMessagesLoading(true);
459
+ void (async () => {
460
+ try {
461
+ const list = await client.getSessionMessages(activeSessionId);
462
+ if (cancelled) return;
463
+ setMessages(list);
464
+ } catch (err) {
465
+ if (cancelled) return;
466
+ if (err instanceof AIHubRequestError) {
467
+ onError?.({
468
+ code: err.code,
469
+ message: err.message,
470
+ ...(err.status !== undefined ? { status: err.status } : {}),
471
+ });
472
+ }
473
+ setMessages([]);
474
+ } finally {
475
+ if (!cancelled) setMessagesLoading(false);
476
+ }
477
+ })();
478
+ return () => {
479
+ cancelled = true;
480
+ };
481
+ }, [activeSessionId, client, onError]);
482
+
483
+ // ── action callbacks (auth + theme) ────────────────────────────────
484
+ const login: AIHubContextValue["login"] = async (username, password) => {
485
+ const result = await client.login(username, password);
486
+ setMustChangePassword(result.mustChangePassword);
487
+ setUser(result.user);
488
+ setDeployment(result.deployment ?? null);
489
+ return result;
490
+ };
491
+
492
+ const logout: AIHubContextValue["logout"] = async () => {
493
+ try {
494
+ await client.logout();
495
+ } finally {
496
+ setUser(null);
497
+ setDeployment(null);
498
+ setMustChangePassword(false);
499
+ resetChatState();
500
+ }
501
+ };
502
+
503
+ const changePassword: AIHubContextValue["changePassword"] = async (
504
+ currentPassword,
505
+ newPassword,
506
+ ) => {
507
+ await client.changePassword(currentPassword, newPassword);
508
+ const context = await client.me();
509
+ setUser(context.user);
510
+ setDeployment(context.deployment);
511
+ setMustChangePassword(false);
512
+ };
513
+
514
+ const setTheme: AIHubContextValue["setTheme"] = async (next) => {
515
+ const previous = theme;
516
+ setThemeState(next);
517
+ writeCachedTheme(next);
518
+ try {
519
+ const context = await client.updatePreferences({ theme: next });
520
+ setUser(context.user);
521
+ setDeployment(context.deployment);
522
+ } catch (err) {
523
+ setThemeState(previous);
524
+ writeCachedTheme(previous);
525
+ throw err;
526
+ }
527
+ };
528
+
529
+ // ── chat actions ───────────────────────────────────────────────────
530
+ const setActiveAgent: AIHubContextValue["setActiveAgent"] = (id) => {
531
+ streamRef.current?.controller.abort();
532
+ streamRef.current = null;
533
+ setActiveAgentIdState(id);
534
+ setActiveSessionIdState(null);
535
+ setMessages([]);
536
+ setStreamingAssistantText(null);
537
+ setLastTurnError(null);
538
+ setIsStoppingTurn(false);
539
+ setPendingDraft(null);
540
+ setStreamState(initialAIHubState());
541
+ };
542
+
543
+ const setActiveSession: AIHubContextValue["setActiveSession"] = (id) => {
544
+ if (id === activeSessionId) return;
545
+ streamRef.current?.controller.abort();
546
+ streamRef.current = null;
547
+ setActiveSessionIdState(id);
548
+ // Clear messages synchronously in the same setState batch as the id
549
+ // change — otherwise the previous session's bubbles flash for the
550
+ // duration of the next session's `/sessions/:id/messages` fetch.
551
+ setMessages([]);
552
+ setMessagesLoading(id !== null);
553
+ setStreamingAssistantText(null);
554
+ setLastTurnError(null);
555
+ setIsStoppingTurn(false);
556
+ setPendingDraft(null);
557
+ setStreamState(initialAIHubState());
558
+ };
559
+
560
+ const createSession: AIHubContextValue["createSession"] = async (title) => {
561
+ if (!activeAgentId) {
562
+ throw new AIHubRequestError(
563
+ "validation",
564
+ "Open an agent before creating a session.",
565
+ );
566
+ }
567
+ const session = await client.createSession({
568
+ agentId: activeAgentId,
569
+ title,
570
+ });
571
+ // Prepend so the new row appears at the top of the list (server orders
572
+ // by updated_at DESC).
573
+ setSessions((prev) => [session, ...prev.filter((s) => s.id !== session.id)]);
574
+ // Same synchronous-clear story as `setActiveSession` — otherwise the
575
+ // previous session's bubbles flash during the brand-new session's
576
+ // (empty) history fetch.
577
+ streamRef.current?.controller.abort();
578
+ streamRef.current = null;
579
+ setActiveSessionIdState(session.id);
580
+ setMessages([]);
581
+ setMessagesLoading(true);
582
+ setStreamingAssistantText(null);
583
+ setLastTurnError(null);
584
+ setStreamState(initialAIHubState());
585
+ // Refresh the agents catalog so the picker's per-caller session_count
586
+ // reflects the new row on the next visit. Best-effort — failure isn't
587
+ // fatal.
588
+ void refreshAgents();
589
+ return session;
590
+ };
591
+
592
+ const patchSession: AIHubContextValue["patchSession"] = async (id, patch) => {
593
+ const updated = await client.patchSession(id, patch);
594
+ setSessions((prev) => {
595
+ const next = prev.map((s) => (s.id === id ? updated : s));
596
+ // If the row was archived and the filter is off, drop it from view.
597
+ if (
598
+ patch.isArchived === true &&
599
+ !includeArchivedSessions
600
+ ) {
601
+ return next.filter((s) => !s.is_archived);
602
+ }
603
+ return next;
604
+ });
605
+ if (patch.isArchived === true && id === activeSessionId) {
606
+ setActiveSessionIdState(null);
607
+ }
608
+ return updated;
609
+ };
610
+
611
+ const sendMessage: AIHubContextValue["sendMessage"] = async (text) => {
612
+ const sessionId = activeSessionId;
613
+ if (!sessionId) {
614
+ throw new AIHubRequestError(
615
+ "validation",
616
+ "Open a session before sending a message.",
617
+ );
618
+ }
619
+ if (typeof text !== "string" || text.length === 0) {
620
+ throw new AIHubRequestError("validation", "Cannot send an empty message.");
621
+ }
622
+ // Optimistically append the user's message so the bubble shows up
623
+ // immediately. The server doesn't echo user messages on the stream —
624
+ // they're already in the conversation by the time we ask Letta to
625
+ // produce a reply. Stream-end refetches `getSessionMessages` and
626
+ // replaces the array with Letta's canonical history, so this
627
+ // pending-* id is short-lived; we dedupe defensively anyway.
628
+ const optimisticUser: AIHubMessage = {
629
+ id: `pending-${Date.now()}`,
630
+ role: "user",
631
+ content: text,
632
+ created_at: new Date().toISOString(),
633
+ };
634
+ setMessages((prev) => {
635
+ if (prev.some((m) => m.id === optimisticUser.id)) return prev;
636
+ return [...prev, optimisticUser];
637
+ });
638
+ setStreamingAssistantText("");
639
+ setLastTurnError(null);
640
+
641
+ const controller = new AbortController();
642
+ streamRef.current = { controller, turnId: null };
643
+
644
+ let accumulated = "";
645
+ // Streaming reasoning + tool-call accumulators — kept locally
646
+ // (closure-scoped) so we can flush them into the canonicalized
647
+ // message at end-of-stream OR clear them on cancel/error. Both
648
+ // mirror to React state so the live bubble renders while the turn
649
+ // runs.
650
+ let accumulatedReasoning = "";
651
+ const toolCallsByCallId = new Map<
652
+ string,
653
+ {
654
+ tool_name: string;
655
+ input: string;
656
+ status: "success" | "error";
657
+ output: string | null;
658
+ error: string | null;
659
+ }
660
+ >();
661
+ function snapshotToolCalls(): ReadonlyArray<AIHubToolCallRecord> {
662
+ const out: AIHubToolCallRecord[] = [];
663
+ for (const [callId, info] of toolCallsByCallId.entries()) {
664
+ out.push({
665
+ tool_call_id: callId,
666
+ tool_name: info.tool_name,
667
+ input: info.input,
668
+ status: info.status,
669
+ output: info.output,
670
+ error: info.error,
671
+ });
672
+ }
673
+ return out;
674
+ }
675
+ setStreamingReasoning(null);
676
+ setStreamingToolCalls([]);
677
+ // Track outcome signals collected during the stream. Used after the
678
+ // loop to decide whether to:
679
+ // - canonicalize against Letta's history (replace optimistic
680
+ // state with the real one)
681
+ // - leave the optimistic state in place + surface an error
682
+ // - leave the optimistic state in place + stay silent (user
683
+ // cancelled — they didn't ask for an error)
684
+ let sawAssistantOutput = false;
685
+ let sawError = false;
686
+ let endReason: "done" | "cancelled" | "error" | null = null;
687
+ try {
688
+ const iter = client.streamChat(
689
+ { sessionId, userMessage: text, effort },
690
+ { signal: controller.signal },
691
+ );
692
+ for await (const event of iter) {
693
+ // Drive the §10 reducer (turn state).
694
+ setStreamState((prev) => reduceAIHubState(prev, event));
695
+ if (event.event === "stream_start") {
696
+ if (streamRef.current) {
697
+ streamRef.current.turnId = event.data.turn_id;
698
+ }
699
+ } else if (event.event === "assistant_delta") {
700
+ accumulated += event.data.delta;
701
+ setStreamingAssistantText(accumulated);
702
+ sawAssistantOutput = true;
703
+ } else if (event.event === "reasoning_delta") {
704
+ accumulatedReasoning += event.data.delta;
705
+ setStreamingReasoning(accumulatedReasoning);
706
+ } else if (event.event === "tool_call_started") {
707
+ const callId = event.data.tool_call_id;
708
+ if (!toolCallsByCallId.has(callId)) {
709
+ toolCallsByCallId.set(callId, {
710
+ tool_name: event.data.tool_name,
711
+ input: "",
712
+ status: "success",
713
+ output: null,
714
+ error: null,
715
+ });
716
+ setStreamingToolCalls(snapshotToolCalls());
717
+ }
718
+ } else if (event.event === "tool_call_input_delta") {
719
+ const info = toolCallsByCallId.get(event.data.tool_call_id);
720
+ if (info) {
721
+ info.input += event.data.partial_json;
722
+ setStreamingToolCalls(snapshotToolCalls());
723
+ }
724
+ } else if (event.event === "tool_call_dispatched") {
725
+ // Phase transition is handled by the reducer; we don't need to
726
+ // touch the accumulator (the input is already captured via
727
+ // tool_call_input_delta — `event.data.input` may be empty per
728
+ // the server's translator).
729
+ } else if (event.event === "tool_call_result") {
730
+ const info = toolCallsByCallId.get(event.data.tool_call_id);
731
+ if (info) {
732
+ if ("output" in event.data) {
733
+ info.status = "success";
734
+ info.output =
735
+ typeof event.data.output === "string"
736
+ ? event.data.output
737
+ : JSON.stringify(event.data.output, null, 2);
738
+ info.error = null;
739
+ } else if ("error" in event.data) {
740
+ info.status = "error";
741
+ info.error =
742
+ typeof event.data.error === "string"
743
+ ? event.data.error
744
+ : JSON.stringify(event.data.error, null, 2);
745
+ info.output = null;
746
+ }
747
+ setStreamingToolCalls(snapshotToolCalls());
748
+ }
749
+ } else if (event.event === "assistant_message_complete") {
750
+ // Dedupe — if `assistant_message_complete` fires twice for the
751
+ // same id (server quirk, hot-reload, SSE reconnect), append
752
+ // only the first occurrence. Enrich the final message with
753
+ // the streaming-accumulated reasoning + tool calls so the
754
+ // historical bubble carries the same data the live bubble
755
+ // showed (the server's `/sessions/:id/messages` projector
756
+ // also returns these fields, but we don't wait for a refetch).
757
+ const finalMsg: AIHubMessage = { ...event.data.message };
758
+ if (accumulatedReasoning.length > 0 && !finalMsg.reasoning) {
759
+ finalMsg.reasoning = accumulatedReasoning;
760
+ }
761
+ const calls = snapshotToolCalls();
762
+ if (calls.length > 0 && !finalMsg.tool_calls) {
763
+ finalMsg.tool_calls = [...calls];
764
+ }
765
+ setMessages((prev) =>
766
+ prev.some((m) => m.id === finalMsg.id) ? prev : [...prev, finalMsg],
767
+ );
768
+ accumulated = "";
769
+ accumulatedReasoning = "";
770
+ toolCallsByCallId.clear();
771
+ setStreamingAssistantText(null);
772
+ setStreamingReasoning(null);
773
+ setStreamingToolCalls([]);
774
+ sawAssistantOutput = true;
775
+ } else if (event.event === "usage") {
776
+ // Live usage stamp — the runner emits this right before
777
+ // `stream_end{done}` carrying the cache-hit/write counts for
778
+ // the turn. We attach it to the matching assistant bubble so
779
+ // `<CacheBadge />` appears immediately (without waiting for
780
+ // the post-stream `/messages` refetch). The refetch path
781
+ // also stamps the same `usage` from the persisted
782
+ // `message_usage` row, so refresh + history retrieval show
783
+ // the badge too.
784
+ const targetId = event.data.letta_message_id;
785
+ if (typeof targetId === "string") {
786
+ const usage = {
787
+ input: event.data.input ?? null,
788
+ output: event.data.output ?? null,
789
+ cache_read: event.data.cache_read ?? null,
790
+ cache_write: event.data.cache_write ?? null,
791
+ reasoning: event.data.reasoning ?? null,
792
+ context: event.data.context ?? null,
793
+ total: event.data.total ?? null,
794
+ };
795
+ setMessages((prev) =>
796
+ prev.map((m) => (m.id === targetId ? { ...m, usage } : m)),
797
+ );
798
+ }
799
+ } else if (event.event === "error") {
800
+ setLastTurnError(event.data.message);
801
+ sawError = true;
802
+ } else if (event.event === "stream_end") {
803
+ // Terminal — record the reason and clear the streaming
804
+ // accumulator. The final assistant message arrived via
805
+ // `assistant_message_complete` in a healthy stream; in
806
+ // `cancelled` / `error` outcomes the accumulator may carry
807
+ // partial text the UI still wants to show, so we flush it
808
+ // into a synthetic message before clearing.
809
+ endReason = event.data.reason;
810
+ if (accumulated.length > 0) {
811
+ const partialId = `partial-${streamRef.current?.turnId ?? Date.now()}`;
812
+ const partialMsg: AIHubMessage = {
813
+ id: partialId,
814
+ role: "assistant",
815
+ content: accumulated,
816
+ created_at: new Date().toISOString(),
817
+ };
818
+ if (accumulatedReasoning.length > 0) {
819
+ partialMsg.reasoning = accumulatedReasoning;
820
+ }
821
+ const calls = snapshotToolCalls();
822
+ if (calls.length > 0) {
823
+ partialMsg.tool_calls = [...calls];
824
+ }
825
+ setMessages((prev) => [...prev, partialMsg]);
826
+ accumulated = "";
827
+ sawAssistantOutput = true;
828
+ }
829
+ accumulatedReasoning = "";
830
+ toolCallsByCallId.clear();
831
+ setStreamingAssistantText(null);
832
+ setStreamingReasoning(null);
833
+ setStreamingToolCalls([]);
834
+ }
835
+ }
836
+ } catch (err) {
837
+ // Aborts (from `stop()` or a session switch) are not errors. We
838
+ // record the outcome explicitly so the post-stream logic below
839
+ // can canonicalize Letta's history (the server may have persisted
840
+ // a partial response before the cancel landed). Anything else is
841
+ // a genuine transport failure.
842
+ if (controller.signal.aborted) {
843
+ endReason = "cancelled";
844
+ setStreamingAssistantText(null);
845
+ setStreamingReasoning(null);
846
+ setStreamingToolCalls([]);
847
+ accumulatedReasoning = "";
848
+ toolCallsByCallId.clear();
849
+ // Transition state out of `running` so the input re-enables
850
+ // immediately. The reducer normally drives this off the server's
851
+ // `(status, phase)` snapshot on `stream_end`, but the abort
852
+ // severed the connection before that event arrived.
853
+ setStreamState((s) => ({ ...s, status: "cancelled", phase: null }));
854
+ } else {
855
+ const msg =
856
+ err instanceof Error ? err.message : "Stream failed.";
857
+ setLastTurnError(msg);
858
+ setStreamingAssistantText(null);
859
+ setStreamingReasoning(null);
860
+ setStreamingToolCalls([]);
861
+ accumulatedReasoning = "";
862
+ toolCallsByCallId.clear();
863
+ if (err instanceof AIHubRequestError) {
864
+ onError?.({
865
+ code: err.code,
866
+ message: err.message,
867
+ ...(err.status !== undefined ? { status: err.status } : {}),
868
+ });
869
+ }
870
+ }
871
+ } finally {
872
+ if (streamRef.current?.controller === controller) {
873
+ streamRef.current = null;
874
+ }
875
+ }
876
+
877
+ // Post-stream housekeeping — three branches, gated on the outcome
878
+ // recorded during the loop / catch:
879
+ //
880
+ // - `cancelled` — user pressed Stop (or switched sessions). Don't
881
+ // show an error; canonicalize so any partial
882
+ // response the agent finished generating before
883
+ // the cancel landed shows up properly.
884
+ // - `error` — server emitted an explicit error event during
885
+ // the turn. `lastTurnError` already carries the
886
+ // specific message; don't canonicalize (the
887
+ // server's history may be empty / partial and
888
+ // overwriting `messages` would erase the
889
+ // optimistic user bubble).
890
+ // - `done` / unknown — happy path OR "stream ended cleanly without
891
+ // producing assistant output" (e.g. an upstream
892
+ // agent-configuration problem returning an empty
893
+ // completion). Canonicalize on real output;
894
+ // otherwise surface a generic user-facing error
895
+ // pointing at the administrator.
896
+ if (endReason === "cancelled") {
897
+ try {
898
+ const canonical = await client.getSessionMessages(sessionId);
899
+ if (activeSessionIdRef.current === sessionId) {
900
+ const merged = mergeOptimisticAffordances(
901
+ canonical,
902
+ messagesRef.current,
903
+ );
904
+ setMessages(merged);
905
+ // Rollback detection: when Letta's authoritative cancel
906
+ // rolled back the in-progress step AND there were no
907
+ // previously-committed steps, the user's message disappears
908
+ // from history entirely (single-step cancel — empirically
909
+ // verified). Test by content (the rolled-back optimistic
910
+ // message had `content === text`; if no canonical user
911
+ // message carries that content, it was rolled back). The
912
+ // earlier `length <= baseline` check was right in the happy
913
+ // case but missed scenarios where the session already had
914
+ // sibling rows (e.g. memory_metadata) whose count made the
915
+ // comparison wobbly.
916
+ const trimmed = text.trim();
917
+ const userMessagePreserved = canonical.some(
918
+ (m) => m.role === "user" && m.content.trim() === trimmed,
919
+ );
920
+ if (!userMessagePreserved) {
921
+ setPendingDraft(text);
922
+ }
923
+ }
924
+ } catch {
925
+ // Canonical refetch failed (network blip while the user was
926
+ // cancelling). Restore the typed text so they can retry; we'd
927
+ // rather over-restore than lose the input.
928
+ setPendingDraft(text);
929
+ }
930
+ setIsStoppingTurn(false);
931
+ } else if (sawError) {
932
+ /* `lastTurnError` already set in the loop — do nothing */
933
+ setIsStoppingTurn(false);
934
+ } else if (!sawAssistantOutput) {
935
+ // The stream finished but produced nothing renderable. Surface a
936
+ // user-facing error — kept generic on purpose (no upstream
937
+ // implementation names; users are pointed at the operator).
938
+ setLastTurnError(
939
+ "The agent didn't produce a response. Contact your administrator if this persists.",
940
+ );
941
+ setIsStoppingTurn(false);
942
+ } else if (!controller.signal.aborted) {
943
+ try {
944
+ const canonical = await client.getSessionMessages(sessionId);
945
+ if (activeSessionIdRef.current === sessionId) {
946
+ // Merge optimistic reasoning + tool_calls into the canonical
947
+ // list: the streaming accumulators captured rich data the
948
+ // server's projector might not yet (Letta's cross-row
949
+ // aggregation has a small window where the
950
+ // tool_return_message hasn't been committed yet, or the
951
+ // operator is running an older server build without the
952
+ // §10.x projector). Preserves the affordances either way.
953
+ const merged = mergeOptimisticAffordances(
954
+ canonical,
955
+ messagesRef.current,
956
+ );
957
+ setMessages(merged);
958
+ }
959
+ } catch {
960
+ /* keep optimistic state; user can refresh */
961
+ }
962
+ setIsStoppingTurn(false);
963
+ } else {
964
+ setIsStoppingTurn(false);
965
+ }
966
+ };
967
+
968
+ const stop: AIHubContextValue["stop"] = async () => {
969
+ const active = streamRef.current;
970
+ if (!active) return;
971
+ const { turnId } = active;
972
+ // The right design here is the OPPOSITE of "abort the iterator":
973
+ // we want the server's authoritative cancel (Letta's
974
+ // `agents.messages.cancel`, fired by `/chat/:id/cancel`) to wind
975
+ // the run down server-side, then deliver `stream_end{cancelled}`
976
+ // naturally on the SSE stream. The post-stream-end branch in
977
+ // `sendMessage` then canonicalizes against Letta's history — which
978
+ // correctly shows the rolled-back state (in-progress step gone;
979
+ // completed steps preserved).
980
+ //
981
+ // Aborting the iterator here would race the cancel: the connection
982
+ // closes before the server can deliver `stream_end{cancelled}`,
983
+ // and we'd lose the chance to capture the canonical end-state
984
+ // through the established post-stream path.
985
+ //
986
+ // The UX gap (network round-trip to fire the cancel + Letta
987
+ // processing time, typically ~300ms) is covered by
988
+ // `isStoppingTurn` — `<MessageInput>` shows "Stopping…" and keeps
989
+ // the input disabled. We don't need a hard-abort timeout for the
990
+ // common case; even a slow Letta cancel resolves in low seconds.
991
+ // If the cancel POST itself fails the runner's local AbortSignal
992
+ // (fired server-side as fallback) still produces `stream_end`.
993
+ setIsStoppingTurn(true);
994
+ if (turnId) {
995
+ try {
996
+ await client.cancelTurn(turnId);
997
+ } catch {
998
+ // Ignored — the server's `/cancel` handler also fires the
999
+ // local registry abort, so the runner will emit `stream_end`
1000
+ // even if our POST hit a transient failure.
1001
+ }
1002
+ }
1003
+ };
1004
+
1005
+ const themeSwitcherEnabled = deployment?.theme_switcher_enabled ?? true;
1006
+ const role: AIHubRole | null = user?.role ?? null;
1007
+
1008
+ // Refresh the feature toggles on demand — used after the super-admin
1009
+ // settings page saves a toggle change (Step 12), so the calling
1010
+ // super_admin sees the effect without a page reload.
1011
+ const refreshFeatureToggles = useCallback(async (): Promise<void> => {
1012
+ try {
1013
+ const toggles = await client.getFeatureToggles();
1014
+ setFeatureToggles(toggles);
1015
+ } catch (err) {
1016
+ if (err instanceof AIHubRequestError) {
1017
+ onError?.({
1018
+ code: err.code,
1019
+ message: err.message,
1020
+ ...(err.status !== undefined ? { status: err.status } : {}),
1021
+ });
1022
+ }
1023
+ }
1024
+ }, [client, onError]);
1025
+
1026
+ const value: AIHubContextValue = {
1027
+ client,
1028
+ user,
1029
+ role,
1030
+ deployment,
1031
+ mustChangePassword,
1032
+ hydrating,
1033
+ login,
1034
+ logout,
1035
+ changePassword,
1036
+ state: streamState,
1037
+ effort,
1038
+ setEffort,
1039
+ theme,
1040
+ setTheme,
1041
+ themeSwitcherEnabled,
1042
+ agents,
1043
+ agentsLoading,
1044
+ refreshAgents,
1045
+ activeAgentId,
1046
+ setActiveAgent,
1047
+ sessions,
1048
+ sessionsLoading,
1049
+ refreshSessions,
1050
+ includeArchivedSessions,
1051
+ setIncludeArchivedSessions,
1052
+ activeSessionId,
1053
+ setActiveSession,
1054
+ createSession,
1055
+ patchSession,
1056
+ messages,
1057
+ messagesLoading,
1058
+ streamingAssistantText,
1059
+ streamingReasoning,
1060
+ streamingToolCalls,
1061
+ sendMessage,
1062
+ stop,
1063
+ isStoppingTurn,
1064
+ pendingDraft,
1065
+ consumePendingDraft: () => {
1066
+ const draft = pendingDraft;
1067
+ if (draft !== null) setPendingDraft(null);
1068
+ return draft;
1069
+ },
1070
+ lastTurnError,
1071
+ featureToggles,
1072
+ refreshFeatureToggles,
1073
+ };
1074
+
1075
+ return (
1076
+ <aiHubContext.Provider value={value}>
1077
+ <div className="data-club-ai-hub" data-theme={theme}>
1078
+ {children}
1079
+ </div>
1080
+ </aiHubContext.Provider>
1081
+ );
1082
+ }
1083
+ // `PatchSessionOptions` is re-exported elsewhere; keep the import live.
1084
+ export type { PatchSessionOptions };