hanseol-dev 5.0.2-dev.99 → 5.0.3-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.js +417 -393
- 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 +1088 -172
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +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, buildDirectHtmlPrompt, 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 = 3;
|
|
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
|
}
|
|
@@ -199,69 +230,50 @@ function parseJsonPlan(raw) {
|
|
|
199
230
|
}
|
|
200
231
|
function extractHtml(raw) {
|
|
201
232
|
const trimmed = raw.trim();
|
|
202
|
-
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html'))
|
|
233
|
+
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html'))
|
|
203
234
|
return trimmed;
|
|
204
|
-
}
|
|
205
235
|
const fenceMatch = trimmed.match(/```(?:html)?\s*\n([\s\S]*?)\n```/);
|
|
206
|
-
if (fenceMatch?.[1])
|
|
236
|
+
if (fenceMatch?.[1])
|
|
207
237
|
return fenceMatch[1].trim();
|
|
208
|
-
}
|
|
209
238
|
const docMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
210
|
-
if (docMatch?.[1])
|
|
239
|
+
if (docMatch?.[1])
|
|
211
240
|
return docMatch[1].trim();
|
|
212
|
-
}
|
|
213
241
|
return null;
|
|
214
242
|
}
|
|
215
|
-
function
|
|
243
|
+
function injectEdgeSizing(html, backgroundColor) {
|
|
216
244
|
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
217
245
|
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;
|
|
246
|
+
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
247
|
if (result.includes('</head>')) {
|
|
222
|
-
result = result.replace('</head>', `${
|
|
248
|
+
result = result.replace('</head>', `${sizingCss}</head>`);
|
|
223
249
|
}
|
|
224
250
|
else if (result.includes('<head>')) {
|
|
225
|
-
result = result.replace('<head>', `<head>${
|
|
251
|
+
result = result.replace('<head>', `<head>${sizingCss}`);
|
|
226
252
|
}
|
|
227
253
|
else if (result.includes('<html')) {
|
|
228
|
-
result = result.replace(/<html[^>]*>/, (
|
|
254
|
+
result = result.replace(/<html[^>]*>/, (m) => `${m}<head>${sizingCss}</head>`);
|
|
229
255
|
}
|
|
230
256
|
else {
|
|
231
|
-
result =
|
|
257
|
+
result = sizingCss + result;
|
|
232
258
|
}
|
|
233
259
|
return result;
|
|
234
260
|
}
|
|
235
|
-
function
|
|
236
|
-
let result = html.replace(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return html.replace('</head>', `${safeStyle}</head>`);
|
|
261
|
+
function injectViewportCss(html, backgroundColor) {
|
|
262
|
+
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
263
|
+
const bgColor = backgroundColor || '#000000';
|
|
264
|
+
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;font-size:26px!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>*:only-child{display:flex!important;flex-direction:column!important;flex:1!important;min-height:1080px!important}body>*:only-child>*:nth-child(2),body>*:only-child>*:nth-child(3),body>*:only-child>*:last-child:not(:first-child){flex:1!important;display:flex!important;align-items:stretch!important}body>.content,body>*:nth-child(2),body>*:nth-child(3),body *>.content,body [class*=content]{flex:1!important;align-items:stretch!important;align-content:stretch!important;grid-auto-rows:1fr!important}[class*=card],[class*=item],[class*=step],[class*=milestone],[class*=feature]{justify-content:flex-start!important;gap:20px!important}</style>`;
|
|
265
|
+
if (result.includes('</head>')) {
|
|
266
|
+
result = result.replace('</head>', `${overrideCss}</head>`);
|
|
267
|
+
}
|
|
268
|
+
else if (result.includes('<head>')) {
|
|
269
|
+
result = result.replace('<head>', `<head>${overrideCss}`);
|
|
270
|
+
}
|
|
271
|
+
else if (result.includes('<html')) {
|
|
272
|
+
result = result.replace(/<html[^>]*>/, (m) => `${m}<head>${overrideCss}</head>`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
result = overrideCss + result;
|
|
251
276
|
}
|
|
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
277
|
return result;
|
|
266
278
|
}
|
|
267
279
|
function injectTitleContrastFix(html, designTextColor) {
|
|
@@ -290,40 +302,10 @@ function injectTitleContrastFix(html, designTextColor) {
|
|
|
290
302
|
}
|
|
291
303
|
return html + script;
|
|
292
304
|
}
|
|
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
305
|
function escapeHtml(text) {
|
|
320
|
-
return text
|
|
321
|
-
.replace(/&/g, '&')
|
|
322
|
-
.replace(/</g, '<')
|
|
323
|
-
.replace(/>/g, '>')
|
|
324
|
-
.replace(/"/g, '"');
|
|
306
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
325
307
|
}
|
|
326
|
-
function buildTitleSlideHtml(design, mainTitle, subtitle, date,
|
|
308
|
+
function buildTitleSlideHtml(design, mainTitle, subtitle, date, _slideNum) {
|
|
327
309
|
return `<!DOCTYPE html>
|
|
328
310
|
<html lang="ko">
|
|
329
311
|
<head>
|
|
@@ -335,29 +317,21 @@ html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
|
335
317
|
body {
|
|
336
318
|
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
337
319
|
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
338
321
|
align-items: center;
|
|
339
322
|
justify-content: center;
|
|
340
|
-
position: relative;
|
|
341
323
|
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
342
324
|
}
|
|
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;
|
|
325
|
+
body::before {
|
|
326
|
+
content: '';
|
|
327
|
+
display: block;
|
|
328
|
+
width: 100%;
|
|
329
|
+
height: 6px;
|
|
356
330
|
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
331
|
+
flex-shrink: 0;
|
|
357
332
|
}
|
|
358
|
-
.content {
|
|
333
|
+
.slide-content {
|
|
359
334
|
text-align: center;
|
|
360
|
-
z-index: 1;
|
|
361
335
|
max-width: 1400px;
|
|
362
336
|
padding: 0 60px;
|
|
363
337
|
}
|
|
@@ -391,32 +365,22 @@ body {
|
|
|
391
365
|
color: rgba(255,255,255,0.55);
|
|
392
366
|
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
393
367
|
}
|
|
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
368
|
</style>
|
|
401
369
|
</head>
|
|
402
370
|
<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">
|
|
371
|
+
<div class="slide-content">
|
|
409
372
|
<div class="main-title">${escapeHtml(mainTitle)}</div>
|
|
410
373
|
<div class="accent-bar"></div>
|
|
411
374
|
${subtitle ? `<div class="subtitle">${escapeHtml(subtitle)}</div>` : ''}
|
|
412
375
|
<div class="date-text">${escapeHtml(date)}</div>
|
|
413
376
|
</div>
|
|
414
|
-
<
|
|
377
|
+
<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
378
|
</body>
|
|
416
379
|
</html>`;
|
|
417
380
|
}
|
|
418
|
-
function buildClosingSlideHtml(design, companyName,
|
|
381
|
+
function buildClosingSlideHtml(design, companyName, _slideNum, language, tagline) {
|
|
419
382
|
const thankYou = language === 'ko' ? '감사합니다' : 'Thank You';
|
|
383
|
+
const taglineHtml = tagline ? `<div class="tagline">${escapeHtml(tagline)}</div>` : '';
|
|
420
384
|
return `<!DOCTYPE html>
|
|
421
385
|
<html lang="${language}">
|
|
422
386
|
<head>
|
|
@@ -428,75 +392,77 @@ html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
|
428
392
|
body {
|
|
429
393
|
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
430
394
|
display: flex;
|
|
395
|
+
flex-direction: column;
|
|
431
396
|
align-items: center;
|
|
432
397
|
justify-content: center;
|
|
433
|
-
position: relative;
|
|
434
398
|
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
435
399
|
}
|
|
436
|
-
|
|
400
|
+
body::before {
|
|
401
|
+
content: '';
|
|
437
402
|
position: absolute;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
403
|
+
top: 0; left: 0; right: 0;
|
|
404
|
+
height: 6px;
|
|
405
|
+
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
441
406
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.bottom-accent {
|
|
407
|
+
body::after {
|
|
408
|
+
content: '';
|
|
445
409
|
position: absolute;
|
|
446
|
-
bottom: 0; left: 0;
|
|
410
|
+
bottom: 0; left: 0; right: 0;
|
|
411
|
+
height: 6px;
|
|
447
412
|
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
448
413
|
}
|
|
449
|
-
.content {
|
|
414
|
+
.slide-content {
|
|
450
415
|
text-align: center;
|
|
451
|
-
|
|
416
|
+
max-width: 1200px;
|
|
452
417
|
}
|
|
453
418
|
.thank-you {
|
|
454
|
-
font-size:
|
|
419
|
+
font-size: 104px;
|
|
455
420
|
font-weight: 800;
|
|
456
421
|
color: #ffffff;
|
|
457
422
|
letter-spacing: -1px;
|
|
458
423
|
text-shadow: 0 6px 40px rgba(0,0,0,0.25);
|
|
459
|
-
margin-bottom:
|
|
424
|
+
margin-bottom: 36px;
|
|
460
425
|
}
|
|
461
426
|
.accent-bar {
|
|
462
|
-
width:
|
|
427
|
+
width: 120px; height: 5px;
|
|
463
428
|
background: ${design.accent_color};
|
|
464
|
-
margin: 0 auto
|
|
429
|
+
margin: 0 auto 36px;
|
|
465
430
|
border-radius: 3px;
|
|
466
431
|
box-shadow: 0 0 20px ${design.accent_color}40;
|
|
467
432
|
}
|
|
468
433
|
.company {
|
|
469
|
-
font-size:
|
|
470
|
-
font-weight:
|
|
471
|
-
color: rgba(255,255,255,0.
|
|
434
|
+
font-size: 44px;
|
|
435
|
+
font-weight: 600;
|
|
436
|
+
color: rgba(255,255,255,0.88);
|
|
472
437
|
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
438
|
+
margin-bottom: 20px;
|
|
473
439
|
}
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
440
|
+
.tagline {
|
|
441
|
+
font-size: 28px;
|
|
442
|
+
font-weight: 400;
|
|
443
|
+
color: rgba(255,255,255,0.60);
|
|
444
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
445
|
+
line-height: 1.6;
|
|
446
|
+
max-width: 900px;
|
|
447
|
+
margin: 0 auto;
|
|
479
448
|
}
|
|
480
449
|
</style>
|
|
481
450
|
</head>
|
|
482
451
|
<body>
|
|
483
|
-
<div class="
|
|
484
|
-
<div class="decor d2"></div>
|
|
485
|
-
<div class="bottom-accent"></div>
|
|
486
|
-
<div class="content">
|
|
452
|
+
<div class="slide-content">
|
|
487
453
|
<div class="thank-you">${escapeHtml(thankYou)}</div>
|
|
488
454
|
<div class="accent-bar"></div>
|
|
489
455
|
<div class="company">${escapeHtml(companyName)}</div>
|
|
456
|
+
${taglineHtml}
|
|
490
457
|
</div>
|
|
491
|
-
<
|
|
458
|
+
<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
459
|
</body>
|
|
493
460
|
</html>`;
|
|
494
461
|
}
|
|
495
462
|
function isOverviewSlide(title, slideIndex) {
|
|
496
463
|
if (slideIndex !== 1)
|
|
497
464
|
return false;
|
|
498
|
-
|
|
499
|
-
return overviewKeywords.test(title);
|
|
465
|
+
return /개요|목차|overview|agenda|outline|순서|발표\s*구성|contents|목록/i.test(title);
|
|
500
466
|
}
|
|
501
467
|
function parseOverviewItems(contentDirection) {
|
|
502
468
|
const items = [];
|
|
@@ -520,11 +486,8 @@ function parseOverviewItems(contentDirection) {
|
|
|
520
486
|
}
|
|
521
487
|
function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
|
|
522
488
|
const badgeColors = [
|
|
523
|
-
design.primary_color,
|
|
524
|
-
design.accent_color,
|
|
525
|
-
design.gradient_end,
|
|
526
|
-
design.primary_color,
|
|
527
|
-
design.accent_color,
|
|
489
|
+
design.primary_color, design.accent_color, design.gradient_end,
|
|
490
|
+
design.primary_color, design.accent_color,
|
|
528
491
|
];
|
|
529
492
|
const itemCount = items.length;
|
|
530
493
|
const topRow = itemCount <= 3 ? items : items.slice(0, Math.ceil(itemCount / 2));
|
|
@@ -584,7 +547,6 @@ body {
|
|
|
584
547
|
display: flex; flex-direction: column;
|
|
585
548
|
align-items: center; text-align: center;
|
|
586
549
|
gap: 12px;
|
|
587
|
-
transition: none;
|
|
588
550
|
}
|
|
589
551
|
.badge {
|
|
590
552
|
width: 44px; height: 44px;
|
|
@@ -653,73 +615,26 @@ function normalizeDesign(raw) {
|
|
|
653
615
|
design_notes: raw['design_notes'] || DEFAULT_DESIGN.design_notes,
|
|
654
616
|
};
|
|
655
617
|
}
|
|
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;
|
|
618
|
+
async function runDesignPhase(llmClient, instruction, phaseLogger) {
|
|
704
619
|
if (phaseLogger)
|
|
705
|
-
phaseLogger('powerpoint-create', '
|
|
620
|
+
phaseLogger('powerpoint-create', 'design', 'Generating design system + slide plan...');
|
|
706
621
|
let plan = null;
|
|
707
622
|
try {
|
|
708
|
-
const
|
|
623
|
+
const res = await llmClient.chatCompletion({
|
|
709
624
|
messages: [
|
|
710
|
-
{ role: 'system', content:
|
|
711
|
-
{ role: 'user', content:
|
|
625
|
+
{ role: 'system', content: PPT_DESIGN_PROMPT },
|
|
626
|
+
{ role: 'user', content: instruction },
|
|
712
627
|
],
|
|
713
|
-
temperature: 0.
|
|
628
|
+
temperature: 0.5,
|
|
714
629
|
max_tokens: 8000,
|
|
715
630
|
});
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
const
|
|
631
|
+
const msg = res.choices[0]?.message;
|
|
632
|
+
const rawPlan = msg ? extractContent(msg) : '';
|
|
633
|
+
const finishReason = res.choices[0]?.finish_reason;
|
|
719
634
|
if (finishReason === 'length') {
|
|
720
|
-
logger.warn('PPT
|
|
635
|
+
logger.warn('PPT design response was truncated (finish_reason=length)');
|
|
721
636
|
}
|
|
722
|
-
logger.debug('PPT
|
|
637
|
+
logger.debug('PPT design raw response', { length: rawPlan.length, finishReason, first200: rawPlan.slice(0, 200) });
|
|
723
638
|
plan = rawPlan ? parseJsonPlan(rawPlan) : null;
|
|
724
639
|
if (plan) {
|
|
725
640
|
plan.design = normalizeDesign(plan.design);
|
|
@@ -727,13 +642,15 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
727
642
|
if (validationError) {
|
|
728
643
|
logger.warn('PPT plan validation failed', { error: validationError });
|
|
729
644
|
if (phaseLogger)
|
|
730
|
-
phaseLogger('powerpoint-create', '
|
|
645
|
+
phaseLogger('powerpoint-create', 'design', `Validation failed: ${validationError}. Retrying...`);
|
|
731
646
|
const retryRes = await llmClient.chatCompletion({
|
|
732
647
|
messages: [
|
|
733
|
-
{ role: 'system', content:
|
|
734
|
-
{ role: 'user', content:
|
|
648
|
+
{ role: 'system', content: PPT_DESIGN_PROMPT },
|
|
649
|
+
{ role: 'user', content: instruction },
|
|
650
|
+
{ role: 'assistant', content: rawPlan },
|
|
651
|
+
{ 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
652
|
],
|
|
736
|
-
temperature: 0.
|
|
653
|
+
temperature: 0.3,
|
|
737
654
|
max_tokens: 8000,
|
|
738
655
|
});
|
|
739
656
|
const retryMsg = retryRes.choices[0]?.message;
|
|
@@ -745,7 +662,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
745
662
|
if (!retryError) {
|
|
746
663
|
plan = retryPlan;
|
|
747
664
|
if (phaseLogger)
|
|
748
|
-
phaseLogger('powerpoint-create', '
|
|
665
|
+
phaseLogger('powerpoint-create', 'design', `Retry succeeded (${plan.slides.length} slides)`);
|
|
749
666
|
}
|
|
750
667
|
else {
|
|
751
668
|
plan = null;
|
|
@@ -757,199 +674,187 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
757
674
|
}
|
|
758
675
|
else {
|
|
759
676
|
if (phaseLogger)
|
|
760
|
-
phaseLogger('powerpoint-create', '
|
|
677
|
+
phaseLogger('powerpoint-create', 'design', `Done (${plan.slides.length} slides, mood: ${plan.design.mood})`);
|
|
761
678
|
}
|
|
762
679
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
logger.warn('PPT design failed', { error: String(e) });
|
|
683
|
+
}
|
|
684
|
+
return plan;
|
|
685
|
+
}
|
|
686
|
+
async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, totalSlides, language) {
|
|
687
|
+
const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
|
|
688
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
689
|
+
const directPrompt = buildDirectHtmlPrompt(slide.title, cleanedDirection, design, slideIndex, totalSlides, language, layoutType);
|
|
690
|
+
try {
|
|
691
|
+
const res = await llmClient.chatCompletion({
|
|
692
|
+
messages: [
|
|
693
|
+
{ role: 'system', content: directPrompt },
|
|
694
|
+
{ role: 'user', content: 'Generate the HTML slide now.' },
|
|
695
|
+
],
|
|
696
|
+
temperature: 0.4,
|
|
697
|
+
max_tokens: 6000,
|
|
698
|
+
});
|
|
699
|
+
const msg = res.choices[0]?.message;
|
|
700
|
+
const rawHtml = msg ? extractContent(msg) : '';
|
|
701
|
+
const html = extractHtml(rawHtml);
|
|
702
|
+
if (html) {
|
|
703
|
+
const validation = validateSlideHtml(html, layoutType);
|
|
704
|
+
if (validation.pass) {
|
|
705
|
+
return { html, isCodeTemplate: false };
|
|
706
|
+
}
|
|
707
|
+
logger.info(`Slide ${slideIndex + 1}: Direct HTML failed validation: ${validation.feedback}`);
|
|
767
708
|
}
|
|
768
709
|
}
|
|
769
710
|
catch (e) {
|
|
770
|
-
logger.warn(
|
|
771
|
-
if (phaseLogger)
|
|
772
|
-
phaseLogger('powerpoint-create', 'planning', 'Planning error. Falling back.');
|
|
711
|
+
logger.warn(`Slide ${slideIndex + 1}: Direct HTML LLM call failed: ${e}`);
|
|
773
712
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
713
|
+
logger.info(`Slide ${slideIndex + 1}: Falling back to code template "${layoutType}"`);
|
|
714
|
+
const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
|
|
715
|
+
try {
|
|
716
|
+
const jsonRes = await llmClient.chatCompletion({
|
|
717
|
+
messages: [
|
|
718
|
+
{ role: 'system', content: jsonPrompt },
|
|
719
|
+
{ role: 'user', content: 'Output the JSON now.' },
|
|
720
|
+
],
|
|
721
|
+
temperature: 0.3,
|
|
722
|
+
max_tokens: 2000,
|
|
723
|
+
});
|
|
724
|
+
const jsonMsg = jsonRes.choices[0]?.message;
|
|
725
|
+
const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
|
|
726
|
+
let slideData = parseContentFillJson(jsonRaw, layoutType);
|
|
727
|
+
if (!slideData) {
|
|
728
|
+
const retryRes = await llmClient.chatCompletion({
|
|
729
|
+
messages: [
|
|
730
|
+
{ role: 'system', content: jsonPrompt },
|
|
731
|
+
{ role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
|
|
732
|
+
],
|
|
733
|
+
temperature: 0.2,
|
|
734
|
+
max_tokens: 2000,
|
|
735
|
+
});
|
|
736
|
+
const retryMsg = retryRes.choices[0]?.message;
|
|
737
|
+
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
738
|
+
slideData = parseContentFillJson(retryRaw, layoutType);
|
|
739
|
+
}
|
|
740
|
+
if (slideData) {
|
|
741
|
+
const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
|
|
742
|
+
return { html, isCodeTemplate: true };
|
|
743
|
+
}
|
|
780
744
|
}
|
|
781
|
-
|
|
782
|
-
logger.warn(`
|
|
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`);
|
|
745
|
+
catch (e) {
|
|
746
|
+
logger.warn(`Slide ${slideIndex + 1}: Code template fallback failed: ${e}`);
|
|
789
747
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
let
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger) {
|
|
751
|
+
const results = new Map();
|
|
752
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
753
|
+
const slide = plan.slides[i];
|
|
754
|
+
if (slide.type === 'title') {
|
|
755
|
+
results.set(i, {
|
|
756
|
+
index: i,
|
|
757
|
+
html: buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, i + 1),
|
|
758
|
+
isCodeTemplate: true,
|
|
759
|
+
});
|
|
799
760
|
}
|
|
800
|
-
else {
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
761
|
+
else if (slide.type === 'closing') {
|
|
762
|
+
const closingTagline = (slide.content_direction || '').replace(/감사합니다|thank\s*you/gi, '').trim() || undefined;
|
|
763
|
+
results.set(i, {
|
|
764
|
+
index: i,
|
|
765
|
+
html: buildClosingSlideHtml(plan.design, companyName, i + 1, language, closingTagline),
|
|
766
|
+
isCodeTemplate: true,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
else if (isOverviewSlide(slide.title, i)) {
|
|
770
|
+
const overviewItems = parseOverviewItems(slide.content_direction || '');
|
|
771
|
+
if (overviewItems.length >= 2) {
|
|
772
|
+
const firstLine = (slide.content_direction || '').split('\n')[0] || '';
|
|
773
|
+
const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
|
|
774
|
+
results.set(i, {
|
|
775
|
+
index: i,
|
|
776
|
+
html: buildOverviewSlideHtml(plan.design, slide.title, overviewSubtitle, overviewItems, i + 1),
|
|
777
|
+
isCodeTemplate: true,
|
|
778
|
+
});
|
|
804
779
|
}
|
|
805
780
|
}
|
|
806
781
|
}
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
let
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
782
|
+
const contentIndices = plan.slides
|
|
783
|
+
.map((s, i) => ({ slide: s, index: i }))
|
|
784
|
+
.filter(({ index }) => !results.has(index));
|
|
785
|
+
if (phaseLogger)
|
|
786
|
+
phaseLogger('powerpoint-create', 'html-generation', `Generating ${contentIndices.length} content slides in parallel (batch size ${MAX_CONCURRENT})...`);
|
|
787
|
+
for (let batch = 0; batch < contentIndices.length; batch += MAX_CONCURRENT) {
|
|
788
|
+
const chunk = contentIndices.slice(batch, batch + MAX_CONCURRENT);
|
|
789
|
+
const promises = chunk.map(({ slide, index }) => generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language)
|
|
790
|
+
.then(result => ({ index, result }))
|
|
791
|
+
.catch(err => {
|
|
792
|
+
logger.warn(`Slide ${index + 1}: Generation error: ${err}`);
|
|
793
|
+
return { index, result: null };
|
|
794
|
+
}));
|
|
795
|
+
const chunkResults = await Promise.all(promises);
|
|
796
|
+
for (const { index, result } of chunkResults) {
|
|
797
|
+
if (result) {
|
|
798
|
+
results.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
799
|
+
}
|
|
820
800
|
}
|
|
801
|
+
const done = Math.min(batch + MAX_CONCURRENT, contentIndices.length);
|
|
802
|
+
if (phaseLogger)
|
|
803
|
+
phaseLogger('powerpoint-create', 'html-generation', `Generated ${done}/${contentIndices.length} content slides`);
|
|
821
804
|
}
|
|
822
|
-
|
|
823
|
-
|
|
805
|
+
return results;
|
|
806
|
+
}
|
|
807
|
+
async function validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger) {
|
|
808
|
+
const failedIndices = [];
|
|
809
|
+
for (const [index, result] of htmlResults) {
|
|
810
|
+
const slide = plan.slides[index];
|
|
811
|
+
if (slide.type === 'title' || slide.type === 'closing')
|
|
812
|
+
continue;
|
|
813
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
814
|
+
const validation = validateSlideHtml(result.html, layoutType);
|
|
815
|
+
if (!validation.pass) {
|
|
816
|
+
logger.info(`Slide ${index + 1}: Post-validation failed: ${validation.feedback}`);
|
|
817
|
+
failedIndices.push(index);
|
|
818
|
+
}
|
|
824
819
|
}
|
|
825
|
-
if (
|
|
826
|
-
|
|
820
|
+
if (failedIndices.length === 0) {
|
|
821
|
+
if (phaseLogger)
|
|
822
|
+
phaseLogger('powerpoint-create', 'validation', 'All slides passed validation');
|
|
823
|
+
return htmlResults;
|
|
827
824
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
825
|
+
if (phaseLogger)
|
|
826
|
+
phaseLogger('powerpoint-create', 'validation', `${failedIndices.length} slides failed validation, regenerating...`);
|
|
827
|
+
for (const index of failedIndices) {
|
|
828
|
+
const slide = plan.slides[index];
|
|
829
|
+
const original = htmlResults.get(index);
|
|
830
|
+
const result = await generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language);
|
|
831
|
+
if (result) {
|
|
832
|
+
const regenHasPlaceholder = hasPlaceholderText(result.html);
|
|
833
|
+
const origHasPlaceholder = hasPlaceholderText(original.html);
|
|
834
|
+
if (!regenHasPlaceholder || origHasPlaceholder) {
|
|
835
|
+
htmlResults.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
834
838
|
}
|
|
839
|
+
return htmlResults;
|
|
840
|
+
}
|
|
841
|
+
async function assemblePresentation(htmlResults, plan, timestamp, savePath, phaseLogger, toolCallLogger) {
|
|
842
|
+
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
843
|
+
ensureTempDir(tempWritePath);
|
|
835
844
|
const builtSlides = [];
|
|
836
845
|
let failCount = 0;
|
|
846
|
+
let totalToolCalls = 0;
|
|
837
847
|
const tempFiles = [];
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
848
|
+
const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
|
|
849
|
+
for (const [index, result] of sortedEntries) {
|
|
850
|
+
const slidePlan = plan.slides[index];
|
|
851
|
+
const slideNum = builtSlides.length + 1;
|
|
841
852
|
if (failCount >= 3) {
|
|
842
853
|
logger.warn('Too many slide failures, stopping');
|
|
843
854
|
break;
|
|
844
855
|
}
|
|
845
856
|
if (phaseLogger)
|
|
846
|
-
phaseLogger('powerpoint-create', '
|
|
847
|
-
let html = null;
|
|
848
|
-
let htmlPrompt = null;
|
|
849
|
-
if (slidePlan.type === 'title') {
|
|
850
|
-
html = buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, slideNum);
|
|
851
|
-
}
|
|
852
|
-
else if (slidePlan.type === 'closing') {
|
|
853
|
-
html = buildClosingSlideHtml(plan.design, companyName, slideNum, language);
|
|
854
|
-
}
|
|
855
|
-
else if (isOverviewSlide(slidePlan.title, i)) {
|
|
856
|
-
const overviewItems = parseOverviewItems(slidePlan.content_direction || '');
|
|
857
|
-
if (overviewItems.length >= 2) {
|
|
858
|
-
const firstLine = (slidePlan.content_direction || '').split('\n')[0] || '';
|
|
859
|
-
const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
|
|
860
|
-
html = buildOverviewSlideHtml(plan.design, slidePlan.title, overviewSubtitle, overviewItems, slideNum);
|
|
861
|
-
logger.info(`Slide ${slideNum}: Using code-generated overview template (${overviewItems.length} items)`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
if (!html && slidePlan.type !== 'title' && slidePlan.type !== 'closing') {
|
|
865
|
-
htmlPrompt = buildSlideHtmlPrompt(slidePlan.title, slidePlan.content_direction || '', plan.design, i, plan.slides.length, language);
|
|
866
|
-
try {
|
|
867
|
-
const htmlRes = await llmClient.chatCompletion({
|
|
868
|
-
messages: [
|
|
869
|
-
{ role: 'system', content: htmlPrompt },
|
|
870
|
-
{ role: 'user', content: 'Generate the HTML slide now.' },
|
|
871
|
-
],
|
|
872
|
-
temperature: 0.3,
|
|
873
|
-
max_tokens: 8000,
|
|
874
|
-
});
|
|
875
|
-
const htmlMsg = htmlRes.choices[0]?.message;
|
|
876
|
-
const rawHtml = htmlMsg ? extractContent(htmlMsg) : '';
|
|
877
|
-
html = extractHtml(rawHtml);
|
|
878
|
-
if (!html && rawHtml.length > 100) {
|
|
879
|
-
logger.warn(`Slide ${slideNum}: HTML extraction failed, retrying`);
|
|
880
|
-
const retryRes = await llmClient.chatCompletion({
|
|
881
|
-
messages: [
|
|
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
|
-
}
|
|
892
|
-
}
|
|
893
|
-
catch (e) {
|
|
894
|
-
logger.warn(`Slide ${slideNum}: LLM call failed: ${e}`);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
if (!html) {
|
|
898
|
-
logger.warn(`Slide ${slideNum}: Failed to generate HTML, skipping`);
|
|
899
|
-
failCount++;
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
if (htmlPrompt) {
|
|
903
|
-
const MAX_REGEN_ATTEMPTS = 5;
|
|
904
|
-
for (let attempt = 0; attempt < MAX_REGEN_ATTEMPTS; attempt++) {
|
|
905
|
-
const measureFileName = `hanseol_measure_${slideNum}_${timestamp}_${attempt}.html`;
|
|
906
|
-
const measureWritePath = path.join(tempWritePath, measureFileName);
|
|
907
|
-
const measureWinPath = `${tempWinPath}\\${measureFileName}`;
|
|
908
|
-
try {
|
|
909
|
-
fs.writeFileSync(measureWritePath, injectMeasureCss(html), 'utf-8');
|
|
910
|
-
const contentHeight = await powerpointClient.measureHtmlHeight(measureWinPath);
|
|
911
|
-
try {
|
|
912
|
-
fs.unlinkSync(measureWritePath);
|
|
913
|
-
}
|
|
914
|
-
catch { }
|
|
915
|
-
if (contentHeight <= 1050) {
|
|
916
|
-
if (attempt > 0)
|
|
917
|
-
logger.info(`Slide ${slideNum}: Fits after ${attempt} regeneration(s) (${contentHeight}px)`);
|
|
918
|
-
break;
|
|
919
|
-
}
|
|
920
|
-
logger.warn(`Slide ${slideNum}: Content overflows (${contentHeight}px > 1050), attempt ${attempt + 1}/${MAX_REGEN_ATTEMPTS}`);
|
|
921
|
-
if (phaseLogger)
|
|
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;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
catch {
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
857
|
+
phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
|
|
953
858
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
954
859
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
955
860
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -957,7 +862,10 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
957
862
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
958
863
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
959
864
|
try {
|
|
960
|
-
const
|
|
865
|
+
const viewportHtml = result.isCodeTemplate
|
|
866
|
+
? injectEdgeSizing(result.html, plan.design.background_color)
|
|
867
|
+
: injectViewportCss(result.html, plan.design.background_color);
|
|
868
|
+
const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
|
|
961
869
|
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
962
870
|
tempFiles.push(htmlWritePath);
|
|
963
871
|
}
|
|
@@ -990,6 +898,10 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
990
898
|
renderSuccess = false;
|
|
991
899
|
}
|
|
992
900
|
}
|
|
901
|
+
try {
|
|
902
|
+
fs.unlinkSync(htmlWritePath);
|
|
903
|
+
}
|
|
904
|
+
catch { }
|
|
993
905
|
if (!renderSuccess) {
|
|
994
906
|
logger.warn(`Slide ${slideNum}: Screenshot rendering failed, skipping`);
|
|
995
907
|
failCount++;
|
|
@@ -1023,7 +935,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1023
935
|
if (bgResult.success) {
|
|
1024
936
|
builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
1025
937
|
try {
|
|
1026
|
-
await powerpointClient.powerpointAddNote(slideNum, html);
|
|
938
|
+
await powerpointClient.powerpointAddNote(slideNum, result.html);
|
|
1027
939
|
}
|
|
1028
940
|
catch { }
|
|
1029
941
|
}
|
|
@@ -1037,7 +949,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1037
949
|
const slideCountResult = await powerpointClient.powerpointGetSlideCount();
|
|
1038
950
|
const totalSlidesInPpt = slideCountResult['slide_count'] || 0;
|
|
1039
951
|
if (totalSlidesInPpt > builtSlides.length) {
|
|
1040
|
-
logger.warn(`PPT has ${totalSlidesInPpt} slides but only ${builtSlides.length} were rendered — deleting ${totalSlidesInPpt - builtSlides.length} trailing blanks`);
|
|
1041
952
|
for (let d = totalSlidesInPpt; d > builtSlides.length; d--) {
|
|
1042
953
|
await powerpointClient.powerpointDeleteSlide(d);
|
|
1043
954
|
totalToolCalls++;
|
|
@@ -1049,6 +960,13 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1049
960
|
}
|
|
1050
961
|
}
|
|
1051
962
|
if (builtSlides.length > 0) {
|
|
963
|
+
if (savePath) {
|
|
964
|
+
const wslSavePath = savePath.replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_m, d) => `/mnt/${d.toLowerCase()}`);
|
|
965
|
+
try {
|
|
966
|
+
fs.unlinkSync(wslSavePath);
|
|
967
|
+
}
|
|
968
|
+
catch { }
|
|
969
|
+
}
|
|
1052
970
|
let saveResult = await powerpointClient.powerpointSave(savePath);
|
|
1053
971
|
totalToolCalls++;
|
|
1054
972
|
if (toolCallLogger)
|
|
@@ -1057,8 +975,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1057
975
|
const fallbackPath = 'C:\\temp\\presentation.pptx';
|
|
1058
976
|
saveResult = await powerpointClient.powerpointSave(fallbackPath);
|
|
1059
977
|
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
978
|
}
|
|
1063
979
|
}
|
|
1064
980
|
for (const tempFile of tempFiles) {
|
|
@@ -1067,9 +983,117 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1067
983
|
}
|
|
1068
984
|
catch { }
|
|
1069
985
|
}
|
|
986
|
+
return { builtSlides, totalToolCalls };
|
|
987
|
+
}
|
|
988
|
+
async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
989
|
+
const startTime = Date.now();
|
|
990
|
+
const phaseLogger = getSubAgentPhaseLogger();
|
|
991
|
+
const toolCallLogger = getSubAgentToolCallLogger();
|
|
992
|
+
const timestamp = Date.now();
|
|
993
|
+
logger.enter('PPT-Create.runStructured.v2');
|
|
994
|
+
const hasKorean = /[\uac00-\ud7af\u1100-\u11ff]/.test(instruction);
|
|
995
|
+
const language = hasKorean ? 'ko' : 'en';
|
|
996
|
+
if (phaseLogger)
|
|
997
|
+
phaseLogger('powerpoint-create', 'init', 'Starting Design phase + opening PowerPoint...');
|
|
998
|
+
const [plan, createResult] = await Promise.all([
|
|
999
|
+
runDesignPhase(llmClient, instruction, phaseLogger),
|
|
1000
|
+
powerpointClient.powerpointCreate(),
|
|
1001
|
+
]);
|
|
1002
|
+
if (!createResult.success) {
|
|
1003
|
+
return { success: false, error: `Failed to create presentation: ${createResult['error']}` };
|
|
1004
|
+
}
|
|
1005
|
+
if (!plan) {
|
|
1006
|
+
logger.error('PPT planning failed after retries — cannot create presentation');
|
|
1007
|
+
return { success: false, error: 'Failed to generate presentation plan. Please try again.' };
|
|
1008
|
+
}
|
|
1009
|
+
if (plan.slides.length > 20) {
|
|
1010
|
+
const firstSlide = plan.slides[0];
|
|
1011
|
+
const lastSlide = plan.slides[plan.slides.length - 1];
|
|
1012
|
+
const contentSlides = plan.slides.slice(1, -1).slice(0, 18);
|
|
1013
|
+
plan.slides = [firstSlide, ...contentSlides, lastSlide];
|
|
1014
|
+
}
|
|
1015
|
+
const userYearMatch = instruction.match(/(\d{4})년/);
|
|
1016
|
+
if (userYearMatch) {
|
|
1017
|
+
const userYear = userYearMatch[1];
|
|
1018
|
+
for (const slide of plan.slides) {
|
|
1019
|
+
if (slide.type === 'content' && slide.content_direction) {
|
|
1020
|
+
if (!slide.content_direction.includes(`${userYear}년`)) {
|
|
1021
|
+
slide.content_direction += ` (Note: This report covers ${userYear}년 data.)`;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
let savePath = explicitSavePath;
|
|
1027
|
+
if (!savePath) {
|
|
1028
|
+
const fullPathMatch = instruction.match(/([A-Za-z]:\\[^\s,]+\.pptx|\/[^\s,]+\.pptx)/i);
|
|
1029
|
+
if (fullPathMatch) {
|
|
1030
|
+
savePath = fullPathMatch[1];
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
const nameMatch = instruction.match(/([\w][\w\-_.]*\.pptx)/i);
|
|
1034
|
+
if (nameMatch) {
|
|
1035
|
+
savePath = `C:\\temp\\${nameMatch[1]}`;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const titleSlidePlanForDate = plan.slides.find(s => s.type === 'title');
|
|
1040
|
+
const dateSearchTexts = [instruction, titleSlidePlanForDate?.title || '', titleSlidePlanForDate?.content_direction || ''];
|
|
1041
|
+
let kstDate = '';
|
|
1042
|
+
for (const text of dateSearchTexts) {
|
|
1043
|
+
const dateMatch = text.match(/(\d{4})년\s*(\d{1,2})\s*(월|분기)/);
|
|
1044
|
+
if (dateMatch) {
|
|
1045
|
+
kstDate = `${dateMatch[1]}년 ${dateMatch[2]}${dateMatch[3]}`;
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (!kstDate) {
|
|
1050
|
+
const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000);
|
|
1051
|
+
kstDate = `${kstNow.getUTCFullYear()}년 ${kstNow.getUTCMonth() + 1}월`;
|
|
1052
|
+
}
|
|
1053
|
+
const titleSlidePlan = plan.slides.find(s => s.type === 'title');
|
|
1054
|
+
const rawTitleText = titleSlidePlan?.title || '';
|
|
1055
|
+
const titleSeps = [' - ', ' – ', ' — ', ': ', ' | '];
|
|
1056
|
+
let companyName = rawTitleText;
|
|
1057
|
+
let titleSubtitle = '';
|
|
1058
|
+
for (const sep of titleSeps) {
|
|
1059
|
+
const idx = rawTitleText.indexOf(sep);
|
|
1060
|
+
if (idx > 0) {
|
|
1061
|
+
companyName = rawTitleText.slice(0, idx).trim();
|
|
1062
|
+
titleSubtitle = rawTitleText.slice(idx + sep.length).trim();
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (!titleSubtitle && titleSlidePlan) {
|
|
1067
|
+
titleSubtitle = ((titleSlidePlan.content_direction || '').split('\n')[0] || '').trim().slice(0, 120);
|
|
1068
|
+
}
|
|
1069
|
+
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
1070
|
+
titleSubtitle = '';
|
|
1071
|
+
}
|
|
1072
|
+
const companyMatch = instruction.match(/회사명\s*[::]\s*([^\s,,、]+)/);
|
|
1073
|
+
if (companyMatch && companyMatch[1]) {
|
|
1074
|
+
const companyName_ = companyMatch[1];
|
|
1075
|
+
if (titleSlidePlan && titleSlidePlan.title.trim() !== companyName_) {
|
|
1076
|
+
const originalTitle = titleSlidePlan.title;
|
|
1077
|
+
titleSlidePlan.title = companyName_;
|
|
1078
|
+
companyName = companyName_;
|
|
1079
|
+
if (!titleSlidePlan.content_direction?.includes(originalTitle)) {
|
|
1080
|
+
const stripped = originalTitle.replace(companyName_, '').replace(/^\s*[-–—:|\s]+/, '').trim();
|
|
1081
|
+
titleSubtitle = stripped || originalTitle;
|
|
1082
|
+
titleSlidePlan.content_direction = titleSubtitle + (titleSlidePlan.content_direction ? '\n' + titleSlidePlan.content_direction : '');
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (phaseLogger)
|
|
1087
|
+
phaseLogger('powerpoint-create', 'html-generation', 'Starting parallel HTML generation...');
|
|
1088
|
+
const htmlResults = await generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger);
|
|
1089
|
+
const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
|
|
1090
|
+
if (phaseLogger)
|
|
1091
|
+
phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
|
|
1092
|
+
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, phaseLogger, toolCallLogger);
|
|
1070
1093
|
const duration = Date.now() - startTime;
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1094
|
+
const slideList = builtSlides.join('\n');
|
|
1095
|
+
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}`;
|
|
1096
|
+
logger.exit('PPT-Create.runStructured.v2', { slideCount: builtSlides.length, totalToolCalls, duration });
|
|
1073
1097
|
return {
|
|
1074
1098
|
success: builtSlides.length > 0,
|
|
1075
1099
|
result: summary,
|