@opengeni/react 0.46.1 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +158 -14
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  23. package/dist/chunk-FK6VG4LL.js.map +1 -0
  24. package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
  25. package/dist/chunk-FWS3KC3Z.js.map +1 -0
  26. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  27. package/dist/chunk-HDBG7XGO.js.map +1 -0
  28. package/dist/chunk-HUIKIDM5.js +2148 -0
  29. package/dist/chunk-HUIKIDM5.js.map +1 -0
  30. package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
  31. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  32. package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
  33. package/dist/chunk-JR3QFY6B.js.map +1 -0
  34. package/dist/chunk-JVYQRRC4.js +53 -0
  35. package/dist/chunk-JVYQRRC4.js.map +1 -0
  36. package/dist/chunk-KCGXPP5V.js +157 -0
  37. package/dist/chunk-KCGXPP5V.js.map +1 -0
  38. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  39. package/dist/chunk-KZ73RL76.js.map +1 -0
  40. package/dist/chunk-PEI6RANI.js +3234 -0
  41. package/dist/chunk-PEI6RANI.js.map +1 -0
  42. package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
  43. package/dist/chunk-ROIRTM37.js.map +1 -0
  44. package/dist/chunk-VZTQ73XT.js +86 -0
  45. package/dist/chunk-VZTQ73XT.js.map +1 -0
  46. package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
  47. package/dist/chunk-WSK7G5LE.js.map +1 -0
  48. package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
  49. package/dist/chunk-WYA65Y3F.js.map +1 -0
  50. package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
  51. package/dist/chunk-X4S56Y2H.js.map +1 -0
  52. package/dist/client.d.ts +7 -1
  53. package/dist/clipboard.d.ts +1 -0
  54. package/dist/clipboard.js +7 -0
  55. package/dist/clipboard.js.map +1 -0
  56. package/dist/commands/index.d.ts +3 -3
  57. package/dist/commands/registry.d.ts +1 -1
  58. package/dist/commands/types.d.ts +1 -1
  59. package/dist/components/approval-surface.d.ts +2 -2
  60. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  61. package/dist/components/artifacts/document-editor.d.ts +172 -0
  62. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  63. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  64. package/dist/components/artifacts/editable-document.d.ts +24 -0
  65. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  66. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  67. package/dist/components/artifacts/index.d.ts +8 -0
  68. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  69. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  70. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  71. package/dist/components/browser-viewer.d.ts +29 -0
  72. package/dist/components/chat-composer.d.ts +13 -8
  73. package/dist/components/code-editor.d.ts +25 -3
  74. package/dist/components/command-palette.d.ts +2 -2
  75. package/dist/components/composer-transcription-control.d.ts +4 -2
  76. package/dist/components/composer.d.ts +29 -15
  77. package/dist/components/computer-viewer.d.ts +23 -0
  78. package/dist/components/copy-button.d.ts +2 -2
  79. package/dist/components/desktop-viewer.d.ts +25 -21
  80. package/dist/components/diff-view.d.ts +1 -1
  81. package/dist/components/enrollment-consent.d.ts +1 -1
  82. package/dist/components/enrollment-device-flow.d.ts +1 -1
  83. package/dist/components/file-browser.d.ts +2 -2
  84. package/dist/components/fleet-tile.d.ts +1 -1
  85. package/dist/components/generated-video-player.d.ts +10 -0
  86. package/dist/components/human-input-form.d.ts +1 -1
  87. package/dist/components/human-input-surface.d.ts +2 -2
  88. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  89. package/dist/components/machine-card.d.ts +2 -2
  90. package/dist/components/machine-dock-bar.d.ts +3 -3
  91. package/dist/components/machine-health-pill.d.ts +2 -2
  92. package/dist/components/machine-input-display.d.ts +1 -1
  93. package/dist/components/machine-metrics.d.ts +2 -2
  94. package/dist/components/machine-status-pill.d.ts +4 -4
  95. package/dist/components/machines/health.d.ts +1 -1
  96. package/dist/components/machines/machine-detail.d.ts +5 -3
  97. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  98. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  99. package/dist/components/machines/series.d.ts +2 -2
  100. package/dist/components/machines-dashboard.d.ts +5 -5
  101. package/dist/components/markdown.d.ts +1 -1
  102. package/dist/components/message-timeline.d.ts +22 -5
  103. package/dist/components/model-picker.d.ts +2 -2
  104. package/dist/components/model-policy-picker.d.ts +7 -7
  105. package/dist/components/pierre-diff.d.ts +1 -1
  106. package/dist/components/pierre-file.d.ts +1 -1
  107. package/dist/components/queue-draft-policy.d.ts +1 -1
  108. package/dist/components/queue-surface-implementation.d.ts +3 -3
  109. package/dist/components/queue-surface-state.d.ts +4 -4
  110. package/dist/components/queue-surface.d.ts +5 -5
  111. package/dist/components/sandbox-files.d.ts +5 -5
  112. package/dist/components/sandbox-terminal.d.ts +50 -4
  113. package/dist/components/sandbox-workspace.d.ts +30 -10
  114. package/dist/components/session-chrome.d.ts +4 -4
  115. package/dist/components/session-status.d.ts +2 -2
  116. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  117. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  118. package/dist/components/timeline-annotations.d.ts +13 -0
  119. package/dist/components/tooltip.d.ts +4 -4
  120. package/dist/components/user-message-body.d.ts +2 -2
  121. package/dist/components/workbench-changes.d.ts +2 -2
  122. package/dist/components/workspace-dock.d.ts +3 -1
  123. package/dist/composer.d.ts +11 -11
  124. package/dist/composer.js +7 -7
  125. package/dist/hooks/internal.d.ts +7 -1
  126. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  130. package/dist/hooks/use-browser-identities.d.ts +26 -0
  131. package/dist/hooks/use-browser-session.d.ts +28 -0
  132. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  133. package/dist/hooks/use-codex-accounts.d.ts +3 -3
  134. package/dist/hooks/use-composer.d.ts +36 -9
  135. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  136. package/dist/hooks/use-computer-session.d.ts +28 -0
  137. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  138. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  139. package/dist/hooks/use-environments.d.ts +11 -5
  140. package/dist/hooks/use-file-attachments.d.ts +1 -1
  141. package/dist/hooks/use-goal.d.ts +2 -2
  142. package/dist/hooks/use-human-input.d.ts +2 -2
  143. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  144. package/dist/hooks/use-machine-chip.d.ts +2 -2
  145. package/dist/hooks/use-machines.d.ts +10 -3
  146. package/dist/hooks/use-packs.d.ts +7 -2
  147. package/dist/hooks/use-rigs.d.ts +1 -1
  148. package/dist/hooks/use-sandbox-files.d.ts +25 -4
  149. package/dist/hooks/use-sandbox-git.d.ts +13 -5
  150. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  151. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  152. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  153. package/dist/hooks/use-session-control.d.ts +1 -1
  154. package/dist/hooks/use-session-events.d.ts +2 -2
  155. package/dist/hooks/use-session-lineage.d.ts +1 -1
  156. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  157. package/dist/hooks/use-session.d.ts +2 -2
  158. package/dist/hooks/use-slash-commands.d.ts +1 -1
  159. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  160. package/dist/hooks/use-transcription.d.ts +2 -2
  161. package/dist/hooks/use-turn-queue.d.ts +2 -2
  162. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  163. package/dist/hooks/use-voice-input.d.ts +6 -4
  164. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  165. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  166. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  167. package/dist/hooks/use-workspaces.d.ts +1 -1
  168. package/dist/index.d.ts +158 -154
  169. package/dist/index.js +1718 -908
  170. package/dist/index.js.map +1 -1
  171. package/dist/interaction.d.ts +23 -0
  172. package/dist/interaction.js +36 -0
  173. package/dist/interaction.js.map +1 -0
  174. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  175. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  176. package/dist/lib/composer-responsive-context.d.ts +9 -0
  177. package/dist/lib/relay-wire.d.ts +4 -0
  178. package/dist/lib/sandbox-liveness.d.ts +9 -0
  179. package/dist/lib/terminal-capability.d.ts +5 -0
  180. package/dist/lib/use-portal-token-style.d.ts +14 -3
  181. package/dist/lib/xterm-theme.d.ts +1 -1
  182. package/dist/machines.d.ts +30 -30
  183. package/dist/machines.js +3 -3
  184. package/dist/provider.d.ts +4 -4
  185. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
  186. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  187. package/dist/realtime/dropdown-menu.d.ts +8 -8
  188. package/dist/realtime/realtime-control.d.ts +15 -6
  189. package/dist/realtime.d.ts +4 -4
  190. package/dist/realtime.js +30 -33
  191. package/dist/realtime.js.map +1 -1
  192. package/dist/session-context.d.ts +19 -1
  193. package/dist/session-ui.d.ts +14 -14
  194. package/dist/session-ui.js +6 -5
  195. package/dist/session.d.ts +28 -28
  196. package/dist/session.js +5 -5
  197. package/dist/timeline/activity-rail.d.ts +6 -3
  198. package/dist/timeline/disclosure-context.d.ts +1 -1
  199. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  200. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  201. package/dist/timeline/index.d.ts +19 -19
  202. package/dist/timeline/parsers.d.ts +6 -3
  203. package/dist/timeline/projection.d.ts +3 -3
  204. package/dist/timeline/registry.d.ts +10 -1
  205. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  206. package/dist/timeline/shared.d.ts +21 -10
  207. package/dist/timeline/tool-diff.d.ts +2 -2
  208. package/dist/timeline/tool-renderers.d.ts +1 -1
  209. package/dist/timeline/turn-summary.d.ts +3 -3
  210. package/dist/timeline/types.d.ts +42 -2
  211. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  212. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  213. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  214. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  215. package/dist/voice-recording-store.d.ts +8 -2
  216. package/package.json +52 -7
  217. package/src/artifacts-document.ts +31 -0
  218. package/src/artifacts-presentation.ts +36 -0
  219. package/src/artifacts-spreadsheet.ts +21 -0
  220. package/src/artifacts.ts +86 -0
  221. package/src/client.ts +90 -1
  222. package/src/clipboard.ts +2 -0
  223. package/src/components/artifacts/artifact-surface.tsx +93 -0
  224. package/src/components/artifacts/document-editor.tsx +2255 -0
  225. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  226. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  227. package/src/components/artifacts/editable-document.tsx +657 -0
  228. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  229. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  230. package/src/components/artifacts/index.ts +99 -0
  231. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  232. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  233. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  234. package/src/components/browser-viewer.tsx +1592 -0
  235. package/src/components/chat-composer.tsx +25 -2
  236. package/src/components/code-editor.tsx +90 -31
  237. package/src/components/command-palette.tsx +1 -1
  238. package/src/components/composer-transcription-control.tsx +51 -22
  239. package/src/components/composer.tsx +49 -23
  240. package/src/components/computer-viewer.tsx +1139 -0
  241. package/src/components/copy-button.tsx +1 -1
  242. package/src/components/desktop-viewer.tsx +103 -75
  243. package/src/components/file-browser.tsx +39 -17
  244. package/src/components/generated-video-player.tsx +89 -0
  245. package/src/components/human-input-form.tsx +2 -2
  246. package/src/components/interactive-workbench-viewers.ts +2 -0
  247. package/src/components/machine-card.tsx +1 -1
  248. package/src/components/machine-input-display.ts +3 -0
  249. package/src/components/machines/machine-detail.tsx +39 -15
  250. package/src/components/machines-dashboard.tsx +7 -6
  251. package/src/components/message-timeline.tsx +327 -37
  252. package/src/components/model-picker.tsx +2 -2
  253. package/src/components/model-policy-picker.tsx +16 -15
  254. package/src/components/queue-surface-implementation.tsx +135 -75
  255. package/src/components/queue-surface-state.tsx +2 -2
  256. package/src/components/sandbox-files.tsx +142 -62
  257. package/src/components/sandbox-terminal.tsx +119 -35
  258. package/src/components/sandbox-workspace.tsx +394 -228
  259. package/src/components/session-chrome.tsx +67 -27
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/tooltip.tsx +13 -16
  264. package/src/components/user-message-body.tsx +1 -1
  265. package/src/components/workbench-changes.tsx +192 -35
  266. package/src/components/workspace-dock.tsx +13 -5
  267. package/src/composer.ts +3 -0
  268. package/src/hooks/internal.ts +132 -38
  269. package/src/hooks/use-attached-browsers.ts +152 -0
  270. package/src/hooks/use-browser-frame-stream.ts +316 -0
  271. package/src/hooks/use-browser-identities.ts +225 -0
  272. package/src/hooks/use-browser-session.ts +476 -0
  273. package/src/hooks/use-browser-sessions.ts +281 -0
  274. package/src/hooks/use-codex-accounts.ts +4 -4
  275. package/src/hooks/use-composer.ts +769 -24
  276. package/src/hooks/use-computer-frame-stream.ts +311 -0
  277. package/src/hooks/use-computer-session.ts +442 -0
  278. package/src/hooks/use-computer-sessions.ts +245 -0
  279. package/src/hooks/use-environments.ts +24 -5
  280. package/src/hooks/use-file-attachments.ts +70 -32
  281. package/src/hooks/use-goal.ts +17 -5
  282. package/src/hooks/use-machine-chip.ts +5 -3
  283. package/src/hooks/use-machines.ts +79 -3
  284. package/src/hooks/use-packs.ts +51 -0
  285. package/src/hooks/use-sandbox-files.ts +382 -88
  286. package/src/hooks/use-sandbox-git.ts +208 -46
  287. package/src/hooks/use-sandbox-terminal.ts +3 -2
  288. package/src/hooks/use-session-capabilities.ts +94 -70
  289. package/src/hooks/use-session-events.ts +18 -4
  290. package/src/hooks/use-terminal-stream.ts +102 -23
  291. package/src/hooks/use-transcription.ts +4 -14
  292. package/src/hooks/use-turn-queue.ts +17 -5
  293. package/src/hooks/use-video-artifact-playback.ts +28 -0
  294. package/src/hooks/use-voice-input.ts +163 -19
  295. package/src/hooks/use-windowed-sections.ts +9 -7
  296. package/src/hooks/use-workspace-edit.ts +2 -1
  297. package/src/index.ts +21 -6
  298. package/src/interaction.ts +71 -0
  299. package/src/lib/composer-responsive-context.ts +15 -0
  300. package/src/lib/relay-wire.ts +4 -0
  301. package/src/lib/sandbox-liveness.ts +11 -0
  302. package/src/lib/terminal-capability.ts +10 -0
  303. package/src/lib/use-portal-token-style.ts +60 -15
  304. package/src/provider.tsx +52 -7
  305. package/src/realtime/dropdown-menu.tsx +15 -19
  306. package/src/realtime/realtime-control.tsx +16 -5
  307. package/src/session-context.ts +90 -0
  308. package/src/timeline/activity-rail.tsx +31 -6
  309. package/src/timeline/index.ts +4 -1
  310. package/src/timeline/parsers.ts +46 -20
  311. package/src/timeline/projection.ts +233 -13
  312. package/src/timeline/registry.ts +19 -0
  313. package/src/timeline/screenshot-lightbox.tsx +30 -9
  314. package/src/timeline/shared.tsx +59 -11
  315. package/src/timeline/tool-renderers.tsx +421 -24
  316. package/src/timeline/turn-summary.tsx +2 -2
  317. package/src/timeline/types.ts +58 -2
  318. package/src/voice-recording-store.ts +15 -0
  319. package/styles/compiled.css +5298 -0
  320. package/styles/compiled.d.ts +2 -0
  321. package/styles/effective-tokens.css +27 -0
  322. package/styles/index.css +18 -0
  323. package/styles/responsive.css +208 -0
  324. package/styles/responsive.d.ts +2 -0
  325. package/styles/tokens.css +50 -17
  326. package/dist/chunk-3PB7MIT6.js.map +0 -1
  327. package/dist/chunk-6EDV6YFE.js.map +0 -1
  328. package/dist/chunk-EB67J347.js.map +0 -1
  329. package/dist/chunk-EKZH6IDG.js.map +0 -1
  330. package/dist/chunk-FGZUCXZF.js.map +0 -1
  331. package/dist/chunk-HFO4ERGQ.js.map +0 -1
  332. package/dist/chunk-JB5725BI.js +0 -48
  333. package/dist/chunk-JB5725BI.js.map +0 -1
  334. package/dist/chunk-LGVMUIRV.js.map +0 -1
  335. package/dist/chunk-LWR4MXSS.js.map +0 -1
  336. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  337. package/dist/chunk-U3CSCMWA.js.map +0 -1
  338. package/dist/chunk-VCBFUSUZ.js.map +0 -1
  339. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  340. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  341. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
@@ -0,0 +1,1020 @@
1
+ import {
2
+ PRESENTATION_ARTIFACT_COMMAND_VERSION,
3
+ PRESENTATION_ARTIFACT_QUERY_MAX_NODES,
4
+ PRESENTATION_ARTIFACT_QUERY_MAX_SLIDES,
5
+ PRESENTATION_ARTIFACT_QUERY_MAX_TEXT_BYTES,
6
+ PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES,
7
+ type EditableArtifactSession,
8
+ type EditablePresentationEditorSlideProjection,
9
+ type PresentationArtifactCommand,
10
+ type PresentationArtifactEditorSceneNode,
11
+ type PresentationArtifactFill,
12
+ type PresentationArtifactLine,
13
+ type PresentationArtifactNodeKind,
14
+ type PresentationArtifactRichText,
15
+ type PresentationArtifactSlideCatalogItem,
16
+ type PresentationArtifactTextParagraph,
17
+ type PresentationArtifactTextStyle,
18
+ } from "@opengeni/sdk/editable-artifacts";
19
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
20
+
21
+ import {
22
+ PresentationProjectionArtifactSurface,
23
+ type PresentationCommit,
24
+ type PresentationCommitHandler,
25
+ type PresentationEditorProjection,
26
+ type PresentationProjectionArtifactSurfaceProps,
27
+ type PresentationProjectionElement,
28
+ type PresentationProjectionFill,
29
+ type PresentationProjectionLine,
30
+ type PresentationProjectionPosition,
31
+ type PresentationProjectionRichText,
32
+ type PresentationProjectionTextStyle,
33
+ type PresentationSlideProjection,
34
+ } from "./presentation-editor";
35
+ import { ArtifactSurface } from "./artifact-surface";
36
+ import {
37
+ asEditableArtifactError,
38
+ editableArtifactAccessRevoked,
39
+ editableArtifactProjectionKey,
40
+ editableArtifactStatusLabel,
41
+ EditableArtifactMessage,
42
+ useEditableArtifactView,
43
+ } from "./editable-artifact-ui";
44
+
45
+ const EMU_PER_CSS_PIXEL = 9_525;
46
+ const PRESENTATION_CATALOG_PAGE = 128;
47
+ const PRESENTATION_MAX_INTERACTIVE_SLIDES = 256;
48
+ const PRESENTATION_MAX_INTERACTIVE_NODES = 16_384;
49
+ const PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES = 4 * 1024 * 1024;
50
+ const PRESENTATION_MAX_CATALOG_PAGES = 32;
51
+ const PRESENTATION_SCENE_CONCURRENCY = 4;
52
+ const PRESENTATION_COMPOSITION_ATTEMPTS = 3;
53
+ const DEFAULT_PRESENTATION_TEXT_STYLE: PresentationArtifactTextStyle = Object.freeze({
54
+ fontFamily: "Arial",
55
+ fontSizeCentipoints: 1_800,
56
+ color: 0x000000ff,
57
+ bold: false,
58
+ italic: false,
59
+ underline: false,
60
+ language: null,
61
+ });
62
+
63
+ export type EditablePresentationArtifactSurfaceProps = Omit<
64
+ PresentationProjectionArtifactSurfaceProps,
65
+ "projection" | "commit" | "readOnly" | "busy" | "subtitle"
66
+ > & {
67
+ session: EditableArtifactSession;
68
+ subtitle?: ReactNode | undefined;
69
+ readOnly?: boolean | undefined;
70
+ /** Called only after the durable SDK command has been accepted locally. */
71
+ onCommit?: PresentationCommitHandler | undefined;
72
+ };
73
+
74
+ export type ComposedPresentationProjection = Readonly<{
75
+ projection: PresentationEditorProjection;
76
+ sceneNodes: ReadonlyMap<string, PresentationArtifactEditorSceneNode>;
77
+ }>;
78
+
79
+ type PresentationProjectionState = {
80
+ composed: ComposedPresentationProjection | null;
81
+ loading: boolean;
82
+ error: Error | null;
83
+ };
84
+
85
+ /**
86
+ * Durable presentation editor over the public SDK. The Worker owns canonical
87
+ * state; React only receives bounded catalog and per-slide editor projections.
88
+ */
89
+ export function EditablePresentationArtifactSurface({
90
+ session,
91
+ title = "Presentation",
92
+ subtitle,
93
+ readOnly = false,
94
+ onCommit,
95
+ onCommandError,
96
+ ...surfaceProps
97
+ }: EditablePresentationArtifactSurfaceProps) {
98
+ const view = useEditableArtifactView(session);
99
+ const invalidator = editableArtifactProjectionKey(view);
100
+ const [retryEpoch, setRetryEpoch] = useState(0);
101
+ const [state, setState] = useState<PresentationProjectionState>({
102
+ composed: null,
103
+ loading: true,
104
+ error: null,
105
+ });
106
+ const loadGeneration = useRef(0);
107
+ const sceneNodes = useRef(new Map<string, PresentationArtifactEditorSceneNode>());
108
+
109
+ useEffect(() => {
110
+ const generation = ++loadGeneration.current;
111
+ let cancelled = false;
112
+ setState((current) => ({ ...current, loading: true, error: null }));
113
+ void composePresentationEditorProjection(session).then(
114
+ (composed) => {
115
+ if (cancelled || generation !== loadGeneration.current) return;
116
+ sceneNodes.current = new Map(composed.sceneNodes);
117
+ setState({ composed, loading: false, error: null });
118
+ },
119
+ (cause) => {
120
+ if (cancelled || generation !== loadGeneration.current) return;
121
+ setState({
122
+ composed: null,
123
+ loading: false,
124
+ error: asEditableArtifactError(cause, "Could not open this presentation"),
125
+ });
126
+ },
127
+ );
128
+ return () => {
129
+ cancelled = true;
130
+ };
131
+ }, [invalidator, retryEpoch, session]);
132
+
133
+ const refresh = useCallback(() => setRetryEpoch((value) => value + 1), []);
134
+ const writable = !readOnly && view.writable;
135
+ const applyCommit = useCallback(
136
+ async (commit: PresentationCommit) => {
137
+ const composed = state.composed;
138
+ if (!composed) throw new Error("Presentation projection is not ready");
139
+ if (!writable) throw new Error("This presentation is read only");
140
+ if (
141
+ commit.revision !== undefined &&
142
+ String(commit.revision) !== String(composed.projection.revision)
143
+ ) {
144
+ throw new Error("The presentation changed; retry this edit on the latest version");
145
+ }
146
+
147
+ if (commit.kind === "slide-insert") {
148
+ await session.createPresentationSlide({
149
+ slideId: commit.slideId,
150
+ index: commit.index,
151
+ });
152
+ notifyPresentationCommit(onCommit, onCommandError, commit);
153
+ refresh();
154
+ return;
155
+ }
156
+ if (commit.kind === "slide-delete") {
157
+ await session.applyPresentationCommands({
158
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
159
+ commands: [{ kind: "slide.delete", id: commit.slideId }],
160
+ });
161
+ notifyPresentationCommit(onCommit, onCommandError, commit);
162
+ refresh();
163
+ return;
164
+ }
165
+ if (commit.kind === "object-insert") {
166
+ await session.applyPresentationCommands({
167
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
168
+ commands: [
169
+ {
170
+ kind: "node.insert",
171
+ owner: { kind: "slide", id: commit.slideId },
172
+ parentId: null,
173
+ index: commit.index,
174
+ node: {
175
+ id: commit.objectId,
176
+ name: "Text box",
177
+ bounds: {
178
+ x: canonicalCoordinate(commit.position.left),
179
+ y: canonicalCoordinate(commit.position.top),
180
+ width: Math.max(1, canonicalCoordinate(commit.position.width)),
181
+ height: Math.max(1, canonicalCoordinate(commit.position.height)),
182
+ },
183
+ transform: { rotation: 0, flipHorizontal: false, flipVertical: false },
184
+ content: {
185
+ kind: "shape",
186
+ geometry: "text-box",
187
+ fill: { kind: "none" },
188
+ line: { fill: { kind: "none" }, width: 0, dash: "solid" },
189
+ text: emptyPresentationText(),
190
+ placeholder: null,
191
+ },
192
+ },
193
+ },
194
+ ],
195
+ });
196
+ notifyPresentationCommit(onCommit, onCommandError, commit);
197
+ refresh();
198
+ return;
199
+ }
200
+ if (commit.kind === "object-delete") {
201
+ await session.applyPresentationCommands({
202
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
203
+ commands: [{ kind: "node.delete", id: commit.objectId }],
204
+ });
205
+ notifyPresentationCommit(onCommit, onCommandError, commit);
206
+ refresh();
207
+ return;
208
+ }
209
+
210
+ const key = sceneNodeKey(commit.slideId, commit.objectId);
211
+ const node = sceneNodes.current.get(key);
212
+ if (!node) throw new Error("The edited presentation object is no longer available");
213
+ if (node.inherited || node.source.kind !== "slide" || node.source.id !== commit.slideId) {
214
+ throw new Error("Inherited master and layout objects are view only in the slide editor");
215
+ }
216
+
217
+ let command: PresentationArtifactCommand;
218
+ let nextNode: PresentationArtifactEditorSceneNode;
219
+ if (commit.kind === "text") {
220
+ if (node.content.kind !== "shape" || node.content.text === null) {
221
+ throw new Error("This presentation object does not contain editable text");
222
+ }
223
+ if (flattenPresentationRichText(node.content.text) !== commit.before) {
224
+ throw new Error("The presentation text changed; retry this edit");
225
+ }
226
+ const text = replacePresentationRichText(node.content.text, commit.before, commit.after);
227
+ const content: PresentationArtifactNodeKind = Object.freeze({ ...node.content, text });
228
+ command = { kind: "node.content.set", id: node.id, content };
229
+ nextNode = Object.freeze({ ...node, content });
230
+ } else {
231
+ const current = normalizeProjectedEditorPosition(projectPosition(node.bounds));
232
+ if (!sameProjectedPosition(current, commit.before)) {
233
+ throw new Error("The presentation object changed; retry this edit");
234
+ }
235
+ const bounds =
236
+ commit.kind === "move"
237
+ ? Object.freeze({
238
+ ...node.bounds,
239
+ x: canonicalCoordinate(commit.after.left),
240
+ y: canonicalCoordinate(commit.after.top),
241
+ })
242
+ : Object.freeze({
243
+ ...node.bounds,
244
+ width: Math.max(1, canonicalCoordinate(commit.after.width)),
245
+ height: Math.max(1, canonicalCoordinate(commit.after.height)),
246
+ });
247
+ command = { kind: "node.bounds.set", id: node.id, bounds };
248
+ nextNode = Object.freeze({ ...node, bounds });
249
+ }
250
+
251
+ await session.applyPresentationCommands({
252
+ version: PRESENTATION_ARTIFACT_COMMAND_VERSION,
253
+ commands: [command],
254
+ });
255
+ sceneNodes.current.set(key, nextNode);
256
+ notifyPresentationCommit(onCommit, onCommandError, commit);
257
+ refresh();
258
+ },
259
+ [onCommandError, onCommit, refresh, session, state.composed, writable],
260
+ );
261
+
262
+ const projection = state.composed?.projection ?? null;
263
+ const status = editableArtifactStatusLabel(view);
264
+ const accessRevoked = editableArtifactAccessRevoked(view);
265
+ return projection && !accessRevoked ? (
266
+ <PresentationProjectionArtifactSurface
267
+ {...surfaceProps}
268
+ title={title}
269
+ subtitle={
270
+ subtitle ??
271
+ `${projection.slides.length} slide${projection.slides.length === 1 ? "" : "s"}${writable ? "" : " · Read only"}`
272
+ }
273
+ projection={projection}
274
+ commit={writable ? applyCommit : undefined}
275
+ readOnly={!writable}
276
+ busy={state.loading}
277
+ onCommandError={onCommandError}
278
+ />
279
+ ) : (
280
+ <ArtifactSurface
281
+ modality="presentation"
282
+ title={title}
283
+ subtitle={subtitle ?? status}
284
+ actions={surfaceProps.actions}
285
+ busy={!accessRevoked && !state.error}
286
+ className={surfaceProps.className}
287
+ >
288
+ <EditableArtifactMessage
289
+ title={
290
+ accessRevoked
291
+ ? "Access changed"
292
+ : state.error
293
+ ? "Could not open this presentation"
294
+ : "Opening presentation"
295
+ }
296
+ detail={accessRevoked ? status : (state.error?.message ?? status)}
297
+ retry={!accessRevoked && state.error ? refresh : undefined}
298
+ />
299
+ </ArtifactSurface>
300
+ );
301
+ }
302
+
303
+ /** Builds a deterministic, bounded catalog + editor-slide projection at one revision. */
304
+ export async function composePresentationEditorProjection(
305
+ session: EditableArtifactSession,
306
+ ): Promise<ComposedPresentationProjection> {
307
+ if (session.modality !== "presentation") {
308
+ throw new Error("Expected a presentation artifact session");
309
+ }
310
+ let lastRevisionError: Error | null = null;
311
+ for (let attempt = 0; attempt < PRESENTATION_COMPOSITION_ATTEMPTS; attempt += 1) {
312
+ try {
313
+ return await composePresentationAttempt(session);
314
+ } catch (cause) {
315
+ const error = asEditableArtifactError(cause, "Could not project this presentation");
316
+ if (
317
+ !isPresentationRevisionDrift(error) ||
318
+ attempt === PRESENTATION_COMPOSITION_ATTEMPTS - 1
319
+ ) {
320
+ throw error;
321
+ }
322
+ lastRevisionError = error;
323
+ }
324
+ }
325
+ throw lastRevisionError ?? new Error("Presentation changed while it was opening");
326
+ }
327
+
328
+ async function composePresentationAttempt(
329
+ session: EditableArtifactSession,
330
+ ): Promise<ComposedPresentationProjection> {
331
+ const metadata = await session.queryPresentation({
332
+ kind: "metadata",
333
+ maxBytes: PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES,
334
+ });
335
+ if (metadata.kind !== "metadata") throw new Error("Presentation metadata response is malformed");
336
+ if (metadata.slides > PRESENTATION_MAX_INTERACTIVE_SLIDES) {
337
+ throw new Error(
338
+ `Presentation exceeds the interactive limit of ${PRESENTATION_MAX_INTERACTIVE_SLIDES} slides`,
339
+ );
340
+ }
341
+ const catalog = await collectSlideCatalog(session, metadata.revision, metadata.slides);
342
+ const scenes: EditablePresentationEditorSlideProjection[] = [];
343
+ for (let start = 0; start < catalog.length; start += PRESENTATION_SCENE_CONCURRENCY) {
344
+ const batch = catalog.slice(start, start + PRESENTATION_SCENE_CONCURRENCY);
345
+ const projected = await Promise.all(
346
+ batch.map((slide) =>
347
+ session.queryPresentationEditorSlide({
348
+ kind: "editor-slide",
349
+ slideId: slide.id,
350
+ maxNodes: PRESENTATION_ARTIFACT_QUERY_MAX_NODES,
351
+ maxTextBytes: PRESENTATION_ARTIFACT_QUERY_MAX_TEXT_BYTES,
352
+ maxBytes: PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES,
353
+ }),
354
+ ),
355
+ );
356
+ for (const scene of projected) {
357
+ if (scene.revision !== metadata.revision) {
358
+ throw new Error("Presentation projection revision drifted");
359
+ }
360
+ if (scene.truncated) {
361
+ throw new Error(`Slide ${scene.slide.index + 1} exceeds the bounded editor projection`);
362
+ }
363
+ scenes.push(scene);
364
+ }
365
+ }
366
+
367
+ const totalNodes = scenes.reduce((sum, scene) => sum + scene.nodes.length, 0);
368
+ const totalText = scenes.reduce((sum, scene) => sum + scene.projectedTextBytes, 0);
369
+ if (totalNodes > PRESENTATION_MAX_INTERACTIVE_NODES) {
370
+ throw new Error(
371
+ `Presentation exceeds the interactive limit of ${PRESENTATION_MAX_INTERACTIVE_NODES} scene nodes`,
372
+ );
373
+ }
374
+ if (totalText > PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES) {
375
+ throw new Error(
376
+ `Presentation exceeds the interactive limit of ${PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES} text bytes`,
377
+ );
378
+ }
379
+ if (scenes.length !== catalog.length) {
380
+ throw new Error("Presentation slides changed while they were opening");
381
+ }
382
+
383
+ const sceneNodeMap = new Map<string, PresentationArtifactEditorSceneNode>();
384
+ const slides = scenes.map((scene, index) => {
385
+ if (scene.slide.id !== catalog[index]?.id || scene.slide.index !== index) {
386
+ throw new Error("Presentation slide catalog changed while it was opening");
387
+ }
388
+ for (const node of scene.nodes) {
389
+ sceneNodeMap.set(sceneNodeKey(scene.slide.id, node.id), node);
390
+ }
391
+ return projectPresentationSlide(scene);
392
+ });
393
+ return Object.freeze({
394
+ projection: Object.freeze({
395
+ revision: metadata.revision.toString(),
396
+ slideSize: Object.freeze({
397
+ width: displayCoordinate(metadata.slideSize.width),
398
+ height: displayCoordinate(metadata.slideSize.height),
399
+ }),
400
+ slides: Object.freeze(slides),
401
+ }),
402
+ sceneNodes: sceneNodeMap,
403
+ });
404
+ }
405
+
406
+ async function collectSlideCatalog(
407
+ session: EditableArtifactSession,
408
+ revision: bigint,
409
+ expectedSlides: number,
410
+ ): Promise<PresentationArtifactSlideCatalogItem[]> {
411
+ const slides: PresentationArtifactSlideCatalogItem[] = [];
412
+ let startSlide = 0;
413
+ let projectedText = 0;
414
+ for (let page = 0; page < PRESENTATION_MAX_CATALOG_PAGES; page += 1) {
415
+ const response = await session.queryPresentationSlideCatalog({
416
+ kind: "slide-catalog",
417
+ startSlide,
418
+ maxSlides: Math.min(
419
+ PRESENTATION_CATALOG_PAGE,
420
+ PRESENTATION_ARTIFACT_QUERY_MAX_SLIDES,
421
+ Math.max(1, expectedSlides - slides.length),
422
+ ),
423
+ maxTextBytes: PRESENTATION_ARTIFACT_QUERY_MAX_TEXT_BYTES,
424
+ maxBytes: PRESENTATION_ARTIFACT_QUERY_RESPONSE_MAX_BYTES,
425
+ });
426
+ if (response.revision !== revision) {
427
+ throw new Error("Presentation projection revision drifted");
428
+ }
429
+ if (response.startSlide !== startSlide) {
430
+ throw new Error("Presentation slide catalog cursor is malformed");
431
+ }
432
+ if (response.slides.some((slide, offset) => slide.index !== startSlide + offset)) {
433
+ throw new Error("Presentation slide catalog order is malformed");
434
+ }
435
+ slides.push(...response.slides);
436
+ projectedText += response.projectedTextBytes;
437
+ if (
438
+ slides.length > PRESENTATION_MAX_INTERACTIVE_SLIDES ||
439
+ projectedText > PRESENTATION_MAX_INTERACTIVE_TEXT_BYTES
440
+ ) {
441
+ throw new Error("Presentation slide catalog exceeds its interactive limit");
442
+ }
443
+ if (response.nextSlide === null) {
444
+ if (response.truncated || slides.length !== expectedSlides) {
445
+ throw new Error("Presentation slide catalog is incomplete");
446
+ }
447
+ return slides;
448
+ }
449
+ if (
450
+ !response.truncated ||
451
+ response.nextSlide <= startSlide ||
452
+ response.nextSlide !== startSlide + response.slides.length ||
453
+ response.slides.length === 0
454
+ ) {
455
+ throw new Error("Presentation slide catalog did not advance");
456
+ }
457
+ startSlide = response.nextSlide;
458
+ }
459
+ throw new Error(`Presentation catalog exceeds ${PRESENTATION_MAX_CATALOG_PAGES} pages`);
460
+ }
461
+
462
+ function projectPresentationSlide(
463
+ scene: EditablePresentationEditorSlideProjection,
464
+ ): PresentationSlideProjection {
465
+ const byId = new Map(scene.nodes.map((node) => [node.id, node] as const));
466
+ if (byId.size !== scene.nodes.length)
467
+ throw new Error("Presentation slide contains duplicate IDs");
468
+ const children = new Map<string | null, PresentationArtifactEditorSceneNode[]>();
469
+ for (const node of scene.nodes) {
470
+ const entries = children.get(node.parentId) ?? [];
471
+ entries.push(node);
472
+ children.set(node.parentId, entries);
473
+ }
474
+ for (const entries of children.values()) {
475
+ entries.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
476
+ }
477
+ const visited = new Set<string>();
478
+ const stack = new Set<string>();
479
+ const projectNode = (
480
+ node: PresentationArtifactEditorSceneNode,
481
+ ): PresentationProjectionElement => {
482
+ if (stack.has(node.id)) throw new Error("Presentation group hierarchy contains a cycle");
483
+ if (visited.has(node.id)) throw new Error("Presentation node appears more than once");
484
+ stack.add(node.id);
485
+ visited.add(node.id);
486
+ const directChildren = children.get(node.id) ?? [];
487
+ let orderedChildren = directChildren;
488
+ if (node.content.kind === "group") {
489
+ const directIds = new Set(directChildren.map((child) => child.id));
490
+ if (
491
+ node.content.children.length !== directChildren.length ||
492
+ node.content.children.some((id) => !directIds.has(id))
493
+ ) {
494
+ throw new Error(`Presentation group ${node.id} hierarchy is inconsistent`);
495
+ }
496
+ orderedChildren = node.content.children.map((id) => byId.get(id)!);
497
+ } else if (directChildren.length > 0) {
498
+ throw new Error(`Presentation non-group node ${node.id} contains children`);
499
+ }
500
+ const result = projectPresentationNode(node, orderedChildren.map(projectNode));
501
+ stack.delete(node.id);
502
+ return result;
503
+ };
504
+ const elements = (children.get(null) ?? []).map(projectNode);
505
+ if (visited.size !== scene.nodes.length) {
506
+ throw new Error("Presentation slide contains orphaned hierarchy nodes");
507
+ }
508
+ return Object.freeze({
509
+ id: scene.slide.id,
510
+ title: scene.slide.title,
511
+ background: projectFill(scene.slide.background),
512
+ elements: Object.freeze(elements),
513
+ layout: scene.slide.layout
514
+ ? Object.freeze({
515
+ id: scene.slide.layout.id,
516
+ name: scene.slide.layout.name,
517
+ masterId: scene.slide.layout.masterId,
518
+ background: projectFill(scene.slide.layout.background),
519
+ })
520
+ : null,
521
+ notes: scene.notes ? projectRichText(scene.notes) : null,
522
+ });
523
+ }
524
+
525
+ function projectPresentationNode(
526
+ node: PresentationArtifactEditorSceneNode,
527
+ children: readonly PresentationProjectionElement[],
528
+ ): PresentationProjectionElement {
529
+ const metadata = {
530
+ nodeSource: { ...node.source },
531
+ inherited: node.inherited,
532
+ parentId: node.parentId,
533
+ order: node.order,
534
+ readOnly: node.inherited || node.source.kind !== "slide",
535
+ } as const;
536
+ const position = projectPosition(node.bounds);
537
+ const rotation = node.transform.rotation / 60_000;
538
+ const content = node.content;
539
+ if (content.kind === "shape") {
540
+ const richText = content.text ? projectRichText(content.text) : undefined;
541
+ const textStyle = richText ? firstTextStyle(richText) : undefined;
542
+ return Object.freeze({
543
+ ...metadata,
544
+ kind: "shape",
545
+ id: node.id,
546
+ name: node.name,
547
+ geometry: projectShapeGeometry(content.geometry),
548
+ position,
549
+ fill: projectFill(content.fill),
550
+ line: projectLine(content.line),
551
+ rotation,
552
+ text: content.text ? flattenPresentationRichText(content.text) : "",
553
+ ...(richText ? { richText } : {}),
554
+ ...(textStyle ? { textStyle } : {}),
555
+ });
556
+ }
557
+ if (content.kind === "group") {
558
+ return Object.freeze({
559
+ ...metadata,
560
+ kind: "group",
561
+ id: node.id,
562
+ name: node.name,
563
+ position,
564
+ childOffset: Object.freeze({
565
+ left: displayCoordinate(content.childOffsetX),
566
+ top: displayCoordinate(content.childOffsetY),
567
+ }),
568
+ childExtent: Object.freeze({
569
+ width: displayCoordinate(content.childExtentWidth),
570
+ height: displayCoordinate(content.childExtentHeight),
571
+ }),
572
+ rotation,
573
+ flipHorizontal: node.transform.flipHorizontal,
574
+ flipVertical: node.transform.flipVertical,
575
+ children: Object.freeze([...children]),
576
+ });
577
+ }
578
+ if (content.kind === "connector") {
579
+ return Object.freeze({
580
+ ...metadata,
581
+ kind: "connector",
582
+ id: node.id,
583
+ name: node.name,
584
+ connectorKind: content.connectorKind,
585
+ position,
586
+ start: Object.freeze({
587
+ nodeId: content.start.nodeId,
588
+ x: displayCoordinate(content.start.x),
589
+ y: displayCoordinate(content.start.y),
590
+ }),
591
+ end: Object.freeze({
592
+ nodeId: content.end.nodeId,
593
+ x: displayCoordinate(content.end.x),
594
+ y: displayCoordinate(content.end.y),
595
+ }),
596
+ line: projectLine(content.line),
597
+ });
598
+ }
599
+ if (content.kind === "chart") {
600
+ const titleRichText = projectRichText(content.title);
601
+ return Object.freeze({
602
+ ...metadata,
603
+ kind: "chart",
604
+ id: node.id,
605
+ name: node.name,
606
+ type: content.chartType,
607
+ position,
608
+ title: flattenPresentationRichText(content.title),
609
+ titleRichText,
610
+ hasLegend: content.hasLegend,
611
+ series: Object.freeze(
612
+ content.series.map((series) => ({
613
+ name: series.name,
614
+ categories: Object.freeze([...series.categories]),
615
+ values: Object.freeze([...series.values]),
616
+ xValues: Object.freeze([...series.xValues]),
617
+ bubbleSizes: Object.freeze([...series.bubbleSizes]),
618
+ })),
619
+ ),
620
+ });
621
+ }
622
+ if (content.kind === "table") {
623
+ return Object.freeze({
624
+ ...metadata,
625
+ kind: "table",
626
+ id: node.id,
627
+ name: node.name,
628
+ position,
629
+ rows: Object.freeze(
630
+ content.rows.map((row) =>
631
+ Object.freeze(
632
+ row.map((cell) =>
633
+ cell
634
+ ? Object.freeze({
635
+ text: flattenPresentationRichText(cell.text),
636
+ richText: projectRichText(cell.text),
637
+ fill: projectFill(cell.fill),
638
+ rowSpan: cell.rowSpan,
639
+ colSpan: cell.columnSpan,
640
+ })
641
+ : null,
642
+ ),
643
+ ),
644
+ ),
645
+ ),
646
+ columnWidths: Object.freeze(content.columnWidths.map(displayCoordinate)),
647
+ rowHeights: Object.freeze(content.rowHeights.map(displayCoordinate)),
648
+ line: projectLine(content.line),
649
+ });
650
+ }
651
+ return Object.freeze({
652
+ ...metadata,
653
+ kind: "image",
654
+ id: node.id,
655
+ name: node.name,
656
+ position,
657
+ alt: content.altText,
658
+ fit: content.fit,
659
+ rotation,
660
+ flipHorizontal: node.transform.flipHorizontal,
661
+ flipVertical: node.transform.flipVertical,
662
+ digest: bytesToHex(content.digest),
663
+ contentType: content.contentType,
664
+ intrinsicWidth: content.intrinsicWidth,
665
+ intrinsicHeight: content.intrinsicHeight,
666
+ });
667
+ }
668
+
669
+ function projectRichText(text: PresentationArtifactRichText): PresentationProjectionRichText {
670
+ return Object.freeze({
671
+ verticalAlignment: text.verticalAlignment,
672
+ paragraphs: Object.freeze(
673
+ text.paragraphs.map((paragraph) =>
674
+ Object.freeze({
675
+ alignment: paragraph.alignment,
676
+ runs: Object.freeze(
677
+ paragraph.runs.map((run) =>
678
+ Object.freeze({
679
+ text: run.text,
680
+ style: projectTextStyle(run.style, paragraph.alignment, text.verticalAlignment),
681
+ language: run.style.language,
682
+ }),
683
+ ),
684
+ ),
685
+ }),
686
+ ),
687
+ ),
688
+ });
689
+ }
690
+
691
+ function projectTextStyle(
692
+ style: PresentationArtifactTextStyle,
693
+ alignment?: PresentationArtifactTextParagraph["alignment"],
694
+ verticalAlignment?: PresentationArtifactRichText["verticalAlignment"],
695
+ ): PresentationProjectionTextStyle {
696
+ return Object.freeze({
697
+ fontFamily: style.fontFamily,
698
+ fontSize: style.fontSizeCentipoints / 100,
699
+ color: rgbaCss(style.color),
700
+ bold: style.bold,
701
+ italic: style.italic,
702
+ underline: style.underline,
703
+ ...(alignment ? { alignment } : {}),
704
+ ...(verticalAlignment ? { verticalAlignment } : {}),
705
+ });
706
+ }
707
+
708
+ function firstTextStyle(
709
+ text: PresentationProjectionRichText,
710
+ ): PresentationProjectionTextStyle | undefined {
711
+ return text.paragraphs[0]?.runs[0]?.style;
712
+ }
713
+
714
+ function projectFill(fill: PresentationArtifactFill): PresentationProjectionFill {
715
+ return fill.kind === "none"
716
+ ? "none"
717
+ : Object.freeze({ type: "solid", color: rgbaCss(fill.color) });
718
+ }
719
+
720
+ function projectLine(line: PresentationArtifactLine): PresentationProjectionLine {
721
+ return Object.freeze({
722
+ style: line.dash,
723
+ fill: projectFill(line.fill),
724
+ width: displayCoordinate(line.width),
725
+ });
726
+ }
727
+
728
+ function projectPosition(
729
+ bounds: PresentationArtifactEditorSceneNode["bounds"],
730
+ ): PresentationProjectionPosition {
731
+ return Object.freeze({
732
+ left: displayCoordinate(bounds.x),
733
+ top: displayCoordinate(bounds.y),
734
+ width: displayCoordinate(bounds.width),
735
+ height: displayCoordinate(bounds.height),
736
+ });
737
+ }
738
+
739
+ function displayCoordinate(value: number): number {
740
+ return value / EMU_PER_CSS_PIXEL;
741
+ }
742
+
743
+ function normalizeProjectedEditorPosition(
744
+ position: PresentationProjectionPosition,
745
+ ): PresentationProjectionPosition {
746
+ const clamp = (value: number) => Math.max(-1_000_000, Math.min(1_000_000, value));
747
+ return {
748
+ left: clamp(position.left),
749
+ top: clamp(position.top),
750
+ width: Math.min(1_000_000, Math.max(8, position.width)),
751
+ height: Math.min(1_000_000, Math.max(8, position.height)),
752
+ };
753
+ }
754
+
755
+ function canonicalCoordinate(value: number): number {
756
+ if (!Number.isFinite(value)) throw new Error("Presentation coordinate must be finite");
757
+ const canonical = Math.round(value * EMU_PER_CSS_PIXEL);
758
+ if (!Number.isSafeInteger(canonical)) throw new Error("Presentation coordinate is out of range");
759
+ return canonical;
760
+ }
761
+
762
+ function sameProjectedPosition(
763
+ left: PresentationProjectionPosition,
764
+ right: PresentationProjectionPosition,
765
+ ): boolean {
766
+ return (
767
+ canonicalCoordinate(left.left) === canonicalCoordinate(right.left) &&
768
+ canonicalCoordinate(left.top) === canonicalCoordinate(right.top) &&
769
+ canonicalCoordinate(left.width) === canonicalCoordinate(right.width) &&
770
+ canonicalCoordinate(left.height) === canonicalCoordinate(right.height)
771
+ );
772
+ }
773
+
774
+ function projectShapeGeometry(
775
+ geometry: Extract<PresentationArtifactNodeKind, { kind: "shape" }>["geometry"],
776
+ ) {
777
+ return {
778
+ "text-box": "textbox",
779
+ rectangle: "rect",
780
+ "rounded-rectangle": "roundRect",
781
+ ellipse: "ellipse",
782
+ triangle: "triangle",
783
+ "right-arrow": "rightArrow",
784
+ line: "line",
785
+ }[geometry] as "textbox" | "rect" | "roundRect" | "ellipse" | "triangle" | "rightArrow" | "line";
786
+ }
787
+
788
+ function emptyPresentationText(): PresentationArtifactRichText {
789
+ return Object.freeze({
790
+ verticalAlignment: "middle",
791
+ paragraphs: Object.freeze([
792
+ Object.freeze({
793
+ alignment: "left",
794
+ runs: Object.freeze([Object.freeze({ text: "", style: DEFAULT_PRESENTATION_TEXT_STYLE })]),
795
+ }),
796
+ ]),
797
+ });
798
+ }
799
+
800
+ export function flattenPresentationRichText(text: PresentationArtifactRichText): string {
801
+ return text.paragraphs
802
+ .map((paragraph) => paragraph.runs.map((run) => run.text).join(""))
803
+ .join("\n");
804
+ }
805
+
806
+ /** Preserves unaffected run styles and paragraph alignment across one UTF-16 text edit. */
807
+ export function replacePresentationRichText(
808
+ richText: PresentationArtifactRichText,
809
+ before: string,
810
+ after: string,
811
+ ): PresentationArtifactRichText {
812
+ const canonicalBefore = flattenPresentationRichText(richText);
813
+ if (canonicalBefore !== before) throw new Error("Presentation rich text does not match its edit");
814
+ if (before === after) return richText;
815
+ const diff = textReplacementRange(before, after);
816
+ const spans = presentationStyleSpans(richText);
817
+ const paragraphStarts = presentationParagraphStarts(richText);
818
+ const insertionStyle = styleAtOffset(spans, diff.start) ?? DEFAULT_PRESENTATION_TEXT_STYLE;
819
+ const insertionAlignment = alignmentAtOffset(richText, paragraphStarts, diff.start);
820
+ const paragraphs: PresentationArtifactTextParagraph[] = [];
821
+ let newOffset = 0;
822
+ for (const paragraphText of after.split("\n")) {
823
+ const alignment = alignmentForNewOffset(
824
+ richText,
825
+ paragraphStarts,
826
+ diff,
827
+ newOffset,
828
+ insertionAlignment,
829
+ );
830
+ const runs: { text: string; style: PresentationArtifactTextStyle }[] = [];
831
+ let localOffset = 0;
832
+ for (const character of paragraphText) {
833
+ const offset = newOffset + localOffset;
834
+ const oldOffset = oldOffsetForNewOffset(diff, offset);
835
+ const style =
836
+ oldOffset === null ? insertionStyle : (styleAtOffset(spans, oldOffset) ?? insertionStyle);
837
+ const previous = runs.at(-1);
838
+ if (previous && samePresentationTextStyle(previous.style, style)) previous.text += character;
839
+ else runs.push({ text: character, style });
840
+ localOffset += character.length;
841
+ }
842
+ paragraphs.push(
843
+ Object.freeze({
844
+ alignment,
845
+ runs: Object.freeze(
846
+ runs.map((run) =>
847
+ Object.freeze({ text: run.text, style: Object.freeze({ ...run.style }) }),
848
+ ),
849
+ ),
850
+ }),
851
+ );
852
+ newOffset += paragraphText.length + 1;
853
+ }
854
+ return Object.freeze({
855
+ verticalAlignment: richText.verticalAlignment,
856
+ paragraphs: Object.freeze(paragraphs),
857
+ });
858
+ }
859
+
860
+ type TextReplacement = {
861
+ start: number;
862
+ oldEnd: number;
863
+ newEnd: number;
864
+ delta: number;
865
+ };
866
+
867
+ function textReplacementRange(before: string, after: string): TextReplacement {
868
+ let start = 0;
869
+ while (
870
+ start < before.length &&
871
+ start < after.length &&
872
+ before.charCodeAt(start) === after.charCodeAt(start)
873
+ ) {
874
+ start += 1;
875
+ }
876
+ while (start > 0 && (splitsSurrogate(before, start) || splitsSurrogate(after, start))) start -= 1;
877
+ let suffix = 0;
878
+ while (
879
+ suffix < before.length - start &&
880
+ suffix < after.length - start &&
881
+ before.charCodeAt(before.length - suffix - 1) === after.charCodeAt(after.length - suffix - 1)
882
+ ) {
883
+ suffix += 1;
884
+ }
885
+ while (
886
+ suffix > 0 &&
887
+ (splitsSurrogate(before, before.length - suffix) ||
888
+ splitsSurrogate(after, after.length - suffix))
889
+ ) {
890
+ suffix -= 1;
891
+ }
892
+ const oldEnd = before.length - suffix;
893
+ const newEnd = after.length - suffix;
894
+ return { start, oldEnd, newEnd, delta: oldEnd - newEnd };
895
+ }
896
+
897
+ function oldOffsetForNewOffset(diff: TextReplacement, offset: number): number | null {
898
+ if (offset < diff.start) return offset;
899
+ if (offset >= diff.newEnd) return offset + diff.delta;
900
+ return null;
901
+ }
902
+
903
+ type PresentationStyleSpan = {
904
+ start: number;
905
+ end: number;
906
+ style: PresentationArtifactTextStyle;
907
+ };
908
+
909
+ function presentationStyleSpans(text: PresentationArtifactRichText): PresentationStyleSpan[] {
910
+ const spans: PresentationStyleSpan[] = [];
911
+ let offset = 0;
912
+ text.paragraphs.forEach((paragraph, paragraphIndex) => {
913
+ for (const run of paragraph.runs) {
914
+ spans.push({ start: offset, end: offset + run.text.length, style: run.style });
915
+ offset += run.text.length;
916
+ }
917
+ if (paragraphIndex < text.paragraphs.length - 1) offset += 1;
918
+ });
919
+ return spans;
920
+ }
921
+
922
+ function presentationParagraphStarts(text: PresentationArtifactRichText): number[] {
923
+ const starts: number[] = [];
924
+ let offset = 0;
925
+ text.paragraphs.forEach((paragraph, index) => {
926
+ starts.push(offset);
927
+ offset += paragraph.runs.reduce((sum, run) => sum + run.text.length, 0);
928
+ if (index < text.paragraphs.length - 1) offset += 1;
929
+ });
930
+ return starts;
931
+ }
932
+
933
+ function styleAtOffset(
934
+ spans: readonly PresentationStyleSpan[],
935
+ offset: number,
936
+ ): PresentationArtifactTextStyle | null {
937
+ const containing = spans.find((span) => offset >= span.start && offset < span.end);
938
+ if (containing) return containing.style;
939
+ const before = [...spans].reverse().find((span) => span.end <= offset);
940
+ return before?.style ?? spans[0]?.style ?? null;
941
+ }
942
+
943
+ function alignmentAtOffset(
944
+ text: PresentationArtifactRichText,
945
+ starts: readonly number[],
946
+ offset: number,
947
+ ): PresentationArtifactTextParagraph["alignment"] {
948
+ let index = 0;
949
+ while (index + 1 < starts.length && (starts[index + 1] ?? Number.POSITIVE_INFINITY) <= offset) {
950
+ index += 1;
951
+ }
952
+ return text.paragraphs[index]?.alignment ?? "left";
953
+ }
954
+
955
+ function alignmentForNewOffset(
956
+ text: PresentationArtifactRichText,
957
+ starts: readonly number[],
958
+ diff: TextReplacement,
959
+ newOffset: number,
960
+ fallback: PresentationArtifactTextParagraph["alignment"],
961
+ ): PresentationArtifactTextParagraph["alignment"] {
962
+ const oldOffset = oldOffsetForNewOffset(diff, newOffset);
963
+ return oldOffset === null ? fallback : alignmentAtOffset(text, starts, oldOffset);
964
+ }
965
+
966
+ function samePresentationTextStyle(
967
+ left: PresentationArtifactTextStyle,
968
+ right: PresentationArtifactTextStyle,
969
+ ): boolean {
970
+ return (
971
+ left.fontFamily === right.fontFamily &&
972
+ left.fontSizeCentipoints === right.fontSizeCentipoints &&
973
+ left.color === right.color &&
974
+ left.bold === right.bold &&
975
+ left.italic === right.italic &&
976
+ left.underline === right.underline &&
977
+ left.language === right.language
978
+ );
979
+ }
980
+
981
+ function splitsSurrogate(value: string, index: number): boolean {
982
+ if (index <= 0 || index >= value.length) return false;
983
+ const left = value.charCodeAt(index - 1);
984
+ const right = value.charCodeAt(index);
985
+ return left >= 0xd800 && left <= 0xdbff && right >= 0xdc00 && right <= 0xdfff;
986
+ }
987
+
988
+ function rgbaCss(value: number): string {
989
+ const hex = (value >>> 0).toString(16).padStart(8, "0");
990
+ return hex.endsWith("ff") ? `#${hex.slice(0, 6)}` : `#${hex}`;
991
+ }
992
+
993
+ function bytesToHex(value: Uint8Array): string {
994
+ return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
995
+ }
996
+
997
+ function sceneNodeKey(slideId: string, nodeId: string): string {
998
+ return `${slideId}:${nodeId}`;
999
+ }
1000
+
1001
+ function isPresentationRevisionDrift(error: Error): boolean {
1002
+ return error.message.includes("changed while") || error.message.includes("revision drifted");
1003
+ }
1004
+
1005
+ function notifyPresentationCommit(
1006
+ observer: PresentationCommitHandler | undefined,
1007
+ onError: ((error: Error) => void) | undefined,
1008
+ commit: PresentationCommit,
1009
+ ): void {
1010
+ try {
1011
+ const result = observer?.(commit);
1012
+ if (result && typeof result.then === "function") {
1013
+ void Promise.resolve(result).catch((cause) =>
1014
+ onError?.(asEditableArtifactError(cause, "Presentation commit observer failed")),
1015
+ );
1016
+ }
1017
+ } catch (cause) {
1018
+ onError?.(asEditableArtifactError(cause, "Presentation commit observer failed"));
1019
+ }
1020
+ }