@agent-native/core 0.186.0-nightly-20260923174435 → 0.186.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  3. package/corpus/templates/analytics/app/components/Markdown.tsx +3 -3
  4. package/corpus/templates/analytics/app/i18n/en-US.ts +1513 -3
  5. package/corpus/templates/analytics/app/routes/$.tsx +2 -2
  6. package/corpus/templates/analytics/app/routes/agents.tsx +2 -2
  7. package/corpus/templates/analytics/app/routes/analyses.$id.tsx +2 -2
  8. package/corpus/templates/analytics/app/routes/analyses._index.tsx +2 -2
  9. package/corpus/templates/analytics/app/routes/ask.tsx +2 -2
  10. package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +2 -2
  11. package/corpus/templates/analytics/app/routes/data-dictionary.tsx +2 -2
  12. package/corpus/templates/analytics/app/routes/data-sources.tsx +2 -2
  13. package/corpus/templates/analytics/app/routes/extensions.$id.tsx +2 -2
  14. package/corpus/templates/analytics/app/routes/extensions._index.tsx +2 -2
  15. package/corpus/templates/analytics/app/routes/monitoring._index.tsx +2 -2
  16. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +2 -2
  17. package/corpus/templates/analytics/app/routes/sessions._index.tsx +2 -2
  18. package/corpus/templates/analytics/app/routes/settings.tsx +2 -2
  19. package/corpus/templates/analytics/app/routes/team.tsx +2 -2
  20. package/corpus/templates/analytics/changelog/2026-09-23-analytics-opens-faster-by-loading-translation-catalogs-and-r.md +6 -0
  21. package/corpus/templates/calendar/CHANGELOG.md +12 -0
  22. package/corpus/templates/calendar/app/i18n/en-US.ts +985 -3
  23. package/corpus/templates/calendar/app/i18n/index.ts +16 -11
  24. package/corpus/templates/calendar/app/routes/_app.$.tsx +2 -2
  25. package/corpus/templates/calendar/app/routes/_app.agent.tsx +2 -2
  26. package/corpus/templates/calendar/app/routes/_app.booking-links.tsx +2 -2
  27. package/corpus/templates/calendar/app/routes/_app.settings.tsx +2 -2
  28. package/corpus/templates/calendar/app/routes/_app.shared-availability.add.tsx +2 -4
  29. package/corpus/templates/calendar/app/routes/_app.team.tsx +2 -2
  30. package/corpus/templates/calendar/app/routes/booking-og-meta.ts +2 -2
  31. package/corpus/templates/calendar/app/routes/booking.manage.$token.tsx +2 -2
  32. package/corpus/templates/calendar/app/routes/event.tsx +2 -2
  33. package/corpus/templates/calendar/changelog/2026-09-23-calendar-opens-faster-by-loading-translated-catalogs-only-wh.md +6 -0
  34. package/corpus/templates/clips/CHANGELOG.md +10 -0
  35. package/corpus/templates/clips/app/components/library/recording-card.tsx +1 -1
  36. package/corpus/templates/clips/app/components/player/agent-view-count.tsx +61 -0
  37. package/corpus/templates/clips/app/components/player/recording-views-badge.tsx +57 -72
  38. package/corpus/templates/clips/changelog/2026-09-23-insights-charts-load-only-when-opened-keeping-the-recording-.md +6 -0
  39. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +1 -1
  40. package/corpus/templates/content/app/components/layout/Header.tsx +1 -1
  41. package/corpus/templates/content/app/components/layout/Layout.tsx +5 -5
  42. package/corpus/templates/content/app/root.tsx +2 -1
  43. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +2 -2
  44. package/corpus/templates/design/CHANGELOG.md +7 -0
  45. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +55 -20
  46. package/corpus/templates/design/app/i18n/en-US.ts +1964 -21
  47. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  48. package/corpus/templates/design/app/pages/Templates.tsx +5 -1
  49. package/corpus/templates/design/app/routes/$.tsx +2 -2
  50. package/corpus/templates/design/app/routes/design-systems.tsx +2 -2
  51. package/corpus/templates/design/app/routes/design-systems_.setup.tsx +2 -2
  52. package/corpus/templates/design/app/routes/design.$id.tsx +2 -2
  53. package/corpus/templates/design/app/routes/extensions.$id.tsx +2 -2
  54. package/corpus/templates/design/app/routes/extensions._index.tsx +2 -2
  55. package/corpus/templates/design/app/routes/extensions.tsx +2 -2
  56. package/corpus/templates/design/app/routes/observability.tsx +2 -2
  57. package/corpus/templates/design/app/routes/present.$id.tsx +2 -2
  58. package/corpus/templates/design/app/routes/settings.tsx +2 -2
  59. package/corpus/templates/design/app/routes/team.tsx +2 -2
  60. package/corpus/templates/design/app/routes/templates.tsx +2 -2
  61. package/corpus/templates/design/app/routes/visual-edit.tsx +2 -2
  62. package/corpus/templates/design/app/routes/visual-edit_.$id.tsx +2 -2
  63. package/corpus/templates/design/changelog/2026-09-23-design-loads-translated-catalogs-and-ai-prompt-tools-only-wh.md +6 -0
  64. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +56 -13
  65. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +2 -9
  66. package/corpus/templates/plan/app/lib/plan-return-path.ts +8 -0
  67. package/corpus/templates/slides/CHANGELOG.md +6 -0
  68. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +6 -17
  69. package/corpus/templates/slides/app/components/editor/DeferredPopoverFallback.tsx +148 -0
  70. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +7 -1
  71. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +66 -42
  72. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +7 -284
  73. package/corpus/templates/slides/app/lib/deferred-editor-surfaces.ts +25 -0
  74. package/corpus/templates/slides/app/lib/portaled-layer.ts +7 -0
  75. package/corpus/templates/slides/app/lib/prompt-file-uploads.ts +269 -0
  76. package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
  77. package/corpus/templates/slides/app/pages/Index.tsx +139 -52
  78. package/corpus/templates/slides/changelog/2026-09-23-slides-loads-ai-prompt-tools-and-extra-slide-controls-only-w.md +6 -0
  79. package/dist/client/agent-chat/index.d.ts +2 -1
  80. package/dist/client/agent-chat/index.js +1 -1
  81. package/dist/client/chat/assistant-chat-history-version.d.ts +13 -0
  82. package/dist/client/chat/assistant-chat-history-version.js +18 -0
  83. package/dist/client/chat/message-components.d.ts +3 -8
  84. package/dist/client/chat/message-components.js +2 -18
  85. package/dist/client/chat/run-recovery.js +2 -1
  86. package/dist/client/lazy-chunk-retry-fallback.d.ts +3 -0
  87. package/dist/client/lazy-chunk-retry-fallback.js +9 -0
  88. package/dist/client/settings/deferred-builder-connect-popover.d.ts +1 -3
  89. package/dist/client/settings/deferred-builder-connect-popover.js +3 -9
  90. package/dist/collab/awareness.d.ts +2 -2
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  93. package/dist/notifications/routes.d.ts +3 -3
  94. package/dist/observability/routes.d.ts +8 -8
  95. package/dist/org/index.d.ts +1 -1
  96. package/dist/progress/routes.d.ts +1 -1
  97. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  98. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  99. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  100. package/dist/secrets/routes.d.ts +3 -3
  101. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  102. package/package.json +8 -4
  103. package/corpus/templates/analytics/app/i18n-data.ts +0 -13975
  104. package/corpus/templates/calendar/app/i18n-data.ts +0 -10572
  105. package/corpus/templates/design/app/i18n-data.ts +0 -17719
@@ -5,6 +5,7 @@ import type {
5
5
  } from "@agent-native/core/client/collab";
6
6
  import { useAvatarUrl } from "@agent-native/core/client/hooks";
7
7
  import { useT } from "@agent-native/core/client/i18n";
8
+ import { LazyChunkErrorBoundary } from "@agent-native/core/client/lazy-chunk-error-boundary";
8
9
  import { DEFAULT_AGENT_IDENTITY } from "@agent-native/toolkit/collab-ui";
9
10
  import {
10
11
  useSortable,
@@ -15,13 +16,13 @@ import { CSS } from "@dnd-kit/utilities";
15
16
  import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
16
17
  import { hashSlideContent, type DeckFitState } from "@shared/slide-fit";
17
18
  import { IconEyeOff } from "@tabler/icons-react";
18
- import { useCallback, useEffect, useRef, useState } from "react";
19
+ import { Suspense, useCallback, useEffect, useRef, useState } from "react";
19
20
  import { toast } from "sonner";
20
21
 
21
22
  import SlideRenderer from "@/components/deck/SlideRenderer";
22
23
  import type { SlideOverflowInfo } from "@/components/deck/SlideRenderer";
23
- import { AddSlidePopover } from "@/components/editor/AddSlidePopover";
24
24
  import { AiEditingMarker } from "@/components/editor/AiEditingMarker";
25
+ import { DeferredPopoverFallback } from "@/components/editor/DeferredPopoverFallback";
25
26
  import GeneratingSlidePreview from "@/components/editor/GeneratingSlidePreview";
26
27
  import {
27
28
  ContextMenu,
@@ -38,6 +39,7 @@ import {
38
39
  } from "@/components/ui/tooltip";
39
40
  import { defaultSlideContent, type Slide } from "@/context/DeckContext";
40
41
  import { getAspectRatioDims, type AspectRatio } from "@/lib/aspect-ratios";
42
+ import { DeferredAddSlidePopover } from "@/lib/deferred-editor-surfaces";
41
43
  import { TAB_ID } from "@/lib/tab-id";
42
44
  import { shortcutLabel } from "@/lib/utils";
43
45
 
@@ -669,6 +671,10 @@ export default function EditorSidebar({
669
671
  const t = useT();
670
672
  const [describeAnchorEl, setDescribeAnchorEl] =
671
673
  useState<HTMLButtonElement | null>(null);
674
+ const closeDescribePopover = useCallback(() => {
675
+ onCloseDescribe();
676
+ setDescribeAnchorEl(null);
677
+ }, [onCloseDescribe]);
672
678
  const [thumbnailListScrolled, setThumbnailListScrolled] = useState(false);
673
679
  const slideButtonRefs = useRef(new Map<string, HTMLButtonElement>());
674
680
  const focusAfterDeleteRef = useRef<string | null>(null);
@@ -1019,47 +1025,65 @@ export default function EditorSidebar({
1019
1025
  </div>
1020
1026
  </div>
1021
1027
  {describeSlideId && describeSlideIndex !== -1 && describeAnchorEl && (
1022
- <AddSlidePopover
1023
- open
1024
- onOpenChange={(open) => {
1025
- if (!open) {
1026
- onCloseDescribe();
1027
- setDescribeAnchorEl(null);
1028
- }
1029
- }}
1030
- anchorRef={{ current: describeAnchorEl }}
1031
- placement="right"
1032
- deckId={deckId}
1033
- deckTitle={deckTitle}
1034
- activeSlideId={describeSlideId}
1035
- activeSlideIndex={describeSlideIndex}
1036
- slideCount={slides.length}
1037
- targetSlideId={describeSlideId}
1038
- agentSubmit={async (message, context) => {
1039
- onAddSlideGeneratingChange?.(true, describeSlideId);
1040
- try {
1041
- await onAwaitAddSlidePersisted?.();
1042
- } catch (error) {
1043
- console.error("Failed to persist new slide:", error);
1044
- onAddSlideGeneratingChange?.(false, null);
1045
- // Only remove the placeholder if it's still untouched —
1046
- // the save retries take long enough that the user could have
1047
- // started editing it directly on the canvas in the meantime,
1048
- // and deleting it would destroy that work.
1049
- const current = slides.find((s) => s.id === describeSlideId);
1050
- if (
1051
- current?.content === defaultSlideContent.blank &&
1052
- !current.notes
1053
- ) {
1054
- onRemoveFailedSlide?.(describeSlideId);
1055
- }
1056
- toast.error(t("editorSidebar.newSlideSaveFailed"));
1057
- return false;
1028
+ <LazyChunkErrorBoundary
1029
+ fallback={
1030
+ <DeferredPopoverFallback
1031
+ surface="add-slide"
1032
+ anchorRef={{ current: describeAnchorEl }}
1033
+ failed
1034
+ onClose={closeDescribePopover}
1035
+ />
1036
+ }
1037
+ >
1038
+ <Suspense
1039
+ fallback={
1040
+ <DeferredPopoverFallback
1041
+ surface="add-slide"
1042
+ anchorRef={{ current: describeAnchorEl }}
1043
+ onClose={closeDescribePopover}
1044
+ />
1058
1045
  }
1059
- addSlideAgentSubmit(message, context);
1060
- return true;
1061
- }}
1062
- />
1046
+ >
1047
+ <DeferredAddSlidePopover
1048
+ open
1049
+ onOpenChange={(open) => {
1050
+ if (!open) closeDescribePopover();
1051
+ }}
1052
+ anchorRef={{ current: describeAnchorEl }}
1053
+ placement="right"
1054
+ deckId={deckId}
1055
+ deckTitle={deckTitle}
1056
+ activeSlideId={describeSlideId}
1057
+ activeSlideIndex={describeSlideIndex}
1058
+ slideCount={slides.length}
1059
+ targetSlideId={describeSlideId}
1060
+ agentSubmit={async (message, context) => {
1061
+ onAddSlideGeneratingChange?.(true, describeSlideId);
1062
+ try {
1063
+ await onAwaitAddSlidePersisted?.();
1064
+ } catch (error) {
1065
+ console.error("Failed to persist new slide:", error);
1066
+ onAddSlideGeneratingChange?.(false, null);
1067
+ // Only remove the placeholder if it's still untouched —
1068
+ // the save retries take long enough that the user could have
1069
+ // started editing it directly on the canvas in the meantime,
1070
+ // and deleting it would destroy that work.
1071
+ const current = slides.find((s) => s.id === describeSlideId);
1072
+ if (
1073
+ current?.content === defaultSlideContent.blank &&
1074
+ !current.notes
1075
+ ) {
1076
+ onRemoveFailedSlide?.(describeSlideId);
1077
+ }
1078
+ toast.error(t("editorSidebar.newSlideSaveFailed"));
1079
+ return false;
1080
+ }
1081
+ addSlideAgentSubmit(message, context);
1082
+ return true;
1083
+ }}
1084
+ />
1085
+ </Suspense>
1086
+ </LazyChunkErrorBoundary>
1063
1087
  )}
1064
1088
  </div>
1065
1089
  );
@@ -1,10 +1,8 @@
1
- import { appBasePath } from "@agent-native/core/client/api-path";
2
1
  import {
3
2
  PromptComposer,
4
3
  type PromptComposerSubmitOptions,
5
4
  useEagerFileUploads,
6
5
  } from "@agent-native/core/client/composer";
7
- import { ensureEmbedAuthFetchInterceptor } from "@agent-native/core/client/host";
8
6
  import { useT } from "@agent-native/core/client/i18n";
9
7
  import {
10
8
  IconArrowLeft,
@@ -18,31 +16,20 @@ import { useState, useEffect, useCallback, useRef } from "react";
18
16
  import { createPortal } from "react-dom";
19
17
  import { toast } from "sonner";
20
18
 
19
+ import { isInsidePortaledLayer } from "@/lib/portaled-layer";
21
20
  import {
22
- canAddInlineImageToPayload,
23
- canInlineImageFile,
24
- readFileAsDataUrl,
25
- } from "@/lib/image-drop-to-agent";
21
+ deleteUploadedPromptFile,
22
+ uploadPromptFiles,
23
+ type UploadedFile,
24
+ } from "@/lib/prompt-file-uploads";
26
25
 
27
- import {
28
- MAX_REFERENCE_FILE_BYTES,
29
- MAX_REFERENCE_FILES,
30
- } from "../../../shared/upload-types";
26
+ import { MAX_REFERENCE_FILE_BYTES } from "../../../shared/upload-types";
31
27
  import { Button } from "../ui/button";
32
28
  import { Input } from "../ui/input";
33
29
  import { GoogleDocImportHint } from "./GoogleDocImportHint";
34
30
  import { GoogleDriveConnectionCta } from "./GoogleDriveConnectionCta";
35
31
 
36
- export interface UploadedFile {
37
- path: string;
38
- url?: string;
39
- /** Browser-only fallback when the upload provider did not return a public URL. */
40
- dataUrl?: string;
41
- originalName: string;
42
- filename: string;
43
- type: string;
44
- size: number;
45
- }
32
+ export type { UploadedFile } from "@/lib/prompt-file-uploads";
46
33
 
47
34
  export interface PromptChatAttachment {
48
35
  type: "file";
@@ -51,52 +38,6 @@ export interface PromptChatAttachment {
51
38
  displayOnly: true;
52
39
  text?: string;
53
40
  }
54
- export async function addInlineImageFallbacks(
55
- files: File[],
56
- uploaded: UploadedFile[],
57
- ): Promise<UploadedFile[]> {
58
- const inlineDataUrls: string[] = [];
59
- const result: UploadedFile[] = [];
60
- for (let index = 0; index < uploaded.length; index++) {
61
- const uploadedFile = uploaded[index];
62
- const file = files[index];
63
- const isImage =
64
- uploadedFile.type.startsWith("image/") ||
65
- Boolean(file?.type.startsWith("image/"));
66
- if (!isImage || !file) {
67
- result.push(uploadedFile);
68
- continue;
69
- }
70
- if (uploadedFile.url) {
71
- const { dataUrl: _dataUrl, ...withoutDataUrl } = uploadedFile;
72
- result.push(withoutDataUrl);
73
- continue;
74
- }
75
- if (uploadedFile.dataUrl) {
76
- if (canAddInlineImageToPayload(inlineDataUrls, uploadedFile.dataUrl)) {
77
- inlineDataUrls.push(uploadedFile.dataUrl);
78
- result.push(uploadedFile);
79
- } else {
80
- const { dataUrl: _dataUrl, ...withoutDataUrl } = uploadedFile;
81
- result.push(withoutDataUrl);
82
- }
83
- continue;
84
- }
85
- if (!canInlineImageFile(file)) {
86
- result.push(uploadedFile);
87
- continue;
88
- }
89
- const dataUrl = await readFileAsDataUrl(file);
90
- if (canAddInlineImageToPayload(inlineDataUrls, dataUrl)) {
91
- inlineDataUrls.push(dataUrl);
92
- result.push({ ...uploadedFile, dataUrl });
93
- } else {
94
- result.push(uploadedFile);
95
- }
96
- }
97
- return result;
98
- }
99
-
100
41
  export async function createPromptChatAttachments(
101
42
  attachments: ReadonlyArray<unknown> | undefined,
102
43
  uploaded: UploadedFile[],
@@ -158,224 +99,6 @@ export async function createPromptChatAttachments(
158
99
  return result;
159
100
  }
160
101
 
161
- // Netlify functions cap request bodies well under what a real PPTX/PDF
162
- // needs, so any file above this size streams through the chunked upload
163
- // endpoints (sub-4 MB slices, reassembled server-side) instead of one
164
- // multipart POST.
165
- const CHUNK_UPLOAD_THRESHOLD_BYTES = 4 * 1024 * 1024;
166
- const CHUNK_SIZE_BYTES = 4 * 1024 * 1024;
167
-
168
- async function readUploadJson(response: Response): Promise<unknown> {
169
- try {
170
- return await response.json();
171
- } catch (error) {
172
- throw new Error(`Upload returned invalid JSON (${response.status})`, {
173
- cause: error,
174
- });
175
- }
176
- }
177
-
178
- function extractErrorMessage(data: unknown): string | null {
179
- if (
180
- data &&
181
- typeof data === "object" &&
182
- "error" in data &&
183
- typeof (data as { error: unknown }).error === "string" &&
184
- (data as { error: string }).error.trim()
185
- ) {
186
- return (data as { error: string }).error;
187
- }
188
- return null;
189
- }
190
-
191
- async function uploadFilesMultipart(files: File[]): Promise<UploadedFile[]> {
192
- const formData = new FormData();
193
- files.forEach((file) => formData.append("files", file));
194
- const response = await fetch(`${appBasePath()}/api/uploads`, {
195
- method: "POST",
196
- body: formData,
197
- credentials: "include",
198
- });
199
- const data = await readUploadJson(response);
200
- if (!response.ok) {
201
- throw new Error(
202
- extractErrorMessage(data) || `Upload failed (${response.status})`,
203
- );
204
- }
205
- if (!Array.isArray(data)) {
206
- throw new Error("Upload failed: invalid response");
207
- }
208
- return data as UploadedFile[];
209
- }
210
-
211
- async function deleteUploadedPromptFile(file: UploadedFile): Promise<void> {
212
- const response = await fetch(`${appBasePath()}/api/uploads`, {
213
- method: "DELETE",
214
- headers: { "content-type": "application/json" },
215
- credentials: "include",
216
- body: JSON.stringify({ path: file.path }),
217
- });
218
- if (!response.ok) {
219
- throw new Error(`Upload cleanup failed (${response.status})`);
220
- }
221
- }
222
-
223
- async function cleanupUploadedPromptFiles(files: UploadedFile[]) {
224
- const results = await Promise.allSettled(
225
- files.map((file) => deleteUploadedPromptFile(file)),
226
- );
227
- results.forEach((result) => {
228
- if (result.status === "rejected") {
229
- console.error("Eager upload cleanup failed", result.reason);
230
- }
231
- });
232
- }
233
-
234
- async function uploadFileChunked(file: File): Promise<UploadedFile> {
235
- const startResponse = await fetch(
236
- `${appBasePath()}/api/uploads-chunked/start`,
237
- {
238
- method: "POST",
239
- headers: { "Content-Type": "application/json" },
240
- credentials: "include",
241
- body: JSON.stringify({
242
- filename: file.name,
243
- mimetype: file.type || "application/octet-stream",
244
- declaredSize: file.size,
245
- }),
246
- },
247
- );
248
- const startData = await readUploadJson(startResponse);
249
- if (!startResponse.ok) {
250
- throw new Error(
251
- extractErrorMessage(startData) ||
252
- `Upload failed (${startResponse.status})`,
253
- );
254
- }
255
- if (
256
- startData &&
257
- typeof startData === "object" &&
258
- (startData as { uploadMode?: unknown }).uploadMode === "multipart"
259
- ) {
260
- const [uploaded] = await uploadFilesMultipart([file]);
261
- if (!uploaded) throw new Error("Upload failed: no file returned");
262
- return uploaded;
263
- }
264
- const sessionId =
265
- startData && typeof startData === "object"
266
- ? (startData as { sessionId?: unknown }).sessionId
267
- : undefined;
268
- if (typeof sessionId !== "string" || !sessionId) {
269
- throw new Error("Upload failed: session ID missing");
270
- }
271
-
272
- const totalChunks = Math.max(1, Math.ceil(file.size / CHUNK_SIZE_BYTES));
273
- for (let index = 0; index < totalChunks; index++) {
274
- const start = index * CHUNK_SIZE_BYTES;
275
- const end = Math.min(start + CHUNK_SIZE_BYTES, file.size);
276
- const isFinal = index === totalChunks - 1;
277
- const chunkResponse = await fetch(
278
- `${appBasePath()}/api/uploads-chunked/${sessionId}/chunk?index=${index}&isFinal=${
279
- isFinal ? "1" : "0"
280
- }`,
281
- {
282
- method: "POST",
283
- credentials: "include",
284
- headers: { "Content-Type": "application/octet-stream" },
285
- body: file.slice(start, end),
286
- },
287
- );
288
- const chunkData = await readUploadJson(chunkResponse);
289
- if (!chunkResponse.ok) {
290
- throw new Error(
291
- extractErrorMessage(chunkData) ||
292
- `Upload failed (${chunkResponse.status})`,
293
- );
294
- }
295
- if (isFinal) {
296
- const result = Array.isArray(chunkData)
297
- ? (chunkData[0] as UploadedFile)
298
- : undefined;
299
- if (!result) throw new Error("Upload failed: no file returned");
300
- return result;
301
- }
302
- }
303
- throw new Error("Upload failed: no final chunk response");
304
- }
305
-
306
- export async function uploadPromptFiles(
307
- files: File[],
308
- ): Promise<UploadedFile[]> {
309
- if (files.length === 0) return [];
310
- if (files.length > MAX_REFERENCE_FILES) {
311
- throw new Error(`Too many files (max ${MAX_REFERENCE_FILES})`);
312
- }
313
- ensureEmbedAuthFetchInterceptor();
314
- const smallIndices = files.flatMap((file, index) =>
315
- file.size <= CHUNK_UPLOAD_THRESHOLD_BYTES ? [index] : [],
316
- );
317
- const largeIndices = files.flatMap((file, index) =>
318
- file.size > CHUNK_UPLOAD_THRESHOLD_BYTES ? [index] : [],
319
- );
320
- const smallPromise =
321
- smallIndices.length > 0
322
- ? uploadFilesMultipart(smallIndices.map((index) => files[index]))
323
- : Promise.resolve([] as UploadedFile[]);
324
- const [smallResult, largeResults] = await Promise.all([
325
- smallPromise.then(
326
- (value) => ({ status: "fulfilled" as const, value }),
327
- (reason) => ({ status: "rejected" as const, reason }),
328
- ),
329
- Promise.allSettled(
330
- largeIndices.map((index) => uploadFileChunked(files[index])),
331
- ),
332
- ]);
333
- const successfulLargeUploads = largeResults.flatMap((result) =>
334
- result.status === "fulfilled" ? [result.value] : [],
335
- );
336
- const failedLargeResult = largeResults.find(
337
- (result) => result.status === "rejected",
338
- );
339
- if (smallResult.status === "rejected") {
340
- await cleanupUploadedPromptFiles([...successfulLargeUploads]);
341
- throw smallResult.reason;
342
- }
343
- if (failedLargeResult) {
344
- await cleanupUploadedPromptFiles([
345
- ...smallResult.value,
346
- ...successfulLargeUploads,
347
- ]);
348
- throw failedLargeResult.reason;
349
- }
350
- const smallUploads = smallResult.value;
351
- const largeUploads = successfulLargeUploads;
352
- if (smallUploads.length !== smallIndices.length) {
353
- throw new Error("Upload failed: response file count did not match request");
354
- }
355
- const uploads = new Array<UploadedFile>(files.length);
356
- smallIndices.forEach((fileIndex, resultIndex) => {
357
- uploads[fileIndex] = smallUploads[resultIndex];
358
- });
359
- largeIndices.forEach((fileIndex, resultIndex) => {
360
- uploads[fileIndex] = largeUploads[resultIndex];
361
- });
362
- return addInlineImageFallbacks(files, uploads);
363
- }
364
-
365
- /**
366
- * Radix popovers portal to `document.body`, so a mousedown inside the model
367
- * picker or attachment menu reads as "outside" any panel that hosts a composer.
368
- * Closing on it unmounts the popover before its own click fires, which looks
369
- * exactly like a dead button.
370
- */
371
- export function isInsidePortaledLayer(target: EventTarget | null): boolean {
372
- return Boolean(
373
- (target as Element | null)?.closest?.(
374
- "[data-radix-popper-content-wrapper]",
375
- ),
376
- );
377
- }
378
-
379
102
  export type PromptImportSource = "pdf" | "pptx" | "google-slides";
380
103
 
381
104
  export type PromptImportSelection =
@@ -0,0 +1,25 @@
1
+ import { lazy } from "react";
2
+
3
+ let addSlidePopoverModule:
4
+ | Promise<typeof import("@/components/editor/AddSlidePopover")>
5
+ | undefined;
6
+
7
+ function loadAddSlidePopover() {
8
+ addSlidePopoverModule ??= import("@/components/editor/AddSlidePopover").catch(
9
+ (error) => {
10
+ addSlidePopoverModule = undefined;
11
+ throw error;
12
+ },
13
+ );
14
+ return addSlidePopoverModule;
15
+ }
16
+
17
+ export function preloadAddSlidePopover() {
18
+ // Hover/focus loads are speculative; the opened lazy render owns recovery.
19
+ void loadAddSlidePopover().catch(() => {});
20
+ }
21
+
22
+ export const DeferredAddSlidePopover = lazy(async () => {
23
+ const module = await loadAddSlidePopover();
24
+ return { default: module.AddSlidePopover };
25
+ });
@@ -0,0 +1,7 @@
1
+ export function isInsidePortaledLayer(target: EventTarget | null): boolean {
2
+ return Boolean(
3
+ (target as Element | null)?.closest?.(
4
+ "[data-radix-popper-content-wrapper]",
5
+ ),
6
+ );
7
+ }