| 1 | import type { SessionDeckGenerationContext } from '../../agent/types' |
| 2 | import { formatLayoutIntentPrompt } from '@shared/layout-intent' |
| 3 | import { isSectionAgendaOutline, type PageReferenceContext } from '@shared/generation' |
| 4 | import { getLayoutMasterTemplate } from '@shared/layout-master' |
| 5 | import { buildGenerationImageIntentRules } from './generation-image-intent-rules' |
| 6 | import { CHART_SKILL_NAME, formatSkillUsageRequirement } from '../../../product-skills/contract' |
| 7 | import { |
| 8 | buildCanvasScenarioContentRules, |
| 9 | buildCanvasScenarioDeliveryGuard, |
| 10 | buildCanvasScenarioExpansionRules, |
| 11 | buildLayoutCollisionRules, |
| 12 | buildPageSemanticStructure, |
| 13 | buildCanvasConstraints, |
| 14 | buildReferenceRangeContentBoundaryRules, |
| 15 | CONTENT_LANGUAGE_RULES, |
| 16 | FRONTEND_CAPABILITIES, |
| 17 | SOURCE_DOCUMENT_FACT_RULE, |
| 18 | SOURCE_DOCUMENT_LOCATE_THEN_READ_RULE, |
| 19 | SOURCE_DOCUMENT_READ_STRATEGY, |
| 20 | SOURCE_GROUNDED_EXPANSION_RULES, |
| 21 | SOURCE_UNSUPPORTED_CLAIMS, |
| 22 | STABLE_HTML_FRAGMENT_PROTOCOL |
| 23 | } from './shared' |
| 24 | import { buildCanvasScenarioBrief, resolveCanvasScenario } from './canvas-scenario' |
| 25 | |
| 26 | export function buildSinglePageGenerationPrompt(args: { |
| 27 | topic: string |
| 28 | deckTitle: string |
| 29 | pageId: string |
| 30 | pageNumber: number |
| 31 | pageTitle: string |
| 32 | pageOutline: string |
| 33 | slideSize: import('@shared/slide-size').SlideSizePreset |
| 34 | layoutIntent?: SessionDeckGenerationContext['outlineItems'][number]['layoutIntent'] |
| 35 | layoutId?: SessionDeckGenerationContext['outlineItems'][number]['layoutId'] |
| 36 | layoutPrompt?: SessionDeckGenerationContext['outlineItems'][number]['layoutPrompt'] |
| 37 | sourceDocumentPaths?: string[] |
| 38 | referenceDocumentPath?: string |
| 39 | pageReferenceContext?: PageReferenceContext |
| 40 | referenceDocumentSnippets?: string |
| 41 | isRetryMode?: boolean |
| 42 | writeToolName?: 'update_single_page_file' | 'update_template_page_file' |
| 43 | retryContext?: { |
| 44 | attempt: number |
| 45 | maxRetries: number |
| 46 | previousError: string |
| 47 | } |
| 48 | visualEnabled?: boolean |
| 49 | hasStyleImageDirection?: boolean |
| 50 | }): string { |
| 51 | const writeToolName = args.writeToolName || 'update_single_page_file' |
| 52 | const previousError = args.retryContext?.previousError || '' |
| 53 | const shouldMentionChartFix = /chart|canvas|PPT\.createChart/i.test(previousError) |
| 54 | const shouldMentionWriteToolFix = |
| 55 | /页面未写入|没有成功调用|not written|update_single_page_file|update_template_page_file|占位|placeholder/i.test( |
| 56 | previousError |
| 57 | ) |
| 58 | const shouldMentionTemplateSkeletonFix = |
| 59 | /模板骨架|skeleton|background\/decorative|背景\/装饰资源|CSS url|SVG image|local asset/i.test( |
| 60 | previousError |
| 61 | ) |
| 62 | const isSectionAgendaPage = |
| 63 | args.pageReferenceContext?.isSectionAgenda || isSectionAgendaOutline(args.pageOutline || '') |
| 64 | const referenceDocumentPath = |
| 65 | args.pageReferenceContext && |
| 66 | args.referenceDocumentPath === args.pageReferenceContext.referenceDocumentPath && |
| 67 | args.sourceDocumentPaths?.includes(args.referenceDocumentPath) |
| 68 | ? args.referenceDocumentPath |
| 69 | : undefined |
| 70 | const referenceTextLocked = Boolean(referenceDocumentPath && args.pageReferenceContext) |
| 71 | const retryInstructions = args.retryContext |
| 72 | ? [ |
| 73 | '', |
| 74 | 'Retry fixes to prioritize:', |
| 75 | `- This is retry ${args.retryContext.attempt}/${args.retryContext.maxRetries}.`, |
| 76 | `- Previous failure: ${previousError}`, |
| 77 | '- Output only a complete creative page fragment. The write tool will add section/main/content semantics when they are missing. Do not output a full document, page shell, or runtime scripts.', |
| 78 | shouldMentionWriteToolFix |
| 79 | ? `- The previous attempt did not write the target page. You must call ${writeToolName}(pageId="${args.pageId}", content=...) before any final response; do not only describe the HTML in the final response.` |
| 80 | : '', |
| 81 | shouldMentionTemplateSkeletonFix |
| 82 | ? '- The previous attempt dropped template skeleton resources. Reread the target template page, find the missing local asset references from the error, and include the corresponding background/decorative layers in the next write.' |
| 83 | : '', |
| 84 | '- Before calling the write tool, mentally validate that the main containers are closed and that no tag is left unfinished at the end.', |
| 85 | '- If the previous issue was unclosed tags, do not patch the broken fragment. Rewrite a simpler, self-contained fragment from scratch with balanced tags and no page shell (section[data-page-scaffold], main[data-role="content"], or runtime frame). Keep the hierarchy shallow and avoid unnecessary wrappers or modules.', |
| 86 | '- If the previous issue was page shell structure, do not include .ppt-page-root, .ppt-page-content, .ppt-page-fit-scope, or data-ppt-guard-root anywhere, including CSS selectors, class names, scripts, and comments.', |
| 87 | shouldMentionChartFix |
| 88 | ? `- The previous issue involved chart API usage. Before repairing or writing chart code: ${formatSkillUsageRequirement(CHART_SKILL_NAME)}` |
| 89 | : '' |
| 90 | ].filter(Boolean) |
| 91 | : [] |
| 92 | const canUseSourceDocuments = |
| 93 | (!isSectionAgendaPage || referenceTextLocked) && Boolean(args.sourceDocumentPaths?.length) |
| 94 | let sourceDocumentInstructions: string[] = [] |
| 95 | if (canUseSourceDocuments && referenceDocumentPath) { |
| 96 | sourceDocumentInstructions = [ |
| 97 | '', |
| 98 | args.referenceDocumentSnippets?.trim() || '', |
| 99 | '', |
| 100 | buildReferenceRangeContentBoundaryRules(referenceDocumentPath, args.pageReferenceContext), |
| 101 | '- Content points and retrieved snippets are for locating the raw source passage only. They are not permission to render planning metadata or normalized snippet text.', |
| 102 | args.isRetryMode |
| 103 | ? '- This is a failed-slide retry. Keep the page within its Reference Range Content Boundary; repair layout without introducing outside facts.' |
| 104 | : '' |
| 105 | ].filter(Boolean) |
| 106 | } else if (canUseSourceDocuments && args.sourceDocumentPaths) { |
| 107 | sourceDocumentInstructions = |
| 108 | args.referenceDocumentSnippets && args.referenceDocumentSnippets.trim().length > 0 |
| 109 | ? [ |
| 110 | '', |
| 111 | args.referenceDocumentSnippets.trim(), |
| 112 | '', |
| 113 | 'Source document requirements:', |
| 114 | '- This slide already has program-side retrieved snippets.', |
| 115 | `- Source document paths: ${args.sourceDocumentPaths.join(', ')}`, |
| 116 | SOURCE_DOCUMENT_READ_STRATEGY, |
| 117 | SOURCE_DOCUMENT_FACT_RULE, |
| 118 | SOURCE_GROUNDED_EXPANSION_RULES, |
| 119 | args.isRetryMode |
| 120 | ? '- This is a failed-slide retry. Match source material only around this slide title and content points; do not reconstruct the whole deck outline.' |
| 121 | : '' |
| 122 | ].filter(Boolean) |
| 123 | : [ |
| 124 | '', |
| 125 | 'Source document requirements:', |
| 126 | `- Source document paths: ${args.sourceDocumentPaths.join(', ')}`, |
| 127 | '- No retrieved snippets matched this slide.', |
| 128 | SOURCE_DOCUMENT_LOCATE_THEN_READ_RULE, |
| 129 | '- First extract keywords, business objects, time points, system names, and metrics from this slide title and content points; then match relevant source passages.', |
| 130 | '- Do not copy the whole document indiscriminately.', |
| 131 | SOURCE_DOCUMENT_FACT_RULE, |
| 132 | SOURCE_GROUNDED_EXPANSION_RULES, |
| 133 | args.isRetryMode |
| 134 | ? '- This is a failed-slide retry. Match source material only around this slide title and content points; do not reconstruct the whole deck outline.' |
| 135 | : '' |
| 136 | ].filter(Boolean) |
| 137 | } |
| 138 | const hasSourceRange = Boolean(args.pageReferenceContext) || /Source range:\s*lines\s+\d+\s*-\s*\d+/i.test(args.pageOutline || '') |
| 139 | const canvasScenario = resolveCanvasScenario(args.slideSize) |
| 140 | const sourceRangeInstructions = |
| 141 | (!isSectionAgendaPage || referenceTextLocked) && |
| 142 | args.sourceDocumentPaths && |
| 143 | args.sourceDocumentPaths.length > 0 && |
| 144 | hasSourceRange |
| 145 | ? [ |
| 146 | '', |
| 147 | 'Range-bound source reading:', |
| 148 | '- Content points include a Source range. Before writing this slide, inspect that source heading/range first through the source-reading skill.', |
| 149 | '- Use retrieved snippets as an index only. If snippets are missing or broad, the Source range remains the primary content boundary.', |
| 150 | referenceTextLocked |
| 151 | ? '- Rephrase and reorganize for the presentation when useful, but keep every displayed fact, qualifier, relationship, and conclusion within the selected range.' |
| 152 | : '- Do not pull facts from unrelated sections just because they match keywords.' |
| 153 | ] |
| 154 | : [] |
| 155 | const sectionAgendaInstructions = isSectionAgendaPage |
| 156 | ? referenceTextLocked |
| 157 | ? [ |
| 158 | '', |
| 159 | 'Section agenda page requirements:', |
| 160 | '- The page is governed by the Reference Range Content Boundary. Use the source heading/range for chapter context.', |
| 161 | args.pageReferenceContext?.agendaItems?.length |
| 162 | ? `- Use only these recorded Agenda items as child topics: ${JSON.stringify(args.pageReferenceContext.agendaItems)}.` |
| 163 | : '- This legacy agenda has no recorded Agenda items; retain its existing agenda behavior without inferring child topics from document body.', |
| 164 | '- Do not add fixed visible labels such as “Agenda”, “目录”, “本章包括”, or “章节导航” unless the source itself contains them.' |
| 165 | ] |
| 166 | : [ |
| 167 | '', |
| 168 | 'Section agenda page requirements:', |
| 169 | '- This slide is a chapter agenda/table-of-contents page.', |
| 170 | '- Use only the child topic names already listed in Content points.', |
| 171 | '- Do not inspect, retrieve, cite, summarize, or expand from the source document for this slide.', |
| 172 | '- Keep it as a presentation agenda: chapter title plus concise child-topic list.' |
| 173 | ] |
| 174 | : [] |
| 175 | const contentDecisionInstructions = referenceTextLocked |
| 176 | ? [ |
| 177 | '', |
| 178 | 'Reference-bound decision before writing:', |
| 179 | '- The selected range supplies the page facts. You may summarize repetition, regroup content, and use neutral structural copy, but may not introduce outside facts or change the source meaning.', |
| 180 | '- Planning text, slide titles, and style guidance may determine the presentation. They must not create unsupported numbers, dates, proper nouns, causal claims, or business conclusions.', |
| 181 | '- When the source is dense, prioritize hierarchy, grouping, and visualization; then use compact internal layout 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.' |
| 182 | ] |
| 183 | : [ |
| 184 | '', |
| 185 | 'Required content enrichment decision before writing:', |
| 186 | '- First use the Canvas scenario rules to decide the page form and focal message; then use the scenario expansion rules only to decide whether the content itself needs enrichment or optimization.', |
| 187 | '- If the content points are only a title, one short sentence, or 1-2 seed phrases, the page is thin: enrich the warranted structure before writing the final content.', |
| 188 | '- If the content points already contain enough facts, the page is not thin: group and compress instead of adding more visible modules.', |
| 189 | '- This content decision happens before animation and final HTML; animation is downstream only and must follow the current canvas scenario, source grounding, and warranted content enrichment.', |
| 190 | '', |
| 191 | 'Expansion selection guardrails:', |
| 192 | '- Treat content points as short seed phrases, not as a checklist that must become one visible card/row per point. Decide which points are primary, grouped support, compact annotations, or lower-priority detail based on the slide title, source range, and available space.', |
| 193 | '- When source documents are present, expansion must be source-grounded through SOURCE_GROUNDED_EXPANSION_RULES: if inspected material is thin, enrich the slide from inspected material; if it is dense, summarize and group.', |
| 194 | `- Do not add generic industry framing, unsupported ${SOURCE_UNSUPPORTED_CLAIMS}, or polished-sounding conclusions that are absent from the source document.`, |
| 195 | '- Do not duplicate the same source facts in multiple large modules. If a fact appears in a timeline/table/chart, do not repeat it again as a separate summary card unless it is the single hero message of the slide.', |
| 196 | '- When there are many same-level points, preserve the main meaning by grouping related points and surfacing only the amount that fits a real slide with actual gaps between independent modules.', |
| 197 | '- When the user provided an explicit list of same-level topics, keep them distinct only where the layout allows; otherwise group under shared headings instead of creating equal-weight modules for every item.', |
| 198 | '- Prefer visualization-friendly expression. When points involve trends, comparisons, or proportions, use charts or data cards when appropriate.' |
| 199 | ] |
| 200 | return [ |
| 201 | `Generate and write only this ${canvasScenario.pageName}. Do not modify other pages.`, |
| 202 | '', |
| 203 | buildCanvasScenarioBrief(args.slideSize), |
| 204 | '', |
| 205 | buildCanvasScenarioContentRules(args.slideSize, { referenceTextLocked }), |
| 206 | '', |
| 207 | `Topic: ${args.topic}`, |
| 208 | `Deck title: ${args.deckTitle}`, |
| 209 | `Target page: ${args.pageId} (slide ${args.pageNumber})`, |
| 210 | `Slide title: ${args.pageTitle}`, |
| 211 | `Content points: ${args.pageOutline || 'Expand from the topic with moderate information density.'}`, |
| 212 | args.layoutIntent ? formatLayoutIntentPrompt(args.layoutIntent) : '', |
| 213 | args.layoutId && args.layoutPrompt ? args.layoutPrompt : '', |
| 214 | args.layoutId && getLayoutMasterTemplate(args.layoutId) |
| 215 | ? buildGenerationImageIntentRules({ |
| 216 | visualEnabled: args.visualEnabled === true, |
| 217 | template: getLayoutMasterTemplate(args.layoutId)!, |
| 218 | hasStyleImageDirection: args.hasStyleImageDirection |
| 219 | }) |
| 220 | : '', |
| 221 | ...sectionAgendaInstructions, |
| 222 | ...sourceDocumentInstructions, |
| 223 | ...sourceRangeInstructions, |
| 224 | '', |
| 225 | CONTENT_LANGUAGE_RULES, |
| 226 | '', |
| 227 | buildPageSemanticStructure(args.slideSize), |
| 228 | '', |
| 229 | buildCanvasConstraints(args.slideSize, { referenceTextLocked }), |
| 230 | '', |
| 231 | buildLayoutCollisionRules(args.slideSize), |
| 232 | '', |
| 233 | buildCanvasScenarioDeliveryGuard(args.slideSize, { referenceTextLocked }), |
| 234 | '', |
| 235 | FRONTEND_CAPABILITIES, |
| 236 | '', |
| 237 | STABLE_HTML_FRAGMENT_PROTOCOL, |
| 238 | ...retryInstructions, |
| 239 | '', |
| 240 | buildCanvasScenarioExpansionRules(args.slideSize, { referenceTextLocked }), |
| 241 | '', |
| 242 | ...contentDecisionInstructions, |
| 243 | '', |
| 244 | 'Single-slide tool constraints:', |
| 245 | `- Required action: call ${writeToolName}(pageId="${args.pageId}", content=complete creative page fragment).`, |
| 246 | `- This is not optional. A final text response without a successful ${writeToolName} tool call means the slide is not generated.`, |
| 247 | '- Do not call update_page_file. In this single-slide run it is intentionally not available.', |
| 248 | writeToolName === 'update_template_page_file' |
| 249 | ? '- Do not call update_single_page_file. This template run exposes update_template_page_file instead.' |
| 250 | : '', |
| 251 | '- content must be a complete creative page fragment. The tool will wrap it with section[data-page-scaffold], main[data-role="content"], editable data-block-id attributes, and the runtime page frame when needed.', |
| 252 | '- The content must not contain <!doctype>, <html>, <head>, <body>, .ppt-page-root, .ppt-page-content, .ppt-page-fit-scope, or data-ppt-guard-root.', |
| 253 | '- The content must be complete and balanced: close your main layout containers and leave no unfinished trailing tags.', |
| 254 | '- After the tool call succeeds, final response should be a short summary only. Do not paste the HTML in the final response.', |
| 255 | '- Do not modify other pages.', |
| 256 | '', |
| 257 | 'Tool context (pre-injected):', |
| 258 | `- Target file: ${args.pageId}.html (virtual path: /${args.pageId}.html)`, |
| 259 | '- Agent workspace root: /' |
| 260 | ].join('\n') |
| 261 | } |
| 262 |