hanseol-dev 5.0.3-dev.3 → 5.0.3-dev.31

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 (27) hide show
  1. package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
  2. package/dist/agents/office/powerpoint-create-agent.js +273 -299
  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 +300 -244
  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-prompts.d.ts +3 -3
  10. package/dist/agents/office/word-create-prompts.d.ts.map +1 -1
  11. package/dist/agents/office/word-create-prompts.js +29 -18
  12. package/dist/agents/office/word-create-prompts.js.map +1 -1
  13. package/dist/constants.d.ts +1 -1
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/constants.js +1 -1
  16. package/dist/constants.js.map +1 -1
  17. package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
  18. package/dist/tools/office/powerpoint-client.js +4 -0
  19. package/dist/tools/office/powerpoint-client.js.map +1 -1
  20. package/dist/tools/office/word-client.d.ts +2 -0
  21. package/dist/tools/office/word-client.d.ts.map +1 -1
  22. package/dist/tools/office/word-client.js +67 -5
  23. package/dist/tools/office/word-client.js.map +1 -1
  24. package/dist/tools/office/word-tools/section-builders.d.ts.map +1 -1
  25. package/dist/tools/office/word-tools/section-builders.js +32 -26
  26. package/dist/tools/office/word-tools/section-builders.js.map +1 -1
  27. 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,48 +403,38 @@ 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) {
406
+ function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
468
407
  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() || '' });
408
+ const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d+[.)]\s*|Step\s*\d+[.:]\s*/i);
409
+ for (const part of parts) {
410
+ const cleaned = part.replace(/Layout:.*$/i, '').trim();
411
+ if (cleaned.length > 5) {
412
+ const sentence = cleaned.split(/[.。!]\s/)[0] || cleaned;
413
+ items.push(sentence.slice(0, 80));
474
414
  }
475
415
  }
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() || '' });
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));
482
422
  }
483
423
  }
484
424
  }
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>`;
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
+ }
503
433
  }
434
+ const bulletItems = items.slice(0, 6);
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 ');
504
438
  return `<!DOCTYPE html>
505
439
  <html lang="ko">
506
440
  <head>
@@ -510,82 +444,30 @@ function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
510
444
  html, body { width: 1920px; height: 1080px; overflow: hidden; }
511
445
  body {
512
446
  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;
447
+ font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
448
+ padding: 80px 100px;
449
+ display: flex;
450
+ flex-direction: column;
572
451
  }
452
+ .header { margin-bottom: 48px; }
453
+ .slide-num { font-size: 14px; color: ${design.accent_color}; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
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; }
455
+ .accent-bar { width: 80px; height: 4px; background: ${design.accent_color}; margin-top: 20px; border-radius: 2px; }
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; }
573
460
  </style>
574
461
  </head>
575
462
  <body>
576
463
  <div class="header">
577
- <div class="header-title">${escapeHtml(title)}</div>
578
- ${subtitle ? `<div class="header-subtitle">${escapeHtml(subtitle)}</div>` : ''}
464
+ <div class="slide-num">SLIDE ${slideNum}</div>
465
+ <h1>${escapeHtml(title)}</h1>
466
+ <div class="accent-bar"></div>
579
467
  </div>
580
468
  <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>` : ''}
469
+ ${pointsHtml}
587
470
  </div>
588
- <div class="page-num">${slideNum}</div>
589
471
  </body>
590
472
  </html>`;
591
473
  }
@@ -683,67 +565,60 @@ async function runDesignPhase(llmClient, instruction, phaseLogger) {
683
565
  }
684
566
  return plan;
685
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
+ }
686
584
  async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, totalSlides, language) {
687
- const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
688
- const layoutType = extractLayoutHint(slide.content_direction || '');
689
- const directPrompt = buildDirectHtmlPrompt(slide.title, cleanedDirection, design, slideIndex, totalSlides, language, layoutType);
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);
690
588
  try {
691
589
  const res = await llmClient.chatCompletion({
692
590
  messages: [
693
- { role: 'system', content: directPrompt },
694
- { role: 'user', content: 'Generate the HTML slide now.' },
591
+ { role: 'system', content: htmlPrompt },
592
+ { role: 'user', content: 'Output the complete HTML now.' },
695
593
  ],
696
- temperature: 0.4,
697
- max_tokens: 6000,
594
+ temperature: 0.6,
595
+ max_tokens: 4000,
698
596
  });
699
597
  const msg = res.choices[0]?.message;
700
598
  const rawHtml = msg ? extractContent(msg) : '';
701
- const html = extractHtml(rawHtml);
702
- if (html) {
703
- const validation = validateSlideHtml(html, layoutType);
704
- if (validation.pass) {
705
- return { html, isCodeTemplate: false };
706
- }
707
- logger.info(`Slide ${slideIndex + 1}: Direct HTML failed validation: ${validation.feedback}`);
599
+ let html = extractHtmlFromResponse(rawHtml);
600
+ if (html && !hasPlaceholderText(html)) {
601
+ return { html, isCodeTemplate: false };
708
602
  }
709
- }
710
- catch (e) {
711
- logger.warn(`Slide ${slideIndex + 1}: Direct HTML LLM call failed: ${e}`);
712
- }
713
- logger.info(`Slide ${slideIndex + 1}: Falling back to code template "${layoutType}"`);
714
- const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
715
- try {
716
- const jsonRes = await llmClient.chatCompletion({
603
+ logger.warn(`Slide ${slideIndex + 1}: First HTML attempt failed. Raw length: ${rawHtml.length}. Retrying...`);
604
+ const retryRes = await llmClient.chatCompletion({
717
605
  messages: [
718
- { role: 'system', content: jsonPrompt },
719
- { 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.' },
720
608
  ],
721
- temperature: 0.3,
722
- max_tokens: 2000,
609
+ temperature: 0.4,
610
+ max_tokens: 4000,
723
611
  });
724
- const jsonMsg = jsonRes.choices[0]?.message;
725
- const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
726
- let slideData = parseContentFillJson(jsonRaw, layoutType);
727
- if (!slideData) {
728
- const retryRes = await llmClient.chatCompletion({
729
- messages: [
730
- { role: 'system', content: jsonPrompt },
731
- { role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
732
- ],
733
- temperature: 0.2,
734
- max_tokens: 2000,
735
- });
736
- const retryMsg = retryRes.choices[0]?.message;
737
- const retryRaw = retryMsg ? extractContent(retryMsg) : '';
738
- slideData = parseContentFillJson(retryRaw, layoutType);
739
- }
740
- if (slideData) {
741
- const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
742
- return { html, isCodeTemplate: true };
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 };
743
617
  }
618
+ logger.warn(`Slide ${slideIndex + 1}: Both HTML attempts failed.`);
744
619
  }
745
620
  catch (e) {
746
- logger.warn(`Slide ${slideIndex + 1}: Code template fallback failed: ${e}`);
621
+ logger.warn(`Slide ${slideIndex + 1}: HTML generation error: ${e}`);
747
622
  }
748
623
  return null;
749
624
  }
@@ -766,18 +641,6 @@ async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstD
766
641
  isCodeTemplate: true,
767
642
  });
768
643
  }
769
- else if (isOverviewSlide(slide.title, i)) {
770
- const overviewItems = parseOverviewItems(slide.content_direction || '');
771
- if (overviewItems.length >= 2) {
772
- const firstLine = (slide.content_direction || '').split('\n')[0] || '';
773
- const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
774
- results.set(i, {
775
- index: i,
776
- html: buildOverviewSlideHtml(plan.design, slide.title, overviewSubtitle, overviewItems, i + 1),
777
- isCodeTemplate: true,
778
- });
779
- }
780
- }
781
644
  }
782
645
  const contentIndices = plan.slides
783
646
  .map((s, i) => ({ slide: s, index: i }))
@@ -797,6 +660,12 @@ async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstD
797
660
  if (result) {
798
661
  results.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
799
662
  }
663
+ else {
664
+ const slide = plan.slides[index];
665
+ const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', index + 1);
666
+ results.set(index, { index, html: fallbackHtml, isCodeTemplate: true });
667
+ logger.warn(`Slide ${index + 1}: Using fallback HTML for "${slide.title}"`);
668
+ }
800
669
  }
801
670
  const done = Math.min(batch + MAX_CONCURRENT, contentIndices.length);
802
671
  if (phaseLogger)
@@ -810,8 +679,7 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
810
679
  const slide = plan.slides[index];
811
680
  if (slide.type === 'title' || slide.type === 'closing')
812
681
  continue;
813
- const layoutType = extractLayoutHint(slide.content_direction || '');
814
- const validation = validateSlideHtml(result.html, layoutType);
682
+ const validation = validateSlideHtml(result.html);
815
683
  if (!validation.pass) {
816
684
  logger.info(`Slide ${index + 1}: Post-validation failed: ${validation.feedback}`);
817
685
  failedIndices.push(index);
@@ -853,23 +721,28 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
853
721
  }
854
722
  return htmlResults;
855
723
  }
856
- 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) {
857
726
  const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
858
727
  ensureTempDir(tempWritePath);
728
+ const totalSlides = htmlResults.size;
729
+ const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
859
730
  const builtSlides = [];
860
- let failCount = 0;
861
731
  let totalToolCalls = 0;
862
732
  const tempFiles = [];
863
- 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;
864
744
  for (const [index, result] of sortedEntries) {
865
- const slidePlan = plan.slides[index];
866
- const slideNum = builtSlides.length + 1;
867
- if (failCount >= 3) {
868
- logger.warn('Too many slide failures, stopping');
869
- break;
870
- }
871
- if (phaseLogger)
872
- phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
745
+ slideNum++;
873
746
  const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
874
747
  const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
875
748
  const htmlWritePath = path.join(tempWritePath, htmlFileName);
@@ -877,101 +750,201 @@ async function assemblePresentation(htmlResults, plan, timestamp, savePath, phas
877
750
  const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
878
751
  const pngWinPath = `${tempWinPath}\\${pngFileName}`;
879
752
  try {
880
- const viewportHtml = result.isCodeTemplate
881
- ? injectEdgeSizing(result.html, plan.design.background_color)
882
- : injectViewportCss(result.html, plan.design.background_color);
883
- const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
884
- fs.writeFileSync(htmlWritePath, processed, 'utf-8');
753
+ const viewportHtml = injectEdgeSizing(result.html, plan.design.background_color);
754
+ fs.writeFileSync(htmlWritePath, viewportHtml, 'utf-8');
885
755
  tempFiles.push(htmlWritePath);
756
+ slideFiles.set(index, { slideNum, htmlWritePath, pngWritePath, htmlWinPath, pngWinPath });
886
757
  }
887
758
  catch (e) {
888
- logger.warn(`Slide ${slideNum}: Failed to write HTML file: ${e}`);
889
- failCount++;
890
- continue;
759
+ logger.warn(`Slide ${slideNum}: Failed to write HTML: ${e}`);
891
760
  }
892
- let renderSuccess = false;
893
- try {
894
- const renderResult = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
895
- totalToolCalls++;
896
- renderSuccess = renderResult.success;
897
- if (!renderSuccess) {
898
- await new Promise(r => setTimeout(r, 2000));
899
- const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
900
- totalToolCalls++;
901
- renderSuccess = retryRender.success;
902
- }
903
- }
904
- catch (e) {
905
- 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;
906
770
  try {
907
- await new Promise(r => setTimeout(r, 2000));
908
- const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
771
+ const result = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
909
772
  totalToolCalls++;
910
- 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
+ }
911
780
  }
912
781
  catch {
913
- 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
+ }
914
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);
915
817
  }
916
- try {
917
- fs.unlinkSync(htmlWritePath);
918
- }
919
- catch { }
920
- if (!renderSuccess) {
921
- logger.warn(`Slide ${slideNum}: Screenshot rendering failed, skipping`);
922
- 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);
923
835
  continue;
924
836
  }
925
- try {
926
- const pngStats = fs.statSync(pngWritePath);
927
- if (pngStats.size < 15000) {
928
- logger.warn(`Slide ${slideNum}: Screenshot too small (${pngStats.size} bytes), likely blank — skipping`);
929
- 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++;
930
860
  try {
931
- 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);
932
878
  }
933
879
  catch { }
934
- continue;
935
880
  }
936
881
  }
937
- catch { }
938
- tempFiles.push(pngWritePath);
939
- const addResult = await powerpointClient.powerpointAddSlide(7);
940
- totalToolCalls++;
941
- if (!addResult.success) {
942
- logger.warn(`Slide ${slideNum}: Failed to add blank slide`);
943
- failCount++;
944
- continue;
945
- }
946
- const bgResult = await powerpointClient.powerpointAddFullSlideImage(slideNum, pngWinPath);
947
- totalToolCalls++;
948
- if (toolCallLogger)
949
- toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum, imagePath: pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, slideNum, totalToolCalls);
950
- if (bgResult.success) {
951
- builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
882
+ if (filled) {
883
+ builtSlides.push(`Slide ${files.slideNum}: ${slidePlan.title} (${slidePlan.type})`);
952
884
  try {
953
- await powerpointClient.powerpointAddNote(slideNum, result.html);
885
+ await powerpointClient.powerpointAddNote(files.slideNum, htmlResult.html);
954
886
  }
955
887
  catch { }
956
888
  }
957
889
  else {
958
- logger.warn(`Slide ${slideNum}: Failed to set background: ${JSON.stringify(bgResult)}`);
959
- failCount++;
890
+ logger.warn(`Slide ${files.slideNum}: All rendering attempts failed, marking for deletion`);
891
+ unfilledSlideNums.push(files.slideNum);
960
892
  }
961
893
  }
962
- 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');
963
911
  try {
964
- const slideCountResult = await powerpointClient.powerpointGetSlideCount();
965
- const totalSlidesInPpt = slideCountResult['slide_count'] || 0;
966
- if (totalSlidesInPpt > builtSlides.length) {
967
- for (let d = totalSlidesInPpt; d > builtSlides.length; d--) {
968
- await powerpointClient.powerpointDeleteSlide(d);
969
- 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');
939
+ }
940
+ try {
941
+ fs.unlinkSync(clPngWrite);
970
942
  }
943
+ catch { }
971
944
  }
972
945
  }
973
946
  catch (e) {
974
- logger.warn(`Failed to clean up trailing slides: ${e}`);
947
+ logger.warn(`Failed to add closing slide: ${e}`);
975
948
  }
976
949
  }
977
950
  if (builtSlides.length > 0) {
@@ -1084,7 +1057,8 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
1084
1057
  if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
1085
1058
  titleSubtitle = '';
1086
1059
  }
1087
- const companyMatch = instruction.match(/회사명\s*[::]\s*([^\s,,、]+)/);
1060
+ const cleanInstruction = instruction.replace(/\*\*/g, '');
1061
+ const companyMatch = cleanInstruction.match(/회사명\s*[::]?\s*([^\s,,、]+)/);
1088
1062
  if (companyMatch && companyMatch[1]) {
1089
1063
  const companyName_ = companyMatch[1];
1090
1064
  if (titleSlidePlan && titleSlidePlan.title.trim() !== companyName_) {
@@ -1104,7 +1078,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
1104
1078
  const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
1105
1079
  if (phaseLogger)
1106
1080
  phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
1107
- 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);
1108
1082
  const duration = Date.now() - startTime;
1109
1083
  const slideList = builtSlides.join('\n');
1110
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}`;