slides-grab 1.0.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/AGENTS.md +80 -0
- package/LICENSE +21 -0
- package/PROGRESS.md +39 -0
- package/README.md +120 -0
- package/SETUP.md +51 -0
- package/bin/ppt-agent.js +204 -0
- package/convert.cjs +184 -0
- package/package.json +51 -0
- package/prd.json +135 -0
- package/prd.md +104 -0
- package/scripts/editor-server.js +779 -0
- package/scripts/html2pdf.js +217 -0
- package/scripts/validate-slides.js +416 -0
- package/skills/ppt-design-skill/SKILL.md +38 -0
- package/skills/ppt-plan-skill/SKILL.md +37 -0
- package/skills/ppt-pptx-skill/SKILL.md +37 -0
- package/skills/ppt-presentation-skill/SKILL.md +57 -0
- package/src/editor/codex-edit.js +213 -0
- package/src/editor/editor.html +1733 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +110 -0
- package/src/editor/js/editor-dom.js +55 -0
- package/src/editor/js/editor-init.js +284 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +264 -0
- package/src/editor/js/editor-send.js +157 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +32 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/screenshot.js +73 -0
- package/src/resolve.js +159 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +41 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
- package/themes/corporate.css +8 -0
- package/themes/executive.css +10 -0
- package/themes/modern-dark.css +9 -0
- package/themes/sage.css +9 -0
- package/themes/warm.css +8 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ppt-pptx-skill
|
|
3
|
+
description: Stage 3 conversion skill for Codex. Convert approved HTML slides to PPTX/PDF and validate artifacts.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Convert slides and run conversion checks
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# PPTX Conversion Skill (Codex)
|
|
9
|
+
|
|
10
|
+
Use this only after the user approves design output.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
Convert reviewed slide HTML into PPTX (and optional PDF) reliably.
|
|
14
|
+
|
|
15
|
+
## Inputs
|
|
16
|
+
- Approved `<slides-dir>/slide-*.html`
|
|
17
|
+
- Optional output path settings
|
|
18
|
+
|
|
19
|
+
## Outputs
|
|
20
|
+
- Presentation artifact (`.pptx` or `.pdf`)
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
1. Confirm user approval for conversion.
|
|
24
|
+
2. Run conversion command:
|
|
25
|
+
- `node .claude/skills/pptx-skill/scripts/html2pptx.js`
|
|
26
|
+
- or `slides-grab convert --slides-dir <path>`
|
|
27
|
+
3. If requested, run PDF conversion:
|
|
28
|
+
- `slides-grab pdf --slides-dir <path>`
|
|
29
|
+
4. Report success/failure with actionable errors.
|
|
30
|
+
|
|
31
|
+
## Rules
|
|
32
|
+
- Do not modify slide content during conversion stage unless explicitly requested.
|
|
33
|
+
- If conversion fails, diagnose and fix root causes in source HTML/CSS.
|
|
34
|
+
|
|
35
|
+
## Reference
|
|
36
|
+
For detailed conversion behavior and tools, use:
|
|
37
|
+
- `.claude/skills/pptx-skill/SKILL.md`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ppt-presentation-skill
|
|
3
|
+
description: End-to-end presentation workflow for Codex. Use when making a full presentation from scratch — planning, designing slides, editing, and exporting.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Full pipeline from topic to PPTX/PDF
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Presentation Skill (Codex) - Full Workflow Orchestrator
|
|
9
|
+
|
|
10
|
+
Guides you through the complete presentation pipeline from topic to exported file.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Workflow
|
|
15
|
+
|
|
16
|
+
### Stage 1 — Plan
|
|
17
|
+
|
|
18
|
+
Use **ppt-plan-skill** (`skills/ppt-plan-skill/SKILL.md`).
|
|
19
|
+
|
|
20
|
+
1. Take user's topic, audience, and tone.
|
|
21
|
+
2. Create `slide-outline.md`.
|
|
22
|
+
3. Present outline to user.
|
|
23
|
+
4. Revise until user explicitly approves.
|
|
24
|
+
|
|
25
|
+
**Do not proceed to Stage 2 without approval.**
|
|
26
|
+
|
|
27
|
+
### Stage 2 — Design
|
|
28
|
+
|
|
29
|
+
Use **ppt-design-skill** (`skills/ppt-design-skill/SKILL.md`).
|
|
30
|
+
|
|
31
|
+
1. Read approved `slide-outline.md`.
|
|
32
|
+
2. Generate `slide-*.html` files in the slides workspace (default: `slides/`).
|
|
33
|
+
3. Build the viewer: `node scripts/build-viewer.js --slides-dir <path>`
|
|
34
|
+
4. Present viewer to user for review.
|
|
35
|
+
5. Revise individual slides based on feedback.
|
|
36
|
+
6. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
|
|
37
|
+
|
|
38
|
+
**Do not proceed to Stage 3 without approval.**
|
|
39
|
+
|
|
40
|
+
### Stage 3 — Export
|
|
41
|
+
|
|
42
|
+
Use **ppt-pptx-skill** (`skills/ppt-pptx-skill/SKILL.md`).
|
|
43
|
+
|
|
44
|
+
1. Confirm user wants conversion.
|
|
45
|
+
2. Export to PPTX: `slides-grab convert --slides-dir <path> --output <name>.pptx`
|
|
46
|
+
3. Export to PDF (if requested): `slides-grab pdf --slides-dir <path> --output <name>.pdf`
|
|
47
|
+
4. Report results.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
1. **Always follow the stage order**: Plan → Design → Export.
|
|
54
|
+
2. **Get explicit user approval** before advancing to the next stage.
|
|
55
|
+
3. **Read each stage's SKILL.md** for detailed rules — this skill only orchestrates.
|
|
56
|
+
4. **Use `decks/<deck-name>/`** as the slides workspace for multi-deck projects.
|
|
57
|
+
5. For full design constraints, refer to `.claude/skills/design-skill/SKILL.md`.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
|
|
5
|
+
export const SLIDE_SIZE = { width: 960, height: 540 };
|
|
6
|
+
|
|
7
|
+
function clamp(value, min, max) {
|
|
8
|
+
return Math.min(max, Math.max(min, value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toFiniteNumber(value, fallback) {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeSelection(rawSelection, slideSize = SLIDE_SIZE) {
|
|
18
|
+
if (!rawSelection || typeof rawSelection !== 'object') {
|
|
19
|
+
throw new Error('Selection is required.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const maxWidth = slideSize.width;
|
|
23
|
+
const maxHeight = slideSize.height;
|
|
24
|
+
|
|
25
|
+
const x1 = clamp(Math.round(toFiniteNumber(rawSelection.x, 0)), 0, maxWidth);
|
|
26
|
+
const y1 = clamp(Math.round(toFiniteNumber(rawSelection.y, 0)), 0, maxHeight);
|
|
27
|
+
const w = Math.max(1, Math.round(toFiniteNumber(rawSelection.width, 1)));
|
|
28
|
+
const h = Math.max(1, Math.round(toFiniteNumber(rawSelection.height, 1)));
|
|
29
|
+
|
|
30
|
+
const x2 = clamp(x1 + w, 0, maxWidth);
|
|
31
|
+
const y2 = clamp(y1 + h, 0, maxHeight);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
x: x1,
|
|
35
|
+
y: y1,
|
|
36
|
+
width: Math.max(1, x2 - x1),
|
|
37
|
+
height: Math.max(1, y2 - y1),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function scaleSelectionToScreenshot(selection, sourceSize, targetSize) {
|
|
42
|
+
const sourceWidth = sourceSize?.width ?? SLIDE_SIZE.width;
|
|
43
|
+
const sourceHeight = sourceSize?.height ?? SLIDE_SIZE.height;
|
|
44
|
+
const targetWidth = targetSize?.width;
|
|
45
|
+
const targetHeight = targetSize?.height;
|
|
46
|
+
|
|
47
|
+
if (!Number.isFinite(targetWidth) || !Number.isFinite(targetHeight)) {
|
|
48
|
+
throw new Error('Target size must include width and height.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sx = targetWidth / sourceWidth;
|
|
52
|
+
const sy = targetHeight / sourceHeight;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
x: Math.max(0, Math.round(selection.x * sx)),
|
|
56
|
+
y: Math.max(0, Math.round(selection.y * sy)),
|
|
57
|
+
width: Math.max(1, Math.round(selection.width * sx)),
|
|
58
|
+
height: Math.max(1, Math.round(selection.height * sy)),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatTargets(targets) {
|
|
63
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
64
|
+
return [' - (No XPath targets were detected for this region.)'];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return targets.slice(0, 12).flatMap((target, index) => {
|
|
68
|
+
const text = typeof target.text === 'string' && target.text.trim() !== ''
|
|
69
|
+
? target.text.trim().replace(/\s+/g, ' ').slice(0, 140)
|
|
70
|
+
: '(no text)';
|
|
71
|
+
return [
|
|
72
|
+
` - Target ${index + 1}`,
|
|
73
|
+
` - XPath: ${target.xpath}`,
|
|
74
|
+
` - Tag: ${target.tag || 'unknown'}`,
|
|
75
|
+
` - Text: ${text}`,
|
|
76
|
+
];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selections = [] }) {
|
|
81
|
+
const sanitizedPrompt = typeof userPrompt === 'string' ? userPrompt.trim() : '';
|
|
82
|
+
if (!sanitizedPrompt) {
|
|
83
|
+
throw new Error('Prompt must be a non-empty string.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const normalizedSlidePath = typeof slidePath === 'string' && slidePath.trim() !== ''
|
|
87
|
+
? slidePath.trim()
|
|
88
|
+
: (typeof slideFile === 'string' && slideFile.trim() !== '' ? `slides/${slideFile.trim()}` : '');
|
|
89
|
+
if (!normalizedSlidePath) throw new Error('Slide path is required.');
|
|
90
|
+
|
|
91
|
+
if (!Array.isArray(selections) || selections.length === 0) {
|
|
92
|
+
throw new Error('At least one selection is required.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const selectionLines = selections.flatMap((selection, index) => {
|
|
96
|
+
const bbox = selection.bbox ?? selection;
|
|
97
|
+
return [
|
|
98
|
+
`Region ${index + 1}`,
|
|
99
|
+
`- Bounding box: x=${bbox.x}, y=${bbox.y}, width=${bbox.width}, height=${bbox.height}`,
|
|
100
|
+
'- XPath targets:',
|
|
101
|
+
...formatTargets(selection.targets),
|
|
102
|
+
'',
|
|
103
|
+
];
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
`Edit ${normalizedSlidePath} only.`,
|
|
108
|
+
'',
|
|
109
|
+
'User edit request:',
|
|
110
|
+
sanitizedPrompt,
|
|
111
|
+
'',
|
|
112
|
+
'Selected regions on slide (960x540 coordinate space):',
|
|
113
|
+
...selectionLines,
|
|
114
|
+
'Rules:',
|
|
115
|
+
'- Modify only the requested slide file.',
|
|
116
|
+
'- Keep existing structure/content unless the request requires a change.',
|
|
117
|
+
'- Keep slide dimensions at 720pt x 405pt.',
|
|
118
|
+
'- Keep text in semantic tags (<p>, <h1>-<h6>, <ul>, <ol>, <li>).',
|
|
119
|
+
'- Return after applying the change.',
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildCodexExecArgs({ prompt, imagePath, model }) {
|
|
124
|
+
const args = [
|
|
125
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
126
|
+
'exec',
|
|
127
|
+
'--color',
|
|
128
|
+
'never',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (typeof model === 'string' && model.trim() !== '') {
|
|
132
|
+
args.push('--model', model.trim());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof imagePath === 'string' && imagePath.trim() !== '') {
|
|
136
|
+
args.push('--image', imagePath.trim());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
args.push('--', prompt);
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const CLAUDE_MODELS = ['claude-opus-4-6', 'claude-sonnet-4-6'];
|
|
144
|
+
|
|
145
|
+
export function isClaudeModel(model) {
|
|
146
|
+
return typeof model === 'string' && CLAUDE_MODELS.includes(model.trim());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildClaudeExecArgs({ prompt, imagePath, model }) {
|
|
150
|
+
const args = [
|
|
151
|
+
'-p',
|
|
152
|
+
'--dangerously-skip-permissions',
|
|
153
|
+
'--model', model.trim(),
|
|
154
|
+
'--max-turns', '30',
|
|
155
|
+
'--verbose',
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
let fullPrompt = prompt;
|
|
159
|
+
if (typeof imagePath === 'string' && imagePath.trim() !== '') {
|
|
160
|
+
fullPrompt = `First, read the annotated screenshot at "${imagePath.trim()}" to see the visual context of the bbox regions highlighted on the slide.\n\n${prompt}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
args.push(fullPrompt);
|
|
164
|
+
return args;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildAnnotationSvg(width, height, bbox) {
|
|
168
|
+
const boxes = Array.isArray(bbox) ? bbox : [bbox];
|
|
169
|
+
|
|
170
|
+
const overlayItems = boxes.flatMap((item, index) => {
|
|
171
|
+
const x = item.x;
|
|
172
|
+
const y = item.y;
|
|
173
|
+
const w = item.width;
|
|
174
|
+
const h = item.height;
|
|
175
|
+
const labelY = Math.max(18, y - 6);
|
|
176
|
+
return [
|
|
177
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="rgba(239,68,68,0.12)"/>`,
|
|
178
|
+
`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#EF4444" stroke-width="4" filter="url(#shadow)"/>`,
|
|
179
|
+
`<rect x="${x}" y="${Math.max(0, labelY - 16)}" width="22" height="18" fill="#EF4444"/>`,
|
|
180
|
+
`<text x="${x + 11}" y="${labelY - 3}" text-anchor="middle" font-size="12" font-family="Arial, sans-serif" fill="#FFFFFF">${index + 1}</text>`,
|
|
181
|
+
];
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return [
|
|
185
|
+
`<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">`,
|
|
186
|
+
'<defs>',
|
|
187
|
+
'<filter id="shadow"><feDropShadow dx="0" dy="0" stdDeviation="2" flood-opacity="0.8"/></filter>',
|
|
188
|
+
'</defs>',
|
|
189
|
+
...overlayItems,
|
|
190
|
+
'</svg>',
|
|
191
|
+
].join('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function writeAnnotatedScreenshot(inputImagePath, outputImagePath, bbox) {
|
|
195
|
+
await mkdir(dirname(outputImagePath), { recursive: true });
|
|
196
|
+
|
|
197
|
+
const image = sharp(inputImagePath);
|
|
198
|
+
const metadata = await image.metadata();
|
|
199
|
+
const width = metadata.width;
|
|
200
|
+
const height = metadata.height;
|
|
201
|
+
|
|
202
|
+
if (!width || !height) {
|
|
203
|
+
throw new Error('Could not read screenshot dimensions.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const svg = buildAnnotationSvg(width, height, bbox);
|
|
207
|
+
const svgBuffer = Buffer.from(svg, 'utf8');
|
|
208
|
+
|
|
209
|
+
await image
|
|
210
|
+
.composite([{ input: svgBuffer, top: 0, left: 0 }])
|
|
211
|
+
.png()
|
|
212
|
+
.toFile(outputImagePath);
|
|
213
|
+
}
|