@agent-native/core 0.149.0 → 0.149.2
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/.agents/skills/dashboard-management/SKILL.md +2 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
- package/corpus/templates/analytics/app/global.css +1 -1
- package/corpus/templates/analytics/app/i18n-data.ts +37 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
- package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/assets/app/global.css +1 -1
- package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/brain/app/global.css +1 -1
- package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +274 -137
- package/corpus/templates/clips/desktop/src/styles.css +35 -0
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/crm/app/global.css +1 -1
- package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -0
- package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
- package/corpus/templates/slides/AGENTS.md +11 -10
- package/corpus/templates/slides/actions/add-deck.ts +2 -0
- package/corpus/templates/slides/actions/add-slide.ts +15 -1
- package/corpus/templates/slides/actions/create-deck.ts +18 -6
- package/corpus/templates/slides/actions/get-deck.ts +29 -1
- package/corpus/templates/slides/actions/patch-deck.ts +139 -10
- package/corpus/templates/slides/actions/save-deck.ts +24 -1
- 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 +10 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -63
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -7
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
- package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
- package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
- package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +16 -27
- package/corpus/templates/slides/app/pages/Index.tsx +256 -98
- 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-ai-editing-avatar-badge.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -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/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/corpus/templates/slides/shared/api.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +135 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
- package/dist/agent/engine/ai-sdk-engine.js +1 -1
- package/dist/agent/production-agent.d.ts +21 -1
- package/dist/agent/production-agent.js +129 -10
- package/dist/agent/tool-approval-store.d.ts +18 -0
- package/dist/agent/tool-approval-store.js +122 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/cli/code-agent-executor.js +11 -2
- package/dist/client/AgentPanel.js +31 -6
- package/dist/client/AssistantChat.d.ts +3 -2
- package/dist/client/AssistantChat.js +7 -20
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +11 -0
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/chat/markdown-renderer.js +14 -2
- package/dist/client/chat/message-components.d.ts +6 -2
- package/dist/client/chat/message-components.js +25 -47
- package/dist/client/chat/tool-call-display.d.ts +3 -5
- package/dist/client/chat/tool-call-display.js +7 -212
- package/dist/client/chat-first/apps-rail.js +3 -3
- package/dist/client/chat-first/browser-pane.js +4 -1
- package/dist/client/chat-first/chat-history.d.ts +3 -1
- package/dist/client/chat-first/chat-history.js +2 -2
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +52 -41
- package/dist/client/chat-first/types.d.ts +3 -0
- package/dist/client/chat-first.d.ts +10 -0
- package/dist/client/chat-first.js +37 -2
- package/dist/client/components/ui/message-scroller.js +4 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +9 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/use-chat-models.js +156 -80
- package/dist/integrations/webhook-handler.js +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/auth.js +262 -31
- package/dist/server/onboarding-html.js +56 -9
- package/dist/shared/reasoning-effort.d.ts +8 -8
- package/dist/shared/reasoning-effort.js +10 -10
- package/dist/shared/streaming-text-smoothing.js +41 -11
- package/dist/styles/agent-native.css +19 -32
- package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
- package/docs/content/template-chat.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ASPECT_RATIOS } from "@shared/aspect-ratios";
|
|
2
|
+
|
|
1
3
|
import type {
|
|
2
4
|
ParsedElement,
|
|
3
5
|
ParsedParagraph,
|
|
@@ -165,10 +167,37 @@ export function convertToSlideHtml(
|
|
|
165
167
|
|
|
166
168
|
const DEFAULT_SLIDE_WIDTH_EMU = 9144000;
|
|
167
169
|
const DEFAULT_SLIDE_HEIGHT_EMU = 5143500;
|
|
168
|
-
const CSS_PX_PER_POINT = 96 / 72;
|
|
169
170
|
const DEFAULT_PPTX_BACKGROUND = "#000000"; // guard:allow-raw-color - preserve PPTX black when no background is declared
|
|
170
171
|
const DEFAULT_PPTX_FOREGROUND = "#ffffff"; // guard:allow-raw-color - preserve PPTX white when no run color is declared
|
|
171
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The absolute px box `toSlidePxX`/`toSlidePxY` scale positions and sizes
|
|
175
|
+
* against. It must match the aspect-ratio preset the deck actually renders
|
|
176
|
+
* into (`ASPECT_RATIOS`, chosen by the import actions' own
|
|
177
|
+
* `nearestAspectRatio`) rather than a fixed 16:9 box: a PDF page or a custom
|
|
178
|
+
* PPTX slide size is routinely portrait or square, and scaling its elements
|
|
179
|
+
* against a 960x540 reference while the deck itself renders in an 864x1080
|
|
180
|
+
* (or other) box stretches every element by the ratio between the two
|
|
181
|
+
* boxes, most visibly squashing everything into the top fraction of a
|
|
182
|
+
* taller-than-540 canvas.
|
|
183
|
+
*/
|
|
184
|
+
function referenceBoxForSlide(
|
|
185
|
+
widthEmu: number,
|
|
186
|
+
heightEmu: number,
|
|
187
|
+
): { width: number; height: number } {
|
|
188
|
+
const target = widthEmu / heightEmu;
|
|
189
|
+
let best: { width: number; height: number } = ASPECT_RATIOS["16:9"];
|
|
190
|
+
let bestDiff = Infinity;
|
|
191
|
+
for (const preset of Object.values(ASPECT_RATIOS)) {
|
|
192
|
+
const diff = Math.abs(preset.width / preset.height - target);
|
|
193
|
+
if (diff < bestDiff) {
|
|
194
|
+
bestDiff = diff;
|
|
195
|
+
best = preset;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { width: best.width, height: best.height };
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
function buildFidelitySlide(
|
|
173
202
|
slide: ParsedSlide,
|
|
174
203
|
imageUrls: string | Record<string, string> | undefined,
|
|
@@ -176,9 +205,10 @@ function buildFidelitySlide(
|
|
|
176
205
|
): string {
|
|
177
206
|
const widthEmu = slide.widthEmu || DEFAULT_SLIDE_WIDTH_EMU;
|
|
178
207
|
const heightEmu = slide.heightEmu || DEFAULT_SLIDE_HEIGHT_EMU;
|
|
208
|
+
const refBox = referenceBoxForSlide(widthEmu, heightEmu);
|
|
179
209
|
const background = slide.backgroundColor ?? DEFAULT_PPTX_BACKGROUND;
|
|
180
210
|
const gridStyle = slide.backgroundGrid
|
|
181
|
-
? `background-image:linear-gradient(to right, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu))}px, transparent ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu))}px),linear-gradient(to bottom, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu))}px, transparent ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu))}px);background-size:${toSlidePxX(slide.backgroundGrid.stepXEmu, widthEmu)}px ${toSlidePxY(slide.backgroundGrid.stepYEmu, heightEmu)}px;background-position:${toSlidePxX(slide.backgroundGrid.offsetXEmu, widthEmu)}px ${toSlidePxY(slide.backgroundGrid.offsetYEmu, heightEmu)}px;background-repeat:repeat;`
|
|
211
|
+
? `background-image:linear-gradient(to right, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu, refBox.width))}px, transparent ${Math.max(0.5, toSlidePxX(slide.backgroundGrid.lineWidthEmu, widthEmu, refBox.width))}px),linear-gradient(to bottom, ${esc(slide.backgroundGrid.color)} 0 ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu, refBox.height))}px, transparent ${Math.max(0.5, toSlidePxY(slide.backgroundGrid.lineWidthEmu, heightEmu, refBox.height))}px);background-size:${toSlidePxX(slide.backgroundGrid.stepXEmu, widthEmu, refBox.width)}px ${toSlidePxY(slide.backgroundGrid.stepYEmu, heightEmu, refBox.height)}px;background-position:${toSlidePxX(slide.backgroundGrid.offsetXEmu, widthEmu, refBox.width)}px ${toSlidePxY(slide.backgroundGrid.offsetYEmu, heightEmu, refBox.height)}px;background-repeat:repeat;`
|
|
182
212
|
: "";
|
|
183
213
|
const elements = slide.elements ?? [];
|
|
184
214
|
const html = elements
|
|
@@ -188,6 +218,7 @@ function buildFidelitySlide(
|
|
|
188
218
|
index,
|
|
189
219
|
widthEmu,
|
|
190
220
|
heightEmu,
|
|
221
|
+
refBox,
|
|
191
222
|
imageUrls,
|
|
192
223
|
themeFont,
|
|
193
224
|
),
|
|
@@ -203,10 +234,11 @@ function buildFidelityElement(
|
|
|
203
234
|
index: number,
|
|
204
235
|
widthEmu: number,
|
|
205
236
|
heightEmu: number,
|
|
237
|
+
refBox: { width: number; height: number },
|
|
206
238
|
imageUrls: string | Record<string, string> | undefined,
|
|
207
239
|
themeFont: string | undefined,
|
|
208
240
|
): string {
|
|
209
|
-
const position = `position: absolute; left: ${toSlidePxX(element.x, widthEmu)}px; top: ${toSlidePxY(element.y, heightEmu)}px; width: ${toSlidePxX(element.width, widthEmu)}px; height: ${toSlidePxY(element.height, heightEmu)}px; z-index: ${index}; box-sizing: border-box;`;
|
|
241
|
+
const position = `position: absolute; left: ${toSlidePxX(element.x, widthEmu, refBox.width)}px; top: ${toSlidePxY(element.y, heightEmu, refBox.height)}px; width: ${toSlidePxX(element.width, widthEmu, refBox.width)}px; height: ${toSlidePxY(element.height, heightEmu, refBox.height)}px; z-index: ${index}; box-sizing: border-box;`;
|
|
210
242
|
const rotation = element.rotation
|
|
211
243
|
? ` transform: rotate(${element.rotation}deg); transform-origin: center center;`
|
|
212
244
|
: "";
|
|
@@ -218,12 +250,18 @@ function buildFidelityElement(
|
|
|
218
250
|
return `<div class="fmd-pptx-image" data-pptx-element-kind="image" data-pptx-image-name="${esc(element.image?.name ?? "image")}"${objectId} style="${position}${rotation} overflow: hidden;">${url ? `<img src="${esc(url)}" alt="" style="${imageStyle}" />` : `<div class="fmd-img-placeholder" style="width:100%;height:100%;">Imported image: ${esc(element.image?.name ?? "image")}</div>`}</div>`;
|
|
219
251
|
}
|
|
220
252
|
|
|
221
|
-
const decoration = shapeDecoration(element, widthEmu);
|
|
253
|
+
const decoration = shapeDecoration(element, widthEmu, refBox.width);
|
|
222
254
|
if (element.kind === "shape") {
|
|
223
255
|
return `<div class="fmd-pptx-shape" data-pptx-element-kind="shape"${objectId} style="${position}${rotation}${decoration}"></div>`;
|
|
224
256
|
}
|
|
225
257
|
|
|
226
|
-
const textStyle = textBoxStyle(
|
|
258
|
+
const textStyle = textBoxStyle(
|
|
259
|
+
element,
|
|
260
|
+
widthEmu,
|
|
261
|
+
heightEmu,
|
|
262
|
+
refBox,
|
|
263
|
+
themeFont,
|
|
264
|
+
);
|
|
227
265
|
const defaultFontWeight = element.placeholderType === "title" ? 700 : 400;
|
|
228
266
|
const paragraphs = (element.paragraphs ?? [])
|
|
229
267
|
.map((paragraph, paragraphIndex) =>
|
|
@@ -231,6 +269,7 @@ function buildFidelityElement(
|
|
|
231
269
|
paragraph,
|
|
232
270
|
paragraphIndex,
|
|
233
271
|
widthEmu,
|
|
272
|
+
refBox.width,
|
|
234
273
|
themeFont,
|
|
235
274
|
defaultFontWeight,
|
|
236
275
|
),
|
|
@@ -239,12 +278,41 @@ function buildFidelityElement(
|
|
|
239
278
|
return `<div class="fmd-pptx-text" data-pptx-element-kind="text"${objectId} style="${position}${rotation}${decoration}${textStyle}">${paragraphs}</div>`;
|
|
240
279
|
}
|
|
241
280
|
|
|
242
|
-
function toSlidePxX(
|
|
243
|
-
|
|
281
|
+
function toSlidePxX(
|
|
282
|
+
valueEmu: number,
|
|
283
|
+
slideWidthEmu: number,
|
|
284
|
+
refWidthPx: number,
|
|
285
|
+
): number {
|
|
286
|
+
return Math.round((valueEmu / slideWidthEmu) * refWidthPx * 1000) / 1000;
|
|
244
287
|
}
|
|
245
288
|
|
|
246
|
-
function toSlidePxY(
|
|
247
|
-
|
|
289
|
+
function toSlidePxY(
|
|
290
|
+
valueEmu: number,
|
|
291
|
+
slideHeightEmu: number,
|
|
292
|
+
refHeightPx: number,
|
|
293
|
+
): number {
|
|
294
|
+
return Math.round((valueEmu / slideHeightEmu) * refHeightPx * 1000) / 1000;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const EMU_PER_POINT = 12700;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A run's font size (and paragraph spacing) is stored in points, a physical
|
|
301
|
+
* unit independent of the source slide's own canvas size — unlike
|
|
302
|
+
* position/size EMUs, a fixed `pt * 96/72` conversion doesn't know how far
|
|
303
|
+
* `toSlidePxX`/`toSlidePxY` scaled that canvas down (or up) to fit the
|
|
304
|
+
* deck's aspect-ratio box. Converting the point value to EMU first and
|
|
305
|
+
* running it through the same `toSlidePxX` scale keeps text sized
|
|
306
|
+
* proportionally to its box on every source slide size, not just the one
|
|
307
|
+
* physical size (10in wide) that happens to make the fixed conversion agree
|
|
308
|
+
* with the 16:9 preset's box.
|
|
309
|
+
*/
|
|
310
|
+
function ptToSlidePx(
|
|
311
|
+
valuePt: number,
|
|
312
|
+
widthEmu: number,
|
|
313
|
+
refWidthPx: number,
|
|
314
|
+
): number {
|
|
315
|
+
return toSlidePxX(valuePt * EMU_PER_POINT, widthEmu, refWidthPx);
|
|
248
316
|
}
|
|
249
317
|
|
|
250
318
|
function imageUrlForElement(
|
|
@@ -263,10 +331,14 @@ function imageRenderStyle(element: ParsedElement): string {
|
|
|
263
331
|
return `display:block;position:absolute;left:${(-crop.left / visibleWidth) * 100}%;top:${(-crop.top / visibleHeight) * 100}%;width:${(1 / visibleWidth) * 100}%;height:${(1 / visibleHeight) * 100}%;object-fit:fill;`;
|
|
264
332
|
}
|
|
265
333
|
|
|
266
|
-
function shapeDecoration(
|
|
334
|
+
function shapeDecoration(
|
|
335
|
+
element: ParsedElement,
|
|
336
|
+
widthEmu: number,
|
|
337
|
+
refWidthPx: number,
|
|
338
|
+
): string {
|
|
267
339
|
const fill = element.fill ? `background: ${esc(element.fill)};` : "";
|
|
268
340
|
const line = element.lineColor
|
|
269
|
-
? `border: ${Math.max(1, toSlidePxX(element.lineWidth ?? 12700, widthEmu))}px solid ${esc(element.lineColor)};`
|
|
341
|
+
? `border: ${Math.max(1, toSlidePxX(element.lineWidth ?? 12700, widthEmu, refWidthPx))}px solid ${esc(element.lineColor)};`
|
|
270
342
|
: "";
|
|
271
343
|
const radius = element.shapeType === "roundRect" ? "border-radius: 6px;" : "";
|
|
272
344
|
return `${fill}${line}${radius}`;
|
|
@@ -276,13 +348,16 @@ function textBoxStyle(
|
|
|
276
348
|
element: ParsedElement,
|
|
277
349
|
widthEmu: number,
|
|
278
350
|
heightEmu: number,
|
|
351
|
+
refBox: { width: number; height: number },
|
|
279
352
|
themeFont: string | undefined,
|
|
280
353
|
): string {
|
|
281
354
|
const padding = element.padding;
|
|
282
|
-
const left = padding ? toSlidePxX(padding.left, widthEmu) : 0;
|
|
283
|
-
const right = padding ? toSlidePxX(padding.right, widthEmu) : 0;
|
|
284
|
-
const top = padding ? toSlidePxY(padding.top, heightEmu) : 0;
|
|
285
|
-
const bottom = padding
|
|
355
|
+
const left = padding ? toSlidePxX(padding.left, widthEmu, refBox.width) : 0;
|
|
356
|
+
const right = padding ? toSlidePxX(padding.right, widthEmu, refBox.width) : 0;
|
|
357
|
+
const top = padding ? toSlidePxY(padding.top, heightEmu, refBox.height) : 0;
|
|
358
|
+
const bottom = padding
|
|
359
|
+
? toSlidePxY(padding.bottom, heightEmu, refBox.height)
|
|
360
|
+
: 0;
|
|
286
361
|
const align = element.paragraphs?.[0]?.alignment ?? "left";
|
|
287
362
|
const vertical =
|
|
288
363
|
element.verticalAlign === "middle"
|
|
@@ -297,37 +372,55 @@ function buildFidelityParagraph(
|
|
|
297
372
|
paragraph: ParsedParagraph,
|
|
298
373
|
paragraphIndex: number,
|
|
299
374
|
widthEmu: number,
|
|
375
|
+
refWidthPx: number,
|
|
300
376
|
themeFont: string | undefined,
|
|
301
377
|
defaultFontWeight: number,
|
|
302
378
|
): string {
|
|
303
379
|
const firstRun = paragraph.runs[0];
|
|
304
|
-
const fontSize = (firstRun?.fontSize ?? 18
|
|
380
|
+
const fontSize = ptToSlidePx(firstRun?.fontSize ?? 18, widthEmu, refWidthPx);
|
|
305
381
|
const lineHeight = paragraph.lineSpacing ?? 1.2;
|
|
382
|
+
const bulletFontSize = ptToSlidePx(
|
|
383
|
+
paragraph.bulletSize ?? firstRun?.fontSize ?? 18,
|
|
384
|
+
widthEmu,
|
|
385
|
+
refWidthPx,
|
|
386
|
+
);
|
|
306
387
|
const bullet = paragraph.bulletChar
|
|
307
|
-
? `<span aria-hidden="true" style="display:inline-block;width:${fontSize * 0.75}px;min-width:${fontSize * 0.75}px;margin-right:${fontSize * 0.65}px;color:${esc(paragraph.bulletColor ?? firstRun?.color ?? DEFAULT_PPTX_FOREGROUND)};font-family:${cssFontFamily(paragraph.bulletFontFamily ?? themeFont)};font-size:${
|
|
388
|
+
? `<span aria-hidden="true" style="display:inline-block;width:${fontSize * 0.75}px;min-width:${fontSize * 0.75}px;margin-right:${fontSize * 0.65}px;color:${esc(paragraph.bulletColor ?? firstRun?.color ?? DEFAULT_PPTX_FOREGROUND)};font-family:${cssFontFamily(paragraph.bulletFontFamily ?? themeFont)};font-size:${bulletFontSize}px;">${esc(paragraph.bulletChar)}</span>`
|
|
308
389
|
: "";
|
|
309
390
|
const marginLeft = paragraph.marginLeftEmu
|
|
310
|
-
? toSlidePxX(paragraph.marginLeftEmu, widthEmu)
|
|
391
|
+
? toSlidePxX(paragraph.marginLeftEmu, widthEmu, refWidthPx)
|
|
311
392
|
: 0;
|
|
312
393
|
const indent = paragraph.indentEmu
|
|
313
|
-
? toSlidePxX(paragraph.indentEmu, widthEmu)
|
|
394
|
+
? toSlidePxX(paragraph.indentEmu, widthEmu, refWidthPx)
|
|
314
395
|
: 0;
|
|
315
396
|
const spacingBefore = paragraph.spaceBeforePt ?? 0;
|
|
316
397
|
const spacingAfter = paragraph.spaceAfterPt ?? 0;
|
|
317
398
|
const bulletMargin = paragraph.bulletChar ? `margin-left:${indent}px;` : "";
|
|
399
|
+
const marginBefore = ptToSlidePx(spacingBefore, widthEmu, refWidthPx);
|
|
400
|
+
const marginAfter = ptToSlidePx(spacingAfter, widthEmu, refWidthPx);
|
|
318
401
|
const text = paragraph.runs
|
|
319
|
-
.map((run) =>
|
|
402
|
+
.map((run) =>
|
|
403
|
+
formatFidelityRun(
|
|
404
|
+
run,
|
|
405
|
+
widthEmu,
|
|
406
|
+
refWidthPx,
|
|
407
|
+
themeFont,
|
|
408
|
+
defaultFontWeight,
|
|
409
|
+
),
|
|
410
|
+
)
|
|
320
411
|
.join("");
|
|
321
|
-
return `<p data-pptx-paragraph="${paragraphIndex}" style="display:block;flex:0 0 auto;text-align:${paragraph.alignment ?? "left"};white-space:pre-wrap;margin:${
|
|
412
|
+
return `<p data-pptx-paragraph="${paragraphIndex}" style="display:block;flex:0 0 auto;text-align:${paragraph.alignment ?? "left"};white-space:pre-wrap;margin:${marginBefore}px 0 ${marginAfter}px;line-height:${lineHeight};font-size:${fontSize}px;min-height:${fontSize * lineHeight}px;padding-left:${marginLeft}px;text-indent:${paragraph.bulletChar ? 0 : indent}px;">${bullet.replace("display:inline-block;", `display:inline-block;${bulletMargin}`)}${text}</p>`;
|
|
322
413
|
}
|
|
323
414
|
|
|
324
415
|
function formatFidelityRun(
|
|
325
416
|
run: ParsedTextRun,
|
|
417
|
+
widthEmu: number,
|
|
418
|
+
refWidthPx: number,
|
|
326
419
|
themeFont: string | undefined,
|
|
327
420
|
defaultFontWeight = 400,
|
|
328
421
|
): string {
|
|
329
422
|
const styles = [
|
|
330
|
-
`font-size:${(run.fontSize ?? 18
|
|
423
|
+
`font-size:${ptToSlidePx(run.fontSize ?? 18, widthEmu, refWidthPx)}px`,
|
|
331
424
|
`font-family:${cssFontFamily(run.fontFamily ?? themeFont)}`,
|
|
332
425
|
`color:${esc(run.color ?? DEFAULT_PPTX_FOREGROUND)}`,
|
|
333
426
|
`font-weight:${run.bold ? 700 : fontWeightForFamily(run.fontFamily, defaultFontWeight)}`,
|
|
@@ -380,14 +380,23 @@ export function mergeLineRuns(items: TextRunBox[]): TextRunBox[] {
|
|
|
380
380
|
prev.italic === item.italic &&
|
|
381
381
|
prev.underline === item.underline &&
|
|
382
382
|
prev.href === item.href;
|
|
383
|
+
const needsSpace =
|
|
384
|
+
prev !== undefined && item.left - prev.right > item.fontSize * 0.25;
|
|
383
385
|
if (prev && sameStyle) {
|
|
384
|
-
const needsSpace = item.left - prev.right > item.fontSize * 0.25;
|
|
385
386
|
prev.text += (needsSpace ? " " : "") + item.text;
|
|
386
387
|
prev.right = Math.max(prev.right, item.right);
|
|
387
388
|
prev.top = Math.min(prev.top, item.top);
|
|
388
389
|
prev.bottom = Math.max(prev.bottom, item.bottom);
|
|
389
390
|
} else {
|
|
390
|
-
|
|
391
|
+
// A word-sized gap has to survive a style change too. Only the
|
|
392
|
+
// same-style branch used to re-add it, so a heading whose colour
|
|
393
|
+
// changed mid-line ("7 Air " + "purifying") lost the space at the
|
|
394
|
+
// boundary and rendered as one jammed-together word.
|
|
395
|
+
const separator =
|
|
396
|
+
needsSpace && !/\s$/.test(prev?.text ?? "") && !/^\s/.test(item.text)
|
|
397
|
+
? " "
|
|
398
|
+
: "";
|
|
399
|
+
runs.push({ ...item, text: separator + item.text });
|
|
391
400
|
}
|
|
392
401
|
}
|
|
393
402
|
return runs;
|
|
@@ -119,7 +119,7 @@ export async function prepareSlidesChatAttachments(args: {
|
|
|
119
119
|
"",
|
|
120
120
|
"File handling rules:",
|
|
121
121
|
"- If the request refers to the current or visible deck, call `view-screen` first to confirm the active deckId, then pass that deckId to import or slide-edit actions.",
|
|
122
|
-
'- PPTX files: when the user wants the visible deck improved, call `import-pptx --filePath "<path>" --deckId <deckId>` first, then
|
|
122
|
+
'- PPTX files: when the user wants the visible deck improved, call `import-pptx --filePath "<path>" --deckId <deckId>` first, then use one patch-deck call with requireAllSourceSlides=true and one content patch per imported slide for a deck-wide restyle. Use update-slide only for a targeted one-slide edit. Do not rebuild the source deck with add-slide.',
|
|
123
123
|
'- PDF and DOCX files: call `import-file --filePath "<path>" --format auto --deckId <deckId>` and use the returned extracted text as source material before creating editable slides. For a visual PDF that the user wants preserved, beautified, or restyled from its original layout, pass `--importIntoDeck true` so each page is imported source-faithfully first; keep the full-page image and style around it, with source text persisted in slide notes for inspection.',
|
|
124
124
|
'- Figma `.fig` files: call `import-file --filePath "<path>" --format fig` to start Builder design-system indexing. Do not create a local design system directly from the upload.',
|
|
125
125
|
"- For deck-generation requests, start mutating promptly: create or update the first slide as soon as source material is extracted, then continue slide-by-slide with add-slide/update-slide.",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { lookup as dnsLookup, type LookupAddress } from "node:dns";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import type { LookupFunction } from "node:net";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isPrivateAddress,
|
|
8
|
+
MAX_PROXIED_IMAGE_BYTES,
|
|
9
|
+
MAX_PROXY_REDIRECTS,
|
|
10
|
+
parseProxyableImageUrl,
|
|
11
|
+
} from "./image-proxy-url.js";
|
|
12
|
+
|
|
13
|
+
export type RemoteImageFailure =
|
|
14
|
+
| "unsupported-url"
|
|
15
|
+
| "blocked-address"
|
|
16
|
+
| "fetch-failed"
|
|
17
|
+
| "too-many-redirects"
|
|
18
|
+
| "not-an-image"
|
|
19
|
+
| "too-large";
|
|
20
|
+
|
|
21
|
+
export type RemoteImageResult =
|
|
22
|
+
| { ok: true; contentType: string; body: Buffer }
|
|
23
|
+
| { ok: false; reason: RemoteImageFailure };
|
|
24
|
+
|
|
25
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A `lookup` implementation that only ever hands the socket an address we have
|
|
29
|
+
* classified as public.
|
|
30
|
+
*
|
|
31
|
+
* Validating the hostname separately and then calling `fetch` leaves a gap: the
|
|
32
|
+
* two resolutions are independent, so a DNS-rebinding host can answer with a
|
|
33
|
+
* public address for the check and a loopback or metadata address for the
|
|
34
|
+
* actual connection. Because Node passes this straight to `net.connect`, the
|
|
35
|
+
* address that is checked here is the address that gets dialled.
|
|
36
|
+
*/
|
|
37
|
+
export const publicOnlyLookup: LookupFunction = (
|
|
38
|
+
hostname,
|
|
39
|
+
options,
|
|
40
|
+
callback,
|
|
41
|
+
) => {
|
|
42
|
+
const wantsAll =
|
|
43
|
+
typeof options === "object" && options !== null && options.all === true;
|
|
44
|
+
const hints = typeof options === "object" && options !== null ? options : {};
|
|
45
|
+
|
|
46
|
+
dnsLookup(hostname, { ...hints, all: true }, (err, addresses) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
(callback as (e: NodeJS.ErrnoException) => void)(err);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const safe = (addresses as LookupAddress[]).filter(
|
|
52
|
+
(entry) => !isPrivateAddress(entry.address),
|
|
53
|
+
);
|
|
54
|
+
if (safe.length === 0) {
|
|
55
|
+
const blocked: NodeJS.ErrnoException = new Error(
|
|
56
|
+
`Refusing to connect to a non-public address for ${hostname}`,
|
|
57
|
+
);
|
|
58
|
+
blocked.code = "EBLOCKED";
|
|
59
|
+
(callback as (e: NodeJS.ErrnoException) => void)(blocked);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (wantsAll) {
|
|
63
|
+
(callback as unknown as (e: null, a: LookupAddress[]) => void)(
|
|
64
|
+
null,
|
|
65
|
+
safe,
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
callback(null, safe[0].address, safe[0].family);
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
interface HopResult {
|
|
74
|
+
status: number;
|
|
75
|
+
headers: http.IncomingHttpHeaders;
|
|
76
|
+
read: () => Promise<Buffer | "too-large">;
|
|
77
|
+
discard: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function requestHop(target: URL): Promise<HopResult> {
|
|
81
|
+
return new Promise<HopResult>((resolve, reject) => {
|
|
82
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
83
|
+
const request = transport.request(
|
|
84
|
+
target,
|
|
85
|
+
{
|
|
86
|
+
method: "GET",
|
|
87
|
+
headers: { Accept: "image/*" },
|
|
88
|
+
lookup: publicOnlyLookup,
|
|
89
|
+
},
|
|
90
|
+
(response) => {
|
|
91
|
+
resolve({
|
|
92
|
+
status: response.statusCode ?? 0,
|
|
93
|
+
headers: response.headers,
|
|
94
|
+
discard: () => response.destroy(),
|
|
95
|
+
read: () =>
|
|
96
|
+
new Promise<Buffer | "too-large">((resolveBody, rejectBody) => {
|
|
97
|
+
const chunks: Buffer[] = [];
|
|
98
|
+
let total = 0;
|
|
99
|
+
response.on("data", (chunk: Buffer) => {
|
|
100
|
+
total += chunk.length;
|
|
101
|
+
// Stop at the cap instead of buffering first and measuring
|
|
102
|
+
// after: a chunked response, or one that lies about
|
|
103
|
+
// Content-Length, would otherwise pull the whole body into
|
|
104
|
+
// memory before anyone checks it.
|
|
105
|
+
if (total > MAX_PROXIED_IMAGE_BYTES) {
|
|
106
|
+
response.destroy();
|
|
107
|
+
resolveBody("too-large");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
chunks.push(chunk);
|
|
111
|
+
});
|
|
112
|
+
response.on("end", () => resolveBody(Buffer.concat(chunks)));
|
|
113
|
+
response.on("error", rejectBody);
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
request.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
120
|
+
request.destroy(new Error("Timed out fetching image"));
|
|
121
|
+
});
|
|
122
|
+
request.on("error", reject);
|
|
123
|
+
request.end();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch a remote image for the proxy route. Every hop is re-parsed against the
|
|
129
|
+
* URL policy and every connection is pinned to a validated public address.
|
|
130
|
+
*/
|
|
131
|
+
export async function fetchRemoteImage(
|
|
132
|
+
raw: string,
|
|
133
|
+
): Promise<RemoteImageResult> {
|
|
134
|
+
const firstTarget = parseProxyableImageUrl(raw);
|
|
135
|
+
if (!firstTarget) return { ok: false, reason: "unsupported-url" };
|
|
136
|
+
let target: URL = firstTarget;
|
|
137
|
+
|
|
138
|
+
for (let hop = 0; hop <= MAX_PROXY_REDIRECTS; hop++) {
|
|
139
|
+
let response: HopResult;
|
|
140
|
+
try {
|
|
141
|
+
response = await requestHop(target);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
reason: code === "EBLOCKED" ? "blocked-address" : "fetch-failed",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (response.status >= 300 && response.status < 400) {
|
|
151
|
+
response.discard();
|
|
152
|
+
const location = response.headers.location;
|
|
153
|
+
const next: URL | null = location
|
|
154
|
+
? parseProxyableImageUrl(new URL(location, target).href)
|
|
155
|
+
: null;
|
|
156
|
+
if (!next) return { ok: false, reason: "fetch-failed" };
|
|
157
|
+
target = next;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (response.status < 200 || response.status >= 300) {
|
|
162
|
+
response.discard();
|
|
163
|
+
return { ok: false, reason: "fetch-failed" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const contentType = String(response.headers["content-type"] ?? "");
|
|
167
|
+
if (!contentType.startsWith("image/")) {
|
|
168
|
+
response.discard();
|
|
169
|
+
return { ok: false, reason: "not-an-image" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const declared = Number(response.headers["content-length"]);
|
|
173
|
+
if (Number.isFinite(declared) && declared > MAX_PROXIED_IMAGE_BYTES) {
|
|
174
|
+
response.discard();
|
|
175
|
+
return { ok: false, reason: "too-large" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let body: Buffer | "too-large";
|
|
179
|
+
try {
|
|
180
|
+
body = await response.read();
|
|
181
|
+
} catch {
|
|
182
|
+
// coercion-ok: a truncated transfer has no partial-success form here;
|
|
183
|
+
// the caller answers 502 either way.
|
|
184
|
+
return { ok: false, reason: "fetch-failed" };
|
|
185
|
+
}
|
|
186
|
+
if (body === "too-large") return { ok: false, reason: "too-large" };
|
|
187
|
+
|
|
188
|
+
return { ok: true, contentType, body };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { ok: false, reason: "too-many-redirects" };
|
|
192
|
+
}
|