hanseol-dev 5.0.2-dev.14 → 5.0.2-dev.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/common/sub-agent.d.ts +14 -0
- package/dist/agents/common/sub-agent.d.ts.map +1 -1
- package/dist/agents/common/sub-agent.js +98 -4
- package/dist/agents/common/sub-agent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/office/excel-agent.d.ts +1 -1
- package/dist/agents/office/excel-agent.d.ts.map +1 -1
- package/dist/agents/office/excel-agent.js +6 -6
- package/dist/agents/office/excel-agent.js.map +1 -1
- package/dist/agents/office/excel-create-agent.d.ts +3 -0
- package/dist/agents/office/excel-create-agent.d.ts.map +1 -0
- package/dist/agents/office/excel-create-agent.js +38 -0
- package/dist/agents/office/excel-create-agent.js.map +1 -0
- package/dist/agents/office/excel-create-prompts.d.ts +4 -0
- package/dist/agents/office/excel-create-prompts.d.ts.map +1 -0
- package/dist/agents/office/excel-create-prompts.js +154 -0
- package/dist/agents/office/excel-create-prompts.js.map +1 -0
- package/dist/agents/office/index.d.ts +6 -3
- package/dist/agents/office/index.d.ts.map +1 -1
- package/dist/agents/office/index.js +6 -3
- package/dist/agents/office/index.js.map +1 -1
- package/dist/agents/office/powerpoint-agent.d.ts +1 -1
- package/dist/agents/office/powerpoint-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-agent.js +6 -6
- package/dist/agents/office/powerpoint-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.d.ts +3 -0
- package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -0
- package/dist/agents/office/powerpoint-create-agent.js +1402 -0
- package/dist/agents/office/powerpoint-create-agent.js.map +1 -0
- package/dist/agents/office/powerpoint-create-prompts.d.ts +42 -0
- package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -0
- package/dist/agents/office/powerpoint-create-prompts.js +933 -0
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -0
- package/dist/agents/office/prompts.d.ts +10 -6
- package/dist/agents/office/prompts.d.ts.map +1 -1
- package/dist/agents/office/prompts.js +314 -61
- package/dist/agents/office/prompts.js.map +1 -1
- package/dist/agents/office/word-agent.d.ts +1 -1
- package/dist/agents/office/word-agent.d.ts.map +1 -1
- package/dist/agents/office/word-agent.js +6 -6
- package/dist/agents/office/word-agent.js.map +1 -1
- package/dist/agents/office/word-create-agent.d.ts +3 -0
- package/dist/agents/office/word-create-agent.d.ts.map +1 -0
- package/dist/agents/office/word-create-agent.js +38 -0
- package/dist/agents/office/word-create-agent.js.map +1 -0
- package/dist/agents/office/word-create-prompts.d.ts +4 -0
- package/dist/agents/office/word-create-prompts.d.ts.map +1 -0
- package/dist/agents/office/word-create-prompts.js +164 -0
- package/dist/agents/office/word-create-prompts.js.map +1 -0
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/pipe/pipe-runner.d.ts.map +1 -1
- package/dist/pipe/pipe-runner.js +20 -0
- package/dist/pipe/pipe-runner.js.map +1 -1
- package/dist/prompts/agents/planning.d.ts.map +1 -1
- package/dist/prompts/agents/planning.js +20 -11
- package/dist/prompts/agents/planning.js.map +1 -1
- package/dist/prompts/shared/tool-usage.d.ts.map +1 -1
- package/dist/prompts/shared/tool-usage.js +6 -3
- package/dist/prompts/shared/tool-usage.js.map +1 -1
- package/dist/prompts/system/plan-execute.js +1 -1
- package/dist/tools/office/excel-client.js +4 -4
- package/dist/tools/office/excel-tools/index.d.ts +2 -0
- package/dist/tools/office/excel-tools/index.d.ts.map +1 -1
- package/dist/tools/office/excel-tools/index.js +12 -0
- package/dist/tools/office/excel-tools/index.js.map +1 -1
- package/dist/tools/office/excel-tools/sheet-builders.d.ts +8 -0
- package/dist/tools/office/excel-tools/sheet-builders.d.ts.map +1 -0
- package/dist/tools/office/excel-tools/sheet-builders.js +414 -0
- package/dist/tools/office/excel-tools/sheet-builders.js.map +1 -0
- package/dist/tools/office/excel-tools.d.ts +1 -1
- package/dist/tools/office/excel-tools.d.ts.map +1 -1
- package/dist/tools/office/excel-tools.js +1 -1
- package/dist/tools/office/excel-tools.js.map +1 -1
- package/dist/tools/office/powerpoint-client.d.ts +3 -0
- package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-client.js +143 -10
- package/dist/tools/office/powerpoint-client.js.map +1 -1
- package/dist/tools/office/powerpoint-tools/export.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-tools/export.js +16 -1
- package/dist/tools/office/powerpoint-tools/export.js.map +1 -1
- package/dist/tools/office/powerpoint-tools/index.d.ts +2 -0
- package/dist/tools/office/powerpoint-tools/index.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-tools/index.js +7 -0
- package/dist/tools/office/powerpoint-tools/index.js.map +1 -1
- package/dist/tools/office/powerpoint-tools/launch.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-tools/launch.js +2 -0
- package/dist/tools/office/powerpoint-tools/launch.js.map +1 -1
- package/dist/tools/office/powerpoint-tools/layout-builders.d.ts +12 -0
- package/dist/tools/office/powerpoint-tools/layout-builders.d.ts.map +1 -0
- package/dist/tools/office/powerpoint-tools/layout-builders.js +785 -0
- package/dist/tools/office/powerpoint-tools/layout-builders.js.map +1 -0
- package/dist/tools/office/powerpoint-tools/slides.js +1 -1
- package/dist/tools/office/powerpoint-tools/slides.js.map +1 -1
- package/dist/tools/office/powerpoint-tools.d.ts +1 -1
- package/dist/tools/office/powerpoint-tools.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-tools.js +1 -1
- package/dist/tools/office/powerpoint-tools.js.map +1 -1
- package/dist/tools/office/word-client.js +4 -4
- package/dist/tools/office/word-tools/index.d.ts +4 -1
- package/dist/tools/office/word-tools/index.d.ts.map +1 -1
- package/dist/tools/office/word-tools/index.js +15 -0
- package/dist/tools/office/word-tools/index.js.map +1 -1
- package/dist/tools/office/word-tools/section-builders.d.ts +10 -0
- package/dist/tools/office/word-tools/section-builders.d.ts.map +1 -0
- package/dist/tools/office/word-tools/section-builders.js +421 -0
- package/dist/tools/office/word-tools/section-builders.js.map +1 -0
- package/dist/tools/office/word-tools.d.ts +1 -1
- package/dist/tools/office/word-tools.d.ts.map +1 -1
- package/dist/tools/office/word-tools.js +1 -1
- package/dist/tools/office/word-tools.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +7 -4
- package/dist/tools/registry.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1402 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { powerpointClient } from '../../tools/office/powerpoint-client.js';
|
|
4
|
+
import { getSubAgentPhaseLogger, getSubAgentToolCallLogger } from '../common/sub-agent.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { getPlatform } from '../../utils/platform-utils.js';
|
|
7
|
+
import { PPT_CREATE_ENHANCEMENT_PROMPT, PPT_STRUCTURED_PLANNING_PROMPT, buildSlideHtmlPrompt, extractLayoutHint, checkLayoutCompliance, postProcessSlideHtml, buildLayoutSetPrompt, parseLayoutSet, buildAllSkeletonsPrompt, parseSkeletons, buildFillPrompt, } from './powerpoint-create-prompts.js';
|
|
8
|
+
const DEFAULT_DESIGN = {
|
|
9
|
+
primary_color: '#1B2A4A',
|
|
10
|
+
accent_color: '#00D4AA',
|
|
11
|
+
background_color: '#FFFFFF',
|
|
12
|
+
text_color: '#1A1A2E',
|
|
13
|
+
accent_light: '#E8F5F0',
|
|
14
|
+
gradient_end: '#2D5F8A',
|
|
15
|
+
font_title: 'Segoe UI',
|
|
16
|
+
font_body: 'Malgun Gothic',
|
|
17
|
+
mood: 'modern-minimal',
|
|
18
|
+
design_notes: 'Clean gradients, card-based layouts',
|
|
19
|
+
};
|
|
20
|
+
function extractContent(msg) {
|
|
21
|
+
const content = msg['content'];
|
|
22
|
+
if (content && content.trim())
|
|
23
|
+
return content;
|
|
24
|
+
const reasoning = msg['reasoning_content'];
|
|
25
|
+
if (reasoning && reasoning.trim())
|
|
26
|
+
return reasoning;
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
function validateAndFixPlan(plan) {
|
|
30
|
+
if (!plan.design)
|
|
31
|
+
return 'Missing design object';
|
|
32
|
+
if (!plan.design.primary_color || !plan.design.accent_color) {
|
|
33
|
+
return 'Missing design colors (primary_color, accent_color)';
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(plan.slides) || plan.slides.length < 3) {
|
|
36
|
+
return 'slides array must have at least 3 entries';
|
|
37
|
+
}
|
|
38
|
+
if (plan.slides.length < 10) {
|
|
39
|
+
return `Only ${plan.slides.length} slides — minimum 10 required (aim for 12-15). Add more content slides with specific data.`;
|
|
40
|
+
}
|
|
41
|
+
if (plan.slides[0]?.type !== 'title') {
|
|
42
|
+
logger.info('Auto-fixing: first slide type changed to "title"');
|
|
43
|
+
plan.slides[0].type = 'title';
|
|
44
|
+
}
|
|
45
|
+
const lastSlide = plan.slides[plan.slides.length - 1];
|
|
46
|
+
if (lastSlide.type !== 'closing') {
|
|
47
|
+
logger.info('Auto-fixing: last slide type changed to "closing"');
|
|
48
|
+
lastSlide.type = 'closing';
|
|
49
|
+
}
|
|
50
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
51
|
+
if (!plan.slides[i].title) {
|
|
52
|
+
plan.slides[i].title = `Slide ${i + 1}`;
|
|
53
|
+
logger.info(`Auto-fixing: slide ${i + 1} missing title, set placeholder`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const layoutOnlyPatterns = [
|
|
57
|
+
/^(?:전체\s*배경|왼쪽에|오른쪽에|중앙에|상단에|하단에)/,
|
|
58
|
+
/#[0-9a-fA-F]{3,8}에서.*그라데이션/,
|
|
59
|
+
/(?:accent_light|primary|gradient_end)\s*(?:배경|글씨|색상)/,
|
|
60
|
+
/^(?:CSS|flexbox|grid|conic-gradient|linear-gradient)/i,
|
|
61
|
+
];
|
|
62
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
63
|
+
const slide = plan.slides[i];
|
|
64
|
+
if (slide.type === 'title' || slide.type === 'closing')
|
|
65
|
+
continue;
|
|
66
|
+
const cd = slide.content_direction || '';
|
|
67
|
+
const hasNumbers = /\d/.test(cd);
|
|
68
|
+
const isLayoutOnly = layoutOnlyPatterns.some(p => p.test(cd));
|
|
69
|
+
if (isLayoutOnly && !hasNumbers) {
|
|
70
|
+
return `Slide ${i + 1} "${slide.title}" content_direction contains layout instructions instead of actual data. content_direction must include REAL text/numbers to display.`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function repairLlmJson(raw) {
|
|
76
|
+
let result = '';
|
|
77
|
+
let inString = false;
|
|
78
|
+
let i = 0;
|
|
79
|
+
while (i < raw.length) {
|
|
80
|
+
const ch = raw[i];
|
|
81
|
+
if (!inString) {
|
|
82
|
+
if (ch === ',') {
|
|
83
|
+
let j = i + 1;
|
|
84
|
+
while (j < raw.length && /\s/.test(raw[j]))
|
|
85
|
+
j++;
|
|
86
|
+
if (j < raw.length && (raw[j] === '}' || raw[j] === ']')) {
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
result += ch;
|
|
92
|
+
if (ch === '"')
|
|
93
|
+
inString = true;
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (ch === '\\') {
|
|
98
|
+
result += ch;
|
|
99
|
+
if (i + 1 < raw.length) {
|
|
100
|
+
result += raw[i + 1];
|
|
101
|
+
i += 2;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === '"') {
|
|
109
|
+
let j = i + 1;
|
|
110
|
+
while (j < raw.length && /[ \t\r\n]/.test(raw[j]))
|
|
111
|
+
j++;
|
|
112
|
+
const next = j < raw.length ? raw[j] : '';
|
|
113
|
+
if (next === '' || /[,:}\]]/.test(next)) {
|
|
114
|
+
result += '"';
|
|
115
|
+
inString = false;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
result += '\\"';
|
|
119
|
+
}
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '\n') {
|
|
124
|
+
result += '\\n';
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '\r') {
|
|
129
|
+
result += '\\n';
|
|
130
|
+
i += (i + 1 < raw.length && raw[i + 1] === '\n') ? 2 : 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === '\t') {
|
|
134
|
+
result += '\\t';
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (ch.charCodeAt(0) < 0x20) {
|
|
139
|
+
i++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
result += ch;
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function parseJsonPlan(raw) {
|
|
148
|
+
let cleaned = raw.trim();
|
|
149
|
+
if (cleaned.startsWith('```')) {
|
|
150
|
+
cleaned = cleaned.replace(/^```(?:json|JSON)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
151
|
+
}
|
|
152
|
+
const firstBrace = cleaned.indexOf('{');
|
|
153
|
+
if (firstBrace > 0) {
|
|
154
|
+
cleaned = cleaned.slice(firstBrace);
|
|
155
|
+
}
|
|
156
|
+
const lastBrace = cleaned.lastIndexOf('}');
|
|
157
|
+
if (lastBrace >= 0 && lastBrace < cleaned.length - 1) {
|
|
158
|
+
cleaned = cleaned.slice(0, lastBrace + 1);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(cleaned);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
logger.debug('parseJsonPlan: direct parse failed', { error: String(e), length: cleaned.length });
|
|
165
|
+
}
|
|
166
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
167
|
+
if (!match)
|
|
168
|
+
return null;
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(match[0]);
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
const repaired = repairLlmJson(match[0]);
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(repaired);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
logger.debug('parseJsonPlan: repaired parse failed', { error: String(e) });
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
let final = repaired;
|
|
182
|
+
let braces = 0, brackets = 0;
|
|
183
|
+
let inStr = false, esc = false;
|
|
184
|
+
for (const ch of final) {
|
|
185
|
+
if (esc) {
|
|
186
|
+
esc = false;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ch === '\\') {
|
|
190
|
+
esc = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (ch === '"') {
|
|
194
|
+
inStr = !inStr;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (inStr)
|
|
198
|
+
continue;
|
|
199
|
+
if (ch === '{')
|
|
200
|
+
braces++;
|
|
201
|
+
else if (ch === '}')
|
|
202
|
+
braces--;
|
|
203
|
+
else if (ch === '[')
|
|
204
|
+
brackets++;
|
|
205
|
+
else if (ch === ']')
|
|
206
|
+
brackets--;
|
|
207
|
+
}
|
|
208
|
+
if (inStr)
|
|
209
|
+
final += '"';
|
|
210
|
+
for (let i = 0; i < brackets; i++)
|
|
211
|
+
final += ']';
|
|
212
|
+
for (let i = 0; i < braces; i++)
|
|
213
|
+
final += '}';
|
|
214
|
+
return JSON.parse(final);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function extractHtml(raw) {
|
|
221
|
+
const trimmed = raw.trim();
|
|
222
|
+
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html')) {
|
|
223
|
+
return trimmed;
|
|
224
|
+
}
|
|
225
|
+
const fenceMatch = trimmed.match(/```(?:html)?\s*\n([\s\S]*?)\n```/);
|
|
226
|
+
if (fenceMatch?.[1]) {
|
|
227
|
+
return fenceMatch[1].trim();
|
|
228
|
+
}
|
|
229
|
+
const docMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
230
|
+
if (docMatch?.[1]) {
|
|
231
|
+
return docMatch[1].trim();
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
function injectViewportCss(html, backgroundColor) {
|
|
236
|
+
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
237
|
+
const bgColor = backgroundColor || '#000000';
|
|
238
|
+
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>`;
|
|
239
|
+
const injection = overrideCss;
|
|
240
|
+
if (result.includes('</head>')) {
|
|
241
|
+
result = result.replace('</head>', `${injection}</head>`);
|
|
242
|
+
}
|
|
243
|
+
else if (result.includes('<head>')) {
|
|
244
|
+
result = result.replace('<head>', `<head>${injection}`);
|
|
245
|
+
}
|
|
246
|
+
else if (result.includes('<html')) {
|
|
247
|
+
result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${injection}</head>`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
result = injection + result;
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
function enforceMinFontSize(html) {
|
|
255
|
+
const MIN_PX = 26;
|
|
256
|
+
let result = html.replace(/font-size:\s*(\d+(?:\.\d+)?)px/g, (_match, size) => {
|
|
257
|
+
const px = parseFloat(size);
|
|
258
|
+
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
259
|
+
});
|
|
260
|
+
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)pt/g, (_match, size) => {
|
|
261
|
+
const pt = parseFloat(size);
|
|
262
|
+
const px = pt * 1.333;
|
|
263
|
+
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
264
|
+
});
|
|
265
|
+
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)r?em/g, (_match, size) => {
|
|
266
|
+
const em = parseFloat(size);
|
|
267
|
+
const px = em * 16;
|
|
268
|
+
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
269
|
+
});
|
|
270
|
+
result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)%/g, (_match, size) => {
|
|
271
|
+
const pct = parseFloat(size);
|
|
272
|
+
const px = pct / 100 * 16;
|
|
273
|
+
return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
|
|
274
|
+
});
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
function ensureSafeBodyPadding(html) {
|
|
278
|
+
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>`;
|
|
279
|
+
if (html.includes('</head>')) {
|
|
280
|
+
return html.replace('</head>', `${safeStyle}</head>`);
|
|
281
|
+
}
|
|
282
|
+
return html;
|
|
283
|
+
}
|
|
284
|
+
function stripGradientTextEffects(html) {
|
|
285
|
+
let result = html.replace(/-webkit-text-fill-color:\s*transparent/gi, '-webkit-text-fill-color:initial');
|
|
286
|
+
result = result.replace(/background-clip:\s*text/gi, 'background-clip:initial');
|
|
287
|
+
result = result.replace(/-webkit-background-clip:\s*text/gi, '-webkit-background-clip:initial');
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
function removeAbsolutePositioning(html) {
|
|
291
|
+
let result = html.replace(/style="([^"]*)position:\s*absolute([^"]*)"/gi, (match, before, after) => {
|
|
292
|
+
if (/(?:width|height):\s*[1-5]?\dpx/i.test(before + after) || /opacity:\s*0\.[0-4]/i.test(before + after)) {
|
|
293
|
+
return match;
|
|
294
|
+
}
|
|
295
|
+
let cleaned = `${before}position:static${after}`;
|
|
296
|
+
cleaned = cleaned.replace(/(?:^|;)\s*(?:top|left|right|bottom):\s*[^;]+/gi, '');
|
|
297
|
+
return `style="${cleaned}"`;
|
|
298
|
+
});
|
|
299
|
+
result = result.replace(/(<style[^>]*>)([\s\S]*?)(<\/style>)/gi, (_m, open, css, close) => {
|
|
300
|
+
let fixed = css.replace(/position:\s*absolute/gi, 'position:static');
|
|
301
|
+
fixed = fixed.replace(/position:\s*static([^}]*)/gi, (ruleMatch) => {
|
|
302
|
+
return ruleMatch.replace(/(?:^|;)\s*(?:top|left|right|bottom):\s*[^;{}]+/gi, '');
|
|
303
|
+
});
|
|
304
|
+
return open + fixed + close;
|
|
305
|
+
});
|
|
306
|
+
result = result.replace(/transform:\s*translate[^;)]*\)/gi, 'transform:none');
|
|
307
|
+
result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*\)/gi, 'transform:none');
|
|
308
|
+
result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*,\s*0\.\d+\s*\)/gi, 'transform:none');
|
|
309
|
+
result = result.replace(/zoom:\s*0\.\d+/gi, 'zoom:1');
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function injectTitleContrastFix(html, designTextColor) {
|
|
313
|
+
const safeColor = designTextColor.replace(/'/g, "\\'");
|
|
314
|
+
const script = `<script>(function(){` +
|
|
315
|
+
`var dc='${safeColor}';` +
|
|
316
|
+
`function lum(c){var m=c.match(/[\\d.]+/g);if(!m||m.length<3)return -1;` +
|
|
317
|
+
`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)});` +
|
|
318
|
+
`return 0.2126*s[0]+0.7152*s[1]+0.0722*s[2]}` +
|
|
319
|
+
`function getBg(el){while(el){var s=getComputedStyle(el);var bg=s.backgroundColor;` +
|
|
320
|
+
`var m=bg.match(/[\\d.]+/g);if(m&&m.length>=3){if(m.length<4||parseFloat(m[3])>0.1)return bg}` +
|
|
321
|
+
`el=el.parentElement}return'rgb(255,255,255)'}` +
|
|
322
|
+
`var els=document.querySelectorAll('h1,h2');` +
|
|
323
|
+
`for(var i=0;i<els.length;i++){var el=els[i];var cs=getComputedStyle(el);` +
|
|
324
|
+
`var tfc=cs.webkitTextFillColor||'';` +
|
|
325
|
+
`if(tfc==='transparent'||tfc==='rgba(0, 0, 0, 0)'){` +
|
|
326
|
+
`el.style.setProperty('-webkit-text-fill-color','initial','important')}` +
|
|
327
|
+
`if(parseFloat(cs.opacity)<0.6){el.style.setProperty('opacity','1','important')}` +
|
|
328
|
+
`var fg=cs.color;var bg=getBg(el);var fl=lum(fg),bl=lum(bg);` +
|
|
329
|
+
`if(fl>=0&&bl>=0){var r=(Math.max(fl,bl)+0.05)/(Math.min(fl,bl)+0.05);` +
|
|
330
|
+
`if(r<3){el.style.setProperty('color',dc,'important');` +
|
|
331
|
+
`el.style.setProperty('-webkit-text-fill-color',dc,'important')}}}` +
|
|
332
|
+
`})()<\/script>`;
|
|
333
|
+
if (html.includes('</body>')) {
|
|
334
|
+
return html.replace('</body>', `${script}</body>`);
|
|
335
|
+
}
|
|
336
|
+
return html + script;
|
|
337
|
+
}
|
|
338
|
+
function injectMeasureCss(html) {
|
|
339
|
+
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
340
|
+
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>`;
|
|
341
|
+
const injection = measureCss;
|
|
342
|
+
if (result.includes('</head>')) {
|
|
343
|
+
result = result.replace('</head>', `${injection}</head>`);
|
|
344
|
+
}
|
|
345
|
+
else if (result.includes('<head>')) {
|
|
346
|
+
result = result.replace('<head>', `<head>${injection}`);
|
|
347
|
+
}
|
|
348
|
+
else if (result.includes('<html')) {
|
|
349
|
+
result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${injection}</head>`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
result = injection + result;
|
|
353
|
+
}
|
|
354
|
+
const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
|
|
355
|
+
if (result.includes('</body>')) {
|
|
356
|
+
result = result.replace('</body>', `${measureScript}</body>`);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
result += measureScript;
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
function injectFillMeasureCss(html) {
|
|
364
|
+
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
365
|
+
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>`;
|
|
366
|
+
const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
|
|
367
|
+
if (result.includes('</head>')) {
|
|
368
|
+
result = result.replace('</head>', `${fillCss}</head>`);
|
|
369
|
+
}
|
|
370
|
+
else if (result.includes('<head>')) {
|
|
371
|
+
result = result.replace('<head>', `<head>${fillCss}`);
|
|
372
|
+
}
|
|
373
|
+
else if (result.includes('<html')) {
|
|
374
|
+
result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${fillCss}</head>`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
result = fillCss + result;
|
|
378
|
+
}
|
|
379
|
+
if (result.includes('</body>')) {
|
|
380
|
+
result = result.replace('</body>', `${measureScript}</body>`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
result += measureScript;
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
function escapeHtml(text) {
|
|
388
|
+
return text
|
|
389
|
+
.replace(/&/g, '&')
|
|
390
|
+
.replace(/</g, '<')
|
|
391
|
+
.replace(/>/g, '>')
|
|
392
|
+
.replace(/"/g, '"');
|
|
393
|
+
}
|
|
394
|
+
function buildTitleSlideHtml(design, mainTitle, subtitle, date, _slideNum) {
|
|
395
|
+
return `<!DOCTYPE html>
|
|
396
|
+
<html lang="ko">
|
|
397
|
+
<head>
|
|
398
|
+
<meta charset="UTF-8">
|
|
399
|
+
<style>
|
|
400
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
401
|
+
html { background-color: ${design.primary_color}; }
|
|
402
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
403
|
+
body {
|
|
404
|
+
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
405
|
+
display: flex;
|
|
406
|
+
flex-direction: column;
|
|
407
|
+
align-items: center;
|
|
408
|
+
justify-content: center;
|
|
409
|
+
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
410
|
+
}
|
|
411
|
+
body::before {
|
|
412
|
+
content: '';
|
|
413
|
+
display: block;
|
|
414
|
+
width: 100%;
|
|
415
|
+
height: 6px;
|
|
416
|
+
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
417
|
+
flex-shrink: 0;
|
|
418
|
+
}
|
|
419
|
+
.slide-content {
|
|
420
|
+
text-align: center;
|
|
421
|
+
max-width: 1400px;
|
|
422
|
+
padding: 0 60px;
|
|
423
|
+
}
|
|
424
|
+
.main-title {
|
|
425
|
+
font-size: 96px;
|
|
426
|
+
font-weight: 800;
|
|
427
|
+
color: #ffffff;
|
|
428
|
+
letter-spacing: -2px;
|
|
429
|
+
text-shadow: 0 6px 40px rgba(0,0,0,0.25);
|
|
430
|
+
line-height: 1.1;
|
|
431
|
+
margin-bottom: 32px;
|
|
432
|
+
}
|
|
433
|
+
.accent-bar {
|
|
434
|
+
width: 120px; height: 5px;
|
|
435
|
+
background: ${design.accent_color};
|
|
436
|
+
margin: 0 auto 32px;
|
|
437
|
+
border-radius: 3px;
|
|
438
|
+
box-shadow: 0 0 20px ${design.accent_color}40;
|
|
439
|
+
}
|
|
440
|
+
.subtitle {
|
|
441
|
+
font-size: 32px;
|
|
442
|
+
font-weight: 400;
|
|
443
|
+
color: rgba(255,255,255,0.88);
|
|
444
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
445
|
+
line-height: 1.5;
|
|
446
|
+
margin-bottom: 16px;
|
|
447
|
+
}
|
|
448
|
+
.date-text {
|
|
449
|
+
font-size: 22px;
|
|
450
|
+
font-weight: 300;
|
|
451
|
+
color: rgba(255,255,255,0.55);
|
|
452
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
453
|
+
}
|
|
454
|
+
</style>
|
|
455
|
+
</head>
|
|
456
|
+
<body>
|
|
457
|
+
<div class="slide-content">
|
|
458
|
+
<div class="main-title">${escapeHtml(mainTitle)}</div>
|
|
459
|
+
<div class="accent-bar"></div>
|
|
460
|
+
${subtitle ? `<div class="subtitle">${escapeHtml(subtitle)}</div>` : ''}
|
|
461
|
+
<div class="date-text">${escapeHtml(date)}</div>
|
|
462
|
+
</div>
|
|
463
|
+
<style>body{align-items:center!important;justify-content:center!important}.slide-content{flex:unset!important;min-height:unset!important;display:flex!important;flex-direction:column!important;align-items:center!important;justify-content:center!important}.slide-content>*{flex:unset!important;min-height:unset!important}</style>
|
|
464
|
+
</body>
|
|
465
|
+
</html>`;
|
|
466
|
+
}
|
|
467
|
+
function buildClosingSlideHtml(design, companyName, _slideNum, language) {
|
|
468
|
+
const thankYou = language === 'ko' ? '감사합니다' : 'Thank You';
|
|
469
|
+
return `<!DOCTYPE html>
|
|
470
|
+
<html lang="${language}">
|
|
471
|
+
<head>
|
|
472
|
+
<meta charset="UTF-8">
|
|
473
|
+
<style>
|
|
474
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
475
|
+
html { background-color: ${design.primary_color}; }
|
|
476
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
477
|
+
body {
|
|
478
|
+
background: linear-gradient(135deg, ${design.primary_color} 0%, ${design.gradient_end} 60%, ${design.primary_color} 100%);
|
|
479
|
+
display: flex;
|
|
480
|
+
flex-direction: column;
|
|
481
|
+
align-items: center;
|
|
482
|
+
justify-content: center;
|
|
483
|
+
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
484
|
+
}
|
|
485
|
+
body::after {
|
|
486
|
+
content: '';
|
|
487
|
+
display: block;
|
|
488
|
+
width: 100%;
|
|
489
|
+
height: 6px;
|
|
490
|
+
background: linear-gradient(90deg, transparent, ${design.accent_color}, transparent);
|
|
491
|
+
flex-shrink: 0;
|
|
492
|
+
}
|
|
493
|
+
.slide-content {
|
|
494
|
+
text-align: center;
|
|
495
|
+
}
|
|
496
|
+
.thank-you {
|
|
497
|
+
font-size: 96px;
|
|
498
|
+
font-weight: 800;
|
|
499
|
+
color: #ffffff;
|
|
500
|
+
letter-spacing: -1px;
|
|
501
|
+
text-shadow: 0 6px 40px rgba(0,0,0,0.25);
|
|
502
|
+
margin-bottom: 32px;
|
|
503
|
+
}
|
|
504
|
+
.accent-bar {
|
|
505
|
+
width: 100px; height: 5px;
|
|
506
|
+
background: ${design.accent_color};
|
|
507
|
+
margin: 0 auto 32px;
|
|
508
|
+
border-radius: 3px;
|
|
509
|
+
box-shadow: 0 0 20px ${design.accent_color}40;
|
|
510
|
+
}
|
|
511
|
+
.company {
|
|
512
|
+
font-size: 36px;
|
|
513
|
+
font-weight: 500;
|
|
514
|
+
color: rgba(255,255,255,0.80);
|
|
515
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
516
|
+
}
|
|
517
|
+
</style>
|
|
518
|
+
</head>
|
|
519
|
+
<body>
|
|
520
|
+
<div class="slide-content">
|
|
521
|
+
<div class="thank-you">${escapeHtml(thankYou)}</div>
|
|
522
|
+
<div class="accent-bar"></div>
|
|
523
|
+
<div class="company">${escapeHtml(companyName)}</div>
|
|
524
|
+
</div>
|
|
525
|
+
<style>body{align-items:center!important;justify-content:center!important}.slide-content{flex:unset!important;min-height:unset!important;display:flex!important;flex-direction:column!important;align-items:center!important;justify-content:center!important}.slide-content>*{flex:unset!important;min-height:unset!important}</style>
|
|
526
|
+
</body>
|
|
527
|
+
</html>`;
|
|
528
|
+
}
|
|
529
|
+
function isOverviewSlide(title, slideIndex) {
|
|
530
|
+
if (slideIndex !== 1)
|
|
531
|
+
return false;
|
|
532
|
+
const overviewKeywords = /개요|목차|overview|agenda|outline|순서|발표\s*구성|contents|목록/i;
|
|
533
|
+
return overviewKeywords.test(title);
|
|
534
|
+
}
|
|
535
|
+
function parseOverviewItems(contentDirection) {
|
|
536
|
+
const items = [];
|
|
537
|
+
const lines = contentDirection.split(/\n/).filter(l => l.trim());
|
|
538
|
+
for (const line of lines) {
|
|
539
|
+
const match = line.match(/^\d+[\.\)]\s*(.+?)(?:\s*[-–—:]\s*(.+))?$/);
|
|
540
|
+
if (match) {
|
|
541
|
+
items.push({ title: match[1].trim(), desc: match[2]?.trim() || '' });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (items.length === 0) {
|
|
545
|
+
const parts = contentDirection.split(/[,;·•]/).map(s => s.trim()).filter(Boolean);
|
|
546
|
+
for (const part of parts) {
|
|
547
|
+
const sepMatch = part.match(/^(.+?)(?:\s*[-–—:]\s*(.+))?$/);
|
|
548
|
+
if (sepMatch) {
|
|
549
|
+
items.push({ title: sepMatch[1].trim(), desc: sepMatch[2]?.trim() || '' });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return items.slice(0, 5);
|
|
554
|
+
}
|
|
555
|
+
function buildOverviewSlideHtml(design, title, subtitle, items, slideNum) {
|
|
556
|
+
const badgeColors = [
|
|
557
|
+
design.primary_color,
|
|
558
|
+
design.accent_color,
|
|
559
|
+
design.gradient_end,
|
|
560
|
+
design.primary_color,
|
|
561
|
+
design.accent_color,
|
|
562
|
+
];
|
|
563
|
+
const itemCount = items.length;
|
|
564
|
+
const topRow = itemCount <= 3 ? items : items.slice(0, Math.ceil(itemCount / 2));
|
|
565
|
+
const bottomRow = itemCount <= 3 ? [] : items.slice(Math.ceil(itemCount / 2));
|
|
566
|
+
function renderCard(item, idx) {
|
|
567
|
+
const color = badgeColors[idx % badgeColors.length];
|
|
568
|
+
return `
|
|
569
|
+
<div class="card">
|
|
570
|
+
<div class="badge" style="background:${color}">${idx + 1}</div>
|
|
571
|
+
<div class="card-title">${escapeHtml(item.title)}</div>
|
|
572
|
+
${item.desc ? `<div class="card-desc">${escapeHtml(item.desc)}</div>` : ''}
|
|
573
|
+
</div>`;
|
|
574
|
+
}
|
|
575
|
+
return `<!DOCTYPE html>
|
|
576
|
+
<html lang="ko">
|
|
577
|
+
<head>
|
|
578
|
+
<meta charset="UTF-8">
|
|
579
|
+
<style>
|
|
580
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
581
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
582
|
+
body {
|
|
583
|
+
background: ${design.background_color};
|
|
584
|
+
font-family: "${design.font_body}", "${design.font_title}", "Segoe UI", "Malgun Gothic", sans-serif;
|
|
585
|
+
display: flex; flex-direction: column;
|
|
586
|
+
word-break: keep-all; overflow-wrap: break-word;
|
|
587
|
+
}
|
|
588
|
+
.header {
|
|
589
|
+
background: linear-gradient(135deg, ${design.primary_color}, ${design.gradient_end});
|
|
590
|
+
padding: 48px 80px 40px;
|
|
591
|
+
flex-shrink: 0;
|
|
592
|
+
}
|
|
593
|
+
.header-title {
|
|
594
|
+
font-size: 52px; font-weight: 800; color: #ffffff;
|
|
595
|
+
font-family: "${design.font_title}", "Segoe UI", sans-serif;
|
|
596
|
+
margin-bottom: 8px;
|
|
597
|
+
}
|
|
598
|
+
.header-subtitle {
|
|
599
|
+
font-size: 24px; font-weight: 400; color: rgba(255,255,255,0.75);
|
|
600
|
+
}
|
|
601
|
+
.content {
|
|
602
|
+
flex: 1; display: flex; flex-direction: column;
|
|
603
|
+
padding: 48px 80px 40px;
|
|
604
|
+
gap: 24px;
|
|
605
|
+
justify-content: center;
|
|
606
|
+
}
|
|
607
|
+
.row {
|
|
608
|
+
display: flex; gap: 24px;
|
|
609
|
+
justify-content: center;
|
|
610
|
+
}
|
|
611
|
+
.card {
|
|
612
|
+
flex: 1;
|
|
613
|
+
max-width: 340px;
|
|
614
|
+
background: #ffffff;
|
|
615
|
+
border-radius: 16px;
|
|
616
|
+
padding: 36px 32px;
|
|
617
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
|
|
618
|
+
display: flex; flex-direction: column;
|
|
619
|
+
align-items: center; text-align: center;
|
|
620
|
+
gap: 12px;
|
|
621
|
+
transition: none;
|
|
622
|
+
}
|
|
623
|
+
.badge {
|
|
624
|
+
width: 44px; height: 44px;
|
|
625
|
+
border-radius: 50%;
|
|
626
|
+
color: #ffffff;
|
|
627
|
+
font-size: 20px; font-weight: 700;
|
|
628
|
+
display: flex; align-items: center; justify-content: center;
|
|
629
|
+
flex-shrink: 0;
|
|
630
|
+
}
|
|
631
|
+
.card-title {
|
|
632
|
+
font-size: 26px; font-weight: 700; color: ${design.text_color};
|
|
633
|
+
line-height: 1.3;
|
|
634
|
+
}
|
|
635
|
+
.card-desc {
|
|
636
|
+
font-size: 22px; font-weight: 400; color: ${design.text_color}aa;
|
|
637
|
+
line-height: 1.5;
|
|
638
|
+
}
|
|
639
|
+
.page-num {
|
|
640
|
+
position: absolute;
|
|
641
|
+
bottom: 24px; right: 44px;
|
|
642
|
+
font-size: 13px;
|
|
643
|
+
color: ${design.text_color}55;
|
|
644
|
+
}
|
|
645
|
+
</style>
|
|
646
|
+
</head>
|
|
647
|
+
<body>
|
|
648
|
+
<div class="header">
|
|
649
|
+
<div class="header-title">${escapeHtml(title)}</div>
|
|
650
|
+
${subtitle ? `<div class="header-subtitle">${escapeHtml(subtitle)}</div>` : ''}
|
|
651
|
+
</div>
|
|
652
|
+
<div class="content">
|
|
653
|
+
<div class="row">
|
|
654
|
+
${topRow.map((item, idx) => renderCard(item, idx)).join('')}
|
|
655
|
+
</div>
|
|
656
|
+
${bottomRow.length > 0 ? `<div class="row">
|
|
657
|
+
${bottomRow.map((item, idx) => renderCard(item, topRow.length + idx)).join('')}
|
|
658
|
+
</div>` : ''}
|
|
659
|
+
</div>
|
|
660
|
+
<div class="page-num">${slideNum}</div>
|
|
661
|
+
</body>
|
|
662
|
+
</html>`;
|
|
663
|
+
}
|
|
664
|
+
function getTempDir() {
|
|
665
|
+
const platform = getPlatform();
|
|
666
|
+
if (platform === 'wsl') {
|
|
667
|
+
return { writePath: '/mnt/c/temp', winPath: 'C:\\temp' };
|
|
668
|
+
}
|
|
669
|
+
return { writePath: 'C:\\temp', winPath: 'C:\\temp' };
|
|
670
|
+
}
|
|
671
|
+
function ensureTempDir(writePath) {
|
|
672
|
+
if (!fs.existsSync(writePath)) {
|
|
673
|
+
fs.mkdirSync(writePath, { recursive: true });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function normalizeDesign(raw) {
|
|
677
|
+
return {
|
|
678
|
+
primary_color: raw['primary_color'] || DEFAULT_DESIGN.primary_color,
|
|
679
|
+
accent_color: raw['accent_color'] || DEFAULT_DESIGN.accent_color,
|
|
680
|
+
background_color: raw['background_color'] || DEFAULT_DESIGN.background_color,
|
|
681
|
+
text_color: raw['text_color'] || DEFAULT_DESIGN.text_color,
|
|
682
|
+
accent_light: raw['accent_light'] || DEFAULT_DESIGN.accent_light,
|
|
683
|
+
gradient_end: raw['gradient_end'] || DEFAULT_DESIGN.gradient_end,
|
|
684
|
+
font_title: raw['font_title'] || DEFAULT_DESIGN.font_title,
|
|
685
|
+
font_body: raw['font_body'] || DEFAULT_DESIGN.font_body,
|
|
686
|
+
mood: raw['mood'] || DEFAULT_DESIGN.mood,
|
|
687
|
+
design_notes: raw['design_notes'] || DEFAULT_DESIGN.design_notes,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
691
|
+
const startTime = Date.now();
|
|
692
|
+
const phaseLogger = getSubAgentPhaseLogger();
|
|
693
|
+
const toolCallLogger = getSubAgentToolCallLogger();
|
|
694
|
+
let totalToolCalls = 0;
|
|
695
|
+
const timestamp = Date.now();
|
|
696
|
+
logger.enter('PPT-Create.runStructured');
|
|
697
|
+
const hasKorean = /[\uac00-\ud7af\u1100-\u11ff]/.test(instruction);
|
|
698
|
+
const language = hasKorean ? 'ko' : 'en';
|
|
699
|
+
if (phaseLogger)
|
|
700
|
+
phaseLogger('powerpoint-create', 'enhancement', 'Generating creative guidance...');
|
|
701
|
+
let guidance = '';
|
|
702
|
+
try {
|
|
703
|
+
const enhRes = await llmClient.chatCompletion({
|
|
704
|
+
messages: [
|
|
705
|
+
{ role: 'system', content: PPT_CREATE_ENHANCEMENT_PROMPT },
|
|
706
|
+
{ role: 'user', content: instruction },
|
|
707
|
+
],
|
|
708
|
+
temperature: 0.7,
|
|
709
|
+
max_tokens: 2000,
|
|
710
|
+
});
|
|
711
|
+
const enhMsg = enhRes.choices[0]?.message;
|
|
712
|
+
guidance = enhMsg ? extractContent(enhMsg) : '';
|
|
713
|
+
if (guidance.length < 1500) {
|
|
714
|
+
logger.warn('PPT enhancement too short, retrying', { length: guidance.length });
|
|
715
|
+
const retryEnhRes = await llmClient.chatCompletion({
|
|
716
|
+
messages: [
|
|
717
|
+
{ role: 'system', content: PPT_CREATE_ENHANCEMENT_PROMPT },
|
|
718
|
+
{ role: 'user', content: `${instruction}\n\nIMPORTANT: Produce a COMPLETE, DETAILED design system and content plan. Include ALL 7 sections: DOCUMENT_TYPE, AUDIENCE, MOOD, COLOR PALETTE (6 hex values), FONTS, SLIDE PLAN (10-15 slides), DESIGN NOTES. Do NOT stop early.` },
|
|
719
|
+
],
|
|
720
|
+
temperature: 0.7,
|
|
721
|
+
max_tokens: 2000,
|
|
722
|
+
});
|
|
723
|
+
const retryMsg = retryEnhRes.choices[0]?.message;
|
|
724
|
+
const retryGuidance = retryMsg ? extractContent(retryMsg) : '';
|
|
725
|
+
if (retryGuidance.length > guidance.length) {
|
|
726
|
+
guidance = retryGuidance;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (phaseLogger)
|
|
730
|
+
phaseLogger('powerpoint-create', 'enhancement', `Done (${guidance.length} chars)`);
|
|
731
|
+
}
|
|
732
|
+
catch (e) {
|
|
733
|
+
logger.warn('PPT enhancement failed, proceeding without', { error: String(e) });
|
|
734
|
+
}
|
|
735
|
+
const enhancedInstruction = guidance
|
|
736
|
+
? `${instruction}\n\n═══ CREATIVE GUIDANCE ═══\n${guidance}\n═══ END GUIDANCE ═══`
|
|
737
|
+
: instruction;
|
|
738
|
+
if (phaseLogger)
|
|
739
|
+
phaseLogger('powerpoint-create', 'planning', 'Generating JSON plan...');
|
|
740
|
+
let plan = null;
|
|
741
|
+
try {
|
|
742
|
+
const planRes = await llmClient.chatCompletion({
|
|
743
|
+
messages: [
|
|
744
|
+
{ role: 'system', content: PPT_STRUCTURED_PLANNING_PROMPT },
|
|
745
|
+
{ role: 'user', content: enhancedInstruction },
|
|
746
|
+
],
|
|
747
|
+
temperature: 0.4,
|
|
748
|
+
max_tokens: 8000,
|
|
749
|
+
});
|
|
750
|
+
const planMsg = planRes.choices[0]?.message;
|
|
751
|
+
const finishReason = planRes.choices[0]?.finish_reason;
|
|
752
|
+
const rawPlan = planMsg ? extractContent(planMsg) : '';
|
|
753
|
+
if (finishReason === 'length') {
|
|
754
|
+
logger.warn('PPT planning response was truncated (finish_reason=length)');
|
|
755
|
+
}
|
|
756
|
+
logger.debug('PPT planning raw response', { length: rawPlan.length, finishReason, first200: rawPlan.slice(0, 200) });
|
|
757
|
+
plan = rawPlan ? parseJsonPlan(rawPlan) : null;
|
|
758
|
+
if (plan) {
|
|
759
|
+
plan.design = normalizeDesign(plan.design);
|
|
760
|
+
const validationError = validateAndFixPlan(plan);
|
|
761
|
+
if (validationError) {
|
|
762
|
+
logger.warn('PPT plan validation failed', { error: validationError });
|
|
763
|
+
if (phaseLogger)
|
|
764
|
+
phaseLogger('powerpoint-create', 'planning', `Validation failed: ${validationError}. Retrying...`);
|
|
765
|
+
const retryRes = await llmClient.chatCompletion({
|
|
766
|
+
messages: [
|
|
767
|
+
{ role: 'system', content: PPT_STRUCTURED_PLANNING_PROMPT },
|
|
768
|
+
{ role: 'user', content: enhancedInstruction },
|
|
769
|
+
{ role: 'assistant', content: rawPlan },
|
|
770
|
+
{ role: 'user', content: `ERROR: ${validationError}\n\nFix the issues and output the corrected JSON. Remember: aim for 10-12 slides with REAL content data in each content_direction.` },
|
|
771
|
+
],
|
|
772
|
+
temperature: 0.2,
|
|
773
|
+
max_tokens: 8000,
|
|
774
|
+
});
|
|
775
|
+
const retryMsg = retryRes.choices[0]?.message;
|
|
776
|
+
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
777
|
+
const retryPlan = retryRaw ? parseJsonPlan(retryRaw) : null;
|
|
778
|
+
if (retryPlan) {
|
|
779
|
+
retryPlan.design = normalizeDesign(retryPlan.design);
|
|
780
|
+
const retryError = validateAndFixPlan(retryPlan);
|
|
781
|
+
if (!retryError) {
|
|
782
|
+
plan = retryPlan;
|
|
783
|
+
if (phaseLogger)
|
|
784
|
+
phaseLogger('powerpoint-create', 'planning', `Retry succeeded (${plan.slides.length} slides)`);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
plan = null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
plan = null;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
if (phaseLogger)
|
|
796
|
+
phaseLogger('powerpoint-create', 'planning', `Done (${plan.slides.length} slides)`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
logger.warn('PPT JSON plan parsing failed');
|
|
801
|
+
if (phaseLogger)
|
|
802
|
+
phaseLogger('powerpoint-create', 'planning', 'JSON parsing failed. Falling back.');
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch (e) {
|
|
806
|
+
logger.warn('PPT planning failed', { error: String(e) });
|
|
807
|
+
if (phaseLogger)
|
|
808
|
+
phaseLogger('powerpoint-create', 'planning', 'Planning error. Falling back.');
|
|
809
|
+
}
|
|
810
|
+
if (!plan) {
|
|
811
|
+
logger.error('PPT planning failed after retries — cannot create presentation');
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
error: 'Failed to generate presentation plan. Please try again.',
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (plan.slides.length > 15) {
|
|
818
|
+
logger.warn(`PPT plan has ${plan.slides.length} slides, capping to 15`);
|
|
819
|
+
const firstSlide = plan.slides[0];
|
|
820
|
+
const lastSlide = plan.slides[plan.slides.length - 1];
|
|
821
|
+
const contentSlides = plan.slides.slice(1, -1).slice(0, 13);
|
|
822
|
+
plan.slides = [firstSlide, ...contentSlides, lastSlide];
|
|
823
|
+
if (phaseLogger)
|
|
824
|
+
phaseLogger('powerpoint-create', 'planning', `Capped to ${plan.slides.length} slides`);
|
|
825
|
+
}
|
|
826
|
+
const userYearMatch = instruction.match(/(\d{4})년/);
|
|
827
|
+
if (userYearMatch) {
|
|
828
|
+
const userYear = userYearMatch[1];
|
|
829
|
+
for (const slide of plan.slides) {
|
|
830
|
+
if (slide.type === 'content' && slide.content_direction) {
|
|
831
|
+
if (!slide.content_direction.includes(`${userYear}년`)) {
|
|
832
|
+
slide.content_direction += ` (Note: This report covers ${userYear}년 data.)`;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const companyMatch = instruction.match(/회사명\s*[::]\s*([^\s,,、]+)/);
|
|
838
|
+
if (companyMatch && companyMatch[1]) {
|
|
839
|
+
const companyName_ = companyMatch[1];
|
|
840
|
+
const titleSlide = plan.slides.find(s => s.type === 'title');
|
|
841
|
+
if (titleSlide) {
|
|
842
|
+
if (titleSlide.title.trim() !== companyName_) {
|
|
843
|
+
const originalTitle = titleSlide.title;
|
|
844
|
+
titleSlide.title = companyName_;
|
|
845
|
+
if (!titleSlide.content_direction?.includes(originalTitle)) {
|
|
846
|
+
const stripped = originalTitle.replace(companyName_, '').replace(/^\s*[-–—:|\s]+/, '').trim();
|
|
847
|
+
const subtitle = stripped || originalTitle;
|
|
848
|
+
titleSlide.content_direction = subtitle + (titleSlide.content_direction ? '\n' + titleSlide.content_direction : '');
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
let layoutTemplates = [];
|
|
854
|
+
let slideSkeletons = new Map();
|
|
855
|
+
if (plan.design) {
|
|
856
|
+
try {
|
|
857
|
+
if (phaseLogger)
|
|
858
|
+
phaseLogger('powerpoint-create', 'layout-set', 'Generating custom layout components...');
|
|
859
|
+
const layoutSetPrompt = buildLayoutSetPrompt(plan.design);
|
|
860
|
+
const layoutRes = await llmClient.chatCompletion({
|
|
861
|
+
messages: [
|
|
862
|
+
{ role: 'system', content: layoutSetPrompt },
|
|
863
|
+
{ role: 'user', content: 'Generate the layout components now. Start with ---LAYOUT: and end each with ---END---.' },
|
|
864
|
+
],
|
|
865
|
+
temperature: 0.7,
|
|
866
|
+
max_tokens: 6000,
|
|
867
|
+
}, { maxRetries: 2 });
|
|
868
|
+
const layoutRaw = layoutRes.choices[0]?.message;
|
|
869
|
+
const layoutText = layoutRaw ? extractContent(layoutRaw) : '';
|
|
870
|
+
layoutTemplates = parseLayoutSet(layoutText);
|
|
871
|
+
logger.info(`Layout Set: generated ${layoutTemplates.length} templates: ${layoutTemplates.map(l => l.name).join(', ')}`);
|
|
872
|
+
if (phaseLogger)
|
|
873
|
+
phaseLogger('powerpoint-create', 'layout-set', `Generated ${layoutTemplates.length} layout templates`);
|
|
874
|
+
}
|
|
875
|
+
catch (e) {
|
|
876
|
+
logger.warn(`Layout Set generation failed, will use fallback: ${e}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (layoutTemplates.length >= 4) {
|
|
880
|
+
try {
|
|
881
|
+
if (phaseLogger)
|
|
882
|
+
phaseLogger('powerpoint-create', 'skeleton', 'Generating slide skeletons...');
|
|
883
|
+
const skeletonPrompt = buildAllSkeletonsPrompt(plan.slides, layoutTemplates);
|
|
884
|
+
const skeletonRes = await llmClient.chatCompletion({
|
|
885
|
+
messages: [
|
|
886
|
+
{ role: 'system', content: skeletonPrompt },
|
|
887
|
+
{ role: 'user', content: 'Generate skeletons for all content slides. Use ===SLIDE:N=== format.' },
|
|
888
|
+
],
|
|
889
|
+
temperature: 0.3,
|
|
890
|
+
max_tokens: 8000,
|
|
891
|
+
}, { maxRetries: 2 });
|
|
892
|
+
const skeletonRaw = skeletonRes.choices[0]?.message;
|
|
893
|
+
const skeletonText = skeletonRaw ? extractContent(skeletonRaw) : '';
|
|
894
|
+
slideSkeletons = parseSkeletons(skeletonText);
|
|
895
|
+
logger.info(`Skeletons: generated ${slideSkeletons.size} slide skeletons`);
|
|
896
|
+
if (phaseLogger)
|
|
897
|
+
phaseLogger('powerpoint-create', 'skeleton', `Generated ${slideSkeletons.size} slide skeletons`);
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
logger.warn(`Skeleton generation failed, will use fallback: ${e}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (phaseLogger)
|
|
904
|
+
phaseLogger('powerpoint-create', 'execution', 'Starting HTML rendering pipeline...');
|
|
905
|
+
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
906
|
+
ensureTempDir(tempWritePath);
|
|
907
|
+
let savePath = explicitSavePath;
|
|
908
|
+
if (!savePath) {
|
|
909
|
+
const fullPathMatch = instruction.match(/([A-Za-z]:\\[^\s,]+\.pptx|\/[^\s,]+\.pptx)/i);
|
|
910
|
+
if (fullPathMatch) {
|
|
911
|
+
savePath = fullPathMatch[1];
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
const nameMatch = instruction.match(/([\w][\w\-_.]*\.pptx)/i);
|
|
915
|
+
if (nameMatch) {
|
|
916
|
+
savePath = `C:\\temp\\${nameMatch[1]}`;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const titleSlidePlanForDate = plan.slides.find(s => s.type === 'title');
|
|
921
|
+
const dateSearchTexts = [
|
|
922
|
+
instruction,
|
|
923
|
+
titleSlidePlanForDate?.title || '',
|
|
924
|
+
titleSlidePlanForDate?.content_direction || '',
|
|
925
|
+
];
|
|
926
|
+
let kstDate = '';
|
|
927
|
+
for (const text of dateSearchTexts) {
|
|
928
|
+
const dateMatch = text.match(/(\d{4})년\s*(\d{1,2})\s*(월|분기)/);
|
|
929
|
+
if (dateMatch) {
|
|
930
|
+
kstDate = `${dateMatch[1]}년 ${dateMatch[2]}${dateMatch[3]}`;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (!kstDate) {
|
|
935
|
+
const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000);
|
|
936
|
+
kstDate = `${kstNow.getUTCFullYear()}년 ${kstNow.getUTCMonth() + 1}월`;
|
|
937
|
+
}
|
|
938
|
+
const titleSlidePlan = plan.slides.find(s => s.type === 'title');
|
|
939
|
+
const rawTitleText = titleSlidePlan?.title || '';
|
|
940
|
+
const titleSeps = [' - ', ' – ', ' — ', ': ', ' | '];
|
|
941
|
+
let companyName = rawTitleText;
|
|
942
|
+
let titleSubtitle = '';
|
|
943
|
+
for (const sep of titleSeps) {
|
|
944
|
+
const idx = rawTitleText.indexOf(sep);
|
|
945
|
+
if (idx > 0) {
|
|
946
|
+
companyName = rawTitleText.slice(0, idx).trim();
|
|
947
|
+
titleSubtitle = rawTitleText.slice(idx + sep.length).trim();
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (!titleSubtitle && titleSlidePlan) {
|
|
952
|
+
titleSubtitle = ((titleSlidePlan.content_direction || '').split('\n')[0] || '').trim().slice(0, 120);
|
|
953
|
+
}
|
|
954
|
+
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
955
|
+
titleSubtitle = '';
|
|
956
|
+
}
|
|
957
|
+
const createResult = await powerpointClient.powerpointCreate();
|
|
958
|
+
totalToolCalls++;
|
|
959
|
+
if (toolCallLogger)
|
|
960
|
+
toolCallLogger('powerpoint-create', 'powerpoint_create', {}, createResult.success ? 'Created' : createResult['error'] || '', createResult.success, 0, totalToolCalls);
|
|
961
|
+
if (!createResult.success) {
|
|
962
|
+
return { success: false, error: `Failed to create presentation: ${createResult['error']}` };
|
|
963
|
+
}
|
|
964
|
+
const builtSlides = [];
|
|
965
|
+
let failCount = 0;
|
|
966
|
+
const tempFiles = [];
|
|
967
|
+
for (let i = 0; i < plan.slides.length; i++) {
|
|
968
|
+
const slidePlan = plan.slides[i];
|
|
969
|
+
const slideNum = i + 1;
|
|
970
|
+
if (failCount >= 3) {
|
|
971
|
+
logger.warn('Too many slide failures, stopping');
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
if (phaseLogger)
|
|
975
|
+
phaseLogger('powerpoint-create', 'execution', `Rendering slide ${slideNum}/${plan.slides.length}: ${slidePlan.title}`);
|
|
976
|
+
let html = null;
|
|
977
|
+
let htmlPrompt = null;
|
|
978
|
+
if (slidePlan.type === 'title') {
|
|
979
|
+
html = buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, slideNum);
|
|
980
|
+
}
|
|
981
|
+
else if (slidePlan.type === 'closing') {
|
|
982
|
+
html = buildClosingSlideHtml(plan.design, companyName, slideNum, language);
|
|
983
|
+
}
|
|
984
|
+
else if (isOverviewSlide(slidePlan.title, i)) {
|
|
985
|
+
const overviewItems = parseOverviewItems(slidePlan.content_direction || '');
|
|
986
|
+
if (overviewItems.length >= 2) {
|
|
987
|
+
const firstLine = (slidePlan.content_direction || '').split('\n')[0] || '';
|
|
988
|
+
const overviewSubtitle = /^\d/.test(firstLine.trim()) ? '' : firstLine.trim();
|
|
989
|
+
html = buildOverviewSlideHtml(plan.design, slidePlan.title, overviewSubtitle, overviewItems, slideNum);
|
|
990
|
+
logger.info(`Slide ${slideNum}: Using code-generated overview template (${overviewItems.length} items)`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
let slideLayoutType = 'cards';
|
|
994
|
+
let usedSkeleton = false;
|
|
995
|
+
if (!html && slidePlan.type !== 'title' && slidePlan.type !== 'closing') {
|
|
996
|
+
const cleanedDirection = (slidePlan.content_direction || '')
|
|
997
|
+
.replace(/\s*Layout\s*:\s*[^\n]*/gi, '')
|
|
998
|
+
.trim();
|
|
999
|
+
slideLayoutType = extractLayoutHint(slidePlan.content_direction || '');
|
|
1000
|
+
const skeletonInfo = slideSkeletons.get(slideNum);
|
|
1001
|
+
const matchedLayout = skeletonInfo
|
|
1002
|
+
? layoutTemplates.find(l => l.name === skeletonInfo.layout)
|
|
1003
|
+
: null;
|
|
1004
|
+
if (skeletonInfo && matchedLayout) {
|
|
1005
|
+
usedSkeleton = true;
|
|
1006
|
+
logger.info(`Slide ${slideNum}: Using skeleton "${skeletonInfo.layout}" from Phase 4 (layoutType: ${slideLayoutType})`);
|
|
1007
|
+
htmlPrompt = buildFillPrompt(skeletonInfo.skeleton, matchedLayout.css, cleanedDirection, slidePlan.title, plan.design, i, plan.slides.length, language);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
logger.info(`Slide ${slideNum}: Fallback to layout-specific prompt → "${slideLayoutType}"`);
|
|
1011
|
+
htmlPrompt = buildSlideHtmlPrompt(slidePlan.title, cleanedDirection, plan.design, i, plan.slides.length, language, slideLayoutType);
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const htmlRes = await llmClient.chatCompletion({
|
|
1015
|
+
messages: [
|
|
1016
|
+
{ role: 'system', content: htmlPrompt },
|
|
1017
|
+
{ role: 'user', content: 'Generate the HTML slide now.' },
|
|
1018
|
+
],
|
|
1019
|
+
temperature: 0.3,
|
|
1020
|
+
max_tokens: 8000,
|
|
1021
|
+
});
|
|
1022
|
+
const htmlMsg = htmlRes.choices[0]?.message;
|
|
1023
|
+
const rawHtml = htmlMsg ? extractContent(htmlMsg) : '';
|
|
1024
|
+
html = extractHtml(rawHtml);
|
|
1025
|
+
if (!html && rawHtml.length > 100) {
|
|
1026
|
+
logger.warn(`Slide ${slideNum}: HTML extraction failed, retrying`);
|
|
1027
|
+
const retryRes = await llmClient.chatCompletion({
|
|
1028
|
+
messages: [
|
|
1029
|
+
{ role: 'system', content: htmlPrompt },
|
|
1030
|
+
{ role: 'user', content: 'Generate the complete HTML document. Start with <!DOCTYPE html> and end with </html>. No markdown fences.' },
|
|
1031
|
+
],
|
|
1032
|
+
temperature: 0.2,
|
|
1033
|
+
max_tokens: 4000,
|
|
1034
|
+
});
|
|
1035
|
+
const retryMsg = retryRes.choices[0]?.message;
|
|
1036
|
+
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
1037
|
+
html = extractHtml(retryRaw);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch (e) {
|
|
1041
|
+
logger.warn(`Slide ${slideNum}: LLM call failed: ${e}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (!html) {
|
|
1045
|
+
logger.warn(`Slide ${slideNum}: Failed to generate HTML, skipping`);
|
|
1046
|
+
failCount++;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
const OVERFLOW_THRESHOLD = 1050;
|
|
1050
|
+
if (htmlPrompt) {
|
|
1051
|
+
const MAX_REGEN_ATTEMPTS = 5;
|
|
1052
|
+
for (let attempt = 0; attempt < MAX_REGEN_ATTEMPTS; attempt++) {
|
|
1053
|
+
const measureFileName = `hanseol_measure_${slideNum}_${timestamp}_${attempt}.html`;
|
|
1054
|
+
const measureWritePath = path.join(tempWritePath, measureFileName);
|
|
1055
|
+
const measureWinPath = `${tempWinPath}\\${measureFileName}`;
|
|
1056
|
+
try {
|
|
1057
|
+
fs.writeFileSync(measureWritePath, injectMeasureCss(html), 'utf-8');
|
|
1058
|
+
const contentHeight = await powerpointClient.measureHtmlHeight(measureWinPath);
|
|
1059
|
+
try {
|
|
1060
|
+
fs.unlinkSync(measureWritePath);
|
|
1061
|
+
}
|
|
1062
|
+
catch { }
|
|
1063
|
+
if (contentHeight <= OVERFLOW_THRESHOLD) {
|
|
1064
|
+
if (attempt > 0)
|
|
1065
|
+
logger.info(`Slide ${slideNum}: Fits after ${attempt} regeneration(s) (${contentHeight}px)`);
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
logger.warn(`Slide ${slideNum}: Content overflows (${contentHeight}px > ${OVERFLOW_THRESHOLD}), attempt ${attempt + 1}/${MAX_REGEN_ATTEMPTS}`);
|
|
1069
|
+
if (phaseLogger)
|
|
1070
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: overflow (${contentHeight}px), regen ${attempt + 1}/${MAX_REGEN_ATTEMPTS}...`);
|
|
1071
|
+
try {
|
|
1072
|
+
const regenRes = await llmClient.chatCompletion({
|
|
1073
|
+
messages: [
|
|
1074
|
+
{ role: 'system', content: htmlPrompt },
|
|
1075
|
+
{ role: 'user', content: `Generate the HTML slide now. CRITICAL: Your previous version was ${contentHeight}px tall but the viewport is only 1080px (usable height ~960px with 60px padding top+bottom). Content was CLIPPED at the bottom by ${contentHeight - 1050}px.\n\nFix by REMOVING items — do NOT shrink fonts:\n- KEEP font sizes: title 40-48px, body 26px minimum. DO NOT reduce below 26px.\n- REMOVE the 1-2 LEAST important bullet points or items from each section\n- Reduce card padding from 40px to 28px, gaps from 24px to 16px\n- If there are 4+ sections/cards, reduce to 3\n- Keep the most impactful data and metrics\n${attempt >= 2 ? '- LAST CHANCE: Keep only 3 sections maximum. Remove all sub-bullets, keep only main points. Padding 20px, gaps 12px.' : ''}\n\nTarget height: 850-1000px. This is attempt ${attempt + 1}.` },
|
|
1076
|
+
],
|
|
1077
|
+
temperature: 0.2,
|
|
1078
|
+
max_tokens: 8000,
|
|
1079
|
+
});
|
|
1080
|
+
const regenMsg = regenRes.choices[0]?.message;
|
|
1081
|
+
const regenRaw = regenMsg ? extractContent(regenMsg) : '';
|
|
1082
|
+
const regenHtml = extractHtml(regenRaw);
|
|
1083
|
+
if (regenHtml) {
|
|
1084
|
+
html = regenHtml;
|
|
1085
|
+
logger.info(`Slide ${slideNum}: Regenerated HTML (attempt ${attempt + 1})`);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
catch (e) {
|
|
1092
|
+
logger.warn(`Slide ${slideNum}: Overflow regeneration attempt ${attempt + 1} failed: ${e}`);
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const MIN_FILL_HEIGHT = 850;
|
|
1102
|
+
const MAX_FILL_ATTEMPTS = 2;
|
|
1103
|
+
if (htmlPrompt && slidePlan.type === 'content') {
|
|
1104
|
+
if (phaseLogger)
|
|
1105
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: starting fill measurement...`);
|
|
1106
|
+
for (let fillAttempt = 0; fillAttempt < MAX_FILL_ATTEMPTS; fillAttempt++) {
|
|
1107
|
+
const fillFileName = `hanseol_fill_${slideNum}_${timestamp}_${fillAttempt}.html`;
|
|
1108
|
+
const fillWritePath = path.join(tempWritePath, fillFileName);
|
|
1109
|
+
const fillWinPath = `${tempWinPath}\\${fillFileName}`;
|
|
1110
|
+
try {
|
|
1111
|
+
fs.writeFileSync(fillWritePath, injectFillMeasureCss(html), 'utf-8');
|
|
1112
|
+
const naturalHeight = await powerpointClient.measureHtmlHeight(fillWinPath);
|
|
1113
|
+
if (naturalHeight > 0) {
|
|
1114
|
+
try {
|
|
1115
|
+
fs.unlinkSync(fillWritePath);
|
|
1116
|
+
}
|
|
1117
|
+
catch { }
|
|
1118
|
+
}
|
|
1119
|
+
if (phaseLogger)
|
|
1120
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill=${naturalHeight}px (${Math.round(naturalHeight / 1080 * 100)}%), threshold=${MIN_FILL_HEIGHT}px`);
|
|
1121
|
+
if (naturalHeight > 0 && naturalHeight < MIN_FILL_HEIGHT) {
|
|
1122
|
+
const fillPct = Math.round(naturalHeight / 1080 * 100);
|
|
1123
|
+
logger.warn(`Slide ${slideNum}: Underfill — natural height ${naturalHeight}px (${fillPct}%). Attempt ${fillAttempt + 1}/${MAX_FILL_ATTEMPTS}...`);
|
|
1124
|
+
if (phaseLogger)
|
|
1125
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: underfill (${fillPct}%), regen attempt ${fillAttempt + 1}...`);
|
|
1126
|
+
try {
|
|
1127
|
+
const fillRes = await llmClient.chatCompletion({
|
|
1128
|
+
messages: [
|
|
1129
|
+
{ role: 'system', content: htmlPrompt },
|
|
1130
|
+
{ role: 'user', content: `Generate the HTML slide now.\n\nCRITICAL FEEDBACK: Your previous version only fills ${fillPct}% of the slide vertically (natural content height: ${naturalHeight}px, slide: 1080px). The slide looks EMPTY with huge blank areas at the bottom.\n\nYou MUST add MORE ACTUAL CONTENT to fill the slide. Target: 85-95% fill. DO NOT exceed 1080px.\n\nADD MORE CONTENT:\n- Each card/section needs 4-6 bullet points (not 2-3)\n- Add a 2-3 sentence descriptive paragraph under each heading\n- Include specific numbers, percentages, and comparisons in every bullet\n- Add a summary/insight box at the bottom of each card\n- Use larger font sizes: titles 36-40px, body 26-28px, metrics 48-56px\n- Use generous padding: 48px 40px inside cards, 24px between items\n- Add a footer bar with additional context or page numbers\n\nDo NOT use CSS tricks (flex:1, min-height) to stretch empty space — add real text content instead.` },
|
|
1131
|
+
],
|
|
1132
|
+
temperature: 0.3,
|
|
1133
|
+
max_tokens: 8000,
|
|
1134
|
+
});
|
|
1135
|
+
const fillMsg = fillRes.choices[0]?.message;
|
|
1136
|
+
const fillRaw = fillMsg ? extractContent(fillMsg) : '';
|
|
1137
|
+
const fillHtml = extractHtml(fillRaw);
|
|
1138
|
+
if (fillHtml) {
|
|
1139
|
+
const overflowCheckFile = `hanseol_ocheck_${slideNum}_${timestamp}_${fillAttempt}.html`;
|
|
1140
|
+
const overflowCheckPath = path.join(tempWritePath, overflowCheckFile);
|
|
1141
|
+
const overflowCheckWin = `${tempWinPath}\\${overflowCheckFile}`;
|
|
1142
|
+
let overflowOk = true;
|
|
1143
|
+
try {
|
|
1144
|
+
fs.writeFileSync(overflowCheckPath, injectMeasureCss(fillHtml), 'utf-8');
|
|
1145
|
+
const regenHeight = await powerpointClient.measureHtmlHeight(overflowCheckWin);
|
|
1146
|
+
try {
|
|
1147
|
+
fs.unlinkSync(overflowCheckPath);
|
|
1148
|
+
}
|
|
1149
|
+
catch { }
|
|
1150
|
+
if (regenHeight > OVERFLOW_THRESHOLD) {
|
|
1151
|
+
logger.warn(`Slide ${slideNum}: Fill regen overflows (${regenHeight}px), keeping original`);
|
|
1152
|
+
overflowOk = false;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
try {
|
|
1157
|
+
fs.unlinkSync(overflowCheckPath);
|
|
1158
|
+
}
|
|
1159
|
+
catch { }
|
|
1160
|
+
}
|
|
1161
|
+
if (!overflowOk)
|
|
1162
|
+
break;
|
|
1163
|
+
html = fillHtml;
|
|
1164
|
+
logger.info(`Slide ${slideNum}: Regenerated with more content (was ${fillPct}% fill, attempt ${fillAttempt + 1})`);
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
catch (e) {
|
|
1169
|
+
logger.warn(`Slide ${slideNum}: Fill regeneration failed: ${e}`);
|
|
1170
|
+
}
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
else if (naturalHeight > 0) {
|
|
1174
|
+
const fillPct = Math.round(naturalHeight / 1080 * 100);
|
|
1175
|
+
logger.info(`Slide ${slideNum}: Fill OK — ${naturalHeight}px (${fillPct}%)`);
|
|
1176
|
+
break;
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
if (phaseLogger)
|
|
1180
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill measure returned 0, skipping`);
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
catch (fillErr) {
|
|
1185
|
+
if (phaseLogger)
|
|
1186
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill measure FAILED: ${fillErr}`);
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (html && slidePlan.type === 'content') {
|
|
1192
|
+
html = postProcessSlideHtml(html);
|
|
1193
|
+
}
|
|
1194
|
+
if (htmlPrompt && html && slidePlan.type === 'content' && !usedSkeleton) {
|
|
1195
|
+
const MAX_LAYOUT_REGEN = 2;
|
|
1196
|
+
for (let layoutAttempt = 0; layoutAttempt < MAX_LAYOUT_REGEN; layoutAttempt++) {
|
|
1197
|
+
const complianceError = checkLayoutCompliance(html, slideLayoutType);
|
|
1198
|
+
if (!complianceError) {
|
|
1199
|
+
if (layoutAttempt > 0) {
|
|
1200
|
+
logger.info(`Slide ${slideNum}: Layout compliance OK after ${layoutAttempt} regen(s)`);
|
|
1201
|
+
}
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
logger.warn(`Slide ${slideNum}: Layout compliance FAILED — ${complianceError}`);
|
|
1205
|
+
if (phaseLogger)
|
|
1206
|
+
phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: wrong layout, regen ${layoutAttempt + 1}/${MAX_LAYOUT_REGEN}...`);
|
|
1207
|
+
try {
|
|
1208
|
+
const regenRes = await llmClient.chatCompletion({
|
|
1209
|
+
messages: [
|
|
1210
|
+
{ role: 'system', content: htmlPrompt },
|
|
1211
|
+
{ role: 'user', content: `Generate the HTML slide now.\n\n⚠⚠⚠ CRITICAL ERROR: ${complianceError}\n\nYou MUST use the "${slideLayoutType}" layout as specified in the REQUIRED LAYOUT section above. Re-read the CSS STRUCTURE section carefully and follow it EXACTLY. Do NOT use cards or any other layout.` },
|
|
1212
|
+
],
|
|
1213
|
+
temperature: 0.2,
|
|
1214
|
+
max_tokens: 8000,
|
|
1215
|
+
});
|
|
1216
|
+
const regenMsg = regenRes.choices[0]?.message;
|
|
1217
|
+
const regenRaw = regenMsg ? extractContent(regenMsg) : '';
|
|
1218
|
+
const regenHtml = extractHtml(regenRaw);
|
|
1219
|
+
if (regenHtml) {
|
|
1220
|
+
html = regenHtml;
|
|
1221
|
+
logger.info(`Slide ${slideNum}: Regenerated for layout compliance (attempt ${layoutAttempt + 1})`);
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
catch (e) {
|
|
1228
|
+
logger.warn(`Slide ${slideNum}: Layout regen failed: ${e}`);
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
1234
|
+
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
1235
|
+
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
1236
|
+
const pngWritePath = path.join(tempWritePath, pngFileName);
|
|
1237
|
+
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
1238
|
+
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
1239
|
+
try {
|
|
1240
|
+
const processed = injectTitleContrastFix(ensureSafeBodyPadding(removeAbsolutePositioning(stripGradientTextEffects(enforceMinFontSize(injectViewportCss(html, plan.design.background_color))))), plan.design.text_color);
|
|
1241
|
+
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
1242
|
+
tempFiles.push(htmlWritePath);
|
|
1243
|
+
}
|
|
1244
|
+
catch (e) {
|
|
1245
|
+
logger.warn(`Slide ${slideNum}: Failed to write HTML file: ${e}`);
|
|
1246
|
+
failCount++;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
let renderSuccess = false;
|
|
1250
|
+
try {
|
|
1251
|
+
const renderResult = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
1252
|
+
totalToolCalls++;
|
|
1253
|
+
renderSuccess = renderResult.success;
|
|
1254
|
+
if (!renderSuccess) {
|
|
1255
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1256
|
+
const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
1257
|
+
totalToolCalls++;
|
|
1258
|
+
renderSuccess = retryRender.success;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
catch (e) {
|
|
1262
|
+
logger.warn(`Slide ${slideNum}: Edge screenshot failed: ${e}`);
|
|
1263
|
+
try {
|
|
1264
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1265
|
+
const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
1266
|
+
totalToolCalls++;
|
|
1267
|
+
renderSuccess = retryRender.success;
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
renderSuccess = false;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
fs.unlinkSync(htmlWritePath);
|
|
1275
|
+
}
|
|
1276
|
+
catch { }
|
|
1277
|
+
if (!renderSuccess) {
|
|
1278
|
+
logger.warn(`Slide ${slideNum}: Screenshot rendering failed, skipping`);
|
|
1279
|
+
failCount++;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
const pngStats = fs.statSync(pngWritePath);
|
|
1284
|
+
if (pngStats.size < 15000) {
|
|
1285
|
+
logger.warn(`Slide ${slideNum}: Screenshot too small (${pngStats.size} bytes), likely blank — skipping`);
|
|
1286
|
+
failCount++;
|
|
1287
|
+
try {
|
|
1288
|
+
fs.unlinkSync(pngWritePath);
|
|
1289
|
+
}
|
|
1290
|
+
catch { }
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
catch { }
|
|
1295
|
+
tempFiles.push(pngWritePath);
|
|
1296
|
+
const addResult = await powerpointClient.powerpointAddSlide(7);
|
|
1297
|
+
totalToolCalls++;
|
|
1298
|
+
if (!addResult.success) {
|
|
1299
|
+
logger.warn(`Slide ${slideNum}: Failed to add blank slide`);
|
|
1300
|
+
failCount++;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(slideNum, pngWinPath);
|
|
1304
|
+
totalToolCalls++;
|
|
1305
|
+
if (toolCallLogger)
|
|
1306
|
+
toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum, imagePath: pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, slideNum, totalToolCalls);
|
|
1307
|
+
if (bgResult.success) {
|
|
1308
|
+
builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
1309
|
+
try {
|
|
1310
|
+
await powerpointClient.powerpointAddNote(slideNum, html);
|
|
1311
|
+
}
|
|
1312
|
+
catch { }
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
logger.warn(`Slide ${slideNum}: Failed to set background: ${JSON.stringify(bgResult)}`);
|
|
1316
|
+
failCount++;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (builtSlides.length > 0) {
|
|
1320
|
+
try {
|
|
1321
|
+
const slideCountResult = await powerpointClient.powerpointGetSlideCount();
|
|
1322
|
+
const totalSlidesInPpt = slideCountResult['slide_count'] || 0;
|
|
1323
|
+
if (totalSlidesInPpt > builtSlides.length) {
|
|
1324
|
+
logger.warn(`PPT has ${totalSlidesInPpt} slides but only ${builtSlides.length} were rendered — deleting ${totalSlidesInPpt - builtSlides.length} trailing blanks`);
|
|
1325
|
+
for (let d = totalSlidesInPpt; d > builtSlides.length; d--) {
|
|
1326
|
+
await powerpointClient.powerpointDeleteSlide(d);
|
|
1327
|
+
totalToolCalls++;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
catch (e) {
|
|
1332
|
+
logger.warn(`Failed to clean up trailing slides: ${e}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (builtSlides.length > 0) {
|
|
1336
|
+
if (savePath) {
|
|
1337
|
+
const wslSavePath = savePath.replace(/\\/g, '/').replace(/^([A-Za-z]):/, (_m, d) => `/mnt/${d.toLowerCase()}`);
|
|
1338
|
+
try {
|
|
1339
|
+
fs.unlinkSync(wslSavePath);
|
|
1340
|
+
}
|
|
1341
|
+
catch { }
|
|
1342
|
+
}
|
|
1343
|
+
let saveResult = await powerpointClient.powerpointSave(savePath);
|
|
1344
|
+
totalToolCalls++;
|
|
1345
|
+
if (toolCallLogger)
|
|
1346
|
+
toolCallLogger('powerpoint-create', 'powerpoint_save', { path: savePath }, saveResult.success ? (saveResult['path'] || 'OK') : (saveResult.error || 'Failed'), saveResult.success, 0, totalToolCalls);
|
|
1347
|
+
if (!saveResult.success && savePath) {
|
|
1348
|
+
const fallbackPath = 'C:\\temp\\presentation.pptx';
|
|
1349
|
+
saveResult = await powerpointClient.powerpointSave(fallbackPath);
|
|
1350
|
+
totalToolCalls++;
|
|
1351
|
+
if (toolCallLogger)
|
|
1352
|
+
toolCallLogger('powerpoint-create', 'powerpoint_save', { path: fallbackPath }, saveResult.success ? (saveResult['path'] || 'OK') : (saveResult.error || 'Failed'), saveResult.success, 0, totalToolCalls);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
for (const tempFile of tempFiles) {
|
|
1356
|
+
try {
|
|
1357
|
+
fs.unlinkSync(tempFile);
|
|
1358
|
+
}
|
|
1359
|
+
catch { }
|
|
1360
|
+
}
|
|
1361
|
+
const duration = Date.now() - startTime;
|
|
1362
|
+
const summary = `Presentation created with ${builtSlides.length} slides (HTML rendering, ${plan.design.mood}):\n${builtSlides.join('\n')}`;
|
|
1363
|
+
logger.exit('PPT-Create.runStructured', { slideCount: builtSlides.length, totalToolCalls, duration });
|
|
1364
|
+
return {
|
|
1365
|
+
success: builtSlides.length > 0,
|
|
1366
|
+
result: summary,
|
|
1367
|
+
metadata: { iterations: plan.slides.length, toolCalls: totalToolCalls, duration },
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
export function createPowerPointCreateRequestTool() {
|
|
1371
|
+
return {
|
|
1372
|
+
definition: {
|
|
1373
|
+
type: 'function',
|
|
1374
|
+
function: {
|
|
1375
|
+
name: 'powerpoint_create_agent',
|
|
1376
|
+
description: 'Autonomous PowerPoint CREATION agent. Creates NEW presentations from scratch with professional slide designs, color schemes, and visual hierarchy. Uses HTML rendering pipeline for maximum design quality — each slide is rendered as a beautiful HTML page and captured as a high-quality image. Give it a topic or outline and it produces a polished, enterprise-grade presentation. For EDITING existing .pptx files, use powerpoint_modify_agent instead.',
|
|
1377
|
+
parameters: {
|
|
1378
|
+
type: 'object',
|
|
1379
|
+
properties: {
|
|
1380
|
+
instruction: {
|
|
1381
|
+
type: 'string',
|
|
1382
|
+
description: 'Detailed instruction for creating a new presentation. Include: topic/title, desired content, and design preferences. The agent autonomously creates a professional presentation with title, content, and closing slides.',
|
|
1383
|
+
},
|
|
1384
|
+
save_path: {
|
|
1385
|
+
type: 'string',
|
|
1386
|
+
description: 'Windows file path to save the presentation (e.g., "C:\\\\temp\\\\pitch.pptx"). MUST be provided if the user specified a save path.',
|
|
1387
|
+
},
|
|
1388
|
+
},
|
|
1389
|
+
required: ['instruction'],
|
|
1390
|
+
},
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
execute: async (args, llmClient) => {
|
|
1394
|
+
const instruction = args['instruction'];
|
|
1395
|
+
const explicitSavePath = args['save_path'];
|
|
1396
|
+
return runStructured(llmClient, instruction, explicitSavePath);
|
|
1397
|
+
},
|
|
1398
|
+
categories: ['llm-agent'],
|
|
1399
|
+
requiresSubLLM: true,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
//# sourceMappingURL=powerpoint-create-agent.js.map
|