173 lines
7.8 KiB
TypeScript
173 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { Blocks, CheckCircle2, LayoutTemplate, Layers3 } from "lucide-react";
|
|
import {
|
|
countUiSurfaceComponents,
|
|
getUiSurfaceChildren,
|
|
getUiSurfaceScreenComponents,
|
|
resolveUiSurfaceTextBinding,
|
|
type UiSurfaceSnapshot,
|
|
} from "@/lib/ui-surface-protocol";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type DetailSurfaceInspectorReviewState = {
|
|
screenIndex: number;
|
|
action?: "pass" | "protocol_patch" | "regenerate_image";
|
|
passed?: boolean;
|
|
};
|
|
|
|
function getActionLabel(action?: DetailSurfaceInspectorReviewState["action"]) {
|
|
if (action === "protocol_patch") return "协议修正";
|
|
if (action === "regenerate_image") return "重生图";
|
|
return "通过";
|
|
}
|
|
|
|
function getActionClassName(action?: DetailSurfaceInspectorReviewState["action"]) {
|
|
if (action === "protocol_patch") {
|
|
return "bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-500/20 dark:bg-blue-500/10 dark:text-blue-300";
|
|
}
|
|
if (action === "regenerate_image") {
|
|
return "bg-red-50 text-red-700 ring-1 ring-inset ring-red-500/20 dark:bg-red-500/10 dark:text-red-300";
|
|
}
|
|
return "bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300";
|
|
}
|
|
|
|
export function DetailSurfaceInspectorPanel(params: {
|
|
surface: UiSurfaceSnapshot | null;
|
|
latestScreenValidation?: DetailSurfaceInspectorReviewState | null;
|
|
}) {
|
|
if (!params.surface) return null;
|
|
|
|
const screenComponents = getUiSurfaceScreenComponents(params.surface);
|
|
const totalComponents = countUiSurfaceComponents(params.surface);
|
|
const totalTextBlocks = countUiSurfaceComponents(params.surface, "detail_text_block");
|
|
const totalScreens = screenComponents.length;
|
|
|
|
return (
|
|
<div className="mt-3 rounded-xl border border-border/50 bg-zinc-50/40 p-3 dark:bg-zinc-900/40">
|
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<LayoutTemplate className="h-4 w-4 text-violet-500" />
|
|
<div>
|
|
<p className="text-[11px] font-medium text-foreground">Detail Surface Inspector</p>
|
|
<p className="mt-0.5 text-[10px] text-muted-foreground">
|
|
<span className="font-mono">{params.surface.surfaceId}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span className="rounded-full bg-violet-50 px-2 py-0.5 text-[10px] font-medium text-violet-700 ring-1 ring-inset ring-violet-500/20 dark:bg-violet-500/10 dark:text-violet-300">
|
|
{params.surface.generationMode === "agent_layout" ? "AGENT Surface" : "Classic Surface"}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
|
<Layers3 className="h-3.5 w-3.5" />
|
|
Screens
|
|
</div>
|
|
<div className="mt-1 text-sm font-semibold text-foreground">{totalScreens}</div>
|
|
</div>
|
|
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
|
<Blocks className="h-3.5 w-3.5" />
|
|
Components
|
|
</div>
|
|
<div className="mt-1 text-sm font-semibold text-foreground">{totalComponents}</div>
|
|
</div>
|
|
<div className="rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
|
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
Text Blocks
|
|
</div>
|
|
<div className="mt-1 text-sm font-semibold text-foreground">{totalTextBlocks}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-2 flex items-center justify-between rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40">
|
|
<div>
|
|
<div className="text-[10px] text-muted-foreground">Surface Validation</div>
|
|
<div className="mt-0.5 text-[11px] font-medium text-foreground">
|
|
{params.surface.validation?.passed === false
|
|
? params.surface.validation.issues[0] || "存在待优化项"
|
|
: "当前 surface 已通过基础校验"}
|
|
</div>
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
|
params.surface.validation?.passed === false
|
|
? "bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300"
|
|
: "bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300",
|
|
)}
|
|
>
|
|
{params.surface.validation?.passed === false ? "待优化" : "通过"}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-3 space-y-2">
|
|
{screenComponents.slice(0, 6).map((screenComponent) => {
|
|
const screenIndex = Number(screenComponent.props?.screenIndex || 0);
|
|
const textStack = getUiSurfaceChildren(params.surface, screenComponent.id).find(
|
|
(component) => component.type === "detail_text_stack",
|
|
);
|
|
const textBlocks = textStack
|
|
? getUiSurfaceChildren(params.surface, textStack.id).filter(
|
|
(component) => component.type === "detail_text_block",
|
|
)
|
|
: [];
|
|
const titleBlock =
|
|
textBlocks.find((component) => component.props?.blockType === "title") || null;
|
|
const subtitleBlock =
|
|
textBlocks.find((component) => component.props?.blockType === "subtitle") || null;
|
|
const isLatestScreen = params.latestScreenValidation?.screenIndex === screenIndex;
|
|
|
|
return (
|
|
<div
|
|
key={screenComponent.id}
|
|
className={cn(
|
|
"rounded-lg border border-border/40 bg-background/80 px-3 py-2 dark:bg-zinc-950/40",
|
|
isLatestScreen && "border-blue-300/60 ring-1 ring-inset ring-blue-500/20",
|
|
)}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="text-[11px] font-medium text-foreground">
|
|
第 {screenIndex} 屏
|
|
{screenComponent.props?.screenType
|
|
? ` · ${String(screenComponent.props?.screenType)}`
|
|
: ""}
|
|
</div>
|
|
<div className="mt-0.5 truncate text-[11px] text-foreground/85">
|
|
{resolveUiSurfaceTextBinding(params.surface, titleBlock, "content") || "未定义标题"}
|
|
</div>
|
|
{subtitleBlock ? (
|
|
<div className="mt-0.5 truncate text-[10px] text-muted-foreground">
|
|
{resolveUiSurfaceTextBinding(params.surface, subtitleBlock, "content")}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<span className="rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
|
|
{textBlocks.length} 块
|
|
</span>
|
|
{isLatestScreen ? (
|
|
<span
|
|
className={cn(
|
|
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
|
getActionClassName(params.latestScreenValidation?.action),
|
|
)}
|
|
>
|
|
{getActionLabel(params.latestScreenValidation?.action)}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|