@opengeni/react 7.4.0 → 7.5.0-canary.36927100590001

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 (210) hide show
  1. package/README.md +124 -27
  2. package/dist/artifacts-document.js +3 -2
  3. package/dist/artifacts-presentation.js +3 -2
  4. package/dist/artifacts-spreadsheet.js +2 -2
  5. package/dist/artifacts.d.ts +1 -0
  6. package/dist/artifacts.js +516 -1413
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/{browser-viewer-2E6R2Y4W.js → browser-viewer-Z7S7QEJL.js} +6 -5
  9. package/dist/chat-interactive-block-UGBTNB3L.js +11 -0
  10. package/dist/{chunk-Y3QTZN2F.js → chunk-2VFNCX4E.js} +6 -6
  11. package/dist/{chunk-UDD6DLLT.js → chunk-3IJJEYW4.js} +15 -3
  12. package/dist/chunk-3IJJEYW4.js.map +1 -0
  13. package/dist/chunk-4RVYOY4O.js +379 -0
  14. package/dist/chunk-4RVYOY4O.js.map +1 -0
  15. package/dist/chunk-5CVXZA3Y.js +292 -0
  16. package/dist/chunk-5CVXZA3Y.js.map +1 -0
  17. package/dist/{chunk-5YK2ZDIG.js → chunk-5J4FOW6Q.js} +2 -10
  18. package/dist/chunk-5J4FOW6Q.js.map +1 -0
  19. package/dist/{chunk-KVERXCFY.js → chunk-5KYZIQFY.js} +40 -3
  20. package/dist/chunk-5KYZIQFY.js.map +1 -0
  21. package/dist/{chunk-KNDIXZN6.js → chunk-7UAMYXJY.js} +25 -18
  22. package/dist/{chunk-KNDIXZN6.js.map → chunk-7UAMYXJY.js.map} +1 -1
  23. package/dist/{chunk-SEEVCT36.js → chunk-A4UJB2MM.js} +2 -2
  24. package/dist/{chunk-X4HIN6TV.js → chunk-AFLPWUQ7.js} +71 -162
  25. package/dist/chunk-AFLPWUQ7.js.map +1 -0
  26. package/dist/chunk-B3V6TG77.js +41 -0
  27. package/dist/chunk-B3V6TG77.js.map +1 -0
  28. package/dist/{chunk-XEJEOAOV.js → chunk-B7WMS3CS.js} +51 -40
  29. package/dist/chunk-B7WMS3CS.js.map +1 -0
  30. package/dist/{chunk-S5JQ4R22.js → chunk-BDKR6BAR.js} +54 -24
  31. package/dist/chunk-BDKR6BAR.js.map +1 -0
  32. package/dist/{chunk-4MDLEHXL.js → chunk-DYWBUX5Q.js} +24 -12
  33. package/dist/chunk-DYWBUX5Q.js.map +1 -0
  34. package/dist/{chunk-YZ3YYMOM.js → chunk-E3G7SAYZ.js} +252 -624
  35. package/dist/chunk-E3G7SAYZ.js.map +1 -0
  36. package/dist/chunk-JT4JW7BC.js +54 -0
  37. package/dist/chunk-JT4JW7BC.js.map +1 -0
  38. package/dist/{chunk-5AZSK6MV.js → chunk-LWNLETQT.js} +5 -5
  39. package/dist/{chunk-USXVQACU.js → chunk-M7LBYP63.js} +5 -4
  40. package/dist/{chunk-USXVQACU.js.map → chunk-M7LBYP63.js.map} +1 -1
  41. package/dist/{chunk-L4OSXQ6H.js → chunk-NBOHNRLJ.js} +1289 -1060
  42. package/dist/chunk-NBOHNRLJ.js.map +1 -0
  43. package/dist/chunk-RHREZ7BO.js +11 -0
  44. package/dist/chunk-RHREZ7BO.js.map +1 -0
  45. package/dist/chunk-SCOTPE2I.js +589 -0
  46. package/dist/chunk-SCOTPE2I.js.map +1 -0
  47. package/dist/chunk-WEHMPOXK.js +39 -0
  48. package/dist/chunk-WEHMPOXK.js.map +1 -0
  49. package/dist/chunk-WRHIZYZF.js +988 -0
  50. package/dist/chunk-WRHIZYZF.js.map +1 -0
  51. package/dist/{chunk-3E2AITTZ.js → chunk-WUISZ3WU.js} +32 -28
  52. package/dist/{chunk-3E2AITTZ.js.map → chunk-WUISZ3WU.js.map} +1 -1
  53. package/dist/chunk-XCHWDJ22.js +107 -0
  54. package/dist/chunk-XCHWDJ22.js.map +1 -0
  55. package/dist/{chunk-DUEBTL3N.js → chunk-Y2PRMYLC.js} +4 -3
  56. package/dist/{chunk-DUEBTL3N.js.map → chunk-Y2PRMYLC.js.map} +1 -1
  57. package/dist/chunk-YBXQDQFX.js +1772 -0
  58. package/dist/chunk-YBXQDQFX.js.map +1 -0
  59. package/dist/chunk-YV4L3ILF.js +9 -0
  60. package/dist/chunk-YV4L3ILF.js.map +1 -0
  61. package/dist/{chunk-V2ABIVYJ.js → chunk-ZXSEDZP7.js} +70 -6
  62. package/dist/chunk-ZXSEDZP7.js.map +1 -0
  63. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  64. package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
  65. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  66. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  67. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  68. package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
  69. package/dist/components/artifacts/index.d.ts +7 -0
  70. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  71. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  72. package/dist/components/artifacts/site-view.d.ts +23 -0
  73. package/dist/components/browser-select-control.d.ts +3 -1
  74. package/dist/components/code-editor.d.ts +1 -4
  75. package/dist/components/desktop-viewer.d.ts +1 -1
  76. package/dist/components/message-timeline.d.ts +20 -3
  77. package/dist/components/open-geni-links.d.ts +16 -0
  78. package/dist/components/session-conversation.d.ts +20 -1
  79. package/dist/components/timeline-pill-geometry.d.ts +8 -0
  80. package/dist/components/timeline-pill-placement.d.ts +24 -0
  81. package/dist/components/usage-limit-notice.d.ts +49 -0
  82. package/dist/components/usage-member-list.d.ts +91 -0
  83. package/dist/components/usage-meter.d.ts +73 -0
  84. package/dist/composer.js +5 -5
  85. package/dist/{computer-viewer-WMJE6OA3.js → computer-viewer-KJWLH3GI.js} +7 -5
  86. package/dist/connect.js +7 -6
  87. package/dist/connect.js.map +1 -1
  88. package/dist/conversation-timeline.d.ts +1 -1
  89. package/dist/desktop.d.ts +7 -0
  90. package/dist/desktop.js +26 -0
  91. package/dist/desktop.js.map +1 -0
  92. package/dist/editor.d.ts +7 -0
  93. package/dist/editor.js +21 -0
  94. package/dist/editor.js.map +1 -0
  95. package/dist/hooks/latest-question.d.ts +1 -1
  96. package/dist/hooks/use-composer.d.ts +4 -0
  97. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  98. package/dist/hooks/use-usage.d.ts +40 -0
  99. package/dist/index.d.ts +7 -2
  100. package/dist/index.js +460 -1386
  101. package/dist/index.js.map +1 -1
  102. package/dist/interaction.js +8 -6
  103. package/dist/interaction.js.map +1 -1
  104. package/dist/{latest-question-QB4EUCW5.js → latest-question-IS445HOW.js} +3 -3
  105. package/dist/lib/format.d.ts +6 -1
  106. package/dist/lib/workbench-peers.d.ts +36 -0
  107. package/dist/machines.js +2 -2
  108. package/dist/{markdown-table-layout-PFUEZ2U4.js → markdown-table-layout-QXSLK4BY.js} +37 -5
  109. package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
  110. package/dist/{queue-surface-implementation-IM2HD3SK.js → queue-surface-implementation-2CAQAIHM.js} +2 -2
  111. package/dist/realtime.js +3 -3
  112. package/dist/{sandbox-files-PXPIZAFG.js → sandbox-files-AEJTWGT4.js} +8 -5
  113. package/dist/sandbox-files-AEJTWGT4.js.map +1 -0
  114. package/dist/session-ui.d.ts +9 -2
  115. package/dist/session-ui.js +39 -20
  116. package/dist/session-ui.js.map +1 -1
  117. package/dist/session.js +4 -4
  118. package/dist/sites.js +4 -289
  119. package/dist/sites.js.map +1 -1
  120. package/dist/terminal.d.ts +8 -0
  121. package/dist/terminal.js +31 -0
  122. package/dist/terminal.js.map +1 -0
  123. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  124. package/dist/timeline/genie-loading.d.ts +3 -1
  125. package/dist/timeline/types.d.ts +8 -1
  126. package/dist/{timeline-annotation-selection-ODLVF4OI.js → timeline-annotation-selection-DMLMM2TX.js} +6 -1
  127. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
  128. package/dist/timeline-pill-geometry-Z23V243M.js +110 -0
  129. package/dist/timeline-pill-geometry-Z23V243M.js.map +1 -0
  130. package/dist/usage/allowance-copy.d.ts +25 -0
  131. package/dist/usage/summary.d.ts +35 -0
  132. package/dist/usage.d.ts +13 -0
  133. package/dist/usage.js +1073 -0
  134. package/dist/usage.js.map +1 -0
  135. package/package.json +19 -3
  136. package/src/artifacts.ts +37 -0
  137. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  138. package/src/components/artifacts/artifact-sandbox.tsx +185 -0
  139. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  140. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  141. package/src/components/artifacts/editable-artifact-ui.tsx +3 -1
  142. package/src/components/artifacts/editable-artifact-view.tsx +187 -0
  143. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  144. package/src/components/artifacts/editable-document.tsx +34 -36
  145. package/src/components/artifacts/editable-presentation.tsx +21 -16
  146. package/src/components/artifacts/index.ts +41 -0
  147. package/src/components/artifacts/latest-task-runner.ts +49 -0
  148. package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
  149. package/src/components/artifacts/site-view.tsx +116 -0
  150. package/src/components/browser-select-control.tsx +4 -1
  151. package/src/components/browser-viewer.tsx +81 -22
  152. package/src/components/code-editor.tsx +13 -16
  153. package/src/components/computer-viewer.tsx +51 -23
  154. package/src/components/desktop-viewer.tsx +1 -1
  155. package/src/components/markdown-table-layout.ts +49 -5
  156. package/src/components/markdown.tsx +5 -2
  157. package/src/components/message-timeline.tsx +557 -516
  158. package/src/components/open-geni-links.tsx +24 -0
  159. package/src/components/sandbox-terminal.tsx +48 -11
  160. package/src/components/session-chrome.tsx +22 -3
  161. package/src/components/session-conversation.tsx +195 -102
  162. package/src/components/timeline-annotation-selection.tsx +7 -0
  163. package/src/components/timeline-pill-geometry.ts +169 -0
  164. package/src/components/timeline-pill-placement.ts +99 -0
  165. package/src/components/usage-limit-notice.tsx +179 -0
  166. package/src/components/usage-member-list.tsx +792 -0
  167. package/src/components/usage-meter.tsx +382 -0
  168. package/src/conversation-timeline.ts +10 -2
  169. package/src/desktop.ts +24 -0
  170. package/src/editor.ts +13 -0
  171. package/src/hooks/use-available-models.ts +1 -1
  172. package/src/hooks/use-composer.ts +57 -2
  173. package/src/hooks/use-desktop-stream.ts +19 -28
  174. package/src/hooks/use-usage.ts +100 -0
  175. package/src/index.ts +22 -0
  176. package/src/lib/format.ts +32 -6
  177. package/src/lib/workbench-peers.ts +62 -0
  178. package/src/session-ui.ts +22 -0
  179. package/src/terminal.ts +26 -0
  180. package/src/timeline/activity-rail.tsx +5 -3
  181. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  182. package/src/timeline/genie-loading.tsx +16 -6
  183. package/src/timeline/projection.ts +78 -4
  184. package/src/timeline/types.ts +8 -0
  185. package/src/usage/allowance-copy.ts +64 -0
  186. package/src/usage/summary.ts +93 -0
  187. package/src/usage.ts +50 -0
  188. package/styles/compiled.css +325 -0
  189. package/dist/chunk-4MDLEHXL.js.map +0 -1
  190. package/dist/chunk-5YK2ZDIG.js.map +0 -1
  191. package/dist/chunk-KVERXCFY.js.map +0 -1
  192. package/dist/chunk-L4OSXQ6H.js.map +0 -1
  193. package/dist/chunk-S5JQ4R22.js.map +0 -1
  194. package/dist/chunk-UDD6DLLT.js.map +0 -1
  195. package/dist/chunk-UGMKPUEM.js +0 -299
  196. package/dist/chunk-UGMKPUEM.js.map +0 -1
  197. package/dist/chunk-V2ABIVYJ.js.map +0 -1
  198. package/dist/chunk-X4HIN6TV.js.map +0 -1
  199. package/dist/chunk-XEJEOAOV.js.map +0 -1
  200. package/dist/chunk-YZ3YYMOM.js.map +0 -1
  201. package/dist/markdown-table-layout-PFUEZ2U4.js.map +0 -1
  202. package/dist/timeline-annotation-selection-ODLVF4OI.js.map +0 -1
  203. /package/dist/{browser-viewer-2E6R2Y4W.js.map → browser-viewer-Z7S7QEJL.js.map} +0 -0
  204. /package/dist/{computer-viewer-WMJE6OA3.js.map → chat-interactive-block-UGBTNB3L.js.map} +0 -0
  205. /package/dist/{chunk-Y3QTZN2F.js.map → chunk-2VFNCX4E.js.map} +0 -0
  206. /package/dist/{chunk-SEEVCT36.js.map → chunk-A4UJB2MM.js.map} +0 -0
  207. /package/dist/{chunk-5AZSK6MV.js.map → chunk-LWNLETQT.js.map} +0 -0
  208. /package/dist/{sandbox-files-PXPIZAFG.js.map → computer-viewer-KJWLH3GI.js.map} +0 -0
  209. /package/dist/{latest-question-QB4EUCW5.js.map → latest-question-IS445HOW.js.map} +0 -0
  210. /package/dist/{queue-surface-implementation-IM2HD3SK.js.map → queue-surface-implementation-2CAQAIHM.js.map} +0 -0
@@ -0,0 +1,260 @@
1
+ import type { OpenGeniClient, ToolGatewayIdentity } from "@opengeni/sdk";
2
+ import { useEffect, useMemo, useState } from "react";
3
+
4
+ import { loadSiteSnapshot } from "../../sites-ui";
5
+ import { useOpenGeniLinkResolver } from "../open-geni-links";
6
+ import {
7
+ ArtifactButton,
8
+ ArtifactLabelsProvider,
9
+ ArtifactSelect,
10
+ useArtifactLabels,
11
+ type ArtifactLabels,
12
+ } from "./artifact-chrome";
13
+ import { ArtifactSandbox } from "./artifact-sandbox";
14
+ import { DeferredChatMedia } from "./deferred-chat-media";
15
+ import { inlineHtmlDocument } from "./inline-html-document";
16
+ import type { PublishedHtmlArtifactToolBridge } from "./published-html-artifact-frame";
17
+
18
+ // Fixed chat viewports keep async Site loads and fragment resize messages from
19
+ // moving the conversation. Full-screen remains available for larger content.
20
+ const INLINE_PREVIEW_HEIGHT = 360;
21
+ const SITE_PREVIEW_HEIGHT = 400;
22
+ const PREVIEW_CHROME_HEIGHT = 50;
23
+
24
+ /** Reads a Site needs to render; the console client, the SDK client, or a proxy. */
25
+ export type SiteSnapshotClient = Pick<
26
+ OpenGeniClient,
27
+ "getWorkspaceArtifact" | "getWorkspaceArtifactHtml"
28
+ > &
29
+ Partial<Pick<OpenGeniClient, "getWorkspaceArtifactContent">>;
30
+
31
+ /** The exact saved Site a tool bridge is pinned to (absent for inline HTML). */
32
+ export type SiteToolScope = {
33
+ artifactId: string;
34
+ siteVersionId: string;
35
+ requestedTools: readonly ToolGatewayIdentity[];
36
+ };
37
+
38
+ /** Host-owned Site API access. Omit it and Sites render without workspace tools. */
39
+ export type SiteToolBridgeFactory = (
40
+ site?: SiteToolScope,
41
+ ) => PublishedHtmlArtifactToolBridge | undefined;
42
+
43
+ export type ChatInteractiveBlockProps = {
44
+ workspaceId: string;
45
+ kind: "html" | "site";
46
+ content: string;
47
+ client: SiteSnapshotClient;
48
+ toolBridge?: SiteToolBridgeFactory | undefined;
49
+ theme?: "light" | "dark" | undefined;
50
+ /** Translations for the preview's copy; defaults to English. */
51
+ labels?: Partial<ArtifactLabels> | undefined;
52
+ };
53
+
54
+ /**
55
+ * Inline preview for an assistant `opengeni-html` or `opengeni-site` fence, as
56
+ * the OpenGeni console renders it. "Open Site" goes through the nearest
57
+ * `resolveLink`, so a host decides where a Site opens.
58
+ */
59
+ export function ChatInteractiveBlock({ labels, ...props }: ChatInteractiveBlockProps) {
60
+ return (
61
+ <ArtifactLabelsProvider labels={labels}>
62
+ <InteractiveBlock {...props} />
63
+ </ArtifactLabelsProvider>
64
+ );
65
+ }
66
+
67
+ function InteractiveBlock(props: ChatInteractiveBlockProps) {
68
+ const labels = useArtifactLabels();
69
+ const height =
70
+ (props.kind === "html" ? INLINE_PREVIEW_HEIGHT : SITE_PREVIEW_HEIGHT) + PREVIEW_CHROME_HEIGHT;
71
+ return (
72
+ <DeferredChatMedia
73
+ key={`${props.workspaceId}:${props.kind}:${props.kind === "site" ? props.content : "inline"}`}
74
+ height={height}
75
+ actionLabel={props.kind === "html" ? labels.loadPreview : labels.loadSitePreview}
76
+ >
77
+ <div style={{ height }}>
78
+ <LoadedChatInteractiveBlock {...props} />
79
+ </div>
80
+ </DeferredChatMedia>
81
+ );
82
+ }
83
+
84
+ function LoadedChatInteractiveBlock(props: ChatInteractiveBlockProps) {
85
+ const labels = useArtifactLabels();
86
+ if (props.kind === "html") return <InlineHtml {...props} />;
87
+ try {
88
+ const value = JSON.parse(props.content);
89
+ if (
90
+ !value ||
91
+ typeof value.siteId !== "string" ||
92
+ !/^[0-9a-f-]{36}$/i.test(value.siteId) ||
93
+ (value.versionId !== undefined &&
94
+ (typeof value.versionId !== "string" || !/^[0-9a-f-]{36}$/i.test(value.versionId)))
95
+ )
96
+ throw new Error("Invalid Site reference");
97
+ return (
98
+ <SiteEmbed
99
+ key={`${props.workspaceId}:${value.siteId}:${value.versionId ?? "current"}`}
100
+ {...props}
101
+ siteId={value.siteId}
102
+ initialVersionId={value.versionId}
103
+ />
104
+ );
105
+ } catch {
106
+ return <p role="alert">{labels.siteReferenceInvalid}</p>;
107
+ }
108
+ }
109
+
110
+ function InlineHtml({ content, toolBridge, theme }: ChatInteractiveBlockProps) {
111
+ const labels = useArtifactLabels();
112
+ const html = useMemo(() => inlineHtmlDocument(content), [content]);
113
+ const bridge = useMemo(() => toolBridge?.(), [toolBridge]);
114
+ return (
115
+ <ArtifactSandbox
116
+ title={labels.preview}
117
+ showTitle={false}
118
+ showLiveStatus={false}
119
+ html={html}
120
+ toolBridge={bridge}
121
+ height={INLINE_PREVIEW_HEIGHT}
122
+ className="h-full"
123
+ fill
124
+ theme={theme}
125
+ />
126
+ );
127
+ }
128
+
129
+ function SiteEmbed(
130
+ props: ChatInteractiveBlockProps & { siteId: string; initialVersionId?: string },
131
+ ) {
132
+ const [versionId, setVersionId] = useState(props.initialVersionId);
133
+ return (
134
+ <SiteEmbedContent
135
+ key={versionId ?? "current"}
136
+ {...props}
137
+ versionId={versionId}
138
+ onVersionChange={setVersionId}
139
+ />
140
+ );
141
+ }
142
+
143
+ function SiteEmbedContent({
144
+ workspaceId,
145
+ client,
146
+ toolBridge,
147
+ theme,
148
+ siteId,
149
+ versionId,
150
+ onVersionChange,
151
+ }: ChatInteractiveBlockProps & {
152
+ siteId: string;
153
+ versionId?: string | undefined;
154
+ onVersionChange: (id: string) => void;
155
+ }) {
156
+ const [loaded, setLoaded] = useState<{
157
+ client: SiteSnapshotClient;
158
+ snapshot: Awaited<ReturnType<typeof loadSiteSnapshot>>;
159
+ } | null>(null);
160
+ const snapshot = loaded?.client === client ? loaded.snapshot : null;
161
+ const [error, setError] = useState(false);
162
+ const [retry, setRetry] = useState(0);
163
+ useEffect(() => {
164
+ const abort = new AbortController();
165
+ setError(false);
166
+ setLoaded(null);
167
+ void loadSiteSnapshot(client, workspaceId, siteId, {
168
+ signal: abort.signal,
169
+ ...(versionId ? { versionId } : {}),
170
+ })
171
+ .then((value) => {
172
+ if (!abort.signal.aborted) setLoaded({ client, snapshot: value });
173
+ })
174
+ .catch(() => {
175
+ if (!abort.signal.aborted) setError(true);
176
+ });
177
+ return () => abort.abort();
178
+ }, [client, workspaceId, siteId, versionId, retry]);
179
+ const content = snapshot?.content;
180
+ const bridge = useMemo(
181
+ () =>
182
+ content
183
+ ? toolBridge?.({
184
+ artifactId: siteId,
185
+ siteVersionId: content.versionId,
186
+ requestedTools: content.requestedTools,
187
+ })
188
+ : undefined,
189
+ [toolBridge, siteId, content],
190
+ );
191
+ const open = useOpenGeniLinkResolver()?.({ kind: "site", artifactId: siteId, workspaceId });
192
+ const labels = useArtifactLabels();
193
+ if (error)
194
+ return (
195
+ <p role="alert">
196
+ {labels.siteLoadFailed}{" "}
197
+ <ArtifactButton size="sm" onClick={() => setRetry((v) => v + 1)}>
198
+ {labels.retry}
199
+ </ArtifactButton>
200
+ </p>
201
+ );
202
+ if (!snapshot) return <p role="status">{labels.loadingSite}</p>;
203
+ if (!content) return <p>{labels.siteUnpublished}</p>;
204
+ return (
205
+ <ArtifactSandbox
206
+ title={snapshot.detail.artifact.title}
207
+ html={content.html}
208
+ toolBridge={bridge}
209
+ height={SITE_PREVIEW_HEIGHT}
210
+ className="h-full"
211
+ fill
212
+ theme={theme}
213
+ headerControls={
214
+ <>
215
+ {/* One published version has nothing to choose; keep the bar for the title. */}
216
+ {snapshot.detail.versions.length > 1 ||
217
+ !snapshot.detail.versions.some((v) => v.id === content.versionId) ? (
218
+ <ArtifactSelect
219
+ aria-label={labels.siteVersion}
220
+ className="bg-transparent text-xs"
221
+ value={content.versionId}
222
+ onChange={(e) => onVersionChange(e.target.value)}
223
+ >
224
+ {!snapshot.detail.versions.some((v) => v.id === content.versionId) && (
225
+ <option value={content.versionId}>{labels.savedVersion}</option>
226
+ )}
227
+ {snapshot.detail.versions.map((v) => (
228
+ <option key={v.id} value={v.id}>
229
+ {labels.version(v.revision)}
230
+ </option>
231
+ ))}
232
+ </ArtifactSelect>
233
+ ) : null}
234
+ {open?.href ? (
235
+ <a
236
+ className="shrink-0 whitespace-nowrap text-xs underline"
237
+ href={open.href}
238
+ data-og-open-site=""
239
+ >
240
+ {labels.openSite}
241
+ </a>
242
+ ) : open?.open ? (
243
+ <button
244
+ type="button"
245
+ className="shrink-0 cursor-pointer whitespace-nowrap text-xs underline"
246
+ data-og-open-site=""
247
+ onClick={() =>
248
+ void Promise.resolve()
249
+ .then(open.open)
250
+ .catch(() => undefined)
251
+ }
252
+ >
253
+ {labels.openSite}
254
+ </button>
255
+ ) : null}
256
+ </>
257
+ }
258
+ />
259
+ );
260
+ }
@@ -0,0 +1,67 @@
1
+ import { useEffect, useRef, useState, type ReactNode } from "react";
2
+
3
+ /**
4
+ * Reserve the chat slot before starting expensive media work. Activation is
5
+ * one-way: scrolling away must not discard an interactive preview's state.
6
+ * The timeline parks at its initial tip in layout effects before this observer
7
+ * is attached, so historical previews above the entry viewport stay dormant.
8
+ */
9
+ export function DeferredChatMedia({
10
+ children,
11
+ height,
12
+ label,
13
+ actionLabel,
14
+ }: {
15
+ children: ReactNode;
16
+ height: number;
17
+ /** What is loading, as in "Load {label}". */
18
+ label?: string | undefined;
19
+ /** The complete (translated) activation label; overrides `label`. */
20
+ actionLabel?: string | undefined;
21
+ }) {
22
+ const host = useRef<HTMLDivElement>(null);
23
+ const [active, setActive] = useState(false);
24
+ useEffect(() => {
25
+ const node = host.current;
26
+ if (active || !node) return;
27
+ if (typeof IntersectionObserver === "undefined") {
28
+ setActive(true);
29
+ return;
30
+ }
31
+ let disposed = false;
32
+ const observer = new IntersectionObserver(
33
+ (entries) => {
34
+ if (disposed || !entries.some((entry) => entry.isIntersecting)) return;
35
+ setActive(true);
36
+ observer.disconnect();
37
+ },
38
+ {
39
+ root: node.closest("[data-og-timeline-scroller]"),
40
+ rootMargin: "200px 0px",
41
+ },
42
+ );
43
+ observer.observe(node);
44
+ return () => {
45
+ disposed = true;
46
+ observer.disconnect();
47
+ };
48
+ }, [active]);
49
+
50
+ return (
51
+ <div ref={host} data-chat-media={active ? "active" : "deferred"}>
52
+ {active ? (
53
+ children
54
+ ) : (
55
+ <div className="flex w-full items-center justify-center" style={{ height }}>
56
+ <button
57
+ type="button"
58
+ className="rounded-md px-3 py-2 text-sm text-fg-muted hover:text-fg focus-visible:outline-2 focus-visible:outline-ring"
59
+ onClick={() => setActive(true)}
60
+ >
61
+ {actionLabel ?? `Load ${label ?? "media"}`}
62
+ </button>
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
@@ -60,10 +60,12 @@ export function EditableArtifactMessage({
60
60
  title,
61
61
  detail,
62
62
  retry,
63
+ retryLabel = "Retry",
63
64
  }: {
64
65
  title: string;
65
66
  detail: string;
66
67
  retry?: (() => void) | undefined;
68
+ retryLabel?: string | undefined;
67
69
  }) {
68
70
  return (
69
71
  <div className="grid h-full min-h-56 place-items-center bg-og-bg p-6 text-center">
@@ -76,7 +78,7 @@ export function EditableArtifactMessage({
76
78
  onClick={retry}
77
79
  className="mt-3 rounded-og-sm border border-og-primary-border bg-og-primary text-og-primary-fg px-3 py-1.5 text-og-sm font-medium hover:bg-og-primary-hover outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
78
80
  >
79
- Retry
81
+ {retryLabel}
80
82
  </button>
81
83
  ) : null}
82
84
  </div>
@@ -0,0 +1,187 @@
1
+ import type { EditableArtifactResource } from "@opengeni/sdk/artifacts";
2
+ import {
3
+ createBrowserEditableArtifactSession,
4
+ type CreateBrowserEditableArtifactSessionOptions,
5
+ type EditableArtifactBrowserRuntime,
6
+ type EditableArtifactCacheAuthority,
7
+ type EditableArtifactModality,
8
+ } from "@opengeni/sdk/editable-artifacts";
9
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
+
11
+ import {
12
+ ArtifactLoading,
13
+ ArtifactProblem,
14
+ artifactLoadErrorView,
15
+ useArtifactLabels,
16
+ type ArtifactLoadErrorView,
17
+ } from "./artifact-chrome";
18
+ import { EditableArtifactWorkbenchHost } from "./editable-artifact-workbench";
19
+
20
+ /**
21
+ * The browser kernels an editor needs, one per modality, plus the host-built
22
+ * Worker URL (for Vite: `@opengeni/sdk/editable-artifacts/worker?worker&url`).
23
+ * Kernels come from `@opengeni/artifact-kernel-wasm-{document,spreadsheet,presentation}`.
24
+ */
25
+ export type EditableArtifactRuntimes = Readonly<{
26
+ document: Omit<EditableArtifactBrowserRuntime, "workerUrl" | "workerFactory">;
27
+ spreadsheet: Omit<EditableArtifactBrowserRuntime, "workerUrl" | "workerFactory">;
28
+ presentation: Omit<EditableArtifactBrowserRuntime, "workerUrl" | "workerFactory">;
29
+ workerUrl: string;
30
+ applicationOrigin?: string | undefined;
31
+ /** Loopback development only. */
32
+ allowInsecureDevelopmentAssets?: boolean | undefined;
33
+ }>;
34
+
35
+ /** Everything needed to open one artifact for the current principal. */
36
+ export type OpenedEditableArtifact = Readonly<{
37
+ artifact: EditableArtifactResource;
38
+ authority: EditableArtifactCacheAuthority;
39
+ replicaId: string;
40
+ }>;
41
+
42
+ export type EditableArtifactViewProps = Readonly<{
43
+ /** Absolute API (or session proxy) base the live transport talks to. */
44
+ baseUrl: URL;
45
+ workspaceId: string;
46
+ artifactId: string;
47
+ /** Resolve metadata, cache authority, and replica; called again on retry. */
48
+ open: (signal: AbortSignal) => Promise<OpenedEditableArtifact>;
49
+ runtimes: EditableArtifactRuntimes;
50
+ transport?: CreateBrowserEditableArtifactSessionOptions["transport"];
51
+ /** Show the editor's own title header (hosts with their own header hide it). */
52
+ showHeader?: boolean | undefined;
53
+ /** Re-open when this changes (for example the host's credential version). */
54
+ authorityKey?: string | undefined;
55
+ describeError?: ((error: unknown) => ArtifactLoadErrorView) | undefined;
56
+ onTitle?: ((title: string) => void) | undefined;
57
+ /** Loading copy; defaults to the `opening` label. */
58
+ loadingLabel?: string | undefined;
59
+ }>;
60
+
61
+ type LoadState =
62
+ | Readonly<{ key: string | null; kind: "loading" }>
63
+ | Readonly<{ key: string; kind: "ready"; value: OpenedEditableArtifact }>
64
+ | Readonly<{ key: string; kind: "error"; error: unknown }>;
65
+
66
+ /**
67
+ * One editable document, spreadsheet, or presentation with its first-party
68
+ * editor. The console dock/page and embedding hosts supply only data access.
69
+ */
70
+ export function EditableArtifactView({
71
+ baseUrl,
72
+ workspaceId,
73
+ artifactId,
74
+ open,
75
+ runtimes,
76
+ transport,
77
+ showHeader = true,
78
+ authorityKey,
79
+ describeError,
80
+ onTitle,
81
+ loadingLabel,
82
+ }: EditableArtifactViewProps) {
83
+ const [loadEpoch, setLoadEpoch] = useState(0);
84
+ const labels = useArtifactLabels();
85
+ const loadKey = JSON.stringify([artifactId, authorityKey, baseUrl.href, workspaceId, loadEpoch]);
86
+ const [state, setState] = useState<LoadState>({ key: null, kind: "loading" });
87
+ const current = state.key === loadKey ? state : null;
88
+ const openRef = useRef(open);
89
+ openRef.current = open;
90
+ const onTitleRef = useRef(onTitle);
91
+ onTitleRef.current = onTitle;
92
+ useEffect(() => {
93
+ const abort = new AbortController();
94
+ setState({ key: loadKey, kind: "loading" });
95
+ void openRef
96
+ .current(abort.signal)
97
+ .then((value) => {
98
+ if (abort.signal.aborted) return;
99
+ setState({ key: loadKey, kind: "ready", value });
100
+ onTitleRef.current?.(value.artifact.title);
101
+ })
102
+ .catch((error: unknown) => {
103
+ if (!abort.signal.aborted) setState({ key: loadKey, kind: "error", error });
104
+ });
105
+ return () => abort.abort();
106
+ }, [loadKey]);
107
+
108
+ const ready = current?.kind === "ready" ? current.value : null;
109
+ const transportRef = useRef(transport);
110
+ transportRef.current = transport;
111
+ const sessionKey = useMemo(
112
+ () =>
113
+ ready
114
+ ? JSON.stringify({
115
+ baseUrl: baseUrl.href,
116
+ authorityKey,
117
+ workspaceId,
118
+ artifact: { id: ready.artifact.id, modality: ready.artifact.modality },
119
+ authority: ready.authority,
120
+ replicaId: ready.replicaId,
121
+ workerUrl: runtimes.workerUrl,
122
+ })
123
+ : "",
124
+ [authorityKey, baseUrl.href, ready, runtimes.workerUrl, workspaceId],
125
+ );
126
+ const createSession = useCallback(() => {
127
+ if (!ready) throw new Error("Artifact is not open");
128
+ return createBrowserEditableArtifactSession({
129
+ baseUrl,
130
+ workspaceId,
131
+ artifact: ready.artifact,
132
+ storageAuthority: ready.authority,
133
+ replicaId: ready.replicaId,
134
+ runtime: {
135
+ ...runtimeFor(runtimes, ready.artifact.modality),
136
+ workerUrl: runtimes.workerUrl,
137
+ ...(runtimes.applicationOrigin ? { applicationOrigin: runtimes.applicationOrigin } : {}),
138
+ ...(runtimes.allowInsecureDevelopmentAssets
139
+ ? { allowInsecureDevelopmentAssets: true }
140
+ : {}),
141
+ },
142
+ ...(transportRef.current ? { transport: transportRef.current } : {}),
143
+ });
144
+ }, [baseUrl, ready, runtimes, workspaceId]);
145
+
146
+ if (!current || current.kind === "loading")
147
+ return <ArtifactLoading label={loadingLabel ?? labels.opening} />;
148
+ if (current.kind === "error") {
149
+ return (
150
+ <ArtifactProblem
151
+ view={
152
+ describeError
153
+ ? describeError(current.error)
154
+ : artifactLoadErrorView(current.error, "editable", labels)
155
+ }
156
+ onRetry={() => setLoadEpoch((value) => value + 1)}
157
+ />
158
+ );
159
+ }
160
+ const { artifact } = current.value;
161
+ const surface = { title: artifact.title, showHeader };
162
+ return (
163
+ <div className="h-full min-h-0 bg-og-bg text-og-fg">
164
+ <EditableArtifactWorkbenchHost
165
+ sessionKey={sessionKey}
166
+ createSession={createSession}
167
+ document={surface}
168
+ spreadsheet={surface}
169
+ presentation={surface}
170
+ />
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function runtimeFor(
176
+ runtimes: EditableArtifactRuntimes,
177
+ modality: EditableArtifactModality,
178
+ ): Omit<EditableArtifactBrowserRuntime, "workerUrl" | "workerFactory"> {
179
+ switch (modality) {
180
+ case "document":
181
+ return runtimes.document;
182
+ case "spreadsheet":
183
+ return runtimes.spreadsheet;
184
+ case "presentation":
185
+ return runtimes.presentation;
186
+ }
187
+ }
@@ -6,6 +6,7 @@ import {
6
6
  } from "@opengeni/sdk/editable-artifacts";
7
7
  import { useCallback, useEffect, useRef, useState } from "react";
8
8
 
9
+ import { useArtifactLabels } from "./artifact-chrome";
9
10
  import { EditableArtifactMessage } from "./editable-artifact-ui";
10
11
  import {
11
12
  EditableDocumentArtifactSurface,
@@ -133,6 +134,7 @@ export function EditableArtifactWorkbenchHost({
133
134
  }, [retryEpoch, sessionKey]);
134
135
 
135
136
  const retry = useCallback(() => setRetryEpoch((value) => value + 1), []);
137
+ const labels = useArtifactLabels();
136
138
  const currentError = state.key === sessionKey ? state.error : null;
137
139
  if (state.key === sessionKey && state.session) {
138
140
  return (
@@ -146,9 +148,10 @@ export function EditableArtifactWorkbenchHost({
146
148
  }
147
149
  return (
148
150
  <EditableArtifactMessage
149
- title={currentError ? "Could not open this artifact" : "Opening artifact"}
150
- detail={currentError?.message ?? "Starting the secure editing session…"}
151
+ title={currentError ? labels.editableErrors.transient.title : labels.opening}
152
+ detail={currentError?.message ?? labels.startingEditor}
151
153
  retry={currentError ? retry : undefined}
154
+ retryLabel={labels.retry}
152
155
  />
153
156
  );
154
157
  }
@@ -9,6 +9,7 @@ import {
9
9
  type EditableArtifactSession,
10
10
  } from "@opengeni/sdk/editable-artifacts";
11
11
  import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
12
+ import { useLatestTaskRunner } from "./latest-task-runner";
12
13
 
13
14
  import {
14
15
  DocumentProjectionArtifactSurface,
@@ -87,6 +88,7 @@ export function EditableDocumentArtifactSurface({
87
88
  error: null,
88
89
  });
89
90
  const loadGeneration = useRef(0);
91
+ const scheduleLoad = useLatestTaskRunner(session);
90
92
  const commandStateRef = useRef<DocumentCommandState | null>(null);
91
93
  if (commandStateRef.current?.session !== session) {
92
94
  commandStateRef.current = {
@@ -107,45 +109,41 @@ export function EditableDocumentArtifactSurface({
107
109
  ? { ...current, loading: true, error: null }
108
110
  : { session, projection: null, loading: true, error: null },
109
111
  );
110
- void composeDocumentEditorProjection(session).then(
111
- (projection) => {
112
- if (
113
- cancelled ||
114
- generation !== loadGeneration.current ||
115
- commandStateRef.current !== commandState
116
- ) {
117
- return;
118
- }
119
- const nextParagraphText = paragraphTextById(projection);
120
- for (const [localId, canonicalId] of commandState.optimisticParagraphIds) {
121
- if (nextParagraphText.has(canonicalId)) continue;
122
- const optimisticText =
123
- commandState.paragraphText.get(canonicalId) ?? commandState.paragraphText.get(localId);
124
- if (optimisticText !== undefined) nextParagraphText.set(canonicalId, optimisticText);
125
- }
126
- commandState.paragraphText = nextParagraphText;
127
- setState({ session, projection, loading: false, error: null });
128
- },
129
- (cause) => {
130
- if (
131
- cancelled ||
132
- generation !== loadGeneration.current ||
133
- commandStateRef.current !== commandState
134
- ) {
135
- return;
136
- }
137
- setState({
138
- session,
139
- projection: null,
140
- loading: false,
141
- error: asEditableArtifactError(cause, "Could not open this document"),
142
- });
143
- },
144
- );
112
+ const superseded = () =>
113
+ cancelled ||
114
+ generation !== loadGeneration.current ||
115
+ commandStateRef.current !== commandState;
116
+ scheduleLoad(async () => {
117
+ if (superseded()) return;
118
+ await composeDocumentEditorProjection(session).then(
119
+ (projection) => {
120
+ if (superseded()) return;
121
+ const nextParagraphText = paragraphTextById(projection);
122
+ for (const [localId, canonicalId] of commandState.optimisticParagraphIds) {
123
+ if (nextParagraphText.has(canonicalId)) continue;
124
+ const optimisticText =
125
+ commandState.paragraphText.get(canonicalId) ??
126
+ commandState.paragraphText.get(localId);
127
+ if (optimisticText !== undefined) nextParagraphText.set(canonicalId, optimisticText);
128
+ }
129
+ commandState.paragraphText = nextParagraphText;
130
+ setState({ session, projection, loading: false, error: null });
131
+ },
132
+ (cause) => {
133
+ if (superseded()) return;
134
+ setState({
135
+ session,
136
+ projection: null,
137
+ loading: false,
138
+ error: asEditableArtifactError(cause, "Could not open this document"),
139
+ });
140
+ },
141
+ );
142
+ });
145
143
  return () => {
146
144
  cancelled = true;
147
145
  };
148
- }, [commandState, invalidator, retryEpoch, session]);
146
+ }, [commandState, invalidator, retryEpoch, scheduleLoad, session]);
149
147
 
150
148
  const refresh = useCallback(() => setRetryEpoch((value) => value + 1), []);
151
149
  const writable = !readOnly && view.writable;