hanseol-dev 5.0.2-dev.98 → 5.0.3-dev.1
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 +394 -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 +1070 -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())
|
|
@@ -35,6 +36,9 @@ function validateAndFixPlan(plan) {
|
|
|
35
36
|
if (!Array.isArray(plan.slides) || plan.slides.length < 3) {
|
|
36
37
|
return 'slides array must have at least 3 entries';
|
|
37
38
|
}
|
|
39
|
+
if (plan.slides.length < 10) {
|
|
40
|
+
return `Only ${plan.slides.length} slides — minimum 10 required (aim for 10-12). Add more content slides with specific data.`;
|
|
41
|
+
}
|
|
38
42
|
if (plan.slides[0]?.type !== 'title') {
|
|
39
43
|
logger.info('Auto-fixing: first slide type changed to "title"');
|
|
40
44
|
plan.slides[0].type = 'title';
|
|
@@ -47,7 +51,23 @@ function validateAndFixPlan(plan) {
|
|
|
47
51
|
for (let i = 0; i < plan.slides.length; i++) {
|
|
48
52
|
if (!plan.slides[i].title) {
|
|
49
53
|
plan.slides[i].title = `Slide ${i + 1}`;
|
|
50
|
-
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const layoutOnlyPatterns = [
|
|
57
|
+
/^(?:전체\s*배경|왼쪽에|오른쪽에|중앙에|상단에|하단에)/,
|
|
58
|
+
/#[0-9a-fA-F]{3,8}에서.*그라데이션/,
|
|
59
|
+
/(?:accent_light|primary|gradient_end)\s*(?:배경|글씨|색상)/,
|
|
60
|
+
/^(?:CSS|flexbox|grid|conic-gradient|linear-gradient)/i,
|
|
61
|
+
];
|
|
62
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
63
|
+
const slide = plan.slides[i];
|
|
64
|
+
if (slide.type === 'title' || slide.type === 'closing')
|
|
65
|
+
continue;
|
|
66
|
+
const cd = slide.content_direction || '';
|
|
67
|
+
const hasNumbers = /\d/.test(cd);
|
|
68
|
+
const isLayoutOnly = layoutOnlyPatterns.some(p => p.test(cd));
|
|
69
|
+
if (isLayoutOnly && !hasNumbers) {
|
|
70
|
+
return `Slide ${i + 1} "${slide.title}" content_direction contains layout instructions instead of actual data.`;
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
return null;
|
|
@@ -130,19 +150,15 @@ function parseJsonPlan(raw) {
|
|
|
130
150
|
cleaned = cleaned.replace(/^```(?:json|JSON)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
131
151
|
}
|
|
132
152
|
const firstBrace = cleaned.indexOf('{');
|
|
133
|
-
if (firstBrace > 0)
|
|
153
|
+
if (firstBrace > 0)
|
|
134
154
|
cleaned = cleaned.slice(firstBrace);
|
|
135
|
-
}
|
|
136
155
|
const lastBrace = cleaned.lastIndexOf('}');
|
|
137
|
-
if (lastBrace >= 0 && lastBrace < cleaned.length - 1)
|
|
156
|
+
if (lastBrace >= 0 && lastBrace < cleaned.length - 1)
|
|
138
157
|
cleaned = cleaned.slice(0, lastBrace + 1);
|
|
139
|
-
}
|
|
140
158
|
try {
|
|
141
159
|
return JSON.parse(cleaned);
|
|
142
160
|
}
|
|
143
|
-
catch
|
|
144
|
-
logger.debug('parseJsonPlan: direct parse failed', { error: String(e), length: cleaned.length });
|
|
145
|
-
}
|
|
161
|
+
catch { }
|
|
146
162
|
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
147
163
|
if (!match)
|
|
148
164
|
return null;
|
|
@@ -154,9 +170,7 @@ function parseJsonPlan(raw) {
|
|
|
154
170
|
try {
|
|
155
171
|
return JSON.parse(repaired);
|
|
156
172
|
}
|
|
157
|
-
catch
|
|
158
|
-
logger.debug('parseJsonPlan: repaired parse failed', { error: String(e) });
|
|
159
|
-
}
|
|
173
|
+
catch { }
|
|
160
174
|
try {
|
|
161
175
|
let final = repaired;
|
|
162
176
|
let braces = 0, brackets = 0;
|
|
@@ -187,9 +201,9 @@ function parseJsonPlan(raw) {
|
|
|
187
201
|
}
|
|
188
202
|
if (inStr)
|
|
189
203
|
final += '"';
|
|
190
|
-
for (let
|
|
204
|
+
for (let x = 0; x < brackets; x++)
|
|
191
205
|
final += ']';
|
|
192
|
-
for (let
|
|
206
|
+
for (let x = 0; x < braces; x++)
|
|
193
207
|
final += '}';
|
|
194
208
|
return JSON.parse(final);
|
|
195
209
|
}
|
|
@@ -199,69 +213,50 @@ function parseJsonPlan(raw) {
|
|
|
199
213
|
}
|
|
200
214
|
function extractHtml(raw) {
|
|
201
215
|
const trimmed = raw.trim();
|
|
202
|
-
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html'))
|
|
216
|
+
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html'))
|
|
203
217
|
return trimmed;
|
|
204
|
-
}
|
|
205
218
|
const fenceMatch = trimmed.match(/```(?:html)?\s*\n([\s\S]*?)\n```/);
|
|
206
|
-
if (fenceMatch?.[1])
|
|
219
|
+
if (fenceMatch?.[1])
|
|
207
220
|
return fenceMatch[1].trim();
|
|
208
|
-
}
|
|
209
221
|
const docMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
210
|
-
if (docMatch?.[1])
|
|
222
|
+
if (docMatch?.[1])
|
|
211
223
|
return docMatch[1].trim();
|
|
212
|
-
}
|
|
213
224
|
return null;
|
|
214
225
|
}
|
|
215
|
-
function
|
|
226
|
+
function injectEdgeSizing(html, backgroundColor) {
|
|
216
227
|
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
217
228
|
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;
|
|
229
|
+
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
230
|
if (result.includes('</head>')) {
|
|
222
|
-
result = result.replace('</head>', `${
|
|
231
|
+
result = result.replace('</head>', `${sizingCss}</head>`);
|
|
223
232
|
}
|
|
224
233
|
else if (result.includes('<head>')) {
|
|
225
|
-
result = result.replace('<head>', `<head>${
|
|
234
|
+
result = result.replace('<head>', `<head>${sizingCss}`);
|
|
226
235
|
}
|
|
227
236
|
else if (result.includes('<html')) {
|
|
228
|
-
result = result.replace(/<html[^>]*>/, (
|
|
237
|
+
result = result.replace(/<html[^>]*>/, (m) => `${m}<head>${sizingCss}</head>`);
|
|
229
238
|
}
|
|
230
239
|
else {
|
|
231
|
-
result =
|
|
240
|
+
result = sizingCss + result;
|
|
232
241
|
}
|
|
233
242
|
return result;
|
|
234
243
|
}
|
|
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>`);
|
|
244
|
+
function injectViewportCss(html, backgroundColor) {
|
|
245
|
+
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
246
|
+
const bgColor = backgroundColor || '#000000';
|
|
247
|
+
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>`;
|
|
248
|
+
if (result.includes('</head>')) {
|
|
249
|
+
result = result.replace('</head>', `${overrideCss}</head>`);
|
|
250
|
+
}
|
|
251
|
+
else if (result.includes('<head>')) {
|
|
252
|
+
result = result.replace('<head>', `<head>${overrideCss}`);
|
|
253
|
+
}
|
|
254
|
+
else if (result.includes('<html')) {
|
|
255
|
+
result = result.replace(/<html[^>]*>/, (m) => `${m}<head>${overrideCss}</head>`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
result = overrideCss + result;
|
|
251
259
|
}
|
|
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
260
|
return result;
|
|
266
261
|
}
|
|
267
262
|
function injectTitleContrastFix(html, designTextColor) {
|
|
@@ -290,40 +285,10 @@ function injectTitleContrastFix(html, designTextColor) {
|
|
|
290
285
|
}
|
|
291
286
|
return html + script;
|
|
292
287
|
}
|
|
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
288
|
function escapeHtml(text) {
|
|
320
|
-
return text
|
|
321
|
-
.replace(/&/g, '&')
|
|
322
|
-
.replace(/</g, '<')
|
|
323
|
-
.replace(/>/g, '>')
|
|
324
|
-
.replace(/"/g, '"');
|
|
289
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
325
290
|
}
|
|
326
|
-
function buildTitleSlideHtml(design, mainTitle, subtitle, date,
|
|
291
|
+
function buildTitleSlideHtml(design, mainTitle, subtitle, date, _slideNum) {
|
|
327
292
|
return `<!DOCTYPE html>
|
|
328
293
|
<html lang="ko">
|
|
329
294
|
<head>
|
|
@@ -335,29 +300,21 @@ html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
|
335
300
|
body {
|
|
336
301
|
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
337
302
|
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
338
304
|
align-items: center;
|
|
339
305
|
justify-content: center;
|
|
340
|
-
position: relative;
|
|
341
306
|
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
342
307
|
}
|
|
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;
|
|
308
|
+
body::before {
|
|
309
|
+
content: '';
|
|
310
|
+
display: block;
|
|
311
|
+
width: 100%;
|
|
312
|
+
height: 6px;
|
|
356
313
|
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
314
|
+
flex-shrink: 0;
|
|
357
315
|
}
|
|
358
|
-
.content {
|
|
316
|
+
.slide-content {
|
|
359
317
|
text-align: center;
|
|
360
|
-
z-index: 1;
|
|
361
318
|
max-width: 1400px;
|
|
362
319
|
padding: 0 60px;
|
|
363
320
|
}
|
|
@@ -391,32 +348,22 @@ body {
|
|
|
391
348
|
color: rgba(255,255,255,0.55);
|
|
392
349
|
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
393
350
|
}
|
|
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
351
|
</style>
|
|
401
352
|
</head>
|
|
402
353
|
<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">
|
|
354
|
+
<div class="slide-content">
|
|
409
355
|
<div class="main-title">${escapeHtml(mainTitle)}</div>
|
|
410
356
|
<div class="accent-bar"></div>
|
|
411
357
|
${subtitle ? `<div class="subtitle">${escapeHtml(subtitle)}</div>` : ''}
|
|
412
358
|
<div class="date-text">${escapeHtml(date)}</div>
|
|
413
359
|
</div>
|
|
414
|
-
<
|
|
360
|
+
<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
361
|
</body>
|
|
416
362
|
</html>`;
|
|
417
363
|
}
|
|
418
|
-
function buildClosingSlideHtml(design, companyName,
|
|
364
|
+
function buildClosingSlideHtml(design, companyName, _slideNum, language, tagline) {
|
|
419
365
|
const thankYou = language === 'ko' ? '감사합니다' : 'Thank You';
|
|
366
|
+
const taglineHtml = tagline ? `<div class="tagline">${escapeHtml(tagline)}</div>` : '';
|
|
420
367
|
return `<!DOCTYPE html>
|
|
421
368
|
<html lang="${language}">
|
|
422
369
|
<head>
|
|
@@ -428,75 +375,77 @@ html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
|
428
375
|
body {
|
|
429
376
|
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
430
377
|
display: flex;
|
|
378
|
+
flex-direction: column;
|
|
431
379
|
align-items: center;
|
|
432
380
|
justify-content: center;
|
|
433
|
-
position: relative;
|
|
434
381
|
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
435
382
|
}
|
|
436
|
-
|
|
383
|
+
body::before {
|
|
384
|
+
content: '';
|
|
437
385
|
position: absolute;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
386
|
+
top: 0; left: 0; right: 0;
|
|
387
|
+
height: 6px;
|
|
388
|
+
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
441
389
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.bottom-accent {
|
|
390
|
+
body::after {
|
|
391
|
+
content: '';
|
|
445
392
|
position: absolute;
|
|
446
|
-
bottom: 0; left: 0;
|
|
393
|
+
bottom: 0; left: 0; right: 0;
|
|
394
|
+
height: 6px;
|
|
447
395
|
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
448
396
|
}
|
|
449
|
-
.content {
|
|
397
|
+
.slide-content {
|
|
450
398
|
text-align: center;
|
|
451
|
-
|
|
399
|
+
max-width: 1200px;
|
|
452
400
|
}
|
|
453
401
|
.thank-you {
|
|
454
|
-
font-size:
|
|
402
|
+
font-size: 104px;
|
|
455
403
|
font-weight: 800;
|
|
456
404
|
color: #ffffff;
|
|
457
405
|
letter-spacing: -1px;
|
|
458
406
|
text-shadow: 0 6px 40px rgba(0,0,0,0.25);
|
|
459
|
-
margin-bottom:
|
|
407
|
+
margin-bottom: 36px;
|
|
460
408
|
}
|
|
461
409
|
.accent-bar {
|
|
462
|
-
width:
|
|
410
|
+
width: 120px; height: 5px;
|
|
463
411
|
background: ${design.accent_color};
|
|
464
|
-
margin: 0 auto
|
|
412
|
+
margin: 0 auto 36px;
|
|
465
413
|
border-radius: 3px;
|
|
466
414
|
box-shadow: 0 0 20px ${design.accent_color}40;
|
|
467
415
|
}
|
|
468
416
|
.company {
|
|
469
|
-
font-size:
|
|
470
|
-
font-weight:
|
|
471
|
-
color: rgba(255,255,255,0.
|
|
417
|
+
font-size: 44px;
|
|
418
|
+
font-weight: 600;
|
|
419
|
+
color: rgba(255,255,255,0.88);
|
|
472
420
|
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
421
|
+
margin-bottom: 20px;
|
|
473
422
|
}
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
423
|
+
.tagline {
|
|
424
|
+
font-size: 28px;
|
|
425
|
+
font-weight: 400;
|
|
426
|
+
color: rgba(255,255,255,0.60);
|
|
427
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
428
|
+
line-height: 1.6;
|
|
429
|
+
max-width: 900px;
|
|
430
|
+
margin: 0 auto;
|
|
479
431
|
}
|
|
480
432
|
</style>
|
|
481
433
|
</head>
|
|
482
434
|
<body>
|
|
483
|
-
<div class="
|
|
484
|
-
<div class="decor d2"></div>
|
|
485
|
-
<div class="bottom-accent"></div>
|
|
486
|
-
<div class="content">
|
|
435
|
+
<div class="slide-content">
|
|
487
436
|
<div class="thank-you">${escapeHtml(thankYou)}</div>
|
|
488
437
|
<div class="accent-bar"></div>
|
|
489
438
|
<div class="company">${escapeHtml(companyName)}</div>
|
|
439
|
+
${taglineHtml}
|
|
490
440
|
</div>
|
|
491
|
-
<
|
|
441
|
+
<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
442
|
</body>
|
|
493
443
|
</html>`;
|
|
494
444
|
}
|
|
495
445
|
function isOverviewSlide(title, slideIndex) {
|
|
496
446
|
if (slideIndex !== 1)
|
|
497
447
|
return false;
|
|
498
|
-
|
|
499
|
-
return overviewKeywords.test(title);
|
|
448
|
+
return /개요|목차|overview|agenda|outline|순서|발표\s*구성|contents|목록/i.test(title);
|
|
500
449
|
}
|
|
501
450
|
function parseOverviewItems(contentDirection) {
|
|
502
451
|
const items = [];
|
|
@@ -520,11 +469,8 @@ function parseOverviewItems(contentDirection) {
|
|
|
520
469
|
}
|
|
521
470
|
function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
|
|
522
471
|
const badgeColors = [
|
|
523
|
-
design.primary_color,
|
|
524
|
-
design.accent_color,
|
|
525
|
-
design.gradient_end,
|
|
526
|
-
design.primary_color,
|
|
527
|
-
design.accent_color,
|
|
472
|
+
design.primary_color, design.accent_color, design.gradient_end,
|
|
473
|
+
design.primary_color, design.accent_color,
|
|
528
474
|
];
|
|
529
475
|
const itemCount = items.length;
|
|
530
476
|
const topRow = itemCount <= 3 ? items : items.slice(0, Math.ceil(itemCount / 2));
|
|
@@ -584,7 +530,6 @@ body {
|
|
|
584
530
|
display: flex; flex-direction: column;
|
|
585
531
|
align-items: center; text-align: center;
|
|
586
532
|
gap: 12px;
|
|
587
|
-
transition: none;
|
|
588
533
|
}
|
|
589
534
|
.badge {
|
|
590
535
|
width: 44px; height: 44px;
|
|
@@ -653,73 +598,26 @@ function normalizeDesign(raw) {
|
|
|
653
598
|
design_notes: raw['design_notes'] || DEFAULT_DESIGN.design_notes,
|
|
654
599
|
};
|
|
655
600
|
}
|
|
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;
|
|
601
|
+
async function runDesignPhase(llmClient, instruction, phaseLogger) {
|
|
704
602
|
if (phaseLogger)
|
|
705
|
-
phaseLogger('powerpoint-create', '
|
|
603
|
+
phaseLogger('powerpoint-create', 'design', 'Generating design system + slide plan...');
|
|
706
604
|
let plan = null;
|
|
707
605
|
try {
|
|
708
|
-
const
|
|
606
|
+
const res = await llmClient.chatCompletion({
|
|
709
607
|
messages: [
|
|
710
|
-
{ role: 'system', content:
|
|
711
|
-
{ role: 'user', content:
|
|
608
|
+
{ role: 'system', content: PPT_DESIGN_PROMPT },
|
|
609
|
+
{ role: 'user', content: instruction },
|
|
712
610
|
],
|
|
713
|
-
temperature: 0.
|
|
611
|
+
temperature: 0.5,
|
|
714
612
|
max_tokens: 8000,
|
|
715
613
|
});
|
|
716
|
-
const
|
|
717
|
-
const
|
|
718
|
-
const
|
|
614
|
+
const msg = res.choices[0]?.message;
|
|
615
|
+
const rawPlan = msg ? extractContent(msg) : '';
|
|
616
|
+
const finishReason = res.choices[0]?.finish_reason;
|
|
719
617
|
if (finishReason === 'length') {
|
|
720
|
-
logger.warn('PPT
|
|
618
|
+
logger.warn('PPT design response was truncated (finish_reason=length)');
|
|
721
619
|
}
|
|
722
|
-
logger.debug('PPT
|
|
620
|
+
logger.debug('PPT design raw response', { length: rawPlan.length, finishReason, first200: rawPlan.slice(0, 200) });
|
|
723
621
|
plan = rawPlan ? parseJsonPlan(rawPlan) : null;
|
|
724
622
|
if (plan) {
|
|
725
623
|
plan.design = normalizeDesign(plan.design);
|
|
@@ -727,13 +625,15 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
727
625
|
if (validationError) {
|
|
728
626
|
logger.warn('PPT plan validation failed', { error: validationError });
|
|
729
627
|
if (phaseLogger)
|
|
730
|
-
phaseLogger('powerpoint-create', '
|
|
628
|
+
phaseLogger('powerpoint-create', 'design', `Validation failed: ${validationError}. Retrying...`);
|
|
731
629
|
const retryRes = await llmClient.chatCompletion({
|
|
732
630
|
messages: [
|
|
733
|
-
{ role: 'system', content:
|
|
734
|
-
{ role: 'user', content:
|
|
631
|
+
{ role: 'system', content: PPT_DESIGN_PROMPT },
|
|
632
|
+
{ role: 'user', content: instruction },
|
|
633
|
+
{ role: 'assistant', content: rawPlan },
|
|
634
|
+
{ 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
635
|
],
|
|
736
|
-
temperature: 0.
|
|
636
|
+
temperature: 0.3,
|
|
737
637
|
max_tokens: 8000,
|
|
738
638
|
});
|
|
739
639
|
const retryMsg = retryRes.choices[0]?.message;
|
|
@@ -745,7 +645,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
745
645
|
if (!retryError) {
|
|
746
646
|
plan = retryPlan;
|
|
747
647
|
if (phaseLogger)
|
|
748
|
-
phaseLogger('powerpoint-create', '
|
|
648
|
+
phaseLogger('powerpoint-create', 'design', `Retry succeeded (${plan.slides.length} slides)`);
|
|
749
649
|
}
|
|
750
650
|
else {
|
|
751
651
|
plan = null;
|
|
@@ -757,199 +657,185 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
757
657
|
}
|
|
758
658
|
else {
|
|
759
659
|
if (phaseLogger)
|
|
760
|
-
phaseLogger('powerpoint-create', '
|
|
660
|
+
phaseLogger('powerpoint-create', 'design', `Done (${plan.slides.length} slides, mood: ${plan.design.mood})`);
|
|
761
661
|
}
|
|
762
662
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
logger.warn('PPT design failed', { error: String(e) });
|
|
666
|
+
}
|
|
667
|
+
return plan;
|
|
668
|
+
}
|
|
669
|
+
async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, totalSlides, language) {
|
|
670
|
+
const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
|
|
671
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
672
|
+
const directPrompt = buildDirectHtmlPrompt(slide.title, cleanedDirection, design, slideIndex, totalSlides, language, layoutType);
|
|
673
|
+
try {
|
|
674
|
+
const res = await llmClient.chatCompletion({
|
|
675
|
+
messages: [
|
|
676
|
+
{ role: 'system', content: directPrompt },
|
|
677
|
+
{ role: 'user', content: 'Generate the HTML slide now.' },
|
|
678
|
+
],
|
|
679
|
+
temperature: 0.4,
|
|
680
|
+
max_tokens: 6000,
|
|
681
|
+
});
|
|
682
|
+
const msg = res.choices[0]?.message;
|
|
683
|
+
const rawHtml = msg ? extractContent(msg) : '';
|
|
684
|
+
const html = extractHtml(rawHtml);
|
|
685
|
+
if (html) {
|
|
686
|
+
const validation = validateSlideHtml(html, layoutType);
|
|
687
|
+
if (validation.pass) {
|
|
688
|
+
return { html, isCodeTemplate: false };
|
|
689
|
+
}
|
|
690
|
+
logger.info(`Slide ${slideIndex + 1}: Direct HTML failed validation: ${validation.feedback}`);
|
|
767
691
|
}
|
|
768
692
|
}
|
|
769
693
|
catch (e) {
|
|
770
|
-
logger.warn(
|
|
771
|
-
if (phaseLogger)
|
|
772
|
-
phaseLogger('powerpoint-create', 'planning', 'Planning error. Falling back.');
|
|
694
|
+
logger.warn(`Slide ${slideIndex + 1}: Direct HTML LLM call failed: ${e}`);
|
|
773
695
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
696
|
+
logger.info(`Slide ${slideIndex + 1}: Falling back to code template "${layoutType}"`);
|
|
697
|
+
const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
|
|
698
|
+
try {
|
|
699
|
+
const jsonRes = await llmClient.chatCompletion({
|
|
700
|
+
messages: [
|
|
701
|
+
{ role: 'system', content: jsonPrompt },
|
|
702
|
+
{ role: 'user', content: 'Output the JSON now.' },
|
|
703
|
+
],
|
|
704
|
+
temperature: 0.3,
|
|
705
|
+
max_tokens: 2000,
|
|
706
|
+
});
|
|
707
|
+
const jsonMsg = jsonRes.choices[0]?.message;
|
|
708
|
+
const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
|
|
709
|
+
let slideData = parseContentFillJson(jsonRaw, layoutType);
|
|
710
|
+
if (!slideData) {
|
|
711
|
+
const retryRes = await llmClient.chatCompletion({
|
|
712
|
+
messages: [
|
|
713
|
+
{ role: 'system', content: jsonPrompt },
|
|
714
|
+
{ role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
|
|
715
|
+
],
|
|
716
|
+
temperature: 0.2,
|
|
717
|
+
max_tokens: 2000,
|
|
718
|
+
});
|
|
719
|
+
const retryMsg = retryRes.choices[0]?.message;
|
|
720
|
+
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
721
|
+
slideData = parseContentFillJson(retryRaw, layoutType);
|
|
722
|
+
}
|
|
723
|
+
if (slideData) {
|
|
724
|
+
const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
|
|
725
|
+
return { html, isCodeTemplate: true };
|
|
726
|
+
}
|
|
780
727
|
}
|
|
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`);
|
|
728
|
+
catch (e) {
|
|
729
|
+
logger.warn(`Slide ${slideIndex + 1}: Code template fallback failed: ${e}`);
|
|
789
730
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
let
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger) {
|
|
734
|
+
const results = new Map();
|
|
735
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
736
|
+
const slide = plan.slides[i];
|
|
737
|
+
if (slide.type === 'title') {
|
|
738
|
+
results.set(i, {
|
|
739
|
+
index: i,
|
|
740
|
+
html: buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, i + 1),
|
|
741
|
+
isCodeTemplate: true,
|
|
742
|
+
});
|
|
799
743
|
}
|
|
800
|
-
else {
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
744
|
+
else if (slide.type === 'closing') {
|
|
745
|
+
const closingTagline = (slide.content_direction || '').replace(/감사합니다|thank\s*you/gi, '').trim() || undefined;
|
|
746
|
+
results.set(i, {
|
|
747
|
+
index: i,
|
|
748
|
+
html: buildClosingSlideHtml(plan.design, companyName, i + 1, language, closingTagline),
|
|
749
|
+
isCodeTemplate: true,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
else if (isOverviewSlide(slide.title, i)) {
|
|
753
|
+
const overviewItems = parseOverviewItems(slide.content_direction || '');
|
|
754
|
+
if (overviewItems.length >= 2) {
|
|
755
|
+
const firstLine = (slide.content_direction || '').split('\n')[0] || '';
|
|
756
|
+
const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
|
|
757
|
+
results.set(i, {
|
|
758
|
+
index: i,
|
|
759
|
+
html: buildOverviewSlideHtml(plan.design, slide.title, overviewSubtitle, overviewItems, i + 1),
|
|
760
|
+
isCodeTemplate: true,
|
|
761
|
+
});
|
|
804
762
|
}
|
|
805
763
|
}
|
|
806
764
|
}
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
let
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
765
|
+
const contentIndices = plan.slides
|
|
766
|
+
.map((s, i) => ({ slide: s, index: i }))
|
|
767
|
+
.filter(({ index }) => !results.has(index));
|
|
768
|
+
if (phaseLogger)
|
|
769
|
+
phaseLogger('powerpoint-create', 'html-generation', `Generating ${contentIndices.length} content slides in parallel (batch size ${MAX_CONCURRENT})...`);
|
|
770
|
+
for (let batch = 0; batch < contentIndices.length; batch += MAX_CONCURRENT) {
|
|
771
|
+
const chunk = contentIndices.slice(batch, batch + MAX_CONCURRENT);
|
|
772
|
+
const promises = chunk.map(({ slide, index }) => generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language)
|
|
773
|
+
.then(result => ({ index, result }))
|
|
774
|
+
.catch(err => {
|
|
775
|
+
logger.warn(`Slide ${index + 1}: Generation error: ${err}`);
|
|
776
|
+
return { index, result: null };
|
|
777
|
+
}));
|
|
778
|
+
const chunkResults = await Promise.all(promises);
|
|
779
|
+
for (const { index, result } of chunkResults) {
|
|
780
|
+
if (result) {
|
|
781
|
+
results.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
782
|
+
}
|
|
820
783
|
}
|
|
784
|
+
const done = Math.min(batch + MAX_CONCURRENT, contentIndices.length);
|
|
785
|
+
if (phaseLogger)
|
|
786
|
+
phaseLogger('powerpoint-create', 'html-generation', `Generated ${done}/${contentIndices.length} content slides`);
|
|
821
787
|
}
|
|
822
|
-
|
|
823
|
-
|
|
788
|
+
return results;
|
|
789
|
+
}
|
|
790
|
+
async function validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger) {
|
|
791
|
+
const failedIndices = [];
|
|
792
|
+
for (const [index, result] of htmlResults) {
|
|
793
|
+
const slide = plan.slides[index];
|
|
794
|
+
if (slide.type === 'title' || slide.type === 'closing')
|
|
795
|
+
continue;
|
|
796
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
797
|
+
const validation = validateSlideHtml(result.html, layoutType);
|
|
798
|
+
if (!validation.pass) {
|
|
799
|
+
logger.info(`Slide ${index + 1}: Post-validation failed: ${validation.feedback}`);
|
|
800
|
+
failedIndices.push(index);
|
|
801
|
+
}
|
|
824
802
|
}
|
|
825
|
-
if (
|
|
826
|
-
|
|
803
|
+
if (failedIndices.length === 0) {
|
|
804
|
+
if (phaseLogger)
|
|
805
|
+
phaseLogger('powerpoint-create', 'validation', 'All slides passed validation');
|
|
806
|
+
return htmlResults;
|
|
827
807
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
808
|
+
if (phaseLogger)
|
|
809
|
+
phaseLogger('powerpoint-create', 'validation', `${failedIndices.length} slides failed validation, regenerating...`);
|
|
810
|
+
for (const index of failedIndices) {
|
|
811
|
+
const slide = plan.slides[index];
|
|
812
|
+
const result = await generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language);
|
|
813
|
+
if (result) {
|
|
814
|
+
htmlResults.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
htmlResults.delete(index);
|
|
818
|
+
}
|
|
834
819
|
}
|
|
820
|
+
return htmlResults;
|
|
821
|
+
}
|
|
822
|
+
async function assemblePresentation(htmlResults, plan, timestamp, savePath, phaseLogger, toolCallLogger) {
|
|
823
|
+
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
824
|
+
ensureTempDir(tempWritePath);
|
|
835
825
|
const builtSlides = [];
|
|
836
826
|
let failCount = 0;
|
|
827
|
+
let totalToolCalls = 0;
|
|
837
828
|
const tempFiles = [];
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
829
|
+
const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
|
|
830
|
+
for (const [index, result] of sortedEntries) {
|
|
831
|
+
const slidePlan = plan.slides[index];
|
|
832
|
+
const slideNum = builtSlides.length + 1;
|
|
841
833
|
if (failCount >= 3) {
|
|
842
834
|
logger.warn('Too many slide failures, stopping');
|
|
843
835
|
break;
|
|
844
836
|
}
|
|
845
837
|
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
|
-
}
|
|
838
|
+
phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
|
|
953
839
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
954
840
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
955
841
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -957,7 +843,10 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
957
843
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
958
844
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
959
845
|
try {
|
|
960
|
-
const
|
|
846
|
+
const viewportHtml = result.isCodeTemplate
|
|
847
|
+
? injectEdgeSizing(result.html, plan.design.background_color)
|
|
848
|
+
: injectViewportCss(result.html, plan.design.background_color);
|
|
849
|
+
const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
|
|
961
850
|
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
962
851
|
tempFiles.push(htmlWritePath);
|
|
963
852
|
}
|
|
@@ -1027,7 +916,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1027
916
|
if (bgResult.success) {
|
|
1028
917
|
builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
1029
918
|
try {
|
|
1030
|
-
await powerpointClient.powerpointAddNote(slideNum, html);
|
|
919
|
+
await powerpointClient.powerpointAddNote(slideNum, result.html);
|
|
1031
920
|
}
|
|
1032
921
|
catch { }
|
|
1033
922
|
}
|
|
@@ -1041,7 +930,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1041
930
|
const slideCountResult = await powerpointClient.powerpointGetSlideCount();
|
|
1042
931
|
const totalSlidesInPpt = slideCountResult['slide_count'] || 0;
|
|
1043
932
|
if (totalSlidesInPpt > builtSlides.length) {
|
|
1044
|
-
logger.warn(`PPT has ${totalSlidesInPpt} slides but only ${builtSlides.length} were rendered — deleting ${totalSlidesInPpt - builtSlides.length} trailing blanks`);
|
|
1045
933
|
for (let d = totalSlidesInPpt; d > builtSlides.length; d--) {
|
|
1046
934
|
await powerpointClient.powerpointDeleteSlide(d);
|
|
1047
935
|
totalToolCalls++;
|
|
@@ -1053,6 +941,13 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1053
941
|
}
|
|
1054
942
|
}
|
|
1055
943
|
if (builtSlides.length > 0) {
|
|
944
|
+
if (savePath) {
|
|
945
|
+
const wslSavePath = savePath.replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_m, d) => `/mnt/${d.toLowerCase()}`);
|
|
946
|
+
try {
|
|
947
|
+
fs.unlinkSync(wslSavePath);
|
|
948
|
+
}
|
|
949
|
+
catch { }
|
|
950
|
+
}
|
|
1056
951
|
let saveResult = await powerpointClient.powerpointSave(savePath);
|
|
1057
952
|
totalToolCalls++;
|
|
1058
953
|
if (toolCallLogger)
|
|
@@ -1061,8 +956,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1061
956
|
const fallbackPath = 'C:\\temp\\presentation.pptx';
|
|
1062
957
|
saveResult = await powerpointClient.powerpointSave(fallbackPath);
|
|
1063
958
|
totalToolCalls++;
|
|
1064
|
-
if (toolCallLogger)
|
|
1065
|
-
toolCallLogger('powerpoint-create', 'powerpoint_save', { path: fallbackPath }, saveResult.success ? (saveResult['path'] || 'OK') : (saveResult.error || 'Failed'), saveResult.success, 0, totalToolCalls);
|
|
1066
959
|
}
|
|
1067
960
|
}
|
|
1068
961
|
for (const tempFile of tempFiles) {
|
|
@@ -1071,9 +964,117 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1071
964
|
}
|
|
1072
965
|
catch { }
|
|
1073
966
|
}
|
|
967
|
+
return { builtSlides, totalToolCalls };
|
|
968
|
+
}
|
|
969
|
+
async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
970
|
+
const startTime = Date.now();
|
|
971
|
+
const phaseLogger = getSubAgentPhaseLogger();
|
|
972
|
+
const toolCallLogger = getSubAgentToolCallLogger();
|
|
973
|
+
const timestamp = Date.now();
|
|
974
|
+
logger.enter('PPT-Create.runStructured.v2');
|
|
975
|
+
const hasKorean = /[\uac00-\ud7af\u1100-\u11ff]/.test(instruction);
|
|
976
|
+
const language = hasKorean ? 'ko' : 'en';
|
|
977
|
+
if (phaseLogger)
|
|
978
|
+
phaseLogger('powerpoint-create', 'init', 'Starting Design phase + opening PowerPoint...');
|
|
979
|
+
const [plan, createResult] = await Promise.all([
|
|
980
|
+
runDesignPhase(llmClient, instruction, phaseLogger),
|
|
981
|
+
powerpointClient.powerpointCreate(),
|
|
982
|
+
]);
|
|
983
|
+
if (!createResult.success) {
|
|
984
|
+
return { success: false, error: `Failed to create presentation: ${createResult['error']}` };
|
|
985
|
+
}
|
|
986
|
+
if (!plan) {
|
|
987
|
+
logger.error('PPT planning failed after retries — cannot create presentation');
|
|
988
|
+
return { success: false, error: 'Failed to generate presentation plan. Please try again.' };
|
|
989
|
+
}
|
|
990
|
+
if (plan.slides.length > 20) {
|
|
991
|
+
const firstSlide = plan.slides[0];
|
|
992
|
+
const lastSlide = plan.slides[plan.slides.length - 1];
|
|
993
|
+
const contentSlides = plan.slides.slice(1, -1).slice(0, 18);
|
|
994
|
+
plan.slides = [firstSlide, ...contentSlides, lastSlide];
|
|
995
|
+
}
|
|
996
|
+
const userYearMatch = instruction.match(/(\d{4})년/);
|
|
997
|
+
if (userYearMatch) {
|
|
998
|
+
const userYear = userYearMatch[1];
|
|
999
|
+
for (const slide of plan.slides) {
|
|
1000
|
+
if (slide.type === 'content' && slide.content_direction) {
|
|
1001
|
+
if (!slide.content_direction.includes(`${userYear}년`)) {
|
|
1002
|
+
slide.content_direction += ` (Note: This report covers ${userYear}년 data.)`;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
let savePath = explicitSavePath;
|
|
1008
|
+
if (!savePath) {
|
|
1009
|
+
const fullPathMatch = instruction.match(/([A-Za-z]:\\[^\s,]+\.pptx|\/[^\s,]+\.pptx)/i);
|
|
1010
|
+
if (fullPathMatch) {
|
|
1011
|
+
savePath = fullPathMatch[1];
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
const nameMatch = instruction.match(/([\w][\w\-_.]*\.pptx)/i);
|
|
1015
|
+
if (nameMatch) {
|
|
1016
|
+
savePath = `C:\\temp\\${nameMatch[1]}`;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const titleSlidePlanForDate = plan.slides.find(s => s.type === 'title');
|
|
1021
|
+
const dateSearchTexts = [instruction, titleSlidePlanForDate?.title || '', titleSlidePlanForDate?.content_direction || ''];
|
|
1022
|
+
let kstDate = '';
|
|
1023
|
+
for (const text of dateSearchTexts) {
|
|
1024
|
+
const dateMatch = text.match(/(\d{4})년\s*(\d{1,2})\s*(월|분기)/);
|
|
1025
|
+
if (dateMatch) {
|
|
1026
|
+
kstDate = `${dateMatch[1]}년 ${dateMatch[2]}${dateMatch[3]}`;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (!kstDate) {
|
|
1031
|
+
const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000);
|
|
1032
|
+
kstDate = `${kstNow.getUTCFullYear()}년 ${kstNow.getUTCMonth() + 1}월`;
|
|
1033
|
+
}
|
|
1034
|
+
const titleSlidePlan = plan.slides.find(s => s.type === 'title');
|
|
1035
|
+
const rawTitleText = titleSlidePlan?.title || '';
|
|
1036
|
+
const titleSeps = [' - ', ' – ', ' — ', ': ', ' | '];
|
|
1037
|
+
let companyName = rawTitleText;
|
|
1038
|
+
let titleSubtitle = '';
|
|
1039
|
+
for (const sep of titleSeps) {
|
|
1040
|
+
const idx = rawTitleText.indexOf(sep);
|
|
1041
|
+
if (idx > 0) {
|
|
1042
|
+
companyName = rawTitleText.slice(0, idx).trim();
|
|
1043
|
+
titleSubtitle = rawTitleText.slice(idx + sep.length).trim();
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (!titleSubtitle && titleSlidePlan) {
|
|
1048
|
+
titleSubtitle = ((titleSlidePlan.content_direction || '').split('\n')[0] || '').trim().slice(0, 120);
|
|
1049
|
+
}
|
|
1050
|
+
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
1051
|
+
titleSubtitle = '';
|
|
1052
|
+
}
|
|
1053
|
+
const companyMatch = instruction.match(/회사명\s*[::]\s*([^\s,,、]+)/);
|
|
1054
|
+
if (companyMatch && companyMatch[1]) {
|
|
1055
|
+
const companyName_ = companyMatch[1];
|
|
1056
|
+
if (titleSlidePlan && titleSlidePlan.title.trim() !== companyName_) {
|
|
1057
|
+
const originalTitle = titleSlidePlan.title;
|
|
1058
|
+
titleSlidePlan.title = companyName_;
|
|
1059
|
+
companyName = companyName_;
|
|
1060
|
+
if (!titleSlidePlan.content_direction?.includes(originalTitle)) {
|
|
1061
|
+
const stripped = originalTitle.replace(companyName_, '').replace(/^\s*[-–—:|\s]+/, '').trim();
|
|
1062
|
+
titleSubtitle = stripped || originalTitle;
|
|
1063
|
+
titleSlidePlan.content_direction = titleSubtitle + (titleSlidePlan.content_direction ? '\n' + titleSlidePlan.content_direction : '');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (phaseLogger)
|
|
1068
|
+
phaseLogger('powerpoint-create', 'html-generation', 'Starting parallel HTML generation...');
|
|
1069
|
+
const htmlResults = await generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger);
|
|
1070
|
+
const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
|
|
1071
|
+
if (phaseLogger)
|
|
1072
|
+
phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
|
|
1073
|
+
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, phaseLogger, toolCallLogger);
|
|
1074
1074
|
const duration = Date.now() - startTime;
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1075
|
+
const slideList = builtSlides.join('\n');
|
|
1076
|
+
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}`;
|
|
1077
|
+
logger.exit('PPT-Create.runStructured.v2', { slideCount: builtSlides.length, totalToolCalls, duration });
|
|
1077
1078
|
return {
|
|
1078
1079
|
success: builtSlides.length > 0,
|
|
1079
1080
|
result: summary,
|