@agent-native/core 0.144.0 → 0.144.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/actions/migrate-first-party-analytics-to-bigquery.ts +1 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +36 -14
- package/corpus/templates/clips/app/components/player/player-controls.tsx +0 -3
- package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -33
- package/corpus/templates/clips/app/components/player/video-player.tsx +58 -11
- package/corpus/templates/clips/changelog/2026-08-06-trimmed-clips-play-on-a-clean-timeline-without-cut-markers.md +6 -0
- package/corpus/templates/dispatch/AGENTS.md +8 -1
- package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -0
- package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +30 -15
- package/corpus/templates/dispatch/app/i18n-data.ts +131 -0
- package/corpus/templates/dispatch/app/routes/admin._index.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/admin.agents.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.approvals.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.apps.$appId.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.apps.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.audit.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.automations.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.destinations.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/admin.dreams.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.identities.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.integrations.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/admin.messaging.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.metrics.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.new-app.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.operations.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.thread-debug.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/admin.transactional-email.$appId.$id.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/admin.transactional-email.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/admin.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.vault.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/admin.workspace.tsx +1 -0
- package/corpus/templates/dispatch/changelog/2026-08-06-dispatch-keeps-everyday-navigation-focused-while-management-.md +6 -0
- package/corpus/templates/slides/AGENTS.md +10 -12
- package/corpus/templates/slides/actions/export-pptx.ts +68 -0
- package/corpus/templates/slides/actions/get-deck.ts +12 -0
- package/corpus/templates/slides/actions/import-file.ts +130 -55
- package/corpus/templates/slides/actions/import-pptx.ts +108 -8
- package/corpus/templates/slides/actions/patch-deck.ts +51 -0
- package/corpus/templates/slides/actions/update-slide.ts +29 -3
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +11 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +72 -11
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +89 -0
- package/corpus/templates/slides/app/pages/Index.tsx +85 -18
- package/corpus/templates/slides/changelog/2026-08-06-uploaded-decks-keep-their-slides-and-visuals-when-you-improv.md +6 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +13 -2
- package/corpus/templates/slides/server/lib/chat-attachments.ts +2 -2
- package/corpus/templates/slides/server/lib/source-import.ts +167 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +8 -0
- package/dist/collab/routes.d.ts +1 -1
- package/dist/jobs/scheduler.js +79 -23
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/getting-started-actions.mdx +38 -189
- package/docs/content/getting-started-database.mdx +80 -164
- package/docs/content/getting-started-pages.mdx +44 -155
- package/docs/content/getting-started.mdx +26 -106
- package/package.json +1 -1
|
@@ -18,6 +18,12 @@ import { notifyClients } from "../server/handlers/decks.js";
|
|
|
18
18
|
import { uploadPptxSlideImages } from "../server/handlers/import/pptx-assets.js";
|
|
19
19
|
import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
|
|
20
20
|
import { setupPdfParse } from "../server/lib/pdf-parse-setup.js";
|
|
21
|
+
import {
|
|
22
|
+
buildSourceImportMetadata,
|
|
23
|
+
mergeSourceImportMetadata,
|
|
24
|
+
sourceImportForDeck,
|
|
25
|
+
type SourceImportSlideSnapshot,
|
|
26
|
+
} from "../server/lib/source-import.js";
|
|
21
27
|
import {
|
|
22
28
|
ASPECT_RATIOS,
|
|
23
29
|
DEFAULT_ASPECT_RATIO,
|
|
@@ -31,7 +37,7 @@ const DEFAULT_MAX_SOURCE_CHARS = 60_000;
|
|
|
31
37
|
export default defineAction({
|
|
32
38
|
description:
|
|
33
39
|
"Import a file (PPTX, DOCX, PDF, FIG) and extract content for creating slides or slide design systems. " +
|
|
34
|
-
"For PPTX files, returns parsed slides with text and layout info ready for conversion. " +
|
|
40
|
+
"For PPTX files, returns parsed slides with text and layout info ready for conversion, or writes positioned source-preserving slides when importIntoDeck is true. " +
|
|
35
41
|
"For DOCX files, returns structured sections extracted from the document. " +
|
|
36
42
|
"For PDF files, returns extracted text organized by page, or source-faithful page-image slides when importIntoDeck is true. " +
|
|
37
43
|
"For Figma .fig files, requires Builder.io (free tier available) and starts Builder design-system indexing; the returned Builder job/design-system ids are the source of truth. " +
|
|
@@ -153,7 +159,37 @@ export default defineAction({
|
|
|
153
159
|
(total, r) => total + r.imageSkippedCount,
|
|
154
160
|
0,
|
|
155
161
|
);
|
|
156
|
-
|
|
162
|
+
const sourceImport = buildSourceImportMetadata({
|
|
163
|
+
format: "pptx",
|
|
164
|
+
slides: pptxResults.map((result) => ({
|
|
165
|
+
id: result.slide.id,
|
|
166
|
+
text: result.sourceText,
|
|
167
|
+
notes: result.slide.notes ?? "",
|
|
168
|
+
imageUrls: result.imageUrls,
|
|
169
|
+
editableText: true,
|
|
170
|
+
})),
|
|
171
|
+
imagesSkipped,
|
|
172
|
+
});
|
|
173
|
+
if (imagesSkipped > 0) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Source-faithful PPTX import could not preserve ${imagesSkipped} image(s). No slides were written. Retry with browser-renderable images or use a PDF export for page-faithful preservation.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const aspectRatio =
|
|
179
|
+
presentation.slides[0]?.widthEmu && presentation.slides[0]?.heightEmu
|
|
180
|
+
? nearestAspectRatio(
|
|
181
|
+
presentation.slides[0].widthEmu,
|
|
182
|
+
presentation.slides[0].heightEmu,
|
|
183
|
+
)
|
|
184
|
+
: undefined;
|
|
185
|
+
await appendDeckSlides(
|
|
186
|
+
deckId,
|
|
187
|
+
title,
|
|
188
|
+
slides,
|
|
189
|
+
"import-file:pptx",
|
|
190
|
+
aspectRatio,
|
|
191
|
+
sourceImport,
|
|
192
|
+
);
|
|
157
193
|
return {
|
|
158
194
|
format: "pptx",
|
|
159
195
|
title,
|
|
@@ -241,9 +277,15 @@ export default defineAction({
|
|
|
241
277
|
// typography) bake their visuals into vector/image page content with
|
|
242
278
|
// no reliable text/shape structure to reconstruct, so importing into a
|
|
243
279
|
// deck rasterizes each page instead of flattening it to generic bullet
|
|
244
|
-
// text.
|
|
245
|
-
//
|
|
246
|
-
|
|
280
|
+
// text. Do not silently fall back to text reconstruction when the
|
|
281
|
+
// renderer is unavailable: that is how a visually complete source deck
|
|
282
|
+
// turns into a misleading, content-lossy import.
|
|
283
|
+
if (importIntoDeck) {
|
|
284
|
+
if (!canvasFactory) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
"Source-faithful PDF import is unavailable in this runtime because the page renderer is not configured. Retry after connecting file storage or use a PDF export with a supported Slides deployment.",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
247
289
|
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
248
290
|
return importPdfPagesAsFullBleedSlides({
|
|
249
291
|
fileBuffer,
|
|
@@ -254,8 +296,6 @@ export default defineAction({
|
|
|
254
296
|
});
|
|
255
297
|
}
|
|
256
298
|
|
|
257
|
-
const { convertSectionsToSlides } =
|
|
258
|
-
await import("../server/handlers/import/html-converter.js");
|
|
259
299
|
const pdf = new PDFParse({
|
|
260
300
|
data: new Uint8Array(fileBuffer),
|
|
261
301
|
CanvasFactory: canvasFactory,
|
|
@@ -270,29 +310,6 @@ export default defineAction({
|
|
|
270
310
|
);
|
|
271
311
|
}
|
|
272
312
|
|
|
273
|
-
if (importIntoDeck) {
|
|
274
|
-
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
275
|
-
const sections = textPages.map((p) => ({
|
|
276
|
-
heading: `Page ${p.num}`,
|
|
277
|
-
content: p.text,
|
|
278
|
-
}));
|
|
279
|
-
const slideHtmlArray = convertSectionsToSlides(sections);
|
|
280
|
-
const slides = slideHtmlArray.map((content) => ({
|
|
281
|
-
id: newSlideId(),
|
|
282
|
-
content,
|
|
283
|
-
layout: "content",
|
|
284
|
-
notes: "",
|
|
285
|
-
}));
|
|
286
|
-
await appendDeckSlides(deckId, title, slides, "import-file:pdf");
|
|
287
|
-
return {
|
|
288
|
-
format: "pdf",
|
|
289
|
-
title,
|
|
290
|
-
pageCount: pages.length,
|
|
291
|
-
slideCount: slides.length,
|
|
292
|
-
deckId,
|
|
293
|
-
imported: true,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
313
|
const totalTextLength = textPages.reduce(
|
|
297
314
|
(sum, p) => sum + p.text.length,
|
|
298
315
|
0,
|
|
@@ -342,7 +359,7 @@ async function importPdfPagesAsFullBleedSlides(args: {
|
|
|
342
359
|
canvasFactory: object;
|
|
343
360
|
}) {
|
|
344
361
|
const { fileBuffer, title, deckId, PDFParse, canvasFactory } = args;
|
|
345
|
-
const { buildFullPageImageSlideHtml
|
|
362
|
+
const { buildFullPageImageSlideHtml } =
|
|
346
363
|
await import("../server/handlers/import/html-converter.js");
|
|
347
364
|
|
|
348
365
|
const pdf = new PDFParse({
|
|
@@ -355,7 +372,15 @@ async function importPdfPagesAsFullBleedSlides(args: {
|
|
|
355
372
|
{ data: Uint8Array; width: number; height: number }
|
|
356
373
|
>;
|
|
357
374
|
try {
|
|
358
|
-
|
|
375
|
+
let textPages: { num: number; text: string }[] = [];
|
|
376
|
+
try {
|
|
377
|
+
textPages = normalizePdfPages(await pdf.getText());
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.warn(
|
|
380
|
+
"[import-file] PDF text extraction failed; preserving rendered pages and leaving notes empty:",
|
|
381
|
+
error,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
359
384
|
// Rasterize the complete page so positioned text, vector shapes, logos,
|
|
360
385
|
// photos, and the source page proportions survive the import together.
|
|
361
386
|
const screenshotResult = await pdf.getScreenshot({
|
|
@@ -371,10 +396,23 @@ async function importPdfPagesAsFullBleedSlides(args: {
|
|
|
371
396
|
height: page.height,
|
|
372
397
|
});
|
|
373
398
|
}
|
|
399
|
+
if (screenshotsByPage.size === 0) {
|
|
400
|
+
throw new Error("The PDF renderer returned no pages.");
|
|
401
|
+
}
|
|
402
|
+
pages = screenshotResult.pages.map((page) => ({
|
|
403
|
+
num: page.pageNumber,
|
|
404
|
+
text:
|
|
405
|
+
textPages.find((textPage) => textPage.num === page.pageNumber)?.text ??
|
|
406
|
+
"",
|
|
407
|
+
}));
|
|
374
408
|
} finally {
|
|
375
409
|
await pdf.destroy();
|
|
376
410
|
}
|
|
377
411
|
|
|
412
|
+
if (pages.length === 0) {
|
|
413
|
+
throw new Error("The PDF renderer returned no importable pages.");
|
|
414
|
+
}
|
|
415
|
+
|
|
378
416
|
// Source decks (e.g. Instagram carousel exports) are commonly portrait or
|
|
379
417
|
// square, not the deck editor's 16:9 default — match the canvas to the
|
|
380
418
|
// first rendered page's own proportions instead of stretching/cropping it.
|
|
@@ -386,24 +424,14 @@ async function importPdfPagesAsFullBleedSlides(args: {
|
|
|
386
424
|
: undefined;
|
|
387
425
|
|
|
388
426
|
const ownerEmail = getRequestUserEmail();
|
|
389
|
-
const
|
|
427
|
+
const imported = await Promise.all(
|
|
390
428
|
pages.map(async (page) => {
|
|
391
|
-
const lines = page.text
|
|
392
|
-
.split(/\r?\n/)
|
|
393
|
-
.map((line) => line.trim())
|
|
394
|
-
.filter(Boolean);
|
|
395
429
|
const screenshot = screenshotsByPage.get(page.num);
|
|
396
430
|
|
|
397
431
|
if (!screenshot) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
id: newSlideId(),
|
|
403
|
-
content: content ?? '<div class="fmd-slide"></div>',
|
|
404
|
-
layout: "content",
|
|
405
|
-
notes: "",
|
|
406
|
-
};
|
|
432
|
+
throw new Error(
|
|
433
|
+
`The PDF renderer did not return page ${page.num}; refusing a partial source import.`,
|
|
434
|
+
);
|
|
407
435
|
}
|
|
408
436
|
|
|
409
437
|
const uploadResult = await uploadFile({
|
|
@@ -418,16 +446,42 @@ async function importPdfPagesAsFullBleedSlides(args: {
|
|
|
418
446
|
"File storage is not configured. Connect Builder.io (free tier available) or another upload provider before importing PDF slides.",
|
|
419
447
|
);
|
|
420
448
|
}
|
|
449
|
+
const id = newSlideId();
|
|
421
450
|
return {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
451
|
+
slide: {
|
|
452
|
+
id,
|
|
453
|
+
content: buildFullPageImageSlideHtml(
|
|
454
|
+
uploadResult.url,
|
|
455
|
+
screenshot.width,
|
|
456
|
+
screenshot.height,
|
|
457
|
+
),
|
|
458
|
+
layout: "full-image",
|
|
459
|
+
notes: page.text,
|
|
460
|
+
},
|
|
461
|
+
snapshot: {
|
|
462
|
+
id,
|
|
463
|
+
text: page.text,
|
|
464
|
+
notes: page.text,
|
|
465
|
+
imageUrls: [uploadResult.url],
|
|
466
|
+
editableText: false,
|
|
467
|
+
} satisfies SourceImportSlideSnapshot,
|
|
426
468
|
};
|
|
427
469
|
}),
|
|
428
470
|
);
|
|
471
|
+
const slides = imported.map((entry) => entry.slide);
|
|
472
|
+
const sourceImport = buildSourceImportMetadata({
|
|
473
|
+
format: "pdf",
|
|
474
|
+
slides: imported.map((entry) => entry.snapshot),
|
|
475
|
+
});
|
|
429
476
|
|
|
430
|
-
await appendDeckSlides(
|
|
477
|
+
await appendDeckSlides(
|
|
478
|
+
deckId,
|
|
479
|
+
title,
|
|
480
|
+
slides,
|
|
481
|
+
"import-file:pdf",
|
|
482
|
+
aspectRatio,
|
|
483
|
+
sourceImport,
|
|
484
|
+
);
|
|
431
485
|
|
|
432
486
|
return {
|
|
433
487
|
format: "pdf",
|
|
@@ -459,6 +513,8 @@ async function buildPptxSlide(
|
|
|
459
513
|
splitByParagraph?: boolean;
|
|
460
514
|
};
|
|
461
515
|
imageSkippedCount: number;
|
|
516
|
+
sourceText: string;
|
|
517
|
+
imageUrls: string[];
|
|
462
518
|
}> {
|
|
463
519
|
const { convertToSlideHtml } =
|
|
464
520
|
await import("../server/handlers/import/html-converter.js");
|
|
@@ -467,9 +523,10 @@ async function buildPptxSlide(
|
|
|
467
523
|
slideIndex,
|
|
468
524
|
ownerEmail,
|
|
469
525
|
});
|
|
526
|
+
const id = newSlideId();
|
|
470
527
|
return {
|
|
471
528
|
slide: {
|
|
472
|
-
id
|
|
529
|
+
id,
|
|
473
530
|
content: convertToSlideHtml(slide, uploadedImages.urls, themeFont),
|
|
474
531
|
layout: slide.layoutHint ?? "content",
|
|
475
532
|
notes: slide.notes,
|
|
@@ -477,6 +534,8 @@ async function buildPptxSlide(
|
|
|
477
534
|
...(slide.splitByParagraph ? { splitByParagraph: true } : {}),
|
|
478
535
|
},
|
|
479
536
|
imageSkippedCount: uploadedImages.imageSkippedCount,
|
|
537
|
+
sourceText: slide.texts.map((text) => text.content).join("\n"),
|
|
538
|
+
imageUrls: Object.values(uploadedImages.urls),
|
|
480
539
|
};
|
|
481
540
|
}
|
|
482
541
|
|
|
@@ -568,6 +627,7 @@ async function appendDeckSlides(
|
|
|
568
627
|
}>,
|
|
569
628
|
source: string,
|
|
570
629
|
aspectRatio?: AspectRatio,
|
|
630
|
+
sourceImport?: ReturnType<typeof buildSourceImportMetadata>,
|
|
571
631
|
) {
|
|
572
632
|
await assertAccess("deck", deckId, "editor");
|
|
573
633
|
|
|
@@ -601,11 +661,18 @@ async function appendDeckSlides(
|
|
|
601
661
|
// every slide already on it.
|
|
602
662
|
const hadExistingSlides = previousSlides.length > 0;
|
|
603
663
|
const nextTitle = hadExistingSlides ? (existing[0].title ?? title) : title;
|
|
664
|
+
const nextSourceImport = sourceImport
|
|
665
|
+
? mergeSourceImportMetadata(
|
|
666
|
+
sourceImportForDeck(previousData.sourceImport),
|
|
667
|
+
sourceImport,
|
|
668
|
+
)
|
|
669
|
+
: sourceImportForDeck(previousData.sourceImport);
|
|
604
670
|
const data = {
|
|
605
671
|
...previousData,
|
|
606
672
|
title: nextTitle,
|
|
607
673
|
slides: [...previousSlides, ...slides],
|
|
608
674
|
...(!hadExistingSlides && aspectRatio ? { aspectRatio } : {}),
|
|
675
|
+
...(nextSourceImport ? { sourceImport: nextSourceImport } : {}),
|
|
609
676
|
updatedAt: writeNow,
|
|
610
677
|
};
|
|
611
678
|
|
|
@@ -626,12 +693,20 @@ async function appendDeckSlides(
|
|
|
626
693
|
}
|
|
627
694
|
|
|
628
695
|
function safeParseDeckData(raw: string): Record<string, unknown> {
|
|
696
|
+
let parsed: unknown;
|
|
629
697
|
try {
|
|
630
|
-
|
|
631
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
698
|
+
parsed = JSON.parse(raw);
|
|
632
699
|
} catch {
|
|
633
|
-
|
|
700
|
+
throw new Error(
|
|
701
|
+
"The target deck contains invalid JSON; refusing to overwrite it.",
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
"The target deck data is invalid; refusing to overwrite it.",
|
|
707
|
+
);
|
|
634
708
|
}
|
|
709
|
+
return parsed as Record<string, unknown>;
|
|
635
710
|
}
|
|
636
711
|
|
|
637
712
|
function stripTags(html: string): string {
|
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
type ParsedImage,
|
|
20
20
|
type ParsedPresentation,
|
|
21
21
|
} from "../server/handlers/import/pptx-parser.js";
|
|
22
|
+
import { buildSourceImportMetadata } from "../server/lib/source-import.js";
|
|
23
|
+
import {
|
|
24
|
+
ASPECT_RATIOS,
|
|
25
|
+
DEFAULT_ASPECT_RATIO,
|
|
26
|
+
type AspectRatio,
|
|
27
|
+
} from "../shared/aspect-ratios.js";
|
|
22
28
|
import { getDeckUrl } from "./_app-url.js";
|
|
23
29
|
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
24
30
|
|
|
@@ -82,6 +88,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
82
88
|
fileBuffer: Buffer;
|
|
83
89
|
title?: string;
|
|
84
90
|
deckId?: string;
|
|
91
|
+
designSystemId?: string | null;
|
|
85
92
|
source?: string;
|
|
86
93
|
imageFallbacks?: ImportedImageFallback[];
|
|
87
94
|
parsedPresentation?: ParsedPresentation;
|
|
@@ -98,6 +105,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
98
105
|
fileBuffer,
|
|
99
106
|
title,
|
|
100
107
|
deckId,
|
|
108
|
+
designSystemId,
|
|
101
109
|
source = "import-pptx",
|
|
102
110
|
imageFallbacks,
|
|
103
111
|
parsedPresentation,
|
|
@@ -133,9 +141,10 @@ export async function importPptxBufferToDeck(args: {
|
|
|
133
141
|
uploadedImages.urls,
|
|
134
142
|
themeFont,
|
|
135
143
|
);
|
|
144
|
+
const id = `slide-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
136
145
|
return {
|
|
137
146
|
slide: {
|
|
138
|
-
id
|
|
147
|
+
id,
|
|
139
148
|
content: html,
|
|
140
149
|
layout: parsedSlide.layoutHint ?? "content",
|
|
141
150
|
notes: parsedSlide.notes,
|
|
@@ -144,6 +153,8 @@ export async function importPptxBufferToDeck(args: {
|
|
|
144
153
|
: {}),
|
|
145
154
|
...(parsedSlide.splitByParagraph ? { splitByParagraph: true } : {}),
|
|
146
155
|
},
|
|
156
|
+
sourceText: parsedSlide.texts.map((text) => text.content).join("\n"),
|
|
157
|
+
imageUrls: Object.values(uploadedImages.urls),
|
|
147
158
|
imageSkippedCount: uploadedImages.imageSkippedCount,
|
|
148
159
|
};
|
|
149
160
|
}),
|
|
@@ -154,6 +165,26 @@ export async function importPptxBufferToDeck(args: {
|
|
|
154
165
|
(total, r) => total + r.imageSkippedCount,
|
|
155
166
|
0,
|
|
156
167
|
);
|
|
168
|
+
if (imagesSkipped > 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Source-faithful PPTX import could not preserve ${imagesSkipped} image(s). No deck was written. Retry with browser-renderable images or use a PDF export for page-faithful preservation.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const sourceImport = buildSourceImportMetadata({
|
|
174
|
+
format: "pptx",
|
|
175
|
+
slides: results.map((result) => ({
|
|
176
|
+
id: result.slide.id,
|
|
177
|
+
text: result.sourceText,
|
|
178
|
+
notes: result.slide.notes ?? "",
|
|
179
|
+
imageUrls: result.imageUrls,
|
|
180
|
+
editableText: true,
|
|
181
|
+
})),
|
|
182
|
+
imagesSkipped,
|
|
183
|
+
});
|
|
184
|
+
const aspectRatio = nearestAspectRatio(
|
|
185
|
+
presentation.slides[0]?.widthEmu,
|
|
186
|
+
presentation.slides[0]?.heightEmu,
|
|
187
|
+
);
|
|
157
188
|
|
|
158
189
|
const db = getDb();
|
|
159
190
|
const now = new Date().toISOString();
|
|
@@ -168,10 +199,25 @@ export async function importPptxBufferToDeck(args: {
|
|
|
168
199
|
throw new Error(`Deck ${deckId} not found`);
|
|
169
200
|
}
|
|
170
201
|
|
|
171
|
-
const
|
|
202
|
+
const previousData = safeParseDeckData(existing[0].data);
|
|
203
|
+
const data = {
|
|
204
|
+
...previousData,
|
|
205
|
+
title: deckTitle,
|
|
206
|
+
slides,
|
|
207
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
208
|
+
sourceImport,
|
|
209
|
+
updatedAt: now,
|
|
210
|
+
};
|
|
172
211
|
await db
|
|
173
212
|
.update(schema.decks)
|
|
174
|
-
.set({
|
|
213
|
+
.set({
|
|
214
|
+
title: deckTitle,
|
|
215
|
+
data: JSON.stringify(data),
|
|
216
|
+
...(designSystemId !== undefined
|
|
217
|
+
? { designSystemId }
|
|
218
|
+
: { designSystemId: existing[0].designSystemId }),
|
|
219
|
+
updatedAt: now,
|
|
220
|
+
})
|
|
175
221
|
.where(eq(schema.decks.id, deckId));
|
|
176
222
|
|
|
177
223
|
notifyClients(deckId);
|
|
@@ -193,13 +239,21 @@ export async function importPptxBufferToDeck(args: {
|
|
|
193
239
|
|
|
194
240
|
// Create new deck
|
|
195
241
|
const id = `deck-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
196
|
-
const data = {
|
|
242
|
+
const data = {
|
|
243
|
+
title: deckTitle,
|
|
244
|
+
slides,
|
|
245
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
246
|
+
sourceImport,
|
|
247
|
+
createdAt: now,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
};
|
|
197
250
|
await db.insert(schema.decks).values({
|
|
198
251
|
id,
|
|
199
252
|
title: deckTitle,
|
|
200
253
|
data: JSON.stringify(data),
|
|
201
254
|
ownerEmail,
|
|
202
255
|
orgId: getRequestOrgId(),
|
|
256
|
+
designSystemId: designSystemId ?? undefined,
|
|
203
257
|
createdAt: now,
|
|
204
258
|
updatedAt: now,
|
|
205
259
|
});
|
|
@@ -222,8 +276,8 @@ export default defineAction({
|
|
|
222
276
|
description:
|
|
223
277
|
"Import a PPTX file and create a slide deck from it. " +
|
|
224
278
|
"Parses the PowerPoint file, extracts text and layout information, " +
|
|
225
|
-
"converts each slide to the app's HTML format, and creates or updates a deck. " +
|
|
226
|
-
"Returns the deck ID and slide count.",
|
|
279
|
+
"converts each slide to the app's positioned HTML format, records source-preservation metadata, and creates or updates a deck. " +
|
|
280
|
+
"If an embedded image cannot be preserved, the action fails before writing a partial deck. Returns the deck ID and slide count.",
|
|
227
281
|
schema: z.object({
|
|
228
282
|
filePath: z
|
|
229
283
|
.string()
|
|
@@ -234,6 +288,11 @@ export default defineAction({
|
|
|
234
288
|
.describe(
|
|
235
289
|
"If provided, import slides into this existing deck (replaces all slides)",
|
|
236
290
|
),
|
|
291
|
+
designSystemId: z
|
|
292
|
+
.string()
|
|
293
|
+
.nullable()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe("Optional design system to link when creating a new deck"),
|
|
237
296
|
title: z
|
|
238
297
|
.string()
|
|
239
298
|
.optional()
|
|
@@ -241,8 +300,49 @@ export default defineAction({
|
|
|
241
300
|
"Deck title — defaults to the title extracted from the presentation",
|
|
242
301
|
),
|
|
243
302
|
}),
|
|
244
|
-
run: async ({ filePath, deckId, title }) => {
|
|
303
|
+
run: async ({ filePath, deckId, title, designSystemId }) => {
|
|
245
304
|
const { data: fileBuffer } = await readUserUploadedFile(filePath);
|
|
246
|
-
return importPptxBufferToDeck({
|
|
305
|
+
return importPptxBufferToDeck({
|
|
306
|
+
fileBuffer,
|
|
307
|
+
deckId,
|
|
308
|
+
title,
|
|
309
|
+
designSystemId,
|
|
310
|
+
});
|
|
247
311
|
},
|
|
248
312
|
});
|
|
313
|
+
|
|
314
|
+
function nearestAspectRatio(
|
|
315
|
+
width: number | undefined,
|
|
316
|
+
height: number | undefined,
|
|
317
|
+
): AspectRatio | undefined {
|
|
318
|
+
if (!width || !height || width <= 0 || height <= 0) return undefined;
|
|
319
|
+
const target = width / height;
|
|
320
|
+
let best: AspectRatio = DEFAULT_ASPECT_RATIO;
|
|
321
|
+
let bestDiff = Infinity;
|
|
322
|
+
for (const key of Object.keys(ASPECT_RATIOS) as AspectRatio[]) {
|
|
323
|
+
const preset = ASPECT_RATIOS[key];
|
|
324
|
+
const diff = Math.abs(preset.width / preset.height - target);
|
|
325
|
+
if (diff < bestDiff) {
|
|
326
|
+
best = key;
|
|
327
|
+
bestDiff = diff;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return best;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function safeParseDeckData(raw: string): Record<string, unknown> {
|
|
334
|
+
let parsed: unknown;
|
|
335
|
+
try {
|
|
336
|
+
parsed = JSON.parse(raw);
|
|
337
|
+
} catch {
|
|
338
|
+
throw new Error(
|
|
339
|
+
"The target deck contains invalid JSON; refusing to overwrite it.",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"The target deck data is invalid; refusing to overwrite it.",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return parsed as Record<string, unknown>;
|
|
348
|
+
}
|
|
@@ -26,6 +26,11 @@ import { z } from "zod";
|
|
|
26
26
|
import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
|
|
27
27
|
import { getDb, schema } from "../server/db/index.js";
|
|
28
28
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
29
|
+
import {
|
|
30
|
+
assertSourceSlidePreserved,
|
|
31
|
+
sourceImportForDeck,
|
|
32
|
+
type SourceImportMetadata,
|
|
33
|
+
} from "../server/lib/source-import.js";
|
|
29
34
|
import { ASPECT_RATIO_VALUES } from "../shared/aspect-ratios.js";
|
|
30
35
|
|
|
31
36
|
// ---------------------------------------------------------------------------
|
|
@@ -79,6 +84,13 @@ const PatchSlideOp = z.object({
|
|
|
79
84
|
op: z.literal("patch-slide"),
|
|
80
85
|
slideId: z.string(),
|
|
81
86
|
fields: SlideFieldsSchema,
|
|
87
|
+
preserveSource: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.default(true)
|
|
91
|
+
.describe(
|
|
92
|
+
"Keep source-imported images and factual copy (default true). Set false only for an explicit rewrite.",
|
|
93
|
+
),
|
|
82
94
|
});
|
|
83
95
|
|
|
84
96
|
/** Delete a single slide by ID */
|
|
@@ -141,6 +153,24 @@ export const OperationSchema = z.discriminatedUnion("op", [
|
|
|
141
153
|
|
|
142
154
|
export type Operation = z.infer<typeof OperationSchema>;
|
|
143
155
|
|
|
156
|
+
export function assertSourceImportOperationsPreserved(
|
|
157
|
+
metadata: SourceImportMetadata | null,
|
|
158
|
+
operations: Operation[],
|
|
159
|
+
): void {
|
|
160
|
+
if (!metadata) return;
|
|
161
|
+
const structuralOperation = operations.find(
|
|
162
|
+
(operation) =>
|
|
163
|
+
operation.op === "delete-slide" ||
|
|
164
|
+
operation.op === "reorder-slides" ||
|
|
165
|
+
operation.op === "add-slide",
|
|
166
|
+
);
|
|
167
|
+
if (!structuralOperation) return;
|
|
168
|
+
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Cannot ${structuralOperation.op} on a source-imported deck while source preservation is enabled. Preserve the imported slide structure, or use an explicit source rewrite workflow.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
// The browser uses the full operation union above. Agents additionally use
|
|
145
175
|
// this action for one bounded, deck-wide layout repair: one patch-slide per
|
|
146
176
|
// slide in a single SQL transaction, followed by a fresh read for verification.
|
|
@@ -404,6 +434,27 @@ export default defineAction({
|
|
|
404
434
|
);
|
|
405
435
|
}
|
|
406
436
|
|
|
437
|
+
const sourceImport = sourceImportForDeck(deck.sourceImport);
|
|
438
|
+
assertSourceImportOperationsPreserved(sourceImport, operations);
|
|
439
|
+
for (const op of operations) {
|
|
440
|
+
if (
|
|
441
|
+
op.op !== "patch-slide" ||
|
|
442
|
+
(op.fields.content === undefined && op.fields.notes === undefined)
|
|
443
|
+
) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
assertSourceSlidePreserved({
|
|
447
|
+
metadata: sourceImport,
|
|
448
|
+
slideId: op.slideId,
|
|
449
|
+
nextContent:
|
|
450
|
+
op.fields.content === undefined
|
|
451
|
+
? undefined
|
|
452
|
+
: normalizeSlidePadding(op.fields.content),
|
|
453
|
+
nextNotes: op.fields.notes,
|
|
454
|
+
preserveSource: op.preserveSource,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
407
458
|
for (const op of operations) {
|
|
408
459
|
applyOperation(deck, op);
|
|
409
460
|
}
|
|
@@ -16,6 +16,10 @@ import { normalizeSlidePadding } from "../app/lib/normalize-slide-padding.js";
|
|
|
16
16
|
import { getDb, schema } from "../server/db/index.js"; // ensure registerShareableResource runs
|
|
17
17
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
18
18
|
import { createDeckVersionSnapshot } from "../server/lib/deck-versions.js";
|
|
19
|
+
import {
|
|
20
|
+
assertSourceSlidePreserved,
|
|
21
|
+
sourceImportForDeck,
|
|
22
|
+
} from "../server/lib/source-import.js";
|
|
19
23
|
import { hashSlideContent } from "../shared/slide-fit.js";
|
|
20
24
|
import { slideLabelFor, touchAgentSlidePresence } from "./_agent-presence.js";
|
|
21
25
|
import {
|
|
@@ -89,7 +93,7 @@ function storedCreativeContext(value: unknown): {
|
|
|
89
93
|
export default defineAction({
|
|
90
94
|
description:
|
|
91
95
|
"Surgically edit a slide's content using search-replace or full replacement. " +
|
|
92
|
-
"Syncs live to open editors. Prefer this over full deck rewrites.",
|
|
96
|
+
"Syncs live to open editors. Prefer this over full deck rewrites. Source-imported slides preserve their original images and factual copy by default.",
|
|
93
97
|
schema: z.object({
|
|
94
98
|
deckId: z.string().describe("Deck ID"),
|
|
95
99
|
slideId: z.string().describe("Slide ID"),
|
|
@@ -105,6 +109,13 @@ export default defineAction({
|
|
|
105
109
|
.string()
|
|
106
110
|
.optional()
|
|
107
111
|
.describe("Full HTML to replace entire slide content"),
|
|
112
|
+
preserveSource: z
|
|
113
|
+
.boolean()
|
|
114
|
+
.optional()
|
|
115
|
+
.default(true)
|
|
116
|
+
.describe(
|
|
117
|
+
"Keep source-imported images and factual copy (default true). Set false only when the user explicitly asks to rewrite the source slide.",
|
|
118
|
+
),
|
|
108
119
|
contextPackId: z
|
|
109
120
|
.string()
|
|
110
121
|
.optional()
|
|
@@ -131,6 +142,7 @@ export default defineAction({
|
|
|
131
142
|
find,
|
|
132
143
|
replace,
|
|
133
144
|
fullContent,
|
|
145
|
+
preserveSource,
|
|
134
146
|
contextPackId,
|
|
135
147
|
contextModeOverride,
|
|
136
148
|
reuseLabels,
|
|
@@ -205,17 +217,31 @@ export default defineAction({
|
|
|
205
217
|
let notFound = false;
|
|
206
218
|
|
|
207
219
|
if (fullContent) {
|
|
208
|
-
|
|
220
|
+
const nextContent = normalizeSlidePadding(fullContent);
|
|
221
|
+
assertSourceSlidePreserved({
|
|
222
|
+
metadata: sourceImportForDeck(deck.sourceImport),
|
|
223
|
+
slideId,
|
|
224
|
+
nextContent,
|
|
225
|
+
preserveSource,
|
|
226
|
+
});
|
|
227
|
+
slide.content = nextContent;
|
|
209
228
|
applied = true;
|
|
210
229
|
} else if (find) {
|
|
211
230
|
const idx = (slide.content as string).indexOf(find);
|
|
212
231
|
if (idx === -1) {
|
|
213
232
|
notFound = true;
|
|
214
233
|
} else {
|
|
215
|
-
|
|
234
|
+
const nextContent =
|
|
216
235
|
slide.content.slice(0, idx) +
|
|
217
236
|
(replace ?? "") +
|
|
218
237
|
slide.content.slice(idx + find.length);
|
|
238
|
+
assertSourceSlidePreserved({
|
|
239
|
+
metadata: sourceImportForDeck(deck.sourceImport),
|
|
240
|
+
slideId,
|
|
241
|
+
nextContent,
|
|
242
|
+
preserveSource,
|
|
243
|
+
});
|
|
244
|
+
slide.content = nextContent;
|
|
219
245
|
applied = true;
|
|
220
246
|
}
|
|
221
247
|
}
|
|
@@ -157,6 +157,17 @@ export function FirstDeckOnboardingFlow({
|
|
|
157
157
|
),
|
|
158
158
|
});
|
|
159
159
|
},
|
|
160
|
+
onSetupFailure: (failedPrompt, _failedFiles, failure) => {
|
|
161
|
+
setPromptInitialText(failedPrompt);
|
|
162
|
+
setPromptInitialTextKey(Date.now());
|
|
163
|
+
setStep("prompt");
|
|
164
|
+
toast.error(t("home.generationStartFailed"), {
|
|
165
|
+
description:
|
|
166
|
+
failure instanceof Error
|
|
167
|
+
? failure.message
|
|
168
|
+
: t("home.generationStartFailedDescription"),
|
|
169
|
+
});
|
|
170
|
+
},
|
|
160
171
|
});
|
|
161
172
|
if (result === "started") onComplete();
|
|
162
173
|
},
|