hanseol-dev 5.0.2-dev.99 → 5.0.3-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/office/powerpoint-create-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.js +445 -404
- package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.d.ts +99 -3
- package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.js +1098 -172
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/tools/office/powerpoint-client.d.ts.map +1 -1
- package/dist/tools/office/powerpoint-client.js +13 -6
- package/dist/tools/office/powerpoint-client.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,53 +1,15 @@
|
|
|
1
|
-
export const
|
|
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
|
-
|
|
4
|
-
Your job: design a cohesive visual system and detailed content plan that produces beautiful HTML slides.
|
|
5
|
-
|
|
6
|
-
OUTPUT FORMAT (structured text, max 1000 words):
|
|
7
|
-
|
|
8
|
-
1. DOCUMENT_TYPE: pitch deck / business report / training / product launch / general
|
|
9
|
-
2. AUDIENCE: Target audience description
|
|
10
|
-
3. MOOD: modern-minimal / bold-energetic / corporate-elegant / warm-friendly / academic-clean
|
|
11
|
-
|
|
12
|
-
4. COLOR PALETTE (6 hex values — choose colors that match the topic and mood):
|
|
13
|
-
- primary: Deep main color for headers and key elements (e.g., #1B2A4A for corporate, #6C63FF for tech)
|
|
14
|
-
- accent: Vibrant contrast color for highlights and CTAs (e.g., #00D4AA, #FF6B35)
|
|
15
|
-
- background: Page background — near white (#F8F9FA) or near black (#0D1117) or subtle tint
|
|
16
|
-
- text: Main text color — dark (#1A1A2E) for light bg, light (#E8E8E8) for dark bg
|
|
17
|
-
- accent_light: Light tint of accent for subtle backgrounds (e.g., #E8F5F0)
|
|
18
|
-
- gradient_end: Secondary gradient color paired with primary (e.g., #2D5F8A)
|
|
19
|
-
|
|
20
|
-
5. FONTS: title_font, body_font (system only: Segoe UI, Malgun Gothic, Arial, Calibri, Georgia)
|
|
21
|
-
6. SLIDE PLAN: 10-15 slides, each with:
|
|
22
|
-
- Slide number and type (title / content / data / comparison / process / highlight / table / closing)
|
|
23
|
-
- Title in user's language
|
|
24
|
-
- Specific content direction (what data, text, visuals to include)
|
|
25
|
-
- CSS layout approach (e.g., "flexbox 3-column card grid with box-shadow", "CSS grid 2×2 with gradient headers", "conic-gradient donut chart + metric cards", "CSS bar chart using flex-end alignment")
|
|
26
|
-
7. DESIGN NOTES: Overall HTML/CSS approach — gradients (linear-gradient, radial-gradient), box-shadows, border-radius, flexbox/grid layouts, CSS shapes, pseudo-elements for decoration
|
|
27
|
-
|
|
28
|
-
KEY PRINCIPLES FOR HTML SLIDES:
|
|
29
|
-
- Each slide is a full 1920×1080px HTML page — use the ENTIRE space (flex:1 to stretch content)
|
|
30
|
-
- CSS-only visuals: gradients, box-shadow, border-radius, conic-gradient for pie/donut charts
|
|
31
|
-
- MAXIMUM 3 cards or data items per slide — whitespace is premium
|
|
32
|
-
- Vary layouts across slides: don't repeat the same card grid. Mix cards, tables, big-number spotlights, split layouts, gradient headers
|
|
33
|
-
- NO images, NO external fonts, NO JavaScript
|
|
34
|
-
|
|
35
|
-
LANGUAGE RULE: ALL slide titles and content MUST be in the SAME language as the user's instruction.
|
|
36
|
-
Korean input → Korean output. English slogans for Korean input = FAILURE.
|
|
37
|
-
|
|
38
|
-
Output structured text only. No preamble.`;
|
|
39
|
-
export const PPT_STRUCTURED_PLANNING_PROMPT = `You are a presentation structure planner. Output ONLY valid JSON — no markdown, no explanation, no code fences.
|
|
40
|
-
|
|
41
|
-
Given the user's instruction and CREATIVE GUIDANCE, produce a JSON object with this exact structure:
|
|
3
|
+
Given the user's instruction, produce a JSON object that combines a cohesive visual design system with a detailed slide plan.
|
|
42
4
|
|
|
43
5
|
{
|
|
44
6
|
"design": {
|
|
45
|
-
"primary_color": "<
|
|
46
|
-
"accent_color": "<
|
|
47
|
-
"background_color": "<
|
|
48
|
-
"text_color": "<
|
|
49
|
-
"accent_light": "<
|
|
50
|
-
"gradient_end": "<
|
|
7
|
+
"primary_color": "<deep color matching the topic>",
|
|
8
|
+
"accent_color": "<vibrant contrast color>",
|
|
9
|
+
"background_color": "<near-white or near-black or subtle tint>",
|
|
10
|
+
"text_color": "<must contrast with background>",
|
|
11
|
+
"accent_light": "<light tint of your accent color>",
|
|
12
|
+
"gradient_end": "<secondary gradient paired with primary>",
|
|
51
13
|
"font_title": "Segoe UI",
|
|
52
14
|
"font_body": "Malgun Gothic",
|
|
53
15
|
"mood": "modern-minimal",
|
|
@@ -60,9 +22,9 @@ Given the user's instruction and CREATIVE GUIDANCE, produce a JSON object with t
|
|
|
60
22
|
]
|
|
61
23
|
}
|
|
62
24
|
|
|
63
|
-
═══ FIELD DEFINITIONS ═══
|
|
64
|
-
• design.primary_color: Deep main color
|
|
65
|
-
• design.accent_color: Vibrant contrast
|
|
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
|
|
66
28
|
• design.background_color: Page background (near white, near black, or subtle tint)
|
|
67
29
|
• design.text_color: Main text color (must contrast with background)
|
|
68
30
|
• design.accent_light: Light tint for subtle section backgrounds
|
|
@@ -74,21 +36,53 @@ Given the user's instruction and CREATIVE GUIDANCE, produce a JSON object with t
|
|
|
74
36
|
|
|
75
37
|
• slides[].type: One of "title", "content", "closing"
|
|
76
38
|
• slides[].title: Slide title in user's language (max 60 chars)
|
|
77
|
-
• slides[].content_direction:
|
|
39
|
+
• slides[].content_direction: The ACTUAL TEXT AND DATA to display on this slide (6-10 sentences).
|
|
40
|
+
|
|
41
|
+
═══ 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
|
|
51
|
+
⚠ Choose colors that MATCH the specific topic. Generic blue = LAZY.
|
|
52
|
+
⚠ 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-).
|
|
78
54
|
|
|
79
|
-
═══
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
61
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
62
|
+
═══ SLIDE STRUCTURE RULES ═══
|
|
63
|
+
⚠ First slide MUST be type "title"
|
|
64
|
+
⚠ Last slide MUST be type "closing" with "감사합니다"/"Thank You" as title
|
|
65
|
+
⚠ 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
|
|
88
82
|
|
|
89
83
|
═══ TEMPLATES ═══
|
|
90
84
|
PITCH DECK (startup/사업계획서/피치덱): 12 slides
|
|
91
|
-
title → problem → solution → market size → product → business model → competition → traction → team →
|
|
85
|
+
title → problem → solution → market size → product(1) → product(2) → business model → competition → traction → team → financials → closing
|
|
92
86
|
|
|
93
87
|
BUSINESS REPORT (보고서/분석/실적): 10 slides
|
|
94
88
|
title → executive summary → key metrics → analysis → comparison → spotlight → breakdown → action plan → recommendations → closing
|
|
@@ -108,17 +102,16 @@ GENERAL (발표/프레젠테이션): 10 slides
|
|
|
108
102
|
If a topic needs 2 visual elements, SPLIT IT INTO 2 SLIDES.
|
|
109
103
|
|
|
110
104
|
GOOD content_direction (ONE visual each):
|
|
111
|
-
✓ "
|
|
112
|
-
✓ "Comparison table: 4 rows
|
|
113
|
-
✓ "
|
|
114
|
-
✓ "
|
|
115
|
-
✓ "
|
|
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"
|
|
116
111
|
|
|
117
112
|
BAD content_direction (MULTIPLE visuals = OVERFLOW):
|
|
118
113
|
✗ "테이블로 수익 모델 + 수익 그래프 + 파트너십 + 라이선싱" ← 4 sections!
|
|
119
114
|
✗ "원형 차트 + 투자 조건 테이블 + 단계적 계획 + 바 차트" ← 4 sections!
|
|
120
|
-
✗ "레이더 차트 + 데이터 바 + 차별화 카드 4개" ← 3 sections!
|
|
121
|
-
✗ "성장 차트 + 시장 분할 파이 + 기회 텍스트 + 과제 텍스트" ← 4 sections!
|
|
122
115
|
|
|
123
116
|
═══ SPLITTING DENSE TOPICS INTO MULTIPLE SLIDES ═══
|
|
124
117
|
⚠ If a user topic has 4+ sub-items, you MUST split it into 2 slides:
|
|
@@ -126,92 +119,297 @@ BAD content_direction (MULTIPLE visuals = OVERFLOW):
|
|
|
126
119
|
• "시장분석" → "시장 규모" (3 big metrics) + "고객 세분화" (pie chart or cards)
|
|
127
120
|
• "투자조건" → "투자 조건" (table) + "자금 사용 계획" (pie or bar chart)
|
|
128
121
|
• "경쟁우위" → "경쟁사 비교" (comparison table) + "핵심 차별화" (3 card grid)
|
|
129
|
-
• "경쟁분석" → "경쟁사 비교" (comparison table ONLY) + "SWOT 분석" (2×2 grid ONLY)
|
|
122
|
+
• "경쟁분석" → "경쟁사 비교" (comparison table ONLY) + "SWOT 분석" (2×2 grid ONLY)
|
|
130
123
|
• "AI 솔루션" → "핵심 기능" (3 card grid) + "진단 프로세스" (process flow)
|
|
131
|
-
• "제품/플랫폼" → "핵심 기능" (3 card grid with feature descriptions)
|
|
132
|
-
• "마케팅/GTM 전략" → "마케팅 채널 전략" (3 cards) + "성과 지표" (metrics or table)
|
|
133
|
-
• "로드맵" → "단기 로드맵
|
|
124
|
+
• "제품/플랫폼" → "핵심 기능" (3 card grid with feature descriptions)
|
|
125
|
+
• "마케팅/GTM 전략" → "마케팅 채널 전략" (3 cards) + "성과 지표" (metrics or table)
|
|
126
|
+
• "로드맵" → "단기 로드맵" (3 milestone cards) + "장기 비전" (3 milestone cards)
|
|
134
127
|
• "팀 소개" (4+ members) → "창업진 소개" (2-3 people) + "핵심 팀원" (2-3 people)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
⚠
|
|
140
|
-
⚠
|
|
141
|
-
⚠
|
|
142
|
-
⚠
|
|
143
|
-
|
|
144
|
-
⚠
|
|
145
|
-
⚠
|
|
146
|
-
⚠
|
|
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 SHORT bullets (max 25 chars each) with specific numbers.
|
|
133
|
+
⚠ Tables: 3-4 data rows × 3-4 columns. 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 + 1-sentence description.
|
|
136
|
+
⚠ Data/metrics: 3 big numbers with trend indicators AND 1-sentence context.
|
|
137
|
+
⚠ Cards: 3-4 cards, each with title + 3 bullets (MAX 3, keep short) + stat metric.
|
|
138
|
+
⚠ Donut/Pie charts: 3-4 segments with labels + summary.
|
|
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!
|
|
147
153
|
|
|
148
154
|
═══ OVERVIEW / AGENDA SLIDES ═══
|
|
149
|
-
⚠ Overview/agenda slides
|
|
150
|
-
Use a numbered list style (1-5) or compact 2-column grid. NEVER use 6-7+ horizontal cards.
|
|
155
|
+
⚠ Overview/agenda slides: MAXIMUM 5 items. Numbered list (1-5) or compact 2-column grid.
|
|
151
156
|
Each item: short title (2-3 words) + 1-line description only.
|
|
152
|
-
If
|
|
153
|
-
Overview slides should be concise — just a navigation map, not a content dump.
|
|
157
|
+
If 10+ sections, group related topics: "시장 및 경쟁" instead of separate "시장 분석" + "경쟁 분석".
|
|
154
158
|
|
|
155
|
-
═══
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
⚠
|
|
171
|
-
|
|
172
|
-
comparison table, timeline with icons, full-width data bar, centered quote/highlight
|
|
173
|
-
|
|
174
|
-
═══ COLOR PALETTE RULES ═══
|
|
175
|
-
⚠ NEVER use placeholder/example colors. EVERY presentation must have a UNIQUE palette:
|
|
176
|
-
• Medical/Health: blues + greens (#0B5394, #27AE60)
|
|
177
|
-
• Tech/AI: purples + cyans (#6C63FF, #00BCD4)
|
|
178
|
-
• Finance: navy + gold (#1A237E, #F4A300)
|
|
179
|
-
• Education: teal + orange (#00796B, #FF7043)
|
|
180
|
-
• Marketing: coral + purple (#FF6B6B, #7C3AED)
|
|
181
|
-
⚠ Choose colors that MATCH the specific topic. Generic blue = LAZY.
|
|
182
|
-
⚠ Ensure accent_color has HIGH contrast with primary_color.
|
|
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.
|
|
183
176
|
|
|
184
177
|
═══ HARD RULES ═══
|
|
185
178
|
⚠ First slide MUST be type "title"
|
|
186
179
|
⚠ Last slide MUST be type "closing" with "감사합니다"/"Thank You" as title
|
|
187
180
|
⚠ ALL titles and content_direction MUST be in the SAME language as the user's instruction
|
|
188
|
-
⚠ content_direction:
|
|
189
|
-
⚠
|
|
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.
|
|
190
184
|
⚠ Each slide's content_direction must be unique and substantive — no generic placeholders
|
|
191
185
|
⚠ NEVER use "스크린샷", "screenshot", "이미지", "사진", "placeholder", "[내용]" in content_direction
|
|
192
|
-
— the system cannot insert images. Describe REAL text/data content to display.
|
|
193
186
|
⚠ Do NOT create a separate "연락처"/"Contact" slide — the closing slide already handles this.
|
|
194
|
-
— Use that slot for valuable content instead (e.g., competitive advantage, key metrics, next steps).
|
|
195
187
|
|
|
196
188
|
Output ONLY the JSON object. No preamble, no markdown fences, no explanation.`;
|
|
197
|
-
export function
|
|
189
|
+
export function extractLayoutHint(contentDirection) {
|
|
190
|
+
const match = contentDirection.match(/Layout:\s*(.+?)$/im);
|
|
191
|
+
if (!match)
|
|
192
|
+
return 'cards';
|
|
193
|
+
const hint = match[1].trim().toLowerCase();
|
|
194
|
+
if (/card|grid/.test(hint))
|
|
195
|
+
return 'cards';
|
|
196
|
+
if (/bar\s*chart/.test(hint))
|
|
197
|
+
return 'bar_chart';
|
|
198
|
+
if (/donut|pie/.test(hint))
|
|
199
|
+
return 'donut_chart';
|
|
200
|
+
if (/comparison\s*table|table/.test(hint))
|
|
201
|
+
return 'table';
|
|
202
|
+
if (/process|flow/.test(hint))
|
|
203
|
+
return 'process_flow';
|
|
204
|
+
if (/big\s*num|metric/.test(hint))
|
|
205
|
+
return 'big_numbers';
|
|
206
|
+
if (/split|2-col|two.col/.test(hint))
|
|
207
|
+
return 'two_col_split';
|
|
208
|
+
if (/timeline|milestone|roadmap/.test(hint))
|
|
209
|
+
return 'timeline';
|
|
210
|
+
if (/progress\s*bar/.test(hint))
|
|
211
|
+
return 'progress_bars';
|
|
212
|
+
if (/hero|spotlight/.test(hint))
|
|
213
|
+
return 'hero_stat';
|
|
214
|
+
return 'cards';
|
|
215
|
+
}
|
|
216
|
+
export function checkLayoutCompliance(html, layoutType) {
|
|
217
|
+
switch (layoutType) {
|
|
218
|
+
case 'donut_chart':
|
|
219
|
+
if (!html.includes('conic-gradient')) {
|
|
220
|
+
return 'WRONG LAYOUT: Expected donut/pie chart with conic-gradient. You MUST use conic-gradient on a border-radius:50% div. Copy the REQUIRED HTML structure from the prompt.';
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case 'bar_chart': {
|
|
224
|
+
const hasFlexEnd = /flex-end/.test(html);
|
|
225
|
+
const barHeights = html.match(/style="[^"]*height:\s*\d+%/g) || [];
|
|
226
|
+
if (!hasFlexEnd || barHeights.length < 2) {
|
|
227
|
+
return `WRONG LAYOUT: Expected CSS bar chart with flex-end + at least 2 bars with height:XX%. Found flex-end:${hasFlexEnd}, bars:${barHeights.length}. Copy the REQUIRED HTML structure with .chart-area, .bar-group, .bar elements.`;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'table':
|
|
232
|
+
if (!html.includes('<table') || !html.includes('<th')) {
|
|
233
|
+
return 'WRONG LAYOUT: Expected HTML <table> with <th> header cells. Copy the REQUIRED HTML structure.';
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case 'process_flow': {
|
|
237
|
+
const arrowCount = (html.match(/→/g) || []).length;
|
|
238
|
+
const hasSteps = /class="[^"]*step/i.test(html);
|
|
239
|
+
if (arrowCount < 2 || !hasSteps) {
|
|
240
|
+
return `WRONG LAYOUT: Expected process flow with step boxes and → arrows. Found arrows:${arrowCount}, steps:${hasSteps}. You MUST have .step divs connected by .arrow divs containing "→".`;
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case 'progress_bars': {
|
|
245
|
+
const hasFill = /bar-fill/i.test(html);
|
|
246
|
+
const hasTrack = /bar-track/i.test(html);
|
|
247
|
+
const widthBars = html.match(/style="[^"]*width:\s*\d+%/g) || [];
|
|
248
|
+
if (!hasFill || !hasTrack || widthBars.length < 2) {
|
|
249
|
+
return `WRONG LAYOUT: Expected progress bars with .bar-track + .bar-fill + width:XX%. Found fill:${hasFill}, track:${hasTrack}, bars:${widthBars.length}. Copy the REQUIRED HTML structure.`;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 'timeline': {
|
|
254
|
+
const hasMilestone = /class="[^"]*milestone/i.test(html);
|
|
255
|
+
const milestoneCount = (html.match(/class="[^"]*milestone[^"]*"/gi) || []).length;
|
|
256
|
+
if (!hasMilestone || milestoneCount < 2) {
|
|
257
|
+
return `WRONG LAYOUT: Expected timeline with milestone cards. Found ${milestoneCount} milestones. You MUST have 3-4 .milestone divs side by side.`;
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case 'big_numbers': {
|
|
262
|
+
const bigFonts = html.match(/font-size:\s*(?:7[2-9]|[89]\d|1[0-2]\d)px/g) || [];
|
|
263
|
+
if (bigFonts.length < 2) {
|
|
264
|
+
return `WRONG LAYOUT: Expected big number metrics with font-size 72-96px. Found ${bigFonts.length} large fonts. Each .metric-card MUST have a .metric-value with font-size:80px.`;
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case 'hero_stat': {
|
|
269
|
+
const heroFont = html.match(/font-size:\s*(?:9[6-9]|1[0-2]\d)px/g) || [];
|
|
270
|
+
if (heroFont.length < 1) {
|
|
271
|
+
return `WRONG LAYOUT: Expected hero stat with font-size 96-128px. You MUST have ONE .hero-number with font-size:128px.`;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
export function buildDirectHtmlPrompt(title, contentDirection, design, slideIndex, totalSlides, language, layoutType) {
|
|
279
|
+
const langRule = language === 'ko'
|
|
280
|
+
? 'ALL visible text MUST be in Korean. Write naturally in Korean. Never use Chinese characters (漢字).'
|
|
281
|
+
: 'ALL visible text MUST be in English.';
|
|
282
|
+
const layoutCss = getLayoutSpecificCss(layoutType, design);
|
|
283
|
+
const styleVariants = [
|
|
284
|
+
`Background: ${design.background_color}. Cards/elements use white background with box-shadow.`,
|
|
285
|
+
`Add a bold accent bar at top (height:4px, linear-gradient(90deg, ${design.accent_color}, ${design.primary_color})). Background: ${design.background_color}.`,
|
|
286
|
+
`Subtle gradient background: linear-gradient(150deg, ${design.background_color} 0%, ${design.accent_light} 50%, ${design.background_color} 100%). Stronger shadows.`,
|
|
287
|
+
];
|
|
288
|
+
const styleGuide = styleVariants[slideIndex % styleVariants.length];
|
|
289
|
+
return `You are a world-class web designer creating a presentation slide as a complete HTML page.
|
|
290
|
+
Output ONLY the complete HTML document (<!DOCTYPE html> to </html>). No explanation, no markdown fences.
|
|
291
|
+
|
|
292
|
+
═══ SLIDE ═══
|
|
293
|
+
Title: "${title}" | Slide ${slideIndex + 1} of ${totalSlides}
|
|
294
|
+
|
|
295
|
+
═══ CONTENT (what to show — generate REAL content based on this, never display it literally) ═══
|
|
296
|
+
${contentDirection}
|
|
297
|
+
|
|
298
|
+
═══ DESIGN SYSTEM ═══
|
|
299
|
+
Primary: ${design.primary_color} | Accent: ${design.accent_color} | BG: ${design.background_color}
|
|
300
|
+
Text: ${design.text_color} | Light: ${design.accent_light} | Gradient: ${design.gradient_end}
|
|
301
|
+
Title Font: ${design.font_title} | Body Font: ${design.font_body} | Mood: ${design.mood}
|
|
302
|
+
|
|
303
|
+
═══ VISUAL STYLE FOR THIS SLIDE ═══
|
|
304
|
+
${styleGuide}
|
|
305
|
+
|
|
306
|
+
═══ MANDATORY CSS BOILERPLATE (copy exactly into <style>) ═══
|
|
307
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
308
|
+
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; }
|
|
309
|
+
body { display:flex; flex-direction:column; padding:60px 80px; height:1080px; background:${design.background_color}; color:${design.text_color}; font-size:26px; }
|
|
310
|
+
.slide-title { flex:0 0 auto; margin-bottom:20px; }
|
|
311
|
+
.slide-title h1 { font-size:48px; font-weight:700; color:${design.primary_color}; font-family:"${design.font_title}","Segoe UI",sans-serif; }
|
|
312
|
+
|
|
313
|
+
═══ STRUCTURE: body has EXACTLY 2 children ═══
|
|
314
|
+
→ .slide-title (flex:0 0 auto) containing h1 with title text + optional accent bar
|
|
315
|
+
→ .content (flex:1) stretching to fill ALL remaining vertical space
|
|
316
|
+
⚠ .content class name is REQUIRED for post-processing.
|
|
317
|
+
⚠ ALL layout elements MUST be DIRECT children of .content — NO wrapper divs.
|
|
318
|
+
⚠ NEVER use position:absolute. Flexbox/grid ONLY.
|
|
319
|
+
|
|
320
|
+
${layoutCss}
|
|
321
|
+
|
|
322
|
+
═══ RULES ═══
|
|
323
|
+
• ${langRule}
|
|
324
|
+
• Complete HTML: <!DOCTYPE html> through </html>. ALL styling in <style>.
|
|
325
|
+
• NO <img>, NO external resources, NO JavaScript, NO external fonts.
|
|
326
|
+
• 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.
|
|
329
|
+
• NEVER use justify-content:center on .content (creates dead space). Use stretch/space-evenly.
|
|
330
|
+
• Use gradients, box-shadow (0 4px 20px rgba(0,0,0,0.06)), border-radius (12-20px).
|
|
331
|
+
• Generate REAL professional content. No placeholders. Specific numbers and data.
|
|
332
|
+
• If user specified a year, USE THAT YEAR. Default to ${new Date().getFullYear()} only when no year given.
|
|
333
|
+
• Page number: bottom-right "${slideIndex + 1}" (12px, opacity 0.4).
|
|
334
|
+
|
|
335
|
+
Output the complete HTML now.`;
|
|
336
|
+
}
|
|
337
|
+
export function validateSlideHtml(html, layoutType) {
|
|
338
|
+
if (!html.includes('<!DOCTYPE') && !html.includes('<!doctype')) {
|
|
339
|
+
return { pass: false, feedback: 'Missing <!DOCTYPE html> declaration. Start with <!DOCTYPE html><html>.' };
|
|
340
|
+
}
|
|
341
|
+
if (!html.includes('<html') || !html.includes('</html>')) {
|
|
342
|
+
return { pass: false, feedback: 'Missing <html> or </html> tags. Output must be a complete HTML document.' };
|
|
343
|
+
}
|
|
344
|
+
const layoutFeedback = checkLayoutCompliance(html, layoutType);
|
|
345
|
+
if (layoutFeedback) {
|
|
346
|
+
return { pass: false, feedback: layoutFeedback };
|
|
347
|
+
}
|
|
348
|
+
if (/<img\s/i.test(html)) {
|
|
349
|
+
return { pass: false, feedback: 'Forbidden: <img> tags are not allowed. Use CSS gradients, shapes, and backgrounds instead.' };
|
|
350
|
+
}
|
|
351
|
+
const absCount = (html.match(/position\s*:\s*absolute/gi) || []).length;
|
|
352
|
+
if (absCount > 6) {
|
|
353
|
+
return { pass: false, feedback: `Too many position:absolute (${absCount}). Use flexbox/grid for main layout. Absolute ok for small decorative elements.` };
|
|
354
|
+
}
|
|
355
|
+
if (/https?:\/\/(?!.*$)/i.test(html) && !/https?:\/\/[^"')\s]*\.(nip\.io|localhost)/i.test(html)) {
|
|
356
|
+
const externalUrls = html.match(/url\(\s*['"]?https?:\/\//gi) || [];
|
|
357
|
+
if (externalUrls.length > 0) {
|
|
358
|
+
return { pass: false, feedback: 'Forbidden: External URLs detected in CSS url(). No external resources allowed.' };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const placeholderPatterns = [
|
|
362
|
+
/Card title \(2-5 words\)/i,
|
|
363
|
+
/Detail with number\/data/i,
|
|
364
|
+
/single emoji/i,
|
|
365
|
+
/Display value \(e\.g\./i,
|
|
366
|
+
/1-2 sentence key insight/i,
|
|
367
|
+
/Category name/i,
|
|
368
|
+
/Another detail/i,
|
|
369
|
+
/Third point/i,
|
|
370
|
+
/Fourth point/i,
|
|
371
|
+
/Brief context/i,
|
|
372
|
+
/Segment name/i,
|
|
373
|
+
];
|
|
374
|
+
for (const pattern of placeholderPatterns) {
|
|
375
|
+
if (pattern.test(html)) {
|
|
376
|
+
return { pass: false, feedback: `Placeholder text detected: "${pattern.source}". Generate REAL content, not schema examples.` };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const textElements = (html.match(/<(p|li|td|th|span|div|h[1-6])[^>]*>[^<]{2,}/gi) || []).length;
|
|
380
|
+
if (textElements < 5) {
|
|
381
|
+
return { pass: false, feedback: `Low content density: only ${textElements} text elements found. Need at least 5 text-bearing elements for a complete slide.` };
|
|
382
|
+
}
|
|
383
|
+
const visibleText = html.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
384
|
+
.replace(/<[^>]+>/g, ' ')
|
|
385
|
+
.replace(/\s+/g, ' ')
|
|
386
|
+
.trim();
|
|
387
|
+
if (visibleText.length > 2200) {
|
|
388
|
+
return { pass: false, feedback: `Content overflow risk: ${visibleText.length} chars of visible text. Reduce to under 2200 chars to prevent overflow on 1920×1080.` };
|
|
389
|
+
}
|
|
390
|
+
const smallFonts = html.match(/font-size\s*:\s*(\d+)px/gi) || [];
|
|
391
|
+
for (const match of smallFonts) {
|
|
392
|
+
const size = parseInt(match.replace(/[^0-9]/g, ''), 10);
|
|
393
|
+
if (size > 0 && size < 18 && size !== 12) {
|
|
394
|
+
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.` };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return { pass: true, feedback: '' };
|
|
398
|
+
}
|
|
399
|
+
export function buildSlideHtmlPrompt(slideTitle, contentDirection, design, slideIndex, totalSlides, language, layoutType = 'cards') {
|
|
198
400
|
const langRule = language === 'ko'
|
|
199
401
|
? 'ALL visible text MUST be in Korean. Write naturally in Korean.'
|
|
200
402
|
: 'ALL visible text MUST be in English.';
|
|
201
403
|
const styleVariants = [
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'GRADIENT_HEADER',
|
|
206
|
-
'FULL_GRADIENT',
|
|
404
|
+
'LIGHT',
|
|
405
|
+
'ACCENT_HEADER',
|
|
406
|
+
'GRADIENT_BG',
|
|
207
407
|
];
|
|
208
408
|
const styleVariant = styleVariants[slideIndex % styleVariants.length];
|
|
209
409
|
const styleGuide = {
|
|
210
|
-
'
|
|
211
|
-
'
|
|
212
|
-
'
|
|
213
|
-
'GRADIENT_HEADER': `Add a bold accent bar at the top (height:8px, background: linear-gradient(90deg, ${design.accent_color}, ${design.primary_color})). Title goes directly below the bar (no large background band — just the accent bar + title text). Content uses ${design.background_color}. ⚠ The accent bar is ONLY 8px tall — do NOT create a large colored header section.`,
|
|
214
|
-
'FULL_GRADIENT': `Use a subtle full-page gradient (background: linear-gradient(150deg, ${design.background_color} 0%, ${design.accent_light} 50%, ${design.background_color} 100%)). Cards float with stronger shadows (box-shadow: 0 8px 32px rgba(0,0,0,0.08)).`,
|
|
410
|
+
'LIGHT': `Background: ${design.background_color}. Title: ${design.primary_color}, 48-56px bold. Cards/elements use white background with box-shadow.`,
|
|
411
|
+
'ACCENT_HEADER': `Add a bold accent bar at top (height:6px, background: linear-gradient(90deg, ${design.accent_color}, ${design.primary_color})). Title directly below. Background: ${design.background_color}.`,
|
|
412
|
+
'GRADIENT_BG': `Subtle gradient background: linear-gradient(150deg, ${design.background_color} 0%, ${design.accent_light} 50%, ${design.background_color} 100%). Elements use stronger shadows (0 8px 32px rgba(0,0,0,0.08)).`,
|
|
215
413
|
};
|
|
216
414
|
return `You are a world-class web designer creating a presentation slide as a complete HTML page.
|
|
217
415
|
Output ONLY the complete HTML document — nothing else. No explanation, no markdown fences.
|
|
@@ -220,9 +418,14 @@ Output ONLY the complete HTML document — nothing else. No explanation, no mark
|
|
|
220
418
|
Title: "${slideTitle}"
|
|
221
419
|
Slide ${slideIndex + 1} of ${totalSlides}
|
|
222
420
|
|
|
223
|
-
═══ CONTENT DIRECTION ═══
|
|
421
|
+
═══ CONTENT DIRECTION (what to show on this slide) ═══
|
|
224
422
|
${contentDirection}
|
|
225
423
|
|
|
424
|
+
⚠⚠⚠ CRITICAL: The content_direction above is a GUIDE for what content to create.
|
|
425
|
+
Generate REAL, specific, professional text based on it. NEVER display the content_direction text literally on the slide.
|
|
426
|
+
If it says "3 big metrics: X, Y, Z" → create 3 beautifully formatted metric cards with X, Y, Z values.
|
|
427
|
+
If it says "Layout: comparison table" → create a table with the DATA mentioned, not the word "comparison table".
|
|
428
|
+
|
|
226
429
|
═══ DESIGN SYSTEM ═══
|
|
227
430
|
Primary: ${design.primary_color} | Accent: ${design.accent_color} | Background: ${design.background_color}
|
|
228
431
|
Text: ${design.text_color} | Accent Light: ${design.accent_light} | Gradient End: ${design.gradient_end}
|
|
@@ -251,6 +454,7 @@ body {
|
|
|
251
454
|
|
|
252
455
|
⚠ word-break:keep-all is CRITICAL for Korean text — without it, words break at random characters.
|
|
253
456
|
⚠ body MUST be display:flex + flex-direction:column + height:1080px for vertical fill.
|
|
457
|
+
⚠ The main content container (the one with cards/table/chart) MUST use class="content" — required for post-processing.
|
|
254
458
|
|
|
255
459
|
═══ TECHNICAL REQUIREMENTS ═══
|
|
256
460
|
1. Complete HTML: <!DOCTYPE html> through </html>
|
|
@@ -261,71 +465,793 @@ body {
|
|
|
261
465
|
6. body IS the container — no wrapper divs. Content goes directly in body.
|
|
262
466
|
|
|
263
467
|
═══ VERTICAL FILL — THE #1 QUALITY RULE ═══
|
|
264
|
-
⚠⚠⚠ Content MUST fill the ENTIRE 1920×1080 slide. Empty
|
|
468
|
+
⚠⚠⚠ Content MUST fill the ENTIRE 1920×1080 slide. Empty space = FAILURE.
|
|
469
|
+
⚠⚠⚠ NEVER have more than 150px of empty whitespace anywhere on the slide.
|
|
265
470
|
|
|
266
|
-
MANDATORY STRUCTURE:
|
|
471
|
+
MANDATORY STRUCTURE (body has EXACTLY 2 direct children):
|
|
267
472
|
body (display:flex, flex-direction:column, height:1080px, padding:60px 80px)
|
|
268
|
-
→ .slide-title (auto
|
|
473
|
+
→ .slide-title (flex:0 0 auto — title text only, NO extra margin-bottom)
|
|
269
474
|
→ .content (flex:1 — STRETCHES to fill ALL remaining vertical space)
|
|
270
475
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
Each card: icon/number at top, title+desc in middle, metric/accent-bar at bottom.
|
|
276
|
-
Cards MUST be tall (flex:1 stretches them to fill .content height).
|
|
277
|
-
|
|
278
|
-
TABLES: .content { flex:1; display:flex; flex-direction:column; }
|
|
279
|
-
table { width:100%; } th { padding:20px 24px; background:${design.primary_color}; color:#fff; }
|
|
280
|
-
td { padding:24px 20px; font-size:22px; } Add a SUMMARY BAR below the table.
|
|
281
|
-
|
|
282
|
-
TIMELINES: .content { flex:1; display:flex; align-items:stretch; gap:32px; }
|
|
283
|
-
Each milestone: tall card (flex:1) with icon circle, title, year, description, and KPI at bottom.
|
|
284
|
-
|
|
285
|
-
2-COL SPLIT: .content { flex:1; display:grid; grid-template-columns:1fr 1fr; gap:32px; }
|
|
286
|
-
Left: chart or big metric. Right: detail cards. Both stretch full height.
|
|
476
|
+
⚠ body MUST have exactly 2 direct children: .slide-title and .content. No accent bars, no spacers, no extra divs between them.
|
|
477
|
+
⚠ .slide-title should include any accent bars/decorations AS PART OF the title section, not as separate body children.
|
|
478
|
+
⚠ .content MUST use flex:1 to stretch. NEVER use justify-content:center on .content — it creates dead space.
|
|
479
|
+
⚠⚠⚠ ALL content elements (cards, bars, table, timeline items) MUST be DIRECT CHILDREN of .content. NEVER wrap them in a container/wrapper div inside .content. Wrong: .content > .wrapper > items. Right: .content > items.
|
|
287
480
|
|
|
288
|
-
|
|
289
|
-
Each: flex:1 card with huge number (72-96px), label, and description paragraph.
|
|
290
|
-
|
|
291
|
-
BAR CHARTS: .content { flex:1; display:flex; flex-direction:column; }
|
|
292
|
-
.chart-area { flex:1; display:flex; align-items:flex-end; gap:48px; padding:40px; }
|
|
293
|
-
Each bar: flex:1, label ABOVE the bar (never beside it). Min label width: 120px.
|
|
294
|
-
⚠ Bar labels: use min-width:120px and text-align:center to prevent character-by-character wrapping.
|
|
481
|
+
${getLayoutSpecificCss(layoutType, design)}
|
|
295
482
|
|
|
296
483
|
═══ DESIGN RULES ═══
|
|
297
|
-
1. Title: 48-64px bold. Body:
|
|
484
|
+
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.
|
|
298
485
|
2. Use gradients, box-shadow (0 4px 20px rgba(0,0,0,0.06)), border-radius (12-20px)
|
|
299
|
-
3.
|
|
300
|
-
4.
|
|
301
|
-
⚠ If content_direction lists
|
|
302
|
-
5.
|
|
303
|
-
6.
|
|
304
|
-
7.
|
|
305
|
-
8.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
486
|
+
3. Follow the REQUIRED LAYOUT specified above exactly. Do NOT substitute a different layout type.
|
|
487
|
+
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.
|
|
488
|
+
⚠ If content_direction lists 5+ items, group them into 4 cards (combine related items).
|
|
489
|
+
5. ⚠⚠⚠ NEVER use position:absolute. ALL layout MUST use flexbox/grid. position:absolute is STRIPPED by the renderer.
|
|
490
|
+
6. ⚠ NEVER use hub-and-spoke or center-circle layouts (a circle in the center with items around it). The overlapping circle ALWAYS covers text. Use a simple card grid instead.
|
|
491
|
+
7. ⚠ MAXIMUM 4 columns in any grid layout. 5+ columns = text too small. Use 2-3 columns + 2 rows instead.
|
|
492
|
+
8. PIE/DONUT: conic-gradient on border-radius:50% div ONLY. No clip-path or rotated divs.
|
|
493
|
+
9. Bar chart labels: min-width:120px, text-align:center. Never let labels wrap per-character.
|
|
494
|
+
10. ⚠ NO LINE CHARTS in CSS — lines/dots never align correctly. Use bar charts (flex-end alignment) or donut charts (conic-gradient) instead.
|
|
495
|
+
11. CSS CHARTS THAT WORK: vertical bar charts (flex-end + height%), horizontal bar charts (width%), donut/pie charts (conic-gradient + border-radius:50%), progress bars (width%). Use these liberally for data visualization.
|
|
496
|
+
12. Table headers: dark background (${design.primary_color}) with WHITE text. Never transparent.
|
|
309
497
|
|
|
310
498
|
═══ SPACE UTILIZATION — NO DEAD ZONES ═══
|
|
311
|
-
⚠ Content MUST be distributed across the full 1080px height. No section should be >
|
|
499
|
+
⚠ Content MUST be distributed across the full 1080px height. No section should be >150px of empty whitespace.
|
|
312
500
|
⚠ If using a colored header/banner area, it MUST NOT exceed 20% of slide height (216px max).
|
|
313
501
|
⚠ The main content area (.content with flex:1) must contain the MAJORITY of visible information.
|
|
314
|
-
⚠
|
|
502
|
+
⚠ Cards stretch to fill height via align-items:stretch from parent — but they MUST have DENSE content inside (see CARD CONTENT DENSITY above).
|
|
503
|
+
⚠ NEVER use justify-content:center or align-items:center on .content — it bunches content in the center with empty space around it.
|
|
504
|
+
⚠ NEVER use justify-content:space-between on individual cards — it creates huge gaps between sparse items. Use flex-start + gap instead.
|
|
505
|
+
⚠ Cards in a row: parent uses align-items:stretch (default) so cards fill the full height.
|
|
506
|
+
|
|
507
|
+
═══ TABLE COMPLETENESS — MANDATORY ═══
|
|
508
|
+
⚠ If you create a table, EVERY cell MUST contain real data. Empty cells = FAILURE.
|
|
509
|
+
⚠ For comparison tables: fill ALL columns for ALL competitors/items with realistic data.
|
|
510
|
+
⚠ Example: 5-company comparison → all 5 columns must have data in every row.
|
|
511
|
+
⚠ NEVER create a table with only 1 column filled — that's not a comparison, it's a list.
|
|
315
512
|
|
|
316
513
|
═══ CONTENT RULES ═══
|
|
317
514
|
• ${langRule}
|
|
318
515
|
• Korean text only — never Chinese characters (漢字), Japanese, or other scripts
|
|
319
516
|
• Generate REAL, specific content — no placeholders
|
|
320
|
-
•
|
|
321
|
-
•
|
|
322
|
-
• ALL content MUST fit within 1080px height.
|
|
517
|
+
• BALANCE density and readability — fill 80-90% of the slide area with meaningful content. Avoid both sparse slides and overloaded slides.
|
|
518
|
+
• If the user specifies a year in their request, USE THAT YEAR faithfully. Only default to ${new Date().getFullYear()} for content with no explicit year.
|
|
519
|
+
• ALL content MUST fit within 1080px height. Use compact padding (24-32px) and dense content. Fill 85-95% of slide area.
|
|
520
|
+
• Each section: 3-5 detailed bullet points with specific data. Dense content beats wide padding.
|
|
521
|
+
• body MUST set font-size: 26px as the base. All text inherits at least 26px unless explicitly larger.
|
|
323
522
|
|
|
324
523
|
═══ PAGE NUMBER ═══
|
|
325
524
|
Bottom-right: "${slideIndex + 1}" (12px, opacity 0.4)
|
|
326
525
|
|
|
327
526
|
Output the complete HTML document now. Start with <!DOCTYPE html> and end with </html>.`;
|
|
328
527
|
}
|
|
528
|
+
export function buildContentFillJsonPrompt(slideTitle, contentDirection, layoutType, language) {
|
|
529
|
+
const langRule = language === 'ko'
|
|
530
|
+
? 'ALL text MUST be in Korean. Write naturally in Korean. Never use Chinese characters (漢字).'
|
|
531
|
+
: 'ALL text MUST be in English.';
|
|
532
|
+
const schemas = {
|
|
533
|
+
cards: `{
|
|
534
|
+
"cards": [
|
|
535
|
+
{ "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% 정확도)" }
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
RULES: 3-4 cards. Each: icon + title + 3 bullets (MAX 3, keep each under 25 chars) + stat.`,
|
|
539
|
+
bar_chart: `{
|
|
540
|
+
"bars": [
|
|
541
|
+
{ "label": "Category name", "value": "Display value (e.g. 1,250억원)", "height": 85 }
|
|
542
|
+
],
|
|
543
|
+
"insight": "1-2 sentence key insight about the data"
|
|
544
|
+
}
|
|
545
|
+
RULES: 4-5 bars (MAX 5). height: 10-90 (tallest=85, others proportional). Values with units. Keep labels under 15 chars.`,
|
|
546
|
+
donut_chart: `{
|
|
547
|
+
"segments": [
|
|
548
|
+
{ "label": "Segment name", "value": "Display value (e.g. 562억원)", "percent": 45 }
|
|
549
|
+
],
|
|
550
|
+
"centerText": "Center label (e.g. 총 1,250억원)",
|
|
551
|
+
"summary": "1-2 sentence summary"
|
|
552
|
+
}
|
|
553
|
+
RULES: 3-5 segments. Percents MUST sum to exactly 100.`,
|
|
554
|
+
table: `{
|
|
555
|
+
"headers": ["Company Name", "Market Share", "Key Strength", "Accuracy"],
|
|
556
|
+
"rows": [["data", "data", "data", "data"]],
|
|
557
|
+
"highlightRow": 0,
|
|
558
|
+
"summary": "1-2 sentence summary"
|
|
559
|
+
}
|
|
560
|
+
RULES: 3-4 columns. 4-5 rows. ALL cells real data. highlightRow: 0-indexed or null.
|
|
561
|
+
⚠ headers MUST be meaningful column names describing the data — NEVER use generic "Column 1", "Column 2" etc.`,
|
|
562
|
+
process_flow: `{
|
|
563
|
+
"steps": [
|
|
564
|
+
{ "title": "Step name (2-4 words)", "desc": "2-3 sentence description with details", "detail": "Duration or metric" }
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
RULES: 3-4 steps (MAX 4). Each: title + detailed description + time/metric.`,
|
|
568
|
+
big_numbers: `{
|
|
569
|
+
"metrics": [
|
|
570
|
+
{ "value": "1,250", "unit": "억원", "label": "Metric Name", "desc": "1-2 sentence context", "trend": "▲ 15%", "positive": true }
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
RULES: 2-3 metrics. value=number only, unit=separate. trend: ▲/▼ + percentage.`,
|
|
574
|
+
timeline: `{
|
|
575
|
+
"milestones": [
|
|
576
|
+
{ "date": "2026 Q1", "title": "Milestone name", "desc": "2-3 sentence description", "kpi": "Target metric" }
|
|
577
|
+
]
|
|
578
|
+
}
|
|
579
|
+
RULES: 3 milestones (MAX 3). Each with date, description, and KPI target.`,
|
|
580
|
+
progress_bars: `{
|
|
581
|
+
"bars": [
|
|
582
|
+
{ "label": "Category name", "value": "Display (e.g. 92%)", "percent": 92, "detail": "Brief context" }
|
|
583
|
+
]
|
|
584
|
+
}
|
|
585
|
+
RULES: 4-6 bars. percent: 5-100. Include context detail for each.`,
|
|
586
|
+
hero_stat: `{
|
|
587
|
+
"number": "97.8",
|
|
588
|
+
"unit": "%",
|
|
589
|
+
"label": "Metric Name",
|
|
590
|
+
"context": "2-3 sentence context explaining significance",
|
|
591
|
+
"supporting": [
|
|
592
|
+
{ "value": "45개국", "label": "Supporting metric name" }
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
RULES: 1 hero number + context + 2-3 supporting metrics.`,
|
|
596
|
+
two_col_split: `{
|
|
597
|
+
"leftTitle": "Left column heading",
|
|
598
|
+
"leftItems": [
|
|
599
|
+
{ "label": "Item name", "value": "Item value with data" }
|
|
600
|
+
],
|
|
601
|
+
"rightTitle": "Right column heading",
|
|
602
|
+
"rightBullets": ["Detailed bullet point with data"]
|
|
603
|
+
}
|
|
604
|
+
RULES: Left: 3-4 key-value items. Right: 3-4 detailed bullets. Keep each bullet under 30 chars.`,
|
|
605
|
+
};
|
|
606
|
+
return `Extract content from the direction below and output ONLY valid JSON.
|
|
607
|
+
Do NOT output markdown fences, explanations, or anything besides the JSON object.
|
|
608
|
+
|
|
609
|
+
SLIDE TITLE: "${slideTitle}"
|
|
610
|
+
LAYOUT TYPE: ${layoutType}
|
|
611
|
+
|
|
612
|
+
CONTENT DIRECTION:
|
|
613
|
+
${contentDirection}
|
|
614
|
+
|
|
615
|
+
REQUIRED JSON SCHEMA:
|
|
616
|
+
${schemas[layoutType]}
|
|
617
|
+
|
|
618
|
+
${langRule}
|
|
619
|
+
Use SPECIFIC numbers, names, percentages from the content direction.
|
|
620
|
+
Each text field must be substantive (not 1-2 generic words).
|
|
621
|
+
If the content direction lacks specific data, generate realistic professional data that fits the topic.
|
|
622
|
+
|
|
623
|
+
Output the JSON object now:`;
|
|
624
|
+
}
|
|
625
|
+
export function parseContentFillJson(raw, layoutType) {
|
|
626
|
+
let cleaned = raw.trim();
|
|
627
|
+
if (cleaned.startsWith('```')) {
|
|
628
|
+
cleaned = cleaned.replace(/^```(?:json|JSON)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
629
|
+
}
|
|
630
|
+
const firstBrace = cleaned.indexOf('{');
|
|
631
|
+
const lastBrace = cleaned.lastIndexOf('}');
|
|
632
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
633
|
+
cleaned = cleaned.slice(firstBrace, lastBrace + 1);
|
|
634
|
+
}
|
|
635
|
+
let parsed = null;
|
|
636
|
+
try {
|
|
637
|
+
parsed = JSON.parse(cleaned);
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
try {
|
|
641
|
+
const repaired = cleaned.replace(/,\s*([}\]])/g, '$1');
|
|
642
|
+
parsed = JSON.parse(repaired);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!parsed)
|
|
649
|
+
return null;
|
|
650
|
+
const jsonStr = JSON.stringify(parsed);
|
|
651
|
+
const ellipsisMatches = jsonStr.match(/"\.\.\."|\u2026/g) || [];
|
|
652
|
+
const totalStringValues = jsonStr.match(/"[^"]+"/g) || [];
|
|
653
|
+
if (ellipsisMatches.length > 0 && totalStringValues.length > 0) {
|
|
654
|
+
const ellipsisRatio = ellipsisMatches.length / totalStringValues.length;
|
|
655
|
+
if (ellipsisRatio > 0.3) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
switch (layoutType) {
|
|
660
|
+
case 'cards':
|
|
661
|
+
if (!Array.isArray(parsed['cards']) || parsed['cards'].length === 0)
|
|
662
|
+
return null;
|
|
663
|
+
break;
|
|
664
|
+
case 'bar_chart':
|
|
665
|
+
if (!Array.isArray(parsed['bars']) || parsed['bars'].length === 0)
|
|
666
|
+
return null;
|
|
667
|
+
break;
|
|
668
|
+
case 'donut_chart':
|
|
669
|
+
if (!Array.isArray(parsed['segments']) || parsed['segments'].length === 0)
|
|
670
|
+
return null;
|
|
671
|
+
break;
|
|
672
|
+
case 'table':
|
|
673
|
+
if (!Array.isArray(parsed['headers']) || !Array.isArray(parsed['rows']))
|
|
674
|
+
return null;
|
|
675
|
+
break;
|
|
676
|
+
case 'process_flow':
|
|
677
|
+
if (!Array.isArray(parsed['steps']) || parsed['steps'].length === 0)
|
|
678
|
+
return null;
|
|
679
|
+
break;
|
|
680
|
+
case 'big_numbers':
|
|
681
|
+
if (!Array.isArray(parsed['metrics']) || parsed['metrics'].length === 0)
|
|
682
|
+
return null;
|
|
683
|
+
break;
|
|
684
|
+
case 'timeline':
|
|
685
|
+
if (!Array.isArray(parsed['milestones']) || parsed['milestones'].length === 0)
|
|
686
|
+
return null;
|
|
687
|
+
break;
|
|
688
|
+
case 'progress_bars':
|
|
689
|
+
if (!Array.isArray(parsed['bars']) || parsed['bars'].length === 0)
|
|
690
|
+
return null;
|
|
691
|
+
break;
|
|
692
|
+
case 'hero_stat':
|
|
693
|
+
if (!parsed['number'])
|
|
694
|
+
return null;
|
|
695
|
+
break;
|
|
696
|
+
case 'two_col_split':
|
|
697
|
+
if (!parsed['leftTitle'] && !parsed['rightTitle'])
|
|
698
|
+
return null;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
return parsed;
|
|
702
|
+
}
|
|
703
|
+
function getCardTextColor(design) {
|
|
704
|
+
const hex = (design.background_color || '#f8f9fa').replace('#', '');
|
|
705
|
+
const r = parseInt(hex.slice(0, 2), 16) || 200;
|
|
706
|
+
const g = parseInt(hex.slice(2, 4), 16) || 200;
|
|
707
|
+
const b = parseInt(hex.slice(4, 6), 16) || 200;
|
|
708
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
709
|
+
return luminance < 0.5 ? '#2D3748' : design.text_color;
|
|
710
|
+
}
|
|
711
|
+
function escapeHtmlTemplate(text) {
|
|
712
|
+
return text
|
|
713
|
+
.replace(/&/g, '&')
|
|
714
|
+
.replace(/</g, '<')
|
|
715
|
+
.replace(/>/g, '>')
|
|
716
|
+
.replace(/"/g, '"');
|
|
717
|
+
}
|
|
718
|
+
function wrapSlide(design, title, slideNum, contentCss, contentHtml, variant = 0) {
|
|
719
|
+
const bgVariants = [
|
|
720
|
+
`background:${design.background_color};`,
|
|
721
|
+
`background:linear-gradient(160deg,${design.background_color} 0%,${design.accent_light}40 100%);`,
|
|
722
|
+
`background:${design.background_color};`,
|
|
723
|
+
];
|
|
724
|
+
const bodyBg = bgVariants[variant % bgVariants.length];
|
|
725
|
+
const accentBar = variant % 3 === 2
|
|
726
|
+
? `<div style="height:4px;background:linear-gradient(90deg,${design.accent_color},${design.primary_color});margin-bottom:12px;border-radius:2px;flex-shrink:0"></div>`
|
|
727
|
+
: '';
|
|
728
|
+
return `<!DOCTYPE html>
|
|
729
|
+
<html lang="ko">
|
|
730
|
+
<head><meta charset="UTF-8"><style>
|
|
731
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
732
|
+
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}
|
|
733
|
+
body{display:flex;flex-direction:column;padding:60px 80px;height:1080px;${bodyBg}color:${design.text_color};font-size:26px}
|
|
734
|
+
.slide-title{flex:0 0 auto;margin-bottom:20px}
|
|
735
|
+
.slide-title h1{font-size:48px;font-weight:700;color:${design.primary_color};font-family:"${design.font_title}","Segoe UI",sans-serif}
|
|
736
|
+
.title-bar{width:80px;height:3px;background:${design.accent_color};border-radius:2px;margin-top:12px}
|
|
737
|
+
.page-number{position:absolute;bottom:20px;right:80px;font-size:12px;opacity:0.4}
|
|
738
|
+
${contentCss}
|
|
739
|
+
</style></head>
|
|
740
|
+
<body>
|
|
741
|
+
<div class="slide-title">${accentBar}<h1>${escapeHtmlTemplate(title)}</h1><div class="title-bar"></div></div>
|
|
742
|
+
${contentHtml}
|
|
743
|
+
<div class="page-number">${slideNum}</div>
|
|
744
|
+
</body></html>`;
|
|
745
|
+
}
|
|
746
|
+
function buildCardsContent(design, data) {
|
|
747
|
+
const n = Math.min((data.cards || []).length, 4);
|
|
748
|
+
const cols = n <= 3 ? `repeat(${n},1fr)` : '1fr 1fr';
|
|
749
|
+
const is2x2 = n === 4;
|
|
750
|
+
const gridExtra = is2x2 ? 'grid-template-rows:1fr 1fr' : 'align-content:center';
|
|
751
|
+
const cardPad = is2x2 ? '28px 24px' : '40px 32px';
|
|
752
|
+
const cardGap = is2x2 ? '10px' : '16px';
|
|
753
|
+
const h2Size = is2x2 ? '32px' : '36px';
|
|
754
|
+
const liSize = is2x2 ? '26px' : '28px';
|
|
755
|
+
const liMargin = is2x2 ? '8px' : '12px';
|
|
756
|
+
const cardText = getCardTextColor(design);
|
|
757
|
+
const css = `.content{flex:1;display:grid;grid-template-columns:${cols};${gridExtra};gap:24px}
|
|
758
|
+
.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}}
|
|
759
|
+
.card-icon{font-size:36px;line-height:1}
|
|
760
|
+
.card h2{font-size:${h2Size};font-weight:700;color:${design.primary_color}}
|
|
761
|
+
.card ul{list-style:none;flex:1}
|
|
762
|
+
.card li{margin-bottom:${liMargin};padding-left:22px;position:relative;font-size:${liSize};line-height:1.4}
|
|
763
|
+
.card li::before{content:"•";color:${design.accent_color};position:absolute;left:0;font-weight:bold}
|
|
764
|
+
.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}}`;
|
|
765
|
+
const maxBullets = 3;
|
|
766
|
+
const cards = (data.cards || []).slice(0, 4).map(c => `
|
|
767
|
+
<div class="card">
|
|
768
|
+
<div class="card-icon">${c.icon || '📌'}</div>
|
|
769
|
+
<h2>${escapeHtmlTemplate(c.title || '')}</h2>
|
|
770
|
+
<ul>${(c.bullets || []).slice(0, maxBullets).map(b => `<li>${escapeHtmlTemplate(b)}</li>`).join('')}</ul>
|
|
771
|
+
${c.stat ? `<div class="card-stat">${escapeHtmlTemplate(c.stat)}</div>` : ''}
|
|
772
|
+
</div>`).join('');
|
|
773
|
+
return { css, html: `<div class="content">${cards}\n</div>` };
|
|
774
|
+
}
|
|
775
|
+
function buildBarChartContent(design, data) {
|
|
776
|
+
const cardText = getCardTextColor(design);
|
|
777
|
+
const css = `.content{flex:1;display:flex;flex-direction:column}
|
|
778
|
+
.chart-area{flex:1;display:flex;align-items:stretch;gap:32px;padding:20px 60px 0}
|
|
779
|
+
.bar-group{flex:1;display:flex;flex-direction:column;align-items:center}
|
|
780
|
+
.bar-spacer{flex:1}
|
|
781
|
+
.bar-value{font-size:28px;font-weight:700;color:${design.primary_color};margin-bottom:8px;flex-shrink:0}
|
|
782
|
+
.bar{width:80%;border-radius:8px 8px 0 0;background:linear-gradient(180deg,${design.accent_color},${design.primary_color});flex-shrink:0}
|
|
783
|
+
.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}
|
|
784
|
+
.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}`;
|
|
785
|
+
const maxH = Math.max(...(data.bars || []).map(b => b.height || 50), 1);
|
|
786
|
+
const bars = (data.bars || []).slice(0, 5).map(b => {
|
|
787
|
+
const normalized = Math.max(8, Math.round(((b.height || 50) / maxH) * 85));
|
|
788
|
+
return `
|
|
789
|
+
<div class="bar-group">
|
|
790
|
+
<div class="bar-spacer"></div>
|
|
791
|
+
<div class="bar-value">${escapeHtmlTemplate(b.value || '')}</div>
|
|
792
|
+
<div class="bar" style="height:${normalized}%"></div>
|
|
793
|
+
<div class="bar-label">${escapeHtmlTemplate(b.label || '')}</div>
|
|
794
|
+
</div>`;
|
|
795
|
+
}).join('');
|
|
796
|
+
return {
|
|
797
|
+
css,
|
|
798
|
+
html: `<div class="content">
|
|
799
|
+
<div class="chart-area">${bars}</div>
|
|
800
|
+
<div class="insight-row">${escapeHtmlTemplate(data.insight || '')}</div>
|
|
801
|
+
</div>`,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function buildDonutChartContent(design, data) {
|
|
805
|
+
const segColors = [design.primary_color, design.accent_color, design.gradient_end, '#FF6B6B', '#4ECDC4', '#45B7D1'];
|
|
806
|
+
const segs = (data.segments || []).slice(0, 6);
|
|
807
|
+
const totalPct = segs.reduce((s, seg) => s + (seg.percent || 0), 0);
|
|
808
|
+
let cumPct = 0;
|
|
809
|
+
const stops = segs.map((s, i) => {
|
|
810
|
+
const start = cumPct;
|
|
811
|
+
const pct = totalPct > 0 ? (s.percent / totalPct) * 100 : 100 / segs.length;
|
|
812
|
+
cumPct += pct;
|
|
813
|
+
return `${segColors[i % segColors.length]} ${start.toFixed(1)}% ${cumPct.toFixed(1)}%`;
|
|
814
|
+
}).join(',');
|
|
815
|
+
const css = `.content{flex:1;display:grid;grid-template-columns:1.2fr 1fr;gap:40px;align-items:center;align-content:center}
|
|
816
|
+
.donut-wrap{display:flex;justify-content:center;align-items:center}
|
|
817
|
+
.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)}
|
|
818
|
+
.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}
|
|
819
|
+
.donut-center{font-size:30px;font-weight:800;color:${design.primary_color};text-align:center;line-height:1.3}
|
|
820
|
+
.legend{display:flex;flex-direction:column;gap:28px}
|
|
821
|
+
.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)}}
|
|
822
|
+
.legend-dot{width:24px;height:24px;border-radius:50%;flex-shrink:0}
|
|
823
|
+
.legend-value{font-weight:700;color:${design.primary_color};margin-left:auto;white-space:nowrap}
|
|
824
|
+
.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}`;
|
|
825
|
+
const legendHtml = segs.map((s, i) => `
|
|
826
|
+
<div class="legend-item">
|
|
827
|
+
<div class="legend-dot" style="background:${segColors[i % segColors.length]}"></div>
|
|
828
|
+
<span>${escapeHtmlTemplate(s.label || '')}</span>
|
|
829
|
+
<span class="legend-value">${escapeHtmlTemplate(s.value || '')} (${s.percent}%)</span>
|
|
830
|
+
</div>`).join('');
|
|
831
|
+
return {
|
|
832
|
+
css,
|
|
833
|
+
html: `<div class="content">
|
|
834
|
+
<div class="donut-wrap">
|
|
835
|
+
<div class="donut" style="background:conic-gradient(${stops})">
|
|
836
|
+
<div class="donut-hole"><div class="donut-center">${escapeHtmlTemplate(data.centerText || '')}</div></div>
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
<div class="legend">${legendHtml}</div>
|
|
840
|
+
${data.summary ? `<div class="chart-summary">${escapeHtmlTemplate(data.summary)}</div>` : ''}
|
|
841
|
+
</div>`,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function buildTableContent(design, data) {
|
|
845
|
+
const cardText = getCardTextColor(design);
|
|
846
|
+
const maxCols = Math.min((data.headers || []).length, 4);
|
|
847
|
+
const css = `.content{flex:1;display:flex;flex-direction:column;justify-content:center}
|
|
848
|
+
.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}
|
|
849
|
+
.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}
|
|
850
|
+
.data-table td{font-size:28px;color:${cardText};padding:22px 28px;border-bottom:1px solid ${design.accent_light};overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
851
|
+
.data-table tr:last-child td{border-bottom:none}
|
|
852
|
+
.data-table tr:nth-child(even){background:${design.accent_light}20}
|
|
853
|
+
.data-table tr.highlight td{background:${design.accent_color}15;font-weight:600}
|
|
854
|
+
.table-summary{margin-top:20px;padding:16px 24px;background:${design.accent_light};border-radius:12px;font-size:26px;color:${cardText};line-height:1.5}`;
|
|
855
|
+
const headers = (data.headers || []).slice(0, maxCols).map(h => `<th>${escapeHtmlTemplate(String(h || '').slice(0, 25))}</th>`).join('');
|
|
856
|
+
const rows = (data.rows || []).slice(0, 5).map((row, ri) => {
|
|
857
|
+
const cls = ri === data.highlightRow ? ' class="highlight"' : '';
|
|
858
|
+
const cells = (row || []).slice(0, maxCols).map(c => `<td>${escapeHtmlTemplate(String(c || '').slice(0, 35))}</td>`).join('');
|
|
859
|
+
return `<tr${cls}>${cells}</tr>`;
|
|
860
|
+
}).join('\n');
|
|
861
|
+
return {
|
|
862
|
+
css,
|
|
863
|
+
html: `<div class="content">
|
|
864
|
+
<table class="data-table"><thead><tr>${headers}</tr></thead><tbody>\n${rows}\n</tbody></table>
|
|
865
|
+
${data.summary ? `<div class="table-summary">${escapeHtmlTemplate(data.summary)}</div>` : ''}
|
|
866
|
+
</div>`,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function buildProcessFlowContent(design, data) {
|
|
870
|
+
const n = Math.min((data.steps || []).length, 4);
|
|
871
|
+
const cardText = getCardTextColor(design);
|
|
872
|
+
const titleSz = n <= 3 ? '34px' : '32px';
|
|
873
|
+
const descSz = n <= 3 ? '28px' : '26px';
|
|
874
|
+
const detailSz = n <= 3 ? '28px' : '26px';
|
|
875
|
+
const css = `.content{flex:1;display:flex;align-items:stretch;gap:0}
|
|
876
|
+
.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)}
|
|
877
|
+
.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}
|
|
878
|
+
.step-title{font-size:${titleSz};font-weight:700;color:${design.primary_color}}
|
|
879
|
+
.step-desc{font-size:${descSz};color:${cardText};line-height:1.5;flex:1}
|
|
880
|
+
.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}
|
|
881
|
+
.arrow{width:48px;display:flex;align-items:center;justify-content:center;font-size:40px;color:${design.accent_color};flex-shrink:0}`;
|
|
882
|
+
const steps = (data.steps || []).slice(0, 4);
|
|
883
|
+
const stepsHtml = steps.map((s, i) => {
|
|
884
|
+
const stepDiv = `
|
|
885
|
+
<div class="step">
|
|
886
|
+
<div class="step-num">${i + 1}</div>
|
|
887
|
+
<div class="step-title">${escapeHtmlTemplate(s.title || '')}</div>
|
|
888
|
+
<div class="step-desc">${escapeHtmlTemplate(s.desc || '')}</div>
|
|
889
|
+
${s.detail ? `<div class="step-detail">${escapeHtmlTemplate(s.detail)}</div>` : ''}
|
|
890
|
+
</div>`;
|
|
891
|
+
return i < steps.length - 1 ? stepDiv + '\n <div class="arrow">→</div>' : stepDiv;
|
|
892
|
+
}).join('');
|
|
893
|
+
return { css, html: `<div class="content">${stepsHtml}\n</div>` };
|
|
894
|
+
}
|
|
895
|
+
function buildBigNumbersContent(design, data) {
|
|
896
|
+
const cardText = getCardTextColor(design);
|
|
897
|
+
const css = `.content{flex:1;display:flex;gap:40px;align-items:stretch}
|
|
898
|
+
.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}}
|
|
899
|
+
.metric-value{font-size:100px;font-weight:800;color:${design.primary_color};line-height:1}
|
|
900
|
+
.metric-unit{font-size:36px;font-weight:600;color:${design.accent_color}}
|
|
901
|
+
.metric-label{font-size:32px;font-weight:600;color:${cardText}}
|
|
902
|
+
.metric-desc{font-size:26px;color:${cardText}aa;line-height:1.5;max-width:90%}
|
|
903
|
+
.metric-trend{font-size:28px;font-weight:600;margin-top:auto}`;
|
|
904
|
+
const metrics = (data.metrics || []).slice(0, 3).map(m => `
|
|
905
|
+
<div class="metric-card">
|
|
906
|
+
<div class="metric-value">${escapeHtmlTemplate(m.value || '')}</div>
|
|
907
|
+
<div class="metric-unit">${escapeHtmlTemplate(m.unit || '')}</div>
|
|
908
|
+
<div class="metric-label">${escapeHtmlTemplate(m.label || '')}</div>
|
|
909
|
+
<div class="metric-desc">${escapeHtmlTemplate(m.desc || '')}</div>
|
|
910
|
+
<div class="metric-trend" style="color:${m.positive !== false ? '#10B981' : '#EF4444'}">${escapeHtmlTemplate(m.trend || '')}</div>
|
|
911
|
+
</div>`).join('');
|
|
912
|
+
return { css, html: `<div class="content">${metrics}\n</div>` };
|
|
913
|
+
}
|
|
914
|
+
function buildTimelineContent(design, data) {
|
|
915
|
+
const titleSz = '38px';
|
|
916
|
+
const descSz = '30px';
|
|
917
|
+
const kpiSz = '28px';
|
|
918
|
+
const pad = '40px 36px';
|
|
919
|
+
const css = `.content{flex:1;display:flex;align-items:stretch;gap:20px}
|
|
920
|
+
.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}
|
|
921
|
+
.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}
|
|
922
|
+
.ms-title{font-size:${titleSz};font-weight:700;color:${design.primary_color}}
|
|
923
|
+
.ms-desc{font-size:${descSz};color:${getCardTextColor(design)};line-height:1.55;flex:1}
|
|
924
|
+
.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}`;
|
|
925
|
+
const milestones = (data.milestones || []).slice(0, 3).map((m) => `
|
|
926
|
+
<div class="milestone">
|
|
927
|
+
<div class="ms-date">${escapeHtmlTemplate(m.date || '')}</div>
|
|
928
|
+
<div class="ms-title">${escapeHtmlTemplate(m.title || '')}</div>
|
|
929
|
+
<div class="ms-desc">${escapeHtmlTemplate(m.desc || '')}</div>
|
|
930
|
+
${m.kpi ? `<div class="ms-kpi">${escapeHtmlTemplate(m.kpi)}</div>` : ''}
|
|
931
|
+
</div>`).join('');
|
|
932
|
+
return { css, html: `<div class="content">${milestones}\n</div>` };
|
|
933
|
+
}
|
|
934
|
+
function buildProgressBarsContent(design, data) {
|
|
935
|
+
const css = `.content{flex:1;display:flex;flex-direction:column;justify-content:space-evenly;gap:0;padding:20px 0}
|
|
936
|
+
.bar-item{display:flex;flex-direction:column;gap:10px}
|
|
937
|
+
.bar-header{display:flex;justify-content:space-between;align-items:baseline}
|
|
938
|
+
.bar-label{font-size:28px;font-weight:600;color:${design.text_color}}
|
|
939
|
+
.bar-val{font-size:28px;font-weight:700;color:${design.primary_color}}
|
|
940
|
+
.bar-track{width:100%;height:44px;background:${design.accent_light};border-radius:22px;overflow:hidden}
|
|
941
|
+
.bar-fill{height:100%;border-radius:22px;background:linear-gradient(90deg,${design.primary_color},${design.accent_color})}
|
|
942
|
+
.bar-detail{font-size:24px;color:${design.text_color}88;margin-top:-2px}`;
|
|
943
|
+
const bars = (data.bars || []).slice(0, 6).map(b => `
|
|
944
|
+
<div class="bar-item">
|
|
945
|
+
<div class="bar-header"><span class="bar-label">${escapeHtmlTemplate(b.label || '')}</span><span class="bar-val">${escapeHtmlTemplate(b.value || '')}</span></div>
|
|
946
|
+
<div class="bar-track"><div class="bar-fill" style="width:${Math.max(5, Math.min(100, b.percent || 50))}%"></div></div>
|
|
947
|
+
${b.detail ? `<div class="bar-detail">${escapeHtmlTemplate(b.detail)}</div>` : ''}
|
|
948
|
+
</div>`).join('');
|
|
949
|
+
return { css, html: `<div class="content">${bars}\n</div>` };
|
|
950
|
+
}
|
|
951
|
+
function buildHeroStatContent(design, data) {
|
|
952
|
+
const css = `.content{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:24px}
|
|
953
|
+
.hero-number{font-size:128px;font-weight:900;color:${design.primary_color};line-height:1}
|
|
954
|
+
.hero-unit{font-size:48px;font-weight:600;color:${design.accent_color}}
|
|
955
|
+
.hero-label{font-size:36px;font-weight:600;color:${design.text_color}}
|
|
956
|
+
.hero-context{font-size:28px;color:${design.text_color}aa;text-align:center;max-width:800px;line-height:1.6}
|
|
957
|
+
.supporting-row{display:flex;gap:60px;margin-top:48px}
|
|
958
|
+
.sup-item{text-align:center}
|
|
959
|
+
.sup-value{font-size:40px;font-weight:700;color:${design.primary_color}}
|
|
960
|
+
.sup-label{font-size:24px;color:${design.text_color}aa;margin-top:8px}`;
|
|
961
|
+
const supporting = (data.supporting || []).slice(0, 3).map(s => `
|
|
962
|
+
<div class="sup-item">
|
|
963
|
+
<div class="sup-value">${escapeHtmlTemplate(s.value || '')}</div>
|
|
964
|
+
<div class="sup-label">${escapeHtmlTemplate(s.label || '')}</div>
|
|
965
|
+
</div>`).join('');
|
|
966
|
+
return {
|
|
967
|
+
css,
|
|
968
|
+
html: `<div class="content">
|
|
969
|
+
<div class="hero-number">${escapeHtmlTemplate(data.number || '')}</div>
|
|
970
|
+
<div class="hero-unit">${escapeHtmlTemplate(data.unit || '')}</div>
|
|
971
|
+
<div class="hero-label">${escapeHtmlTemplate(data.label || '')}</div>
|
|
972
|
+
<div class="hero-context">${escapeHtmlTemplate(data.context || '')}</div>
|
|
973
|
+
${supporting ? `<div class="supporting-row">${supporting}</div>` : ''}
|
|
974
|
+
</div>`,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function buildTwoColSplitContent(design, data) {
|
|
978
|
+
const css = `.content{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:stretch}
|
|
979
|
+
.col{display:flex;flex-direction:column;gap:14px;justify-content:space-evenly}
|
|
980
|
+
.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}}
|
|
981
|
+
.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)}}
|
|
982
|
+
.kv-label{color:${getCardTextColor(design)};font-weight:500}
|
|
983
|
+
.kv-value{color:${design.primary_color};font-weight:700;font-size:28px}
|
|
984
|
+
.col ul{list-style:none;display:flex;flex-direction:column;gap:10px}
|
|
985
|
+
.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)}}
|
|
986
|
+
.col li::before{content:"•";color:${design.accent_color};position:absolute;left:12px;font-weight:bold}`;
|
|
987
|
+
const leftItems = (data.leftItems || []).slice(0, 4).map(item => `
|
|
988
|
+
<div class="kv-item"><span class="kv-label">${escapeHtmlTemplate(item.label || '')}</span><span class="kv-value">${escapeHtmlTemplate(item.value || '')}</span></div>`).join('');
|
|
989
|
+
const rightBullets = (data.rightBullets || []).slice(0, 4).map(b => `<li>${escapeHtmlTemplate(b)}</li>`).join('');
|
|
990
|
+
return {
|
|
991
|
+
css,
|
|
992
|
+
html: `<div class="content">
|
|
993
|
+
<div class="col">
|
|
994
|
+
<h2>${escapeHtmlTemplate(data.leftTitle || '')}</h2>
|
|
995
|
+
${leftItems}
|
|
996
|
+
</div>
|
|
997
|
+
<div class="col">
|
|
998
|
+
<h2>${escapeHtmlTemplate(data.rightTitle || '')}</h2>
|
|
999
|
+
<ul>${rightBullets}</ul>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>`,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
export function buildContentSlideHtml(design, title, layoutType, data, slideNum, variant = 0) {
|
|
1005
|
+
const builders = {
|
|
1006
|
+
cards: (d, dt) => buildCardsContent(d, dt),
|
|
1007
|
+
bar_chart: (d, dt) => buildBarChartContent(d, dt),
|
|
1008
|
+
donut_chart: (d, dt) => buildDonutChartContent(d, dt),
|
|
1009
|
+
table: (d, dt) => buildTableContent(d, dt),
|
|
1010
|
+
process_flow: (d, dt) => buildProcessFlowContent(d, dt),
|
|
1011
|
+
big_numbers: (d, dt) => buildBigNumbersContent(d, dt),
|
|
1012
|
+
timeline: (d, dt) => buildTimelineContent(d, dt),
|
|
1013
|
+
progress_bars: (d, dt) => buildProgressBarsContent(d, dt),
|
|
1014
|
+
hero_stat: (d, dt) => buildHeroStatContent(d, dt),
|
|
1015
|
+
two_col_split: (d, dt) => buildTwoColSplitContent(d, dt),
|
|
1016
|
+
};
|
|
1017
|
+
const builder = builders[layoutType] || builders.cards;
|
|
1018
|
+
const content = builder(design, data);
|
|
1019
|
+
return wrapSlide(design, title, slideNum, content.css, content.html, variant);
|
|
1020
|
+
}
|
|
1021
|
+
function getLayoutSpecificCss(layoutType, design) {
|
|
1022
|
+
const layouts = {
|
|
1023
|
+
cards: `═══ REQUIRED LAYOUT: CARD GRID ═══
|
|
1024
|
+
⚠⚠⚠ You MUST create a CARD GRID layout. Using any other layout = FAILURE.
|
|
1025
|
+
|
|
1026
|
+
CSS:
|
|
1027
|
+
.content { flex:1; display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; gap:24px; }
|
|
1028
|
+
.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; }
|
|
1029
|
+
|
|
1030
|
+
⚠ 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.
|
|
1031
|
+
• 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.
|
|
1032
|
+
• If only 3 items: grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr; (all 3 in one row)
|
|
1033
|
+
• MINIMUM CARD CONTENT: title + 3 bullet points with numbers/data + bottom metric. A card with only 1 bullet = FAILURE.
|
|
1034
|
+
• Card bottom stat: use margin-top:auto to push it to card bottom, creating visual anchor.
|
|
1035
|
+
• ⚠ NEVER have empty grid cells. If you have 3 items, use 3 columns. If 4, use 2×2.`,
|
|
1036
|
+
bar_chart: `═══ REQUIRED LAYOUT: BAR CHART ═══
|
|
1037
|
+
⚠⚠⚠ You MUST create a CSS BAR CHART with vertical bars. Cards/numbers/tables = FAILURE.
|
|
1038
|
+
|
|
1039
|
+
CSS:
|
|
1040
|
+
.content { flex:1; display:flex; flex-direction:column; }
|
|
1041
|
+
.chart-area { flex:1; display:flex; align-items:flex-end; gap:32px; padding:40px 60px 20px; }
|
|
1042
|
+
.bar-group { flex:1; display:flex; flex-direction:column; align-items:center; gap:8px; }
|
|
1043
|
+
.bar-value { font-size:28px; font-weight:700; color:${design.primary_color}; }
|
|
1044
|
+
.bar { width:80%; border-radius:8px 8px 0 0; background:linear-gradient(180deg, ${design.accent_color}, ${design.primary_color}); min-height:20px; }
|
|
1045
|
+
.bar-label { font-size:26px; font-weight:600; color:${design.text_color}; text-align:center; min-width:120px; }
|
|
1046
|
+
.insight-row { padding:20px 60px; background:${design.accent_light}; border-radius:12px; margin-top:20px; font-size:26px; color:${design.text_color}; }
|
|
1047
|
+
|
|
1048
|
+
CRITICAL PATTERN — your .content div MUST contain this structure:
|
|
1049
|
+
<div class="content">
|
|
1050
|
+
<div class="chart-area">
|
|
1051
|
+
<div class="bar-group">
|
|
1052
|
+
<div class="bar-value">VALUE</div>
|
|
1053
|
+
<div class="bar" style="height:75%"></div>
|
|
1054
|
+
<div class="bar-label">LABEL</div>
|
|
1055
|
+
</div>
|
|
1056
|
+
<!-- 3-6 bar-groups like above -->
|
|
1057
|
+
</div>
|
|
1058
|
+
<div class="insight-row">KEY INSIGHT TEXT</div>
|
|
1059
|
+
</div>
|
|
1060
|
+
|
|
1061
|
+
⚠ Each bar MUST have style="height:XX%" (tallest=85%, others proportional). align-items:flex-end on .chart-area makes bars grow upward.
|
|
1062
|
+
⚠ The insight-row at bottom provides context and fills space. Be creative with the insight text.
|
|
1063
|
+
⚠⚠⚠ .chart-area and .insight-row MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
|
|
1064
|
+
donut_chart: `═══ REQUIRED LAYOUT: DONUT/PIE CHART ═══
|
|
1065
|
+
⚠⚠⚠ You MUST create a CSS DONUT CHART using conic-gradient. Cards/tables = FAILURE.
|
|
1066
|
+
|
|
1067
|
+
CSS:
|
|
1068
|
+
.content { flex:1; display:grid; grid-template-columns:1fr 1fr; gap:40px; align-items:center; }
|
|
1069
|
+
.donut-wrap { display:flex; justify-content:center; align-items:center; }
|
|
1070
|
+
.donut { width:400px; height:400px; border-radius:50%; display:flex; align-items:center; justify-content:center; }
|
|
1071
|
+
.donut-hole { width:200px; height:200px; border-radius:50%; background:${design.background_color}; display:flex; align-items:center; justify-content:center; }
|
|
1072
|
+
.donut-center { font-size:48px; font-weight:800; color:${design.primary_color}; }
|
|
1073
|
+
.legend { display:flex; flex-direction:column; gap:28px; }
|
|
1074
|
+
.legend-item { display:flex; align-items:center; gap:16px; font-size:28px; }
|
|
1075
|
+
.legend-dot { width:20px; height:20px; border-radius:50%; flex-shrink:0; }
|
|
1076
|
+
|
|
1077
|
+
CRITICAL PATTERN — the donut MUST use conic-gradient with ACTUAL data percentages:
|
|
1078
|
+
<div class="donut" style="background:conic-gradient(${design.primary_color} 0% 45%, ${design.accent_color} 45% 75%, ${design.gradient_end} 75% 100%);">
|
|
1079
|
+
|
|
1080
|
+
⚠ conic-gradient segments MUST add to 100%. Adjust percentages to match the ACTUAL DATA.
|
|
1081
|
+
⚠ Left: donut with center hole showing total/summary. Right: legend with colored dots + labels + values.`,
|
|
1082
|
+
table: `═══ REQUIRED LAYOUT: DATA TABLE ═══
|
|
1083
|
+
⚠⚠⚠ You MUST create a styled HTML TABLE with <table>/<th>/<td>. Cards = FAILURE.
|
|
1084
|
+
|
|
1085
|
+
CSS:
|
|
1086
|
+
.content { flex:1; display:flex; flex-direction:column; }
|
|
1087
|
+
table { width:100%; border-collapse:separate; border-spacing:0; border-radius:12px; overflow:hidden; flex:1; }
|
|
1088
|
+
th { padding:20px 24px; background:${design.primary_color}; color:#fff; font-size:26px; font-weight:700; text-align:left; }
|
|
1089
|
+
td { padding:20px 24px; font-size:26px; border-bottom:1px solid ${design.accent_light}; color:${design.text_color}; }
|
|
1090
|
+
tr:nth-child(even) td { background:${design.accent_light}40; }
|
|
1091
|
+
tr.highlight td { background:${design.accent_color}15; font-weight:600; }
|
|
1092
|
+
.summary-bar { margin-top:auto; padding:20px 24px; background:${design.accent_light}; border-radius:12px; font-size:26px; color:${design.text_color}; }
|
|
1093
|
+
|
|
1094
|
+
⚠ table uses flex:1 to stretch vertically. Use larger padding on td (28-32px) if fewer rows to fill space.
|
|
1095
|
+
⚠ Header: dark ${design.primary_color} background with WHITE text. 6-8 rows × 3-5 columns. EVERY cell real data.
|
|
1096
|
+
⚠ Add .summary-bar below table with margin-top:auto to anchor it at the bottom and fill remaining space.
|
|
1097
|
+
⚠⚠⚠ <table> and .summary-bar MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
|
|
1098
|
+
process_flow: `═══ REQUIRED LAYOUT: PROCESS FLOW ═══
|
|
1099
|
+
⚠⚠⚠ You MUST create a HORIZONTAL PROCESS FLOW with step boxes + arrows (→). Cards without arrows = FAILURE.
|
|
1100
|
+
|
|
1101
|
+
CSS:
|
|
1102
|
+
.content { flex:1; display:flex; align-items:stretch; gap:0; }
|
|
1103
|
+
.step { flex:1; display:flex; flex-direction:column; align-items:center; padding:32px 20px; background:${design.accent_light}; border-radius:16px; text-align:center; gap:16px; }
|
|
1104
|
+
.step-number { 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; }
|
|
1105
|
+
.step-title { font-size:28px; font-weight:700; color:${design.primary_color}; }
|
|
1106
|
+
.step-desc { font-size:24px; color:${design.text_color}; line-height:1.5; flex:1; }
|
|
1107
|
+
.step-time { font-size:24px; color:${design.accent_color}; font-weight:600; margin-top:auto; padding-top:12px; border-top:2px solid ${design.accent_color}40; }
|
|
1108
|
+
.arrow { width:60px; display:flex; align-items:center; justify-content:center; font-size:44px; color:${design.accent_color}; flex-shrink:0; }
|
|
1109
|
+
|
|
1110
|
+
CRITICAL PATTERN — your .content MUST alternate .step and .arrow divs:
|
|
1111
|
+
<div class="content">
|
|
1112
|
+
<div class="step">
|
|
1113
|
+
<div class="step-number">1</div>
|
|
1114
|
+
<div class="step-title">STEP TITLE</div>
|
|
1115
|
+
<div class="step-desc">Description text</div>
|
|
1116
|
+
<div class="step-time">Duration/detail</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
<div class="arrow">→</div>
|
|
1119
|
+
<div class="step">...</div>
|
|
1120
|
+
<div class="arrow">→</div>
|
|
1121
|
+
<div class="step">...</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
|
|
1124
|
+
⚠ 3-5 steps with → arrows between them. align-items:stretch makes all steps equal height.
|
|
1125
|
+
⚠ .step-time with margin-top:auto anchors it at the bottom of each step.
|
|
1126
|
+
⚠⚠⚠ .step and .arrow divs MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
|
|
1127
|
+
big_numbers: `═══ REQUIRED LAYOUT: BIG NUMBER METRICS ═══
|
|
1128
|
+
⚠⚠⚠ You MUST create BIG NUMBER SPOTLIGHT cards with 72-96px numbers. Tables/small text = FAILURE.
|
|
1129
|
+
|
|
1130
|
+
CSS:
|
|
1131
|
+
.content { flex:1; display:flex; gap:40px; align-items:stretch; }
|
|
1132
|
+
.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; }
|
|
1133
|
+
.metric-value { font-size:80px; font-weight:800; color:${design.primary_color}; line-height:1; }
|
|
1134
|
+
.metric-unit { font-size:32px; font-weight:600; color:${design.accent_color}; }
|
|
1135
|
+
.metric-label { font-size:28px; font-weight:600; color:${design.text_color}; }
|
|
1136
|
+
.metric-desc { font-size:24px; color:${design.text_color}aa; line-height:1.5; }
|
|
1137
|
+
.metric-trend { font-size:26px; font-weight:600; margin-top:auto; }
|
|
1138
|
+
|
|
1139
|
+
⚠ 2-3 metric cards side by side. Each: huge number (font-size:80px), unit, label, description, trend.
|
|
1140
|
+
⚠ align-items:stretch makes cards fill full height. justify-content:center within each card centers content.
|
|
1141
|
+
⚠ Trend colors: green (#10B981) for positive ▲, red (#EF4444) for negative ▼.
|
|
1142
|
+
⚠⚠⚠ .metric-card divs MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
|
|
1143
|
+
two_col_split: `═══ REQUIRED LAYOUT: 2-COLUMN SPLIT ═══
|
|
1144
|
+
⚠⚠⚠ You MUST create a 2-COLUMN layout. Single column = FAILURE.
|
|
1145
|
+
|
|
1146
|
+
CSS:
|
|
1147
|
+
.content { flex:1; display:grid; grid-template-columns:1fr 1fr; gap:32px; }
|
|
1148
|
+
.left-col { display:flex; flex-direction:column; gap:20px; justify-content:center; padding:20px; }
|
|
1149
|
+
.right-col { display:flex; flex-direction:column; gap:20px; justify-content:center; padding:20px; }
|
|
1150
|
+
|
|
1151
|
+
⚠ Left column: primary content (big metric, chart, or main visual). Right: supporting detail cards or text.
|
|
1152
|
+
⚠ Both columns stretch full height. Keep text LARGE (28-32px) — don't try to squeeze too much.
|
|
1153
|
+
⚠ MAX 3-4 items per column. If content overflows, reduce items instead of shrinking text.`,
|
|
1154
|
+
timeline: `═══ REQUIRED LAYOUT: TIMELINE ═══
|
|
1155
|
+
⚠⚠⚠ You MUST create a HORIZONTAL TIMELINE with milestone cards. Generic cards = FAILURE.
|
|
1156
|
+
|
|
1157
|
+
CSS:
|
|
1158
|
+
.content { flex:1; display:flex; align-items:stretch; gap:24px; }
|
|
1159
|
+
.milestone { flex:1; display:flex; flex-direction:column; padding:32px; border-radius:16px; background:#fff; box-shadow:0 4px 20px rgba(0,0,0,0.06); gap:16px; }
|
|
1160
|
+
.milestone-date { font-size:24px; font-weight:700; color:${design.accent_color}; }
|
|
1161
|
+
.milestone-icon { width:52px; height:52px; border-radius:50%; background:linear-gradient(135deg, ${design.primary_color}, ${design.gradient_end}); display:flex; align-items:center; justify-content:center; color:#fff; font-size:24px; }
|
|
1162
|
+
.milestone-title { font-size:30px; font-weight:700; color:${design.primary_color}; }
|
|
1163
|
+
.milestone-desc { font-size:26px; color:${design.text_color}; line-height:1.5; flex:1; }
|
|
1164
|
+
.milestone-kpi { font-size:24px; font-weight:600; color:${design.accent_color}; margin-top:auto; padding-top:16px; border-top:2px solid ${design.accent_light}; }
|
|
1165
|
+
|
|
1166
|
+
⚠ 3-4 .milestone cards side by side. Each: date → icon → title → description → KPI at bottom.
|
|
1167
|
+
⚠ align-items:stretch + flex:1 on .milestone-desc ensures all cards are equal height.
|
|
1168
|
+
⚠ .milestone-kpi with margin-top:auto anchors it at the bottom of each card.
|
|
1169
|
+
⚠⚠⚠ .milestone divs MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
|
|
1170
|
+
progress_bars: `═══ REQUIRED LAYOUT: PROGRESS BARS ═══
|
|
1171
|
+
⚠⚠⚠ You MUST create FULL-WIDTH HORIZONTAL PROGRESS BARS. Small bars inside cards = FAILURE.
|
|
1172
|
+
|
|
1173
|
+
CSS:
|
|
1174
|
+
.content { flex:1; display:flex; flex-direction:column; justify-content:space-evenly; gap:0; padding:20px 0; }
|
|
1175
|
+
.bar-item { display:flex; flex-direction:column; gap:10px; }
|
|
1176
|
+
.bar-header { display:flex; justify-content:space-between; align-items:baseline; }
|
|
1177
|
+
.bar-label { font-size:28px; font-weight:600; color:${design.text_color}; }
|
|
1178
|
+
.bar-value { font-size:28px; font-weight:700; color:${design.primary_color}; }
|
|
1179
|
+
.bar-track { width:100%; height:44px; background:${design.accent_light}; border-radius:22px; overflow:hidden; }
|
|
1180
|
+
.bar-fill { height:100%; border-radius:22px; background:linear-gradient(90deg, ${design.primary_color}, ${design.accent_color}); }
|
|
1181
|
+
|
|
1182
|
+
CRITICAL PATTERN — your .content MUST be a vertical stack of .bar-item divs:
|
|
1183
|
+
<div class="content">
|
|
1184
|
+
<div class="bar-item">
|
|
1185
|
+
<div class="bar-header">
|
|
1186
|
+
<span class="bar-label">Category Name</span>
|
|
1187
|
+
<span class="bar-value">85%</span>
|
|
1188
|
+
</div>
|
|
1189
|
+
<div class="bar-track"><div class="bar-fill" style="width:85%"></div></div>
|
|
1190
|
+
</div>
|
|
1191
|
+
<!-- repeat 4-6 bar-items -->
|
|
1192
|
+
</div>
|
|
1193
|
+
|
|
1194
|
+
⚠ justify-content:space-evenly distributes bars across the full height — NO empty bottom.
|
|
1195
|
+
⚠ Each bar: header row (label + percentage) + track div containing fill div with style="width:XX%".
|
|
1196
|
+
⚠ 4-5 bars (MAX 5). Bar height:44px. Use gradient fills. Labels under 15 chars. The structure above is non-negotiable.
|
|
1197
|
+
⚠⚠⚠ .bar-item divs MUST be DIRECT children of .content. Do NOT wrap them in a container div.`,
|
|
1198
|
+
hero_stat: `═══ REQUIRED LAYOUT: HERO STAT ═══
|
|
1199
|
+
⚠⚠⚠ You MUST create a SINGLE LARGE CENTRAL METRIC. Cards/tables = FAILURE.
|
|
1200
|
+
|
|
1201
|
+
CSS:
|
|
1202
|
+
.content { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:24px; }
|
|
1203
|
+
.hero-number { font-size:128px; font-weight:900; color:${design.primary_color}; line-height:1; }
|
|
1204
|
+
.hero-unit { font-size:48px; font-weight:600; color:${design.accent_color}; }
|
|
1205
|
+
.hero-label { font-size:36px; font-weight:600; color:${design.text_color}; }
|
|
1206
|
+
.hero-context { font-size:28px; color:${design.text_color}aa; text-align:center; max-width:800px; line-height:1.6; }
|
|
1207
|
+
.supporting-row { display:flex; gap:60px; margin-top:48px; }
|
|
1208
|
+
.supporting-item { text-align:center; }
|
|
1209
|
+
.supporting-value { font-size:40px; font-weight:700; color:${design.primary_color}; }
|
|
1210
|
+
.supporting-label { font-size:24px; color:${design.text_color}aa; margin-top:8px; }
|
|
1211
|
+
|
|
1212
|
+
⚠ ONE giant number (font-size:128px) center stage. Description below. Optional 2-3 supporting metrics in a row.
|
|
1213
|
+
⚠ This is the ONLY layout where justify-content:center on .content is correct.`,
|
|
1214
|
+
};
|
|
1215
|
+
return layouts[layoutType] || layouts.cards;
|
|
1216
|
+
}
|
|
1217
|
+
export function postProcessSlideHtml(html) {
|
|
1218
|
+
const fixStyle = `<style id="viewport-fill">
|
|
1219
|
+
body{display:flex!important;flex-direction:column!important;height:1080px!important;min-height:1080px!important;overflow:hidden!important;font-size:max(26px,1.35vw)}
|
|
1220
|
+
body>*:first-child{flex:0 0 auto!important}
|
|
1221
|
+
body>*:not(:first-child):not(style):not(script){flex:1 1 0!important;min-height:0!important;align-content:stretch!important;align-items:stretch!important}
|
|
1222
|
+
body>*:not(:first-child):not(style):not(script)>*{align-self:stretch!important;min-height:0}
|
|
1223
|
+
.slide-title{flex:0 0 auto!important}
|
|
1224
|
+
.content{flex:1 1 0!important;min-height:0!important;align-content:stretch!important;align-items:stretch!important}
|
|
1225
|
+
.content>*{align-self:stretch!important;min-height:0}
|
|
1226
|
+
.content>:only-child{display:flex!important;flex-direction:column!important;justify-content:space-evenly!important;flex:1 1 0!important;min-height:0!important}
|
|
1227
|
+
</style>`;
|
|
1228
|
+
if (html.includes('</head>')) {
|
|
1229
|
+
return html.replace('</head>', fixStyle + '</head>');
|
|
1230
|
+
}
|
|
1231
|
+
if (html.includes('</style>')) {
|
|
1232
|
+
return html.replace(/<\/style>(?![\s\S]*<\/style>)/, fixStyle + '</style>');
|
|
1233
|
+
}
|
|
1234
|
+
return html;
|
|
1235
|
+
}
|
|
1236
|
+
export function buildReviewPrompt(html, expectedLayout, slideTitle) {
|
|
1237
|
+
return `You are a presentation slide quality reviewer. Evaluate this HTML slide and output ONLY a JSON object.
|
|
1238
|
+
|
|
1239
|
+
EXPECTED LAYOUT: "${expectedLayout}"
|
|
1240
|
+
SLIDE TITLE: "${slideTitle}"
|
|
1241
|
+
|
|
1242
|
+
HTML (first 3000 chars):
|
|
1243
|
+
${html.slice(0, 3000)}
|
|
1244
|
+
|
|
1245
|
+
Evaluate:
|
|
1246
|
+
1. LAYOUT (1-10): Does it use "${expectedLayout}" layout? Wrong layout type = score 1.
|
|
1247
|
+
2. READABILITY (1-10): Font sizes ≥26px? Good contrast? Text readable?
|
|
1248
|
+
3. FILL (1-10): Content fills 80-90% of 1920×1080? No huge empty areas?
|
|
1249
|
+
|
|
1250
|
+
Output JSON ONLY (no markdown fences):
|
|
1251
|
+
{"layout":N,"readability":N,"fill":N,"pass":true/false,"feedback":"specific fix needed or empty string"}
|
|
1252
|
+
|
|
1253
|
+
PASS = all scores ≥ 7. FAIL = any score < 7.`;
|
|
1254
|
+
}
|
|
329
1255
|
export const PPT_CREATE_SYSTEM_PROMPT = `You are an elite PowerPoint presentation creator.
|
|
330
1256
|
You build NEW presentations using high-level layout builder tools.
|
|
331
1257
|
Each tool call creates ONE complete, professionally-designed slide.
|