@agent-native/core 0.148.1 → 0.149.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
- package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
- package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +6 -1
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
- package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
- package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
- package/corpus/templates/content/actions/_property-utils.ts +11 -1
- package/corpus/templates/content/actions/view-screen.ts +62 -12
- package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +3 -0
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
- package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
- package/corpus/templates/design/actions/get-design-template.ts +178 -0
- package/corpus/templates/design/actions/list-designs.ts +3 -1
- package/corpus/templates/design/actions/view-screen.ts +31 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +78 -0
- package/corpus/templates/design/app/lib/design-authors.ts +64 -0
- package/corpus/templates/design/app/pages/Index.tsx +114 -4
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
- package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
- package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/AGENTS.md +2 -2
- package/corpus/templates/forms/actions/create-form.ts +1 -1
- package/corpus/templates/forms/actions/update-form.ts +5 -5
- package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/slides/AGENTS.md +4 -3
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
- package/corpus/templates/slides/actions/patch-deck.ts +18 -2
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +22 -75
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -3
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +27 -4
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -14
- package/corpus/templates/slides/app/pages/Index.tsx +124 -135
- package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/dist/agent/engine/registry.d.ts +8 -2
- package/dist/agent/engine/registry.js +63 -29
- package/dist/agent/production-agent.js +7 -0
- package/dist/agent/run-manager.d.ts +5 -4
- package/dist/agent/run-manager.js +6 -5
- package/dist/cli/claude-code-participant.d.ts +2 -1
- package/dist/cli/claude-code-participant.js +19 -0
- package/dist/cli/code-agent-executor.js +223 -0
- package/dist/cli/create-workspace.js +3 -3
- package/dist/cli/create.d.ts +3 -2
- package/dist/cli/create.js +20 -24
- package/dist/client/AgentPanel.js +8 -4
- package/dist/client/AssistantChat.js +34 -1
- package/dist/client/agent-chat-adapter.d.ts +1 -0
- package/dist/client/agent-chat-adapter.js +10 -2
- package/dist/client/chat/markdown-renderer.d.ts +1 -0
- package/dist/client/chat/markdown-renderer.js +4 -0
- package/dist/client/chat/message-components.d.ts +1 -0
- package/dist/client/chat/message-components.js +9 -1
- package/dist/client/chat/tool-call-display.d.ts +8 -0
- package/dist/client/chat/tool-call-display.js +30 -21
- package/dist/client/chat-first/app-pane.js +1 -1
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/chat-history.d.ts +1 -1
- package/dist/client/chat-first/chat-history.js +2 -0
- package/dist/client/chat-first/primary-nav.js +1 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +2 -2
- package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
- package/dist/client/settings/SettingsTabsPage.js +2 -2
- package/dist/client/sse-event-processor.d.ts +8 -0
- package/dist/client/sse-event-processor.js +13 -6
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +5 -7
- package/dist/deploy/build.js +19 -12
- package/dist/localization/default-messages.js +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/scripts/call-agent.js +3 -3
- package/dist/scripts/describe-workspace-apps.js +1 -1
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/context-tools.js +1 -1
- package/dist/server/agent-chat/prompt-resources.js +1 -1
- package/dist/server/agent-chat-plugin.js +1 -1
- package/dist/server/auth.js +190 -42
- package/dist/server/better-auth-migrations.js +53 -0
- package/dist/server/http-response-telemetry.js +7 -1
- package/dist/server/onboarding-html.js +119 -33
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/password-policy.d.ts +2 -1
- package/dist/shared/password-policy.js +2 -1
- package/dist/styles/agent-conversation.css +20 -3
- package/dist/styles/agent-native.css +14 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
- package/dist/templates/default/app/i18n/de-DE.ts +1 -1
- package/dist/templates/default/app/i18n/en-US.ts +1 -1
- package/dist/templates/default/app/i18n/es-ES.ts +1 -1
- package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
- package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
- package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
- package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
- package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
- package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
- package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
- package/docs/content/cloneable-saas.mdx +6 -6
- package/docs/content/deployment.mdx +10 -8
- package/docs/content/environment-variables.mdx +4 -3
- package/package.json +2 -2
- package/src/templates/default/app/i18n/ar-SA.ts +1 -1
- package/src/templates/default/app/i18n/de-DE.ts +1 -1
- package/src/templates/default/app/i18n/en-US.ts +1 -1
- package/src/templates/default/app/i18n/es-ES.ts +1 -1
- package/src/templates/default/app/i18n/fr-FR.ts +1 -1
- package/src/templates/default/app/i18n/hi-IN.ts +1 -1
- package/src/templates/default/app/i18n/ja-JP.ts +1 -1
- package/src/templates/default/app/i18n/ko-KR.ts +1 -1
- package/src/templates/default/app/i18n/pt-BR.ts +1 -1
- package/src/templates/default/app/i18n/zh-CN.ts +1 -1
- package/src/templates/default/app/i18n/zh-TW.ts +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
|
@@ -119,6 +119,8 @@ import {
|
|
|
119
119
|
getSlideTextBoxDefaultColor,
|
|
120
120
|
getSlideSelectionIdentity,
|
|
121
121
|
getSlideSelectionMode,
|
|
122
|
+
findPersistedImageObject,
|
|
123
|
+
isDeletableFlowImage,
|
|
122
124
|
removeSlideObjectAndLayoutSpacer,
|
|
123
125
|
resolveSlideObjectContainingBlock,
|
|
124
126
|
type CopiedSlideObjects,
|
|
@@ -2404,9 +2406,24 @@ export default function SlideEditor({
|
|
|
2404
2406
|
clearMultiSelection();
|
|
2405
2407
|
} else {
|
|
2406
2408
|
const element = resolveSelectedElement();
|
|
2407
|
-
if (!element
|
|
2408
|
-
|
|
2409
|
-
|
|
2409
|
+
if (!element) return;
|
|
2410
|
+
if (isPersistedFreeformObject(element)) {
|
|
2411
|
+
e.preventDefault();
|
|
2412
|
+
removeSlideObjectAndLayoutSpacer(element);
|
|
2413
|
+
} else if (isDeletableFlowImage(element)) {
|
|
2414
|
+
e.preventDefault();
|
|
2415
|
+
// An imported PPTX/PDF image is an <img> (or placeholder) nested in
|
|
2416
|
+
// an absolutely positioned wrapper that carries the persisted object
|
|
2417
|
+
// id. Removing only the inner node would leave that wrapper and its
|
|
2418
|
+
// durable metadata behind as an invisible ghost object.
|
|
2419
|
+
const owner = findPersistedImageObject(element, slideContent);
|
|
2420
|
+
if (owner) removeSlideObjectAndLayoutSpacer(owner);
|
|
2421
|
+
else element.remove();
|
|
2422
|
+
setSelectedImg(null);
|
|
2423
|
+
setImageOverlay(null);
|
|
2424
|
+
} else {
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2410
2427
|
clearSelectedElement();
|
|
2411
2428
|
}
|
|
2412
2429
|
|
|
@@ -4328,6 +4345,7 @@ export default function SlideEditor({
|
|
|
4328
4345
|
edits={activeSlideEdits}
|
|
4329
4346
|
resolveRect={resolveCanvasRect}
|
|
4330
4347
|
containerRef={slideCanvasRef}
|
|
4348
|
+
outlineOnly
|
|
4331
4349
|
/>
|
|
4332
4350
|
)}
|
|
4333
4351
|
{(agentActive || passivePresentUsers.length > 0) && (
|
|
@@ -4412,7 +4430,12 @@ export default function SlideEditor({
|
|
|
4412
4430
|
onSendToAgent={sendSelectionToAgent}
|
|
4413
4431
|
/>
|
|
4414
4432
|
|
|
4415
|
-
<BlockBubbleMenu
|
|
4433
|
+
<BlockBubbleMenu
|
|
4434
|
+
editingEl={editingEl}
|
|
4435
|
+
slideId={slide.id}
|
|
4436
|
+
deckId={deckId}
|
|
4437
|
+
onCommitInlineEdit={exitInlineEdit}
|
|
4438
|
+
/>
|
|
4416
4439
|
|
|
4417
4440
|
{pendingUpdateCount > 0 && (
|
|
4418
4441
|
<div className="absolute top-4 right-4 z-50">
|
|
@@ -521,6 +521,51 @@ export function removeSlideObjectAndLayoutSpacer(element: HTMLElement): void {
|
|
|
521
521
|
element.remove();
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Whether Delete should remove `element` even though it is not a freeform
|
|
526
|
+
* canvas object.
|
|
527
|
+
*
|
|
528
|
+
* Flow-layout nodes are deliberately excluded from object operations, because
|
|
529
|
+
* deleting an arbitrary one collapses the layout around it. An image is the
|
|
530
|
+
* exception: it is a leaf, users select it directly, and removing it leaves
|
|
531
|
+
* the surrounding grid or card intact. Without this, selecting a picture in a
|
|
532
|
+
* card grid and pressing Delete silently did nothing.
|
|
533
|
+
*/
|
|
534
|
+
export function isDeletableFlowImage(element: HTMLElement): boolean {
|
|
535
|
+
return (
|
|
536
|
+
element.tagName === "IMG" ||
|
|
537
|
+
element.classList.contains("fmd-img-placeholder")
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The persisted image object that owns `element`, if any.
|
|
543
|
+
*
|
|
544
|
+
* PPTX/PDF import wraps each picture in an absolutely positioned
|
|
545
|
+
* `.fmd-pptx-image` div carrying the durable `data-slide-object-id`, with the
|
|
546
|
+
* `<img>` (or an empty placeholder) inside it. Deleting the inner node alone
|
|
547
|
+
* leaves that wrapper behind as an invisible object that still occupies its
|
|
548
|
+
* slot and still round-trips through save. Matching on the image wrapper
|
|
549
|
+
* specifically — rather than any positioned ancestor — keeps this from
|
|
550
|
+
* swallowing a whole card or column that merely contains a picture.
|
|
551
|
+
*/
|
|
552
|
+
export function findPersistedImageObject(
|
|
553
|
+
element: HTMLElement,
|
|
554
|
+
root: HTMLElement,
|
|
555
|
+
): HTMLElement | null {
|
|
556
|
+
let current: HTMLElement | null = element;
|
|
557
|
+
while (current && current !== root && root.contains(current)) {
|
|
558
|
+
const isImageWrapper =
|
|
559
|
+
current.classList.contains("fmd-pptx-image") ||
|
|
560
|
+
current.getAttribute("data-pptx-element-kind") === "image";
|
|
561
|
+
if (isImageWrapper && current.getAttribute("data-slide-object-id")) {
|
|
562
|
+
return current;
|
|
563
|
+
}
|
|
564
|
+
current = current.parentElement;
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
524
569
|
/** Convert a viewport click into the unscaled fmd-slide coordinate system. */
|
|
525
570
|
export function clientPointToSlideCoordinates(
|
|
526
571
|
clientX: number,
|
|
@@ -9,7 +9,9 @@ import { toast } from "sonner";
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
NewDeckReferenceStep,
|
|
12
|
+
type ImportedReference,
|
|
12
13
|
type NewDeckReferenceSelection,
|
|
14
|
+
type NewDeckReferenceSource,
|
|
13
15
|
} from "@/components/editor/NewDeckReferenceStep";
|
|
14
16
|
import {
|
|
15
17
|
uploadPromptFiles,
|
|
@@ -204,37 +206,159 @@ export function FirstDeckOnboardingFlow({
|
|
|
204
206
|
);
|
|
205
207
|
|
|
206
208
|
const handleReferenceImport = useCallback(
|
|
207
|
-
async (files: File[]) => {
|
|
209
|
+
async (files: File[]): Promise<ImportedReference | null> => {
|
|
208
210
|
setReferenceImporting(true);
|
|
209
211
|
try {
|
|
210
212
|
const uploaded = await uploadPromptFiles(files);
|
|
211
213
|
const pptxReference = uploaded.find((file) =>
|
|
212
214
|
file.originalName.toLowerCase().endsWith(".pptx"),
|
|
213
215
|
);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
const pdfReference = uploaded.find((file) =>
|
|
217
|
+
file.originalName.toLowerCase().endsWith(".pdf"),
|
|
218
|
+
);
|
|
219
|
+
let importedReference: ImportedReference | null = null;
|
|
218
220
|
let generationFiles = uploaded;
|
|
219
221
|
if (pptxReference) {
|
|
220
222
|
const imported = (await callAction("import-pptx", {
|
|
221
223
|
filePath: pptxReference.path,
|
|
222
|
-
})) as {
|
|
223
|
-
|
|
224
|
+
})) as {
|
|
225
|
+
id?: unknown;
|
|
226
|
+
imported?: unknown;
|
|
227
|
+
slideCount?: unknown;
|
|
228
|
+
title?: unknown;
|
|
229
|
+
};
|
|
230
|
+
if (
|
|
231
|
+
typeof imported.id !== "string" ||
|
|
232
|
+
!imported.id ||
|
|
233
|
+
imported.imported !== true ||
|
|
234
|
+
typeof imported.slideCount !== "number" ||
|
|
235
|
+
imported.slideCount < 1
|
|
236
|
+
) {
|
|
224
237
|
throw new Error("The imported presentation did not create a deck.");
|
|
225
238
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
importedReference = {
|
|
240
|
+
id: imported.id,
|
|
241
|
+
title:
|
|
242
|
+
typeof imported.title === "string" && imported.title
|
|
243
|
+
? imported.title
|
|
244
|
+
: t("home.importedReferenceDeck"),
|
|
245
|
+
source: "pptx",
|
|
231
246
|
};
|
|
232
247
|
generationFiles = uploaded.filter((file) => file !== pptxReference);
|
|
248
|
+
} else if (pdfReference) {
|
|
249
|
+
const referenceDeck = createDeck(undefined, {
|
|
250
|
+
noDefaultSlides: true,
|
|
251
|
+
});
|
|
252
|
+
const persisted = await ensureDeckPersisted(referenceDeck.id);
|
|
253
|
+
if (!persisted.persisted) {
|
|
254
|
+
deleteDeck(referenceDeck.id);
|
|
255
|
+
throw new Error(
|
|
256
|
+
describeDeckPersistenceFailure(
|
|
257
|
+
persisted,
|
|
258
|
+
"The PDF reference deck could not be saved.",
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const imported = (await callAction("import-file", {
|
|
264
|
+
filePath: pdfReference.path,
|
|
265
|
+
format: "pdf",
|
|
266
|
+
deckId: referenceDeck.id,
|
|
267
|
+
importIntoDeck: true,
|
|
268
|
+
})) as {
|
|
269
|
+
imported?: unknown;
|
|
270
|
+
deckId?: unknown;
|
|
271
|
+
pageCount?: unknown;
|
|
272
|
+
title?: unknown;
|
|
273
|
+
};
|
|
274
|
+
if (
|
|
275
|
+
imported.imported !== true ||
|
|
276
|
+
imported.deckId !== referenceDeck.id ||
|
|
277
|
+
typeof imported.pageCount !== "number" ||
|
|
278
|
+
imported.pageCount < 1
|
|
279
|
+
) {
|
|
280
|
+
throw new Error("The PDF reference deck could not be imported.");
|
|
281
|
+
}
|
|
282
|
+
importedReference = {
|
|
283
|
+
id: referenceDeck.id,
|
|
284
|
+
title:
|
|
285
|
+
typeof imported.title === "string" && imported.title
|
|
286
|
+
? imported.title
|
|
287
|
+
: t("home.importedReferenceDeck"),
|
|
288
|
+
source: "pdf",
|
|
289
|
+
};
|
|
290
|
+
generationFiles = uploaded.filter((file) => file !== pdfReference);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
deleteDeck(referenceDeck.id);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
233
295
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
296
|
+
setPromptFiles((current) => [...current, ...generationFiles]);
|
|
297
|
+
if (importedReference) {
|
|
298
|
+
await reloadDecks();
|
|
299
|
+
rememberReference({ id: importedReference.id, kind: "deck" });
|
|
300
|
+
}
|
|
301
|
+
return importedReference;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
toast.error(t("editorToolbar.uploadFailed"), {
|
|
304
|
+
description:
|
|
305
|
+
error instanceof Error
|
|
306
|
+
? error.message
|
|
307
|
+
: t("editorToolbar.importFailedDescription"),
|
|
308
|
+
});
|
|
309
|
+
return null;
|
|
310
|
+
} finally {
|
|
311
|
+
setReferenceImporting(false);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
[
|
|
315
|
+
callAction,
|
|
316
|
+
createDeck,
|
|
317
|
+
deleteDeck,
|
|
318
|
+
ensureDeckPersisted,
|
|
319
|
+
reloadDecks,
|
|
320
|
+
rememberReference,
|
|
321
|
+
t,
|
|
322
|
+
],
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const handleReferenceSourceImport = useCallback(
|
|
326
|
+
async (
|
|
327
|
+
source: NewDeckReferenceSource,
|
|
328
|
+
): Promise<ImportedReference | null> => {
|
|
329
|
+
if (source.kind !== "google-docs") return null;
|
|
330
|
+
setReferenceImporting(true);
|
|
331
|
+
try {
|
|
332
|
+
const imported = (await callAction("import-google-slides-reference", {
|
|
333
|
+
presentationUrl: source.value,
|
|
334
|
+
})) as {
|
|
335
|
+
id?: unknown;
|
|
336
|
+
imported?: unknown;
|
|
337
|
+
slideCount?: unknown;
|
|
338
|
+
title?: unknown;
|
|
339
|
+
};
|
|
340
|
+
if (
|
|
341
|
+
typeof imported.id !== "string" ||
|
|
342
|
+
!imported.id ||
|
|
343
|
+
imported.imported !== true ||
|
|
344
|
+
typeof imported.slideCount !== "number" ||
|
|
345
|
+
imported.slideCount < 1
|
|
346
|
+
) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
"The Google Slides presentation did not create a deck.",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const importedReference: ImportedReference = {
|
|
352
|
+
id: imported.id,
|
|
353
|
+
title:
|
|
354
|
+
typeof imported.title === "string" && imported.title
|
|
355
|
+
? imported.title
|
|
356
|
+
: t("home.importedReferenceDeck"),
|
|
357
|
+
source: "google-slides",
|
|
358
|
+
};
|
|
359
|
+
await reloadDecks();
|
|
360
|
+
rememberReference({ id: importedReference.id, kind: "deck" });
|
|
361
|
+
return importedReference;
|
|
238
362
|
} catch (error) {
|
|
239
363
|
toast.error(t("editorToolbar.uploadFailed"), {
|
|
240
364
|
description:
|
|
@@ -242,11 +366,12 @@ export function FirstDeckOnboardingFlow({
|
|
|
242
366
|
? error.message
|
|
243
367
|
: t("editorToolbar.importFailedDescription"),
|
|
244
368
|
});
|
|
369
|
+
return null;
|
|
245
370
|
} finally {
|
|
246
371
|
setReferenceImporting(false);
|
|
247
372
|
}
|
|
248
373
|
},
|
|
249
|
-
[
|
|
374
|
+
[callAction, reloadDecks, rememberReference, t],
|
|
250
375
|
);
|
|
251
376
|
|
|
252
377
|
const handleReferenceSkip = useCallback(() => {
|
|
@@ -267,6 +392,7 @@ export function FirstDeckOnboardingFlow({
|
|
|
267
392
|
recentReferences={recentReferences}
|
|
268
393
|
onSelect={handleReferenceSelect}
|
|
269
394
|
onImport={handleReferenceImport}
|
|
395
|
+
onImportSource={handleReferenceSourceImport}
|
|
270
396
|
onSkip={handleReferenceSkip}
|
|
271
397
|
onOpenChange={(open) => {
|
|
272
398
|
if (!open) setStep("prompt");
|
|
@@ -278,7 +404,6 @@ export function FirstDeckOnboardingFlow({
|
|
|
278
404
|
chooseDeckLabel={t("home.referenceDeckPlaceholder")}
|
|
279
405
|
importingLabel={t("raw.uploading")}
|
|
280
406
|
skipLabel={t("home.referenceDeckNone")}
|
|
281
|
-
defaultSuffix={t("home.defaultSuffix")}
|
|
282
407
|
starredLabel={t("home.referenceDeckStarredGroup")}
|
|
283
408
|
otherDecksLabel={t("home.referenceDeckOtherGroup")}
|
|
284
409
|
searchDecksLabel={t("root.searchDecks")}
|
|
@@ -672,6 +672,12 @@ const messages = {
|
|
|
672
672
|
chooseReferences: "Choose references",
|
|
673
673
|
addDesignSystem: "+ Design system",
|
|
674
674
|
importFrom: "Import from",
|
|
675
|
+
imported: "Imported",
|
|
676
|
+
importedReferenceDeck: "Imported reference deck",
|
|
677
|
+
referenceImportSuccess: "Imported successfully",
|
|
678
|
+
referenceImportSelected: '"{{title}}" is now the reference deck.',
|
|
679
|
+
continue: "Continue",
|
|
680
|
+
continueToGenerate: "Continue to generate",
|
|
675
681
|
designSystem: "Design system",
|
|
676
682
|
referenceDeck: "Reference deck",
|
|
677
683
|
referenceDeckPlaceholder: "Match the style of an existing deck",
|
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
* largest rendered element so a thumbnail's transform: scale(0.25)
|
|
11
11
|
* doesn't shrink the captured pixels.
|
|
12
12
|
*/
|
|
13
|
+
import { appBasePath } from "@agent-native/core/client/api-path";
|
|
14
|
+
|
|
13
15
|
import { type AspectRatio, getAspectRatioDims } from "./aspect-ratios";
|
|
14
16
|
import { importExportModule } from "./dynamic-import";
|
|
15
17
|
|
|
18
|
+
/** Same-origin URL that re-serves a remote image, bypassing its missing CORS. */
|
|
19
|
+
export function imageProxyUrl(src: string): string {
|
|
20
|
+
return `${appBasePath()}/api/image-proxy?url=${encodeURIComponent(src)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
/**
|
|
17
24
|
* Cross-origin <img> elements without an explicit `crossOrigin="anonymous"`
|
|
18
25
|
* attribute taint the canvas when rasterized via <foreignObject>, producing
|
|
@@ -21,8 +28,15 @@ import { importExportModule } from "./dynamic-import";
|
|
|
21
28
|
* setting the attribute and re-assigning the same src. This is the root
|
|
22
29
|
* cause of the "blank images in exported PDF" bug Rochkind reported.
|
|
23
30
|
*/
|
|
24
|
-
export async function preloadImagesWithCors(
|
|
31
|
+
export async function preloadImagesWithCors(
|
|
32
|
+
root: HTMLElement,
|
|
33
|
+
): Promise<() => void> {
|
|
25
34
|
const imgs = Array.from(root.querySelectorAll<HTMLImageElement>("img"));
|
|
35
|
+
// The slide DOM is the live editor canvas. Anything rewritten here would
|
|
36
|
+
// otherwise be picked up by the next save and persisted into the deck, so
|
|
37
|
+
// every mutation is recorded and undone once the capture is done.
|
|
38
|
+
const restores: Array<() => void> = [];
|
|
39
|
+
|
|
26
40
|
await Promise.all(
|
|
27
41
|
imgs.map(async (img) => {
|
|
28
42
|
const src = img.currentSrc || img.src;
|
|
@@ -35,30 +49,59 @@ export async function preloadImagesWithCors(root: HTMLElement): Promise<void> {
|
|
|
35
49
|
isCrossOrigin = false;
|
|
36
50
|
}
|
|
37
51
|
if (!isCrossOrigin) return;
|
|
52
|
+
|
|
53
|
+
const originalCrossOrigin = img.getAttribute("crossorigin");
|
|
54
|
+
const originalSrc = img.getAttribute("src");
|
|
55
|
+
const restore = () => {
|
|
56
|
+
if (originalCrossOrigin === null) img.removeAttribute("crossorigin");
|
|
57
|
+
else img.setAttribute("crossorigin", originalCrossOrigin);
|
|
58
|
+
if (originalSrc === null) img.removeAttribute("src");
|
|
59
|
+
else img.setAttribute("src", originalSrc);
|
|
60
|
+
};
|
|
61
|
+
|
|
38
62
|
if (img.crossOrigin === "anonymous") {
|
|
39
63
|
// Already CORS-enabled; just make sure it's decoded.
|
|
40
64
|
try {
|
|
41
65
|
await img.decode();
|
|
66
|
+
return;
|
|
42
67
|
} catch {
|
|
43
|
-
|
|
68
|
+
// coercion-ok: a failed direct load is the signal to try the proxy,
|
|
69
|
+
// and the proxy attempt below reports its own failure.
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
img.crossOrigin = "anonymous";
|
|
73
|
+
// Re-set src to retrigger the load with the new CORS attribute.
|
|
74
|
+
img.src = src;
|
|
75
|
+
restores.push(restore);
|
|
76
|
+
try {
|
|
77
|
+
await img.decode();
|
|
78
|
+
return;
|
|
79
|
+
} catch {
|
|
80
|
+
// coercion-ok: same as above — this is the CORS probe, not the
|
|
81
|
+
// final outcome.
|
|
44
82
|
}
|
|
45
|
-
return;
|
|
46
83
|
}
|
|
84
|
+
|
|
85
|
+
// The host does not send Access-Control-Allow-Origin, and no client-side
|
|
86
|
+
// flag can override that. Re-serve the image from our own origin so the
|
|
87
|
+
// canvas stays clean instead of rasterizing a blank rect.
|
|
88
|
+
if (!restores.includes(restore)) restores.push(restore);
|
|
47
89
|
img.crossOrigin = "anonymous";
|
|
48
|
-
|
|
49
|
-
img.src = src;
|
|
90
|
+
img.src = imageProxyUrl(src);
|
|
50
91
|
try {
|
|
51
92
|
await img.decode();
|
|
52
93
|
} catch (err) {
|
|
53
|
-
// Server didn't return Access-Control-Allow-Origin. The screenshot
|
|
54
|
-
// will be blank for this image — log so the user can swap the host.
|
|
55
94
|
console.warn(
|
|
56
|
-
`[export-pdf]
|
|
95
|
+
`[export-pdf] image could not be loaded for export: ${src}`,
|
|
57
96
|
err,
|
|
58
97
|
);
|
|
59
98
|
}
|
|
60
99
|
}),
|
|
61
100
|
);
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
for (const restore of restores) restore();
|
|
104
|
+
};
|
|
62
105
|
}
|
|
63
106
|
|
|
64
107
|
export function findSlideExportSource(
|
|
@@ -132,23 +175,39 @@ export async function exportDeckAsPdf(
|
|
|
132
175
|
// Force CORS-enabled re-fetch on every cross-origin <img> before
|
|
133
176
|
// capture — otherwise the canvas tainting check inside modern-screenshot
|
|
134
177
|
// produces a blank rect for the image.
|
|
135
|
-
await preloadImagesWithCors(source);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
178
|
+
const restoreImages = await preloadImagesWithCors(source);
|
|
179
|
+
|
|
180
|
+
let dataUrl: string;
|
|
181
|
+
try {
|
|
182
|
+
dataUrl = await domToJpeg(source, {
|
|
183
|
+
width: dims.width,
|
|
184
|
+
height: dims.height,
|
|
185
|
+
scale: 2, // 2x for crisp text
|
|
186
|
+
// guard:allow-raw-color — a PDF page has no theme to follow.
|
|
187
|
+
backgroundColor: "#000000",
|
|
188
|
+
quality: 0.92,
|
|
189
|
+
// Pair with the in-DOM CORS preload above. modern-screenshot's
|
|
190
|
+
// internal image fetcher needs no-cache so re-issued requests don't
|
|
191
|
+
// get served the original tainted (no-CORS) response from the HTTP
|
|
192
|
+
// cache, and an anonymous-CORS request mode so the response itself
|
|
193
|
+
// is usable on a clean canvas.
|
|
194
|
+
//
|
|
195
|
+
// `same-origin` rather than `omit`: images the preload rewrote to
|
|
196
|
+
// /api/image-proxy are same-origin and that route needs the session
|
|
197
|
+
// cookie, so omitting credentials would 401 exactly the images this
|
|
198
|
+
// is meant to rescue. Cross-origin requests still go out anonymously,
|
|
199
|
+
// which is what CORS mode requires.
|
|
200
|
+
fetch: {
|
|
201
|
+
requestInit: {
|
|
202
|
+
cache: "no-cache",
|
|
203
|
+
mode: "cors",
|
|
204
|
+
credentials: "same-origin",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
} finally {
|
|
209
|
+
restoreImages();
|
|
210
|
+
}
|
|
152
211
|
|
|
153
212
|
if (i > 0) pdf.addPage([dims.width, dims.height], orientation);
|
|
154
213
|
pdf.addImage(dataUrl, "JPEG", 0, 0, dims.width, dims.height);
|
|
@@ -2,12 +2,18 @@ export function shouldShowNewDeckGeneratingOverlay({
|
|
|
2
2
|
generating,
|
|
3
3
|
isNewDeckCreation,
|
|
4
4
|
slideCount,
|
|
5
|
+
generationStarted,
|
|
5
6
|
}: {
|
|
6
7
|
generating: boolean;
|
|
7
8
|
isNewDeckCreation: boolean;
|
|
8
9
|
slideCount?: number | null;
|
|
10
|
+
generationStarted: boolean;
|
|
9
11
|
}): boolean {
|
|
10
|
-
return
|
|
12
|
+
return (
|
|
13
|
+
isNewDeckCreation &&
|
|
14
|
+
(slideCount ?? 0) === 0 &&
|
|
15
|
+
(generating || !generationStarted)
|
|
16
|
+
);
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export function shouldShowNewDeckGeneratingProgress({
|
|
@@ -9,6 +9,16 @@ export type ImportedSourceDeck = {
|
|
|
9
9
|
imagesSkipped: number;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* PDF/PPTX import renders every page (image extraction, per-page fidelity
|
|
14
|
+
* parsing) and can run well past the client's default 60s action timeout on
|
|
15
|
+
* large or image-heavy files. A timeout here only aborts the client's wait —
|
|
16
|
+
* the server keeps importing and the deck still ends up with slides — so the
|
|
17
|
+
* default made the editor silently fail on a deck that had, or was about to
|
|
18
|
+
* have, real content.
|
|
19
|
+
*/
|
|
20
|
+
export const IMPORT_ACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
12
22
|
function sourceFormat(file: UploadedFile): "pdf" | "pptx" | null {
|
|
13
23
|
const name = file.originalName.toLowerCase();
|
|
14
24
|
if (name.endsWith(".pptx")) return "pptx";
|
|
@@ -57,6 +67,7 @@ export async function importUploadedDeckIntoDeck(
|
|
|
57
67
|
deckId,
|
|
58
68
|
importIntoDeck: true,
|
|
59
69
|
},
|
|
70
|
+
{ timeoutMs: IMPORT_ACTION_TIMEOUT_MS },
|
|
60
71
|
),
|
|
61
72
|
);
|
|
62
73
|
if (result.imported !== true || result.deckId !== deckId) {
|
|
@@ -287,6 +287,7 @@ export default function DeckEditor() {
|
|
|
287
287
|
generating,
|
|
288
288
|
isNewDeckCreation: wasNewDeckCreation.current,
|
|
289
289
|
slideCount,
|
|
290
|
+
generationStarted: newDeckGenerationStarted.current,
|
|
290
291
|
});
|
|
291
292
|
const { designSystem, imageStyleReferenceUrls } = useDeckDesignSystem(
|
|
292
293
|
deck?.designSystemId,
|
|
@@ -326,7 +327,9 @@ export default function DeckEditor() {
|
|
|
326
327
|
|
|
327
328
|
const showQuestionFlow = Boolean(questionFlowQuestions?.length);
|
|
328
329
|
const generatingSlideVisible =
|
|
329
|
-
canEdit &&
|
|
330
|
+
canEdit &&
|
|
331
|
+
!showQuestionFlow &&
|
|
332
|
+
(isNewDeckGenerating || addSlideGenerating || showNewDeckGeneratingOverlay);
|
|
330
333
|
const showCurrentSlideEditor =
|
|
331
334
|
!generatingSlideSelected &&
|
|
332
335
|
!showNewDeckGeneratingOverlay &&
|
|
@@ -342,12 +345,47 @@ export default function DeckEditor() {
|
|
|
342
345
|
if (!generating) setAddSlideGenerating(false);
|
|
343
346
|
}, [generating]);
|
|
344
347
|
|
|
345
|
-
|
|
348
|
+
// Below `md` the rail is a drawer behind a full-viewport dimming scrim; at
|
|
349
|
+
// `md` and up it's docked with no scrim. `sidebarOpen` is seeded from the
|
|
350
|
+
// width at mount only, so a window that starts wide and is then narrowed
|
|
351
|
+
// (or an editor opened in a resizable preview pane) keeps `sidebarOpen`
|
|
352
|
+
// true while the scrim stops being `md:hidden` — dimming the whole editor
|
|
353
|
+
// with no way to dismiss it.
|
|
346
354
|
useEffect(() => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}, [
|
|
355
|
+
const onResize = () => setSidebarOpen(window.innerWidth >= 768);
|
|
356
|
+
window.addEventListener("resize", onResize);
|
|
357
|
+
return () => window.removeEventListener("resize", onResize);
|
|
358
|
+
}, []);
|
|
359
|
+
|
|
360
|
+
const previousSlideIdsRef = useRef<string[]>([]);
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
const currentSlideIds = deck?.slides.map((slide) => slide.id) ?? [];
|
|
363
|
+
const previousSlideIds = previousSlideIdsRef.current;
|
|
364
|
+
const addedSlide = deck?.slides.find(
|
|
365
|
+
(slide) => !previousSlideIds.includes(slide.id),
|
|
366
|
+
);
|
|
367
|
+
const slideWasAdded = currentSlideIds.length > previousSlideIds.length;
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
slideWasAdded &&
|
|
371
|
+
addedSlide &&
|
|
372
|
+
(generating ||
|
|
373
|
+
isNewDeckGenerating ||
|
|
374
|
+
addSlideGenerating ||
|
|
375
|
+
generatingSlideSelected)
|
|
376
|
+
) {
|
|
377
|
+
setActiveSlideId(addedSlide.id);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
previousSlideIdsRef.current = currentSlideIds;
|
|
381
|
+
if (slideWasAdded) setGeneratingSlideSelected(false);
|
|
382
|
+
}, [
|
|
383
|
+
addSlideGenerating,
|
|
384
|
+
deck,
|
|
385
|
+
generating,
|
|
386
|
+
generatingSlideSelected,
|
|
387
|
+
isNewDeckGenerating,
|
|
388
|
+
]);
|
|
351
389
|
|
|
352
390
|
useEffect(() => {
|
|
353
391
|
if (
|
|
@@ -1128,14 +1166,6 @@ export default function DeckEditor() {
|
|
|
1128
1166
|
setActiveSlideId(slideId);
|
|
1129
1167
|
if (window.innerWidth < 768) setSidebarOpen(false);
|
|
1130
1168
|
}}
|
|
1131
|
-
onDuplicateSlide={(slideId) => duplicateSlide(id, slideId)}
|
|
1132
|
-
onDeleteSlide={(slideId) => {
|
|
1133
|
-
const idx = deck.slides.findIndex((s) => s.id === slideId);
|
|
1134
|
-
const nextSlide =
|
|
1135
|
-
deck.slides[idx + 1] || deck.slides[idx - 1];
|
|
1136
|
-
deleteSlideWithUndo(id, slideId);
|
|
1137
|
-
if (nextSlide) setActiveSlideId(nextSlide.id);
|
|
1138
|
-
}}
|
|
1139
1169
|
readOnly={!canEdit}
|
|
1140
1170
|
slidePresence={slidePresence}
|
|
1141
1171
|
recentEdits={deckRecentEdits}
|