/* ============================================================
   act1.jsx — Act 1: continuous cinematic narrative
   多入口轮播的手机 → 点发送，整屏退场、只剩气泡 →
   气泡飞入「本机 Agent」被解析 → 解析完才浮现路由网络 →
   各 Agent 分头执行(联网/本地/邮件) → 结果汇总回主 Agent →
   回到聊天，AI 直接把结果回过来
   ============================================================ */
window.AP_SCREENS = window.AP_SCREENS || [];

const RESEARCH = {
  id: "research",
  proactive: false,
  title: "了解一家公司，今天你得翻五个地方",
  solve: "顺手的入口说一句话，本机 Agent 把公开信息和你授权的私有资料一起查，直接给你一份结论。",
  hello: "你好，我是装在你电脑上的 Agent。把任务发给我，我现场跑给你看。",
  task: "帮我调研一下 X 公司，重点看团队和最近融资",
  nodes: [
    { id: "web", brand: "web", label: "公开网络", tag: "联网检索", tagTone: "blue", x: 17, y: 26 },
    { id: "runner", brand: "server", label: "远端 Runner", tag: "借闲置 Token", tagTone: "blue", x: 50, y: 13 },
    { id: "model", brand: "openai", label: "国产高性价比模型", tag: "够用就好", tagTone: "green", x: 83, y: 26 },
    { id: "feishu", brand: "feishu", label: "飞书资料库", tag: "本地文件", tagTone: "amber", x: 84, y: 76 },
    { id: "notion", brand: "notion", label: "Notion 数据库", tag: "写入沉淀", tagTone: "neutral", x: 16, y: 76 },
  ],
  card: {
    title: "X 公司 · 调研已交付",
    lines: ["团队：3 位核心来自头部 AI 实验室", "融资：上轮 $12M A 轮，头部基金领投", "风险：核心专利仍在申请阶段"],
  },
  tail: "完整报告已存入「飞书 · 调研库」，并同步到 Notion。",
  readyHint: "一句话，事就办成了 — 轻点看跨工具的活怎么串成一串",
};

/* 场景二 · 跨工具编排：在微信发一句话，约会 / 找文件 / 发给另一个人 一起办妥。
   复用调研那套传送带（DispatchStage），但子 Agent / 工具 / 解析维度换成编排版。 */
/* 痛点2 重点是「工具调度」，不是路由。路由一步带过（要操作本地 App → 只能本机 Agent），
   篇幅留给桌面端的工具编排。这里只留两条最关键的判断。 */
const ORCHESTRATE_DIMS = [
  { icon: "shield", k: "要碰本地 App", v: "找本地文件、操作飞书 / 微信 → 云端 Agent 够不着" },
  { icon: "speed", k: "执行方式", v: "用 Computer Use，像你本人一样在桌面上点" },
];
/* 痛点2 演的是「工具调度 / 编排」：选中本机 Agent 后，由它一个端到端、按顺序在桌面上编排多个 App。
   via = 调用方式：cu 本机 Computer Use 模拟点（云端做不到）/ cli 走 CLI·API 接口 / fs 直接读本机文件。 */
const ORCHESTRATE_AGENTS = [
  { id: "local", brand: "localagent", name: "本机 Agent", source: "本机编排", tag: "AgentPilot 本机", color: "#5e8a72", sel: true,
    why: "在本机按顺序操作多个 App",
    tools: [
      { brand: "feishu", app: "飞书日历", label: "约周三 15:00 会议", via: "cli" },
      { brand: "folder", app: "本地文件", label: "翻出《方案 v3》", via: "fs" },
      { brand: "wechat", app: "微信", label: "发给张总", via: "cu" },
      { brand: "feishu", app: "飞书待办", label: "记跟进提醒", via: "cli" },
    ] },
];
const ORCHESTRATE = {
  id: "orchestrate",
  proactive: false,
  title: "一件事拆成好几步，得在几个 App 之间手动倒腾",
  solve: "还是一句话，本机 Agent 跨飞书、微信、本地文件按顺序办妥，连发微信这种本机操作都用 Computer Use 替你点。",
  hello: "你好，我是装在你电脑上的 Agent。把任务发给我，我现场跑给你看。",
  task: "周三下午帮我约一下张总，把上次那版方案从本地找出来发微信给他，再记个待办提醒我跟进",
  agents: ORCHESTRATE_AGENTS,
  parseDims: ORCHESTRATE_DIMS,
  dispatchMode: "orchestrate",
  footLabel: "判断要碰本地 App → 选中本机 Agent",
  card: {
    title: "已替你办妥 · 3 件事",
    lines: [
      "会议：周三 15:00 已约张总，飞书日历邀请已发出",
      "文件：本地找到《方案 v3》，已通过微信发给张总",
      "待办：已在飞书记下，周四上午提醒你跟进张总的反馈",
    ],
  },
  tail: "会议进了飞书日历，方案已从你本地发到张总的微信，跟进待办也记好了。",
  handoff: "报告先给你。要不要顺手把后续也办了 —— 约个会、把某份资料发给谁、再记个待办？告诉我一句就行。",
  readyHint: "约会 · 找文件 · 发消息，一句话连成一串 — 轻点看它主动出发",
};

const ALERT = {
  id: "alert",
  proactive: true,
  title: "该关注的信息散在各处，等你刷到常常慢了一步",
  solve: "本机 Agent 主动盯着你关心的方向，一命中就替你查实、整理，最后只在一个地方提醒你。",
  task: "持续盯着你关注的方向",
  /* 左 = 它替你盯着的信息源（流进）· 右 = 整理好只在一处交付（流出） */
  nodes: [
    { id: "mail", brand: "gmail", label: "邮箱", tag: "盯着", tagTone: "neutral", x: 15, y: 20 },
    { id: "x", brand: "x", label: "X / Twitter", tag: "盯着", tagTone: "neutral", x: 11, y: 45 },
    { id: "hn", brand: "hackernews", label: "Hacker News", tag: "盯着", tagTone: "neutral", x: 15, y: 70 },
    { id: "web", brand: "web", label: "公开网络", tag: "查团队 / 融资", tagTone: "blue", x: 26, y: 84 },
    { id: "feishu", brand: "feishu", label: "飞书数据库", tag: "写入", tagTone: "green", x: 85, y: 35 },
    { id: "notion", brand: "notion", label: "Notion", tag: "写入", tagTone: "green", x: 85, y: 65 },
  ],
  leadHead: "你不用一直盯着",
  leadBody: "我主动替你盯住这些方向 —— 一有动静，自己出发去查实。",
  card: {
    title: "主动预警 · 一条新产品线索",
    lines: ["Hacker News 今日热帖出现新产品，命中你的关注词", "团队：2 人，均来自头部模型团队", "融资：种子轮 $3M，本周刚官宣"],
  },
  lead: "我主动发现一条值得你关注的：",
  tail: "团队 / 产品 / 融资轮次已整理，并写入你的飞书数据库与 Notion。",
  readyHint: "信息不管落在哪，最后只在一个地方找你 — 轻点看它们共用的引擎",
};

/* Plan A — one phone, its chrome cycles through entries.
   同一台手机、同一段对话，外壳在多个入口之间轮播。 */
const ENTRY_SKINS = [
  { brand: "wechat",   name: "微信",     outBg: "#95ec69", outFg: "#0d2912", sendBg: "#07c160" },
  { brand: "slack",    name: "Slack",    outBg: "#611f69", outFg: "#ffffff", sendBg: "#007a5a" },
  { brand: "telegram", name: "Telegram", outBg: "#eeffde", outFg: "#1b2a17", sendBg: "#54a9eb" },
  { brand: "feishu",   name: "飞书",     outBg: "#4b8bff", outFg: "#ffffff", sendBg: "#2860e1" },
  { brand: "discord",  name: "Discord",  outBg: "#5865f2", outFg: "#ffffff", sendBg: "#4752c4" },
  { brand: "imessage", name: "iMessage", outBg: "#0b93f6", outFg: "#ffffff", sendBg: "#0b93f6" },
];

const SendIcon = () => <Icon name="ArrowUp" size={18} stroke={2.6} color="#fff" />;

/* chat avatar: bot (AgentPilot mark) or the active entry brand */
function PhoneAvatar({ skin, me }) {
  if (me) return <span className="wx-av me"><BrandTile brand={skin.brand} size={32} radius={8} soft={true} /></span>;
  return <span className="wx-av bot" />;
}

function MsgRow({ m, skin }) {
  if (m.type === "typing") {
    return (
      <div className="wx-row in">
        <div className="bubble in"><span className="typing-dots"><i /><i /><i /></span></div>
      </div>
    );
  }
  const me = m.type === "out";
  return (
    <div className={"wx-row " + (me ? "out" : "in")}>
      {m.type === "card" ? (
        <div className="deliver-card">
          <div className="dc-title">{m.card.title}</div>
          <div className="dc-lines">
            {m.card.lines.map((l, i) => (
              <div className="dc-line" key={i}><span className="dc-tick">✓</span><span>{l}</span></div>
            ))}
          </div>
        </div>
      ) : (
        <div className={"bubble " + (me ? "out" : "in")}
             style={me ? { background: skin.outBg, color: skin.outFg } : null}>{m.text}</div>
      )}
    </div>
  );
}

/* realistic iPhone — no status bar, no nav chrome; header is just the live entry */
function RealPhone({ skin, msgs, footer, bodyRef }) {
  return (
    <div className="iphone">
      <div className="ip-island" />
      <div className="ip-screen">
        <div className="wx-head">
          <BrandTile brand={skin.brand} size={26} radius={8} soft={false} />
          <span className="wx-app">{skin.name}</span>
        </div>
        <div className="wx-body" ref={bodyRef}>
          {msgs.map((m, i) => <MsgRow key={i} m={m} skin={skin} />)}
        </div>
        {footer && <div className="wx-input">{footer}</div>}
      </div>
    </div>
  );
}

/* sub-agents / models — identity (and color) comes from the real brand, not a side bar */
/* low-saturation accents — only used for the sweeping light + thin wires, not card fills */
/* 低饱和身份色对齐 tokens.css 的 --tier-*（本机=雾绿 / 质量=暖金 / 性价比=石板靛）。
   这些值会被拼接 alpha 十六进制后缀（如 +"3a"）作为 sweep 高光，故须保留 hex 字面量。 */
/* 多个备选 Agent —— 路由据四维权衡选中「一个」最匹配的，其余排除。
   被选中的那一个端到端独立完成：自己多次调工具 / 联网检索 / 写入。
   sel:true 即被选中；未选中的给 rejWhy（为什么不合适）。 */
/* 三个候选与第三幕「智能路由」屏（act3 CANDIDATES）保持一致：claude / localagent / rogo，
   只是这里是「调研」场景，落选理由按调研改写；选中仍是本机 Agent。 */
const AGENTS = [
  { id: "remote", brand: "claude", name: "远端旗舰", source: "云端通用", tag: "能力强", color: "#a8895c", sel: false,
    cap: "能力最强、最通用", rejWhy: "拿不到本机资料，成本偏高", tools: [] },
  { id: "local", brand: "localagent", name: "本机 Agent", source: "本机运行", tag: "够本机", color: "#5e8a72", sel: true,
    cap: "本机运行、接本地模型", why: "本机资料 + 联网查实后交付",
    tools: [{ brand: "folder", label: "本地调研文件夹", cu: true }, { brand: "web", label: "Web 搜索" }, { brand: "hackernews", label: "Hacker News" }, { brand: "notion", label: "写入 Notion" }] },
  { id: "vertical", brand: "rogo", name: "垂直 Agent", source: "云端垂直", tag: "不匹配", color: "#5b6478", sel: false,
    cap: "垂直领域专长、调用方便", rejWhy: "专业但够不到本机资料", tools: [] },
];
const HUB_SWEEP = "#5b6478";   /* 引擎核 sweep：石板靛 --accent */
const AGENT_Y = [0.2, 0.5, 0.8];
const TOOL_Y = [[0.14, 0.28], [0.43, 0.57], [0.72, 0.86]];

/* the routing dimensions AgentPilot weighs while parsing — makes the decision visible */
const PARSE_DIMS = [
  { icon: "shield", k: "隐私 · 风险", v: "含私有资料 → 敏感的留在本机" },
  { icon: "cost", k: "成本", v: "日常调研 → 优先低成本路径" },
  { icon: "quality", k: "质量", v: "团队 / 融资要查实 → 要够强、能自校验" },
  { icon: "speed", k: "难度", v: "多步调研 + 交叉验证 → 选能力够的 Agent" },
];
/* 路由维度图标 → Lucide 名（统一走 Icon 组件） */
const DIM_LUCIDE = { shield: "ShieldCheck", cost: "CircleDollarSign", quality: "Star", speed: "Gauge" };
function DimIcon({ name }) {
  return <Icon name={DIM_LUCIDE[name] || "Circle"} size={15} />;
}

/* Conveyor dispatch: the running node stays centered; finished columns slide left out of the card.
   col0 AgentPilot(解析) → col1 子 Agent → col2 工具 → col3 AgentPilot(汇总) → 手机 */
const DP_COL = { parse: 0, collapsed: 0, agents: 1, tools: 2, merge: 3, done: 3 };

/* 调用方式徽标：区分本机模拟操作 / 接口调用 / 直接读文件 —— 同一个 Agent 调度不同手段 */
function ViaBadge({ via }) {
  if (via === "cu") return <span className="seq-via cu"><Icon name="MousePointerClick" size={11} />Computer Use</span>;
  if (via === "cli") return <span className="seq-via cli">CLI · API</span>;
  return <span className="seq-via fs">本机读取</span>;
}

function DispatchStage({ phase, hubMsg, speed = 1, runToken = 0, agents = AGENTS, parseDims = PARSE_DIMS, footLabel = "据四维权衡", mode = "route" }) {
  const orchestrate = mode === "orchestrate";   // 痛点2：工具调度模式 —— 弱化路由、强化工具序列
  /* 路由是「选一个」：选中的那一个 Agent 端到端干完（自己调多个工具），其余只是被排除的备选。 */
  const sel = agents.find((a) => a.sel) || agents[0];
  const selIdx = agents.indexOf(sel);
  const multi = agents.length > 1;
  /* 「选择」是有时序的动作：进入 agents 阶段后先并列权衡（hub 向三个都连 faint 线、卡片「评估中」），
     停一拍再「拍板」——选中的高亮、其余变灰，连线只留选中那条。picked=是否已拍板。 */
  const [picked, setPicked] = useState(false);
  /* 工具调度模式的「编排感」：tools 阶段里步骤逐个执行（0→1→2→3），体现按顺序接力。
     seqActive = 当前正在执行的步骤序号；merge/done 阶段全部完成。 */
  const [seqActive, setSeqActive] = useState(-1);
  const [ref, { w }] = useSize();
  const W = (ref.current && ref.current.clientWidth) ? ref.current.clientWidth : w;
  const gap = Math.min(W * 0.52, 440);
  const active = DP_COL[phase] != null ? DP_COL[phase] : 0;
  const camX = W * 0.5 - active * gap;
  const colX = (c) => c * gap;

  const showAgents = active >= 1, showTools = active >= 2, showMerge = active >= 3;

  /* nodes are laid out by flexbox (browser handles vertical distribution reliably);
     wires are drawn to fixed layout anchors on those cards, so they do not drift. */
  const worldRef = useRef(null);
  const nodeRefs = useRef({});
  const setNode = (id, anchor = "center") => (el) => { if (el) nodeRefs.current[id] = { el, anchor }; };
  const [pts, setPts] = useState({});
  const [worldH, setWorldH] = useState(0);

  useLayoutEffect(() => {
    const localBox = (el, root) => {
      let x = 0, y = 0;
      for (let n = el; n && n !== root; n = n.offsetParent) {
        x += n.offsetLeft || 0;
        y += n.offsetTop || 0;
      }
      return { x, y, w: el.offsetWidth, h: el.offsetHeight };
    };
    const pointFor = (box, anchor) => {
      if (anchor === "left") return { x: box.x, y: box.y + box.h / 2 };
      if (anchor === "right") return { x: box.x + box.w, y: box.y + box.h / 2 };
      return { x: box.x + box.w / 2, y: box.y + box.h / 2 };
    };
    const measure = () => {
      const root = worldRef.current;
      if (!root) return;
      const next = {};
      Object.keys(nodeRefs.current).forEach((id) => {
        const item = nodeRefs.current[id];
        const el = item && item.el;
        if (!el || !el.isConnected) return;
        next[id] = pointFor(localBox(el, root), item.anchor);
      });
      setPts(next); setWorldH(root.offsetHeight);
    };
    /* 用 offset layout 坐标量锚点，避开入场/高亮 transform；
       每个阶段只定一次几何，线不会在连过去之后跟着动画沉降抖动。 */
    let raf = 0;
    measure();
    raf = requestAnimationFrame(measure);
    return () => cancelAnimationFrame(raf);
  }, [phase, W, active]);

  /* 「拍板」时序：进入 agents 阶段先权衡一拍再选中；其余阶段直接定型（tools/merge/done 已选好）。 */
  useEffect(() => {
    if (phase === "agents") {
      setPicked(false);
      const t = setTimeout(() => setPicked(true), 1100);
      return () => clearTimeout(t);
    }
    setPicked(active >= 2);   // tools 之后保持选中态；parse/collapsed 阶段未选
  }, [phase, active, runToken]);

  /* 工具调度模式：tools 阶段步骤逐个执行（接力编排）；merge/done 视为全部完成；其它阶段重置。 */
  useEffect(() => {
    if (!orchestrate) return;
    if (phase === "tools") {
      setSeqActive(0);
      const n = sel.tools.length;
      const timers = [];
      for (let k = 1; k < n; k++) timers.push(setTimeout(() => setSeqActive(k), k * 620));
      return () => timers.forEach(clearTimeout);
    }
    setSeqActive(phase === "merge" || phase === "done" ? sel.tools.length : -1);
  }, [orchestrate, phase, runToken, sel.tools.length]);

  const P = pts;
  const rays = [];
  const wireColor = "var(--accent)";
  /* hub → Agent：三条连线始终都在（不消失）。权衡时全是脉冲 faint 线；拍板后只把选中那条点亮，
     其余保持静态 faint（仍连着，只是没被选中）。 */
  if (showAgents && P.hub) {
    agents.forEach((a, i) => {
      if (!P["a" + i]) return;
      const isSel = i === selIdx;
      const lit = (picked || !multi) && isSel;     // 被选中且已拍板 → 点亮实线 + 流光
      rays.push({ k: "h" + i, a: P.hub_r || P.hub, b: P["a" + i], color: wireColor,
                  weigh: !lit, pulse: !picked && multi, flow: lit && phase === "agents" });
    });
  }
  /* 选中的那一个 Agent → 它自己的各个工具（「调用」阶段从 Agent 连入工具图标左侧） */
  if (showTools) sel.tools.forEach((t, j) => {
    const tk = "t" + selIdx + "_" + j;
    if (P["a" + selIdx] && P[tk]) {
      rays.push({ k: tk, a: P["a" + selIdx + "_r"] || P["a" + selIdx], b: P[tk], color: wireColor, flow: phase === "tools" });
    }
  });
  /* 「汇总」阶段 → 交付：工具调度模式从每张序列卡片的「右侧末端」引出汇聚；路由模式仍从工具图标引出。 */
  if (showMerge && P.merge) sel.tools.forEach((t, j) => {
    const tk = "t" + selIdx + "_" + j;
    const src = orchestrate ? (P["sr" + j] || P[tk + "_r"] || P[tk]) : (P[tk + "_r"] || P[tk]);
    if (src) rays.push({ k: "m" + selIdx + "_" + j, a: src, b: P.merge_l || P.merge, color: wireColor, flow: phase === "merge" });
  });

  return (
    <div className="dispatch" ref={ref} style={{ "--spd": speed }}>
      <div className="route-grid" />
      <div className="dp-world" ref={worldRef} style={{ transform: `translateX(${camX}px)` }}>
        <svg className="dp-wires" width={Math.max(1, colX(3) + W)} height={Math.max(1, worldH)}>
          <defs>
            {/* 彗星柔光：高斯模糊 + 原图叠合，给流动光点拖尾 */}
            <filter id="dpGlow" x="-30%" y="-30%" width="160%" height="160%">
              <feGaussianBlur stdDeviation="2.4" result="b" />
              <feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge>
            </filter>
          </defs>
          {rays.map((r, ri) => {
            const d = wirePath(r.a.x, r.a.y, r.b.x, r.b.y);
            return (
              <g key={r.k}>
                {/* ① 常驻底线：极低对比发丝线，始终在位（双层流光的底层） */}
                <path className="wire-base" d={d} />
                {/* ② 顶层点亮线：描边生长（权衡线用更细更淡的虚线；拍板前脉冲，拍板后未选中的静止留底） */}
                <path id={"dw-" + r.k} className={r.weigh ? ("wire-weigh" + (r.pulse ? " pulse" : "")) : "wire-line"} d={d} pathLength="1"
                      style={{ stroke: r.color, animationDuration: (0.6 * speed) + "s" }} />
                {/* ③ 流光统一为柔光小点，沿路径运行；不再用短线段扫动 */}
                {r.flow && (
                  <circle className="wire-dot" r="3.2" style={{ color: r.color }}>
                    <animateMotion dur={(2.6 * speed) + "s"} begin={(-0.5 * ri) + "s"} repeatCount="indefinite"
                                   keyPoints="0;1" keyTimes="0;1" calcMode="linear">
                      <mpath href={"#dw-" + r.k} />
                    </animateMotion>
                  </circle>
                )}
              </g>
            );
          })}
        </svg>

        {/* col0 — the SAME AgentPilot node: expands to analyze, then collapses & fans out */}
        <div className="dp-col center" style={{ left: colX(0) }}>
          {orchestrate ? (
            /* 工具调度模式：路由一步带过。固定尺寸卡片，只换文案 + 转圈小动画，绝不变大小。 */
            <div ref={setNode("hub")} className={"dp-hub dp-azcard dp-az-orch" + (phase === "parse" ? " dp-sweep" : "")} style={{ "--sweep": HUB_SWEEP + "2b" }}>
              <span ref={setNode("hub_r", "right")} className="dp-anchor" aria-hidden="true" />
              <div className="dp-az-head">AgentPilot · {phase === "parse" ? "智能路由" : "已选定"}</div>
              <div className="dp-az-quick">
                {phase === "parse" && <span className="azq-spin" />}
                <span className="azq-t">{phase === "parse" ? "正在分析 —— 要操作本地 App，落到本机 Agent" : "已选定 · 本机 Agent，开始编排"}</span>
              </div>
            </div>
          ) : (
            <div ref={setNode("hub")} className={"dp-hub dp-azcard" + (phase === "parse" ? " expanded dp-sweep" : " run")} style={{ "--sweep": HUB_SWEEP + "2b" }}>
              <span ref={setNode("hub_r", "right")} className="dp-anchor" aria-hidden="true" />
              <div className="dp-az-head">AgentPilot · {phase === "parse" ? "解析任务" : "已选定 · 智能路由"}</div>
              <div className="dp-az-collapse" key={"az-" + runToken}>
                <div className="dp-az-rows">
                  {parseDims.map((dm, i) => (
                    <div key={dm.k} className="dim-row" style={{ "--d": (0.1 + i * 0.4) + "s" }}>
                      <span className="dim-ic"><DimIcon name={dm.icon} /></span>
                      <span className="dim-k">{dm.k}</span>
                      <span className="dim-vwrap"><span className="dim-skel" /><span className="dim-v">{dm.v}</span></span>
                      <span className="dim-ok">✓</span>
                    </div>
                  ))}
                </div>
                <div className="dp-az-foot" style={{ "--d": (0.1 + parseDims.length * 0.4) + "s" }}>
                  <span className="azf-label">{footLabel}</span>
                  {/* 还在分析，不预告赢家，只给「下一步去比对」的指向 */}
                  <span className="azf-agents">
                    <span className="azf-pending">从候选 Agent 里挑最合适的一个<span className="azf-arrow">→</span></span>
                  </span>
                </div>
              </div>
            </div>
          )}
        </div>

        {/* col1 — 多个备选 Agent：先并列「评估中」权衡，拍板后选中的高亮、其余变灰被排除 */}
        {showAgents && (
          <div className="dp-col" style={{ left: colX(1) }}>
            {agents.map((a, i) => {
              const isSel = i === selIdx;
              const decided = picked || !multi;   // 已拍板 / 仅一个候选 → 直接定型
              const stateCls = !decided ? " eval" : (isSel ? " sel" : " rej");
              return (
                <div key={a.id}
                     ref={setNode("a" + i, "left")}
                     className={"dp-agent in" + stateCls + (isSel && decided && phase === "agents" ? " dp-sweep" : "")}
                     style={{ "--sweep": a.color + "3a" }}>
                  <span ref={setNode("a" + i + "_r", "right")} className="dp-anchor" aria-hidden="true" />
                  <div className="dpa-head">
                    <span className="dpa-ic"><BrandTile brand={a.brand} size={34} radius={10} /></span>
                    <div className="dpa-meta">
                      <div className="dpa-name">{a.name}</div>
                      <div className="dpa-summary" style={{ color: a.color }}>{a.source}</div>
                    </div>
                    <span className={"dpa-flag " + (!decided ? "eval" : (isSel ? "sel" : "rej"))}>
                      {!decided ? <><span className="dpa-evaldot" />评估中</> : (isSel ? "✓ 选中" : "✕")}
                    </span>
                  </div>
                  {/* 正文从一开始就显示各自的 profile（不再 cap→rejWhy 瞬切），拍板时只高亮/置灰，文案不跳变 */}
                  <div className="dpa-why">{isSel ? a.why : a.rejWhy}</div>
                </div>
              );
            })}
          </div>
        )}

        {/* col2 · 路由模式 — 被选中的 Agent 自己调用的多个工具（联网检索 / 读本机文件 / 写入） */}
        {showTools && !orchestrate && (
          <div className="dp-col" style={{ left: colX(2) }}>
            <div className="dp-tgroup">
              {sel.tools.map((t, j) => (
                <div key={j} className="dp-tool in">
                  <span
                    ref={(el) => {
                      setNode("t" + selIdx + "_" + j, "left")(el);
                      setNode("t" + selIdx + "_" + j + "_r", "right")(el);
                    }}
                    className="dpt-ic">
                    <BrandTile brand={t.brand} size={34} radius={10} glow={phase === "tools"} />
                  </span>
                  <span className="dpt-label">{t.label}</span>
                  {t.cu && <span className="dpt-cu">Computer Use</span>}
                  {phase === "merge" && <span className="dpt-check"><DoneTick /></span>}
                </div>
              ))}
            </div>
          </div>
        )}

        {/* col2 · 工具调度模式 — 一个 Agent 按顺序接力编排多个 App（逐步执行体现「编排」） */}
        {showTools && orchestrate && (
          <div className="dp-col" style={{ left: colX(2) }}>
            <div className={"dp-seq sa-" + Math.max(0, seqActive)}>
              <div className="dp-seq-cap"><span className="seq-cap-ic"><Icon name="Workflow" size={13} /></span>按顺序编排 {sel.tools.length} 个 App</div>
              {sel.tools.map((t, j) => {
                const st = j < seqActive ? "done" : j === seqActive ? "run" : "wait";
                return (
                  <div key={j} ref={setNode("t" + selIdx + "_" + j, "left")} className={"dp-seqstep in s-" + st} style={{ "--si": j }}>
                    <span className="seq-n">{st === "done" ? "✓" : st === "run" ? <span className="seq-spin" /> : j + 1}</span>
                    <span ref={setNode("t" + selIdx + "_" + j + "_r", "right")} className="seq-ic">
                      <BrandTile brand={t.brand} size={26} radius={8} glow={st === "run"} />
                    </span>
                    <span className="seq-act"><b className="seq-app">{t.app}</b>{t.label}</span>
                    <ViaBadge via={t.via} />
                    {/* 汇总连线锚点：卡片右侧末端中点 */}
                    <span ref={setNode("sr" + j)} className="seq-rt" aria-hidden="true" />
                  </div>
                );
              })}
            </div>
          </div>
        )}

        {/* col3 — AgentPilot re-centers to aggregate */}
        {showMerge && (
          <div className="dp-col center" style={{ left: colX(3) }}>
            <div ref={setNode("merge")} className={"dp-hub run" + (phase === "merge" ? " dp-sweep" : "")} style={{ "--sweep": HUB_SWEEP + "33" }}>
              <span ref={setNode("merge_l", "left")} className="dp-anchor" aria-hidden="true" />
              <div className="dph-title">AgentPilot</div>
              <div className="dph-sub">{phase === "done" ? "已完成 · 准备交付" : "归纳成稿 · 准备交付"}</div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function Narrative({ active, bus, config, configs }) {
  const seq = configs || [config];                     // one phone conversation can run several tasks in a row
  const [taskIdx, setTaskIdx] = useState(0);
  const cfg = seq[taskIdx];                             // the task currently on stage
  const proactive = !configs && config.proactive;
  const [skinIdx, setSkinIdx] = useState(0);
  const [stage, setStage] = useState(proactive ? "agent" : "compose");
  const [msgs, setMsgs] = useState(proactive ? [] : [{ type: "in", text: seq[0].hello }]);
  const [hub, setHub] = useState(proactive ? "working" : "idle");
  const [parsed, setParsed] = useState(proactive);     // routing network only after parse
  const [lit, setLit] = useState([]);
  const [done, setDone] = useState([]);
  const [dim, setDim] = useState([]);
  const [conv, setConv] = useState(false);             // results converge back to hub
  const [dphase, setDphase] = useState("parse");       // dispatch conveyor phase (research)
  const [hubMsg, setHubMsg] = useState("接收任务…");    // narration inside the AgentPilot hub
  const [status, setStatus] = useState(proactive ? "主动监测中…" : "待命中");
  const [detect, setDetect] = useState(false);
  const [premise, setPremise] = useState(proactive);   // proactive: lead-in beat before wires fan out
  const [inIds, setInIds] = useState([]);              // proactive: per-phase inward-flowing node ids
  const [started, setStarted] = useState(false);
  const [parseToken, setParseToken] = useState(0);     // bumps on parse entry → replays the analysis row animation
  const [speed] = useState(1);                         // DispatchStage internal slow-mo (kept = 1)
  const tl = useTimeline();
  const startedRef = useRef(false);
  const bodyRef = useRef(null), stageRef = useRef(null), fieldRef = useRef(null), flyRef = useRef(null);

  const skin = ENTRY_SKINS[skinIdx % ENTRY_SKINS.length];
  const showAgent = stage === "agent" || stage === "synth";
  const phoneReceded = stage === "recede" || stage === "agent" || stage === "synth";

  /* Plan A: cycle entry skins while composing, freeze on send */
  useEffect(() => {
    if (proactive || started || taskIdx > 0) return;   // only cycle entries before the very first send
    const id = setInterval(() => setSkinIdx((i) => i + 1), 1400);
    return () => clearInterval(id);
  }, [proactive, started, taskIdx]);

  useEffect(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [msgs, stage]);

  useEffect(() => {
    bus.setReady(false); bus.setHint(null);
    return () => tl.clear();   // alert autoplays via the scrubber; research waits for send
  }, []);

  /* imperatively drive the flying bubble (FLIP through-line) */
  function flyTo(x, y, scale, opacity, withTransition) {
    const el = flyRef.current; if (!el) return;
    el.style.transition = withTransition
      ? "left .92s var(--ease), top .92s var(--ease), transform .55s var(--ease), opacity .45s ease"
      : "none";
    el.style.left = x + "px"; el.style.top = y + "px";
    el.style.transform = `translate(-50%, -50%) scale(${scale})`;
    el.style.opacity = String(opacity);
  }

  /* ---- debug: jump to / hold any phase, and replay ---- */
  function replay() {
    tl.clear(); startedRef.current = false; setStarted(false);
    setStage("compose"); setDphase("parse"); setHub("idle"); setMsgs([{ type: "in", text: seq[0].hello }]);
    if (flyRef.current) flyRef.current.style.opacity = 0;
    setTimeout(() => runTask(), 80);
  }

  /* ---- 跨痛点逐拍（供调试 scrub 用）----
     一段对话里串了多个痛点（调研 → 编排）。下面这组按任务下标 i 定位，
     让 scrub 能贯穿「全部痛点的每一拍」，而不只是第一个。 */
  /* 任务 i 之前的对话：开场白 + 此前每个痛点完整交付 + 切到下一个痛点的衔接语 */
  function convoBefore(i) {
    const m = [{ type: "in", text: seq[0].hello }];
    for (let k = 0; k < i; k++) {
      const c = seq[k];
      m.push({ type: "out", text: c.task });
      m.push({ type: "card", card: c.card });
      m.push({ type: "in", text: c.tail });
      const nx = seq[k + 1];
      if (nx && nx.handoff) m.push({ type: "in", text: nx.handoff });   // 衔接语取自下一个痛点配置
    }
    return m;
  }
  function baseMsgsFor(i) { return [...convoBefore(i), { type: "out", text: seq[i].task }]; }
  /* 把第 i 个痛点定格在阶段 p（parse/collapsed/agents/tools/merge/return） */
  function goPhaseFor(i, p) {
    tl.clear(); startedRef.current = true; setStarted(true); setTaskIdx(i);
    if (flyRef.current) flyRef.current.style.opacity = 0;
    const c = seq[i];
    if (p === "return") {
      setStage("return"); setDphase("done");
      setMsgs([...baseMsgsFor(i), { type: "card", card: c.card }, { type: "in", text: c.tail }]);
      return;
    }
    const synth = p === "merge";
    setMsgs(baseMsgsFor(i));
    setStage(synth ? "synth" : "agent"); setHub(synth ? "synth" : "working"); setDphase(p);
    if (p === "parse") setParseToken((t) => t + 1);
  }
  /* 回到最初：第一个痛点的撰写态 */
  function resetFlow() {
    tl.clear(); startedRef.current = false; setStarted(false); setTaskIdx(0);
    if (flyRef.current) flyRef.current.style.opacity = 0;
    setStage("compose"); setDphase("parse"); setHub("idle"); setMsgs([{ type: "in", text: seq[0].hello }]);
  }

  /* ---- proactive (alert) scrubbing: jump to / hold any phase ---- */
  /* per-phase inward-flow (node -> hub) ids; everything else lit flows outward (hub -> node) */
  function resetAlert() {
    tl.clear(); startedRef.current = false; setStarted(false);
    if (flyRef.current) flyRef.current.style.opacity = 0;
    setStage("agent"); setHub("working"); setDetect(false); setConv(true); setPremise(true);
    setLit([]); setDim([]); setDone([]); setInIds([]);
    setStatus("主动监测 · 待命"); setMsgs([]);
  }
  function goAlertPhase(p) {
    tl.clear(); startedRef.current = true; setStarted(true);
    if (flyRef.current) flyRef.current.style.opacity = 0;
    setMsgs([]); setConv(true);
    if (p === "premise") {
      setPremise(true); setStage("agent"); setHub("working"); setDetect(false);
      setLit([]); setDim([]); setDone([]); setInIds([]); setStatus("主动监测 · 待命"); return;
    }
    setPremise(false);
    if (p === "monitor") {
      // 主动「出发」：信号从中心向外扫描每个信息源
      setStage("agent"); setHub("working"); setDetect(false);
      setLit(["mail", "x", "hn"]); setDim([]); setDone([]); setInIds([]);
      setStatus("主动盯着 · 邮箱 / X / Hacker News"); return;
    }
    if (p === "detect") {
      // 命中：那一条信号反向收回中心
      setStage("agent"); setHub("working"); setDetect(true);
      setLit(["hn"]); setDim(["mail", "x"]); setDone([]); setInIds(["hn"]);
      setStatus("命中关注 · Hacker News 出现新产品"); return;
    }
    if (p === "research") {
      // 自己出发去公开网络查实，结果回流
      setStage("agent"); setHub("working"); setDetect(true);
      setLit(["web"]); setDim(["mail", "x", "hn"]); setDone([]); setInIds(["web"]);
      setStatus("主动查实 · 团队与融资"); return;
    }
    const back = p === "return";
    // 交付：整理好向右写出
    setStage(back ? "return" : "synth"); setHub("synth"); setDetect(true); setConv(true);
    setLit(["feishu", "notion"]); setDim(["mail", "x", "hn", "web"]); setDone(["feishu", "notion"]);
    setInIds([]); setStatus("写入飞书 / Notion · 只在一处提醒你");
    if (back) setMsgs([{ type: "in", text: cfg.lead }, { type: "card", card: cfg.card }, { type: "in", text: cfg.tail }]);
  }

  const scrub = useScrub(proactive ? {
    autoplay: true,
    duration: 13200,
    reset: () => { resetAlert(); bus.setReady(false); bus.setHint(null); },
    frames: [
      { t: 0, label: "开场", apply: () => goAlertPhase("premise") },
      { t: 2400, label: "出发", apply: () => goAlertPhase("monitor") },
      { t: 5000, label: "命中", apply: () => goAlertPhase("detect") },
      { t: 7200, label: "查实", apply: () => goAlertPhase("research") },
      { t: 9400, label: "写入", apply: () => goAlertPhase("synth") },
      { t: 11800, label: "回信", apply: () => goAlertPhase("return") },
      { t: 12500, label: "完成", apply: () => { bus.setReady(true); bus.setHint(cfg.readyHint); } },
    ],
  } : (() => {
    /* 帧表覆盖「全部痛点」的每一拍：每个痛点都走 解析→收起→分配→调用→汇总→交付，
       每一拍都是停顿点(stop)，调试面板可逐拍前后步进 / 跳转 / 拖拽。 */
    const PH = [
      { p: "parse",     label: "解析", dt: 2200 },
      { p: "collapsed", label: "收起", dt: 700 },
      { p: "agents",    label: "分配", dt: 1900 },
      { p: "tools",     label: "调用", dt: 2600 },
      { p: "merge",     label: "汇总", dt: 1500 },
      { p: "return",    label: "交付", dt: 1500 },
    ];
    const multi = seq.length > 1;
    const frames = [];
    let t = 800;
    seq.forEach((c, i) => {
      PH.forEach((ph) => {
        const isFinal = i === seq.length - 1 && ph.p === "return";
        frames.push({
          t, stop: true,
          label: (multi ? (i + 1) + "·" : "") + ph.label,
          apply: () => { goPhaseFor(i, ph.p); if (isFinal) { bus.setReady(true); bus.setHint(c.readyHint); } },
        });
        t += ph.dt;
      });
    });
    return {
      autoplay: false,
      reset: () => { resetFlow(); bus.setReady(false); bus.setHint(null); },
      frames,
    };
  })());

  function runTask() {
    if (startedRef.current) return;
    startedRef.current = true;
    setStarted(true);
    const c = cfg;                                   // freeze the task being sent this click
    const last = taskIdx >= seq.length - 1;
    setMsgs((m) => [...m, { type: "out", text: c.task }]);

    /* lift the just-sent bubble out and arc it toward the hub */
    requestAnimationFrame(() => {
      const s = stageRef.current.getBoundingClientRect();
      const sentEl = bodyRef.current && bodyRef.current.querySelector(".wx-row.out:last-child .bubble");
      const aRect = (sentEl || fieldRef.current).getBoundingClientRect();
      flyTo(aRect.left - s.left + aRect.width / 2, aRect.top - s.top + aRect.height / 2, 1, 1, false);
      requestAnimationFrame(() => {
        setStage("recede");
        flyRef.current.classList.add("flying");
        flyTo(s.width * 0.5, s.height * 0.52, 1, 1, true);
      });
    });

    /* 1) bubble absorbed; AgentPilot centered, narrating its analysis */
    tl.at(1000, () => {
      const s = stageRef.current.getBoundingClientRect();
      flyTo(s.width * 0.5, s.height * 0.5, 0.16, 0, true);
      setStage("agent"); setHub("working"); setDphase("parse"); setParseToken((t) => t + 1);
      setStatus("本机 AgentPilot · 解析中");
      if (flyRef.current) flyRef.current.classList.remove("flying");
    });
    /* rows analyze one by one (~2s), then the card collapses (centered), then it fans out */
    tl.at(3100, () => { setDphase("collapsed"); setStatus("已按 隐私 / 成本 / 质量 / 速度 等维度拆解"); });
    tl.at(3800, () => { setDphase("agents"); setStatus("分配给各 Agent 执行"); });
    tl.at(5600, () => { setDphase("tools"); setStatus("各 Agent 调用工具执行"); });
    tl.at(7800, () => { setDphase("merge"); setHub("synth"); setStage("synth"); setStatus("结果汇总回 AgentPilot · 整理梳理"); });
    tl.at(9000, () => setDphase("done"));
    /* back to chat — the agent just replies; make sure the flying bubble is fully gone */
    tl.at(9600, () => {
      setStage("return"); setMsgs((m) => [...m, { type: "typing" }]);
      if (flyRef.current) { flyRef.current.classList.remove("flying"); flyRef.current.style.opacity = 0; }
    });
    tl.at(10400, () => setMsgs((m) => m.filter((x) => x.type !== "typing").concat([{ type: "card", card: c.card }, { type: "in", text: c.tail }])));
    if (last) {
      tl.at(10900, () => { bus.setReady(true); bus.setHint(c.readyHint); });
    } else {
      /* hand off to the next task in the SAME conversation: pre-fill its prompt, re-arm the send button */
      const nextC = seq[taskIdx + 1];
      tl.at(11100, () => {
        startedRef.current = false; setStarted(false);
        setTaskIdx((i) => i + 1);
        setStage("return"); setDphase("parse"); setHub("idle"); setStatus("待命中");
        if (nextC && nextC.handoff) setMsgs((m) => [...m, { type: "in", text: nextC.handoff }]);
      });
    }
  }

  function runAlert() {
    if (started) return;
    setStarted(true);
    setHub("working"); setLit(["mail", "x", "hn"]); setStatus("主动监测中 · 邮箱 / X / Hacker News");
    tl.at(1700, () => { setDetect(true); setDim(["mail", "x"]); setLit(["hn"]); setStatus("命中关注 · Hacker News 出现新产品"); });
    tl.at(3000, () => { setDim([]); setLit(["hn", "web"]); setStatus("主动启动调研 · 查团队与融资"); });
    tl.at(4200, () => { setHub("synth"); setStage("synth"); setConv(true); setDone(["feishu", "notion"]); setLit(["hn", "web", "feishu", "notion"]); setStatus("写入飞书 / Notion"); });
    tl.at(5300, () => { setStage("return"); setMsgs((m) => [...m, { type: "typing" }]); });
    tl.at(6000, () => setMsgs((m) => m.filter((x) => x.type !== "typing").concat([{ type: "in", text: cfg.lead }, { type: "card", card: cfg.card }, { type: "in", text: cfg.tail }])));
    tl.at(6500, () => { bus.setReady(true); bus.setHint(cfg.readyHint); });
  }

  const sendFooter = proactive ? null : (
    <>
      <div className="wx-inbar">
        {/* 最后一个痛点发出后就清空输入框 —— 整段对话已结束 */}
        <div className={"wx-in-field" + (started && taskIdx === seq.length - 1 ? " empty" : "")} ref={fieldRef}>
          {started && taskIdx === seq.length - 1 ? <span className="wx-in-ph">发消息…</span> : cfg.task}
        </div>
        <button className={"wx-send no-advance" + (started ? "" : " cue")}
                style={{ background: skin.sendBg }} onClick={runTask} disabled={started} aria-label="发送">
          <SendIcon />
        </button>
      </div>
      {!started && taskIdx === 0 && (
        <div className="send-guide no-advance" onClick={runTask}>
          <span className="sg-label">点这里发送，亲手跑一遍</span>
          <svg className="sg-arrow" viewBox="0 0 66 50" fill="none" aria-hidden="true">
            <path className="sg-line" d="M5 7 C 33 1, 56 13, 56 37" pathLength="1" />
            <path className="sg-tip" d="M47 29 L57 40 L65 29" pathLength="1" />
          </svg>
        </div>
      )}
    </>
  );

  return (
    <div className="nar">
      {/* 同一段对话里换任务（调研 → 编排）时，手机与消息延续不变，仅顶部文案做柔和「替换」：
          key 绑定 taskIdx，切任务时整组标题淡出旧、淡入新（与 handoff 消息同拍）。 */}
      <div className="nar-head" key={"h" + taskIdx}>
        <h2 className="h-title nar-swap">{cfg.title}</h2>
        {cfg.solve && <p className="nar-sub nar-swap">{cfg.solve}</p>}
      </div>

      <div className="nar-stage" ref={stageRef}>
        {/* phone layer — compose & return */}
        <div className={"phone-layer" + (phoneReceded ? " receded" : "")}>
          <RealPhone skin={skin} msgs={msgs} footer={sendFooter} bodyRef={bodyRef} />
          {stage === "compose" && (
            <div className="entry-strip">
              <span>同一段对话，换个入口也一样</span>
              <span className="es-dots">
                {ENTRY_SKINS.map((sk, i) => (
                  <span key={sk.brand} className={"es-dot" + (i === skinIdx % ENTRY_SKINS.length ? " on" : "")}>
                    <BrandTile brand={sk.brand} size={18} radius={5} soft={false} />
                  </span>
                ))}
              </span>
            </div>
          )}
        </div>

        {/* flying bubble — the through-line element across the transition */}
        <div className="fly-bubble" ref={flyRef}>
          <div className="fb-inner">
            <span className="fb-pill" style={{ background: skin.outBg, color: skin.outFg }}>{cfg.task}</span>
          </div>
        </div>

        {/* agent layer — parse, then dispatch / fan-out / converge */}
        <div className={"agent-layer" + (showAgent ? " on" : "")}>
          <div className="agent-top">
            <span className="agent-badge">本机 AgentPilot</span>
            <span className="agent-status">{status}</span>
          </div>
          <div className="agent-stage">
            {proactive ? (
              <RoutingStage
                hub={{ label: "本机 Agent", sub: "主动监测" }}
                hubPos={{ x: 50, y: 50 }}
                nodes={premise ? [] : (parsed ? cfg.nodes : [])}
                lit={lit} done={done} dim={dim}
                inIds={inIds}
                sideLabels={null}
                running={hub !== "idle"} hubState={hub}
                showHubRings={false}
              >
                {premise && (
                  <div className="proactive-lead">
                    <span className="pl-head fade-up">{cfg.leadHead}</span>
                    <span className="pl-body fade-up">{cfg.leadBody}</span>
                  </div>
                )}
                {detect && !premise && <div className="detect-callout fade-up">⚡ 命中你的关注词</div>}
              </RoutingStage>
            ) : (
              <DispatchStage phase={dphase} hubMsg={hubMsg} speed={speed} runToken={parseToken}
                agents={cfg.agents} parseDims={cfg.parseDims} footLabel={cfg.footLabel} mode={cfg.dispatchMode} />
            )}
          </div>
        </div>

        {/* debug panel — scrub the routing animation across phases */}
        <Scrubber ctl={scrub} label={proactive ? "主动预警动画" : "调研路由动画"} />
      </div>
    </div>
  );
}

function Act1Flow(props) { return <Narrative {...props} configs={[RESEARCH, ORCHESTRATE]} />; }
function Act1Alert(props) { return <Narrative {...props} config={ALERT} />; }

/* ---- 三种「活」的形态各异的微路由图（同一台引擎，不同角色） ---- */
function MiniRouteDiagram({ kind }) {
  // 产品引擎标记：这一页讲 AgentPilot 这台引擎本身，不用本机 Agent 设备图标
  const Hub = ({ x, y }) => (
    <g className="mr-hub-g" transform={`translate(${x} ${y})`}>
      <rect x="-17" y="-17" width="34" height="34" rx="11" className="mr-hub-box" />
      <circle r="4.5" className="mr-hub-dot" />
    </g>
  );
  if (kind === "fanout") {
    // 一个源 → 在多个候选 Agent 中只选中一个（中间那路高亮，另两路置灰）
    const tx = 208, ys = [26, 58, 90], sel = 1;
    return (
      <svg className="mr-svg" viewBox="0 0 240 116" preserveAspectRatio="xMidYMid meet">
        {ys.map((y, i) => (
          <path key={i} className={"mr-edge" + (i === sel ? " is-sel" : " is-dim")}
            d={`M49 58 C110 58 150 ${y} ${tx - 9} ${y}`} style={{ "--d": `${0.05 * i}s` }} />
        ))}
        {ys.map((y, i) => (
          <circle key={i} cx={tx} cy={y} r="7" className={"mr-node" + (i === sel ? " is-sel" : " is-dim")} />
        ))}
        <Hub x={32} y={58} />
      </svg>
    );
  }
  if (kind === "converge") {
    // 多源信号 → 反向收敛到一个判断
    const sx = 32, ys = [26, 58, 90];
    return (
      <svg className="mr-svg" viewBox="0 0 240 116" preserveAspectRatio="xMidYMid meet">
        {ys.map((y, i) => (
          <path key={i} className="mr-edge" d={`M${sx + 9} ${y} C90 ${y} 130 58 191 58`} style={{ "--d": `${0.05 * i}s` }} />
        ))}
        {ys.map((y, i) => <circle key={i} cx={sx} cy={y} r="7" className="mr-node" />)}
        <Hub x={208} y={58} />
      </svg>
    );
  }
  // chain — 跨工具按序接力，Agent 居中调度
  const xs = [36, 120, 204];
  return (
    <svg className="mr-svg" viewBox="0 0 240 116" preserveAspectRatio="xMidYMid meet">
      <path className="mr-edge" d={`M${xs[0] + 18} 58 L${xs[1] - 18} 58`} style={{ "--d": "0s" }} />
      <path className="mr-edge" d={`M${xs[1] + 18} 58 L${xs[2] - 18} 58`} style={{ "--d": "0.08s" }} />
      <rect x={xs[0] - 16} y={42} width="32" height="32" rx="10" className="mr-tool" />
      <rect x={xs[2] - 16} y={42} width="32" height="32" rx="10" className="mr-tool" />
      <Hub x={xs[1]} y={58} />
    </svg>
  );
}

/* ---- 收口屏：三图叠合点破引擎（自动播放） ---- */
function Act1Engine({ active, bus }) {
  const [stacked, setStacked] = useState(false);    // 三图收拢、叠成一摞
  const [revealed, setRevealed] = useState(false);  // 这摞化开 → 浮出引擎
  const scrub = useScrub({
    duration: 3600,
    reset: () => { setStacked(false); setRevealed(false); bus.setReady(false); bus.setHint(null); },
    frames: [
      { t: 900,  label: "叠合", apply: () => { setStacked(true); setRevealed(false); } },
      { t: 2100, label: "化作引擎", apply: () => { setStacked(true); setRevealed(true); } },
      { t: 3000, label: "完成", apply: () => { bus.setReady(true); bus.setHint("同一台引擎 — 轻点看看这是谁做的"); } },
    ],
  });
  // 顺序与投资人刚刚走过的三个痛点一致：智能路由 → 工具编排 → 主动预警
  // 三张图都是「同一个被选中的 Agent 自己多次调用」的不同形态，不是把任务拆给多个 Agent
  const cards = [
    { name: "智能路由", tag: "选中一个 Agent，多路查证", cls: "c1", kind: "fanout" },
    { name: "工具编排", tag: "一个 Agent 跨应用接力", cls: "c2", kind: "chain" },
    { name: "主动预警", tag: "一个 Agent 盯多源，收敛成判断", cls: "c3", kind: "converge" },
  ];
  return (
    <div className="engine">
      <Scrubber ctl={scrub} label="收口 · 三图叠合" />
      <div className="engine-head">
        <h2 className="h-title">不一样的活，背后是同一台引擎</h2>
      </div>
      <div className={"engine-stage" + (stacked ? " stacked" : "") + (revealed ? " revealed" : "")}>
        {cards.map((c, i) => (
          <div key={i} className={"mini-route " + c.cls}>
            <div className="mr-head">
              <span className="mr-name">{c.name}</span>
              <span className="mr-tag">{c.tag}</span>
            </div>
            <MiniRouteDiagram kind={c.kind} />
          </div>
        ))}
        <div className={"engine-core" + (revealed ? " on" : "")}>
          <div className="ec-flow">
            <div className="ec-step">
              <span className="ec-eye">入口</span>
              <span className="ec-title">你顺手的地方</span>
              <span className="ec-cap">微信 / 飞书 / 邮件…</span>
            </div>
            <span className="ec-arrow" />
            <div className="ec-step hub">
              <span className="ec-eye">路由</span>
              <span className="ec-title">选中一个 Agent</span>
              <span className="ec-cap">判断交给谁 · 走哪 · 什么成本</span>
            </div>
            <span className="ec-arrow" />
            <div className="ec-step">
              <span className="ec-eye">执行</span>
              <span className="ec-title">够到文件与工具</span>
              <span className="ec-cap">按授权，在本机完成</span>
            </div>
            <span className="ec-arrow" />
            <div className="ec-step">
              <span className="ec-eye">交付</span>
              <span className="ec-title">结果 · 沉淀分流</span>
              <span className="ec-cap">交付并归档回流</span>
            </div>
          </div>
        </div>
      </div>
      <div className="engine-foot">
        {revealed && (
          <p className="engine-line fade-up">
            入口随你习惯，智能路由判断这件事该<b>交给哪个 Agent、走哪、什么成本</b>，
            再由<b>被选中的那一个 Agent</b> 端到端干完，最后交付一个符合预期的结果。<b>这就是智能路由，交付的是结果。</b>
          </p>
        )}
      </div>
    </div>
  );
}

window.AP_SCREENS.push(
  { id: "act1-flow", chapter: "体验", Comp: Act1Flow },
  { id: "act1-alert", chapter: "体验", Comp: Act1Alert },
  { id: "act1-engine", chapter: "体验", Comp: Act1Engine },
);
