diff --git a/components/canvas/DetailUiDomRenderer.tsx b/components/canvas/DetailUiDomRenderer.tsx
new file mode 100644
index 000000000..9e3ec5c1c
--- /dev/null
+++ b/components/canvas/DetailUiDomRenderer.tsx
@@ -0,0 +1,360 @@
+"use client";
+
+import type { CSSProperties } from "react";
+import { ArrowRight, CheckCircle2, ShieldCheck, Sparkles } from "lucide-react";
+import { cn } from "@/lib/utils";
+import type {
+ DetailUiDslNode,
+ DetailUiDslRegion,
+ DetailUiDslV2Document,
+} from "@/lib/detail-ui-dsl-v2";
+
+function normalizedRegionToPixels(params: {
+ region: DetailUiDslRegion;
+ width: number;
+ height: number;
+}) {
+ return {
+ x: params.region.x * params.width,
+ y: params.region.y * params.height,
+ width: params.region.width * params.width,
+ height: params.region.height * params.height,
+ };
+}
+
+function iconFor(name?: string, size = 16) {
+ const style = { width: size, height: size };
+ if (name === "shield") return ;
+ if (name === "arrow") return ;
+ if (name === "check") return ;
+ return ;
+}
+
+function panelClass(variant?: string, tone?: string) {
+ if (variant === "glass") {
+ if (tone === "dark") {
+ return "bg-slate-900/72 border border-white/10 text-white shadow-[0_18px_50px_rgba(15,23,42,0.26)] backdrop-blur-xl";
+ }
+ if (tone === "accent") {
+ return "bg-blue-600/88 border border-blue-300/30 text-white shadow-[0_20px_56px_rgba(37,99,235,0.28)] backdrop-blur-xl";
+ }
+ return "bg-white/84 border border-white/50 text-slate-950 shadow-[0_20px_56px_rgba(15,23,42,0.18)] backdrop-blur-xl";
+ }
+
+ if (variant === "ghost") {
+ return "bg-transparent text-slate-900";
+ }
+
+ return tone === "dark"
+ ? "bg-slate-900 text-white"
+ : tone === "accent"
+ ? "bg-blue-600 text-white"
+ : "bg-white text-slate-950";
+}
+
+function renderNode(node: DetailUiDslNode): JSX.Element {
+ if (node.kind === "panel") {
+ return (
+
+ {node.children.map(renderNode)}
+
+ );
+ }
+
+ if (node.kind === "stack") {
+ return (
+
+ {node.children.map(renderNode)}
+
+ );
+ }
+
+ if (node.kind === "row") {
+ const columns = Math.max(1, node.columns || node.children.length || 1);
+ return (
+
+ {node.children.map(renderNode)}
+
+ );
+ }
+
+ if (node.kind === "badge") {
+ return (
+
+ {iconFor(node.icon, 14)}
+ {node.text}
+
+ );
+ }
+
+ if (node.kind === "heading") {
+ return (
+
+ {node.text}
+
+ );
+ }
+
+ if (node.kind === "body") {
+ return (
+
+ {node.text}
+
+ );
+ }
+
+ if (node.kind === "feature-card") {
+ return (
+
+ {node.tag ? (
+
+ {iconFor(node.icon, 12)}
+ {node.tag}
+
+ ) : null}
+
{node.title}
+ {node.body ? (
+
+ {node.body}
+
+ ) : null}
+
+ );
+ }
+
+ if (node.kind === "spec-table") {
+ return (
+
+ {node.rows.map((row, index) => (
+
0 && "border-t border-slate-100/90",
+ )}
+ >
+
+ {row.label}
+
+
+ {row.value}
+
+
+ ))}
+
+ );
+ }
+
+ if (node.kind === "compare") {
+ return (
+
+
+
+ {node.before.title}
+
+
+ {node.before.body}
+
+
+
+
+ {node.after.title}
+
+
+ {node.after.body}
+
+
+
+ );
+ }
+
+ if (node.kind === "proof") {
+ return (
+
+ {iconFor(node.icon, 15)}
+ {node.text}
+
+ );
+ }
+
+ if (node.kind === "cta") {
+ return (
+
+ {node.headline ? (
+
{node.headline}
+ ) : null}
+
+
+ {node.supportText ? (
+
+ {node.supportText}
+
+ ) : null}
+
+
+ );
+ }
+
+ return ;
+}
+
+export function DetailUiDomRenderer(params: {
+ document: DetailUiDslV2Document | null;
+ imageMetrics?: {
+ offsetX: number;
+ offsetY: number;
+ renderedWidth: number;
+ renderedHeight: number;
+ } | null;
+ className?: string;
+}) {
+ if (!params.document) return null;
+
+ const renderWidth =
+ params.imageMetrics?.renderedWidth && params.imageMetrics.renderedWidth > 0
+ ? params.imageMetrics.renderedWidth
+ : params.document.screenSize.width;
+ const renderHeight =
+ params.imageMetrics?.renderedHeight && params.imageMetrics.renderedHeight > 0
+ ? params.imageMetrics.renderedHeight
+ : params.document.screenSize.height;
+ const scale = Math.max(
+ 0.55,
+ Math.min(
+ 2.4,
+ Math.min(
+ renderWidth / Math.max(1, params.document.screenSize.width),
+ renderHeight / Math.max(1, params.document.screenSize.height),
+ ),
+ ),
+ );
+
+ const containerStyle: CSSProperties =
+ params.imageMetrics && params.imageMetrics.renderedWidth > 0 && params.imageMetrics.renderedHeight > 0
+ ? {
+ position: "absolute",
+ left: `${params.imageMetrics.offsetX}px`,
+ top: `${params.imageMetrics.offsetY}px`,
+ width: `${params.imageMetrics.renderedWidth}px`,
+ height: `${params.imageMetrics.renderedHeight}px`,
+ }
+ : {
+ position: "absolute",
+ inset: "0px",
+ };
+
+ const region = normalizedRegionToPixels({
+ region: params.document.region,
+ width: renderWidth,
+ height: renderHeight,
+ });
+
+ return (
+
+
+
+ {params.document.nodes.map((node) => renderNode(node))}
+
+
+
+ );
+}
diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx
index 5a94c3fd3..b9f7ec11d 100644
--- a/components/canvas/nodes/ImageNode.tsx
+++ b/components/canvas/nodes/ImageNode.tsx
@@ -36,8 +36,10 @@ import { SceneFusionDialog } from "@/components/scene-fusion-dialog";
import { ImageAnnotator } from "@/components/image-annotator";
import { AgentExecutionSnapshotDialog } from "@/components/agents/AgentExecutionSnapshotDialog";
import { DetailSurfaceOverlayRenderer } from "@/components/canvas/DetailSurfaceOverlayRenderer";
+import { DetailUiDomRenderer } from "@/components/canvas/DetailUiDomRenderer";
import type { PickedObject } from "@/store/canvas";
import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
+import { buildDetailUiDslV2 } from "@/lib/detail-ui-dsl-v2-builder";
import {
normalizeDetailUiSubjectAnalysis,
type DetailUiSubjectAnalysis,
@@ -381,6 +383,13 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
const detailSubjectAnalysis = useMemo(() => {
return normalizeDetailUiSubjectAnalysis(data.metadata?.detailSubjectAnalysis);
}, [data.metadata]);
+ const detailUiDslDocument = useMemo(() => {
+ if (!detailUiScreenOverlay) return null;
+ return buildDetailUiDslV2({
+ overlay: detailUiScreenOverlay,
+ subjectAnalysis: detailSubjectAnalysis,
+ });
+ }, [detailSubjectAnalysis, detailUiScreenOverlay]);
const shouldRenderDetailOverlay =
data.metadata?.detailGenerationMode === "agent_layout" && Boolean(detailUiScreenOverlay);
const shortAgentExecutionSnapshotId = agentExecutionSnapshotId
@@ -2545,11 +2554,18 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
}}
/>
{shouldRenderDetailOverlay ? (
-
+ detailUiDslDocument ? (
+
+ ) : (
+
+ )
) : null}
>
)}
diff --git a/lib/detail-ui-dsl-v2-builder.ts b/lib/detail-ui-dsl-v2-builder.ts
new file mode 100644
index 000000000..0a2e8d45c
--- /dev/null
+++ b/lib/detail-ui-dsl-v2-builder.ts
@@ -0,0 +1,302 @@
+import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
+import type { DetailUiSubjectAnalysis } from "@/lib/detail-ui-subject-analysis";
+import type { DetailUiDslRegion, DetailUiDslV2Document, DetailUiDslNode } from "@/lib/detail-ui-dsl-v2";
+
+type ScreenData = NonNullable;
+
+function splitCopy(text?: string) {
+ const parts = String(text || "")
+ .split(/[。\n!!??;;]+/g)
+ .map((item) => item.replace(/\s+/g, " ").trim())
+ .filter(Boolean);
+
+ if (parts.length === 0) return [];
+ if (parts.length <= 4) return parts;
+ return [
+ parts.slice(0, 2).join(" · "),
+ parts.slice(2, 4).join(" · "),
+ parts.slice(4).join(" · "),
+ ].filter(Boolean);
+}
+
+function pickLayoutVariant(params: {
+ screenIndex: number;
+ totalScreens?: number;
+ screenType?: string;
+ subject?: DetailUiSubjectAnalysis | null;
+}) {
+ if ((params.totalScreens || 0) > 0 && params.screenIndex === params.totalScreens) {
+ return "cta-focus" as const;
+ }
+ if (params.subject?.dominantHorizontal === "left") {
+ return "hero-right-panel" as const;
+ }
+ if (params.subject?.dominantHorizontal === "right") {
+ return "hero-left-panel" as const;
+ }
+ if (params.screenType === "C") {
+ return "floating-split" as const;
+ }
+ return "top-banner-bottom-grid" as const;
+}
+
+function normalizedRegionToPixels(region: DetailUiDslRegion, width: number, height: number) {
+ return {
+ x: Math.round(region.x * width),
+ y: Math.round(region.y * height),
+ width: Math.round(region.width * width),
+ height: Math.round(region.height * height),
+ };
+}
+
+function pickRegion(params: {
+ variant: DetailUiDslV2Document["layoutVariant"];
+ subject?: DetailUiSubjectAnalysis | null;
+}) {
+ if (params.variant === "hero-right-panel") {
+ return params.subject?.safeZones.right || {
+ x: 0.54,
+ y: 0.06,
+ width: 0.38,
+ height: 0.84,
+ };
+ }
+ if (params.variant === "hero-left-panel") {
+ return params.subject?.safeZones.left || {
+ x: 0.06,
+ y: 0.06,
+ width: 0.38,
+ height: 0.84,
+ };
+ }
+ if (params.variant === "cta-focus") {
+ return params.subject?.safeZones.right || params.subject?.safeZones.left || {
+ x: 0.52,
+ y: 0.10,
+ width: 0.40,
+ height: 0.80,
+ };
+ }
+ if (params.variant === "floating-split") {
+ return params.subject?.safeZones.top || {
+ x: 0.06,
+ y: 0.05,
+ width: 0.88,
+ height: 0.36,
+ };
+ }
+ return params.subject?.safeZones.top || {
+ x: 0.06,
+ y: 0.05,
+ width: 0.88,
+ height: 0.46,
+ };
+}
+
+function buildFeatureNodes(params: {
+ screen: ScreenData;
+}) {
+ const chunks = splitCopy(params.screen.copy);
+ const featureSource =
+ chunks.length > 0 ? chunks : [params.screen.subtitle || params.screen.title].filter(Boolean);
+
+ return featureSource.slice(0, 3).map((item, index) => {
+ const tone = index === 0 ? "primary" : index === 1 ? "secondary" : "neutral";
+ return {
+ kind: "feature-card",
+ id: `feature-${index + 1}`,
+ icon: "sparkle",
+ tone,
+ tag: index === 0 ? "核心升级" : index === 1 ? "场景收益" : "舒适证明",
+ title: item,
+ body:
+ index === 0
+ ? params.screen.subtitle || params.screen.objective || ""
+ : index === 1
+ ? params.screen.objective || params.screen.layoutNote || ""
+ : params.screen.layoutNote || params.screen.subtitle || "",
+ } satisfies DetailUiDslNode;
+ });
+}
+
+function buildSpecRows(screen: ScreenData) {
+ return [
+ { label: "核心卖点", value: screen.title || "核心升级" },
+ ...(screen.objective ? [{ label: "适用场景", value: screen.objective }] : []),
+ ...(screen.subtitle ? [{ label: "结果表达", value: screen.subtitle }] : []),
+ ...(screen.layoutNote ? [{ label: "设计要求", value: screen.layoutNote }] : []),
+ ];
+}
+
+export function buildDetailUiDslV2(params: {
+ overlay: DetailUiScreenOverlaySnapshot;
+ subjectAnalysis?: DetailUiSubjectAnalysis | null;
+}) {
+ const screen = params.overlay.dataModel.screen;
+ if (!screen) return null;
+
+ const totalScreens = Number(params.overlay.surfaceId.split("-")[3] || 0) || undefined;
+ const variant = pickLayoutVariant({
+ screenIndex: params.overlay.screenIndex,
+ totalScreens,
+ screenType: screen.screenType,
+ subject: params.subjectAnalysis,
+ });
+ const region = pickRegion({
+ variant,
+ subject: params.subjectAnalysis,
+ });
+
+ const nodes: DetailUiDslNode[] = [];
+
+ const heroPanel: DetailUiDslNode = {
+ kind: "panel",
+ id: "hero-panel",
+ variant: "glass",
+ tone: variant === "hero-right-panel" ? "light" : "dark",
+ direction: "column",
+ gap: 18,
+ children: [
+ {
+ kind: "badge",
+ id: "screen-badge",
+ icon: "sparkle",
+ text:
+ screen.screenType === "A"
+ ? "技术证明"
+ : screen.screenType === "B"
+ ? "场景共鸣"
+ : screen.screenType === "C"
+ ? "细节证明"
+ : `第${screen.index}屏`,
+ tone: screen.screenType === "A" ? "neutral" : "primary",
+ },
+ {
+ kind: "heading",
+ id: "hero-title",
+ text: screen.title || "",
+ level: 1,
+ emphasis: "hero",
+ },
+ ...(screen.subtitle
+ ? [
+ {
+ kind: "body",
+ id: "hero-subtitle",
+ text: screen.subtitle,
+ emphasis: "strong",
+ } satisfies DetailUiDslNode,
+ ]
+ : []),
+ ],
+ };
+
+ nodes.push(heroPanel);
+
+ const featureNodes = buildFeatureNodes({ screen });
+ const specRows = buildSpecRows(screen);
+
+ if (variant === "floating-split") {
+ nodes.push({
+ kind: "row",
+ id: "compare-row",
+ columns: 2,
+ gap: 16,
+ children: [
+ {
+ kind: "compare",
+ id: "compare-panel",
+ before: {
+ title: "旧方式",
+ body: featureNodes[0]?.title || "久坐容易疲劳、支撑不足",
+ },
+ after: {
+ title: "现在",
+ body: featureNodes[1]?.title || screen.subtitle || screen.title,
+ },
+ },
+ {
+ kind: "stack",
+ id: "support-stack",
+ gap: 14,
+ children: [
+ ...(featureNodes.slice(0, 2) as DetailUiDslNode[]),
+ {
+ kind: "proof",
+ id: "proof-bar",
+ icon: "shield",
+ text: screen.layoutNote || screen.objective || screen.subtitle || screen.title,
+ },
+ ],
+ },
+ ],
+ });
+ } else if (variant === "hero-left-panel" || variant === "hero-right-panel") {
+ nodes.push({
+ kind: "stack",
+ id: "support-column",
+ gap: 16,
+ children: [
+ ...featureNodes,
+ ...(screen.screenType === "A"
+ ? [
+ {
+ kind: "spec-table",
+ id: "spec-table",
+ rows: specRows,
+ } satisfies DetailUiDslNode,
+ ]
+ : []),
+ {
+ kind: "proof",
+ id: "proof-bar",
+ icon: "shield",
+ text: screen.layoutNote || screen.objective || screen.subtitle || screen.title,
+ },
+ ],
+ });
+ } else {
+ nodes.push({
+ kind: "stack",
+ id: "feature-grid-stack",
+ gap: 16,
+ children: [
+ {
+ kind: "row",
+ id: "feature-row",
+ columns: Math.min(3, Math.max(1, featureNodes.length)),
+ gap: 12,
+ children: featureNodes as DetailUiDslNode[],
+ },
+ {
+ kind: "proof",
+ id: "proof-bar",
+ icon: "shield",
+ text: screen.layoutNote || screen.objective || screen.subtitle || screen.title,
+ },
+ ],
+ });
+ }
+
+ if (variant === "cta-focus" || /(下单|购买|转化|cta)/i.test(screen.layoutNote || "")) {
+ nodes.push({
+ kind: "cta",
+ id: "cta-section",
+ headline: screen.title,
+ buttonText: screen.screenType === "C" ? "立即对比体验" : "立即了解详情",
+ supportText: screen.subtitle || screen.objective || "继续查看完整卖点与场景说明",
+ });
+ }
+
+ return {
+ version: "detail-ui-dsl/v2",
+ surfaceId: params.overlay.surfaceId,
+ screenIndex: params.overlay.screenIndex,
+ generationMode: params.overlay.generationMode,
+ screenSize: params.overlay.screenSize,
+ designStyle: params.overlay.dataModel.globalStyle,
+ region,
+ layoutVariant: variant,
+ nodes,
+ } satisfies DetailUiDslV2Document;
+}
diff --git a/lib/detail-ui-dsl-v2.ts b/lib/detail-ui-dsl-v2.ts
new file mode 100644
index 000000000..9c33b37d8
--- /dev/null
+++ b/lib/detail-ui-dsl-v2.ts
@@ -0,0 +1,106 @@
+import type { DetailUiGlobalStyle } from "@/lib/detail-ui-protocol";
+
+export type DetailUiDslRegion = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+};
+
+export type DetailUiDslNode =
+ | {
+ kind: "panel";
+ id: string;
+ variant?: "glass" | "solid" | "ghost";
+ tone?: "light" | "dark" | "accent";
+ region?: DetailUiDslRegion;
+ direction?: "column" | "row";
+ gap?: number;
+ children: DetailUiDslNode[];
+ }
+ | {
+ kind: "stack";
+ id: string;
+ gap?: number;
+ children: DetailUiDslNode[];
+ }
+ | {
+ kind: "row";
+ id: string;
+ gap?: number;
+ columns?: number;
+ children: DetailUiDslNode[];
+ }
+ | {
+ kind: "badge";
+ id: string;
+ icon?: "sparkle" | "shield" | "arrow" | "check";
+ text: string;
+ tone?: "primary" | "secondary" | "neutral";
+ }
+ | {
+ kind: "heading";
+ id: string;
+ text: string;
+ level?: 1 | 2 | 3;
+ emphasis?: "hero" | "section";
+ }
+ | {
+ kind: "body";
+ id: string;
+ text: string;
+ emphasis?: "normal" | "muted" | "strong";
+ }
+ | {
+ kind: "feature-card";
+ id: string;
+ icon?: "sparkle" | "shield" | "arrow" | "check";
+ tone?: "primary" | "secondary" | "neutral";
+ tag?: string;
+ title: string;
+ body?: string;
+ }
+ | {
+ kind: "spec-table";
+ id: string;
+ rows: Array<{ label: string; value: string }>;
+ }
+ | {
+ kind: "compare";
+ id: string;
+ before: { title: string; body: string };
+ after: { title: string; body: string };
+ }
+ | {
+ kind: "proof";
+ id: string;
+ icon?: "shield" | "check" | "sparkle";
+ text: string;
+ }
+ | {
+ kind: "cta";
+ id: string;
+ headline?: string;
+ buttonText: string;
+ supportText?: string;
+ };
+
+export interface DetailUiDslV2Document {
+ version: "detail-ui-dsl/v2";
+ surfaceId: string;
+ screenIndex: number;
+ generationMode: "classic" | "agent_layout";
+ screenSize: {
+ width: number;
+ height: number;
+ };
+ designStyle?: DetailUiGlobalStyle;
+ region: DetailUiDslRegion;
+ layoutVariant:
+ | "hero-left-panel"
+ | "hero-right-panel"
+ | "top-banner-bottom-grid"
+ | "floating-split"
+ | "cta-focus";
+ nodes: DetailUiDslNode[];
+}