fix: surface parsed PSD templates in agent dialog
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-27 21:27:04 +08:00
parent e23912d48f
commit e562e48c13
2 changed files with 108 additions and 10 deletions

View File

@ -10,6 +10,7 @@ import {
type AgentPsdTemplateMutationResponse,
} from "@/lib/agents/psd-template-config";
import {
type ParsedPsdTemplateLayer,
parsePsdTemplateFile,
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
@ -40,22 +41,105 @@ function getErrorMessage(error: unknown) {
return "PSD 解析失败";
}
function getCanvasOverlapArea(params: {
x: number;
y: number;
width: number;
height: number;
canvasWidth: number;
canvasHeight: number;
}) {
const left = Math.max(0, params.x);
const top = Math.max(0, params.y);
const right = Math.min(params.canvasWidth, params.x + params.width);
const bottom = Math.min(params.canvasHeight, params.y + params.height);
return Math.max(0, right - left) * Math.max(0, bottom - top);
}
function pickPrimaryImageLayerId(params: {
parsedLayers: Awaited<ReturnType<typeof parsePsdTemplateFile>>["layers"];
canvasWidth: number;
canvasHeight: number;
}) {
const { parsedLayers, canvasWidth, canvasHeight } = params;
const imageLayers = parsedLayers.filter((layer) => layer.slotType === "image");
if (imageLayers.length === 0) {
return null;
}
const backgroundPattern = /(背景|底图|bg|background)/i;
const scored = imageLayers.map((layer) => {
const overlapArea = getCanvasOverlapArea({
x: layer.x,
y: layer.y,
width: layer.width,
height: layer.height,
canvasWidth,
canvasHeight,
});
const layerArea = Math.max(1, layer.width * layer.height);
const coversWholeCanvas =
overlapArea >= canvasWidth * canvasHeight * 0.96 &&
layer.x <= 0 &&
layer.y <= 0 &&
layer.width >= canvasWidth * 0.96 &&
layer.height >= canvasHeight * 0.96;
const backgroundPenalty =
backgroundPattern.test(layer.layerName) || backgroundPattern.test(layer.layerPath) || coversWholeCanvas
? 0.45
: 1;
return {
layer,
score: Math.max(overlapArea, layerArea * 0.5) * backgroundPenalty,
};
});
scored.sort((left, right) => right.score - left.score);
return scored[0]?.layer.id || null;
}
function shouldAutoEditableTextLayer(
layer: ParsedPsdTemplateLayer
) {
const text = String(layer.defaultText || "").replace(/\s+/g, "");
if (!text) {
return false;
}
return (
layer.width >= 80 ||
layer.height >= 30 ||
text.length >= 4 ||
/[A-Za-z0-9]{3,}/.test(text)
);
}
function buildInitialTemplateLayers(params: {
parsedLayers: Awaited<ReturnType<typeof parsePsdTemplateFile>>["layers"];
fonts: Awaited<ReturnType<typeof readAgentPsdFonts>>;
canvasWidth: number;
canvasHeight: number;
}) {
const { parsedLayers, fonts } = params;
const { parsedLayers, fonts, canvasWidth, canvasHeight } = params;
const primaryImageLayerId = pickPrimaryImageLayerId({
parsedLayers,
canvasWidth,
canvasHeight,
});
return parsedLayers.map((layer): AgentPsdTemplateLayerSlot => {
if (layer.slotType === "text") {
const matchedFont = findBestMatchingPsdFont({
detectedFontNames: layer.detectedFontNames || [],
fonts,
});
const isEditable = shouldAutoEditableTextLayer(layer);
return {
...layer,
slotType: "text",
isEditable: false,
isEditable,
isRequired: false,
placeholder: layer.defaultText || layer.layerName,
detectedFontName: layer.detectedFontName,
@ -75,13 +159,14 @@ function buildInitialTemplateLayers(params: {
};
}
const isPrimaryImageLayer = layer.id === primaryImageLayerId;
return {
...layer,
slotType: "image",
isEditable: false,
isEditable: isPrimaryImageLayer,
isRequired: false,
isDefaultResultSource: false,
allowUserUploadOverride: true,
isDefaultResultSource: isPrimaryImageLayer,
allowUserUploadOverride: isPrimaryImageLayer,
replaceMode: "cover",
};
});
@ -207,6 +292,8 @@ export async function POST(
const initialLayers = buildInitialTemplateLayers({
parsedLayers: parsedTemplate.layers,
fonts,
canvasWidth: parsedTemplate.width,
canvasHeight: parsedTemplate.height,
});
await updateParseProgress({
@ -236,6 +323,13 @@ export async function POST(
toArrayBuffer(preparedAssets.renderBaseBuffer)
);
const hasEditableLayers = preparedAssets.layers.some((layer) => layer.isEditable);
const nextStatus =
currentTemplate.status === "active" ||
(currentTemplate.layers.length === 0 && hasEditableLayers)
? "active"
: currentTemplate.status;
const updatedTemplate = await updateTemplate(id, session.user.id, (template) => ({
...template,
previewUrl: uploadedPreview.url,
@ -246,6 +340,7 @@ export async function POST(
height: parsedTemplate.height,
layerCount: parsedTemplate.layers.length,
layers: preparedAssets.layers,
status: nextStatus,
parseStatus: "completed",
parseProgress: 100,
parseStage: "解析完成",

View File

@ -174,8 +174,11 @@ export function AgentPsdTemplateEditorDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden p-0">
<DialogHeader className="border-b px-6 py-5">
<DialogContent
overlayClassName="bg-black/85 backdrop-blur-sm"
className="max-h-[90vh] max-w-6xl overflow-hidden border border-white/10 bg-[#09090b] p-0 text-white shadow-[0_30px_120px_rgba(0,0,0,0.65)]"
>
<DialogHeader className="border-b border-white/10 bg-[#09090b] px-6 py-5">
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-orange-500" />
套用 PSD 模板
@ -186,7 +189,7 @@ export function AgentPsdTemplateEditorDialog({
</DialogHeader>
<div className="grid min-h-0 gap-0 lg:grid-cols-[360px,minmax(0,1fr),minmax(0,1fr)]">
<div className="border-r p-5">
<div className="border-r border-white/10 bg-[#09090b] p-5">
<div className="space-y-4">
<div className="space-y-2">
<Label>选择模板</Label>
@ -266,7 +269,7 @@ export function AgentPsdTemplateEditorDialog({
</div>
</div>
<div className="border-r p-5">
<div className="border-r border-white/10 bg-[#09090b] p-5">
<div className="mb-4">
<div className="text-sm font-medium">槽位内容</div>
<div className="mt-1 text-xs text-muted-foreground">
@ -430,7 +433,7 @@ export function AgentPsdTemplateEditorDialog({
</ScrollArea>
</div>
<div className="p-5">
<div className="bg-[#09090b] p-5">
<div className="mb-4">
<div className="text-sm font-medium">套版结果</div>
<div className="mt-1 text-xs text-muted-foreground">