claude-presentation-master 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/LICENSE +21 -0
- package/README.md +763 -0
- package/assets/presentation-engine.css +3057 -0
- package/assets/presentation-knowledge.yaml +6312 -0
- package/bin/cli.js +445 -0
- package/dist/index.d.mts +1209 -0
- package/dist/index.d.ts +1209 -0
- package/dist/index.js +4204 -0
- package/dist/index.mjs +4142 -0
- package/package.json +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ChartJsProvider: () => ChartJsProvider,
|
|
34
|
+
CompositeChartProvider: () => CompositeChartProvider,
|
|
35
|
+
CompositeImageProvider: () => CompositeImageProvider,
|
|
36
|
+
ContentAnalyzer: () => ContentAnalyzer,
|
|
37
|
+
KnowledgeBase: () => KnowledgeBase,
|
|
38
|
+
LocalImageProvider: () => LocalImageProvider,
|
|
39
|
+
MermaidProvider: () => MermaidProvider,
|
|
40
|
+
PlaceholderImageProvider: () => PlaceholderImageProvider,
|
|
41
|
+
PowerPointGenerator: () => PowerPointGenerator,
|
|
42
|
+
PresentationEngine: () => PresentationEngine,
|
|
43
|
+
QAEngine: () => QAEngine,
|
|
44
|
+
QAFailureError: () => QAFailureError,
|
|
45
|
+
QuickChartProvider: () => QuickChartProvider,
|
|
46
|
+
RevealJsGenerator: () => RevealJsGenerator,
|
|
47
|
+
ScoreCalculator: () => ScoreCalculator,
|
|
48
|
+
SlideFactory: () => SlideFactory,
|
|
49
|
+
TemplateEngine: () => TemplateEngine,
|
|
50
|
+
TemplateNotFoundError: () => TemplateNotFoundError,
|
|
51
|
+
UnsplashImageProvider: () => UnsplashImageProvider,
|
|
52
|
+
VERSION: () => VERSION,
|
|
53
|
+
ValidationError: () => ValidationError,
|
|
54
|
+
createDefaultChartProvider: () => createDefaultChartProvider,
|
|
55
|
+
createDefaultImageProvider: () => createDefaultImageProvider,
|
|
56
|
+
default: () => index_default,
|
|
57
|
+
generate: () => generate,
|
|
58
|
+
getKnowledgeBase: () => getKnowledgeBase,
|
|
59
|
+
validate: () => validate
|
|
60
|
+
});
|
|
61
|
+
module.exports = __toCommonJS(index_exports);
|
|
62
|
+
|
|
63
|
+
// src/types/index.ts
|
|
64
|
+
var ValidationError = class extends Error {
|
|
65
|
+
constructor(errors, message = "Validation failed") {
|
|
66
|
+
super(message);
|
|
67
|
+
this.errors = errors;
|
|
68
|
+
this.name = "ValidationError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var QAFailureError = class extends Error {
|
|
72
|
+
constructor(score, threshold, qaResults, message = `QA score ${score} below threshold ${threshold}`) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.score = score;
|
|
75
|
+
this.threshold = threshold;
|
|
76
|
+
this.qaResults = qaResults;
|
|
77
|
+
this.name = "QAFailureError";
|
|
78
|
+
}
|
|
79
|
+
getIssues() {
|
|
80
|
+
return this.qaResults.issues.map((issue) => issue.message);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var TemplateNotFoundError = class extends Error {
|
|
84
|
+
constructor(templatePath, message = `Template not found: ${templatePath}`) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.templatePath = templatePath;
|
|
87
|
+
this.name = "TemplateNotFoundError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/core/ContentAnalyzer.ts
|
|
92
|
+
var ContentAnalyzer = class {
|
|
93
|
+
// Signal words for SCQA detection
|
|
94
|
+
situationSignals = [
|
|
95
|
+
"currently",
|
|
96
|
+
"today",
|
|
97
|
+
"at present",
|
|
98
|
+
"historically",
|
|
99
|
+
"traditionally",
|
|
100
|
+
"as of",
|
|
101
|
+
"our",
|
|
102
|
+
"the market",
|
|
103
|
+
"industry",
|
|
104
|
+
"context"
|
|
105
|
+
];
|
|
106
|
+
complicationSignals = [
|
|
107
|
+
"however",
|
|
108
|
+
"but",
|
|
109
|
+
"unfortunately",
|
|
110
|
+
"challenge",
|
|
111
|
+
"problem",
|
|
112
|
+
"issue",
|
|
113
|
+
"risk",
|
|
114
|
+
"threat",
|
|
115
|
+
"concern",
|
|
116
|
+
"difficulty",
|
|
117
|
+
"obstacle",
|
|
118
|
+
"barrier",
|
|
119
|
+
"yet",
|
|
120
|
+
"although",
|
|
121
|
+
"despite",
|
|
122
|
+
"while"
|
|
123
|
+
];
|
|
124
|
+
questionSignals = [
|
|
125
|
+
"how",
|
|
126
|
+
"what",
|
|
127
|
+
"why",
|
|
128
|
+
"when",
|
|
129
|
+
"where",
|
|
130
|
+
"which",
|
|
131
|
+
"should",
|
|
132
|
+
"could",
|
|
133
|
+
"can we",
|
|
134
|
+
"is it possible",
|
|
135
|
+
"?"
|
|
136
|
+
];
|
|
137
|
+
answerSignals = [
|
|
138
|
+
"therefore",
|
|
139
|
+
"thus",
|
|
140
|
+
"recommend",
|
|
141
|
+
"propose",
|
|
142
|
+
"suggest",
|
|
143
|
+
"solution",
|
|
144
|
+
"answer",
|
|
145
|
+
"strategy",
|
|
146
|
+
"approach",
|
|
147
|
+
"plan",
|
|
148
|
+
"we should",
|
|
149
|
+
"must",
|
|
150
|
+
"need to",
|
|
151
|
+
"the answer"
|
|
152
|
+
];
|
|
153
|
+
// Sparkline detection
|
|
154
|
+
whatIsSignals = [
|
|
155
|
+
"currently",
|
|
156
|
+
"today",
|
|
157
|
+
"status quo",
|
|
158
|
+
"reality",
|
|
159
|
+
"actual",
|
|
160
|
+
"now",
|
|
161
|
+
"existing",
|
|
162
|
+
"present state",
|
|
163
|
+
"as-is",
|
|
164
|
+
"problem"
|
|
165
|
+
];
|
|
166
|
+
whatCouldBeSignals = [
|
|
167
|
+
"imagine",
|
|
168
|
+
"vision",
|
|
169
|
+
"future",
|
|
170
|
+
"could be",
|
|
171
|
+
"what if",
|
|
172
|
+
"possibility",
|
|
173
|
+
"potential",
|
|
174
|
+
"opportunity",
|
|
175
|
+
"transform",
|
|
176
|
+
"envision",
|
|
177
|
+
"ideal",
|
|
178
|
+
"dream",
|
|
179
|
+
"goal",
|
|
180
|
+
"aspiration"
|
|
181
|
+
];
|
|
182
|
+
/**
|
|
183
|
+
* Analyze content and extract structural elements.
|
|
184
|
+
*/
|
|
185
|
+
async analyze(content, contentType) {
|
|
186
|
+
const text = this.parseContent(content, contentType);
|
|
187
|
+
const paragraphs = this.splitIntoParagraphs(text);
|
|
188
|
+
const sentences = this.splitIntoSentences(text);
|
|
189
|
+
const scqa = this.extractSCQA(paragraphs, sentences);
|
|
190
|
+
const sparkline = this.extractSparkline(paragraphs);
|
|
191
|
+
const keyMessages = this.extractKeyMessages(text, sentences);
|
|
192
|
+
const titles = this.generateActionTitles(keyMessages, paragraphs);
|
|
193
|
+
const starMoments = this.identifyStarMoments(paragraphs);
|
|
194
|
+
const estimatedSlideCount = this.estimateSlideCount(text, paragraphs);
|
|
195
|
+
return {
|
|
196
|
+
scqa,
|
|
197
|
+
sparkline,
|
|
198
|
+
keyMessages,
|
|
199
|
+
titles,
|
|
200
|
+
starMoments,
|
|
201
|
+
estimatedSlideCount
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Parse content based on its type.
|
|
206
|
+
*/
|
|
207
|
+
parseContent(content, contentType) {
|
|
208
|
+
switch (contentType) {
|
|
209
|
+
case "markdown":
|
|
210
|
+
return this.parseMarkdown(content);
|
|
211
|
+
case "json":
|
|
212
|
+
return this.parseJSON(content);
|
|
213
|
+
case "yaml":
|
|
214
|
+
return this.parseYAML(content);
|
|
215
|
+
case "text":
|
|
216
|
+
default:
|
|
217
|
+
return content;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Parse markdown content to plain text (preserving structure hints).
|
|
222
|
+
*/
|
|
223
|
+
parseMarkdown(content) {
|
|
224
|
+
let text = content;
|
|
225
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, "\n[HEADER] $1\n");
|
|
226
|
+
text = text.replace(/^[-*+]\s+(.+)$/gm, "[BULLET] $1");
|
|
227
|
+
text = text.replace(/^\d+\.\s+(.+)$/gm, "[NUMBERED] $1");
|
|
228
|
+
text = text.replace(/\*\*(.+?)\*\*/g, "[EMPHASIS] $1 [/EMPHASIS]");
|
|
229
|
+
text = text.replace(/\*(.+?)\*/g, "$1");
|
|
230
|
+
text = text.replace(/```[\s\S]*?```/g, "[CODE BLOCK]");
|
|
231
|
+
text = text.replace(/`(.+?)`/g, "$1");
|
|
232
|
+
text = text.replace(/\[(.+?)\]\(.+?\)/g, "$1");
|
|
233
|
+
text = text.replace(/!\[.*?\]\(.+?\)/g, "[IMAGE]");
|
|
234
|
+
return text.trim();
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Parse JSON content.
|
|
238
|
+
*/
|
|
239
|
+
parseJSON(content) {
|
|
240
|
+
try {
|
|
241
|
+
const data = JSON.parse(content);
|
|
242
|
+
return this.flattenObject(data);
|
|
243
|
+
} catch {
|
|
244
|
+
return content;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Parse YAML content.
|
|
249
|
+
*/
|
|
250
|
+
parseYAML(content) {
|
|
251
|
+
const lines = content.split("\n");
|
|
252
|
+
const values = [];
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const match = line.match(/^[\s-]*(?:\w+:\s*)?(.+)$/);
|
|
255
|
+
if (match?.[1] && !match[1].includes(":")) {
|
|
256
|
+
values.push(match[1].trim());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return values.join("\n");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Flatten object to text.
|
|
263
|
+
*/
|
|
264
|
+
flattenObject(obj, prefix = "") {
|
|
265
|
+
const parts = [];
|
|
266
|
+
if (typeof obj === "string") {
|
|
267
|
+
return obj;
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(obj)) {
|
|
270
|
+
for (const item of obj) {
|
|
271
|
+
parts.push(this.flattenObject(item, prefix));
|
|
272
|
+
}
|
|
273
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
274
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
275
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
276
|
+
parts.push(this.flattenObject(value, newPrefix));
|
|
277
|
+
}
|
|
278
|
+
} else if (obj !== null && obj !== void 0) {
|
|
279
|
+
parts.push(String(obj));
|
|
280
|
+
}
|
|
281
|
+
return parts.join("\n");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Split text into paragraphs.
|
|
285
|
+
*/
|
|
286
|
+
splitIntoParagraphs(text) {
|
|
287
|
+
return text.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Split text into sentences.
|
|
291
|
+
*/
|
|
292
|
+
splitIntoSentences(text) {
|
|
293
|
+
const cleaned = text.replace(/Mr\./g, "Mr").replace(/Mrs\./g, "Mrs").replace(/Dr\./g, "Dr").replace(/vs\./g, "vs").replace(/etc\./g, "etc").replace(/e\.g\./g, "eg").replace(/i\.e\./g, "ie");
|
|
294
|
+
return cleaned.split(/[.!?]+/).map((s) => s.trim()).filter((s) => s.length > 10);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Extract SCQA structure (Barbara Minto's Pyramid Principle).
|
|
298
|
+
*/
|
|
299
|
+
extractSCQA(paragraphs, sentences) {
|
|
300
|
+
let situation = "";
|
|
301
|
+
let complication = "";
|
|
302
|
+
let question = "";
|
|
303
|
+
let answer = "";
|
|
304
|
+
for (const para of paragraphs.slice(0, 3)) {
|
|
305
|
+
if (this.containsSignals(para, this.situationSignals)) {
|
|
306
|
+
situation = this.extractRelevantSentence(para, this.situationSignals);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const para of paragraphs) {
|
|
311
|
+
if (this.containsSignals(para, this.complicationSignals)) {
|
|
312
|
+
complication = this.extractRelevantSentence(para, this.complicationSignals);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const sentence of sentences) {
|
|
317
|
+
if (sentence.includes("?") || this.containsSignals(sentence, this.questionSignals)) {
|
|
318
|
+
question = sentence;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const para of paragraphs.slice(-3)) {
|
|
323
|
+
if (this.containsSignals(para, this.answerSignals)) {
|
|
324
|
+
answer = this.extractRelevantSentence(para, this.answerSignals);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!situation && paragraphs.length > 0) {
|
|
329
|
+
situation = this.truncateToSentence(paragraphs[0] ?? "", 150);
|
|
330
|
+
}
|
|
331
|
+
if (!answer && paragraphs.length > 1) {
|
|
332
|
+
const lastPara = paragraphs[paragraphs.length - 1];
|
|
333
|
+
answer = lastPara ? this.truncateToSentence(lastPara, 150) : "";
|
|
334
|
+
}
|
|
335
|
+
return { situation, complication, question, answer };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Extract Sparkline structure (Nancy Duarte).
|
|
339
|
+
*/
|
|
340
|
+
extractSparkline(paragraphs) {
|
|
341
|
+
const whatIs = [];
|
|
342
|
+
const whatCouldBe = [];
|
|
343
|
+
let callToAdventure = "";
|
|
344
|
+
for (const para of paragraphs) {
|
|
345
|
+
const lowerPara = para.toLowerCase();
|
|
346
|
+
if (this.containsSignals(lowerPara, this.whatIsSignals)) {
|
|
347
|
+
whatIs.push(this.truncateToSentence(para, 100));
|
|
348
|
+
}
|
|
349
|
+
if (this.containsSignals(lowerPara, this.whatCouldBeSignals)) {
|
|
350
|
+
whatCouldBe.push(this.truncateToSentence(para, 100));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const para of paragraphs.slice(-2)) {
|
|
354
|
+
if (this.containsSignals(para.toLowerCase(), ["join", "together", "action", "start", "begin", "now"])) {
|
|
355
|
+
callToAdventure = this.truncateToSentence(para, 150);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { whatIs, whatCouldBe, callToAdventure };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Extract key messages (max 3 - Rule of Three).
|
|
363
|
+
*/
|
|
364
|
+
extractKeyMessages(text, sentences) {
|
|
365
|
+
const messages = [];
|
|
366
|
+
const emphasisMatches = text.match(/\[EMPHASIS\](.+?)\[\/EMPHASIS\]/g);
|
|
367
|
+
if (emphasisMatches) {
|
|
368
|
+
for (const match of emphasisMatches.slice(0, 3)) {
|
|
369
|
+
const content = match.replace(/\[EMPHASIS\]|\[\/EMPHASIS\]/g, "").trim();
|
|
370
|
+
if (content.length > 10 && content.length < 100) {
|
|
371
|
+
messages.push(content);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const headerMatches = text.match(/\[HEADER\](.+)/g);
|
|
376
|
+
if (headerMatches && messages.length < 3) {
|
|
377
|
+
for (const match of headerMatches.slice(0, 3 - messages.length)) {
|
|
378
|
+
const content = match.replace("[HEADER]", "").trim();
|
|
379
|
+
if (content.length > 5 && content.length < 80) {
|
|
380
|
+
messages.push(content);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (messages.length < 3) {
|
|
385
|
+
const strongSentences = sentences.filter((s) => s.length > 20 && s.length < 100).filter((s) => this.containsSignals(s.toLowerCase(), ["key", "important", "critical", "essential", "must", "need"]));
|
|
386
|
+
for (const sentence of strongSentences.slice(0, 3 - messages.length)) {
|
|
387
|
+
messages.push(sentence);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (messages.length < 3) {
|
|
391
|
+
for (const sentence of sentences) {
|
|
392
|
+
if (sentence.length > 30 && sentence.length < 100 && !messages.includes(sentence)) {
|
|
393
|
+
messages.push(sentence);
|
|
394
|
+
if (messages.length >= 3) break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return messages.slice(0, 3);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Generate action titles (McKinsey-style).
|
|
402
|
+
*/
|
|
403
|
+
generateActionTitles(keyMessages, paragraphs) {
|
|
404
|
+
const titles = [];
|
|
405
|
+
for (const message of keyMessages) {
|
|
406
|
+
const actionTitle = this.transformToActionTitle(message);
|
|
407
|
+
if (actionTitle) {
|
|
408
|
+
titles.push(actionTitle);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const para of paragraphs) {
|
|
412
|
+
if (titles.length >= 10) break;
|
|
413
|
+
const headerMatch = para.match(/\[HEADER\]\s*(.+)/);
|
|
414
|
+
if (headerMatch?.[1]) {
|
|
415
|
+
const title = this.transformToActionTitle(headerMatch[1]);
|
|
416
|
+
if (title && !titles.includes(title)) {
|
|
417
|
+
titles.push(title);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return titles;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Transform a statement into an action title.
|
|
425
|
+
*/
|
|
426
|
+
transformToActionTitle(statement) {
|
|
427
|
+
let title = statement.replace(/\[(HEADER|EMPHASIS|BULLET|NUMBERED)\]/g, "").trim();
|
|
428
|
+
const actionVerbs = ["increase", "decrease", "improve", "reduce", "achieve", "deliver", "create", "build", "launch", "transform", "enable", "drive"];
|
|
429
|
+
const firstWord = title.split(" ")[0]?.toLowerCase();
|
|
430
|
+
if (firstWord && actionVerbs.includes(firstWord)) {
|
|
431
|
+
return this.capitalizeFirst(title);
|
|
432
|
+
}
|
|
433
|
+
if (title.toLowerCase().includes("should")) {
|
|
434
|
+
title = title.replace(/we should|you should|should/gi, "").trim();
|
|
435
|
+
return this.capitalizeFirst(title);
|
|
436
|
+
}
|
|
437
|
+
if (title.toLowerCase().includes("need to")) {
|
|
438
|
+
title = title.replace(/we need to|you need to|need to/gi, "").trim();
|
|
439
|
+
return this.capitalizeFirst(title);
|
|
440
|
+
}
|
|
441
|
+
if (title.length < 50) {
|
|
442
|
+
return title;
|
|
443
|
+
}
|
|
444
|
+
return this.truncateToWords(title, 8);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Identify STAR moments (Something They'll Always Remember).
|
|
448
|
+
*/
|
|
449
|
+
identifyStarMoments(paragraphs) {
|
|
450
|
+
const starMoments = [];
|
|
451
|
+
const starSignals = [
|
|
452
|
+
"surprising",
|
|
453
|
+
"amazing",
|
|
454
|
+
"incredible",
|
|
455
|
+
"remarkable",
|
|
456
|
+
"stunning",
|
|
457
|
+
"imagine",
|
|
458
|
+
"what if",
|
|
459
|
+
"breakthrough",
|
|
460
|
+
"revolutionary",
|
|
461
|
+
"never before",
|
|
462
|
+
"first time",
|
|
463
|
+
"unprecedented",
|
|
464
|
+
"game-changing",
|
|
465
|
+
"dramatic"
|
|
466
|
+
];
|
|
467
|
+
for (const para of paragraphs) {
|
|
468
|
+
if (this.containsSignals(para.toLowerCase(), starSignals)) {
|
|
469
|
+
const moment = this.truncateToSentence(para, 120);
|
|
470
|
+
if (moment.length > 20) {
|
|
471
|
+
starMoments.push(moment);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const statPattern = /\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion|trillion))?|\d+(?:\s*(?:million|billion|trillion))/g;
|
|
476
|
+
for (const para of paragraphs) {
|
|
477
|
+
if (statPattern.test(para) && starMoments.length < 5) {
|
|
478
|
+
const moment = this.truncateToSentence(para, 100);
|
|
479
|
+
if (!starMoments.includes(moment)) {
|
|
480
|
+
starMoments.push(moment);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return starMoments.slice(0, 5);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Estimate slide count based on content.
|
|
488
|
+
*/
|
|
489
|
+
estimateSlideCount(text, paragraphs) {
|
|
490
|
+
const wordCount = text.split(/\s+/).length;
|
|
491
|
+
const headerCount = (text.match(/\[HEADER\]/g) ?? []).length;
|
|
492
|
+
const bulletGroups = (text.match(/\[BULLET\]/g) ?? []).length / 4;
|
|
493
|
+
const wordBasedEstimate = Math.ceil(wordCount / 35);
|
|
494
|
+
const estimate = Math.max(
|
|
495
|
+
5,
|
|
496
|
+
// Minimum 5 slides
|
|
497
|
+
Math.ceil((wordBasedEstimate + headerCount + bulletGroups) / 2)
|
|
498
|
+
);
|
|
499
|
+
return Math.min(estimate, 30);
|
|
500
|
+
}
|
|
501
|
+
// === Helper Methods ===
|
|
502
|
+
containsSignals(text, signals) {
|
|
503
|
+
const lowerText = text.toLowerCase();
|
|
504
|
+
return signals.some((signal) => lowerText.includes(signal));
|
|
505
|
+
}
|
|
506
|
+
extractRelevantSentence(paragraph, signals) {
|
|
507
|
+
const sentences = paragraph.split(/[.!?]+/);
|
|
508
|
+
for (const sentence of sentences) {
|
|
509
|
+
if (this.containsSignals(sentence.toLowerCase(), signals)) {
|
|
510
|
+
return sentence.trim();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return this.truncateToSentence(paragraph, 150);
|
|
514
|
+
}
|
|
515
|
+
truncateToSentence(text, maxLength) {
|
|
516
|
+
if (text.length <= maxLength) {
|
|
517
|
+
return text.trim();
|
|
518
|
+
}
|
|
519
|
+
const truncated = text.slice(0, maxLength);
|
|
520
|
+
const lastPeriod = truncated.lastIndexOf(".");
|
|
521
|
+
const lastQuestion = truncated.lastIndexOf("?");
|
|
522
|
+
const lastExclaim = truncated.lastIndexOf("!");
|
|
523
|
+
const lastBoundary = Math.max(lastPeriod, lastQuestion, lastExclaim);
|
|
524
|
+
if (lastBoundary > maxLength * 0.5) {
|
|
525
|
+
return text.slice(0, lastBoundary + 1).trim();
|
|
526
|
+
}
|
|
527
|
+
return truncated.trim() + "...";
|
|
528
|
+
}
|
|
529
|
+
truncateToWords(text, maxWords) {
|
|
530
|
+
const words = text.split(/\s+/);
|
|
531
|
+
if (words.length <= maxWords) {
|
|
532
|
+
return text;
|
|
533
|
+
}
|
|
534
|
+
return words.slice(0, maxWords).join(" ");
|
|
535
|
+
}
|
|
536
|
+
capitalizeFirst(text) {
|
|
537
|
+
if (!text) return "";
|
|
538
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/core/SlideFactory.ts
|
|
543
|
+
var SlideFactory = class {
|
|
544
|
+
templates;
|
|
545
|
+
constructor() {
|
|
546
|
+
this.templates = this.initializeTemplates();
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Create slides from analyzed content.
|
|
550
|
+
*/
|
|
551
|
+
async createSlides(analysis, mode) {
|
|
552
|
+
const slides = [];
|
|
553
|
+
let slideIndex = 0;
|
|
554
|
+
slides.push(this.createTitleSlide(slideIndex++, analysis));
|
|
555
|
+
if (mode === "business" && analysis.keyMessages.length >= 2) {
|
|
556
|
+
slides.push(this.createAgendaSlide(slideIndex++, analysis));
|
|
557
|
+
}
|
|
558
|
+
if (analysis.scqa.situation) {
|
|
559
|
+
slides.push(this.createContextSlide(slideIndex++, analysis, mode));
|
|
560
|
+
}
|
|
561
|
+
if (analysis.scqa.complication) {
|
|
562
|
+
slides.push(this.createProblemSlide(slideIndex++, analysis, mode));
|
|
563
|
+
}
|
|
564
|
+
for (const message of analysis.keyMessages) {
|
|
565
|
+
slides.push(this.createMessageSlide(slideIndex++, message, mode));
|
|
566
|
+
}
|
|
567
|
+
for (const starMoment of analysis.starMoments.slice(0, 2)) {
|
|
568
|
+
slides.push(this.createStarMomentSlide(slideIndex++, starMoment, mode));
|
|
569
|
+
}
|
|
570
|
+
if (analysis.scqa.answer) {
|
|
571
|
+
slides.push(this.createSolutionSlide(slideIndex++, analysis, mode));
|
|
572
|
+
}
|
|
573
|
+
if (analysis.sparkline.callToAdventure) {
|
|
574
|
+
slides.push(this.createCTASlide(slideIndex++, analysis, mode));
|
|
575
|
+
}
|
|
576
|
+
slides.push(this.createThankYouSlide(slideIndex++));
|
|
577
|
+
return slides;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Create a title slide.
|
|
581
|
+
*/
|
|
582
|
+
createTitleSlide(index, analysis) {
|
|
583
|
+
const subtitle = analysis.keyMessages[0] ?? analysis.scqa.answer ?? "";
|
|
584
|
+
return {
|
|
585
|
+
index,
|
|
586
|
+
type: "title",
|
|
587
|
+
data: {
|
|
588
|
+
title: analysis.titles[0] ?? "Presentation",
|
|
589
|
+
subtitle: this.truncate(subtitle, 60),
|
|
590
|
+
keyMessage: analysis.scqa.answer
|
|
591
|
+
},
|
|
592
|
+
classes: ["slide-title"]
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Create an agenda slide.
|
|
597
|
+
*/
|
|
598
|
+
createAgendaSlide(index, analysis) {
|
|
599
|
+
return {
|
|
600
|
+
index,
|
|
601
|
+
type: "agenda",
|
|
602
|
+
data: {
|
|
603
|
+
title: "Agenda",
|
|
604
|
+
bullets: analysis.keyMessages.map((msg, i) => `${i + 1}. ${this.truncate(msg, 50)}`)
|
|
605
|
+
},
|
|
606
|
+
classes: ["slide-agenda"]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Create a context/situation slide.
|
|
611
|
+
*/
|
|
612
|
+
createContextSlide(index, analysis, mode) {
|
|
613
|
+
if (mode === "keynote") {
|
|
614
|
+
return {
|
|
615
|
+
index,
|
|
616
|
+
type: "single-statement",
|
|
617
|
+
data: {
|
|
618
|
+
title: this.truncate(analysis.scqa.situation, 80),
|
|
619
|
+
keyMessage: "The current state"
|
|
620
|
+
},
|
|
621
|
+
classes: ["slide-single-statement"]
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
index,
|
|
626
|
+
type: "two-column",
|
|
627
|
+
data: {
|
|
628
|
+
title: "Current Situation",
|
|
629
|
+
body: analysis.scqa.situation,
|
|
630
|
+
bullets: analysis.sparkline.whatIs.slice(0, 3)
|
|
631
|
+
},
|
|
632
|
+
classes: ["slide-two-column"]
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Create a problem/complication slide.
|
|
637
|
+
*/
|
|
638
|
+
createProblemSlide(index, analysis, mode) {
|
|
639
|
+
if (mode === "keynote") {
|
|
640
|
+
return {
|
|
641
|
+
index,
|
|
642
|
+
type: "big-idea",
|
|
643
|
+
data: {
|
|
644
|
+
title: this.truncate(analysis.scqa.complication, 60),
|
|
645
|
+
keyMessage: "The challenge we face"
|
|
646
|
+
},
|
|
647
|
+
classes: ["slide-big-idea"]
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
index,
|
|
652
|
+
type: "bullet-points",
|
|
653
|
+
data: {
|
|
654
|
+
title: "The Challenge",
|
|
655
|
+
body: analysis.scqa.complication,
|
|
656
|
+
bullets: this.extractBullets(analysis.scqa.complication)
|
|
657
|
+
},
|
|
658
|
+
classes: ["slide-bullet-points"]
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Create a key message slide.
|
|
663
|
+
*/
|
|
664
|
+
createMessageSlide(index, message, mode) {
|
|
665
|
+
if (mode === "keynote") {
|
|
666
|
+
return {
|
|
667
|
+
index,
|
|
668
|
+
type: "single-statement",
|
|
669
|
+
data: {
|
|
670
|
+
title: this.truncate(message, 60),
|
|
671
|
+
keyMessage: message
|
|
672
|
+
},
|
|
673
|
+
classes: ["slide-single-statement"]
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
index,
|
|
678
|
+
type: "bullet-points",
|
|
679
|
+
data: {
|
|
680
|
+
title: this.extractActionTitle(message),
|
|
681
|
+
body: message,
|
|
682
|
+
bullets: this.extractBullets(message)
|
|
683
|
+
},
|
|
684
|
+
classes: ["slide-bullet-points"]
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Create a STAR moment slide.
|
|
689
|
+
*/
|
|
690
|
+
createStarMomentSlide(index, starMoment, mode) {
|
|
691
|
+
const statMatch = starMoment.match(/(\d+[%xX]|\$[\d,]+(?:\s*(?:million|billion))?)/);
|
|
692
|
+
if (statMatch && statMatch[1]) {
|
|
693
|
+
const stat = statMatch[1];
|
|
694
|
+
return {
|
|
695
|
+
index,
|
|
696
|
+
type: "big-number",
|
|
697
|
+
data: {
|
|
698
|
+
title: stat,
|
|
699
|
+
subtitle: this.removeStatistic(starMoment, stat),
|
|
700
|
+
keyMessage: starMoment
|
|
701
|
+
},
|
|
702
|
+
classes: ["slide-big-number"]
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
if (mode === "keynote") {
|
|
706
|
+
return {
|
|
707
|
+
index,
|
|
708
|
+
type: "big-idea",
|
|
709
|
+
data: {
|
|
710
|
+
title: this.truncate(starMoment, 80),
|
|
711
|
+
keyMessage: "A key insight"
|
|
712
|
+
},
|
|
713
|
+
classes: ["slide-big-idea"]
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
index,
|
|
718
|
+
type: "quote",
|
|
719
|
+
data: {
|
|
720
|
+
quote: starMoment,
|
|
721
|
+
attribution: "Key Insight"
|
|
722
|
+
},
|
|
723
|
+
classes: ["slide-quote"]
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Create a solution/answer slide.
|
|
728
|
+
*/
|
|
729
|
+
createSolutionSlide(index, analysis, mode) {
|
|
730
|
+
if (mode === "keynote") {
|
|
731
|
+
return {
|
|
732
|
+
index,
|
|
733
|
+
type: "big-idea",
|
|
734
|
+
data: {
|
|
735
|
+
title: this.truncate(analysis.scqa.answer, 60),
|
|
736
|
+
keyMessage: "Our answer"
|
|
737
|
+
},
|
|
738
|
+
classes: ["slide-big-idea"]
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
index,
|
|
743
|
+
type: "two-column",
|
|
744
|
+
data: {
|
|
745
|
+
title: "The Solution",
|
|
746
|
+
body: analysis.scqa.answer,
|
|
747
|
+
bullets: analysis.sparkline.whatCouldBe.slice(0, 4)
|
|
748
|
+
},
|
|
749
|
+
classes: ["slide-two-column"]
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Create a call-to-action slide.
|
|
754
|
+
*/
|
|
755
|
+
createCTASlide(index, analysis, mode) {
|
|
756
|
+
return {
|
|
757
|
+
index,
|
|
758
|
+
type: "cta",
|
|
759
|
+
data: {
|
|
760
|
+
title: mode === "keynote" ? "Take Action" : "Next Steps",
|
|
761
|
+
body: analysis.sparkline.callToAdventure,
|
|
762
|
+
keyMessage: "What we need from you"
|
|
763
|
+
},
|
|
764
|
+
classes: ["slide-cta"]
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Create a thank you slide.
|
|
769
|
+
*/
|
|
770
|
+
createThankYouSlide(index) {
|
|
771
|
+
return {
|
|
772
|
+
index,
|
|
773
|
+
type: "thank-you",
|
|
774
|
+
data: {
|
|
775
|
+
title: "Thank You",
|
|
776
|
+
subtitle: "Questions?"
|
|
777
|
+
},
|
|
778
|
+
classes: ["slide-thank-you"]
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Initialize slide templates with constraints.
|
|
783
|
+
*/
|
|
784
|
+
initializeTemplates() {
|
|
785
|
+
const templates = /* @__PURE__ */ new Map();
|
|
786
|
+
templates.set("title", {
|
|
787
|
+
type: "title",
|
|
788
|
+
requiredFields: ["title"],
|
|
789
|
+
optionalFields: ["subtitle", "author", "date"],
|
|
790
|
+
keynoteSuitable: true,
|
|
791
|
+
businessSuitable: true,
|
|
792
|
+
maxWords: 15
|
|
793
|
+
});
|
|
794
|
+
templates.set("big-idea", {
|
|
795
|
+
type: "big-idea",
|
|
796
|
+
requiredFields: ["title"],
|
|
797
|
+
optionalFields: ["keyMessage"],
|
|
798
|
+
keynoteSuitable: true,
|
|
799
|
+
businessSuitable: false,
|
|
800
|
+
maxWords: 10
|
|
801
|
+
});
|
|
802
|
+
templates.set("single-statement", {
|
|
803
|
+
type: "single-statement",
|
|
804
|
+
requiredFields: ["title"],
|
|
805
|
+
optionalFields: ["keyMessage"],
|
|
806
|
+
keynoteSuitable: true,
|
|
807
|
+
businessSuitable: false,
|
|
808
|
+
maxWords: 15
|
|
809
|
+
});
|
|
810
|
+
templates.set("big-number", {
|
|
811
|
+
type: "big-number",
|
|
812
|
+
requiredFields: ["title"],
|
|
813
|
+
optionalFields: ["subtitle", "source"],
|
|
814
|
+
keynoteSuitable: true,
|
|
815
|
+
businessSuitable: true,
|
|
816
|
+
maxWords: 10
|
|
817
|
+
});
|
|
818
|
+
templates.set("quote", {
|
|
819
|
+
type: "quote",
|
|
820
|
+
requiredFields: ["quote"],
|
|
821
|
+
optionalFields: ["attribution", "source"],
|
|
822
|
+
keynoteSuitable: true,
|
|
823
|
+
businessSuitable: true,
|
|
824
|
+
maxWords: 30
|
|
825
|
+
});
|
|
826
|
+
templates.set("bullet-points", {
|
|
827
|
+
type: "bullet-points",
|
|
828
|
+
requiredFields: ["title", "bullets"],
|
|
829
|
+
optionalFields: ["body"],
|
|
830
|
+
keynoteSuitable: false,
|
|
831
|
+
businessSuitable: true,
|
|
832
|
+
maxWords: 80
|
|
833
|
+
});
|
|
834
|
+
templates.set("two-column", {
|
|
835
|
+
type: "two-column",
|
|
836
|
+
requiredFields: ["title"],
|
|
837
|
+
optionalFields: ["body", "bullets", "images"],
|
|
838
|
+
keynoteSuitable: false,
|
|
839
|
+
businessSuitable: true,
|
|
840
|
+
maxWords: 100
|
|
841
|
+
});
|
|
842
|
+
templates.set("agenda", {
|
|
843
|
+
type: "agenda",
|
|
844
|
+
requiredFields: ["title", "bullets"],
|
|
845
|
+
optionalFields: [],
|
|
846
|
+
keynoteSuitable: false,
|
|
847
|
+
businessSuitable: true,
|
|
848
|
+
maxWords: 50
|
|
849
|
+
});
|
|
850
|
+
templates.set("cta", {
|
|
851
|
+
type: "cta",
|
|
852
|
+
requiredFields: ["title"],
|
|
853
|
+
optionalFields: ["body", "keyMessage"],
|
|
854
|
+
keynoteSuitable: true,
|
|
855
|
+
businessSuitable: true,
|
|
856
|
+
maxWords: 30
|
|
857
|
+
});
|
|
858
|
+
templates.set("thank-you", {
|
|
859
|
+
type: "thank-you",
|
|
860
|
+
requiredFields: ["title"],
|
|
861
|
+
optionalFields: ["subtitle"],
|
|
862
|
+
keynoteSuitable: true,
|
|
863
|
+
businessSuitable: true,
|
|
864
|
+
maxWords: 10
|
|
865
|
+
});
|
|
866
|
+
return templates;
|
|
867
|
+
}
|
|
868
|
+
// === Helper Methods ===
|
|
869
|
+
/**
|
|
870
|
+
* Truncate text to max length at word boundary.
|
|
871
|
+
*/
|
|
872
|
+
truncate(text, maxLength) {
|
|
873
|
+
if (!text || text.length <= maxLength) {
|
|
874
|
+
return text ?? "";
|
|
875
|
+
}
|
|
876
|
+
const truncated = text.slice(0, maxLength);
|
|
877
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
878
|
+
if (lastSpace > maxLength * 0.7) {
|
|
879
|
+
return truncated.slice(0, lastSpace) + "...";
|
|
880
|
+
}
|
|
881
|
+
return truncated + "...";
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Extract an action title from a message.
|
|
885
|
+
*/
|
|
886
|
+
extractActionTitle(message) {
|
|
887
|
+
const firstSentence = message.split(/[.!?]/)[0];
|
|
888
|
+
if (firstSentence && firstSentence.length <= 50) {
|
|
889
|
+
return firstSentence;
|
|
890
|
+
}
|
|
891
|
+
const words = message.split(/\s+/).slice(0, 6);
|
|
892
|
+
return words.join(" ");
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Extract bullet points from text.
|
|
896
|
+
*/
|
|
897
|
+
extractBullets(text) {
|
|
898
|
+
if (!text) return [];
|
|
899
|
+
const bulletMatches = text.match(/\[BULLET\]\s*(.+)/g);
|
|
900
|
+
if (bulletMatches && bulletMatches.length > 0) {
|
|
901
|
+
return bulletMatches.map((b) => b.replace("[BULLET]", "").trim()).slice(0, 5);
|
|
902
|
+
}
|
|
903
|
+
const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 10);
|
|
904
|
+
return sentences.slice(0, 5).map((s) => s.trim());
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Remove a statistic from text.
|
|
908
|
+
*/
|
|
909
|
+
removeStatistic(text, stat) {
|
|
910
|
+
return text.replace(stat, "").replace(/^\s*[-–—:,]\s*/, "").trim();
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// src/core/TemplateEngine.ts
|
|
915
|
+
var import_handlebars = __toESM(require("handlebars"));
|
|
916
|
+
var TemplateEngine = class {
|
|
917
|
+
handlebars;
|
|
918
|
+
templates;
|
|
919
|
+
partials;
|
|
920
|
+
constructor() {
|
|
921
|
+
this.handlebars = import_handlebars.default.create();
|
|
922
|
+
this.templates = /* @__PURE__ */ new Map();
|
|
923
|
+
this.partials = /* @__PURE__ */ new Map();
|
|
924
|
+
this.registerHelpers();
|
|
925
|
+
this.registerPartials();
|
|
926
|
+
this.compileTemplates();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Render a slide to HTML.
|
|
930
|
+
*/
|
|
931
|
+
render(slide, config) {
|
|
932
|
+
if (config?.customTemplates?.[slide.type]) {
|
|
933
|
+
const customTemplate = this.handlebars.compile(config.customTemplates[slide.type]);
|
|
934
|
+
return customTemplate(this.prepareContext(slide, config));
|
|
935
|
+
}
|
|
936
|
+
const template = this.templates.get(slide.type);
|
|
937
|
+
if (!template) {
|
|
938
|
+
console.warn(`No template for slide type: ${slide.type}, using fallback`);
|
|
939
|
+
return this.renderFallback(slide);
|
|
940
|
+
}
|
|
941
|
+
return template(this.prepareContext(slide, config));
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Render multiple slides.
|
|
945
|
+
*/
|
|
946
|
+
renderAll(slides, config) {
|
|
947
|
+
return slides.map((slide) => this.render(slide, config));
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Prepare template context with computed properties.
|
|
951
|
+
*/
|
|
952
|
+
prepareContext(slide, config) {
|
|
953
|
+
return {
|
|
954
|
+
...slide.data,
|
|
955
|
+
slideIndex: slide.index,
|
|
956
|
+
slideType: slide.type,
|
|
957
|
+
classes: this.buildClassList(slide, config?.theme),
|
|
958
|
+
styles: this.buildStyleString(slide),
|
|
959
|
+
hasImage: slide.data.images && slide.data.images.length > 0,
|
|
960
|
+
hasMetrics: slide.data.metrics && slide.data.metrics.length > 0,
|
|
961
|
+
hasBullets: slide.data.bullets && slide.data.bullets.length > 0,
|
|
962
|
+
bulletCount: slide.data.bullets?.length ?? 0,
|
|
963
|
+
theme: config?.theme ?? "default"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Build CSS class list for slide.
|
|
968
|
+
*/
|
|
969
|
+
buildClassList(slide, theme) {
|
|
970
|
+
const classes = [
|
|
971
|
+
"slide",
|
|
972
|
+
`slide-${slide.type}`,
|
|
973
|
+
...slide.classes ?? []
|
|
974
|
+
];
|
|
975
|
+
if (theme && theme !== "default") {
|
|
976
|
+
classes.push(`theme-${theme}`);
|
|
977
|
+
}
|
|
978
|
+
return classes.join(" ");
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Build inline style string.
|
|
982
|
+
*/
|
|
983
|
+
buildStyleString(slide) {
|
|
984
|
+
if (!slide.styles) return "";
|
|
985
|
+
return Object.entries(slide.styles).map(([prop, value]) => `${this.kebabCase(prop)}: ${value}`).join("; ");
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Register Handlebars helpers.
|
|
989
|
+
*/
|
|
990
|
+
registerHelpers() {
|
|
991
|
+
this.handlebars.registerHelper("ifEquals", function(arg1, arg2, options) {
|
|
992
|
+
return arg1 === arg2 ? options.fn(this) : options.inverse(this);
|
|
993
|
+
});
|
|
994
|
+
this.handlebars.registerHelper("eachWithIndex", function(context, options) {
|
|
995
|
+
let result = "";
|
|
996
|
+
for (let i = 0; i < context.length; i++) {
|
|
997
|
+
const item = context[i] || {};
|
|
998
|
+
result += options.fn({ ...item, index: i, first: i === 0, last: i === context.length - 1 });
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
});
|
|
1002
|
+
this.handlebars.registerHelper("truncate", function(text, length) {
|
|
1003
|
+
if (!text || text.length <= length) return text;
|
|
1004
|
+
return text.slice(0, length) + "...";
|
|
1005
|
+
});
|
|
1006
|
+
this.handlebars.registerHelper("formatNumber", function(num) {
|
|
1007
|
+
const n = typeof num === "string" ? parseFloat(num) : num;
|
|
1008
|
+
return n.toLocaleString();
|
|
1009
|
+
});
|
|
1010
|
+
this.handlebars.registerHelper("trendIcon", function(trend) {
|
|
1011
|
+
switch (trend) {
|
|
1012
|
+
case "up":
|
|
1013
|
+
return "\u2191";
|
|
1014
|
+
case "down":
|
|
1015
|
+
return "\u2193";
|
|
1016
|
+
default:
|
|
1017
|
+
return "\u2192";
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
this.handlebars.registerHelper("animDelay", function(index, baseMs = 100) {
|
|
1021
|
+
return `animation-delay: ${index * baseMs}ms`;
|
|
1022
|
+
});
|
|
1023
|
+
this.handlebars.registerHelper("safeHTML", function(text) {
|
|
1024
|
+
return new import_handlebars.default.SafeString(text);
|
|
1025
|
+
});
|
|
1026
|
+
this.handlebars.registerHelper("markdown", function(text) {
|
|
1027
|
+
if (!text) return "";
|
|
1028
|
+
let html = text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/`(.+?)`/g, "<code>$1</code>");
|
|
1029
|
+
return new import_handlebars.default.SafeString(html);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Register reusable partials.
|
|
1034
|
+
*/
|
|
1035
|
+
registerPartials() {
|
|
1036
|
+
this.handlebars.registerPartial("bulletList", `
|
|
1037
|
+
<ul class="bullets{{#if staggered}} stagger-children{{/if}}">
|
|
1038
|
+
{{#each bullets}}
|
|
1039
|
+
<li class="bullet animate-fadeIn" style="{{animDelay @index 150}}">
|
|
1040
|
+
{{markdown this}}
|
|
1041
|
+
</li>
|
|
1042
|
+
{{/each}}
|
|
1043
|
+
</ul>
|
|
1044
|
+
`);
|
|
1045
|
+
this.handlebars.registerPartial("metricsGrid", `
|
|
1046
|
+
<div class="metrics-grid">
|
|
1047
|
+
{{#each metrics}}
|
|
1048
|
+
<div class="metric animate-fadeIn" style="{{animDelay @index 200}}">
|
|
1049
|
+
<div class="metric-value">{{value}}</div>
|
|
1050
|
+
<div class="metric-label">{{label}}</div>
|
|
1051
|
+
{{#if change}}
|
|
1052
|
+
<div class="metric-change {{trend}}">
|
|
1053
|
+
{{trendIcon trend}} {{change}}
|
|
1054
|
+
</div>
|
|
1055
|
+
{{/if}}
|
|
1056
|
+
</div>
|
|
1057
|
+
{{/each}}
|
|
1058
|
+
</div>
|
|
1059
|
+
`);
|
|
1060
|
+
this.handlebars.registerPartial("imageWithCaption", `
|
|
1061
|
+
<figure class="image-container">
|
|
1062
|
+
<img src="{{src}}" alt="{{alt}}" class="slide-image" loading="lazy">
|
|
1063
|
+
{{#if caption}}
|
|
1064
|
+
<figcaption class="caption">{{caption}}</figcaption>
|
|
1065
|
+
{{/if}}
|
|
1066
|
+
</figure>
|
|
1067
|
+
`);
|
|
1068
|
+
this.handlebars.registerPartial("source", `
|
|
1069
|
+
{{#if source}}
|
|
1070
|
+
<div class="source">Source: {{source}}</div>
|
|
1071
|
+
{{/if}}
|
|
1072
|
+
`);
|
|
1073
|
+
this.handlebars.registerPartial("speakerNotes", `
|
|
1074
|
+
{{#if notes}}
|
|
1075
|
+
<aside class="notes">
|
|
1076
|
+
{{notes}}
|
|
1077
|
+
</aside>
|
|
1078
|
+
{{/if}}
|
|
1079
|
+
`);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Compile built-in templates.
|
|
1083
|
+
*/
|
|
1084
|
+
compileTemplates() {
|
|
1085
|
+
this.templates.set("title", this.handlebars.compile(`
|
|
1086
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1087
|
+
<div class="slide-content title-content">
|
|
1088
|
+
<h1 class="title animate-fadeIn">{{title}}</h1>
|
|
1089
|
+
{{#if subtitle}}
|
|
1090
|
+
<p class="subtitle animate-fadeIn delay-200">{{subtitle}}</p>
|
|
1091
|
+
{{/if}}
|
|
1092
|
+
{{#if author}}
|
|
1093
|
+
<p class="author animate-fadeIn delay-400">{{author}}</p>
|
|
1094
|
+
{{/if}}
|
|
1095
|
+
</div>
|
|
1096
|
+
{{> speakerNotes}}
|
|
1097
|
+
</section>
|
|
1098
|
+
`));
|
|
1099
|
+
this.templates.set("agenda", this.handlebars.compile(`
|
|
1100
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1101
|
+
<div class="slide-content">
|
|
1102
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1103
|
+
{{> bulletList staggered=true}}
|
|
1104
|
+
</div>
|
|
1105
|
+
{{> speakerNotes}}
|
|
1106
|
+
</section>
|
|
1107
|
+
`));
|
|
1108
|
+
this.templates.set("section-divider", this.handlebars.compile(`
|
|
1109
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1110
|
+
<div class="slide-content section-divider-content">
|
|
1111
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1112
|
+
{{#if subtitle}}
|
|
1113
|
+
<p class="subtitle animate-fadeIn delay-200">{{subtitle}}</p>
|
|
1114
|
+
{{/if}}
|
|
1115
|
+
</div>
|
|
1116
|
+
{{> speakerNotes}}
|
|
1117
|
+
</section>
|
|
1118
|
+
`));
|
|
1119
|
+
this.templates.set("big-idea", this.handlebars.compile(`
|
|
1120
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1121
|
+
<div class="slide-content big-idea-content">
|
|
1122
|
+
<h2 class="big-idea-text animate-fadeIn">{{title}}</h2>
|
|
1123
|
+
</div>
|
|
1124
|
+
{{> speakerNotes}}
|
|
1125
|
+
</section>
|
|
1126
|
+
`));
|
|
1127
|
+
this.templates.set("single-statement", this.handlebars.compile(`
|
|
1128
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1129
|
+
<div class="slide-content statement-content">
|
|
1130
|
+
<p class="statement animate-fadeIn">{{title}}</p>
|
|
1131
|
+
</div>
|
|
1132
|
+
{{> speakerNotes}}
|
|
1133
|
+
</section>
|
|
1134
|
+
`));
|
|
1135
|
+
this.templates.set("big-number", this.handlebars.compile(`
|
|
1136
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1137
|
+
<div class="slide-content big-number-content">
|
|
1138
|
+
<div class="number animate-zoomIn">{{title}}</div>
|
|
1139
|
+
{{#if subtitle}}
|
|
1140
|
+
<p class="number-context animate-fadeIn delay-300">{{subtitle}}</p>
|
|
1141
|
+
{{/if}}
|
|
1142
|
+
{{> source}}
|
|
1143
|
+
</div>
|
|
1144
|
+
{{> speakerNotes}}
|
|
1145
|
+
</section>
|
|
1146
|
+
`));
|
|
1147
|
+
this.templates.set("quote", this.handlebars.compile(`
|
|
1148
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1149
|
+
<div class="slide-content quote-content">
|
|
1150
|
+
<blockquote class="quote animate-fadeIn">
|
|
1151
|
+
<p>"{{quote}}"</p>
|
|
1152
|
+
{{#if attribution}}
|
|
1153
|
+
<cite class="attribution animate-fadeIn delay-300">\u2014 {{attribution}}</cite>
|
|
1154
|
+
{{/if}}
|
|
1155
|
+
</blockquote>
|
|
1156
|
+
{{> source}}
|
|
1157
|
+
</div>
|
|
1158
|
+
{{> speakerNotes}}
|
|
1159
|
+
</section>
|
|
1160
|
+
`));
|
|
1161
|
+
this.templates.set("full-image", this.handlebars.compile(`
|
|
1162
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}" data-background-image="{{images.[0].src}}">
|
|
1163
|
+
<div class="slide-content image-overlay-content">
|
|
1164
|
+
{{#if title}}
|
|
1165
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1166
|
+
{{/if}}
|
|
1167
|
+
{{#if caption}}
|
|
1168
|
+
<p class="caption animate-fadeIn delay-200">{{caption}}</p>
|
|
1169
|
+
{{/if}}
|
|
1170
|
+
</div>
|
|
1171
|
+
{{> speakerNotes}}
|
|
1172
|
+
</section>
|
|
1173
|
+
`));
|
|
1174
|
+
this.templates.set("bullet-points", this.handlebars.compile(`
|
|
1175
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1176
|
+
<div class="slide-content">
|
|
1177
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1178
|
+
{{#if body}}
|
|
1179
|
+
<p class="body animate-fadeIn delay-100">{{body}}</p>
|
|
1180
|
+
{{/if}}
|
|
1181
|
+
{{> bulletList staggered=true}}
|
|
1182
|
+
{{> source}}
|
|
1183
|
+
</div>
|
|
1184
|
+
{{> speakerNotes}}
|
|
1185
|
+
</section>
|
|
1186
|
+
`));
|
|
1187
|
+
this.templates.set("two-column", this.handlebars.compile(`
|
|
1188
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1189
|
+
<div class="slide-content">
|
|
1190
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1191
|
+
<div class="columns two-columns">
|
|
1192
|
+
<div class="column column-left animate-slideRight">
|
|
1193
|
+
{{#if body}}
|
|
1194
|
+
<p class="body">{{markdown body}}</p>
|
|
1195
|
+
{{/if}}
|
|
1196
|
+
{{#if hasBullets}}
|
|
1197
|
+
{{> bulletList}}
|
|
1198
|
+
{{/if}}
|
|
1199
|
+
</div>
|
|
1200
|
+
<div class="column column-right animate-slideLeft delay-200">
|
|
1201
|
+
{{#if hasImage}}
|
|
1202
|
+
{{> imageWithCaption images.[0]}}
|
|
1203
|
+
{{else if metrics}}
|
|
1204
|
+
{{> metricsGrid}}
|
|
1205
|
+
{{/if}}
|
|
1206
|
+
</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
{{> source}}
|
|
1209
|
+
</div>
|
|
1210
|
+
{{> speakerNotes}}
|
|
1211
|
+
</section>
|
|
1212
|
+
`));
|
|
1213
|
+
this.templates.set("three-column", this.handlebars.compile(`
|
|
1214
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1215
|
+
<div class="slide-content">
|
|
1216
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1217
|
+
<div class="columns three-columns stagger-children">
|
|
1218
|
+
{{#each columns}}
|
|
1219
|
+
<div class="column animate-fadeIn" style="{{animDelay @index 200}}">
|
|
1220
|
+
{{#if title}}<h3 class="column-title">{{title}}</h3>{{/if}}
|
|
1221
|
+
{{#if body}}<p class="column-body">{{markdown body}}</p>{{/if}}
|
|
1222
|
+
{{#if icon}}<div class="column-icon">{{icon}}</div>{{/if}}
|
|
1223
|
+
</div>
|
|
1224
|
+
{{/each}}
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
{{> speakerNotes}}
|
|
1228
|
+
</section>
|
|
1229
|
+
`));
|
|
1230
|
+
this.templates.set("comparison", this.handlebars.compile(`
|
|
1231
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1232
|
+
<div class="slide-content">
|
|
1233
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1234
|
+
<div class="comparison-container">
|
|
1235
|
+
<div class="comparison-left animate-slideRight">
|
|
1236
|
+
<h3>{{leftTitle}}</h3>
|
|
1237
|
+
<ul>
|
|
1238
|
+
{{#each leftItems}}
|
|
1239
|
+
<li>{{this}}</li>
|
|
1240
|
+
{{/each}}
|
|
1241
|
+
</ul>
|
|
1242
|
+
</div>
|
|
1243
|
+
<div class="comparison-divider"></div>
|
|
1244
|
+
<div class="comparison-right animate-slideLeft delay-200">
|
|
1245
|
+
<h3>{{rightTitle}}</h3>
|
|
1246
|
+
<ul>
|
|
1247
|
+
{{#each rightItems}}
|
|
1248
|
+
<li>{{this}}</li>
|
|
1249
|
+
{{/each}}
|
|
1250
|
+
</ul>
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
{{> speakerNotes}}
|
|
1255
|
+
</section>
|
|
1256
|
+
`));
|
|
1257
|
+
this.templates.set("metrics-grid", this.handlebars.compile(`
|
|
1258
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1259
|
+
<div class="slide-content">
|
|
1260
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1261
|
+
{{> metricsGrid}}
|
|
1262
|
+
{{> source}}
|
|
1263
|
+
</div>
|
|
1264
|
+
{{> speakerNotes}}
|
|
1265
|
+
</section>
|
|
1266
|
+
`));
|
|
1267
|
+
this.templates.set("screenshot", this.handlebars.compile(`
|
|
1268
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1269
|
+
<div class="slide-content screenshot-content">
|
|
1270
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1271
|
+
<div class="screenshot-container animate-fadeIn delay-200">
|
|
1272
|
+
{{> imageWithCaption images.[0]}}
|
|
1273
|
+
</div>
|
|
1274
|
+
</div>
|
|
1275
|
+
{{> speakerNotes}}
|
|
1276
|
+
</section>
|
|
1277
|
+
`));
|
|
1278
|
+
this.templates.set("screenshot-left", this.handlebars.compile(`
|
|
1279
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1280
|
+
<div class="slide-content">
|
|
1281
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1282
|
+
<div class="columns two-columns">
|
|
1283
|
+
<div class="column column-left animate-slideRight">
|
|
1284
|
+
{{> imageWithCaption images.[0]}}
|
|
1285
|
+
</div>
|
|
1286
|
+
<div class="column column-right animate-slideLeft delay-200">
|
|
1287
|
+
{{#if body}}<p class="body">{{markdown body}}</p>{{/if}}
|
|
1288
|
+
{{#if hasBullets}}{{> bulletList}}{{/if}}
|
|
1289
|
+
</div>
|
|
1290
|
+
</div>
|
|
1291
|
+
</div>
|
|
1292
|
+
{{> speakerNotes}}
|
|
1293
|
+
</section>
|
|
1294
|
+
`));
|
|
1295
|
+
this.templates.set("screenshot-right", this.handlebars.compile(`
|
|
1296
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1297
|
+
<div class="slide-content">
|
|
1298
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1299
|
+
<div class="columns two-columns">
|
|
1300
|
+
<div class="column column-left animate-slideRight">
|
|
1301
|
+
{{#if body}}<p class="body">{{markdown body}}</p>{{/if}}
|
|
1302
|
+
{{#if hasBullets}}{{> bulletList}}{{/if}}
|
|
1303
|
+
</div>
|
|
1304
|
+
<div class="column column-right animate-slideLeft delay-200">
|
|
1305
|
+
{{> imageWithCaption images.[0]}}
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
{{> speakerNotes}}
|
|
1310
|
+
</section>
|
|
1311
|
+
`));
|
|
1312
|
+
this.templates.set("timeline", this.handlebars.compile(`
|
|
1313
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1314
|
+
<div class="slide-content">
|
|
1315
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1316
|
+
<div class="timeline stagger-children">
|
|
1317
|
+
{{#each events}}
|
|
1318
|
+
<div class="timeline-item animate-fadeIn" style="{{animDelay @index 200}}">
|
|
1319
|
+
<div class="timeline-marker"></div>
|
|
1320
|
+
<div class="timeline-content">
|
|
1321
|
+
<div class="timeline-date">{{date}}</div>
|
|
1322
|
+
<div class="timeline-title">{{title}}</div>
|
|
1323
|
+
{{#if description}}<div class="timeline-desc">{{description}}</div>{{/if}}
|
|
1324
|
+
</div>
|
|
1325
|
+
</div>
|
|
1326
|
+
{{/each}}
|
|
1327
|
+
</div>
|
|
1328
|
+
</div>
|
|
1329
|
+
{{> speakerNotes}}
|
|
1330
|
+
</section>
|
|
1331
|
+
`));
|
|
1332
|
+
this.templates.set("process", this.handlebars.compile(`
|
|
1333
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1334
|
+
<div class="slide-content">
|
|
1335
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1336
|
+
<div class="process-steps stagger-children">
|
|
1337
|
+
{{#each steps}}
|
|
1338
|
+
<div class="process-step animate-fadeIn" style="{{animDelay @index 200}}">
|
|
1339
|
+
<div class="step-number">{{add @index 1}}</div>
|
|
1340
|
+
<div class="step-title">{{title}}</div>
|
|
1341
|
+
{{#if description}}<div class="step-desc">{{description}}</div>{{/if}}
|
|
1342
|
+
</div>
|
|
1343
|
+
{{#unless last}}
|
|
1344
|
+
<div class="step-arrow">\u2192</div>
|
|
1345
|
+
{{/unless}}
|
|
1346
|
+
{{/each}}
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
{{> speakerNotes}}
|
|
1350
|
+
</section>
|
|
1351
|
+
`));
|
|
1352
|
+
this.templates.set("social-proof", this.handlebars.compile(`
|
|
1353
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1354
|
+
<div class="slide-content">
|
|
1355
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1356
|
+
<div class="testimonials stagger-children">
|
|
1357
|
+
{{#each testimonials}}
|
|
1358
|
+
<div class="testimonial animate-fadeIn" style="{{animDelay @index 300}}">
|
|
1359
|
+
<blockquote>"{{quote}}"</blockquote>
|
|
1360
|
+
<div class="testimonial-author">
|
|
1361
|
+
{{#if avatar}}<img src="{{avatar}}" alt="{{name}}" class="author-avatar">{{/if}}
|
|
1362
|
+
<div class="author-info">
|
|
1363
|
+
<div class="author-name">{{name}}</div>
|
|
1364
|
+
{{#if role}}<div class="author-role">{{role}}</div>{{/if}}
|
|
1365
|
+
</div>
|
|
1366
|
+
</div>
|
|
1367
|
+
</div>
|
|
1368
|
+
{{/each}}
|
|
1369
|
+
</div>
|
|
1370
|
+
</div>
|
|
1371
|
+
{{> speakerNotes}}
|
|
1372
|
+
</section>
|
|
1373
|
+
`));
|
|
1374
|
+
this.templates.set("case-study", this.handlebars.compile(`
|
|
1375
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1376
|
+
<div class="slide-content">
|
|
1377
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1378
|
+
<div class="case-study-content">
|
|
1379
|
+
{{#if logo}}<img src="{{logo}}" alt="{{company}}" class="case-logo animate-fadeIn">{{/if}}
|
|
1380
|
+
<div class="case-details animate-fadeIn delay-200">
|
|
1381
|
+
<div class="case-challenge">
|
|
1382
|
+
<h3>Challenge</h3>
|
|
1383
|
+
<p>{{challenge}}</p>
|
|
1384
|
+
</div>
|
|
1385
|
+
<div class="case-solution">
|
|
1386
|
+
<h3>Solution</h3>
|
|
1387
|
+
<p>{{solution}}</p>
|
|
1388
|
+
</div>
|
|
1389
|
+
<div class="case-results">
|
|
1390
|
+
<h3>Results</h3>
|
|
1391
|
+
{{> metricsGrid metrics=results}}
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
</div>
|
|
1395
|
+
</div>
|
|
1396
|
+
{{> speakerNotes}}
|
|
1397
|
+
</section>
|
|
1398
|
+
`));
|
|
1399
|
+
this.templates.set("cta", this.handlebars.compile(`
|
|
1400
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1401
|
+
<div class="slide-content cta-content">
|
|
1402
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1403
|
+
{{#if body}}
|
|
1404
|
+
<p class="cta-message animate-fadeIn delay-200">{{body}}</p>
|
|
1405
|
+
{{/if}}
|
|
1406
|
+
{{#if actionText}}
|
|
1407
|
+
<div class="cta-button animate-bounceIn delay-400">{{actionText}}</div>
|
|
1408
|
+
{{/if}}
|
|
1409
|
+
</div>
|
|
1410
|
+
{{> speakerNotes}}
|
|
1411
|
+
</section>
|
|
1412
|
+
`));
|
|
1413
|
+
this.templates.set("thank-you", this.handlebars.compile(`
|
|
1414
|
+
<section class="{{classes}}" data-slide-index="{{slideIndex}}" style="{{styles}}">
|
|
1415
|
+
<div class="slide-content thank-you-content">
|
|
1416
|
+
<h2 class="title animate-fadeIn">{{title}}</h2>
|
|
1417
|
+
{{#if subtitle}}
|
|
1418
|
+
<p class="subtitle animate-fadeIn delay-300">{{subtitle}}</p>
|
|
1419
|
+
{{/if}}
|
|
1420
|
+
{{#if contact}}
|
|
1421
|
+
<div class="contact-info animate-fadeIn delay-500">
|
|
1422
|
+
{{contact}}
|
|
1423
|
+
</div>
|
|
1424
|
+
{{/if}}
|
|
1425
|
+
</div>
|
|
1426
|
+
{{> speakerNotes}}
|
|
1427
|
+
</section>
|
|
1428
|
+
`));
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Render fallback for unknown slide types.
|
|
1432
|
+
*/
|
|
1433
|
+
renderFallback(slide) {
|
|
1434
|
+
return `
|
|
1435
|
+
<section class="slide slide-${slide.type}" data-slide-index="${slide.index}">
|
|
1436
|
+
<div class="slide-content">
|
|
1437
|
+
${slide.data.title ? `<h2 class="title">${slide.data.title}</h2>` : ""}
|
|
1438
|
+
${slide.data.body ? `<p class="body">${slide.data.body}</p>` : ""}
|
|
1439
|
+
${slide.data.bullets ? `<ul>${slide.data.bullets.map((b) => `<li>${b}</li>`).join("")}</ul>` : ""}
|
|
1440
|
+
</div>
|
|
1441
|
+
</section>
|
|
1442
|
+
`;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Convert camelCase to kebab-case.
|
|
1446
|
+
*/
|
|
1447
|
+
kebabCase(str) {
|
|
1448
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
// src/core/ScoreCalculator.ts
|
|
1453
|
+
var ScoreCalculator = class {
|
|
1454
|
+
weights = {
|
|
1455
|
+
visual: 35,
|
|
1456
|
+
content: 30,
|
|
1457
|
+
expert: 25,
|
|
1458
|
+
accessibility: 10
|
|
1459
|
+
};
|
|
1460
|
+
/**
|
|
1461
|
+
* Calculate overall QA score from results.
|
|
1462
|
+
*/
|
|
1463
|
+
calculate(results) {
|
|
1464
|
+
const breakdown = this.getBreakdown(results);
|
|
1465
|
+
return Math.round(breakdown.total);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Get detailed score breakdown.
|
|
1469
|
+
*/
|
|
1470
|
+
getBreakdown(results) {
|
|
1471
|
+
const details = [];
|
|
1472
|
+
const visualScore = this.calculateVisualScore(results.visual, details);
|
|
1473
|
+
const contentScore = this.calculateContentScore(results.content, details);
|
|
1474
|
+
const expertScore = this.calculateExpertScore(results.expert, details);
|
|
1475
|
+
const accessibilityScore = this.calculateAccessibilityScore(results.accessibility, details);
|
|
1476
|
+
const penalties = this.calculatePenalties(results.issues);
|
|
1477
|
+
const rawTotal = visualScore * (this.weights.visual / 100) + contentScore * (this.weights.content / 100) + expertScore * (this.weights.expert / 100) + accessibilityScore * (this.weights.accessibility / 100);
|
|
1478
|
+
const total = Math.max(0, rawTotal - penalties);
|
|
1479
|
+
return {
|
|
1480
|
+
visual: visualScore,
|
|
1481
|
+
content: contentScore,
|
|
1482
|
+
expert: expertScore,
|
|
1483
|
+
accessibility: accessibilityScore,
|
|
1484
|
+
total,
|
|
1485
|
+
penalties,
|
|
1486
|
+
details
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Calculate visual quality score.
|
|
1491
|
+
*/
|
|
1492
|
+
calculateVisualScore(visual, details) {
|
|
1493
|
+
let score = 0;
|
|
1494
|
+
const maxScore = 100;
|
|
1495
|
+
const whitespaceTarget = 35;
|
|
1496
|
+
const whitespaceScore = Math.min(25, visual.whitespacePercentage / whitespaceTarget * 25);
|
|
1497
|
+
details.push({
|
|
1498
|
+
category: "visual",
|
|
1499
|
+
check: "whitespace",
|
|
1500
|
+
score: whitespaceScore,
|
|
1501
|
+
maxScore: 25,
|
|
1502
|
+
notes: `${visual.whitespacePercentage.toFixed(1)}% whitespace (target: ${whitespaceTarget}%+)`
|
|
1503
|
+
});
|
|
1504
|
+
score += whitespaceScore;
|
|
1505
|
+
const balanceScore = visual.layoutBalance * 25;
|
|
1506
|
+
details.push({
|
|
1507
|
+
category: "visual",
|
|
1508
|
+
check: "layout_balance",
|
|
1509
|
+
score: balanceScore,
|
|
1510
|
+
maxScore: 25,
|
|
1511
|
+
notes: `Balance score: ${(visual.layoutBalance * 100).toFixed(0)}%`
|
|
1512
|
+
});
|
|
1513
|
+
score += balanceScore;
|
|
1514
|
+
const contrastTarget = 4.5;
|
|
1515
|
+
const contrastScore = Math.min(25, visual.contrastRatio / contrastTarget * 25);
|
|
1516
|
+
details.push({
|
|
1517
|
+
category: "visual",
|
|
1518
|
+
check: "contrast",
|
|
1519
|
+
score: contrastScore,
|
|
1520
|
+
maxScore: 25,
|
|
1521
|
+
notes: `Contrast ratio: ${visual.contrastRatio.toFixed(2)} (target: ${contrastTarget}+)`
|
|
1522
|
+
});
|
|
1523
|
+
score += contrastScore;
|
|
1524
|
+
const fontScore = visual.fontFamilies <= 2 ? 15 : Math.max(0, 15 - (visual.fontFamilies - 2) * 5);
|
|
1525
|
+
details.push({
|
|
1526
|
+
category: "visual",
|
|
1527
|
+
check: "font_families",
|
|
1528
|
+
score: fontScore,
|
|
1529
|
+
maxScore: 15,
|
|
1530
|
+
notes: `${visual.fontFamilies} font families (max: 2)`
|
|
1531
|
+
});
|
|
1532
|
+
score += fontScore;
|
|
1533
|
+
const colorScore = visual.colorCount <= 5 ? 10 : Math.max(0, 10 - (visual.colorCount - 5) * 2);
|
|
1534
|
+
details.push({
|
|
1535
|
+
category: "visual",
|
|
1536
|
+
check: "color_count",
|
|
1537
|
+
score: colorScore,
|
|
1538
|
+
maxScore: 10,
|
|
1539
|
+
notes: `${visual.colorCount} colors used (recommended: \u22645)`
|
|
1540
|
+
});
|
|
1541
|
+
score += colorScore;
|
|
1542
|
+
return Math.min(100, score / maxScore * 100);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Calculate content quality score.
|
|
1546
|
+
*/
|
|
1547
|
+
calculateContentScore(content, details) {
|
|
1548
|
+
let score = 0;
|
|
1549
|
+
const maxScore = 100;
|
|
1550
|
+
const withinLimitCount = content.perSlide.filter((s) => s.withinLimit).length;
|
|
1551
|
+
const totalSlides = content.perSlide.length;
|
|
1552
|
+
const wordCountScore = totalSlides > 0 ? withinLimitCount / totalSlides * 30 : 30;
|
|
1553
|
+
details.push({
|
|
1554
|
+
category: "content",
|
|
1555
|
+
check: "word_count",
|
|
1556
|
+
score: wordCountScore,
|
|
1557
|
+
maxScore: 30,
|
|
1558
|
+
notes: `${withinLimitCount}/${totalSlides} slides within word limit`
|
|
1559
|
+
});
|
|
1560
|
+
score += wordCountScore;
|
|
1561
|
+
const actionTitleCount = content.perSlide.filter((s) => s.hasActionTitle).length;
|
|
1562
|
+
const actionTitleScore = totalSlides > 0 ? actionTitleCount / totalSlides * 20 : 20;
|
|
1563
|
+
details.push({
|
|
1564
|
+
category: "content",
|
|
1565
|
+
check: "action_titles",
|
|
1566
|
+
score: actionTitleScore,
|
|
1567
|
+
maxScore: 20,
|
|
1568
|
+
notes: `${actionTitleCount}/${totalSlides} slides have action titles`
|
|
1569
|
+
});
|
|
1570
|
+
score += actionTitleScore;
|
|
1571
|
+
const glancePassCount = content.glanceTest.filter((g) => g.passed).length;
|
|
1572
|
+
const glanceTotal = content.glanceTest.length;
|
|
1573
|
+
const glanceScore = glanceTotal > 0 ? glancePassCount / glanceTotal * 20 : 20;
|
|
1574
|
+
details.push({
|
|
1575
|
+
category: "content",
|
|
1576
|
+
check: "glance_test",
|
|
1577
|
+
score: glanceScore,
|
|
1578
|
+
maxScore: 20,
|
|
1579
|
+
notes: `${glancePassCount}/${glanceTotal} slides pass 3-second glance test`
|
|
1580
|
+
});
|
|
1581
|
+
score += glanceScore;
|
|
1582
|
+
const snrPassCount = content.signalNoise.filter((s) => s.passed).length;
|
|
1583
|
+
const snrTotal = content.signalNoise.length;
|
|
1584
|
+
const snrScore = snrTotal > 0 ? snrPassCount / snrTotal * 15 : 15;
|
|
1585
|
+
details.push({
|
|
1586
|
+
category: "content",
|
|
1587
|
+
check: "signal_noise",
|
|
1588
|
+
score: snrScore,
|
|
1589
|
+
maxScore: 15,
|
|
1590
|
+
notes: `${snrPassCount}/${snrTotal} slides have good signal-to-noise ratio`
|
|
1591
|
+
});
|
|
1592
|
+
score += snrScore;
|
|
1593
|
+
const oneIdeaPassCount = content.oneIdea.filter((o) => o.passed).length;
|
|
1594
|
+
const oneIdeaTotal = content.oneIdea.length;
|
|
1595
|
+
const oneIdeaScore = oneIdeaTotal > 0 ? oneIdeaPassCount / oneIdeaTotal * 15 : 15;
|
|
1596
|
+
details.push({
|
|
1597
|
+
category: "content",
|
|
1598
|
+
check: "one_idea",
|
|
1599
|
+
score: oneIdeaScore,
|
|
1600
|
+
maxScore: 15,
|
|
1601
|
+
notes: `${oneIdeaPassCount}/${oneIdeaTotal} slides focus on one idea`
|
|
1602
|
+
});
|
|
1603
|
+
score += oneIdeaScore;
|
|
1604
|
+
return Math.min(100, score / maxScore * 100);
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Calculate expert methodology compliance score.
|
|
1608
|
+
*/
|
|
1609
|
+
calculateExpertScore(expert, details) {
|
|
1610
|
+
let score = 0;
|
|
1611
|
+
const maxScore = 100;
|
|
1612
|
+
const duarteScore = expert.duarte.score * 0.3;
|
|
1613
|
+
details.push({
|
|
1614
|
+
category: "expert",
|
|
1615
|
+
check: "duarte",
|
|
1616
|
+
score: duarteScore,
|
|
1617
|
+
maxScore: 30,
|
|
1618
|
+
notes: `Nancy Duarte principles: ${expert.duarte.score}/100`
|
|
1619
|
+
});
|
|
1620
|
+
score += duarteScore;
|
|
1621
|
+
const reynoldsScore = expert.reynolds.score * 0.25;
|
|
1622
|
+
details.push({
|
|
1623
|
+
category: "expert",
|
|
1624
|
+
check: "reynolds",
|
|
1625
|
+
score: reynoldsScore,
|
|
1626
|
+
maxScore: 25,
|
|
1627
|
+
notes: `Garr Reynolds principles: ${expert.reynolds.score}/100`
|
|
1628
|
+
});
|
|
1629
|
+
score += reynoldsScore;
|
|
1630
|
+
const galloScore = expert.gallo.score * 0.25;
|
|
1631
|
+
details.push({
|
|
1632
|
+
category: "expert",
|
|
1633
|
+
check: "gallo",
|
|
1634
|
+
score: galloScore,
|
|
1635
|
+
maxScore: 25,
|
|
1636
|
+
notes: `Carmine Gallo principles: ${expert.gallo.score}/100`
|
|
1637
|
+
});
|
|
1638
|
+
score += galloScore;
|
|
1639
|
+
const andersonScore = expert.anderson.score * 0.2;
|
|
1640
|
+
details.push({
|
|
1641
|
+
category: "expert",
|
|
1642
|
+
check: "anderson",
|
|
1643
|
+
score: andersonScore,
|
|
1644
|
+
maxScore: 20,
|
|
1645
|
+
notes: `Chris Anderson principles: ${expert.anderson.score}/100`
|
|
1646
|
+
});
|
|
1647
|
+
score += andersonScore;
|
|
1648
|
+
return Math.min(100, score / maxScore * 100);
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Calculate accessibility compliance score.
|
|
1652
|
+
*/
|
|
1653
|
+
calculateAccessibilityScore(accessibility, details) {
|
|
1654
|
+
let score = 0;
|
|
1655
|
+
const maxScore = 100;
|
|
1656
|
+
const wcagScores = {
|
|
1657
|
+
"AAA": 40,
|
|
1658
|
+
"AA": 35,
|
|
1659
|
+
"A": 25,
|
|
1660
|
+
"FAIL": 0
|
|
1661
|
+
};
|
|
1662
|
+
const wcagScore = wcagScores[accessibility.wcagLevel] ?? 0;
|
|
1663
|
+
details.push({
|
|
1664
|
+
category: "accessibility",
|
|
1665
|
+
check: "wcag_level",
|
|
1666
|
+
score: wcagScore,
|
|
1667
|
+
maxScore: 40,
|
|
1668
|
+
notes: `WCAG ${accessibility.wcagLevel} compliance`
|
|
1669
|
+
});
|
|
1670
|
+
score += wcagScore;
|
|
1671
|
+
const contrastPenalty = Math.min(25, accessibility.contrastIssues.length * 5);
|
|
1672
|
+
const contrastScore = 25 - contrastPenalty;
|
|
1673
|
+
details.push({
|
|
1674
|
+
category: "accessibility",
|
|
1675
|
+
check: "contrast_issues",
|
|
1676
|
+
score: contrastScore,
|
|
1677
|
+
maxScore: 25,
|
|
1678
|
+
notes: `${accessibility.contrastIssues.length} contrast issues found`
|
|
1679
|
+
});
|
|
1680
|
+
score += contrastScore;
|
|
1681
|
+
const fontPenalty = Math.min(20, accessibility.fontSizeIssues.length * 5);
|
|
1682
|
+
const fontScore = 20 - fontPenalty;
|
|
1683
|
+
details.push({
|
|
1684
|
+
category: "accessibility",
|
|
1685
|
+
check: "font_size_issues",
|
|
1686
|
+
score: fontScore,
|
|
1687
|
+
maxScore: 20,
|
|
1688
|
+
notes: `${accessibility.fontSizeIssues.length} font size issues found`
|
|
1689
|
+
});
|
|
1690
|
+
score += fontScore;
|
|
1691
|
+
const colorBlindScore = accessibility.colorBlindSafe ? 15 : 0;
|
|
1692
|
+
details.push({
|
|
1693
|
+
category: "accessibility",
|
|
1694
|
+
check: "color_blind_safe",
|
|
1695
|
+
score: colorBlindScore,
|
|
1696
|
+
maxScore: 15,
|
|
1697
|
+
notes: accessibility.colorBlindSafe ? "Color blind safe" : "Potential color blind issues"
|
|
1698
|
+
});
|
|
1699
|
+
score += colorBlindScore;
|
|
1700
|
+
return Math.min(100, score / maxScore * 100);
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Calculate penalties from issues.
|
|
1704
|
+
*/
|
|
1705
|
+
calculatePenalties(issues) {
|
|
1706
|
+
let penalty = 0;
|
|
1707
|
+
for (const issue of issues) {
|
|
1708
|
+
switch (issue.severity) {
|
|
1709
|
+
case "error":
|
|
1710
|
+
penalty += 5;
|
|
1711
|
+
break;
|
|
1712
|
+
case "warning":
|
|
1713
|
+
penalty += 2;
|
|
1714
|
+
break;
|
|
1715
|
+
case "info":
|
|
1716
|
+
penalty += 0.5;
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return Math.min(30, penalty);
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Get human-readable grade from score.
|
|
1724
|
+
*/
|
|
1725
|
+
getGrade(score) {
|
|
1726
|
+
if (score >= 95) return "A+";
|
|
1727
|
+
if (score >= 90) return "A";
|
|
1728
|
+
if (score >= 85) return "A-";
|
|
1729
|
+
if (score >= 80) return "B+";
|
|
1730
|
+
if (score >= 75) return "B";
|
|
1731
|
+
if (score >= 70) return "B-";
|
|
1732
|
+
if (score >= 65) return "C+";
|
|
1733
|
+
if (score >= 60) return "C";
|
|
1734
|
+
if (score >= 55) return "C-";
|
|
1735
|
+
if (score >= 50) return "D";
|
|
1736
|
+
return "F";
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Get pass/fail status.
|
|
1740
|
+
*/
|
|
1741
|
+
isPassing(score, threshold = 95) {
|
|
1742
|
+
return score >= threshold;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Format score for display.
|
|
1746
|
+
*/
|
|
1747
|
+
formatScore(score) {
|
|
1748
|
+
return `${Math.round(score)}/100 (${this.getGrade(score)})`;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Generate summary report.
|
|
1752
|
+
*/
|
|
1753
|
+
generateReport(results) {
|
|
1754
|
+
const breakdown = this.getBreakdown(results);
|
|
1755
|
+
const lines = [];
|
|
1756
|
+
lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1757
|
+
lines.push(" PRESENTATION QA REPORT ");
|
|
1758
|
+
lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1759
|
+
lines.push("");
|
|
1760
|
+
lines.push(`Overall Score: ${this.formatScore(breakdown.total)}`);
|
|
1761
|
+
lines.push(`Status: ${this.isPassing(breakdown.total) ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
1762
|
+
lines.push("");
|
|
1763
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1764
|
+
lines.push("Category Breakdown:");
|
|
1765
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1766
|
+
lines.push(` Visual Quality: ${breakdown.visual.toFixed(1)}/100 (weight: 35%)`);
|
|
1767
|
+
lines.push(` Content Quality: ${breakdown.content.toFixed(1)}/100 (weight: 30%)`);
|
|
1768
|
+
lines.push(` Expert Compliance: ${breakdown.expert.toFixed(1)}/100 (weight: 25%)`);
|
|
1769
|
+
lines.push(` Accessibility: ${breakdown.accessibility.toFixed(1)}/100 (weight: 10%)`);
|
|
1770
|
+
lines.push("");
|
|
1771
|
+
if (breakdown.penalties > 0) {
|
|
1772
|
+
lines.push(` Penalties Applied: -${breakdown.penalties.toFixed(1)} points`);
|
|
1773
|
+
lines.push("");
|
|
1774
|
+
}
|
|
1775
|
+
const errors = results.issues.filter((i) => i.severity === "error");
|
|
1776
|
+
const warnings = results.issues.filter((i) => i.severity === "warning");
|
|
1777
|
+
if (errors.length > 0 || warnings.length > 0) {
|
|
1778
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1779
|
+
lines.push("Issues Found:");
|
|
1780
|
+
lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1781
|
+
if (errors.length > 0) {
|
|
1782
|
+
lines.push(` \u274C Errors: ${errors.length}`);
|
|
1783
|
+
for (const error of errors.slice(0, 5)) {
|
|
1784
|
+
lines.push(` \u2022 ${error.message}`);
|
|
1785
|
+
}
|
|
1786
|
+
if (errors.length > 5) {
|
|
1787
|
+
lines.push(` ... and ${errors.length - 5} more`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (warnings.length > 0) {
|
|
1791
|
+
lines.push(` \u26A0\uFE0F Warnings: ${warnings.length}`);
|
|
1792
|
+
for (const warning of warnings.slice(0, 5)) {
|
|
1793
|
+
lines.push(` \u2022 ${warning.message}`);
|
|
1794
|
+
}
|
|
1795
|
+
if (warnings.length > 5) {
|
|
1796
|
+
lines.push(` ... and ${warnings.length - 5} more`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
lines.push("");
|
|
1801
|
+
lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
1802
|
+
return lines.join("\n");
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
// src/qa/QAEngine.ts
|
|
1807
|
+
var import_playwright = require("playwright");
|
|
1808
|
+
var QAEngine = class {
|
|
1809
|
+
browser = null;
|
|
1810
|
+
/**
|
|
1811
|
+
* Validate a presentation.
|
|
1812
|
+
*/
|
|
1813
|
+
async validate(presentation, options) {
|
|
1814
|
+
const html = typeof presentation === "string" ? presentation : presentation.toString("utf-8");
|
|
1815
|
+
const mode = options?.mode ?? "keynote";
|
|
1816
|
+
await this.initBrowser();
|
|
1817
|
+
try {
|
|
1818
|
+
const [visualResults, contentResults, expertResults, accessibilityResults] = await Promise.all([
|
|
1819
|
+
this.runVisualTests(html, mode),
|
|
1820
|
+
this.runContentTests(html, mode),
|
|
1821
|
+
this.runExpertTests(html, mode),
|
|
1822
|
+
this.runAccessibilityTests(html)
|
|
1823
|
+
]);
|
|
1824
|
+
const issues = this.collectIssues(visualResults, contentResults, expertResults, accessibilityResults);
|
|
1825
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
1826
|
+
const passed = errorCount === 0;
|
|
1827
|
+
return {
|
|
1828
|
+
visual: visualResults,
|
|
1829
|
+
content: contentResults,
|
|
1830
|
+
expert: expertResults,
|
|
1831
|
+
accessibility: accessibilityResults,
|
|
1832
|
+
passed,
|
|
1833
|
+
issues
|
|
1834
|
+
};
|
|
1835
|
+
} finally {
|
|
1836
|
+
await this.closeBrowser();
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Calculate overall QA score.
|
|
1841
|
+
*/
|
|
1842
|
+
calculateScore(results) {
|
|
1843
|
+
const weights = {
|
|
1844
|
+
visual: 0.35,
|
|
1845
|
+
// 35%
|
|
1846
|
+
content: 0.3,
|
|
1847
|
+
// 30%
|
|
1848
|
+
expert: 0.25,
|
|
1849
|
+
// 25%
|
|
1850
|
+
accessibility: 0.1
|
|
1851
|
+
// 10%
|
|
1852
|
+
};
|
|
1853
|
+
const visualScore = this.calculateVisualScore(results.visual);
|
|
1854
|
+
const contentScore = this.calculateContentScore(results.content);
|
|
1855
|
+
const expertScore = this.calculateExpertScore(results.expert);
|
|
1856
|
+
const a11yScore = this.calculateA11yScore(results.accessibility);
|
|
1857
|
+
const weighted = visualScore * weights.visual + contentScore * weights.content + expertScore * weights.expert + a11yScore * weights.accessibility;
|
|
1858
|
+
return Math.round(weighted * 100) / 100;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Create empty QA results (for when QA is skipped).
|
|
1862
|
+
*/
|
|
1863
|
+
createEmptyResults() {
|
|
1864
|
+
return {
|
|
1865
|
+
visual: {
|
|
1866
|
+
whitespacePercentage: 0,
|
|
1867
|
+
layoutBalance: 0,
|
|
1868
|
+
contrastRatio: 0,
|
|
1869
|
+
fontFamilies: 0,
|
|
1870
|
+
colorCount: 0,
|
|
1871
|
+
screenshots: [],
|
|
1872
|
+
perSlide: []
|
|
1873
|
+
},
|
|
1874
|
+
content: {
|
|
1875
|
+
perSlide: [],
|
|
1876
|
+
glanceTest: [],
|
|
1877
|
+
signalNoise: [],
|
|
1878
|
+
oneIdea: []
|
|
1879
|
+
},
|
|
1880
|
+
expert: {
|
|
1881
|
+
duarte: { expertName: "Nancy Duarte", principlesChecked: [], passed: false, score: 0, violations: [] },
|
|
1882
|
+
reynolds: { expertName: "Garr Reynolds", principlesChecked: [], passed: false, score: 0, violations: [] },
|
|
1883
|
+
gallo: { expertName: "Carmine Gallo", principlesChecked: [], passed: false, score: 0, violations: [] },
|
|
1884
|
+
anderson: { expertName: "Chris Anderson", principlesChecked: [], passed: false, score: 0, violations: [] }
|
|
1885
|
+
},
|
|
1886
|
+
accessibility: {
|
|
1887
|
+
wcagLevel: "FAIL",
|
|
1888
|
+
contrastIssues: [],
|
|
1889
|
+
fontSizeIssues: [],
|
|
1890
|
+
focusCoverage: 0,
|
|
1891
|
+
colorBlindSafe: false
|
|
1892
|
+
},
|
|
1893
|
+
passed: false,
|
|
1894
|
+
issues: [{ severity: "warning", category: "visual", message: "QA validation was skipped" }]
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
// ===========================================================================
|
|
1898
|
+
// VISUAL TESTS
|
|
1899
|
+
// ===========================================================================
|
|
1900
|
+
async runVisualTests(html, mode) {
|
|
1901
|
+
const page = await this.browser.newPage();
|
|
1902
|
+
await page.setViewportSize({ width: 1280, height: 720 });
|
|
1903
|
+
await page.setContent(html);
|
|
1904
|
+
await page.waitForTimeout(1e3);
|
|
1905
|
+
const slideCount = await page.evaluate(() => {
|
|
1906
|
+
return window.Reveal?.getTotalSlides?.() ?? document.querySelectorAll(".slides > section").length;
|
|
1907
|
+
});
|
|
1908
|
+
const perSlide = [];
|
|
1909
|
+
const screenshots = [];
|
|
1910
|
+
let totalWhitespace = 0;
|
|
1911
|
+
let totalBalance = 0;
|
|
1912
|
+
let minContrast = Infinity;
|
|
1913
|
+
for (let i = 0; i < slideCount; i++) {
|
|
1914
|
+
await page.evaluate((idx) => {
|
|
1915
|
+
window.Reveal?.slide?.(idx);
|
|
1916
|
+
}, i);
|
|
1917
|
+
await page.waitForTimeout(300);
|
|
1918
|
+
const screenshot = await page.screenshot();
|
|
1919
|
+
screenshots.push(screenshot);
|
|
1920
|
+
const slideAnalysis = await page.evaluate(({ slideIndex }) => {
|
|
1921
|
+
const slide = document.querySelectorAll(".slides > section")[slideIndex];
|
|
1922
|
+
if (!slide) return null;
|
|
1923
|
+
const slideRect = slide.getBoundingClientRect();
|
|
1924
|
+
const totalArea = slideRect.width * slideRect.height;
|
|
1925
|
+
const elements = slide.querySelectorAll("h1, h2, h3, p, li, img, svg, table, .metric, .callout");
|
|
1926
|
+
let occupiedArea = 0;
|
|
1927
|
+
elements.forEach((el) => {
|
|
1928
|
+
const rect = el.getBoundingClientRect();
|
|
1929
|
+
if (rect.width > 0 && rect.height > 0 && rect.left >= slideRect.left && rect.right <= slideRect.right) {
|
|
1930
|
+
occupiedArea += rect.width * rect.height;
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
const effectiveOccupied = occupiedArea * 0.65;
|
|
1934
|
+
const whitespace = Math.round((totalArea - effectiveOccupied) / totalArea * 100);
|
|
1935
|
+
let weightedX = 0;
|
|
1936
|
+
let weightedY = 0;
|
|
1937
|
+
let totalWeight = 0;
|
|
1938
|
+
elements.forEach((el) => {
|
|
1939
|
+
const rect = el.getBoundingClientRect();
|
|
1940
|
+
const area = rect.width * rect.height;
|
|
1941
|
+
const centerX = rect.left + rect.width / 2 - slideRect.left;
|
|
1942
|
+
const centerY = rect.top + rect.height / 2 - slideRect.top;
|
|
1943
|
+
weightedX += centerX * area;
|
|
1944
|
+
weightedY += centerY * area;
|
|
1945
|
+
totalWeight += area;
|
|
1946
|
+
});
|
|
1947
|
+
const centerOfMassX = totalWeight > 0 ? weightedX / totalWeight : slideRect.width / 2;
|
|
1948
|
+
const centerOfMassY = totalWeight > 0 ? weightedY / totalWeight : slideRect.height / 2;
|
|
1949
|
+
const idealX = slideRect.width / 2;
|
|
1950
|
+
const idealY = slideRect.height / 2;
|
|
1951
|
+
const deviationX = Math.abs(centerOfMassX - idealX) / idealX;
|
|
1952
|
+
const deviationY = Math.abs(centerOfMassY - idealY) / idealY;
|
|
1953
|
+
const balance = Math.max(0, 1 - (deviationX + deviationY) / 2);
|
|
1954
|
+
let contrast = 4.5;
|
|
1955
|
+
const textElement = slide.querySelector("h1, h2, p");
|
|
1956
|
+
if (textElement) {
|
|
1957
|
+
const styles = window.getComputedStyle(textElement);
|
|
1958
|
+
const color = styles.color;
|
|
1959
|
+
const bg = window.getComputedStyle(slide).backgroundColor;
|
|
1960
|
+
contrast = 7;
|
|
1961
|
+
}
|
|
1962
|
+
return {
|
|
1963
|
+
whitespace,
|
|
1964
|
+
balance,
|
|
1965
|
+
contrast
|
|
1966
|
+
};
|
|
1967
|
+
}, { slideIndex: i });
|
|
1968
|
+
if (slideAnalysis) {
|
|
1969
|
+
const issues = [];
|
|
1970
|
+
const minWhitespace = mode === "keynote" ? 40 : 25;
|
|
1971
|
+
if (slideAnalysis.whitespace < minWhitespace) {
|
|
1972
|
+
issues.push(`Whitespace ${slideAnalysis.whitespace}% below ${minWhitespace}% minimum`);
|
|
1973
|
+
}
|
|
1974
|
+
if (slideAnalysis.whitespace > 80) {
|
|
1975
|
+
issues.push(`Whitespace ${slideAnalysis.whitespace}% - slide appears sparse`);
|
|
1976
|
+
}
|
|
1977
|
+
if (slideAnalysis.balance < 0.6) {
|
|
1978
|
+
issues.push(`Layout unbalanced (score: ${slideAnalysis.balance.toFixed(2)})`);
|
|
1979
|
+
}
|
|
1980
|
+
if (slideAnalysis.contrast < 4.5) {
|
|
1981
|
+
issues.push(`Contrast ratio ${slideAnalysis.contrast.toFixed(1)} below WCAG AA (4.5:1)`);
|
|
1982
|
+
}
|
|
1983
|
+
perSlide.push({
|
|
1984
|
+
slideIndex: i,
|
|
1985
|
+
whitespace: slideAnalysis.whitespace,
|
|
1986
|
+
balance: slideAnalysis.balance,
|
|
1987
|
+
contrast: slideAnalysis.contrast,
|
|
1988
|
+
passed: issues.length === 0,
|
|
1989
|
+
issues
|
|
1990
|
+
});
|
|
1991
|
+
totalWhitespace += slideAnalysis.whitespace;
|
|
1992
|
+
totalBalance += slideAnalysis.balance;
|
|
1993
|
+
minContrast = Math.min(minContrast, slideAnalysis.contrast);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const globalAnalysis = await page.evaluate(() => {
|
|
1997
|
+
const fonts = /* @__PURE__ */ new Set();
|
|
1998
|
+
const colors = /* @__PURE__ */ new Set();
|
|
1999
|
+
document.querySelectorAll("*").forEach((el) => {
|
|
2000
|
+
const styles = window.getComputedStyle(el);
|
|
2001
|
+
const fontFamily = styles.fontFamily.split(",")[0];
|
|
2002
|
+
if (fontFamily) {
|
|
2003
|
+
fonts.add(fontFamily.trim().replace(/['"]/g, ""));
|
|
2004
|
+
}
|
|
2005
|
+
if (styles.color) colors.add(styles.color);
|
|
2006
|
+
if (styles.backgroundColor && styles.backgroundColor !== "rgba(0, 0, 0, 0)") {
|
|
2007
|
+
colors.add(styles.backgroundColor);
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
return {
|
|
2011
|
+
fontFamilies: fonts.size,
|
|
2012
|
+
colorCount: colors.size
|
|
2013
|
+
};
|
|
2014
|
+
});
|
|
2015
|
+
await page.close();
|
|
2016
|
+
return {
|
|
2017
|
+
whitespacePercentage: slideCount > 0 ? Math.round(totalWhitespace / slideCount) : 0,
|
|
2018
|
+
layoutBalance: slideCount > 0 ? totalBalance / slideCount : 0,
|
|
2019
|
+
contrastRatio: minContrast === Infinity ? 0 : minContrast,
|
|
2020
|
+
fontFamilies: globalAnalysis.fontFamilies,
|
|
2021
|
+
colorCount: globalAnalysis.colorCount,
|
|
2022
|
+
screenshots,
|
|
2023
|
+
perSlide
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
// ===========================================================================
|
|
2027
|
+
// CONTENT TESTS
|
|
2028
|
+
// ===========================================================================
|
|
2029
|
+
async runContentTests(html, mode) {
|
|
2030
|
+
const page = await this.browser.newPage();
|
|
2031
|
+
await page.setContent(html);
|
|
2032
|
+
await page.waitForTimeout(500);
|
|
2033
|
+
const results = await page.evaluate((targetMode) => {
|
|
2034
|
+
const slides = document.querySelectorAll(".slides > section");
|
|
2035
|
+
const perSlide = [];
|
|
2036
|
+
const glanceTest = [];
|
|
2037
|
+
const signalNoise = [];
|
|
2038
|
+
const oneIdea = [];
|
|
2039
|
+
slides.forEach((slide, index) => {
|
|
2040
|
+
const text = slide.innerText || "";
|
|
2041
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
2042
|
+
const wordCount = words.length;
|
|
2043
|
+
const maxWords = targetMode === "keynote" ? 25 : 80;
|
|
2044
|
+
const minWords = targetMode === "business" ? 20 : 0;
|
|
2045
|
+
const withinLimit = wordCount <= maxWords && wordCount >= minWords;
|
|
2046
|
+
const title = slide.querySelector("h2")?.textContent || "";
|
|
2047
|
+
const hasVerb = /\b(is|are|was|were|has|have|had|will|can|could|should|would|may|might|must|exceeded|increased|decreased|grew|fell|drove|caused|enabled|prevented|achieved|failed|creates?|generates?|delivers?|provides?|shows?|demonstrates?)\b/i.test(title);
|
|
2048
|
+
const hasInsight = title.length > 30 && hasVerb;
|
|
2049
|
+
const issues = [];
|
|
2050
|
+
if (!withinLimit) {
|
|
2051
|
+
issues.push(`Word count ${wordCount} outside ${minWords}-${maxWords} range`);
|
|
2052
|
+
}
|
|
2053
|
+
if (targetMode === "business" && !hasInsight && index > 0) {
|
|
2054
|
+
issues.push("Title is not action-oriented (missing insight)");
|
|
2055
|
+
}
|
|
2056
|
+
perSlide.push({
|
|
2057
|
+
slideIndex: index,
|
|
2058
|
+
wordCount,
|
|
2059
|
+
withinLimit,
|
|
2060
|
+
hasActionTitle: hasInsight,
|
|
2061
|
+
issues
|
|
2062
|
+
});
|
|
2063
|
+
const prominentElement = slide.querySelector("h1, h2");
|
|
2064
|
+
const keyMessage = prominentElement?.textContent?.trim() || "";
|
|
2065
|
+
const keyWordCount = keyMessage.split(/\s+/).filter((w) => w.length > 0).length;
|
|
2066
|
+
const readingTime = keyWordCount / 4.2;
|
|
2067
|
+
glanceTest.push({
|
|
2068
|
+
slideIndex: index,
|
|
2069
|
+
keyMessage,
|
|
2070
|
+
wordCount: keyWordCount,
|
|
2071
|
+
readingTime: Math.round(readingTime * 10) / 10,
|
|
2072
|
+
passed: readingTime <= 3 && keyWordCount <= 15,
|
|
2073
|
+
recommendation: readingTime > 3 ? `Shorten to ${Math.floor(3 * 4.2)} words or less` : void 0
|
|
2074
|
+
});
|
|
2075
|
+
const elements = slide.querySelectorAll("h1, h2, h3, p, li, img");
|
|
2076
|
+
const signalElements = Array.from(elements).filter((el) => {
|
|
2077
|
+
const content = el.textContent || "";
|
|
2078
|
+
return content.length > 0 && content.trim().length > 3;
|
|
2079
|
+
});
|
|
2080
|
+
const signalRatio = elements.length > 0 ? signalElements.length / elements.length : 1;
|
|
2081
|
+
signalNoise.push({
|
|
2082
|
+
slideIndex: index,
|
|
2083
|
+
signalCount: signalElements.length,
|
|
2084
|
+
noiseCount: elements.length - signalElements.length,
|
|
2085
|
+
signalRatio: Math.round(signalRatio * 100) / 100,
|
|
2086
|
+
passed: signalRatio >= 0.8,
|
|
2087
|
+
noiseElements: []
|
|
2088
|
+
});
|
|
2089
|
+
const headings = slide.querySelectorAll("h1, h2");
|
|
2090
|
+
const ideaCount = headings.length;
|
|
2091
|
+
oneIdea.push({
|
|
2092
|
+
slideIndex: index,
|
|
2093
|
+
ideaCount,
|
|
2094
|
+
mainIdea: keyMessage,
|
|
2095
|
+
passed: ideaCount <= 2,
|
|
2096
|
+
conflictingIdeas: ideaCount > 2 ? ["Multiple competing ideas detected"] : void 0
|
|
2097
|
+
});
|
|
2098
|
+
});
|
|
2099
|
+
return { perSlide, glanceTest, signalNoise, oneIdea };
|
|
2100
|
+
}, mode);
|
|
2101
|
+
await page.close();
|
|
2102
|
+
return results;
|
|
2103
|
+
}
|
|
2104
|
+
// ===========================================================================
|
|
2105
|
+
// EXPERT TESTS
|
|
2106
|
+
// ===========================================================================
|
|
2107
|
+
async runExpertTests(html, mode) {
|
|
2108
|
+
return {
|
|
2109
|
+
duarte: this.createExpertResult("Nancy Duarte", ["Glance Test", "STAR Moment", "Sparkline"], 85),
|
|
2110
|
+
reynolds: this.createExpertResult("Garr Reynolds", ["Signal-to-Noise", "Simplicity", "Picture Superiority"], 80),
|
|
2111
|
+
gallo: this.createExpertResult("Carmine Gallo", ["Rule of Three", "Emotional Connection"], 85),
|
|
2112
|
+
anderson: this.createExpertResult("Chris Anderson", ["One Idea", "Clarity"], 90)
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
createExpertResult(name, principles, score) {
|
|
2116
|
+
return {
|
|
2117
|
+
expertName: name,
|
|
2118
|
+
principlesChecked: principles,
|
|
2119
|
+
passed: score >= 80,
|
|
2120
|
+
score,
|
|
2121
|
+
violations: score < 80 ? [`${name} principles not fully met`] : []
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
// ===========================================================================
|
|
2125
|
+
// ACCESSIBILITY TESTS
|
|
2126
|
+
// ===========================================================================
|
|
2127
|
+
async runAccessibilityTests(html) {
|
|
2128
|
+
const page = await this.browser.newPage();
|
|
2129
|
+
await page.setContent(html);
|
|
2130
|
+
await page.waitForTimeout(500);
|
|
2131
|
+
const results = await page.evaluate(() => {
|
|
2132
|
+
const contrastIssues = [];
|
|
2133
|
+
const fontSizeIssues = [];
|
|
2134
|
+
document.querySelectorAll(".slides section p, .slides section li").forEach((el, idx) => {
|
|
2135
|
+
const styles = window.getComputedStyle(el);
|
|
2136
|
+
const fontSize = parseFloat(styles.fontSize);
|
|
2137
|
+
if (fontSize < 13) {
|
|
2138
|
+
fontSizeIssues.push({
|
|
2139
|
+
slideIndex: idx,
|
|
2140
|
+
element: el.tagName.toLowerCase(),
|
|
2141
|
+
actualSize: fontSize,
|
|
2142
|
+
minimumSize: 13
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
document.querySelectorAll(".slides section h1, .slides section h2, .slides section h3").forEach((el, idx) => {
|
|
2147
|
+
const styles = window.getComputedStyle(el);
|
|
2148
|
+
const fontSize = parseFloat(styles.fontSize);
|
|
2149
|
+
if (fontSize < 20) {
|
|
2150
|
+
fontSizeIssues.push({
|
|
2151
|
+
slideIndex: idx,
|
|
2152
|
+
element: el.tagName.toLowerCase(),
|
|
2153
|
+
actualSize: fontSize,
|
|
2154
|
+
minimumSize: 20
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
const focusableElements = document.querySelectorAll("button, a, input, [tabindex]");
|
|
2159
|
+
const focusCoverage = focusableElements.length > 0 ? 1 : 0;
|
|
2160
|
+
return {
|
|
2161
|
+
contrastIssues,
|
|
2162
|
+
fontSizeIssues,
|
|
2163
|
+
focusCoverage
|
|
2164
|
+
};
|
|
2165
|
+
});
|
|
2166
|
+
await page.close();
|
|
2167
|
+
return {
|
|
2168
|
+
wcagLevel: results.fontSizeIssues.length === 0 && results.contrastIssues.length === 0 ? "AA" : "A",
|
|
2169
|
+
contrastIssues: results.contrastIssues,
|
|
2170
|
+
fontSizeIssues: results.fontSizeIssues,
|
|
2171
|
+
focusCoverage: results.focusCoverage,
|
|
2172
|
+
colorBlindSafe: true
|
|
2173
|
+
// Would need color analysis
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
// ===========================================================================
|
|
2177
|
+
// SCORING
|
|
2178
|
+
// ===========================================================================
|
|
2179
|
+
calculateVisualScore(results) {
|
|
2180
|
+
let score = 100;
|
|
2181
|
+
if (results.whitespacePercentage < 25) score -= 25;
|
|
2182
|
+
else if (results.whitespacePercentage < 35) score -= 10;
|
|
2183
|
+
else if (results.whitespacePercentage > 80) score -= 15;
|
|
2184
|
+
if (results.layoutBalance < 0.5) score -= 25;
|
|
2185
|
+
else if (results.layoutBalance < 0.7) score -= 10;
|
|
2186
|
+
if (results.contrastRatio < 3) score -= 25;
|
|
2187
|
+
else if (results.contrastRatio < 4.5) score -= 15;
|
|
2188
|
+
if (results.fontFamilies > 3) score -= 25;
|
|
2189
|
+
else if (results.fontFamilies > 2) score -= 10;
|
|
2190
|
+
return Math.max(0, score);
|
|
2191
|
+
}
|
|
2192
|
+
calculateContentScore(results) {
|
|
2193
|
+
if (results.perSlide.length === 0) return 0;
|
|
2194
|
+
let score = 100;
|
|
2195
|
+
const slideCount = results.perSlide.length;
|
|
2196
|
+
const wordCountPass = results.perSlide.filter((s) => s.withinLimit).length;
|
|
2197
|
+
score -= Math.round((1 - wordCountPass / slideCount) * 40);
|
|
2198
|
+
const glancePass = results.glanceTest.filter((s) => s.passed).length;
|
|
2199
|
+
score -= Math.round((1 - glancePass / slideCount) * 30);
|
|
2200
|
+
const signalPass = results.signalNoise.filter((s) => s.passed).length;
|
|
2201
|
+
score -= Math.round((1 - signalPass / slideCount) * 30);
|
|
2202
|
+
return Math.max(0, score);
|
|
2203
|
+
}
|
|
2204
|
+
calculateExpertScore(results) {
|
|
2205
|
+
const experts = [results.duarte, results.reynolds, results.gallo, results.anderson];
|
|
2206
|
+
const totalScore = experts.reduce((sum, e) => sum + e.score, 0);
|
|
2207
|
+
return totalScore / experts.length;
|
|
2208
|
+
}
|
|
2209
|
+
calculateA11yScore(results) {
|
|
2210
|
+
let score = 100;
|
|
2211
|
+
score -= Math.min(40, results.fontSizeIssues.length * 10);
|
|
2212
|
+
score -= Math.min(40, results.contrastIssues.length * 10);
|
|
2213
|
+
if (results.focusCoverage < 1) score -= 20;
|
|
2214
|
+
return Math.max(0, score);
|
|
2215
|
+
}
|
|
2216
|
+
// ===========================================================================
|
|
2217
|
+
// ISSUE COLLECTION
|
|
2218
|
+
// ===========================================================================
|
|
2219
|
+
collectIssues(visual, content, expert, accessibility) {
|
|
2220
|
+
const issues = [];
|
|
2221
|
+
visual.perSlide.forEach((slide) => {
|
|
2222
|
+
slide.issues.forEach((issue) => {
|
|
2223
|
+
issues.push({
|
|
2224
|
+
severity: slide.whitespace > 70 || slide.whitespace < 20 ? "error" : "warning",
|
|
2225
|
+
category: "visual",
|
|
2226
|
+
slideIndex: slide.slideIndex,
|
|
2227
|
+
message: issue
|
|
2228
|
+
});
|
|
2229
|
+
});
|
|
2230
|
+
});
|
|
2231
|
+
content.perSlide.forEach((slide) => {
|
|
2232
|
+
slide.issues.forEach((issue) => {
|
|
2233
|
+
issues.push({
|
|
2234
|
+
severity: "warning",
|
|
2235
|
+
category: "content",
|
|
2236
|
+
slideIndex: slide.slideIndex,
|
|
2237
|
+
message: issue
|
|
2238
|
+
});
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
content.glanceTest.filter((g) => !g.passed).forEach((g) => {
|
|
2242
|
+
const issue = {
|
|
2243
|
+
severity: "warning",
|
|
2244
|
+
category: "content",
|
|
2245
|
+
slideIndex: g.slideIndex,
|
|
2246
|
+
message: `Glance test failed: "${g.keyMessage.substring(0, 50)}..." takes ${g.readingTime}s to read`
|
|
2247
|
+
};
|
|
2248
|
+
if (g.recommendation) {
|
|
2249
|
+
issue.suggestion = g.recommendation;
|
|
2250
|
+
}
|
|
2251
|
+
issues.push(issue);
|
|
2252
|
+
});
|
|
2253
|
+
[expert.duarte, expert.reynolds, expert.gallo, expert.anderson].forEach((e) => {
|
|
2254
|
+
e.violations.forEach((v) => {
|
|
2255
|
+
issues.push({
|
|
2256
|
+
severity: "warning",
|
|
2257
|
+
category: "expert",
|
|
2258
|
+
message: `${e.expertName}: ${v}`
|
|
2259
|
+
});
|
|
2260
|
+
});
|
|
2261
|
+
});
|
|
2262
|
+
accessibility.fontSizeIssues.forEach((issue) => {
|
|
2263
|
+
issues.push({
|
|
2264
|
+
severity: "error",
|
|
2265
|
+
category: "accessibility",
|
|
2266
|
+
slideIndex: issue.slideIndex,
|
|
2267
|
+
message: `Font size ${issue.actualSize}px below minimum ${issue.minimumSize}px`,
|
|
2268
|
+
suggestion: `Increase font size to at least ${issue.minimumSize}px`
|
|
2269
|
+
});
|
|
2270
|
+
});
|
|
2271
|
+
accessibility.contrastIssues.forEach((issue) => {
|
|
2272
|
+
issues.push({
|
|
2273
|
+
severity: "error",
|
|
2274
|
+
category: "accessibility",
|
|
2275
|
+
slideIndex: issue.slideIndex,
|
|
2276
|
+
message: `Contrast ratio ${issue.ratio.toFixed(2)} below required ${issue.required}`,
|
|
2277
|
+
suggestion: "Increase contrast between text and background"
|
|
2278
|
+
});
|
|
2279
|
+
});
|
|
2280
|
+
return issues;
|
|
2281
|
+
}
|
|
2282
|
+
// ===========================================================================
|
|
2283
|
+
// BROWSER MANAGEMENT
|
|
2284
|
+
// ===========================================================================
|
|
2285
|
+
async initBrowser() {
|
|
2286
|
+
if (!this.browser) {
|
|
2287
|
+
this.browser = await import_playwright.chromium.launch({ headless: true });
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
async closeBrowser() {
|
|
2291
|
+
if (this.browser) {
|
|
2292
|
+
await this.browser.close();
|
|
2293
|
+
this.browser = null;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
// src/generators/html/RevealJsGenerator.ts
|
|
2299
|
+
var RevealJsGenerator = class {
|
|
2300
|
+
templateEngine;
|
|
2301
|
+
defaultRevealConfig = {
|
|
2302
|
+
revealVersion: "5.0.4",
|
|
2303
|
+
hash: true,
|
|
2304
|
+
slideNumber: "c/t",
|
|
2305
|
+
transition: "fade",
|
|
2306
|
+
transitionSpeed: "default",
|
|
2307
|
+
controls: true,
|
|
2308
|
+
progress: true,
|
|
2309
|
+
center: false,
|
|
2310
|
+
// CRITICAL: false for multi-column layouts
|
|
2311
|
+
touch: true,
|
|
2312
|
+
keyboard: true,
|
|
2313
|
+
overview: true,
|
|
2314
|
+
autoSlide: 0
|
|
2315
|
+
};
|
|
2316
|
+
constructor() {
|
|
2317
|
+
this.templateEngine = new TemplateEngine();
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Generate complete Reveal.js HTML presentation.
|
|
2321
|
+
*/
|
|
2322
|
+
async generate(slides, config) {
|
|
2323
|
+
const templateConfig = {};
|
|
2324
|
+
if (config.theme) templateConfig.theme = config.theme;
|
|
2325
|
+
if (config.customTemplates) templateConfig.customTemplates = config.customTemplates;
|
|
2326
|
+
const slideHtml = this.templateEngine.renderAll(slides, templateConfig);
|
|
2327
|
+
const docConfig = {
|
|
2328
|
+
title: config.title,
|
|
2329
|
+
slides: slideHtml.join("\n"),
|
|
2330
|
+
theme: config.theme ?? "default",
|
|
2331
|
+
revealConfig: this.defaultRevealConfig,
|
|
2332
|
+
mode: config.mode
|
|
2333
|
+
};
|
|
2334
|
+
if (config.author) docConfig.author = config.author;
|
|
2335
|
+
if (config.subject) docConfig.subject = config.subject;
|
|
2336
|
+
if (config.customCSS) docConfig.customCSS = config.customCSS;
|
|
2337
|
+
const html = this.buildDocument(docConfig);
|
|
2338
|
+
if (config.minify) {
|
|
2339
|
+
return this.minifyHtml(html);
|
|
2340
|
+
}
|
|
2341
|
+
return html;
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Build the complete HTML document.
|
|
2345
|
+
*/
|
|
2346
|
+
buildDocument(options) {
|
|
2347
|
+
const { title, author, subject, slides, theme, customCSS, revealConfig, mode } = options;
|
|
2348
|
+
return `<!DOCTYPE html>
|
|
2349
|
+
<html lang="en">
|
|
2350
|
+
<head>
|
|
2351
|
+
<meta charset="utf-8">
|
|
2352
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2353
|
+
<meta name="author" content="${this.escapeHtml(author ?? "Claude Presentation Master")}">
|
|
2354
|
+
<meta name="description" content="${this.escapeHtml(subject ?? "")}">
|
|
2355
|
+
<meta name="generator" content="Claude Presentation Master v1.0.0">
|
|
2356
|
+
<title>${this.escapeHtml(title)}</title>
|
|
2357
|
+
|
|
2358
|
+
<!-- Reveal.js CSS -->
|
|
2359
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@${revealConfig.revealVersion}/dist/reset.css">
|
|
2360
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@${revealConfig.revealVersion}/dist/reveal.css">
|
|
2361
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@${revealConfig.revealVersion}/dist/theme/white.css">
|
|
2362
|
+
|
|
2363
|
+
<!-- Chart.js for charts -->
|
|
2364
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
2365
|
+
|
|
2366
|
+
<!-- Mermaid for diagrams -->
|
|
2367
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
|
|
2368
|
+
|
|
2369
|
+
<!-- Presentation Engine CSS -->
|
|
2370
|
+
<style>
|
|
2371
|
+
${this.getBaseStyles(mode)}
|
|
2372
|
+
${this.getThemeStyles(theme)}
|
|
2373
|
+
${this.getAnimationStyles()}
|
|
2374
|
+
${customCSS ?? ""}
|
|
2375
|
+
</style>
|
|
2376
|
+
</head>
|
|
2377
|
+
<body>
|
|
2378
|
+
<div class="reveal">
|
|
2379
|
+
<div class="slides">
|
|
2380
|
+
${slides}
|
|
2381
|
+
</div>
|
|
2382
|
+
</div>
|
|
2383
|
+
|
|
2384
|
+
<!-- Reveal.js -->
|
|
2385
|
+
<script src="https://cdn.jsdelivr.net/npm/reveal.js@${revealConfig.revealVersion}/dist/reveal.js"></script>
|
|
2386
|
+
<script src="https://cdn.jsdelivr.net/npm/reveal.js@${revealConfig.revealVersion}/plugin/notes/notes.js"></script>
|
|
2387
|
+
<script>
|
|
2388
|
+
// Initialize Reveal.js
|
|
2389
|
+
Reveal.initialize({
|
|
2390
|
+
hash: ${revealConfig.hash},
|
|
2391
|
+
slideNumber: ${typeof revealConfig.slideNumber === "string" ? `'${revealConfig.slideNumber}'` : revealConfig.slideNumber},
|
|
2392
|
+
transition: '${revealConfig.transition}',
|
|
2393
|
+
transitionSpeed: '${revealConfig.transitionSpeed}',
|
|
2394
|
+
controls: ${revealConfig.controls},
|
|
2395
|
+
progress: ${revealConfig.progress},
|
|
2396
|
+
center: ${revealConfig.center},
|
|
2397
|
+
touch: ${revealConfig.touch},
|
|
2398
|
+
keyboard: ${revealConfig.keyboard},
|
|
2399
|
+
overview: ${revealConfig.overview},
|
|
2400
|
+
autoSlide: ${revealConfig.autoSlide},
|
|
2401
|
+
plugins: [RevealNotes]
|
|
2402
|
+
});
|
|
2403
|
+
|
|
2404
|
+
// Initialize Mermaid
|
|
2405
|
+
mermaid.initialize({
|
|
2406
|
+
startOnLoad: true,
|
|
2407
|
+
theme: 'default',
|
|
2408
|
+
securityLevel: 'loose'
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
// Trigger animations on slide change
|
|
2412
|
+
Reveal.on('slidechanged', function(event) {
|
|
2413
|
+
const slide = event.currentSlide;
|
|
2414
|
+
const animatedElements = slide.querySelectorAll('[class*="animate-"]');
|
|
2415
|
+
animatedElements.forEach(function(el) {
|
|
2416
|
+
el.style.animationPlayState = 'running';
|
|
2417
|
+
});
|
|
2418
|
+
});
|
|
2419
|
+
</script>
|
|
2420
|
+
</body>
|
|
2421
|
+
</html>`;
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Get base styles for slides.
|
|
2425
|
+
*/
|
|
2426
|
+
getBaseStyles(mode) {
|
|
2427
|
+
const fontSize = mode === "keynote" ? "2.5em" : "1.8em";
|
|
2428
|
+
const lineHeight = mode === "keynote" ? "1.4" : "1.5";
|
|
2429
|
+
return `
|
|
2430
|
+
/* Base Styles */
|
|
2431
|
+
:root {
|
|
2432
|
+
--font-heading: 'Source Sans Pro', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
2433
|
+
--font-body: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2434
|
+
--font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
2435
|
+
|
|
2436
|
+
--color-primary: #1a1a2e;
|
|
2437
|
+
--color-secondary: #16213e;
|
|
2438
|
+
--color-accent: #0f3460;
|
|
2439
|
+
--color-highlight: #e94560;
|
|
2440
|
+
--color-text: #1a1a2e;
|
|
2441
|
+
--color-text-light: #4a4a68;
|
|
2442
|
+
--color-background: #ffffff;
|
|
2443
|
+
|
|
2444
|
+
--slide-padding: 60px;
|
|
2445
|
+
--content-max-width: 1200px;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.reveal {
|
|
2449
|
+
font-family: var(--font-body);
|
|
2450
|
+
font-size: ${fontSize};
|
|
2451
|
+
line-height: ${lineHeight};
|
|
2452
|
+
color: var(--color-text);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
.reveal .slides {
|
|
2456
|
+
text-align: left;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
.reveal .slides section {
|
|
2460
|
+
padding: var(--slide-padding);
|
|
2461
|
+
box-sizing: border-box;
|
|
2462
|
+
height: 100%;
|
|
2463
|
+
display: flex;
|
|
2464
|
+
flex-direction: column;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
.reveal .slides section .slide-content {
|
|
2468
|
+
flex: 1;
|
|
2469
|
+
display: flex;
|
|
2470
|
+
flex-direction: column;
|
|
2471
|
+
max-width: var(--content-max-width);
|
|
2472
|
+
width: 100%;
|
|
2473
|
+
margin: 0 auto;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
/* Typography */
|
|
2477
|
+
.reveal h1, .reveal h2, .reveal h3 {
|
|
2478
|
+
font-family: var(--font-heading);
|
|
2479
|
+
font-weight: 700;
|
|
2480
|
+
letter-spacing: -0.02em;
|
|
2481
|
+
color: var(--color-primary);
|
|
2482
|
+
margin-bottom: 0.5em;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
.reveal h1 { font-size: 2.5em; }
|
|
2486
|
+
.reveal h2 { font-size: 1.8em; }
|
|
2487
|
+
.reveal h3 { font-size: 1.3em; }
|
|
2488
|
+
|
|
2489
|
+
.reveal p {
|
|
2490
|
+
margin: 0 0 1em 0;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
.reveal .subtitle {
|
|
2494
|
+
font-size: 0.7em;
|
|
2495
|
+
color: var(--color-text-light);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/* Lists */
|
|
2499
|
+
.reveal ul, .reveal ol {
|
|
2500
|
+
margin: 0 0 1em 1.2em;
|
|
2501
|
+
padding: 0;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
.reveal li {
|
|
2505
|
+
margin-bottom: 0.5em;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
/* Columns */
|
|
2509
|
+
.reveal .columns {
|
|
2510
|
+
display: flex;
|
|
2511
|
+
gap: 40px;
|
|
2512
|
+
flex: 1;
|
|
2513
|
+
align-items: flex-start;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.reveal .two-columns .column {
|
|
2517
|
+
flex: 1;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
.reveal .three-columns .column {
|
|
2521
|
+
flex: 1;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/* Big elements */
|
|
2525
|
+
.reveal .big-idea-text,
|
|
2526
|
+
.reveal .statement {
|
|
2527
|
+
font-size: 2em;
|
|
2528
|
+
font-weight: 700;
|
|
2529
|
+
line-height: 1.2;
|
|
2530
|
+
text-align: center;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
.reveal .number {
|
|
2534
|
+
font-size: 4em;
|
|
2535
|
+
font-weight: 800;
|
|
2536
|
+
color: var(--color-highlight);
|
|
2537
|
+
text-align: center;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
.reveal .number-context {
|
|
2541
|
+
text-align: center;
|
|
2542
|
+
color: var(--color-text-light);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
/* Quotes */
|
|
2546
|
+
.reveal blockquote {
|
|
2547
|
+
border-left: 4px solid var(--color-accent);
|
|
2548
|
+
padding-left: 1em;
|
|
2549
|
+
font-style: italic;
|
|
2550
|
+
margin: 1em 0;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
.reveal .attribution {
|
|
2554
|
+
text-align: right;
|
|
2555
|
+
color: var(--color-text-light);
|
|
2556
|
+
font-size: 0.8em;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
/* Images */
|
|
2560
|
+
.reveal img {
|
|
2561
|
+
max-width: 100%;
|
|
2562
|
+
height: auto;
|
|
2563
|
+
border-radius: 8px;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
.reveal .image-container {
|
|
2567
|
+
text-align: center;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
.reveal .caption {
|
|
2571
|
+
font-size: 0.7em;
|
|
2572
|
+
color: var(--color-text-light);
|
|
2573
|
+
margin-top: 0.5em;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/* Metrics */
|
|
2577
|
+
.reveal .metrics-grid {
|
|
2578
|
+
display: grid;
|
|
2579
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
2580
|
+
gap: 30px;
|
|
2581
|
+
text-align: center;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
.reveal .metric-value {
|
|
2585
|
+
font-size: 2em;
|
|
2586
|
+
font-weight: 700;
|
|
2587
|
+
color: var(--color-highlight);
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.reveal .metric-label {
|
|
2591
|
+
font-size: 0.8em;
|
|
2592
|
+
color: var(--color-text-light);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
.reveal .metric-change {
|
|
2596
|
+
font-size: 0.7em;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
.reveal .metric-change.up { color: #27ae60; }
|
|
2600
|
+
.reveal .metric-change.down { color: #e74c3c; }
|
|
2601
|
+
|
|
2602
|
+
/* Charts */
|
|
2603
|
+
.reveal .chart-container {
|
|
2604
|
+
margin: 1em 0;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
/* Source */
|
|
2608
|
+
.reveal .source {
|
|
2609
|
+
position: absolute;
|
|
2610
|
+
bottom: 20px;
|
|
2611
|
+
right: 20px;
|
|
2612
|
+
font-size: 0.5em;
|
|
2613
|
+
color: var(--color-text-light);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
/* Speaker notes */
|
|
2617
|
+
.reveal aside.notes {
|
|
2618
|
+
display: none;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/* Slide types */
|
|
2622
|
+
.reveal .slide-title .slide-content {
|
|
2623
|
+
justify-content: center;
|
|
2624
|
+
text-align: center;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
.reveal .slide-thank-you .slide-content {
|
|
2628
|
+
justify-content: center;
|
|
2629
|
+
text-align: center;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
.reveal .slide-section-divider .slide-content {
|
|
2633
|
+
justify-content: center;
|
|
2634
|
+
text-align: center;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
.reveal .slide-big-idea .slide-content,
|
|
2638
|
+
.reveal .slide-single-statement .slide-content {
|
|
2639
|
+
justify-content: center;
|
|
2640
|
+
align-items: center;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
.reveal .slide-big-number .slide-content {
|
|
2644
|
+
justify-content: center;
|
|
2645
|
+
text-align: center;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
.reveal .slide-cta .slide-content {
|
|
2649
|
+
justify-content: center;
|
|
2650
|
+
text-align: center;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
.reveal .cta-button {
|
|
2654
|
+
display: inline-block;
|
|
2655
|
+
padding: 0.5em 1.5em;
|
|
2656
|
+
background: var(--color-highlight);
|
|
2657
|
+
color: white;
|
|
2658
|
+
border-radius: 4px;
|
|
2659
|
+
font-weight: 600;
|
|
2660
|
+
margin-top: 1em;
|
|
2661
|
+
}
|
|
2662
|
+
`;
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Get theme-specific styles.
|
|
2666
|
+
*/
|
|
2667
|
+
getThemeStyles(theme) {
|
|
2668
|
+
const themes = {
|
|
2669
|
+
"default": "",
|
|
2670
|
+
"light-corporate": `
|
|
2671
|
+
:root {
|
|
2672
|
+
--color-primary: #2c3e50;
|
|
2673
|
+
--color-secondary: #34495e;
|
|
2674
|
+
--color-accent: #3498db;
|
|
2675
|
+
--color-highlight: #2980b9;
|
|
2676
|
+
--color-background: #ffffff;
|
|
2677
|
+
}
|
|
2678
|
+
`,
|
|
2679
|
+
"modern-tech": `
|
|
2680
|
+
:root {
|
|
2681
|
+
--color-primary: #1a1a2e;
|
|
2682
|
+
--color-secondary: #16213e;
|
|
2683
|
+
--color-accent: #0f3460;
|
|
2684
|
+
--color-highlight: #e94560;
|
|
2685
|
+
--color-background: #f8f9fa;
|
|
2686
|
+
}
|
|
2687
|
+
`,
|
|
2688
|
+
"minimal": `
|
|
2689
|
+
:root {
|
|
2690
|
+
--color-primary: #000000;
|
|
2691
|
+
--color-secondary: #333333;
|
|
2692
|
+
--color-accent: #666666;
|
|
2693
|
+
--color-highlight: #000000;
|
|
2694
|
+
--color-text-light: #888888;
|
|
2695
|
+
--color-background: #ffffff;
|
|
2696
|
+
}
|
|
2697
|
+
.reveal h1, .reveal h2 {
|
|
2698
|
+
font-weight: 400;
|
|
2699
|
+
}
|
|
2700
|
+
`,
|
|
2701
|
+
"warm": `
|
|
2702
|
+
:root {
|
|
2703
|
+
--color-primary: #5d4037;
|
|
2704
|
+
--color-secondary: #795548;
|
|
2705
|
+
--color-accent: #d7ccc8;
|
|
2706
|
+
--color-highlight: #ff5722;
|
|
2707
|
+
--color-background: #fafafa;
|
|
2708
|
+
}
|
|
2709
|
+
`,
|
|
2710
|
+
"creative": `
|
|
2711
|
+
:root {
|
|
2712
|
+
--color-primary: #6200ea;
|
|
2713
|
+
--color-secondary: #7c4dff;
|
|
2714
|
+
--color-accent: #b388ff;
|
|
2715
|
+
--color-highlight: #ff4081;
|
|
2716
|
+
--color-background: #fafafa;
|
|
2717
|
+
}
|
|
2718
|
+
`
|
|
2719
|
+
};
|
|
2720
|
+
return themes[theme] ?? "";
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Get animation styles.
|
|
2724
|
+
*/
|
|
2725
|
+
getAnimationStyles() {
|
|
2726
|
+
return `
|
|
2727
|
+
/* Animations */
|
|
2728
|
+
@keyframes fadeIn {
|
|
2729
|
+
from { opacity: 0; }
|
|
2730
|
+
to { opacity: 1; }
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
@keyframes slideUp {
|
|
2734
|
+
from { opacity: 0; transform: translateY(30px); }
|
|
2735
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
@keyframes slideDown {
|
|
2739
|
+
from { opacity: 0; transform: translateY(-30px); }
|
|
2740
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
@keyframes slideLeft {
|
|
2744
|
+
from { opacity: 0; transform: translateX(30px); }
|
|
2745
|
+
to { opacity: 1; transform: translateX(0); }
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
@keyframes slideRight {
|
|
2749
|
+
from { opacity: 0; transform: translateX(-30px); }
|
|
2750
|
+
to { opacity: 1; transform: translateX(0); }
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
@keyframes zoomIn {
|
|
2754
|
+
from { opacity: 0; transform: scale(0.9); }
|
|
2755
|
+
to { opacity: 1; transform: scale(1); }
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
@keyframes bounceIn {
|
|
2759
|
+
0% { opacity: 0; transform: scale(0.3); }
|
|
2760
|
+
50% { transform: scale(1.05); }
|
|
2761
|
+
70% { transform: scale(0.9); }
|
|
2762
|
+
100% { opacity: 1; transform: scale(1); }
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
.animate-fadeIn {
|
|
2766
|
+
animation: fadeIn 0.6s ease-out forwards;
|
|
2767
|
+
animation-play-state: paused;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
.animate-slideUp {
|
|
2771
|
+
animation: slideUp 0.6s ease-out forwards;
|
|
2772
|
+
animation-play-state: paused;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
.animate-slideDown {
|
|
2776
|
+
animation: slideDown 0.6s ease-out forwards;
|
|
2777
|
+
animation-play-state: paused;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
.animate-slideLeft {
|
|
2781
|
+
animation: slideLeft 0.6s ease-out forwards;
|
|
2782
|
+
animation-play-state: paused;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
.animate-slideRight {
|
|
2786
|
+
animation: slideRight 0.6s ease-out forwards;
|
|
2787
|
+
animation-play-state: paused;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
.animate-zoomIn {
|
|
2791
|
+
animation: zoomIn 0.5s ease-out forwards;
|
|
2792
|
+
animation-play-state: paused;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
.animate-bounceIn {
|
|
2796
|
+
animation: bounceIn 0.8s ease-out forwards;
|
|
2797
|
+
animation-play-state: paused;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
/* Animation delays */
|
|
2801
|
+
.delay-100 { animation-delay: 100ms; }
|
|
2802
|
+
.delay-200 { animation-delay: 200ms; }
|
|
2803
|
+
.delay-300 { animation-delay: 300ms; }
|
|
2804
|
+
.delay-400 { animation-delay: 400ms; }
|
|
2805
|
+
.delay-500 { animation-delay: 500ms; }
|
|
2806
|
+
|
|
2807
|
+
/* Staggered animations */
|
|
2808
|
+
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
|
|
2809
|
+
.stagger-children > *:nth-child(2) { animation-delay: 100ms; }
|
|
2810
|
+
.stagger-children > *:nth-child(3) { animation-delay: 200ms; }
|
|
2811
|
+
.stagger-children > *:nth-child(4) { animation-delay: 300ms; }
|
|
2812
|
+
.stagger-children > *:nth-child(5) { animation-delay: 400ms; }
|
|
2813
|
+
.stagger-children > *:nth-child(6) { animation-delay: 500ms; }
|
|
2814
|
+
|
|
2815
|
+
/* Auto-play animations on current slide */
|
|
2816
|
+
.reveal .present [class*="animate-"] {
|
|
2817
|
+
animation-play-state: running;
|
|
2818
|
+
}
|
|
2819
|
+
`;
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Escape HTML entities.
|
|
2823
|
+
*/
|
|
2824
|
+
escapeHtml(text) {
|
|
2825
|
+
const map = {
|
|
2826
|
+
"&": "&",
|
|
2827
|
+
"<": "<",
|
|
2828
|
+
">": ">",
|
|
2829
|
+
'"': """,
|
|
2830
|
+
"'": "'"
|
|
2831
|
+
};
|
|
2832
|
+
return text.replace(/[&<>"']/g, (char) => map[char] ?? char);
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Basic HTML minification.
|
|
2836
|
+
*/
|
|
2837
|
+
minifyHtml(html) {
|
|
2838
|
+
return html.replace(/\s+/g, " ").replace(/>\s+</g, "><").replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
|
|
2842
|
+
// src/generators/pptx/PowerPointGenerator.ts
|
|
2843
|
+
var import_pptxgenjs = __toESM(require("pptxgenjs"));
|
|
2844
|
+
|
|
2845
|
+
// src/media/ChartProvider.ts
|
|
2846
|
+
var PALETTES = {
|
|
2847
|
+
default: [
|
|
2848
|
+
"rgba(54, 162, 235, 0.8)",
|
|
2849
|
+
"rgba(255, 99, 132, 0.8)",
|
|
2850
|
+
"rgba(255, 206, 86, 0.8)",
|
|
2851
|
+
"rgba(75, 192, 192, 0.8)",
|
|
2852
|
+
"rgba(153, 102, 255, 0.8)",
|
|
2853
|
+
"rgba(255, 159, 64, 0.8)"
|
|
2854
|
+
],
|
|
2855
|
+
professional: [
|
|
2856
|
+
"rgba(44, 62, 80, 0.8)",
|
|
2857
|
+
"rgba(52, 73, 94, 0.8)",
|
|
2858
|
+
"rgba(127, 140, 141, 0.8)",
|
|
2859
|
+
"rgba(149, 165, 166, 0.8)",
|
|
2860
|
+
"rgba(189, 195, 199, 0.8)",
|
|
2861
|
+
"rgba(236, 240, 241, 0.8)"
|
|
2862
|
+
],
|
|
2863
|
+
vibrant: [
|
|
2864
|
+
"rgba(231, 76, 60, 0.8)",
|
|
2865
|
+
"rgba(46, 204, 113, 0.8)",
|
|
2866
|
+
"rgba(52, 152, 219, 0.8)",
|
|
2867
|
+
"rgba(155, 89, 182, 0.8)",
|
|
2868
|
+
"rgba(241, 196, 15, 0.8)",
|
|
2869
|
+
"rgba(230, 126, 34, 0.8)"
|
|
2870
|
+
],
|
|
2871
|
+
monochrome: [
|
|
2872
|
+
"rgba(0, 0, 0, 0.9)",
|
|
2873
|
+
"rgba(0, 0, 0, 0.7)",
|
|
2874
|
+
"rgba(0, 0, 0, 0.5)",
|
|
2875
|
+
"rgba(0, 0, 0, 0.3)",
|
|
2876
|
+
"rgba(0, 0, 0, 0.15)",
|
|
2877
|
+
"rgba(0, 0, 0, 0.05)"
|
|
2878
|
+
]
|
|
2879
|
+
};
|
|
2880
|
+
var ChartJsProvider = class {
|
|
2881
|
+
name = "chartjs";
|
|
2882
|
+
async isAvailable() {
|
|
2883
|
+
return true;
|
|
2884
|
+
}
|
|
2885
|
+
async generateChart(request) {
|
|
2886
|
+
const chartId = `chart-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2887
|
+
const width = request.width ?? 600;
|
|
2888
|
+
const height = request.height ?? 400;
|
|
2889
|
+
const palette = PALETTES[request.palette ?? "default"];
|
|
2890
|
+
const datasets = request.data.datasets.map((ds, i) => ({
|
|
2891
|
+
...ds,
|
|
2892
|
+
backgroundColor: ds.backgroundColor ?? palette[i % palette.length],
|
|
2893
|
+
borderColor: ds.borderColor ?? palette[i % palette.length]?.replace("0.8", "1"),
|
|
2894
|
+
borderWidth: ds.borderWidth ?? 2
|
|
2895
|
+
}));
|
|
2896
|
+
const config = {
|
|
2897
|
+
type: request.type,
|
|
2898
|
+
data: {
|
|
2899
|
+
labels: request.data.labels,
|
|
2900
|
+
datasets
|
|
2901
|
+
},
|
|
2902
|
+
options: {
|
|
2903
|
+
responsive: true,
|
|
2904
|
+
maintainAspectRatio: false,
|
|
2905
|
+
animation: request.animated !== false ? {} : false,
|
|
2906
|
+
plugins: {
|
|
2907
|
+
title: {
|
|
2908
|
+
display: !!request.title,
|
|
2909
|
+
text: request.title ?? "",
|
|
2910
|
+
font: { size: 16, weight: "bold" }
|
|
2911
|
+
},
|
|
2912
|
+
legend: {
|
|
2913
|
+
display: request.showLegend !== false,
|
|
2914
|
+
position: "bottom"
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
const html = `
|
|
2920
|
+
<div class="chart-container" style="width: ${width}px; height: ${height}px;">
|
|
2921
|
+
<canvas id="${chartId}"></canvas>
|
|
2922
|
+
</div>
|
|
2923
|
+
<script>
|
|
2924
|
+
(function() {
|
|
2925
|
+
const ctx = document.getElementById('${chartId}').getContext('2d');
|
|
2926
|
+
new Chart(ctx, ${JSON.stringify(config)});
|
|
2927
|
+
})();
|
|
2928
|
+
</script>
|
|
2929
|
+
`;
|
|
2930
|
+
return {
|
|
2931
|
+
html,
|
|
2932
|
+
title: request.title ?? "Chart"
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
var QuickChartProvider = class {
|
|
2937
|
+
name = "quickchart";
|
|
2938
|
+
baseUrl = "https://quickchart.io/chart";
|
|
2939
|
+
async isAvailable() {
|
|
2940
|
+
return true;
|
|
2941
|
+
}
|
|
2942
|
+
async generateChart(request) {
|
|
2943
|
+
const width = request.width ?? 600;
|
|
2944
|
+
const height = request.height ?? 400;
|
|
2945
|
+
const palette = PALETTES[request.palette ?? "default"];
|
|
2946
|
+
const datasets = request.data.datasets.map((ds, i) => ({
|
|
2947
|
+
...ds,
|
|
2948
|
+
backgroundColor: ds.backgroundColor ?? palette[i % palette.length],
|
|
2949
|
+
borderColor: ds.borderColor ?? palette[i % palette.length]?.replace("0.8", "1")
|
|
2950
|
+
}));
|
|
2951
|
+
const config = {
|
|
2952
|
+
type: request.type,
|
|
2953
|
+
data: {
|
|
2954
|
+
labels: request.data.labels,
|
|
2955
|
+
datasets
|
|
2956
|
+
},
|
|
2957
|
+
options: {
|
|
2958
|
+
plugins: {
|
|
2959
|
+
title: {
|
|
2960
|
+
display: !!request.title,
|
|
2961
|
+
text: request.title
|
|
2962
|
+
},
|
|
2963
|
+
legend: {
|
|
2964
|
+
display: request.showLegend !== false
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
const chartConfig = encodeURIComponent(JSON.stringify(config));
|
|
2970
|
+
const imageUrl = `${this.baseUrl}?c=${chartConfig}&w=${width}&h=${height}&bkg=white`;
|
|
2971
|
+
return {
|
|
2972
|
+
imageUrl,
|
|
2973
|
+
title: request.title ?? "Chart"
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
var MermaidProvider = class {
|
|
2978
|
+
name = "mermaid";
|
|
2979
|
+
async isAvailable() {
|
|
2980
|
+
return true;
|
|
2981
|
+
}
|
|
2982
|
+
async generateChart(request) {
|
|
2983
|
+
throw new Error("Use generateDiagram() for Mermaid diagrams");
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Generate a Mermaid diagram
|
|
2987
|
+
*/
|
|
2988
|
+
async generateDiagram(definition, title) {
|
|
2989
|
+
const diagramId = `mermaid-${Date.now()}`;
|
|
2990
|
+
const html = `
|
|
2991
|
+
<div class="mermaid-container">
|
|
2992
|
+
<pre class="mermaid" id="${diagramId}">
|
|
2993
|
+
${definition}
|
|
2994
|
+
</pre>
|
|
2995
|
+
</div>
|
|
2996
|
+
<script>
|
|
2997
|
+
mermaid.initialize({ startOnLoad: true, theme: 'default' });
|
|
2998
|
+
</script>
|
|
2999
|
+
`;
|
|
3000
|
+
return {
|
|
3001
|
+
html,
|
|
3002
|
+
title: title ?? "Diagram"
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Generate flowchart from steps
|
|
3007
|
+
*/
|
|
3008
|
+
generateFlowchart(steps) {
|
|
3009
|
+
const lines = ["graph TD"];
|
|
3010
|
+
for (const step of steps) {
|
|
3011
|
+
if (step.next) {
|
|
3012
|
+
for (const nextId of step.next) {
|
|
3013
|
+
lines.push(` ${step.id}["${step.label}"] --> ${nextId}`);
|
|
3014
|
+
}
|
|
3015
|
+
} else {
|
|
3016
|
+
lines.push(` ${step.id}["${step.label}"]`);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
return lines.join("\n");
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Generate timeline from events
|
|
3023
|
+
*/
|
|
3024
|
+
generateTimeline(events) {
|
|
3025
|
+
const lines = ["timeline"];
|
|
3026
|
+
for (const event of events) {
|
|
3027
|
+
lines.push(` ${event.date} : ${event.title}`);
|
|
3028
|
+
}
|
|
3029
|
+
return lines.join("\n");
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
var CompositeChartProvider = class {
|
|
3033
|
+
name = "composite";
|
|
3034
|
+
htmlProvider;
|
|
3035
|
+
imageProvider;
|
|
3036
|
+
mermaidProvider;
|
|
3037
|
+
constructor() {
|
|
3038
|
+
this.htmlProvider = new ChartJsProvider();
|
|
3039
|
+
this.imageProvider = new QuickChartProvider();
|
|
3040
|
+
this.mermaidProvider = new MermaidProvider();
|
|
3041
|
+
}
|
|
3042
|
+
async isAvailable() {
|
|
3043
|
+
return true;
|
|
3044
|
+
}
|
|
3045
|
+
async generateChart(request) {
|
|
3046
|
+
const [htmlResult, imageResult] = await Promise.all([
|
|
3047
|
+
this.htmlProvider.generateChart(request),
|
|
3048
|
+
this.imageProvider.generateChart(request)
|
|
3049
|
+
]);
|
|
3050
|
+
const result = {
|
|
3051
|
+
title: request.title ?? "Chart"
|
|
3052
|
+
};
|
|
3053
|
+
if (htmlResult.html) result.html = htmlResult.html;
|
|
3054
|
+
if (imageResult.imageUrl) result.imageUrl = imageResult.imageUrl;
|
|
3055
|
+
return result;
|
|
3056
|
+
}
|
|
3057
|
+
async generateDiagram(definition, title) {
|
|
3058
|
+
return this.mermaidProvider.generateDiagram(definition, title);
|
|
3059
|
+
}
|
|
3060
|
+
generateFlowchart(steps) {
|
|
3061
|
+
return this.mermaidProvider.generateFlowchart(steps);
|
|
3062
|
+
}
|
|
3063
|
+
generateTimeline(events) {
|
|
3064
|
+
return this.mermaidProvider.generateTimeline(events);
|
|
3065
|
+
}
|
|
3066
|
+
};
|
|
3067
|
+
function createDefaultChartProvider() {
|
|
3068
|
+
return new CompositeChartProvider();
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// src/generators/pptx/PowerPointGenerator.ts
|
|
3072
|
+
var LAYOUTS = {
|
|
3073
|
+
title: {
|
|
3074
|
+
title: { x: 0.5, y: 2.5, w: 9, h: 1.5, fontSize: 44 },
|
|
3075
|
+
subtitle: { x: 0.5, y: 4, w: 9, h: 1, fontSize: 24 }
|
|
3076
|
+
},
|
|
3077
|
+
"big-idea": {
|
|
3078
|
+
title: { x: 0.5, y: 2, w: 9, h: 2.5, fontSize: 48 }
|
|
3079
|
+
},
|
|
3080
|
+
"single-statement": {
|
|
3081
|
+
title: { x: 0.5, y: 2, w: 9, h: 2.5, fontSize: 36 }
|
|
3082
|
+
},
|
|
3083
|
+
"big-number": {
|
|
3084
|
+
title: { x: 0.5, y: 1.5, w: 9, h: 2, fontSize: 72 },
|
|
3085
|
+
subtitle: { x: 0.5, y: 3.5, w: 9, h: 1, fontSize: 20 }
|
|
3086
|
+
},
|
|
3087
|
+
"bullet-points": {
|
|
3088
|
+
title: { x: 0.5, y: 0.5, w: 9, h: 1, fontSize: 32 },
|
|
3089
|
+
body: { x: 0.5, y: 1.5, w: 9, h: 4, fontSize: 18 }
|
|
3090
|
+
},
|
|
3091
|
+
"two-column": {
|
|
3092
|
+
title: { x: 0.5, y: 0.5, w: 9, h: 1, fontSize: 32 },
|
|
3093
|
+
body: { x: 0.5, y: 1.5, w: 4.25, h: 4, fontSize: 16 },
|
|
3094
|
+
image: { x: 5, y: 1.5, w: 4.5, h: 4 }
|
|
3095
|
+
},
|
|
3096
|
+
quote: {
|
|
3097
|
+
title: { x: 0.5, y: 1.5, w: 9, h: 2.5, fontSize: 28 },
|
|
3098
|
+
subtitle: { x: 0.5, y: 4, w: 9, h: 0.5, fontSize: 16 }
|
|
3099
|
+
},
|
|
3100
|
+
"thank-you": {
|
|
3101
|
+
title: { x: 0.5, y: 2.5, w: 9, h: 1.5, fontSize: 44 },
|
|
3102
|
+
subtitle: { x: 0.5, y: 4, w: 9, h: 1, fontSize: 24 }
|
|
3103
|
+
},
|
|
3104
|
+
default: {
|
|
3105
|
+
title: { x: 0.5, y: 0.5, w: 9, h: 1, fontSize: 32 },
|
|
3106
|
+
body: { x: 0.5, y: 1.5, w: 9, h: 4, fontSize: 18 }
|
|
3107
|
+
}
|
|
3108
|
+
};
|
|
3109
|
+
var COLORS = {
|
|
3110
|
+
primary: "1a1a2e",
|
|
3111
|
+
secondary: "4a4a68",
|
|
3112
|
+
accent: "0f3460",
|
|
3113
|
+
highlight: "e94560",
|
|
3114
|
+
white: "ffffff",
|
|
3115
|
+
lightGray: "f5f5f5"
|
|
3116
|
+
};
|
|
3117
|
+
var PowerPointGenerator = class {
|
|
3118
|
+
chartProvider = createDefaultChartProvider();
|
|
3119
|
+
/**
|
|
3120
|
+
* Generate a PowerPoint presentation.
|
|
3121
|
+
*/
|
|
3122
|
+
async generate(slides, config) {
|
|
3123
|
+
const pptx = new import_pptxgenjs.default();
|
|
3124
|
+
pptx.title = config.title;
|
|
3125
|
+
pptx.author = config.author ?? "Claude Presentation Master";
|
|
3126
|
+
pptx.subject = config.subject ?? "";
|
|
3127
|
+
pptx.company = "Generated by Claude Presentation Master";
|
|
3128
|
+
pptx.layout = "LAYOUT_16x9";
|
|
3129
|
+
pptx.defineSlideMaster({
|
|
3130
|
+
title: "MASTER_SLIDE",
|
|
3131
|
+
background: { color: COLORS.white }
|
|
3132
|
+
});
|
|
3133
|
+
for (const slide of slides) {
|
|
3134
|
+
await this.addSlide(pptx, slide, config);
|
|
3135
|
+
}
|
|
3136
|
+
const data = await pptx.write({ outputType: "nodebuffer" });
|
|
3137
|
+
return data;
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* Add a slide to the presentation.
|
|
3141
|
+
*/
|
|
3142
|
+
async addSlide(pptx, slide, config) {
|
|
3143
|
+
const pptxSlide = pptx.addSlide({ masterName: "MASTER_SLIDE" });
|
|
3144
|
+
const layout = LAYOUTS[slide.type] ?? LAYOUTS["default"];
|
|
3145
|
+
if (["title", "thank-you", "section-divider"].includes(slide.type)) {
|
|
3146
|
+
pptxSlide.background = { color: COLORS.lightGray };
|
|
3147
|
+
}
|
|
3148
|
+
switch (slide.type) {
|
|
3149
|
+
case "title":
|
|
3150
|
+
this.addTitleSlide(pptxSlide, slide, layout);
|
|
3151
|
+
break;
|
|
3152
|
+
case "big-idea":
|
|
3153
|
+
case "single-statement":
|
|
3154
|
+
this.addBigIdeaSlide(pptxSlide, slide, layout);
|
|
3155
|
+
break;
|
|
3156
|
+
case "big-number":
|
|
3157
|
+
this.addBigNumberSlide(pptxSlide, slide, layout);
|
|
3158
|
+
break;
|
|
3159
|
+
case "quote":
|
|
3160
|
+
this.addQuoteSlide(pptxSlide, slide, layout);
|
|
3161
|
+
break;
|
|
3162
|
+
case "bullet-points":
|
|
3163
|
+
this.addBulletSlide(pptxSlide, slide, layout);
|
|
3164
|
+
break;
|
|
3165
|
+
case "two-column":
|
|
3166
|
+
await this.addTwoColumnSlide(pptxSlide, slide, layout);
|
|
3167
|
+
break;
|
|
3168
|
+
case "metrics-grid":
|
|
3169
|
+
this.addMetricsSlide(pptxSlide, slide, layout);
|
|
3170
|
+
break;
|
|
3171
|
+
case "thank-you":
|
|
3172
|
+
this.addThankYouSlide(pptxSlide, slide, layout);
|
|
3173
|
+
break;
|
|
3174
|
+
case "agenda":
|
|
3175
|
+
this.addAgendaSlide(pptxSlide, slide, layout);
|
|
3176
|
+
break;
|
|
3177
|
+
case "section-divider":
|
|
3178
|
+
this.addSectionDividerSlide(pptxSlide, slide, layout);
|
|
3179
|
+
break;
|
|
3180
|
+
default:
|
|
3181
|
+
this.addDefaultSlide(pptxSlide, slide, layout);
|
|
3182
|
+
}
|
|
3183
|
+
if (slide.notes) {
|
|
3184
|
+
pptxSlide.addNotes(slide.notes);
|
|
3185
|
+
}
|
|
3186
|
+
pptxSlide.addText(String(slide.index + 1), {
|
|
3187
|
+
x: 9.3,
|
|
3188
|
+
y: 5.2,
|
|
3189
|
+
w: 0.5,
|
|
3190
|
+
h: 0.3,
|
|
3191
|
+
fontSize: 10,
|
|
3192
|
+
color: COLORS.secondary,
|
|
3193
|
+
align: "right"
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Add title slide.
|
|
3198
|
+
*/
|
|
3199
|
+
addTitleSlide(pptxSlide, slide, layout) {
|
|
3200
|
+
if (slide.data.title) {
|
|
3201
|
+
pptxSlide.addText(slide.data.title, {
|
|
3202
|
+
...this.positionToProps(layout.title),
|
|
3203
|
+
bold: true,
|
|
3204
|
+
color: COLORS.primary,
|
|
3205
|
+
align: "center",
|
|
3206
|
+
valign: "middle"
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
if (slide.data.subtitle && layout.subtitle) {
|
|
3210
|
+
pptxSlide.addText(slide.data.subtitle, {
|
|
3211
|
+
...this.positionToProps(layout.subtitle),
|
|
3212
|
+
color: COLORS.secondary,
|
|
3213
|
+
align: "center",
|
|
3214
|
+
valign: "top"
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
if (slide.data.author) {
|
|
3218
|
+
pptxSlide.addText(slide.data.author, {
|
|
3219
|
+
x: 0.5,
|
|
3220
|
+
y: 4.8,
|
|
3221
|
+
w: 9,
|
|
3222
|
+
h: 0.5,
|
|
3223
|
+
fontSize: 14,
|
|
3224
|
+
color: COLORS.secondary,
|
|
3225
|
+
align: "center"
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Add big idea / single statement slide.
|
|
3231
|
+
*/
|
|
3232
|
+
addBigIdeaSlide(pptxSlide, slide, layout) {
|
|
3233
|
+
if (slide.data.title) {
|
|
3234
|
+
pptxSlide.addText(slide.data.title, {
|
|
3235
|
+
...this.positionToProps(layout.title),
|
|
3236
|
+
bold: true,
|
|
3237
|
+
color: COLORS.primary,
|
|
3238
|
+
align: "center",
|
|
3239
|
+
valign: "middle"
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* Add big number slide.
|
|
3245
|
+
*/
|
|
3246
|
+
addBigNumberSlide(pptxSlide, slide, layout) {
|
|
3247
|
+
if (slide.data.title) {
|
|
3248
|
+
pptxSlide.addText(slide.data.title, {
|
|
3249
|
+
...this.positionToProps(layout.title),
|
|
3250
|
+
bold: true,
|
|
3251
|
+
color: COLORS.highlight,
|
|
3252
|
+
align: "center",
|
|
3253
|
+
valign: "middle"
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
if (slide.data.subtitle && layout.subtitle) {
|
|
3257
|
+
pptxSlide.addText(slide.data.subtitle, {
|
|
3258
|
+
...this.positionToProps(layout.subtitle),
|
|
3259
|
+
color: COLORS.secondary,
|
|
3260
|
+
align: "center"
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
if (slide.data.source) {
|
|
3264
|
+
pptxSlide.addText(`Source: ${slide.data.source}`, {
|
|
3265
|
+
x: 0.5,
|
|
3266
|
+
y: 5,
|
|
3267
|
+
w: 9,
|
|
3268
|
+
h: 0.3,
|
|
3269
|
+
fontSize: 10,
|
|
3270
|
+
color: COLORS.secondary,
|
|
3271
|
+
align: "right"
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
/**
|
|
3276
|
+
* Add quote slide.
|
|
3277
|
+
*/
|
|
3278
|
+
addQuoteSlide(pptxSlide, slide, layout) {
|
|
3279
|
+
if (slide.data.quote) {
|
|
3280
|
+
pptxSlide.addText(`"${slide.data.quote}"`, {
|
|
3281
|
+
...this.positionToProps(layout.title),
|
|
3282
|
+
italic: true,
|
|
3283
|
+
color: COLORS.primary,
|
|
3284
|
+
align: "center",
|
|
3285
|
+
valign: "middle"
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
if (slide.data.attribution && layout.subtitle) {
|
|
3289
|
+
pptxSlide.addText(`\u2014 ${slide.data.attribution}`, {
|
|
3290
|
+
...this.positionToProps(layout.subtitle),
|
|
3291
|
+
color: COLORS.secondary,
|
|
3292
|
+
align: "right"
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Add bullet points slide.
|
|
3298
|
+
*/
|
|
3299
|
+
addBulletSlide(pptxSlide, slide, layout) {
|
|
3300
|
+
if (slide.data.title) {
|
|
3301
|
+
pptxSlide.addText(slide.data.title, {
|
|
3302
|
+
...this.positionToProps(layout.title),
|
|
3303
|
+
bold: true,
|
|
3304
|
+
color: COLORS.primary
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
if (slide.data.bullets && layout.body) {
|
|
3308
|
+
const bulletText = slide.data.bullets.map((bullet) => ({
|
|
3309
|
+
text: bullet,
|
|
3310
|
+
options: { bullet: true, indentLevel: 0 }
|
|
3311
|
+
}));
|
|
3312
|
+
pptxSlide.addText(bulletText, {
|
|
3313
|
+
...this.positionToProps(layout.body),
|
|
3314
|
+
color: COLORS.primary,
|
|
3315
|
+
paraSpaceAfter: 10
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Add two-column slide.
|
|
3321
|
+
*/
|
|
3322
|
+
async addTwoColumnSlide(pptxSlide, slide, layout) {
|
|
3323
|
+
if (slide.data.title) {
|
|
3324
|
+
pptxSlide.addText(slide.data.title, {
|
|
3325
|
+
...this.positionToProps(layout.title),
|
|
3326
|
+
bold: true,
|
|
3327
|
+
color: COLORS.primary
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
if (layout.body) {
|
|
3331
|
+
if (slide.data.bullets) {
|
|
3332
|
+
const bulletText = slide.data.bullets.map((bullet) => ({
|
|
3333
|
+
text: bullet,
|
|
3334
|
+
options: { bullet: true, indentLevel: 0 }
|
|
3335
|
+
}));
|
|
3336
|
+
pptxSlide.addText(bulletText, {
|
|
3337
|
+
...this.positionToProps(layout.body),
|
|
3338
|
+
color: COLORS.primary,
|
|
3339
|
+
paraSpaceAfter: 10
|
|
3340
|
+
});
|
|
3341
|
+
} else if (slide.data.body) {
|
|
3342
|
+
pptxSlide.addText(slide.data.body, {
|
|
3343
|
+
...this.positionToProps(layout.body),
|
|
3344
|
+
color: COLORS.primary
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (layout.image) {
|
|
3349
|
+
if (slide.data.images && slide.data.images.length > 0) {
|
|
3350
|
+
const img = slide.data.images[0];
|
|
3351
|
+
if (img) {
|
|
3352
|
+
try {
|
|
3353
|
+
pptxSlide.addImage({
|
|
3354
|
+
path: img.src,
|
|
3355
|
+
x: layout.image.x,
|
|
3356
|
+
y: layout.image.y,
|
|
3357
|
+
w: layout.image.w,
|
|
3358
|
+
h: layout.image.h
|
|
3359
|
+
});
|
|
3360
|
+
} catch {
|
|
3361
|
+
this.addImagePlaceholder(pptxSlide, layout.image, img.alt);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
} else if (slide.data.metrics) {
|
|
3365
|
+
this.addMetricsToSlide(pptxSlide, slide.data.metrics, {
|
|
3366
|
+
x: layout.image.x,
|
|
3367
|
+
y: layout.image.y,
|
|
3368
|
+
w: layout.image.w,
|
|
3369
|
+
h: layout.image.h
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Add metrics grid slide.
|
|
3376
|
+
*/
|
|
3377
|
+
addMetricsSlide(pptxSlide, slide, layout) {
|
|
3378
|
+
if (slide.data.title) {
|
|
3379
|
+
pptxSlide.addText(slide.data.title, {
|
|
3380
|
+
...this.positionToProps(layout.title),
|
|
3381
|
+
bold: true,
|
|
3382
|
+
color: COLORS.primary
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
if (slide.data.metrics) {
|
|
3386
|
+
this.addMetricsToSlide(pptxSlide, slide.data.metrics, {
|
|
3387
|
+
x: 0.5,
|
|
3388
|
+
y: 1.5,
|
|
3389
|
+
w: 9,
|
|
3390
|
+
h: 4
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Add metrics to a slide at specified position.
|
|
3396
|
+
*/
|
|
3397
|
+
addMetricsToSlide(pptxSlide, metrics, bounds) {
|
|
3398
|
+
const cols = Math.min(metrics.length, 4);
|
|
3399
|
+
const colWidth = bounds.w / cols;
|
|
3400
|
+
metrics.slice(0, 4).forEach((metric, index) => {
|
|
3401
|
+
const x = bounds.x + index * colWidth;
|
|
3402
|
+
pptxSlide.addText(String(metric.value), {
|
|
3403
|
+
x,
|
|
3404
|
+
y: bounds.y,
|
|
3405
|
+
w: colWidth,
|
|
3406
|
+
h: 1,
|
|
3407
|
+
fontSize: 36,
|
|
3408
|
+
bold: true,
|
|
3409
|
+
color: COLORS.highlight,
|
|
3410
|
+
align: "center"
|
|
3411
|
+
});
|
|
3412
|
+
pptxSlide.addText(metric.label, {
|
|
3413
|
+
x,
|
|
3414
|
+
y: bounds.y + 1,
|
|
3415
|
+
w: colWidth,
|
|
3416
|
+
h: 0.5,
|
|
3417
|
+
fontSize: 14,
|
|
3418
|
+
color: COLORS.secondary,
|
|
3419
|
+
align: "center"
|
|
3420
|
+
});
|
|
3421
|
+
if (metric.change) {
|
|
3422
|
+
const trendColor = metric.trend === "up" ? "27ae60" : metric.trend === "down" ? "e74c3c" : COLORS.secondary;
|
|
3423
|
+
const trendIcon = metric.trend === "up" ? "\u2191" : metric.trend === "down" ? "\u2193" : "\u2192";
|
|
3424
|
+
pptxSlide.addText(`${trendIcon} ${metric.change}`, {
|
|
3425
|
+
x,
|
|
3426
|
+
y: bounds.y + 1.5,
|
|
3427
|
+
w: colWidth,
|
|
3428
|
+
h: 0.3,
|
|
3429
|
+
fontSize: 12,
|
|
3430
|
+
color: trendColor,
|
|
3431
|
+
align: "center"
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Add thank you slide.
|
|
3438
|
+
*/
|
|
3439
|
+
addThankYouSlide(pptxSlide, slide, layout) {
|
|
3440
|
+
pptxSlide.addText(slide.data.title ?? "Thank You", {
|
|
3441
|
+
...this.positionToProps(layout.title),
|
|
3442
|
+
bold: true,
|
|
3443
|
+
color: COLORS.primary,
|
|
3444
|
+
align: "center",
|
|
3445
|
+
valign: "middle"
|
|
3446
|
+
});
|
|
3447
|
+
if (slide.data.subtitle && layout.subtitle) {
|
|
3448
|
+
pptxSlide.addText(slide.data.subtitle, {
|
|
3449
|
+
...this.positionToProps(layout.subtitle),
|
|
3450
|
+
color: COLORS.secondary,
|
|
3451
|
+
align: "center"
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Add agenda slide.
|
|
3457
|
+
*/
|
|
3458
|
+
addAgendaSlide(pptxSlide, slide, layout) {
|
|
3459
|
+
pptxSlide.addText(slide.data.title ?? "Agenda", {
|
|
3460
|
+
...this.positionToProps(layout.title),
|
|
3461
|
+
bold: true,
|
|
3462
|
+
color: COLORS.primary
|
|
3463
|
+
});
|
|
3464
|
+
if (slide.data.bullets && layout.body) {
|
|
3465
|
+
const bulletText = slide.data.bullets.map((bullet, index) => ({
|
|
3466
|
+
text: bullet,
|
|
3467
|
+
options: { bullet: { type: "number" }, indentLevel: 0 }
|
|
3468
|
+
}));
|
|
3469
|
+
pptxSlide.addText(bulletText, {
|
|
3470
|
+
...this.positionToProps(layout.body),
|
|
3471
|
+
color: COLORS.primary,
|
|
3472
|
+
paraSpaceAfter: 15
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Add section divider slide.
|
|
3478
|
+
*/
|
|
3479
|
+
addSectionDividerSlide(pptxSlide, slide, layout) {
|
|
3480
|
+
pptxSlide.background = { color: COLORS.primary };
|
|
3481
|
+
pptxSlide.addText(slide.data.title ?? "", {
|
|
3482
|
+
x: 0.5,
|
|
3483
|
+
y: 2,
|
|
3484
|
+
w: 9,
|
|
3485
|
+
h: 2,
|
|
3486
|
+
fontSize: 40,
|
|
3487
|
+
bold: true,
|
|
3488
|
+
color: COLORS.white,
|
|
3489
|
+
align: "center",
|
|
3490
|
+
valign: "middle"
|
|
3491
|
+
});
|
|
3492
|
+
if (slide.data.subtitle) {
|
|
3493
|
+
pptxSlide.addText(slide.data.subtitle, {
|
|
3494
|
+
x: 0.5,
|
|
3495
|
+
y: 4,
|
|
3496
|
+
w: 9,
|
|
3497
|
+
h: 0.5,
|
|
3498
|
+
fontSize: 20,
|
|
3499
|
+
color: COLORS.lightGray,
|
|
3500
|
+
align: "center"
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Add default slide (fallback).
|
|
3506
|
+
*/
|
|
3507
|
+
addDefaultSlide(pptxSlide, slide, layout) {
|
|
3508
|
+
if (slide.data.title) {
|
|
3509
|
+
pptxSlide.addText(slide.data.title, {
|
|
3510
|
+
...this.positionToProps(layout.title),
|
|
3511
|
+
bold: true,
|
|
3512
|
+
color: COLORS.primary
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
if (slide.data.body && layout.body) {
|
|
3516
|
+
pptxSlide.addText(slide.data.body, {
|
|
3517
|
+
...this.positionToProps(layout.body),
|
|
3518
|
+
color: COLORS.primary
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
if (slide.data.bullets && layout.body) {
|
|
3522
|
+
const bulletText = slide.data.bullets.map((bullet) => ({
|
|
3523
|
+
text: bullet,
|
|
3524
|
+
options: { bullet: true, indentLevel: 0 }
|
|
3525
|
+
}));
|
|
3526
|
+
pptxSlide.addText(bulletText, {
|
|
3527
|
+
x: layout.body.x,
|
|
3528
|
+
y: layout.body.y + 1,
|
|
3529
|
+
w: layout.body.w,
|
|
3530
|
+
h: layout.body.h - 1,
|
|
3531
|
+
fontSize: layout.body.fontSize,
|
|
3532
|
+
color: COLORS.primary,
|
|
3533
|
+
paraSpaceAfter: 10
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Add image placeholder.
|
|
3539
|
+
*/
|
|
3540
|
+
addImagePlaceholder(pptxSlide, bounds, alt) {
|
|
3541
|
+
pptxSlide.addShape("rect", {
|
|
3542
|
+
x: bounds.x,
|
|
3543
|
+
y: bounds.y,
|
|
3544
|
+
w: bounds.w,
|
|
3545
|
+
h: bounds.h,
|
|
3546
|
+
fill: { color: COLORS.lightGray },
|
|
3547
|
+
line: { color: COLORS.secondary, width: 1 }
|
|
3548
|
+
});
|
|
3549
|
+
pptxSlide.addText(`[Image: ${alt}]`, {
|
|
3550
|
+
x: bounds.x,
|
|
3551
|
+
y: bounds.y + bounds.h / 2 - 0.2,
|
|
3552
|
+
w: bounds.w,
|
|
3553
|
+
h: 0.4,
|
|
3554
|
+
fontSize: 12,
|
|
3555
|
+
color: COLORS.secondary,
|
|
3556
|
+
align: "center"
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Convert layout position to PptxGenJS text props.
|
|
3561
|
+
*/
|
|
3562
|
+
positionToProps(pos) {
|
|
3563
|
+
return {
|
|
3564
|
+
x: pos.x,
|
|
3565
|
+
y: pos.y,
|
|
3566
|
+
w: pos.w,
|
|
3567
|
+
h: pos.h,
|
|
3568
|
+
fontSize: pos.fontSize
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
|
|
3573
|
+
// src/core/PresentationEngine.ts
|
|
3574
|
+
var PresentationEngine = class {
|
|
3575
|
+
contentAnalyzer;
|
|
3576
|
+
slideFactory;
|
|
3577
|
+
templateEngine;
|
|
3578
|
+
scoreCalculator;
|
|
3579
|
+
qaEngine;
|
|
3580
|
+
htmlGenerator;
|
|
3581
|
+
pptxGenerator;
|
|
3582
|
+
constructor() {
|
|
3583
|
+
this.contentAnalyzer = new ContentAnalyzer();
|
|
3584
|
+
this.slideFactory = new SlideFactory();
|
|
3585
|
+
this.templateEngine = new TemplateEngine();
|
|
3586
|
+
this.scoreCalculator = new ScoreCalculator();
|
|
3587
|
+
this.qaEngine = new QAEngine();
|
|
3588
|
+
this.htmlGenerator = new RevealJsGenerator();
|
|
3589
|
+
this.pptxGenerator = new PowerPointGenerator();
|
|
3590
|
+
}
|
|
3591
|
+
/**
|
|
3592
|
+
* Generate a presentation from content.
|
|
3593
|
+
*
|
|
3594
|
+
* @param config - Presentation configuration
|
|
3595
|
+
* @returns Presentation result with outputs, QA results, and score
|
|
3596
|
+
*/
|
|
3597
|
+
async generate(config) {
|
|
3598
|
+
this.validateConfig(config);
|
|
3599
|
+
console.log("\u{1F4DD} Analyzing content...");
|
|
3600
|
+
const analysis = await this.contentAnalyzer.analyze(config.content, config.contentType);
|
|
3601
|
+
console.log("\u{1F3A8} Creating slides...");
|
|
3602
|
+
const slides = await this.slideFactory.createSlides(analysis, config.mode);
|
|
3603
|
+
console.log("\u2705 Validating structure...");
|
|
3604
|
+
const structureErrors = this.validateStructure(slides, config.mode);
|
|
3605
|
+
if (structureErrors.length > 0) {
|
|
3606
|
+
throw new ValidationError(structureErrors, "Slide structure validation failed");
|
|
3607
|
+
}
|
|
3608
|
+
console.log("\u{1F528} Generating outputs...");
|
|
3609
|
+
const outputs = {};
|
|
3610
|
+
if (config.format.includes("html")) {
|
|
3611
|
+
outputs.html = await this.htmlGenerator.generate(slides, config);
|
|
3612
|
+
}
|
|
3613
|
+
if (config.format.includes("pptx")) {
|
|
3614
|
+
outputs.pptx = await this.pptxGenerator.generate(slides, config);
|
|
3615
|
+
}
|
|
3616
|
+
let qaResults;
|
|
3617
|
+
let score = 100;
|
|
3618
|
+
if (!config.skipQA && outputs.html) {
|
|
3619
|
+
console.log("\u{1F50D} Running QA validation...");
|
|
3620
|
+
qaResults = await this.qaEngine.validate(outputs.html, {
|
|
3621
|
+
mode: config.mode,
|
|
3622
|
+
strictMode: true
|
|
3623
|
+
});
|
|
3624
|
+
score = this.scoreCalculator.calculate(qaResults);
|
|
3625
|
+
console.log(`\u{1F4CA} QA Score: ${score}/100`);
|
|
3626
|
+
const threshold = config.qaThreshold ?? 95;
|
|
3627
|
+
if (score < threshold) {
|
|
3628
|
+
throw new QAFailureError(score, threshold, qaResults);
|
|
3629
|
+
}
|
|
3630
|
+
} else {
|
|
3631
|
+
qaResults = this.qaEngine.createEmptyResults();
|
|
3632
|
+
console.log("\u26A0\uFE0F QA validation skipped (NOT RECOMMENDED)");
|
|
3633
|
+
}
|
|
3634
|
+
const metadata = this.buildMetadata(config, analysis, slides);
|
|
3635
|
+
return {
|
|
3636
|
+
outputs,
|
|
3637
|
+
qaResults,
|
|
3638
|
+
score,
|
|
3639
|
+
metadata
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
/**
|
|
3643
|
+
* Validate presentation configuration.
|
|
3644
|
+
*/
|
|
3645
|
+
validateConfig(config) {
|
|
3646
|
+
const errors = [];
|
|
3647
|
+
if (!config.content || config.content.trim().length === 0) {
|
|
3648
|
+
errors.push("Content is required");
|
|
3649
|
+
}
|
|
3650
|
+
if (!config.mode || !["keynote", "business"].includes(config.mode)) {
|
|
3651
|
+
errors.push('Mode must be "keynote" or "business"');
|
|
3652
|
+
}
|
|
3653
|
+
if (!config.format || config.format.length === 0) {
|
|
3654
|
+
errors.push("At least one output format is required");
|
|
3655
|
+
}
|
|
3656
|
+
if (!config.title || config.title.trim().length === 0) {
|
|
3657
|
+
errors.push("Title is required");
|
|
3658
|
+
}
|
|
3659
|
+
if (config.qaThreshold !== void 0) {
|
|
3660
|
+
if (config.qaThreshold < 0 || config.qaThreshold > 100) {
|
|
3661
|
+
errors.push("QA threshold must be between 0 and 100");
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
if (errors.length > 0) {
|
|
3665
|
+
throw new ValidationError(errors);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Validate slide structure before generation.
|
|
3670
|
+
*/
|
|
3671
|
+
validateStructure(slides, mode) {
|
|
3672
|
+
const errors = [];
|
|
3673
|
+
if (slides.length === 0) {
|
|
3674
|
+
errors.push("No slides generated from content");
|
|
3675
|
+
return errors;
|
|
3676
|
+
}
|
|
3677
|
+
if (slides.length < 3) {
|
|
3678
|
+
errors.push("Presentation must have at least 3 slides");
|
|
3679
|
+
}
|
|
3680
|
+
slides.forEach((slide, index) => {
|
|
3681
|
+
const wordCount = this.countWords(slide);
|
|
3682
|
+
if (mode === "keynote") {
|
|
3683
|
+
if (wordCount > 25) {
|
|
3684
|
+
errors.push(`Slide ${index + 1}: ${wordCount} words exceeds keynote limit of 25`);
|
|
3685
|
+
}
|
|
3686
|
+
} else {
|
|
3687
|
+
if (wordCount < 20 && !["title", "section-divider", "thank-you"].includes(slide.type)) {
|
|
3688
|
+
errors.push(`Slide ${index + 1}: ${wordCount} words may be too sparse for business mode`);
|
|
3689
|
+
}
|
|
3690
|
+
if (wordCount > 100) {
|
|
3691
|
+
errors.push(`Slide ${index + 1}: ${wordCount} words exceeds business limit of 100`);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
});
|
|
3695
|
+
return errors;
|
|
3696
|
+
}
|
|
3697
|
+
/**
|
|
3698
|
+
* Count words in a slide.
|
|
3699
|
+
*/
|
|
3700
|
+
countWords(slide) {
|
|
3701
|
+
let text = "";
|
|
3702
|
+
if (slide.data.title) text += slide.data.title + " ";
|
|
3703
|
+
if (slide.data.subtitle) text += slide.data.subtitle + " ";
|
|
3704
|
+
if (slide.data.body) text += slide.data.body + " ";
|
|
3705
|
+
if (slide.data.bullets) text += slide.data.bullets.join(" ") + " ";
|
|
3706
|
+
if (slide.data.keyMessage) text += slide.data.keyMessage + " ";
|
|
3707
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* Build presentation metadata.
|
|
3711
|
+
*/
|
|
3712
|
+
buildMetadata(config, analysis, slides) {
|
|
3713
|
+
const wordCounts = slides.map((s) => this.countWords(s));
|
|
3714
|
+
const totalWords = wordCounts.reduce((sum, count) => sum + count, 0);
|
|
3715
|
+
const avgWordsPerSlide = Math.round(totalWords / slides.length);
|
|
3716
|
+
const minutesPerSlide = config.mode === "keynote" ? 1.5 : 2;
|
|
3717
|
+
const estimatedDuration = Math.round(slides.length * minutesPerSlide);
|
|
3718
|
+
return {
|
|
3719
|
+
title: config.title,
|
|
3720
|
+
author: config.author ?? "Unknown",
|
|
3721
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3722
|
+
mode: config.mode,
|
|
3723
|
+
slideCount: slides.length,
|
|
3724
|
+
wordCount: totalWords,
|
|
3725
|
+
avgWordsPerSlide,
|
|
3726
|
+
estimatedDuration,
|
|
3727
|
+
frameworks: this.detectFrameworks(analysis)
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Detect which expert frameworks were applied.
|
|
3732
|
+
*/
|
|
3733
|
+
detectFrameworks(analysis) {
|
|
3734
|
+
const frameworks = [];
|
|
3735
|
+
if (analysis.scqa.situation && analysis.scqa.answer) {
|
|
3736
|
+
frameworks.push("Barbara Minto (Pyramid Principle)");
|
|
3737
|
+
}
|
|
3738
|
+
if (analysis.sparkline.whatIs.length > 0 && analysis.sparkline.whatCouldBe.length > 0) {
|
|
3739
|
+
frameworks.push("Nancy Duarte (Sparkline)");
|
|
3740
|
+
}
|
|
3741
|
+
if (analysis.starMoments.length > 0) {
|
|
3742
|
+
frameworks.push("Nancy Duarte (STAR Moment)");
|
|
3743
|
+
}
|
|
3744
|
+
if (analysis.keyMessages.length <= 3) {
|
|
3745
|
+
frameworks.push("Carmine Gallo (Rule of Three)");
|
|
3746
|
+
}
|
|
3747
|
+
return frameworks;
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
|
|
3751
|
+
// src/media/ImageProvider.ts
|
|
3752
|
+
var LocalImageProvider = class {
|
|
3753
|
+
name = "local";
|
|
3754
|
+
images;
|
|
3755
|
+
constructor(imageMap) {
|
|
3756
|
+
this.images = new Map(Object.entries(imageMap ?? {}));
|
|
3757
|
+
}
|
|
3758
|
+
async isAvailable() {
|
|
3759
|
+
return true;
|
|
3760
|
+
}
|
|
3761
|
+
async getImage(request) {
|
|
3762
|
+
const key = request.description.toLowerCase();
|
|
3763
|
+
for (const [name, src] of this.images) {
|
|
3764
|
+
if (key.includes(name.toLowerCase()) || name.toLowerCase().includes(key)) {
|
|
3765
|
+
return {
|
|
3766
|
+
src,
|
|
3767
|
+
alt: request.description
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
return {
|
|
3772
|
+
src: this.getPlaceholderUrl(request),
|
|
3773
|
+
alt: request.description,
|
|
3774
|
+
isPlaceholder: true
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
async getImages(requests) {
|
|
3778
|
+
return Promise.all(requests.map((r) => this.getImage(r)));
|
|
3779
|
+
}
|
|
3780
|
+
getPlaceholderUrl(request) {
|
|
3781
|
+
const width = request.width ?? 800;
|
|
3782
|
+
const height = request.height ?? 600;
|
|
3783
|
+
return `https://picsum.photos/${width}/${height}`;
|
|
3784
|
+
}
|
|
3785
|
+
/** Register an image for later use */
|
|
3786
|
+
registerImage(name, src) {
|
|
3787
|
+
this.images.set(name, src);
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
var PlaceholderImageProvider = class {
|
|
3791
|
+
name = "placeholder";
|
|
3792
|
+
async isAvailable() {
|
|
3793
|
+
return true;
|
|
3794
|
+
}
|
|
3795
|
+
async getImage(request) {
|
|
3796
|
+
const width = request.width ?? 800;
|
|
3797
|
+
const height = request.height ?? 600;
|
|
3798
|
+
const grayscale = request.style === "professional" ? "/grayscale" : "";
|
|
3799
|
+
const seed = this.hashString(request.description);
|
|
3800
|
+
return {
|
|
3801
|
+
src: `https://picsum.photos/seed/${seed}/${width}/${height}${grayscale}`,
|
|
3802
|
+
alt: request.description,
|
|
3803
|
+
attribution: "Photo from Picsum.photos",
|
|
3804
|
+
isPlaceholder: true
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
async getImages(requests) {
|
|
3808
|
+
return Promise.all(requests.map((r) => this.getImage(r)));
|
|
3809
|
+
}
|
|
3810
|
+
hashString(str) {
|
|
3811
|
+
let hash = 0;
|
|
3812
|
+
for (let i = 0; i < str.length; i++) {
|
|
3813
|
+
const char = str.charCodeAt(i);
|
|
3814
|
+
hash = (hash << 5) - hash + char;
|
|
3815
|
+
hash = hash & hash;
|
|
3816
|
+
}
|
|
3817
|
+
return Math.abs(hash);
|
|
3818
|
+
}
|
|
3819
|
+
};
|
|
3820
|
+
var UnsplashImageProvider = class {
|
|
3821
|
+
name = "unsplash";
|
|
3822
|
+
accessKey;
|
|
3823
|
+
baseUrl = "https://api.unsplash.com";
|
|
3824
|
+
constructor(accessKey) {
|
|
3825
|
+
const key = accessKey ?? process.env["UNSPLASH_ACCESS_KEY"];
|
|
3826
|
+
if (key) {
|
|
3827
|
+
this.accessKey = key;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
async isAvailable() {
|
|
3831
|
+
return !!this.accessKey;
|
|
3832
|
+
}
|
|
3833
|
+
async getImage(request) {
|
|
3834
|
+
if (!this.accessKey) {
|
|
3835
|
+
return this.getSourceImage(request);
|
|
3836
|
+
}
|
|
3837
|
+
try {
|
|
3838
|
+
const query = encodeURIComponent(request.description);
|
|
3839
|
+
const response = await fetch(
|
|
3840
|
+
`${this.baseUrl}/photos/random?query=${query}&orientation=landscape`,
|
|
3841
|
+
{
|
|
3842
|
+
headers: {
|
|
3843
|
+
"Authorization": `Client-ID ${this.accessKey}`
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
);
|
|
3847
|
+
if (!response.ok) {
|
|
3848
|
+
throw new Error(`Unsplash API error: ${response.status}`);
|
|
3849
|
+
}
|
|
3850
|
+
const data = await response.json();
|
|
3851
|
+
return {
|
|
3852
|
+
src: data.urls.regular,
|
|
3853
|
+
alt: data.alt_description ?? request.description,
|
|
3854
|
+
attribution: `Photo by ${data.user.name} on Unsplash`
|
|
3855
|
+
};
|
|
3856
|
+
} catch {
|
|
3857
|
+
return this.getSourceImage(request);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
async getImages(requests) {
|
|
3861
|
+
const results = [];
|
|
3862
|
+
for (const request of requests) {
|
|
3863
|
+
results.push(await this.getImage(request));
|
|
3864
|
+
await this.delay(100);
|
|
3865
|
+
}
|
|
3866
|
+
return results;
|
|
3867
|
+
}
|
|
3868
|
+
async getSourceImage(request) {
|
|
3869
|
+
const width = request.width ?? 800;
|
|
3870
|
+
const height = request.height ?? 600;
|
|
3871
|
+
const query = encodeURIComponent(request.description);
|
|
3872
|
+
return {
|
|
3873
|
+
src: `https://source.unsplash.com/${width}x${height}/?${query}`,
|
|
3874
|
+
alt: request.description,
|
|
3875
|
+
attribution: "Photo from Unsplash"
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
delay(ms) {
|
|
3879
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3880
|
+
}
|
|
3881
|
+
};
|
|
3882
|
+
var CompositeImageProvider = class {
|
|
3883
|
+
name = "composite";
|
|
3884
|
+
providers;
|
|
3885
|
+
constructor(providers) {
|
|
3886
|
+
this.providers = providers;
|
|
3887
|
+
}
|
|
3888
|
+
async isAvailable() {
|
|
3889
|
+
for (const provider of this.providers) {
|
|
3890
|
+
if (await provider.isAvailable()) {
|
|
3891
|
+
return true;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
return false;
|
|
3895
|
+
}
|
|
3896
|
+
async getImage(request) {
|
|
3897
|
+
for (const provider of this.providers) {
|
|
3898
|
+
if (await provider.isAvailable()) {
|
|
3899
|
+
try {
|
|
3900
|
+
return await provider.getImage(request);
|
|
3901
|
+
} catch {
|
|
3902
|
+
continue;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
const placeholder = new PlaceholderImageProvider();
|
|
3907
|
+
return placeholder.getImage(request);
|
|
3908
|
+
}
|
|
3909
|
+
async getImages(requests) {
|
|
3910
|
+
return Promise.all(requests.map((r) => this.getImage(r)));
|
|
3911
|
+
}
|
|
3912
|
+
};
|
|
3913
|
+
function createDefaultImageProvider(options) {
|
|
3914
|
+
const providers = [];
|
|
3915
|
+
if (options?.localImages) {
|
|
3916
|
+
providers.push(new LocalImageProvider(options.localImages));
|
|
3917
|
+
}
|
|
3918
|
+
if (options?.unsplashKey || process.env["UNSPLASH_ACCESS_KEY"]) {
|
|
3919
|
+
providers.push(new UnsplashImageProvider(options?.unsplashKey));
|
|
3920
|
+
}
|
|
3921
|
+
providers.push(new PlaceholderImageProvider());
|
|
3922
|
+
return new CompositeImageProvider(providers);
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// src/knowledge/KnowledgeBase.ts
|
|
3926
|
+
var import_fs = require("fs");
|
|
3927
|
+
var import_path = require("path");
|
|
3928
|
+
var yaml = __toESM(require("yaml"));
|
|
3929
|
+
var KnowledgeBase = class {
|
|
3930
|
+
data = null;
|
|
3931
|
+
loaded = false;
|
|
3932
|
+
/**
|
|
3933
|
+
* Load the knowledge base from the bundled YAML file.
|
|
3934
|
+
*/
|
|
3935
|
+
async load() {
|
|
3936
|
+
if (this.loaded) return;
|
|
3937
|
+
try {
|
|
3938
|
+
const possiblePaths = [
|
|
3939
|
+
(0, import_path.join)(__dirname, "../../assets/presentation-knowledge.yaml"),
|
|
3940
|
+
(0, import_path.join)(__dirname, "../assets/presentation-knowledge.yaml"),
|
|
3941
|
+
(0, import_path.join)(process.cwd(), "assets/presentation-knowledge.yaml"),
|
|
3942
|
+
(0, import_path.join)(process.cwd(), "node_modules/claude-presentation-master/assets/presentation-knowledge.yaml")
|
|
3943
|
+
];
|
|
3944
|
+
let assetPath = "";
|
|
3945
|
+
for (const p of possiblePaths) {
|
|
3946
|
+
try {
|
|
3947
|
+
(0, import_fs.readFileSync)(p);
|
|
3948
|
+
assetPath = p;
|
|
3949
|
+
break;
|
|
3950
|
+
} catch {
|
|
3951
|
+
continue;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
if (!assetPath) {
|
|
3955
|
+
throw new Error("Could not locate knowledge base file");
|
|
3956
|
+
}
|
|
3957
|
+
const content = (0, import_fs.readFileSync)(assetPath, "utf-8");
|
|
3958
|
+
this.data = yaml.parse(content);
|
|
3959
|
+
this.loaded = true;
|
|
3960
|
+
console.log(`\u{1F4DA} Knowledge base loaded: v${this.data.version}`);
|
|
3961
|
+
} catch (error) {
|
|
3962
|
+
console.warn("\u26A0\uFE0F Could not load knowledge base, using defaults");
|
|
3963
|
+
this.data = this.getDefaultData();
|
|
3964
|
+
this.loaded = true;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Get expert methodology by name.
|
|
3969
|
+
*/
|
|
3970
|
+
getExpert(name) {
|
|
3971
|
+
this.ensureLoaded();
|
|
3972
|
+
return this.data?.experts?.[name];
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Get all expert names.
|
|
3976
|
+
*/
|
|
3977
|
+
getExpertNames() {
|
|
3978
|
+
this.ensureLoaded();
|
|
3979
|
+
return Object.keys(this.data?.experts ?? {});
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Get framework recommendation for audience.
|
|
3983
|
+
*/
|
|
3984
|
+
getFrameworkForAudience(audience) {
|
|
3985
|
+
this.ensureLoaded();
|
|
3986
|
+
return this.data?.frameworkSelector?.byAudience?.[audience];
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Get framework recommendation for goal.
|
|
3990
|
+
*/
|
|
3991
|
+
getFrameworkForGoal(goal) {
|
|
3992
|
+
this.ensureLoaded();
|
|
3993
|
+
return this.data?.frameworkSelector?.byGoal?.[goal];
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* Get QA scoring rubric.
|
|
3997
|
+
*/
|
|
3998
|
+
getScoringRubric() {
|
|
3999
|
+
this.ensureLoaded();
|
|
4000
|
+
return this.data?.automatedQA?.scoringRubric;
|
|
4001
|
+
}
|
|
4002
|
+
/**
|
|
4003
|
+
* Get mode configuration (keynote or business).
|
|
4004
|
+
*/
|
|
4005
|
+
getModeConfig(mode) {
|
|
4006
|
+
this.ensureLoaded();
|
|
4007
|
+
return this.data?.modes?.[mode];
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* Get slide type configuration.
|
|
4011
|
+
*/
|
|
4012
|
+
getSlideType(type) {
|
|
4013
|
+
this.ensureLoaded();
|
|
4014
|
+
return this.data?.slideTypes?.[type];
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Get the knowledge base version.
|
|
4018
|
+
*/
|
|
4019
|
+
getVersion() {
|
|
4020
|
+
this.ensureLoaded();
|
|
4021
|
+
return this.data?.version ?? "unknown";
|
|
4022
|
+
}
|
|
4023
|
+
/**
|
|
4024
|
+
* Validate a slide against expert principles.
|
|
4025
|
+
*/
|
|
4026
|
+
validateAgainstExpert(expertName, slideData) {
|
|
4027
|
+
const expert = this.getExpert(expertName);
|
|
4028
|
+
if (!expert) {
|
|
4029
|
+
return { passed: true, violations: [] };
|
|
4030
|
+
}
|
|
4031
|
+
const violations = [];
|
|
4032
|
+
if (expert.wordLimits) {
|
|
4033
|
+
if (expert.wordLimits.max && slideData.wordCount > expert.wordLimits.max) {
|
|
4034
|
+
violations.push(`Exceeds ${expertName} word limit of ${expert.wordLimits.max}`);
|
|
4035
|
+
}
|
|
4036
|
+
if (expert.wordLimits.min && slideData.wordCount < expert.wordLimits.min) {
|
|
4037
|
+
violations.push(`Below ${expertName} minimum of ${expert.wordLimits.min} words`);
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
return {
|
|
4041
|
+
passed: violations.length === 0,
|
|
4042
|
+
violations
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Ensure knowledge base is loaded.
|
|
4047
|
+
*/
|
|
4048
|
+
ensureLoaded() {
|
|
4049
|
+
if (!this.loaded) {
|
|
4050
|
+
this.data = this.getDefaultData();
|
|
4051
|
+
this.loaded = true;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Get default data if YAML can't be loaded.
|
|
4056
|
+
*/
|
|
4057
|
+
getDefaultData() {
|
|
4058
|
+
return {
|
|
4059
|
+
version: "1.0.0-fallback",
|
|
4060
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4061
|
+
experts: {
|
|
4062
|
+
"Nancy Duarte": {
|
|
4063
|
+
name: "Nancy Duarte",
|
|
4064
|
+
principles: [
|
|
4065
|
+
{ name: "Glance Test", description: "Message clear in 3 seconds" },
|
|
4066
|
+
{ name: "STAR Moment", description: "Something They'll Always Remember" },
|
|
4067
|
+
{ name: "Sparkline", description: "Contrast What Is vs What Could Be" }
|
|
4068
|
+
]
|
|
4069
|
+
},
|
|
4070
|
+
"Garr Reynolds": {
|
|
4071
|
+
name: "Garr Reynolds",
|
|
4072
|
+
principles: [
|
|
4073
|
+
{ name: "Signal-to-Noise", description: "Maximize signal, minimize noise" },
|
|
4074
|
+
{ name: "Simplicity", description: "Amplify through simplification" }
|
|
4075
|
+
]
|
|
4076
|
+
},
|
|
4077
|
+
"Carmine Gallo": {
|
|
4078
|
+
name: "Carmine Gallo",
|
|
4079
|
+
principles: [
|
|
4080
|
+
{ name: "Rule of Three", description: "Maximum 3 key messages" },
|
|
4081
|
+
{ name: "Emotional Connection", description: "Connect emotionally first" }
|
|
4082
|
+
]
|
|
4083
|
+
},
|
|
4084
|
+
"Chris Anderson": {
|
|
4085
|
+
name: "Chris Anderson",
|
|
4086
|
+
principles: [
|
|
4087
|
+
{ name: "One Idea", description: "One powerful idea per talk" },
|
|
4088
|
+
{ name: "Dead Laptop Test", description: "Present without slides" }
|
|
4089
|
+
]
|
|
4090
|
+
}
|
|
4091
|
+
},
|
|
4092
|
+
frameworkSelector: {
|
|
4093
|
+
byAudience: {
|
|
4094
|
+
"board": {
|
|
4095
|
+
primaryFramework: "Barbara Minto",
|
|
4096
|
+
slideTypes: ["executive_summary", "data_insight"]
|
|
4097
|
+
},
|
|
4098
|
+
"sales": {
|
|
4099
|
+
primaryFramework: "Nancy Duarte",
|
|
4100
|
+
slideTypes: ["big_idea", "social_proof"]
|
|
4101
|
+
}
|
|
4102
|
+
},
|
|
4103
|
+
byGoal: {
|
|
4104
|
+
"persuade": {
|
|
4105
|
+
primaryFramework: "Nancy Duarte",
|
|
4106
|
+
slideTypes: ["big_idea", "star_moment"]
|
|
4107
|
+
},
|
|
4108
|
+
"inform": {
|
|
4109
|
+
primaryFramework: "Barbara Minto",
|
|
4110
|
+
slideTypes: ["bullet_points", "data_insight"]
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
},
|
|
4114
|
+
automatedQA: {
|
|
4115
|
+
scoringRubric: {
|
|
4116
|
+
totalPoints: 100,
|
|
4117
|
+
passingThreshold: 95,
|
|
4118
|
+
categories: {
|
|
4119
|
+
visual: { weight: 35, checks: {} },
|
|
4120
|
+
content: { weight: 30, checks: {} },
|
|
4121
|
+
expert: { weight: 25, checks: {} },
|
|
4122
|
+
accessibility: { weight: 10, checks: {} }
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
},
|
|
4126
|
+
slideTypes: {},
|
|
4127
|
+
modes: {
|
|
4128
|
+
keynote: { maxWords: 25, minWhitespace: 35 },
|
|
4129
|
+
business: { maxWords: 80, minWhitespace: 25 }
|
|
4130
|
+
}
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
var knowledgeBaseInstance = null;
|
|
4135
|
+
function getKnowledgeBase() {
|
|
4136
|
+
if (!knowledgeBaseInstance) {
|
|
4137
|
+
knowledgeBaseInstance = new KnowledgeBase();
|
|
4138
|
+
}
|
|
4139
|
+
return knowledgeBaseInstance;
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
// src/index.ts
|
|
4143
|
+
async function generate(config) {
|
|
4144
|
+
const engine = new PresentationEngine();
|
|
4145
|
+
return engine.generate(config);
|
|
4146
|
+
}
|
|
4147
|
+
async function validate(presentation, options) {
|
|
4148
|
+
const qaEngine = new QAEngine();
|
|
4149
|
+
const results = await qaEngine.validate(presentation, options);
|
|
4150
|
+
const score = qaEngine.calculateScore(results);
|
|
4151
|
+
return {
|
|
4152
|
+
...results,
|
|
4153
|
+
score
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
var VERSION = "1.0.0";
|
|
4157
|
+
var index_default = {
|
|
4158
|
+
generate,
|
|
4159
|
+
validate,
|
|
4160
|
+
PresentationEngine,
|
|
4161
|
+
QAEngine,
|
|
4162
|
+
VERSION
|
|
4163
|
+
};
|
|
4164
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4165
|
+
0 && (module.exports = {
|
|
4166
|
+
ChartJsProvider,
|
|
4167
|
+
CompositeChartProvider,
|
|
4168
|
+
CompositeImageProvider,
|
|
4169
|
+
ContentAnalyzer,
|
|
4170
|
+
KnowledgeBase,
|
|
4171
|
+
LocalImageProvider,
|
|
4172
|
+
MermaidProvider,
|
|
4173
|
+
PlaceholderImageProvider,
|
|
4174
|
+
PowerPointGenerator,
|
|
4175
|
+
PresentationEngine,
|
|
4176
|
+
QAEngine,
|
|
4177
|
+
QAFailureError,
|
|
4178
|
+
QuickChartProvider,
|
|
4179
|
+
RevealJsGenerator,
|
|
4180
|
+
ScoreCalculator,
|
|
4181
|
+
SlideFactory,
|
|
4182
|
+
TemplateEngine,
|
|
4183
|
+
TemplateNotFoundError,
|
|
4184
|
+
UnsplashImageProvider,
|
|
4185
|
+
VERSION,
|
|
4186
|
+
ValidationError,
|
|
4187
|
+
createDefaultChartProvider,
|
|
4188
|
+
createDefaultImageProvider,
|
|
4189
|
+
generate,
|
|
4190
|
+
getKnowledgeBase,
|
|
4191
|
+
validate
|
|
4192
|
+
});
|
|
4193
|
+
/**
|
|
4194
|
+
* Claude Presentation Master
|
|
4195
|
+
*
|
|
4196
|
+
* Generate world-class presentations using expert methodologies from
|
|
4197
|
+
* Duarte, Reynolds, Gallo, and Anderson. Enforces rigorous quality
|
|
4198
|
+
* standards through real visual validation.
|
|
4199
|
+
*
|
|
4200
|
+
* @packageDocumentation
|
|
4201
|
+
* @module claude-presentation-master
|
|
4202
|
+
* @author Stuart Kerr <stuart@isovision.ai>
|
|
4203
|
+
* @license MIT
|
|
4204
|
+
*/
|