返回 presentation-ai
layout-serializer.ts
根目录 / src / components / notebook / presentation / utils / layout-serializer.ts
1 import { type PlateSlide } from "./parser";
2 import { serializeSlideToXml } from "./slide-serializer";
3 import { TEMPLATE_DEFINITIONS } from "./templates";
4
5 type SerializedLayoutPromptItem = {
6 id: string;
7 xml: string;
8 };
9
10 type SerializeLayoutAssignmentsOptions = {
11 layoutIds: string[];
12 outlineItemIds?: string[];
13 overrides?: Record<string, string | null>;
14 };
15
16 function createLayoutLookup(): Map<
17 string,
18 (typeof TEMPLATE_DEFINITIONS)[number]
19 > {
20 const layoutById = new Map<string, (typeof TEMPLATE_DEFINITIONS)[number]>();
21
22 for (const layout of TEMPLATE_DEFINITIONS) {
23 layoutById.set(layout.id, layout);
24
25 for (const legacyId of layout.legacyIds ?? []) {
26 layoutById.set(legacyId, layout);
27 }
28 }
29
30 return layoutById;
31 }
32
33 function uniqueLayoutIds(layoutIds: readonly string[]): string[] {
34 return [
35 ...new Set(layoutIds.filter((layoutId) => layoutId.trim().length > 0)),
36 ];
37 }
38
39 function getLayoutsById(layoutIds: string[]): SerializedLayoutPromptItem[] {
40 const layoutById = createLayoutLookup();
41
42 return uniqueLayoutIds(layoutIds).flatMap((layoutId) => {
43 const layout = layoutById.get(layoutId);
44
45 if (!layout) {
46 return [];
47 }
48
49 const slide: PlateSlide = {
50 id: "",
51 ...layout.template,
52 content: layout.template.content ?? [],
53 };
54
55 return [
56 {
57 id: layoutId,
58 xml: serializeSlideToXml(slide, { mode: "layoutPrompt" }),
59 },
60 ];
61 });
62 }
63
64 function formatLayoutForPrompt(layout: SerializedLayoutPromptItem): string {
65 return `\`\`\`xml
66 ${layout.xml}
67 \`\`\``;
68 }
69
70 function getAssignedLayoutIds(
71 overrides: Record<string, string | null>,
72 ): string[] {
73 return Object.values(overrides).filter(
74 (layoutId): layoutId is string =>
75 typeof layoutId === "string" && layoutId.trim().length > 0,
76 );
77 }
78
79 export function getBestFitLayoutIds(
80 layoutIds: string[],
81 overrides: Record<string, string | null> = {},
82 ): string[] {
83 const assignedLayoutIds = new Set(getAssignedLayoutIds(overrides));
84
85 return uniqueLayoutIds(layoutIds).filter(
86 (layoutId) => !assignedLayoutIds.has(layoutId),
87 );
88 }
89
90 export function serializeLayoutsForPrompt(layoutIds: string[]): string {
91 const layouts = getLayoutsById(layoutIds);
92
93 if (layouts.length === 0) {
94 return "";
95 }
96
97 return `Selected XML layouts:
98 Use every selected XML layout exactly once on the slide where it best fits the outline, unless a slide-specific assignment says otherwise.
99 Preserve each selected layout's XML tags, attributes, order, and nesting. Replace every instructional placeholder with slide-specific content.
100
101 ${layouts.map(formatLayoutForPrompt).join("\n\n")}`;
102 }
103
104 export function serializeLayoutAssignmentsForPrompt({
105 layoutIds,
106 outlineItemIds = [],
107 overrides = {},
108 }: SerializeLayoutAssignmentsOptions): Record<number, string> {
109 const layoutDetails = new Map(
110 getLayoutsById([...layoutIds, ...getAssignedLayoutIds(overrides)]).map(
111 (layout) => [layout.id, formatLayoutForPrompt(layout)],
112 ),
113 );
114 const hints: Record<number, string> = {};
115
116 outlineItemIds.forEach((outlineId, index) => {
117 const layoutId = overrides[outlineId];
118
119 if (!layoutId) {
120 return;
121 }
122
123 const layoutDetail = layoutDetails.get(layoutId);
124 if (layoutDetail) {
125 hints[index] = layoutDetail;
126 }
127 });
128
129 for (const [key, layoutId] of Object.entries(overrides)) {
130 const parsedIndex = Number.parseInt(key, 10);
131 const index = key === "0" ? 0 : parsedIndex - 1;
132
133 if (!Number.isInteger(index) || index < 0 || !layoutId || hints[index]) {
134 continue;
135 }
136
137 const layoutDetail = layoutDetails.get(layoutId);
138 if (layoutDetail) {
139 hints[index] = layoutDetail;
140 }
141 }
142
143 return hints;
144 }
145
145 lines TYPESCRIPT