| 1 | import type { SessionDeckGenerationContext } from '../../agent/types' |
| 2 | import { isSectionAgendaOutline } from '@shared/generation' |
| 3 | import { |
| 4 | buildLayoutCollisionRules, |
| 5 | buildPageSemanticStructure, |
| 6 | buildCanvasConstraints, |
| 7 | buildCanvasScenarioContentRules, |
| 8 | buildCanvasScenarioDeliveryGuard, |
| 9 | buildCanvasScenarioExpansionRules, |
| 10 | buildContentWritingRules, |
| 11 | CONTENT_LANGUAGE_RULES, |
| 12 | buildReferenceRangeContentBoundaryRules, |
| 13 | FRONTEND_CAPABILITIES, |
| 14 | SOURCE_DOCUMENT_FACT_RULE, |
| 15 | SOURCE_DOCUMENT_READ_STRATEGY, |
| 16 | SOURCE_GROUNDED_EXPANSION_RULES, |
| 17 | STABLE_HTML_FRAGMENT_PROTOCOL, |
| 18 | STYLE_FIDELITY_RULES, |
| 19 | buildOutlinePageList, |
| 20 | formatDesignContract, |
| 21 | resolveContextStylePrompt |
| 22 | } from './shared' |
| 23 | import { formatAnimationPreferencesForPageWriting } from './animation-preferences' |
| 24 | import { buildCanvasScenarioBrief, resolveCanvasScenario } from './canvas-scenario' |
| 25 | import { createPromptCatalog } from '../catalog' |
| 26 | |
| 27 | import deckSystemTemplate from '../templates/deck-system/system.md?raw' |
| 28 | |
| 29 | type DeckSystemTemplateVars = { |
| 30 | system: { |
| 31 | pageWriteRequirement: string |
| 32 | canvasIdentity: string |
| 33 | pageName: string |
| 34 | canvasScenarioBrief: string |
| 35 | canvasScenarioContentRules: string |
| 36 | contentLanguageRules: string |
| 37 | templateOrCreativeInstructions: string |
| 38 | sourceDocumentInstructions: string |
| 39 | canvasConstraints: string |
| 40 | layoutCollisionRules: string |
| 41 | canvasScenarioDeliveryGuard: string |
| 42 | pageSemanticStructure: string |
| 43 | canvasScenarioExpansionRules: string |
| 44 | frontendCapabilities: string |
| 45 | animationPreferencePromptWithSpacing: string |
| 46 | contentWritingRules: string |
| 47 | stableHtmlFragmentProtocol: string |
| 48 | templateAssetGuards: string |
| 49 | pageWriteConstraint: string |
| 50 | executionFlow: string |
| 51 | topic: string |
| 52 | deckTitle: string |
| 53 | slideCount: number |
| 54 | targetInfo: string |
| 55 | targetFileLine: string |
| 56 | pageList: string |
| 57 | presetLabel: string |
| 58 | presetId: string |
| 59 | stylePrompt: string |
| 60 | designContract: string |
| 61 | styleFidelityRules: string |
| 62 | finalWriteToolName: string |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | const deckSystemPromptCatalog = createPromptCatalog<DeckSystemTemplateVars>({ |
| 67 | system: deckSystemTemplate.trimEnd() |
| 68 | }) |
| 69 | |
| 70 | export function buildDeckAgentSystemPrompt( |
| 71 | styleId: string | null | undefined, |
| 72 | context: SessionDeckGenerationContext |
| 73 | ): string { |
| 74 | void styleId |
| 75 | const { presetLabel, presetId, stylePrompt } = resolveContextStylePrompt(context) |
| 76 | const pageList = buildOutlinePageList(context) |
| 77 | const statusLanguage = context.appLocale === 'en' ? 'English' : 'Simplified Chinese' |
| 78 | |
| 79 | const targetInfo = context.selectedPageId |
| 80 | ? `This run may only modify: ${context.selectedPageId}` |
| 81 | : context.selectPageIds?.length |
| 82 | ? `This run may only modify selected pages: ${context.selectPageIds.join(', ')}` |
| 83 | : 'This run may modify all pages.' |
| 84 | const targetPagePath = |
| 85 | context.selectedPageId && context.pageFileMap[context.selectedPageId] |
| 86 | ? `/${context.selectedPageId}.html` |
| 87 | : undefined |
| 88 | const isSinglePageTask = |
| 89 | context.mode !== 'edit' && |
| 90 | (Boolean(context.selectedPageId) || |
| 91 | (Array.isArray(context.selectPageIds) && context.selectPageIds.length === 1) || |
| 92 | (Array.isArray(context.allowedPageIds) && context.allowedPageIds.length === 1) || |
| 93 | context.outlineTitles.length === 1) |
| 94 | const isSectionAgendaSinglePageTask = |
| 95 | isSinglePageTask && |
| 96 | context.outlineItems.length === 1 && |
| 97 | isSectionAgendaOutline(context.outlineItems[0]?.contentOutline || '') |
| 98 | const referenceTextLocked = Boolean( |
| 99 | context.referenceDocumentPath && context.pageReferenceContext |
| 100 | ) |
| 101 | const isTemplateGeneration = context.templatePageReadRequired === true |
| 102 | const singlePageWriteToolName = isTemplateGeneration |
| 103 | ? 'update_template_page_file' |
| 104 | : 'update_single_page_file' |
| 105 | const step3Instruction = isSinglePageTask |
| 106 | ? context.templatePageReadRequired |
| 107 | ? '3. Required: after reading the target template page with read_file, call update_template_page_file(pageId=target page, content). A final text response without the read_file + update_template_page_file sequence is a failed generation.' |
| 108 | : '3. Required: call update_single_page_file(pageId=target page, content). A final text response without this tool call is a failed generation.' |
| 109 | : '3. Call update_page_file(content) page by page. For multi-page generation, write each target page file in order. You may pass pageId to override automatic targeting.' |
| 110 | const sourceDocumentPaths = |
| 111 | isSectionAgendaSinglePageTask && !referenceTextLocked |
| 112 | ? [] |
| 113 | : (context.sourceDocumentPaths || []).filter(Boolean) |
| 114 | const isRetryMode = context.mode === 'retry' |
| 115 | const animationPreferencePrompt = formatAnimationPreferencesForPageWriting( |
| 116 | context.animationPreferences |
| 117 | ) |
| 118 | const canvasScenario = resolveCanvasScenario(context.slideSize) |
| 119 | const sourceDocumentInstructions = |
| 120 | referenceTextLocked && context.referenceDocumentPath |
| 121 | ? `\n\n${[ |
| 122 | buildReferenceRangeContentBoundaryRules( |
| 123 | context.referenceDocumentPath, |
| 124 | context.pageReferenceContext |
| 125 | ), |
| 126 | '- The outline may contain a Source heading, Source range, and Agenda items JSON. Read that structured context before writing; it is metadata for grounding, not visible page copy.', |
| 127 | '- Use the selected range as the page factual boundary. Rephrase and visualize when useful without changing source relationships or qualifiers.', |
| 128 | isRetryMode |
| 129 | ? "- This is a failed-slide retry. Keep this page within its source range; repair layout without introducing outside facts." |
| 130 | : '- For cross-page context, use grep and targeted reads of related source passages rather than reading the whole document. Those passages may guide continuity, but facts rendered on this page must stay within its selected source range.' |
| 131 | ].join('\n')}` |
| 132 | : sourceDocumentPaths.length > 0 |
| 133 | ? `\n\n${[ |
| 134 | '## Source documents (highest-priority content evidence)', |
| 135 | 'This session comes from user-uploaded documents. Generated content must prioritize source-document facts; do not rely only on the summary or page outline.', |
| 136 | 'Single-page prompts may include program-side retrieved snippets.', |
| 137 | SOURCE_DOCUMENT_READ_STRATEGY, |
| 138 | 'If snippets are insufficient, conflicting, or missing key facts, follow the source-reading skill against these source documents:', |
| 139 | ...sourceDocumentPaths.map((docPath) => `- ${docPath}`), |
| 140 | SOURCE_DOCUMENT_FACT_RULE, |
| 141 | SOURCE_GROUNDED_EXPANSION_RULES, |
| 142 | isRetryMode |
| 143 | ? '- This is a failed-slide retry. Match source material only around the failed slide title and outline; do not reconstruct the whole deck outline.' |
| 144 | : "- This is initial page generation. Follow the established page outline slide by slide; do not prematurely insert other slides' material.", |
| 145 | 'If the source document conflicts with additional user requirements, follow the user requirements. If the page outline conflicts with source details, follow source-document facts.' |
| 146 | ].join('\n')}` |
| 147 | : '' |
| 148 | const templateOrCreativeInstructions = isTemplateGeneration |
| 149 | ? [ |
| 150 | '## 模板还原优先', |
| 151 | '- 当前是模板生成,不追求重新设计、视觉惊喜或主动变化。', |
| 152 | '- 每页先继承目标模板页的页面角色、版式骨架、背景/装饰层、留白节奏、字体尺度、组件形状和配色关系。', |
| 153 | '- 只替换旧业务内容:标题、正文、指标、图表数据、案例、结论和与新主题冲突的内容素材。', |
| 154 | '- 为适配新内容可以做局部微调,但不能把模板页改成另一套构图或另一套视觉系统。', |
| 155 | referenceTextLocked |
| 156 | ? '- Reference Range Content Boundary applies. If content is dense, first clarify hierarchy, group related material, and use a compact internal layout; reduce decoration and internal padding next while preserving actual nonzero gaps between independent modules; use bounded internal-module scaling only as a final measure. Never scale the page root, section/page shell, `main[data-role="content"]`, or canvas.' |
| 157 | : '- 如果内容放不下,优先压缩文案/合并模块,不要通过新增大量卡片或重排整页来破坏模板。' |
| 158 | ].join('\n') |
| 159 | : [ |
| 160 | '## 创意变化', |
| 161 | '- 在统一风格内制造每页的视觉惊喜:变化主视觉位置、标题进入方式、信息节奏、留白比例或局部装饰语言。', |
| 162 | '- 每页至少有一个清晰的视觉焦点,可以是关键数字、图表、概念符号、时间节点或一句核心判断。', |
| 163 | '- 惊喜感服务于内容理解;不要为了变化加入无关装饰、复杂嵌套、遮挡文字或难以维护的结构。', |
| 164 | `- 同一套 ${canvasScenario.sequenceName} 内避免连续页面使用完全相同的标题位置、卡片网格和背景分区。` |
| 165 | ].join('\n') |
| 166 | const templateAssetGuards = isTemplateGeneration |
| 167 | ? [ |
| 168 | '- In template generation, dropping inspected background images, decorative layers, CSS url(...) references, masks, overlays, or the containers that render them is a failed generation unless the user explicitly requested removal.', |
| 169 | '- Because page write tools rebuild the slide from your submitted fragment, include the required template background/decorative layers or exact local asset references inside that fragment.', |
| 170 | '' |
| 171 | ].join('\n') |
| 172 | : '' |
| 173 | const pageWriteConstraint = isSinglePageTask |
| 174 | ? `- 不要调用 edit_file / write_file / update_page_file${ |
| 175 | isTemplateGeneration ? ' / update_single_page_file' : '' |
| 176 | };单页任务必须调用 ${singlePageWriteToolName}(pageId, content) 并成功落盘后才能最终回复` |
| 177 | : '- 不要调用 edit_file / write_file 直接覆盖页面文件,统一用 update_page_file(content)' |
| 178 | const executionFlow = isSinglePageTask |
| 179 | ? context.templatePageReadRequired |
| 180 | ? [ |
| 181 | `1. Mandatory first action: call read_file(path="${targetPagePath || '/<pageId>.html'}", offset=0, limit=1200) to inspect the copied template page before writing.`, |
| 182 | '2. Preserve the inspected page visual system: background images, texture images, decorative assets, masks, overlays, CSS background-image/url(...) references, <img src>, SVG image href, font scale, spacing rhythm, color language, and structural wrappers unless the user explicitly asks to remove them.', |
| 183 | ' Background/decorative assets are template skeleton, not stale business content; replacing facts and text must not remove the visual shell.', |
| 184 | ` The content fragment you pass to ${singlePageWriteToolName} must explicitly carry those required layers or exact local asset references.`, |
| 185 | sourceDocumentPaths.length > 0 |
| 186 | ? `3. Required before writing: follow the source-reading skill for targeted source inspection (${sourceDocumentPaths.join(', ')}).` |
| 187 | : '3. Analyze the new slide content requirements from the context provided.', |
| 188 | step3Instruction, |
| 189 | '4. Send a short summary as your final response.' |
| 190 | ].join('\n') |
| 191 | : [ |
| 192 | sourceDocumentPaths.length > 0 |
| 193 | ? `1. Required before writing: follow the source-reading skill for targeted source inspection (${sourceDocumentPaths.join(', ')}).` |
| 194 | : '1. Analyze the slide requirements from the context provided.', |
| 195 | step3Instruction, |
| 196 | '3. Send a short summary as your final response.' |
| 197 | ].join('\n') |
| 198 | : [ |
| 199 | '1. get_session_context — read the session context and constraints', |
| 200 | sourceDocumentPaths.length > 0 |
| 201 | ? `2. Use retrieved source-document snippets as an index, follow the source-reading skill for targeted source inspection (${sourceDocumentPaths.join(', ')}), then call report_generation_status('Analyzing request', ...)` |
| 202 | : "2. report_generation_status('Analyzing request', ...) — report start", |
| 203 | ` report_generation_status labels and details must be written in ${statusLanguage}, because they are application UI logs.`, |
| 204 | ' This status/log language is independent from deck content language. Deck content must still follow the Content language rules.', |
| 205 | ' progress must be a numeric literal such as 10, 35, or 88. Do not pass strings such as "10".', |
| 206 | ' Progress must be detailed and monotonic. Suggested ranges: Analyzing request (8-18) / Reading context (18-30) / Writing pages (30-88, linear by page) / Verifying (88-96) / Completed (98-100).', |
| 207 | ' Report once for each major action so the UI does not stay silent for too long.', |
| 208 | step3Instruction, |
| 209 | '4. verify_completion() — check whether target pages are filled', |
| 210 | "5. If pages are still empty, continue filling them, then report_generation_status('Generation completed', ...)" |
| 211 | ].join('\n') |
| 212 | return deckSystemPromptCatalog.render('system', { |
| 213 | pageWriteRequirement: isTemplateGeneration |
| 214 | ? 'You MUST call update_template_page_file to write the current template page.' |
| 215 | : 'You MUST call update_single_page_file (single-page) or update_page_file (multi-page) to write every page.', |
| 216 | canvasIdentity: canvasScenario.identity, |
| 217 | pageName: canvasScenario.pageName, |
| 218 | canvasScenarioBrief: buildCanvasScenarioBrief(context.slideSize), |
| 219 | canvasScenarioContentRules: buildCanvasScenarioContentRules(context.slideSize, { |
| 220 | referenceTextLocked |
| 221 | }), |
| 222 | contentLanguageRules: CONTENT_LANGUAGE_RULES, |
| 223 | templateOrCreativeInstructions, |
| 224 | sourceDocumentInstructions, |
| 225 | canvasConstraints: buildCanvasConstraints(context.slideSize, { referenceTextLocked }), |
| 226 | layoutCollisionRules: buildLayoutCollisionRules(context.slideSize), |
| 227 | canvasScenarioDeliveryGuard: buildCanvasScenarioDeliveryGuard(context.slideSize, { |
| 228 | referenceTextLocked |
| 229 | }), |
| 230 | pageSemanticStructure: buildPageSemanticStructure(context.slideSize), |
| 231 | canvasScenarioExpansionRules: buildCanvasScenarioExpansionRules(context.slideSize, { |
| 232 | referenceTextLocked |
| 233 | }), |
| 234 | frontendCapabilities: FRONTEND_CAPABILITIES, |
| 235 | animationPreferencePromptWithSpacing: animationPreferencePrompt |
| 236 | ? `${animationPreferencePrompt}\n\n` |
| 237 | : '', |
| 238 | contentWritingRules: buildContentWritingRules({ referenceTextLocked }), |
| 239 | stableHtmlFragmentProtocol: STABLE_HTML_FRAGMENT_PROTOCOL, |
| 240 | templateAssetGuards, |
| 241 | pageWriteConstraint, |
| 242 | executionFlow, |
| 243 | topic: context.topic, |
| 244 | deckTitle: context.deckTitle, |
| 245 | slideCount: context.outlineTitles.length, |
| 246 | targetInfo, |
| 247 | targetFileLine: targetPagePath ? `Target file: ${targetPagePath}` : '', |
| 248 | pageList, |
| 249 | presetLabel, |
| 250 | presetId, |
| 251 | stylePrompt, |
| 252 | designContract: formatDesignContract(context.designContract), |
| 253 | styleFidelityRules: STYLE_FIDELITY_RULES, |
| 254 | finalWriteToolName: isTemplateGeneration |
| 255 | ? 'update_template_page_file' |
| 256 | : 'update_single_page_file (or update_page_file)' |
| 257 | }) |
| 258 | } |
| 259 |