@vertesia/ui 0.80.0-dev.20251121 → 0.80.0

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 (277) hide show
  1. package/README.md +105 -0
  2. package/lib/esm/core/components/MenuList.js +2 -5
  3. package/lib/esm/core/components/MenuList.js.map +1 -1
  4. package/lib/esm/core/components/MessageBox.js +1 -1
  5. package/lib/esm/core/components/MessageBox.js.map +1 -1
  6. package/lib/esm/core/components/shadcn/dialog.js +16 -2
  7. package/lib/esm/core/components/shadcn/dialog.js.map +1 -1
  8. package/lib/esm/core/components/shadcn/filters/filter/SelectFilter.js +6 -9
  9. package/lib/esm/core/components/shadcn/filters/filter/SelectFilter.js.map +1 -1
  10. package/lib/esm/core/components/shadcn/filters/filterBar.js +1 -1
  11. package/lib/esm/core/components/shadcn/filters/filterBar.js.map +1 -1
  12. package/lib/esm/core/components/shadcn/selectBox.js +23 -5
  13. package/lib/esm/core/components/shadcn/selectBox.js.map +1 -1
  14. package/lib/esm/core/components/shadcn/tabs.js +3 -3
  15. package/lib/esm/core/components/shadcn/tabs.js.map +1 -1
  16. package/lib/esm/env/index.js +3 -0
  17. package/lib/esm/env/index.js.map +1 -1
  18. package/lib/esm/features/agent/chat/AgentChart.js +184 -0
  19. package/lib/esm/features/agent/chat/AgentChart.js.map +1 -0
  20. package/lib/esm/features/agent/chat/ModernAgentConversation.js +87 -10
  21. package/lib/esm/features/agent/chat/ModernAgentConversation.js.map +1 -1
  22. package/lib/esm/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +6 -2
  23. package/lib/esm/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  24. package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js +4 -4
  25. package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  26. package/lib/esm/features/agent/chat/ModernAgentOutput/InlineSlidingPlanPanel.js +4 -1
  27. package/lib/esm/features/agent/chat/ModernAgentOutput/InlineSlidingPlanPanel.js.map +1 -1
  28. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageInput.js +12 -4
  29. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  30. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageItem.js +60 -56
  31. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  32. package/lib/esm/features/agent/chat/index.js +1 -0
  33. package/lib/esm/features/agent/chat/index.js.map +1 -1
  34. package/lib/esm/features/agent/createChartTool.js +354 -0
  35. package/lib/esm/features/agent/createChartTool.js.map +1 -0
  36. package/lib/esm/features/agent/examples.js +295 -0
  37. package/lib/esm/features/agent/examples.js.map +1 -0
  38. package/lib/esm/features/agent/index.js +2 -0
  39. package/lib/esm/features/agent/index.js.map +1 -1
  40. package/lib/esm/features/agent/visualization.js +165 -0
  41. package/lib/esm/features/agent/visualization.js.map +1 -0
  42. package/lib/esm/features/facets/CollectionsFacetsNav.js +5 -1
  43. package/lib/esm/features/facets/CollectionsFacetsNav.js.map +1 -1
  44. package/lib/esm/features/index.js +1 -0
  45. package/lib/esm/features/index.js.map +1 -1
  46. package/lib/esm/features/layout/GenericPageNavHeader.js +14 -4
  47. package/lib/esm/features/layout/GenericPageNavHeader.js.map +1 -1
  48. package/lib/esm/features/magic-pdf/AnnotatedImageSlider.js +268 -0
  49. package/lib/esm/features/magic-pdf/AnnotatedImageSlider.js.map +1 -0
  50. package/lib/esm/features/magic-pdf/DownloadPopover.js +11 -11
  51. package/lib/esm/features/magic-pdf/DownloadPopover.js.map +1 -1
  52. package/lib/esm/features/magic-pdf/ExtractedContentView.js +77 -0
  53. package/lib/esm/features/magic-pdf/ExtractedContentView.js.map +1 -0
  54. package/lib/esm/features/magic-pdf/MagicPdfProvider.js +242 -0
  55. package/lib/esm/features/magic-pdf/MagicPdfProvider.js.map +1 -0
  56. package/lib/esm/features/magic-pdf/MagicPdfView.js +41 -47
  57. package/lib/esm/features/magic-pdf/MagicPdfView.js.map +1 -1
  58. package/lib/esm/features/pdf-viewer/PdfPageRenderer.js +261 -0
  59. package/lib/esm/features/pdf-viewer/PdfPageRenderer.js.map +1 -0
  60. package/lib/esm/features/pdf-viewer/PdfPageSlider.js +276 -0
  61. package/lib/esm/features/pdf-viewer/PdfPageSlider.js.map +1 -0
  62. package/lib/esm/features/pdf-viewer/SimplePdfViewer.js +71 -0
  63. package/lib/esm/features/pdf-viewer/SimplePdfViewer.js.map +1 -0
  64. package/lib/esm/features/pdf-viewer/index.js +4 -0
  65. package/lib/esm/features/pdf-viewer/index.js.map +1 -0
  66. package/lib/esm/features/store/collections/EditCollectionView.js +3 -5
  67. package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
  68. package/lib/esm/features/store/collections/SharedPropsEditor.js +1 -2
  69. package/lib/esm/features/store/collections/SharedPropsEditor.js.map +1 -1
  70. package/lib/esm/features/store/objects/DocumentSearchResults.js +0 -7
  71. package/lib/esm/features/store/objects/DocumentSearchResults.js.map +1 -1
  72. package/lib/esm/features/store/objects/components/ContentOverview.js +273 -83
  73. package/lib/esm/features/store/objects/components/ContentOverview.js.map +1 -1
  74. package/lib/esm/features/store/objects/components/useContentPanelHooks.js +153 -0
  75. package/lib/esm/features/store/objects/components/useContentPanelHooks.js.map +1 -0
  76. package/lib/esm/features/store/objects/layout/DocumentTableColumn.js +3 -3
  77. package/lib/esm/features/store/objects/layout/DocumentTableColumn.js.map +1 -1
  78. package/lib/esm/features/store/objects/layout/renderers.js +13 -0
  79. package/lib/esm/features/store/objects/layout/renderers.js.map +1 -1
  80. package/lib/esm/features/utils/index.js +2 -0
  81. package/lib/esm/features/utils/index.js.map +1 -1
  82. package/lib/esm/features/utils/mimeType.js +8 -0
  83. package/lib/esm/features/utils/mimeType.js.map +1 -1
  84. package/lib/esm/features/utils/print.js +181 -0
  85. package/lib/esm/features/utils/print.js.map +1 -0
  86. package/lib/esm/features/utils/workflowStatus.js +43 -0
  87. package/lib/esm/features/utils/workflowStatus.js.map +1 -0
  88. package/lib/esm/router/HistoryNavigator.js +22 -2
  89. package/lib/esm/router/HistoryNavigator.js.map +1 -1
  90. package/lib/esm/shell/login/UserInfo.js +2 -1
  91. package/lib/esm/shell/login/UserInfo.js.map +1 -1
  92. package/lib/esm/shell/login/UserSessionMenu.js +7 -1
  93. package/lib/esm/shell/login/UserSessionMenu.js.map +1 -1
  94. package/lib/esm/widgets/form/Form.js +6 -2
  95. package/lib/esm/widgets/form/Form.js.map +1 -1
  96. package/lib/esm/widgets/markdown/MarkdownRenderer.js +226 -4
  97. package/lib/esm/widgets/markdown/MarkdownRenderer.js.map +1 -1
  98. package/lib/esm/widgets/schema-editor/ManagedSchema.js +0 -3
  99. package/lib/esm/widgets/schema-editor/ManagedSchema.js.map +1 -1
  100. package/lib/esm/widgets/schema-editor/json-schema4-utils.js +1 -1
  101. package/lib/esm/widgets/schema-editor/json-schema4-utils.js.map +1 -1
  102. package/lib/esm/widgets/xml-viewer/components/XMLViewer.js +18 -9
  103. package/lib/esm/widgets/xml-viewer/components/XMLViewer.js.map +1 -1
  104. package/lib/esm/widgets/xml-viewer/constants/index.js +10 -0
  105. package/lib/esm/widgets/xml-viewer/constants/index.js.map +1 -1
  106. package/lib/tsconfig.tsbuildinfo +1 -1
  107. package/lib/types/core/components/MessageBox.d.ts.map +1 -1
  108. package/lib/types/core/components/shadcn/dialog.d.ts +2 -1
  109. package/lib/types/core/components/shadcn/dialog.d.ts.map +1 -1
  110. package/lib/types/core/components/shadcn/filters/filterBar.d.ts.map +1 -1
  111. package/lib/types/core/components/shadcn/selectBox.d.ts +5 -1
  112. package/lib/types/core/components/shadcn/selectBox.d.ts.map +1 -1
  113. package/lib/types/core/components/shadcn/tabs.d.ts +3 -1
  114. package/lib/types/core/components/shadcn/tabs.d.ts.map +1 -1
  115. package/lib/types/env/index.d.ts +2 -0
  116. package/lib/types/env/index.d.ts.map +1 -1
  117. package/lib/types/features/agent/chat/AgentChart.d.ts +48 -0
  118. package/lib/types/features/agent/chat/AgentChart.d.ts.map +1 -0
  119. package/lib/types/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  120. package/lib/types/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -2
  121. package/lib/types/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  122. package/lib/types/features/agent/chat/ModernAgentOutput/Header.d.ts +2 -1
  123. package/lib/types/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  124. package/lib/types/features/agent/chat/ModernAgentOutput/InlineSlidingPlanPanel.d.ts +4 -2
  125. package/lib/types/features/agent/chat/ModernAgentOutput/InlineSlidingPlanPanel.d.ts.map +1 -1
  126. package/lib/types/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +2 -4
  127. package/lib/types/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  128. package/lib/types/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  129. package/lib/types/features/agent/chat/index.d.ts +1 -0
  130. package/lib/types/features/agent/chat/index.d.ts.map +1 -1
  131. package/lib/types/features/agent/createChartTool.d.ts +178 -0
  132. package/lib/types/features/agent/createChartTool.d.ts.map +1 -0
  133. package/lib/types/features/agent/examples.d.ts +59 -0
  134. package/lib/types/features/agent/examples.d.ts.map +1 -0
  135. package/lib/types/features/agent/index.d.ts +2 -0
  136. package/lib/types/features/agent/index.d.ts.map +1 -1
  137. package/lib/types/features/agent/visualization.d.ts +95 -0
  138. package/lib/types/features/agent/visualization.d.ts.map +1 -0
  139. package/lib/types/features/facets/CollectionsFacetsNav.d.ts.map +1 -1
  140. package/lib/types/features/index.d.ts +1 -0
  141. package/lib/types/features/index.d.ts.map +1 -1
  142. package/lib/types/features/layout/GenericPageNavHeader.d.ts.map +1 -1
  143. package/lib/types/features/magic-pdf/AnnotatedImageSlider.d.ts +13 -0
  144. package/lib/types/features/magic-pdf/AnnotatedImageSlider.d.ts.map +1 -0
  145. package/lib/types/features/magic-pdf/DownloadPopover.d.ts.map +1 -1
  146. package/lib/types/features/magic-pdf/ExtractedContentView.d.ts +8 -0
  147. package/lib/types/features/magic-pdf/ExtractedContentView.d.ts.map +1 -0
  148. package/lib/types/features/magic-pdf/MagicPdfProvider.d.ts +58 -0
  149. package/lib/types/features/magic-pdf/MagicPdfProvider.d.ts.map +1 -0
  150. package/lib/types/features/magic-pdf/MagicPdfView.d.ts +1 -1
  151. package/lib/types/features/magic-pdf/MagicPdfView.d.ts.map +1 -1
  152. package/lib/types/features/pdf-viewer/PdfPageRenderer.d.ts +83 -0
  153. package/lib/types/features/pdf-viewer/PdfPageRenderer.d.ts.map +1 -0
  154. package/lib/types/features/pdf-viewer/PdfPageSlider.d.ts +29 -0
  155. package/lib/types/features/pdf-viewer/PdfPageSlider.d.ts.map +1 -0
  156. package/lib/types/features/pdf-viewer/SimplePdfViewer.d.ts +19 -0
  157. package/lib/types/features/pdf-viewer/SimplePdfViewer.d.ts.map +1 -0
  158. package/lib/types/features/pdf-viewer/index.d.ts +4 -0
  159. package/lib/types/features/pdf-viewer/index.d.ts.map +1 -0
  160. package/lib/types/features/store/collections/EditCollectionView.d.ts.map +1 -1
  161. package/lib/types/features/store/collections/SharedPropsEditor.d.ts.map +1 -1
  162. package/lib/types/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
  163. package/lib/types/features/store/objects/components/ContentOverview.d.ts.map +1 -1
  164. package/lib/types/features/store/objects/components/useContentPanelHooks.d.ts +30 -0
  165. package/lib/types/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -0
  166. package/lib/types/features/store/objects/layout/renderers.d.ts.map +1 -1
  167. package/lib/types/features/utils/index.d.ts +2 -0
  168. package/lib/types/features/utils/index.d.ts.map +1 -1
  169. package/lib/types/features/utils/mimeType.d.ts +1 -0
  170. package/lib/types/features/utils/mimeType.d.ts.map +1 -1
  171. package/lib/types/features/utils/print.d.ts +10 -0
  172. package/lib/types/features/utils/print.d.ts.map +1 -0
  173. package/lib/types/features/utils/workflowStatus.d.ts +10 -0
  174. package/lib/types/features/utils/workflowStatus.d.ts.map +1 -0
  175. package/lib/types/router/HistoryNavigator.d.ts +3 -0
  176. package/lib/types/router/HistoryNavigator.d.ts.map +1 -1
  177. package/lib/types/shell/login/UserInfo.d.ts.map +1 -1
  178. package/lib/types/shell/login/UserSessionMenu.d.ts.map +1 -1
  179. package/lib/types/widgets/form/Form.d.ts.map +1 -1
  180. package/lib/types/widgets/markdown/MarkdownRenderer.d.ts +5 -1
  181. package/lib/types/widgets/markdown/MarkdownRenderer.d.ts.map +1 -1
  182. package/lib/types/widgets/schema-editor/ManagedSchema.d.ts.map +1 -1
  183. package/lib/types/widgets/xml-viewer/components/XMLViewer.d.ts.map +1 -1
  184. package/lib/types/widgets/xml-viewer/constants/index.d.ts +10 -0
  185. package/lib/types/widgets/xml-viewer/constants/index.d.ts.map +1 -1
  186. package/lib/vertesia-ui-core.js +1 -1
  187. package/lib/vertesia-ui-core.js.map +1 -1
  188. package/lib/vertesia-ui-env.js +1 -1
  189. package/lib/vertesia-ui-env.js.map +1 -1
  190. package/lib/vertesia-ui-features.js +1 -1
  191. package/lib/vertesia-ui-features.js.map +1 -1
  192. package/lib/vertesia-ui-layout.js +1 -1
  193. package/lib/vertesia-ui-layout.js.map +1 -1
  194. package/lib/vertesia-ui-router.js +1 -1
  195. package/lib/vertesia-ui-router.js.map +1 -1
  196. package/lib/vertesia-ui-session.js +1 -1
  197. package/lib/vertesia-ui-session.js.map +1 -1
  198. package/lib/vertesia-ui-shell.js +1 -1
  199. package/lib/vertesia-ui-shell.js.map +1 -1
  200. package/lib/vertesia-ui-widgets.js +1 -1
  201. package/lib/vertesia-ui-widgets.js.map +1 -1
  202. package/package.json +11 -8
  203. package/src/core/components/MenuList.tsx +3 -6
  204. package/src/core/components/MessageBox.tsx +7 -2
  205. package/src/core/components/SelectBox.tsx +1 -1
  206. package/src/core/components/shadcn/dialog.tsx +19 -1
  207. package/src/core/components/shadcn/filters/filter/SelectFilter.tsx +31 -31
  208. package/src/core/components/shadcn/filters/filterBar.tsx +1 -0
  209. package/src/core/components/shadcn/selectBox.tsx +32 -6
  210. package/src/core/components/shadcn/tabs.tsx +3 -2
  211. package/src/env/index.ts +5 -0
  212. package/src/features/agent/CHART_INSTRUCTIONS.md +228 -0
  213. package/src/features/agent/chat/AgentChart.tsx +601 -0
  214. package/src/features/agent/chat/ModernAgentConversation.tsx +123 -11
  215. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +8 -2
  216. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +12 -2
  217. package/src/features/agent/chat/ModernAgentOutput/InlineSlidingPlanPanel.tsx +6 -1
  218. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +15 -10
  219. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +122 -87
  220. package/src/features/agent/chat/index.ts +1 -0
  221. package/src/features/agent/createChartTool.ts +364 -0
  222. package/src/features/agent/examples.ts +321 -0
  223. package/src/features/agent/index.ts +2 -0
  224. package/src/features/agent/visualization.ts +227 -0
  225. package/src/features/facets/CollectionsFacetsNav.tsx +5 -1
  226. package/src/features/index.ts +1 -0
  227. package/src/features/layout/GenericPageNavHeader.tsx +15 -4
  228. package/src/features/magic-pdf/AnnotatedImageSlider.tsx +482 -0
  229. package/src/features/magic-pdf/DownloadPopover.tsx +45 -40
  230. package/src/features/magic-pdf/ExtractedContentView.tsx +132 -0
  231. package/src/features/magic-pdf/MagicPdfProvider.tsx +297 -0
  232. package/src/features/magic-pdf/MagicPdfView.tsx +184 -91
  233. package/src/features/pdf-viewer/PdfPageRenderer.tsx +612 -0
  234. package/src/features/pdf-viewer/PdfPageSlider.tsx +473 -0
  235. package/src/features/pdf-viewer/SimplePdfViewer.tsx +142 -0
  236. package/src/features/pdf-viewer/index.ts +3 -0
  237. package/src/features/store/collections/EditCollectionView.tsx +3 -5
  238. package/src/features/store/collections/SharedPropsEditor.tsx +1 -2
  239. package/src/features/store/objects/DocumentSearchResults.tsx +0 -7
  240. package/src/features/store/objects/components/ContentOverview.tsx +677 -210
  241. package/src/features/store/objects/components/useContentPanelHooks.ts +169 -0
  242. package/src/features/store/objects/layout/DocumentTableColumn.tsx +3 -3
  243. package/src/features/store/objects/layout/knowledge.md +1 -0
  244. package/src/features/store/objects/layout/renderers.tsx +25 -0
  245. package/src/features/utils/index.ts +3 -1
  246. package/src/features/utils/mimeType.ts +10 -1
  247. package/src/features/utils/print.ts +189 -0
  248. package/src/features/utils/workflowStatus.ts +44 -0
  249. package/src/router/HistoryNavigator.ts +30 -2
  250. package/src/shell/login/UserInfo.tsx +2 -0
  251. package/src/shell/login/UserSessionMenu.tsx +12 -1
  252. package/src/widgets/form/Form.tsx +8 -3
  253. package/src/widgets/markdown/MarkdownRenderer.tsx +350 -6
  254. package/src/widgets/schema-editor/ManagedSchema.ts +0 -3
  255. package/src/widgets/schema-editor/json-schema4-utils.ts +1 -1
  256. package/src/widgets/xml-viewer/components/XMLViewer.tsx +22 -10
  257. package/src/widgets/xml-viewer/constants/index.ts +11 -0
  258. package/lib/esm/features/magic-pdf/PageSlider.js +0 -70
  259. package/lib/esm/features/magic-pdf/PageSlider.js.map +0 -1
  260. package/lib/esm/features/magic-pdf/PdfPageProvider.js +0 -188
  261. package/lib/esm/features/magic-pdf/PdfPageProvider.js.map +0 -1
  262. package/lib/esm/features/magic-pdf/TextPageView.js +0 -62
  263. package/lib/esm/features/magic-pdf/TextPageView.js.map +0 -1
  264. package/lib/esm/features/magic-pdf/useResizeOnDrag.js +0 -34
  265. package/lib/esm/features/magic-pdf/useResizeOnDrag.js.map +0 -1
  266. package/lib/types/features/magic-pdf/PageSlider.d.ts +0 -9
  267. package/lib/types/features/magic-pdf/PageSlider.d.ts.map +0 -1
  268. package/lib/types/features/magic-pdf/PdfPageProvider.d.ts +0 -39
  269. package/lib/types/features/magic-pdf/PdfPageProvider.d.ts.map +0 -1
  270. package/lib/types/features/magic-pdf/TextPageView.d.ts +0 -8
  271. package/lib/types/features/magic-pdf/TextPageView.d.ts.map +0 -1
  272. package/lib/types/features/magic-pdf/useResizeOnDrag.d.ts +0 -9
  273. package/lib/types/features/magic-pdf/useResizeOnDrag.d.ts.map +0 -1
  274. package/src/features/magic-pdf/PageSlider.tsx +0 -142
  275. package/src/features/magic-pdf/PdfPageProvider.tsx +0 -310
  276. package/src/features/magic-pdf/TextPageView.tsx +0 -91
  277. package/src/features/magic-pdf/useResizeOnDrag.ts +0 -42
@@ -0,0 +1,482 @@
1
+ import { Button, Center, VTooltip } from "@vertesia/ui/core";
2
+ import clsx from "clsx";
3
+ import { ChevronsDown, ChevronsUp, Image, Loader2, Maximize, Minus, Plus, ScanSearch } from "lucide-react";
4
+ import { useRef, KeyboardEvent, useState, useEffect, useCallback } from "react";
5
+ import { ImageType, useMagicPdfContext } from "./MagicPdfProvider";
6
+
7
+ // Zoom levels as percentages (100 = fit to width)
8
+ const ZOOM_LEVELS = [50, 75, 100, 125, 150, 200, 300];
9
+ const DEFAULT_ZOOM = 100;
10
+
11
+ // Default aspect ratio (letter size) used before first image loads
12
+ const DEFAULT_ASPECT_RATIO = 11 / 8.5; // height / width
13
+
14
+ // Generate page order radiating outward from current page
15
+ // e.g., if current=5 and total=10: [5, 6, 4, 7, 3, 8, 2, 9, 1, 10]
16
+ function getPageLoadOrder(currentPage: number, totalPages: number): number[] {
17
+ const order: number[] = [currentPage];
18
+ let offset = 1;
19
+
20
+ while (order.length < totalPages) {
21
+ const next = currentPage + offset;
22
+ const prev = currentPage - offset;
23
+
24
+ if (next <= totalPages) order.push(next);
25
+ if (prev >= 1) order.push(prev);
26
+
27
+ offset++;
28
+ }
29
+
30
+ return order;
31
+ }
32
+
33
+ interface AnnotatedImageSliderProps {
34
+ currentPage: number;
35
+ onChange: (pageNumber: number) => void;
36
+ className?: string;
37
+ }
38
+
39
+ /**
40
+ * Image-based page slider that displays annotated/instrumented page images.
41
+ * Progressively loads images starting from current page and radiating outward.
42
+ * Loads first image immediately to determine aspect ratio for stable layout.
43
+ */
44
+ export function AnnotatedImageSlider({ className, currentPage, onChange }: AnnotatedImageSliderProps) {
45
+ const [imageType, setImageType] = useState<ImageType>(ImageType.instrumented);
46
+ const [aspectRatio, setAspectRatio] = useState<number>(DEFAULT_ASPECT_RATIO);
47
+ const [loadedUrls, setLoadedUrls] = useState<Map<number, string>>(new Map());
48
+ const [zoom, setZoom] = useState<number>(DEFAULT_ZOOM);
49
+ const loadedPagesRef = useRef<Set<number>>(new Set());
50
+ const ref = useRef<HTMLDivElement>(null);
51
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
52
+ const isProgrammaticScrollRef = useRef(false);
53
+ const { imageProvider, count } = useMagicPdfContext();
54
+
55
+ const zoomIn = useCallback(() => {
56
+ let currentIndex = ZOOM_LEVELS.findIndex(level => level >= zoom);
57
+ if (currentIndex === -1) {
58
+ currentIndex = ZOOM_LEVELS.length - 1;
59
+ }
60
+ const nextIndex = Math.min(currentIndex + 1, ZOOM_LEVELS.length - 1);
61
+ setZoom(ZOOM_LEVELS[nextIndex]);
62
+ }, [zoom]);
63
+
64
+ const zoomOut = useCallback(() => {
65
+ let currentIndex = ZOOM_LEVELS.findIndex(level => level >= zoom);
66
+ if (currentIndex === -1) {
67
+ currentIndex = ZOOM_LEVELS.length - 1;
68
+ }
69
+ const prevIndex = Math.max(currentIndex - 1, 0);
70
+ setZoom(ZOOM_LEVELS[prevIndex]);
71
+ }, [zoom]);
72
+
73
+ const fitToView = useCallback(() => {
74
+ setZoom(DEFAULT_ZOOM);
75
+ }, []);
76
+
77
+ // Load first image to determine aspect ratio
78
+ useEffect(() => {
79
+ imageProvider.getPageImageUrl(1, imageType)
80
+ .then((url) => {
81
+ const img = new window.Image();
82
+ img.onload = () => {
83
+ if (img.width > 0 && img.height > 0) {
84
+ setAspectRatio(img.height / img.width);
85
+ }
86
+ };
87
+ img.src = url;
88
+ })
89
+ .catch(() => {
90
+ // Keep default aspect ratio on error
91
+ });
92
+ }, [imageProvider, imageType]);
93
+
94
+ // Track the current imageType for change detection
95
+ const prevImageTypeRef = useRef(imageType);
96
+
97
+ // Progressive loading: load pages in parallel, prioritized from current page outward
98
+ useEffect(() => {
99
+ let cancelled = false;
100
+
101
+ // Check if imageType changed - if so, reset and reload all pages
102
+ const imageTypeChanged = prevImageTypeRef.current !== imageType;
103
+ if (imageTypeChanged) {
104
+ prevImageTypeRef.current = imageType;
105
+ loadedPagesRef.current = new Set();
106
+ setLoadedUrls(new Map());
107
+ }
108
+
109
+ const loadOrder = getPageLoadOrder(currentPage, count);
110
+
111
+ // Load all pages in parallel, but they're already prioritized by loadOrder
112
+ // The imageProvider handles deduplication via its pending map
113
+ const loadPage = async (page: number) => {
114
+ if (cancelled || loadedPagesRef.current.has(page)) return;
115
+
116
+ try {
117
+ const url = await imageProvider.getPageImageUrl(page, imageType);
118
+ if (!cancelled) {
119
+ loadedPagesRef.current.add(page);
120
+ setLoadedUrls(prev => new Map(prev).set(page, url));
121
+ }
122
+ } catch {
123
+ // Skip failed pages
124
+ }
125
+ };
126
+
127
+ // Start all loads in parallel - prioritized pages will update state first
128
+ // since they're fetched first in the loadOrder
129
+ loadOrder.forEach(page => loadPage(page));
130
+
131
+ return () => {
132
+ cancelled = true;
133
+ };
134
+ }, [currentPage, count, imageType, imageProvider]);
135
+
136
+ // Scroll to current page when zoom changes to preserve position
137
+ const prevZoomRef = useRef(zoom);
138
+ useEffect(() => {
139
+ if (prevZoomRef.current !== zoom && scrollContainerRef.current) {
140
+ prevZoomRef.current = zoom;
141
+
142
+ // Mark as programmatic scroll to avoid triggering onChange
143
+ isProgrammaticScrollRef.current = true;
144
+
145
+ const thumbnail = scrollContainerRef.current.querySelector(`[data-page="${currentPage}"]`);
146
+ if (thumbnail) {
147
+ // Use requestAnimationFrame to wait for DOM to update with new sizes
148
+ requestAnimationFrame(() => {
149
+ thumbnail.scrollIntoView({
150
+ behavior: 'instant',
151
+ block: 'center',
152
+ });
153
+ // Reset after scroll completes
154
+ requestAnimationFrame(() => {
155
+ isProgrammaticScrollRef.current = false;
156
+ });
157
+ });
158
+ } else {
159
+ isProgrammaticScrollRef.current = false;
160
+ }
161
+ }
162
+ }, [zoom, currentPage]);
163
+
164
+ // Jump to current page when it changes (user navigation via buttons/input)
165
+ const prevPageRef = useRef(currentPage);
166
+ useEffect(() => {
167
+ if (prevPageRef.current !== currentPage && scrollContainerRef.current) {
168
+ prevPageRef.current = currentPage;
169
+
170
+ // Mark as programmatic scroll to avoid triggering onChange
171
+ isProgrammaticScrollRef.current = true;
172
+
173
+ const thumbnail = scrollContainerRef.current.querySelector(`[data-page="${currentPage}"]`);
174
+ if (thumbnail) {
175
+ thumbnail.scrollIntoView({
176
+ behavior: 'instant',
177
+ block: 'nearest',
178
+ });
179
+ }
180
+
181
+ // Reset after a short delay to allow scroll event to fire
182
+ requestAnimationFrame(() => {
183
+ isProgrammaticScrollRef.current = false;
184
+ });
185
+ }
186
+ }, [currentPage]);
187
+
188
+ // Update current page based on scroll position (when user scrolls manually)
189
+ useEffect(() => {
190
+ const container = scrollContainerRef.current;
191
+ if (!container) return;
192
+
193
+ let scrollDebounceTimer: ReturnType<typeof setTimeout> | null = null;
194
+
195
+ const handleScroll = () => {
196
+ // Skip if this is a programmatic scroll
197
+ if (isProgrammaticScrollRef.current) return;
198
+
199
+ // Debounce scroll updates
200
+ if (scrollDebounceTimer) clearTimeout(scrollDebounceTimer);
201
+ scrollDebounceTimer = setTimeout(() => {
202
+ // Find the page element closest to the center of the viewport
203
+ const containerRect = container.getBoundingClientRect();
204
+ const containerCenter = containerRect.top + containerRect.height / 2;
205
+
206
+ let closestPage = currentPage;
207
+ let closestDistance = Infinity;
208
+
209
+ for (let i = 1; i <= count; i++) {
210
+ const pageEl = container.querySelector(`[data-page="${i}"]`);
211
+ if (pageEl) {
212
+ const pageRect = pageEl.getBoundingClientRect();
213
+ const pageCenter = pageRect.top + pageRect.height / 2;
214
+ const distance = Math.abs(pageCenter - containerCenter);
215
+
216
+ if (distance < closestDistance) {
217
+ closestDistance = distance;
218
+ closestPage = i;
219
+ }
220
+ }
221
+ }
222
+
223
+ if (closestPage !== currentPage) {
224
+ prevPageRef.current = closestPage;
225
+ onChange(closestPage);
226
+ }
227
+ }, 50);
228
+ };
229
+
230
+ container.addEventListener('scroll', handleScroll, { passive: true });
231
+
232
+ return () => {
233
+ if (scrollDebounceTimer) clearTimeout(scrollDebounceTimer);
234
+ container.removeEventListener('scroll', handleScroll);
235
+ };
236
+ }, [count, currentPage, onChange]);
237
+
238
+ const goPrev = () => {
239
+ if (currentPage > 1) {
240
+ onChange(currentPage - 1);
241
+ }
242
+ };
243
+
244
+ const goNext = () => {
245
+ if (currentPage < count) {
246
+ onChange(currentPage + 1);
247
+ }
248
+ };
249
+
250
+ return (
251
+ <div ref={ref} className={clsx('flex flex-col items-stretch gap-y-2', className)}>
252
+ <div className="relative flex items-center justify-center px-2 h-9">
253
+ <Button variant="ghost" size="xs" onClick={goPrev} alt="Previous page">
254
+ <ChevronsUp className='size-4' />
255
+ </Button>
256
+ <div className="absolute left-2 flex items-center gap-x-1">
257
+ <ImageTypeButton
258
+ type={ImageType.original}
259
+ currentType={imageType}
260
+ onClick={() => setImageType(ImageType.original)}
261
+ icon={<Image className="size-4" />}
262
+ tooltip="Original images"
263
+ />
264
+ <ImageTypeButton
265
+ type={ImageType.instrumented}
266
+ currentType={imageType}
267
+ onClick={() => setImageType(ImageType.instrumented)}
268
+ icon={<ScanSearch className="size-4" />}
269
+ tooltip="Instrumented images"
270
+ />
271
+ <div className="w-px h-4 bg-border mx-1" />
272
+ <ZoomControls
273
+ zoom={zoom}
274
+ onZoomIn={zoomIn}
275
+ onZoomOut={zoomOut}
276
+ onFitToView={fitToView}
277
+ canZoomIn={zoom < ZOOM_LEVELS[ZOOM_LEVELS.length - 1]}
278
+ canZoomOut={zoom > ZOOM_LEVELS[0]}
279
+ />
280
+ </div>
281
+ <div className="absolute right-2">
282
+ <PageNavigator currentPage={currentPage} totalPages={count} onChange={onChange} />
283
+ </div>
284
+ </div>
285
+ <div ref={scrollContainerRef} className='flex flex-col items-center gap-2 flex-1 overflow-y-auto px-2'>
286
+ {Array.from({ length: count }, (_, index) => (
287
+ <PageThumbnail
288
+ key={index}
289
+ currentPage={currentPage}
290
+ pageNumber={index + 1}
291
+ aspectRatio={aspectRatio}
292
+ zoom={zoom}
293
+ url={loadedUrls.get(index + 1)}
294
+ onSelect={() => onChange(index + 1)}
295
+ />
296
+ ))}
297
+ </div>
298
+ <div className="flex items-center justify-center h-9">
299
+ <Button variant="ghost" size="xs" onClick={goNext} alt="Next page">
300
+ <ChevronsDown className='size-4' />
301
+ </Button>
302
+ </div>
303
+ </div>
304
+ );
305
+ }
306
+
307
+ interface ImageTypeButtonProps {
308
+ type: ImageType;
309
+ currentType: ImageType;
310
+ onClick: () => void;
311
+ icon: React.ReactNode;
312
+ tooltip: string;
313
+ }
314
+ function ImageTypeButton({ type, currentType, onClick, icon, tooltip }: ImageTypeButtonProps) {
315
+ const isSelected = type === currentType;
316
+ return (
317
+ <VTooltip description={tooltip} placement="bottom" size="xs">
318
+ <button
319
+ className={clsx(
320
+ "p-1 rounded cursor-pointer transition-colors",
321
+ isSelected
322
+ ? "text-primary bg-primary/10"
323
+ : "text-muted-foreground hover:text-foreground hover:bg-muted"
324
+ )}
325
+ onClick={onClick}
326
+ >
327
+ {icon}
328
+ </button>
329
+ </VTooltip>
330
+ );
331
+ }
332
+
333
+ interface ZoomControlsProps {
334
+ zoom: number;
335
+ onZoomIn: () => void;
336
+ onZoomOut: () => void;
337
+ onFitToView: () => void;
338
+ canZoomIn: boolean;
339
+ canZoomOut: boolean;
340
+ }
341
+ function ZoomControls({ zoom, onZoomIn, onZoomOut, onFitToView, canZoomIn, canZoomOut }: ZoomControlsProps) {
342
+ return (
343
+ <div className="flex items-center gap-x-0.5">
344
+ <VTooltip description="Zoom out" placement="bottom" size="xs">
345
+ <button
346
+ className={clsx(
347
+ "p-1 rounded cursor-pointer transition-colors",
348
+ canZoomOut
349
+ ? "text-muted-foreground hover:text-foreground hover:bg-muted"
350
+ : "text-muted-foreground/40 cursor-not-allowed"
351
+ )}
352
+ onClick={onZoomOut}
353
+ disabled={!canZoomOut}
354
+ >
355
+ <Minus className="size-4" />
356
+ </button>
357
+ </VTooltip>
358
+ <span className="text-xs text-muted-foreground min-w-[32px] text-center">
359
+ {zoom}%
360
+ </span>
361
+ <VTooltip description="Zoom in" placement="bottom" size="xs">
362
+ <button
363
+ className={clsx(
364
+ "p-1 rounded cursor-pointer transition-colors",
365
+ canZoomIn
366
+ ? "text-muted-foreground hover:text-foreground hover:bg-muted"
367
+ : "text-muted-foreground/40 cursor-not-allowed"
368
+ )}
369
+ onClick={onZoomIn}
370
+ disabled={!canZoomIn}
371
+ >
372
+ <Plus className="size-4" />
373
+ </button>
374
+ </VTooltip>
375
+ <VTooltip description="Fit to width" placement="bottom" size="xs">
376
+ <button
377
+ className={clsx(
378
+ "p-1 rounded cursor-pointer transition-colors",
379
+ zoom !== DEFAULT_ZOOM
380
+ ? "text-muted-foreground hover:text-foreground hover:bg-muted"
381
+ : "text-muted-foreground/40"
382
+ )}
383
+ onClick={onFitToView}
384
+ >
385
+ <Maximize className="size-4" />
386
+ </button>
387
+ </VTooltip>
388
+ </div>
389
+ );
390
+ }
391
+
392
+ interface PageThumbnailProps {
393
+ pageNumber: number;
394
+ currentPage: number;
395
+ aspectRatio: number;
396
+ zoom: number;
397
+ url?: string;
398
+ onSelect: () => void;
399
+ }
400
+ function PageThumbnail({ pageNumber, currentPage, aspectRatio, zoom, url, onSelect }: PageThumbnailProps) {
401
+ const isSelected = pageNumber === currentPage;
402
+ const widthPercent = zoom;
403
+
404
+ return (
405
+ <div
406
+ className="p-2 hover:bg-muted rounded-md flex flex-col items-center"
407
+ data-page={pageNumber}
408
+ style={{ width: `${widthPercent}%` }}
409
+ >
410
+ <div
411
+ className={clsx(
412
+ 'relative border-[2px] cursor-pointer overflow-hidden flex items-center justify-center bg-muted/50 w-full',
413
+ isSelected ? "border-primary" : "border-border"
414
+ )}
415
+ style={{ aspectRatio: `1 / ${aspectRatio}` }}
416
+ onClick={onSelect}
417
+ >
418
+ {url ? (
419
+ <img src={url} alt={`Page ${pageNumber}`} className="w-full" />
420
+ ) : (
421
+ <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
422
+ )}
423
+ </div>
424
+ <Center className="text-sm text-muted-foreground pt-1 font-semibold">{pageNumber}</Center>
425
+ </div>
426
+ );
427
+ }
428
+
429
+ interface PageNavigatorProps {
430
+ currentPage: number;
431
+ totalPages: number;
432
+ onChange: (page: number) => void;
433
+ }
434
+ function PageNavigator({ currentPage, totalPages, onChange }: PageNavigatorProps) {
435
+ const inputRef = useRef<HTMLInputElement>(null);
436
+ const [inputValue, setInputValue] = useState(String(currentPage));
437
+
438
+ // Sync input value when currentPage changes externally
439
+ useEffect(() => {
440
+ setInputValue(String(currentPage));
441
+ }, [currentPage]);
442
+
443
+ const handleSubmit = () => {
444
+ const page = parseInt(inputValue, 10);
445
+ if (!isNaN(page) && page >= 1 && page <= totalPages) {
446
+ onChange(page);
447
+ } else {
448
+ // Reset to current page if invalid
449
+ setInputValue(String(currentPage));
450
+ }
451
+ };
452
+
453
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
454
+ if (e.key === 'Enter') {
455
+ handleSubmit();
456
+ inputRef.current?.blur();
457
+ } else if (e.key === 'Escape') {
458
+ setInputValue(String(currentPage));
459
+ inputRef.current?.blur();
460
+ }
461
+ };
462
+
463
+ const handleBlur = () => {
464
+ handleSubmit();
465
+ };
466
+
467
+ return (
468
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
469
+ <span>Page</span>
470
+ <input
471
+ ref={inputRef}
472
+ type="text"
473
+ value={inputValue}
474
+ onChange={(e) => setInputValue(e.target.value)}
475
+ onKeyDown={handleKeyDown}
476
+ onBlur={handleBlur}
477
+ className="w-8 h-5 text-center text-xs px-1 py-0 bg-background border border-border rounded focus:outline-none focus:border-primary"
478
+ />
479
+ <span>/ {totalPages}</span>
480
+ </div>
481
+ );
482
+ }
@@ -1,8 +1,9 @@
1
1
  import { ContentObject, DocumentMetadata } from "@vertesia/common";
2
+ import { Button } from "@vertesia/ui/core";
2
3
  import { useUserSession } from "@vertesia/ui/session";
3
4
  import { Popover } from "@vertesia/ui/widgets";
4
- import { CloudDownload } from "lucide-react";
5
- import { getResourceUrl } from "./PdfPageProvider";
5
+ import { Download } from "lucide-react";
6
+ import { getResourceUrl } from "./MagicPdfProvider";
6
7
 
7
8
  interface DownloadPopoverProps {
8
9
  object: ContentObject;
@@ -12,7 +13,7 @@ export function DownloadPopover({ object }: DownloadPopoverProps) {
12
13
  const onDownload = (name: string) => {
13
14
  getResourceUrl(client, object.id, name).then(url => window.open(url, '_blank'));
14
15
  }
15
-
16
+
16
17
  const getProcessorType = (): string => {
17
18
  if (object.metadata?.type === "document") {
18
19
  const docMetadata = object.metadata as DocumentMetadata;
@@ -20,46 +21,50 @@ export function DownloadPopover({ object }: DownloadPopoverProps) {
20
21
  }
21
22
  return "xml"; // default
22
23
  };
23
-
24
+
24
25
  const processorType = getProcessorType();
25
-
26
- const renderDownloadOptions = () => {
27
- if (processorType === "markdown") {
28
- return (
29
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("document.md")}>
30
- document.md
31
- </button>
32
- );
33
- }
34
-
35
- // Default XML processor options
26
+
27
+ const buttonClass = "p-2 cursor-pointer hover:bg-muted text-left text-sm";
28
+
29
+ // For markdown processor, only one download option - render simple button
30
+ if (processorType === "markdown") {
36
31
  return (
37
- <>
38
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("annotated.pdf")}>
39
- annotated.pdf
40
- </button>
41
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("document.xml")}>
42
- document.xml
43
- </button>
44
- <button className="p-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-100" onClick={() => onDownload("analyzed-pages.json")}>
45
- analyzed-pages.json
46
- </button>
47
- </>
32
+ <Button
33
+ variant="ghost"
34
+ size="xs"
35
+ onClick={() => onDownload("document.md")}
36
+ alt="Download"
37
+ >
38
+ <Download className='size-4' />
39
+ </Button>
48
40
  );
49
- };
50
-
41
+ }
42
+
43
+ // Default XML processor - multiple options, use popover
51
44
  return (
52
- <div className="absolute bottom-[58px] right-[20px] w-[36px] h-[36px] cursor-pointer text-indigo-400 border-indigo-400 hover:border-indigo-500 hover:text-indigo-500 border-2 rounded-full shadow-xs flex items-center justify-center">
53
- <Popover strategy='absolute' placement='top-end' zIndex={100} offset={20}>
54
- <Popover.Trigger click>
55
- <CloudDownload className='size-6' />
56
- </Popover.Trigger>
57
- <Popover.Content>
58
- <div className="rounded-md shadow-md border border-gray-100 bg-white dark:bg-slate-50 dark:border-slate-100 min-w-[200px] flex flex-col divide-y">
59
- {renderDownloadOptions()}
60
- </div>
61
- </Popover.Content>
62
- </Popover>
63
- </div>
45
+ <Popover strategy='absolute' placement='bottom-start' zIndex={100} offset={8}>
46
+ <Popover.Trigger click>
47
+ <Button
48
+ variant="ghost"
49
+ size="xs"
50
+ alt="Download"
51
+ >
52
+ <Download className='size-4' />
53
+ </Button>
54
+ </Popover.Trigger>
55
+ <Popover.Content>
56
+ <div className="rounded-md shadow-md border border-border bg-popover min-w-[200px] flex flex-col divide-y divide-border">
57
+ <button className={buttonClass} onClick={() => onDownload("annotated.pdf")}>
58
+ annotated.pdf
59
+ </button>
60
+ <button className={buttonClass} onClick={() => onDownload("document.xml")}>
61
+ document.xml
62
+ </button>
63
+ <button className={buttonClass} onClick={() => onDownload("analyzed-pages.json")}>
64
+ analyzed-pages.json
65
+ </button>
66
+ </div>
67
+ </Popover.Content>
68
+ </Popover>
64
69
  )
65
70
  }