@vertesia/ui 1.5.0-dev.20260717.131047Z → 1.5.0-dev.20260722.120446Z

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 (329) hide show
  1. package/lib/core/components/SidePanel.js +1 -1
  2. package/lib/core/components/SidePanel.js.map +1 -1
  3. package/lib/core/components/shadcn/button.d.ts +10 -3
  4. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  5. package/lib/core/components/shadcn/button.js +41 -9
  6. package/lib/core/components/shadcn/button.js.map +1 -1
  7. package/lib/core/components/shadcn/modal/dialog.js +1 -1
  8. package/lib/core/components/shadcn/modal/dialog.js.map +1 -1
  9. package/lib/core/components/shadcn/theme/ThemeSwitcher.js +1 -1
  10. package/lib/core/components/shadcn/theme/ThemeSwitcher.js.map +1 -1
  11. package/lib/features/agent/chat/AgentChatPlaybackControls.d.ts.map +1 -1
  12. package/lib/features/agent/chat/AgentChatPlaybackControls.js +2 -2
  13. package/lib/features/agent/chat/AgentChatPlaybackControls.js.map +1 -1
  14. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
  15. package/lib/features/agent/chat/AgentRequestInputOverlay.js +2 -2
  16. package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
  17. package/lib/features/agent/chat/ArtifactsTab.d.ts.map +1 -1
  18. package/lib/features/agent/chat/ArtifactsTab.js +5 -2
  19. package/lib/features/agent/chat/ArtifactsTab.js.map +1 -1
  20. package/lib/features/agent/chat/ImageLightbox.d.ts.map +1 -1
  21. package/lib/features/agent/chat/ImageLightbox.js +1 -1
  22. package/lib/features/agent/chat/ImageLightbox.js.map +1 -1
  23. package/lib/features/agent/chat/ModernAgentConversation.js +1 -1
  24. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  25. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  26. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +5 -5
  27. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  28. package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  29. package/lib/features/agent/chat/ModernAgentOutput/Header.js +4 -2
  30. package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  31. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  32. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +2 -2
  33. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  34. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  35. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -1
  36. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  37. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts +6 -0
  38. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts.map +1 -1
  39. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js +15 -1
  40. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js.map +1 -1
  41. package/lib/features/environment/SelectEnvironment.d.ts +28 -0
  42. package/lib/features/environment/SelectEnvironment.d.ts.map +1 -0
  43. package/lib/features/environment/SelectEnvironment.js +97 -0
  44. package/lib/features/environment/SelectEnvironment.js.map +1 -0
  45. package/lib/features/environment/index.d.ts +2 -0
  46. package/lib/features/environment/index.d.ts.map +1 -0
  47. package/lib/features/environment/index.js +2 -0
  48. package/lib/features/environment/index.js.map +1 -0
  49. package/lib/features/index.d.ts +2 -0
  50. package/lib/features/index.d.ts.map +1 -1
  51. package/lib/features/index.js +2 -0
  52. package/lib/features/index.js.map +1 -1
  53. package/lib/features/magic-pdf/AnnotatedImageSlider.js +12 -12
  54. package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
  55. package/lib/features/magic-pdf/DownloadPopover.d.ts.map +1 -1
  56. package/lib/features/magic-pdf/DownloadPopover.js +3 -17
  57. package/lib/features/magic-pdf/DownloadPopover.js.map +1 -1
  58. package/lib/features/magic-pdf/ExtractedContentView.js +2 -6
  59. package/lib/features/magic-pdf/ExtractedContentView.js.map +1 -1
  60. package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.d.ts +25 -0
  61. package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.d.ts.map +1 -0
  62. package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.js +42 -0
  63. package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.js.map +1 -0
  64. package/lib/features/magic-pdf/GroundedExtractionView.d.ts +27 -0
  65. package/lib/features/magic-pdf/GroundedExtractionView.d.ts.map +1 -0
  66. package/lib/features/magic-pdf/GroundedExtractionView.js +377 -0
  67. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -0
  68. package/lib/features/magic-pdf/MagicPdfProvider.d.ts +0 -2
  69. package/lib/features/magic-pdf/MagicPdfProvider.d.ts.map +1 -1
  70. package/lib/features/magic-pdf/MagicPdfProvider.js +15 -42
  71. package/lib/features/magic-pdf/MagicPdfProvider.js.map +1 -1
  72. package/lib/features/magic-pdf/MagicPdfView.d.ts.map +1 -1
  73. package/lib/features/magic-pdf/MagicPdfView.js +2 -19
  74. package/lib/features/magic-pdf/MagicPdfView.js.map +1 -1
  75. package/lib/features/magic-pdf/index.d.ts +1 -0
  76. package/lib/features/magic-pdf/index.d.ts.map +1 -1
  77. package/lib/features/magic-pdf/index.js +1 -0
  78. package/lib/features/magic-pdf/index.js.map +1 -1
  79. package/lib/features/magic-pdf/types.d.ts +1 -1
  80. package/lib/features/magic-pdf/types.d.ts.map +1 -1
  81. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  82. package/lib/features/oauth/McpConnectionsButton.js +2 -2
  83. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  84. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  85. package/lib/features/oauth/McpConnectionsDialog.js +1 -1
  86. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  87. package/lib/features/oauth/RemoteMcpConnectionButton.d.ts.map +1 -1
  88. package/lib/features/oauth/RemoteMcpConnectionButton.js +1 -1
  89. package/lib/features/oauth/RemoteMcpConnectionButton.js.map +1 -1
  90. package/lib/features/pdf-viewer/PdfPageSlider.js +2 -2
  91. package/lib/features/pdf-viewer/PdfPageSlider.js.map +1 -1
  92. package/lib/features/pdf-viewer/SimplePdfViewer.d.ts.map +1 -1
  93. package/lib/features/pdf-viewer/SimplePdfViewer.js +3 -3
  94. package/lib/features/pdf-viewer/SimplePdfViewer.js.map +1 -1
  95. package/lib/features/permissions/UserPermissionsProvider.js +1 -1
  96. package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
  97. package/lib/features/store/objects/DocumentQuickFilter.d.ts.map +1 -1
  98. package/lib/features/store/objects/DocumentQuickFilter.js +5 -5
  99. package/lib/features/store/objects/DocumentQuickFilter.js.map +1 -1
  100. package/lib/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
  101. package/lib/features/store/objects/DocumentSearchResults.js +2 -2
  102. package/lib/features/store/objects/DocumentSearchResults.js.map +1 -1
  103. package/lib/features/store/objects/components/ContentDispositionButton.d.ts.map +1 -1
  104. package/lib/features/store/objects/components/ContentDispositionButton.js +1 -1
  105. package/lib/features/store/objects/components/ContentDispositionButton.js.map +1 -1
  106. package/lib/features/store/objects/components/ContentOverview.d.ts.map +1 -1
  107. package/lib/features/store/objects/components/ContentOverview.js +78 -21
  108. package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
  109. package/lib/features/store/objects/components/DocumentIcon.js +1 -1
  110. package/lib/features/store/objects/components/VectorSearchWidget.js +1 -1
  111. package/lib/features/store/objects/components/VectorSearchWidget.js.map +1 -1
  112. package/lib/features/store/objects/components/useContentPanelHooks.d.ts +1 -2
  113. package/lib/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
  114. package/lib/features/store/objects/components/useContentPanelHooks.js +4 -8
  115. package/lib/features/store/objects/components/useContentPanelHooks.js.map +1 -1
  116. package/lib/features/store/objects/layout/renderers.d.ts.map +1 -1
  117. package/lib/features/store/objects/layout/renderers.js +2 -2
  118. package/lib/features/store/objects/layout/renderers.js.map +1 -1
  119. package/lib/features/store/objects/selection/SelectionActions.js +1 -1
  120. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  121. package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
  122. package/lib/features/store/types/ContentObjectTypesTable.js +4 -4
  123. package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
  124. package/lib/features/store/types/IntakePolicyEditor.d.ts +15 -0
  125. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -0
  126. package/lib/features/store/types/IntakePolicyEditor.js +455 -0
  127. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -0
  128. package/lib/features/store/types/IntakePolicyForm.d.ts +11 -0
  129. package/lib/features/store/types/IntakePolicyForm.d.ts.map +1 -0
  130. package/lib/features/store/types/IntakePolicyForm.js +91 -0
  131. package/lib/features/store/types/IntakePolicyForm.js.map +1 -0
  132. package/lib/features/store/types/SelectContentTypeModal.js +1 -1
  133. package/lib/features/store/types/SelectContentTypeModal.js.map +1 -1
  134. package/lib/features/store/types/index.d.ts +2 -0
  135. package/lib/features/store/types/index.d.ts.map +1 -1
  136. package/lib/features/store/types/index.js +2 -0
  137. package/lib/features/store/types/index.js.map +1 -1
  138. package/lib/features/store/types/intake-policy-editor.logic.d.ts +7 -0
  139. package/lib/features/store/types/intake-policy-editor.logic.d.ts.map +1 -0
  140. package/lib/features/store/types/intake-policy-editor.logic.js +60 -0
  141. package/lib/features/store/types/intake-policy-editor.logic.js.map +1 -0
  142. package/lib/features/store/types/typeCatalogCache.d.ts +5 -0
  143. package/lib/features/store/types/typeCatalogCache.d.ts.map +1 -0
  144. package/lib/features/store/types/typeCatalogCache.js +40 -0
  145. package/lib/features/store/types/typeCatalogCache.js.map +1 -0
  146. package/lib/features/user/UserInfo.js +7 -3
  147. package/lib/features/user/UserInfo.js.map +1 -1
  148. package/lib/features/views/ViewExperience.d.ts +20 -0
  149. package/lib/features/views/ViewExperience.d.ts.map +1 -0
  150. package/lib/features/views/ViewExperience.js +188 -0
  151. package/lib/features/views/ViewExperience.js.map +1 -0
  152. package/lib/features/views/ViewNavigation.d.ts +3 -0
  153. package/lib/features/views/ViewNavigation.d.ts.map +1 -0
  154. package/lib/features/views/ViewNavigation.js +36 -0
  155. package/lib/features/views/ViewNavigation.js.map +1 -0
  156. package/lib/features/views/ViewResults.d.ts +9 -0
  157. package/lib/features/views/ViewResults.d.ts.map +1 -0
  158. package/lib/features/views/ViewResults.js +229 -0
  159. package/lib/features/views/ViewResults.js.map +1 -0
  160. package/lib/features/views/ViewSearch.d.ts +3 -0
  161. package/lib/features/views/ViewSearch.d.ts.map +1 -0
  162. package/lib/features/views/ViewSearch.js +74 -0
  163. package/lib/features/views/ViewSearch.js.map +1 -0
  164. package/lib/features/views/index.d.ts +7 -0
  165. package/lib/features/views/index.d.ts.map +1 -0
  166. package/lib/features/views/index.js +7 -0
  167. package/lib/features/views/index.js.map +1 -0
  168. package/lib/features/views/types.d.ts +39 -0
  169. package/lib/features/views/types.d.ts.map +1 -0
  170. package/lib/features/views/types.js +2 -0
  171. package/lib/features/views/types.js.map +1 -0
  172. package/lib/features/views/viewState.d.ts +14 -0
  173. package/lib/features/views/viewState.d.ts.map +1 -0
  174. package/lib/features/views/viewState.js +117 -0
  175. package/lib/features/views/viewState.js.map +1 -0
  176. package/lib/i18n/locales/ar.json +16 -0
  177. package/lib/i18n/locales/de.json +12 -0
  178. package/lib/i18n/locales/en.json +245 -33
  179. package/lib/i18n/locales/es.json +13 -0
  180. package/lib/i18n/locales/fr.json +13 -0
  181. package/lib/i18n/locales/it.json +13 -0
  182. package/lib/i18n/locales/ja.json +11 -0
  183. package/lib/i18n/locales/ko.json +11 -0
  184. package/lib/i18n/locales/pt.json +13 -0
  185. package/lib/i18n/locales/ru.json +14 -0
  186. package/lib/i18n/locales/tr.json +12 -0
  187. package/lib/i18n/locales/zh-TW.json +11 -0
  188. package/lib/i18n/locales/zh.json +11 -0
  189. package/lib/router/HistoryNavigator.d.ts.map +1 -1
  190. package/lib/router/HistoryNavigator.js +23 -13
  191. package/lib/router/HistoryNavigator.js.map +1 -1
  192. package/lib/router/Nav.d.ts.map +1 -1
  193. package/lib/router/Nav.js +15 -6
  194. package/lib/router/Nav.js.map +1 -1
  195. package/lib/session/UserSession.d.ts.map +1 -1
  196. package/lib/session/UserSession.js +5 -3
  197. package/lib/session/UserSession.js.map +1 -1
  198. package/lib/vertesia-ui-core.js +4 -4
  199. package/lib/vertesia-ui-core.js.map +1 -1
  200. package/lib/vertesia-ui-features.js +34 -27
  201. package/lib/vertesia-ui-features.js.map +1 -1
  202. package/lib/vertesia-ui-i18n.js +15 -15
  203. package/lib/vertesia-ui-i18n.js.map +1 -1
  204. package/lib/vertesia-ui-router.js +1 -1
  205. package/lib/vertesia-ui-router.js.map +1 -1
  206. package/lib/vertesia-ui-session.js +1 -1
  207. package/lib/vertesia-ui-session.js.map +1 -1
  208. package/lib/vertesia-ui-shell.js +1 -1
  209. package/lib/vertesia-ui-shell.js.map +1 -1
  210. package/lib/vertesia-ui-widgets.js +7 -7
  211. package/lib/vertesia-ui-widgets.js.map +1 -1
  212. package/lib/widgets/form/Form.js +1 -1
  213. package/lib/widgets/form/Form.js.map +1 -1
  214. package/lib/widgets/json-view/JSONSwitcher.js +1 -1
  215. package/lib/widgets/json-view/JSONSwitcher.js.map +1 -1
  216. package/lib/widgets/json-view/JSONView.js +6 -6
  217. package/lib/widgets/json-view/JSONView.js.map +1 -1
  218. package/lib/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -1
  219. package/lib/widgets/monacoEditor/MonacoEditor.js +29 -0
  220. package/lib/widgets/monacoEditor/MonacoEditor.js.map +1 -1
  221. package/lib/widgets/schema-editor/ManagedSchema.d.ts +8 -0
  222. package/lib/widgets/schema-editor/ManagedSchema.d.ts.map +1 -1
  223. package/lib/widgets/schema-editor/ManagedSchema.js +16 -0
  224. package/lib/widgets/schema-editor/ManagedSchema.js.map +1 -1
  225. package/lib/widgets/schema-editor/editor/EditableSchemaProperty.d.ts +2 -0
  226. package/lib/widgets/schema-editor/editor/EditableSchemaProperty.d.ts.map +1 -1
  227. package/lib/widgets/schema-editor/editor/EditableSchemaProperty.js +1 -0
  228. package/lib/widgets/schema-editor/editor/EditableSchemaProperty.js.map +1 -1
  229. package/lib/widgets/schema-editor/editor/PropertyViewer.d.ts.map +1 -1
  230. package/lib/widgets/schema-editor/editor/PropertyViewer.js +4 -1
  231. package/lib/widgets/schema-editor/editor/PropertyViewer.js.map +1 -1
  232. package/lib/widgets/schema-editor/editor/SchemaEditor.d.ts.map +1 -1
  233. package/lib/widgets/schema-editor/editor/SchemaEditor.js +22 -11
  234. package/lib/widgets/schema-editor/editor/SchemaEditor.js.map +1 -1
  235. package/package.json +32 -32
  236. package/src/__tests__/a11y.test.tsx +73 -0
  237. package/src/core/components/SidePanel.tsx +1 -1
  238. package/src/core/components/shadcn/button.tsx +67 -12
  239. package/src/core/components/shadcn/modal/dialog.tsx +1 -1
  240. package/src/core/components/shadcn/theme/ThemeSwitcher.tsx +1 -1
  241. package/src/features/agent/chat/AgentChatPlaybackControls.tsx +0 -5
  242. package/src/features/agent/chat/AgentRequestInputOverlay.test.tsx +31 -0
  243. package/src/features/agent/chat/AgentRequestInputOverlay.tsx +6 -2
  244. package/src/features/agent/chat/ArtifactsTab.tsx +19 -9
  245. package/src/features/agent/chat/ImageLightbox.tsx +0 -1
  246. package/src/features/agent/chat/ModernAgentConversation.test.tsx +48 -0
  247. package/src/features/agent/chat/ModernAgentConversation.tsx +0 -2
  248. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +6 -1
  249. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +21 -15
  250. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +3 -3
  251. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +0 -2
  252. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +4 -1
  253. package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.test.ts +80 -0
  254. package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.ts +21 -1
  255. package/src/features/environment/SelectEnvironment.tsx +186 -0
  256. package/src/features/environment/index.ts +1 -0
  257. package/src/features/index.ts +2 -0
  258. package/src/features/magic-pdf/AnnotatedImageSlider.tsx +62 -66
  259. package/src/features/magic-pdf/DownloadPopover.tsx +5 -48
  260. package/src/features/magic-pdf/ExtractedContentView.tsx +2 -14
  261. package/src/features/magic-pdf/GroundedExtractionAssistantPanel.tsx +86 -0
  262. package/src/features/magic-pdf/GroundedExtractionView.test.tsx +65 -0
  263. package/src/features/magic-pdf/GroundedExtractionView.tsx +1055 -0
  264. package/src/features/magic-pdf/MagicPdfProvider.tsx +17 -47
  265. package/src/features/magic-pdf/MagicPdfView.tsx +3 -51
  266. package/src/features/magic-pdf/index.ts +1 -0
  267. package/src/features/magic-pdf/types.ts +1 -1
  268. package/src/features/oauth/McpConnectionsButton.tsx +0 -2
  269. package/src/features/oauth/McpConnectionsDialog.tsx +0 -1
  270. package/src/features/oauth/RemoteMcpConnectionButton.tsx +0 -1
  271. package/src/features/pdf-viewer/PdfPageSlider.tsx +5 -5
  272. package/src/features/pdf-viewer/SimplePdfViewer.tsx +11 -12
  273. package/src/features/permissions/UserPermissionsProvider.tsx +1 -1
  274. package/src/features/store/objects/DocumentQuickFilter.tsx +13 -15
  275. package/src/features/store/objects/DocumentSearchResults.tsx +3 -6
  276. package/src/features/store/objects/components/ContentDispositionButton.tsx +5 -1
  277. package/src/features/store/objects/components/ContentOverview.tsx +144 -44
  278. package/src/features/store/objects/components/DocumentIcon.tsx +1 -1
  279. package/src/features/store/objects/components/VectorSearchWidget.tsx +2 -2
  280. package/src/features/store/objects/components/useContentPanelHooks.ts +3 -15
  281. package/src/features/store/objects/layout/renderers.tsx +1 -2
  282. package/src/features/store/objects/selection/SelectionActions.tsx +1 -1
  283. package/src/features/store/types/ContentObjectTypesTable.tsx +12 -14
  284. package/src/features/store/types/IntakePolicyEditor.tsx +679 -0
  285. package/src/features/store/types/IntakePolicyForm.tsx +838 -0
  286. package/src/features/store/types/SelectContentTypeModal.tsx +2 -2
  287. package/src/features/store/types/index.ts +2 -0
  288. package/src/features/store/types/intake-policy-editor.logic.test.ts +109 -0
  289. package/src/features/store/types/intake-policy-editor.logic.ts +87 -0
  290. package/src/features/store/types/typeCatalogCache.ts +45 -0
  291. package/src/features/user/UserInfo.tsx +7 -7
  292. package/src/features/views/ViewExperience.test.tsx +273 -0
  293. package/src/features/views/ViewExperience.tsx +408 -0
  294. package/src/features/views/ViewNavigation.test.tsx +86 -0
  295. package/src/features/views/ViewNavigation.tsx +187 -0
  296. package/src/features/views/ViewResults.test.ts +82 -0
  297. package/src/features/views/ViewResults.tsx +609 -0
  298. package/src/features/views/ViewSearch.test.tsx +147 -0
  299. package/src/features/views/ViewSearch.tsx +205 -0
  300. package/src/features/views/index.ts +6 -0
  301. package/src/features/views/types.ts +57 -0
  302. package/src/features/views/viewState.test.ts +111 -0
  303. package/src/features/views/viewState.ts +132 -0
  304. package/src/i18n/locales/ar.json +16 -0
  305. package/src/i18n/locales/de.json +12 -0
  306. package/src/i18n/locales/en.json +965 -753
  307. package/src/i18n/locales/es.json +13 -0
  308. package/src/i18n/locales/fr.json +13 -0
  309. package/src/i18n/locales/it.json +13 -0
  310. package/src/i18n/locales/ja.json +11 -0
  311. package/src/i18n/locales/ko.json +11 -0
  312. package/src/i18n/locales/pt.json +13 -0
  313. package/src/i18n/locales/ru.json +14 -0
  314. package/src/i18n/locales/tr.json +12 -0
  315. package/src/i18n/locales/zh-TW.json +11 -0
  316. package/src/i18n/locales/zh.json +11 -0
  317. package/src/router/HistoryNavigator.test.ts +48 -0
  318. package/src/router/HistoryNavigator.ts +24 -13
  319. package/src/router/Nav.tsx +18 -8
  320. package/src/session/UserSession.test.ts +39 -0
  321. package/src/session/UserSession.ts +5 -3
  322. package/src/widgets/form/Form.tsx +1 -1
  323. package/src/widgets/json-view/JSONSwitcher.tsx +2 -2
  324. package/src/widgets/json-view/JSONView.tsx +7 -7
  325. package/src/widgets/monacoEditor/MonacoEditor.tsx +29 -0
  326. package/src/widgets/schema-editor/ManagedSchema.ts +22 -0
  327. package/src/widgets/schema-editor/editor/EditableSchemaProperty.tsx +3 -0
  328. package/src/widgets/schema-editor/editor/PropertyViewer.tsx +14 -3
  329. package/src/widgets/schema-editor/editor/SchemaEditor.tsx +80 -29
@@ -0,0 +1,1055 @@
1
+ import {
2
+ Badge,
3
+ Button,
4
+ cn,
5
+ ErrorBox,
6
+ errorMessage,
7
+ ResizableHandle,
8
+ ResizablePanel,
9
+ ResizablePanelGroup,
10
+ Spinner,
11
+ useFetch,
12
+ VTooltip,
13
+ } from '@vertesia/ui/core';
14
+ import { useUITranslation } from '@vertesia/ui/i18n';
15
+ import { useUserSession } from '@vertesia/ui/session';
16
+ import {
17
+ CheckCircle2,
18
+ ChevronLeft,
19
+ ChevronRight,
20
+ Eye,
21
+ FileDown,
22
+ FileJson2,
23
+ FileText,
24
+ Grid3X3,
25
+ Image as ImageIcon,
26
+ Sparkles,
27
+ X,
28
+ } from 'lucide-react';
29
+ import { useEffect, useMemo, useState } from 'react';
30
+ import { GroundedExtractionAssistantPanel } from './GroundedExtractionAssistantPanel';
31
+
32
+ const ADVANCED_PROCESSING_PREFIX = 'magic-pdf';
33
+
34
+ interface GroundedBox {
35
+ x: number;
36
+ y: number;
37
+ w: number;
38
+ h: number;
39
+ }
40
+
41
+ /**
42
+ * Citation paths sometimes carry a leading "data." or "$." (the extraction
43
+ * schema nests the object under "data", and some models echo it). The server
44
+ * strips it for value resolution; the UI must match on the same normalized form
45
+ * so per-field scores, checkmarks and click-to-locate work for both variants.
46
+ */
47
+ function normalizeCitationPath(path: string): string {
48
+ return path.replace(/^\$\.?/, '').replace(/^data\./, '');
49
+ }
50
+
51
+ interface GroundedCitation {
52
+ path: string;
53
+ page: number;
54
+ block_ids: number[];
55
+ cells?: { start: string; end: string };
56
+ verified: boolean;
57
+ source_text?: string;
58
+ value?: string;
59
+ boxes: GroundedBox[];
60
+ misaligned?: boolean;
61
+ snapped?: boolean;
62
+ reviewed?: boolean;
63
+ confidence?: number;
64
+ }
65
+
66
+ interface GroundedExtractionFile {
67
+ version: number;
68
+ object_id: string;
69
+ run_id: string;
70
+ model?: string;
71
+ generated_at: string;
72
+ pages: Record<string, { width: number; height: number; rotation?: number }>;
73
+ data: Record<string, unknown>;
74
+ citations: GroundedCitation[];
75
+ /** Mean provenance confidence in [0,1]; 1.0 = verified against a digital text layer */
76
+ confidence?: number;
77
+ /** Content hardness: 0 = clean digital text, 1 = unreadable */
78
+ hardness?: { score: number; escalated?: boolean };
79
+ conflicts?: { path: string; kept: unknown; dropped: unknown }[];
80
+ /** Doc-level trust verdict: whether the content can be used without a human check */
81
+ verdict?: 'good_to_go' | 'needs_review';
82
+ /** One-sentence rationale for `verdict` */
83
+ verdict_reason?: string;
84
+ /** Post-extraction review result, when a review ran */
85
+ review?: {
86
+ assessment: 'complete' | 'issues_found';
87
+ summary?: string;
88
+ corrections_applied?: number;
89
+ verdict?: 'good_to_go' | 'needs_review';
90
+ verdict_reason?: string;
91
+ };
92
+ }
93
+
94
+ interface GroundedExtractionViewProps {
95
+ objectId: string;
96
+ onClose?: () => void;
97
+ }
98
+
99
+ type PageImageMode = 'original' | 'grid';
100
+
101
+ /** True when a grounded extraction result exists for the object */
102
+ export function useGroundedExtractionAvailable(objectId: string): boolean {
103
+ const { client } = useUserSession();
104
+ const [available, setAvailable] = useState(false);
105
+ useEffect(() => {
106
+ let cancelled = false;
107
+ setAvailable(false);
108
+ client.files
109
+ .getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/grounded-extraction.json`)
110
+ .then((r) => fetch(r.url, { method: 'HEAD' }))
111
+ .then((res) => {
112
+ if (!cancelled) setAvailable(res.ok);
113
+ })
114
+ .catch(() => {
115
+ // no grounded extraction for this object
116
+ });
117
+ return () => {
118
+ cancelled = true;
119
+ };
120
+ }, [client, objectId]);
121
+ return available;
122
+ }
123
+
124
+ export interface GroundedSummary {
125
+ confidence?: number;
126
+ verified: number;
127
+ total: number;
128
+ verdict?: 'good_to_go' | 'needs_review';
129
+ verdict_reason?: string;
130
+ }
131
+
132
+ /** Lightweight summary of the object's grounded extraction, or null when none exists */
133
+ export function useGroundedSummary(objectId: string): GroundedSummary | null {
134
+ const { client } = useUserSession();
135
+ const [summary, setSummary] = useState<GroundedSummary | null>(null);
136
+ useEffect(() => {
137
+ let cancelled = false;
138
+ setSummary(null);
139
+ client.files
140
+ .getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/grounded-extraction.json`)
141
+ .then((r) => fetch(r.url))
142
+ .then((res) => (res.ok ? res.json() : null))
143
+ .then((file: GroundedExtractionFile | null) => {
144
+ if (cancelled || !file?.citations) return;
145
+ setSummary({
146
+ confidence: file.confidence,
147
+ verified: file.citations.filter((c) => c.verified).length,
148
+ total: file.citations.length,
149
+ verdict: file.verdict,
150
+ verdict_reason: file.verdict_reason,
151
+ });
152
+ })
153
+ .catch(() => {
154
+ // no grounded extraction for this object
155
+ });
156
+ return () => {
157
+ cancelled = true;
158
+ };
159
+ }, [client, objectId]);
160
+ return summary;
161
+ }
162
+
163
+ /**
164
+ * Embeddable grounded-extraction review panel (fills its container): extracted
165
+ * properties on one side, source pages with citation boxes on the other.
166
+ * Clicking a property highlights where in the document it was read.
167
+ */
168
+ export function GroundedExtractionPanel({ objectId, onClose }: GroundedExtractionViewProps) {
169
+ const { t } = useUITranslation();
170
+ const { client } = useUserSession();
171
+
172
+ const {
173
+ data: extraction,
174
+ error,
175
+ isLoading,
176
+ refetch,
177
+ } = useFetch<GroundedExtractionFile>(async () => {
178
+ const response = await client.files.getDownloadUrl(
179
+ `${ADVANCED_PROCESSING_PREFIX}/${objectId}/grounded-extraction.json`,
180
+ );
181
+ const result = await fetch(response.url);
182
+ if (!result.ok) {
183
+ throw new Error(t('grounded.notFound'));
184
+ }
185
+ return result.json();
186
+ }, [objectId]);
187
+
188
+ if (error) {
189
+ return (
190
+ <div className="flex h-full items-center justify-center">
191
+ <div className="flex flex-col items-center gap-4 p-8 max-w-md">
192
+ <ErrorBox title={t('grounded.failedToLoad')}>{errorMessage(error)}</ErrorBox>
193
+ {onClose && (
194
+ <Button variant="outline" onClick={onClose}>
195
+ {t('pdf.close')}
196
+ </Button>
197
+ )}
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ if (isLoading || !extraction) {
204
+ return (
205
+ <div className="flex h-full items-center justify-center">
206
+ <Spinner size="lg" />
207
+ </div>
208
+ );
209
+ }
210
+
211
+ return (
212
+ <div className="relative h-full">
213
+ <GroundedExtractionViewImpl
214
+ extraction={extraction}
215
+ objectId={objectId}
216
+ onClose={onClose}
217
+ onExtractionChanged={refetch}
218
+ />
219
+ </div>
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Full-screen route wrapper around GroundedExtractionPanel.
225
+ */
226
+ export function GroundedExtractionView({ objectId, onClose }: GroundedExtractionViewProps) {
227
+ return (
228
+ <div className="fixed inset-0 bg-background z-50">
229
+ <GroundedExtractionPanel objectId={objectId} onClose={onClose} />
230
+ </div>
231
+ );
232
+ }
233
+
234
+ function GroundedExtractionViewImpl({
235
+ extraction,
236
+ objectId,
237
+ onClose,
238
+ onExtractionChanged,
239
+ }: {
240
+ extraction: GroundedExtractionFile;
241
+ objectId: string;
242
+ onClose?: () => void;
243
+ onExtractionChanged?: () => void;
244
+ }) {
245
+ const { t } = useUITranslation();
246
+ const { client } = useUserSession();
247
+ const pageNumbers = useMemo(
248
+ () =>
249
+ Object.keys(extraction.pages)
250
+ .map((p) => parseInt(p, 10))
251
+ .sort((a, b) => a - b),
252
+ [extraction.pages],
253
+ );
254
+
255
+ const downloadArtifact = (path: string) => {
256
+ client.files
257
+ .getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/${path}`)
258
+ .then((r) => window.open(r.url, '_blank'))
259
+ .catch(() => {
260
+ // artifact absent for this document
261
+ });
262
+ };
263
+ const [page, setPage] = useState(pageNumbers[0] ?? 1);
264
+ const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined);
265
+ const [showBreakdown, setShowBreakdown] = useState(false);
266
+ const [showVerdict, setShowVerdict] = useState(false);
267
+ const [pageImageMode, setPageImageMode] = useState<PageImageMode>('original');
268
+ const [assistantOpen, setAssistantOpen] = useState(false);
269
+
270
+ const breakdown = useMemo(() => {
271
+ const groups = { digital: 0, ocr: 0, reviewerConfirmed: 0, snapped: 0, imageRead: 0 };
272
+ const unverified: GroundedCitation[] = [];
273
+ for (const c of extraction.citations) {
274
+ if (c.verified) {
275
+ // Programmatic verification: the value matched the extracted text
276
+ // (digital text layer verbatim, or the OCR text).
277
+ if (c.snapped) groups.snapped++;
278
+ else if ((c.confidence ?? 0) >= 1) groups.digital++;
279
+ else groups.ocr++;
280
+ } else if (c.reviewed) {
281
+ // Model verification: the reviewer read the page image and confirmed
282
+ // the value. This is a strong signal — and the ONLY one available for
283
+ // scanned/handwritten content, where there is no text layer to match.
284
+ groups.reviewerConfirmed++;
285
+ } else {
286
+ // Read from the image but neither text-matched nor reviewer-confirmed.
287
+ groups.imageRead++;
288
+ unverified.push(c);
289
+ }
290
+ }
291
+ // Two kinds of verification, both trustworthy:
292
+ // - digitally verified: matched the document's text (digital layer or OCR)
293
+ // - model verified: the reviewer confirmed it against the page image
294
+ const digitallyVerified = groups.digital + groups.ocr + groups.snapped;
295
+ const modelVerified = groups.reviewerConfirmed;
296
+ return {
297
+ groups,
298
+ unverified,
299
+ digitallyVerified,
300
+ modelVerified,
301
+ totalVerified: digitallyVerified + modelVerified,
302
+ };
303
+ }, [extraction.citations]);
304
+
305
+ const citationsByPath = useMemo(() => {
306
+ const map = new Map<string, GroundedCitation[]>();
307
+ for (const citation of extraction.citations) {
308
+ const key = normalizeCitationPath(citation.path);
309
+ const list = map.get(key) ?? [];
310
+ list.push(citation);
311
+ map.set(key, list);
312
+ }
313
+ return map;
314
+ }, [extraction.citations]);
315
+
316
+ const totalCitations = extraction.citations.length;
317
+
318
+ const selectPath = (path: string) => {
319
+ setSelectedPath(path);
320
+ const citation = citationsByPath.get(normalizeCitationPath(path))?.[0];
321
+ if (citation) {
322
+ setPage(citation.page);
323
+ }
324
+ };
325
+
326
+ const pageIndex = pageNumbers.indexOf(page);
327
+ const selectedCitation = selectedPath
328
+ ? citationsByPath.get(normalizeCitationPath(selectedPath))?.find((citation) => citation.page === page)
329
+ : undefined;
330
+ const selectedCellRange = selectedCitation?.cells
331
+ ? selectedCitation.cells.start === selectedCitation.cells.end
332
+ ? selectedCitation.cells.start
333
+ : `${selectedCitation.cells.start}:${selectedCitation.cells.end}`
334
+ : undefined;
335
+
336
+ return (
337
+ <ResizablePanelGroup direction="horizontal" className="absolute inset-0">
338
+ <ResizablePanel defaultSize={55} minSize={25} className="flex flex-col bg-muted">
339
+ {/* Page navigation */}
340
+ <div className="relative flex h-9 shrink-0 items-center justify-center bg-sidebar px-2 border-b border-sidebar-border">
341
+ {selectedCellRange && (
342
+ <Badge
343
+ variant="secondary"
344
+ className="absolute start-2 max-w-32 truncate font-mono text-[11px]"
345
+ title={selectedCellRange}
346
+ >
347
+ {selectedCellRange}
348
+ </Badge>
349
+ )}
350
+ <div className="flex items-center gap-2">
351
+ <Button
352
+ variant="ghost"
353
+ size="xs"
354
+ aria-label={t('grounded.previousPage')}
355
+ disabled={pageIndex <= 0}
356
+ onClick={() => setPage(pageNumbers[pageIndex - 1])}
357
+ >
358
+ <ChevronLeft className="size-4" />
359
+ </Button>
360
+ <span className="text-xs text-muted-foreground">
361
+ {t('pdf.pageOf', { pageNumber: page, totalPages: pageNumbers.length })}
362
+ </span>
363
+ <Button
364
+ variant="ghost"
365
+ size="xs"
366
+ aria-label={t('grounded.nextPage')}
367
+ disabled={pageIndex >= pageNumbers.length - 1}
368
+ onClick={() => setPage(pageNumbers[pageIndex + 1])}
369
+ >
370
+ <ChevronRight className="size-4" />
371
+ </Button>
372
+ </div>
373
+ <fieldset className="absolute end-2 flex items-center gap-0.5 rounded border-0 bg-muted p-0.5">
374
+ <legend className="sr-only">{t('grounded.checkerboard')}</legend>
375
+ <VTooltip description={t('pdf.originalImages')} placement="bottom" size="xs" asChild>
376
+ <Button
377
+ variant={pageImageMode === 'original' ? 'outline' : 'ghost'}
378
+ size="xs"
379
+ aria-label={t('pdf.originalImages')}
380
+ aria-pressed={pageImageMode === 'original'}
381
+ className="h-6 gap-1 px-1.5"
382
+ onClick={() => setPageImageMode('original')}
383
+ >
384
+ <ImageIcon className="size-3.5" />
385
+ <span className="hidden sm:inline">{t('pdf.page')}</span>
386
+ </Button>
387
+ </VTooltip>
388
+ <VTooltip description={t('grounded.checkerboard')} placement="bottom" size="xs" asChild>
389
+ <Button
390
+ variant={pageImageMode === 'grid' ? 'outline' : 'ghost'}
391
+ size="xs"
392
+ aria-label={t('grounded.checkerboard')}
393
+ aria-pressed={pageImageMode === 'grid'}
394
+ className="h-6 gap-1 px-1.5"
395
+ onClick={() => setPageImageMode('grid')}
396
+ >
397
+ <Grid3X3 className="size-3.5" />
398
+ <span className="hidden sm:inline">{t('grounded.checkerboard')}</span>
399
+ </Button>
400
+ </VTooltip>
401
+ </fieldset>
402
+ </div>
403
+ <div className="flex-1 overflow-auto p-2">
404
+ <PageWithOverlay
405
+ objectId={objectId}
406
+ page={page}
407
+ dims={extraction.pages[String(page)]}
408
+ citations={extraction.citations.filter((c) => c.page === page)}
409
+ selectedPath={selectedPath}
410
+ onSelectPath={setSelectedPath}
411
+ imageMode={pageImageMode}
412
+ />
413
+ </div>
414
+ </ResizablePanel>
415
+ <ResizableHandle className="w-[4px] bg-border cursor-ew-resize" />
416
+ <ResizablePanel defaultSize={45} minSize={20} className="flex flex-col">
417
+ {/* Header */}
418
+ <div className="flex h-9 items-center justify-between shrink-0 bg-sidebar px-2 border-b border-sidebar-border">
419
+ <span className="text-sm font-medium">{t('grounded.title')}</span>
420
+ <div className="flex items-center gap-2">
421
+ <VTooltip description={t('grounded.assistantTooltip')} placement="bottom" size="xs" asChild>
422
+ <Button
423
+ variant={assistantOpen ? 'primary' : 'ghost'}
424
+ size="xs"
425
+ aria-label={t('grounded.assistant')}
426
+ onClick={() => setAssistantOpen((v) => !v)}
427
+ >
428
+ <Sparkles className="size-4" />
429
+ </Button>
430
+ </VTooltip>
431
+ {extraction.verdict && (
432
+ <button
433
+ type="button"
434
+ className="cursor-pointer"
435
+ onClick={() => setShowVerdict((v) => !v)}
436
+ aria-expanded={showVerdict}
437
+ aria-label={t('grounded.verdictDetails')}
438
+ >
439
+ <Badge variant={extraction.verdict === 'good_to_go' ? 'success' : 'attention'}>
440
+ {extraction.verdict === 'good_to_go'
441
+ ? t('grounded.verdictGoodToGo')
442
+ : t('grounded.verdictNeedsReview')}
443
+ </Badge>
444
+ </button>
445
+ )}
446
+ {typeof extraction.hardness?.score === 'number' && (
447
+ <Badge
448
+ variant={
449
+ extraction.hardness.score >= 0.5
450
+ ? 'attention'
451
+ : extraction.hardness.score >= 0.2
452
+ ? 'info'
453
+ : 'secondary'
454
+ }
455
+ title={t('grounded.hardnessHint')}
456
+ >
457
+ {t('grounded.hardness', {
458
+ percent: Math.round(extraction.hardness.score * 100),
459
+ })}
460
+ {extraction.hardness.escalated ? ' ↑' : ''}
461
+ </Badge>
462
+ )}
463
+ {extraction.citations.length > 0 &&
464
+ (() => {
465
+ // Real verification: fraction of values actually confirmed —
466
+ // digitally (verbatim/OCR match) OR by the reviewer against the
467
+ // image. NOT mean confidence, which scores image-read values high
468
+ // even when nothing was verified (e.g. 98% "confidence" on 2/64).
469
+ const verifiedPct = Math.round(
470
+ (100 * breakdown.totalVerified) / extraction.citations.length,
471
+ );
472
+ return (
473
+ <Badge
474
+ variant={
475
+ verifiedPct >= 95
476
+ ? 'success'
477
+ : verifiedPct >= 50
478
+ ? 'attention'
479
+ : 'destructive'
480
+ }
481
+ title={t('grounded.confidenceHint')}
482
+ >
483
+ {t('grounded.confidence', { percent: verifiedPct })}
484
+ </Badge>
485
+ );
486
+ })()}
487
+ <button
488
+ type="button"
489
+ className="flex cursor-pointer items-center gap-1"
490
+ onClick={() => setShowBreakdown((v) => !v)}
491
+ aria-expanded={showBreakdown}
492
+ aria-label={t('grounded.breakdownTitle')}
493
+ >
494
+ {breakdown.digitallyVerified > 0 && (
495
+ <Badge variant="success" title={t('grounded.digitalVerifiedHint')}>
496
+ {t('grounded.digitalVerifiedOf', {
497
+ count: breakdown.digitallyVerified,
498
+ total: totalCitations,
499
+ })}
500
+ </Badge>
501
+ )}
502
+ {breakdown.modelVerified > 0 && (
503
+ <Badge variant="success" title={t('grounded.modelVerifiedHint')}>
504
+ {t('grounded.modelVerifiedOf', {
505
+ count: breakdown.modelVerified,
506
+ total: totalCitations,
507
+ })}
508
+ </Badge>
509
+ )}
510
+ {breakdown.groups.imageRead > 0 && (
511
+ <Badge variant="attention" title={t('grounded.unverifiedHint')}>
512
+ {t('grounded.unverifiedCount', { count: breakdown.groups.imageRead })}
513
+ </Badge>
514
+ )}
515
+ </button>
516
+ <Button
517
+ variant="ghost"
518
+ size="xs"
519
+ aria-label={t('grounded.downloadCitations')}
520
+ title={t('grounded.downloadCitations')}
521
+ onClick={() => downloadArtifact('grounded-extraction.json')}
522
+ >
523
+ <FileJson2 className="size-4" />
524
+ </Button>
525
+ <Button
526
+ variant="ghost"
527
+ size="xs"
528
+ aria-label={t('grounded.downloadBlocks')}
529
+ title={t('grounded.downloadBlocks')}
530
+ onClick={() => downloadArtifact(`pages/page-${page}.json`)}
531
+ >
532
+ <FileText className="size-4" />
533
+ </Button>
534
+ <Button
535
+ variant="ghost"
536
+ size="xs"
537
+ aria-label={t('grounded.downloadAnnotated')}
538
+ title={t('grounded.downloadAnnotated')}
539
+ onClick={() => downloadArtifact('grounded-annotated.pdf')}
540
+ >
541
+ <FileDown className="size-4" />
542
+ </Button>
543
+ {!!onClose && (
544
+ <Button variant="ghost" size="xs" onClick={onClose} aria-label={t('pdf.close')}>
545
+ <X className="size-4" />
546
+ </Button>
547
+ )}
548
+ </div>
549
+ </div>
550
+ {showVerdict && extraction.verdict && (
551
+ <div className="shrink-0 border-b border-border bg-background px-3 py-2 text-sm">
552
+ <div className="flex items-center gap-2">
553
+ <span
554
+ className={cn(
555
+ 'font-medium',
556
+ extraction.verdict === 'good_to_go' ? 'text-success' : 'text-attention',
557
+ )}
558
+ >
559
+ {extraction.verdict === 'good_to_go'
560
+ ? t('grounded.verdictGoodToGo')
561
+ : t('grounded.verdictNeedsReview')}
562
+ </span>
563
+ <span className="text-xs text-muted-foreground">
564
+ {extraction.review
565
+ ? t('grounded.verdictByReviewer')
566
+ : t('grounded.verdictByConfidence')}
567
+ </span>
568
+ </div>
569
+ {(extraction.verdict_reason ?? extraction.review?.verdict_reason) && (
570
+ <p className="mt-1 text-xs">
571
+ {extraction.verdict_reason ?? extraction.review?.verdict_reason}
572
+ </p>
573
+ )}
574
+ {extraction.review?.summary && (
575
+ <p className="mt-2 text-xs text-muted-foreground">
576
+ <span className="font-medium text-foreground">{t('grounded.reviewSummary')}: </span>
577
+ {extraction.review.summary}
578
+ </p>
579
+ )}
580
+ {typeof extraction.review?.corrections_applied === 'number' &&
581
+ extraction.review.corrections_applied > 0 && (
582
+ <p className="mt-1 text-xs text-info">
583
+ {t('grounded.reviewCorrections', {
584
+ count: extraction.review.corrections_applied,
585
+ })}
586
+ </p>
587
+ )}
588
+ </div>
589
+ )}
590
+ {showBreakdown && (
591
+ <div className="shrink-0 border-b border-border bg-background px-3 py-2 text-sm max-h-72 overflow-auto">
592
+ <div className="font-medium mb-1">{t('grounded.breakdownTitle')}</div>
593
+ <div className="grid grid-cols-2 gap-x-4 text-xs">
594
+ <span className="col-span-2 mt-1 font-medium text-success">
595
+ {t('grounded.digitalVerifiedOf', {
596
+ count: breakdown.digitallyVerified,
597
+ total: totalCitations,
598
+ })}
599
+ </span>
600
+ <span className="ps-2 text-muted-foreground">{t('grounded.breakdownDigital')}</span>
601
+ <span className="text-end text-success">{breakdown.groups.digital}</span>
602
+ <span className="ps-2 text-muted-foreground">{t('grounded.breakdownOcr')}</span>
603
+ <span className="text-end text-success">{breakdown.groups.ocr}</span>
604
+ <span className="ps-2 text-muted-foreground">{t('grounded.breakdownSnapped')}</span>
605
+ <span className="text-end text-success">{breakdown.groups.snapped}</span>
606
+ <span className="col-span-2 mt-2 font-medium text-success">
607
+ {t('grounded.modelVerifiedOf', {
608
+ count: breakdown.modelVerified,
609
+ total: totalCitations,
610
+ })}
611
+ </span>
612
+ <span className="ps-2 text-muted-foreground">{t('grounded.breakdownReviewer')}</span>
613
+ <span className="text-end text-success">{breakdown.groups.reviewerConfirmed}</span>
614
+ {breakdown.groups.imageRead > 0 && (
615
+ <>
616
+ <span className="col-span-2 mt-2 font-medium text-attention">
617
+ {t('grounded.unverifiedCount', { count: breakdown.groups.imageRead })}
618
+ </span>
619
+ <span className="ps-2 text-muted-foreground">
620
+ {t('grounded.breakdownImageRead')}
621
+ </span>
622
+ <span className="text-end text-attention">{breakdown.groups.imageRead}</span>
623
+ </>
624
+ )}
625
+ </div>
626
+ <p className="mt-2 text-xs text-muted-foreground">{t('grounded.breakdownVerificationHelp')}</p>
627
+ {breakdown.unverified.length > 0 && (
628
+ <div className="mt-2">
629
+ <div className="text-xs font-medium text-muted-foreground mb-1">
630
+ {t('grounded.breakdownUnverifiedList')}
631
+ </div>
632
+ <ul className="space-y-0.5">
633
+ {breakdown.unverified.map((c) => (
634
+ <li key={c.path}>
635
+ <button
636
+ type="button"
637
+ className="w-full text-start text-xs rounded px-1 py-0.5 hover:bg-muted cursor-pointer"
638
+ onClick={() => {
639
+ selectPath(c.path);
640
+ setShowBreakdown(false);
641
+ }}
642
+ >
643
+ <span className="text-muted-foreground">{c.path}</span>{' '}
644
+ <span>{String(c.value ?? '')}</span>
645
+ </button>
646
+ </li>
647
+ ))}
648
+ </ul>
649
+ </div>
650
+ )}
651
+ </div>
652
+ )}
653
+ {/* Extracted data */}
654
+ <div className="flex-1 overflow-auto p-3">
655
+ <DataNode
656
+ value={extraction.data}
657
+ path=""
658
+ citationsByPath={citationsByPath}
659
+ selectedPath={selectedPath}
660
+ onSelect={selectPath}
661
+ />
662
+ {extraction.conflicts && extraction.conflicts.length > 0 && (
663
+ <div className="mt-4 text-xs text-attention">
664
+ {t('grounded.conflicts', { count: extraction.conflicts.length })}
665
+ </div>
666
+ )}
667
+ </div>
668
+ </ResizablePanel>
669
+ {assistantOpen && (
670
+ <>
671
+ <ResizableHandle className="w-[4px] bg-border cursor-ew-resize" />
672
+ <ResizablePanel defaultSize={30} minSize={20} className="flex flex-col border-s border-border">
673
+ <GroundedExtractionAssistantPanel
674
+ objectId={objectId}
675
+ onExtractionChanged={onExtractionChanged}
676
+ selectedField={selectedPath}
677
+ onClose={() => setAssistantOpen(false)}
678
+ />
679
+ </ResizablePanel>
680
+ </>
681
+ )}
682
+ </ResizablePanelGroup>
683
+ );
684
+ }
685
+
686
+ function PageWithOverlay({
687
+ objectId,
688
+ page,
689
+ dims,
690
+ citations,
691
+ selectedPath,
692
+ onSelectPath,
693
+ imageMode,
694
+ }: {
695
+ objectId: string;
696
+ page: number;
697
+ dims?: { width: number; height: number; rotation?: number };
698
+ citations: GroundedCitation[];
699
+ selectedPath?: string;
700
+ onSelectPath: (path: string) => void;
701
+ imageMode: PageImageMode;
702
+ }) {
703
+ const { t } = useUITranslation();
704
+ const { client } = useUserSession();
705
+ const [imageUrl, setImageUrl] = useState<string | undefined>(undefined);
706
+
707
+ useEffect(() => {
708
+ let cancelled = false;
709
+ setImageUrl(undefined);
710
+ client.files
711
+ .getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/pages/page-${page}.${imageMode}.jpg`)
712
+ .then((r) => {
713
+ if (!cancelled) setImageUrl(r.url);
714
+ })
715
+ .catch((err: unknown) => console.warn('Failed to load page image', err));
716
+ return () => {
717
+ cancelled = true;
718
+ };
719
+ }, [client, imageMode, objectId, page]);
720
+
721
+ if (!imageUrl || !dims) {
722
+ return (
723
+ <div className="flex items-center justify-center h-64">
724
+ <Spinner />
725
+ </div>
726
+ );
727
+ }
728
+
729
+ // Citation boxes live in the page's (possibly OCR-rotated) coordinate space,
730
+ // which `dims` describes. They overlay directly on a container that is that
731
+ // same space — so when the page is rotated we rotate the image to fill that
732
+ // space rather than transforming every box.
733
+ const boxOverlays = citations.flatMap((citation) =>
734
+ citation.boxes.map((box, i) => {
735
+ const isSelected =
736
+ selectedPath !== undefined &&
737
+ normalizeCitationPath(selectedPath) === normalizeCitationPath(citation.path);
738
+ const cellRange = citation.cells
739
+ ? citation.cells.start === citation.cells.end
740
+ ? citation.cells.start
741
+ : `${citation.cells.start}:${citation.cells.end}`
742
+ : undefined;
743
+ return (
744
+ <button
745
+ key={`${citation.path}-${i}`}
746
+ type="button"
747
+ aria-label={citation.path}
748
+ title={`${citation.path}${cellRange ? `\n${cellRange}` : ''}${citation.source_text ? `\n${citation.source_text}` : ''}`}
749
+ onClick={() => onSelectPath(normalizeCitationPath(citation.path))}
750
+ className={cn(
751
+ 'absolute cursor-pointer border rounded-[1px] transition-colors',
752
+ citation.misaligned && 'border-dashed',
753
+ isSelected
754
+ ? 'border-2 border-destructive bg-destructive/20 z-10'
755
+ : selectedPath
756
+ ? 'border-info/30 hover:border-info'
757
+ : citation.verified
758
+ ? 'border-success/70 hover:bg-success/20'
759
+ : citation.reviewed
760
+ ? 'border-info/70 hover:bg-info/20'
761
+ : 'border-attention/80 hover:bg-attention/20',
762
+ )}
763
+ style={{
764
+ // inline position: the app's button base styles override the
765
+ // `absolute` utility class with position: relative
766
+ position: 'absolute',
767
+ left: `${(box.x / dims.width) * 100}%`,
768
+ top: `${(box.y / dims.height) * 100}%`,
769
+ width: `${(box.w / dims.width) * 100}%`,
770
+ height: `${(box.h / dims.height) * 100}%`,
771
+ }}
772
+ />
773
+ );
774
+ }),
775
+ );
776
+
777
+ // The stored page image is in the source orientation; the OCR pass may have
778
+ // rotated the page to upright to read it, so `rotation` (CW degrees) is how
779
+ // much the image must turn to match the citation coordinates. A 90/270 turn
780
+ // also transposes the image box, so it is sized to the container's transpose.
781
+ const rot = (((dims.rotation ?? 0) % 360) + 360) % 360;
782
+ if (rot !== 0) {
783
+ const transpose = rot === 90 || rot === 270;
784
+ return (
785
+ <div className="relative w-full" style={{ aspectRatio: `${dims.width} / ${dims.height}` }}>
786
+ <img
787
+ src={imageUrl}
788
+ alt={t('grounded.pageAlt', { page })}
789
+ className="absolute select-none"
790
+ style={{
791
+ left: '50%',
792
+ top: '50%',
793
+ width: `${(transpose ? dims.height / dims.width : 1) * 100}%`,
794
+ height: `${(transpose ? dims.width / dims.height : 1) * 100}%`,
795
+ maxWidth: 'none',
796
+ transform: `translate(-50%, -50%) rotate(${rot}deg)`,
797
+ transformOrigin: 'center center',
798
+ }}
799
+ />
800
+ {boxOverlays}
801
+ </div>
802
+ );
803
+ }
804
+
805
+ return (
806
+ <div className="relative w-full">
807
+ <img src={imageUrl} alt={t('grounded.pageAlt', { page })} className="w-full select-none" />
808
+ {boxOverlays}
809
+ </div>
810
+ );
811
+ }
812
+
813
+ function DataNode({
814
+ value,
815
+ path,
816
+ citationsByPath,
817
+ selectedPath,
818
+ onSelect,
819
+ }: {
820
+ value: unknown;
821
+ path: string;
822
+ citationsByPath: Map<string, GroundedCitation[]>;
823
+ selectedPath?: string;
824
+ onSelect: (path: string) => void;
825
+ }) {
826
+ if (Array.isArray(value)) {
827
+ if (value.length > 0 && value.every((item) => item !== null && typeof item === 'object')) {
828
+ return (
829
+ <ArrayTable
830
+ items={value as Record<string, unknown>[]}
831
+ path={path}
832
+ citationsByPath={citationsByPath}
833
+ selectedPath={selectedPath}
834
+ onSelect={onSelect}
835
+ />
836
+ );
837
+ }
838
+ return (
839
+ <div className="space-y-1">
840
+ {value.map((item, i) => (
841
+ <DataNode
842
+ key={`${path}[${i}]`}
843
+ value={item}
844
+ path={`${path}[${i}]`}
845
+ citationsByPath={citationsByPath}
846
+ selectedPath={selectedPath}
847
+ onSelect={onSelect}
848
+ />
849
+ ))}
850
+ </div>
851
+ );
852
+ }
853
+ if (value !== null && typeof value === 'object') {
854
+ return (
855
+ <div className={path ? 'ps-3 border-s border-border space-y-0.5' : 'space-y-0.5'}>
856
+ {Object.entries(value as Record<string, unknown>).map(([key, child]) => {
857
+ const childPath = path ? `${path}.${key}` : key;
858
+ const isLeaf = child === null || typeof child !== 'object';
859
+ if (isLeaf) {
860
+ return (
861
+ <LeafRow
862
+ key={childPath}
863
+ label={key}
864
+ value={child}
865
+ path={childPath}
866
+ citationsByPath={citationsByPath}
867
+ selectedPath={selectedPath}
868
+ onSelect={onSelect}
869
+ />
870
+ );
871
+ }
872
+ // Arrays of objects render as a table with its own labeled header
873
+ const rendersOwnLabel =
874
+ Array.isArray(child) &&
875
+ child.length > 0 &&
876
+ child.every((item) => item !== null && typeof item === 'object');
877
+ return (
878
+ <div key={childPath} className="pt-1">
879
+ {!rendersOwnLabel && (
880
+ <div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
881
+ {key}
882
+ </div>
883
+ )}
884
+ <DataNode
885
+ value={child}
886
+ path={childPath}
887
+ citationsByPath={citationsByPath}
888
+ selectedPath={selectedPath}
889
+ onSelect={onSelect}
890
+ />
891
+ </div>
892
+ );
893
+ })}
894
+ </div>
895
+ );
896
+ }
897
+ return null;
898
+ }
899
+
900
+ function ArrayTable({
901
+ items,
902
+ path,
903
+ citationsByPath,
904
+ selectedPath,
905
+ onSelect,
906
+ }: {
907
+ items: Record<string, unknown>[];
908
+ path: string;
909
+ citationsByPath: Map<string, GroundedCitation[]>;
910
+ selectedPath?: string;
911
+ onSelect: (path: string) => void;
912
+ }) {
913
+ const { t } = useUITranslation();
914
+ const columns = useMemo(() => {
915
+ const keys = new Set<string>();
916
+ for (const item of items) {
917
+ for (const key of Object.keys(item)) keys.add(key);
918
+ }
919
+ return Array.from(keys);
920
+ }, [items]);
921
+
922
+ return (
923
+ <div className="pt-1">
924
+ <div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
925
+ {path.split('.').pop()} ({items.length})
926
+ </div>
927
+ <div className="overflow-x-auto">
928
+ <table className="w-full text-xs">
929
+ <thead>
930
+ <tr className="text-start text-muted-foreground">
931
+ {columns.map((col) => (
932
+ <th key={col} scope="col" className="py-1 pe-2 font-medium">
933
+ {col}
934
+ </th>
935
+ ))}
936
+ </tr>
937
+ </thead>
938
+ <tbody>
939
+ {items.map((item, i) => (
940
+ <tr key={`${path}[${i}]`} className="border-t border-border/50">
941
+ {columns.map((col) => {
942
+ const cellPath = `${path}[${i}].${col}`;
943
+ const citation = citationsByPath.get(cellPath)?.[0];
944
+ const isSelected = selectedPath === cellPath;
945
+ return (
946
+ <td key={col} className="py-0.5 pe-2">
947
+ <button
948
+ type="button"
949
+ onClick={() => onSelect(cellPath)}
950
+ className={cn(
951
+ 'text-start rounded px-1 w-full',
952
+ isSelected
953
+ ? 'bg-destructive/15 ring-1 ring-destructive'
954
+ : citation
955
+ ? 'hover:bg-muted cursor-pointer'
956
+ : 'text-muted-foreground cursor-default',
957
+ )}
958
+ >
959
+ {formatValue(item[col])}
960
+ {citation && typeof citation.confidence === 'number' && (
961
+ <span
962
+ className={cn(
963
+ 'ms-1 text-[10px]',
964
+ scoreColor(citation.confidence),
965
+ )}
966
+ title={t('grounded.confidenceHint')}
967
+ >
968
+ {formatScore(citation.confidence)}
969
+ </span>
970
+ )}
971
+ </button>
972
+ </td>
973
+ );
974
+ })}
975
+ </tr>
976
+ ))}
977
+ </tbody>
978
+ </table>
979
+ </div>
980
+ </div>
981
+ );
982
+ }
983
+
984
+ function LeafRow({
985
+ label,
986
+ value,
987
+ path,
988
+ citationsByPath,
989
+ selectedPath,
990
+ onSelect,
991
+ }: {
992
+ label: string;
993
+ value: unknown;
994
+ path: string;
995
+ citationsByPath: Map<string, GroundedCitation[]>;
996
+ selectedPath?: string;
997
+ onSelect: (path: string) => void;
998
+ }) {
999
+ const { t } = useUITranslation();
1000
+ const citation = citationsByPath.get(path)?.[0];
1001
+ const isSelected = selectedPath === path;
1002
+
1003
+ return (
1004
+ <button
1005
+ type="button"
1006
+ onClick={() => citation && onSelect(path)}
1007
+ className={cn(
1008
+ 'flex items-center gap-2 w-full text-start text-sm rounded px-1.5 py-0.5',
1009
+ isSelected ? 'bg-destructive/15 ring-1 ring-destructive' : citation ? 'hover:bg-muted' : '',
1010
+ !citation && 'cursor-default',
1011
+ )}
1012
+ >
1013
+ <span className="text-muted-foreground min-w-28 shrink-0">{label}</span>
1014
+ <span className="flex-1 wrap-break-word">{formatValue(value)}</span>
1015
+ {citation && typeof citation.confidence === 'number' && (
1016
+ <span
1017
+ className={cn('text-xs shrink-0', scoreColor(citation.confidence))}
1018
+ title={t('grounded.confidenceHint')}
1019
+ >
1020
+ {formatScore(citation.confidence)}
1021
+ </span>
1022
+ )}
1023
+ {citation &&
1024
+ (citation.verified ? (
1025
+ <CheckCircle2 aria-label={t('grounded.verified')} className="size-3.5 shrink-0 text-success" />
1026
+ ) : citation.reviewed ? (
1027
+ <Sparkles aria-label={t('grounded.aiVerified')} className="size-3.5 shrink-0 text-info" />
1028
+ ) : (
1029
+ <Eye aria-label={t('grounded.readFromImage')} className="size-3.5 shrink-0 text-attention" />
1030
+ ))}
1031
+ </button>
1032
+ );
1033
+ }
1034
+
1035
+ /**
1036
+ * Tier scores (1.0, 0.99, 0.5...) are policy values and display as integers;
1037
+ * OCR-measured scores carry real decimals from tesseract and keep two.
1038
+ */
1039
+ function formatScore(confidence: number): string {
1040
+ const pct = confidence * 100;
1041
+ return Number.isInteger(pct) ? `${pct}%` : `${(Math.floor(pct * 100) / 100).toFixed(2)}%`;
1042
+ }
1043
+
1044
+ function scoreColor(confidence: number): string {
1045
+ if (confidence >= 0.95) return 'text-success';
1046
+ if (confidence >= 0.7) return 'text-attention';
1047
+ return 'text-destructive';
1048
+ }
1049
+
1050
+ function formatValue(value: unknown): string {
1051
+ if (value === null || value === undefined || value === '') return '—';
1052
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
1053
+ if (typeof value === 'object') return JSON.stringify(value);
1054
+ return String(value);
1055
+ }