返回 oh-my-ppt
canvas-scenario.ts
根目录 / src / main / agent-runtime / prompt / composers / canvas-scenario.ts
1 // Canvas-dependent prompt vocabulary belongs with Runtime prompt composers.
2 import { requireSlideSize, type SlideSizePreset, type SlideSizePresetId } from '@shared/slide-size'
3
4 export type CanvasScenarioId =
5 | 'presentation-wide'
6 | 'presentation-standard'
7 | 'square-card'
8 | 'mobile-story'
9 | 'poster-card'
10 | 'social-note'
11
12 interface CanvasScenario {
13 id: CanvasScenarioId
14 label: string
15 artifactName: string
16 pageName: string
17 sequenceName: string
18 identity: string
19 editIdentity: string
20 }
21
22 export function resolveCanvasScenario(input: SlideSizePreset): CanvasScenario {
23 const slideSize = requireSlideSize(input)
24 const scenarioIdByPreset: Record<SlideSizePresetId, CanvasScenarioId> = {
25 'wide-16-9': 'presentation-wide',
26 'standard-4-3': 'presentation-standard',
27 'square-1-1': 'square-card',
28 'vertical-9-16': 'mobile-story',
29 'vertical-3-4': 'poster-card',
30 'xiaohongshu-note': 'social-note'
31 }
32
33 const scenarioId = scenarioIdByPreset[slideSize.id]
34 if (!scenarioId) {
35 throw new Error(`No canvas scenario prompt configured for slide size: ${slideSize.id}`)
36 }
37
38 switch (scenarioId) {
39 case 'presentation-wide':
40 return {
41 id: 'presentation-wide',
42 label: '16:9 PPT 演示',
43 artifactName: 'PPT presentation',
44 pageName: 'slide',
45 sequenceName: 'deck',
46 identity:
47 'You are a PPT generation expert responsible for turning a planned page outline into slide HTML content.',
48 editIdentity: 'You are a PPT incremental editing expert focused on page-quality changes.'
49 }
50 case 'presentation-standard':
51 return {
52 id: 'presentation-standard',
53 label: '4:3 传统演示',
54 artifactName: '4:3 presentation',
55 pageName: 'slide',
56 sequenceName: 'deck',
57 identity:
58 'You are a 4:3 presentation generation expert responsible for turning a planned outline into readable slide HTML content.',
59 editIdentity: 'You are a 4:3 presentation editing expert focused on page-quality changes.'
60 }
61 case 'mobile-story':
62 return {
63 id: 'mobile-story',
64 label: '移动端竖屏内容',
65 artifactName: 'vertical mobile story',
66 pageName: 'screen',
67 sequenceName: 'screen sequence',
68 identity:
69 'You are a vertical mobile story generation expert responsible for turning a planned outline into 9:16 screen HTML content.',
70 editIdentity: 'You are a vertical mobile story editing expert focused on screen-quality changes.'
71 }
72 case 'square-card':
73 return {
74 id: 'square-card',
75 label: '1:1 方形内容卡',
76 artifactName: 'square content card',
77 pageName: 'card',
78 sequenceName: 'card sequence',
79 identity:
80 'You are a square content-card generation expert responsible for turning a planned outline into 1:1 card HTML content.',
81 editIdentity: 'You are a square content-card editing expert focused on card-quality changes.'
82 }
83 case 'poster-card':
84 return {
85 id: 'poster-card',
86 label: '竖版海报信息卡',
87 artifactName: 'vertical poster card',
88 pageName: 'card',
89 sequenceName: 'card sequence',
90 identity:
91 'You are a vertical poster-card generation expert responsible for turning a planned outline into 3:4 information-card HTML content.',
92 editIdentity: 'You are a vertical poster-card editing expert focused on card-quality changes.'
93 }
94 case 'social-note':
95 return {
96 id: 'social-note',
97 label: '小红书图文笔记',
98 artifactName: 'Xiaohongshu note',
99 pageName: 'note page',
100 sequenceName: 'note sequence',
101 identity:
102 'You are a Xiaohongshu note generation expert responsible for turning a planned outline into collectible note-page HTML content.',
103 editIdentity: 'You are a Xiaohongshu note editing expert focused on note-page quality.'
104 }
105 }
106 }
107
108 export function buildCanvasScenarioBrief(input: SlideSizePreset): string {
109 const scenario = resolveCanvasScenario(input)
110 return [
111 '## Canvas scenario',
112 `- Current scenario: ${scenario.label}.`,
113 `- Treat each output as a ${scenario.pageName} in a ${scenario.sequenceName}, not as a generic 16:9 PPT unless this scenario explicitly says so.`,
114 `- Product form: ${scenario.artifactName}.`
115 ].join('\n')
116 }
117
118 export function buildCanvasScenarioContentRules(input: SlideSizePreset): string {
119 const scenario = resolveCanvasScenario(input)
120 const common = [
121 '- **一个焦点**:这页让观众先看什么、记住哪一句?围绕唯一焦点组织,其余是它的支撑;靠大小 / 位置 / 颜色分出层级,不要所有模块等权平铺。',
122 '- **过密先自我总结**:如果素材 / 当前页内容一眼看会超出当前画布或形成高密度信息墙,写 HTML 前先总结成一个主旨和少量支撑组;只把总结后的结构上屏。',
123 '- **构图平衡**:元素的视觉重量(大 / 深 / 彩色 = 重,小 / 浅 = 轻)在画布上分布平衡,不偏一边、不堆一角。'
124 ]
125
126 if (scenario.id === 'presentation-wide') {
127 return [
128 '## 场景内容组织:16:9 PPT 演示',
129 '- **3 秒主旨**:PPT 是演讲辅助,不是文档浏览。写页面前先定一句观众 3 秒内能抓住的话;标题、主图表、关键数字和结论都围绕这句话服务。',
130 ...common,
131 '- **留白是设计,不是待填的空**:每处留白要么是刻意的 framing / 呼吸,要么就重排消除;绝不留不小心的失衡空缺。',
132 '- **量的多少不是问题,平衡才是**:内容少就收敛成低密度 hero / 大图表 / 时间线 / 结构图焦点;内容多就先总结、分组、压缩、留呼吸感。'
133 ].join('\n')
134 }
135
136 if (scenario.id === 'presentation-standard') {
137 return [
138 '## 场景内容组织:4:3 传统演示',
139 '- **演示主旨优先**:4:3 仍是演示页,但横向空间更少。每页保留一个清楚观点,避免把 16:9 的宽屏三列直接压进方正画布。',
140 ...common,
141 '- **更方正的层级**:优先上下两段、中心主体 + 周边辅助、或 2×2 以内的信息结构;少用长横向时间线和宽表格。',
142 '- **可投影可阅读**:标题、主视觉和结论必须远距离可读,辅助信息宁可合并为短标签,不做密集脚注墙。'
143 ].join('\n')
144 }
145
146 if (scenario.id === 'mobile-story') {
147 return [
148 '## 场景内容组织:移动端竖屏',
149 '- **首屏抓人**:顶部 25%-35% 应给出标题钩子、核心判断或强视觉锚点,让手机用户一眼知道为什么继续看。',
150 '- **上下阅读路径**:按从上到下的叙事组织信息,优先标题钩子 → 主体解释 → 关键证据/步骤 → 底部结论,不要照搬横向三列。',
151 '- **分屏节奏**:把内容组织成 2-4 个纵向段落或模块,每段只承担一个阅读动作;模块之间留清楚呼吸,不堆小卡片。',
152 '- **移动端可读**:正文宁可少而清楚,不把表格、宽图表、长句密集塞进窄屏。'
153 ].join('\n')
154 }
155
156 if (scenario.id === 'square-card') {
157 return [
158 '## 场景内容组织:1:1 方形内容卡',
159 '- **中心焦点**:方形画布最怕平均摊开。先确定一个居中或略偏上的主视觉/主结论/关键数字,让用户一眼抓住重点。',
160 '- **方形秩序**:优先中心主体 + 周边支撑、上下两段、2×2 信息块、环绕式解释或图文对半;不要照搬宽屏左右大分栏或长横向时间线。',
161 '- **少而完整**:适合做概念卡、总结卡、对比卡、清单卡、社媒封面和单图知识卡;控制在标题、主体、2-4 个支撑点、结论/来源几层内。',
162 '- **四边平衡**:上下左右留白都要稳定,避免内容只堆上半区或一侧,底部不能只是装饰空白。'
163 ].join('\n')
164 }
165
166 if (scenario.id === 'poster-card') {
167 return [
168 '## 场景内容组织:竖版海报信息卡',
169 '- **主视觉锚点**:先确定一个最大的信息或视觉锚点,可以是大标题、关键数字、产品/人物/概念图形或核心结论。',
170 '- **少层级强秩序**:海报卡不是文档页。控制在标题、主视觉、2-4 个支撑点、结论/来源这几层内。',
171 '- **信息卡阅读**:适合做清单、对比、步骤、概念解释和结论卡;不要做满屏段落或复杂宽表格。',
172 '- **边界留白**:四边和模块间距必须显得有设计感,不能因为竖版空间高就一路堆到底。'
173 ].join('\n')
174 }
175
176 if (scenario.id === 'social-note') {
177 return [
178 '## 场景内容组织:小红书图文笔记',
179 '- **标题钩子**:顶部必须有明确标题钩子或利益点,让用户知道这页值得停留、截图或收藏。',
180 '- **收藏价值**:优先组织成可保存的清单、步骤、避坑、对比、模板、结论卡;不要做纯演讲页或宽屏 PPT 结构。',
181 '- **图文分段**:用上下模块栈承载 2-4 个信息段,每段有清楚小标题或视觉锚点;避免横向三列和复杂表格。',
182 '- **口吻清楚但不花哨**:可以更像笔记,但事实、指标、来源仍需严谨;不要为了平台感编造案例或结论。'
183 ].join('\n')
184 }
185
186 throw new Error(`No content prompt configured for canvas scenario: ${scenario.id}`)
187 }
188
189 export function buildCanvasScenarioExpansionRules(input: SlideSizePreset): string {
190 const scenario = resolveCanvasScenario(input)
191 const common = [
192 '- **补结构 ≠ 编造事实**:内容少时,可以从现有标题 / 要点推导解释、影响、对比、机制、so-what 表达和视觉结构;禁止捏造源里没有的具体数字、日期、案例、引用、人名、来源或新结论。',
193 '- **够了就压缩**:如果已有足够事实或结构,不要再新增同级模块;改为取舍、分组、压缩和换表达。',
194 '- **收在当前画布内**:扩展后的可见内容必须适合当前画布,不靠缩小字号、堆卡片或塞满边角解决。'
195 ]
196
197 if (scenario.id === 'presentation-wide' || scenario.id === 'presentation-standard') {
198 return [
199 '## 内容丰富与优化规则(演示页)',
200 '- 写 HTML 前判断:这一页是内容足够,只需取舍 / 分组 / 压缩;还是内容真的偏薄,需要补论证结构、解释关系或 so-what 表达。',
201 ...common,
202 '- 演示页的“够”是一个明确观点 + 2-4 个支撑组 / 证据轨,而不是把素材逐条搬运成可见模块。',
203 '- 不扩展时也不能小卡片堆顶部留空底:用低密度 hero / 大图表 / 时间线 / 结构图撑住页面。'
204 ].join('\n')
205 }
206
207 if (scenario.id === 'mobile-story') {
208 return [
209 '## 内容丰富与优化规则(移动端竖屏)',
210 '- 写 HTML 前判断:是否已经有清楚的首屏钩子、主体解释和底部结论;缺哪一段就补结构,不补无来源事实。',
211 ...common,
212 '- 竖屏的“够”是 2-4 个顺序阅读段落,而不是一堆平级卡片。',
213 '- 内容多时优先拆成纵向分段和短句,不做宽表格或密集脚注墙。'
214 ].join('\n')
215 }
216
217 if (scenario.id === 'poster-card') {
218 return [
219 '## 内容丰富与优化规则(竖版海报信息卡)',
220 '- 写 HTML 前判断:是否已经有主视觉锚点、少量支撑信息和明确结论;缺的是结构感时补视觉层级,不补长段文字。',
221 ...common,
222 '- 海报卡的“够”是少量高辨识信息层级;已有 3-5 个关键点通常就应压缩表达,而不是继续扩写。',
223 '- 内容少时放大核心信息和视觉锚点,内容多时合并为清单/对比/步骤,不做文档页。'
224 ].join('\n')
225 }
226
227 if (scenario.id === 'square-card') {
228 return [
229 '## 内容丰富与优化规则(1:1 方形内容卡)',
230 '- 写 HTML 前判断:是否已经有一个强焦点、少量支撑信息和可截图/可复述的结论;缺的是结构感时补视觉层级,不补长文档内容。',
231 ...common,
232 '- 方形卡的“够”是一个核心观点 + 2-4 个支撑点;已有关键点时应强化焦点和分组,而不是继续扩写。',
233 '- 内容多时压成 2×2、上下两段或中心主体 + 周边短标签;内容少时放大主结论和视觉锚点。'
234 ].join('\n')
235 }
236
237 if (scenario.id === 'social-note') {
238 return [
239 '## 内容丰富与优化规则(小红书图文笔记)',
240 '- 写 HTML 前判断:是否已经有标题钩子、可收藏要点和解释/步骤/避坑结构;缺结构就补笔记结构,不编造事实。',
241 ...common,
242 '- 小红书页的“够”是让用户能快速保存或复述:3-6 个短要点、步骤、对比或结论通常比长篇论证更有效。',
243 '- 内容多时做“分组 + 小标题 + 重点标注”,内容少时强化标题利益点和一个可执行结论。'
244 ].join('\n')
245 }
246
247 throw new Error(`No expansion prompt configured for canvas scenario: ${scenario.id}`)
248 }
249
250 export function buildCanvasScenarioDeliveryGuard(input: SlideSizePreset): string {
251 const scenario = resolveCanvasScenario(input)
252
253 if (scenario.id === 'presentation-wide' || scenario.id === 'presentation-standard') {
254 return [
255 '## 交付前版面检查(演示页)',
256 '- 形服务于魂:先确认页面有一个 3 秒可读的主旨,再检查这个主旨有没有对应的承重结构(大图表、hero 数字、矩阵、时间线、对比区或结论区)。',
257 '- 非 cover / quote / divider / 纯氛围页,正文不能全部停在上半屏:如果标题 + 主要模块只占画布上半部,下面只有 footer/source 或大片空底,这是结构失败。',
258 '- 低密度可以留白,但留白必须围绕一个足够大的焦点形成 framing;不要把几个小卡片、小图表排在顶部,然后把下半屏留成未设计的空白。',
259 '- 视觉重心可以略高于几何中心,让投影/大屏观看更舒服;这不是把正文堆到上方。',
260 '- 主图表页不能让主 chart 只有 220–280px 且没有其他主视觉承重;如果 chart 是主要证据,优先给足主图区或改成更合适的结构。',
261 '- 写入前做一次 mental bounding-box check:忽略背景装饰和 footer/source 后,主要内容应形成清楚的上/中/下或左/右结构。'
262 ].join('\n')
263 }
264
265 if (scenario.id === 'mobile-story') {
266 return [
267 '## 交付前版面检查(移动端竖屏)',
268 '- 首屏必须有吸引点:顶部区域不能只是小标题或空背景,应能看到标题钩子、核心判断或强视觉锚点。',
269 '- 上下阅读路径必须连续:用户从上滑到下时,能按顺序读到主旨、解释、证据/步骤和结论。',
270 '- 不要把横向三列、宽表格或大量并排卡片硬塞进窄屏;宽向信息必须转成纵向分组。',
271 '- 底部不能只是空白或零散 footer;如果有结论,应在底部形成收束。'
272 ].join('\n')
273 }
274
275 if (scenario.id === 'poster-card') {
276 return [
277 '## 交付前版面检查(竖版海报信息卡)',
278 '- 一眼必须有主视觉锚点:最大标题、关键数字、图形或结论不能被一堆小模块淹没。',
279 '- 层级不能过多:如果出现太多同级卡片、脚注、标签和小标题,先合并成更少的信息组。',
280 '- 海报边界要稳:四边留白、模块间距和底部收束都要显得有设计意图。',
281 '- 不要让页面变成竖版文档:长段落、密集表格和满屏小字都是失败。'
282 ].join('\n')
283 }
284
285 if (scenario.id === 'square-card') {
286 return [
287 '## 交付前版面检查(1:1 方形内容卡)',
288 '- 一眼必须有中心焦点:最大标题、关键数字、概念图形或核心结论不能被平级小模块稀释。',
289 '- 四边视觉重量要平衡:不要只占上半区、左侧或某个角落;留白必须围绕主体形成 framing。',
290 '- 不要套宽屏 PPT 骨架:避免长横向时间线、宽表格、三列 dashboard 和底部卡片排。',
291 '- 内容应像一张完整卡片:标题、主体、支撑和收束都在方形画布内形成闭合。'
292 ].join('\n')
293 }
294
295 if (scenario.id === 'social-note') {
296 return [
297 '## 交付前版面检查(小红书图文笔记)',
298 '- 顶部标题钩子必须清楚:用户应能立刻知道这页解决什么问题、提供什么清单或给出什么结论。',
299 '- 页面要有收藏价值:至少形成清单、步骤、对比、避坑、模板或关键结论中的一种,而不是普通演示页。',
300 '- 上下模块节奏要像图文笔记:2-4 个可扫读段落,每段有小标题、重点标注或视觉锚点。',
301 '- 不要套 16:9 PPT 骨架:避免宽屏分栏和普通演示结构。'
302 ].join('\n')
303 }
304
305 throw new Error(`No delivery prompt configured for canvas scenario: ${scenario.id}`)
306 }
307
307 lines TYPESCRIPT