import type { DetailGenerationMode, DetailUiGlobalStyle, DetailUiProtocol, DetailUiScreenInput, DetailUiScreenProtocol, DetailUiScreenReviewPatch, } from "@/lib/detail-ui-protocol"; import type { UiSurfaceComponent, UiSurfacePatch, UiSurfaceSnapshot, } from "@/lib/ui-surface-protocol"; function createDetailSurfaceId(params: { generationMode: DetailGenerationMode; screenCount: number; }) { return `detail-surface-${params.generationMode}-${params.screenCount}-790x1300`; } function createRootComponent(): UiSurfaceComponent { return { id: "detail-page-root", type: "detail_page_root", parentId: null, props: { role: "detail_page", }, }; } type DetailSurfaceLayoutVariant = "hero_left" | "hero_right" | "split_bottom"; function splitBodyCopyIntoHighlights(copy?: string) { const segments = String(copy || "") .split(/[。\n!!??;;]+/g) .map((segment) => segment.replace(/\s+/g, " ").trim()) .filter(Boolean); if (segments.length === 0) return []; if (segments.length <= 3) return segments; return [ segments.slice(0, 2).join(" · "), segments.slice(2, 4).join(" · "), segments.slice(4).join(" · "), ].filter(Boolean); } function pickScreenLayoutVariant(params: { screenIndex: number; screenType?: string; }): DetailSurfaceLayoutVariant { if (params.screenIndex === 1 || params.screenType === "B") { return "hero_left"; } if (params.screenType === "A") { return "hero_right"; } return "split_bottom"; } function buildScreenBadgeLabel(params: { screenIndex: number; screenType?: string; objective?: string; }) { const normalizedObjective = String(params.objective || "").replace(/\s+/g, " ").trim(); if (normalizedObjective) return normalizedObjective.slice(0, 18); if (params.screenType === "A") return "技术证明"; if (params.screenType === "B") return "场景共鸣"; if (params.screenType === "C") return "细节证明"; return `第 ${params.screenIndex} 屏`; } function createStyledSurfaceTextComponent(params: { id: string; type: "detail_card" | "detail_badge" | "detail_text_block"; parentId: string; frame: { x: number; y: number; width: number; height: number }; zIndex?: number; content?: string; bindingPath?: string; role: string; style: Record; metadata?: Record; }) { return { id: params.id, type: params.type, parentId: params.parentId, props: { role: params.role, content: params.content || "", }, bindings: params.bindingPath ? { content: { kind: "text" as const, path: params.bindingPath, }, } : undefined, layout: { frame: params.frame, zIndex: params.zIndex || 1, }, style: params.style, metadata: params.metadata, } satisfies UiSurfaceComponent; } function createSectionComponent(params: { id: string; parentId: string; role: string; frame: { x: number; y: number; width: number; height: number }; zIndex?: number; metadata?: Record; }) { return { id: params.id, type: "detail_section" as const, parentId: params.parentId, props: { role: params.role, }, layout: { frame: params.frame, zIndex: params.zIndex || 1, }, metadata: params.metadata, } satisfies UiSurfaceComponent; } function buildSectionedTextComponents(params: { screenId: string; textStackId: string; screenIndex: number; screenSize: { width: number; height: number }; screen: Pick< DetailUiScreenInput, "title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote" >; screenProtocol?: DetailUiScreenProtocol; }) { const safeArea = params.screenProtocol?.safeArea || { top: 64, bottom: 360, left: 56, right: 56, }; const contentWidth = Math.max( 260, Math.round(params.screenSize.width - safeArea.left - safeArea.right), ); const variant = pickScreenLayoutVariant({ screenIndex: params.screenIndex, screenType: params.screen.screenType, }); const badgeLabel = buildScreenBadgeLabel({ screenIndex: params.screenIndex, screenType: params.screen.screenType, objective: params.screen.objective, }); const bodyHighlights = splitBodyCopyIntoHighlights(params.screen.copy); const titleBlock = params.screenProtocol?.blocks.find((block) => block.type === "title") || null; const subtitleBlock = params.screenProtocol?.blocks.find((block) => block.type === "subtitle") || null; const bodyBlock = params.screenProtocol?.blocks.find((block) => block.type === "body") || null; const cardX = variant === "hero_right" ? Math.round(params.screenSize.width - safeArea.right - Math.min(430, contentWidth)) : safeArea.left; const cardWidth = Math.min(430, contentWidth); const titleY = safeArea.top + 42; const subtitleY = titleY + Math.max(124, Math.round(Number(titleBlock?.frame.height || 132))) + 14; const components: UiSurfaceComponent[] = []; components.push( createSectionComponent({ id: `${params.screenId}-section-hero`, parentId: params.textStackId, role: "hero_section", frame: { x: cardX, y: safeArea.top, width: cardWidth, height: Math.min(360, params.screenSize.height - safeArea.top - 120), }, metadata: { screenIndex: params.screenIndex, variant, }, }), ); components.push( createStyledSurfaceTextComponent({ id: `${params.screenId}-badge`, type: "detail_badge", parentId: `${params.screenId}-section-hero`, frame: { x: cardX, y: safeArea.top, width: Math.min(156, Math.max(104, badgeLabel.length * 18)), height: 34, }, zIndex: 3, content: badgeLabel, role: "screen_badge", style: { fontSize: 16, fontWeight: "700", color: "#f8fafc", textAlign: "center", lineHeight: 1.15, letterSpacing: 0.4, backgroundColor: "rgba(15,23,42,0.82)", borderColor: "rgba(255,255,255,0.18)", borderWidth: 1, borderRadius: 999, padding: 8, boxShadow: "0 10px 28px rgba(15,23,42,0.20)", backdropBlur: 8, textTransform: "uppercase", }, metadata: { screenIndex: params.screenIndex, componentRole: "badge", }, }), ); components.push( createStyledSurfaceTextComponent({ id: `${params.screenId}-title-card`, type: "detail_card", parentId: `${params.screenId}-section-hero`, frame: { x: cardX, y: titleY, width: cardWidth, height: Math.max(132, Math.round(Number(titleBlock?.frame.height || 132) + 34)), }, zIndex: 2, bindingPath: `/screens/${params.screenIndex - 1}/title`, role: "title_card", style: { fontFamily: titleBlock?.style.fontFamily || undefined, fontSize: Math.max(34, Number(titleBlock?.style.fontSize || 52)), fontWeight: titleBlock?.style.fontWeight || "800", color: "#0f172a", textAlign: "left", lineHeight: 1.08, letterSpacing: -0.8, backgroundColor: "rgba(255,255,255,0.86)", borderColor: "rgba(255,255,255,0.38)", borderWidth: 1, borderRadius: 28, padding: 24, boxShadow: "0 22px 58px rgba(15,23,42,0.18)", backdropBlur: 14, }, metadata: { screenIndex: params.screenIndex, componentRole: "title", }, }), ); if (params.screen.subtitle) { components.push( createStyledSurfaceTextComponent({ id: `${params.screenId}-subtitle-card`, type: "detail_card", parentId: `${params.screenId}-section-hero`, frame: { x: cardX, y: subtitleY, width: Math.min(cardWidth, 360), height: Math.max(72, Math.round(Number(subtitleBlock?.frame.height || 64) + 26)), }, zIndex: 2, bindingPath: `/screens/${params.screenIndex - 1}/subtitle`, role: "subtitle_card", style: { fontFamily: subtitleBlock?.style.fontFamily || undefined, fontSize: Math.max(22, Number(subtitleBlock?.style.fontSize || 28)), fontWeight: subtitleBlock?.style.fontWeight || "700", color: "#f8fafc", textAlign: "left", lineHeight: 1.18, backgroundColor: variant === "hero_right" ? "rgba(37,99,235,0.82)" : "rgba(30,41,59,0.78)", borderColor: "rgba(255,255,255,0.18)", borderWidth: 1, borderRadius: 22, padding: 18, boxShadow: "0 18px 42px rgba(15,23,42,0.18)", backdropBlur: 12, }, metadata: { screenIndex: params.screenIndex, componentRole: "subtitle", }, }), ); } const supportingSectionY = variant === "split_bottom" ? Math.round(params.screenSize.height * 0.72) : subtitleY + 96; const supportingSectionHeight = variant === "split_bottom" ? Math.min(320, params.screenSize.height - supportingSectionY - 80) : 240; components.push( createSectionComponent({ id: `${params.screenId}-section-supporting`, parentId: params.textStackId, role: variant === "split_bottom" ? "supporting_bottom_section" : "supporting_grid", frame: { x: safeArea.left, y: supportingSectionY, width: contentWidth, height: supportingSectionHeight, }, zIndex: 1, metadata: { screenIndex: params.screenIndex, variant, }, }), ); const supportingCards = bodyHighlights.length > 0 ? bodyHighlights.slice(0, 3) : [String(params.screen.copy || "").trim()].filter(Boolean); const cardGap = 14; const supportColumns = variant === "split_bottom" ? Math.min(3, supportingCards.length) : Math.min(2, Math.max(1, supportingCards.length)); const supportCardWidth = Math.floor( (contentWidth - cardGap * Math.max(0, supportColumns - 1)) / supportColumns, ); supportingCards.forEach((highlight, index) => { const column = supportColumns === 0 ? 0 : index % supportColumns; const row = supportColumns === 0 ? 0 : Math.floor(index / supportColumns); const x = safeArea.left + column * (supportCardWidth + cardGap); const y = supportingSectionY + row * 116; components.push( createStyledSurfaceTextComponent({ id: `${params.screenId}-support-card-${index + 1}`, type: "detail_card", parentId: `${params.screenId}-section-supporting`, frame: { x, y, width: supportCardWidth, height: variant === "split_bottom" ? 102 : 96, }, zIndex: 2, content: highlight, role: "support_card", style: { fontSize: bodyBlock ? Math.max(18, Number(bodyBlock.style.fontSize || 22)) : 20, fontWeight: "600", color: "#111827", textAlign: "left", lineHeight: 1.32, backgroundColor: variant === "split_bottom" ? "rgba(255,255,255,0.92)" : "rgba(248,250,252,0.82)", borderColor: index === 0 ? "rgba(59,130,246,0.42)" : "rgba(148,163,184,0.28)", borderWidth: 1, borderRadius: 24, padding: 18, boxShadow: "0 18px 42px rgba(15,23,42,0.12)", backdropBlur: 12, }, metadata: { screenIndex: params.screenIndex, componentRole: "support_card", }, }), ); }); if (params.screen.layoutNote) { const noteY = Math.min( params.screenSize.height - 116, supportingSectionY + supportingSectionHeight - 74, ); components.push( createStyledSurfaceTextComponent({ id: `${params.screenId}-proof-card`, type: "detail_card", parentId: `${params.screenId}-section-supporting`, frame: { x: variant === "hero_right" ? Math.round(params.screenSize.width - safeArea.right - Math.min(260, contentWidth * 0.46)) : safeArea.left, y: noteY, width: Math.min(260, contentWidth * 0.46), height: 62, }, zIndex: 2, content: String(params.screen.layoutNote || "").replace(/\s+/g, " ").trim(), role: "proof_chip", style: { fontSize: 16, fontWeight: "600", color: "#1e293b", textAlign: "left", lineHeight: 1.22, backgroundColor: "rgba(255,255,255,0.88)", borderColor: "rgba(15,23,42,0.10)", borderWidth: 1, borderRadius: 18, padding: 14, boxShadow: "0 14px 36px rgba(15,23,42,0.12)", backdropBlur: 10, }, metadata: { screenIndex: params.screenIndex, componentRole: "proof_chip", }, }), ); } return components; } function createScreenSubtree(params: { screenIndex: number; screen: Pick< DetailUiScreenInput, "title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote" >; screenSize: { width: number; height: number }; screenProtocol?: DetailUiScreenProtocol; rootId?: string; }) { const screenId = params.rootId || `detail-screen-${params.screenIndex}`; const backgroundId = `${screenId}-background`; const textStackId = `${screenId}-text-stack`; const components: UiSurfaceComponent[] = [ { id: screenId, type: "detail_screen", parentId: "detail-page-root", props: { screenIndex: params.screenIndex, screenType: params.screen.screenType || "", objective: params.screen.objective || "", layoutNote: params.screen.layoutNote || "", }, metadata: { screenIndex: params.screenIndex, }, }, { id: backgroundId, type: "detail_background_image", parentId: screenId, slot: "background", props: { screenIndex: params.screenIndex, }, metadata: { screenIndex: params.screenIndex, }, }, { id: textStackId, type: "detail_text_stack", parentId: screenId, slot: "content", props: { screenIndex: params.screenIndex, safeArea: params.screenProtocol?.safeArea || null, }, metadata: { screenIndex: params.screenIndex, }, }, ]; components.push( ...buildSectionedTextComponents({ screenId, textStackId, screenIndex: params.screenIndex, screenSize: params.screenSize, screen: params.screen, screenProtocol: params.screenProtocol, }), ); return components; } function buildDetailSurfaceDataModel(params: { summary?: string; screens: DetailUiScreenInput[]; globalStyle?: DetailUiGlobalStyle; }) { return { summary: params.summary || "", globalStyle: params.globalStyle || {}, screens: params.screens.map((screen) => ({ index: screen.index, title: String(screen.title || "").trim(), subtitle: String(screen.subtitle || "").trim(), copy: String(screen.copy || "").trim(), screenType: screen.screenType || "", objective: screen.objective || "", layoutNote: screen.layoutNote || "", })), }; } export function buildDetailUiSurfaceSnapshot(params: { surfaceId?: string; generationMode: DetailGenerationMode; screenSize: { width: number; height: number }; summary?: string; globalStyle?: DetailUiGlobalStyle; screens: DetailUiScreenInput[]; uiProtocol?: DetailUiProtocol; uiValidation?: { passed: boolean; issues: string[]; }; }) { const surfaceId = params.surfaceId || createDetailSurfaceId({ generationMode: params.generationMode, screenCount: params.screens.length, }); const components: UiSurfaceComponent[] = [createRootComponent()]; params.screens.forEach((screen) => { const screenProtocol = params.uiProtocol?.screens.find( (item) => item.index === screen.index, ); components.push( ...createScreenSubtree({ screenIndex: screen.index, screen, screenSize: params.screenSize, screenProtocol, }), ); }); return { version: "ui-surface/v1", surfaceId, surfaceType: "detail_page", generationMode: params.generationMode, rootComponentId: "detail-page-root", screenSize: params.screenSize, components, dataModel: buildDetailSurfaceDataModel({ summary: params.summary, screens: params.screens, globalStyle: params.globalStyle, }), validation: params.uiValidation ? { passed: params.uiValidation.passed, issues: params.uiValidation.issues, source: "plan", } : undefined, metadata: { detailScreenCount: params.screens.length, }, } satisfies UiSurfaceSnapshot; } export function buildDetailUiSurfacePatchFromReview(params: { surfaceId: string; screenIndex: number; screen: Pick< DetailUiScreenInput, "index" | "title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote" >; uiPatch?: DetailUiScreenReviewPatch; uiValidation?: { passed: boolean; issues: string[]; }; }) { if (!params.uiPatch) return null; const screenId = `detail-screen-${params.screenIndex}`; const textStackId = `${screenId}-text-stack`; const replacementComponents = createScreenSubtree({ screenIndex: params.screenIndex, screen: params.screen, screenSize: { width: 790, height: 1300, }, screenProtocol: params.uiPatch.recommendedScreen, rootId: screenId, }).filter( (component) => component.id !== screenId && component.id !== `${screenId}-background`, ); return { version: "ui-surface-patch/v1", surfaceId: params.surfaceId, surfaceType: "detail_page", operations: [ { type: "replace_component_subtree", componentId: textStackId, components: replacementComponents, }, { type: "set_data", updates: [ { path: `/screens/${params.screenIndex - 1}/title`, value: String(params.screen.title || "").trim(), }, { path: `/screens/${params.screenIndex - 1}/subtitle`, value: String(params.screen.subtitle || "").trim(), }, { path: `/screens/${params.screenIndex - 1}/copy`, value: String(params.screen.copy || "").trim(), }, { path: `/screens/${params.screenIndex - 1}/layoutNote`, value: String(params.screen.layoutNote || "").trim(), }, ], }, ...(params.uiValidation ? [ { type: "set_validation" as const, validation: { passed: params.uiValidation.passed, issues: params.uiValidation.issues, source: "review" as const, }, }, ] : []), ], } satisfies UiSurfacePatch; } export function findDetailSurfaceScreenComponent( snapshot: UiSurfaceSnapshot | null | undefined, screenIndex: number, ) { if (!snapshot) return null; return ( snapshot.components.find( (component) => component.type === "detail_screen" && Number(component.props?.screenIndex || 0) === screenIndex, ) || null ); }