| 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 |