slides-grab 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/editor-server.js +30 -61
- package/skills/slides-grab-design/SKILL.md +15 -8
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +45 -0
- package/skills/slides-grab-design/references/design-rules.md +2 -0
- package/skills/slides-grab-design/references/design-system-full.md +5 -3
- package/skills/slides-grab-design/references/detailed-design-rules.md +1 -1
- package/src/editor/codex-edit.js +165 -15
- package/src/editor/edit-subprocess.js +97 -0
- package/src/image-contract.js +6 -6
- package/src/validation/core.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slides-grab",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vkehfdl1",
|
package/scripts/editor-server.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { readdir, readFile, writeFile, mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { watch as fsWatch } from 'node:fs';
|
|
5
5
|
import { basename, dirname, join, resolve, relative, sep } from 'node:path';
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
7
|
import { tmpdir } from 'node:os';
|
|
9
8
|
|
|
@@ -18,6 +17,10 @@ import {
|
|
|
18
17
|
scaleSelectionToScreenshot,
|
|
19
18
|
writeAnnotatedScreenshot,
|
|
20
19
|
} from '../src/editor/codex-edit.js';
|
|
20
|
+
import {
|
|
21
|
+
parseEditTimeoutMs,
|
|
22
|
+
runEditSubprocess,
|
|
23
|
+
} from '../src/editor/edit-subprocess.js';
|
|
21
24
|
import { buildSlideRuntimeHtml } from '../src/image-contract.js';
|
|
22
25
|
|
|
23
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -45,6 +48,7 @@ const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
|
45
48
|
|
|
46
49
|
const MAX_RUNS = 200;
|
|
47
50
|
const MAX_LOG_CHARS = 800_000;
|
|
51
|
+
const EDIT_TIMEOUT_MS = parseEditTimeoutMs();
|
|
48
52
|
|
|
49
53
|
function printUsage() {
|
|
50
54
|
process.stdout.write(`Usage: slides-grab edit [options]\n\n`);
|
|
@@ -233,37 +237,24 @@ function randomRunId() {
|
|
|
233
237
|
return `run-${ts}-${rand}`;
|
|
234
238
|
}
|
|
235
239
|
|
|
240
|
+
function mirrorRunLog(onLog) {
|
|
241
|
+
return (stream, chunk) => {
|
|
242
|
+
onLog(stream, chunk);
|
|
243
|
+
process[stream].write(chunk);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
236
247
|
function spawnCodexEdit({ prompt, imagePath, model, cwd, onLog }) {
|
|
237
248
|
const codexBin = process.env.PPT_AGENT_CODEX_BIN || 'codex';
|
|
238
249
|
const args = buildCodexExecArgs({ prompt, imagePath, model });
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const text = chunk.toString();
|
|
248
|
-
stdout += text;
|
|
249
|
-
onLog('stdout', text);
|
|
250
|
-
process.stdout.write(text);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
child.stderr.on('data', (chunk) => {
|
|
254
|
-
const text = chunk.toString();
|
|
255
|
-
stderr += text;
|
|
256
|
-
onLog('stderr', text);
|
|
257
|
-
process.stderr.write(text);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
child.on('close', (code) => {
|
|
261
|
-
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
child.on('error', (error) => {
|
|
265
|
-
rejectPromise(error);
|
|
266
|
-
});
|
|
250
|
+
return runEditSubprocess({
|
|
251
|
+
bin: codexBin,
|
|
252
|
+
args,
|
|
253
|
+
cwd,
|
|
254
|
+
stdio: 'pipe',
|
|
255
|
+
timeoutMs: EDIT_TIMEOUT_MS,
|
|
256
|
+
engineLabel: 'Codex',
|
|
257
|
+
onLog: mirrorRunLog(onLog),
|
|
267
258
|
});
|
|
268
259
|
}
|
|
269
260
|
|
|
@@ -275,37 +266,15 @@ function spawnClaudeEdit({ prompt, imagePath, model, cwd, onLog }) {
|
|
|
275
266
|
const env = { ...process.env };
|
|
276
267
|
delete env.CLAUDECODE;
|
|
277
268
|
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
child.stdout.on('data', (chunk) => {
|
|
289
|
-
const text = chunk.toString();
|
|
290
|
-
stdout += text;
|
|
291
|
-
onLog('stdout', text);
|
|
292
|
-
process.stdout.write(text);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
child.stderr.on('data', (chunk) => {
|
|
296
|
-
const text = chunk.toString();
|
|
297
|
-
stderr += text;
|
|
298
|
-
onLog('stderr', text);
|
|
299
|
-
process.stderr.write(text);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
child.on('close', (code) => {
|
|
303
|
-
resolvePromise({ code: code ?? 1, stdout, stderr });
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
child.on('error', (error) => {
|
|
307
|
-
rejectPromise(error);
|
|
308
|
-
});
|
|
269
|
+
return runEditSubprocess({
|
|
270
|
+
bin: claudeBin,
|
|
271
|
+
args,
|
|
272
|
+
cwd,
|
|
273
|
+
env,
|
|
274
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
275
|
+
timeoutMs: EDIT_TIMEOUT_MS,
|
|
276
|
+
engineLabel: 'Claude',
|
|
277
|
+
onLog: mirrorRunLog(onLog),
|
|
309
278
|
});
|
|
310
279
|
}
|
|
311
280
|
|
|
@@ -674,7 +643,7 @@ async function startServer(opts) {
|
|
|
674
643
|
const success = result.code === 0;
|
|
675
644
|
const message = success
|
|
676
645
|
? `${engineLabel} edit completed.`
|
|
677
|
-
: `${engineLabel} exited with code ${result.code}
|
|
646
|
+
: (result.timeoutMessage || `${engineLabel} exited with code ${result.code}.`);
|
|
678
647
|
|
|
679
648
|
runStore.finishRun(runId, {
|
|
680
649
|
status: success ? 'success' : 'failed',
|
|
@@ -23,21 +23,27 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
23
23
|
|
|
24
24
|
## Workflow
|
|
25
25
|
1. Read approved `slide-outline.md`.
|
|
26
|
-
2.
|
|
27
|
-
3.
|
|
28
|
-
4.
|
|
29
|
-
5.
|
|
30
|
-
6.
|
|
31
|
-
7.
|
|
32
|
-
8.
|
|
26
|
+
2. Before generating slides, write a quick **visual thesis** (mood/material/energy), a **content plan** (opener → support/proof → detail/story → close/CTA), and the core design tokens (background, surface, text, muted, accent + display/headline/body/caption roles).
|
|
27
|
+
3. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
|
|
28
|
+
4. If the deck needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), create the diagram in `tldraw`, export it with `slides-grab tldraw`, and treat the result as a local slide asset under `<slides-dir>/assets/`.
|
|
29
|
+
5. Run `slides-grab validate --slides-dir <path>` after generation or edits.
|
|
30
|
+
6. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
|
|
31
|
+
7. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
|
|
32
|
+
8. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
|
|
33
|
+
9. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
|
|
34
|
+
10. Keep revising until user approves conversion stage.
|
|
33
35
|
|
|
34
36
|
## Rules
|
|
35
37
|
- Keep slide size 720pt x 405pt.
|
|
36
38
|
- Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).
|
|
37
39
|
- Put local images under `<slides-dir>/assets/` and reference them as `./assets/<file>`.
|
|
38
40
|
- Allow `data:` URLs when the slide must be fully self-contained.
|
|
39
|
-
-
|
|
41
|
+
- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
|
|
40
42
|
- Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.
|
|
43
|
+
- Default to one job per slide, one dominant visual anchor, and copy that scans in seconds.
|
|
44
|
+
- Treat opening slides and section dividers like posters, not dashboards.
|
|
45
|
+
- Default to cardless layouts; only add a card when it improves structure or comprehension.
|
|
46
|
+
- Use whitespace, alignment, scale, cropping, and contrast before adding decorative chrome.
|
|
41
47
|
- Prefer `tldraw` for complex diagrams instead of recreating dense node/edge diagrams directly in HTML/CSS.
|
|
42
48
|
- Use `slides-grab tldraw` plus `templates/diagram-tldraw.html` when that gives a cleaner, more export-friendly result.
|
|
43
49
|
- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.
|
|
@@ -48,4 +54,5 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
48
54
|
For full constraints and style system, follow:
|
|
49
55
|
- `references/design-rules.md`
|
|
50
56
|
- `references/detailed-design-rules.md`
|
|
57
|
+
- `references/beautiful-slide-defaults.md` — slide-specific art direction defaults adapted from OpenAI's frontend design guidance
|
|
51
58
|
- `references/design-system-full.md` — archived full design system, templates, and advanced pattern guidance
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Beautiful Slide Defaults
|
|
2
|
+
|
|
3
|
+
Slide-specific art direction guidance adapted from OpenAI's frontend design guidance for GPT-5.4. Use it to make HTML slides feel deliberate, premium, and instantly scannable without breaking `slides-grab`'s export constraints.
|
|
4
|
+
|
|
5
|
+
## Working Model
|
|
6
|
+
|
|
7
|
+
Before building the deck, write two things:
|
|
8
|
+
|
|
9
|
+
- **visual thesis** — one sentence describing the mood, material, energy, and imagery treatment
|
|
10
|
+
- **content plan** — opener → support/proof → detail/story → close/CTA or decision
|
|
11
|
+
|
|
12
|
+
If the style direction is still open, gather visual references or a mood board first. Define the core tokens early: `background`, `surface`, `primary text`, `muted text`, `accent`, plus typography roles for `display`, `headline`, `body`, and `caption`.
|
|
13
|
+
|
|
14
|
+
## Beautiful Defaults for Slides
|
|
15
|
+
|
|
16
|
+
- Start with composition, not components.
|
|
17
|
+
- Treat the opening slide like a poster and make the title or brand the loudest text.
|
|
18
|
+
- Give each slide one job, one primary takeaway, and one dominant visual anchor.
|
|
19
|
+
- Keep copy short enough to scan in seconds.
|
|
20
|
+
- Use whitespace, alignment, scale, cropping, and contrast before adding chrome.
|
|
21
|
+
- Limit the system by default: two typefaces max and one accent color.
|
|
22
|
+
- Default to cardless layouts. Prefer sections, grids, media blocks, dividers, and strong negative space.
|
|
23
|
+
- Use real imagery, product views, diagrams, or data as the main visual idea. Decorative gradients and abstract filler do not count.
|
|
24
|
+
- Keep the first slide free of secondary clutter such as stat strips, metadata piles, or multiple competing callouts unless the brief explicitly demands them.
|
|
25
|
+
|
|
26
|
+
## Narrative Sequence for Decks
|
|
27
|
+
|
|
28
|
+
Use a narrative rhythm that feels intentional:
|
|
29
|
+
|
|
30
|
+
1. **Opener** — identity, premise, or promise
|
|
31
|
+
2. **Support / proof** — key evidence, context, or concrete value
|
|
32
|
+
3. **Detail / story** — workflow, mechanism, or deeper explanation
|
|
33
|
+
4. **Close / CTA** — decision, recommendation, next step, or final message
|
|
34
|
+
|
|
35
|
+
Section dividers should reset the visual tempo. Alternate dense proof slides with simpler image-led or statement-led slides so the deck keeps breathing.
|
|
36
|
+
|
|
37
|
+
## Review Litmus
|
|
38
|
+
|
|
39
|
+
Before showing the deck, ask:
|
|
40
|
+
|
|
41
|
+
- Can the audience grasp the main point of each slide in 3–5 seconds?
|
|
42
|
+
- Does each slide have one dominant idea instead of multiple competing blocks?
|
|
43
|
+
- Is there one real visual anchor, not just decoration?
|
|
44
|
+
- Would this still feel premium without shadows, cards, or extra chrome?
|
|
45
|
+
- Can any line of copy, badge, or callout be removed without losing meaning?
|
|
@@ -18,8 +18,10 @@ These are the packaged design rules for installable `slides-grab` skills.
|
|
|
18
18
|
## Asset rules
|
|
19
19
|
- Store deck-local assets in `<slides-dir>/assets/`
|
|
20
20
|
- Reference deck-local assets as `./assets/<file>`
|
|
21
|
+
- If an image comes from the web, download it into `<slides-dir>/assets/` before referencing it
|
|
21
22
|
- Use `tldraw`-generated local assets for complex diagrams when possible
|
|
22
23
|
- Allow `data:` URLs only when the slide must be fully self-contained
|
|
24
|
+
- Do not leave remote `http(s)://` image URLs in saved slide HTML
|
|
23
25
|
- Never use absolute filesystem paths
|
|
24
26
|
|
|
25
27
|
## Package-published theme references
|
|
@@ -461,11 +461,13 @@ Store the image at `<slides-dir>/assets/team-photo.png`.
|
|
|
461
461
|
<img src="data:image/svg+xml;base64,..." alt="Illustration" style="width: 220pt; height: 140pt; object-fit: cover;">
|
|
462
462
|
```
|
|
463
463
|
|
|
464
|
-
#### Remote URL (
|
|
464
|
+
#### Remote URL Source (Download Before Saving)
|
|
465
465
|
```html
|
|
466
466
|
<img src="https://images.example.com/hero.png" alt="Hero image" style="width: 220pt; height: 140pt; object-fit: cover;">
|
|
467
467
|
```
|
|
468
468
|
|
|
469
|
+
If the image source starts on the web, download it into `<slides-dir>/assets/` and change the saved slide HTML to `./assets/<file>`.
|
|
470
|
+
|
|
469
471
|
#### Placeholder (Image Stand-In)
|
|
470
472
|
```html
|
|
471
473
|
<div data-image-placeholder style="width: 220pt; height: 140pt; border: 1px dashed #c7c7c7; background: #f3f4f6; display: flex; align-items: center; justify-content: center;">
|
|
@@ -478,7 +480,7 @@ Rules:
|
|
|
478
480
|
- Use `./assets/<file>` as the default image contract for slide HTML.
|
|
479
481
|
- Keep slide assets in `<slides-dir>/assets/`.
|
|
480
482
|
- `data:` URLs are allowed for fully self-contained slides.
|
|
481
|
-
-
|
|
483
|
+
- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
|
|
482
484
|
- Do not use absolute filesystem paths in slide HTML.
|
|
483
485
|
- Do not use non-body `background-image` for content imagery; use `<img>` instead.
|
|
484
486
|
- Use `data-image-placeholder` to reserve space when no image is available yet.
|
|
@@ -573,6 +575,6 @@ This skill is **Stage 2**. It works from the `slide-outline.md` approved by the
|
|
|
573
575
|
|
|
574
576
|
1. **CSS gradients**: Not supported in PowerPoint conversion — replace with background images
|
|
575
577
|
2. **Webfonts**: Always include the Pretendard CDN link
|
|
576
|
-
3. **Image paths**: Use `./assets/<file>` from each `slide-XX.html`; avoid absolute filesystem paths
|
|
578
|
+
3. **Image paths**: Use `./assets/<file>` from each `slide-XX.html`; avoid absolute filesystem paths and do not leave remote `http(s)://` image URLs in saved slide HTML
|
|
577
579
|
4. **Colors**: Always include `#` prefix in CSS
|
|
578
580
|
5. **Text rules**: Never place text directly in div/span
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- Keep slide assets in `<slides-dir>/assets/`.
|
|
12
12
|
- Use `tldraw`-generated assets for complex diagrams whenever possible.
|
|
13
13
|
- `data:` URLs are allowed for fully self-contained slides.
|
|
14
|
-
-
|
|
14
|
+
- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
|
|
15
15
|
- Do not use absolute filesystem paths in slide HTML.
|
|
16
16
|
- Do not use non-body `background-image` for content imagery; use `<img>` instead.
|
|
17
17
|
- Use `data-image-placeholder` to reserve space when no image is available yet.
|
package/src/editor/codex-edit.js
CHANGED
|
@@ -9,13 +9,52 @@ export const SLIDE_SIZE = { width: 960, height: 540 };
|
|
|
9
9
|
|
|
10
10
|
const PPT_DESIGN_SKILL_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'SKILL.md');
|
|
11
11
|
const DETAILED_DESIGN_SKILL_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'references', 'detailed-design-rules.md');
|
|
12
|
+
const BEAUTIFUL_SLIDE_DEFAULTS_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'references', 'beautiful-slide-defaults.md');
|
|
13
|
+
const EDITOR_PPT_DESIGN_SECTION_HEADINGS = [
|
|
14
|
+
'## Workflow',
|
|
15
|
+
'## Rules',
|
|
16
|
+
];
|
|
12
17
|
const DETAILED_DESIGN_SECTION_HEADINGS = [
|
|
13
18
|
'## Base Settings',
|
|
14
|
-
'### 4. Image Usage Rules (Local Asset / Data URL / Remote URL / Placeholder)',
|
|
15
19
|
'## Text Usage Rules',
|
|
16
20
|
'## Workflow (Stage 2: Design + Human Review)',
|
|
17
21
|
'## Important Notes',
|
|
18
22
|
];
|
|
23
|
+
const BEAUTIFUL_SLIDE_DEFAULTS_SECTION_HEADINGS = [
|
|
24
|
+
'## Working Model',
|
|
25
|
+
'## Beautiful Defaults for Slides',
|
|
26
|
+
'## Narrative Sequence for Decks',
|
|
27
|
+
'## Review Litmus',
|
|
28
|
+
];
|
|
29
|
+
const EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS = [
|
|
30
|
+
/visual thesis/i,
|
|
31
|
+
/content plan/i,
|
|
32
|
+
/dominant visual anchor/i,
|
|
33
|
+
/cardless layouts/i,
|
|
34
|
+
/whitespace, alignment, scale, cropping, and contrast/i,
|
|
35
|
+
/opening slides and section dividers like posters/i,
|
|
36
|
+
];
|
|
37
|
+
const EDITOR_PPT_DESIGN_SKILL_FALLBACK = [
|
|
38
|
+
'## Workflow',
|
|
39
|
+
'1. Read approved `slide-outline.md` or the existing slide before editing.',
|
|
40
|
+
'2. Run `slides-grab validate --slides-dir <path>` after generation or edits.',
|
|
41
|
+
'3. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.',
|
|
42
|
+
'4. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.',
|
|
43
|
+
'5. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.',
|
|
44
|
+
'6. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.',
|
|
45
|
+
'7. Keep revising until user approves conversion stage.',
|
|
46
|
+
'',
|
|
47
|
+
'## Rules',
|
|
48
|
+
'- Keep slide size 720pt x 405pt.',
|
|
49
|
+
'- Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).',
|
|
50
|
+
'- Put local images under `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
|
|
51
|
+
'- Allow `data:` URLs when the slide must be fully self-contained.',
|
|
52
|
+
'- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
|
|
53
|
+
'- Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.',
|
|
54
|
+
'- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.',
|
|
55
|
+
'- Do not start conversion before approval.',
|
|
56
|
+
'- Use the packaged CLI and bundled references only; do not depend on unpublished agent-specific files.',
|
|
57
|
+
].join('\n');
|
|
19
58
|
const DETAILED_DESIGN_SKILL_FALLBACK = [
|
|
20
59
|
'## Base Settings',
|
|
21
60
|
'',
|
|
@@ -29,7 +68,7 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
|
|
|
29
68
|
'- Use ./assets/<file> as the default image contract for slide HTML.',
|
|
30
69
|
'- Keep slide assets in <slides-dir>/assets/.',
|
|
31
70
|
'- data: URLs are allowed for fully self-contained slides.',
|
|
32
|
-
'-
|
|
71
|
+
'- Do not leave remote http(s):// image URLs in saved slide HTML; download source images into <slides-dir>/assets/ and reference them as ./assets/<file>.',
|
|
33
72
|
'- Do not use absolute filesystem paths in slide HTML.',
|
|
34
73
|
'- Do not use non-body background-image for content imagery; use <img> instead.',
|
|
35
74
|
'- Use data-image-placeholder to reserve space when no image is available yet.',
|
|
@@ -51,9 +90,38 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
|
|
|
51
90
|
'- Always include # prefix in CSS colors.',
|
|
52
91
|
'- Never place text directly in div/span.',
|
|
53
92
|
].join('\n');
|
|
93
|
+
const BEAUTIFUL_SLIDE_DEFAULTS_FALLBACK = [
|
|
94
|
+
'## Working Model',
|
|
95
|
+
'',
|
|
96
|
+
'Before building the deck, write two things:',
|
|
97
|
+
'- **visual thesis** — one sentence describing the mood, material, energy, and imagery treatment.',
|
|
98
|
+
'- **content plan** — opener → support/proof → detail/story → close/CTA or decision.',
|
|
99
|
+
'- Define design tokens early: background, surface, primary text, muted text, accent, plus display/headline/body/caption roles.',
|
|
100
|
+
'',
|
|
101
|
+
'## Beautiful Defaults for Slides',
|
|
102
|
+
'- Start with composition, not components.',
|
|
103
|
+
'- Treat the opening slide like a poster and make the title or brand the loudest text.',
|
|
104
|
+
'- Give each slide one job, one primary takeaway, and one dominant visual anchor.',
|
|
105
|
+
'- Keep copy short enough to scan in seconds.',
|
|
106
|
+
'- Use whitespace, alignment, scale, cropping, and contrast before adding chrome.',
|
|
107
|
+
'- Limit the system by default: two typefaces max and one accent color.',
|
|
108
|
+
'- Default to cardless layouts unless a card improves structure or understanding.',
|
|
109
|
+
'',
|
|
110
|
+
'## Narrative Sequence for Decks',
|
|
111
|
+
'- Opener → support/proof → detail/story → close/CTA or decision.',
|
|
112
|
+
'- Section dividers should reset the visual tempo.',
|
|
113
|
+
'',
|
|
114
|
+
'## Review Litmus',
|
|
115
|
+
'- Can the audience grasp the main point of each slide in 3–5 seconds?',
|
|
116
|
+
'- Does the slide have one dominant idea instead of competing blocks?',
|
|
117
|
+
'- Is there one real visual anchor, not just decoration?',
|
|
118
|
+
'- Would this still feel premium without shadows, cards, or extra chrome?',
|
|
119
|
+
].join('\n');
|
|
54
120
|
|
|
55
121
|
let cachedPptDesignSkillPrompt = null;
|
|
56
|
-
let
|
|
122
|
+
let cachedEditorPptDesignSkillPrompt = null;
|
|
123
|
+
let cachedStructuralDesignSkillPrompt = null;
|
|
124
|
+
let cachedSlideArtDirectionPrompt = null;
|
|
57
125
|
|
|
58
126
|
function clamp(value, min, max) {
|
|
59
127
|
return Math.min(max, Math.max(min, value));
|
|
@@ -142,6 +210,25 @@ export function getPptDesignSkillPrompt() {
|
|
|
142
210
|
return cachedPptDesignSkillPrompt;
|
|
143
211
|
}
|
|
144
212
|
|
|
213
|
+
function getEditorPptDesignSkillPrompt() {
|
|
214
|
+
if (cachedEditorPptDesignSkillPrompt !== null) {
|
|
215
|
+
return cachedEditorPptDesignSkillPrompt;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const prompt = loadMarkdownSections(
|
|
219
|
+
PPT_DESIGN_SKILL_PATH,
|
|
220
|
+
EDITOR_PPT_DESIGN_SECTION_HEADINGS,
|
|
221
|
+
EDITOR_PPT_DESIGN_SKILL_FALLBACK,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
cachedEditorPptDesignSkillPrompt = pruneDuplicateLines(
|
|
225
|
+
prompt,
|
|
226
|
+
EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return cachedEditorPptDesignSkillPrompt;
|
|
230
|
+
}
|
|
231
|
+
|
|
145
232
|
function extractMarkdownSection(markdown, heading) {
|
|
146
233
|
const lines = markdown.split('\n');
|
|
147
234
|
const startIndex = lines.findIndex((line) => line.trim() === heading.trim());
|
|
@@ -168,25 +255,74 @@ function extractMarkdownSection(markdown, heading) {
|
|
|
168
255
|
return extracted.join('\n').trim();
|
|
169
256
|
}
|
|
170
257
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
258
|
+
function pruneDuplicateLines(markdown, patterns) {
|
|
259
|
+
const lines = markdown.split('\n');
|
|
260
|
+
const filtered = [];
|
|
261
|
+
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
if (patterns.some((pattern) => pattern.test(line))) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const previousLine = filtered.at(-1) ?? '';
|
|
268
|
+
if (line.trim() === '' && previousLine.trim() === '') {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
filtered.push(line);
|
|
174
273
|
}
|
|
175
274
|
|
|
275
|
+
return filtered.join('\n').trim();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function loadMarkdownSections(markdownPath, headings, fallback) {
|
|
176
279
|
try {
|
|
177
|
-
const markdown = readFileSync(
|
|
178
|
-
const sections =
|
|
280
|
+
const markdown = readFileSync(markdownPath, 'utf8');
|
|
281
|
+
const sections = headings
|
|
179
282
|
.map((heading) => extractMarkdownSection(markdown, heading))
|
|
180
283
|
.filter(Boolean);
|
|
181
284
|
|
|
182
|
-
|
|
285
|
+
return sections.length > 0
|
|
183
286
|
? sections.join('\n\n')
|
|
184
|
-
:
|
|
287
|
+
: fallback;
|
|
185
288
|
} catch {
|
|
186
|
-
|
|
289
|
+
return fallback;
|
|
187
290
|
}
|
|
291
|
+
}
|
|
188
292
|
|
|
189
|
-
|
|
293
|
+
function getStructuralDesignSkillPrompt() {
|
|
294
|
+
if (cachedStructuralDesignSkillPrompt !== null) {
|
|
295
|
+
return cachedStructuralDesignSkillPrompt;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
cachedStructuralDesignSkillPrompt = loadMarkdownSections(
|
|
299
|
+
DETAILED_DESIGN_SKILL_PATH,
|
|
300
|
+
DETAILED_DESIGN_SECTION_HEADINGS,
|
|
301
|
+
DETAILED_DESIGN_SKILL_FALLBACK,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
return cachedStructuralDesignSkillPrompt;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getSlideArtDirectionPrompt() {
|
|
308
|
+
if (cachedSlideArtDirectionPrompt !== null) {
|
|
309
|
+
return cachedSlideArtDirectionPrompt;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
cachedSlideArtDirectionPrompt = loadMarkdownSections(
|
|
313
|
+
BEAUTIFUL_SLIDE_DEFAULTS_PATH,
|
|
314
|
+
BEAUTIFUL_SLIDE_DEFAULTS_SECTION_HEADINGS,
|
|
315
|
+
BEAUTIFUL_SLIDE_DEFAULTS_FALLBACK,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return cachedSlideArtDirectionPrompt;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function getDetailedDesignSkillPrompt() {
|
|
322
|
+
return [
|
|
323
|
+
getStructuralDesignSkillPrompt(),
|
|
324
|
+
getSlideArtDirectionPrompt(),
|
|
325
|
+
].filter(Boolean).join('\n\n');
|
|
190
326
|
}
|
|
191
327
|
|
|
192
328
|
export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selections = [] }) {
|
|
@@ -215,7 +351,7 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
|
|
|
215
351
|
];
|
|
216
352
|
});
|
|
217
353
|
|
|
218
|
-
const pptDesignSkillPrompt =
|
|
354
|
+
const pptDesignSkillPrompt = getEditorPptDesignSkillPrompt();
|
|
219
355
|
const skillLines = pptDesignSkillPrompt
|
|
220
356
|
? [
|
|
221
357
|
'Project skill guidance (follow strictly):',
|
|
@@ -224,7 +360,7 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
|
|
|
224
360
|
'',
|
|
225
361
|
]
|
|
226
362
|
: [];
|
|
227
|
-
const detailedDesignSkillPrompt =
|
|
363
|
+
const detailedDesignSkillPrompt = getStructuralDesignSkillPrompt();
|
|
228
364
|
const detailedSkillLines = detailedDesignSkillPrompt
|
|
229
365
|
? [
|
|
230
366
|
'Detailed design/export guardrails (selected from the full design system):',
|
|
@@ -233,23 +369,37 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
|
|
|
233
369
|
'',
|
|
234
370
|
]
|
|
235
371
|
: [];
|
|
372
|
+
const slideArtDirectionPrompt = getSlideArtDirectionPrompt();
|
|
373
|
+
const slideArtDirectionLines = slideArtDirectionPrompt
|
|
374
|
+
? [
|
|
375
|
+
'Slide art direction defaults (packaged guidance for beautiful HTML slides):',
|
|
376
|
+
`Primary source: ${BEAUTIFUL_SLIDE_DEFAULTS_PATH}`,
|
|
377
|
+
slideArtDirectionPrompt,
|
|
378
|
+
'',
|
|
379
|
+
]
|
|
380
|
+
: [];
|
|
236
381
|
|
|
237
382
|
return [
|
|
238
383
|
`Edit ${normalizedSlidePath} only.`,
|
|
239
384
|
'',
|
|
240
385
|
...skillLines,
|
|
241
386
|
...detailedSkillLines,
|
|
387
|
+
...slideArtDirectionLines,
|
|
242
388
|
'User edit request:',
|
|
243
389
|
sanitizedPrompt,
|
|
244
390
|
'',
|
|
245
391
|
'Selected regions on slide (960x540 coordinate space):',
|
|
246
392
|
...selectionLines,
|
|
247
393
|
'Rules:',
|
|
248
|
-
'-
|
|
394
|
+
'- Edit only the requested slide HTML file among slide-*.html files.',
|
|
395
|
+
'- Do not modify any other slide HTML files unless explicitly requested.',
|
|
249
396
|
'- Keep existing structure/content unless the request requires a change.',
|
|
250
397
|
'- Keep slide dimensions at 720pt x 405pt.',
|
|
251
398
|
'- Keep text in semantic tags (<p>, <h1>-<h6>, <ul>, <ol>, <li>).',
|
|
399
|
+
'- You may add or update supporting files required for the requested slide, including local assets under <slides-dir>/assets/ and tldraw source/export files used to generate those assets.',
|
|
400
|
+
'- If you create or update a supporting asset, store it under <slides-dir>/assets/ and reference it from the requested slide as ./assets/<file>.',
|
|
252
401
|
'- Keep local assets under ./assets/ and preserve portable relative paths.',
|
|
402
|
+
'- Do not modify unrelated assets, shared resources, or generated files that are not required for the requested slide.',
|
|
253
403
|
'- Do not persist runtime-only editor/viewer injections such as <base>, debug scripts, or viewer wrapper markup into the slide file.',
|
|
254
404
|
'- Return after applying the change.',
|
|
255
405
|
].join('\n');
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_EDIT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
export const EDIT_TIMEOUT_EXIT_CODE = 124;
|
|
5
|
+
export const EDIT_TIMEOUT_ENV_VAR = 'PPT_AGENT_EDIT_TIMEOUT_MS';
|
|
6
|
+
export const EDIT_TIMEOUT_KILL_SIGNAL = 'SIGTERM';
|
|
7
|
+
export const EDIT_TIMEOUT_FORCE_KILL_AFTER_MS = 5_000;
|
|
8
|
+
|
|
9
|
+
export function parseEditTimeoutMs(rawValue = process.env[EDIT_TIMEOUT_ENV_VAR]) {
|
|
10
|
+
if (rawValue == null || rawValue === '') {
|
|
11
|
+
return DEFAULT_EDIT_TIMEOUT_MS;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const timeoutMs = Number(rawValue);
|
|
15
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
16
|
+
return DEFAULT_EDIT_TIMEOUT_MS;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Math.floor(timeoutMs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildEditTimeoutMessage({ engineLabel = 'Editor process', timeoutMs }) {
|
|
23
|
+
return `${engineLabel} edit timed out after ${timeoutMs}ms and was terminated.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runEditSubprocess({
|
|
27
|
+
bin,
|
|
28
|
+
args,
|
|
29
|
+
cwd,
|
|
30
|
+
env,
|
|
31
|
+
stdio = ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
timeoutMs = DEFAULT_EDIT_TIMEOUT_MS,
|
|
33
|
+
engineLabel,
|
|
34
|
+
onLog = () => {},
|
|
35
|
+
spawnImpl = spawn,
|
|
36
|
+
}) {
|
|
37
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
38
|
+
const child = spawnImpl(bin, args, { cwd, env, stdio });
|
|
39
|
+
|
|
40
|
+
let stdout = '';
|
|
41
|
+
let stderr = '';
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
let settled = false;
|
|
44
|
+
let forceKillTimer = null;
|
|
45
|
+
|
|
46
|
+
const timeoutMessage = buildEditTimeoutMessage({ engineLabel, timeoutMs });
|
|
47
|
+
|
|
48
|
+
const timeoutTimer = setTimeout(() => {
|
|
49
|
+
timedOut = true;
|
|
50
|
+
const messageLine = `${timeoutMessage}\n`;
|
|
51
|
+
stderr += messageLine;
|
|
52
|
+
onLog('stderr', messageLine);
|
|
53
|
+
child.kill(EDIT_TIMEOUT_KILL_SIGNAL);
|
|
54
|
+
forceKillTimer = setTimeout(() => {
|
|
55
|
+
child.kill('SIGKILL');
|
|
56
|
+
}, EDIT_TIMEOUT_FORCE_KILL_AFTER_MS);
|
|
57
|
+
forceKillTimer.unref?.();
|
|
58
|
+
}, timeoutMs);
|
|
59
|
+
timeoutTimer.unref?.();
|
|
60
|
+
|
|
61
|
+
child.stdout?.on('data', (chunk) => {
|
|
62
|
+
const text = chunk.toString();
|
|
63
|
+
stdout += text;
|
|
64
|
+
onLog('stdout', text);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.stderr?.on('data', (chunk) => {
|
|
68
|
+
const text = chunk.toString();
|
|
69
|
+
stderr += text;
|
|
70
|
+
onLog('stderr', text);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
child.on('close', (code, signal) => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timeoutTimer);
|
|
77
|
+
clearTimeout(forceKillTimer);
|
|
78
|
+
resolvePromise({
|
|
79
|
+
code: timedOut ? EDIT_TIMEOUT_EXIT_CODE : (code ?? 1),
|
|
80
|
+
stdout,
|
|
81
|
+
stderr,
|
|
82
|
+
signal: timedOut ? (signal || EDIT_TIMEOUT_KILL_SIGNAL) : signal,
|
|
83
|
+
timedOut,
|
|
84
|
+
timeoutMs: timedOut ? timeoutMs : null,
|
|
85
|
+
timeoutMessage: timedOut ? timeoutMessage : null,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.on('error', (error) => {
|
|
90
|
+
if (settled) return;
|
|
91
|
+
settled = true;
|
|
92
|
+
clearTimeout(timeoutTimer);
|
|
93
|
+
clearTimeout(forceKillTimer);
|
|
94
|
+
rejectPromise(error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/image-contract.js
CHANGED
|
@@ -66,9 +66,9 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
|
|
|
66
66
|
|
|
67
67
|
if (classification.kind === 'remote-url') {
|
|
68
68
|
issues.push({
|
|
69
|
-
severity: '
|
|
69
|
+
severity: 'critical',
|
|
70
70
|
code: 'remote-image-url',
|
|
71
|
-
message: 'Remote image
|
|
71
|
+
message: 'Remote image URLs are unsupported in saved slide HTML. Download the image into ./assets/<file> instead.',
|
|
72
72
|
slide: slideFile,
|
|
73
73
|
...entry,
|
|
74
74
|
});
|
|
@@ -77,9 +77,9 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
|
|
|
77
77
|
|
|
78
78
|
if (classification.kind === 'remote-url-insecure') {
|
|
79
79
|
issues.push({
|
|
80
|
-
severity: '
|
|
80
|
+
severity: 'critical',
|
|
81
81
|
code: 'remote-image-url-insecure',
|
|
82
|
-
message: '
|
|
82
|
+
message: 'Remote http:// image URLs are unsupported in saved slide HTML. Download the image into ./assets/<file> instead.',
|
|
83
83
|
slide: slideFile,
|
|
84
84
|
...entry,
|
|
85
85
|
});
|
|
@@ -183,11 +183,11 @@ export function buildSlideRuntimeHtml(html, { baseHref, slideFile }) {
|
|
|
183
183
|
const src = (image.getAttribute('src') || '').trim();
|
|
184
184
|
if (!src || src.startsWith('data:')) continue;
|
|
185
185
|
if (src.startsWith('https://')) {
|
|
186
|
-
|
|
186
|
+
fail('remote image URL is unsupported in saved slides; download it into ./assets/<file>', { src });
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
189
|
if (src.startsWith('http://')) {
|
|
190
|
-
|
|
190
|
+
fail('remote http:// image URL is unsupported in saved slides; download it into ./assets/<file>', { src });
|
|
191
191
|
continue;
|
|
192
192
|
}
|
|
193
193
|
if (absolutePathRe.test(src) || src.startsWith('/')) {
|
package/src/validation/core.js
CHANGED
|
@@ -622,6 +622,10 @@ const EXPORT_BLOCKING_IMAGE_CONTRACT_CODES = new Set([
|
|
|
622
622
|
'absolute-filesystem-image-path',
|
|
623
623
|
'missing-local-asset',
|
|
624
624
|
'missing-local-background-asset',
|
|
625
|
+
'remote-background-image-url',
|
|
626
|
+
'remote-background-image-url-insecure',
|
|
627
|
+
'remote-image-url',
|
|
628
|
+
'remote-image-url-insecure',
|
|
625
629
|
'root-relative-image-path',
|
|
626
630
|
'unsupported-background-image',
|
|
627
631
|
]);
|