import { Fragment, useState } from "react";
import type { CSSProperties } from "react";
import { Play } from "lucide-react";
import type { Asset, BillingSummary, Product, Project, Team, User } from "../types";
import type { Notice, Page } from "./route-config";
import { money, stageOrder, statusPill } from "./stage-config";
import { CornerMarks, Decorations, Sidebar, ToastLike } from "../components/app-shell";
import { IconKitSvg } from "../components/IconKitSvg";

// 真实资产缩略图注入:与全站一致用 --mock-media-url(.placeholder.has-mock-media 负责 cover 裁切 + 8px 圆角)
const mediaStyle = (url: string): CSSProperties => ({ ["--mock-media-url"]: `url(${url})` } as CSSProperties);
// 基础资产组 kind → 中文区块名(对齐 design.md 商品/人物/场景三类)
const KIND_LABEL: Record<string, string> = { product: "商品", person: "人物", scene: "场景" };
// 视频片段状态 pill 文案(色调走 statusPill:ok/info/err/neutral)
function statusLabel(status: string): string {
  if (["succeeded", "completed", "done", "ok"].includes(status)) return "完成";
  if (["failed", "error"].includes(status)) return "失败";
  if (["running", "queued", "polling"].includes(status)) return "生成中";
  if (status === "needs_review") return "待确认";
  return "待生成";
}
// 毫秒 → mm:ss(时间轴 / 字幕展示)
function fmtMs(ms: number): string {
  const total = Math.max(0, Math.round(ms / 1000));
  return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
}

// 时间轴 clip 真实定位:start_ms / duration_ms 相对轨道总长(timeline.duration_seconds)算百分比
function clipLayout(startMs: number, durationMs: number, totalMs: number): { leftPct: number; widthPct: number } {
  const total = totalMs > 0 ? totalMs : 1;
  return { leftPct: Math.max(0, (startMs / total) * 100), widthPct: Math.max(0, Math.min(100, (durationMs / total) * 100)) };
}
// 标尺刻度:按轨道总秒数生成每秒一刻,偶数秒为主刻度带秒标
function buildRuler(totalSec: number): Array<{ leftPct: number; major: boolean; t?: string }> {
  const secs = Math.max(1, Math.round(totalSec));
  const ticks: Array<{ leftPct: number; major: boolean; t?: string }> = [];
  for (let s = 0; s <= secs; s += 1) {
    const major = s % 2 === 0;
    ticks.push({ leftPct: (s / secs) * 100, major, t: major ? `${s}s` : undefined });
  }
  return ticks;
}

// 装饰用伪波形(BGM 轨铺底,纯视觉,无真实波形数据时使用)
const ED_WAVE: Array<[number, number]> = [[8,4],[6,8],[3,14],[7,6],[4,12],[2,16],[6,8],[8,4],[5,10],[3,14],[7,6],[4,12],[6,8],[2,16],[5,10],[7,6],[3,14],[6,8],[8,4],[4,12],[2,16],[5,10],[7,6],[3,14],[6,8],[4,12],[8,4],[5,10],[2,16],[7,6],[3,14],[6,8],[4,12],[8,4],[5,10],[3,14],[6,8],[2,16],[7,6],[4,12],[5,10],[3,14],[6,8],[8,4],[4,12],[2,16],[5,10],[7,6],[3,14],[6,8],[8,4],[4,12],[2,16],[6,8],[5,10],[3,14],[7,6],[4,12],[8,4],[6,8],[2,16],[5,10],[3,14],[7,6],[6,8],[4,12],[8,4],[5,10],[2,16],[7,6],[3,14],[6,8],[4,12],[8,4],[5,10],[3,14],[6,8],[2,16],[7,6],[4,12],[5,10],[3,14],[6,8],[8,4],[4,12],[2,16],[5,10],[7,6],[3,14],[6,8],[8,4],[4,12],[2,16],[6,8],[5,10],[3,14],[7,6],[4,12],[8,4],[6,8],[2,16],[5,10],[3,14],[7,6],[6,8],[4,12],[8,4],[5,10],[2,16],[7,6],[3,14],[6,8],[4,12],[8,4],[5,10],[3,14],[6,8],[2,16],[7,6],[4,12],[5,10],[3,14],[6,8],[8,4],[4,12],[2,16],[5,10],[7,6],[3,14],[6,8],[8,4],[4,12],[2,16],[6,8],[5,10],[3,14],[7,6],[4,12],[8,4],[6,8],[2,16],[5,10],[3,14],[7,6],[6,8],[4,12],[8,4],[5,10],[3,14],[6,8]];
const STAGE_STEPS: Array<{ n: number; label: string }> = [
  { n: 1, label: "脚本" },
  { n: 2, label: "基础资产" },
  { n: 3, label: "故事板" },
  { n: 4, label: "视频" },
  { n: 5, label: "拼接导出" }
];

export function PipelinePage(props: {
  project: Project;
  loading: boolean;
  navigate: (page: Page, options?: { projectId?: string; productId?: string }) => void;
  user: User;
  team: Team;
  products: Product[];
  projects: Project[];
  assets: Asset[];
  billing: BillingSummary | null;
  notice: Notice | null;
  unreadCount: number;
  avatarChar: string;
  logout: () => void;
  onRefresh: () => void;
  onGenerateScript: (prompt: string) => void;
  onAdoptScript: (scriptId: string) => void;
  onGenerateBaseAsset: (kind: "product" | "person" | "scene", prompt: string) => void;
  onGenerateStoryboard: (prompt: string) => void;
  onSkipStoryboard: () => void;
  onSubmitVideo: (segmentId: string, prompt: string) => void;
  onPollVideo: (segmentId: string) => void;
  onSubmitAllVideos: (prompt: string) => void;
  onPollAllVideos: () => void;
  onSubmitExport: () => void;
}) {
  const {
    project, loading, navigate, user, team, products, projects, assets, billing, notice, unreadCount, avatarChar, logout,
    onGenerateScript, onGenerateBaseAsset, onGenerateStoryboard, onSkipStoryboard,
    onSubmitVideo, onSubmitAllVideos, onSubmitExport
  } = props;

  // ── 资产解析:把各阶段引用的 asset id → 真实缩略图 preview_url(主图优先,其次首张)──
  const byId = new Map(assets.map((a) => [a.id, a] as const));
  const assetUrl = (id: string | null | undefined): string => {
    if (!id) return "";
    const a = byId.get(id);
    return a?.files?.find((f) => f.is_primary)?.preview_url || a?.files?.[0]?.preview_url || "";
  };
  const assetName = (id: string | null | undefined): string => (id ? byId.get(id)?.name || "" : "");

  // ── Stage 2:基础资产按 kind 分组(product/person/scene),保持设计稿三区顺序 ──
  const groups = project.base_asset_groups ?? [];
  const groupsByKind = (kind: string) => groups.filter((g) => g.kind === kind);
  const KIND_ORDER: Array<"product" | "person" | "scene"> = ["product", "person", "scene"];

  // ── Stage 3:取已采用(is_adopted)的故事板版本,无则取第一版 ──
  const storyboards = project.storyboard_versions ?? [];
  const adoptedStoryboard = storyboards.find((s) => s.is_adopted) || storyboards[0] || null;
  const sbFrames = [...(adoptedStoryboard?.frames ?? [])].sort((a, b) => a.sort_order - b.sort_order);
  const [sbSelected, setSbSelected] = useState(0);
  const sbActiveFrame = sbFrames[Math.min(sbSelected, Math.max(0, sbFrames.length - 1))] || null;

  // ── Stage 4:视频片段(adopted_asset 缩略图 + 状态 pill + 时长)──
  const segments = [...(project.video_segments ?? [])].sort((a, b) => a.sort_order - b.sort_order);
  const segDone = segments.filter((s) => ["succeeded", "completed", "done"].includes(s.status)).length;
  const segTotalSec = segments.reduce((sum, s) => sum + (s.target_duration_seconds || 0), 0);

  // ── Stage 5:时间轴 / 字幕 / BGM(真实定位,轨道总长用 timeline.duration_seconds)──
  const timeline = project.timeline;
  const tlTotalMs = (timeline?.duration_seconds || 0) * 1000;
  const tlClips = [...(timeline?.clips ?? [])].sort((a, b) => a.sort_order - b.sort_order);
  const tlRulerMs = tlTotalMs > 0 ? tlTotalMs : tlClips.reduce((m, c) => Math.max(m, c.start_ms + c.duration_ms), 0) || 15000;
  const ruler = buildRuler(tlRulerMs / 1000);
  const subtitleTrack = (timeline?.subtitle_tracks ?? []).find((t) => t.enabled) || (timeline?.subtitle_tracks ?? [])[0] || null;
  const subtitleCues = [...(subtitleTrack?.content ?? [])].sort((a, b) => a.start_ms - b.start_ms);
  const bgmTracks = timeline?.bgm_tracks ?? [];

  // 步进器:对齐镜像 activateStage 逻辑。默认(无 hash)pane=脚本(1) 但步进器 active=项目真实阶段;
  // 一旦导航(hash 或点击),active 跟随所看阶段,completed=max(项目阶段-1, 所看阶段-1)。
  const projectStage = project.status === "completed" ? 5 : Math.max(1, (stageOrder as readonly string[]).indexOf(project.current_stage) + 1);
  const initHash = typeof location !== "undefined" ? location.hash.match(/#stage-(\d)/) : null;
  const [viewStage, setViewStage] = useState(initHash ? Number(initHash[1]) : 1);
  const [navigated, setNavigated] = useState(Boolean(initHash));
  const activeDot = navigated ? viewStage : projectStage;
  const completed = Math.max(projectStage - 1, activeDot - 1);
  const [chatText, setChatText] = useState("");
  const [storyboardPrompt, setStoryboardPrompt] = useState("统一商品、人物、场景风格,生成可直接指导视频的分镜图");
  const [videoPrompt, setVideoPrompt] = useState("竖屏电商短视频,镜头稳定,商品露出清晰,节奏有转化感");
  const canExport = project.video_segments.length > 0 && project.video_segments.every((segment) => Boolean(segment.adopted_version));
  // 真实商品名 + 封面资产 id(商品组无 adopted_asset 时,商品缩图回退到商品库封面)
  const productRecord = products.find((item) => item.id === project.product);
  const productName = productRecord?.title || "透真补水面膜";
  const productCover = productRecord?.cover_asset || productRecord?.images?.find((img) => img.is_primary)?.asset || productRecord?.images?.[0]?.asset || null;

  function goStage(n: number) {
    setViewStage(n);
    setNavigated(true);
    if (typeof location !== "undefined") location.hash = `stage-${n}`;
  }
  function dotCls(n: number) {
    if (n === activeDot) return "sp-dot active";
    if (n <= completed) return "sp-dot done";
    return "sp-dot";
  }

  return (
    <div className="app pipeline-page">
      <Sidebar page="pipeline" navigate={navigate} user={user} team={team} products={products} projects={projects} />
      <main>
        <Decorations />
        <header className="topbar">
          <div className="pipeline-topbar-left">
            <a className="btn btn-ghost pipeline-back" href="/projects" onClick={(event) => { event.preventDefault(); navigate("projects"); }} aria-label="返回视频项目">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M19 12H5" /><path d="M12 19l-7-7 7-7" /></svg>
              返回视频项目
            </a>
            <div className="pipeline-topbar-title" title={project.name}>
              {project.name}<span className="mono">// PIPELINE</span>
            </div>
          </div>
          <div className="right">
            <span className="balance-chip" onClick={() => navigate("account")}>
              <IconKitSvg name="creditCard" />
              余额 <strong>{money(billing?.account.balance)}</strong>
            </span>
            <button className="icon-btn" type="button" onClick={() => navigate("messages")} title="消息中心">
              <IconKitSvg name="bell" />
              {unreadCount > 0 && <span className="count-noti">{unreadCount}</span>}
            </button>
            <div className="topbar-avatar" onDoubleClick={logout} title="账户(双击退出)">
              <span>{avatarChar}</span>
            </div>
          </div>
          <div className="stage-pill" id="stage-pill">
            {STAGE_STEPS.map((step, index) => (
              <Fragment key={step.n}>
                <a className={dotCls(step.n)} data-stage={step.n} href={`#stage-${step.n}`} onClick={(event) => { event.preventDefault(); goStage(step.n); }}>
                  <span className="d"></span><span className="l">{step.label}</span>
                </a>
                {index < STAGE_STEPS.length - 1 && <span className={`sp-line${step.n <= completed ? " done" : ""}`}></span>}
              </Fragment>
            ))}
          </div>
        </header>

        <div className="content content--fh content--fh-flat" id="page-content">
          <CornerMarks />
          {notice && <ToastLike notice={notice} />}

          {/* ============= STAGE 1 · 脚本 ============= */}
          <section className={`stage${viewStage === 1 ? " active" : ""}`} data-stage-pane="1">
            <div className="stage-script">
              <div className="pane shot-list">
                <div className="pane-h">
                  <div className="shot-headline">
                    <strong>镜头脚本</strong>
                    <span className="muted-2 mono" id="shots-meta" style={{ fontSize: "11px" }}>· 空 · 待生成</span>
                  </div>
                  <div className="script-brief-summary" aria-label="当前创作方向">
                    <span className="pill neutral script-brief-pill"><span className="k">来源</span><span className="v" id="brief-source">未选择</span></span>
                    <span className="pill neutral script-brief-pill"><span className="k">风格</span><span className="v" id="brief-style">待确认</span></span>
                    <span className="pill neutral script-brief-pill"><span className="k">人物</span><span className="v" id="brief-persona">待确认</span></span>
                  </div>
                  <div className="script-tags" id="script-tags">
                    <div className="tag-group" data-kind="char">
                      <span className="tg-lbl">// 人物</span>
                      <button className="tag-add" type="button" aria-label="添加人物">+</button>
                    </div>
                    <div className="tag-group" data-kind="scene">
                      <span className="tg-lbl">// 场景</span>
                      <button className="tag-add" type="button" aria-label="添加场景">+</button>
                    </div>
                  </div>
                  <span className="spacer"></span>
                  <button className="btn btn-ghost btn-sm" type="button" id="chat-regen-btn">↻ 整体重写</button>
                </div>
                <div className="shots-body" id="shots-body">
                  <div className="shots-empty">
                    <div className="empty-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="2" /><path d="M3 10h18M9 5v14" /></svg></div>
                    <div className="empty-title">还没有镜头脚本</div>
                    <div className="empty-hint">// 跟右侧脚本助手对话<br />选择一种方式生成你的第一稿</div>
                  </div>
                </div>
              </div>

              <div className="stage-script-gutter" id="stage-script-gutter" role="separator" aria-orientation="vertical" aria-label="拖动调整脚本助手宽度"></div>

              <div className="pane chat-pane">
                <div className="pane-h">
                  <div className="ai-avatar">AI</div>
                  <strong>脚本助手</strong>
                  <span className="muted-2 mono" style={{ fontSize: "11px" }}>· GPT-4o</span>
                  <span className="spacer"></span>
                  <button className="btn btn-ghost btn-sm" type="button" id="chat-clear-btn">清空对话</button>
                </div>
                <div className="chat-body" id="chat-body">
                  <div className="chat-empty">
                    <div className="ce-title">选择一种生成方式开始</div>
                    <div className="ce-hint">// 三种,由「最省事」到「最保真原意」</div>
                    <div className="chat-modes">
                      <button className="chat-mode primary" type="button" data-mode="ai" disabled={loading} onClick={() => onGenerateScript("AI 全生 · 突出商品卖点,节奏紧凑,适合短视频投放")}><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z" /></svg>AI 全生</button>
                      <button className="chat-mode" type="button" data-mode="theme"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18h6" /><path d="M10 22h4" /><path d="M15 14a4.65 4.65 0 0 0 1.4-2.5A6 6 0 1 0 6 8c0 1 .23 2.23 1.5 3.5" /></svg>一句话主题</button>
                      <button className="chat-mode" type="button" data-mode="manual"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6" /></svg>自带脚本</button>
                    </div>
                  </div>
                </div>
                <div className="chat-input">
                  <div className="chat-input-card">
                    <div className="chat-attach-row" id="chat-attach-row" hidden></div>
                    <textarea className="chat-input-area" id="chat-textarea" placeholder="直接说怎么改,如:更像小红书种草 / 换成熬夜党" rows={2} value={chatText} onChange={(event) => setChatText(event.target.value)}></textarea>
                    <div className="chat-input-foot">
                      <button className="chat-icon-btn" id="chat-upload-btn" type="button" title="上传脚本附件" aria-label="上传脚本附件">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12h14" /></svg>
                      </button>
                      <span className="spacer"></span>
                      <button className="chat-send-btn" id="chat-send-btn" type="button" title="发送" aria-label="发送" disabled={loading || !chatText.trim()} onClick={() => { onGenerateScript(chatText.trim()); setChatText(""); }}>
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </div>

            <div className="stage-foot">
              <div className="info"><span className="mono">[ LLM 用量 ~2.4k tokens · ¥0.04 · 失败不扣 · 通过后扣 ]</span></div>
              <div className="hstack">
                <button className="btn" type="button" disabled={loading} onClick={() => onGenerateScript("整体重新生成 · 突出商品卖点,节奏紧凑")}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg> 重新生成全部</button>
                <button className="btn btn-primary btn-lg" type="button" onClick={() => goStage(2)}>确认脚本,进入下一步 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
              </div>
            </div>
          </section>

          {/* ============= STAGE 2 · 基础资产(真实 base_asset_groups,按 kind 分组)============= */}
          {viewStage === 2 && (() => {
            const productGroup = groupsByKind("product")[0] || null;
            const productAssetUrl = assetUrl(productGroup?.adopted_asset) || assetUrl(productGroup?.candidate_assets?.[0]) || assetUrl(productCover);
            const productCandidates = (productGroup?.candidate_assets ?? []).filter((id) => id !== productGroup?.adopted_asset);
            return (
            <section className="stage active" data-stage-pane="2">
              <div className="stage-assets">
                <div className="asset-side">
                  {KIND_ORDER.map((kind) => {
                    const list = groupsByKind(kind);
                    const adopted = list.filter((g) => g.adopted_asset).length;
                    return (
                      <div className={`ttab${kind === "product" ? " active" : ""}`} key={kind} data-jump={`asset-sec-${kind}`}>
                        <span>{KIND_LABEL[kind]}</span><span className="num">{list.length ? `${adopted}/${list.length}` : "0"}</span>
                      </div>
                    );
                  })}
                  <div className="info">
                    基础资产是后续故事板的素材。所有卡片同时展示,点左侧分类直接定位。
                    <br /><br />
                    <strong className="mono">// 人物 +¥0.20/张</strong>
                    <strong className="mono">// 场景 +¥0.15/张</strong>
                    <span style={{ color: "var(--black-alpha-48)" }}>商品图无成本(直接复用商品库)</span>
                  </div>
                </div>

                <div className="asset-main">
                  <section className="asset-sec" id="asset-sec-product">
                    <div className="sec-h"><h3>商品 · <span id="asset-prod-name">{productName}</span></h3><span className="spacer"></span></div>
                    <div className="prod-row">
                      <div className="asset-card-2 prod-lib-card" data-asset-kind="product" data-asset-id={productGroup?.adopted_asset || "prod-main"} id="asset-prod-card">
                        <div className={`placeholder prod-thumb${productAssetUrl ? " has-mock-media" : ""}`} style={productAssetUrl ? mediaStyle(productAssetUrl) : undefined}>
                          {!productGroup?.adopted_asset && (
                            <span className="tri-missing-badge" id="asset-prod-tri-badge" tabIndex={0} role="button" aria-label="缺三视图,查看说明">
                              <span className="ico" aria-hidden="true"></span>
                              <span className="lbl-mono">缺三视图</span>
                              <span className="tri-missing-pop" role="tooltip">
                                <span className="pop-h">
                                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 9v4M12 17h.01" /><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /></svg>
                                  MISSING TRI-VIEW
                                </span>
                                <span className="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
                                <span className="pop-tip">建议:点右下 <b>AI 生成三视图</b> 先补齐三视图,再发起后续生成。</span>
                              </span>
                            </span>
                          )}
                          <span className="ph-frame" id="asset-prod-thumb-label">{productName} · 主图</span>
                        </div>
                        <div className="prod-body">
                          <div className="prod-name" id="asset-prod-card-name">{productName}</div>
                          <div className="prod-cat">{products.find((item) => item.id === project.product)?.category || "未分类"}</div>
                          <div className="prod-date">{(project.created_at || "").slice(0, 10)} 创建</div>
                        </div>
                        <div className="prod-action" id="asset-prod-action">
                          <button className="btn-aigen" type="button" data-stop id="asset-prod-aigen-btn" disabled={loading} onClick={() => onGenerateBaseAsset("product", `${productName} 三视图`)}>
                            <svg className="ai-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3z" /><path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7L19 14z" /></svg>
                            AI 生成三视图
                          </button>
                        </div>
                      </div>
                      <div className={`prod-preview${productCandidates.length ? " show" : ""}`} id="asset-prod-preview">
                        <div className="prod-preview-h">// 候选三视图 · <span id="prod-preview-status">{productCandidates.length} 张</span></div>
                        <div className={`placeholder prod-preview-img${assetUrl(productCandidates[0]) ? " has-mock-media" : ""}`} id="prod-preview-img" style={assetUrl(productCandidates[0]) ? mediaStyle(assetUrl(productCandidates[0])) : undefined}><span className="ph-frame">候选 #1</span></div>
                        <div className="prod-preview-foot" id="prod-preview-foot">
                          {productCandidates.slice(0, 4).map((id) => (
                            <div className={`placeholder${assetUrl(id) ? " has-mock-media" : ""}`} key={id} style={{ ...(assetUrl(id) ? mediaStyle(assetUrl(id)) : {}), width: "44px", height: "44px", flex: "0 0 44px" }}><span className="ph-frame"></span></div>
                          ))}
                        </div>
                      </div>
                    </div>
                  </section>

                  {(["person", "scene"] as const).map((kind) => {
                    const list = groupsByKind(kind);
                    return (
                      <section className="asset-sec" id={`asset-sec-${kind}`} key={kind}>
                        <div className="sec-h"><h3>{KIND_LABEL[kind]} · {list.length} 个</h3><span className="spacer"></span></div>
                        {list.length ? (
                          <div className="asset-grid-2">
                            {list.map((group, gi) => {
                              const mainUrl = assetUrl(group.adopted_asset) || assetUrl(group.candidate_assets?.[0]);
                              const cands = (group.candidate_assets ?? []).filter((id) => id !== group.adopted_asset).slice(0, 4);
                              return (
                                <div className="asset-card-2" data-asset-kind={kind} data-asset-id={group.id} key={group.id}>
                                  <div className={`placeholder thumb-2${mainUrl ? " has-mock-media" : ""}`} style={mainUrl ? mediaStyle(mainUrl) : undefined}>
                                    <span className="ph-frame">{assetName(group.adopted_asset) || `${KIND_LABEL[kind]} ${gi + 1}`}</span>
                                  </div>
                                  <div className="body-2">
                                    <div className="hstack">
                                      <strong style={{ fontSize: "13.5px" }}>{assetName(group.adopted_asset) || `${KIND_LABEL[kind]} ${gi + 1}`}</strong>
                                      <span className="spacer"></span>
                                      {group.adopted_asset
                                        ? <span className="pill ok"><span className="dot"></span>已采用</span>
                                        : <span className="pill neutral"><span className="dot"></span>待采用</span>}
                                    </div>
                                    <div className="prompt-box" contentEditable suppressContentEditableWarning spellCheck={false} data-stop>{group.prompt || "（暂无提示词）"}</div>
                                    {cands.length > 0 && (
                                      <div className="hstack" style={{ marginTop: "10px", gap: "6px", flexWrap: "wrap" }}>
                                        {cands.map((id) => (
                                          <div className={`placeholder${assetUrl(id) ? " has-mock-media" : ""}`} key={id} style={{ ...(assetUrl(id) ? mediaStyle(assetUrl(id)) : {}), width: "40px", height: "40px", flex: "0 0 40px" }}><span className="ph-frame"></span></div>
                                        ))}
                                      </div>
                                    )}
                                  </div>
                                </div>
                              );
                            })}
                          </div>
                        ) : (
                          <div className="placeholder" style={{ minHeight: "120px" }}><span className="ph-frame">// 暂无{KIND_LABEL[kind]}资产 · 待生成</span></div>
                        )}
                      </section>
                    );
                  })}
                </div>
              </div>

              <div className="stage-foot">
                <div className="info"><span className="mono">[ 基础资产同时展示 · 商品图复用商品库 · 失败不扣 ]</span></div>
                <div className="hstack">
                  <button className="btn" type="button" onClick={() => goStage(1)}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7" /></svg> 返回脚本</button>
                  <button className="btn btn-primary btn-lg" type="button" onClick={() => goStage(3)}>确认资产,进入故事板 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
                </div>
              </div>
            </section>
            );
          })()}
          {/* ============= STAGE 3 · 故事板(采用版的 frames,真图 + 镜头提示词)============= */}
          {viewStage === 3 && (
            <section className="stage active" data-stage-pane="3">
              <div className="stage-storyboard">
                <div className="sb-canvas">
                  <div className="sb-scenes-col" id="sb-scenes-row">
                    {sbFrames.length ? sbFrames.map((frame, idx) => {
                      const url = assetUrl(frame.asset);
                      return (
                        <div className={`sb-scene-thumb${idx === sbSelected ? " selected" : ""}`} key={frame.id} data-sid={frame.id} onClick={() => setSbSelected(idx)}>
                          <div className={`placeholder${url ? " has-mock-media" : ""}`} style={url ? mediaStyle(url) : undefined}><span className="ph-frame">场 {idx + 1}</span></div>
                          <div className="nm">场 {idx + 1}</div>
                          <div className="sub">#{frame.sort_order + 1}</div>
                        </div>
                      );
                    }) : <div className="placeholder" style={{ aspectRatio: "1" }}><span className="ph-frame">// 暂无</span></div>}
                  </div>
                  {(() => {
                    const url = assetUrl(sbActiveFrame?.asset);
                    return (
                      <div className={`placeholder sb-main-img${url ? " has-mock-media" : ""}`} id="sb-main-img" style={url ? mediaStyle(url) : undefined}>
                        <span className="ph-frame">{sbActiveFrame ? `场 ${sbSelected + 1}` : "// 故事板未生成"}</span>
                      </div>
                    );
                  })()}
                </div>

                <div className="sb-side">
                  <div className="pane" style={{ padding: "18px" }}>
                    <div className="hstack" style={{ marginBottom: "10px" }}>
                      <strong style={{ fontSize: "14px" }}>故事板 · <span id="sb-side-scene">{sbActiveFrame ? `场 ${sbSelected + 1}` : "—"}</span></strong>
                      <span className="spacer"></span>
                      {adoptedStoryboard
                        ? <span className="pill ok"><span className="dot"></span>已生成</span>
                        : <span className="pill neutral"><span className="dot"></span>未生成</span>}
                    </div>
                    <div className="muted-2" style={{ fontSize: "12px", lineHeight: 1.55, marginBottom: "10px" }}>整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。</div>
                    <div className="sb-rerun-note">
                      <span className="warn-ic" aria-hidden="true">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4M12 17h.01" /></svg>
                      </span>
                      <div className="note-copy"><strong>仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href="#stage-1" onClick={(event) => { event.preventDefault(); goStage(1); }}>Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。</div>
                    </div>
                    <div className="muted mono" style={{ fontSize: "11px", fontWeight: 500, marginBottom: "6px", letterSpacing: ".04em" }}>// 本场提示词</div>
                    <div className="prompt-edit" contentEditable suppressContentEditableWarning id="sb-prompt-edit">{sbActiveFrame?.prompt || adoptedStoryboard?.prompt || "（暂无提示词）"}</div>
                    <div className="sb-stage-actions">
                      <button className="pill-cta heat" type="button" id="sb-rerun-btn" disabled={loading} onClick={() => onGenerateStoryboard(storyboardPrompt)}>
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>
                        整张重跑
                      </button>
                      <span className="spacer"></span>
                      <span className="muted-2 mono" style={{ fontSize: "11px", alignSelf: "center" }}>~¥0.45/场</span>
                    </div>
                    <div className="sb-history">
                      <div className="sb-history-h">// 历史版本(<span id="sb-history-ct">{storyboards.length}</span>)</div>
                      <div className="sb-history-row" id="sb-history-row">
                        {storyboards.length ? storyboards.map((ver) => {
                          const cover = assetUrl([...(ver.frames ?? [])].sort((a, b) => a.sort_order - b.sort_order)[0]?.asset);
                          return (
                            <div className={`sb-history-thumb${ver.is_adopted ? " current" : ""}`} key={ver.id} data-vi={ver.id}>
                              <div className={`placeholder${cover ? " has-mock-media" : ""}`} style={cover ? mediaStyle(cover) : undefined}><span className="ph-frame">{ver.is_adopted ? "采用" : "历史"}</span></div>
                              <div className="ts">{(ver.created_at || "").slice(11, 16) || "--:--"}</div>
                            </div>
                          );
                        }) : <span className="muted-2 mono" style={{ fontSize: "11px" }}>// 暂无历史</span>}
                      </div>
                    </div>
                    <div className="divider" style={{ marginTop: "16px" }}></div>
                    <div className="muted mono" style={{ fontSize: "11px", fontWeight: 500, marginBottom: "8px", letterSpacing: ".04em" }}>// 绑定的资产</div>
                    <div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }} id="sb-bound-assets">
                      {groups.filter((g) => g.adopted_asset).length ? groups.filter((g) => g.adopted_asset).map((g) => (
                        <span className="asset-tag" key={g.id}><span className="dotc"></span>{assetName(g.adopted_asset) || KIND_LABEL[g.kind] || g.kind}({KIND_LABEL[g.kind] || g.kind})</span>
                      )) : <span className="muted-2 mono" style={{ fontSize: "11px" }}>// 暂无绑定资产</span>}
                    </div>
                  </div>
                </div>
              </div>

              <div className="stage-foot">
                <div className="info"><span className="mono">[ image-2 整张输出 · {sbFrames.length} 场 · 整张重跑,失败不扣 ]</span></div>
                <div className="hstack">
                  <button className="btn" type="button" onClick={() => goStage(2)}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7" /></svg> 返回资产</button>
                  <button className="btn btn-primary btn-lg" type="button" onClick={() => goStage(4)}>确认故事板,开始生成视频 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
                </div>
              </div>
            </section>
          )}
          {/* ============= STAGE 4 · 视频(video_segments,adopted_asset 缩略 + 状态 + 时长)============= */}
          {viewStage === 4 && (() => {
            const pct = segments.length ? Math.round((segDone / segments.length) * 100) : 0;
            return (
            <section className="stage active" data-stage-pane="4">
              <div className="queue-bar">
                <div>
                  <div style={{ fontSize: "14px", fontWeight: 600 }}>视频生成 · {segDone} / {segments.length} 完成</div>
                  <div className="muted-2 mono" style={{ fontSize: "11px", marginTop: "3px", letterSpacing: ".02em" }}>// 每场 Seedance 生成 · {segments.length ? (segDone === segments.length ? "已完成所有场次" : "生成中") : "暂无片段"}</div>
                </div>
                <div className="bar-wrap"><span style={{ width: `${pct}%` }}></span></div>
                <span className="muted mono" style={{ fontSize: "12px" }}>{pct}%</span>
                <button className="btn btn-sm" type="button" disabled={loading || !segments.length} onClick={() => onSubmitAllVideos(videoPrompt)}>↻ 全部重跑</button>
                <button className="btn btn-sm" type="button">
                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: "4px" }}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M17 8l-5-5-5 5" /><path d="M12 3v12" /></svg>
                  上传视频
                </button>
              </div>

              {segments.length ? (
                <div className="video-grid" id="video-grid">
                  {segments.map((seg) => {
                    const url = assetUrl(seg.adopted_asset);
                    const tone = statusPill(seg.status);
                    return (
                      <div className="video-card" key={seg.id} data-video-id={seg.id}>
                        <div className={`placeholder video-thumb${url ? " has-mock-media" : ""}`} style={url ? mediaStyle(url) : undefined}>
                          <span className="ph-frame">场 {seg.sort_order + 1}</span>
                          {url && <div className="play"><div className="btn-play"><Play size={14} fill="currentColor" /></div></div>}
                        </div>
                        <div className="body">
                          <div className="video-card-head">
                            <strong className="video-card-title">场 {seg.sort_order + 1}</strong>
                            <span className={`pill ${tone}`}><span className="dot"></span>{statusLabel(seg.status)}</span>
                          </div>
                          <div className="video-meta">{seg.target_duration_seconds}s{seg.error_message ? ` · ${seg.error_message}` : ""}</div>
                          <div className="video-actions">
                            <button className="btn btn-ghost btn-sm" type="button" data-vstop disabled={loading} onClick={() => onSubmitVideo(seg.id, `${videoPrompt} 第 ${seg.sort_order + 1} 段，时长 ${seg.target_duration_seconds} 秒`)}>重跑</button>
                            <span className="spacer"></span>
                            <button className="btn btn-ghost btn-sm" type="button" data-vstop disabled={!url}>下载</button>
                          </div>
                        </div>
                      </div>
                    );
                  })}
                </div>
              ) : (
                <div className="placeholder" style={{ minHeight: "200px", margin: "18px 28px" }}><span className="ph-frame">// 暂无视频片段 · 先在故事板确认后生成</span></div>
              )}

              <div className="stage-foot">
                <div className="info"><span className="mono">[ 已完成 {segDone} 场 · 总时长 {segTotalSec}s · 失败不扣 · 通过后扣 ]</span></div>
                <div className="hstack">
                  <button className="btn" type="button" onClick={() => goStage(3)}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7" /></svg> 返回故事板</button>
                  <button className="btn btn-primary btn-lg" type="button" onClick={() => goStage(5)}>确认视频,进入拼接 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg></button>
                </div>
              </div>
            </section>
            );
          })()}
          {/* ============= STAGE 5 · 拼接导出(timeline.clips / subtitle_tracks / bgm_tracks 真实定位)============= */}
          {viewStage === 5 && (() => {
            const previewUrl = assetUrl(tlClips[0]?.asset) || assetUrl(segments.find((s) => s.adopted_asset)?.adopted_asset);
            const aspect = timeline?.aspect_ratio || "9:16";
            const resolution = timeline?.resolution || "1080×1920";
            const bgm = bgmTracks[0] || null;
            const bgmName = assetName(bgm?.asset) || (bgm ? "背景音乐" : "");
            return (
            <section className="stage active" data-stage-pane="5">
              <div className="editor">
                <div className="editor-preview">
                  <div className={`canvas${previewUrl ? " has-mock-media" : ""}`} id="ed-canvas" style={previewUrl ? mediaStyle(previewUrl) : undefined}><span id="ed-canvas-label">{aspect} 预览 · {resolution}</span></div>
                  <div className="controls">
                    <button className="ctl-btn" type="button" id="ed-prev-btn" title="上一帧 (←)"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M3 3v10l4-5zM9 3v10l4-5z" fill="currentColor" /></svg></button>
                    <button className="ctl-btn" type="button" id="ed-play-btn" title="播放 / 暂停 (空格)"><svg id="ed-play-icon" width="16" height="16" viewBox="0 0 16 16"><path d="M5 4l7 4-7 4z" fill="currentColor" /></svg></button>
                    <button className="ctl-btn" type="button" id="ed-next-btn" title="下一帧 (→)"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M13 3v10l-4-5zM7 3v10l-4-5z" fill="currentColor" /></svg></button>
                    <span className="muted mono" style={{ fontSize: "12px", marginLeft: "8px" }}><span id="ed-cur-time">0:00</span> / <span id="ed-total-time">{fmtMs(tlRulerMs)}</span></span>
                  </div>
                </div>

                <div className="editor-props">
                  <div className="props-tabs"><div className="active">字幕</div><div>转场</div><div>BGM</div></div>
                  <div className="muted mono" style={{ fontSize: "11px", fontWeight: 500, marginBottom: "8px", letterSpacing: ".04em" }}>// 字幕样式</div>
                  <div className="style-swatch">
                    <div className="swatch-card selected"><div className="demo">真实分享</div><div className="nm">朴素白底</div></div>
                    <div className="swatch-card"><div className="demo b">真实分享</div><div className="nm">影视黑底</div></div>
                    <div className="swatch-card"><div className="demo c">真实分享</div><div className="nm">手写描边</div></div>
                    <div className="swatch-card"><div className="demo d">真实分享</div><div className="nm">综艺暖黄</div></div>
                  </div>
                  <div className="divider"></div>
                  <div className="muted mono" style={{ fontSize: "11px", fontWeight: 500, marginBottom: "8px", letterSpacing: ".04em" }}>// 时间轴(<span id="ed-inspect-name">{timeline?.name || "未命名"}</span>)</div>
                  <div className="props-row"><span className="k">总时长</span><input className="input-mini" id="ed-inspect-start" defaultValue={fmtMs(tlRulerMs)} readOnly /></div>
                  <div className="props-row"><span className="k">片段</span><input className="input-mini" id="ed-inspect-dur" defaultValue={`${tlClips.length} 段`} readOnly /></div>
                  <div className="props-row"><span className="k">字幕</span><input className="input-mini" defaultValue={`${subtitleCues.length} 条`} readOnly /></div>
                  <div className="props-row"><span className="k">分辨率</span><span className="mono" style={{ fontSize: "11.5px" }}>{resolution}</span></div>
                  {bgm && (
                    <>
                      <div className="divider"></div>
                      <div className="muted mono" style={{ fontSize: "11px", fontWeight: 500, marginBottom: "8px", letterSpacing: ".04em" }}>// BGM</div>
                      <div className="props-row" style={{ borderBottom: 0 }}><span style={{ fontSize: "12px", flex: 1 }}>{bgmName} · 音量 {bgm.volume}</span><button className="btn btn-ghost btn-sm" type="button">替换</button></div>
                    </>
                  )}
                </div>

                <div className="timeline" id="ed-timeline">
                  <div className="tl-toolbar">
                    <button className="tl-action" type="button" title="撤销"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7v6h6" /><path d="M21 17a9 9 0 0 0-15-6.7L3 13" /></svg></button>
                    <button className="tl-action" type="button" title="重做"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 7v6h-6" /><path d="M3 17a9 9 0 0 1 15-6.7L21 13" /></svg></button>
                    <span className="tl-sep"></span>
                    <button className="tl-action" type="button" title="在播放头处分割选中片段"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="6" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M20 4L8.12 15.88" /><path d="M14.47 14.48L20 20" /><path d="M8.12 8.12L12 12" /></svg>分割</button>
                    <button className="tl-action" type="button" title="复制选中片段"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>复制</button>
                    <button className="tl-action danger" type="button" title="删除选中片段 (Delete)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><path d="M19 6l-1.5 14a2 2 0 0 1-2 1.8H8.5a2 2 0 0 1-2-1.8L5 6" /></svg>删除</button>
                    <span className="spacer"></span>
                    <div className="tl-zoom"><span className="lbl">// zoom</span><input type="range" min={50} max={200} defaultValue={100} /></div>
                  </div>

                  <div className="tl-ruler">
                    <div className="l">// time</div>
                    <div className="rule-track" id="ed-ruler">
                      {ruler.map((tick, i) => (
                        <span className={`tick ${tick.major ? "major" : "minor"}`} key={i} style={{ left: `${tick.leftPct}%` }}>{tick.t && <span className="t">{tick.t}</span>}</span>
                      ))}
                    </div>
                  </div>

                  <div className="tl-track video-track">
                    <div className="label video"><span className="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" /><path d="M7 2v20M17 2v20M2 12h20M2 7h5M2 17h5M17 17h5M17 7h5" /></svg></span>视频</div>
                    <div className="lane" id="ed-lane-video" data-track="video">
                      {tlClips.length ? tlClips.map((clip, idx) => {
                        const { leftPct, widthPct } = clipLayout(clip.start_ms, clip.duration_ms, tlRulerMs);
                        const lbl = assetName(clip.asset) || `片段 ${idx + 1}`;
                        const frameCount = Math.max(1, Math.round(clip.duration_ms / 1000));
                        return (
                          <div className="clip video" key={clip.id} data-track="video" data-label={lbl} style={{ left: `${leftPct}%`, width: `${widthPct}%` }}>
                            <span className="frames">{Array.from({ length: frameCount + 1 }).map((_, i) => <span className="fr" key={i}></span>)}</span>
                            <span className="num">{idx + 1}</span><span className="lbl">{lbl}</span>
                          </div>
                        );
                      }) : <span className="muted-2 mono" style={{ position: "absolute", left: "8px", top: "50%", transform: "translateY(-50%)", fontSize: "11px" }}>// 暂无片段</span>}
                    </div>
                  </div>

                  <div className="tl-track subtitle-track">
                    <div className="label subtitle"><span className="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7V4h16v3" /><path d="M9 20h6" /><path d="M12 4v16" /></svg></span>字幕</div>
                    <div className="lane" id="ed-lane-subtitle" data-track="subtitle">
                      {subtitleCues.map((cue, i) => {
                        const next = subtitleCues[i + 1];
                        const endMs = next ? next.start_ms : tlRulerMs;
                        const { leftPct, widthPct } = clipLayout(cue.start_ms, Math.max(0, endMs - cue.start_ms), tlRulerMs);
                        return (
                          <div className="clip subtitle" key={i} data-track="subtitle" data-label={cue.text} style={{ left: `${leftPct}%`, width: `${widthPct}%` }}><span className="lbl">{cue.text}</span></div>
                        );
                      })}
                      <div className="playhead" id="ed-playhead" style={{ left: "0%" }}><span className="ph-grab"></span></div>
                    </div>
                  </div>

                  {bgmTracks.length > 0 && (
                    <div className="tl-track bgm-track">
                      <div className="label bgm"><span className="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" /></svg></span>BGM</div>
                      <div className="lane">
                        {bgmTracks.map((track) => {
                          const { leftPct, widthPct } = clipLayout(track.start_ms, Math.max(0, tlRulerMs - track.start_ms), tlRulerMs);
                          const name = assetName(track.asset) || "背景音乐";
                          return (
                            <div className="clip bgm" key={track.id} data-track="bgm" data-label={name} style={{ left: `${leftPct}%`, width: `${widthPct}%` }}>
                              <span className="wave"><svg viewBox="0 0 600 20" preserveAspectRatio="none" fill="currentColor">{ED_WAVE.map(([y, h], i) => <rect key={i} x={i * 4} y={y} width="2" height={h} />)}</svg></span>
                              <span className="lbl">{name} · 音量 {track.volume}</span>
                            </div>
                          );
                        })}
                      </div>
                    </div>
                  )}
                </div>
              </div>

              <div className="stage-foot">
                <div className="info"><span className="mono">[ 时间轴 {fmtMs(tlRulerMs)} · {tlClips.length} 段 · 拼接 / 导出全程 0 token ]</span></div>
                <div className="hstack">
                  <button className="btn" type="button" onClick={() => goStage(4)}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 19l-7-7 7-7" /></svg> 返回片段</button>
                  <button className="btn" type="button">保存草稿</button>
                  <button className="btn btn-primary btn-lg" type="button" disabled={!canExport} onClick={onSubmitExport}>导出 MP4 · {resolution.includes("1080") || resolution.includes("1920") ? "1080P" : resolution} {aspect} <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 4v12m0 0l-5-5m5 5l5-5M4 20h16" /></svg></button>
                </div>
              </div>
            </section>
            );
          })()}
        </div>
      </main>
    </div>
  );
}
