hanseol-dev 5.0.3-dev.4 → 5.0.3-dev.41

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.
Files changed (29) hide show
  1. package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
  2. package/dist/agents/office/powerpoint-create-agent.js +251 -331
  3. package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
  4. package/dist/agents/office/powerpoint-create-prompts.d.ts +3 -2
  5. package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
  6. package/dist/agents/office/powerpoint-create-prompts.js +305 -247
  7. package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
  8. package/dist/agents/office/word-create-agent.js +4 -4
  9. package/dist/agents/office/word-create-agent.js.map +1 -1
  10. package/dist/agents/office/word-create-prompts.d.ts +3 -3
  11. package/dist/agents/office/word-create-prompts.d.ts.map +1 -1
  12. package/dist/agents/office/word-create-prompts.js +103 -42
  13. package/dist/agents/office/word-create-prompts.js.map +1 -1
  14. package/dist/constants.d.ts +1 -1
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +1 -1
  17. package/dist/constants.js.map +1 -1
  18. package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
  19. package/dist/tools/office/powerpoint-client.js +4 -0
  20. package/dist/tools/office/powerpoint-client.js.map +1 -1
  21. package/dist/tools/office/word-client.d.ts +15 -0
  22. package/dist/tools/office/word-client.d.ts.map +1 -1
  23. package/dist/tools/office/word-client.js +228 -5
  24. package/dist/tools/office/word-client.js.map +1 -1
  25. package/dist/tools/office/word-tools/section-builders.d.ts +2 -0
  26. package/dist/tools/office/word-tools/section-builders.d.ts.map +1 -1
  27. package/dist/tools/office/word-tools/section-builders.js +189 -34
  28. package/dist/tools/office/word-tools/section-builders.js.map +1 -1
  29. 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, buildDirectHtmlPrompt, validateSlideHtml, extractLayoutHint, buildContentFillJsonPrompt, parseContentFillJson, buildContentSlideHtml, } from './powerpoint-create-prompts.js';
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,50 +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
249
  function escapeHtml(text) {
306
250
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
307
251
  }
@@ -459,139 +403,9 @@ body::after {
459
403
  </body>
460
404
  </html>`;
461
405
  }
462
- function isOverviewSlide(title, slideIndex) {
463
- if (slideIndex !== 1)
464
- return false;
465
- return /개요|목차|overview|agenda|outline|순서|발표\s*구성|contents|목록/i.test(title);
466
- }
467
- function parseOverviewItems(contentDirection) {
468
- const items = [];
469
- const lines = contentDirection.split(/\n/).filter(l => l.trim());
470
- for (const line of lines) {
471
- const match = line.match(/^\d+[\.\)]\s*(.+?)(?:\s*[-–—:]\s*(.+))?$/);
472
- if (match) {
473
- items.push({ title: match[1].trim(), desc: match[2]?.trim() || '' });
474
- }
475
- }
476
- if (items.length === 0) {
477
- const parts = contentDirection.split(/[,;·•]/).map(s => s.trim()).filter(Boolean);
478
- for (const part of parts) {
479
- const sepMatch = part.match(/^(.+?)(?:\s*[-–—:]\s*(.+))?$/);
480
- if (sepMatch) {
481
- items.push({ title: sepMatch[1].trim(), desc: sepMatch[2]?.trim() || '' });
482
- }
483
- }
484
- }
485
- return items.slice(0, 5);
486
- }
487
- function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
488
- const badgeColors = [
489
- design.primary_color, design.accent_color, design.gradient_end,
490
- design.primary_color, design.accent_color,
491
- ];
492
- const itemCount = items.length;
493
- const topRow = itemCount <= 3 ? items : items.slice(0, Math.ceil(itemCount / 2));
494
- const bottomRow = itemCount <= 3 ? [] : items.slice(Math.ceil(itemCount / 2));
495
- function renderCard(item, idx) {
496
- const color = badgeColors[idx % badgeColors.length];
497
- return `
498
- <div class="card">
499
- <div class="badge" style="background:${color}">${idx + 1}</div>
500
- <div class="card-title">${escapeHtml(item.title)}</div>
501
- ${item.desc ? `<div class="card-desc">${escapeHtml(item.desc)}</div>` : ''}
502
- </div>`;
503
- }
504
- return `<!DOCTYPE html>
505
- <html lang="ko">
506
- <head>
507
- <meta charset="UTF-8">
508
- <style>
509
- * { margin: 0; padding: 0; box-sizing: border-box; }
510
- html, body { width: 1920px; height: 1080px; overflow: hidden; }
511
- body {
512
- background: ${design.background_color};
513
- font-family: "${design.font_body}", "${design.font_title}", "Segoe UI", "Malgun Gothic", sans-serif;
514
- display: flex; flex-direction: column;
515
- word-break: keep-all; overflow-wrap: break-word;
516
- }
517
- .header {
518
- background: linear-gradient(135deg, ${design.primary_color}, ${design.gradient_end});
519
- padding: 48px 80px 40px;
520
- flex-shrink: 0;
521
- }
522
- .header-title {
523
- font-size: 52px; font-weight: 800; color: #ffffff;
524
- font-family: "${design.font_title}", "Segoe UI", sans-serif;
525
- margin-bottom: 8px;
526
- }
527
- .header-subtitle {
528
- font-size: 24px; font-weight: 400; color: rgba(255,255,255,0.75);
529
- }
530
- .content {
531
- flex: 1; display: flex; flex-direction: column;
532
- padding: 48px 80px 40px;
533
- gap: 24px;
534
- justify-content: center;
535
- }
536
- .row {
537
- display: flex; gap: 24px;
538
- justify-content: center;
539
- }
540
- .card {
541
- flex: 1;
542
- max-width: 340px;
543
- background: #ffffff;
544
- border-radius: 16px;
545
- padding: 36px 32px;
546
- box-shadow: 0 4px 20px rgba(0,0,0,0.06);
547
- display: flex; flex-direction: column;
548
- align-items: center; text-align: center;
549
- gap: 12px;
550
- }
551
- .badge {
552
- width: 44px; height: 44px;
553
- border-radius: 50%;
554
- color: #ffffff;
555
- font-size: 20px; font-weight: 700;
556
- display: flex; align-items: center; justify-content: center;
557
- flex-shrink: 0;
558
- }
559
- .card-title {
560
- font-size: 26px; font-weight: 700; color: ${design.text_color};
561
- line-height: 1.3;
562
- }
563
- .card-desc {
564
- font-size: 22px; font-weight: 400; color: ${design.text_color}aa;
565
- line-height: 1.5;
566
- }
567
- .page-num {
568
- position: absolute;
569
- bottom: 24px; right: 44px;
570
- font-size: 13px;
571
- color: ${design.text_color}55;
572
- }
573
- </style>
574
- </head>
575
- <body>
576
- <div class="header">
577
- <div class="header-title">${escapeHtml(title)}</div>
578
- ${subtitle ? `<div class="header-subtitle">${escapeHtml(subtitle)}</div>` : ''}
579
- </div>
580
- <div class="content">
581
- <div class="row">
582
- ${topRow.map((item, idx) => renderCard(item, idx)).join('')}
583
- </div>
584
- ${bottomRow.length > 0 ? `<div class="row">
585
- ${bottomRow.map((item, idx) => renderCard(item, topRow.length + idx)).join('')}
586
- </div>` : ''}
587
- </div>
588
- <div class="page-num">${slideNum}</div>
589
- </body>
590
- </html>`;
591
- }
592
406
  function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
593
407
  const items = [];
594
- const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d+\.\s*/);
408
+ const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d+[.)]\s*|Step\s*\d+[.:]\s*/i);
595
409
  for (const part of parts) {
596
410
  const cleaned = part.replace(/Layout:.*$/i, '').trim();
597
411
  if (cleaned.length > 5) {
@@ -599,8 +413,28 @@ function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
599
413
  items.push(sentence.slice(0, 80));
600
414
  }
601
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
+ }
602
434
  const bulletItems = items.slice(0, 6);
603
- const bulletsHtml = bulletItems.map(item => `<div class="bullet"><span class="dot"></span><span>${escapeHtml(item)}</span></div>`).join('\n ');
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 ');
604
438
  return `<!DOCTYPE html>
605
439
  <html lang="ko">
606
440
  <head>
@@ -619,9 +453,10 @@ body {
619
453
  .slide-num { font-size: 14px; color: ${design.accent_color}; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
620
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; }
621
455
  .accent-bar { width: 80px; height: 4px; background: ${design.accent_color}; margin-top: 20px; border-radius: 2px; }
622
- .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 20px; }
623
- .bullet { display: flex; align-items: flex-start; gap: 16px; font-size: 26px; color: ${design.text_color}; line-height: 1.5; }
624
- .dot { width: 10px; height: 10px; border-radius: 50%; background: ${design.accent_color}; flex-shrink: 0; margin-top: 10px; }
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; }
625
460
  </style>
626
461
  </head>
627
462
  <body>
@@ -631,7 +466,7 @@ h1 { font-size: 52px; font-weight: 700; color: ${design.text_color}; font-family
631
466
  <div class="accent-bar"></div>
632
467
  </div>
633
468
  <div class="content">
634
- ${bulletsHtml}
469
+ ${pointsHtml}
635
470
  </div>
636
471
  </body>
637
472
  </html>`;
@@ -730,67 +565,60 @@ async function runDesignPhase(llmClient, instruction, phaseLogger) {
730
565
  }
731
566
  return plan;
732
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
+ }
733
584
  async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, totalSlides, language) {
734
- const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
735
- const layoutType = extractLayoutHint(slide.content_direction || '');
736
- const directPrompt = buildDirectHtmlPrompt(slide.title, cleanedDirection, design, slideIndex, totalSlides, language, layoutType);
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);
737
588
  try {
738
589
  const res = await llmClient.chatCompletion({
739
590
  messages: [
740
- { role: 'system', content: directPrompt },
741
- { role: 'user', content: 'Generate the HTML slide now.' },
591
+ { role: 'system', content: htmlPrompt },
592
+ { role: 'user', content: 'Output the complete HTML now.' },
742
593
  ],
743
- temperature: 0.4,
744
- max_tokens: 6000,
594
+ temperature: 0.6,
595
+ max_tokens: 4000,
745
596
  });
746
597
  const msg = res.choices[0]?.message;
747
598
  const rawHtml = msg ? extractContent(msg) : '';
748
- const html = extractHtml(rawHtml);
749
- if (html) {
750
- const validation = validateSlideHtml(html, layoutType);
751
- if (validation.pass) {
752
- return { html, isCodeTemplate: false };
753
- }
754
- 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 };
755
602
  }
756
- }
757
- catch (e) {
758
- logger.warn(`Slide ${slideIndex + 1}: Direct HTML LLM call failed: ${e}`);
759
- }
760
- logger.info(`Slide ${slideIndex + 1}: Falling back to code template "${layoutType}"`);
761
- const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
762
- try {
763
- 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({
764
605
  messages: [
765
- { role: 'system', content: jsonPrompt },
766
- { role: 'user', content: 'Output the JSON now.' },
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.' },
767
608
  ],
768
- temperature: 0.3,
769
- max_tokens: 2000,
609
+ temperature: 0.4,
610
+ max_tokens: 4000,
770
611
  });
771
- const jsonMsg = jsonRes.choices[0]?.message;
772
- const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
773
- let slideData = parseContentFillJson(jsonRaw, layoutType);
774
- if (!slideData) {
775
- const retryRes = await llmClient.chatCompletion({
776
- messages: [
777
- { role: 'system', content: jsonPrompt },
778
- { role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
779
- ],
780
- temperature: 0.2,
781
- max_tokens: 2000,
782
- });
783
- const retryMsg = retryRes.choices[0]?.message;
784
- const retryRaw = retryMsg ? extractContent(retryMsg) : '';
785
- slideData = parseContentFillJson(retryRaw, layoutType);
786
- }
787
- if (slideData) {
788
- const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
789
- 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 };
790
617
  }
618
+ logger.warn(`Slide ${slideIndex + 1}: Both HTML attempts failed.`);
791
619
  }
792
620
  catch (e) {
793
- logger.warn(`Slide ${slideIndex + 1}: Code template fallback failed: ${e}`);
621
+ logger.warn(`Slide ${slideIndex + 1}: HTML generation error: ${e}`);
794
622
  }
795
623
  return null;
796
624
  }
@@ -813,18 +641,6 @@ async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstD
813
641
  isCodeTemplate: true,
814
642
  });
815
643
  }
816
- else if (isOverviewSlide(slide.title, i)) {
817
- const overviewItems = parseOverviewItems(slide.content_direction || '');
818
- if (overviewItems.length >= 2) {
819
- const firstLine = (slide.content_direction || '').split('\n')[0] || '';
820
- const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
821
- results.set(i, {
822
- index: i,
823
- html: buildOverviewSlideHtml(plan.design, slide.title, overviewSubtitle, overviewItems, i + 1),
824
- isCodeTemplate: true,
825
- });
826
- }
827
- }
828
644
  }
829
645
  const contentIndices = plan.slides
830
646
  .map((s, i) => ({ slide: s, index: i }))
@@ -863,8 +679,7 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
863
679
  const slide = plan.slides[index];
864
680
  if (slide.type === 'title' || slide.type === 'closing')
865
681
  continue;
866
- const layoutType = extractLayoutHint(slide.content_direction || '');
867
- const validation = validateSlideHtml(result.html, layoutType);
682
+ const validation = validateSlideHtml(result.html);
868
683
  if (!validation.pass) {
869
684
  logger.info(`Slide ${index + 1}: Post-validation failed: ${validation.feedback}`);
870
685
  failedIndices.push(index);
@@ -906,23 +721,28 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
906
721
  }
907
722
  return htmlResults;
908
723
  }
909
- async function assemblePresentation(htmlResults, plan, timestamp, savePath, phaseLogger, toolCallLogger) {
724
+ const MAX_SCREENSHOT_CONCURRENT = 4;
725
+ async function assemblePresentation(htmlResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger) {
910
726
  const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
911
727
  ensureTempDir(tempWritePath);
728
+ const totalSlides = htmlResults.size;
729
+ const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
912
730
  const builtSlides = [];
913
- let failCount = 0;
914
731
  let totalToolCalls = 0;
915
732
  const tempFiles = [];
916
- const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
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;
917
744
  for (const [index, result] of sortedEntries) {
918
- const slidePlan = plan.slides[index];
919
- const slideNum = builtSlides.length + 1;
920
- if (failCount >= 3) {
921
- logger.warn('Too many slide failures, stopping');
922
- break;
923
- }
924
- if (phaseLogger)
925
- phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
745
+ slideNum++;
926
746
  const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
927
747
  const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
928
748
  const htmlWritePath = path.join(tempWritePath, htmlFileName);
@@ -930,101 +750,201 @@ async function assemblePresentation(htmlResults, plan, timestamp, savePath, phas
930
750
  const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
931
751
  const pngWinPath = `${tempWinPath}\\${pngFileName}`;
932
752
  try {
933
- const viewportHtml = result.isCodeTemplate
934
- ? injectEdgeSizing(result.html, plan.design.background_color)
935
- : injectViewportCss(result.html, plan.design.background_color);
936
- const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
937
- fs.writeFileSync(htmlWritePath, processed, 'utf-8');
753
+ const viewportHtml = injectEdgeSizing(result.html, plan.design.background_color);
754
+ fs.writeFileSync(htmlWritePath, viewportHtml, 'utf-8');
938
755
  tempFiles.push(htmlWritePath);
756
+ slideFiles.set(index, { slideNum, htmlWritePath, pngWritePath, htmlWinPath, pngWinPath });
939
757
  }
940
758
  catch (e) {
941
- logger.warn(`Slide ${slideNum}: Failed to write HTML file: ${e}`);
942
- failCount++;
943
- continue;
944
- }
945
- let renderSuccess = false;
946
- try {
947
- const renderResult = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
948
- totalToolCalls++;
949
- renderSuccess = renderResult.success;
950
- if (!renderSuccess) {
951
- await new Promise(r => setTimeout(r, 2000));
952
- const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
953
- totalToolCalls++;
954
- renderSuccess = retryRender.success;
955
- }
759
+ logger.warn(`Slide ${slideNum}: Failed to write HTML: ${e}`);
956
760
  }
957
- catch (e) {
958
- 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;
959
770
  try {
960
- await new Promise(r => setTimeout(r, 2000));
961
- const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
771
+ const result = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
962
772
  totalToolCalls++;
963
- renderSuccess = retryRender.success;
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
+ }
964
780
  }
965
781
  catch {
966
- renderSuccess = false;
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
+ }
967
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);
968
817
  }
969
- try {
970
- fs.unlinkSync(htmlWritePath);
971
- }
972
- catch { }
973
- if (!renderSuccess) {
974
- logger.warn(`Slide ${slideNum}: Screenshot rendering failed, skipping`);
975
- failCount++;
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);
976
835
  continue;
977
836
  }
978
- try {
979
- const pngStats = fs.statSync(pngWritePath);
980
- if (pngStats.size < 15000) {
981
- logger.warn(`Slide ${slideNum}: Screenshot too small (${pngStats.size} bytes), likely blank — skipping`);
982
- failCount++;
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++;
983
860
  try {
984
- fs.unlinkSync(pngWritePath);
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);
985
878
  }
986
879
  catch { }
987
- continue;
988
880
  }
989
881
  }
990
- catch { }
991
- tempFiles.push(pngWritePath);
992
- const addResult = await powerpointClient.powerpointAddSlide(7);
993
- totalToolCalls++;
994
- if (!addResult.success) {
995
- logger.warn(`Slide ${slideNum}: Failed to add blank slide`);
996
- failCount++;
997
- continue;
998
- }
999
- const bgResult = await powerpointClient.powerpointAddFullSlideImage(slideNum, pngWinPath);
1000
- totalToolCalls++;
1001
- if (toolCallLogger)
1002
- toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum, imagePath: pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, slideNum, totalToolCalls);
1003
- if (bgResult.success) {
1004
- builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
882
+ if (filled) {
883
+ builtSlides.push(`Slide ${files.slideNum}: ${slidePlan.title} (${slidePlan.type})`);
1005
884
  try {
1006
- await powerpointClient.powerpointAddNote(slideNum, result.html);
885
+ await powerpointClient.powerpointAddNote(files.slideNum, htmlResult.html);
1007
886
  }
1008
887
  catch { }
1009
888
  }
1010
889
  else {
1011
- logger.warn(`Slide ${slideNum}: Failed to set background: ${JSON.stringify(bgResult)}`);
1012
- failCount++;
890
+ logger.warn(`Slide ${files.slideNum}: All rendering attempts failed, marking for deletion`);
891
+ unfilledSlideNums.push(files.slideNum);
1013
892
  }
1014
893
  }
1015
- if (builtSlides.length > 0) {
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');
1016
911
  try {
1017
- const slideCountResult = await powerpointClient.powerpointGetSlideCount();
1018
- const totalSlidesInPpt = slideCountResult['slide_count'] || 0;
1019
- if (totalSlidesInPpt > builtSlides.length) {
1020
- for (let d = totalSlidesInPpt; d > builtSlides.length; d--) {
1021
- await powerpointClient.powerpointDeleteSlide(d);
1022
- totalToolCalls++;
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');
1023
939
  }
940
+ try {
941
+ fs.unlinkSync(clPngWrite);
942
+ }
943
+ catch { }
1024
944
  }
1025
945
  }
1026
946
  catch (e) {
1027
- logger.warn(`Failed to clean up trailing slides: ${e}`);
947
+ logger.warn(`Failed to add closing slide: ${e}`);
1028
948
  }
1029
949
  }
1030
950
  if (builtSlides.length > 0) {
@@ -1158,7 +1078,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
1158
1078
  const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
1159
1079
  if (phaseLogger)
1160
1080
  phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
1161
- 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);
1162
1082
  const duration = Date.now() - startTime;
1163
1083
  const slideList = builtSlides.join('\n');
1164
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}`;