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

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 (307) hide show
  1. package/lib/core/components/shadcn/filters/filterBar.d.ts.map +1 -1
  2. package/lib/core/components/shadcn/filters/filterBar.js +1 -1
  3. package/lib/core/components/shadcn/filters/filterBar.js.map +1 -1
  4. package/lib/core/components/shadcn/modal/dialog.d.ts +1 -1
  5. package/lib/core/components/shadcn/modal/dialog.d.ts.map +1 -1
  6. package/lib/core/components/shadcn/modal/dialog.js +2 -0
  7. package/lib/core/components/shadcn/modal/dialog.js.map +1 -1
  8. package/lib/core/components/shadcn/tabs.d.ts +12 -1
  9. package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
  10. package/lib/core/components/shadcn/tabs.js +12 -4
  11. package/lib/core/components/shadcn/tabs.js.map +1 -1
  12. package/lib/features/agent/chat/AgentRightPanel.d.ts +5 -1
  13. package/lib/features/agent/chat/AgentRightPanel.d.ts.map +1 -1
  14. package/lib/features/agent/chat/AgentRightPanel.js +2 -2
  15. package/lib/features/agent/chat/AgentRightPanel.js.map +1 -1
  16. package/lib/features/agent/chat/ArtifactsTab.d.ts +5 -1
  17. package/lib/features/agent/chat/ArtifactsTab.d.ts.map +1 -1
  18. package/lib/features/agent/chat/ArtifactsTab.js +48 -11
  19. package/lib/features/agent/chat/ArtifactsTab.js.map +1 -1
  20. package/lib/features/agent/chat/DocumentEditingActionCard.d.ts +6 -0
  21. package/lib/features/agent/chat/DocumentEditingActionCard.d.ts.map +1 -0
  22. package/lib/features/agent/chat/DocumentEditingActionCard.js +129 -0
  23. package/lib/features/agent/chat/DocumentEditingActionCard.js.map +1 -0
  24. package/lib/features/agent/chat/ModernAgentConversation.d.ts +19 -1
  25. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  26. package/lib/features/agent/chat/ModernAgentConversation.js +125 -27
  27. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  28. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.d.ts +45 -0
  29. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.d.ts.map +1 -0
  30. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.js +40 -0
  31. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.js.map +1 -0
  32. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
  33. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  34. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +94 -49
  35. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  36. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.d.ts +2 -1
  37. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.d.ts.map +1 -1
  38. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.js +9 -5
  39. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.js.map +1 -1
  40. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts +25 -0
  41. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts.map +1 -0
  42. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js +33 -0
  43. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js.map +1 -0
  44. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
  45. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  46. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +22 -24
  47. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  48. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.d.ts +14 -0
  49. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.d.ts.map +1 -0
  50. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.js +21 -0
  51. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.js.map +1 -0
  52. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.d.ts +30 -0
  53. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.d.ts.map +1 -0
  54. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.js +53 -0
  55. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.js.map +1 -0
  56. package/lib/features/agent/chat/ModernAgentOutput/ToolCallGroup.d.ts.map +1 -1
  57. package/lib/features/agent/chat/ModernAgentOutput/ToolCallGroup.js +21 -2
  58. package/lib/features/agent/chat/ModernAgentOutput/ToolCallGroup.js.map +1 -1
  59. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.d.ts +6 -0
  60. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.d.ts.map +1 -0
  61. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.js +29 -0
  62. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.js.map +1 -0
  63. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.d.ts +33 -0
  64. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.d.ts.map +1 -0
  65. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.js +129 -0
  66. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.js.map +1 -0
  67. package/lib/features/agent/chat/ModernAgentOutput/utils.d.ts +7 -0
  68. package/lib/features/agent/chat/ModernAgentOutput/utils.d.ts.map +1 -1
  69. package/lib/features/agent/chat/ModernAgentOutput/utils.js +11 -1
  70. package/lib/features/agent/chat/ModernAgentOutput/utils.js.map +1 -1
  71. package/lib/features/agent/chat/hooks/useAgentStream.d.ts +1 -1
  72. package/lib/features/agent/chat/hooks/useAgentStream.d.ts.map +1 -1
  73. package/lib/features/agent/chat/hooks/useAgentStream.js +24 -9
  74. package/lib/features/agent/chat/hooks/useAgentStream.js.map +1 -1
  75. package/lib/features/agent/chat/index.d.ts +2 -1
  76. package/lib/features/agent/chat/index.d.ts.map +1 -1
  77. package/lib/features/agent/chat/index.js +3 -0
  78. package/lib/features/agent/chat/index.js.map +1 -1
  79. package/lib/features/store/objects/DocumentSearchResults.js +2 -2
  80. package/lib/features/store/objects/DocumentSearchResults.js.map +1 -1
  81. package/lib/features/store/objects/components/ContentOverview.d.ts +2 -1
  82. package/lib/features/store/objects/components/ContentOverview.d.ts.map +1 -1
  83. package/lib/features/store/objects/components/ContentOverview.js +86 -15
  84. package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
  85. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.d.ts +16 -0
  86. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.d.ts.map +1 -0
  87. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.js +116 -0
  88. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.js.map +1 -0
  89. package/lib/features/store/objects/components/DocumentEditingLockStatus.d.ts +7 -0
  90. package/lib/features/store/objects/components/DocumentEditingLockStatus.d.ts.map +1 -0
  91. package/lib/features/store/objects/components/DocumentEditingLockStatus.js +13 -0
  92. package/lib/features/store/objects/components/DocumentEditingLockStatus.js.map +1 -0
  93. package/lib/features/store/objects/components/DocumentEditingPanel.d.ts +48 -0
  94. package/lib/features/store/objects/components/DocumentEditingPanel.d.ts.map +1 -0
  95. package/lib/features/store/objects/components/DocumentEditingPanel.js +720 -0
  96. package/lib/features/store/objects/components/DocumentEditingPanel.js.map +1 -0
  97. package/lib/features/store/objects/components/TextEditorPanel.d.ts.map +1 -1
  98. package/lib/features/store/objects/components/TextEditorPanel.js +22 -9
  99. package/lib/features/store/objects/components/TextEditorPanel.js.map +1 -1
  100. package/lib/features/store/objects/components/documentArtifactRefs.d.ts +26 -0
  101. package/lib/features/store/objects/components/documentArtifactRefs.d.ts.map +1 -0
  102. package/lib/features/store/objects/components/documentArtifactRefs.js +52 -0
  103. package/lib/features/store/objects/components/documentArtifactRefs.js.map +1 -0
  104. package/lib/features/store/objects/components/documentEditingAgentConfig.d.ts +4 -0
  105. package/lib/features/store/objects/components/documentEditingAgentConfig.d.ts.map +1 -0
  106. package/lib/features/store/objects/components/documentEditingAgentConfig.js +40 -0
  107. package/lib/features/store/objects/components/documentEditingAgentConfig.js.map +1 -0
  108. package/lib/features/store/objects/components/documentEditingPrompts.d.ts +3 -0
  109. package/lib/features/store/objects/components/documentEditingPrompts.d.ts.map +1 -0
  110. package/lib/features/store/objects/components/documentEditingPrompts.js +13 -0
  111. package/lib/features/store/objects/components/documentEditingPrompts.js.map +1 -0
  112. package/lib/features/store/objects/components/documentEditingRun.d.ts +27 -0
  113. package/lib/features/store/objects/components/documentEditingRun.d.ts.map +1 -0
  114. package/lib/features/store/objects/components/documentEditingRun.js +126 -0
  115. package/lib/features/store/objects/components/documentEditingRun.js.map +1 -0
  116. package/lib/features/store/objects/components/documentEditingTarget.d.ts +20 -0
  117. package/lib/features/store/objects/components/documentEditingTarget.d.ts.map +1 -0
  118. package/lib/features/store/objects/components/documentEditingTarget.js +40 -0
  119. package/lib/features/store/objects/components/documentEditingTarget.js.map +1 -0
  120. package/lib/features/store/objects/components/index.d.ts +1 -0
  121. package/lib/features/store/objects/components/index.d.ts.map +1 -1
  122. package/lib/features/store/objects/components/index.js +1 -0
  123. package/lib/features/store/objects/components/index.js.map +1 -1
  124. package/lib/features/store/objects/components/useContentPanelHooks.d.ts +5 -1
  125. package/lib/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
  126. package/lib/features/store/objects/components/useContentPanelHooks.js +53 -29
  127. package/lib/features/store/objects/components/useContentPanelHooks.js.map +1 -1
  128. package/lib/features/store/objects/components/useDownloadFile.d.ts +2 -1
  129. package/lib/features/store/objects/components/useDownloadFile.d.ts.map +1 -1
  130. package/lib/features/store/objects/components/useDownloadFile.js +3 -0
  131. package/lib/features/store/objects/components/useDownloadFile.js.map +1 -1
  132. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +1 -1
  133. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +1 -1
  134. package/lib/i18n/locales/ar.json +118 -1
  135. package/lib/i18n/locales/de.json +118 -1
  136. package/lib/i18n/locales/en.json +118 -0
  137. package/lib/i18n/locales/es.json +118 -1
  138. package/lib/i18n/locales/fr.json +118 -1
  139. package/lib/i18n/locales/it.json +118 -1
  140. package/lib/i18n/locales/ja.json +118 -1
  141. package/lib/i18n/locales/ko.json +118 -1
  142. package/lib/i18n/locales/pt.json +118 -1
  143. package/lib/i18n/locales/ru.json +118 -1
  144. package/lib/i18n/locales/tr.json +118 -1
  145. package/lib/i18n/locales/zh-TW.json +118 -1
  146. package/lib/i18n/locales/zh.json +118 -1
  147. package/lib/rich-text/EditorToolbar.d.ts +37 -0
  148. package/lib/rich-text/EditorToolbar.d.ts.map +1 -0
  149. package/lib/rich-text/EditorToolbar.js +215 -0
  150. package/lib/rich-text/EditorToolbar.js.map +1 -0
  151. package/lib/rich-text/VertesiaRichTextEditor.d.ts +26 -0
  152. package/lib/rich-text/VertesiaRichTextEditor.d.ts.map +1 -0
  153. package/lib/rich-text/VertesiaRichTextEditor.js +67 -0
  154. package/lib/rich-text/VertesiaRichTextEditor.js.map +1 -0
  155. package/lib/rich-text/index.d.ts +2 -0
  156. package/lib/rich-text/index.d.ts.map +1 -0
  157. package/lib/rich-text/index.js +2 -0
  158. package/lib/rich-text/index.js.map +1 -0
  159. package/lib/vertesia-ui-core.js +2 -2
  160. package/lib/vertesia-ui-core.js.map +1 -1
  161. package/lib/vertesia-ui-features.js +35 -30
  162. package/lib/vertesia-ui-features.js.map +1 -1
  163. package/lib/vertesia-ui-i18n.js +14 -14
  164. package/lib/vertesia-ui-i18n.js.map +1 -1
  165. package/lib/vertesia-ui-rich-text.js +5 -0
  166. package/lib/vertesia-ui-rich-text.js.map +1 -0
  167. package/lib/vertesia-ui-widgets.js +25 -7
  168. package/lib/vertesia-ui-widgets.js.map +1 -1
  169. package/lib/widgets/markdown/AgentResourceResolver.d.ts +31 -0
  170. package/lib/widgets/markdown/AgentResourceResolver.d.ts.map +1 -0
  171. package/lib/widgets/markdown/AgentResourceResolver.js +39 -0
  172. package/lib/widgets/markdown/AgentResourceResolver.js.map +1 -0
  173. package/lib/widgets/markdown/ArtifactEditingSurface.d.ts +63 -0
  174. package/lib/widgets/markdown/ArtifactEditingSurface.d.ts.map +1 -0
  175. package/lib/widgets/markdown/ArtifactEditingSurface.js +582 -0
  176. package/lib/widgets/markdown/ArtifactEditingSurface.js.map +1 -0
  177. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.d.ts +69 -0
  178. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.d.ts.map +1 -0
  179. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.js +374 -0
  180. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.js.map +1 -0
  181. package/lib/widgets/markdown/MarkdownLink.d.ts +3 -1
  182. package/lib/widgets/markdown/MarkdownLink.d.ts.map +1 -1
  183. package/lib/widgets/markdown/MarkdownLink.js +41 -14
  184. package/lib/widgets/markdown/MarkdownLink.js.map +1 -1
  185. package/lib/widgets/markdown/MarkdownRenderer.d.ts +8 -1
  186. package/lib/widgets/markdown/MarkdownRenderer.d.ts.map +1 -1
  187. package/lib/widgets/markdown/MarkdownRenderer.js +20 -4
  188. package/lib/widgets/markdown/MarkdownRenderer.js.map +1 -1
  189. package/lib/widgets/markdown/index.d.ts +5 -1
  190. package/lib/widgets/markdown/index.d.ts.map +1 -1
  191. package/lib/widgets/markdown/index.js +5 -1
  192. package/lib/widgets/markdown/index.js.map +1 -1
  193. package/lib/widgets/markdown/normalizeCustomSchemeLinks.d.ts.map +1 -1
  194. package/lib/widgets/markdown/normalizeCustomSchemeLinks.js +14 -2
  195. package/lib/widgets/markdown/normalizeCustomSchemeLinks.js.map +1 -1
  196. package/lib/widgets/markdown/textDiff.d.ts +52 -0
  197. package/lib/widgets/markdown/textDiff.d.ts.map +1 -0
  198. package/lib/widgets/markdown/textDiff.js +362 -0
  199. package/lib/widgets/markdown/textDiff.js.map +1 -0
  200. package/lib/widgets/markdown/useArtifactContent.d.ts +5 -3
  201. package/lib/widgets/markdown/useArtifactContent.d.ts.map +1 -1
  202. package/lib/widgets/markdown/useArtifactContent.js +6 -3
  203. package/lib/widgets/markdown/useArtifactContent.js.map +1 -1
  204. package/lib/widgets/markdown/useResolvedUrl.d.ts +1 -5
  205. package/lib/widgets/markdown/useResolvedUrl.d.ts.map +1 -1
  206. package/lib/widgets/markdown/useResolvedUrl.js +19 -31
  207. package/lib/widgets/markdown/useResolvedUrl.js.map +1 -1
  208. package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts +9 -0
  209. package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts.map +1 -1
  210. package/lib/widgets/xml-viewer/hooks/useXMLViewer.js +20 -17
  211. package/lib/widgets/xml-viewer/hooks/useXMLViewer.js.map +1 -1
  212. package/package.json +13 -9
  213. package/src/__tests__/a11y.test.tsx +4 -1
  214. package/src/__tests__/axe-helper.ts +17 -7
  215. package/src/__tests__/setup.ts +25 -0
  216. package/src/core/components/shadcn/filters/filterBar.tsx +1 -0
  217. package/src/core/components/shadcn/modal/dialog.tsx +3 -1
  218. package/src/core/components/shadcn/tabs.test.tsx +65 -0
  219. package/src/core/components/shadcn/tabs.tsx +25 -3
  220. package/src/features/agent/chat/AgentRightPanel.tsx +16 -1
  221. package/src/features/agent/chat/ArtifactsTab.test.tsx +55 -0
  222. package/src/features/agent/chat/ArtifactsTab.tsx +203 -52
  223. package/src/features/agent/chat/DocumentEditingActionCard.test.tsx +105 -0
  224. package/src/features/agent/chat/DocumentEditingActionCard.tsx +210 -0
  225. package/src/features/agent/chat/ModernAgentConversation.test.tsx +237 -40
  226. package/src/features/agent/chat/ModernAgentConversation.tsx +247 -75
  227. package/src/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.test.tsx +79 -0
  228. package/src/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.tsx +94 -0
  229. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +257 -34
  230. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +217 -105
  231. package/src/features/agent/chat/ModernAgentOutput/AttachmentPreview.tsx +17 -0
  232. package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.test.tsx +44 -0
  233. package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.tsx +88 -0
  234. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +30 -46
  235. package/src/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.tsx +40 -0
  236. package/src/features/agent/chat/ModernAgentOutput/ResourceChip.test.tsx +148 -0
  237. package/src/features/agent/chat/ModernAgentOutput/ResourceChip.tsx +141 -0
  238. package/src/features/agent/chat/ModernAgentOutput/ToolCallGroup.tsx +33 -1
  239. package/src/features/agent/chat/ModernAgentOutput/resourceLinks.tsx +43 -0
  240. package/src/features/agent/chat/ModernAgentOutput/resourceSummary.test.ts +229 -0
  241. package/src/features/agent/chat/ModernAgentOutput/resourceSummary.ts +152 -0
  242. package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +17 -0
  243. package/src/features/agent/chat/ModernAgentOutput/utils.ts +14 -1
  244. package/src/features/agent/chat/hooks/useAgentStream.test.tsx +188 -0
  245. package/src/features/agent/chat/hooks/useAgentStream.ts +28 -8
  246. package/src/features/agent/chat/index.ts +11 -0
  247. package/src/features/store/objects/DocumentSearchResults.tsx +2 -2
  248. package/src/features/store/objects/components/ContentOverview.tsx +216 -65
  249. package/src/features/store/objects/components/DocumentEditingConfigurationSelector.test.ts +34 -0
  250. package/src/features/store/objects/components/DocumentEditingConfigurationSelector.tsx +211 -0
  251. package/src/features/store/objects/components/DocumentEditingLockStatus.test.tsx +32 -0
  252. package/src/features/store/objects/components/DocumentEditingLockStatus.tsx +45 -0
  253. package/src/features/store/objects/components/DocumentEditingPanel.test.tsx +336 -0
  254. package/src/features/store/objects/components/DocumentEditingPanel.tsx +1236 -0
  255. package/src/features/store/objects/components/TextEditorPanel.test.tsx +48 -0
  256. package/src/features/store/objects/components/TextEditorPanel.tsx +41 -20
  257. package/src/features/store/objects/components/documentArtifactRefs.test.ts +65 -0
  258. package/src/features/store/objects/components/documentArtifactRefs.ts +72 -0
  259. package/src/features/store/objects/components/documentEditingAgentConfig.test.ts +28 -0
  260. package/src/features/store/objects/components/documentEditingAgentConfig.ts +41 -0
  261. package/src/features/store/objects/components/documentEditingPrompts.test.ts +26 -0
  262. package/src/features/store/objects/components/documentEditingPrompts.ts +12 -0
  263. package/src/features/store/objects/components/documentEditingRun.test.ts +176 -0
  264. package/src/features/store/objects/components/documentEditingRun.ts +184 -0
  265. package/src/features/store/objects/components/documentEditingTarget.test.ts +96 -0
  266. package/src/features/store/objects/components/documentEditingTarget.ts +63 -0
  267. package/src/features/store/objects/components/index.ts +1 -0
  268. package/src/features/store/objects/components/useContentPanelHooks.ts +52 -27
  269. package/src/features/store/objects/components/useDownloadFile.test.ts +20 -0
  270. package/src/features/store/objects/components/useDownloadFile.ts +5 -1
  271. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +2 -2
  272. package/src/i18n/locales/ar.json +118 -1
  273. package/src/i18n/locales/de.json +118 -1
  274. package/src/i18n/locales/en.json +118 -0
  275. package/src/i18n/locales/es.json +118 -1
  276. package/src/i18n/locales/fr.json +118 -1
  277. package/src/i18n/locales/it.json +118 -1
  278. package/src/i18n/locales/ja.json +118 -1
  279. package/src/i18n/locales/ko.json +118 -1
  280. package/src/i18n/locales/pt.json +118 -1
  281. package/src/i18n/locales/ru.json +118 -1
  282. package/src/i18n/locales/tr.json +118 -1
  283. package/src/i18n/locales/zh-TW.json +118 -1
  284. package/src/i18n/locales/zh.json +118 -1
  285. package/src/rich-text/EditorToolbar.tsx +708 -0
  286. package/src/rich-text/VertesiaRichTextEditor.test.tsx +211 -0
  287. package/src/rich-text/VertesiaRichTextEditor.tsx +241 -0
  288. package/src/rich-text/index.ts +9 -0
  289. package/src/session/UserSession.test.ts +11 -11
  290. package/src/widgets/markdown/AgentResourceResolver.test.ts +24 -0
  291. package/src/widgets/markdown/AgentResourceResolver.tsx +70 -0
  292. package/src/widgets/markdown/ArtifactEditingSurface.test.tsx +537 -0
  293. package/src/widgets/markdown/ArtifactEditingSurface.tsx +878 -0
  294. package/src/widgets/markdown/CollaborativeMarkdownRenderer.test.tsx +484 -0
  295. package/src/widgets/markdown/CollaborativeMarkdownRenderer.tsx +825 -0
  296. package/src/widgets/markdown/MarkdownLink.test.tsx +122 -0
  297. package/src/widgets/markdown/MarkdownLink.tsx +102 -17
  298. package/src/widgets/markdown/MarkdownRenderer.tsx +30 -3
  299. package/src/widgets/markdown/index.ts +39 -1
  300. package/src/widgets/markdown/normalizeCustomSchemeLinks.ts +15 -2
  301. package/src/widgets/markdown/textDiff.test.ts +156 -0
  302. package/src/widgets/markdown/textDiff.ts +399 -0
  303. package/src/widgets/markdown/useArtifactContent.ts +9 -3
  304. package/src/widgets/markdown/useResolvedUrl.test.ts +29 -40
  305. package/src/widgets/markdown/useResolvedUrl.ts +33 -35
  306. package/src/widgets/xml-viewer/hooks/useXMLViewer.test.ts +26 -0
  307. package/src/widgets/xml-viewer/hooks/useXMLViewer.ts +23 -18
@@ -0,0 +1,825 @@
1
+ import {
2
+ Button,
3
+ cn,
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ Textarea,
9
+ VTooltip,
10
+ } from '@vertesia/ui/core';
11
+ import { useUITranslation } from '@vertesia/ui/i18n';
12
+ import type { Element } from 'hast';
13
+ import {
14
+ BarChart3,
15
+ Code2,
16
+ Heading2,
17
+ Link,
18
+ List,
19
+ MessageSquare,
20
+ Pencil,
21
+ Pilcrow,
22
+ Plus,
23
+ Quote,
24
+ Send,
25
+ Table2,
26
+ X,
27
+ } from 'lucide-react';
28
+ import React, {
29
+ createContext,
30
+ lazy,
31
+ Suspense,
32
+ useCallback,
33
+ useContext,
34
+ useEffect,
35
+ useMemo,
36
+ useRef,
37
+ useState,
38
+ } from 'react';
39
+ import type { Components } from 'react-markdown';
40
+ import { MarkdownRenderer, type MarkdownRendererProps } from './MarkdownRenderer';
41
+
42
+ export type MarkdownEditingResource =
43
+ | {
44
+ kind: 'store_document';
45
+ document_id: string;
46
+ name?: string;
47
+ }
48
+ | {
49
+ kind: 'agent_artifact';
50
+ run_id: string;
51
+ path: string;
52
+ };
53
+
54
+ export type MarkdownBlockType =
55
+ | 'heading'
56
+ | 'paragraph'
57
+ | 'list'
58
+ /** Legacy anchors from before lists became the selectable unit. */
59
+ | 'list_item'
60
+ | 'blockquote'
61
+ | 'code_block'
62
+ | 'table'
63
+ /** Thematic break / horizontal rule (`---`). */
64
+ | 'separator';
65
+
66
+ /** Container blocks whose inner blocks must not offer their own selection controls. */
67
+ const CONTAINER_BLOCK_TYPES = new Set<MarkdownBlockType>(['list', 'blockquote', 'table']);
68
+
69
+ export interface MarkdownBlockAnchor {
70
+ block_id: string;
71
+ block_type: MarkdownBlockType;
72
+ heading_path?: string[];
73
+ source_range?: {
74
+ start: number;
75
+ end: number;
76
+ };
77
+ exact_text: string;
78
+ prefix?: string;
79
+ suffix?: string;
80
+ /** Zero-based occurrence among identical blocks under the same heading path. */
81
+ occurrence_index?: number;
82
+ }
83
+
84
+ export interface MarkdownEditingAction {
85
+ operation_id: string;
86
+ resource: MarkdownEditingResource;
87
+ base_version?: string;
88
+ action: 'comment' | 'edit';
89
+ anchor: MarkdownBlockAnchor;
90
+ comment?: string;
91
+ user_change?: {
92
+ before: string;
93
+ after: string;
94
+ };
95
+ /** True when the client already committed this change to the resource. */
96
+ applied?: boolean;
97
+ /** Revision/document id produced by an applied change. */
98
+ updated_document_id?: string;
99
+ }
100
+
101
+ export interface CollaborativeMarkdownRendererProps extends Omit<MarkdownRendererProps, 'children' | 'components'> {
102
+ children: string;
103
+ resource: MarkdownEditingResource;
104
+ baseVersion?: string;
105
+ components?: Components;
106
+ readOnly?: boolean;
107
+ /** Keep comments available when content mutations are read-only (for example while the agent is working). */
108
+ allowComments?: boolean;
109
+ highlightChangesFrom?: string;
110
+ highlightVersion?: number;
111
+ onAction: (action: MarkdownEditingAction) => void | Promise<void>;
112
+ }
113
+
114
+ interface MarkdownBlockProps extends React.HTMLAttributes<HTMLElement> {
115
+ node?: Element;
116
+ children?: React.ReactNode;
117
+ }
118
+
119
+ type EditingMode = 'comment' | 'edit' | 'insert';
120
+
121
+ interface ActiveEditing {
122
+ anchor: MarkdownBlockAnchor;
123
+ mode: EditingMode;
124
+ initialDraft?: string;
125
+ }
126
+
127
+ interface OrphanedDraft extends ActiveEditing {
128
+ draft: string;
129
+ }
130
+
131
+ interface CollaborativeMarkdownContextValue {
132
+ markdown: string;
133
+ artifactRunId?: string;
134
+ readOnly: boolean;
135
+ allowComments: boolean;
136
+ activeEditing?: ActiveEditing;
137
+ highlightChangesFrom?: string;
138
+ flashChangedBlocks: boolean;
139
+ beginEditing: (anchor: MarkdownBlockAnchor, mode: EditingMode, initialDraft?: string) => void;
140
+ cancelEditing: () => void;
141
+ updateDraft: (draft: string) => void;
142
+ submit: (anchor: MarkdownBlockAnchor, mode: EditingMode, draft: string) => Promise<void>;
143
+ }
144
+
145
+ const CollaborativeMarkdownContext = createContext<CollaborativeMarkdownContextValue | null>(null);
146
+
147
+ /** True inside a selectable container block (list, blockquote, table). */
148
+ const NestedMarkdownBlockContext = createContext(false);
149
+
150
+ const MarkdownComponentEditor = lazy(() =>
151
+ import('@vertesia/ui/rich-text').then((module) => ({ default: module.VertesiaMarkdownComponentEditor })),
152
+ );
153
+
154
+ const CONTEXT_LENGTH = 80;
155
+ const ATX_HEADING = /^ {0,3}(#{1,6})[\t ]+(.+?)[\t ]*#*[\t ]*$/;
156
+ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
157
+
158
+ function createOperationId(): string {
159
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
160
+ return crypto.randomUUID();
161
+ }
162
+ return `edit-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
163
+ }
164
+
165
+ function createMarkdownBlockLocator(
166
+ markdown: string,
167
+ node: Element | undefined,
168
+ blockType: MarkdownBlockType,
169
+ ): MarkdownBlockAnchor {
170
+ const start = node?.position?.start.offset;
171
+ const end = node?.position?.end.offset;
172
+ const hasRange = typeof start === 'number' && typeof end === 'number' && start >= 0 && end >= start;
173
+ const exactText = hasRange ? markdown.slice(start, end) : '';
174
+
175
+ return {
176
+ block_id: hasRange ? `${blockType}:${start}:${end}` : `${blockType}:${createOperationId()}`,
177
+ block_type: blockType,
178
+ ...(hasRange ? { source_range: { start, end } } : {}),
179
+ exact_text: exactText,
180
+ ...(hasRange && start > 0 ? { prefix: markdown.slice(Math.max(0, start - CONTEXT_LENGTH), start) } : {}),
181
+ ...(hasRange && end < markdown.length
182
+ ? { suffix: markdown.slice(end, Math.min(markdown.length, end + CONTEXT_LENGTH)) }
183
+ : {}),
184
+ };
185
+ }
186
+
187
+ function enrichMarkdownBlockAnchor(markdown: string, anchor: MarkdownBlockAnchor): MarkdownBlockAnchor {
188
+ const start = anchor.source_range?.start;
189
+ if (typeof start !== 'number') return anchor;
190
+ const headingPath = getHeadingPath(markdown, start);
191
+ const occurrenceIndex = getOccurrenceIndex(markdown, anchor.exact_text, start, headingPath);
192
+ return {
193
+ ...anchor,
194
+ ...(headingPath.length > 0 ? { heading_path: headingPath } : {}),
195
+ occurrence_index: occurrenceIndex,
196
+ };
197
+ }
198
+
199
+ export function createMarkdownBlockAnchor(
200
+ markdown: string,
201
+ node: Element | undefined,
202
+ blockType: MarkdownBlockType,
203
+ ): MarkdownBlockAnchor {
204
+ return enrichMarkdownBlockAnchor(markdown, createMarkdownBlockLocator(markdown, node, blockType));
205
+ }
206
+
207
+ function getHeadingPath(markdown: string, offset: number): string[] {
208
+ const headings: string[] = [];
209
+ let inFence: '`' | '~' | null = null;
210
+ let cursor = 0;
211
+
212
+ for (const line of markdown.slice(0, offset).split('\n')) {
213
+ const fenceMatch = FENCE.exec(line);
214
+ if (fenceMatch) {
215
+ const marker = fenceMatch[1]?.[0] as '`' | '~' | undefined;
216
+ if (!inFence) inFence = marker ?? null;
217
+ else if (marker === inFence) inFence = null;
218
+ cursor += line.length + 1;
219
+ continue;
220
+ }
221
+
222
+ if (!inFence) {
223
+ const headingMatch = ATX_HEADING.exec(line);
224
+ if (headingMatch) {
225
+ const level = headingMatch[1]?.length ?? 1;
226
+ const title = headingMatch[2]?.trim();
227
+ if (title) {
228
+ headings.length = level - 1;
229
+ headings[level - 1] = title;
230
+ }
231
+ }
232
+ }
233
+
234
+ cursor += line.length + 1;
235
+ if (cursor > offset) break;
236
+ }
237
+
238
+ return headings.filter((heading): heading is string => Boolean(heading));
239
+ }
240
+
241
+ function getOccurrenceIndex(markdown: string, exactText: string, offset: number, headingPath: string[]): number {
242
+ if (!exactText) return 0;
243
+
244
+ let occurrenceIndex = 0;
245
+ let candidate = markdown.indexOf(exactText);
246
+ while (candidate >= 0 && candidate < offset) {
247
+ const candidateHeadingPath = getHeadingPath(markdown, candidate);
248
+ if (
249
+ candidateHeadingPath.length === headingPath.length &&
250
+ candidateHeadingPath.every((heading, index) => heading === headingPath[index])
251
+ ) {
252
+ occurrenceIndex++;
253
+ }
254
+ candidate = markdown.indexOf(exactText, candidate + Math.max(1, exactText.length));
255
+ }
256
+ return occurrenceIndex;
257
+ }
258
+
259
+ function resourceReference(resource: MarkdownEditingResource): string {
260
+ if (resource.kind === 'store_document') {
261
+ return `store:${resource.document_id}`;
262
+ }
263
+ return `artifact:${resource.path} (run ${resource.run_id})`;
264
+ }
265
+
266
+ function markdownFence(...values: string[]): string {
267
+ let longestRun = 0;
268
+ for (const value of values) {
269
+ for (const match of value.matchAll(/`+/g)) {
270
+ longestRun = Math.max(longestRun, match[0].length);
271
+ }
272
+ }
273
+ return '`'.repeat(Math.max(3, longestRun + 1));
274
+ }
275
+
276
+ /**
277
+ * Apply a user edit to the markdown source. Returns the updated markdown, or
278
+ * undefined when the anchored content cannot be located unambiguously.
279
+ */
280
+ export function applyMarkdownEditingChange(markdown: string, action: MarkdownEditingAction): string | undefined {
281
+ const change = action.user_change;
282
+ if (!change) return undefined;
283
+ const { before, after } = change;
284
+
285
+ const range = action.anchor.source_range;
286
+ if (range && markdown.slice(range.start, range.end) === before) {
287
+ return markdown.slice(0, range.start) + after + markdown.slice(range.end);
288
+ }
289
+
290
+ const prefix = action.anchor.prefix;
291
+ if (prefix) {
292
+ const anchored = markdown.indexOf(prefix + before);
293
+ if (anchored >= 0 && markdown.indexOf(prefix + before, anchored + 1) < 0) {
294
+ const start = anchored + prefix.length;
295
+ return markdown.slice(0, start) + after + markdown.slice(start + before.length);
296
+ }
297
+ }
298
+
299
+ const first = markdown.indexOf(before);
300
+ if (first < 0 || markdown.indexOf(before, first + 1) >= 0) return undefined;
301
+ return markdown.slice(0, first) + after + markdown.slice(first + before.length);
302
+ }
303
+
304
+ export function formatMarkdownEditingAction(action: MarkdownEditingAction): string {
305
+ const reference = resourceReference(action.resource);
306
+ const location = action.anchor.block_type.replace('_', ' ');
307
+
308
+ if (action.action === 'edit' && action.user_change && action.applied) {
309
+ const fence = markdownFence(action.user_change.before, action.user_change.after);
310
+ return [
311
+ `I already edited the ${location} in ${reference} myself and the change is saved` +
312
+ `${action.updated_document_id ? ` as revision ${action.updated_document_id}` : ''}.`,
313
+ `Operation: ${action.operation_id}`,
314
+ 'This is a notification only. Do not apply this change again and do not modify the document in ' +
315
+ 'response; acknowledge briefly and use the latest content going forward.',
316
+ '',
317
+ 'Before:',
318
+ `${fence}markdown`,
319
+ action.user_change.before,
320
+ fence,
321
+ '',
322
+ 'After:',
323
+ `${fence}markdown`,
324
+ action.user_change.after,
325
+ fence,
326
+ ].join('\n');
327
+ }
328
+
329
+ if (action.action === 'edit' && action.user_change) {
330
+ const fence = markdownFence(action.user_change.before, action.user_change.after);
331
+ return [
332
+ `Apply my edit to the ${location} in ${reference}, then update the resource.`,
333
+ `Operation: ${action.operation_id}`,
334
+ 'Preserve all content outside the selected block exactly; do not regenerate unrelated sections.',
335
+ '',
336
+ 'Before:',
337
+ `${fence}markdown`,
338
+ action.user_change.before,
339
+ fence,
340
+ '',
341
+ 'After:',
342
+ `${fence}markdown`,
343
+ action.user_change.after,
344
+ fence,
345
+ ].join('\n');
346
+ }
347
+
348
+ const fence = markdownFence(action.anchor.exact_text);
349
+ return [
350
+ `Update the ${location} in ${reference} based on this comment:`,
351
+ `Operation: ${action.operation_id}`,
352
+ 'Preserve all content outside the selected block exactly; do not regenerate unrelated sections.',
353
+ '',
354
+ action.comment ?? '',
355
+ '',
356
+ 'Selected content:',
357
+ `${fence}markdown`,
358
+ action.anchor.exact_text,
359
+ fence,
360
+ ].join('\n');
361
+ }
362
+
363
+ function useCollaborativeMarkdownContext(): CollaborativeMarkdownContextValue {
364
+ const context = useContext(CollaborativeMarkdownContext);
365
+ if (!context) {
366
+ throw new Error('Collaborative Markdown blocks must be rendered inside CollaborativeMarkdownRenderer');
367
+ }
368
+ return context;
369
+ }
370
+
371
+ function CollaborativeBlockEditor({ anchor, mode, initialDraft }: ActiveEditing) {
372
+ const { t } = useUITranslation();
373
+ const { artifactRunId, readOnly, allowComments, cancelEditing, submit, updateDraft } =
374
+ useCollaborativeMarkdownContext();
375
+ const [draft, setDraft] = useState(
376
+ mode === 'edit' ? anchor.exact_text : mode === 'insert' ? (initialDraft ?? '') : '',
377
+ );
378
+ const [isSubmitting, setIsSubmitting] = useState(false);
379
+ const isActionDisabled = mode === 'comment' ? !allowComments : readOnly;
380
+
381
+ const changeDraft = (nextDraft: string) => {
382
+ setDraft(nextDraft);
383
+ updateDraft(nextDraft);
384
+ };
385
+
386
+ const submitDraft = async () => {
387
+ if (!draft.trim() || isSubmitting) return;
388
+ setIsSubmitting(true);
389
+ try {
390
+ await submit(anchor, mode, mode === 'comment' ? draft.trim() : draft);
391
+ } finally {
392
+ setIsSubmitting(false);
393
+ }
394
+ };
395
+
396
+ return (
397
+ <div className="mb-2 space-y-2">
398
+ {mode === 'edit' || mode === 'insert' ? (
399
+ <Suspense
400
+ fallback={
401
+ <Textarea
402
+ value={draft}
403
+ onChange={(event) => changeDraft(event.target.value)}
404
+ placeholder={t('agent.editSelectionPlaceholder')}
405
+ rows={6}
406
+ autoFocus
407
+ />
408
+ }
409
+ >
410
+ <MarkdownComponentEditor
411
+ value={draft}
412
+ onChange={changeDraft}
413
+ artifactRunId={artifactRunId}
414
+ editable={!readOnly}
415
+ externalValueSync="manual"
416
+ autoFocus
417
+ />
418
+ </Suspense>
419
+ ) : (
420
+ <Textarea
421
+ value={draft}
422
+ onChange={(event) => changeDraft(event.target.value)}
423
+ placeholder={t('agent.commentOnSelectionPlaceholder')}
424
+ rows={3}
425
+ autoFocus
426
+ disabled={!allowComments}
427
+ />
428
+ )}
429
+ <div className="flex justify-end gap-2">
430
+ <Button variant="outline" size="sm" onClick={cancelEditing} disabled={isSubmitting}>
431
+ <X className="me-1 size-4" />
432
+ {t('store.cancelEdit')}
433
+ </Button>
434
+ <Button
435
+ size="sm"
436
+ onClick={() => void submitDraft()}
437
+ disabled={isActionDisabled || !draft.trim() || isSubmitting}
438
+ >
439
+ <Send className="me-1 size-4" />
440
+ {t('agent.send')}
441
+ </Button>
442
+ </div>
443
+ </div>
444
+ );
445
+ }
446
+
447
+ function createBlockComponent(
448
+ tag: keyof React.JSX.IntrinsicElements,
449
+ blockType: MarkdownBlockType,
450
+ ExistingComponent: React.ElementType | undefined,
451
+ ) {
452
+ return function CollaborativeBlock({ node, children: blockChildren, ...props }: MarkdownBlockProps) {
453
+ const { t } = useUITranslation();
454
+ const {
455
+ markdown,
456
+ readOnly,
457
+ allowComments,
458
+ activeEditing,
459
+ highlightChangesFrom,
460
+ flashChangedBlocks,
461
+ beginEditing,
462
+ } = useCollaborativeMarkdownContext();
463
+ const isNested = useContext(NestedMarkdownBlockContext);
464
+ const rendered = ExistingComponent
465
+ ? React.createElement(ExistingComponent, { node, ...props }, blockChildren)
466
+ : React.createElement(tag, props, blockChildren);
467
+
468
+ // Blocks inside a selectable container (a bullet's paragraph, a nested list, a
469
+ // quoted paragraph) are edited through their container, not individually.
470
+ if (isNested) return rendered;
471
+
472
+ const anchor = createMarkdownBlockLocator(markdown, node, blockType);
473
+ const hasAnchor = Boolean(anchor.source_range && anchor.exact_text);
474
+ const isEditable = !readOnly && hasAnchor;
475
+ const isCommentable = allowComments && hasAnchor;
476
+ const isSelected = activeEditing?.anchor.block_id === anchor.block_id;
477
+ // While editing a block's content, the editor replaces the rendered block. Comment/insert
478
+ // keep the rendered block visible as context.
479
+ const isEditingContent = isSelected && activeEditing?.mode === 'edit';
480
+ const isChanged =
481
+ flashChangedBlocks &&
482
+ Boolean(anchor.exact_text) &&
483
+ Boolean(highlightChangesFrom) &&
484
+ !highlightChangesFrom?.includes(anchor.exact_text);
485
+ const containerClassName = cn(
486
+ 'group/collab relative rounded-md border transition-colors duration-700',
487
+ // Negative margins keep the document's flow spacing while giving the hover/selection box
488
+ // padding on all sides — otherwise text (notably the first block, whose prose margin-top
489
+ // is 0) sits flush against the top of its highlight box.
490
+ '-mx-2 -my-1 px-2 py-1',
491
+ isEditingContent
492
+ ? 'border-transparent'
493
+ : isSelected
494
+ ? 'border-info bg-mixer-info/5'
495
+ : isEditable || isCommentable
496
+ ? 'border-transparent hover:border-mixer-info/30 hover:bg-mixer-muted/10'
497
+ : 'border-transparent',
498
+ isChanged && 'border-mixer-success/50 bg-mixer-success/15 ring-1 ring-mixer-success/20',
499
+ );
500
+ const showControls = isEditable || isCommentable || isSelected;
501
+ const controls = showControls ? (
502
+ <>
503
+ <div
504
+ className={cn(
505
+ 'absolute end-1 top-1 z-10 flex items-center gap-0.5 rounded-md border bg-background p-0.5 shadow-sm',
506
+ ((!isEditable && !isCommentable) || isEditingContent) && 'hidden',
507
+ isSelected
508
+ ? 'opacity-100'
509
+ : 'pointer-events-none opacity-0 group-hover/collab:pointer-events-auto group-hover/collab:opacity-100 group-focus-within/collab:pointer-events-auto group-focus-within/collab:opacity-100',
510
+ )}
511
+ >
512
+ {isCommentable ? (
513
+ <VTooltip description={t('agent.commentOnSelection')} asChild>
514
+ <Button
515
+ variant="ghost"
516
+ size="sm"
517
+ className="h-7 w-7 p-0"
518
+ aria-label={t('agent.commentOnSelection')}
519
+ onClick={() => beginEditing(anchor, 'comment')}
520
+ >
521
+ <MessageSquare className="size-4" />
522
+ </Button>
523
+ </VTooltip>
524
+ ) : null}
525
+ {isEditable ? (
526
+ <VTooltip description={t('agent.editSelection')} asChild>
527
+ <Button
528
+ variant="ghost"
529
+ size="sm"
530
+ className="h-7 w-7 p-0"
531
+ aria-label={t('agent.editSelection')}
532
+ onClick={() => beginEditing(anchor, 'edit')}
533
+ >
534
+ <Pencil className="size-4" />
535
+ </Button>
536
+ </VTooltip>
537
+ ) : null}
538
+ {isEditable ? (
539
+ <DropdownMenu>
540
+ <VTooltip description={t('agent.insertComponent')} asChild>
541
+ <DropdownMenuTrigger asChild>
542
+ <Button
543
+ variant="ghost"
544
+ size="sm"
545
+ className="h-7 w-7 p-0"
546
+ aria-label={t('agent.insertComponent')}
547
+ >
548
+ <Plus className="size-4" />
549
+ </Button>
550
+ </DropdownMenuTrigger>
551
+ </VTooltip>
552
+ <DropdownMenuContent align="end" className="w-52">
553
+ <DropdownMenuItem
554
+ onClick={() => beginEditing(anchor, 'insert', t('richText.paragraph'))}
555
+ >
556
+ <Pilcrow />
557
+ {t('richText.paragraph')}
558
+ </DropdownMenuItem>
559
+ <DropdownMenuItem
560
+ onClick={() => beginEditing(anchor, 'insert', `## ${t('richText.heading2')}`)}
561
+ >
562
+ <Heading2 />
563
+ {t('richText.heading2')}
564
+ </DropdownMenuItem>
565
+ <DropdownMenuItem
566
+ onClick={() => beginEditing(anchor, 'insert', `- ${t('richText.bulletList')}`)}
567
+ >
568
+ <List />
569
+ {t('richText.bulletList')}
570
+ </DropdownMenuItem>
571
+ <DropdownMenuItem
572
+ onClick={() => beginEditing(anchor, 'insert', `> ${t('richText.blockquote')}`)}
573
+ >
574
+ <Quote />
575
+ {t('richText.blockquote')}
576
+ </DropdownMenuItem>
577
+ <DropdownMenuItem onClick={() => beginEditing(anchor, 'insert', '```\nCode\n```')}>
578
+ <Code2 />
579
+ {t('richText.codeBlock')}
580
+ </DropdownMenuItem>
581
+ <DropdownMenuItem
582
+ onClick={() =>
583
+ beginEditing(anchor, 'insert', '| Column | Value |\n| --- | --- |\n| A | 1 |')
584
+ }
585
+ >
586
+ <Table2 />
587
+ {t('richText.table')}
588
+ </DropdownMenuItem>
589
+ <DropdownMenuItem
590
+ onClick={() =>
591
+ beginEditing(
592
+ anchor,
593
+ 'insert',
594
+ `[${t('agent.insertLink')}](https://example.com)`,
595
+ )
596
+ }
597
+ >
598
+ <Link />
599
+ {t('agent.insertLink')}
600
+ </DropdownMenuItem>
601
+ <DropdownMenuItem
602
+ onClick={() =>
603
+ beginEditing(
604
+ anchor,
605
+ 'insert',
606
+ '```chart\n{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","data":{"values":[{"category":"A","value":1}]},"mark":"bar","encoding":{"x":{"field":"category","type":"nominal"},"y":{"field":"value","type":"quantitative"}}}\n```',
607
+ )
608
+ }
609
+ >
610
+ <BarChart3 />
611
+ {t('agent.insertChart')}
612
+ </DropdownMenuItem>
613
+ </DropdownMenuContent>
614
+ </DropdownMenu>
615
+ ) : null}
616
+ </div>
617
+ {isSelected && activeEditing ? (
618
+ <CollaborativeBlockEditor
619
+ key={`${activeEditing.anchor.block_id}:${activeEditing.mode}:${activeEditing.initialDraft ?? ''}`}
620
+ anchor={activeEditing.anchor}
621
+ mode={activeEditing.mode}
622
+ initialDraft={activeEditing.initialDraft}
623
+ />
624
+ ) : null}
625
+ </>
626
+ ) : null;
627
+
628
+ const content = CONTAINER_BLOCK_TYPES.has(blockType) ? (
629
+ <NestedMarkdownBlockContext.Provider value={true}>{rendered}</NestedMarkdownBlockContext.Provider>
630
+ ) : (
631
+ rendered
632
+ );
633
+
634
+ return (
635
+ <div className={containerClassName} data-collaborative-block={anchor.block_id}>
636
+ {isEditingContent ? null : content}
637
+ {controls}
638
+ </div>
639
+ );
640
+ };
641
+ }
642
+
643
+ export function CollaborativeMarkdownRenderer({
644
+ children: markdown,
645
+ resource,
646
+ baseVersion,
647
+ artifactRunId,
648
+ components,
649
+ readOnly = false,
650
+ allowComments = !readOnly,
651
+ highlightChangesFrom,
652
+ highlightVersion = 0,
653
+ onAction,
654
+ className,
655
+ ...markdownProps
656
+ }: CollaborativeMarkdownRendererProps) {
657
+ const { t } = useUITranslation();
658
+ const [activeEditing, setActiveEditing] = useState<ActiveEditing>();
659
+ const [orphanedDraft, setOrphanedDraft] = useState<OrphanedDraft>();
660
+ const [flashChangedBlocks, setFlashChangedBlocks] = useState(false);
661
+ const draftRef = useRef('');
662
+
663
+ useEffect(() => {
664
+ if (!highlightChangesFrom || highlightVersion <= 0) return;
665
+ setFlashChangedBlocks(true);
666
+ const timeoutId = window.setTimeout(() => setFlashChangedBlocks(false), 1800);
667
+ return () => window.clearTimeout(timeoutId);
668
+ }, [highlightChangesFrom, highlightVersion]);
669
+
670
+ useEffect(() => {
671
+ const range = activeEditing?.anchor.source_range;
672
+ if (!activeEditing || !range) return;
673
+ if (markdown.slice(range.start, range.end) === activeEditing.anchor.exact_text) return;
674
+
675
+ const draft = draftRef.current;
676
+ const initialDraft =
677
+ activeEditing.mode === 'edit'
678
+ ? activeEditing.anchor.exact_text
679
+ : activeEditing.mode === 'insert'
680
+ ? (activeEditing.initialDraft ?? '')
681
+ : '';
682
+ if (draft.trim() && draft !== initialDraft) {
683
+ setOrphanedDraft({ ...activeEditing, draft });
684
+ }
685
+ setActiveEditing(undefined);
686
+ }, [activeEditing, markdown]);
687
+
688
+ const collaborativeComponents = useMemo<Components>(() => {
689
+ return {
690
+ ...components,
691
+ p: createBlockComponent('p', 'paragraph', components?.p as React.ElementType | undefined),
692
+ h1: createBlockComponent('h1', 'heading', components?.h1 as React.ElementType | undefined),
693
+ h2: createBlockComponent('h2', 'heading', components?.h2 as React.ElementType | undefined),
694
+ h3: createBlockComponent('h3', 'heading', components?.h3 as React.ElementType | undefined),
695
+ h4: createBlockComponent('h4', 'heading', components?.h4 as React.ElementType | undefined),
696
+ h5: createBlockComponent('h5', 'heading', components?.h5 as React.ElementType | undefined),
697
+ h6: createBlockComponent('h6', 'heading', components?.h6 as React.ElementType | undefined),
698
+ ul: createBlockComponent('ul', 'list', components?.ul as React.ElementType | undefined),
699
+ ol: createBlockComponent('ol', 'list', components?.ol as React.ElementType | undefined),
700
+ blockquote: createBlockComponent(
701
+ 'blockquote',
702
+ 'blockquote',
703
+ components?.blockquote as React.ElementType | undefined,
704
+ ),
705
+ pre: createBlockComponent('pre', 'code_block', components?.pre as React.ElementType | undefined),
706
+ table: createBlockComponent('table', 'table', components?.table as React.ElementType | undefined),
707
+ hr: createBlockComponent('hr', 'separator', components?.hr as React.ElementType | undefined),
708
+ };
709
+ }, [components]);
710
+
711
+ const beginEditing = useCallback(
712
+ (anchor: MarkdownBlockAnchor, mode: EditingMode, initialDraft?: string) => {
713
+ if ((mode === 'comment' && !allowComments) || (mode !== 'comment' && readOnly)) return;
714
+ const enrichedAnchor = enrichMarkdownBlockAnchor(markdown, anchor);
715
+ draftRef.current = mode === 'edit' ? enrichedAnchor.exact_text : (initialDraft ?? '');
716
+ setActiveEditing({ anchor: enrichedAnchor, mode, initialDraft });
717
+ },
718
+ [allowComments, markdown, readOnly],
719
+ );
720
+
721
+ const cancelEditing = useCallback(() => {
722
+ draftRef.current = '';
723
+ setActiveEditing(undefined);
724
+ }, []);
725
+
726
+ const updateDraft = useCallback((draft: string) => {
727
+ draftRef.current = draft;
728
+ }, []);
729
+
730
+ const submit = useCallback(
731
+ async (anchor: MarkdownBlockAnchor, mode: EditingMode, draft: string) => {
732
+ if ((mode === 'comment' && !allowComments) || (mode !== 'comment' && readOnly)) return;
733
+ const after = mode === 'insert' ? `${anchor.exact_text.replace(/\s+$/, '')}\n\n${draft.trim()}` : draft;
734
+ const action: MarkdownEditingAction = {
735
+ operation_id: createOperationId(),
736
+ resource,
737
+ base_version: baseVersion,
738
+ action: mode === 'insert' ? 'edit' : mode,
739
+ anchor,
740
+ ...(mode === 'comment' ? { comment: draft } : { user_change: { before: anchor.exact_text, after } }),
741
+ };
742
+
743
+ await onAction(action);
744
+ draftRef.current = '';
745
+ setActiveEditing(undefined);
746
+ },
747
+ [allowComments, baseVersion, onAction, readOnly, resource],
748
+ );
749
+
750
+ const contextValue = useMemo<CollaborativeMarkdownContextValue>(
751
+ () => ({
752
+ markdown,
753
+ artifactRunId,
754
+ readOnly,
755
+ allowComments,
756
+ activeEditing,
757
+ highlightChangesFrom,
758
+ flashChangedBlocks,
759
+ beginEditing,
760
+ cancelEditing,
761
+ updateDraft,
762
+ submit,
763
+ }),
764
+ [
765
+ activeEditing,
766
+ allowComments,
767
+ artifactRunId,
768
+ beginEditing,
769
+ cancelEditing,
770
+ flashChangedBlocks,
771
+ highlightChangesFrom,
772
+ markdown,
773
+ readOnly,
774
+ submit,
775
+ updateDraft,
776
+ ],
777
+ );
778
+
779
+ return (
780
+ <CollaborativeMarkdownContext.Provider value={contextValue}>
781
+ {orphanedDraft ? (
782
+ <div className="not-prose mb-3 rounded-md border border-mixer-attention/35 bg-mixer-attention/10 p-3">
783
+ <div className="flex items-start justify-between gap-3">
784
+ <div>
785
+ <div className="text-sm font-semibold text-foreground">
786
+ {t('agent.documentEditingDraftPreserved')}
787
+ </div>
788
+ <div className="mt-0.5 text-xs leading-4 text-muted">
789
+ {t('agent.documentEditingDraftPreservedDescription')}
790
+ </div>
791
+ </div>
792
+ <Button
793
+ variant="ghost"
794
+ size="sm"
795
+ className="h-7 w-7 shrink-0 p-0"
796
+ onClick={() => setOrphanedDraft(undefined)}
797
+ aria-label={t('agent.close')}
798
+ >
799
+ <X className="size-4" />
800
+ </Button>
801
+ </div>
802
+ <Textarea
803
+ className="mt-2"
804
+ value={orphanedDraft.draft}
805
+ onChange={(event) =>
806
+ setOrphanedDraft((current) =>
807
+ current ? { ...current, draft: event.target.value } : current,
808
+ )
809
+ }
810
+ rows={4}
811
+ />
812
+ </div>
813
+ ) : null}
814
+ <MarkdownRenderer
815
+ {...markdownProps}
816
+ artifactRunId={artifactRunId}
817
+ className={className}
818
+ components={collaborativeComponents}
819
+ preserveSourcePositions
820
+ >
821
+ {markdown}
822
+ </MarkdownRenderer>
823
+ </CollaborativeMarkdownContext.Provider>
824
+ );
825
+ }