Files
2026-07-13 13:08:41 +08:00

218 lines
5.5 KiB
TypeScript

import fs from "fs/promises";
import path from "path";
import { compileTemplateSchema } from "@/lib/compile-template-schema";
import { getFastApiAuthHeaders, getFastApiBaseUrl } from "@/lib/fastapi-internal";
export type BuiltinLayoutSlide = {
id: string;
name: string;
description: string;
json_schema: unknown;
};
const DEFAULT_ICON_WEIGHT = "bold";
const ALLOWED_ICON_WEIGHTS = new Set([
"bold",
"duotone",
"fill",
"light",
"regular",
"thin",
]);
function normalizeIconWeight(value: unknown): string {
if (typeof value !== "string") {
return DEFAULT_ICON_WEIGHT;
}
const normalized = value.trim().toLowerCase().replace("_", "-");
return ALLOWED_ICON_WEIGHTS.has(normalized) ? normalized : DEFAULT_ICON_WEIGHT;
}
function getIconWeightFromSettings(settings: Record<string, unknown>): string {
return normalizeIconWeight(settings.icon_weight);
}
/**
* Build layout + JSON schemas for a built-in template directory without importing
* `presentation-templates/index.tsx` (that pulls Recharts/client bundles into RSC).
*
* Scans `app/presentation-templates/<group>/*.tsx` and runs the same schema compiler
* the UI uses. Slide IDs match `getSchemaByTemplateId` in `presentation-templates/index.tsx`
* (`id` is the fully-qualified `<group>:<layoutId>` form used by layout lookup).
* Slide order is sorted by filename for stability (may differ slightly from registration
* order in index.tsx).
*/
export async function buildBuiltinTemplateLayoutPayload(group: string): Promise<{
name: string;
ordered: boolean;
icon_weight: string;
slides: BuiltinLayoutSlide[];
} | null> {
const dir = path.join(
process.cwd(),
"app",
"presentation-templates",
group,
);
try {
await fs.access(dir);
} catch {
return null;
}
let ordered = false;
let icon_weight = DEFAULT_ICON_WEIGHT;
try {
const raw = await fs.readFile(path.join(dir, "settings.json"), "utf8");
const s = JSON.parse(raw) as Record<string, unknown> & { ordered?: boolean };
if (typeof s.ordered === "boolean") {
ordered = s.ordered;
}
icon_weight = getIconWeightFromSettings(s);
} catch {
// settings.json optional
}
const entries = await fs.readdir(dir, { withFileTypes: true });
const tsxFiles = entries
.filter((e) => e.isFile() && e.name.endsWith(".tsx"))
.map((e) => e.name)
.filter(
(n) =>
!n.includes(".test.") &&
!n.includes(".spec.") &&
n !== "ExampleSlideLayoutTemplate.tsx",
)
.sort();
const slides: BuiltinLayoutSlide[] = [];
for (const file of tsxFiles) {
const full = path.join(dir, file);
const code = await fs.readFile(full, "utf8");
const compiled = compileTemplateSchema(code);
if (!compiled) {
continue;
}
const qualifiedLayoutId = compiled.layoutId.includes(":")
? compiled.layoutId
: `${group}:${compiled.layoutId}`;
slides.push({
id: qualifiedLayoutId,
name: compiled.layoutName,
description: compiled.layoutDescription,
json_schema: compiled.schemaJSON,
});
}
if (slides.length === 0) {
return null;
}
return { name: group, ordered, icon_weight, slides };
}
export type CustomLayoutCompileInput = {
layout_id: string;
layout_name: string;
layout_code: string;
};
/**
* Compile custom template slide layouts stored in the DB (same IDs as /schema?group=custom-*).
*/
export function buildCustomTemplateLayoutPayload(
group: string,
layouts: CustomLayoutCompileInput[],
): {
name: string;
ordered: boolean;
icon_weight: string;
slides: BuiltinLayoutSlide[];
} | null {
if (!group.startsWith("custom-") || layouts.length === 0) {
return null;
}
const slides: BuiltinLayoutSlide[] = [];
for (const layout of layouts) {
const compiled = compileTemplateSchema(layout.layout_code);
if (!compiled) {
console.warn(
`[server-template-layouts] failed to compile custom layout ${layout.layout_name}`,
);
continue;
}
const qualifiedLayoutId = `${group}:${compiled.layoutId}`;
slides.push({
id: qualifiedLayoutId,
name: compiled.layoutName || layout.layout_name,
description: compiled.layoutDescription,
json_schema: compiled.schemaJSON,
});
}
if (slides.length === 0) {
return null;
}
return {
name: group,
ordered: false,
icon_weight: DEFAULT_ICON_WEIGHT,
slides,
};
}
type FastApiTemplateLayoutsResponse = {
layouts?: Array<{
layout_id: string;
layout_name: string;
layout_code: string;
}>;
};
/**
* Fetch custom template slide code from FastAPI and compile (for `/api/template?group=custom-*`).
*/
export async function buildCustomTemplateLayoutPayloadFromApi(
group: string,
): Promise<{
name: string;
ordered: boolean;
icon_weight: string;
slides: BuiltinLayoutSlide[];
} | null> {
if (!group.startsWith("custom-")) {
return null;
}
const url = `${getFastApiBaseUrl()}/api/v1/ppt/template/${encodeURIComponent(group)}/layouts`;
const response = await fetch(url, {
headers: getFastApiAuthHeaders(),
cache: "no-store",
});
if (!response.ok) {
return null;
}
const data = (await response.json()) as FastApiTemplateLayoutsResponse;
const layouts = data.layouts ?? [];
if (layouts.length === 0) {
return null;
}
return buildCustomTemplateLayoutPayload(
group,
layouts.map((layout) => ({
layout_id: layout.layout_id,
layout_name: layout.layout_name,
layout_code: layout.layout_code,
})),
);
}