hanseol-dev 5.0.3-dev.5 → 5.0.3-dev.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/agents/office/excel-create-prompts.d.ts +1 -1
  2. package/dist/agents/office/excel-create-prompts.d.ts.map +1 -1
  3. package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
  4. package/dist/agents/office/powerpoint-create-agent.js +251 -346
  5. package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
  6. package/dist/agents/office/powerpoint-create-prompts.d.ts +3 -2
  7. package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
  8. package/dist/agents/office/powerpoint-create-prompts.js +301 -257
  9. package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
  10. package/dist/agents/office/prompts.d.ts +4 -4
  11. package/dist/agents/office/prompts.d.ts.map +1 -1
  12. package/dist/agents/office/prompts.js +2 -2
  13. package/dist/agents/office/word-create-agent.js +4 -4
  14. package/dist/agents/office/word-create-agent.js.map +1 -1
  15. package/dist/agents/office/word-create-prompts.d.ts +3 -3
  16. package/dist/agents/office/word-create-prompts.d.ts.map +1 -1
  17. package/dist/agents/office/word-create-prompts.js +104 -42
  18. package/dist/agents/office/word-create-prompts.js.map +1 -1
  19. package/dist/constants.d.ts +1 -1
  20. package/dist/constants.d.ts.map +1 -1
  21. package/dist/constants.js +1 -1
  22. package/dist/constants.js.map +1 -1
  23. package/dist/prompts/agents/planning.d.ts.map +1 -1
  24. package/dist/prompts/agents/planning.js +7 -0
  25. package/dist/prompts/agents/planning.js.map +1 -1
  26. package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
  27. package/dist/tools/office/powerpoint-client.js +4 -0
  28. package/dist/tools/office/powerpoint-client.js.map +1 -1
  29. package/dist/tools/office/word-client.d.ts +15 -0
  30. package/dist/tools/office/word-client.d.ts.map +1 -1
  31. package/dist/tools/office/word-client.js +256 -5
  32. package/dist/tools/office/word-client.js.map +1 -1
  33. package/dist/tools/office/word-tools/section-builders.d.ts +2 -0
  34. package/dist/tools/office/word-tools/section-builders.d.ts.map +1 -1
  35. package/dist/tools/office/word-tools/section-builders.js +215 -47
  36. package/dist/tools/office/word-tools/section-builders.js.map +1 -1
  37. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  export const PPT_DESIGN_PROMPT = `You are an elite presentation design consultant AND structure planner. Output ONLY valid JSON — no markdown, no explanation, no code fences.
2
2
 
3
- Given the user's instruction, produce a JSON object that combines a cohesive visual design system with a detailed slide plan.
3
+ Given the user's instruction, produce a JSON object with a visual design system and a detailed slide plan.
4
4
 
5
5
  {
6
6
  "design": {
@@ -22,170 +22,87 @@ Given the user's instruction, produce a JSON object that combines a cohesive vis
22
22
  ]
23
23
  }
24
24
 
25
- ═══ DESIGN SYSTEM FIELD DEFINITIONS ═══
26
- design.primary_color: Deep main color for headers and key elements
27
- design.accent_color: Vibrant contrast color for highlights and CTAs
28
- design.background_color: Page background (near white, near black, or subtle tint)
29
- design.text_color: Main text color (must contrast with background)
30
- design.accent_light: Light tint for subtle section backgrounds
31
- design.gradient_end: Paired gradient color for primary
32
- design.font_title: System font for titles (Segoe UI, Arial, Georgia, Calibri)
33
- design.font_body: System font for body (Malgun Gothic, Segoe UI, Arial, Calibri)
34
- • design.mood: One of modern-minimal, bold-energetic, corporate-elegant, warm-friendly, academic-clean
35
- • design.design_notes: 1-2 sentence visual approach description
36
-
37
- • slides[].type: One of "title", "content", "closing"
38
- • slides[].title: Slide title in user's language (max 60 chars)
39
- • slides[].content_direction: The ACTUAL TEXT AND DATA to display on this slide (6-10 sentences).
25
+ ═══ DESIGN SYSTEM ═══
26
+ • primary_color: Deep main color for headers and key elements
27
+ • accent_color: Vibrant contrast color for highlights and CTAs
28
+ • background_color: Page background (near white, near black, or subtle tint)
29
+ • text_color: Main text color (must contrast with background)
30
+ • accent_light: Light tint for subtle section backgrounds
31
+ • gradient_end: Paired gradient color for primary
32
+ • font_title / font_body: System fonts only (Segoe UI, Arial, Georgia, Calibri, Malgun Gothic)
33
+ mood: One of modern-minimal, bold-energetic, corporate-elegant, warm-friendly, academic-clean
40
34
 
41
35
  ═══ COLOR PALETTE — CREATIVE PSYCHOLOGY ═══
42
- NEVER use placeholder/example colors. EVERY presentation must have a UNIQUE palette.
43
- Choose colors that evoke the RIGHT emotional response for the topic:
44
- Medical/Health: blues + greens (#0B5394, #27AE60) — trust, healing, precision
45
- Tech/AI/Startup: purples + cyans (#6C63FF, #00BCD4) innovation, future, energy
46
- • Finance/Investment: navy + gold (#1A237E, #F4A300) — authority, wealth, stability
47
- • Education/Training: teal + orange (#00796B, #FF7043) — growth, warmth, curiosity
48
- • Marketing/Creative: coral + purple (#FF6B6B, #7C3AED) — boldness, creativity, passion
49
- • Environment/Sustainability: deep green + amber (#1B5E20, #FFA000) — nature, urgency
50
- • Government/Policy: dark blue + red (#1A237E, #C62828) — authority, importance
36
+ ⚠ EVERY presentation must have a UNIQUE palette matching the topic's emotional tone.
37
+ Think about color psychology: trust (blues), growth (greens), urgency (reds/oranges),
38
+ innovation (purples/cyans), warmth (corals/browns), elegance (charcoals/rose), wealth (navy/gold).
39
+ Pick DEEP saturated colors for primary, VIBRANT contrasting for accent.
51
40
  ⚠ Choose colors that MATCH the specific topic. Generic blue = LAZY.
52
41
  ⚠ Ensure accent_color has HIGH contrast with primary_color.
53
- ⚠ If background is dark: text_color must be light (#E8E8E8+). If light: text_color must be dark (#1A1A2E-).
54
-
55
- ═══ MOOD VISUAL GUIDE ═══
56
- • modern-minimal: Clean lines, subtle shadows, generous whitespace, thin borders, smooth radius (12-16px)
57
- • bold-energetic: Strong gradients, deep shadows, thick accent bars, high contrast, punchy colors
58
- • corporate-elegant: Refined spacing, subtle gradients, structured grids, sophisticated feel
59
- • warm-friendly: Rounded corners (20px+), soft shadows, inviting spacing, gentle transitions
60
- • academic-clean: Tight grids, minimal decoration, clear hierarchy, data-first design
61
-
62
- ═══ SLIDE STRUCTURE RULES ═══
63
- ⚠ First slide MUST be type "title"
64
- ⚠ Last slide MUST be type "closing" with "감사합니다"/"Thank You" as title
42
+
43
+ ═══ SLIDE STRUCTURE ═══
44
+ First slide MUST be type "title". Last slide MUST be type "closing".
65
45
  ⚠ Minimum 10, maximum 13 slides total. Aim for 10-12.
66
- FEWER, HIGHER-QUALITY slides > many mediocre slides. 12 excellent slides beats 15 mixed ones.
67
- ⚠ Current year: \${new Date().getFullYear()}. For dates/timelines/roadmaps with no explicit year, use \${new Date().getFullYear()} or later. If the user specifies a year, USE THAT YEAR.
68
-
69
- ═══ TITLE & CLOSING SLIDE FORMAT ═══
70
- Title slide (FIRST):
71
- title: Company/topic name ONLY max 3-4 words, rendered at 96px by system
72
- GOOD: "MediAI" ✓ GOOD: "삼성전자 AI센터"
73
- BAD: "MediAI - 인공지능으로 의료 혁신을 선도합니다" (too long, wraps at 96px)
74
- content_direction: The actual subtitle/tagline TEXT. 1-2 lines, under 120 chars.
75
- GOOD: "AI 기반 의료 진단 혁신 플랫폼 — 투자유치 발표자료"
76
- ✗ BAD: "회사 로고, 슬로건, 연락처 정보" (instruction, not subtitle)
77
- Do NOT include visual/layout instructions — title uses a fixed premium template
78
- Closing slide (LAST):
79
- title: "감사합니다" (Korean) or "Thank You" (English)
80
- content_direction: Same company/topic name as title slide
81
- Do NOT include visual/layout instructions closing uses a fixed premium template
82
-
83
- ═══ TEMPLATES ═══
84
- PITCH DECK (startup/사업계획서/피치덱): 12 slides
85
- title problem solution market size product(1) product(2) business model → competition → traction → team → financials → closing
86
-
87
- BUSINESS REPORT (보고서/분석/실적): 10 slides
88
- title executive summary key metrics analysis comparison spotlight → breakdown → action plan → recommendations → closing
89
-
90
- TRAINING (교육/세미나/강의): 10 slides
91
- title objectives core concepts → key data process details case study → summary → key takeaway → closing
92
-
93
- PRODUCT LAUNCH (제품/출시/소개): 11 slides
94
- title market need overview key feature → features → metrics → use cases → pricing → timeline → CTA → closing
95
-
96
- GENERAL (발표/프레젠테이션): 10 slides
97
- title overview key points data detail spotlight comparison process summary closing
98
-
99
- ═══ THE #1 RULE ONE VISUAL APPROACH PER SLIDE ═══
100
- ⚠⚠⚠ Each slide's content_direction MUST describe EXACTLY ONE visual layout.
101
- The HTML renderer creates ONE visual element per slide — no more.
102
- If a topic needs 2 visual elements, SPLIT IT INTO 2 SLIDES.
103
-
104
- GOOD content_direction (ONE visual each):
105
- "총매출 1,250억원(전년비 15%↑), 영업이익 180억원(14.4%), 순이익 120억원. Layout: bar chart"
106
- "Comparison table: 4 rows × 3 columns. Highlight MediAI row. Layout: comparison table"
107
- "$45B 시장 규모, 28% 연평균 성장률, 5% 1년 점유율 목표. Layout: big numbers"
108
- "데이터 수집 전처리 AI 분석 결과 리포트 → 의사 확인. Layout: process flow"
109
- "매출 구성: 구독형 45%, 라이선스 30%, 컨설팅 25%. Layout: donut chart"
110
- "목표 달성률: 매출 92%, 고객 확보 87%, 시장점유율 73%, 만족도 95%. Layout: progress bars"
111
-
112
- BAD content_direction (MULTIPLE visuals = OVERFLOW):
113
- ✗ "테이블로 수익 모델 + 수익 그래프 + 파트너십 + 라이선싱" ← 4 sections!
114
- ✗ "원형 차트 + 투자 조건 테이블 + 단계적 계획 + 바 차트" ← 4 sections!
115
-
116
- ═══ SPLITTING DENSE TOPICS INTO MULTIPLE SLIDES ═══
117
- ⚠ If a user topic has 4+ sub-items, you MUST split it into 2 slides:
118
- • "비즈니스모델" → "수익 모델" (pricing table) + "수익 성장 전망" (bar chart)
119
- • "시장분석" → "시장 규모" (3 big metrics) + "고객 세분화" (pie chart or cards)
120
- • "투자조건" → "투자 조건" (table) + "자금 사용 계획" (pie or bar chart)
121
- • "경쟁우위" → "경쟁사 비교" (comparison table) + "핵심 차별화" (3 card grid)
122
- • "경쟁분석" → "경쟁사 비교" (comparison table ONLY) + "SWOT 분석" (2×2 grid ONLY)
123
- • "AI 솔루션" → "핵심 기능" (3 card grid) + "진단 프로세스" (process flow)
124
- • "제품/플랫폼" → "핵심 기능" (3 card grid with feature descriptions)
125
- • "마케팅/GTM 전략" → "마케팅 채널 전략" (3 cards) + "성과 지표" (metrics or table)
126
- • "로드맵" → "단기 로드맵" (3 milestone cards) + "장기 비전" (3 milestone cards)
127
- • "팀 소개" (4+ members) → "창업진 소개" (2-3 people) + "핵심 팀원" (2-3 people)
128
-
129
- ═══ CONTENT DENSITY — FILL THE SLIDE ═══
130
- ⚠⚠⚠ Each slide is 1920×1080px. Content MUST fill 85-95% of vertical space. EMPTY slides = FAILURE.
131
- ⚠ content_direction MUST contain 6-10 sentences of ACTUAL DATA and details.
132
- ⚠ Each item: title + 3-5 detailed bullets with specific numbers, names, and descriptions.
133
- ⚠ Tables: 4-5 data rows × 3-4 columns for rich comparison. Fill EVERY cell with real data. Column headers MUST be descriptive — NEVER generic "Column 1".
134
- ⚠ Timeline/Roadmap: 3 milestones (MAX 3) with dates, descriptions, and key metrics.
135
- ⚠ Process/Flow: 3-4 steps (MAX 4) — each step with short title + concise description.
136
- ⚠ Data/metrics: 3-4 big numbers with trend indicators AND supporting context text below each.
137
- ⚠ Cards: 3-4 cards, each with a title + 3-4 bullet points + a summary sentence.
138
- ⚠ Donut/Pie charts: include 4-5 segments with labels + a summary box below the chart.
139
- ⚠ The more SPECIFIC DATA you include in content_direction, the better the slide will look.
140
-
141
- ═══ content_direction QUALITY RULES ═══
142
- ⚠ content_direction = REAL CONTENT the viewer will read. NOT layout instructions.
143
- Include: specific numbers, names, descriptions, bullet point text, table data.
144
- MUST be DETAILED — 6-10 sentences with specific data. Sparse directions produce empty slides.
145
- Each item/card/row MUST include: title + 3-4 supporting details with specific data.
146
- Optionally end with ONE short layout hint (e.g., "Layout: 3-column cards" or "Layout: comparison table").
147
-
148
- ✓ GOOD: "총매출 1,250억원(전년비 15%↑), 영업이익 180억원(14.4%), 순이익 95억원(7.6%). 사업부별: 클라우드 45%(562억), AI솔루션 30%(375억), 컨설팅 25%(312억). 클라우드가 전년비 32% 성장하며 최대 성장 동력. Layout: bar chart"
149
- ✓ GOOD: "3 core services: (1) 클라우드 인프라 — 하이브리드 클라우드 구축, AWS/Azure 호환, 99.9% SLA 보장, 50+ 기업 고객 (2) AI 솔루션 — 자연어처리/영상분석, 진단 정확도 98.2%, 월 100만건 처리 (3) 컨설팅 — 디지털 전환 자문, 평균 ROI 250%, 12개월 효과. Layout: 3-column cards"
150
- ✗ BAD: "클라우드, AI, 컨설팅 3개 서비스" ← Too sparse!
151
- ✗ BAD: "왼쪽에 개요 텍스트, 오른쪽에 카드" ← Layout instruction, NOT content!
152
- ✗ BAD: "#accent_light 배경, #primary 글씨" ← CSS instruction, NOT content!
46
+ Current year: \${new Date().getFullYear()}.
47
+
48
+ ═══ TITLE & CLOSING FORMAT ═══
49
+ Title slide:
50
+ title: Company/topic name ONLY — max 3-4 words (rendered at 96px)
51
+ "Acme Corp" ✓ "프로젝트 알파" ✗ "Acme Corp - 2025 혁신 전략 보고서" (too long)
52
+ content_direction: The actual subtitle/tagline TEXT (1-2 lines, under 120 chars)
53
+ Closing slide:
54
+ title: "감사합니다" (Korean) / "Thank You" (English)
55
+ content_direction: Company/topic name
56
+
57
+ ═══ SLIDE STRUCTURE STRATEGY ═══
58
+ Do NOT follow a fixed template. Instead, analyze the user's topic and determine the most logical slide flow yourself.
59
+ Think about what information the audience NEEDS and in what ORDER.
60
+ General principles:
61
+ - Start with context/background before diving into details
62
+ - Build a narrative arc: setup → evidence → insight → action
63
+ - Each slide should have a DISTINCT purpose — no redundant slides
64
+ - End with actionable conclusions or key takeaways before closing
65
+ The slide structure should feel CUSTOM-TAILORED to the specific topic, not a generic template.
66
+
67
+ ═══ content_direction = THE #1 PRIORITY ═══
68
+ ⚠⚠⚠ content_direction is the REAL DATA AND TEXT that will appear on the slide.
69
+ ⚠ Each content_direction MUST be 6-10 sentences of SPECIFIC DATA:
70
+ - Include: numbers, percentages, names, descriptions, dates, comparisons
71
+ - The MORE specific data you provide, the better the slide will look
72
+ - Each item/section: title + 3-4 supporting details with real numbers
73
+ content_direction MUST describe ONE focused topic per slide.
74
+ If a topic has 4+ sub-items, SPLIT into 2 slides.
75
+ ⚠ NEVER include layout/CSS instructions in content_direction. Just the DATA.
76
+ GOOD: "2024년 국내 시장 규모 4.8조 원. 전년 대비 23% 성장. 주요 성장 동력: 신규 고객 유입 35% 증가, 기존 고객 유지율 89%, 프리미엄 세그먼트 매출 비중 41%. 2027년 시장 전망 7.2조 원."
77
+ GOOD: "핵심 제품 A: 월간 구독형 서비스. 기본 플랜 29만원, 프로 플랜 59만원, 엔터프라이즈 맞춤 견적. 주요 기능: 실시간 분석 대시보드, 자동 보고서 생성, API 연동. 도입 기업 120개사, 평균 고객 만족도 94.2%."
78
+ ✗ BAD: "3개 서비스 소개. Layout: cards" ← Too sparse, has layout hint!
79
+ BAD: "왼쪽에 텍스트, 오른쪽에 차트" Layout instruction!
80
+
81
+ ═══ SLIDE CONTENT VARIETY ═══
82
+ Each slide's content should naturally suggest a DIFFERENT visual treatment.
83
+ Vary what you write about across slides:
84
+ - Some slides: key metrics/numbers (naturally displayed as large metric spotlights)
85
+ - Some slides: comparison data (naturally shown as tables or side-by-side)
86
+ - Some slides: step-by-step processes (naturally shown as flows)
87
+ - Some slides: category breakdowns (naturally shown as charts or card grids)
88
+ - Some slides: timeline/roadmap items (naturally shown as milestone sequences)
89
+ - Some slides: detailed feature descriptions (naturally shown as rich cards)
90
+ Don't make every slide a list of items. Mix data-heavy slides with narrative slides.
91
+ ⚠ AVOID having 3+ consecutive slides with the same content structure (e.g., all lists of 3-4 items).
153
92
 
154
93
  ═══ OVERVIEW / AGENDA SLIDES ═══
155
- Overview/agenda slides: MAXIMUM 5 items. Numbered list (1-5) or compact 2-column grid.
156
- Each item: short title (2-3 words) + 1-line description only.
157
- If 10+ sections, group related topics: "시장 경쟁" instead of separate "시장 분석" + "경쟁 분석".
158
-
159
- ═══ LAYOUT VARIETY — MANDATORY ═══
160
- ⚠⚠⚠ You MUST vary the visual approach. No more than 2 slides with the same layout hint.
161
- Available layouts (use at least 5 different types across the presentation):
162
- - "2×2 grid": 4 cards in 2 rows × 2 columns (MAX 2 slides)
163
- - "bar chart": CSS vertical/horizontal bars showing data trends
164
- - "donut chart": conic-gradient pie/donut with metric labels
165
- - "comparison table": styled table with 3-4 rows, colored headers
166
- - "process flow": horizontal step boxes connected by arrows (→)
167
- - "big numbers": 2-3 large metric spotlights (72-96px) with trend indicators
168
- - "2-column split": left panel + right content
169
- - "timeline": 3-4 milestone steps horizontal with dates
170
- - "progress bars": horizontal bars showing completion percentages
171
- - "hero stat": one large central metric with supporting context
172
- ⚠ Each content_direction MUST end with a "Layout:" hint specifying the visual type.
173
- ⚠ If your plan has 3+ slides all using "cards/grid" layout, REWRITE to use charts, tables, flows instead.
174
- ⚠ A 10-slide deck MUST have: at least 2 chart/data-viz slides, at least 1 table slide, at least 1 process/flow slide.
175
- ⚠ NEVER plan a presentation where every slide is cards — that is LAZY and BORING.
94
+ AVOID overview/agenda/TOC slides they waste space and add no real content.
95
+ Instead, jump straight into substantive content after the title slide.
96
+ If absolutely needed: MAXIMUM 5 items with short titles only.
176
97
 
177
98
  ═══ HARD RULES ═══
178
- ⚠ First slide MUST be type "title"
179
- ⚠ Last slide MUST be type "closing" with "감사합니다"/"Thank You" as title
180
99
  ⚠ ALL titles and content_direction MUST be in the SAME language as the user's instruction
181
- ⚠ content_direction: MUST contain ACTUAL TEXT/DATA. Layout hint is optional at the END.
182
- content_direction that is ONLY layout/visual instructions (no real data) = FAILURE.
183
- HARD MAXIMUM: 13 slides total. Slides beyond 13 are DISCARDED by the system.
184
- Each slide's content_direction must be unique and substantive no generic placeholders
185
- ⚠ NEVER use "스크린샷", "screenshot", "이미지", "사진", "placeholder", "[내용]" in content_direction
186
- Do NOT create a separate "연락처"/"Contact" slide — the closing slide already handles this.
187
-
188
- Output ONLY the JSON object. No preamble, no markdown fences, no explanation.`;
100
+ ⚠ content_direction with NO real data = FAILURE
101
+ HARD MAXIMUM: 13 slides. Slides beyond 13 are DISCARDED.
102
+ NEVER use "스크린샷", "screenshot", "이미지", "사진", "placeholder" in content_direction
103
+ Do NOT create a separate "연락처" slideclosing handles this.
104
+
105
+ Output ONLY the JSON object.`;
189
106
  export function extractLayoutHint(contentDirection) {
190
107
  const match = contentDirection.match(/Layout:\s*(.+?)$/im);
191
108
  if (!match)
@@ -324,8 +241,8 @@ ${layoutCss}
324
241
  • Complete HTML: <!DOCTYPE html> through </html>. ALL styling in <style>.
325
242
  • NO <img>, NO external resources, NO JavaScript, NO external fonts.
326
243
  • System fonts ONLY. CSS for visuals: gradients, shapes, shadows, borders.
327
- • Title: 48-64px bold. Body: 26-32px. MINIMUM any text: 26px. Reduce items instead of shrinking.
328
- • Content fills 85-95% of 1080px height. Empty space = FAILURE.
244
+ • Title: 42-48px bold. Body: 26-32px. MINIMUM any text: 24px. If text needs to shrink below 24px, remove content instead.
245
+ • Content fills 85-95% of 1080px height. Empty space = FAILURE. MAX ~1000 chars of visible text.
329
246
  • NEVER use justify-content:center on .content (creates dead space). Use stretch/space-evenly.
330
247
  • Use gradients, box-shadow (0 4px 20px rgba(0,0,0,0.06)), border-radius (12-20px).
331
248
  • Generate REAL professional content. No placeholders. Specific numbers and data.
@@ -334,27 +251,120 @@ ${layoutCss}
334
251
 
335
252
  Output the complete HTML now.`;
336
253
  }
337
- export function validateSlideHtml(html, layoutType) {
254
+ export function buildFreeHtmlPrompt(title, contentDirection, design, slideIndex, totalSlides, language) {
255
+ const langRule = language === 'ko'
256
+ ? 'ALL visible text MUST be in Korean. Write naturally in Korean. Never use Chinese characters (漢字).'
257
+ : 'ALL visible text MUST be in English.';
258
+ const variants = [
259
+ `Background: ${design.background_color}. Elements use white background with box-shadow.`,
260
+ `Top accent bar (height:4px, linear-gradient(90deg, ${design.accent_color}, ${design.primary_color})). Background: ${design.background_color}.`,
261
+ `Subtle gradient background: linear-gradient(150deg, ${design.background_color} 0%, ${design.accent_light}40 100%). Stronger shadows.`,
262
+ ];
263
+ const styleGuide = variants[slideIndex % variants.length];
264
+ return `You are a world-class presentation designer. Create ONE slide as a complete HTML page.
265
+ Output ONLY the complete HTML (<!DOCTYPE html> to </html>). No explanation, no markdown fences.
266
+
267
+ ═══ SLIDE ${slideIndex + 1} OF ${totalSlides} ═══
268
+ Title: "${title}"
269
+
270
+ ═══ CONTENT TO VISUALIZE ═══
271
+ ${contentDirection}
272
+
273
+ ═══ DESIGN SYSTEM ═══
274
+ Primary: ${design.primary_color} | Accent: ${design.accent_color} | BG: ${design.background_color}
275
+ Text: ${design.text_color} | Light: ${design.accent_light} | Gradient: ${design.gradient_end}
276
+ Title Font: ${design.font_title} | Body Font: ${design.font_body} | Mood: ${design.mood}
277
+
278
+ ═══ STYLE FOR THIS SLIDE ═══
279
+ ${styleGuide}
280
+
281
+ ═══ YOUR CREATIVE MISSION ═══
282
+ Design the BEST possible visual layout for THIS specific content.
283
+ Think like a professional designer: what visual structure best communicates this information?
284
+
285
+ You have FULL CREATIVE FREEDOM. Choose the most appropriate visual approach:
286
+ • Card grids (2×2, 1×3, 1×4) with icons, stats, bullet details
287
+ • Styled data tables (<table>) with colored headers and alternating rows
288
+ • CSS bar charts: vertical bars using flex-end alignment + height percentages
289
+ • Donut/pie charts: conic-gradient on border-radius:50% elements
290
+ • Horizontal progress bars with labeled tracks and percentage fills
291
+ • Step-by-step process flows with numbered circles + arrow (→) connectors
292
+ • Timeline layouts with dated milestone cards in a horizontal row
293
+ • Dashboard panels: 2-3 large metric spotlights (80-100px numbers)
294
+ • 2-column split: left summary/data + right detailed content or bullets
295
+ • Feature showcases with emoji/icon badges
296
+ • Itinerary/schedule: day-by-day breakdown with locations and activities
297
+ • Ranking/leaderboard: ordered items with visual indicators
298
+ • Comparison matrices: side-by-side analysis with visual scoring
299
+ • SWOT or quadrant grids for strategic analysis
300
+ • Package/pricing comparison: side-by-side product cards with highlights
301
+ • Any OTHER CSS-only visual that serves the content perfectly
302
+
303
+ ⚠ Choose the layout that BEST fits THIS content. Match layout to content type: schedules for timelines, charts for financial data, comparison matrices for competitive analysis, card grids for feature showcases. Be creative and appropriate.
304
+
305
+ ═══ MANDATORY CSS BOILERPLATE (copy into <style>) ═══
306
+ * { margin:0; padding:0; box-sizing:border-box; }
307
+ html, body { width:1920px; height:1080px; overflow:hidden; font-family:"${design.font_body}","${design.font_title}","Segoe UI","Malgun Gothic",Arial,sans-serif; word-break:keep-all; overflow-wrap:break-word; }
308
+ body { display:flex; flex-direction:column; padding:60px 80px; height:1080px; background:${design.background_color}; color:${design.text_color}; font-size:26px; }
309
+
310
+ ═══ HTML STRUCTURE ═══
311
+ body has EXACTLY 2 direct children:
312
+ 1. .slide-title (flex:0 0 auto) — h1 with title + accent bar
313
+ .slide-title h1 { font-size:48px; font-weight:700; color:${design.primary_color}; font-family:"${design.font_title}","Segoe UI",sans-serif; }
314
+ Below h1: <div style="width:80px;height:3px;background:${design.accent_color};border-radius:2px;margin-top:12px"></div>
315
+ 2. .content (flex:1) — stretches to fill ALL remaining vertical space
316
+ ⚠ .content class name is REQUIRED.
317
+ ⚠ ALL layout elements MUST be DIRECT children of .content — no wrapper divs.
318
+
319
+ ═══ DESIGN RULES ═══
320
+ • ${langRule}
321
+ • Complete HTML: <!DOCTYPE html> through </html>. ALL styling in <style>.
322
+ • NO <img>, NO external resources, NO JavaScript, NO external fonts.
323
+ • System fonts ONLY. CSS for visuals: gradients, shapes, shadows, borders.
324
+ • Title: 42-48px bold. Body: 28-32px. MINIMUM any visible text: 24px (except page numbers at 12px).
325
+ • ⚠ Korean text is WIDER and DENSER than English — use FEWER items with LARGER fonts. Each card/element: MAX 2-3 short lines.
326
+ • ⚠ If you need to shrink text below 24px, you have TOO MUCH content. Remove sections or shorten text instead.
327
+ • ⚠ ABSOLUTELY NO OVERFLOW — content MUST fit within 1080px total height. It is BETTER to have 20% empty space than 1px of clipping. Limit yourself to 3-4 major content elements max.
328
+ • The available height for .content is approximately 900px (1080 - 60px top padding - 60px bottom padding - ~60px title). Design within this constraint.
329
+ • ⚠ MAXIMUM visible text: ~800 characters. More than this WILL cause overflow. Be concise — short labels, brief bullet points (max 8-10 words each).
330
+ • NEVER use position:absolute for layout (ok for page numbers). Use flexbox/grid.
331
+ • NEVER use justify-content:center on .content — it creates dead space. Use stretch/space-evenly.
332
+ • Use gradients, box-shadow (0 4px 20px rgba(0,0,0,0.06)), border-radius (12-20px).
333
+ • Cards/elements: white (#fff) background with subtle shadow on light slides.
334
+ • Table headers: dark background (${design.primary_color}) with white text.
335
+ • Generate REAL professional content from the direction. No placeholders.
336
+ • ⚠ NEVER use bracket placeholders like [Team Name], [Email], [YYYY], [Author]. Instead, INVENT realistic fictional content for ALL fields — names, emails, dates, numbers, etc.
337
+ • If user specified a year, USE THAT YEAR. Default to ${new Date().getFullYear()} only when no year given.
338
+ • Page number: bottom-right "${slideIndex + 1}" (12px, opacity 0.4, position:absolute ok for this).
339
+
340
+ Output the complete HTML now.`;
341
+ }
342
+ export function validateSlideHtml(html, _layoutType) {
338
343
  if (!html.includes('<!DOCTYPE') && !html.includes('<!doctype')) {
339
344
  return { pass: false, feedback: 'Missing <!DOCTYPE html> declaration. Start with <!DOCTYPE html><html>.' };
340
345
  }
341
346
  if (!html.includes('<html') || !html.includes('</html>')) {
342
347
  return { pass: false, feedback: 'Missing <html> or </html> tags. Output must be a complete HTML document.' };
343
348
  }
344
- const layoutFeedback = checkLayoutCompliance(html, layoutType);
345
- if (layoutFeedback) {
346
- return { pass: false, feedback: layoutFeedback };
347
- }
348
349
  if (/<img\s/i.test(html)) {
349
350
  return { pass: false, feedback: 'Forbidden: <img> tags are not allowed. Use CSS gradients, shapes, and backgrounds instead.' };
350
351
  }
351
- if (/position\s*:\s*absolute/i.test(html)) {
352
- return { pass: false, feedback: 'Forbidden: position:absolute is not allowed. Use flexbox or grid for all layout.' };
352
+ if (/<script[\s>]/i.test(html)) {
353
+ return { pass: false, feedback: 'Forbidden: <script> tags are not allowed. This is a static slide no JavaScript.' };
354
+ }
355
+ const absCount = (html.match(/position\s*:\s*absolute/gi) || []).length;
356
+ if (absCount > 6) {
357
+ return { pass: false, feedback: `Too many position:absolute (${absCount}). Use flexbox/grid for main layout.` };
358
+ }
359
+ const externalUrls = html.match(/url\(\s*['"]?https?:\/\//gi) || [];
360
+ if (externalUrls.length > 0) {
361
+ return { pass: false, feedback: 'Forbidden: External URLs detected in CSS url(). No external resources allowed.' };
353
362
  }
354
- if (/https?:\/\/(?!.*$)/i.test(html) && !/https?:\/\/[^"')\s]*\.(nip\.io|localhost)/i.test(html)) {
355
- const externalUrls = html.match(/url\(\s*['"]?https?:\/\//gi) || [];
356
- if (externalUrls.length > 0) {
357
- return { pass: false, feedback: 'Forbidden: External URLs detected in CSS url(). No external resources allowed.' };
363
+ const scaleMatches = html.match(/transform\s*:[^;]*scale\(\s*([\d.]+)/gi) || [];
364
+ for (const m of scaleMatches) {
365
+ const val = parseFloat(m.replace(/.*scale\(\s*/i, ''));
366
+ if (val > 0 && val < 0.9) {
367
+ return { pass: false, feedback: `Forbidden: transform:scale(${val}) shrinks content. Use full 1920×1080 layout without scaling.` };
358
368
  }
359
369
  }
360
370
  const placeholderPatterns = [
@@ -364,33 +374,40 @@ export function validateSlideHtml(html, layoutType) {
364
374
  /Display value \(e\.g\./i,
365
375
  /1-2 sentence key insight/i,
366
376
  /Category name/i,
367
- /Another detail/i,
368
- /Third point/i,
369
- /Fourth point/i,
370
- /Brief context/i,
371
377
  /Segment name/i,
378
+ /Lorem ipsum/i,
379
+ /\[placeholder\]/i,
380
+ /\[내용\]/i,
381
+ /\[.{2,20}을\s*입력/i,
382
+ /\[YYYY/i,
383
+ /\[이메일/i,
384
+ /\[작성자/i,
385
+ /\[직급/i,
386
+ /\[NNNN\]/i,
387
+ /\[MM월/i,
388
+ /\[담당자\s*명\]/i,
372
389
  ];
373
390
  for (const pattern of placeholderPatterns) {
374
391
  if (pattern.test(html)) {
375
- return { pass: false, feedback: `Placeholder text detected: "${pattern.source}". Generate REAL content, not schema examples.` };
392
+ return { pass: false, feedback: `Placeholder text detected: "${pattern.source}". Generate REAL content.` };
376
393
  }
377
394
  }
378
395
  const textElements = (html.match(/<(p|li|td|th|span|div|h[1-6])[^>]*>[^<]{2,}/gi) || []).length;
379
396
  if (textElements < 5) {
380
- return { pass: false, feedback: `Low content density: only ${textElements} text elements found. Need at least 5 text-bearing elements for a complete slide.` };
397
+ return { pass: false, feedback: `Low content density: only ${textElements} text elements. Need at least 5.` };
381
398
  }
382
399
  const visibleText = html.replace(/<style[\s\S]*?<\/style>/gi, '')
383
400
  .replace(/<[^>]+>/g, ' ')
384
401
  .replace(/\s+/g, ' ')
385
402
  .trim();
386
- if (visibleText.length > 3000) {
387
- return { pass: false, feedback: `Content overflow risk: ${visibleText.length} chars of visible text. Reduce to under 3000 chars to prevent overflow on 1920×1080.` };
403
+ if (visibleText.length > 1200) {
404
+ return { pass: false, feedback: `Content overflow risk: ${visibleText.length} chars of visible text. Reduce to under 1200 chars. Remove 1-2 sections or shorten text to prevent bottom clipping.` };
388
405
  }
389
406
  const smallFonts = html.match(/font-size\s*:\s*(\d+)px/gi) || [];
390
407
  for (const match of smallFonts) {
391
408
  const size = parseInt(match.replace(/[^0-9]/g, ''), 10);
392
- if (size > 0 && size < 18 && size !== 12) {
393
- return { pass: false, feedback: `Font too small: found font-size:${size}px. Minimum allowed is 18px (except 12px for page numbers). Increase font size or reduce content.` };
409
+ if (size > 0 && size < 24 && size > 13) {
410
+ return { pass: false, feedback: `Font too small: found font-size:${size}px. Minimum allowed is 24px (except 12-13px for page numbers). Increase font size or reduce content.` };
394
411
  }
395
412
  }
396
413
  return { pass: true, feedback: '' };
@@ -480,7 +497,7 @@ MANDATORY STRUCTURE (body has EXACTLY 2 direct children):
480
497
  ${getLayoutSpecificCss(layoutType, design)}
481
498
 
482
499
  ═══ DESIGN RULES ═══
483
- 1. Title: 48-64px bold. Body: 26-32px. MINIMUM any visible text: 26px. NEVER use font-size below 26px for any text element. If content doesn't fit at 26px, REDUCE ITEM COUNT instead of shrinking text. Card descriptions: 26-30px. Card titles: 32-40px. Labels/captions: 26px minimum. body { font-size: 26px; } is MANDATORY.
500
+ 1. Title: 42-48px bold. Body: 26-32px. MINIMUM any visible text: 24px. NEVER use font-size below 24px. If content doesn't fit at 24px, REDUCE ITEM COUNT instead of shrinking text. Card descriptions: 26-30px. Card titles: 32-40px. Labels/captions: 24px minimum. body { font-size: 26px; } is MANDATORY. MAX visible text ~1000 characters.
484
501
  2. Use gradients, box-shadow (0 4px 20px rgba(0,0,0,0.06)), border-radius (12-20px)
485
502
  3. Follow the REQUIRED LAYOUT specified above exactly. Do NOT substitute a different layout type.
486
503
  4. LAYOUT LIMITS: MAX 4 cards (2×2 grid), MAX 2 cards per row. Tables: 5-8 rows for rich data. Timeline: 4-5 steps.
@@ -531,64 +548,64 @@ export function buildContentFillJsonPrompt(slideTitle, contentDirection, layoutT
531
548
  const schemas = {
532
549
  cards: `{
533
550
  "cards": [
534
- { "icon": "single emoji", "title": "Card title (2-5 words)", "bullets": ["Detail with number/data", "Another detail", "Third point", "Fourth point"], "stat": "Key metric (e.g., 97.8% 정확도)" }
551
+ { "icon": "single emoji", "title": "Short card title (2-5 words)", "bullets": ["Detail with data", "Another detail", "Third point"], "stat": "Key metric with number" }
535
552
  ]
536
553
  }
537
- RULES: 3-4 cards. Each: icon + title + 4-5 bullets with specific data + stat.`,
554
+ RULES: 3-4 cards. Each: icon + title + 3 bullets (MAX 3, keep each under 25 chars) + stat.`,
538
555
  bar_chart: `{
539
556
  "bars": [
540
- { "label": "Category name", "value": "Display value (e.g. 1,250억원)", "height": 85 }
557
+ { "label": "Category name (under 15 chars)", "value": "Number with unit", "height": 85 }
541
558
  ],
542
559
  "insight": "1-2 sentence key insight about the data"
543
560
  }
544
- RULES: 4-6 bars. height: 10-90 (tallest=85, others proportional). Values with units.`,
561
+ RULES: 4-5 bars (MAX 5). height: 10-90 (tallest=85, others proportional). Values must include units.`,
545
562
  donut_chart: `{
546
563
  "segments": [
547
- { "label": "Segment name", "value": "Display value (e.g. 562억원)", "percent": 45 }
564
+ { "label": "Segment name", "value": "Number with unit", "percent": 45 }
548
565
  ],
549
- "centerText": "Center label (e.g. 총 1,250억원)",
566
+ "centerText": "Total or summary label",
550
567
  "summary": "1-2 sentence summary"
551
568
  }
552
569
  RULES: 3-5 segments. Percents MUST sum to exactly 100.`,
553
570
  table: `{
554
- "headers": ["Company Name", "Market Share", "Key Strength", "Accuracy"],
555
- "rows": [["data", "data", "data", "data"]],
571
+ "headers": ["Descriptive column name", "Another column", "Third column", "Fourth column"],
572
+ "rows": [["real data", "real data", "real data", "real data"]],
556
573
  "highlightRow": 0,
557
574
  "summary": "1-2 sentence summary"
558
575
  }
559
- RULES: 3-4 columns. 4-5 rows. ALL cells real data. highlightRow: 0-indexed or null.
560
- ⚠ headers MUST be meaningful column names describing the data — NEVER use generic "Column 1", "Column 2" etc.`,
576
+ RULES: 3-4 columns. 4-5 rows. ALL cells must contain real data. highlightRow: 0-indexed or null.
577
+ ⚠ headers MUST be meaningful column names — NEVER use generic "Column 1", "Column 2".`,
561
578
  process_flow: `{
562
579
  "steps": [
563
- { "title": "Step name (2-4 words)", "desc": "2-3 sentence description with details", "detail": "Duration or metric" }
580
+ { "title": "Step name (2-4 words)", "desc": "2-3 sentence description with specific details", "detail": "Duration or key metric" }
564
581
  ]
565
582
  }
566
583
  RULES: 3-4 steps (MAX 4). Each: title + detailed description + time/metric.`,
567
584
  big_numbers: `{
568
585
  "metrics": [
569
- { "value": "1,250", "unit": "억원", "label": "Metric Name", "desc": "1-2 sentence context", "trend": "▲ 15%", "positive": true }
586
+ { "value": "Number only", "unit": "Unit text", "label": "Metric name", "desc": "1-2 sentence context", "trend": "▲ or ▼ + percentage", "positive": true }
570
587
  ]
571
588
  }
572
- RULES: 2-3 metrics. value=number only, unit=separate. trend: ▲/▼ + percentage.`,
589
+ RULES: 2-3 metrics. value=number only, unit=separate field. trend: ▲/▼ + percentage.`,
573
590
  timeline: `{
574
591
  "milestones": [
575
- { "date": "2026 Q1", "title": "Milestone name", "desc": "2-3 sentence description", "kpi": "Target metric" }
592
+ { "date": "Date or period", "title": "Milestone name", "desc": "2-3 sentence description", "kpi": "Target metric" }
576
593
  ]
577
594
  }
578
595
  RULES: 3 milestones (MAX 3). Each with date, description, and KPI target.`,
579
596
  progress_bars: `{
580
597
  "bars": [
581
- { "label": "Category name", "value": "Display (e.g. 92%)", "percent": 92, "detail": "Brief context" }
598
+ { "label": "Category name", "value": "Display value with unit", "percent": 75, "detail": "Brief context" }
582
599
  ]
583
600
  }
584
601
  RULES: 4-6 bars. percent: 5-100. Include context detail for each.`,
585
602
  hero_stat: `{
586
- "number": "97.8",
587
- "unit": "%",
588
- "label": "Metric Name",
603
+ "number": "The big number",
604
+ "unit": "Unit or symbol",
605
+ "label": "What this number measures",
589
606
  "context": "2-3 sentence context explaining significance",
590
607
  "supporting": [
591
- { "value": "45개국", "label": "Supporting metric name" }
608
+ { "value": "Number with unit", "label": "Supporting metric name" }
592
609
  ]
593
610
  }
594
611
  RULES: 1 hero number + context + 2-3 supporting metrics.`,
@@ -600,7 +617,7 @@ RULES: 1 hero number + context + 2-3 supporting metrics.`,
600
617
  "rightTitle": "Right column heading",
601
618
  "rightBullets": ["Detailed bullet point with data"]
602
619
  }
603
- RULES: Left: 3-4 key-value items. Right: 3-4 detailed bullets. Keep each bullet under 30 chars.`,
620
+ RULES: Left: 3-4 key-value items with REAL data. Right: 3-4 detailed bullets with REAL data. Keep each bullet under 30 chars.`,
604
621
  };
605
622
  return `Extract content from the direction below and output ONLY valid JSON.
606
623
  Do NOT output markdown fences, explanations, or anything besides the JSON object.
@@ -646,6 +663,19 @@ export function parseContentFillJson(raw, layoutType) {
646
663
  }
647
664
  if (!parsed)
648
665
  return null;
666
+ const jsonStr = JSON.stringify(parsed);
667
+ const ellipsisMatches = jsonStr.match(/"\.\.\."|\u2026/g) || [];
668
+ const totalStringValues = jsonStr.match(/"[^"]+"/g) || [];
669
+ if (ellipsisMatches.length > 0 && totalStringValues.length > 0) {
670
+ const ellipsisRatio = ellipsisMatches.length / totalStringValues.length;
671
+ if (ellipsisRatio > 0.3) {
672
+ return null;
673
+ }
674
+ }
675
+ const stringLiteralMatches = jsonStr.match(/"string"/g) || [];
676
+ if (stringLiteralMatches.length >= 2) {
677
+ return null;
678
+ }
649
679
  switch (layoutType) {
650
680
  case 'cards':
651
681
  if (!Array.isArray(parsed['cards']) || parsed['cards'].length === 0)
@@ -686,8 +716,15 @@ export function parseContentFillJson(raw, layoutType) {
686
716
  case 'two_col_split':
687
717
  if (!parsed['leftTitle'] && !parsed['rightTitle'])
688
718
  return null;
719
+ if (!Array.isArray(parsed['leftItems']) || parsed['leftItems'].length === 0)
720
+ return null;
721
+ if (!Array.isArray(parsed['rightBullets']) || parsed['rightBullets'].length === 0)
722
+ return null;
689
723
  break;
690
724
  }
725
+ const allText = JSON.stringify(parsed).replace(/[{}\[\]",:]/g, '').trim();
726
+ if (allText.length < 50)
727
+ return null;
691
728
  return parsed;
692
729
  }
693
730
  function getCardTextColor(design) {
@@ -738,21 +775,21 @@ function buildCardsContent(design, data) {
738
775
  const cols = n <= 3 ? `repeat(${n},1fr)` : '1fr 1fr';
739
776
  const is2x2 = n === 4;
740
777
  const gridExtra = is2x2 ? 'grid-template-rows:1fr 1fr' : 'grid-template-rows:1fr';
741
- const cardPad = is2x2 ? '28px 24px' : '36px 32px';
742
- const cardGap = is2x2 ? '10px' : '14px';
743
- const h2Size = is2x2 ? '32px' : '34px';
744
- const liSize = is2x2 ? '26px' : '28px';
745
- const liMargin = is2x2 ? '8px' : '12px';
778
+ const cardPad = is2x2 ? '28px 24px' : '48px 36px';
779
+ const cardGap = is2x2 ? '10px' : '16px';
780
+ const h2Size = is2x2 ? '32px' : '40px';
781
+ const liSize = is2x2 ? '26px' : '30px';
782
+ const liMargin = is2x2 ? '8px' : '14px';
746
783
  const cardText = getCardTextColor(design);
747
784
  const css = `.content{flex:1;display:grid;grid-template-columns:${cols};${gridExtra};gap:24px}
748
785
  .card{background:#fff;border-radius:16px;padding:${cardPad};box-shadow:0 4px 20px rgba(0,0,0,0.06);display:flex;flex-direction:column;gap:${cardGap};overflow:hidden;color:${cardText}}
749
786
  .card-icon{font-size:36px;line-height:1}
750
787
  .card h2{font-size:${h2Size};font-weight:700;color:${design.primary_color}}
751
- .card ul{list-style:none;flex:1}
788
+ .card ul{list-style:none;flex:1;display:flex;flex-direction:column;justify-content:center}
752
789
  .card li{margin-bottom:${liMargin};padding-left:22px;position:relative;font-size:${liSize};line-height:1.4}
753
790
  .card li::before{content:"•";color:${design.accent_color};position:absolute;left:0;font-weight:bold}
754
791
  .card-stat{margin-top:auto;padding-top:12px;border-top:2px solid ${design.accent_light};font-size:24px;font-weight:600;color:${design.accent_color}}`;
755
- const maxBullets = n <= 3 ? 5 : 3;
792
+ const maxBullets = 3;
756
793
  const cards = (data.cards || []).slice(0, 4).map(c => `
757
794
  <div class="card">
758
795
  <div class="card-icon">${c.icon || '📌'}</div>
@@ -773,7 +810,7 @@ function buildBarChartContent(design, data) {
773
810
  .bar-label{font-size:24px;font-weight:600;color:${design.text_color};text-align:center;min-width:120px;word-break:keep-all;padding:12px 0;flex-shrink:0}
774
811
  .insight-row{padding:20px 60px;background:${design.accent_light};border-radius:12px;margin-top:16px;font-size:26px;color:${cardText};line-height:1.5;flex-shrink:0}`;
775
812
  const maxH = Math.max(...(data.bars || []).map(b => b.height || 50), 1);
776
- const bars = (data.bars || []).slice(0, 6).map(b => {
813
+ const bars = (data.bars || []).slice(0, 5).map(b => {
777
814
  const normalized = Math.max(8, Math.round(((b.height || 50) / maxH) * 85));
778
815
  return `
779
816
  <div class="bar-group">
@@ -802,16 +839,16 @@ function buildDonutChartContent(design, data) {
802
839
  cumPct += pct;
803
840
  return `${segColors[i % segColors.length]} ${start.toFixed(1)}% ${cumPct.toFixed(1)}%`;
804
841
  }).join(',');
805
- const css = `.content{flex:1;display:grid;grid-template-columns:1.2fr 1fr;gap:40px;align-items:center;align-content:center}
842
+ const css = `.content{flex:1;display:grid;grid-template-columns:1.2fr 1fr;gap:40px;align-items:center;align-content:center;padding:0}
806
843
  .donut-wrap{display:flex;justify-content:center;align-items:center}
807
- .donut{width:500px;height:500px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 32px rgba(0,0,0,0.08)}
808
- .donut-hole{width:240px;height:240px;border-radius:50%;background:${design.background_color};display:flex;align-items:center;justify-content:center;flex-direction:column;gap:4px}
809
- .donut-center{font-size:30px;font-weight:800;color:${design.primary_color};text-align:center;line-height:1.3}
810
- .legend{display:flex;flex-direction:column;gap:28px}
811
- .legend-item{display:flex;align-items:center;gap:16px;font-size:28px;padding:12px 16px;background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,0.04);color:${getCardTextColor(design)}}
844
+ .donut{width:600px;height:600px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 32px rgba(0,0,0,0.08)}
845
+ .donut-hole{width:290px;height:290px;border-radius:50%;background:${design.background_color};display:flex;align-items:center;justify-content:center;flex-direction:column;gap:4px}
846
+ .donut-center{font-size:34px;font-weight:800;color:${design.primary_color};text-align:center;line-height:1.3}
847
+ .legend{display:flex;flex-direction:column;gap:24px}
848
+ .legend-item{display:flex;align-items:center;gap:16px;font-size:28px;padding:14px 18px;background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,0.04);color:${getCardTextColor(design)}}
812
849
  .legend-dot{width:24px;height:24px;border-radius:50%;flex-shrink:0}
813
850
  .legend-value{font-weight:700;color:${design.primary_color};margin-left:auto;white-space:nowrap}
814
- .chart-summary{grid-column:1/3;padding:16px 24px;background:${design.accent_light};border-radius:12px;font-size:26px;color:${getCardTextColor(design)};line-height:1.5}`;
851
+ .chart-summary{padding:16px 20px;background:${design.accent_light};border-radius:12px;font-size:24px;color:${getCardTextColor(design)};line-height:1.5;margin-top:8px}`;
815
852
  const legendHtml = segs.map((s, i) => `
816
853
  <div class="legend-item">
817
854
  <div class="legend-dot" style="background:${segColors[i % segColors.length]}"></div>
@@ -826,26 +863,27 @@ function buildDonutChartContent(design, data) {
826
863
  <div class="donut-hole"><div class="donut-center">${escapeHtmlTemplate(data.centerText || '')}</div></div>
827
864
  </div>
828
865
  </div>
829
- <div class="legend">${legendHtml}</div>
830
- ${data.summary ? `<div class="chart-summary">${escapeHtmlTemplate(data.summary)}</div>` : ''}
866
+ <div class="legend">${legendHtml}
867
+ ${data.summary ? `<div class="chart-summary">${escapeHtmlTemplate(data.summary)}</div>` : ''}
868
+ </div>
831
869
  </div>`,
832
870
  };
833
871
  }
834
872
  function buildTableContent(design, data) {
835
873
  const cardText = getCardTextColor(design);
836
- const css = `.content{flex:1;display:flex;flex-direction:column}
837
- .data-table{width:100%;border-collapse:collapse;background:#fff;box-shadow:0 4px 12px rgba(0,0,0,0.05);flex:1}
838
- .data-table th{background:${design.primary_color};color:#fff;font-size:28px;text-align:left;padding:22px 28px;font-weight:700}
839
- .data-table td{font-size:28px;color:${cardText};padding:22px 28px;border-bottom:1px solid ${design.accent_light}}
874
+ const maxCols = Math.min((data.headers || []).length, 4);
875
+ const css = `.content{flex:1;display:flex;flex-direction:column;justify-content:center}
876
+ .data-table{width:100%;border-collapse:collapse;background:#fff;box-shadow:0 4px 12px rgba(0,0,0,0.05);border-radius:12px;overflow:hidden;table-layout:fixed}
877
+ .data-table th{background:${design.primary_color};color:#fff;font-size:28px;text-align:left;padding:22px 28px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
878
+ .data-table td{font-size:28px;color:${cardText};padding:22px 28px;border-bottom:1px solid ${design.accent_light};overflow:hidden;word-break:break-word}
840
879
  .data-table tr:last-child td{border-bottom:none}
841
880
  .data-table tr:nth-child(even){background:${design.accent_light}20}
842
881
  .data-table tr.highlight td{background:${design.accent_color}15;font-weight:600}
843
- .table-summary{margin-top:auto;padding:16px 24px;background:${design.accent_light};border-radius:12px;font-size:26px;color:${cardText};line-height:1.5}`;
844
- const headers = (data.headers || []).slice(0, 4).map(h => `<th>${escapeHtmlTemplate(h)}</th>`).join('');
845
- const maxCols = Math.min((data.headers || []).length, 4);
882
+ .table-summary{margin-top:20px;padding:16px 24px;background:${design.accent_light};border-radius:12px;font-size:26px;color:${cardText};line-height:1.5}`;
883
+ const headers = (data.headers || []).slice(0, maxCols).map(h => `<th>${escapeHtmlTemplate(String(h || '').slice(0, 25))}</th>`).join('');
846
884
  const rows = (data.rows || []).slice(0, 5).map((row, ri) => {
847
885
  const cls = ri === data.highlightRow ? ' class="highlight"' : '';
848
- const cells = (row || []).slice(0, maxCols).map(c => `<td>${escapeHtmlTemplate(String(c || ''))}</td>`).join('');
886
+ const cells = (row || []).slice(0, maxCols).map(c => `<td>${escapeHtmlTemplate(String(c || '').slice(0, 50))}</td>`).join('');
849
887
  return `<tr${cls}>${cells}</tr>`;
850
888
  }).join('\n');
851
889
  return {
@@ -863,19 +901,21 @@ function buildProcessFlowContent(design, data) {
863
901
  const descSz = n <= 3 ? '28px' : '26px';
864
902
  const detailSz = n <= 3 ? '28px' : '26px';
865
903
  const css = `.content{flex:1;display:flex;align-items:stretch;gap:0}
866
- .step{flex:1;display:flex;flex-direction:column;align-items:center;padding:36px 24px;background:#fff;border-radius:16px;text-align:center;gap:16px;box-shadow:0 4px 20px rgba(0,0,0,0.06)}
904
+ .step{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px 24px;background:#fff;border-radius:16px;text-align:center;gap:16px;box-shadow:0 4px 20px rgba(0,0,0,0.06)}
867
905
  .step-num{width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,${design.primary_color},${design.gradient_end});color:#fff;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;flex-shrink:0}
868
906
  .step-title{font-size:${titleSz};font-weight:700;color:${design.primary_color}}
869
- .step-desc{font-size:${descSz};color:${cardText};line-height:1.5;flex:1}
870
- .step-detail{font-size:${detailSz};color:${design.accent_color};font-weight:600;padding-top:14px;border-top:2px solid ${design.accent_color}40;margin-top:auto}
907
+ .step-desc{font-size:${descSz};color:${cardText};line-height:1.5}
908
+ .step-detail{font-size:${detailSz};color:${design.accent_color};font-weight:600;padding-top:14px;border-top:2px solid ${design.accent_color}40}
871
909
  .arrow{width:48px;display:flex;align-items:center;justify-content:center;font-size:40px;color:${design.accent_color};flex-shrink:0}`;
872
910
  const steps = (data.steps || []).slice(0, 4);
911
+ const maxDescLen = n <= 3 ? 120 : 80;
873
912
  const stepsHtml = steps.map((s, i) => {
913
+ const desc = (s.desc || '').slice(0, maxDescLen);
874
914
  const stepDiv = `
875
915
  <div class="step">
876
916
  <div class="step-num">${i + 1}</div>
877
917
  <div class="step-title">${escapeHtmlTemplate(s.title || '')}</div>
878
- <div class="step-desc">${escapeHtmlTemplate(s.desc || '')}</div>
918
+ <div class="step-desc">${escapeHtmlTemplate(desc)}</div>
879
919
  ${s.detail ? `<div class="step-detail">${escapeHtmlTemplate(s.detail)}</div>` : ''}
880
920
  </div>`;
881
921
  return i < steps.length - 1 ? stepDiv + '\n <div class="arrow">→</div>' : stepDiv;
@@ -884,13 +924,13 @@ function buildProcessFlowContent(design, data) {
884
924
  }
885
925
  function buildBigNumbersContent(design, data) {
886
926
  const cardText = getCardTextColor(design);
887
- const css = `.content{flex:1;display:flex;gap:40px;align-items:stretch}
888
- .metric-card{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:48px 32px;border-radius:20px;background:#fff;box-shadow:0 4px 24px rgba(0,0,0,0.06);text-align:center;gap:20px}
889
- .metric-value{font-size:88px;font-weight:800;color:${design.primary_color};line-height:1}
890
- .metric-unit{font-size:34px;font-weight:600;color:${design.accent_color}}
891
- .metric-label{font-size:30px;font-weight:600;color:${cardText}}
892
- .metric-desc{font-size:26px;color:${cardText}aa;line-height:1.5}
893
- .metric-trend{font-size:28px;font-weight:600;margin-top:auto}`;
927
+ const css = `.content{flex:1;display:flex;gap:40px;align-items:center}
928
+ .metric-card{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:56px 36px;border-radius:20px;background:#fff;box-shadow:0 6px 28px rgba(0,0,0,0.08);text-align:center;gap:20px;border-top:4px solid ${design.accent_color}}
929
+ .metric-value{font-size:100px;font-weight:800;color:${design.primary_color};line-height:1}
930
+ .metric-unit{font-size:36px;font-weight:600;color:${design.accent_color}}
931
+ .metric-label{font-size:32px;font-weight:600;color:${cardText}}
932
+ .metric-desc{font-size:26px;color:${cardText}aa;line-height:1.5;max-width:90%}
933
+ .metric-trend{font-size:28px;font-weight:600}`;
894
934
  const metrics = (data.metrics || []).slice(0, 3).map(m => `
895
935
  <div class="metric-card">
896
936
  <div class="metric-value">${escapeHtmlTemplate(m.value || '')}</div>
@@ -902,16 +942,16 @@ function buildBigNumbersContent(design, data) {
902
942
  return { css, html: `<div class="content">${metrics}\n</div>` };
903
943
  }
904
944
  function buildTimelineContent(design, data) {
905
- const titleSz = '38px';
906
- const descSz = '30px';
907
- const kpiSz = '28px';
908
- const pad = '40px 36px';
909
- const css = `.content{flex:1;display:flex;align-items:stretch;gap:20px}
910
- .milestone{flex:1;display:flex;flex-direction:column;padding:${pad};border-radius:16px;background:#fff;box-shadow:0 4px 20px rgba(0,0,0,0.06);gap:16px}
911
- .ms-date{display:inline-block;padding:8px 16px;border-radius:8px;background:${design.primary_color};color:#fff;font-size:24px;font-weight:700;align-self:flex-start}
945
+ const titleSz = '40px';
946
+ const descSz = '32px';
947
+ const kpiSz = '30px';
948
+ const pad = '44px 40px';
949
+ const css = `.content{flex:1;display:flex;align-items:stretch;gap:24px}
950
+ .milestone{flex:1;display:flex;flex-direction:column;justify-content:center;padding:${pad};border-radius:16px;background:#fff;box-shadow:0 4px 20px rgba(0,0,0,0.06);gap:18px}
951
+ .ms-date{display:inline-block;padding:8px 18px;border-radius:8px;background:${design.primary_color};color:#fff;font-size:26px;font-weight:700;align-self:flex-start}
912
952
  .ms-title{font-size:${titleSz};font-weight:700;color:${design.primary_color}}
913
- .ms-desc{font-size:${descSz};color:${getCardTextColor(design)};line-height:1.55;flex:1}
914
- .ms-kpi{font-size:${kpiSz};font-weight:600;color:${design.accent_color};padding-top:16px;border-top:2px solid ${design.accent_light};margin-top:auto}`;
953
+ .ms-desc{font-size:${descSz};color:${getCardTextColor(design)};line-height:1.55}
954
+ .ms-kpi{font-size:${kpiSz};font-weight:600;color:${design.accent_color};padding-top:16px;border-top:2px solid ${design.accent_light}}`;
915
955
  const milestones = (data.milestones || []).slice(0, 3).map((m) => `
916
956
  <div class="milestone">
917
957
  <div class="ms-date">${escapeHtmlTemplate(m.date || '')}</div>
@@ -922,14 +962,18 @@ function buildTimelineContent(design, data) {
922
962
  return { css, html: `<div class="content">${milestones}\n</div>` };
923
963
  }
924
964
  function buildProgressBarsContent(design, data) {
925
- const css = `.content{flex:1;display:flex;flex-direction:column;justify-content:space-evenly;gap:0;padding:20px 0}
965
+ const barCount = Math.min((data.bars || []).length, 6);
966
+ const barGap = barCount >= 5 ? '28px' : '44px';
967
+ const barHeight = barCount >= 5 ? '44px' : '56px';
968
+ const barRadius = barCount >= 5 ? '22px' : '28px';
969
+ const css = `.content{flex:1;display:flex;flex-direction:column;justify-content:center;gap:${barGap};padding:20px 0}
926
970
  .bar-item{display:flex;flex-direction:column;gap:10px}
927
971
  .bar-header{display:flex;justify-content:space-between;align-items:baseline}
928
- .bar-label{font-size:28px;font-weight:600;color:${design.text_color}}
929
- .bar-val{font-size:28px;font-weight:700;color:${design.primary_color}}
930
- .bar-track{width:100%;height:44px;background:${design.accent_light};border-radius:22px;overflow:hidden}
931
- .bar-fill{height:100%;border-radius:22px;background:linear-gradient(90deg,${design.primary_color},${design.accent_color})}
932
- .bar-detail{font-size:22px;color:${design.text_color}88;margin-top:-2px}`;
972
+ .bar-label{font-size:32px;font-weight:600;color:${design.text_color}}
973
+ .bar-val{font-size:32px;font-weight:700;color:${design.primary_color}}
974
+ .bar-track{width:100%;height:${barHeight};background:${design.accent_light};border-radius:${barRadius};overflow:hidden}
975
+ .bar-fill{height:100%;border-radius:${barRadius};background:linear-gradient(90deg,${design.primary_color},${design.accent_color})}
976
+ .bar-detail{font-size:26px;color:${design.text_color}88;margin-top:-2px}`;
933
977
  const bars = (data.bars || []).slice(0, 6).map(b => `
934
978
  <div class="bar-item">
935
979
  <div class="bar-header"><span class="bar-label">${escapeHtmlTemplate(b.label || '')}</span><span class="bar-val">${escapeHtmlTemplate(b.value || '')}</span></div>
@@ -968,11 +1012,11 @@ function buildTwoColSplitContent(design, data) {
968
1012
  const css = `.content{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:stretch}
969
1013
  .col{display:flex;flex-direction:column;gap:14px;justify-content:space-evenly}
970
1014
  .col h2{font-size:36px;font-weight:700;color:${design.primary_color};margin-bottom:12px;padding-bottom:12px;border-bottom:3px solid ${design.accent_color}}
971
- .kv-item{display:flex;justify-content:space-between;align-items:center;padding:22px 24px;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.04);font-size:28px;color:${getCardTextColor(design)}}
1015
+ .kv-item{display:flex;justify-content:space-between;align-items:center;padding:28px 28px;background:#fff;border-radius:14px;box-shadow:0 2px 12px rgba(0,0,0,0.04);font-size:30px;color:${getCardTextColor(design)}}
972
1016
  .kv-label{color:${getCardTextColor(design)};font-weight:500}
973
- .kv-value{color:${design.primary_color};font-weight:700;font-size:28px}
974
- .col ul{list-style:none;display:flex;flex-direction:column;gap:10px}
975
- .col li{padding:16px 16px 16px 30px;position:relative;font-size:28px;line-height:1.5;background:#fff;border-radius:10px;box-shadow:0 1px 8px rgba(0,0,0,0.03);color:${getCardTextColor(design)}}
1017
+ .kv-value{color:${design.primary_color};font-weight:700;font-size:30px}
1018
+ .col ul{list-style:none;display:flex;flex-direction:column;gap:12px}
1019
+ .col li{padding:20px 20px 20px 34px;position:relative;font-size:28px;line-height:1.5;background:#fff;border-radius:12px;box-shadow:0 1px 8px rgba(0,0,0,0.03);color:${getCardTextColor(design)}}
976
1020
  .col li::before{content:"•";color:${design.accent_color};position:absolute;left:12px;font-weight:bold}`;
977
1021
  const leftItems = (data.leftItems || []).slice(0, 4).map(item => `
978
1022
  <div class="kv-item"><span class="kv-label">${escapeHtmlTemplate(item.label || '')}</span><span class="kv-value">${escapeHtmlTemplate(item.value || '')}</span></div>`).join('');
@@ -1018,9 +1062,9 @@ CSS:
1018
1062
  .card { display:flex; flex-direction:column; justify-content:flex-start; gap:12px; padding:32px 28px; border-radius:16px; background:#fff; box-shadow:0 4px 20px rgba(0,0,0,0.06); overflow:hidden; }
1019
1063
 
1020
1064
  ⚠ grid-template-rows:1fr 1fr is CRITICAL — it forces cards to stretch and fill vertical space. Without it, cards collapse to content height leaving huge empty bottom.
1021
- • MAX 4 cards (2×2). Each: icon/badge + title (32-40px) + 4-5 bullets (26px) with specific data + stat/metric at bottom.
1065
+ • MAX 4 cards (2×2). Each: icon/badge + title (32-40px) + 3 bullets (26px, MAX 3 per card, each under 25 chars) + stat/metric at bottom.
1022
1066
  • If only 3 items: grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr; (all 3 in one row)
1023
- • MINIMUM CARD CONTENT: title + 4-5 detailed bullet points with numbers/data + bottom metric. A card with only 2 bullets = FAILURE.
1067
+ • MINIMUM CARD CONTENT: title + 3 bullet points with numbers/data + bottom metric. A card with only 1 bullet = FAILURE.
1024
1068
  • Card bottom stat: use margin-top:auto to push it to card bottom, creating visual anchor.
1025
1069
  • ⚠ NEVER have empty grid cells. If you have 3 items, use 3 columns. If 4, use 2×2.`,
1026
1070
  bar_chart: `═══ REQUIRED LAYOUT: BAR CHART ═══
@@ -1183,7 +1227,7 @@ CRITICAL PATTERN — your .content MUST be a vertical stack of .bar-item divs:
1183
1227
 
1184
1228
  ⚠ justify-content:space-evenly distributes bars across the full height — NO empty bottom.
1185
1229
  ⚠ Each bar: header row (label + percentage) + track div containing fill div with style="width:XX%".
1186
- ⚠ 4-6 bars. Bar height:44px. Use gradient fills. The structure above is non-negotiable.
1230
+ ⚠ 4-5 bars (MAX 5). Bar height:44px. Use gradient fills. Labels under 15 chars. The structure above is non-negotiable.
1187
1231
  ⚠⚠⚠ .bar-item divs MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
1188
1232
  hero_stat: `═══ REQUIRED LAYOUT: HERO STAT ═══
1189
1233
  ⚠⚠⚠ You MUST create a SINGLE LARGE CENTRAL METRIC. Cards/tables = FAILURE.