@agent-native/core 0.157.8 → 0.157.10
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/purge-first-party-analytics-postgres.ts +26 -8
- package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +261 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +75 -31
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
- package/corpus/templates/analytics/server/plugins/db.ts +139 -0
- package/corpus/templates/slides/AGENTS.md +14 -10
- package/corpus/templates/slides/actions/export-pptx.ts +1311 -121
- package/corpus/templates/slides/actions/import-file.ts +28 -1
- package/corpus/templates/slides/actions/import-pptx.ts +69 -42
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
- package/corpus/templates/slides/app/global.css +19 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +601 -30
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +1240 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +329 -64
- package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
- package/corpus/templates/slides/server/lib/source-import.ts +10 -1
- package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
- package/dist/agent/harness/migrations.d.ts +4 -0
- package/dist/agent/harness/migrations.js +60 -0
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.js +15 -0
- package/dist/agent/run-loop-with-resume.d.ts +1 -0
- package/dist/agent/run-loop-with-resume.js +44 -12
- package/dist/agent/run-migrations.d.ts +8 -0
- package/dist/agent/run-migrations.js +61 -0
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +35 -0
- package/dist/chat-threads/migrations.d.ts +1 -0
- package/dist/chat-threads/migrations.js +1 -0
- package/dist/chat-threads/schema-migrations.d.ts +8 -0
- package/dist/chat-threads/schema-migrations.js +60 -0
- package/dist/cli/code-agent-connector.d.ts +4 -1
- package/dist/cli/code-agent-connector.js +154 -23
- package/dist/cli/code-agent-executor.js +255 -13
- package/dist/cli/code-agent-runs.d.ts +1 -0
- package/dist/cli/code-agent-runs.js +2 -1
- package/dist/cli/portal-workspace.d.ts +49 -0
- package/dist/cli/portal-workspace.js +320 -0
- package/dist/client/AssistantChat.d.ts +1 -1
- package/dist/client/AssistantChat.js +43 -61
- package/dist/client/ConnectBuilderCard.js +33 -5
- package/dist/client/agent-chat-adapter.js +4 -0
- package/dist/client/analytics-platform.d.ts +2 -0
- package/dist/client/analytics-platform.js +12 -0
- package/dist/client/analytics.d.ts +3 -0
- package/dist/client/analytics.js +10 -1
- package/dist/client/chat/index.d.ts +1 -0
- package/dist/client/chat/index.js +1 -0
- package/dist/client/chat/message-components.d.ts +2 -0
- package/dist/client/chat/message-components.js +11 -1
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +2 -2
- 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/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +38 -36
- package/dist/client/chat-first/types.d.ts +5 -0
- package/dist/client/chat-first.d.ts +1 -1
- package/dist/client/chat-first.js +4 -4
- package/dist/client/desktop-local-code-change.d.ts +10 -0
- package/dist/client/desktop-local-code-change.js +16 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/observability/ThumbsFeedback.js +53 -26
- package/dist/client/observability/useObservability.d.ts +1 -1
- package/dist/client/observability/useObservability.js +6 -3
- package/dist/client/terminal/AgentTerminal.d.ts +9 -1
- package/dist/client/terminal/AgentTerminal.js +38 -3
- package/dist/client/terminal/index.d.ts +1 -1
- package/dist/client/terminal/index.js +1 -1
- package/dist/client/track.js +9 -1
- package/dist/client/use-action.js +3 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/deploy/build.js +1 -1
- package/dist/feature-flags/a2a-action-route.js +4 -2
- package/dist/feature-flags/permissions.d.ts +2 -0
- package/dist/feature-flags/permissions.js +6 -1
- package/dist/ingestion/pptx.d.ts +92 -1
- package/dist/ingestion/pptx.js +1180 -115
- package/dist/integrations/index.d.ts +3 -3
- package/dist/integrations/index.js +2 -2
- package/dist/integrations/plugin.js +95 -6
- package/dist/integrations/remote-commands-store.d.ts +5 -0
- package/dist/integrations/remote-commands-store.js +63 -22
- package/dist/integrations/remote-devices-store.d.ts +2 -1
- package/dist/integrations/remote-devices-store.js +46 -0
- package/dist/integrations/remote-types.d.ts +20 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
- package/dist/jobs/actions/list-recurring-jobs.js +5 -1
- package/dist/jobs/frontmatter.d.ts +16 -0
- package/dist/jobs/frontmatter.js +53 -0
- package/dist/jobs/remote-execution.d.ts +42 -0
- package/dist/jobs/remote-execution.js +287 -0
- package/dist/jobs/scheduler.js +102 -3
- package/dist/jobs/tools.js +50 -3
- package/dist/localization/core-messages/ar-SA.js +4 -0
- package/dist/localization/core-messages/de-DE.js +4 -0
- package/dist/localization/core-messages/en-US.d.ts +4 -0
- package/dist/localization/core-messages/en-US.js +4 -0
- package/dist/localization/core-messages/es-ES.js +4 -0
- package/dist/localization/core-messages/fr-FR.js +4 -0
- package/dist/localization/core-messages/hi-IN.js +4 -0
- package/dist/localization/core-messages/ja-JP.js +4 -0
- package/dist/localization/core-messages/ko-KR.js +4 -0
- package/dist/localization/core-messages/pt-BR.js +4 -0
- package/dist/localization/core-messages/zh-CN.js +4 -0
- package/dist/localization/core-messages/zh-TW.js +4 -0
- package/dist/localization/core-messages.d.ts +4 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/feedback.js +40 -16
- package/dist/observability/routes.d.ts +6 -6
- package/dist/observability/routes.js +15 -3
- package/dist/observability/store.js +2 -0
- package/dist/observability/traces.js +5 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +7 -5
- package/dist/server/action-routes.js +13 -6
- package/dist/server/agent-chat-plugin.js +6 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-run-context.d.ts +1 -0
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/better-auth-instance.js +4 -0
- package/dist/server/core-routes-plugin.js +8 -1
- package/dist/server/realtime-voice.js +2 -1
- package/dist/server/release-migrations.js +16 -0
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/analytics-platform.d.ts +6 -0
- package/dist/shared/analytics-platform.js +14 -0
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/styles/agent-native.css +83 -103
- package/dist/terminal/cli-registry.js +5 -0
- package/dist/tracking/registry.js +9 -1
- package/dist/triggers/actions/list-automations.d.ts +3 -0
- package/dist/triggers/actions/list-automations.js +3 -0
- package/dist/triggers/actions/manage-automation.d.ts +9 -0
- package/dist/triggers/actions/manage-automation.js +14 -2
- package/dist/triggers/actions.js +82 -7
- package/dist/usage/migrations.d.ts +4 -0
- package/dist/usage/migrations.js +45 -0
- package/docs/content/template-content-developers.mdx +48 -0
- package/docs/content/toolkit-ui.mdx +57 -0
- package/package.json +3 -3
|
@@ -14,7 +14,10 @@ import { z } from "zod";
|
|
|
14
14
|
|
|
15
15
|
import { getDb, schema } from "../server/db/index.js";
|
|
16
16
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assertPptxImagesRenderable,
|
|
19
|
+
uploadPptxSlideImages,
|
|
20
|
+
} from "../server/handlers/import/pptx-assets.js";
|
|
18
21
|
import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
|
|
19
22
|
import { setupPdfParse } from "../server/lib/pdf-parse-setup.js";
|
|
20
23
|
import {
|
|
@@ -148,6 +151,7 @@ export default defineAction({
|
|
|
148
151
|
await assertAccess("deck", deckId, "editor");
|
|
149
152
|
const pptxOwnerEmail = getRequestUserEmail();
|
|
150
153
|
if (!pptxOwnerEmail) throw new Error("no authenticated user");
|
|
154
|
+
assertPptxImagesRenderable(presentation.slides);
|
|
151
155
|
const pptxThemeFont = presentation.theme?.fonts?.[0];
|
|
152
156
|
const uploadLimit = pLimit(4);
|
|
153
157
|
const pptxResults = await Promise.all(
|
|
@@ -162,6 +166,10 @@ export default defineAction({
|
|
|
162
166
|
(total, r) => total + r.imageSkippedCount,
|
|
163
167
|
0,
|
|
164
168
|
);
|
|
169
|
+
const tablesDegraded = presentation.slides.reduce(
|
|
170
|
+
(total, s) => total + (s.tablesDegraded ?? 0),
|
|
171
|
+
0,
|
|
172
|
+
);
|
|
165
173
|
const sourceImport = buildSourceImportMetadata({
|
|
166
174
|
format: "pptx",
|
|
167
175
|
slides: pptxResults.map((result) => ({
|
|
@@ -172,6 +180,7 @@ export default defineAction({
|
|
|
172
180
|
editableText: true,
|
|
173
181
|
})),
|
|
174
182
|
imagesSkipped,
|
|
183
|
+
tablesDegraded,
|
|
175
184
|
});
|
|
176
185
|
if (imagesSkipped > 0) {
|
|
177
186
|
throw new Error(
|
|
@@ -193,6 +202,7 @@ export default defineAction({
|
|
|
193
202
|
aspectRatio,
|
|
194
203
|
sourceImport,
|
|
195
204
|
pptxResults[0]?.sourceText,
|
|
205
|
+
presentation.theme,
|
|
196
206
|
);
|
|
197
207
|
return {
|
|
198
208
|
format: "pptx",
|
|
@@ -202,6 +212,7 @@ export default defineAction({
|
|
|
202
212
|
deckId,
|
|
203
213
|
imported: true,
|
|
204
214
|
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
215
|
+
...(tablesDegraded > 0 ? { tablesDegraded } : {}),
|
|
205
216
|
};
|
|
206
217
|
}
|
|
207
218
|
|
|
@@ -527,9 +538,14 @@ async function importPdfPagesWithFidelity(args: {
|
|
|
527
538
|
),
|
|
528
539
|
);
|
|
529
540
|
const slides = imported.map((entry) => entry.slide);
|
|
541
|
+
const imagesSkipped = fidelityPages.reduce(
|
|
542
|
+
(total, page) => total + page.imagesSkipped,
|
|
543
|
+
0,
|
|
544
|
+
);
|
|
530
545
|
const sourceImport = buildSourceImportMetadata({
|
|
531
546
|
format: "pdf",
|
|
532
547
|
slides: imported.map((entry) => entry.snapshot),
|
|
548
|
+
imagesSkipped,
|
|
533
549
|
});
|
|
534
550
|
|
|
535
551
|
const importedTitle = await appendDeckSlides(
|
|
@@ -550,6 +566,7 @@ async function importPdfPagesWithFidelity(args: {
|
|
|
550
566
|
aspectRatio,
|
|
551
567
|
deckId,
|
|
552
568
|
imported: true,
|
|
569
|
+
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
553
570
|
};
|
|
554
571
|
}
|
|
555
572
|
|
|
@@ -688,6 +705,7 @@ async function appendDeckSlides(
|
|
|
688
705
|
aspectRatio?: AspectRatio,
|
|
689
706
|
sourceImport?: ReturnType<typeof buildSourceImportMetadata>,
|
|
690
707
|
titleSource?: unknown,
|
|
708
|
+
theme?: import("../server/handlers/import/pptx-parser.js").ParsedPresentation["theme"],
|
|
691
709
|
): Promise<string> {
|
|
692
710
|
await assertAccess("deck", deckId, "editor");
|
|
693
711
|
|
|
@@ -736,6 +754,15 @@ async function appendDeckSlides(
|
|
|
736
754
|
title: nextTitle,
|
|
737
755
|
slides: [...previousSlides, ...slides],
|
|
738
756
|
...(!hadExistingSlides && aspectRatio ? { aspectRatio } : {}),
|
|
757
|
+
// Same rule as aspectRatio above: an appended file's palette only
|
|
758
|
+
// becomes the deck's theme when the deck had no slides yet, so
|
|
759
|
+
// appending onto an existing deck can't silently restyle every slide
|
|
760
|
+
// already on it. Deliberately keyed on slides rather than on whether a
|
|
761
|
+
// theme already exists: export writes one deck-level theme (OOXML allows
|
|
762
|
+
// one per master), so with zero slides there is nothing to protect, and
|
|
763
|
+
// refusing a new theme there would strand a deck whose slides were
|
|
764
|
+
// deleted with the old file's palette, unfixable by re-importing.
|
|
765
|
+
...(!hadExistingSlides && theme ? { theme } : {}),
|
|
739
766
|
...(nextSourceImport ? { sourceImport: nextSourceImport } : {}),
|
|
740
767
|
updatedAt: writeNow,
|
|
741
768
|
};
|
|
@@ -12,7 +12,10 @@ import { z } from "zod";
|
|
|
12
12
|
import { getDb, schema } from "../server/db/index.js";
|
|
13
13
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
14
14
|
import { convertToSlideHtml } from "../server/handlers/import/html-converter.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
assertPptxImagesRenderable,
|
|
17
|
+
uploadPptxSlideImages,
|
|
18
|
+
} from "../server/handlers/import/pptx-assets.js";
|
|
16
19
|
import {
|
|
17
20
|
parsePptx,
|
|
18
21
|
type ParsedElement,
|
|
@@ -27,6 +30,7 @@ import {
|
|
|
27
30
|
} from "../shared/aspect-ratios.js";
|
|
28
31
|
import { getDeckUrl } from "./_app-url.js";
|
|
29
32
|
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
33
|
+
import { withDeckLock } from "./patch-deck.js";
|
|
30
34
|
|
|
31
35
|
export interface ImportedImageFallback {
|
|
32
36
|
slideIndex: number;
|
|
@@ -100,6 +104,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
100
104
|
imported: true;
|
|
101
105
|
url: string;
|
|
102
106
|
imagesSkipped?: number;
|
|
107
|
+
tablesDegraded?: number;
|
|
103
108
|
}> {
|
|
104
109
|
const {
|
|
105
110
|
fileBuffer,
|
|
@@ -120,9 +125,18 @@ export async function importPptxBufferToDeck(args: {
|
|
|
120
125
|
// Check edit access before uploading any embedded images — uploads are
|
|
121
126
|
// a side effect with real storage cost, so an unauthorized caller must
|
|
122
127
|
// be rejected before that side effect happens, not after.
|
|
128
|
+
const db = getDb();
|
|
123
129
|
if (deckId) {
|
|
124
130
|
await assertAccess("deck", deckId, "editor");
|
|
131
|
+
const [deck] = await db
|
|
132
|
+
.select()
|
|
133
|
+
.from(schema.decks)
|
|
134
|
+
.where(eq(schema.decks.id, deckId));
|
|
135
|
+
if (!deck) {
|
|
136
|
+
throw new Error(`Deck ${deckId} not found`);
|
|
137
|
+
}
|
|
125
138
|
}
|
|
139
|
+
assertPptxImagesRenderable(presentation.slides);
|
|
126
140
|
|
|
127
141
|
// Convert each parsed slide to its positioned scene graph, uploading every
|
|
128
142
|
// browser-renderable image so the imported deck keeps the source layering
|
|
@@ -156,6 +170,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
156
170
|
sourceText: parsedSlide.texts.map((text) => text.content).join("\n"),
|
|
157
171
|
imageUrls: Object.values(uploadedImages.urls),
|
|
158
172
|
imageSkippedCount: uploadedImages.imageSkippedCount,
|
|
173
|
+
tablesDegraded: parsedSlide.tablesDegraded ?? 0,
|
|
159
174
|
};
|
|
160
175
|
}),
|
|
161
176
|
),
|
|
@@ -165,6 +180,10 @@ export async function importPptxBufferToDeck(args: {
|
|
|
165
180
|
(total, r) => total + r.imageSkippedCount,
|
|
166
181
|
0,
|
|
167
182
|
);
|
|
183
|
+
const tablesDegraded = results.reduce(
|
|
184
|
+
(total, r) => total + r.tablesDegraded,
|
|
185
|
+
0,
|
|
186
|
+
);
|
|
168
187
|
if (imagesSkipped > 0) {
|
|
169
188
|
throw new Error(
|
|
170
189
|
`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.`,
|
|
@@ -180,61 +199,67 @@ export async function importPptxBufferToDeck(args: {
|
|
|
180
199
|
editableText: true,
|
|
181
200
|
})),
|
|
182
201
|
imagesSkipped,
|
|
202
|
+
tablesDegraded,
|
|
183
203
|
});
|
|
184
204
|
const aspectRatio = nearestAspectRatio(
|
|
185
205
|
presentation.slides[0]?.widthEmu,
|
|
186
206
|
presentation.slides[0]?.heightEmu,
|
|
187
207
|
);
|
|
188
208
|
|
|
189
|
-
const db = getDb();
|
|
190
209
|
const now = new Date().toISOString();
|
|
191
210
|
|
|
192
211
|
if (deckId) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
return withDeckLock(deckId, async () => {
|
|
213
|
+
// Image uploads happen before this lock because they are independent of
|
|
214
|
+
// the deck row. Re-read inside the lock so the replacement is based on
|
|
215
|
+
// the latest deck data rather than the preflight snapshot.
|
|
216
|
+
const [latestDeck] = await db
|
|
217
|
+
.select()
|
|
218
|
+
.from(schema.decks)
|
|
219
|
+
.where(eq(schema.decks.id, deckId));
|
|
220
|
+
if (!latestDeck) {
|
|
221
|
+
throw new Error(`Deck ${deckId} not found`);
|
|
222
|
+
}
|
|
201
223
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
title: deckTitle,
|
|
206
|
-
slides,
|
|
207
|
-
...(aspectRatio ? { aspectRatio } : {}),
|
|
208
|
-
sourceImport,
|
|
209
|
-
updatedAt: now,
|
|
210
|
-
};
|
|
211
|
-
await db
|
|
212
|
-
.update(schema.decks)
|
|
213
|
-
.set({
|
|
224
|
+
const previousData = safeParseDeckData(latestDeck.data);
|
|
225
|
+
const data = {
|
|
226
|
+
...previousData,
|
|
214
227
|
title: deckTitle,
|
|
215
|
-
|
|
216
|
-
...(
|
|
217
|
-
|
|
218
|
-
|
|
228
|
+
slides,
|
|
229
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
230
|
+
...(presentation.theme ? { theme: presentation.theme } : {}),
|
|
231
|
+
sourceImport,
|
|
219
232
|
updatedAt: now,
|
|
220
|
-
}
|
|
221
|
-
|
|
233
|
+
};
|
|
234
|
+
await db
|
|
235
|
+
.update(schema.decks)
|
|
236
|
+
.set({
|
|
237
|
+
title: deckTitle,
|
|
238
|
+
data: JSON.stringify(data),
|
|
239
|
+
...(designSystemId !== undefined
|
|
240
|
+
? { designSystemId }
|
|
241
|
+
: { designSystemId: latestDeck.designSystemId }),
|
|
242
|
+
updatedAt: now,
|
|
243
|
+
})
|
|
244
|
+
.where(eq(schema.decks.id, deckId));
|
|
222
245
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
notifyClients(deckId);
|
|
247
|
+
await writeAppState("refresh-signal", {
|
|
248
|
+
ts: now,
|
|
249
|
+
source,
|
|
250
|
+
});
|
|
228
251
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
return {
|
|
253
|
+
id: deckId,
|
|
254
|
+
title: deckTitle,
|
|
255
|
+
slideCount: slides.length,
|
|
256
|
+
theme: presentation.theme,
|
|
257
|
+
imported: true,
|
|
258
|
+
url: getDeckUrl(deckId),
|
|
259
|
+
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
260
|
+
...(tablesDegraded > 0 ? { tablesDegraded } : {}),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
238
263
|
}
|
|
239
264
|
|
|
240
265
|
// Create new deck
|
|
@@ -243,6 +268,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
243
268
|
title: deckTitle,
|
|
244
269
|
slides,
|
|
245
270
|
...(aspectRatio ? { aspectRatio } : {}),
|
|
271
|
+
...(presentation.theme ? { theme: presentation.theme } : {}),
|
|
246
272
|
sourceImport,
|
|
247
273
|
createdAt: now,
|
|
248
274
|
updatedAt: now,
|
|
@@ -269,6 +295,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
269
295
|
imported: true,
|
|
270
296
|
url: getDeckUrl(id),
|
|
271
297
|
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
298
|
+
...(tablesDegraded > 0 ? { tablesDegraded } : {}),
|
|
272
299
|
};
|
|
273
300
|
}
|
|
274
301
|
|
|
@@ -528,6 +528,11 @@ function useSlideAutofit(
|
|
|
528
528
|
|
|
529
529
|
root.addEventListener("load", scheduleMeasure, true);
|
|
530
530
|
document.fonts?.ready.then(scheduleMeasure).catch(() => {});
|
|
531
|
+
// An imported deck's webfonts are requested only once its HTML is on
|
|
532
|
+
// screen, so they can land after `fonts.ready` has already settled. That
|
|
533
|
+
// reflows text without touching the DOM the observers watch, leaving the
|
|
534
|
+
// fit transform measured against the fallback font.
|
|
535
|
+
document.fonts?.addEventListener("loadingdone", scheduleMeasure);
|
|
531
536
|
|
|
532
537
|
// `rootMargin` measures a thumbnail just before it scrolls in, so the fit
|
|
533
538
|
// transform is already applied by the time it is on screen.
|
|
@@ -554,6 +559,7 @@ function useSlideAutofit(
|
|
|
554
559
|
mutationObserver.disconnect();
|
|
555
560
|
visibilityObserver?.disconnect();
|
|
556
561
|
root.removeEventListener("load", scheduleMeasure, true);
|
|
562
|
+
document.fonts?.removeEventListener("loadingdone", scheduleMeasure);
|
|
557
563
|
};
|
|
558
564
|
}, [canvasWidth, canvasHeight, fitKey, ref]);
|
|
559
565
|
}
|
|
@@ -596,6 +602,186 @@ function AutoFitContent({
|
|
|
596
602
|
);
|
|
597
603
|
}
|
|
598
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Google Fonts families an imported deck may name. Split by what the `css2`
|
|
607
|
+
* endpoint accepts for each: the first list has a real 100..900 variable
|
|
608
|
+
* weight axis, the rest only serve discrete weights. Asking for an axis a
|
|
609
|
+
* family does not have is a 400 for the whole request, so both lists were
|
|
610
|
+
* checked against the live endpoint rather than guessed from the name.
|
|
611
|
+
*/
|
|
612
|
+
const VARIABLE_AXIS_GOOGLE_FONTS = [
|
|
613
|
+
"Archivo",
|
|
614
|
+
"Asap",
|
|
615
|
+
"Catamaran",
|
|
616
|
+
"Chivo",
|
|
617
|
+
"DM Sans",
|
|
618
|
+
"Epilogue",
|
|
619
|
+
"Exo 2",
|
|
620
|
+
"Geist",
|
|
621
|
+
"Heebo",
|
|
622
|
+
"Inter",
|
|
623
|
+
"Jost",
|
|
624
|
+
"League Spartan",
|
|
625
|
+
"Lexend",
|
|
626
|
+
"Libre Franklin",
|
|
627
|
+
"Montserrat",
|
|
628
|
+
"Noto Sans",
|
|
629
|
+
"Noto Serif",
|
|
630
|
+
"Onest",
|
|
631
|
+
"Outfit",
|
|
632
|
+
"Overpass",
|
|
633
|
+
"Public Sans",
|
|
634
|
+
"Raleway",
|
|
635
|
+
"Roboto",
|
|
636
|
+
"Roboto Condensed",
|
|
637
|
+
"Roboto Slab",
|
|
638
|
+
"Urbanist",
|
|
639
|
+
"Work Sans",
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
const STATIC_WEIGHT_GOOGLE_FONTS = [
|
|
643
|
+
"Abril Fatface",
|
|
644
|
+
"Anton",
|
|
645
|
+
"Arimo",
|
|
646
|
+
"Assistant",
|
|
647
|
+
"Barlow",
|
|
648
|
+
"Barlow Condensed",
|
|
649
|
+
"Bebas Neue",
|
|
650
|
+
"Bodoni Moda",
|
|
651
|
+
"Bricolage Grotesque",
|
|
652
|
+
"Cabin",
|
|
653
|
+
"Caveat",
|
|
654
|
+
"Cormorant Garamond",
|
|
655
|
+
"Cousine",
|
|
656
|
+
"Crimson Text",
|
|
657
|
+
"Dancing Script",
|
|
658
|
+
"David Libre",
|
|
659
|
+
"EB Garamond",
|
|
660
|
+
"Figtree",
|
|
661
|
+
"Fira Sans",
|
|
662
|
+
"Hind",
|
|
663
|
+
"Homemade Apple",
|
|
664
|
+
"IBM Plex Sans",
|
|
665
|
+
"Inconsolata",
|
|
666
|
+
"Instrument Sans",
|
|
667
|
+
"Josefin Sans",
|
|
668
|
+
"Kanit",
|
|
669
|
+
"Karla",
|
|
670
|
+
"Lato",
|
|
671
|
+
"Libre Baskerville",
|
|
672
|
+
"Lora",
|
|
673
|
+
"Manrope",
|
|
674
|
+
"Merriweather",
|
|
675
|
+
"Mulish",
|
|
676
|
+
"Nova Square",
|
|
677
|
+
"Nunito",
|
|
678
|
+
"Nunito Sans",
|
|
679
|
+
"Open Sans",
|
|
680
|
+
"Oswald",
|
|
681
|
+
"Oxygen",
|
|
682
|
+
"PT Sans",
|
|
683
|
+
"PT Serif",
|
|
684
|
+
"Pacifico",
|
|
685
|
+
"Playfair Display",
|
|
686
|
+
"Plus Jakarta Sans",
|
|
687
|
+
"Poppins",
|
|
688
|
+
"Prompt",
|
|
689
|
+
"Quicksand",
|
|
690
|
+
"Red Hat Display",
|
|
691
|
+
"Roboto Mono",
|
|
692
|
+
"Rubik",
|
|
693
|
+
"Schibsted Grotesk",
|
|
694
|
+
"Sora",
|
|
695
|
+
"Source Sans 3",
|
|
696
|
+
"Space Grotesk",
|
|
697
|
+
"Syne",
|
|
698
|
+
"Teko",
|
|
699
|
+
"Tinos",
|
|
700
|
+
"Titillium Web",
|
|
701
|
+
"Ubuntu",
|
|
702
|
+
"Yanone Kaffeesatz",
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
/** Families decks still name under a name Google Fonts does not serve. */
|
|
706
|
+
const GOOGLE_FONT_ALIASES: Record<string, string> = {
|
|
707
|
+
"source sans pro": "source sans 3",
|
|
708
|
+
bodoni: "bodoni moda",
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* PPTX stores a weight as part of the typeface name — a Work Sans deck is full
|
|
713
|
+
* of runs set in `Work Sans Medium` — but a webfont family covers its whole
|
|
714
|
+
* weight range, so the suffixed name matches nothing and falls back to the
|
|
715
|
+
* generic sans that made every imported deck look unstyled.
|
|
716
|
+
*/
|
|
717
|
+
const FONT_WEIGHT_SUFFIX =
|
|
718
|
+
/\s+(?:thin|extra ?light|ultra ?light|light|book|regular|normal|medium|semi ?bold|demi ?bold|bold|extra ?bold|ultra ?bold|black|heavy|italic|oblique)$/i;
|
|
719
|
+
|
|
720
|
+
const GOOGLE_FONTS = new Map<string, { family: string; href: string }>();
|
|
721
|
+
for (const [families, axis] of [
|
|
722
|
+
[VARIABLE_AXIS_GOOGLE_FONTS, "ital,wght@0,100..900;1,100..900"],
|
|
723
|
+
[STATIC_WEIGHT_GOOGLE_FONTS, "ital,wght@0,400;0,700;1,400;1,700"],
|
|
724
|
+
] as const) {
|
|
725
|
+
for (const family of families) {
|
|
726
|
+
GOOGLE_FONTS.set(family.toLowerCase(), {
|
|
727
|
+
family,
|
|
728
|
+
href: `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}:${axis}&display=swap`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** The webfont that should render `name`, or undefined when none can. */
|
|
734
|
+
export function resolveImportedFont(
|
|
735
|
+
name: string,
|
|
736
|
+
): { family: string; href: string } | undefined {
|
|
737
|
+
const key = name
|
|
738
|
+
.replace(/["']/g, "")
|
|
739
|
+
.trim()
|
|
740
|
+
.replace(/\s+/g, " ")
|
|
741
|
+
.toLowerCase();
|
|
742
|
+
if (!key) return undefined;
|
|
743
|
+
const lookup = (candidate: string) =>
|
|
744
|
+
GOOGLE_FONTS.get(GOOGLE_FONT_ALIASES[candidate] ?? candidate);
|
|
745
|
+
return lookup(key) ?? lookup(key.replace(FONT_WEIGHT_SUFFIX, "").trim());
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Point every quoted `font-family` in imported slide HTML at a family we can
|
|
750
|
+
* actually serve, and collect the stylesheets that serve them. Families we
|
|
751
|
+
* cannot serve are left exactly as authored so the browser can still match a
|
|
752
|
+
* locally installed copy.
|
|
753
|
+
*/
|
|
754
|
+
export function prepareImportedFonts(html: string): {
|
|
755
|
+
html: string;
|
|
756
|
+
hrefs: string[];
|
|
757
|
+
} {
|
|
758
|
+
const hrefs = new Set<string>();
|
|
759
|
+
const rewritten = html.replace(
|
|
760
|
+
/(font-family:\s*)'([^']*)'/gi,
|
|
761
|
+
(match, prefix: string, name: string) => {
|
|
762
|
+
const font = resolveImportedFont(name);
|
|
763
|
+
if (!font) return match;
|
|
764
|
+
hrefs.add(font.href);
|
|
765
|
+
return `${prefix}'${font.family}'`;
|
|
766
|
+
},
|
|
767
|
+
);
|
|
768
|
+
return { html: rewritten, hrefs: [...hrefs] };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const injectedFontHrefs = new Set<string>();
|
|
772
|
+
|
|
773
|
+
function loadImportedFonts(hrefs: string[]) {
|
|
774
|
+
if (typeof document === "undefined") return;
|
|
775
|
+
for (const href of hrefs) {
|
|
776
|
+
if (injectedFontHrefs.has(href)) continue;
|
|
777
|
+
injectedFontHrefs.add(href);
|
|
778
|
+
const link = document.createElement("link");
|
|
779
|
+
link.rel = "stylesheet";
|
|
780
|
+
link.href = href;
|
|
781
|
+
document.head.appendChild(link);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
599
785
|
/** Renders blank slide HTML content and applies white filter to logo images */
|
|
600
786
|
function BlankSlideContent({ content }: { content: string }) {
|
|
601
787
|
const scopeId = `slide-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
@@ -607,36 +793,43 @@ function BlankSlideContent({ content }: { content: string }) {
|
|
|
607
793
|
// literal each render therefore wipes any DOM mutations made on children. That
|
|
608
794
|
// includes the per-block `contentEditable="true"` set by SlideEditor's
|
|
609
795
|
// double-click-to-edit flow, which made inline text editing appear to do nothing.
|
|
610
|
-
const { mermaidBlocks, htmlWithPlaceholders, dangerousHtml } =
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
796
|
+
const { mermaidBlocks, htmlWithPlaceholders, dangerousHtml, fontHrefs } =
|
|
797
|
+
useMemo(() => {
|
|
798
|
+
// Extract mermaid blocks BEFORE sanitization — see mermaid-blocks.ts for
|
|
799
|
+
// why (sanitizer HTML-escaping breaks the mermaid parser).
|
|
800
|
+
const { blocks, contentWithPlaceholders } = extractMermaidBlocks(content);
|
|
801
|
+
|
|
802
|
+
// Apply white filter to all logo images (brandfetch, logo.dev, etc.) for dark backgrounds
|
|
803
|
+
const sanitized = sanitizeSlideHtml(
|
|
804
|
+
contentWithPlaceholders.replace(
|
|
805
|
+
/(<img\s+(?=[^>]*src="[^"]*(?:brandfetch|logo\.dev)[^"]*")[^>]*)(\/?>)/gi,
|
|
806
|
+
(_match, before, close) => {
|
|
807
|
+
if (before.includes('style="')) {
|
|
808
|
+
return (
|
|
809
|
+
before.replace(
|
|
810
|
+
'style="',
|
|
811
|
+
'style="filter:brightness(0) invert(1);',
|
|
812
|
+
) + close
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return before + ' style="filter:brightness(0) invert(1);"' + close;
|
|
816
|
+
},
|
|
817
|
+
),
|
|
818
|
+
{ scopeSelector },
|
|
819
|
+
);
|
|
820
|
+
const { html: processed, hrefs } = prepareImportedFonts(sanitized);
|
|
633
821
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
822
|
+
return {
|
|
823
|
+
mermaidBlocks: blocks,
|
|
824
|
+
htmlWithPlaceholders: processed,
|
|
825
|
+
dangerousHtml: { __html: processed },
|
|
826
|
+
fontHrefs: hrefs,
|
|
827
|
+
};
|
|
828
|
+
}, [content]);
|
|
829
|
+
|
|
830
|
+
useEffect(() => {
|
|
831
|
+
loadImportedFonts(fontHrefs);
|
|
832
|
+
}, [fontHrefs]);
|
|
640
833
|
|
|
641
834
|
if (mermaidBlocks.length > 0) {
|
|
642
835
|
return (
|
|
@@ -35,12 +35,52 @@ import {
|
|
|
35
35
|
DropdownMenuSubContent,
|
|
36
36
|
DropdownMenuSubTrigger,
|
|
37
37
|
} from "@/components/ui/dropdown-menu";
|
|
38
|
+
import { useDecks } from "@/context/DeckContext";
|
|
38
39
|
import type { GoogleSlidesExportResult } from "@/lib/export-google-slides-client";
|
|
39
40
|
|
|
40
41
|
/** Google Slides' File → Import dialog, primed to ask for a file. */
|
|
41
42
|
const GOOGLE_SLIDES_IMPORT_URL =
|
|
42
43
|
"https://docs.google.com/presentation/u/0/?usp=import";
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The importer stamps this on every slide it writes, and `parseSlideHtml` in
|
|
47
|
+
* actions/export-pptx.ts branches on the same marker: those slides carry the
|
|
48
|
+
* source file's own geometry, which the server emits as real `custGeom` vector
|
|
49
|
+
* shapes. dom-to-pptx has no custGeom at all and rasterizes them to PNGs.
|
|
50
|
+
*/
|
|
51
|
+
const IMPORTED_SLIDE_MARKER = 'data-imported-pptx="true"';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Objects whose geometry only exists once a browser has laid the slide out:
|
|
55
|
+
* `freezeSlideElementForFreeform` and the text-box tool mint
|
|
56
|
+
* `data-slide-object-id` client-side, while every object the importer emits
|
|
57
|
+
* also carries `data-pptx-element-kind`. The server has no layout engine to
|
|
58
|
+
* measure the former, so it refuses those slides rather than reflowing them.
|
|
59
|
+
*/
|
|
60
|
+
const BROWSER_AUTHORED_OBJECT =
|
|
61
|
+
"[data-slide-object-id]:not([data-pptx-element-kind]), .fmd-freeform-object";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the vector-capable server exporter can render this deck losslessly.
|
|
65
|
+
* `get-deck` returns the import receipt alongside the deck body, so the client
|
|
66
|
+
* deck carries `sourceImport` at runtime even though the type predates it.
|
|
67
|
+
*/
|
|
68
|
+
export function canExportPptxFromServer(
|
|
69
|
+
deck:
|
|
70
|
+
| { sourceImport?: unknown; slides: { content?: string }[] }
|
|
71
|
+
| null
|
|
72
|
+
| undefined,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (!deck?.sourceImport || deck.slides.length === 0) return false;
|
|
75
|
+
return deck.slides.every((slide) => {
|
|
76
|
+
const html = slide.content ?? "";
|
|
77
|
+
if (!html.includes(IMPORTED_SLIDE_MARKER)) return false;
|
|
78
|
+
return !new DOMParser()
|
|
79
|
+
.parseFromString(html, "text/html")
|
|
80
|
+
.querySelector(BROWSER_AUTHORED_OBJECT);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
interface ExportMenuProps {
|
|
45
85
|
deckId: string;
|
|
46
86
|
deckTitle: string;
|
|
@@ -76,6 +116,7 @@ export const ExportMenu = forwardRef<ExportMenuHandle, ExportMenuProps>(
|
|
|
76
116
|
ref,
|
|
77
117
|
) {
|
|
78
118
|
const t = useT();
|
|
119
|
+
const { getDeck, flushDeckSave } = useDecks();
|
|
79
120
|
const [googleSlidesImportOpen, setGoogleSlidesImportOpen] = useState(false);
|
|
80
121
|
const googleSlidesImportTarget = useRef<Window | null>(null);
|
|
81
122
|
const triggerBlobDownload = (blob: Blob, filename: string) => {
|
|
@@ -108,8 +149,38 @@ export const ExportMenu = forwardRef<ExportMenuHandle, ExportMenuProps>(
|
|
|
108
149
|
}
|
|
109
150
|
};
|
|
110
151
|
|
|
152
|
+
const exportPptxFromServer = async () => {
|
|
153
|
+
// The server exports the persisted deck, so an unflushed edit would be
|
|
154
|
+
// missing from the file the user just asked for.
|
|
155
|
+
await flushDeckSave(deckId);
|
|
156
|
+
const res = await fetch(`${appBasePath()}/api/exports/pptx`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ deckId }),
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
await readErrorMessage(res, t("editorExport.exportPptxError")),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
triggerBlobDownload(
|
|
167
|
+
await res.blob(),
|
|
168
|
+
filenameFromDisposition(
|
|
169
|
+
res.headers.get("content-disposition"),
|
|
170
|
+
".pptx",
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
111
175
|
const handleExportPptx = async () => {
|
|
112
176
|
try {
|
|
177
|
+
// An imported deck's shapes survive only on the server path. Falling
|
|
178
|
+
// back to the browser exporter on failure would hand back rasterized
|
|
179
|
+
// silhouettes of the same deck without saying so.
|
|
180
|
+
if (canExportPptxFromServer(getDeck(deckId))) {
|
|
181
|
+
await exportPptxFromServer();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
113
184
|
await onExportPptx();
|
|
114
185
|
} catch (err) {
|
|
115
186
|
console.error("Export failed:", err);
|