ai-chatbot/lib/detail-ui-templates.ts
Your Name 139b14b3c6
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add subject-safe detail overlay templates
2026-03-22 02:29:09 +08:00

215 lines
8.1 KiB
TypeScript

export type DetailUiTemplateId =
| "hero_poster_left"
| "hero_poster_right"
| "compare_dual_column"
| "feature_grid_bottom"
| "cta_focus";
export interface DetailUiTemplateFrame {
x: number;
y: number;
width: number;
height: number;
}
export interface DetailUiTemplateSpec {
id: DetailUiTemplateId;
subjectAvoidanceBox: DetailUiTemplateFrame;
badgeFrame: DetailUiTemplateFrame;
titleFrame: DetailUiTemplateFrame;
subtitleFrame: DetailUiTemplateFrame;
featureTagFrames: DetailUiTemplateFrame[];
featureCardFrames: DetailUiTemplateFrame[];
specTableSectionFrame: DetailUiTemplateFrame;
comparisonBeforeFrame: DetailUiTemplateFrame;
comparisonAfterFrame: DetailUiTemplateFrame;
proofFrame: DetailUiTemplateFrame;
ctaSupportFrame: DetailUiTemplateFrame;
ctaButtonFrame: DetailUiTemplateFrame;
ctaBannerFrame: DetailUiTemplateFrame;
}
const CANVAS_WIDTH = 790;
const CANVAS_HEIGHT = 1300;
const SUBJECT_BOX_CENTER: DetailUiTemplateFrame = {
x: 170,
y: 360,
width: 460,
height: 800,
};
const TEMPLATE_REGISTRY: Record<DetailUiTemplateId, DetailUiTemplateSpec> = {
hero_poster_left: {
id: "hero_poster_left",
subjectAvoidanceBox: SUBJECT_BOX_CENTER,
badgeFrame: { x: 56, y: 56, width: 120, height: 36 },
titleFrame: { x: 56, y: 110, width: 390, height: 136 },
subtitleFrame: { x: 56, y: 264, width: 338, height: 88 },
featureTagFrames: [
{ x: 56, y: 384, width: 96, height: 28 },
{ x: 56, y: 510, width: 96, height: 28 },
{ x: 56, y: 636, width: 96, height: 28 },
],
featureCardFrames: [
{ x: 56, y: 404, width: 310, height: 114 },
{ x: 56, y: 530, width: 310, height: 114 },
{ x: 56, y: 656, width: 310, height: 114 },
],
specTableSectionFrame: { x: 56, y: 794, width: 330, height: 210 },
comparisonBeforeFrame: { x: 56, y: 794, width: 156, height: 164 },
comparisonAfterFrame: { x: 228, y: 794, width: 158, height: 164 },
proofFrame: { x: 56, y: 1022, width: 330, height: 64 },
ctaSupportFrame: { x: 56, y: 1104, width: 330, height: 68 },
ctaButtonFrame: { x: 56, y: 1186, width: 206, height: 56 },
ctaBannerFrame: { x: 274, y: 1186, width: 188, height: 56 },
},
hero_poster_right: {
id: "hero_poster_right",
subjectAvoidanceBox: SUBJECT_BOX_CENTER,
badgeFrame: { x: 618, y: 56, width: 116, height: 36 },
titleFrame: { x: 366, y: 110, width: 368, height: 136 },
subtitleFrame: { x: 410, y: 264, width: 324, height: 88 },
featureTagFrames: [
{ x: 422, y: 384, width: 92, height: 28 },
{ x: 422, y: 510, width: 92, height: 28 },
{ x: 422, y: 636, width: 92, height: 28 },
],
featureCardFrames: [
{ x: 422, y: 404, width: 312, height: 114 },
{ x: 422, y: 530, width: 312, height: 114 },
{ x: 422, y: 656, width: 312, height: 114 },
],
specTableSectionFrame: { x: 404, y: 794, width: 330, height: 210 },
comparisonBeforeFrame: { x: 404, y: 794, width: 156, height: 164 },
comparisonAfterFrame: { x: 576, y: 794, width: 158, height: 164 },
proofFrame: { x: 404, y: 1022, width: 330, height: 64 },
ctaSupportFrame: { x: 348, y: 1104, width: 386, height: 68 },
ctaButtonFrame: { x: 528, y: 1186, width: 206, height: 56 },
ctaBannerFrame: { x: 348, y: 1186, width: 168, height: 56 },
},
compare_dual_column: {
id: "compare_dual_column",
subjectAvoidanceBox: SUBJECT_BOX_CENTER,
badgeFrame: { x: 56, y: 56, width: 120, height: 36 },
titleFrame: { x: 56, y: 108, width: 340, height: 122 },
subtitleFrame: { x: 56, y: 246, width: 340, height: 78 },
featureTagFrames: [
{ x: 56, y: 350, width: 92, height: 28 },
{ x: 56, y: 722, width: 92, height: 28 },
{ x: 56, y: 1096, width: 92, height: 28 },
],
featureCardFrames: [
{ x: 56, y: 376, width: 330, height: 110 },
{ x: 56, y: 748, width: 330, height: 110 },
{ x: 56, y: 1122, width: 330, height: 94 },
],
specTableSectionFrame: { x: 404, y: 350, width: 330, height: 208 },
comparisonBeforeFrame: { x: 404, y: 350, width: 156, height: 178 },
comparisonAfterFrame: { x: 576, y: 350, width: 158, height: 178 },
proofFrame: { x: 404, y: 560, width: 330, height: 64 },
ctaSupportFrame: { x: 404, y: 640, width: 330, height: 68 },
ctaButtonFrame: { x: 404, y: 726, width: 194, height: 56 },
ctaBannerFrame: { x: 610, y: 726, width: 124, height: 56 },
},
feature_grid_bottom: {
id: "feature_grid_bottom",
subjectAvoidanceBox: SUBJECT_BOX_CENTER,
badgeFrame: { x: 56, y: 56, width: 120, height: 36 },
titleFrame: { x: 56, y: 104, width: 420, height: 126 },
subtitleFrame: { x: 56, y: 248, width: 360, height: 84 },
featureTagFrames: [
{ x: 56, y: 884, width: 92, height: 28 },
{ x: 314, y: 884, width: 92, height: 28 },
{ x: 572, y: 884, width: 92, height: 28 },
],
featureCardFrames: [
{ x: 56, y: 906, width: 220, height: 124 },
{ x: 286, y: 906, width: 220, height: 124 },
{ x: 516, y: 906, width: 218, height: 124 },
],
specTableSectionFrame: { x: 56, y: 1048, width: 398, height: 196 },
comparisonBeforeFrame: { x: 470, y: 1048, width: 128, height: 176 },
comparisonAfterFrame: { x: 606, y: 1048, width: 128, height: 176 },
proofFrame: { x: 56, y: 1254, width: 330, height: 40 },
ctaSupportFrame: { x: 398, y: 1234, width: 336, height: 52 },
ctaButtonFrame: { x: 536, y: 1168, width: 198, height: 50 },
ctaBannerFrame: { x: 398, y: 1168, width: 126, height: 50 },
},
cta_focus: {
id: "cta_focus",
subjectAvoidanceBox: SUBJECT_BOX_CENTER,
badgeFrame: { x: 56, y: 56, width: 120, height: 36 },
titleFrame: { x: 56, y: 108, width: 420, height: 126 },
subtitleFrame: { x: 56, y: 248, width: 360, height: 84 },
featureTagFrames: [
{ x: 56, y: 860, width: 92, height: 28 },
{ x: 56, y: 982, width: 92, height: 28 },
{ x: 56, y: 1104, width: 92, height: 28 },
],
featureCardFrames: [
{ x: 56, y: 882, width: 338, height: 110 },
{ x: 56, y: 1004, width: 338, height: 110 },
{ x: 56, y: 1126, width: 338, height: 110 },
],
specTableSectionFrame: { x: 414, y: 870, width: 320, height: 180 },
comparisonBeforeFrame: { x: 414, y: 1062, width: 152, height: 154 },
comparisonAfterFrame: { x: 582, y: 1062, width: 152, height: 154 },
proofFrame: { x: 414, y: 882, width: 320, height: 60 },
ctaSupportFrame: { x: 414, y: 954, width: 320, height: 68 },
ctaButtonFrame: { x: 414, y: 1140, width: 320, height: 56 },
ctaBannerFrame: { x: 414, y: 1206, width: 320, height: 48 },
},
};
function rectsIntersect(a: DetailUiTemplateFrame, b: DetailUiTemplateFrame) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
);
}
function clampFrame(frame: DetailUiTemplateFrame) {
return {
x: Math.max(20, Math.min(CANVAS_WIDTH - frame.width - 20, frame.x)),
y: Math.max(20, Math.min(CANVAS_HEIGHT - frame.height - 20, frame.y)),
width: frame.width,
height: frame.height,
};
}
export function moveFrameForSubjectAvoidance(
frame: DetailUiTemplateFrame,
avoid: DetailUiTemplateFrame,
preference: "top" | "left" | "right" | "bottom",
) {
if (!rectsIntersect(frame, avoid)) return clampFrame(frame);
const gap = 18;
if (preference === "top") {
return clampFrame({ ...frame, y: avoid.y - frame.height - gap });
}
if (preference === "left") {
return clampFrame({ ...frame, x: avoid.x - frame.width - gap });
}
if (preference === "right") {
return clampFrame({ ...frame, x: avoid.x + avoid.width + gap });
}
return clampFrame({ ...frame, y: avoid.y + avoid.height + gap });
}
export function resolveDetailUiTemplate(params: {
screenIndex: number;
totalScreens?: number;
screenType?: string;
}): DetailUiTemplateSpec {
if ((params.totalScreens || 0) > 0 && params.screenIndex === params.totalScreens) {
return TEMPLATE_REGISTRY.cta_focus;
}
if (params.screenType === "A") return TEMPLATE_REGISTRY.hero_poster_right;
if (params.screenType === "C") return TEMPLATE_REGISTRY.compare_dual_column;
if (params.screenIndex <= 2) return TEMPLATE_REGISTRY.hero_poster_left;
return TEMPLATE_REGISTRY.feature_grid_bottom;
}