ai-chatbot/lib/detail-ui-renderer.ts
Your Name 3e4594273b
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add detail ui protocol foundation
2026-03-21 19:30:57 +08:00

176 lines
5.2 KiB
TypeScript

import type {
DetailUiProtocol,
DetailUiScreenReviewPatch,
} from "@/lib/detail-ui-protocol";
import type { CanvasNode, TextNodeData } from "@/types/canvas";
type CanvasTextNodeDraft = Omit<CanvasNode, "id" | "createdAt" | "updatedAt">;
function createTextNodeDraft(params: {
parentId: string;
x: number;
y: number;
width: number;
height: number;
content: string;
fontSize: number;
fontWeight: string;
color: string;
lineHeight: number;
textAlign: "left" | "center" | "right";
metadata?: Record<string, unknown>;
}): CanvasTextNodeDraft {
return {
type: "text",
position: { x: params.x, y: params.y },
size: { width: params.width, height: params.height },
data: {
content: params.content,
fontSize: params.fontSize,
fontWeight: params.fontWeight,
color: params.color,
lineHeight: params.lineHeight,
textAlign: params.textAlign,
transparent: true,
metadata: params.metadata,
} as TextNodeData & { metadata?: Record<string, unknown> },
connections: [],
parentId: params.parentId,
};
}
export function buildDetailTextLayerNodesFromProtocol(params: {
parentNodeId: string;
parentPosition: { x: number; y: number };
parentSize: { width: number; height: number };
screenSize: { width: number; height: number };
screenProtocol?: DetailUiProtocol["screens"][number];
}) {
const protocolBlocks = params.screenProtocol?.blocks || [];
if (protocolBlocks.length === 0) return [] as CanvasTextNodeDraft[];
const scaleX = params.parentSize.width / Math.max(1, params.screenSize.width);
const scaleY = params.parentSize.height / Math.max(1, params.screenSize.height);
return protocolBlocks.map((block) =>
createTextNodeDraft({
parentId: params.parentNodeId,
x: params.parentPosition.x + Math.round(block.frame.x * scaleX),
y: params.parentPosition.y + Math.round(block.frame.y * scaleY),
width: Math.max(120, Math.round(block.frame.width * scaleX)),
height: Math.max(48, Math.round(block.frame.height * scaleY)),
content: block.content,
fontSize: Math.max(12, Math.round(block.style.fontSize * Math.min(scaleX, scaleY))),
fontWeight: block.style.fontWeight,
color: block.style.color,
lineHeight: block.style.lineHeight,
textAlign: block.style.textAlign,
metadata: {
detailUiBlockId: block.id,
detailUiBlockType: block.type,
detailUiScreenIndex: params.screenProtocol?.index,
},
}),
);
}
export function buildDetailTextLayerNodesFromReviewPatch(params: {
parentNodeId: string;
parentPosition: { x: number; y: number };
parentSize: { width: number; height: number };
screenSize: { width: number; height: number };
uiPatch?: DetailUiScreenReviewPatch;
}) {
if (!params.uiPatch) return [] as CanvasTextNodeDraft[];
return buildDetailTextLayerNodesFromProtocol({
parentNodeId: params.parentNodeId,
parentPosition: params.parentPosition,
parentSize: params.parentSize,
screenSize: params.screenSize,
screenProtocol: params.uiPatch.recommendedScreen,
});
}
export function buildLegacyDetailTextLayerNodes(params: {
parentNodeId: string;
parentPosition: { x: number; y: number };
parentWidth: number;
screenIndex: number;
title?: string;
subtitle?: string;
body?: string;
titleFontSize: number;
titleFontWeight: string;
subtitleFontSize: number;
subtitleFontWeight: string;
bodyFontSize: number;
bodyFontWeight: string;
}) {
const safeLeft = Math.round(params.parentWidth * 0.07);
const textWidth = Math.max(240, Math.round(params.parentWidth - safeLeft * 2));
const drafts: CanvasTextNodeDraft[] = [];
const titleText = String(params.title || "").trim();
const subtitleText = String(params.subtitle || "").trim();
const bodyText = String(params.body || "").trim();
if (titleText) {
drafts.push(
createTextNodeDraft({
parentId: params.parentNodeId,
x: params.parentPosition.x + safeLeft,
y: params.parentPosition.y + 72,
width: textWidth,
height: 140,
content: titleText,
fontSize:
params.screenIndex === 0
? params.titleFontSize
: Math.round(params.titleFontSize * 0.86),
fontWeight: params.titleFontWeight,
color: "#111827",
lineHeight: 1.15,
textAlign: "left",
}),
);
}
if (subtitleText) {
drafts.push(
createTextNodeDraft({
parentId: params.parentNodeId,
x: params.parentPosition.x + safeLeft,
y: params.parentPosition.y + 214,
width: textWidth,
height: 120,
content: subtitleText,
fontSize: params.subtitleFontSize,
fontWeight: params.subtitleFontWeight,
color: "#374151",
lineHeight: 1.22,
textAlign: "left",
}),
);
}
if (bodyText) {
drafts.push(
createTextNodeDraft({
parentId: params.parentNodeId,
x: params.parentPosition.x + safeLeft,
y: params.parentPosition.y + 304,
width: textWidth,
height: 260,
content: bodyText,
fontSize: params.bodyFontSize,
fontWeight: params.bodyFontWeight,
color: "#4b5563",
lineHeight: 1.38,
textAlign: "left",
}),
);
}
return drafts;
}