返回 presentation-ai
customization.ts
根目录 / src / lib / presentation / customization.ts
1 import {
2 type ThemeBackground,
3 type ThemeProperties,
4 } from "@/lib/presentation/themes";
5 import { type PresentationGenerationAspectRatio } from "./aspect-ratio";
6 import {
7 isBuiltInPresentationTheme,
8 resolvePresentationThemeData,
9 type PresentationThemeSelectionSource,
10 } from "./theme-resolution";
11
12 export type PresentationCustomization = {
13 themeData?: ThemeProperties | null;
14 themeDataByTheme?: Record<string, ThemeProperties | null | undefined>;
15 generatedThemeData?: ThemeProperties | null;
16 themeSource?: PresentationThemeSelectionSource;
17 pageStyle?: string | null;
18 presentationStyle?: string | null;
19 generationAspectRatio?: PresentationGenerationAspectRatio;
20 textContent?: "minimal" | "concise" | "detailed" | "extensive";
21 tone?:
22 | "auto"
23 | "general"
24 | "persuasive"
25 | "inspiring"
26 | "instructive"
27 | "engaging";
28 audience?:
29 | "auto"
30 | "general"
31 | "business"
32 | "investor"
33 | "teacher"
34 | "student";
35 scenario?:
36 | "auto"
37 | "general"
38 | "analysis-report"
39 | "teaching-training"
40 | "promotional-materials"
41 | "public-speeches";
42 pageBackground?: {
43 override?: string | null;
44 type?: ThemeBackground["type"];
45 gradient?: ThemeBackground["gradient"];
46 imageUrl?: string | null;
47 };
48 selectedSlideTemplates?: string[];
49 outlineItemIds?: string[];
50 outlineTemplateOverrides?: Record<string, string | null>;
51 };
52
53 export type PresentationCustomizationState = {
54 customThemeData: ThemeProperties | null;
55 themeDataByTheme?: Record<string, ThemeProperties | null | undefined>;
56 generatedThemeData?: ThemeProperties | null;
57 theme?: string | null;
58 pageStyle: string;
59 presentationStyle: string;
60 generationAspectRatio: PresentationGenerationAspectRatio;
61 textContent: "minimal" | "concise" | "detailed" | "extensive";
62 tone:
63 | "auto"
64 | "general"
65 | "persuasive"
66 | "inspiring"
67 | "instructive"
68 | "engaging";
69 audience:
70 | "auto"
71 | "general"
72 | "business"
73 | "investor"
74 | "teacher"
75 | "student";
76 scenario:
77 | "auto"
78 | "general"
79 | "analysis-report"
80 | "teaching-training"
81 | "promotional-materials"
82 | "public-speeches";
83 pageBackground: Record<string, unknown>;
84 selectedSlideTemplates?: string[];
85 outlineItemIds?: string[];
86 outlineTemplateOverrides?: Record<string, string | null>;
87 };
88
89 export function getPresentationCustomization(
90 value: unknown,
91 ): PresentationCustomization | null {
92 if (!value || typeof value !== "object") return null;
93 return value as PresentationCustomization;
94 }
95
96 export function buildPresentationCustomization(
97 state: PresentationCustomizationState,
98 ): PresentationCustomization {
99 const isUserTheme =
100 typeof state.theme === "string" &&
101 state.theme.length > 0 &&
102 state.theme !== "auto" &&
103 !isBuiltInPresentationTheme(state.theme);
104 const customization: PresentationCustomization = {
105 generatedThemeData: state.generatedThemeData ?? undefined,
106 pageStyle: state.pageStyle ?? undefined,
107 presentationStyle: state.presentationStyle ?? undefined,
108 generationAspectRatio: state.generationAspectRatio,
109 textContent: state.textContent,
110 tone: state.tone,
111 audience: state.audience,
112 scenario: state.scenario,
113 };
114
115 if (state.theme !== undefined) {
116 customization.themeSource = state.theme === "auto" ? "auto" : "selected";
117 }
118
119 const themeDataByTheme = { ...(state.themeDataByTheme ?? {}) };
120
121 if (state.customThemeData && state.theme && !isUserTheme) {
122 themeDataByTheme[state.theme] = state.customThemeData;
123 }
124
125 if (Object.keys(themeDataByTheme).length > 0) {
126 customization.themeDataByTheme = {
127 ...themeDataByTheme,
128 };
129 }
130
131 if (state.customThemeData && state.theme === undefined) {
132 customization.themeData = state.customThemeData;
133 }
134
135 const pageBackground = extractPageBackgroundFromConfig(state.pageBackground);
136 if (pageBackground) {
137 customization.pageBackground = pageBackground;
138 }
139
140 if (state.selectedSlideTemplates && state.selectedSlideTemplates.length > 0) {
141 customization.selectedSlideTemplates = state.selectedSlideTemplates;
142 }
143
144 if (state.outlineItemIds && state.outlineItemIds.length > 0) {
145 customization.outlineItemIds = state.outlineItemIds;
146 }
147
148 if (
149 state.outlineTemplateOverrides &&
150 Object.keys(state.outlineTemplateOverrides).length > 0
151 ) {
152 customization.outlineTemplateOverrides = state.outlineTemplateOverrides;
153 }
154
155 return customization;
156 }
157
158 export function normalizeSelectedSlideTemplates(value: unknown): string[] {
159 if (!Array.isArray(value)) {
160 return [];
161 }
162
163 return value.filter((item): item is string => typeof item === "string");
164 }
165
166 export function normalizeOutlineTemplateOverrides(
167 value: unknown,
168 ): Record<string, string | null> {
169 if (!value || typeof value !== "object" || Array.isArray(value)) {
170 return {};
171 }
172
173 return Object.entries(value).reduce<Record<string, string | null>>(
174 (overrides, [key, templateId]) => {
175 if (typeof templateId === "string" || templateId === null) {
176 overrides[key] = templateId;
177 }
178
179 return overrides;
180 },
181 {},
182 );
183 }
184
185 export function getPresentationThemeCustomization(
186 customization: PresentationCustomization | null | undefined,
187 theme: string | null | undefined,
188 options: { includeLegacyThemeData?: boolean } = {},
189 ): ThemeProperties | null {
190 if (!customization) {
191 return null;
192 }
193
194 if (theme && customization.themeDataByTheme?.[theme]) {
195 return resolvePresentationThemeData({
196 customThemeData: customization.themeDataByTheme[theme],
197 theme: null,
198 });
199 }
200
201 if (theme && options.includeLegacyThemeData === false) {
202 return null;
203 }
204
205 return resolvePresentationThemeData({
206 customThemeData: customization.themeData,
207 theme: null,
208 });
209 }
210
211 export function getPresentationThemeDataByTheme(
212 customization: PresentationCustomization | null | undefined,
213 ): Record<string, ThemeProperties | null | undefined> {
214 const themeDataByTheme = customization?.themeDataByTheme;
215
216 if (!themeDataByTheme || typeof themeDataByTheme !== "object") {
217 return {};
218 }
219
220 return Object.entries(themeDataByTheme).reduce<
221 Record<string, ThemeProperties | null | undefined>
222 >((acc, [theme, themeData]) => {
223 if (themeData === null) {
224 acc[theme] = null;
225 return acc;
226 }
227
228 const parsed = resolvePresentationThemeData({
229 customThemeData: themeData,
230 theme: null,
231 });
232 if (parsed) {
233 acc[theme] = parsed;
234 }
235
236 return acc;
237 }, {});
238 }
239
240 function extractPageBackgroundFromConfig(
241 config: Record<string, unknown>,
242 ): PresentationCustomization["pageBackground"] | null {
243 if (!config) return null;
244
245 const override =
246 typeof config.backgroundOverride === "string"
247 ? (config.backgroundOverride as string)
248 : null;
249 const type =
250 typeof config.backgroundType === "string"
251 ? (config.backgroundType as ThemeBackground["type"])
252 : undefined;
253 const gradient = config.backgroundGradient as ThemeBackground["gradient"];
254 const imageUrl =
255 typeof config.backgroundImageUrl === "string"
256 ? (config.backgroundImageUrl as string)
257 : null;
258
259 if (!override && !type && !gradient && !imageUrl) return null;
260
261 return {
262 override: override ?? undefined,
263 type,
264 gradient,
265 imageUrl: imageUrl ?? undefined,
266 };
267 }
268
269 export function applyPageBackgroundToConfig(
270 pageBackground:
271 | PresentationCustomization["pageBackground"]
272 | null
273 | undefined,
274 config: Record<string, unknown>,
275 ): Record<string, unknown> {
276 if (!pageBackground) return config;
277
278 const next = { ...(config ?? {}) };
279
280 delete next.backgroundOverride;
281 delete next.backgroundType;
282 delete next.backgroundGradient;
283 delete next.backgroundImageUrl;
284
285 if (pageBackground.type) next.backgroundType = pageBackground.type;
286 if (pageBackground.override)
287 next.backgroundOverride = pageBackground.override;
288 if (pageBackground.gradient)
289 next.backgroundGradient = pageBackground.gradient;
290 if (pageBackground.imageUrl)
291 next.backgroundImageUrl = pageBackground.imageUrl;
292
293 return next;
294 }
295
295 lines TYPESCRIPT