hanseol-dev 5.0.3-dev.6 → 5.0.3-dev.61
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/common/sub-agent.d.ts +0 -4
- package/dist/agents/common/sub-agent.d.ts.map +1 -1
- package/dist/agents/common/sub-agent.js +6 -10
- package/dist/agents/common/sub-agent.js.map +1 -1
- package/dist/agents/office/excel-agent.js +1 -1
- package/dist/agents/office/excel-agent.js.map +1 -1
- package/dist/agents/office/excel-create-agent.d.ts.map +1 -1
- package/dist/agents/office/excel-create-agent.js +1 -3
- package/dist/agents/office/excel-create-agent.js.map +1 -1
- package/dist/agents/office/excel-create-prompts.d.ts +1 -1
- package/dist/agents/office/excel-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/excel-create-prompts.js +24 -11
- package/dist/agents/office/excel-create-prompts.js.map +1 -1
- package/dist/agents/office/powerpoint-agent.js +1 -1
- package/dist/agents/office/powerpoint-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.js +251 -346
- package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.d.ts +3 -2
- package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.js +290 -256
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
- package/dist/agents/office/prompts.d.ts +4 -4
- package/dist/agents/office/prompts.d.ts.map +1 -1
- package/dist/agents/office/prompts.js +2 -2
- package/dist/agents/office/word-agent.js +1 -1
- package/dist/agents/office/word-agent.js.map +1 -1
- package/dist/agents/office/word-create-agent.d.ts.map +1 -1
- package/dist/agents/office/word-create-agent.js +2 -4
- package/dist/agents/office/word-create-agent.js.map +1 -1
- package/dist/agents/office/word-create-prompts.d.ts +3 -3
- package/dist/agents/office/word-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/word-create-prompts.js +167 -43
- package/dist/agents/office/word-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/prompts/agents/planning.d.ts.map +1 -1
- package/dist/prompts/agents/planning.js +7 -0
- package/dist/prompts/agents/planning.js.map +1 -1
- package/dist/tools/office/excel-tools/sheet-builders.d.ts.map +1 -1
- package/dist/tools/office/excel-tools/sheet-builders.js +36 -7
- package/dist/tools/office/excel-tools/sheet-builders.js.map +1 -1
- package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-client.js +4 -0
- package/dist/tools/office/powerpoint-client.js.map +1 -1
- package/dist/tools/office/word-client.d.ts +15 -0
- package/dist/tools/office/word-client.d.ts.map +1 -1
- package/dist/tools/office/word-client.js +256 -5
- package/dist/tools/office/word-client.js.map +1 -1
- package/dist/tools/office/word-tools/section-builders.d.ts +2 -0
- package/dist/tools/office/word-tools/section-builders.d.ts.map +1 -1
- package/dist/tools/office/word-tools/section-builders.js +230 -50
- package/dist/tools/office/word-tools/section-builders.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 { PPT_DESIGN_PROMPT,
|
|
7
|
+
import { PPT_DESIGN_PROMPT, validateSlideHtml, buildFreeHtmlPrompt, } from './powerpoint-create-prompts.js';
|
|
8
8
|
const DEFAULT_DESIGN = {
|
|
9
9
|
primary_color: '#1B2A4A',
|
|
10
10
|
accent_color: '#00D4AA',
|
|
@@ -228,18 +228,6 @@ function parseJsonPlan(raw) {
|
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
-
function extractHtml(raw) {
|
|
232
|
-
const trimmed = raw.trim();
|
|
233
|
-
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html'))
|
|
234
|
-
return trimmed;
|
|
235
|
-
const fenceMatch = trimmed.match(/```(?:html)?\s*\n([\s\S]*?)\n```/);
|
|
236
|
-
if (fenceMatch?.[1])
|
|
237
|
-
return fenceMatch[1].trim();
|
|
238
|
-
const docMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
239
|
-
if (docMatch?.[1])
|
|
240
|
-
return docMatch[1].trim();
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
231
|
function injectEdgeSizing(html, backgroundColor) {
|
|
244
232
|
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
245
233
|
const bgColor = backgroundColor || '#000000';
|
|
@@ -258,64 +246,6 @@ function injectEdgeSizing(html, backgroundColor) {
|
|
|
258
246
|
}
|
|
259
247
|
return result;
|
|
260
248
|
}
|
|
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;
|
|
276
|
-
}
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
279
|
-
function injectTitleContrastFix(html, designTextColor) {
|
|
280
|
-
const safeColor = designTextColor.replace(/'/g, "\\'");
|
|
281
|
-
const script = `<script>(function(){` +
|
|
282
|
-
`var dc='${safeColor}';` +
|
|
283
|
-
`function lum(c){var m=c.match(/[\\d.]+/g);if(!m||m.length<3)return -1;` +
|
|
284
|
-
`var s=[m[0]/255,m[1]/255,m[2]/255].map(function(v){return v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4)});` +
|
|
285
|
-
`return 0.2126*s[0]+0.7152*s[1]+0.0722*s[2]}` +
|
|
286
|
-
`function getBg(el){while(el){var s=getComputedStyle(el);var bg=s.backgroundColor;` +
|
|
287
|
-
`var m=bg.match(/[\\d.]+/g);if(m&&m.length>=3){if(m.length<4||parseFloat(m[3])>0.1)return bg}` +
|
|
288
|
-
`el=el.parentElement}return'rgb(255,255,255)'}` +
|
|
289
|
-
`var els=document.querySelectorAll('h1,h2');` +
|
|
290
|
-
`for(var i=0;i<els.length;i++){var el=els[i];var cs=getComputedStyle(el);` +
|
|
291
|
-
`var tfc=cs.webkitTextFillColor||'';` +
|
|
292
|
-
`if(tfc==='transparent'||tfc==='rgba(0, 0, 0, 0)'){` +
|
|
293
|
-
`el.style.setProperty('-webkit-text-fill-color','initial','important')}` +
|
|
294
|
-
`if(parseFloat(cs.opacity)<0.6){el.style.setProperty('opacity','1','important')}` +
|
|
295
|
-
`var fg=cs.color;var bg=getBg(el);var fl=lum(fg),bl=lum(bg);` +
|
|
296
|
-
`if(fl>=0&&bl>=0){var r=(Math.max(fl,bl)+0.05)/(Math.min(fl,bl)+0.05);` +
|
|
297
|
-
`if(r<3){el.style.setProperty('color',dc,'important');` +
|
|
298
|
-
`el.style.setProperty('-webkit-text-fill-color',dc,'important')}}}` +
|
|
299
|
-
`})()<\/script>`;
|
|
300
|
-
if (html.includes('</body>')) {
|
|
301
|
-
return html.replace('</body>', `${script}</body>`);
|
|
302
|
-
}
|
|
303
|
-
return html + script;
|
|
304
|
-
}
|
|
305
|
-
function injectOverflowScale(html) {
|
|
306
|
-
const script = `<script>(function(){` +
|
|
307
|
-
`var b=document.body;var h=b.scrollHeight;` +
|
|
308
|
-
`if(h>1085){var s=Math.max(0.75,1080/h);` +
|
|
309
|
-
`b.style.transform='scale('+s+')';` +
|
|
310
|
-
`b.style.transformOrigin='top left';` +
|
|
311
|
-
`b.style.width=(1920/s)+'px';` +
|
|
312
|
-
`b.style.height=(1080/s)+'px'}` +
|
|
313
|
-
`})()<\/script>`;
|
|
314
|
-
if (html.includes('</body>')) {
|
|
315
|
-
return html.replace('</body>', `${script}</body>`);
|
|
316
|
-
}
|
|
317
|
-
return html + script;
|
|
318
|
-
}
|
|
319
249
|
function escapeHtml(text) {
|
|
320
250
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
321
251
|
}
|
|
@@ -473,139 +403,9 @@ body::after {
|
|
|
473
403
|
</body>
|
|
474
404
|
</html>`;
|
|
475
405
|
}
|
|
476
|
-
function isOverviewSlide(title, slideIndex) {
|
|
477
|
-
if (slideIndex !== 1)
|
|
478
|
-
return false;
|
|
479
|
-
return /개요|목차|overview|agenda|outline|순서|발표\s*구성|contents|목록/i.test(title);
|
|
480
|
-
}
|
|
481
|
-
function parseOverviewItems(contentDirection) {
|
|
482
|
-
const items = [];
|
|
483
|
-
const lines = contentDirection.split(/\n/).filter(l => l.trim());
|
|
484
|
-
for (const line of lines) {
|
|
485
|
-
const match = line.match(/^\d+[\.\)]\s*(.+?)(?:\s*[-–—:]\s*(.+))?$/);
|
|
486
|
-
if (match) {
|
|
487
|
-
items.push({ title: match[1].trim(), desc: match[2]?.trim() || '' });
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (items.length === 0) {
|
|
491
|
-
const parts = contentDirection.split(/[,;·•]/).map(s => s.trim()).filter(Boolean);
|
|
492
|
-
for (const part of parts) {
|
|
493
|
-
const sepMatch = part.match(/^(.+?)(?:\s*[-–—:]\s*(.+))?$/);
|
|
494
|
-
if (sepMatch) {
|
|
495
|
-
items.push({ title: sepMatch[1].trim(), desc: sepMatch[2]?.trim() || '' });
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return items.slice(0, 5);
|
|
500
|
-
}
|
|
501
|
-
function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
|
|
502
|
-
const badgeColors = [
|
|
503
|
-
design.primary_color, design.accent_color, design.gradient_end,
|
|
504
|
-
design.primary_color, design.accent_color,
|
|
505
|
-
];
|
|
506
|
-
const itemCount = items.length;
|
|
507
|
-
const topRow = itemCount <= 3 ? items : items.slice(0, Math.ceil(itemCount / 2));
|
|
508
|
-
const bottomRow = itemCount <= 3 ? [] : items.slice(Math.ceil(itemCount / 2));
|
|
509
|
-
function renderCard(item, idx) {
|
|
510
|
-
const color = badgeColors[idx % badgeColors.length];
|
|
511
|
-
return `
|
|
512
|
-
<div class="card">
|
|
513
|
-
<div class="badge" style="background:${color}">${idx + 1}</div>
|
|
514
|
-
<div class="card-title">${escapeHtml(item.title)}</div>
|
|
515
|
-
${item.desc ? `<div class="card-desc">${escapeHtml(item.desc)}</div>` : ''}
|
|
516
|
-
</div>`;
|
|
517
|
-
}
|
|
518
|
-
return `<!DOCTYPE html>
|
|
519
|
-
<html lang="ko">
|
|
520
|
-
<head>
|
|
521
|
-
<meta charset="UTF-8">
|
|
522
|
-
<style>
|
|
523
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
524
|
-
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
525
|
-
body {
|
|
526
|
-
background: ${design.background_color};
|
|
527
|
-
font-family: "${design.font_body}", "${design.font_title}", "Segoe UI", "Malgun Gothic", sans-serif;
|
|
528
|
-
display: flex; flex-direction: column;
|
|
529
|
-
word-break: keep-all; overflow-wrap: break-word;
|
|
530
|
-
}
|
|
531
|
-
.header {
|
|
532
|
-
background: linear-gradient(135deg, ${design.primary_color}, ${design.gradient_end});
|
|
533
|
-
padding: 48px 80px 40px;
|
|
534
|
-
flex-shrink: 0;
|
|
535
|
-
}
|
|
536
|
-
.header-title {
|
|
537
|
-
font-size: 52px; font-weight: 800; color: #ffffff;
|
|
538
|
-
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
539
|
-
margin-bottom: 8px;
|
|
540
|
-
}
|
|
541
|
-
.header-subtitle {
|
|
542
|
-
font-size: 24px; font-weight: 400; color: rgba(255,255,255,0.75);
|
|
543
|
-
}
|
|
544
|
-
.content {
|
|
545
|
-
flex: 1; display: flex; flex-direction: column;
|
|
546
|
-
padding: 48px 80px 40px;
|
|
547
|
-
gap: 24px;
|
|
548
|
-
justify-content: center;
|
|
549
|
-
}
|
|
550
|
-
.row {
|
|
551
|
-
display: flex; gap: 24px;
|
|
552
|
-
justify-content: center;
|
|
553
|
-
}
|
|
554
|
-
.card {
|
|
555
|
-
flex: 1;
|
|
556
|
-
max-width: 340px;
|
|
557
|
-
background: #ffffff;
|
|
558
|
-
border-radius: 16px;
|
|
559
|
-
padding: 36px 32px;
|
|
560
|
-
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
|
|
561
|
-
display: flex; flex-direction: column;
|
|
562
|
-
align-items: center; text-align: center;
|
|
563
|
-
gap: 12px;
|
|
564
|
-
}
|
|
565
|
-
.badge {
|
|
566
|
-
width: 44px; height: 44px;
|
|
567
|
-
border-radius: 50%;
|
|
568
|
-
color: #ffffff;
|
|
569
|
-
font-size: 20px; font-weight: 700;
|
|
570
|
-
display: flex; align-items: center; justify-content: center;
|
|
571
|
-
flex-shrink: 0;
|
|
572
|
-
}
|
|
573
|
-
.card-title {
|
|
574
|
-
font-size: 26px; font-weight: 700; color: ${design.text_color};
|
|
575
|
-
line-height: 1.3;
|
|
576
|
-
}
|
|
577
|
-
.card-desc {
|
|
578
|
-
font-size: 22px; font-weight: 400; color: ${design.text_color}aa;
|
|
579
|
-
line-height: 1.5;
|
|
580
|
-
}
|
|
581
|
-
.page-num {
|
|
582
|
-
position: absolute;
|
|
583
|
-
bottom: 24px; right: 44px;
|
|
584
|
-
font-size: 13px;
|
|
585
|
-
color: ${design.text_color}55;
|
|
586
|
-
}
|
|
587
|
-
</style>
|
|
588
|
-
</head>
|
|
589
|
-
<body>
|
|
590
|
-
<div class="header">
|
|
591
|
-
<div class="header-title">${escapeHtml(title)}</div>
|
|
592
|
-
${subtitle ? `<div class="header-subtitle">${escapeHtml(subtitle)}</div>` : ''}
|
|
593
|
-
</div>
|
|
594
|
-
<div class="content">
|
|
595
|
-
<div class="row">
|
|
596
|
-
${topRow.map((item, idx) => renderCard(item, idx)).join('')}
|
|
597
|
-
</div>
|
|
598
|
-
${bottomRow.length > 0 ? `<div class="row">
|
|
599
|
-
${bottomRow.map((item, idx) => renderCard(item, topRow.length + idx)).join('')}
|
|
600
|
-
</div>` : ''}
|
|
601
|
-
</div>
|
|
602
|
-
<div class="page-num">${slideNum}</div>
|
|
603
|
-
</body>
|
|
604
|
-
</html>`;
|
|
605
|
-
}
|
|
606
406
|
function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
|
|
607
407
|
const items = [];
|
|
608
|
-
const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d
|
|
408
|
+
const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d+[.)]\s*|Step\s*\d+[.:]\s*/i);
|
|
609
409
|
for (const part of parts) {
|
|
610
410
|
const cleaned = part.replace(/Layout:.*$/i, '').trim();
|
|
611
411
|
if (cleaned.length > 5) {
|
|
@@ -613,8 +413,28 @@ function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
|
|
|
613
413
|
items.push(sentence.slice(0, 80));
|
|
614
414
|
}
|
|
615
415
|
}
|
|
416
|
+
if (items.length < 3) {
|
|
417
|
+
const commaParts = contentDirection.split(/[,;,;]\s*/);
|
|
418
|
+
for (const part of commaParts) {
|
|
419
|
+
const cleaned = part.replace(/Layout:.*$/i, '').trim();
|
|
420
|
+
if (cleaned.length > 8 && !items.includes(cleaned.slice(0, 80))) {
|
|
421
|
+
items.push(cleaned.slice(0, 80));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (items.length < 3) {
|
|
426
|
+
const sentences = contentDirection.split(/[.。!?]\s+/);
|
|
427
|
+
for (const s of sentences) {
|
|
428
|
+
const cleaned = s.replace(/Layout:.*$/i, '').trim();
|
|
429
|
+
if (cleaned.length > 10 && !items.some(i => i.startsWith(cleaned.slice(0, 20)))) {
|
|
430
|
+
items.push(cleaned.slice(0, 80));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
616
434
|
const bulletItems = items.slice(0, 6);
|
|
617
|
-
const
|
|
435
|
+
const useGrid = bulletItems.length >= 4;
|
|
436
|
+
const gridCols = useGrid ? 'grid-template-columns:1fr 1fr' : 'grid-template-columns:1fr';
|
|
437
|
+
const pointsHtml = bulletItems.map((item, i) => `<div class="point"><div class="point-num">${i + 1}</div><div class="point-text">${escapeHtml(item)}</div></div>`).join('\n ');
|
|
618
438
|
return `<!DOCTYPE html>
|
|
619
439
|
<html lang="ko">
|
|
620
440
|
<head>
|
|
@@ -633,9 +453,10 @@ body {
|
|
|
633
453
|
.slide-num { font-size: 14px; color: ${design.accent_color}; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
|
|
634
454
|
h1 { font-size: 52px; font-weight: 700; color: ${design.text_color}; font-family: "${design.font_title}", "Segoe UI", sans-serif; line-height: 1.2; }
|
|
635
455
|
.accent-bar { width: 80px; height: 4px; background: ${design.accent_color}; margin-top: 20px; border-radius: 2px; }
|
|
636
|
-
.content { flex: 1; display:
|
|
637
|
-
.
|
|
638
|
-
.
|
|
456
|
+
.content { flex: 1; display: grid; ${gridCols}; gap: 24px; align-content: center; }
|
|
457
|
+
.point { display: flex; align-items: flex-start; gap: 20px; padding: 28px 32px; background: #fff; border-radius: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.06); }
|
|
458
|
+
.point-num { width: 48px; height: 48px; border-radius: 50%; background: ${design.accent_color}; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 700; flex-shrink: 0; }
|
|
459
|
+
.point-text { font-size: 26px; line-height: 1.5; color: ${design.text_color}; flex: 1; }
|
|
639
460
|
</style>
|
|
640
461
|
</head>
|
|
641
462
|
<body>
|
|
@@ -645,7 +466,7 @@ h1 { font-size: 52px; font-weight: 700; color: ${design.text_color}; font-family
|
|
|
645
466
|
<div class="accent-bar"></div>
|
|
646
467
|
</div>
|
|
647
468
|
<div class="content">
|
|
648
|
-
${
|
|
469
|
+
${pointsHtml}
|
|
649
470
|
</div>
|
|
650
471
|
</body>
|
|
651
472
|
</html>`;
|
|
@@ -744,67 +565,60 @@ async function runDesignPhase(llmClient, instruction, phaseLogger) {
|
|
|
744
565
|
}
|
|
745
566
|
return plan;
|
|
746
567
|
}
|
|
568
|
+
function extractHtmlFromResponse(raw) {
|
|
569
|
+
const trimmed = raw.trim();
|
|
570
|
+
if (/^<!DOCTYPE/i.test(trimmed) || /^<html/i.test(trimmed)) {
|
|
571
|
+
return trimmed;
|
|
572
|
+
}
|
|
573
|
+
const fenceMatch = trimmed.match(/```(?:html)?\s*\n?(<!DOCTYPE[\s\S]*?<\/html>)\s*\n?```/i);
|
|
574
|
+
if (fenceMatch)
|
|
575
|
+
return fenceMatch[1];
|
|
576
|
+
const htmlMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
577
|
+
if (htmlMatch)
|
|
578
|
+
return htmlMatch[1];
|
|
579
|
+
const htmlTagMatch = trimmed.match(/(<html[\s\S]*<\/html>)/i);
|
|
580
|
+
if (htmlTagMatch)
|
|
581
|
+
return '<!DOCTYPE html>\n' + htmlTagMatch[1];
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
747
584
|
async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, totalSlides, language) {
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
const
|
|
585
|
+
const contentDirection = (slide.content_direction || '').trim();
|
|
586
|
+
logger.info(`Slide ${slideIndex + 1}: Generating free-form HTML for "${slide.title}"`);
|
|
587
|
+
const htmlPrompt = buildFreeHtmlPrompt(slide.title, contentDirection, design, slideIndex, totalSlides, language);
|
|
751
588
|
try {
|
|
752
589
|
const res = await llmClient.chatCompletion({
|
|
753
590
|
messages: [
|
|
754
|
-
{ role: 'system', content:
|
|
755
|
-
{ role: 'user', content: '
|
|
591
|
+
{ role: 'system', content: htmlPrompt },
|
|
592
|
+
{ role: 'user', content: 'Output the complete HTML now.' },
|
|
756
593
|
],
|
|
757
|
-
temperature: 0.
|
|
758
|
-
max_tokens:
|
|
594
|
+
temperature: 0.6,
|
|
595
|
+
max_tokens: 4000,
|
|
759
596
|
});
|
|
760
597
|
const msg = res.choices[0]?.message;
|
|
761
598
|
const rawHtml = msg ? extractContent(msg) : '';
|
|
762
|
-
|
|
763
|
-
if (html) {
|
|
764
|
-
|
|
765
|
-
if (validation.pass) {
|
|
766
|
-
return { html, isCodeTemplate: false };
|
|
767
|
-
}
|
|
768
|
-
logger.info(`Slide ${slideIndex + 1}: Direct HTML failed validation: ${validation.feedback}`);
|
|
599
|
+
let html = extractHtmlFromResponse(rawHtml);
|
|
600
|
+
if (html && !hasPlaceholderText(html)) {
|
|
601
|
+
return { html, isCodeTemplate: false };
|
|
769
602
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
logger.warn(`Slide ${slideIndex + 1}: Direct HTML LLM call failed: ${e}`);
|
|
773
|
-
}
|
|
774
|
-
logger.info(`Slide ${slideIndex + 1}: Falling back to code template "${layoutType}"`);
|
|
775
|
-
const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
|
|
776
|
-
try {
|
|
777
|
-
const jsonRes = await llmClient.chatCompletion({
|
|
603
|
+
logger.warn(`Slide ${slideIndex + 1}: First HTML attempt failed. Raw length: ${rawHtml.length}. Retrying...`);
|
|
604
|
+
const retryRes = await llmClient.chatCompletion({
|
|
778
605
|
messages: [
|
|
779
|
-
{ role: 'system', content:
|
|
780
|
-
{ role: 'user', content: 'Output the
|
|
606
|
+
{ role: 'system', content: htmlPrompt },
|
|
607
|
+
{ role: 'user', content: 'Output ONLY the complete HTML document. Start with <!DOCTYPE html> and end with </html>. No explanation.' },
|
|
781
608
|
],
|
|
782
|
-
temperature: 0.
|
|
783
|
-
max_tokens:
|
|
609
|
+
temperature: 0.4,
|
|
610
|
+
max_tokens: 4000,
|
|
784
611
|
});
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
if (!
|
|
789
|
-
|
|
790
|
-
messages: [
|
|
791
|
-
{ role: 'system', content: jsonPrompt },
|
|
792
|
-
{ role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
|
|
793
|
-
],
|
|
794
|
-
temperature: 0.2,
|
|
795
|
-
max_tokens: 2000,
|
|
796
|
-
});
|
|
797
|
-
const retryMsg = retryRes.choices[0]?.message;
|
|
798
|
-
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
799
|
-
slideData = parseContentFillJson(retryRaw, layoutType);
|
|
800
|
-
}
|
|
801
|
-
if (slideData) {
|
|
802
|
-
const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
|
|
803
|
-
return { html, isCodeTemplate: true };
|
|
612
|
+
const retryMsg = retryRes.choices[0]?.message;
|
|
613
|
+
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
614
|
+
html = extractHtmlFromResponse(retryRaw);
|
|
615
|
+
if (html && !hasPlaceholderText(html)) {
|
|
616
|
+
return { html, isCodeTemplate: false };
|
|
804
617
|
}
|
|
618
|
+
logger.warn(`Slide ${slideIndex + 1}: Both HTML attempts failed.`);
|
|
805
619
|
}
|
|
806
620
|
catch (e) {
|
|
807
|
-
logger.warn(`Slide ${slideIndex + 1}:
|
|
621
|
+
logger.warn(`Slide ${slideIndex + 1}: HTML generation error: ${e}`);
|
|
808
622
|
}
|
|
809
623
|
return null;
|
|
810
624
|
}
|
|
@@ -827,18 +641,6 @@ async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstD
|
|
|
827
641
|
isCodeTemplate: true,
|
|
828
642
|
});
|
|
829
643
|
}
|
|
830
|
-
else if (isOverviewSlide(slide.title, i)) {
|
|
831
|
-
const overviewItems = parseOverviewItems(slide.content_direction || '');
|
|
832
|
-
if (overviewItems.length >= 2) {
|
|
833
|
-
const firstLine = (slide.content_direction || '').split('\n')[0] || '';
|
|
834
|
-
const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
|
|
835
|
-
results.set(i, {
|
|
836
|
-
index: i,
|
|
837
|
-
html: buildOverviewSlideHtml(plan.design, slide.title, overviewSubtitle, overviewItems, i + 1),
|
|
838
|
-
isCodeTemplate: true,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
644
|
}
|
|
843
645
|
const contentIndices = plan.slides
|
|
844
646
|
.map((s, i) => ({ slide: s, index: i }))
|
|
@@ -877,8 +679,7 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
|
|
|
877
679
|
const slide = plan.slides[index];
|
|
878
680
|
if (slide.type === 'title' || slide.type === 'closing')
|
|
879
681
|
continue;
|
|
880
|
-
const
|
|
881
|
-
const validation = validateSlideHtml(result.html, layoutType);
|
|
682
|
+
const validation = validateSlideHtml(result.html);
|
|
882
683
|
if (!validation.pass) {
|
|
883
684
|
logger.info(`Slide ${index + 1}: Post-validation failed: ${validation.feedback}`);
|
|
884
685
|
failedIndices.push(index);
|
|
@@ -920,23 +721,28 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
|
|
|
920
721
|
}
|
|
921
722
|
return htmlResults;
|
|
922
723
|
}
|
|
923
|
-
|
|
724
|
+
const MAX_SCREENSHOT_CONCURRENT = 4;
|
|
725
|
+
async function assemblePresentation(htmlResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger) {
|
|
924
726
|
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
925
727
|
ensureTempDir(tempWritePath);
|
|
728
|
+
const totalSlides = htmlResults.size;
|
|
729
|
+
const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
|
|
926
730
|
const builtSlides = [];
|
|
927
|
-
let failCount = 0;
|
|
928
731
|
let totalToolCalls = 0;
|
|
929
732
|
const tempFiles = [];
|
|
930
|
-
const
|
|
733
|
+
const initCountRes = await powerpointClient.powerpointGetSlideCount();
|
|
734
|
+
const initSlideCount = initCountRes['slide_count'] || 1;
|
|
735
|
+
const slidesToAdd = Math.max(0, totalSlides - initSlideCount);
|
|
736
|
+
if (phaseLogger)
|
|
737
|
+
phaseLogger('powerpoint-create', 'assembly', `Pre-creating ${totalSlides} slides (existing: ${initSlideCount}, adding: ${slidesToAdd})...`);
|
|
738
|
+
for (let i = 0; i < slidesToAdd; i++) {
|
|
739
|
+
await powerpointClient.powerpointAddSlide(7);
|
|
740
|
+
totalToolCalls++;
|
|
741
|
+
}
|
|
742
|
+
const slideFiles = new Map();
|
|
743
|
+
let slideNum = 0;
|
|
931
744
|
for (const [index, result] of sortedEntries) {
|
|
932
|
-
|
|
933
|
-
const slideNum = builtSlides.length + 1;
|
|
934
|
-
if (failCount >= 3) {
|
|
935
|
-
logger.warn('Too many slide failures, stopping');
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
if (phaseLogger)
|
|
939
|
-
phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
|
|
745
|
+
slideNum++;
|
|
940
746
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
941
747
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
942
748
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -944,102 +750,201 @@ async function assemblePresentation(htmlResults, plan, timestamp, savePath, phas
|
|
|
944
750
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
945
751
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
946
752
|
try {
|
|
947
|
-
const viewportHtml = result.
|
|
948
|
-
|
|
949
|
-
: injectViewportCss(result.html, plan.design.background_color);
|
|
950
|
-
const contrastFixed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
|
|
951
|
-
const processed = injectOverflowScale(contrastFixed);
|
|
952
|
-
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
753
|
+
const viewportHtml = injectEdgeSizing(result.html, plan.design.background_color);
|
|
754
|
+
fs.writeFileSync(htmlWritePath, viewportHtml, 'utf-8');
|
|
953
755
|
tempFiles.push(htmlWritePath);
|
|
756
|
+
slideFiles.set(index, { slideNum, htmlWritePath, pngWritePath, htmlWinPath, pngWinPath });
|
|
954
757
|
}
|
|
955
758
|
catch (e) {
|
|
956
|
-
logger.warn(`Slide ${slideNum}: Failed to write HTML
|
|
957
|
-
failCount++;
|
|
958
|
-
continue;
|
|
759
|
+
logger.warn(`Slide ${slideNum}: Failed to write HTML: ${e}`);
|
|
959
760
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
renderSuccess = retryRender.success;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
catch (e) {
|
|
973
|
-
logger.warn(`Slide ${slideNum}: Edge screenshot failed: ${e}`);
|
|
761
|
+
}
|
|
762
|
+
if (phaseLogger)
|
|
763
|
+
phaseLogger('powerpoint-create', 'assembly', `Rendering ${slideFiles.size} screenshots in parallel (batch ${MAX_SCREENSHOT_CONCURRENT})...`);
|
|
764
|
+
const screenshotEntries = [...slideFiles.entries()].sort((a, b) => a[0] - b[0]);
|
|
765
|
+
const screenshotSuccess = new Set();
|
|
766
|
+
for (let batch = 0; batch < screenshotEntries.length; batch += MAX_SCREENSHOT_CONCURRENT) {
|
|
767
|
+
const chunk = screenshotEntries.slice(batch, batch + MAX_SCREENSHOT_CONCURRENT);
|
|
768
|
+
const batchResults = await Promise.all(chunk.map(async ([index, files]) => {
|
|
769
|
+
let success = false;
|
|
974
770
|
try {
|
|
975
|
-
|
|
976
|
-
const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
771
|
+
const result = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
977
772
|
totalToolCalls++;
|
|
978
|
-
|
|
773
|
+
success = result.success;
|
|
774
|
+
if (!success) {
|
|
775
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
776
|
+
const retry = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
777
|
+
totalToolCalls++;
|
|
778
|
+
success = retry.success;
|
|
779
|
+
}
|
|
979
780
|
}
|
|
980
781
|
catch {
|
|
981
|
-
|
|
782
|
+
try {
|
|
783
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
784
|
+
const retry = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
785
|
+
totalToolCalls++;
|
|
786
|
+
success = retry.success;
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
success = false;
|
|
790
|
+
}
|
|
982
791
|
}
|
|
792
|
+
try {
|
|
793
|
+
fs.unlinkSync(files.htmlWritePath);
|
|
794
|
+
}
|
|
795
|
+
catch { }
|
|
796
|
+
if (success) {
|
|
797
|
+
try {
|
|
798
|
+
const stat = fs.statSync(files.pngWritePath);
|
|
799
|
+
if (stat.size < 15000) {
|
|
800
|
+
logger.warn(`Slide ${files.slideNum}: Screenshot too small (${stat.size} bytes)`);
|
|
801
|
+
success = false;
|
|
802
|
+
try {
|
|
803
|
+
fs.unlinkSync(files.pngWritePath);
|
|
804
|
+
}
|
|
805
|
+
catch { }
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch { }
|
|
809
|
+
}
|
|
810
|
+
if (success)
|
|
811
|
+
tempFiles.push(files.pngWritePath);
|
|
812
|
+
return { index, success };
|
|
813
|
+
}));
|
|
814
|
+
for (const { index, success } of batchResults) {
|
|
815
|
+
if (success)
|
|
816
|
+
screenshotSuccess.add(index);
|
|
983
817
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
818
|
+
const done = Math.min(batch + MAX_SCREENSHOT_CONCURRENT, screenshotEntries.length);
|
|
819
|
+
if (phaseLogger)
|
|
820
|
+
phaseLogger('powerpoint-create', 'assembly', `Screenshots: ${done}/${screenshotEntries.length} done`);
|
|
821
|
+
}
|
|
822
|
+
if (phaseLogger)
|
|
823
|
+
phaseLogger('powerpoint-create', 'assembly', `Filling ${totalSlides} slides...`);
|
|
824
|
+
const unfilledSlideNums = [];
|
|
825
|
+
for (const [index] of sortedEntries) {
|
|
826
|
+
const files = slideFiles.get(index);
|
|
827
|
+
const slidePlan = plan.slides[index];
|
|
828
|
+
const htmlResult = htmlResults.get(index);
|
|
829
|
+
if (!files) {
|
|
830
|
+
const posInSorted = sortedEntries.findIndex(([idx]) => idx === index);
|
|
831
|
+
const inferredSlideNum = posInSorted >= 0 ? posInSorted + 1 : -1;
|
|
832
|
+
logger.warn(`Slide index ${index} (slideNum ${inferredSlideNum}): No file info, marking for deletion`);
|
|
833
|
+
if (inferredSlideNum > 0)
|
|
834
|
+
unfilledSlideNums.push(inferredSlideNum);
|
|
991
835
|
continue;
|
|
992
836
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
837
|
+
let filled = false;
|
|
838
|
+
if (screenshotSuccess.has(index)) {
|
|
839
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(files.slideNum, files.pngWinPath);
|
|
840
|
+
totalToolCalls++;
|
|
841
|
+
if (toolCallLogger)
|
|
842
|
+
toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum: files.slideNum, imagePath: files.pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, files.slideNum, totalToolCalls);
|
|
843
|
+
filled = bgResult.success;
|
|
844
|
+
}
|
|
845
|
+
if (!filled) {
|
|
846
|
+
logger.info(`Slide ${files.slideNum}: Primary screenshot failed, trying fallback rendering...`);
|
|
847
|
+
const slide = plan.slides[index];
|
|
848
|
+
const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', files.slideNum);
|
|
849
|
+
const fbHtmlName = `hanseol_fb_${files.slideNum}_${timestamp}.html`;
|
|
850
|
+
const fbPngName = `hanseol_fb_${files.slideNum}_${timestamp}.png`;
|
|
851
|
+
const fbHtmlWrite = path.join(tempWritePath, fbHtmlName);
|
|
852
|
+
const fbPngWrite = path.join(tempWritePath, fbPngName);
|
|
853
|
+
const fbHtmlWin = `${tempWinPath}\\${fbHtmlName}`;
|
|
854
|
+
const fbPngWin = `${tempWinPath}\\${fbPngName}`;
|
|
855
|
+
try {
|
|
856
|
+
const viewportHtml = injectEdgeSizing(fallbackHtml, plan.design.background_color);
|
|
857
|
+
fs.writeFileSync(fbHtmlWrite, viewportHtml, 'utf-8');
|
|
858
|
+
const fbResult = await powerpointClient.renderHtmlToImage(fbHtmlWin, fbPngWin);
|
|
859
|
+
totalToolCalls++;
|
|
998
860
|
try {
|
|
999
|
-
fs.unlinkSync(
|
|
861
|
+
fs.unlinkSync(fbHtmlWrite);
|
|
862
|
+
}
|
|
863
|
+
catch { }
|
|
864
|
+
if (fbResult.success) {
|
|
865
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(files.slideNum, fbPngWin);
|
|
866
|
+
totalToolCalls++;
|
|
867
|
+
filled = bgResult.success;
|
|
868
|
+
try {
|
|
869
|
+
fs.unlinkSync(fbPngWrite);
|
|
870
|
+
}
|
|
871
|
+
catch { }
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (e) {
|
|
875
|
+
logger.warn(`Slide ${files.slideNum}: Fallback rendering also failed: ${e}`);
|
|
876
|
+
try {
|
|
877
|
+
fs.unlinkSync(fbHtmlWrite);
|
|
1000
878
|
}
|
|
1001
879
|
catch { }
|
|
1002
|
-
continue;
|
|
1003
880
|
}
|
|
1004
881
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const addResult = await powerpointClient.powerpointAddSlide(7);
|
|
1008
|
-
totalToolCalls++;
|
|
1009
|
-
if (!addResult.success) {
|
|
1010
|
-
logger.warn(`Slide ${slideNum}: Failed to add blank slide`);
|
|
1011
|
-
failCount++;
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
const bgResult = await powerpointClient.powerpointAddFullSlideImage(slideNum, pngWinPath);
|
|
1015
|
-
totalToolCalls++;
|
|
1016
|
-
if (toolCallLogger)
|
|
1017
|
-
toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum, imagePath: pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, slideNum, totalToolCalls);
|
|
1018
|
-
if (bgResult.success) {
|
|
1019
|
-
builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
882
|
+
if (filled) {
|
|
883
|
+
builtSlides.push(`Slide ${files.slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
1020
884
|
try {
|
|
1021
|
-
await powerpointClient.powerpointAddNote(slideNum,
|
|
885
|
+
await powerpointClient.powerpointAddNote(files.slideNum, htmlResult.html);
|
|
1022
886
|
}
|
|
1023
887
|
catch { }
|
|
1024
888
|
}
|
|
1025
889
|
else {
|
|
1026
|
-
logger.warn(`Slide ${slideNum}:
|
|
1027
|
-
|
|
890
|
+
logger.warn(`Slide ${files.slideNum}: All rendering attempts failed, marking for deletion`);
|
|
891
|
+
unfilledSlideNums.push(files.slideNum);
|
|
1028
892
|
}
|
|
1029
893
|
}
|
|
1030
|
-
if (
|
|
894
|
+
if (unfilledSlideNums.length > 0) {
|
|
895
|
+
const sortedDesc = [...unfilledSlideNums].sort((a, b) => b - a);
|
|
896
|
+
for (const slideNum of sortedDesc) {
|
|
897
|
+
try {
|
|
898
|
+
await powerpointClient.powerpointDeleteSlide(slideNum);
|
|
899
|
+
totalToolCalls++;
|
|
900
|
+
logger.info(`Deleted unfilled slide ${slideNum}`);
|
|
901
|
+
}
|
|
902
|
+
catch (e) {
|
|
903
|
+
logger.warn(`Failed to delete unfilled slide ${slideNum}: ${e}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const hasClosingInPlan = plan.slides.some(s => s.type === 'closing');
|
|
908
|
+
const hasClosingBuilt = builtSlides.some(s => s.includes('(closing)'));
|
|
909
|
+
if (hasClosingInPlan && !hasClosingBuilt && builtSlides.length > 0) {
|
|
910
|
+
logger.info('Closing slide was lost during assembly — adding it now');
|
|
1031
911
|
try {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
912
|
+
await powerpointClient.powerpointAddSlide(7);
|
|
913
|
+
totalToolCalls++;
|
|
914
|
+
const slideCountRes = await powerpointClient.powerpointGetSlideCount();
|
|
915
|
+
const newSlideNum = slideCountRes['slide_count'] || builtSlides.length + 1;
|
|
916
|
+
const closingPlan = plan.slides.find(s => s.type === 'closing');
|
|
917
|
+
const closingTagline = (closingPlan.content_direction || '').replace(/감사합니다|thank\s*you/gi, '').trim() || undefined;
|
|
918
|
+
const closingHtml = buildClosingSlideHtml(plan.design, companyName, newSlideNum, language, closingTagline);
|
|
919
|
+
const clHtmlName = `hanseol_closing_${timestamp}.html`;
|
|
920
|
+
const clPngName = `hanseol_closing_${timestamp}.png`;
|
|
921
|
+
const clHtmlWrite = path.join(tempWritePath, clHtmlName);
|
|
922
|
+
const clPngWrite = path.join(tempWritePath, clPngName);
|
|
923
|
+
const clHtmlWin = `${tempWinPath}\\${clHtmlName}`;
|
|
924
|
+
const clPngWin = `${tempWinPath}\\${clPngName}`;
|
|
925
|
+
const viewportHtml = injectEdgeSizing(closingHtml, plan.design.background_color);
|
|
926
|
+
fs.writeFileSync(clHtmlWrite, viewportHtml, 'utf-8');
|
|
927
|
+
const ssResult = await powerpointClient.renderHtmlToImage(clHtmlWin, clPngWin);
|
|
928
|
+
totalToolCalls++;
|
|
929
|
+
try {
|
|
930
|
+
fs.unlinkSync(clHtmlWrite);
|
|
931
|
+
}
|
|
932
|
+
catch { }
|
|
933
|
+
if (ssResult.success) {
|
|
934
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(newSlideNum, clPngWin);
|
|
935
|
+
totalToolCalls++;
|
|
936
|
+
if (bgResult.success) {
|
|
937
|
+
builtSlides.push(`Slide ${newSlideNum}: ${closingPlan.title} (closing)`);
|
|
938
|
+
logger.info('Closing slide added successfully');
|
|
1038
939
|
}
|
|
940
|
+
try {
|
|
941
|
+
fs.unlinkSync(clPngWrite);
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
1039
944
|
}
|
|
1040
945
|
}
|
|
1041
946
|
catch (e) {
|
|
1042
|
-
logger.warn(`Failed to
|
|
947
|
+
logger.warn(`Failed to add closing slide: ${e}`);
|
|
1043
948
|
}
|
|
1044
949
|
}
|
|
1045
950
|
if (builtSlides.length > 0) {
|
|
@@ -1173,7 +1078,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1173
1078
|
const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
|
|
1174
1079
|
if (phaseLogger)
|
|
1175
1080
|
phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
|
|
1176
|
-
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, phaseLogger, toolCallLogger);
|
|
1081
|
+
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger);
|
|
1177
1082
|
const duration = Date.now() - startTime;
|
|
1178
1083
|
const slideList = builtSlides.join('\n');
|
|
1179
1084
|
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}`;
|