hanseol-dev 5.0.2-dev.99 → 5.0.3-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.js +445 -404
- package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.d.ts +99 -3
- package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.js +1098 -172
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-client.js +13 -6
- package/dist/tools/office/powerpoint-client.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { powerpointClient } from '../../tools/office/powerpoint-client.js';
|
|
|
4
4
|
import { getSubAgentPhaseLogger, getSubAgentToolCallLogger } from '../common/sub-agent.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
import { getPlatform } from '../../utils/platform-utils.js';
|
|
7
|
-
import {
|
|
7
|
+
import { PPT_DESIGN_PROMPT, validateSlideHtml, extractLayoutHint, buildContentFillJsonPrompt, parseContentFillJson, buildContentSlideHtml, } from './powerpoint-create-prompts.js';
|
|
8
8
|
const DEFAULT_DESIGN = {
|
|
9
9
|
primary_color: '#1B2A4A',
|
|
10
10
|
accent_color: '#00D4AA',
|
|
@@ -17,6 +17,7 @@ const DEFAULT_DESIGN = {
|
|
|
17
17
|
mood: 'modern-minimal',
|
|
18
18
|
design_notes: 'Clean gradients, card-based layouts',
|
|
19
19
|
};
|
|
20
|
+
const MAX_CONCURRENT = 5;
|
|
20
21
|
function extractContent(msg) {
|
|
21
22
|
const content = msg['content'];
|
|
22
23
|
if (content && content.trim())
|
|
@@ -26,6 +27,23 @@ function extractContent(msg) {
|
|
|
26
27
|
return reasoning;
|
|
27
28
|
return '';
|
|
28
29
|
}
|
|
30
|
+
function hasPlaceholderText(html) {
|
|
31
|
+
const placeholderPatterns = [
|
|
32
|
+
'Card title (2-5 words)',
|
|
33
|
+
'Detail with number/data',
|
|
34
|
+
'single emoji',
|
|
35
|
+
'Display value (e.g.',
|
|
36
|
+
'Category name',
|
|
37
|
+
'1-2 sentence key insight',
|
|
38
|
+
'Another detail',
|
|
39
|
+
'Third point',
|
|
40
|
+
'Fourth point',
|
|
41
|
+
'Brief context',
|
|
42
|
+
'Segment name',
|
|
43
|
+
];
|
|
44
|
+
const lowerHtml = html.toLowerCase();
|
|
45
|
+
return placeholderPatterns.some(p => lowerHtml.includes(p.toLowerCase()));
|
|
46
|
+
}
|
|
29
47
|
function validateAndFixPlan(plan) {
|
|
30
48
|
if (!plan.design)
|
|
31
49
|
return 'Missing design object';
|
|
@@ -35,6 +53,9 @@ function validateAndFixPlan(plan) {
|
|
|
35
53
|
if (!Array.isArray(plan.slides) || plan.slides.length < 3) {
|
|
36
54
|
return 'slides array must have at least 3 entries';
|
|
37
55
|
}
|
|
56
|
+
if (plan.slides.length < 10) {
|
|
57
|
+
return `Only ${plan.slides.length} slides — minimum 10 required (aim for 10-12). Add more content slides with specific data.`;
|
|
58
|
+
}
|
|
38
59
|
if (plan.slides[0]?.type !== 'title') {
|
|
39
60
|
logger.info('Auto-fixing: first slide type changed to "title"');
|
|
40
61
|
plan.slides[0].type = 'title';
|
|
@@ -47,7 +68,23 @@ function validateAndFixPlan(plan) {
|
|
|
47
68
|
for (let i = 0; i < plan.slides.length; i++) {
|
|
48
69
|
if (!plan.slides[i].title) {
|
|
49
70
|
plan.slides[i].title = `Slide ${i + 1}`;
|
|
50
|
-
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const layoutOnlyPatterns = [
|
|
74
|
+
/^(?:전체\s*배경|왼쪽에|오른쪽에|중앙에|상단에|하단에)/,
|
|
75
|
+
/#[0-9a-fA-F]{3,8}에서.*그라데이션/,
|
|
76
|
+
/(?:accent_light|primary|gradient_end)\s*(?:배경|글씨|색상)/,
|
|
77
|
+
/^(?:CSS|flexbox|grid|conic-gradient|linear-gradient)/i,
|
|
78
|
+
];
|
|
79
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
80
|
+
const slide = plan.slides[i];
|
|
81
|
+
if (slide.type === 'title' || slide.type === 'closing')
|
|
82
|
+
continue;
|
|
83
|
+
const cd = slide.content_direction || '';
|
|
84
|
+
const hasNumbers = /\d/.test(cd);
|
|
85
|
+
const isLayoutOnly = layoutOnlyPatterns.some(p => p.test(cd));
|
|
86
|
+
if (isLayoutOnly && !hasNumbers) {
|
|
87
|
+
return `Slide ${i + 1} "${slide.title}" content_direction contains layout instructions instead of actual data.`;
|
|
51
88
|
}
|
|
52
89
|
}
|
|
53
90
|
return null;
|
|
@@ -130,19 +167,15 @@ function parseJsonPlan(raw) {
|
|
|
130
167
|
cleaned = cleaned.replace(/^```(?:json|JSON)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
131
168
|
}
|
|
132
169
|
const firstBrace = cleaned.indexOf('{');
|
|
133
|
-
if (firstBrace > 0)
|
|
170
|
+
if (firstBrace > 0)
|
|
134
171
|
cleaned = cleaned.slice(firstBrace);
|
|
135
|
-
}
|
|
136
172
|
const lastBrace = cleaned.lastIndexOf('}');
|
|
137
|
-
if (lastBrace >= 0 && lastBrace < cleaned.length - 1)
|
|
173
|
+
if (lastBrace >= 0 && lastBrace < cleaned.length - 1)
|
|
138
174
|
cleaned = cleaned.slice(0, lastBrace + 1);
|
|
139
|
-
}
|
|
140
175
|
try {
|
|
141
176
|
return JSON.parse(cleaned);
|
|
142
177
|
}
|
|
143
|
-
catch
|
|
144
|
-
logger.debug('parseJsonPlan: direct parse failed', { error: String(e), length: cleaned.length });
|
|
145
|
-
}
|
|
178
|
+
catch { }
|
|
146
179
|
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
147
180
|
if (!match)
|
|
148
181
|
return null;
|
|
@@ -154,9 +187,7 @@ function parseJsonPlan(raw) {
|
|
|
154
187
|
try {
|
|
155
188
|
return JSON.parse(repaired);
|
|
156
189
|
}
|
|
157
|
-
catch
|
|
158
|
-
logger.debug('parseJsonPlan: repaired parse failed', { error: String(e) });
|
|
159
|
-
}
|
|
190
|
+
catch { }
|
|
160
191
|
try {
|
|
161
192
|
let final = repaired;
|
|
162
193
|
let braces = 0, brackets = 0;
|
|
@@ -187,9 +218,9 @@ function parseJsonPlan(raw) {
|
|
|
187
218
|
}
|
|
188
219
|
if (inStr)
|
|
189
220
|
final += '"';
|
|
190
|
-
for (let
|
|
221
|
+
for (let x = 0; x < brackets; x++)
|
|
191
222
|
final += ']';
|
|
192
|
-
for (let
|
|
223
|
+
for (let x = 0; x < braces; x++)
|
|
193
224
|
final += '}';
|
|
194
225
|
return JSON.parse(final);
|
|
195
226
|
}
|
|
@@ -197,71 +228,22 @@ function parseJsonPlan(raw) {
|
|
|
197
228
|
return null;
|
|
198
229
|
}
|
|
199
230
|
}
|
|
200
|
-
function
|
|
201
|
-
const trimmed = raw.trim();
|
|
202
|
-
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html')) {
|
|
203
|
-
return trimmed;
|
|
204
|
-
}
|
|
205
|
-
const fenceMatch = trimmed.match(/```(?:html)?\s*\n([\s\S]*?)\n```/);
|
|
206
|
-
if (fenceMatch?.[1]) {
|
|
207
|
-
return fenceMatch[1].trim();
|
|
208
|
-
}
|
|
209
|
-
const docMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
210
|
-
if (docMatch?.[1]) {
|
|
211
|
-
return docMatch[1].trim();
|
|
212
|
-
}
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
function injectViewportCss(html, backgroundColor) {
|
|
231
|
+
function injectEdgeSizing(html, backgroundColor) {
|
|
216
232
|
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
217
233
|
const bgColor = backgroundColor || '#000000';
|
|
218
|
-
const
|
|
219
|
-
const overrideCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word}html{width:2040px!important;height:1200px!important;overflow:hidden!important;margin:0!important;background-color:${bgColor}!important;zoom:1!important}body{width:1920px!important;height:1080px!important;min-width:1920px!important;min-height:1080px!important;overflow:hidden!important;margin:0!important;display:flex!important;flex-direction:column!important;zoom:1!important}body>div,body>main,body>section,body>article,body>header,body>footer{max-width:none!important;zoom:1!important}body>*>div,body>*>main,body>*>section,body>*>article{max-width:none!important;zoom:1!important}body>*:nth-child(2){flex:1!important;align-items:stretch!important;align-content:stretch!important;justify-content:center!important}</style>`;
|
|
220
|
-
const injection = viewportMeta + overrideCss;
|
|
234
|
+
const sizingCss = `<style id="edge-sizing">html{width:2040px!important;height:1200px!important;overflow:hidden!important;margin:0!important;background-color:${bgColor}!important;zoom:1!important}body{width:1920px!important;height:1080px!important;min-width:1920px!important;min-height:1080px!important;overflow:hidden!important;margin:0!important;zoom:1!important}</style>`;
|
|
221
235
|
if (result.includes('</head>')) {
|
|
222
|
-
result = result.replace('</head>', `${
|
|
236
|
+
result = result.replace('</head>', `${sizingCss}</head>`);
|
|
223
237
|
}
|
|
224
238
|
else if (result.includes('<head>')) {
|
|
225
|
-
result = result.replace('<head>', `<head>${
|
|
239
|
+
result = result.replace('<head>', `<head>${sizingCss}`);
|
|
226
240
|
}
|
|
227
241
|
else if (result.includes('<html')) {
|
|
228
|
-
result = result.replace(/<html[^>]*>/, (
|
|
242
|
+
result = result.replace(/<html[^>]*>/, (m) => `${m}<head>${sizingCss}</head>`);
|
|
229
243
|
}
|
|
230
244
|
else {
|
|
231
|
-
result =
|
|
232
|
-
}
|
|
233
|
-
return result;
|
|
234
|
-
}
|
|
235
|
-
function enforceMinFontSize(html) {
|
|
236
|
-
let result = html.replace(/font-size:\s*(\d+(?:\.\d+)?)px/g, (_match, size) => {
|
|
237
|
-
const px = parseFloat(size);
|
|
238
|
-
return (px > 12 && px < 22) ? 'font-size:22px' : _match;
|
|
239
|
-
});
|
|
240
|
-
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)pt/g, (_match, size) => {
|
|
241
|
-
const pt = parseFloat(size);
|
|
242
|
-
const px = pt * 1.333;
|
|
243
|
-
return (px > 12 && px < 22) ? 'font-size:22px' : _match;
|
|
244
|
-
});
|
|
245
|
-
return result;
|
|
246
|
-
}
|
|
247
|
-
function ensureSafeBodyPadding(html) {
|
|
248
|
-
const safeStyle = `<style>body{padding-top:20px!important;padding-bottom:40px!important;min-height:auto!important}body>div:last-child,body>section:last-child{margin-bottom:0!important;padding-bottom:0!important}</style>`;
|
|
249
|
-
if (html.includes('</head>')) {
|
|
250
|
-
return html.replace('</head>', `${safeStyle}</head>`);
|
|
245
|
+
result = sizingCss + result;
|
|
251
246
|
}
|
|
252
|
-
return html;
|
|
253
|
-
}
|
|
254
|
-
function removeAbsolutePositioning(html) {
|
|
255
|
-
let result = html.replace(/style="([^"]*)position:\s*absolute([^"]*)"/gi, (match, before, after) => {
|
|
256
|
-
if (/(?:width|height):\s*[1-5]?\dpx/i.test(before + after) || /opacity:\s*0\.[0-4]/i.test(before + after)) {
|
|
257
|
-
return match;
|
|
258
|
-
}
|
|
259
|
-
return `style="${before}position:relative${after}"`;
|
|
260
|
-
});
|
|
261
|
-
result = result.replace(/(\.(?:title|heading|label|text|badge|stat|value|card|item|metric)[^{]*\{[^}]*)position:\s*absolute/gi, '$1position:relative');
|
|
262
|
-
result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*\)/gi, 'transform:none');
|
|
263
|
-
result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*,\s*0\.\d+\s*\)/gi, 'transform:none');
|
|
264
|
-
result = result.replace(/zoom:\s*0\.\d+/gi, 'zoom:1');
|
|
265
247
|
return result;
|
|
266
248
|
}
|
|
267
249
|
function injectTitleContrastFix(html, designTextColor) {
|
|
@@ -290,40 +272,10 @@ function injectTitleContrastFix(html, designTextColor) {
|
|
|
290
272
|
}
|
|
291
273
|
return html + script;
|
|
292
274
|
}
|
|
293
|
-
function injectMeasureCss(html) {
|
|
294
|
-
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
295
|
-
const viewportMeta = `<meta name="viewport" content="width=2040">`;
|
|
296
|
-
const measureCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word}html{width:2040px!important;margin:0!important}body{width:1920px!important;min-width:1920px!important;min-height:1080px!important;margin:0!important;display:flex!important;flex-direction:column!important}body>div,body>main,body>section,body>article,body>header,body>footer{max-width:none!important}</style>`;
|
|
297
|
-
const injection = viewportMeta + measureCss;
|
|
298
|
-
if (result.includes('</head>')) {
|
|
299
|
-
result = result.replace('</head>', `${injection}</head>`);
|
|
300
|
-
}
|
|
301
|
-
else if (result.includes('<head>')) {
|
|
302
|
-
result = result.replace('<head>', `<head>${injection}`);
|
|
303
|
-
}
|
|
304
|
-
else if (result.includes('<html')) {
|
|
305
|
-
result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${injection}</head>`);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
result = injection + result;
|
|
309
|
-
}
|
|
310
|
-
const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
|
|
311
|
-
if (result.includes('</body>')) {
|
|
312
|
-
result = result.replace('</body>', `${measureScript}</body>`);
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
result += measureScript;
|
|
316
|
-
}
|
|
317
|
-
return result;
|
|
318
|
-
}
|
|
319
275
|
function escapeHtml(text) {
|
|
320
|
-
return text
|
|
321
|
-
.replace(/&/g, '&')
|
|
322
|
-
.replace(/</g, '<')
|
|
323
|
-
.replace(/>/g, '>')
|
|
324
|
-
.replace(/"/g, '"');
|
|
276
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
325
277
|
}
|
|
326
|
-
function buildTitleSlideHtml(design, mainTitle, subtitle, date,
|
|
278
|
+
function buildTitleSlideHtml(design, mainTitle, subtitle, date, _slideNum) {
|
|
327
279
|
return `<!DOCTYPE html>
|
|
328
280
|
<html lang="ko">
|
|
329
281
|
<head>
|
|
@@ -335,29 +287,21 @@ html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
|
335
287
|
body {
|
|
336
288
|
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
337
289
|
display: flex;
|
|
290
|
+
flex-direction: column;
|
|
338
291
|
align-items: center;
|
|
339
292
|
justify-content: center;
|
|
340
|
-
position: relative;
|
|
341
293
|
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
342
294
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
.d1 { width: 700px; height: 700px; top: -250px; right: -150px; }
|
|
350
|
-
.d2 { width: 500px; height: 500px; bottom: -180px; left: -120px; }
|
|
351
|
-
.d3 { width: 250px; height: 250px; top: 40%; left: 8%; background: rgba(255,255,255,0.02); }
|
|
352
|
-
.d4 { width: 180px; height: 180px; bottom: 15%; right: 12%; background: rgba(255,255,255,0.02); }
|
|
353
|
-
.top-accent {
|
|
354
|
-
position: absolute;
|
|
355
|
-
top: 0; left: 0; width: 100%; height: 6px;
|
|
295
|
+
body::before {
|
|
296
|
+
content: '';
|
|
297
|
+
display: block;
|
|
298
|
+
width: 100%;
|
|
299
|
+
height: 6px;
|
|
356
300
|
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
301
|
+
flex-shrink: 0;
|
|
357
302
|
}
|
|
358
|
-
.content {
|
|
303
|
+
.slide-content {
|
|
359
304
|
text-align: center;
|
|
360
|
-
z-index: 1;
|
|
361
305
|
max-width: 1400px;
|
|
362
306
|
padding: 0 60px;
|
|
363
307
|
}
|
|
@@ -391,32 +335,22 @@ body {
|
|
|
391
335
|
color: rgba(255,255,255,0.55);
|
|
392
336
|
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
393
337
|
}
|
|
394
|
-
.page-num {
|
|
395
|
-
position: absolute;
|
|
396
|
-
bottom: 24px; right: 44px;
|
|
397
|
-
font-size: 13px;
|
|
398
|
-
color: rgba(255,255,255,0.35);
|
|
399
|
-
}
|
|
400
338
|
</style>
|
|
401
339
|
</head>
|
|
402
340
|
<body>
|
|
403
|
-
<div class="
|
|
404
|
-
<div class="decor d1"></div>
|
|
405
|
-
<div class="decor d2"></div>
|
|
406
|
-
<div class="decor d3"></div>
|
|
407
|
-
<div class="decor d4"></div>
|
|
408
|
-
<div class="content">
|
|
341
|
+
<div class="slide-content">
|
|
409
342
|
<div class="main-title">${escapeHtml(mainTitle)}</div>
|
|
410
343
|
<div class="accent-bar"></div>
|
|
411
344
|
${subtitle ? `<div class="subtitle">${escapeHtml(subtitle)}</div>` : ''}
|
|
412
345
|
<div class="date-text">${escapeHtml(date)}</div>
|
|
413
346
|
</div>
|
|
414
|
-
<
|
|
347
|
+
<style>body{align-items:center!important;justify-content:center!important}.slide-content{flex:unset!important;min-height:unset!important;display:flex!important;flex-direction:column!important;align-items:center!important;justify-content:center!important}.slide-content>*{flex:unset!important;min-height:unset!important}</style>
|
|
415
348
|
</body>
|
|
416
349
|
</html>`;
|
|
417
350
|
}
|
|
418
|
-
function buildClosingSlideHtml(design, companyName,
|
|
351
|
+
function buildClosingSlideHtml(design, companyName, _slideNum, language, tagline) {
|
|
419
352
|
const thankYou = language === 'ko' ? '감사합니다' : 'Thank You';
|
|
353
|
+
const taglineHtml = tagline ? `<div class="tagline">${escapeHtml(tagline)}</div>` : '';
|
|
420
354
|
return `<!DOCTYPE html>
|
|
421
355
|
<html lang="${language}">
|
|
422
356
|
<head>
|
|
@@ -428,75 +362,77 @@ html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
|
428
362
|
body {
|
|
429
363
|
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
430
364
|
display: flex;
|
|
365
|
+
flex-direction: column;
|
|
431
366
|
align-items: center;
|
|
432
367
|
justify-content: center;
|
|
433
|
-
position: relative;
|
|
434
368
|
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
435
369
|
}
|
|
436
|
-
|
|
370
|
+
body::before {
|
|
371
|
+
content: '';
|
|
437
372
|
position: absolute;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
373
|
+
top: 0; left: 0; right: 0;
|
|
374
|
+
height: 6px;
|
|
375
|
+
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
441
376
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.bottom-accent {
|
|
377
|
+
body::after {
|
|
378
|
+
content: '';
|
|
445
379
|
position: absolute;
|
|
446
|
-
bottom: 0; left: 0;
|
|
380
|
+
bottom: 0; left: 0; right: 0;
|
|
381
|
+
height: 6px;
|
|
447
382
|
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
448
383
|
}
|
|
449
|
-
.content {
|
|
384
|
+
.slide-content {
|
|
450
385
|
text-align: center;
|
|
451
|
-
|
|
386
|
+
max-width: 1200px;
|
|
452
387
|
}
|
|
453
388
|
.thank-you {
|
|
454
|
-
font-size:
|
|
389
|
+
font-size: 104px;
|
|
455
390
|
font-weight: 800;
|
|
456
391
|
color: #ffffff;
|
|
457
392
|
letter-spacing: -1px;
|
|
458
393
|
text-shadow: 0 6px 40px rgba(0,0,0,0.25);
|
|
459
|
-
margin-bottom:
|
|
394
|
+
margin-bottom: 36px;
|
|
460
395
|
}
|
|
461
396
|
.accent-bar {
|
|
462
|
-
width:
|
|
397
|
+
width: 120px; height: 5px;
|
|
463
398
|
background: ${design.accent_color};
|
|
464
|
-
margin: 0 auto
|
|
399
|
+
margin: 0 auto 36px;
|
|
465
400
|
border-radius: 3px;
|
|
466
401
|
box-shadow: 0 0 20px ${design.accent_color}40;
|
|
467
402
|
}
|
|
468
403
|
.company {
|
|
469
|
-
font-size:
|
|
470
|
-
font-weight:
|
|
471
|
-
color: rgba(255,255,255,0.
|
|
404
|
+
font-size: 44px;
|
|
405
|
+
font-weight: 600;
|
|
406
|
+
color: rgba(255,255,255,0.88);
|
|
472
407
|
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
408
|
+
margin-bottom: 20px;
|
|
473
409
|
}
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
410
|
+
.tagline {
|
|
411
|
+
font-size: 28px;
|
|
412
|
+
font-weight: 400;
|
|
413
|
+
color: rgba(255,255,255,0.60);
|
|
414
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
415
|
+
line-height: 1.6;
|
|
416
|
+
max-width: 900px;
|
|
417
|
+
margin: 0 auto;
|
|
479
418
|
}
|
|
480
419
|
</style>
|
|
481
420
|
</head>
|
|
482
421
|
<body>
|
|
483
|
-
<div class="
|
|
484
|
-
<div class="decor d2"></div>
|
|
485
|
-
<div class="bottom-accent"></div>
|
|
486
|
-
<div class="content">
|
|
422
|
+
<div class="slide-content">
|
|
487
423
|
<div class="thank-you">${escapeHtml(thankYou)}</div>
|
|
488
424
|
<div class="accent-bar"></div>
|
|
489
425
|
<div class="company">${escapeHtml(companyName)}</div>
|
|
426
|
+
${taglineHtml}
|
|
490
427
|
</div>
|
|
491
|
-
<
|
|
428
|
+
<style>body{align-items:center!important;justify-content:center!important}.slide-content{flex:unset!important;min-height:unset!important;display:flex!important;flex-direction:column!important;align-items:center!important;justify-content:center!important}.slide-content>*{flex:unset!important;min-height:unset!important}</style>
|
|
492
429
|
</body>
|
|
493
430
|
</html>`;
|
|
494
431
|
}
|
|
495
432
|
function isOverviewSlide(title, slideIndex) {
|
|
496
433
|
if (slideIndex !== 1)
|
|
497
434
|
return false;
|
|
498
|
-
|
|
499
|
-
return overviewKeywords.test(title);
|
|
435
|
+
return /개요|목차|overview|agenda|outline|순서|발표\s*구성|contents|목록/i.test(title);
|
|
500
436
|
}
|
|
501
437
|
function parseOverviewItems(contentDirection) {
|
|
502
438
|
const items = [];
|
|
@@ -520,11 +456,8 @@ function parseOverviewItems(contentDirection) {
|
|
|
520
456
|
}
|
|
521
457
|
function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
|
|
522
458
|
const badgeColors = [
|
|
523
|
-
design.primary_color,
|
|
524
|
-
design.accent_color,
|
|
525
|
-
design.gradient_end,
|
|
526
|
-
design.primary_color,
|
|
527
|
-
design.accent_color,
|
|
459
|
+
design.primary_color, design.accent_color, design.gradient_end,
|
|
460
|
+
design.primary_color, design.accent_color,
|
|
528
461
|
];
|
|
529
462
|
const itemCount = items.length;
|
|
530
463
|
const topRow = itemCount <= 3 ? items : items.slice(0, Math.ceil(itemCount / 2));
|
|
@@ -584,7 +517,6 @@ body {
|
|
|
584
517
|
display: flex; flex-direction: column;
|
|
585
518
|
align-items: center; text-align: center;
|
|
586
519
|
gap: 12px;
|
|
587
|
-
transition: none;
|
|
588
520
|
}
|
|
589
521
|
.badge {
|
|
590
522
|
width: 44px; height: 44px;
|
|
@@ -627,6 +559,53 @@ body {
|
|
|
627
559
|
</body>
|
|
628
560
|
</html>`;
|
|
629
561
|
}
|
|
562
|
+
function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
|
|
563
|
+
const items = [];
|
|
564
|
+
const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d+\.\s*/);
|
|
565
|
+
for (const part of parts) {
|
|
566
|
+
const cleaned = part.replace(/Layout:.*$/i, '').trim();
|
|
567
|
+
if (cleaned.length > 5) {
|
|
568
|
+
const sentence = cleaned.split(/[.。!]\s/)[0] || cleaned;
|
|
569
|
+
items.push(sentence.slice(0, 80));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const bulletItems = items.slice(0, 6);
|
|
573
|
+
const bulletsHtml = bulletItems.map(item => `<div class="bullet"><span class="dot"></span><span>${escapeHtml(item)}</span></div>`).join('\n ');
|
|
574
|
+
return `<!DOCTYPE html>
|
|
575
|
+
<html lang="ko">
|
|
576
|
+
<head>
|
|
577
|
+
<meta charset="UTF-8">
|
|
578
|
+
<style>
|
|
579
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
580
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
581
|
+
body {
|
|
582
|
+
background: ${design.background_color};
|
|
583
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
584
|
+
padding: 80px 100px;
|
|
585
|
+
display: flex;
|
|
586
|
+
flex-direction: column;
|
|
587
|
+
}
|
|
588
|
+
.header { margin-bottom: 48px; }
|
|
589
|
+
.slide-num { font-size: 14px; color: ${design.accent_color}; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
|
|
590
|
+
h1 { font-size: 52px; font-weight: 700; color: ${design.text_color}; font-family: "${design.font_title}", "Segoe UI", sans-serif; line-height: 1.2; }
|
|
591
|
+
.accent-bar { width: 80px; height: 4px; background: ${design.accent_color}; margin-top: 20px; border-radius: 2px; }
|
|
592
|
+
.content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 20px; }
|
|
593
|
+
.bullet { display: flex; align-items: flex-start; gap: 16px; font-size: 26px; color: ${design.text_color}; line-height: 1.5; }
|
|
594
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; background: ${design.accent_color}; flex-shrink: 0; margin-top: 10px; }
|
|
595
|
+
</style>
|
|
596
|
+
</head>
|
|
597
|
+
<body>
|
|
598
|
+
<div class="header">
|
|
599
|
+
<div class="slide-num">SLIDE ${slideNum}</div>
|
|
600
|
+
<h1>${escapeHtml(title)}</h1>
|
|
601
|
+
<div class="accent-bar"></div>
|
|
602
|
+
</div>
|
|
603
|
+
<div class="content">
|
|
604
|
+
${bulletsHtml}
|
|
605
|
+
</div>
|
|
606
|
+
</body>
|
|
607
|
+
</html>`;
|
|
608
|
+
}
|
|
630
609
|
function getTempDir() {
|
|
631
610
|
const platform = getPlatform();
|
|
632
611
|
if (platform === 'wsl') {
|
|
@@ -653,73 +632,26 @@ function normalizeDesign(raw) {
|
|
|
653
632
|
design_notes: raw['design_notes'] || DEFAULT_DESIGN.design_notes,
|
|
654
633
|
};
|
|
655
634
|
}
|
|
656
|
-
async function
|
|
657
|
-
const startTime = Date.now();
|
|
658
|
-
const phaseLogger = getSubAgentPhaseLogger();
|
|
659
|
-
const toolCallLogger = getSubAgentToolCallLogger();
|
|
660
|
-
let totalToolCalls = 0;
|
|
661
|
-
const timestamp = Date.now();
|
|
662
|
-
logger.enter('PPT-Create.runStructured');
|
|
663
|
-
const hasKorean = /[\uac00-\ud7af\u1100-\u11ff]/.test(instruction);
|
|
664
|
-
const language = hasKorean ? 'ko' : 'en';
|
|
665
|
-
if (phaseLogger)
|
|
666
|
-
phaseLogger('powerpoint-create', 'enhancement', 'Generating creative guidance...');
|
|
667
|
-
let guidance = '';
|
|
668
|
-
try {
|
|
669
|
-
const enhRes = await llmClient.chatCompletion({
|
|
670
|
-
messages: [
|
|
671
|
-
{ role: 'system', content: PPT_CREATE_ENHANCEMENT_PROMPT },
|
|
672
|
-
{ role: 'user', content: instruction },
|
|
673
|
-
],
|
|
674
|
-
temperature: 0.7,
|
|
675
|
-
max_tokens: 2000,
|
|
676
|
-
});
|
|
677
|
-
const enhMsg = enhRes.choices[0]?.message;
|
|
678
|
-
guidance = enhMsg ? extractContent(enhMsg) : '';
|
|
679
|
-
if (guidance.length < 500) {
|
|
680
|
-
logger.warn('PPT enhancement too short, retrying', { length: guidance.length });
|
|
681
|
-
const retryEnhRes = await llmClient.chatCompletion({
|
|
682
|
-
messages: [
|
|
683
|
-
{ role: 'system', content: PPT_CREATE_ENHANCEMENT_PROMPT },
|
|
684
|
-
{ role: 'user', content: `${instruction}\n\nIMPORTANT: Produce a COMPLETE, DETAILED design system and content plan. Include ALL 7 sections: DOCUMENT_TYPE, AUDIENCE, MOOD, COLOR PALETTE (6 hex values), FONTS, SLIDE PLAN (10-15 slides), DESIGN NOTES. Do NOT stop early.` },
|
|
685
|
-
],
|
|
686
|
-
temperature: 0.7,
|
|
687
|
-
max_tokens: 2000,
|
|
688
|
-
});
|
|
689
|
-
const retryMsg = retryEnhRes.choices[0]?.message;
|
|
690
|
-
const retryGuidance = retryMsg ? extractContent(retryMsg) : '';
|
|
691
|
-
if (retryGuidance.length > guidance.length) {
|
|
692
|
-
guidance = retryGuidance;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (phaseLogger)
|
|
696
|
-
phaseLogger('powerpoint-create', 'enhancement', `Done (${guidance.length} chars)`);
|
|
697
|
-
}
|
|
698
|
-
catch (e) {
|
|
699
|
-
logger.warn('PPT enhancement failed, proceeding without', { error: String(e) });
|
|
700
|
-
}
|
|
701
|
-
const enhancedInstruction = guidance
|
|
702
|
-
? `${instruction}\n\n═══ CREATIVE GUIDANCE ═══\n${guidance}\n═══ END GUIDANCE ═══`
|
|
703
|
-
: instruction;
|
|
635
|
+
async function runDesignPhase(llmClient, instruction, phaseLogger) {
|
|
704
636
|
if (phaseLogger)
|
|
705
|
-
phaseLogger('powerpoint-create', '
|
|
637
|
+
phaseLogger('powerpoint-create', 'design', 'Generating design system + slide plan...');
|
|
706
638
|
let plan = null;
|
|
707
639
|
try {
|
|
708
|
-
const
|
|
640
|
+
const res = await llmClient.chatCompletion({
|
|
709
641
|
messages: [
|
|
710
|
-
{ role: 'system', content:
|
|
711
|
-
{ role: 'user', content:
|
|
642
|
+
{ role: 'system', content: PPT_DESIGN_PROMPT },
|
|
643
|
+
{ role: 'user', content: instruction },
|
|
712
644
|
],
|
|
713
|
-
temperature: 0.
|
|
645
|
+
temperature: 0.5,
|
|
714
646
|
max_tokens: 8000,
|
|
715
647
|
});
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
const
|
|
648
|
+
const msg = res.choices[0]?.message;
|
|
649
|
+
const rawPlan = msg ? extractContent(msg) : '';
|
|
650
|
+
const finishReason = res.choices[0]?.finish_reason;
|
|
719
651
|
if (finishReason === 'length') {
|
|
720
|
-
logger.warn('PPT
|
|
652
|
+
logger.warn('PPT design response was truncated (finish_reason=length)');
|
|
721
653
|
}
|
|
722
|
-
logger.debug('PPT
|
|
654
|
+
logger.debug('PPT design raw response', { length: rawPlan.length, finishReason, first200: rawPlan.slice(0, 200) });
|
|
723
655
|
plan = rawPlan ? parseJsonPlan(rawPlan) : null;
|
|
724
656
|
if (plan) {
|
|
725
657
|
plan.design = normalizeDesign(plan.design);
|
|
@@ -727,13 +659,15 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
727
659
|
if (validationError) {
|
|
728
660
|
logger.warn('PPT plan validation failed', { error: validationError });
|
|
729
661
|
if (phaseLogger)
|
|
730
|
-
phaseLogger('powerpoint-create', '
|
|
662
|
+
phaseLogger('powerpoint-create', 'design', `Validation failed: ${validationError}. Retrying...`);
|
|
731
663
|
const retryRes = await llmClient.chatCompletion({
|
|
732
664
|
messages: [
|
|
733
|
-
{ role: 'system', content:
|
|
734
|
-
{ role: 'user', content:
|
|
665
|
+
{ role: 'system', content: PPT_DESIGN_PROMPT },
|
|
666
|
+
{ role: 'user', content: instruction },
|
|
667
|
+
{ role: 'assistant', content: rawPlan },
|
|
668
|
+
{ role: 'user', content: `ERROR: ${validationError}\n\nFix the issues and output the corrected JSON. Remember: aim for 10-12 slides with REAL content data.` },
|
|
735
669
|
],
|
|
736
|
-
temperature: 0.
|
|
670
|
+
temperature: 0.3,
|
|
737
671
|
max_tokens: 8000,
|
|
738
672
|
});
|
|
739
673
|
const retryMsg = retryRes.choices[0]?.message;
|
|
@@ -745,7 +679,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
745
679
|
if (!retryError) {
|
|
746
680
|
plan = retryPlan;
|
|
747
681
|
if (phaseLogger)
|
|
748
|
-
phaseLogger('powerpoint-create', '
|
|
682
|
+
phaseLogger('powerpoint-create', 'design', `Retry succeeded (${plan.slides.length} slides)`);
|
|
749
683
|
}
|
|
750
684
|
else {
|
|
751
685
|
plan = null;
|
|
@@ -757,199 +691,188 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
757
691
|
}
|
|
758
692
|
else {
|
|
759
693
|
if (phaseLogger)
|
|
760
|
-
phaseLogger('powerpoint-create', '
|
|
694
|
+
phaseLogger('powerpoint-create', 'design', `Done (${plan.slides.length} slides, mood: ${plan.design.mood})`);
|
|
761
695
|
}
|
|
762
696
|
}
|
|
763
|
-
else {
|
|
764
|
-
logger.warn('PPT JSON plan parsing failed');
|
|
765
|
-
if (phaseLogger)
|
|
766
|
-
phaseLogger('powerpoint-create', 'planning', 'JSON parsing failed. Falling back.');
|
|
767
|
-
}
|
|
768
697
|
}
|
|
769
698
|
catch (e) {
|
|
770
|
-
logger.warn('PPT
|
|
771
|
-
if (phaseLogger)
|
|
772
|
-
phaseLogger('powerpoint-create', 'planning', 'Planning error. Falling back.');
|
|
773
|
-
}
|
|
774
|
-
if (!plan) {
|
|
775
|
-
logger.error('PPT planning failed after retries — cannot create presentation');
|
|
776
|
-
return {
|
|
777
|
-
success: false,
|
|
778
|
-
error: 'Failed to generate presentation plan. Please try again.',
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
if (plan.slides.length > 15) {
|
|
782
|
-
logger.warn(`PPT plan has ${plan.slides.length} slides, capping to 15`);
|
|
783
|
-
const firstSlide = plan.slides[0];
|
|
784
|
-
const lastSlide = plan.slides[plan.slides.length - 1];
|
|
785
|
-
const contentSlides = plan.slides.slice(1, -1).slice(0, 13);
|
|
786
|
-
plan.slides = [firstSlide, ...contentSlides, lastSlide];
|
|
787
|
-
if (phaseLogger)
|
|
788
|
-
phaseLogger('powerpoint-create', 'planning', `Capped to ${plan.slides.length} slides`);
|
|
699
|
+
logger.warn('PPT design failed', { error: String(e) });
|
|
789
700
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
701
|
+
return plan;
|
|
702
|
+
}
|
|
703
|
+
async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, _totalSlides, language) {
|
|
704
|
+
const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
|
|
705
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
706
|
+
logger.info(`Slide ${slideIndex + 1}: Generating code template "${layoutType}"`);
|
|
707
|
+
const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
|
|
708
|
+
try {
|
|
709
|
+
const jsonRes = await llmClient.chatCompletion({
|
|
710
|
+
messages: [
|
|
711
|
+
{ role: 'system', content: jsonPrompt },
|
|
712
|
+
{ role: 'user', content: 'Output the JSON now.' },
|
|
713
|
+
],
|
|
714
|
+
temperature: 0.3,
|
|
715
|
+
max_tokens: 2000,
|
|
716
|
+
});
|
|
717
|
+
const jsonMsg = jsonRes.choices[0]?.message;
|
|
718
|
+
const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
|
|
719
|
+
let slideData = parseContentFillJson(jsonRaw, layoutType);
|
|
720
|
+
if (!slideData) {
|
|
721
|
+
logger.warn(`Slide ${slideIndex + 1}: JSON fill parse failed (attempt 1). Layout: ${layoutType}. Raw length: ${jsonRaw.length}. First 200 chars: ${jsonRaw.slice(0, 200)}`);
|
|
722
|
+
const retryRes = await llmClient.chatCompletion({
|
|
723
|
+
messages: [
|
|
724
|
+
{ role: 'system', content: jsonPrompt },
|
|
725
|
+
{ role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
|
|
726
|
+
],
|
|
727
|
+
temperature: 0.2,
|
|
728
|
+
max_tokens: 2000,
|
|
729
|
+
});
|
|
730
|
+
const retryMsg = retryRes.choices[0]?.message;
|
|
731
|
+
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
732
|
+
slideData = parseContentFillJson(retryRaw, layoutType);
|
|
733
|
+
if (!slideData) {
|
|
734
|
+
logger.warn(`Slide ${slideIndex + 1}: JSON fill parse failed (attempt 2). Raw length: ${retryRaw.length}. First 200 chars: ${retryRaw.slice(0, 200)}`);
|
|
804
735
|
}
|
|
805
736
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const titleSlidePlan = plan.slides.find(s => s.type === 'title');
|
|
810
|
-
const rawTitleText = titleSlidePlan?.title || '';
|
|
811
|
-
const titleSeps = [' - ', ' – ', ' — ', ': ', ' | '];
|
|
812
|
-
let companyName = rawTitleText;
|
|
813
|
-
let titleSubtitle = '';
|
|
814
|
-
for (const sep of titleSeps) {
|
|
815
|
-
const idx = rawTitleText.indexOf(sep);
|
|
816
|
-
if (idx > 0) {
|
|
817
|
-
companyName = rawTitleText.slice(0, idx).trim();
|
|
818
|
-
titleSubtitle = rawTitleText.slice(idx + sep.length).trim();
|
|
819
|
-
break;
|
|
737
|
+
if (slideData) {
|
|
738
|
+
const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
|
|
739
|
+
return { html, isCodeTemplate: true };
|
|
820
740
|
}
|
|
821
741
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
826
|
-
titleSubtitle = '';
|
|
827
|
-
}
|
|
828
|
-
const createResult = await powerpointClient.powerpointCreate();
|
|
829
|
-
totalToolCalls++;
|
|
830
|
-
if (toolCallLogger)
|
|
831
|
-
toolCallLogger('powerpoint-create', 'powerpoint_create', {}, createResult.success ? 'Created' : createResult['error'] || '', createResult.success, 0, totalToolCalls);
|
|
832
|
-
if (!createResult.success) {
|
|
833
|
-
return { success: false, error: `Failed to create presentation: ${createResult['error']}` };
|
|
742
|
+
catch (e) {
|
|
743
|
+
logger.warn(`Slide ${slideIndex + 1}: Code template fallback failed: ${e}`);
|
|
834
744
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger) {
|
|
748
|
+
const results = new Map();
|
|
838
749
|
for (let i = 0; i < plan.slides.length; i++) {
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
phaseLogger('powerpoint-create', 'execution', `Rendering slide ${slideNum}/${plan.slides.length}: ${slidePlan.title}`);
|
|
847
|
-
let html = null;
|
|
848
|
-
let htmlPrompt = null;
|
|
849
|
-
if (slidePlan.type === 'title') {
|
|
850
|
-
html = buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, slideNum);
|
|
750
|
+
const slide = plan.slides[i];
|
|
751
|
+
if (slide.type === 'title') {
|
|
752
|
+
results.set(i, {
|
|
753
|
+
index: i,
|
|
754
|
+
html: buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, i + 1),
|
|
755
|
+
isCodeTemplate: true,
|
|
756
|
+
});
|
|
851
757
|
}
|
|
852
|
-
else if (
|
|
853
|
-
|
|
758
|
+
else if (slide.type === 'closing') {
|
|
759
|
+
const closingTagline = (slide.content_direction || '').replace(/감사합니다|thank\s*you/gi, '').trim() || undefined;
|
|
760
|
+
results.set(i, {
|
|
761
|
+
index: i,
|
|
762
|
+
html: buildClosingSlideHtml(plan.design, companyName, i + 1, language, closingTagline),
|
|
763
|
+
isCodeTemplate: true,
|
|
764
|
+
});
|
|
854
765
|
}
|
|
855
|
-
else if (isOverviewSlide(
|
|
856
|
-
const overviewItems = parseOverviewItems(
|
|
766
|
+
else if (isOverviewSlide(slide.title, i)) {
|
|
767
|
+
const overviewItems = parseOverviewItems(slide.content_direction || '');
|
|
857
768
|
if (overviewItems.length >= 2) {
|
|
858
|
-
const firstLine = (
|
|
769
|
+
const firstLine = (slide.content_direction || '').split('\n')[0] || '';
|
|
859
770
|
const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
|
|
860
|
-
|
|
861
|
-
|
|
771
|
+
results.set(i, {
|
|
772
|
+
index: i,
|
|
773
|
+
html: buildOverviewSlideHtml(plan.design, slide.title, overviewSubtitle, overviewItems, i + 1),
|
|
774
|
+
isCodeTemplate: true,
|
|
775
|
+
});
|
|
862
776
|
}
|
|
863
777
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
{ role: 'system', content: htmlPrompt },
|
|
883
|
-
{ role: 'user', content: 'Generate the complete HTML document. Start with <!DOCTYPE html> and end with </html>. No markdown fences.' },
|
|
884
|
-
],
|
|
885
|
-
temperature: 0.2,
|
|
886
|
-
max_tokens: 4000,
|
|
887
|
-
});
|
|
888
|
-
const retryMsg = retryRes.choices[0]?.message;
|
|
889
|
-
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
890
|
-
html = extractHtml(retryRaw);
|
|
891
|
-
}
|
|
778
|
+
}
|
|
779
|
+
const contentIndices = plan.slides
|
|
780
|
+
.map((s, i) => ({ slide: s, index: i }))
|
|
781
|
+
.filter(({ index }) => !results.has(index));
|
|
782
|
+
if (phaseLogger)
|
|
783
|
+
phaseLogger('powerpoint-create', 'html-generation', `Generating ${contentIndices.length} content slides in parallel (batch size ${MAX_CONCURRENT})...`);
|
|
784
|
+
for (let batch = 0; batch < contentIndices.length; batch += MAX_CONCURRENT) {
|
|
785
|
+
const chunk = contentIndices.slice(batch, batch + MAX_CONCURRENT);
|
|
786
|
+
const promises = chunk.map(({ slide, index }) => generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language)
|
|
787
|
+
.then(result => ({ index, result }))
|
|
788
|
+
.catch(err => {
|
|
789
|
+
logger.warn(`Slide ${index + 1}: Generation error: ${err}`);
|
|
790
|
+
return { index, result: null };
|
|
791
|
+
}));
|
|
792
|
+
const chunkResults = await Promise.all(promises);
|
|
793
|
+
for (const { index, result } of chunkResults) {
|
|
794
|
+
if (result) {
|
|
795
|
+
results.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
892
796
|
}
|
|
893
|
-
|
|
894
|
-
|
|
797
|
+
else {
|
|
798
|
+
const slide = plan.slides[index];
|
|
799
|
+
const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', index + 1);
|
|
800
|
+
results.set(index, { index, html: fallbackHtml, isCodeTemplate: true });
|
|
801
|
+
logger.warn(`Slide ${index + 1}: Using fallback HTML for "${slide.title}"`);
|
|
895
802
|
}
|
|
896
803
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
804
|
+
const done = Math.min(batch + MAX_CONCURRENT, contentIndices.length);
|
|
805
|
+
if (phaseLogger)
|
|
806
|
+
phaseLogger('powerpoint-create', 'html-generation', `Generated ${done}/${contentIndices.length} content slides`);
|
|
807
|
+
}
|
|
808
|
+
return results;
|
|
809
|
+
}
|
|
810
|
+
async function validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger) {
|
|
811
|
+
const failedIndices = [];
|
|
812
|
+
for (const [index, result] of htmlResults) {
|
|
813
|
+
const slide = plan.slides[index];
|
|
814
|
+
if (slide.type === 'title' || slide.type === 'closing')
|
|
900
815
|
continue;
|
|
816
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
817
|
+
const validation = validateSlideHtml(result.html, layoutType);
|
|
818
|
+
if (!validation.pass) {
|
|
819
|
+
logger.info(`Slide ${index + 1}: Post-validation failed: ${validation.feedback}`);
|
|
820
|
+
failedIndices.push(index);
|
|
901
821
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: overflow (${contentHeight}px), regen ${attempt + 1}/${MAX_REGEN_ATTEMPTS}...`);
|
|
923
|
-
try {
|
|
924
|
-
const regenRes = await llmClient.chatCompletion({
|
|
925
|
-
messages: [
|
|
926
|
-
{ role: 'system', content: htmlPrompt },
|
|
927
|
-
{ role: 'user', content: `Generate the HTML slide now. CRITICAL: Your previous version was ${contentHeight}px tall but the slide is only 1080px. Content was CLIPPED at the bottom.\n\nYou MUST:\n- Use FEWER items (max 3 cards, max 3 rows, max 3 metrics)\n- SHORTER text per item (1-2 lines max)\n- SIMPLER layout with generous padding\n- body padding: 60px 80px minimum\n- Do NOT exceed 3 content blocks total\n\nThis is attempt ${attempt + 1}. ${attempt >= 2 ? 'DRASTICALLY simplify — use only 2 main items with large text.' : ''}` },
|
|
928
|
-
],
|
|
929
|
-
temperature: 0.2,
|
|
930
|
-
max_tokens: 8000,
|
|
931
|
-
});
|
|
932
|
-
const regenMsg = regenRes.choices[0]?.message;
|
|
933
|
-
const regenRaw = regenMsg ? extractContent(regenMsg) : '';
|
|
934
|
-
const regenHtml = extractHtml(regenRaw);
|
|
935
|
-
if (regenHtml) {
|
|
936
|
-
html = regenHtml;
|
|
937
|
-
logger.info(`Slide ${slideNum}: Regenerated HTML (attempt ${attempt + 1})`);
|
|
938
|
-
}
|
|
939
|
-
else {
|
|
940
|
-
break;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
catch (e) {
|
|
944
|
-
logger.warn(`Slide ${slideNum}: Overflow regeneration attempt ${attempt + 1} failed: ${e}`);
|
|
945
|
-
break;
|
|
822
|
+
}
|
|
823
|
+
if (failedIndices.length === 0) {
|
|
824
|
+
if (phaseLogger)
|
|
825
|
+
phaseLogger('powerpoint-create', 'validation', 'All slides passed validation');
|
|
826
|
+
return htmlResults;
|
|
827
|
+
}
|
|
828
|
+
if (phaseLogger)
|
|
829
|
+
phaseLogger('powerpoint-create', 'validation', `${failedIndices.length} slides failed validation, regenerating in parallel...`);
|
|
830
|
+
for (let batch = 0; batch < failedIndices.length; batch += MAX_CONCURRENT) {
|
|
831
|
+
const chunk = failedIndices.slice(batch, batch + MAX_CONCURRENT);
|
|
832
|
+
const promises = chunk.map(async (index) => {
|
|
833
|
+
const slide = plan.slides[index];
|
|
834
|
+
const original = htmlResults.get(index);
|
|
835
|
+
try {
|
|
836
|
+
const result = await generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language);
|
|
837
|
+
if (result) {
|
|
838
|
+
const regenHasPlaceholder = hasPlaceholderText(result.html);
|
|
839
|
+
const origHasPlaceholder = hasPlaceholderText(original.html);
|
|
840
|
+
if (!regenHasPlaceholder || origHasPlaceholder) {
|
|
841
|
+
return { index, result: { index, html: result.html, isCodeTemplate: result.isCodeTemplate } };
|
|
946
842
|
}
|
|
947
843
|
}
|
|
948
|
-
catch {
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
844
|
}
|
|
845
|
+
catch (e) {
|
|
846
|
+
logger.warn(`Slide ${index + 1}: Regen error: ${e}`);
|
|
847
|
+
}
|
|
848
|
+
return { index, result: null };
|
|
849
|
+
});
|
|
850
|
+
const chunkResults = await Promise.all(promises);
|
|
851
|
+
for (const { index, result } of chunkResults) {
|
|
852
|
+
if (result) {
|
|
853
|
+
htmlResults.set(index, result);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return htmlResults;
|
|
858
|
+
}
|
|
859
|
+
async function assemblePresentation(htmlResults, plan, timestamp, savePath, phaseLogger, toolCallLogger) {
|
|
860
|
+
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
861
|
+
ensureTempDir(tempWritePath);
|
|
862
|
+
const builtSlides = [];
|
|
863
|
+
let failCount = 0;
|
|
864
|
+
let totalToolCalls = 0;
|
|
865
|
+
const tempFiles = [];
|
|
866
|
+
const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
|
|
867
|
+
for (const [index, result] of sortedEntries) {
|
|
868
|
+
const slidePlan = plan.slides[index];
|
|
869
|
+
const slideNum = builtSlides.length + 1;
|
|
870
|
+
if (failCount >= 3) {
|
|
871
|
+
logger.warn('Too many slide failures, stopping');
|
|
872
|
+
break;
|
|
952
873
|
}
|
|
874
|
+
if (phaseLogger)
|
|
875
|
+
phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
|
|
953
876
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
954
877
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
955
878
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -957,7 +880,8 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
957
880
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
958
881
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
959
882
|
try {
|
|
960
|
-
const
|
|
883
|
+
const viewportHtml = injectEdgeSizing(result.html, plan.design.background_color);
|
|
884
|
+
const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
|
|
961
885
|
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
962
886
|
tempFiles.push(htmlWritePath);
|
|
963
887
|
}
|
|
@@ -990,6 +914,10 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
990
914
|
renderSuccess = false;
|
|
991
915
|
}
|
|
992
916
|
}
|
|
917
|
+
try {
|
|
918
|
+
fs.unlinkSync(htmlWritePath);
|
|
919
|
+
}
|
|
920
|
+
catch { }
|
|
993
921
|
if (!renderSuccess) {
|
|
994
922
|
logger.warn(`Slide ${slideNum}: Screenshot rendering failed, skipping`);
|
|
995
923
|
failCount++;
|
|
@@ -1023,7 +951,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1023
951
|
if (bgResult.success) {
|
|
1024
952
|
builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
1025
953
|
try {
|
|
1026
|
-
await powerpointClient.powerpointAddNote(slideNum, html);
|
|
954
|
+
await powerpointClient.powerpointAddNote(slideNum, result.html);
|
|
1027
955
|
}
|
|
1028
956
|
catch { }
|
|
1029
957
|
}
|
|
@@ -1037,7 +965,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1037
965
|
const slideCountResult = await powerpointClient.powerpointGetSlideCount();
|
|
1038
966
|
const totalSlidesInPpt = slideCountResult['slide_count'] || 0;
|
|
1039
967
|
if (totalSlidesInPpt > builtSlides.length) {
|
|
1040
|
-
logger.warn(`PPT has ${totalSlidesInPpt} slides but only ${builtSlides.length} were rendered — deleting ${totalSlidesInPpt - builtSlides.length} trailing blanks`);
|
|
1041
968
|
for (let d = totalSlidesInPpt; d > builtSlides.length; d--) {
|
|
1042
969
|
await powerpointClient.powerpointDeleteSlide(d);
|
|
1043
970
|
totalToolCalls++;
|
|
@@ -1049,6 +976,13 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1049
976
|
}
|
|
1050
977
|
}
|
|
1051
978
|
if (builtSlides.length > 0) {
|
|
979
|
+
if (savePath) {
|
|
980
|
+
const wslSavePath = savePath.replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_m, d) => `/mnt/${d.toLowerCase()}`);
|
|
981
|
+
try {
|
|
982
|
+
fs.unlinkSync(wslSavePath);
|
|
983
|
+
}
|
|
984
|
+
catch { }
|
|
985
|
+
}
|
|
1052
986
|
let saveResult = await powerpointClient.powerpointSave(savePath);
|
|
1053
987
|
totalToolCalls++;
|
|
1054
988
|
if (toolCallLogger)
|
|
@@ -1057,8 +991,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1057
991
|
const fallbackPath = 'C:\\temp\\presentation.pptx';
|
|
1058
992
|
saveResult = await powerpointClient.powerpointSave(fallbackPath);
|
|
1059
993
|
totalToolCalls++;
|
|
1060
|
-
if (toolCallLogger)
|
|
1061
|
-
toolCallLogger('powerpoint-create', 'powerpoint_save', { path: fallbackPath }, saveResult.success ? (saveResult['path'] || 'OK') : (saveResult.error || 'Failed'), saveResult.success, 0, totalToolCalls);
|
|
1062
994
|
}
|
|
1063
995
|
}
|
|
1064
996
|
for (const tempFile of tempFiles) {
|
|
@@ -1067,9 +999,118 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1067
999
|
}
|
|
1068
1000
|
catch { }
|
|
1069
1001
|
}
|
|
1002
|
+
return { builtSlides, totalToolCalls };
|
|
1003
|
+
}
|
|
1004
|
+
async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
1005
|
+
const startTime = Date.now();
|
|
1006
|
+
const phaseLogger = getSubAgentPhaseLogger();
|
|
1007
|
+
const toolCallLogger = getSubAgentToolCallLogger();
|
|
1008
|
+
const timestamp = Date.now();
|
|
1009
|
+
logger.enter('PPT-Create.runStructured.v2');
|
|
1010
|
+
const hasKorean = /[\uac00-\ud7af\u1100-\u11ff]/.test(instruction);
|
|
1011
|
+
const language = hasKorean ? 'ko' : 'en';
|
|
1012
|
+
if (phaseLogger)
|
|
1013
|
+
phaseLogger('powerpoint-create', 'init', 'Starting Design phase + opening PowerPoint...');
|
|
1014
|
+
const [plan, createResult] = await Promise.all([
|
|
1015
|
+
runDesignPhase(llmClient, instruction, phaseLogger),
|
|
1016
|
+
powerpointClient.powerpointCreate(),
|
|
1017
|
+
]);
|
|
1018
|
+
if (!createResult.success) {
|
|
1019
|
+
return { success: false, error: `Failed to create presentation: ${createResult['error']}` };
|
|
1020
|
+
}
|
|
1021
|
+
if (!plan) {
|
|
1022
|
+
logger.error('PPT planning failed after retries — cannot create presentation');
|
|
1023
|
+
return { success: false, error: 'Failed to generate presentation plan. Please try again.' };
|
|
1024
|
+
}
|
|
1025
|
+
if (plan.slides.length > 20) {
|
|
1026
|
+
const firstSlide = plan.slides[0];
|
|
1027
|
+
const lastSlide = plan.slides[plan.slides.length - 1];
|
|
1028
|
+
const contentSlides = plan.slides.slice(1, -1).slice(0, 18);
|
|
1029
|
+
plan.slides = [firstSlide, ...contentSlides, lastSlide];
|
|
1030
|
+
}
|
|
1031
|
+
const userYearMatch = instruction.match(/(\d{4})년/);
|
|
1032
|
+
if (userYearMatch) {
|
|
1033
|
+
const userYear = userYearMatch[1];
|
|
1034
|
+
for (const slide of plan.slides) {
|
|
1035
|
+
if (slide.type === 'content' && slide.content_direction) {
|
|
1036
|
+
if (!slide.content_direction.includes(`${userYear}년`)) {
|
|
1037
|
+
slide.content_direction += ` (Note: This report covers ${userYear}년 data.)`;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
let savePath = explicitSavePath;
|
|
1043
|
+
if (!savePath) {
|
|
1044
|
+
const fullPathMatch = instruction.match(/([A-Za-z]:\\[^\s,]+\.pptx|\/[^\s,]+\.pptx)/i);
|
|
1045
|
+
if (fullPathMatch) {
|
|
1046
|
+
savePath = fullPathMatch[1];
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
const nameMatch = instruction.match(/([\w][\w\-_.]*\.pptx)/i);
|
|
1050
|
+
if (nameMatch) {
|
|
1051
|
+
savePath = `C:\\temp\\${nameMatch[1]}`;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const titleSlidePlanForDate = plan.slides.find(s => s.type === 'title');
|
|
1056
|
+
const dateSearchTexts = [instruction, titleSlidePlanForDate?.title || '', titleSlidePlanForDate?.content_direction || ''];
|
|
1057
|
+
let kstDate = '';
|
|
1058
|
+
for (const text of dateSearchTexts) {
|
|
1059
|
+
const dateMatch = text.match(/(\d{4})년\s*(\d{1,2})\s*(월|분기)/);
|
|
1060
|
+
if (dateMatch) {
|
|
1061
|
+
kstDate = `${dateMatch[1]}년 ${dateMatch[2]}${dateMatch[3]}`;
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (!kstDate) {
|
|
1066
|
+
const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000);
|
|
1067
|
+
kstDate = `${kstNow.getUTCFullYear()}년 ${kstNow.getUTCMonth() + 1}월`;
|
|
1068
|
+
}
|
|
1069
|
+
const titleSlidePlan = plan.slides.find(s => s.type === 'title');
|
|
1070
|
+
const rawTitleText = titleSlidePlan?.title || '';
|
|
1071
|
+
const titleSeps = [' - ', ' – ', ' — ', ': ', ' | '];
|
|
1072
|
+
let companyName = rawTitleText;
|
|
1073
|
+
let titleSubtitle = '';
|
|
1074
|
+
for (const sep of titleSeps) {
|
|
1075
|
+
const idx = rawTitleText.indexOf(sep);
|
|
1076
|
+
if (idx > 0) {
|
|
1077
|
+
companyName = rawTitleText.slice(0, idx).trim();
|
|
1078
|
+
titleSubtitle = rawTitleText.slice(idx + sep.length).trim();
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (!titleSubtitle && titleSlidePlan) {
|
|
1083
|
+
titleSubtitle = ((titleSlidePlan.content_direction || '').split('\n')[0] || '').trim().slice(0, 120);
|
|
1084
|
+
}
|
|
1085
|
+
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
1086
|
+
titleSubtitle = '';
|
|
1087
|
+
}
|
|
1088
|
+
const cleanInstruction = instruction.replace(/\*\*/g, '');
|
|
1089
|
+
const companyMatch = cleanInstruction.match(/회사명\s*[::]?\s*([^\s,,、]+)/);
|
|
1090
|
+
if (companyMatch && companyMatch[1]) {
|
|
1091
|
+
const companyName_ = companyMatch[1];
|
|
1092
|
+
if (titleSlidePlan && titleSlidePlan.title.trim() !== companyName_) {
|
|
1093
|
+
const originalTitle = titleSlidePlan.title;
|
|
1094
|
+
titleSlidePlan.title = companyName_;
|
|
1095
|
+
companyName = companyName_;
|
|
1096
|
+
if (!titleSlidePlan.content_direction?.includes(originalTitle)) {
|
|
1097
|
+
const stripped = originalTitle.replace(companyName_, '').replace(/^\s*[-–—:|\s]+/, '').trim();
|
|
1098
|
+
titleSubtitle = stripped || originalTitle;
|
|
1099
|
+
titleSlidePlan.content_direction = titleSubtitle + (titleSlidePlan.content_direction ? '\n' + titleSlidePlan.content_direction : '');
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (phaseLogger)
|
|
1104
|
+
phaseLogger('powerpoint-create', 'html-generation', 'Starting parallel HTML generation...');
|
|
1105
|
+
const htmlResults = await generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger);
|
|
1106
|
+
const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
|
|
1107
|
+
if (phaseLogger)
|
|
1108
|
+
phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
|
|
1109
|
+
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, phaseLogger, toolCallLogger);
|
|
1070
1110
|
const duration = Date.now() - startTime;
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1111
|
+
const slideList = builtSlides.join('\n');
|
|
1112
|
+
const summary = `Presentation COMPLETE — ${builtSlides.length} slides created and saved successfully.\nAll requested topics are covered across these slides. Do NOT add more slides or call powerpoint_modify_agent.\n\nSlides:\n${slideList}`;
|
|
1113
|
+
logger.exit('PPT-Create.runStructured.v2', { slideCount: builtSlides.length, totalToolCalls, duration });
|
|
1073
1114
|
return {
|
|
1074
1115
|
success: builtSlides.length > 0,
|
|
1075
1116
|
result: summary,
|