"use client"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, RefreshCw, Plus, Sparkle, Crop, Copy, PencilLine, Trash2, Save } from "lucide-react"
import {
  frameUrl, cleanedFrameUrl, apiAssetUrl,
  describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement,
  generateSceneAsset, generateSubjectAssets,
  type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type SubjectKind,
} from "@/lib/api"
import { toast } from "sonner"

interface Props {
  jobId: string
  frames: KeyFrame[]
  activeIndex: number | null
  selected: Set<number>
  onClose: () => void
  onChange: (idx: number) => void
  onToggleSelect: (idx: number) => void
  onJobUpdate?: (job: Job) => void
  onSwitchPanel?: (key: string) => void
  onCopyImage?: (ref: ImageRef) => void
  embedded?: boolean
}

const OBJECT_VIEW_OPTIONS = [
  ["front", "正面"],
  ["back", "背面"],
  ["left", "左侧"],
  ["right", "右侧"],
  ["top", "顶部"],
  ["bottom", "底部"],
]

const LIVING_VIEW_OPTIONS = [
  ["front", "正面"],
  ["back", "背面"],
  ["side", "侧面"],
  ["side_walk", "走路"],
  ["expression_happy", "喜"],
  ["expression_angry", "怒"],
  ["expression_sad", "哀"],
  ["expression_relaxed", "乐/放松"],
  ["action_sit", "坐"],
  ["action_hold", "手持"],
  ["action_use", "使用"],
]

type LightboxTab = "clean" | "scene" | "subject" | "review"

const LIGHTBOX_TABS: Array<{ key: LightboxTab; label: string }> = [
  { key: "clean", label: "原图/清洗" },
  { key: "scene", label: "场景图" },
  { key: "subject", label: "主体资产" },
  { key: "review", label: "审核" },
]

export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, onChange, onToggleSelect, onJobUpdate, onSwitchPanel, onCopyImage, embedded = false }: Props) {
  const [describing, setDescribing] = useState(false)
  const [cleaningFrameIds, setCleaningFrameIds] = useState<Set<number>>(new Set())
  const [batchCleaning, setBatchCleaning] = useState(false)
  const [batchCleanupProgress, setBatchCleanupProgress] = useState<{ done: number; total: number; failed: number } | null>(null)
  const [applying, setApplying] = useState(false)
  const [sceneGenerating, setSceneGenerating] = useState(false)
  const [subjectGenerating, setSubjectGenerating] = useState<string | null>(null)
  const [assetSize, setAssetSize] = useState<AssetSize>("source")
  const [subjectKinds, setSubjectKinds] = useState<Record<string, SubjectKind>>({})
  const [subjectBackgrounds, setSubjectBackgrounds] = useState<Record<string, AssetBackground>>({})
  const [subjectViews, setSubjectViews] = useState<Record<string, string[]>>({})
  const [activeTab, setActiveTab] = useState<LightboxTab>("clean")
  const [editingElement, setEditingElement] = useState<{
    id: string
    name_zh: string
    name_en: string
    position: string
  } | null>(null)
  const [mounted, setMounted] = useState(false)
  // 画框模式 + 多选区（相对坐标 0-1）
  type Region = { x: number; y: number; w: number; h: number }
  const [cropMode, setCropMode] = useState(false)
  const [regions, setRegions] = useState<Region[]>([])
  const [draftRegion, setDraftRegion] = useState<Region | null>(null)  // 当前正在拖的
  const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null)
  const imgWrapRef = useRef<HTMLDivElement>(null)
  const activeIndexRef = useRef<number | null>(activeIndex)
  useEffect(() => setMounted(true), [])
  useEffect(() => { activeIndexRef.current = activeIndex }, [activeIndex])

  // 切换分镜时清空选区
  useEffect(() => {
    setCropMode(false)
    setRegions([])
    setDraftRegion(null)
    setDragStart(null)
  }, [activeIndex])

  useEffect(() => {
    if (activeIndex === null) return
    const onKey = (e: KeyboardEvent) => {
      const inField = ["INPUT", "TEXTAREA"].includes((e.target as HTMLElement).tagName)
      if (e.key === "Escape") onClose()
      if (!inField && activeIndex !== null) {
        const pos = frames.findIndex((x) => x.index === activeIndex)
        if (e.key === "ArrowLeft" && pos > 0) onChange(frames[pos - 1].index)
        if (e.key === "ArrowRight" && pos < frames.length - 1) onChange(frames[pos + 1].index)
      }
    }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [activeIndex, frames.length, onClose, onChange])

  const f = activeIndex !== null ? frames.find((x) => x.index === activeIndex) : undefined
  const arrayPos = f ? frames.findIndex((x) => x.index === f.index) : -1

  if (activeIndex === null || !f || !mounted) return null
  const desc = f.description
  const elements = f.elements ?? []
  const hasCleaned = !!f.cleaned_url
  const latestSceneAsset = f.scene_assets?.[f.scene_assets.length - 1] ?? null
  const isCleaningCurrentFrame = cleaningFrameIds.has(f.index)
  const cleanedFrameCount = frames.filter((frame) => frame.cleaned_applied || frame.cleaned_url).length
  const pendingCleanFrames = frames.filter((frame) => !frame.cleaned_applied && !frame.cleaned_url)
  const selectedFrameIndices = Array.from(selected).sort((a, b) => a - b)
  const sharedSubjectFrameIndices = selectedFrameIndices.length > 1 ? selectedFrameIndices : [f.index]
  const subjectAssetCount = elements.reduce((sum, item) => sum + (item.subject_assets?.length ?? 0), 0)
  const qualityWarnings = [
    ...(f.quality_report?.warnings ?? []),
    ...(latestSceneAsset?.quality_report?.warnings ?? []),
  ]

  const handleDescribe = async () => {
    setDescribing(true)
    try {
      const updated = await describeFrame(jobId, f.index)
      onJobUpdate?.(updated)
      toast.success(`分镜 ${f.index + 1} 识别完成`)
    } catch (e) {
      toast.error("识别失败：" + (e instanceof Error ? e.message : String(e)))
    } finally {
      setDescribing(false)
    }
  }

  const handleCleanup = async (useRegions = false) => {
    const frameIdx = f.index
    const usable = useRegions ? regions.filter((r) => r.w >= 0.03 && r.h >= 0.03) : null
    setCleaningFrameIds((prev) => new Set(prev).add(frameIdx))
    try {
      const updated = await cleanupFrame(jobId, frameIdx, usable && usable.length > 0 ? usable : null)
      onJobUpdate?.(updated)
      toast.success(`分镜 ${frameIdx + 1} 清洗完成${usable && usable.length > 0 ? `（${usable.length} 个区域）` : ""}`)
      if (useRegions && activeIndexRef.current === frameIdx) {
        setCropMode(false); setRegions([]); setDraftRegion(null)
      }
    } catch (e) {
      toast.error("清洗失败：" + (e instanceof Error ? e.message : String(e)))
    } finally {
      setCleaningFrameIds((prev) => {
        const next = new Set(prev)
        next.delete(frameIdx)
        return next
      })
    }
  }

  const handleCleanupAllFrames = async () => {
    const targets = (pendingCleanFrames.length > 0 ? pendingCleanFrames : frames)
      .filter((frame) => !cleaningFrameIds.has(frame.index))
    if (targets.length === 0) return
    setBatchCleaning(true)
    setBatchCleanupProgress({ done: 0, total: targets.length, failed: 0 })
    let failed = 0
    try {
      for (let i = 0; i < targets.length; i += 1) {
        const frame = targets[i]
        try {
          const updated = await cleanupFrame(jobId, frame.index, null)
          onJobUpdate?.(updated)
        } catch (e) {
          failed += 1
          console.error("batch cleanup failed", frame.index, e)
        } finally {
          setBatchCleanupProgress({ done: i + 1, total: targets.length, failed })
        }
      }
      if (failed > 0) {
        toast.error(`批量清洗完成，${failed} 张失败，可单张重试`)
      } else {
        toast.success(`已生成 ${targets.length} 张清洗版，逐张审核即可`)
      }
    } finally {
      setBatchCleaning(false)
    }
  }

  const handleGenerateSceneAsset = async () => {
    setSceneGenerating(true)
    try {
      const updated = await generateSceneAsset(jobId, f.index, { size: assetSize })
      onJobUpdate?.(updated)
      toast.success(`分镜 ${f.index + 1} 场景图已生成`)
    } catch (e) {
      toast.error("场景图生成失败：" + (e instanceof Error ? e.message : String(e)))
    } finally {
      setSceneGenerating(false)
    }
  }

  const handleGenerateSubjectPackage = async (elementId: string) => {
    const kind = subjectKinds[elementId] ?? "object"
    const defaultViews = (kind === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS).map(([value]) => value)
    const views = subjectViews[elementId]?.length ? subjectViews[elementId] : defaultViews
    setSubjectGenerating(elementId)
    try {
      const updated = await generateSubjectAssets(jobId, f.index, elementId, {
        subject_kind: kind,
        background: subjectBackgrounds[elementId] ?? "white",
        size: assetSize,
        source_frame_indices: sharedSubjectFrameIndices,
        views,
      })
      onJobUpdate?.(updated)
      toast.success(`主体资产包已生成 · ${views.length} 张`)
    } catch (e) {
      toast.error("主体资产包生成失败：" + (e instanceof Error ? e.message : String(e)))
    } finally {
      setSubjectGenerating(null)
    }
  }

  const toggleSubjectView = (elementId: string, view: string, kind: SubjectKind) => {
    const defaults = (kind === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS).map(([value]) => value)
    setSubjectViews((prev) => {
      const current = prev[elementId] ?? defaults
      const next = current.includes(view) ? current.filter((x) => x !== view) : [...current, view]
      return { ...prev, [elementId]: next }
    })
  }

  // 画框 mouse handlers — 坐标基于 img wrapper 相对位置
  const getRelXY = (clientX: number, clientY: number) => {
    const el = imgWrapRef.current
    if (!el) return null
    const r = el.getBoundingClientRect()
    return {
      x: Math.max(0, Math.min(1, (clientX - r.left) / r.width)),
      y: Math.max(0, Math.min(1, (clientY - r.top) / r.height)),
    }
  }
  const onCropMouseDown = (e: React.MouseEvent) => {
    if (!cropMode) return
    e.preventDefault()
    const p = getRelXY(e.clientX, e.clientY)
    if (!p) return
    setDragStart(p)
    setDraftRegion({ x: p.x, y: p.y, w: 0, h: 0 })
  }
  const onCropMouseMove = (e: React.MouseEvent) => {
    if (!cropMode || !dragStart) return
    const p = getRelXY(e.clientX, e.clientY)
    if (!p) return
    setDraftRegion({
      x: Math.min(dragStart.x, p.x),
      y: Math.min(dragStart.y, p.y),
      w: Math.abs(p.x - dragStart.x),
      h: Math.abs(p.y - dragStart.y),
    })
  }
  const onCropMouseUp = () => {
    if (draftRegion && draftRegion.w >= 0.02 && draftRegion.h >= 0.02) {
      setRegions((prev) => [...prev, draftRegion])
    }
    setDraftRegion(null)
    setDragStart(null)
  }

  const handleApplyCleaned = async () => {
    setApplying(true)
    try {
      const updated = await applyCleanedFrame(jobId, f.index)
      onJobUpdate?.(updated)
      toast.success(`分镜 ${f.index + 1} 已替换为清洗版`)
    } catch (e) {
      toast.error("替换失败：" + (e instanceof Error ? e.message : String(e)))
    } finally {
      setApplying(false)
    }
  }

  const handleDiscardCleaned = async () => {
    try {
      const updated = await discardCleanedFrame(jobId, f.index)
      onJobUpdate?.(updated)
      toast.success(`已丢弃清洗版`)
    } catch (e) {
      toast.error("丢弃失败：" + (e instanceof Error ? e.message : String(e)))
    }
  }

  const handleAddElement = async (name_zh: string, name_en?: string, position?: string, source: "auto" | "manual" = "manual") => {
    const zh = name_zh.trim()
    if (!zh) return
    try {
      const updated = await addElement(jobId, f.index, { name_zh: zh, name_en, position, source })
      onJobUpdate?.(updated)
    } catch (e) {
      toast.error("加入失败：" + (e instanceof Error ? e.message : String(e)))
    }
  }

  const handleDeleteElement = async (id: string) => {
    try {
      const updated = await deleteElement(jobId, f.index, id)
      onJobUpdate?.(updated)
      if (editingElement?.id === id) setEditingElement(null)
    } catch (e) {
      toast.error("删除失败：" + (e instanceof Error ? e.message : String(e)))
    }
  }

  const handleUpdateElement = async () => {
    if (!editingElement || !editingElement.name_zh.trim()) return
    try {
      const updated = await updateElement(jobId, f.index, editingElement.id, {
        name_zh: editingElement.name_zh,
        name_en: editingElement.name_en,
        position: editingElement.position,
      })
      onJobUpdate?.(updated)
      setEditingElement(null)
      toast.success("主体已更新")
    } catch (e) {
      toast.error("更新失败：" + (e instanceof Error ? e.message : String(e)))
    }
  }

  // cleaned_url 是 /jobs/.../cleaned.jpg?t=<timestamp> 形式（后端写时带）
  // 这里直接当 absolute path 拼到 API_BASE 上即可：用 cleanedFrameUrl 但带 bust
  const cleanedSrc = (() => {
    if (!f.cleaned_url) return null
    const ts = f.cleaned_url.match(/t=(\d+)/)?.[1]
    return cleanedFrameUrl(jobId, f.index, ts)
  })()
  // bust cache：替换后 frames/{idx}.jpg 内容已变，要刷新
  const mainSrc = `${frameUrl(jobId, f.index)}${f.cleaned_applied ? "?applied=1" : ""}`

  const content = (
    <div
      onClick={(e) => e.stopPropagation()}
      className={embedded
        ? "h-full overflow-hidden flex flex-col"
        : "fixed z-[100] rounded-2xl border border-white/15 bg-black/70 backdrop-blur-2xl overflow-hidden flex flex-col"}
      style={embedded ? {
        height: "100%",
        background: "transparent",
      } : {
        top: 80,
        right: 16,
        width: 740,
        maxHeight: "calc(100vh - 96px)",
        boxShadow: "0 40px 100px -20px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05)",
        animation: "drawer-in 0.24s cubic-bezier(0.32, 0.72, 0, 1)",
      }}
    >
      {/* 顶部工具栏 */}
      {!embedded && (
        <div
          className="flex items-center justify-between px-4 py-2 text-white"
          style={{ background: "linear-gradient(135deg, #f59e0b, #ef4444)" }}
        >
          <div className="flex items-center gap-2">
            <button
              onClick={(e) => { e.stopPropagation(); if (arrayPos > 0) onChange(frames[arrayPos - 1].index) }}
              disabled={arrayPos <= 0}
              className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed"
            >
              <ChevronLeft className="h-4 w-4" />
            </button>
            <button
              onClick={(e) => { e.stopPropagation(); if (arrayPos < frames.length - 1) onChange(frames[arrayPos + 1].index) }}
              disabled={arrayPos >= frames.length - 1}
              className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed"
            >
              <ChevronRight className="h-4 w-4" />
            </button>
            <span className="text-[11px] font-mono text-white/70 ml-1">
              分镜 {String(arrayPos + 1).padStart(2, "0")} / {String(frames.length).padStart(2, "0")}
              <span className="mx-1.5 text-white/30">·</span>
              <span className="text-white/60">{f.timestamp.toFixed(2)}s</span>
            </span>
          </div>
          <button
            onClick={onClose}
            className="h-7 w-7 rounded-md bg-white/10 hover:bg-white/20 text-white inline-flex items-center justify-center"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
      )}

      <div className="flex items-center gap-1 border-b border-white/10 bg-black/28 px-3 py-2">
        {LIGHTBOX_TABS.map((tab) => (
          <button
            key={tab.key}
            type="button"
            onClick={() => setActiveTab(tab.key)}
            className={`h-7 rounded-md px-2.5 text-[11px] font-medium transition ${
              activeTab === tab.key
                ? "bg-white text-black shadow"
                : "bg-white/[0.06] text-white/58 hover:bg-white/[0.12] hover:text-white"
            }`}
          >
            {tab.label}
          </button>
        ))}
        <div className="ml-auto hidden items-center gap-2 text-[10px] text-white/42 sm:flex">
          <span>{latestSceneAsset ? "场景已生成" : "场景待生成"}</span>
          <span>·</span>
          <span>{subjectAssetCount > 0 ? `${subjectAssetCount} 主体资产` : "主体待生成"}</span>
        </div>
      </div>

      {/* 主体 — 左：大图 + 清洗状态；右：主体识别 + 主体资产 */}
      <div className="flex gap-3 p-3 overflow-hidden flex-1 min-h-0">
        {/* 左侧大图区 */}
        <div
          className="flex flex-col items-stretch gap-2 overflow-y-auto pr-1"
          style={{ flex: "1 1 320px", minWidth: 200, maxWidth: 420, minHeight: 0 }}
        >
          {/* 上方：主图 + 画框 overlay */}
          <div
            ref={imgWrapRef}
            className={`relative ${cropMode ? "cursor-crosshair select-none" : ""}`}
            onMouseDown={onCropMouseDown}
            onMouseMove={onCropMouseMove}
            onMouseUp={onCropMouseUp}
            onMouseLeave={onCropMouseUp}
          >
            <img
              src={mainSrc}
              alt={`frame ${f.index}`}
              className="rounded-lg object-contain w-full pointer-events-none"
              style={{ maxHeight: hasCleaned ? "38vh" : "62vh" }}
              draggable={false}
            />
            <div className="absolute top-2 left-2 text-[9.5px] px-1.5 py-0.5 rounded backdrop-blur bg-black/50 text-white/80 pointer-events-none">
              {f.cleaned_applied ? "✨ 已替换为清洗版" : "原图"}
            </div>

            {/* 已确认的多个选区 */}
            {cropMode && regions.map((r, i) => (
              <div key={i} className="absolute pointer-events-none border-2 border-cyan-300/90 bg-cyan-300/10"
                style={{
                  left: `${r.x * 100}%`,
                  top: `${r.y * 100}%`,
                  width: `${r.w * 100}%`,
                  height: `${r.h * 100}%`,
                }}
              >
                <span className="absolute -top-4 left-0 text-[9px] px-1 py-0 rounded-sm bg-cyan-300 text-black font-bold leading-tight">#{i + 1}</span>
              </div>
            ))}

            {/* 当前正在拖的草稿框 */}
            {cropMode && draftRegion && draftRegion.w > 0 && draftRegion.h > 0 && (
              <div className="absolute pointer-events-none border-2 border-cyan-300 border-dashed shadow-[0_0_0_1px_rgba(0,0,0,0.4)]"
                style={{
                  left: `${draftRegion.x * 100}%`,
                  top: `${draftRegion.y * 100}%`,
                  width: `${draftRegion.w * 100}%`,
                  height: `${draftRegion.h * 100}%`,
                }}
              />
            )}

            {/* 画框模式角标（小，左上） — 不再遮挡画面 */}
            {cropMode && (
              <div className="absolute top-2 right-2 text-[9.5px] px-1.5 py-0.5 rounded backdrop-blur bg-cyan-500/85 text-white pointer-events-none font-medium">
                画框 · 已选 {regions.length}
              </div>
            )}
          </div>

          {activeTab === "clean" && (
            <>
          <div className="rounded-lg border border-cyan-300/20 bg-cyan-500/[0.08] p-2">
            <div className="mb-2 flex items-center justify-between gap-2">
              <div>
                <div className="text-[11.5px] font-semibold text-white">批量清洗</div>
                <div className="mt-0.5 text-[9.5px] text-white/42">
                  先批量生成待审核清洗版，不直接覆盖原图
                </div>
              </div>
              <span className="shrink-0 rounded bg-black/35 px-1.5 py-0.5 text-[9.5px] font-mono text-white/55">
                {cleanedFrameCount}/{frames.length}
              </span>
            </div>
            {batchCleanupProgress && (
              <div className="mb-2">
                <div className="mb-1 flex items-center justify-between text-[9.5px] text-white/45">
                  <span>{batchCleaning ? "清洗中" : "最近批量清洗"}</span>
                  <span>{batchCleanupProgress.done}/{batchCleanupProgress.total}{batchCleanupProgress.failed ? ` · 失败 ${batchCleanupProgress.failed}` : ""}</span>
                </div>
                <div className="h-1.5 overflow-hidden rounded-full bg-white/10">
                  <div
                    className="h-full rounded-full bg-cyan-300 transition-all"
                    style={{ width: `${Math.round((batchCleanupProgress.done / Math.max(1, batchCleanupProgress.total)) * 100)}%` }}
                  />
                </div>
              </div>
            )}
            <button
              type="button"
              onClick={handleCleanupAllFrames}
              disabled={batchCleaning || cropMode || frames.length === 0}
              className="w-full rounded-md bg-cyan-500/75 px-2 py-1.5 text-[11px] font-medium text-white transition hover:bg-cyan-400 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1.5"
              title="自动清洗所有未处理关键帧；不满意的帧再手工框选清洗"
            >
              {batchCleaning ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
              {batchCleaning
                ? "批量清洗中…"
                : pendingCleanFrames.length > 0
                  ? `一键清洗未处理 ${pendingCleanFrames.length} 张`
                  : `重新清洗全部 ${frames.length} 张`}
            </button>
          </div>

          {/* 画框工具栏 */}
          {cropMode ? (
            <div className="space-y-1.5">
              <div className="text-[10px] text-white/55 leading-snug">
                {regions.length === 0
                  ? "在图上拖动鼠标 → 框选要清洗的水印、字幕、平台 UI 或杂物"
                  : `已框 ${regions.length} 个 · 继续加框或点击「去掉」批量清洗`}
              </div>
              <div className="flex items-center gap-1">
                <button
                  onClick={() => handleCleanup(true)}
                  disabled={isCleaningCurrentFrame || batchCleaning || regions.length === 0}
                  className="flex-1 px-1.5 py-1.5 rounded-md text-[10.5px] font-medium inline-flex items-center justify-center gap-1 transition bg-cyan-500 hover:bg-cyan-400 text-white disabled:opacity-40 disabled:cursor-not-allowed"
                  title="批量清洗所有框内"
                >
                  {isCleaningCurrentFrame ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkle className="h-3 w-3" />}
                  {isCleaningCurrentFrame ? "去掉中" : `去掉${regions.length > 1 ? ` ${regions.length}` : ""}`}
                </button>
                <button
                  onClick={() => setRegions((prev) => prev.slice(0, -1))}
                  disabled={regions.length === 0}
                  className="px-1.5 py-1.5 rounded-md text-[10.5px] bg-white/10 hover:bg-white/20 text-white disabled:opacity-30 disabled:cursor-not-allowed"
                  title="撤销上一个框"
                >
                  ↶
                </button>
                <button
                  onClick={() => { setCropMode(false); setRegions([]); setDraftRegion(null); setDragStart(null) }}
                  className="px-1.5 py-1.5 rounded-md text-[10.5px] bg-white/10 hover:bg-white/20 text-white"
                  title="退出画框"
                >
                  <X className="h-3 w-3" />
                </button>
              </div>
            </div>
          ) : (
            <button
              onClick={() => { setCropMode(true); setRegions([]) }}
              className="w-full px-3 py-1.5 rounded-md text-[10.5px] font-medium inline-flex items-center justify-center gap-1.5 transition bg-white/[0.06] hover:bg-cyan-500/30 border border-white/15 hover:border-cyan-300/50 text-white/80 hover:text-white"
              title="可连续画多个框 · 批量清洗局部水印或杂物"
            >
              <Crop className="h-3 w-3" />
              框选清洗区域（可多框）
            </button>
          )}

          {/* 下方：清洗版（有待应用版本时显示） */}
          {hasCleaned && cleanedSrc && (
            <div className="relative rounded-lg border border-emerald-400/40 bg-emerald-500/5 p-2 space-y-1.5">
              <div className="flex items-center justify-between pr-5">
                <div className="text-[10px] text-emerald-300 inline-flex items-center gap-1 font-medium">
                  <Sparkle className="h-2.5 w-2.5" />
                  清洗结果
                </div>
                <span className="text-[9px] text-white/40">待应用</span>
              </div>
              <button
                onClick={handleDiscardCleaned}
                title="丢弃这次清洗结果"
                className="absolute top-1.5 right-1.5 h-5 w-5 rounded-full bg-black/40 hover:bg-rose-500/80 text-white/70 hover:text-white inline-flex items-center justify-center transition"
              >
                <X className="h-2.5 w-2.5" />
              </button>
              <img
                src={cleanedSrc}
                alt={`cleaned ${f.index}`}
                className="rounded-md object-contain w-full"
                style={{ maxHeight: "32vh" }}
              />
              <button
                onClick={handleApplyCleaned}
                disabled={applying}
                className="w-full px-2 py-1.5 rounded-md text-[11px] font-medium inline-flex items-center justify-center gap-1 transition bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-40 disabled:cursor-not-allowed"
                title="替换原图为这张干净版 · 后续场景图、主体资产和分镜编排都基于干净版"
              >
                {applying ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
                {applying ? "替换中…" : "替换原图"}
              </button>
            </div>
          )}

          {/* 清洗按钮（全图） */}
          <button
            onClick={() => handleCleanup(false)}
            disabled={isCleaningCurrentFrame || batchCleaning || cropMode}
            className="w-full px-3 py-1.5 rounded-md text-[11.5px] font-medium inline-flex items-center justify-center gap-1.5 transition bg-gradient-to-r from-cyan-500/80 to-emerald-500/80 hover:from-cyan-500 hover:to-emerald-500 text-white disabled:opacity-40 disabled:cursor-not-allowed"
            title="清掉水印 / @用户名 / 字幕 / 平台 logo"
          >
            {isCleaningCurrentFrame ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkle className="h-3.5 w-3.5" />}
            {isCleaningCurrentFrame ? "清洗中…（5-15 秒）" : hasCleaned ? "重新清洗" : f.cleaned_applied ? "再次清洗" : "清洗水印"}
          </button>
            </>
          )}

          {activeTab === "scene" && (
          <div className="rounded-lg border border-white/10 bg-white/[0.035] p-2">
            <div className="mb-2 flex items-center justify-between gap-2">
              <div className="text-[11.5px] font-semibold text-white">场景图</div>
              <select
                value={assetSize}
                onChange={(e) => setAssetSize(e.target.value as AssetSize)}
                className="rounded border border-white/10 bg-black/35 px-1.5 py-0.5 text-[10px] text-white/75 outline-none"
                title="资产输出尺寸"
              >
                <option value="source">原尺寸</option>
                <option value="1024">1024</option>
                <option value="1536">1536</option>
                <option value="2048">2048</option>
              </select>
            </div>
            {latestSceneAsset ? (
              <div className="mb-2 overflow-hidden rounded-md border border-emerald-300/25 bg-black/30">
                <img src={apiAssetUrl(latestSceneAsset.url)} alt={latestSceneAsset.label} className="max-h-36 w-full object-contain bg-black" />
                <div className="flex items-center justify-between gap-2 border-t border-white/10 px-2 py-1 text-[9.5px] text-white/50">
                  <span>{latestSceneAsset.width}×{latestSceneAsset.height}</span>
                  {onCopyImage && (
                    <button
                      type="button"
                      onClick={() => onCopyImage({ kind: "asset", frame_idx: f.index, element_id: latestSceneAsset.id, cutout_id: latestSceneAsset.id, label: latestSceneAsset.label })}
                      className="inline-flex items-center gap-1 rounded bg-violet-500/70 px-1.5 py-0.5 text-white hover:bg-violet-400"
                    >
                      <Copy className="h-2.5 w-2.5" />
                      复制
                    </button>
                  )}
                </div>
              </div>
            ) : null}
            {latestSceneAsset?.quality_report?.warnings?.length ? (
              <div className="mb-2 rounded border border-amber-300/25 bg-amber-500/10 px-2 py-1 text-[10px] leading-snug text-amber-100/85">
                {latestSceneAsset.quality_report.warnings[0]}
              </div>
            ) : null}
            <button
              type="button"
              onClick={handleGenerateSceneAsset}
              disabled={sceneGenerating || isCleaningCurrentFrame || batchCleaning}
              className="w-full rounded-md bg-emerald-500/65 px-2 py-1.5 text-[11px] font-medium text-white transition hover:bg-emerald-400 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1"
              title="生成一张去水印、高清增强后的场景参考图"
            >
              {sceneGenerating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkles className="h-3 w-3" />}
              {sceneGenerating ? "生成场景图中…" : latestSceneAsset ? "重新生成场景图" : "生成场景图"}
            </button>
          </div>
          )}

          {activeTab === "review" && (
            <div className="space-y-2 rounded-lg border border-white/10 bg-white/[0.035] p-2">
              <div className="text-[11.5px] font-semibold text-white">素材审核</div>
              <div className="grid grid-cols-2 gap-1.5 text-[10.5px]">
                <div className={`rounded border px-2 py-1 ${f.cleaned_applied || hasCleaned ? "border-emerald-300/30 bg-emerald-500/10 text-emerald-100" : "border-white/10 bg-black/25 text-white/55"}`}>
                  {f.cleaned_applied ? "已应用清洗" : hasCleaned ? "清洗待确认" : "未清洗"}
                </div>
                <div className={`rounded border px-2 py-1 ${latestSceneAsset ? "border-emerald-300/30 bg-emerald-500/10 text-emerald-100" : "border-white/10 bg-black/25 text-white/55"}`}>
                  {latestSceneAsset ? "场景图已生成" : "场景图未生成"}
                </div>
                <div className={`rounded border px-2 py-1 ${subjectAssetCount > 0 ? "border-violet-300/35 bg-violet-500/12 text-violet-100" : "border-white/10 bg-black/25 text-white/55"}`}>
                  {subjectAssetCount > 0 ? `${subjectAssetCount} 张主体资产` : "主体包未生成"}
                </div>
                <div className={`rounded border px-2 py-1 ${qualityWarnings.length ? "border-amber-300/35 bg-amber-500/12 text-amber-100" : "border-emerald-300/30 bg-emerald-500/10 text-emerald-100"}`}>
                  {qualityWarnings.length ? `${qualityWarnings.length} 个风险` : "质量可用"}
                </div>
              </div>
              {qualityWarnings.length > 0 && (
                <div className="rounded border border-amber-300/25 bg-amber-500/10 px-2 py-1.5 text-[10px] leading-snug text-amber-100/85">
                  {qualityWarnings.slice(0, 3).map((warning, i) => (
                    <div key={i}>{warning}</div>
                  ))}
                </div>
              )}
              <div className="text-[10px] leading-relaxed text-white/42">
                审核通过后，把场景图和主体资产复制到分镜槽位；当前不会自动覆盖素材，避免模型误改细节。
              </div>
            </div>
          )}

          <div className="rounded-md border border-emerald-300/20 bg-emerald-500/10 px-3 py-2 text-[11px] leading-relaxed text-emerald-50/80">
            <div className="mb-0.5 inline-flex items-center gap-1 font-medium text-emerald-100">
              <Check className="h-3 w-3" />
              已在素材准备流程
            </div>
            <div className="text-emerald-50/55">
              抽帧留下的关键帧默认都会参与清洗、场景图和主体资产准备，不需要在这里单独选用。
            </div>
          </div>
        </div>

        {/* 右侧主体识别 + 主体资产 */}
        <div className="flex flex-col gap-2.5 overflow-y-auto flex-1 min-h-0" style={{ minWidth: 240 }}>
          {activeTab === "clean" && (
            <section className="rounded-lg border border-cyan-300/15 bg-cyan-500/[0.08] p-3 text-[11px] leading-relaxed text-white/58">
              <div className="mb-1.5 text-[12.5px] font-semibold text-white">清洗审核</div>
              可以先一键清洗全部素材帧，系统只生成待审核清洗版，不直接覆盖原图。看到没处理好的帧，再回到这张图用框选区域做局部清洗。
            </section>
          )}
          {activeTab === "scene" && (
            <section className="rounded-lg border border-emerald-300/15 bg-emerald-500/[0.08] p-3 text-[11px] leading-relaxed text-white/58">
              <div className="mb-1.5 text-[12.5px] font-semibold text-white">场景资产规则</div>
              每张已选关键帧只需要一张主场景图。它用于后续生视频的空间、构图和光线参考；生成后仍需人工确认水印和细节是否被误改。
              {f.scene_assets?.length ? (
                <div className="mt-2 grid grid-cols-2 gap-2">
                  {f.scene_assets.slice(-4).map((asset) => (
                    <div key={asset.id} className="overflow-hidden rounded-md border border-white/10 bg-black/35">
                      <img src={apiAssetUrl(asset.url)} alt={asset.label} className="h-24 w-full object-contain" />
                      <div className="px-1.5 py-1 text-[9.5px] font-mono text-white/45">{asset.width}×{asset.height}</div>
                    </div>
                  ))}
                </div>
              ) : null}
            </section>
          )}
          {activeTab === "review" && (
            <section className="rounded-lg border border-white/10 bg-white/[0.035] p-3 text-[11px] leading-relaxed text-white/58">
              <div className="mb-2 text-[12.5px] font-semibold text-white">当前帧状态</div>
              <div className="space-y-1.5">
                <div>清洗：{f.cleaned_applied ? "已应用" : hasCleaned ? "待确认" : "未处理"}</div>
                <div>场景图：{latestSceneAsset ? `${latestSceneAsset.width}×${latestSceneAsset.height}` : "未生成"}</div>
                <div>主体候选：{elements.length} 个</div>
                <div>主体资产：{subjectAssetCount} 张</div>
              </div>
            </section>
          )}
          {/* 主体识别 */}
          <section className={activeTab === "subject" ? "" : "hidden"}>
            <div className="flex items-center justify-between mb-2">
              <div className="flex items-center gap-1.5 text-white text-[12.5px] font-semibold">
                <Eye className="h-3.5 w-3.5" />
                主体识别
                {desc && <span className="text-[10px] text-emerald-400 font-mono ml-1">已识别</span>}
              </div>
              <button
                onClick={handleDescribe}
                disabled={describing}
                className="text-[10.5px] text-white/70 hover:text-white px-2 py-0.5 rounded border border-white/20 hover:border-white/40 disabled:opacity-50 inline-flex items-center gap-1"
                title="识别画面主体候选"
              >
                {describing ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <RefreshCw className="h-2.5 w-2.5" />}
                {desc ? "重新识别" : "识别主体"}
              </button>
            </div>

            {!desc ? (
              <div className="rounded-lg border border-dashed border-white/15 bg-white/[0.03] p-3 text-[11.5px] text-white/50 leading-relaxed">
                {describing ? (
                  <div className="flex items-center gap-1.5 text-white/60">
                    <Loader2 className="h-3 w-3 animate-spin" />
                    画面主体识别中…
                  </div>
                ) : (
                  <>点击「识别主体」让 Vision 给出可制作主体资产的候选对象</>
                )}
              </div>
            ) : (
              <div className="space-y-2.5 text-[11.5px]">
                {desc.scene && (
                  <div className="rounded-md bg-violet-500/10 border border-violet-400/25 px-2.5 py-2">
                    <div className="text-[9.5px] uppercase tracking-widest text-violet-300 mb-1">场景</div>
                    <div className="text-white leading-relaxed">{desc.scene}</div>
                  </div>
                )}
                {desc.style && (
                  <div className="rounded-md bg-amber-500/10 border border-amber-400/25 px-2.5 py-2">
                    <div className="text-[9.5px] uppercase tracking-widest text-amber-300 mb-1">风格</div>
                    <div className="text-white leading-relaxed">{desc.style}</div>
                  </div>
                )}
                {desc.objects && desc.objects.length > 0 && (
                  <div className="rounded-md bg-pink-500/10 border border-pink-400/25 px-2.5 py-1.5">
                    <div className="text-[9.5px] uppercase tracking-widest text-pink-300/90 mb-1.5">
                      候选主体 · 点击加入「主体清单」
                    </div>
                    <div className="space-y-1">
                      {desc.objects.map((o, i) => {
                        const alreadyIn = elements.some((e) => e.name_zh === o.name)
                        return (
                          <button
                            key={i}
                            onClick={() => !alreadyIn && handleAddElement(o.name, o.extract_prompt, o.position, "auto")}
                            disabled={alreadyIn}
                            title={alreadyIn ? "已加入主体清单" : (o.position ? `位置：${o.position}` : undefined)}
                            className={`w-full text-left rounded border px-2 py-1 transition group/o ${
                              alreadyIn
                                ? "bg-emerald-500/10 border-emerald-400/30 cursor-default"
                                : "bg-white/[0.03] hover:bg-white/[0.08] border-white/8 hover:border-pink-300/40"
                            }`}
                          >
                            <div className="flex items-center gap-1 text-white text-[11px] font-medium">
                              <span className="truncate">{o.name}</span>
                              {alreadyIn ? (
                                <Check className="h-2.5 w-2.5 text-emerald-300 shrink-0 ml-auto" />
                              ) : (
                                <Plus className="h-2.5 w-2.5 text-pink-300/60 opacity-0 group-hover/o:opacity-100 transition shrink-0 ml-auto" />
                              )}
                            </div>
                            {o.extract_prompt && (
                              <div className="text-[9.5px] text-white/40 mt-0.5 truncate font-mono leading-tight">{o.extract_prompt}</div>
                            )}
                          </button>
                        )
                      })}
                    </div>
                  </div>
                )}
              </div>
            )}
          </section>

          {/* 主体清单（持久化） */}
          <section className={activeTab === "subject" ? "" : "hidden"}>
            <div className="flex items-center gap-1.5 mb-2 text-white text-[12.5px] font-semibold">
              <Sparkles className="h-3.5 w-3.5" />
              主体清单
              {elements.length > 0 && (
                <span className="text-[10px] text-white/35 font-mono ml-0.5">· {elements.length}</span>
              )}
              <span className="text-[9.5px] text-white/35 font-normal ml-auto">→ 先修正，再生成资产</span>
            </div>
            <div className="mb-2 rounded-md border border-white/10 bg-white/[0.035] px-2.5 py-1.5 text-[10.5px] leading-relaxed text-white/45">
              Vision 只负责给主体候选。这里确认名称、类型、背景和需要的视角/动作/表情，再生成主体资产包。
            </div>

            {elements.length > 0 && (
              <div className="space-y-2 mb-2">
                {elements.map((e) => {
                  const hasRegion = !!e.region
                  const isEditing = editingElement?.id === e.id
                  const currentKind = subjectKinds[e.id] ?? e.subject_kind ?? "object"
                  const currentBg = subjectBackgrounds[e.id] ?? e.cutout_background ?? "white"
                  const viewOptions = currentKind === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS
                  const activeViews = subjectViews[e.id] ?? viewOptions.map(([value]) => value)
                  const subjectAssets = e.subject_assets ?? []
                  const isSubjectGenerating = subjectGenerating === e.id
                  return (
                    <div
                      key={e.id}
                      className={`relative rounded-md border p-2 ${
                        isEditing
                          ? "bg-violet-500/10 border-violet-300/45"
                          : "bg-white/[0.04] border-white/10"
                      }`}
                    >
                      {/* 顶部：名字 + 操作 */}
                      <div className="mb-2 space-y-2">
                        {isEditing ? (
                          <div className="space-y-1.5">
                            <input
                              value={editingElement.name_zh}
                              onChange={(ev) => setEditingElement({ ...editingElement, name_zh: ev.target.value })}
                              placeholder="主体名称"
                              className="w-full rounded-md border border-white/15 bg-black/35 px-2 py-1.5 text-[12px] text-white outline-none placeholder:text-white/30 focus:border-violet-300/60"
                            />
                            <input
                              value={editingElement.name_en}
                              onChange={(ev) => setEditingElement({ ...editingElement, name_en: ev.target.value })}
                              placeholder="英文主体提示，可手动修正"
                              className="w-full rounded-md border border-white/15 bg-black/35 px-2 py-1.5 text-[11px] font-mono text-white outline-none placeholder:text-white/30 focus:border-violet-300/60"
                            />
                            <input
                              value={editingElement.position}
                              onChange={(ev) => setEditingElement({ ...editingElement, position: ev.target.value })}
                              placeholder="位置 / 备注，例如：画面左下角、手里拿着"
                              className="w-full rounded-md border border-white/15 bg-black/35 px-2 py-1.5 text-[11px] text-white outline-none placeholder:text-white/30 focus:border-violet-300/60"
                            />
                          </div>
                        ) : (
                          <div className="min-w-0">
                            <div className="flex items-center gap-1 text-white text-[12px] font-medium leading-tight">
                              <span className="truncate">{e.name_zh}</span>
                              {e.source === "auto" && (
                                <span className="text-[8.5px] text-pink-300/70 font-mono">识别</span>
                              )}
                              {e.source === "region" && (
                                <span className="text-[8.5px] text-cyan-300/80 font-mono">框选</span>
                              )}
                              {hasRegion && (
                                <span className="text-[8.5px] text-white/40 font-mono">· 有区域</span>
                              )}
                              {subjectAssets.length > 0 && (
                                <span className="text-[8.5px] text-white/40 font-mono">· {subjectAssets.length} 张资产</span>
                              )}
                            </div>
                            <div className="mt-0.5 text-[10px] font-mono leading-tight truncate text-white/45">
                              {e.name_en || <span className="text-white/30">(无英文，可点改名补充)</span>}
                            </div>
                            {e.position && (
                              <div className="mt-0.5 text-[9.5px] leading-tight truncate text-white/35">
                                {e.position}
                              </div>
                            )}
                          </div>
                        )}

                        <div className="flex items-center gap-1">
                          {isEditing ? (
                            <>
                              <button
                                onClick={handleUpdateElement}
                                disabled={!editingElement.name_zh.trim()}
                                className="flex-1 rounded px-2 py-1 text-[10.5px] font-medium inline-flex items-center justify-center gap-1 bg-violet-500 hover:bg-violet-400 text-white disabled:opacity-40"
                              >
                                <Save className="h-3 w-3" />
                                保存修改
                              </button>
                              <button
                                onClick={() => setEditingElement(null)}
                                className="rounded px-2 py-1 text-[10.5px] bg-white/10 hover:bg-white/20 text-white"
                              >
                                取消
                              </button>
                            </>
                          ) : (
                            <>
                              <button
                                onClick={() => setEditingElement({
                                  id: e.id,
                                  name_zh: e.name_zh,
                                  name_en: e.name_en || "",
                                  position: e.position || "",
                                })}
                                className="rounded px-2 py-1 text-[10.5px] inline-flex items-center gap-1 bg-white/8 hover:bg-white/15 text-white/75 hover:text-white border border-white/10"
                              >
                                <PencilLine className="h-3 w-3" />
                                改名
                              </button>
                              <button
                                onClick={() => handleDeleteElement(e.id)}
                                className="ml-auto rounded px-2 py-1 text-[10.5px] inline-flex items-center gap-1 bg-rose-500/15 hover:bg-rose-500/30 text-rose-100 border border-rose-300/20"
                                title="删除这个主体候选和它的主体资产"
                              >
                                <Trash2 className="h-3 w-3" />
                                删主体
                              </button>
                            </>
                          )}
                        </div>
                      </div>

                      <div className="mt-2 rounded-md border border-violet-300/15 bg-violet-500/[0.08] p-2">
                        <div className="mb-2 flex items-center justify-between gap-2">
                          <div className="text-[11px] font-semibold text-white/90">主体资产包</div>
                          <span className="text-[9.5px] font-mono text-white/35">
                            {sharedSubjectFrameIndices.length > 1 ? `${sharedSubjectFrameIndices.length} 帧参考` : "当前帧参考"}
                          </span>
                        </div>
                        <div className="mb-2 grid grid-cols-3 gap-1">
                          <select
                            value={currentKind}
                            onChange={(ev) => {
                              const next = ev.target.value as SubjectKind
                              setSubjectKinds((prev) => ({ ...prev, [e.id]: next }))
                              setSubjectViews((prev) => ({ ...prev, [e.id]: (next === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS).map(([value]) => value) }))
                            }}
                            className="rounded border border-white/10 bg-black/35 px-1.5 py-1 text-[10px] text-white/75 outline-none"
                          >
                            <option value="object">物体</option>
                            <option value="living">人物/生物</option>
                          </select>
                          <select
                            value={currentBg}
                            onChange={(ev) => setSubjectBackgrounds((prev) => ({ ...prev, [e.id]: ev.target.value as AssetBackground }))}
                            className="rounded border border-white/10 bg-black/35 px-1.5 py-1 text-[10px] text-white/75 outline-none"
                          >
                            <option value="white">白底</option>
                            <option value="black">黑底</option>
                          </select>
                          <button
                            type="button"
                            onClick={() => handleGenerateSubjectPackage(e.id)}
                            disabled={isSubjectGenerating || activeViews.length === 0}
                            className="rounded bg-violet-500/70 px-1.5 py-1 text-[10px] font-medium text-white transition hover:bg-violet-400 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1"
                            title="生成多视角 / 动作 / 表情主体资产"
                          >
                            {isSubjectGenerating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Wand2 className="h-3 w-3" />}
                            {isSubjectGenerating ? "生成" : "生成"}
                          </button>
                        </div>
                        <div className="mb-2 flex flex-wrap gap-1">
                          {viewOptions.map(([value, label]) => {
                            const active = activeViews.includes(value)
                            return (
                              <button
                                key={value}
                                type="button"
                                onClick={() => toggleSubjectView(e.id, value, currentKind)}
                                className={`rounded border px-1.5 py-0.5 text-[9.5px] transition ${
                                  active
                                    ? "border-violet-300/60 bg-violet-500/40 text-white"
                                    : "border-white/10 bg-black/25 text-white/45 hover:text-white"
                                }`}
                              >
                                {label}
                              </button>
                            )
                          })}
                        </div>
                        {subjectAssets.length > 0 && (
                          <div className="flex flex-wrap gap-1.5">
                            {subjectAssets.slice(-12).map((asset) => (
                              <div key={asset.id} className="relative overflow-hidden rounded-md border border-white/10 bg-white" style={{ width: 88, height: 112 }}>
                                <img src={apiAssetUrl(asset.url)} alt={asset.label} className="h-[82px] w-full object-contain" />
                                <div className="absolute left-0 top-0 rounded-br bg-black/70 px-1 text-[8.5px] text-white">
                                  {asset.label.replace(`${e.name_zh} · `, "")}
                                </div>
                                <div className="flex h-[30px] border-t border-black/10 bg-black text-white">
                                  {onCopyImage && (
                                    <button
                                      type="button"
                                      onClick={(ev) => {
                                        ev.preventDefault(); ev.stopPropagation()
                                        onCopyImage({
                                          kind: "asset",
                                          frame_idx: f.index,
                                          element_id: asset.id,
                                          cutout_id: asset.id,
                                          label: asset.label,
                                        })
                                      }}
                                      className="flex-1 inline-flex items-center justify-center gap-1 text-[9.5px] hover:bg-violet-500/70 transition"
                                    >
                                      <Copy className="h-2.5 w-2.5" />
                                      复制
                                    </button>
                                  )}
                                </div>
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                    </div>
                  )
                })}
              </div>
            )}

            <div className="mt-1.5 text-[10px] text-white/35 leading-relaxed">
              主体候选来自识别结果；确认名称、类型和视角后生成主体资产包。
            </div>
          </section>
        </div>
      </div>

      <div className="px-4 py-1.5 text-[10px] text-white/40 font-mono text-center border-t border-white/5 bg-white/[0.02]">
        ←/→ 切换关键帧 · ESC 关闭
      </div>
    </div>
  )

  return embedded ? content : createPortal(content, document.body)
}
