@vertesia/ui 1.5.0-dev.20260910.052912Z → 1.6.0-dev.20260912.110338Z

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 (389) hide show
  1. package/README.md +125 -0
  2. package/lib/boot/branding.d.ts +58 -0
  3. package/lib/boot/branding.d.ts.map +1 -0
  4. package/lib/boot/branding.js +62 -0
  5. package/lib/boot/branding.js.map +1 -0
  6. package/lib/boot/index.d.ts +27 -0
  7. package/lib/boot/index.d.ts.map +1 -0
  8. package/lib/boot/index.js +325 -0
  9. package/lib/boot/index.js.map +1 -0
  10. package/lib/boot/vite.d.ts +63 -0
  11. package/lib/boot/vite.d.ts.map +1 -0
  12. package/lib/boot/vite.js +108 -0
  13. package/lib/boot/vite.js.map +1 -0
  14. package/lib/core/components/Link.js +1 -1
  15. package/lib/core/components/Link.js.map +1 -1
  16. package/lib/core/components/Spinner.js +1 -1
  17. package/lib/core/components/Spinner.js.map +1 -1
  18. package/lib/core/components/TagsInput.js +7 -7
  19. package/lib/core/components/TagsInput.js.map +1 -1
  20. package/lib/core/components/shadcn/badge.js +1 -1
  21. package/lib/core/components/shadcn/badge.js.map +1 -1
  22. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  23. package/lib/core/components/shadcn/button.js +3 -4
  24. package/lib/core/components/shadcn/button.js.map +1 -1
  25. package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js +2 -2
  26. package/lib/core/components/shadcn/filters/comboBox/DateCombobox.js.map +1 -1
  27. package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js +1 -1
  28. package/lib/core/components/shadcn/filters/comboBox/SelectCombobox.js.map +1 -1
  29. package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js +1 -1
  30. package/lib/core/components/shadcn/filters/comboBox/StringListCombobox.js.map +1 -1
  31. package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js +1 -1
  32. package/lib/core/components/shadcn/filters/comboBox/TextCombobox.js.map +1 -1
  33. package/lib/core/components/shadcn/filters/filter/dateFilter.js +1 -1
  34. package/lib/core/components/shadcn/filters/filter/dateFilter.js.map +1 -1
  35. package/lib/core/components/shadcn/filters/filter-styles.d.ts +1 -1
  36. package/lib/core/components/shadcn/filters/filter-styles.d.ts.map +1 -1
  37. package/lib/core/components/shadcn/filters/filter-styles.js +4 -4
  38. package/lib/core/components/shadcn/filters/filters.js +1 -1
  39. package/lib/core/components/shadcn/filters/filters.js.map +1 -1
  40. package/lib/core/components/shadcn/label.js +1 -1
  41. package/lib/core/components/shadcn/label.js.map +1 -1
  42. package/lib/core/components/shadcn/overflowTabs.js +2 -2
  43. package/lib/core/components/shadcn/overflowTabs.js.map +1 -1
  44. package/lib/core/components/shadcn/tabs.js +1 -1
  45. package/lib/core/components/shadcn/tabs.js.map +1 -1
  46. package/lib/env/index.d.ts +28 -1
  47. package/lib/env/index.d.ts.map +1 -1
  48. package/lib/env/index.js +51 -3
  49. package/lib/env/index.js.map +1 -1
  50. package/lib/features/activity-doc/ActivityDoc.js +1 -1
  51. package/lib/features/activity-doc/ActivityDoc.js.map +1 -1
  52. package/lib/features/agent/PayloadBuilder.d.ts +4 -0
  53. package/lib/features/agent/PayloadBuilder.d.ts.map +1 -1
  54. package/lib/features/agent/PayloadBuilder.js.map +1 -1
  55. package/lib/features/agent/chat/AgentRunFeedback.d.ts +59 -0
  56. package/lib/features/agent/chat/AgentRunFeedback.d.ts.map +1 -0
  57. package/lib/features/agent/chat/AgentRunFeedback.js +286 -0
  58. package/lib/features/agent/chat/AgentRunFeedback.js.map +1 -0
  59. package/lib/features/agent/chat/AskUserWidget.d.ts +1 -0
  60. package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
  61. package/lib/features/agent/chat/AskUserWidget.js +15 -4
  62. package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
  63. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  64. package/lib/features/agent/chat/ModernAgentConversation.js +3 -2
  65. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  66. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
  67. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  68. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +16 -13
  69. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  70. package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  71. package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  72. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
  73. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  74. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +30 -8
  75. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  76. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.d.ts.map +1 -1
  77. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js +2 -0
  78. package/lib/features/agent/chat/ModernAgentOutput/SummaryConversation.js.map +1 -1
  79. package/lib/features/agent/chat/index.d.ts +1 -0
  80. package/lib/features/agent/chat/index.d.ts.map +1 -1
  81. package/lib/features/agent/chat/index.js +1 -0
  82. package/lib/features/agent/chat/index.js.map +1 -1
  83. package/lib/features/facets/AgentRunnerFacetsNav.d.ts +8 -0
  84. package/lib/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -1
  85. package/lib/features/facets/AgentRunnerFacetsNav.js +12 -0
  86. package/lib/features/facets/AgentRunnerFacetsNav.js.map +1 -1
  87. package/lib/features/layout/GenericPageNavHeader.js +1 -1
  88. package/lib/features/layout/GenericPageNavHeader.js.map +1 -1
  89. package/lib/features/magic-pdf/AnnotatedImageSlider.js +1 -1
  90. package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
  91. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  92. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  93. package/lib/features/permissions/UserPermissionsProvider.d.ts +15 -1
  94. package/lib/features/permissions/UserPermissionsProvider.d.ts.map +1 -1
  95. package/lib/features/permissions/UserPermissionsProvider.js +20 -12
  96. package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
  97. package/lib/features/store/objects/DocumentPreviewPanel.js +5 -5
  98. package/lib/features/store/objects/DocumentPreviewPanel.js.map +1 -1
  99. package/lib/features/store/objects/components/DocumentInput.js +1 -1
  100. package/lib/features/store/objects/components/DocumentInput.js.map +1 -1
  101. package/lib/features/store/objects/selection/SelectionActions.js +1 -1
  102. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  103. package/lib/features/store/objects/upload/DocumentUploadModal.js +1 -1
  104. package/lib/features/store/objects/upload/DocumentUploadModal.js.map +1 -1
  105. package/lib/features/store/types/TypeRegistry.d.ts +130 -0
  106. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  107. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +130 -0
  108. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  109. package/lib/features/user/UserInfo.js +1 -1
  110. package/lib/features/user/UserInfo.js.map +1 -1
  111. package/lib/i18n/index.d.ts +5 -4
  112. package/lib/i18n/index.d.ts.map +1 -1
  113. package/lib/i18n/index.js +16 -2
  114. package/lib/i18n/index.js.map +1 -1
  115. package/lib/i18n/locales/ar.json +30 -0
  116. package/lib/i18n/locales/de.json +30 -0
  117. package/lib/i18n/locales/en.json +30 -0
  118. package/lib/i18n/locales/es.json +30 -0
  119. package/lib/i18n/locales/fr.json +30 -0
  120. package/lib/i18n/locales/it.json +30 -0
  121. package/lib/i18n/locales/ja.json +30 -0
  122. package/lib/i18n/locales/ko.json +30 -0
  123. package/lib/i18n/locales/pt.json +30 -0
  124. package/lib/i18n/locales/ru.json +30 -0
  125. package/lib/i18n/locales/tr.json +30 -0
  126. package/lib/i18n/locales/zh-TW.json +30 -0
  127. package/lib/i18n/locales/zh.json +30 -0
  128. package/lib/layout/ErrorPage.js +1 -1
  129. package/lib/layout/ErrorPage.js.map +1 -1
  130. package/lib/layout/Navbar.d.ts.map +1 -1
  131. package/lib/layout/Navbar.js +3 -2
  132. package/lib/layout/Navbar.js.map +1 -1
  133. package/lib/router/RouteComponent.d.ts.map +1 -1
  134. package/lib/router/RouteComponent.js +15 -12
  135. package/lib/router/RouteComponent.js.map +1 -1
  136. package/lib/session/UserSession.d.ts.map +1 -1
  137. package/lib/session/UserSession.js +6 -3
  138. package/lib/session/UserSession.js.map +1 -1
  139. package/lib/session/UserSessionProvider.d.ts +2 -1
  140. package/lib/session/UserSessionProvider.d.ts.map +1 -1
  141. package/lib/session/UserSessionProvider.js +10 -1
  142. package/lib/session/UserSessionProvider.js.map +1 -1
  143. package/lib/session/auth/composable.d.ts.map +1 -1
  144. package/lib/session/auth/composable.js +88 -3
  145. package/lib/session/auth/composable.js.map +1 -1
  146. package/lib/session/auth/domainRouting.d.ts +22 -12
  147. package/lib/session/auth/domainRouting.d.ts.map +1 -1
  148. package/lib/session/auth/domainRouting.js +35 -21
  149. package/lib/session/auth/domainRouting.js.map +1 -1
  150. package/lib/session/auth/useAuthState.d.ts +9 -4
  151. package/lib/session/auth/useAuthState.d.ts.map +1 -1
  152. package/lib/session/auth/useAuthState.js +9 -4
  153. package/lib/session/auth/useAuthState.js.map +1 -1
  154. package/lib/session/index.d.ts.map +1 -1
  155. package/lib/session/index.js +3 -1
  156. package/lib/session/index.js.map +1 -1
  157. package/lib/shell/AuthScreenPreview.d.ts +12 -0
  158. package/lib/shell/AuthScreenPreview.d.ts.map +1 -0
  159. package/lib/shell/AuthScreenPreview.js +140 -0
  160. package/lib/shell/AuthScreenPreview.js.map +1 -0
  161. package/lib/shell/BrandedAuthScreens.d.ts +19 -0
  162. package/lib/shell/BrandedAuthScreens.d.ts.map +1 -0
  163. package/lib/shell/BrandedAuthScreens.js +64 -0
  164. package/lib/shell/BrandedAuthScreens.js.map +1 -0
  165. package/lib/shell/SplashScreen.d.ts +13 -2
  166. package/lib/shell/SplashScreen.d.ts.map +1 -1
  167. package/lib/shell/SplashScreen.js +21 -20
  168. package/lib/shell/SplashScreen.js.map +1 -1
  169. package/lib/shell/VertesiaShell.d.ts +15 -4
  170. package/lib/shell/VertesiaShell.d.ts.map +1 -1
  171. package/lib/shell/VertesiaShell.js +8 -3
  172. package/lib/shell/VertesiaShell.js.map +1 -1
  173. package/lib/shell/apps/StandaloneApp.js +5 -1
  174. package/lib/shell/apps/StandaloneApp.js.map +1 -1
  175. package/lib/shell/index.d.ts +6 -0
  176. package/lib/shell/index.d.ts.map +1 -1
  177. package/lib/shell/index.js +5 -0
  178. package/lib/shell/index.js.map +1 -1
  179. package/lib/shell/login/SignInFlow.d.ts +68 -0
  180. package/lib/shell/login/SignInFlow.d.ts.map +1 -0
  181. package/lib/shell/login/SignInFlow.js +108 -0
  182. package/lib/shell/login/SignInFlow.js.map +1 -0
  183. package/lib/shell/login/SignInPageShell.d.ts +13 -0
  184. package/lib/shell/login/SignInPageShell.d.ts.map +1 -0
  185. package/lib/shell/login/SignInPageShell.js +14 -0
  186. package/lib/shell/login/SignInPageShell.js.map +1 -0
  187. package/lib/shell/login/SignInPrimitives.d.ts +3 -3
  188. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  189. package/lib/shell/login/SignInPrimitives.js +13 -11
  190. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  191. package/lib/shell/login/SignInRestrictedEnvStep.d.ts +5 -0
  192. package/lib/shell/login/SignInRestrictedEnvStep.d.ts.map +1 -1
  193. package/lib/shell/login/SignInRestrictedEnvStep.js +8 -4
  194. package/lib/shell/login/SignInRestrictedEnvStep.js.map +1 -1
  195. package/lib/shell/login/SigninScreen.d.ts +38 -3
  196. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  197. package/lib/shell/login/SigninScreen.js +33 -86
  198. package/lib/shell/login/SigninScreen.js.map +1 -1
  199. package/lib/shell/login/SignupForm.d.ts +3 -1
  200. package/lib/shell/login/SignupForm.d.ts.map +1 -1
  201. package/lib/shell/login/SignupForm.js +7 -3
  202. package/lib/shell/login/SignupForm.js.map +1 -1
  203. package/lib/shell/login/signInUtils.d.ts +11 -0
  204. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  205. package/lib/shell/login/signInUtils.js +29 -5
  206. package/lib/shell/login/signInUtils.js.map +1 -1
  207. package/lib/vertesia-ui-boot.js +237 -0
  208. package/lib/vertesia-ui-boot.js.map +1 -0
  209. package/lib/vertesia-ui-core.js +46 -46
  210. package/lib/vertesia-ui-core.js.map +1 -1
  211. package/lib/vertesia-ui-env.js +1 -1
  212. package/lib/vertesia-ui-env.js.map +1 -1
  213. package/lib/vertesia-ui-features.js +222 -208
  214. package/lib/vertesia-ui-features.js.map +1 -1
  215. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js +5 -0
  216. package/lib/vertesia-ui-i18n.B3NFclwl.chunk.js.map +1 -0
  217. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js +5 -0
  218. package/lib/vertesia-ui-i18n.BWSrNIxc.chunk.js.map +1 -0
  219. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js +5 -0
  220. package/lib/vertesia-ui-i18n.BWvWeB1p.chunk.js.map +1 -0
  221. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js +5 -0
  222. package/lib/vertesia-ui-i18n.BZCXvTC1.chunk.js.map +1 -0
  223. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js +5 -0
  224. package/lib/vertesia-ui-i18n.BbJMvFZW.chunk.js.map +1 -0
  225. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js +5 -0
  226. package/lib/vertesia-ui-i18n.CE5ldwpt.chunk.js.map +1 -0
  227. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js +5 -0
  228. package/lib/vertesia-ui-i18n.CMC0IShY.chunk.js.map +1 -0
  229. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js +5 -0
  230. package/lib/vertesia-ui-i18n.CkcRqBKU.chunk.js.map +1 -0
  231. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js +5 -0
  232. package/lib/vertesia-ui-i18n.D-Mq1sw_.chunk.js.map +1 -0
  233. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js +5 -0
  234. package/lib/vertesia-ui-i18n.DkK68-A-.chunk.js.map +1 -0
  235. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js +5 -0
  236. package/lib/vertesia-ui-i18n.XE_dcCg6.chunk.js.map +1 -0
  237. package/lib/vertesia-ui-i18n.js +2 -2
  238. package/lib/vertesia-ui-i18n.js.map +1 -1
  239. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js +5 -0
  240. package/lib/vertesia-ui-i18n.qgNqMPvK.chunk.js.map +1 -0
  241. package/lib/vertesia-ui-layout.js +13 -13
  242. package/lib/vertesia-ui-layout.js.map +1 -1
  243. package/lib/vertesia-ui-rich-text.js +68 -68
  244. package/lib/vertesia-ui-rich-text.js.map +1 -1
  245. package/lib/vertesia-ui-router.js +1 -1
  246. package/lib/vertesia-ui-router.js.map +1 -1
  247. package/lib/vertesia-ui-session.js +1 -1
  248. package/lib/vertesia-ui-session.js.map +1 -1
  249. package/lib/vertesia-ui-shell.js +58 -33
  250. package/lib/vertesia-ui-shell.js.map +1 -1
  251. package/lib/vertesia-ui-widgets.js +94 -94
  252. package/lib/vertesia-ui-widgets.js.map +1 -1
  253. package/lib/widgets/json-view/JSONCode.js +1 -1
  254. package/lib/widgets/json-view/JSONCode.js.map +1 -1
  255. package/lib/widgets/upload/DropZone.d.ts.map +1 -1
  256. package/lib/widgets/upload/DropZone.js +1 -1
  257. package/lib/widgets/upload/DropZone.js.map +1 -1
  258. package/llms.txt +6 -2
  259. package/package.json +44 -35
  260. package/src/__tests__/a11y.test.tsx +25 -0
  261. package/src/__tests__/theme.test.ts +91 -0
  262. package/src/boot/branding.test.ts +72 -0
  263. package/src/boot/branding.ts +108 -0
  264. package/src/boot/index.test.ts +216 -0
  265. package/src/boot/index.ts +356 -0
  266. package/src/boot/vite.test.ts +93 -0
  267. package/src/boot/vite.ts +118 -0
  268. package/src/core/components/Link.tsx +1 -1
  269. package/src/core/components/Spinner.tsx +1 -1
  270. package/src/core/components/TagsInput.tsx +7 -7
  271. package/src/core/components/shadcn/badge.tsx +1 -1
  272. package/src/core/components/shadcn/button.tsx +3 -5
  273. package/src/core/components/shadcn/filters/comboBox/DateCombobox.tsx +2 -2
  274. package/src/core/components/shadcn/filters/comboBox/SelectCombobox.tsx +1 -1
  275. package/src/core/components/shadcn/filters/comboBox/StringListCombobox.tsx +1 -1
  276. package/src/core/components/shadcn/filters/comboBox/TextCombobox.tsx +1 -1
  277. package/src/core/components/shadcn/filters/filter/dateFilter.tsx +1 -1
  278. package/src/core/components/shadcn/filters/filter-styles.ts +4 -4
  279. package/src/core/components/shadcn/filters/filters.tsx +1 -1
  280. package/src/core/components/shadcn/label.tsx +1 -1
  281. package/src/core/components/shadcn/overflowTabs.tsx +2 -2
  282. package/src/core/components/shadcn/tabs.tsx +1 -1
  283. package/src/css/utilities.css +7 -0
  284. package/src/env/index.test.ts +100 -0
  285. package/src/env/index.ts +66 -3
  286. package/src/features/activity-doc/ActivityDoc.tsx +1 -1
  287. package/src/features/agent/PayloadBuilder.tsx +4 -0
  288. package/src/features/agent/chat/AgentRunFeedback.test.tsx +330 -0
  289. package/src/features/agent/chat/AgentRunFeedback.tsx +452 -0
  290. package/src/features/agent/chat/AskUserWidget.test.tsx +24 -2
  291. package/src/features/agent/chat/AskUserWidget.tsx +19 -4
  292. package/src/features/agent/chat/ModernAgentConversation.tsx +46 -42
  293. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +207 -0
  294. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +50 -31
  295. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +4 -0
  296. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +42 -8
  297. package/src/features/agent/chat/ModernAgentOutput/SummaryConversation.ts +2 -0
  298. package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +14 -0
  299. package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +0 -5
  300. package/src/features/agent/chat/index.ts +6 -0
  301. package/src/features/facets/AgentRunnerFacetsNav.tsx +26 -0
  302. package/src/features/layout/GenericPageNavHeader.test.tsx +36 -0
  303. package/src/features/layout/GenericPageNavHeader.tsx +1 -1
  304. package/src/features/magic-pdf/AnnotatedImageSlider.tsx +1 -1
  305. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  306. package/src/features/permissions/UserPermissionsProvider.test.tsx +50 -1
  307. package/src/features/permissions/UserPermissionsProvider.tsx +82 -44
  308. package/src/features/store/objects/DocumentPreviewPanel.tsx +10 -10
  309. package/src/features/store/objects/components/DocumentInput.tsx +1 -1
  310. package/src/features/store/objects/selection/SelectionActions.tsx +4 -1
  311. package/src/features/store/objects/upload/DocumentUploadModal.tsx +1 -1
  312. package/src/features/user/UserInfo.tsx +1 -1
  313. package/src/i18n/index.tsx +25 -2
  314. package/src/i18n/locales/ar.json +30 -0
  315. package/src/i18n/locales/de.json +30 -0
  316. package/src/i18n/locales/en.json +30 -0
  317. package/src/i18n/locales/es.json +30 -0
  318. package/src/i18n/locales/fr.json +30 -0
  319. package/src/i18n/locales/it.json +30 -0
  320. package/src/i18n/locales/ja.json +30 -0
  321. package/src/i18n/locales/ko.json +30 -0
  322. package/src/i18n/locales/pt.json +30 -0
  323. package/src/i18n/locales/ru.json +30 -0
  324. package/src/i18n/locales/tr.json +30 -0
  325. package/src/i18n/locales/zh-TW.json +30 -0
  326. package/src/i18n/locales/zh.json +30 -0
  327. package/src/layout/ErrorPage.tsx +1 -1
  328. package/src/layout/Navbar.test.tsx +58 -0
  329. package/src/layout/Navbar.tsx +5 -2
  330. package/src/router/RouteComponent.test.tsx +65 -1
  331. package/src/router/RouteComponent.tsx +18 -15
  332. package/src/session/UserSession.test.ts +65 -0
  333. package/src/session/UserSession.ts +6 -4
  334. package/src/session/UserSessionProvider.strict-mode.test.tsx +44 -0
  335. package/src/session/UserSessionProvider.test.ts +62 -1
  336. package/src/session/UserSessionProvider.tsx +9 -1
  337. package/src/session/auth/composable.renewal.test.ts +170 -0
  338. package/src/session/auth/composable.test.ts +23 -1
  339. package/src/session/auth/composable.ts +101 -2
  340. package/src/session/auth/domainRouting.test.ts +35 -65
  341. package/src/session/auth/domainRouting.ts +51 -34
  342. package/src/session/auth/useAuthState.ts +11 -4
  343. package/src/session/index.ts +3 -1
  344. package/src/shell/AuthScreenPreview.test.tsx +44 -0
  345. package/src/shell/AuthScreenPreview.tsx +228 -0
  346. package/src/shell/BrandedAuthScreens.test.tsx +181 -0
  347. package/src/shell/BrandedAuthScreens.tsx +117 -0
  348. package/src/shell/SplashScreen.test.tsx +37 -0
  349. package/src/shell/SplashScreen.tsx +45 -40
  350. package/src/shell/VertesiaShell.tsx +49 -22
  351. package/src/shell/apps/StandaloneApp.tsx +5 -1
  352. package/src/shell/index.tsx +6 -0
  353. package/src/shell/login/SignInFlow.tsx +213 -0
  354. package/src/shell/login/SignInPageShell.tsx +62 -0
  355. package/src/shell/login/SignInPrimitives.tsx +14 -11
  356. package/src/shell/login/SignInRestrictedEnvStep.tsx +19 -7
  357. package/src/shell/login/SigninScreen.test.tsx +104 -0
  358. package/src/shell/login/SigninScreen.tsx +115 -145
  359. package/src/shell/login/SignupForm.tsx +8 -3
  360. package/src/shell/login/signInUtils.ts +30 -5
  361. package/src/widgets/json-view/JSONCode.test.tsx +2 -2
  362. package/src/widgets/json-view/JSONCode.tsx +1 -1
  363. package/src/widgets/markdown/markdown.css +21 -21
  364. package/src/widgets/upload/DropZone.tsx +2 -4
  365. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
  366. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
  367. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
  368. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
  369. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
  370. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
  371. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
  372. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
  373. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
  374. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
  375. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
  376. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
  377. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
  378. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
  379. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
  380. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
  381. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
  382. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
  383. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
  384. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
  385. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
  386. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
  387. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
  388. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
  389. package/src/session/auth/authState.test.ts +0 -35
@@ -0,0 +1,452 @@
1
+ import {
2
+ AGENT_RUN_FEEDBACK_COMMENT_MAX_LENGTH,
3
+ type AgentMessage,
4
+ type AgentRunFeedbackEntry,
5
+ type AgentRunFeedbackPayload,
6
+ type AgentRunFeedbackRating,
7
+ type AgentRunFeedbackReasonCode,
8
+ type AgentRunFeedbackStatus,
9
+ } from '@vertesia/common';
10
+ import {
11
+ Button,
12
+ cn,
13
+ FormItem,
14
+ Modal,
15
+ ModalBody,
16
+ ModalFooter,
17
+ ModalTitle,
18
+ SelectBox,
19
+ Textarea,
20
+ useToast,
21
+ VTooltip,
22
+ } from '@vertesia/ui/core';
23
+ import { useUITranslation } from '@vertesia/ui/i18n';
24
+ import { useUserSession } from '@vertesia/ui/session';
25
+ import { ThumbsDown, ThumbsUp } from 'lucide-react';
26
+ import { createContext, type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react';
27
+
28
+ /**
29
+ * Which rating each reason code belongs to.
30
+ *
31
+ * Written as a `Record` over the published enum on purpose: adding a code to
32
+ * `AgentRunFeedbackReasonCode` breaks this file until someone decides where it belongs, so the
33
+ * picker can never silently stop offering a code the API accepts. `other` is offered on both sides.
34
+ */
35
+ const REASON_CODE_RATINGS: Record<AgentRunFeedbackReasonCode, readonly AgentRunFeedbackRating[]> = {
36
+ accurate: ['up'],
37
+ helpful: ['up'],
38
+ fast: ['up'],
39
+ well_explained: ['up'],
40
+ wrong_result: ['down'],
41
+ incomplete: ['down'],
42
+ misunderstood_request: ['down'],
43
+ too_slow: ['down'],
44
+ tool_failure: ['down'],
45
+ unsafe_action: ['down'],
46
+ other: ['up', 'down'],
47
+ };
48
+
49
+ const REASON_CODES = Object.keys(REASON_CODE_RATINGS) as AgentRunFeedbackReasonCode[];
50
+
51
+ export function agentRunFeedbackReasonCodes(rating: AgentRunFeedbackRating): AgentRunFeedbackReasonCode[] {
52
+ return REASON_CODES.filter((code) => REASON_CODE_RATINGS[code].includes(rating));
53
+ }
54
+
55
+ /**
56
+ * Opaque identifier of one message inside its run, used as the feedback `message_id`. Agent
57
+ * messages carry no id of their own; the workstream plus the timestamp is what the transcript
58
+ * itself keys on.
59
+ */
60
+ export function agentMessageFeedbackId(message: Pick<AgentMessage, 'workstream_id' | 'timestamp'>): string {
61
+ return `${message.workstream_id || 'main'}:${message.timestamp}`;
62
+ }
63
+
64
+ /** Idempotency key for one submission; the server counts a retried id once. */
65
+ function newFeedbackId(): string {
66
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) return crypto.randomUUID();
67
+ return `fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
68
+ }
69
+
70
+ /** `recorded` and `replaced` both mean the rating is in; only `disabled` is a non-outcome. */
71
+ function isAccepted(status: AgentRunFeedbackStatus): boolean {
72
+ return status === 'recorded' || status === 'replaced';
73
+ }
74
+
75
+ /** The subject of one rating: the run as a whole, or one message of it. */
76
+ interface FeedbackScope {
77
+ agentRunId: string;
78
+ messageId?: string;
79
+ messageSeq?: number;
80
+ }
81
+
82
+ function scopeKeyOf({ agentRunId, messageId, messageSeq }: FeedbackScope): string {
83
+ return `${agentRunId}|${messageId ?? ''}|${messageSeq ?? ''}`;
84
+ }
85
+
86
+ function entryScopeKey(agentRunId: string, entry: Pick<AgentRunFeedbackEntry, 'message_id' | 'message_seq'>): string {
87
+ return scopeKeyOf({ agentRunId, messageId: entry.message_id, messageSeq: entry.message_seq });
88
+ }
89
+
90
+ /**
91
+ * The stored rating of the current user for every scope of one run, read once from the run
92
+ * document so a reload shows the thumbs where they were left.
93
+ *
94
+ * `null` while the run has not been read yet, so a control can tell "nothing recorded" from
95
+ * "not known yet".
96
+ */
97
+ interface AgentRunFeedbackHydration {
98
+ agentRunId: string;
99
+ ratingFor(scope: FeedbackScope): AgentRunFeedbackRating | undefined;
100
+ /** A rating this session just recorded, so a remounted control shows it without a refetch. */
101
+ remember(scope: FeedbackScope, rating: AgentRunFeedbackRating): void;
102
+ }
103
+
104
+ const AgentRunFeedbackContext = createContext<AgentRunFeedbackHydration | null>(null);
105
+
106
+ /**
107
+ * The server stores the rater as a principal ref (`user:<id>`), the same string the auth layer
108
+ * builds from the token: `user_id` when the token carries one, else `sub`.
109
+ */
110
+ function currentPrincipalRef(user: { sub?: string; user_id?: string } | undefined): string | undefined {
111
+ const id = user?.user_id ?? user?.sub;
112
+ return id ? `user:${id}` : undefined;
113
+ }
114
+
115
+ /**
116
+ * The current user's active rating per scope. A replaced entry is history; among the rest the
117
+ * latest `rated_at` wins, which is also what the server's one-vote-per-scope rule keeps.
118
+ */
119
+ export function activeRatingsByScope(
120
+ agentRunId: string,
121
+ entries: readonly AgentRunFeedbackEntry[] | undefined,
122
+ principalRef: string | undefined,
123
+ ): Map<string, AgentRunFeedbackRating> {
124
+ const latest = new Map<string, AgentRunFeedbackEntry>();
125
+ if (!principalRef) return new Map();
126
+ for (const entry of entries ?? []) {
127
+ if (entry.user_id !== principalRef || entry.replaced_at) continue;
128
+ const key = entryScopeKey(agentRunId, entry);
129
+ const seen = latest.get(key);
130
+ if (!seen || seen.rated_at < entry.rated_at) latest.set(key, entry);
131
+ }
132
+ return new Map(Array.from(latest, ([key, entry]) => [key, entry.rating]));
133
+ }
134
+
135
+ /**
136
+ * Loads the ratings the current user already gave on `agentRunId` and hands them to every
137
+ * {@link AgentRunFeedback} rendered underneath, so the thumbs survive a reload of the
138
+ * conversation. One read per run, however many messages carry a control.
139
+ *
140
+ * Without a provider the controls still work; they just start blank.
141
+ */
142
+ export function AgentRunFeedbackProvider({ agentRunId, children }: { agentRunId: string; children: ReactNode }) {
143
+ const { client, user } = useUserSession();
144
+ const principalRef = currentPrincipalRef(user);
145
+ const [loaded, setLoaded] = useState<{ agentRunId: string; ratings: Map<string, AgentRunFeedbackRating> }>();
146
+ const [remembered, setRemembered] = useState<Map<string, AgentRunFeedbackRating>>(() => new Map());
147
+ // Read through a ref: the run is re-read when the run or the user changes, not whenever the
148
+ // session hands out a new client object.
149
+ const clientRef = useRef(client);
150
+ clientRef.current = client;
151
+
152
+ useEffect(() => {
153
+ let cancelled = false;
154
+ // The run document carries the retained entries; a failure here only costs the hydration,
155
+ // never the ability to rate.
156
+ clientRef.current.agents
157
+ .retrieve(agentRunId)
158
+ .then((run) => {
159
+ if (cancelled) return;
160
+ setLoaded({ agentRunId, ratings: activeRatingsByScope(agentRunId, run.feedback, principalRef) });
161
+ })
162
+ .catch(() => {
163
+ if (!cancelled) setLoaded({ agentRunId, ratings: new Map() });
164
+ });
165
+ return () => {
166
+ cancelled = true;
167
+ };
168
+ }, [agentRunId, principalRef]);
169
+
170
+ const value = useMemo<AgentRunFeedbackHydration>(
171
+ () => ({
172
+ agentRunId,
173
+ ratingFor: (scope) => {
174
+ const key = scopeKeyOf(scope);
175
+ return remembered.get(key) ?? (loaded?.agentRunId === agentRunId ? loaded.ratings.get(key) : undefined);
176
+ },
177
+ remember: (scope, rating) => {
178
+ setRemembered((previous) => new Map(previous).set(scopeKeyOf(scope), rating));
179
+ },
180
+ }),
181
+ [agentRunId, loaded, remembered],
182
+ );
183
+
184
+ return <AgentRunFeedbackContext.Provider value={value}>{children}</AgentRunFeedbackContext.Provider>;
185
+ }
186
+
187
+ export interface AgentRunFeedbackProps {
188
+ /** The run being rated. */
189
+ agentRunId: string;
190
+ /** Rate one message rather than the run as a whole. */
191
+ messageId?: string;
192
+ /** Position of the rated message in the run. Sent alongside or instead of `messageId`. */
193
+ messageSeq?: number;
194
+ /** `compact` matches the icon-button sizing used by the conversation header's compact variant. */
195
+ variant?: 'full' | 'compact';
196
+ /** `inline` fades the control until the pointer is over the message it belongs to. */
197
+ tone?: 'default' | 'inline';
198
+ className?: string;
199
+ /** Called after every accepted round trip, with what the server said it did with the rating. */
200
+ onRecorded?: (payload: AgentRunFeedbackPayload, status: AgentRunFeedbackStatus) => void;
201
+ }
202
+
203
+ /**
204
+ * Thumbs up / thumbs down on an agent run.
205
+ *
206
+ * The click records the rating on its own, immediately — that is the signal being collected, and
207
+ * making the user fill a form first would cost most of it. Nothing opens on its own: once the
208
+ * rating is in, a quiet "Tell us more" link appears next to the thumbs, and only that opens the
209
+ * detail dialog for the reason code and an optional comment. Submitting it sends a revision of the
210
+ * same rating, which the server supersedes the first one with.
211
+ *
212
+ * Every submission carries a fresh `feedback_id`: it is the server's idempotency key, so a retried
213
+ * request cannot count twice, and a second vote by the same user on the same scope replaces the
214
+ * first (`replaced`) rather than adding to it. Both `recorded` and `replaced` mean the rating is in.
215
+ *
216
+ * The outcome is read from the response's `status`, not from the HTTP code: a deployment that does
217
+ * not collect ratings answers 200 with `disabled` and has counted nothing. Saying "thanks" there
218
+ * would be a lie, so the control says the rating was not recorded and stops offering itself.
219
+ */
220
+ export function AgentRunFeedback({
221
+ agentRunId,
222
+ messageId,
223
+ messageSeq,
224
+ variant = 'full',
225
+ tone = 'default',
226
+ className,
227
+ onRecorded,
228
+ }: AgentRunFeedbackProps) {
229
+ const { t } = useUITranslation();
230
+ const { client } = useUserSession();
231
+ const toast = useToast();
232
+ const hydration = useContext(AgentRunFeedbackContext);
233
+ const [ownRating, setOwnRating] = useState<AgentRunFeedbackRating>();
234
+ // What this session recorded wins over what the run document said at load time; a stored
235
+ // rating from a previous session fills in when nothing was clicked here yet.
236
+ const rating =
237
+ ownRating ??
238
+ (hydration?.agentRunId === agentRunId ? hydration.ratingFor({ agentRunId, messageId, messageSeq }) : undefined);
239
+ const [pendingRating, setPendingRating] = useState<AgentRunFeedbackRating>();
240
+ const [isUnavailable, setIsUnavailable] = useState(false);
241
+ const [isDetailOpen, setIsDetailOpen] = useState(false);
242
+ const [isSubmittingDetail, setIsSubmittingDetail] = useState(false);
243
+ const [reasonCode, setReasonCode] = useState<AgentRunFeedbackReasonCode>();
244
+ const [comment, setComment] = useState('');
245
+
246
+ const scope = messageId !== undefined || messageSeq !== undefined ? 'message' : 'run';
247
+
248
+ /**
249
+ * The subject this control is rating. Every piece of state below belongs to it and to nothing
250
+ * else, so when it changes they all have to go.
251
+ *
252
+ * Without this the component kept its rating, its reason code, its comment and its open dialog
253
+ * across a switch to a different run: the previous conversation's thumb stayed lit on the new
254
+ * one, and submitting a dialog left open across the switch sent the old comment against the new
255
+ * run's id — `send` reads `agentRunId` at call time, so the request went to the new run
256
+ * carrying text written about the old one. A parent that remounts per run hid it; the component
257
+ * is exported from `@vertesia/ui` and cannot assume one does.
258
+ *
259
+ * Reset during render, which is React's documented way to adjust state when a prop changes: it
260
+ * re-renders before committing, so nothing paints with the stale value the way a `useEffect`
261
+ * reset would.
262
+ */
263
+ const scopeKey = `${agentRunId}|${messageId ?? ''}|${messageSeq ?? ''}`;
264
+ const [lastScopeKey, setLastScopeKey] = useState(scopeKey);
265
+ const currentScopeKey = useRef(scopeKey);
266
+ if (lastScopeKey !== scopeKey) {
267
+ setLastScopeKey(scopeKey);
268
+ currentScopeKey.current = scopeKey;
269
+ setOwnRating(undefined);
270
+ setPendingRating(undefined);
271
+ setIsDetailOpen(false);
272
+ setIsSubmittingDetail(false);
273
+ setReasonCode(undefined);
274
+ setComment('');
275
+ // `isUnavailable` is NOT reset: `disabled` is a property of the deployment, not of the run,
276
+ // so a control that has already learned the pipeline is off stays hidden rather than
277
+ // re-offering itself on every switch and collecting clicks nothing records.
278
+ }
279
+
280
+ const send = async (
281
+ value: AgentRunFeedbackRating,
282
+ detail: boolean,
283
+ ): Promise<AgentRunFeedbackStatus | undefined> => {
284
+ const payload: AgentRunFeedbackPayload = {
285
+ feedback_id: newFeedbackId(),
286
+ rating: value,
287
+ ...(detail && reasonCode !== undefined ? { reason_code: reasonCode } : {}),
288
+ ...(detail && comment.trim().length > 0 ? { comment: comment.trim() } : {}),
289
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
290
+ ...(messageSeq !== undefined ? { message_seq: messageSeq } : {}),
291
+ };
292
+ // Captured before the await: a response that lands after the user has moved to another run
293
+ // describes a subject this control is no longer showing, and applying it would light a
294
+ // thumb for a rating given somewhere else.
295
+ const sentScopeKey = scopeKey;
296
+ try {
297
+ const response = await client.agents.recordFeedback(agentRunId, payload);
298
+ if (sentScopeKey !== currentScopeKey.current) return undefined;
299
+ if (isAccepted(response.status)) {
300
+ onRecorded?.(payload, response.status);
301
+ setOwnRating(value);
302
+ hydration?.remember({ agentRunId, messageId, messageSeq }, value);
303
+ return response.status;
304
+ }
305
+ // `disabled` means this deployment collects no ratings; there is nothing to retry, so
306
+ // the control removes itself rather than inviting a second useless click.
307
+ setIsUnavailable(response.status === 'disabled');
308
+ toast({
309
+ status: 'info',
310
+ title: t('agent.feedback.notRecorded'),
311
+ description: t(`agent.feedback.status.${response.status}`),
312
+ duration: 4000,
313
+ });
314
+ return response.status;
315
+ } catch (error: unknown) {
316
+ if (sentScopeKey !== currentScopeKey.current) return undefined;
317
+ toast({
318
+ status: 'error',
319
+ title: t('agent.feedback.failed'),
320
+ description: error instanceof Error ? error.message : String(error),
321
+ duration: 4000,
322
+ });
323
+ return undefined;
324
+ }
325
+ };
326
+
327
+ const handleRate = async (value: AgentRunFeedbackRating) => {
328
+ if (pendingRating) return;
329
+ setPendingRating(value);
330
+ try {
331
+ await send(value, false);
332
+ } finally {
333
+ setPendingRating(undefined);
334
+ }
335
+ };
336
+
337
+ const openDetail = () => {
338
+ if (!rating) return;
339
+ setIsDetailOpen(true);
340
+ };
341
+
342
+ const handleSubmitDetail = async () => {
343
+ if (!rating) return;
344
+ setIsSubmittingDetail(true);
345
+ try {
346
+ const status = await send(rating, true);
347
+ if (status && isAccepted(status)) {
348
+ setIsDetailOpen(false);
349
+ toast({ status: 'success', title: t('agent.feedback.thanks'), duration: 2000 });
350
+ }
351
+ } finally {
352
+ setIsSubmittingDetail(false);
353
+ }
354
+ };
355
+
356
+ if (isUnavailable) return null;
357
+
358
+ const isCompact = variant === 'compact';
359
+ const upLabel = scope === 'message' ? t('agent.feedback.rateMessageUp') : t('agent.feedback.rateRunUp');
360
+ const downLabel = scope === 'message' ? t('agent.feedback.rateMessageDown') : t('agent.feedback.rateRunDown');
361
+
362
+ const thumb = (value: AgentRunFeedbackRating, label: string, Icon: typeof ThumbsUp) => (
363
+ <VTooltip description={label} asChild>
364
+ <Button
365
+ type="button"
366
+ variant="ghost"
367
+ size={isCompact ? 'icon' : 'xs'}
368
+ aria-label={label}
369
+ aria-pressed={rating === value}
370
+ disabled={pendingRating !== undefined}
371
+ onClick={() => void handleRate(value)}
372
+ className={cn(
373
+ 'transition-colors duration-200 rounded-md',
374
+ isCompact && 'size-8 rounded-lg',
375
+ rating === value && (value === 'up' ? 'text-success' : 'text-destructive'),
376
+ )}
377
+ >
378
+ <Icon className="size-4" />
379
+ </Button>
380
+ </VTooltip>
381
+ );
382
+
383
+ return (
384
+ <div
385
+ className={cn(
386
+ 'flex items-center gap-0.5',
387
+ // Inline under an answer the control should not compete with the text: it sits
388
+ // faded until the pointer is over the message, and stays visible once a rating is
389
+ // in so the user can find "Tell us more" again.
390
+ tone === 'inline' &&
391
+ !rating &&
392
+ 'opacity-40 transition-opacity duration-200 group-hover:opacity-100 focus-within:opacity-100',
393
+ className,
394
+ )}
395
+ >
396
+ {thumb('up', upLabel, ThumbsUp)}
397
+ {thumb('down', downLabel, ThumbsDown)}
398
+ {rating && !isDetailOpen && (
399
+ <Button
400
+ type="button"
401
+ variant="ghost"
402
+ size="xs"
403
+ onClick={openDetail}
404
+ className="ms-1 text-xs text-muted hover:text-foreground"
405
+ >
406
+ {t('agent.feedback.tellUsMore')}
407
+ </Button>
408
+ )}
409
+ <Modal isOpen={isDetailOpen} onClose={() => setIsDetailOpen(false)} size="md" disableCloseOnClickOutside>
410
+ <ModalTitle>{t('agent.feedback.detailTitle')}</ModalTitle>
411
+ <ModalBody>
412
+ <div className="flex flex-col gap-4">
413
+ <p className="text-sm text-muted">{t('agent.feedback.detailHelp')}</p>
414
+ <FormItem label={t('agent.feedback.reasonLabel')}>
415
+ <SelectBox<AgentRunFeedbackReasonCode>
416
+ options={rating ? agentRunFeedbackReasonCodes(rating) : []}
417
+ value={reasonCode}
418
+ onChange={setReasonCode}
419
+ optionLabel={(code) => t(`agent.feedback.reason.${code}`)}
420
+ placeholder={t('agent.feedback.reasonPlaceholder')}
421
+ aria-label={t('agent.feedback.reasonLabel')}
422
+ isClearable
423
+ />
424
+ </FormItem>
425
+ <FormItem label={t('agent.feedback.commentLabel')} helpText={t('agent.feedback.commentHelp')}>
426
+ <Textarea
427
+ value={comment}
428
+ onChange={(e) => setComment(e.target.value)}
429
+ maxLength={AGENT_RUN_FEEDBACK_COMMENT_MAX_LENGTH}
430
+ minLines={3}
431
+ maxLines={8}
432
+ placeholder={t('agent.feedback.commentPlaceholder')}
433
+ />
434
+ </FormItem>
435
+ </div>
436
+ </ModalBody>
437
+ <ModalFooter>
438
+ <Button variant="ghost" onClick={() => setIsDetailOpen(false)} disabled={isSubmittingDetail}>
439
+ {t('agent.feedback.skip')}
440
+ </Button>
441
+ <Button
442
+ onClick={() => void handleSubmitDetail()}
443
+ isLoading={isSubmittingDetail}
444
+ disabled={isSubmittingDetail || (reasonCode === undefined && comment.trim().length === 0)}
445
+ >
446
+ {t('agent.feedback.submit')}
447
+ </Button>
448
+ </ModalFooter>
449
+ </Modal>
450
+ </div>
451
+ );
452
+ }
@@ -1,3 +1,5 @@
1
+ import { fireEvent, screen } from '@testing-library/react';
2
+ import { Env } from '@vertesia/ui/env';
1
3
  import type { ReactNode } from 'react';
2
4
  import { describe, expect, it, vi } from 'vitest';
3
5
  import { renderWithProviders } from '../../../__tests__/test-utils.js';
@@ -18,7 +20,7 @@ function getScrollablePrompt(container: HTMLElement): HTMLElement {
18
20
 
19
21
  describe('AskUserWidget', () => {
20
22
  it('does not crash when runtime options data is not an array', () => {
21
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
23
+ const warn = vi.spyOn(Env.logger, 'warn').mockImplementation(() => undefined);
22
24
 
23
25
  expect(() =>
24
26
  renderWithProviders(
@@ -32,7 +34,27 @@ describe('AskUserWidget', () => {
32
34
  ),
33
35
  ).not.toThrow();
34
36
 
35
- expect(warn).toHaveBeenCalledWith('[AskUserWidget] Invalid options received; rendering without options.');
37
+ expect(warn).toHaveBeenCalledWith('AskUserWidget received invalid options; rendering without options', {
38
+ vertesia: { component: 'AskUserWidget', received_type: 'string' },
39
+ });
40
+ warn.mockRestore();
41
+ });
42
+
43
+ it.each([false, true])('rejects malformed entries in compact=%s without breaking text submission', (compact) => {
44
+ const onSubmit = vi.fn();
45
+ const warn = vi.spyOn(Env.logger, 'warn').mockImplementation(() => undefined);
46
+ renderWithProviders(
47
+ <AskUserWidget
48
+ question="Choose"
49
+ compact={compact}
50
+ options={[null] as unknown as AskUserOption[]}
51
+ allowFreeResponse
52
+ onSubmit={onSubmit}
53
+ />,
54
+ );
55
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: 'My answer' } });
56
+ fireEvent.click(screen.getByRole('button', { name: 'Send' }));
57
+ expect(onSubmit).toHaveBeenCalledWith('My answer');
36
58
  warn.mockRestore();
37
59
  });
38
60
 
@@ -13,6 +13,21 @@ export interface AskUserOption {
13
13
  icon?: React.ReactNode;
14
14
  }
15
15
 
16
+ export function isAskUserOptions(value: unknown): value is AskUserOption[] {
17
+ return (
18
+ Array.isArray(value) &&
19
+ value.every((option: unknown) => {
20
+ if (!option || typeof option !== 'object' || Array.isArray(option)) return false;
21
+ const { id, label, description } = option as Partial<AskUserOption>;
22
+ return (
23
+ typeof id === 'string' &&
24
+ typeof label === 'string' &&
25
+ (description == null || typeof description === 'string')
26
+ );
27
+ })
28
+ );
29
+ }
30
+
16
31
  /** Props for the AskUserWidget component */
17
32
  export interface AskUserWidgetProps {
18
33
  /** The question or prompt to display */
@@ -153,11 +168,12 @@ export function AskUserWidget({
153
168
 
154
169
  const styles = VARIANT_STYLES[variant];
155
170
  const DefaultIcon = VARIANT_ICONS[variant];
156
- const safeOptions = Array.isArray(options) ? options : [];
171
+ const validOptions = isAskUserOptions(options);
172
+ const safeOptions = validOptions ? options : [];
157
173
  const invalidOptionsReported = React.useRef(false);
158
174
 
159
175
  React.useEffect(() => {
160
- if (options === undefined || Array.isArray(options) || invalidOptionsReported.current) return;
176
+ if (options === undefined || validOptions || invalidOptionsReported.current) return;
161
177
 
162
178
  invalidOptionsReported.current = true;
163
179
  Env.logger.warn('AskUserWidget received invalid options; rendering without options', {
@@ -166,8 +182,7 @@ export function AskUserWidget({
166
182
  received_type: typeof options,
167
183
  },
168
184
  });
169
- console.warn('[AskUserWidget] Invalid options received; rendering without options.');
170
- }, [options]);
185
+ }, [options, validOptions]);
171
186
 
172
187
  const toggleOption = (optionId: string) => {
173
188
  setSelectedOptions((prev) => {
@@ -45,6 +45,7 @@ import { AgentApprovalModeSelector } from './AgentApprovalModeSelector';
45
45
  import { AgentChatPlaybackControls } from './AgentChatPlaybackControls';
46
46
  import { AgentRequestInputOverlay } from './AgentRequestInputOverlay';
47
47
  import { AgentRightPanel, type WorkstreamInfo } from './AgentRightPanel.js';
48
+ import { AgentRunFeedbackProvider } from './AgentRunFeedback';
48
49
  import { AnimatedThinkingDots, PulsatingCircle } from './AnimatedThinkingDots';
49
50
  import { extractFilesFromClipboard } from './clipboardFiles.js';
50
51
  import { useAgentPlans } from './hooks/useAgentPlans.js';
@@ -1229,7 +1230,7 @@ function StartWorkflowView({
1229
1230
  >
1230
1231
  {/* Drag overlay for full-panel file drop */}
1231
1232
  {canStageFiles && isDragOver && (
1232
- <div className="absolute inset-0 flex items-center justify-center bg-info-background z-50 pointer-events-none rounded-lg">
1233
+ <div className="absolute inset-0 flex items-center justify-center bg-info z-50 pointer-events-none rounded-lg">
1233
1234
  <div className="text-info font-medium flex items-center gap-2 text-lg">
1234
1235
  <UploadIcon className="size-6" />
1235
1236
  Drop files to stage for upload
@@ -2808,47 +2809,50 @@ function ModernAgentConversationInner({
2808
2809
  {messages.length === 0 && !effectiveIsCompleted && pendingStartMessage && pendingStartTimestamp ? (
2809
2810
  <PendingStartConversation message={pendingStartMessage} startedAt={pendingStartTimestamp} />
2810
2811
  ) : (
2811
- <AllMessagesMixed
2812
- messages={renderedMessages}
2813
- workstreamSourceMessages={renderedWorkstreamSourceMessages}
2814
- bottomRef={bottomRef as React.RefObject<HTMLDivElement>}
2815
- isCompleted={displayedIsCompleted}
2816
- plan={getActivePlan.plan}
2817
- workstreamStatus={getActivePlan.workstreamStatus}
2818
- showPlanPanel={showRightPanelProp && showSlidingPanel}
2819
- onTogglePlanPanel={handleTogglePlanPanel}
2820
- plans={plans}
2821
- activePlanIndex={activePlanIndex}
2822
- onChangePlan={handleChangePlan}
2823
- taskLabels={taskLabels}
2824
- streamingMessages={displayedStreamingMessages}
2825
- onSendMessage={isPlaybackLive ? handleSendMessage : undefined}
2826
- onOpenArtifact={showArtifacts ? handleOpenArtifact : undefined}
2827
- messageItemClassNames={messageItemClassNames}
2828
- messageStyleOverrides={messageStyleOverrides}
2829
- toolCallGroupClassNames={toolCallGroupClassNames}
2830
- hideToolCallsInViewMode={hideToolCallsInViewMode}
2831
- streamingMessageClassNames={streamingMessageClassNames}
2832
- batchProgressPanelClassNames={batchProgressPanelClassNames}
2833
- artifactRunId={agentRunId}
2834
- viewMode={viewMode}
2835
- hideWorkstreamTabs={hideWorkstreamTabs}
2836
- workingIndicatorClassName={workingIndicatorClassName}
2837
- messageListClassName={messageListClassName}
2838
- StoreLinkComponent={effectiveStoreLinkComponent}
2839
- CollectionLinkComponent={CollectionLinkComponent}
2840
- prependFriendlyMessage={prependFriendlyMessage}
2841
- initialRequestData={initialRequestData}
2842
- initialRequestSchema={initialRequestSchema}
2843
- initialRequestTitle={initialRequestTitle}
2844
- initialRequestTemplate={initialRequestTemplate}
2845
- showInitialRequest={initialHistoryStatus === 'empty' && messages.length === 0}
2846
- hiddenMessageTypes={hiddenMessageTypes}
2847
- disableAutoScroll={!isPlaybackLive}
2848
- renderRequestInputControls={!shouldShowRequestInputOverlay}
2849
- activeWorkstream={activeWorkstream}
2850
- onActiveWorkstreamChange={setActiveWorkstream}
2851
- />
2812
+ <AgentRunFeedbackProvider agentRunId={agentRunId}>
2813
+ <AllMessagesMixed
2814
+ messages={renderedMessages}
2815
+ workstreamSourceMessages={renderedWorkstreamSourceMessages}
2816
+ bottomRef={bottomRef as React.RefObject<HTMLDivElement>}
2817
+ isCompleted={displayedIsCompleted}
2818
+ plan={getActivePlan.plan}
2819
+ workstreamStatus={getActivePlan.workstreamStatus}
2820
+ showPlanPanel={showRightPanelProp && showSlidingPanel}
2821
+ onTogglePlanPanel={handleTogglePlanPanel}
2822
+ plans={plans}
2823
+ activePlanIndex={activePlanIndex}
2824
+ onChangePlan={handleChangePlan}
2825
+ taskLabels={taskLabels}
2826
+ streamingMessages={displayedStreamingMessages}
2827
+ onSendMessage={isPlaybackLive ? handleSendMessage : undefined}
2828
+ onOpenArtifact={showArtifacts ? handleOpenArtifact : undefined}
2829
+ messageItemClassNames={messageItemClassNames}
2830
+ messageStyleOverrides={messageStyleOverrides}
2831
+ toolCallGroupClassNames={toolCallGroupClassNames}
2832
+ hideToolCallsInViewMode={hideToolCallsInViewMode}
2833
+ streamingMessageClassNames={streamingMessageClassNames}
2834
+ batchProgressPanelClassNames={batchProgressPanelClassNames}
2835
+ artifactRunId={agentRunId}
2836
+ agentRunId={agentRunId}
2837
+ viewMode={viewMode}
2838
+ hideWorkstreamTabs={hideWorkstreamTabs}
2839
+ workingIndicatorClassName={workingIndicatorClassName}
2840
+ messageListClassName={messageListClassName}
2841
+ StoreLinkComponent={effectiveStoreLinkComponent}
2842
+ CollectionLinkComponent={CollectionLinkComponent}
2843
+ prependFriendlyMessage={prependFriendlyMessage}
2844
+ initialRequestData={initialRequestData}
2845
+ initialRequestSchema={initialRequestSchema}
2846
+ initialRequestTitle={initialRequestTitle}
2847
+ initialRequestTemplate={initialRequestTemplate}
2848
+ showInitialRequest={initialHistoryStatus === 'empty' && messages.length === 0}
2849
+ hiddenMessageTypes={hiddenMessageTypes}
2850
+ disableAutoScroll={!isPlaybackLive}
2851
+ renderRequestInputControls={!shouldShowRequestInputOverlay}
2852
+ activeWorkstream={activeWorkstream}
2853
+ onActiveWorkstreamChange={setActiveWorkstream}
2854
+ />
2855
+ </AgentRunFeedbackProvider>
2852
2856
  )}
2853
2857
 
2854
2858
  {shouldShowRequestInputOverlay ? (