import type { DetailGenerationMode, DetailUiGlobalStyle, DetailUiProtocol, DetailUiScreenInput, DetailUiScreenProtocol, DetailUiScreenReviewPatch, } from "@/lib/detail-ui-protocol"; import type { UiSurfaceComponent, UiSurfacePatch, UiSurfaceSnapshot, } from "@/lib/ui-surface-protocol"; function mapBlockTypeToBindingKey(blockType: "title" | "subtitle" | "body") { if (blockType === "title") return "title"; if (blockType === "subtitle") return "subtitle"; return "copy"; } 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", }, }; } function createScreenSubtree(params: { screenIndex: number; screen: Pick< DetailUiScreenInput, "title" | "subtitle" | "copy" | "screenType" | "objective" | "layoutNote" >; 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, }, }, ]; const blocks = params.screenProtocol?.blocks || []; blocks.forEach((block, blockIndex) => { const bindingKey = mapBlockTypeToBindingKey(block.type); components.push({ id: `${screenId}-text-${blockIndex + 1}`, type: "detail_text_block", parentId: textStackId, props: { blockType: block.type, frame: block.frame, textKey: bindingKey, }, bindings: { content: { kind: "text", path: `/screens/${params.screenIndex - 1}/${bindingKey}`, }, }, style: { fontFamily: block.style.fontFamily || "", fontSize: block.style.fontSize, fontWeight: block.style.fontWeight, lineHeight: block.style.lineHeight, color: block.style.color, textAlign: block.style.textAlign, }, layout: { frame: block.frame, }, metadata: { screenIndex: params.screenIndex, blockType: block.type, blockId: block.id, }, }); }); 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, 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, screenProtocol: params.uiPatch.recommendedScreen, rootId: screenId, }).filter((component) => component.id === textStackId || component.parentId === textStackId); 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 ); }