返回 oh-my-ppt
layout-master.ts
根目录 / src / shared / layout-master.ts
1 import { LAYOUT_INTENTS, normalizeLayoutIntent, type LayoutIntent } from './layout-intent'
2
3 export const MASTER_LAYOUTS_FILENAME = 'layouts.json'
4 export const MASTER_LAYOUTS_RELATIVE_PATH = `master/${MASTER_LAYOUTS_FILENAME}`
5 export const MASTER_LAYOUTS_VERSION = 1 as const
6
7 export const LAYOUT_MASTER_CATEGORIES = [
8 'cover',
9 'content',
10 'comparison',
11 'data',
12 'narrative',
13 'closing'
14 ] as const
15
16 export type LayoutMasterCategory = (typeof LAYOUT_MASTER_CATEGORIES)[number]
17
18 export type LayoutMasterTemplate = {
19 id: string
20 intent: LayoutIntent
21 category: LayoutMasterCategory
22 name: string
23 nameZh: string
24 description: string
25 descriptionZh: string
26 preview:
27 | 'title-center'
28 | 'title-split'
29 | 'editorial'
30 | 'two-column'
31 | 'metric-grid'
32 | 'chart-side'
33 | 'versus'
34 | 'timeline'
35 | 'process'
36 | 'quote'
37 | 'image-focus'
38 | 'closing'
39 prompt: string
40 }
41
42 export type SessionLayoutLibrary = {
43 version: typeof MASTER_LAYOUTS_VERSION
44 mappings: Record<LayoutIntent, string>
45 }
46
47 export type SessionLayoutLibraryStatus = {
48 library: SessionLayoutLibrary
49 exists: boolean
50 revision: string
51 }
52
53 const LAYOUT_MASTER_TEMPLATES: LayoutMasterTemplate[] = [
54 {
55 id: 'cover-statement',
56 intent: 'cover',
57 category: 'cover',
58 name: 'Statement cover',
59 nameZh: '主张式封面',
60 description: 'A single message with restrained supporting detail.',
61 descriptionZh: '单一核心主张,配合克制的辅助信息。',
62 preview: 'title-center',
63 prompt:
64 'Use a single dominant title or claim with generous negative space. Keep supporting information small and grouped; give one visual or decorative anchor a clear secondary role.'
65 },
66 {
67 id: 'cover-split',
68 intent: 'cover',
69 category: 'cover',
70 name: 'Split cover',
71 nameZh: '左右分屏封面',
72 description: 'A clear title block balanced by one hero visual.',
73 descriptionZh: '清晰标题区与单个主视觉平衡构成。',
74 preview: 'title-split',
75 prompt:
76 'Use an asymmetric split composition: title and context occupy one side, while one hero visual or visual field occupies the other. Keep the title block compact and make the split deliberate.'
77 },
78 {
79 id: 'content-editorial',
80 intent: 'concept',
81 category: 'content',
82 name: 'Editorial content',
83 nameZh: '编辑式内容页',
84 description: 'A title-led narrative with one clear reading path.',
85 descriptionZh: '标题主导的叙事内容,阅读路径明确。',
86 preview: 'editorial',
87 prompt:
88 'Use an editorial composition: establish a strong title zone, one primary idea or visual anchor, and a small number of supporting modules. Preserve a clear top-to-bottom or left-to-right reading path.'
89 },
90 {
91 id: 'content-two-column',
92 intent: 'concept',
93 category: 'content',
94 name: 'Two-column narrative',
95 nameZh: '双栏叙事页',
96 description: 'Two related content groups with intentional imbalance.',
97 descriptionZh: '两个相关内容组,以有意的不对称形成层级。',
98 preview: 'two-column',
99 prompt:
100 'Use two related columns with intentional hierarchy rather than equal card stacks. Give one column a primary role and use the other for explanation, evidence, or a supporting visual.'
101 },
102 {
103 id: 'data-metrics',
104 intent: 'data-focus',
105 category: 'data',
106 name: 'Metric focus',
107 nameZh: '核心指标页',
108 description: 'A key number or chart supported by concise evidence.',
109 descriptionZh: '核心数字或图表主导,配合简洁证据。',
110 preview: 'metric-grid',
111 prompt:
112 'Make one metric, trend, or chart the dominant visual anchor. Support it with no more than three concise evidence modules and make numeric hierarchy immediately scannable.'
113 },
114 {
115 id: 'data-chart-side',
116 intent: 'data-focus',
117 category: 'data',
118 name: 'Chart with takeaway',
119 nameZh: '图表结论页',
120 description: 'A chart-led area paired with a decisive takeaway.',
121 descriptionZh: '图表主区域配合明确结论。',
122 preview: 'chart-side',
123 prompt:
124 'Allocate a substantial chart or data visualization area and pair it with one concise takeaway panel. Let the chart carry the evidence and keep surrounding labels restrained.'
125 },
126 {
127 id: 'comparison-versus',
128 intent: 'comparison',
129 category: 'comparison',
130 name: 'Versus comparison',
131 nameZh: '正反对比',
132 description: 'Two alternatives aligned against shared criteria.',
133 descriptionZh: '两个方案围绕共用维度对齐比较。',
134 preview: 'versus',
135 prompt:
136 'Use two clearly separated alternatives aligned against the same comparison criteria. Keep their visual weight balanced, make differences explicit, and reserve a short conclusion area.'
137 },
138 {
139 id: 'comparison-matrix',
140 intent: 'comparison',
141 category: 'comparison',
142 name: 'Comparison matrix',
143 nameZh: '矩阵对比',
144 description: 'A compact shared-criteria comparison with one recommendation.',
145 descriptionZh: '围绕共用维度紧凑比较,并给出一个建议。',
146 preview: 'two-column',
147 prompt:
148 'Use a compact comparison matrix or aligned criterion rows. Surface the most meaningful distinction visually, then close with one recommendation or implication rather than repeating every point.'
149 },
150 {
151 id: 'timeline-progress',
152 intent: 'timeline',
153 category: 'narrative',
154 name: 'Progress timeline',
155 nameZh: '进程时间线',
156 description: 'A sequence of stages with one highlighted moment.',
157 descriptionZh: '阶段推进的时间线,突出一个关键节点。',
158 preview: 'timeline',
159 prompt:
160 'Use a clear chronological progression with a limited number of stages. Emphasize the most important moment or transition and keep supporting detail attached to its stage.'
161 },
162 {
163 id: 'timeline-milestones',
164 intent: 'timeline',
165 category: 'narrative',
166 name: 'Milestone story',
167 nameZh: '里程碑叙事',
168 description: 'A milestone sequence with a strong present or future state.',
169 descriptionZh: '里程碑序列,突出当前或未来状态。',
170 preview: 'timeline',
171 prompt:
172 'Use a milestone sequence that leads clearly to a highlighted current, decision, or future state. Give the highlighted destination more space than the historical steps.'
173 },
174 {
175 id: 'concept-hierarchy',
176 intent: 'concept',
177 category: 'content',
178 name: 'Concept hierarchy',
179 nameZh: '概念层级',
180 description: 'One central idea and grouped supporting concepts.',
181 descriptionZh: '一个中心概念,搭配分组的支撑信息。',
182 preview: 'process',
183 prompt:
184 'Use one central concept or proposition with a small number of grouped supporting concepts. Make the hierarchy visible through scale and proximity, not a dense network of arrows.'
185 },
186 {
187 id: 'process-flow',
188 intent: 'process',
189 category: 'narrative',
190 name: 'Flow process',
191 nameZh: '流程机制',
192 description: 'A directional flow with visible cause and effect.',
193 descriptionZh: '方向明确的流程,清楚表达因果关系。',
194 preview: 'process',
195 prompt:
196 'Use a directional process or mechanism with visible handoffs between steps. Keep each stage concise and make the causal or operational flow legible at a glance.'
197 },
198 {
199 id: 'process-cycle',
200 intent: 'process',
201 category: 'narrative',
202 name: 'Cycle process',
203 nameZh: '循环机制',
204 description: 'A recurring system with a deliberate feedback loop.',
205 descriptionZh: '循环系统,明确表现反馈关系。',
206 preview: 'process',
207 prompt:
208 'Use a compact recurring cycle when the mechanism includes feedback or iteration. Make the loop legible, but keep labels and step count restrained so the process reads in one glance.'
209 },
210 {
211 id: 'summary-takeaway',
212 intent: 'summary',
213 category: 'closing',
214 name: 'Key takeaway',
215 nameZh: '结论总结',
216 description: 'One conclusion supported by compact proof points.',
217 descriptionZh: '一个结论,配合紧凑的支撑证据。',
218 preview: 'closing',
219 prompt:
220 'Lead with one decisive conclusion. Use a compact set of supporting proof points or next actions beneath it, with the conclusion visually stronger than every support module.'
221 },
222 {
223 id: 'summary-evidence',
224 intent: 'summary',
225 category: 'closing',
226 name: 'Evidence recap',
227 nameZh: '证据回顾',
228 description: 'A concise conclusion supported by a few memorable facts.',
229 descriptionZh: '简洁结论配合少量关键事实回顾。',
230 preview: 'closing',
231 prompt:
232 'Use a concise conclusion with two to four memorable proof points. Let evidence appear as a compact recap, leaving enough negative space for the conclusion to remain dominant.'
233 },
234 {
235 id: 'quote-focus',
236 intent: 'quote',
237 category: 'content',
238 name: 'Quote focus',
239 nameZh: '引言聚焦',
240 description: 'A statement-led composition with minimal context.',
241 descriptionZh: '语句主导的构图,只保留最少必要背景。',
242 preview: 'quote',
243 prompt:
244 'Make the statement the visual anchor. Use an expressive type hierarchy and only minimal attribution or supporting context; do not dilute it with ordinary card grids.'
245 },
246 {
247 id: 'quote-side-note',
248 intent: 'quote',
249 category: 'content',
250 name: 'Quote with context',
251 nameZh: '引言与注释',
252 description: 'A strong statement paired with a compact contextual note.',
253 descriptionZh: '重点语句配合紧凑的背景说明。',
254 preview: 'quote',
255 prompt:
256 'Use a large statement zone paired with one compact context, source, or implication note. Maintain the statement as the visual anchor and keep the secondary note clearly subordinate.'
257 },
258 {
259 id: 'image-spotlight',
260 intent: 'image-focus',
261 category: 'content',
262 name: 'Image spotlight',
263 nameZh: '视觉聚焦',
264 description: 'A dominant visual field with concise supporting copy.',
265 descriptionZh: '主视觉区域占主导,文字简洁辅助。',
266 preview: 'image-focus',
267 prompt:
268 'Give one image, product visual, or illustrated field dominant space. Keep text to a concise title and short supporting copy, positioned to complement rather than compete with the visual.'
269 },
270 {
271 id: 'image-caption',
272 intent: 'image-focus',
273 category: 'content',
274 name: 'Visual caption',
275 nameZh: '图片注释页',
276 description: 'A visual field supported by a structured caption block.',
277 descriptionZh: '视觉主区域配合有层级的说明文字。',
278 preview: 'image-focus',
279 prompt:
280 'Use a dominant visual field with a structured caption or annotation block. The supporting text should interpret the visual, not compete with it or turn into a generic card grid.'
281 }
282 ]
283
284 const DEFAULT_LAYOUT_MAPPINGS: Record<LayoutIntent, string> = {
285 cover: 'cover-statement',
286 'data-focus': 'data-metrics',
287 comparison: 'comparison-versus',
288 timeline: 'timeline-progress',
289 concept: 'content-editorial',
290 process: 'process-flow',
291 summary: 'summary-takeaway',
292 quote: 'quote-focus',
293 'image-focus': 'image-spotlight'
294 }
295
296 const TEMPLATE_BY_ID = new Map(LAYOUT_MASTER_TEMPLATES.map((template) => [template.id, template]))
297
298 const isRecord = (value: unknown): value is Record<string, unknown> =>
299 Boolean(value) && typeof value === 'object' && !Array.isArray(value)
300
301 export const getLayoutMasterTemplates = (): LayoutMasterTemplate[] =>
302 LAYOUT_MASTER_TEMPLATES.map((template) => ({ ...template }))
303
304 export const getDefaultLayoutMasterMappings = (): Record<LayoutIntent, string> => ({
305 ...DEFAULT_LAYOUT_MAPPINGS
306 })
307
308 export const getLayoutMasterTemplate = (value: unknown): LayoutMasterTemplate | null => {
309 const id = typeof value === 'string' ? value.trim() : ''
310 const template = TEMPLATE_BY_ID.get(id)
311 return template ? { ...template } : null
312 }
313
314 export const buildDefaultSessionLayoutLibrary = (): SessionLayoutLibrary => ({
315 version: MASTER_LAYOUTS_VERSION,
316 mappings: getDefaultLayoutMasterMappings()
317 })
318
319 export const normalizeSessionLayoutLibrary = (value: unknown): SessionLayoutLibrary => {
320 const input = isRecord(value) ? value : {}
321 const rawMappings = isRecord(input.mappings) ? input.mappings : {}
322 const mappings = getDefaultLayoutMasterMappings()
323 for (const intent of LAYOUT_INTENTS) {
324 const candidate = rawMappings[intent]
325 const template = getLayoutMasterTemplate(candidate)
326 if (template && template.intent === intent) mappings[intent] = template.id
327 }
328 return { version: MASTER_LAYOUTS_VERSION, mappings }
329 }
330
331 export const isValidSessionLayoutLibrary = (value: unknown): value is SessionLayoutLibrary => {
332 if (!isRecord(value) || value.version !== MASTER_LAYOUTS_VERSION) return false
333 const mappings = value.mappings
334 if (!isRecord(mappings)) return false
335 return LAYOUT_INTENTS.every((intent) => {
336 const template = getLayoutMasterTemplate(mappings[intent])
337 return template?.intent === intent
338 })
339 }
340
341 export const resolveLayoutMasterTemplate = (
342 library: unknown,
343 intent: LayoutIntent | undefined
344 ): LayoutMasterTemplate => {
345 const normalizedIntent = normalizeLayoutIntent(intent)
346 const normalizedLibrary = normalizeSessionLayoutLibrary(library)
347 return (
348 getLayoutMasterTemplate(normalizedLibrary.mappings[normalizedIntent]) ||
349 getLayoutMasterTemplate(DEFAULT_LAYOUT_MAPPINGS[normalizedIntent]) ||
350 getLayoutMasterTemplates()[0]
351 )
352 }
353
354 export const formatLayoutMasterPrompt = (template: LayoutMasterTemplate): string =>
355 [
356 `Selected layout master: ${template.name} (${template.id}).`,
357 `Composition contract: ${template.prompt}`,
358 'Treat this as a flexible information architecture, not a pixel-for-pixel template. Keep the current style contract authoritative for visual language, and vary imagery, decoration, emphasis, and local composition to fit the content.'
359 ].join('\n')
360
360 lines TYPESCRIPT