import type { DetailUiGlobalStyle } from "@/lib/detail-ui-protocol"; import type { DetailUiDesignSkillSummary } from "@/lib/detail-ui-design-skill"; import type { UiScreenDocument, UiScreenLayer } from "@/lib/ui-screen/schema"; export const DETAIL_BRAND_STYLE_BOARD_VERSION = "detail-brand-style-board/v1"; type DetailStyleBoardBrief = { productName?: string; brandTone?: string; coreNeed?: string; usageScenario?: string; coreSellingPoint?: string; }; type PaletteSwatch = { id: string; name: string; role: string; hex: string; }; function normalizeText(value: unknown, fallback = "") { return String(value || "").replace(/\s+/g, " ").trim() || fallback; } function extractHexColors(value: unknown) { const matched = normalizeText(value).match(/#[0-9a-fA-F]{6}\b/g) || []; return Array.from(new Set(matched.map((item) => item.toLowerCase()))).slice(0, 6); } function hashText(value: string) { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = (hash * 31 + value.charCodeAt(index)) >>> 0; } return hash; } function chooseBasePalette(params: { language: "zh" | "en"; brief?: DetailStyleBoardBrief; globalStyle?: DetailUiGlobalStyle; }): PaletteSwatch[] { const extracted = extractHexColors(params.globalStyle?.palette); if (extracted.length >= 3) { const fallbacks = ["#f8fafc", "#0f172a", "#0071e3", "#14b8a6", "#f59e0b"]; const colors = [...extracted, ...fallbacks].slice(0, 5); return [ { id: "primary", name: "Primary", role: "主行动/重点卖点", hex: colors[0]! }, { id: "ink", name: "Ink", role: "标题/强文字", hex: colors[1]! }, { id: "surface", name: "Surface", role: "面板/留白", hex: colors[2]! }, { id: "accent", name: "Accent", role: "证据/对比", hex: colors[3]! }, { id: "warm", name: "Warm", role: "价格/提醒", hex: colors[4]! }, ]; } const tone = normalizeText(params.brief?.brandTone).toLowerCase(); const seed = `${params.brief?.productName || ""} ${params.brief?.coreSellingPoint || ""} ${tone}`; const presetIndex = hashText(seed) % 5; const reliable = /专业|可靠|信任|稳|medical|pro|trust|reliable|clinical/.test(tone); const premium = /高级|高端|轻奢|premium|luxury|minimal|质感/.test(tone); const friendly = /亲和|温暖|活泼|friendly|warm|soft|生活/.test(tone); const tech = /科技|智能|未来|tech|smart|digital|ai/.test(tone); if (premium) { return [ { id: "primary", name: "Obsidian", role: "主标题/高级底色", hex: "#111827" }, { id: "ink", name: "Graphite", role: "正文文字", hex: "#1f2937" }, { id: "surface", name: "Porcelain", role: "大面积背景", hex: "#f8fafc" }, { id: "accent", name: "Champagne", role: "重点证明/质感线", hex: "#d6a756" }, { id: "support", name: "Mist Blue", role: "辅助信息", hex: "#dbeafe" }, ]; } if (friendly) { return [ { id: "primary", name: "Coral", role: "主 CTA/亲和重点", hex: "#e85d75" }, { id: "ink", name: "Cocoa Ink", role: "标题文字", hex: "#312e2b" }, { id: "surface", name: "Milk", role: "干净背景", hex: "#fff7ed" }, { id: "accent", name: "Fresh Mint", role: "舒适/安心", hex: "#2dd4bf" }, { id: "support", name: "Sky", role: "辅助信息", hex: "#dbeafe" }, ]; } if (tech) { return [ { id: "primary", name: "Electric Blue", role: "主行动/科技重点", hex: "#2563eb" }, { id: "ink", name: "Deep Navy", role: "标题文字", hex: "#0f172a" }, { id: "surface", name: "Ice", role: "背景/面板", hex: "#eff6ff" }, { id: "accent", name: "Signal Green", role: "性能/通过", hex: "#22c55e" }, { id: "support", name: "Violet", role: "辅助强调", hex: "#8b5cf6" }, ]; } if (reliable || presetIndex <= 2) { return [ { id: "primary", name: "Apple Blue", role: "主 CTA/核心卖点", hex: "#0071e3" }, { id: "ink", name: "Slate Ink", role: "标题/正文", hex: "#0f172a" }, { id: "surface", name: "Cloud", role: "大面积留白", hex: "#f5f5f7" }, { id: "accent", name: "Teal Proof", role: "证据/安心", hex: "#14b8a6" }, { id: "support", name: "Amber", role: "提醒/价格", hex: "#f59e0b" }, ]; } return [ { id: "primary", name: "Blue", role: "主 CTA/核心卖点", hex: "#2563eb" }, { id: "ink", name: "Ink", role: "标题/正文", hex: "#111827" }, { id: "surface", name: "Surface", role: "背景/卡片", hex: "#f8fafc" }, { id: "accent", name: "Emerald", role: "证明/通过", hex: "#10b981" }, { id: "support", name: "Rose", role: "提醒/差异", hex: "#f43f5e" }, ]; } function hexToRgb(hex: string) { const value = hex.replace("#", ""); return { r: Number.parseInt(value.slice(0, 2), 16), g: Number.parseInt(value.slice(2, 4), 16), b: Number.parseInt(value.slice(4, 6), 16), }; } function contrastText(hex: string) { const { r, g, b } = hexToRgb(hex); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.62 ? "#0f172a" : "#ffffff"; } function textLayer(params: { id: string; text: string; fontSize: number; fontWeight?: number | string; color?: string; lineHeight?: number; textAlign?: "left" | "center" | "right"; }): UiScreenLayer { return { id: params.id, kind: "text", text: params.text, style: { fontSize: params.fontSize, fontWeight: params.fontWeight || 500, lineHeight: params.lineHeight || 1.25, color: params.color || "#0f172a", textAlign: params.textAlign, letterSpacing: 0, }, }; } function badgeLayer(params: { id: string; label: string; tone?: "primary" | "secondary" | "neutral" | "success"; }): UiScreenLayer { return { id: params.id, kind: "badge", label: params.label, tone: params.tone || "neutral", style: { fontSize: 14, fontWeight: 700, paddingX: 12, paddingY: 7, }, }; } function colorSwatchLayer(swatch: PaletteSwatch): UiScreenLayer { const textColor = contrastText(swatch.hex); return { id: `swatch-${swatch.id}`, kind: "card", name: swatch.name, style: { background: swatch.hex, color: textColor, borderRadius: 10, padding: 16, gap: 8, boxShadow: "0 10px 26px rgba(15,23,42,0.08)", }, children: [ textLayer({ id: `swatch-${swatch.id}-name`, text: swatch.name, fontSize: 18, fontWeight: 700, color: textColor, }), textLayer({ id: `swatch-${swatch.id}-hex`, text: swatch.hex.toUpperCase(), fontSize: 15, fontWeight: 650, color: textColor, }), textLayer({ id: `swatch-${swatch.id}-role`, text: swatch.role, fontSize: 13, fontWeight: 500, color: textColor, lineHeight: 1.35, }), ], }; } function sectionFrame(params: { id: string; bounds: { x: number; y: number; width: number; height: number }; children: UiScreenLayer[]; gap?: number; background?: string; }): UiScreenLayer { return { id: params.id, kind: "stack", bounds: params.bounds, direction: "column", align: "stretch", justify: "start", style: { background: params.background || "rgba(255,255,255,0.82)", borderColor: "rgba(15,23,42,0.08)", borderWidth: 1, borderRadius: 12, padding: 22, gap: params.gap || 14, boxShadow: "0 12px 30px rgba(15,23,42,0.08)", }, children: params.children, }; } export function buildDetailBrandStyleBoard(params: { language: "zh" | "en"; prompt: string; brief?: DetailStyleBoardBrief; globalStyle?: DetailUiGlobalStyle; designSkill?: DetailUiDesignSkillSummary; }): UiScreenDocument { const isZh = params.language === "zh"; const palette = chooseBasePalette({ language: params.language, brief: params.brief, globalStyle: params.globalStyle, }); const primary = palette[0]?.hex || "#0071e3"; const ink = palette[1]?.hex || "#0f172a"; const surface = palette[2]?.hex || "#f5f5f7"; const accent = palette[3]?.hex || "#14b8a6"; const productName = normalizeText(params.brief?.productName, isZh ? "详情页品牌" : "Detail Brand"); const coreNeed = normalizeText(params.brief?.coreNeed, isZh ? "核心购买理由" : "Core buying reason"); const brandTone = normalizeText(params.brief?.brandTone, isZh ? "克制、专业、可上线" : "restrained, professional, production-ready"); const fontFamily = normalizeText(params.globalStyle?.typography?.fontFamily) || "Noto Sans SC, PingFang SC, Microsoft YaHei, Inter, Arial, sans-serif"; const checklist = params.designSkill?.reviewChecklist?.length ? params.designSkill.reviewChecklist.slice(0, 4) : isZh ? ["主体完整不被遮挡", "UI 覆盖小于45%", "三层字体层级统一", "不生成假文字/假按钮"] : ["Subject unobstructed", "UI coverage under 45%", "Three consistent type levels", "No fake image text/UI"]; return { version: "ui-screen/v1", id: `detail-brand-style-board-${Date.now()}`, title: isZh ? "品牌 UI 配色板" : "Brand UI Style Board", width: 790, height: 1080, tokens: { colors: { primary, ink, surface, accent, }, typography: { title: { fontFamily, fontSize: 54, fontWeight: 700, lineHeight: 1.08 }, subtitle: { fontFamily, fontSize: 28, fontWeight: 600, lineHeight: 1.2 }, body: { fontFamily, fontSize: 18, fontWeight: 500, lineHeight: 1.42 }, }, radii: { panel: 12, card: 10, pill: 999, }, shadows: { soft: "0 12px 30px rgba(15,23,42,0.08)", }, }, layers: [ { id: "board-background", kind: "frame", bounds: { x: 0, y: 0, width: 790, height: 1080 }, style: { background: `linear-gradient(180deg, ${surface} 0%, #ffffff 52%, rgba(248,250,252,0.96) 100%)`, }, }, { id: "board-header", kind: "stack", bounds: { x: 48, y: 44, width: 694, height: 154 }, direction: "column", align: "start", justify: "start", style: { gap: 12, fontFamily }, children: [ badgeLayer({ id: "board-badge", label: isZh ? "品牌 UI 基准 / 先锁定再生成" : "Brand UI Baseline / Lock Before Generate", tone: "primary", }), textLayer({ id: "board-title", text: isZh ? `${productName} 视觉系统` : `${productName} Visual System`, fontSize: 50, fontWeight: 700, lineHeight: 1.08, color: ink, }), textLayer({ id: "board-subtitle", text: isZh ? `${brandTone};围绕「${coreNeed}」建立统一详情页语言。` : `${brandTone}; build one detail-page language around "${coreNeed}".`, fontSize: 21, fontWeight: 500, lineHeight: 1.34, color: "#475569", }), ], }, { id: "palette-section", kind: "grid", bounds: { x: 48, y: 234, width: 694, height: 208 }, columns: 5, style: { gap: 12 }, children: palette.map(colorSwatchLayer), }, sectionFrame({ id: "type-section", bounds: { x: 48, y: 480, width: 694, height: 202 }, children: [ textLayer({ id: "type-section-title", text: isZh ? "字体层级" : "Typography", fontSize: 22, fontWeight: 700, color: ink, }), textLayer({ id: "type-hero", text: isZh ? "主标题:一眼说清为什么值得买" : "Hero: Say why it is worth buying", fontSize: 40, fontWeight: 700, lineHeight: 1.08, color: ink, }), textLayer({ id: "type-subtitle", text: isZh ? "副标题:一句结果句,承接核心需求" : "Subtitle: one result sentence tied to the need", fontSize: 25, fontWeight: 600, color: "#334155", }), textLayer({ id: "type-body", text: isZh ? "正文控制在 2-4 行,解释场景、证据和转化理由,不堆满信息。" : "Body stays within 2-4 lines: scene, proof, and conversion reason without clutter.", fontSize: 18, fontWeight: 500, lineHeight: 1.42, color: "#64748b", }), ], }), sectionFrame({ id: "component-section", bounds: { x: 48, y: 714, width: 694, height: 142 }, children: [ textLayer({ id: "component-title", text: isZh ? "组件基调" : "Component Tone", fontSize: 22, fontWeight: 700, color: ink, }), { id: "component-row", kind: "grid", columns: 3, style: { gap: 12 }, children: [ { id: "component-cta", kind: "button", label: isZh ? "主行动按钮" : "Primary CTA", tone: "primary", style: { background: primary, color: contrastText(primary), borderRadius: 999, paddingX: 18, paddingY: 12, fontSize: 15, fontWeight: 700, }, }, { id: "component-proof", kind: "badge", label: isZh ? "证据标签" : "Proof Badge", tone: "success", style: { background: accent, color: contrastText(accent), borderRadius: 999, paddingX: 14, paddingY: 9, fontSize: 14, fontWeight: 700, }, }, { id: "component-panel", kind: "card", style: { background: "#ffffff", borderColor: "rgba(15,23,42,0.10)", borderWidth: 1, borderRadius: 10, padding: 14, boxShadow: "0 10px 24px rgba(15,23,42,0.08)", }, children: [ textLayer({ id: "component-panel-label", text: isZh ? "轻面板 / 不挡主体" : "Light panel / keep subject clear", fontSize: 14, fontWeight: 650, color: ink, }), ], }, ], }, ], }), sectionFrame({ id: "rules-section", bounds: { x: 48, y: 888, width: 694, height: 140 }, background: "rgba(15,23,42,0.92)", children: [ textLayer({ id: "rules-title", text: isZh ? "后续详情页必须遵守" : "Rules For The Following Screens", fontSize: 21, fontWeight: 700, color: "#f8fafc", }), { id: "rules-grid", kind: "grid", columns: 2, style: { gap: 10 }, children: checklist.map((item, index) => textLayer({ id: `rule-${index + 1}`, text: `0${index + 1}. ${item}`, fontSize: 15, fontWeight: 550, lineHeight: 1.34, color: "#e2e8f0", }) ), }, ], }), ], metadata: { source: "detail-brand-style-board", detailBrandStyleBoardVersion: DETAIL_BRAND_STYLE_BOARD_VERSION, prompt: normalizeText(params.prompt).slice(0, 800), globalStyle: params.globalStyle || null, designSkill: params.designSkill || null, palette, }, }; }