feat: accept kimi direct detail dsl output
This commit is contained in:
parent
05cf3ae54e
commit
9276e6c35c
@ -19,6 +19,10 @@ import {
|
|||||||
type DetailUiProtocol,
|
type DetailUiProtocol,
|
||||||
} from "@/lib/detail-ui-protocol";
|
} from "@/lib/detail-ui-protocol";
|
||||||
import { buildDetailUiSurfaceSnapshot } from "@/lib/detail-ui-surface-adapter";
|
import { buildDetailUiSurfaceSnapshot } from "@/lib/detail-ui-surface-adapter";
|
||||||
|
import {
|
||||||
|
normalizeDetailUiDslV2Document,
|
||||||
|
type DetailUiDslV2Document,
|
||||||
|
} from "@/lib/detail-ui-dsl-v2";
|
||||||
import { validateDetailUiProtocol } from "@/lib/detail-ui-validator";
|
import { validateDetailUiProtocol } from "@/lib/detail-ui-validator";
|
||||||
import type { UiSurfaceSnapshot } from "@/lib/ui-surface-protocol";
|
import type { UiSurfaceSnapshot } from "@/lib/ui-surface-protocol";
|
||||||
|
|
||||||
@ -241,6 +245,7 @@ type DetailPlanResponse = {
|
|||||||
screens: DetailPlanScreen[];
|
screens: DetailPlanScreen[];
|
||||||
uiProtocol?: DetailUiProtocol;
|
uiProtocol?: DetailUiProtocol;
|
||||||
uiSurface?: UiSurfaceSnapshot;
|
uiSurface?: UiSurfaceSnapshot;
|
||||||
|
uiDslV2Screens?: DetailUiDslV2Document[];
|
||||||
model?: {
|
model?: {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -2008,8 +2013,11 @@ export async function POST(request: Request) {
|
|||||||
(hasBundleImages
|
(hasBundleImages
|
||||||
? `14) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n`
|
? `14) 已提供搭配产品图:必须新增且仅新增1屏“场景化产品搭配展示”,该屏设定 bundleShowcase=true,并在 bundleReason 说明搭配策略;其余屏设为 bundleShowcase=false。\n`
|
||||||
: `14) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) +
|
: `14) 未提供搭配产品图:所有分镜均设置 bundleShowcase=false。\n`) +
|
||||||
|
(generationMode === "agent_layout"
|
||||||
|
? `15) 当前为 AGENT 详情模式:除 screens 外,还必须返回 dslV2Screens。每个 dslV2Screens[i] 都是可直接渲染的 detail-ui-dsl/v2 文档,必须包含 version、screenIndex、layoutVariant、region、nodes。nodes 仅允许这些 kind:panel、stack、row、badge、heading、body、feature-card、spec-table、compare、proof、cta。\n`
|
||||||
|
: "") +
|
||||||
`输出格式:\n` +
|
`输出格式:\n` +
|
||||||
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"首屏示例:甩甩杆,自拍不手抖(6-20字)","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}]}`,
|
`{"summary":"简述","globalStyle":{"designLanguage":"一句话风格方向","typography":{"fontFamily":"字体家族","title":"主标题规范","subtitle":"副标题规范","body":"正文规范"},"layout":{"grid":"栅格","safeArea":"安全区","rhythm":"留白节奏"},"palette":"配色规范"},"screens":[{"index":1,"screenType":"B","objective":"本屏目标","title":"首屏示例:甩甩杆,自拍不手抖(6-20字)","subtitle":"一句副标题","copy":"正文文案","layoutNote":"排版说明","useModel":false,"modelReason":"是否需要人物及原因","bundleShowcase":false,"bundleReason":"是否为搭配展示页及原因","fabe":{"b":"利益","f":"特征","a":"优势","e":"证据"},"visualPrompt":"用于生图的完整提示词,必须包含790x1300与统一字体排版"}],"dslV2Screens":[{"version":"detail-ui-dsl/v2","screenIndex":1,"layoutVariant":"hero-left-panel","region":{"x":0.06,"y":0.05,"width":0.88,"height":0.46},"nodes":[{"kind":"panel","id":"hero-panel","variant":"glass","tone":"light","direction":"column","gap":18,"children":[{"kind":"badge","id":"screen-badge","icon":"sparkle","text":"核心升级","tone":"primary"},{"kind":"heading","id":"hero-title","text":"护腰椅,久坐不腰酸","level":1,"emphasis":"hero"},{"kind":"body","id":"hero-subtitle","text":"在家里加班,也能护腰/舒适","emphasis":"strong"}]}]}]}`,
|
||||||
temperature: 0.25,
|
temperature: 0.25,
|
||||||
...(localDetailAgentResolved ? { temperature: 1 } : {}),
|
...(localDetailAgentResolved ? { temperature: 1 } : {}),
|
||||||
maxTokens: 1800,
|
maxTokens: 1800,
|
||||||
@ -2088,7 +2096,12 @@ export async function POST(request: Request) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = extractJsonBlock<{ summary?: unknown; globalStyle?: unknown; screens?: unknown }>(text || "");
|
const parsed = extractJsonBlock<{
|
||||||
|
summary?: unknown;
|
||||||
|
globalStyle?: unknown;
|
||||||
|
screens?: unknown;
|
||||||
|
dslV2Screens?: unknown;
|
||||||
|
}>(text || "");
|
||||||
const globalStyleInitial = sanitizeGlobalStyle(parsed?.globalStyle, fallback.globalStyle);
|
const globalStyleInitial = sanitizeGlobalStyle(parsed?.globalStyle, fallback.globalStyle);
|
||||||
const rawScreens = sanitizePlannedScreens(parsed?.screens, fallback.screens, language).map((item) =>
|
const rawScreens = sanitizePlannedScreens(parsed?.screens, fallback.screens, language).map((item) =>
|
||||||
hasModelImage
|
hasModelImage
|
||||||
@ -2202,6 +2215,21 @@ export async function POST(request: Request) {
|
|||||||
: `${summaryRaw || fallback.summary}\n双总监预审未通过,已自动回退到稳定分镜模板。`,
|
: `${summaryRaw || fallback.summary}\n双总监预审未通过,已自动回退到稳定分镜模板。`,
|
||||||
language
|
language
|
||||||
);
|
);
|
||||||
|
const uiDslV2Screens =
|
||||||
|
!shouldFallback && !preflight.rewriteApplied && Array.isArray(parsed?.dslV2Screens)
|
||||||
|
? parsed.dslV2Screens
|
||||||
|
.map((item) => normalizeDetailUiDslV2Document(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((item) => ({
|
||||||
|
...item!,
|
||||||
|
surfaceId: `detail-dsl-${generationMode}-${item!.screenIndex}`,
|
||||||
|
generationMode,
|
||||||
|
screenSize: {
|
||||||
|
width: 790,
|
||||||
|
height: 1300,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const response: DetailPlanResponse = {
|
const response: DetailPlanResponse = {
|
||||||
summary,
|
summary,
|
||||||
@ -2253,6 +2281,7 @@ export async function POST(request: Request) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
|
uiDslV2Screens,
|
||||||
model: {
|
model: {
|
||||||
id: selectedModelRef.id,
|
id: selectedModelRef.id,
|
||||||
displayName: selectedModelRef.displayName,
|
displayName: selectedModelRef.displayName,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { createPortal } from "react-dom";
|
|||||||
import type { DetailUiProtocol } from "@/lib/detail-ui-protocol";
|
import type { DetailUiProtocol } from "@/lib/detail-ui-protocol";
|
||||||
import type { DetailUiScreenReviewPatch } from "@/lib/detail-ui-protocol";
|
import type { DetailUiScreenReviewPatch } from "@/lib/detail-ui-protocol";
|
||||||
import { buildDetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
|
import { buildDetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
|
||||||
|
import type { DetailUiDslV2Document } from "@/lib/detail-ui-dsl-v2";
|
||||||
import {
|
import {
|
||||||
buildDetailTextLayerNodes,
|
buildDetailTextLayerNodes,
|
||||||
buildDetailTextLayerNodesFromReviewPatch,
|
buildDetailTextLayerNodesFromReviewPatch,
|
||||||
@ -1265,6 +1266,7 @@ type DetailPlanResponse = {
|
|||||||
};
|
};
|
||||||
uiProtocol?: DetailUiProtocol;
|
uiProtocol?: DetailUiProtocol;
|
||||||
uiSurface?: UiSurfaceSnapshot;
|
uiSurface?: UiSurfaceSnapshot;
|
||||||
|
uiDslV2Screens?: DetailUiDslV2Document[];
|
||||||
};
|
};
|
||||||
type DetailPlanScreenSpec = NonNullable<DetailPlanResponse["screens"]>[number];
|
type DetailPlanScreenSpec = NonNullable<DetailPlanResponse["screens"]>[number];
|
||||||
|
|
||||||
@ -6777,6 +6779,10 @@ export function CanvasChatPanel({
|
|||||||
surface: currentDetailSurfaceSnapshot,
|
surface: currentDetailSurfaceSnapshot,
|
||||||
screenIndex: index + 1,
|
screenIndex: index + 1,
|
||||||
}),
|
}),
|
||||||
|
detailUiDslDocument:
|
||||||
|
Array.isArray(planData.uiDslV2Screens) && planData.uiDslV2Screens[index]
|
||||||
|
? planData.uiDslV2Screens[index]
|
||||||
|
: null,
|
||||||
detailSubjectAnalysis,
|
detailSubjectAnalysis,
|
||||||
detailGeneratingPlaceholder: false,
|
detailGeneratingPlaceholder: false,
|
||||||
detailProgressLabel: `该屏完成 ${index + 1}/${prompts.length}`,
|
detailProgressLabel: `该屏完成 ${index + 1}/${prompts.length}`,
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { DetailUiDomRenderer } from "@/components/canvas/DetailUiDomRenderer";
|
|||||||
import type { PickedObject } from "@/store/canvas";
|
import type { PickedObject } from "@/store/canvas";
|
||||||
import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
|
import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
|
||||||
import { buildDetailUiDslV2 } from "@/lib/detail-ui-dsl-v2-builder";
|
import { buildDetailUiDslV2 } from "@/lib/detail-ui-dsl-v2-builder";
|
||||||
|
import type { DetailUiDslV2Document } from "@/lib/detail-ui-dsl-v2";
|
||||||
import {
|
import {
|
||||||
normalizeDetailUiSubjectAnalysis,
|
normalizeDetailUiSubjectAnalysis,
|
||||||
type DetailUiSubjectAnalysis,
|
type DetailUiSubjectAnalysis,
|
||||||
@ -383,13 +384,19 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
const detailSubjectAnalysis = useMemo(() => {
|
const detailSubjectAnalysis = useMemo(() => {
|
||||||
return normalizeDetailUiSubjectAnalysis(data.metadata?.detailSubjectAnalysis);
|
return normalizeDetailUiSubjectAnalysis(data.metadata?.detailSubjectAnalysis);
|
||||||
}, [data.metadata]);
|
}, [data.metadata]);
|
||||||
|
const detailUiDslDocumentFromMetadata = useMemo(() => {
|
||||||
|
const raw = data.metadata?.detailUiDslDocument;
|
||||||
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||||
|
return raw as DetailUiDslV2Document;
|
||||||
|
}, [data.metadata]);
|
||||||
const detailUiDslDocument = useMemo(() => {
|
const detailUiDslDocument = useMemo(() => {
|
||||||
|
if (detailUiDslDocumentFromMetadata) return detailUiDslDocumentFromMetadata;
|
||||||
if (!detailUiScreenOverlay) return null;
|
if (!detailUiScreenOverlay) return null;
|
||||||
return buildDetailUiDslV2({
|
return buildDetailUiDslV2({
|
||||||
overlay: detailUiScreenOverlay,
|
overlay: detailUiScreenOverlay,
|
||||||
subjectAnalysis: detailSubjectAnalysis,
|
subjectAnalysis: detailSubjectAnalysis,
|
||||||
});
|
});
|
||||||
}, [detailSubjectAnalysis, detailUiScreenOverlay]);
|
}, [detailSubjectAnalysis, detailUiDslDocumentFromMetadata, detailUiScreenOverlay]);
|
||||||
const shouldRenderDetailOverlay =
|
const shouldRenderDetailOverlay =
|
||||||
data.metadata?.detailGenerationMode === "agent_layout" && Boolean(detailUiScreenOverlay);
|
data.metadata?.detailGenerationMode === "agent_layout" && Boolean(detailUiScreenOverlay);
|
||||||
const shortAgentExecutionSnapshotId = agentExecutionSnapshotId
|
const shortAgentExecutionSnapshotId = agentExecutionSnapshotId
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export type DetailUiDslNode =
|
|||||||
| {
|
| {
|
||||||
kind: "proof";
|
kind: "proof";
|
||||||
id: string;
|
id: string;
|
||||||
icon?: "shield" | "check" | "sparkle";
|
icon?: "shield" | "check" | "sparkle" | "arrow";
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@ -104,3 +104,261 @@ export interface DetailUiDslV2Document {
|
|||||||
| "cta-focus";
|
| "cta-focus";
|
||||||
nodes: DetailUiDslNode[];
|
nodes: DetailUiDslNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clamp01(value: unknown, fallback: number) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed)) return fallback;
|
||||||
|
return Math.max(0, Math.min(1, parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeString(value: unknown, fallback = "") {
|
||||||
|
return String(value || "").replace(/\s+/g, " ").trim() || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRegion(value: unknown): DetailUiDslRegion | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||||
|
const raw = value as Record<string, unknown>;
|
||||||
|
const region = {
|
||||||
|
x: clamp01(raw.x, 0),
|
||||||
|
y: clamp01(raw.y, 0),
|
||||||
|
width: clamp01(raw.width, 0),
|
||||||
|
height: clamp01(raw.height, 0),
|
||||||
|
} satisfies DetailUiDslRegion;
|
||||||
|
if (region.width <= 0 || region.height <= 0) return null;
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNode(value: unknown): DetailUiDslNode | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||||
|
const raw = value as Record<string, unknown>;
|
||||||
|
const kind = normalizeString(raw.kind);
|
||||||
|
const id = normalizeString(raw.id);
|
||||||
|
if (!kind || !id) return null;
|
||||||
|
|
||||||
|
if (kind === "panel") {
|
||||||
|
const children = Array.isArray(raw.children)
|
||||||
|
? raw.children.map(normalizeNode).filter(Boolean) as DetailUiDslNode[]
|
||||||
|
: [];
|
||||||
|
if (children.length === 0) return null;
|
||||||
|
return {
|
||||||
|
kind: "panel",
|
||||||
|
id,
|
||||||
|
variant:
|
||||||
|
raw.variant === "glass" || raw.variant === "solid" || raw.variant === "ghost"
|
||||||
|
? raw.variant
|
||||||
|
: undefined,
|
||||||
|
tone:
|
||||||
|
raw.tone === "light" || raw.tone === "dark" || raw.tone === "accent"
|
||||||
|
? raw.tone
|
||||||
|
: undefined,
|
||||||
|
region: normalizeRegion(raw.region) || undefined,
|
||||||
|
direction: raw.direction === "row" ? "row" : "column",
|
||||||
|
gap: Number.isFinite(Number(raw.gap)) ? Math.max(0, Number(raw.gap)) : undefined,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "stack") {
|
||||||
|
const children = Array.isArray(raw.children)
|
||||||
|
? raw.children.map(normalizeNode).filter(Boolean) as DetailUiDslNode[]
|
||||||
|
: [];
|
||||||
|
if (children.length === 0) return null;
|
||||||
|
return {
|
||||||
|
kind: "stack",
|
||||||
|
id,
|
||||||
|
gap: Number.isFinite(Number(raw.gap)) ? Math.max(0, Number(raw.gap)) : undefined,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "row") {
|
||||||
|
const children = Array.isArray(raw.children)
|
||||||
|
? raw.children.map(normalizeNode).filter(Boolean) as DetailUiDslNode[]
|
||||||
|
: [];
|
||||||
|
if (children.length === 0) return null;
|
||||||
|
return {
|
||||||
|
kind: "row",
|
||||||
|
id,
|
||||||
|
gap: Number.isFinite(Number(raw.gap)) ? Math.max(0, Number(raw.gap)) : undefined,
|
||||||
|
columns: Number.isFinite(Number(raw.columns)) ? Math.max(1, Number(raw.columns)) : undefined,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "badge") {
|
||||||
|
const text = normalizeString(raw.text);
|
||||||
|
if (!text) return null;
|
||||||
|
return {
|
||||||
|
kind: "badge",
|
||||||
|
id,
|
||||||
|
icon:
|
||||||
|
raw.icon === "sparkle" || raw.icon === "shield" || raw.icon === "arrow" || raw.icon === "check"
|
||||||
|
? raw.icon
|
||||||
|
: undefined,
|
||||||
|
text,
|
||||||
|
tone:
|
||||||
|
raw.tone === "primary" || raw.tone === "secondary" || raw.tone === "neutral"
|
||||||
|
? raw.tone
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "heading") {
|
||||||
|
const text = normalizeString(raw.text);
|
||||||
|
if (!text) return null;
|
||||||
|
return {
|
||||||
|
kind: "heading",
|
||||||
|
id,
|
||||||
|
text,
|
||||||
|
level: raw.level === 2 || raw.level === 3 ? raw.level : 1,
|
||||||
|
emphasis: raw.emphasis === "section" ? "section" : "hero",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "body") {
|
||||||
|
const text = normalizeString(raw.text);
|
||||||
|
if (!text) return null;
|
||||||
|
return {
|
||||||
|
kind: "body",
|
||||||
|
id,
|
||||||
|
text,
|
||||||
|
emphasis:
|
||||||
|
raw.emphasis === "muted" || raw.emphasis === "strong"
|
||||||
|
? raw.emphasis
|
||||||
|
: "normal",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "feature-card") {
|
||||||
|
const title = normalizeString(raw.title);
|
||||||
|
if (!title) return null;
|
||||||
|
return {
|
||||||
|
kind: "feature-card",
|
||||||
|
id,
|
||||||
|
icon:
|
||||||
|
raw.icon === "sparkle" || raw.icon === "shield" || raw.icon === "arrow" || raw.icon === "check"
|
||||||
|
? raw.icon
|
||||||
|
: undefined,
|
||||||
|
tone:
|
||||||
|
raw.tone === "primary" || raw.tone === "secondary" || raw.tone === "neutral"
|
||||||
|
? raw.tone
|
||||||
|
: undefined,
|
||||||
|
tag: normalizeString(raw.tag) || undefined,
|
||||||
|
title,
|
||||||
|
body: normalizeString(raw.body) || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "spec-table") {
|
||||||
|
const rows = Array.isArray(raw.rows)
|
||||||
|
? raw.rows
|
||||||
|
.map((row) => {
|
||||||
|
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
||||||
|
const rowRaw = row as Record<string, unknown>;
|
||||||
|
const label = normalizeString(rowRaw.label);
|
||||||
|
const valueText = normalizeString(rowRaw.value);
|
||||||
|
if (!label || !valueText) return null;
|
||||||
|
return { label, value: valueText };
|
||||||
|
})
|
||||||
|
.filter(Boolean) as Array<{ label: string; value: string }>
|
||||||
|
: [];
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
return { kind: "spec-table", id, rows };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "compare") {
|
||||||
|
const beforeRaw =
|
||||||
|
raw.before && typeof raw.before === "object" && !Array.isArray(raw.before)
|
||||||
|
? (raw.before as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const afterRaw =
|
||||||
|
raw.after && typeof raw.after === "object" && !Array.isArray(raw.after)
|
||||||
|
? (raw.after as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
if (!beforeRaw || !afterRaw) return null;
|
||||||
|
const beforeTitle = normalizeString(beforeRaw.title);
|
||||||
|
const beforeBody = normalizeString(beforeRaw.body);
|
||||||
|
const afterTitle = normalizeString(afterRaw.title);
|
||||||
|
const afterBody = normalizeString(afterRaw.body);
|
||||||
|
if (!beforeTitle || !beforeBody || !afterTitle || !afterBody) return null;
|
||||||
|
return {
|
||||||
|
kind: "compare",
|
||||||
|
id,
|
||||||
|
before: { title: beforeTitle, body: beforeBody },
|
||||||
|
after: { title: afterTitle, body: afterBody },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "proof") {
|
||||||
|
const text = normalizeString(raw.text);
|
||||||
|
if (!text) return null;
|
||||||
|
return {
|
||||||
|
kind: "proof",
|
||||||
|
id,
|
||||||
|
icon:
|
||||||
|
raw.icon === "sparkle" || raw.icon === "shield" || raw.icon === "arrow" || raw.icon === "check"
|
||||||
|
? raw.icon
|
||||||
|
: undefined,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "cta") {
|
||||||
|
const buttonText = normalizeString(raw.buttonText);
|
||||||
|
if (!buttonText) return null;
|
||||||
|
return {
|
||||||
|
kind: "cta",
|
||||||
|
id,
|
||||||
|
headline: normalizeString(raw.headline) || undefined,
|
||||||
|
buttonText,
|
||||||
|
supportText: normalizeString(raw.supportText) || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDetailUiDslV2Document(
|
||||||
|
value: unknown,
|
||||||
|
): DetailUiDslV2Document | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||||
|
const raw = value as Record<string, unknown>;
|
||||||
|
const region = normalizeRegion(raw.region);
|
||||||
|
if (!region) return null;
|
||||||
|
|
||||||
|
const nodes = Array.isArray(raw.nodes)
|
||||||
|
? raw.nodes.map(normalizeNode).filter(Boolean) as DetailUiDslNode[]
|
||||||
|
: [];
|
||||||
|
if (nodes.length === 0) return null;
|
||||||
|
|
||||||
|
const layoutVariant =
|
||||||
|
raw.layoutVariant === "hero-right-panel" ||
|
||||||
|
raw.layoutVariant === "top-banner-bottom-grid" ||
|
||||||
|
raw.layoutVariant === "floating-split" ||
|
||||||
|
raw.layoutVariant === "cta-focus"
|
||||||
|
? raw.layoutVariant
|
||||||
|
: "hero-left-panel";
|
||||||
|
|
||||||
|
const screenSizeRaw =
|
||||||
|
raw.screenSize && typeof raw.screenSize === "object" && !Array.isArray(raw.screenSize)
|
||||||
|
? (raw.screenSize as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: "detail-ui-dsl/v2",
|
||||||
|
surfaceId: normalizeString(raw.surfaceId, "detail-ui-dsl-v2"),
|
||||||
|
screenIndex: Math.max(1, Number(raw.screenIndex || 1)),
|
||||||
|
generationMode: raw.generationMode === "classic" ? "classic" : "agent_layout",
|
||||||
|
screenSize: {
|
||||||
|
width: Math.max(1, Number(screenSizeRaw.width || 790)),
|
||||||
|
height: Math.max(1, Number(screenSizeRaw.height || 1300)),
|
||||||
|
},
|
||||||
|
designStyle:
|
||||||
|
raw.designStyle && typeof raw.designStyle === "object" && !Array.isArray(raw.designStyle)
|
||||||
|
? (raw.designStyle as DetailUiGlobalStyle)
|
||||||
|
: undefined,
|
||||||
|
region,
|
||||||
|
layoutVariant,
|
||||||
|
nodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user