hanseol-dev 5.0.2-dev.151 → 5.0.2-dev.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.js +38 -398
- package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.d.ts +87 -0
- package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.js +450 -0
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"powerpoint-create-agent.d.ts","sourceRoot":"","sources":["../../../src/agents/office/powerpoint-create-agent.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"powerpoint-create-agent.d.ts","sourceRoot":"","sources":["../../../src/agents/office/powerpoint-create-agent.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;AA6vChE,wBAAgB,iCAAiC,IAAI,YAAY,CAkChE"}
|
|
@@ -4,22 +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_CREATE_ENHANCEMENT_PROMPT, PPT_STRUCTURED_PLANNING_PROMPT, buildSlideHtmlPrompt, extractLayoutHint,
|
|
8
|
-
function getUnderfillFixByLayout(layoutType) {
|
|
9
|
-
const fixes = {
|
|
10
|
-
cards: 'ADD MORE CONTENT to cards:\n- Each card needs 4-5 bullet points (not 2-3)\n- Add a 2-sentence descriptive paragraph under each heading\n- Add a metric/stat at the bottom of each card\n- Use generous padding: 40px inside cards, 20px between items',
|
|
11
|
-
progress_bars: 'ADD MORE BARS and detail:\n- Increase to 5-6 bars (not 3-4)\n- Add a 1-line detail text below each bar explaining context\n- Add an insight summary box at the very bottom\n- Use larger bar height (48px) and label font (28px)',
|
|
12
|
-
bar_chart: 'ADD MORE to the chart:\n- Add more bars (5-6 total)\n- Make bars taller (tallest 85%)\n- Add a 2-3 line insight section at the bottom\n- Increase font sizes: values 32px, labels 28px',
|
|
13
|
-
donut_chart: 'ADD MORE to donut and legend:\n- Add 4-5 segments with detailed labels\n- Each legend item: name + absolute value + percentage\n- Add a 2-sentence summary paragraph below the legend\n- Make donut larger (450px)',
|
|
14
|
-
table: 'ADD MORE ROWS and content:\n- Increase to 6-8 data rows\n- Fill ALL cells with real data\n- Add a summary bar below the table with key insight\n- Use larger padding on td (28-32px)',
|
|
15
|
-
timeline: 'ADD MORE DETAIL to milestones:\n- Each milestone: 3-4 sentence description (not 1 line)\n- Add a KPI metric at the bottom of each milestone card\n- Use 3-4 milestone cards (not 2)\n- Add date labels and context',
|
|
16
|
-
process_flow: 'ADD MORE DETAIL to steps:\n- Each step: 2-3 sentence description (not 1 line)\n- Add time/metric at the bottom of each step\n- Use step numbers + titles\n- Increase padding for readability',
|
|
17
|
-
big_numbers: 'ADD MORE CONTEXT:\n- Each metric: number + unit + label + 2-3 sentence context + trend indicator\n- Ensure 3 metric cards minimum\n- Add supporting text below each number\n- Make numbers larger (80px)',
|
|
18
|
-
two_col_split: 'ADD MORE CONTENT:\n- Left column: primary visual with supporting context\n- Right column: 4-5 detailed bullet points\n- Both columns must be content-rich\n- Use generous font sizes (28-30px)',
|
|
19
|
-
hero_stat: 'ADD MORE CONTEXT:\n- Central number must be 128px\n- Add 2-3 sentence context paragraph below\n- Add row of 2-3 supporting metrics\n- Include trend indicators and labels',
|
|
20
|
-
};
|
|
21
|
-
return fixes[layoutType] || fixes['cards'];
|
|
22
|
-
}
|
|
7
|
+
import { PPT_CREATE_ENHANCEMENT_PROMPT, PPT_STRUCTURED_PLANNING_PROMPT, buildSlideHtmlPrompt, extractLayoutHint, buildContentFillJsonPrompt, parseContentFillJson, buildContentSlideHtml, } from './powerpoint-create-prompts.js';
|
|
23
8
|
const DEFAULT_DESIGN = {
|
|
24
9
|
primary_color: '#1B2A4A',
|
|
25
10
|
accent_color: '#00D4AA',
|
|
@@ -266,64 +251,6 @@ function injectViewportCss(html, backgroundColor) {
|
|
|
266
251
|
}
|
|
267
252
|
return result;
|
|
268
253
|
}
|
|
269
|
-
function enforceMinFontSize(html) {
|
|
270
|
-
const MIN_PX = 26;
|
|
271
|
-
let result = html.replace(/font-size:\s*(\d+(?:\.\d+)?)px/g, (_match, size) => {
|
|
272
|
-
const px = parseFloat(size);
|
|
273
|
-
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
274
|
-
});
|
|
275
|
-
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)pt/g, (_match, size) => {
|
|
276
|
-
const pt = parseFloat(size);
|
|
277
|
-
const px = pt * 1.333;
|
|
278
|
-
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
279
|
-
});
|
|
280
|
-
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)r?em/g, (_match, size) => {
|
|
281
|
-
const em = parseFloat(size);
|
|
282
|
-
const px = em * 16;
|
|
283
|
-
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
284
|
-
});
|
|
285
|
-
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)%/g, (_match, size) => {
|
|
286
|
-
const pct = parseFloat(size);
|
|
287
|
-
const px = pct / 100 * 16;
|
|
288
|
-
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
289
|
-
});
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
|
-
function ensureSafeBodyPadding(html) {
|
|
293
|
-
const safeStyle = `<style>body{padding-top:20px!important;padding-bottom:40px!important;min-height:auto!important}body>div:last-child,body>section:last-child{margin-bottom:0!important;padding-bottom:0!important}</style>`;
|
|
294
|
-
if (html.includes('</head>')) {
|
|
295
|
-
return html.replace('</head>', `${safeStyle}</head>`);
|
|
296
|
-
}
|
|
297
|
-
return html;
|
|
298
|
-
}
|
|
299
|
-
function stripGradientTextEffects(html) {
|
|
300
|
-
let result = html.replace(/-webkit-text-fill-color:\s*transparent/gi, '-webkit-text-fill-color:initial');
|
|
301
|
-
result = result.replace(/background-clip:\s*text/gi, 'background-clip:initial');
|
|
302
|
-
result = result.replace(/-webkit-background-clip:\s*text/gi, '-webkit-background-clip:initial');
|
|
303
|
-
return result;
|
|
304
|
-
}
|
|
305
|
-
function removeAbsolutePositioning(html) {
|
|
306
|
-
let result = html.replace(/style="([^"]*)position:\s*absolute([^"]*)"/gi, (match, before, after) => {
|
|
307
|
-
if (/(?:width|height):\s*[1-5]?\dpx/i.test(before + after) || /opacity:\s*0\.[0-4]/i.test(before + after)) {
|
|
308
|
-
return match;
|
|
309
|
-
}
|
|
310
|
-
let cleaned = `${before}position:static${after}`;
|
|
311
|
-
cleaned = cleaned.replace(/(?:^|;)\s*(?:top|left|right|bottom):\s*[^;]+/gi, '');
|
|
312
|
-
return `style="${cleaned}"`;
|
|
313
|
-
});
|
|
314
|
-
result = result.replace(/(<style[^>]*>)([\s\S]*?)(<\/style>)/gi, (_m, open, css, close) => {
|
|
315
|
-
let fixed = css.replace(/position:\s*absolute/gi, 'position:static');
|
|
316
|
-
fixed = fixed.replace(/position:\s*static([^}]*)/gi, (ruleMatch) => {
|
|
317
|
-
return ruleMatch.replace(/(?:^|;)\s*(?:top|left|right|bottom):\s*[^;{}]+/gi, '');
|
|
318
|
-
});
|
|
319
|
-
return open + fixed + close;
|
|
320
|
-
});
|
|
321
|
-
result = result.replace(/transform:\s*translate[^;)]*\)/gi, 'transform:none');
|
|
322
|
-
result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*\)/gi, 'transform:none');
|
|
323
|
-
result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*,\s*0\.\d+\s*\)/gi, 'transform:none');
|
|
324
|
-
result = result.replace(/zoom:\s*0\.\d+/gi, 'zoom:1');
|
|
325
|
-
return result;
|
|
326
|
-
}
|
|
327
254
|
function injectTitleContrastFix(html, designTextColor) {
|
|
328
255
|
const safeColor = designTextColor.replace(/'/g, "\\'");
|
|
329
256
|
const script = `<script>(function(){` +
|
|
@@ -350,61 +277,6 @@ function injectTitleContrastFix(html, designTextColor) {
|
|
|
350
277
|
}
|
|
351
278
|
return html + script;
|
|
352
279
|
}
|
|
353
|
-
function injectMeasureCss(html) {
|
|
354
|
-
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
355
|
-
const measureCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word;overflow:visible!important;max-height:none!important}html{width:2040px!important;height:auto!important;min-height:auto!important;margin:0!important}body{width:1920px!important;min-width:1920px!important;height:auto!important;min-height:auto!important;overflow:visible!important;margin:0!important}</style>`;
|
|
356
|
-
const injection = measureCss;
|
|
357
|
-
if (result.includes('</head>')) {
|
|
358
|
-
result = result.replace('</head>', `${injection}</head>`);
|
|
359
|
-
}
|
|
360
|
-
else if (result.includes('<head>')) {
|
|
361
|
-
result = result.replace('<head>', `<head>${injection}`);
|
|
362
|
-
}
|
|
363
|
-
else if (result.includes('<html')) {
|
|
364
|
-
result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${injection}</head>`);
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
result = injection + result;
|
|
368
|
-
}
|
|
369
|
-
const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
|
|
370
|
-
if (result.includes('</body>')) {
|
|
371
|
-
result = result.replace('</body>', `${measureScript}</body>`);
|
|
372
|
-
}
|
|
373
|
-
else if (result.includes('</html>')) {
|
|
374
|
-
result = result.replace('</html>', `<body>${measureScript}</body></html>`);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
result += `</style></head><body>${measureScript}</body></html>`;
|
|
378
|
-
}
|
|
379
|
-
return result;
|
|
380
|
-
}
|
|
381
|
-
function injectFillMeasureCss(html) {
|
|
382
|
-
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
383
|
-
const fillCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word;min-height:auto!important;height:auto!important}html{width:2040px!important;margin:0!important;height:auto!important}body{width:1920px!important;min-width:1920px!important;margin:0!important;display:block!important;height:auto!important;min-height:auto!important;overflow:visible!important}body>*{flex:none!important}body *{flex-grow:0!important;flex-shrink:1!important;flex-basis:auto!important}</style>`;
|
|
384
|
-
const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
|
|
385
|
-
if (result.includes('</head>')) {
|
|
386
|
-
result = result.replace('</head>', `${fillCss}</head>`);
|
|
387
|
-
}
|
|
388
|
-
else if (result.includes('<head>')) {
|
|
389
|
-
result = result.replace('<head>', `<head>${fillCss}`);
|
|
390
|
-
}
|
|
391
|
-
else if (result.includes('<html')) {
|
|
392
|
-
result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${fillCss}</head>`);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
result = fillCss + result;
|
|
396
|
-
}
|
|
397
|
-
if (result.includes('</body>')) {
|
|
398
|
-
result = result.replace('</body>', `${measureScript}</body>`);
|
|
399
|
-
}
|
|
400
|
-
else if (result.includes('</html>')) {
|
|
401
|
-
result = result.replace('</html>', `<body>${measureScript}</body></html>`);
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
result += `</style></head><body>${measureScript}</body></html>`;
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
407
|
-
}
|
|
408
280
|
function escapeHtml(text) {
|
|
409
281
|
return text
|
|
410
282
|
.replace(/&/g, '&')
|
|
@@ -871,56 +743,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
871
743
|
}
|
|
872
744
|
}
|
|
873
745
|
}
|
|
874
|
-
let layoutTemplates = [];
|
|
875
|
-
let slideSkeletons = new Map();
|
|
876
|
-
if (plan.design) {
|
|
877
|
-
try {
|
|
878
|
-
if (phaseLogger)
|
|
879
|
-
phaseLogger('powerpoint-create', 'layout-set', 'Generating custom layout components...');
|
|
880
|
-
const layoutSetPrompt = buildLayoutSetPrompt(plan.design);
|
|
881
|
-
const layoutRes = await llmClient.chatCompletion({
|
|
882
|
-
messages: [
|
|
883
|
-
{ role: 'system', content: layoutSetPrompt },
|
|
884
|
-
{ role: 'user', content: 'Generate the layout components now. Start with ---LAYOUT: and end each with ---END---.' },
|
|
885
|
-
],
|
|
886
|
-
temperature: 0.7,
|
|
887
|
-
max_tokens: 6000,
|
|
888
|
-
}, { maxRetries: 2 });
|
|
889
|
-
const layoutRaw = layoutRes.choices[0]?.message;
|
|
890
|
-
const layoutText = layoutRaw ? extractContent(layoutRaw) : '';
|
|
891
|
-
layoutTemplates = parseLayoutSet(layoutText);
|
|
892
|
-
logger.info(`Layout Set: generated ${layoutTemplates.length} templates: ${layoutTemplates.map(l => l.name).join(', ')}`);
|
|
893
|
-
if (phaseLogger)
|
|
894
|
-
phaseLogger('powerpoint-create', 'layout-set', `Generated ${layoutTemplates.length} layout templates`);
|
|
895
|
-
}
|
|
896
|
-
catch (e) {
|
|
897
|
-
logger.warn(`Layout Set generation failed, will use fallback: ${e}`);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
if (layoutTemplates.length >= 4) {
|
|
901
|
-
try {
|
|
902
|
-
if (phaseLogger)
|
|
903
|
-
phaseLogger('powerpoint-create', 'skeleton', 'Generating slide skeletons...');
|
|
904
|
-
const skeletonPrompt = buildAllSkeletonsPrompt(plan.slides, layoutTemplates);
|
|
905
|
-
const skeletonRes = await llmClient.chatCompletion({
|
|
906
|
-
messages: [
|
|
907
|
-
{ role: 'system', content: skeletonPrompt },
|
|
908
|
-
{ role: 'user', content: 'Generate skeletons for all content slides. Use ===SLIDE:N=== format.' },
|
|
909
|
-
],
|
|
910
|
-
temperature: 0.3,
|
|
911
|
-
max_tokens: 8000,
|
|
912
|
-
}, { maxRetries: 2 });
|
|
913
|
-
const skeletonRaw = skeletonRes.choices[0]?.message;
|
|
914
|
-
const skeletonText = skeletonRaw ? extractContent(skeletonRaw) : '';
|
|
915
|
-
slideSkeletons = parseSkeletons(skeletonText);
|
|
916
|
-
logger.info(`Skeletons: generated ${slideSkeletons.size} slide skeletons`);
|
|
917
|
-
if (phaseLogger)
|
|
918
|
-
phaseLogger('powerpoint-create', 'skeleton', `Generated ${slideSkeletons.size} slide skeletons`);
|
|
919
|
-
}
|
|
920
|
-
catch (e) {
|
|
921
|
-
logger.warn(`Skeleton generation failed, will use fallback: ${e}`);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
746
|
if (phaseLogger)
|
|
925
747
|
phaseLogger('powerpoint-create', 'execution', 'Starting HTML rendering pipeline...');
|
|
926
748
|
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
@@ -995,7 +817,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
995
817
|
if (phaseLogger)
|
|
996
818
|
phaseLogger('powerpoint-create', 'execution', `Rendering slide ${slideNum}/${plan.slides.length}: ${slidePlan.title}`);
|
|
997
819
|
let html = null;
|
|
998
|
-
let htmlPrompt = null;
|
|
999
820
|
if (slidePlan.type === 'title') {
|
|
1000
821
|
html = buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, slideNum);
|
|
1001
822
|
}
|
|
@@ -1011,254 +832,73 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1011
832
|
logger.info(`Slide ${slideNum}: Using code-generated overview template (${overviewItems.length} items)`);
|
|
1012
833
|
}
|
|
1013
834
|
}
|
|
1014
|
-
let slideLayoutType = 'cards';
|
|
1015
|
-
let usedSkeleton = false;
|
|
1016
835
|
if (!html && slidePlan.type !== 'title' && slidePlan.type !== 'closing') {
|
|
1017
836
|
const cleanedDirection = (slidePlan.content_direction || '')
|
|
1018
837
|
.replace(/\s*Layout\s*:\s*[^\n]*/gi, '')
|
|
1019
838
|
.trim();
|
|
1020
|
-
slideLayoutType = extractLayoutHint(slidePlan.content_direction || '');
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
? layoutTemplates.find(l => l.name === skeletonInfo.layout)
|
|
1024
|
-
: null;
|
|
1025
|
-
if (skeletonInfo && matchedLayout) {
|
|
1026
|
-
usedSkeleton = true;
|
|
1027
|
-
logger.info(`Slide ${slideNum}: Using skeleton "${skeletonInfo.layout}" from Phase 4 (layoutType: ${slideLayoutType})`);
|
|
1028
|
-
htmlPrompt = buildFillPrompt(skeletonInfo.skeleton, matchedLayout.css, cleanedDirection, slidePlan.title, plan.design, i, plan.slides.length, language, slideLayoutType);
|
|
1029
|
-
}
|
|
1030
|
-
else {
|
|
1031
|
-
logger.info(`Slide ${slideNum}: Fallback to layout-specific prompt → "${slideLayoutType}"`);
|
|
1032
|
-
htmlPrompt = buildSlideHtmlPrompt(slidePlan.title, cleanedDirection, plan.design, i, plan.slides.length, language, slideLayoutType);
|
|
1033
|
-
}
|
|
839
|
+
const slideLayoutType = extractLayoutHint(slidePlan.content_direction || '');
|
|
840
|
+
const jsonPrompt = buildContentFillJsonPrompt(slidePlan.title, cleanedDirection, slideLayoutType, language);
|
|
841
|
+
let slideData = null;
|
|
1034
842
|
try {
|
|
1035
|
-
const
|
|
843
|
+
const jsonRes = await llmClient.chatCompletion({
|
|
1036
844
|
messages: [
|
|
1037
|
-
{ role: 'system', content:
|
|
1038
|
-
{ role: 'user', content: '
|
|
845
|
+
{ role: 'system', content: jsonPrompt },
|
|
846
|
+
{ role: 'user', content: 'Output the JSON now.' },
|
|
1039
847
|
],
|
|
1040
848
|
temperature: 0.3,
|
|
1041
|
-
max_tokens:
|
|
849
|
+
max_tokens: 2000,
|
|
1042
850
|
});
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
if (!
|
|
1047
|
-
logger.warn(`Slide ${slideNum}:
|
|
851
|
+
const jsonMsg = jsonRes.choices[0]?.message;
|
|
852
|
+
const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
|
|
853
|
+
slideData = parseContentFillJson(jsonRaw, slideLayoutType);
|
|
854
|
+
if (!slideData) {
|
|
855
|
+
logger.warn(`Slide ${slideNum}: JSON parse failed, retrying`);
|
|
1048
856
|
const retryRes = await llmClient.chatCompletion({
|
|
1049
857
|
messages: [
|
|
1050
|
-
{ role: 'system', content:
|
|
1051
|
-
{ role: 'user', content: '
|
|
858
|
+
{ role: 'system', content: jsonPrompt },
|
|
859
|
+
{ role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
|
|
1052
860
|
],
|
|
1053
861
|
temperature: 0.2,
|
|
1054
|
-
max_tokens:
|
|
862
|
+
max_tokens: 2000,
|
|
1055
863
|
});
|
|
1056
864
|
const retryMsg = retryRes.choices[0]?.message;
|
|
1057
865
|
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
1058
|
-
|
|
866
|
+
slideData = parseContentFillJson(retryRaw, slideLayoutType);
|
|
1059
867
|
}
|
|
1060
868
|
}
|
|
1061
869
|
catch (e) {
|
|
1062
|
-
logger.warn(`Slide ${slideNum}: LLM call failed: ${e}`);
|
|
870
|
+
logger.warn(`Slide ${slideNum}: JSON fill LLM call failed: ${e}`);
|
|
1063
871
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
failCount++;
|
|
1068
|
-
continue;
|
|
1069
|
-
}
|
|
1070
|
-
const OVERFLOW_THRESHOLD = 1080;
|
|
1071
|
-
if (htmlPrompt) {
|
|
1072
|
-
const MAX_REGEN_ATTEMPTS = 2;
|
|
1073
|
-
for (let attempt = 0; attempt < MAX_REGEN_ATTEMPTS; attempt++) {
|
|
1074
|
-
const measureFileName = `hanseol_measure_${slideNum}_${timestamp}_${attempt}.html`;
|
|
1075
|
-
const measureWritePath = path.join(tempWritePath, measureFileName);
|
|
1076
|
-
const measureWinPath = `${tempWinPath}\\${measureFileName}`;
|
|
1077
|
-
try {
|
|
1078
|
-
fs.writeFileSync(measureWritePath, injectMeasureCss(html), 'utf-8');
|
|
1079
|
-
const contentHeight = await powerpointClient.measureHtmlHeight(measureWinPath);
|
|
1080
|
-
try {
|
|
1081
|
-
fs.unlinkSync(measureWritePath);
|
|
1082
|
-
}
|
|
1083
|
-
catch { }
|
|
1084
|
-
if (contentHeight <= OVERFLOW_THRESHOLD) {
|
|
1085
|
-
if (attempt > 0)
|
|
1086
|
-
logger.info(`Slide ${slideNum}: Fits after ${attempt} regeneration(s) (${contentHeight}px)`);
|
|
1087
|
-
break;
|
|
1088
|
-
}
|
|
1089
|
-
logger.warn(`Slide ${slideNum}: Content overflows (${contentHeight}px > ${OVERFLOW_THRESHOLD}), attempt ${attempt + 1}/${MAX_REGEN_ATTEMPTS}`);
|
|
1090
|
-
if (phaseLogger)
|
|
1091
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: overflow (${contentHeight}px), regen ${attempt + 1}/${MAX_REGEN_ATTEMPTS}...`);
|
|
1092
|
-
try {
|
|
1093
|
-
const regenRes = await llmClient.chatCompletion({
|
|
1094
|
-
messages: [
|
|
1095
|
-
{ role: 'system', content: htmlPrompt },
|
|
1096
|
-
{ role: 'user', content: `Generate the HTML slide now. Your previous version was ${contentHeight}px tall — it MUST fit in 1080px (with 60px top+bottom padding = 960px usable). Content was clipped by ${contentHeight - 1080}px.\n\nFix:\n- Reduce content: remove 2-3 least important bullets or rows from each section\n- Compact padding: cards 24px, gaps 12-16px\n- Keep fonts: title 40-48px, body 26px min\n- If 4+ cards/sections → reduce to 3\n- Target: 850-960px content height` },
|
|
1097
|
-
],
|
|
1098
|
-
temperature: 0.2,
|
|
1099
|
-
max_tokens: 8000,
|
|
1100
|
-
});
|
|
1101
|
-
const regenMsg = regenRes.choices[0]?.message;
|
|
1102
|
-
const regenRaw = regenMsg ? extractContent(regenMsg) : '';
|
|
1103
|
-
const regenHtml = extractHtml(regenRaw);
|
|
1104
|
-
if (regenHtml) {
|
|
1105
|
-
html = regenHtml;
|
|
1106
|
-
logger.info(`Slide ${slideNum}: Regenerated HTML (attempt ${attempt + 1})`);
|
|
1107
|
-
}
|
|
1108
|
-
else {
|
|
1109
|
-
break;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
catch (e) {
|
|
1113
|
-
logger.warn(`Slide ${slideNum}: Overflow regeneration attempt ${attempt + 1} failed: ${e}`);
|
|
1114
|
-
break;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
catch {
|
|
1118
|
-
break;
|
|
1119
|
-
}
|
|
872
|
+
if (slideData) {
|
|
873
|
+
html = buildContentSlideHtml(plan.design, slidePlan.title, slideLayoutType, slideData, slideNum, i);
|
|
874
|
+
logger.info(`Slide ${slideNum}: Code template "${slideLayoutType}" generated`);
|
|
1120
875
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
if (htmlPrompt && slidePlan.type === 'content') {
|
|
1125
|
-
if (phaseLogger)
|
|
1126
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: starting fill measurement...`);
|
|
1127
|
-
for (let fillAttempt = 0; fillAttempt < MAX_FILL_ATTEMPTS; fillAttempt++) {
|
|
1128
|
-
const fillFileName = `hanseol_fill_${slideNum}_${timestamp}_${fillAttempt}.html`;
|
|
1129
|
-
const fillWritePath = path.join(tempWritePath, fillFileName);
|
|
1130
|
-
const fillWinPath = `${tempWinPath}\\${fillFileName}`;
|
|
1131
|
-
try {
|
|
1132
|
-
fs.writeFileSync(fillWritePath, injectFillMeasureCss(html), 'utf-8');
|
|
1133
|
-
const naturalHeight = await powerpointClient.measureHtmlHeight(fillWinPath);
|
|
1134
|
-
if (naturalHeight > 0) {
|
|
1135
|
-
try {
|
|
1136
|
-
fs.unlinkSync(fillWritePath);
|
|
1137
|
-
}
|
|
1138
|
-
catch { }
|
|
1139
|
-
}
|
|
1140
|
-
if (phaseLogger)
|
|
1141
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill=${naturalHeight}px (${Math.round(naturalHeight / 1080 * 100)}%), threshold=${MIN_FILL_HEIGHT}px`);
|
|
1142
|
-
if (naturalHeight > 0 && naturalHeight < MIN_FILL_HEIGHT) {
|
|
1143
|
-
const fillPct = Math.round(naturalHeight / 1080 * 100);
|
|
1144
|
-
logger.warn(`Slide ${slideNum}: Underfill — natural height ${naturalHeight}px (${fillPct}%). Attempt ${fillAttempt + 1}/${MAX_FILL_ATTEMPTS}...`);
|
|
1145
|
-
if (phaseLogger)
|
|
1146
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: underfill (${fillPct}%), regen attempt ${fillAttempt + 1}...`);
|
|
1147
|
-
try {
|
|
1148
|
-
const htmlExcerpt = html.length > 2500 ? html.slice(0, 2500) + '\n...(truncated)' : html;
|
|
1149
|
-
const isCatastrophic = fillPct < 50;
|
|
1150
|
-
const fillRes = await llmClient.chatCompletion({
|
|
1151
|
-
messages: [
|
|
1152
|
-
{ role: 'system', content: htmlPrompt },
|
|
1153
|
-
{ role: 'user', content: `${isCatastrophic ? '⚠⚠⚠ CATASTROPHIC FAILURE — COMPLETE REGENERATION REQUIRED ⚠⚠⚠\n\n' : ''}REJECTED HTML (fills only ${fillPct}% — ${naturalHeight}px of 1080px):\n\`\`\`html\n${htmlExcerpt}\n\`\`\`\n\nThis slide was REJECTED because the content is too sparse. ${isCatastrophic ? 'It fills LESS THAN HALF the slide — this is unacceptable.' : ''}\n\nYou MUST regenerate from scratch with SIGNIFICANTLY MORE content:\n1. Each card/section: 4-5 bullet points with specific numbers (not 2 sparse lines)\n2. ALL content elements MUST be DIRECT children of .content — NO wrapper divs\n3. Tables: 6-8 data rows. Progress bars: 5-6 bars. Timeline: 4-5 detailed milestones.\n4. Target content height: ${Math.round(1080 * 0.85)}-${Math.round(1080 * 0.95)}px\n\n${getUnderfillFixByLayout(slideLayoutType)}\n\nDo NOT repeat the same sparse structure. Add REAL detailed content.\nOutput the COMPLETE HTML from <!DOCTYPE html> to </html>.` },
|
|
1154
|
-
],
|
|
1155
|
-
temperature: isCatastrophic ? 0.5 : 0.3,
|
|
1156
|
-
max_tokens: 8000,
|
|
1157
|
-
});
|
|
1158
|
-
const fillMsg = fillRes.choices[0]?.message;
|
|
1159
|
-
const fillRaw = fillMsg ? extractContent(fillMsg) : '';
|
|
1160
|
-
const fillHtml = extractHtml(fillRaw);
|
|
1161
|
-
if (fillHtml) {
|
|
1162
|
-
const overflowCheckFile = `hanseol_ocheck_${slideNum}_${timestamp}_${fillAttempt}.html`;
|
|
1163
|
-
const overflowCheckPath = path.join(tempWritePath, overflowCheckFile);
|
|
1164
|
-
const overflowCheckWin = `${tempWinPath}\\${overflowCheckFile}`;
|
|
1165
|
-
let overflowOk = true;
|
|
1166
|
-
try {
|
|
1167
|
-
fs.writeFileSync(overflowCheckPath, injectMeasureCss(fillHtml), 'utf-8');
|
|
1168
|
-
const regenHeight = await powerpointClient.measureHtmlHeight(overflowCheckWin);
|
|
1169
|
-
try {
|
|
1170
|
-
fs.unlinkSync(overflowCheckPath);
|
|
1171
|
-
}
|
|
1172
|
-
catch { }
|
|
1173
|
-
if (regenHeight > OVERFLOW_THRESHOLD) {
|
|
1174
|
-
logger.warn(`Slide ${slideNum}: Fill regen overflows (${regenHeight}px), keeping original`);
|
|
1175
|
-
overflowOk = false;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
catch {
|
|
1179
|
-
try {
|
|
1180
|
-
fs.unlinkSync(overflowCheckPath);
|
|
1181
|
-
}
|
|
1182
|
-
catch { }
|
|
1183
|
-
}
|
|
1184
|
-
if (!overflowOk)
|
|
1185
|
-
break;
|
|
1186
|
-
html = fillHtml;
|
|
1187
|
-
logger.info(`Slide ${slideNum}: Regenerated with more content (was ${fillPct}% fill, attempt ${fillAttempt + 1})`);
|
|
1188
|
-
continue;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
catch (e) {
|
|
1192
|
-
logger.warn(`Slide ${slideNum}: Fill regeneration failed: ${e}`);
|
|
1193
|
-
}
|
|
1194
|
-
break;
|
|
1195
|
-
}
|
|
1196
|
-
else if (naturalHeight > 0) {
|
|
1197
|
-
const fillPct = Math.round(naturalHeight / 1080 * 100);
|
|
1198
|
-
logger.info(`Slide ${slideNum}: Fill OK — ${naturalHeight}px (${fillPct}%)`);
|
|
1199
|
-
break;
|
|
1200
|
-
}
|
|
1201
|
-
else {
|
|
1202
|
-
if (phaseLogger)
|
|
1203
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill measure returned 0, skipping`);
|
|
1204
|
-
break;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
catch (fillErr) {
|
|
1208
|
-
if (phaseLogger)
|
|
1209
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill measure FAILED: ${fillErr}`);
|
|
1210
|
-
break;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
if (html && slidePlan.type === 'content') {
|
|
1215
|
-
html = postProcessSlideHtml(html);
|
|
1216
|
-
if (slideNum <= 3) {
|
|
1217
|
-
try {
|
|
1218
|
-
fs.writeFileSync(path.join(tempWritePath, `hanseol_debug_slide_${slideNum}.html`), html, 'utf-8');
|
|
1219
|
-
}
|
|
1220
|
-
catch { }
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
if (htmlPrompt && html && slidePlan.type === 'content' && !usedSkeleton) {
|
|
1224
|
-
const MAX_LAYOUT_REGEN = 2;
|
|
1225
|
-
for (let layoutAttempt = 0; layoutAttempt < MAX_LAYOUT_REGEN; layoutAttempt++) {
|
|
1226
|
-
const complianceError = checkLayoutCompliance(html, slideLayoutType);
|
|
1227
|
-
if (!complianceError) {
|
|
1228
|
-
if (layoutAttempt > 0) {
|
|
1229
|
-
logger.info(`Slide ${slideNum}: Layout compliance OK after ${layoutAttempt} regen(s)`);
|
|
1230
|
-
}
|
|
1231
|
-
break;
|
|
1232
|
-
}
|
|
1233
|
-
logger.warn(`Slide ${slideNum}: Layout compliance FAILED — ${complianceError}`);
|
|
1234
|
-
if (phaseLogger)
|
|
1235
|
-
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: wrong layout, regen ${layoutAttempt + 1}/${MAX_LAYOUT_REGEN}...`);
|
|
876
|
+
else {
|
|
877
|
+
logger.warn(`Slide ${slideNum}: JSON fill failed, falling back to LLM HTML generation`);
|
|
878
|
+
const fallbackPrompt = buildSlideHtmlPrompt(slidePlan.title, cleanedDirection, plan.design, i, plan.slides.length, language, slideLayoutType);
|
|
1236
879
|
try {
|
|
1237
|
-
const
|
|
880
|
+
const htmlRes = await llmClient.chatCompletion({
|
|
1238
881
|
messages: [
|
|
1239
|
-
{ role: 'system', content:
|
|
1240
|
-
{ role: 'user', content:
|
|
882
|
+
{ role: 'system', content: fallbackPrompt },
|
|
883
|
+
{ role: 'user', content: 'Generate the HTML slide now.' },
|
|
1241
884
|
],
|
|
1242
|
-
temperature: 0.
|
|
885
|
+
temperature: 0.3,
|
|
1243
886
|
max_tokens: 8000,
|
|
1244
887
|
});
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
if (regenHtml) {
|
|
1249
|
-
html = regenHtml;
|
|
1250
|
-
logger.info(`Slide ${slideNum}: Regenerated for layout compliance (attempt ${layoutAttempt + 1})`);
|
|
1251
|
-
}
|
|
1252
|
-
else {
|
|
1253
|
-
break;
|
|
1254
|
-
}
|
|
888
|
+
const htmlMsg = htmlRes.choices[0]?.message;
|
|
889
|
+
const rawHtml = htmlMsg ? extractContent(htmlMsg) : '';
|
|
890
|
+
html = extractHtml(rawHtml);
|
|
1255
891
|
}
|
|
1256
892
|
catch (e) {
|
|
1257
|
-
logger.warn(`Slide ${slideNum}:
|
|
1258
|
-
break;
|
|
893
|
+
logger.warn(`Slide ${slideNum}: LLM HTML fallback failed: ${e}`);
|
|
1259
894
|
}
|
|
1260
895
|
}
|
|
1261
896
|
}
|
|
897
|
+
if (!html) {
|
|
898
|
+
logger.warn(`Slide ${slideNum}: Failed to generate HTML, skipping`);
|
|
899
|
+
failCount++;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
1262
902
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
1263
903
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
1264
904
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -1266,7 +906,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1266
906
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
1267
907
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
1268
908
|
try {
|
|
1269
|
-
const processed = injectTitleContrastFix(
|
|
909
|
+
const processed = injectTitleContrastFix(injectViewportCss(html, plan.design.background_color), plan.design.text_color);
|
|
1270
910
|
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
1271
911
|
tempFiles.push(htmlWritePath);
|
|
1272
912
|
}
|