// interest-modal.jsx — Two-stage interest picker.
// Stage 1: pick up to 3 tiles from domain_taxonomy (depth 1 + top depth-2 by node_count).
// Stage 2: optional — per pick, narrow to ≤3 Layer-3 sub-paths.
// On confirm, emits picks: [{ slot:0|1|2, tile_id, paths:string[], label_en, label_zh }].
//
// Slot is assigned by selection order (first selected → slot 0, etc.) and maps to a
// color in the COLOR_SLOTS palette (Linear Purple / Mint Teal / Amber Mist).

const { useEffect: useEffectIM, useState: useStateIM, useMemo: useMemoIM } = React;

const COLOR_SLOTS = [
  { name: "Linear Purple", swatch: "linear-gradient(135deg,#5E6AD2,#7C8CFF)" },
  { name: "Mint Teal",     swatch: "linear-gradient(135deg,#2FAE8F,#6BC9B1)" },
  { name: "Amber Mist",    swatch: "linear-gradient(135deg,#C68A28,#F2B45A)" },
];

const MAX_PICKS = 3;
const MAX_REFINE_PATHS = 3;

// domain_taxonomy.label_en/zh are sub-topic-flavored descriptions (e.g. "Corporate Finance
// and Accounting Theory" for social_sciences.management) rather than canonical domain
// names. Override with a curated map so the modal shows what users expect.
const CANON_LABELS = {
  "social_sciences.management":              { en: "Management",             zh: "管理学" },
  "social_sciences.law":                     { en: "Law",                    zh: "法学" },
  "social_sciences.economics":               { en: "Economics",              zh: "经济学" },
  "social_sciences.political_science":       { en: "Political Science",      zh: "政治学" },
  "social_sciences.sociology":               { en: "Sociology",              zh: "社会学" },
  "social_sciences.psychology":              { en: "Psychology",             zh: "心理学" },
  "social_sciences.history":                 { en: "History (Social)",       zh: "历史 (社科)" },
  "social_sciences.policy":                  { en: "Public Policy",          zh: "公共政策" },
  "social_sciences.social_work":             { en: "Social Work",            zh: "社会工作" },
  "physical_sciences.computer_science":      { en: "Computer Science",       zh: "计算机科学" },
  "physical_sciences.mathematics":           { en: "Mathematics",            zh: "数学" },
  "physical_sciences.physics":               { en: "Physics",                zh: "物理学" },
  "physical_sciences.chemistry":             { en: "Chemistry",              zh: "化学" },
  "physical_sciences.statistics":            { en: "Statistics",             zh: "统计学" },
  "physical_sciences.electrical_engineering":{ en: "Electrical Engineering", zh: "电气工程" },
  "physical_sciences.mechanical_engineering":{ en: "Mechanical Engineering", zh: "机械工程" },
  "physical_sciences.civil_engineering":     { en: "Civil Engineering",      zh: "土木工程" },
  "physical_sciences.chemical_engineering":  { en: "Chemical Engineering",   zh: "化学工程" },
  "physical_sciences.materials_science":     { en: "Materials Science",      zh: "材料科学" },
  "physical_sciences.industrial_engineering":{ en: "Industrial Engineering", zh: "工业工程" },
  "physical_sciences.earth_science":         { en: "Earth Science",          zh: "地球科学" },
  "physical_sciences.engineering":           { en: "Engineering",            zh: "工程学" },
  "humanities.philosophy":                   { en: "Philosophy",             zh: "哲学" },
  "humanities.literature":                   { en: "Literature",             zh: "文学" },
  "humanities.literary_studies":             { en: "Literary Studies",       zh: "文学研究" },
  "humanities.linguistics":                  { en: "Linguistics",            zh: "语言学" },
  "humanities.history":                      { en: "History (Humanities)",   zh: "历史 (人文)" },
  "humanities.chinese_studies":              { en: "Chinese Studies",        zh: "汉学" },
  "humanities.art_history":                  { en: "Art History",            zh: "艺术史" },
  "life_sciences.biology":                   { en: "Biology",                zh: "生物学" },
  "life_sciences.horticulture":              { en: "Horticulture",           zh: "园艺学" },
  "health_sciences.anesthesiology":          { en: "Anesthesiology",         zh: "麻醉学" },
};

// Humanize an underscore_separated_path segment → Title Case English.
function humanize(s) {
  if (!s) return "";
  return s.split("_").map(w => w ? w[0].toUpperCase() + w.slice(1) : "").join(" ");
}

// Resolve a path to canonical { en, zh }. For paths not in the map, falls back to
// humanizing the last segment (used by Stage-2 layer-3 sub-paths and any unmapped tiles).
function cleanLabel(path) {
  if (path && CANON_LABELS[path]) return CANON_LABELS[path];
  const seg = (path || "").split(".").pop() || "";
  const en = humanize(seg);
  return { en, zh: en };  // no curated zh for unknown paths
}

// Expose for graph.jsx (domain panel chips reuse the same canonicalization).
window.kgCleanLabel = cleanLabel;

function tileDisplayName(t) {
  const cl = cleanLabel(t.path);
  // For curated layer-2 tiles, prefer Chinese (we have one); else fall back to English.
  return cl.zh || cl.en || (t.path || "");
}

// Single tile button (used by Stage-1 grid and AI suggestions). No "Linear/Mint/Amber"
// text on selected tiles per user request — just the colored swatch is enough.
function renderTile(t, selectedIds, toggleTile) {
  const id = t.path;
  const idx = selectedIds.indexOf(id);
  const on = idx >= 0;
  const slotSwatch = on ? COLOR_SLOTS[idx].swatch : null;
  const atCap = !on && selectedIds.length >= MAX_PICKS;
  return (
    <button
      key={id}
      className={"onboard-chip" + (on ? " on" : "")}
      onClick={() => toggleTile(t)}
      title={t.path}
      disabled={atCap}
      style={{ opacity: atCap ? 0.45 : 1, cursor: atCap ? "not-allowed" : "pointer" }}
    >
      <span
        className="swatch"
        style={{
          background: slotSwatch || "var(--text-faint)",
          width: on ? 12 : 8,
          height: on ? 12 : 8,
          borderRadius: on ? 3 : 999,
        }}
      />
      <span style={{ flex: 1 }}>{tileDisplayName(t)}</span>
      <span
        style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--text-faint)" }}
        title={`${t.node_count} nodes`}
      >
        {t.node_count}
      </span>
    </button>
  );
}

function InterestModal({ initialPicks, onConfirm, onCancel }) {
  // Tiles loaded from fetchInterestOptions
  const [tiles, setTiles]   = useStateIM(null);   // null = loading
  const [tilesErr, setTilesErr] = useStateIM("");

  // Stage: 1 = pick tiles, 2 = refine
  const [stage, setStage] = useStateIM(1);

  // Selected tile_ids in selection order (max 3). Slot = index.
  const [selectedIds, setSelectedIds] = useStateIM(() =>
    (initialPicks || []).slice(0, MAX_PICKS).map(p => p.tile_id)
  );

  // Per-tile_id: refined sub-path ids (max 3). If empty, the tile path is used.
  const [refineMap, setRefineMap] = useStateIM(() => {
    const m = {};
    (initialPicks || []).forEach(p => {
      if (p.paths && p.paths.length === 1 && p.paths[0] === p.tile_id) {
        m[p.tile_id] = [];
      } else if (p.paths) {
        m[p.tile_id] = [...p.paths];
      }
    });
    return m;
  });

  // Subcategory cache per tile_id (lazy fetched on Stage 2 entry).
  const [subcatMap, setSubcatMap] = useStateIM({});       // tile_id → array | "loading" | "error"

  // Stage-1 search state. query drives substring filter; if empty filter result,
  // searchByVector is called as a fallback and the top-3 layer-2 paths are surfaced.
  const [query, setQuery] = useStateIM("");
  const [llmSuggestions, setLlmSuggestions] = useStateIM(null);   // null=idle | array
  const [llmLoading, setLlmLoading] = useStateIM(false);

  // pickQueries[tile_id] = the search query active when the user added that pick.
  // Stage 2 uses this to re-rank that pick's layer-3 children by relevance.
  const [pickQueries, setPickQueries] = useStateIM(() => {
    const m = {};
    (initialPicks || []).forEach(p => { if (p.query) m[p.tile_id] = p.query; });
    return m;
  });

  // rankingMap[tile_id] = Map<subPath, hitCount> | "loading" | null
  const [rankingMap, setRankingMap] = useStateIM({});

  // Load tile list once.
  useEffectIM(() => {
    let cancelled = false;
    setTiles(null); setTilesErr("");
    window.graphRepo.fetchInterestOptions(30, 50)
      .then(arr => { if (!cancelled) setTiles(arr); })
      .catch(e => { if (!cancelled) setTilesErr(e.message || String(e)); });
    return () => { cancelled = true; };
  }, []);

  // Substring filter (English + Chinese + path; case-insensitive).
  const filteredTiles = useMemoIM(() => {
    if (!tiles) return null;
    const q = query.trim().toLowerCase();
    if (!q) return tiles;
    return tiles.filter(t => {
      const cl = cleanLabel(t.path);
      return (
        (cl.en || "").toLowerCase().includes(q) ||
        (cl.zh || "").includes(query.trim()) ||
        (t.path || "").toLowerCase().includes(q)
      );
    });
  }, [tiles, query]);

  // LLM fallback: when query has no direct substring match (and is ≥2 chars), embed it
  // and find nearest public_nodes via match_public_nodes; aggregate their domain_path
  // to layer-2 and surface the top 3 that exist as tiles. Debounced 500ms.
  useEffectIM(() => {
    setLlmSuggestions(null);
    setLlmLoading(false);
    const q = query.trim();
    if (!q || q.length < 2 || !tiles) return;
    if (!filteredTiles || filteredTiles.length > 0) return; // direct match present
    let cancelled = false;
    setLlmLoading(true);
    const tilesByPath = new Map(tiles.map(t => [t.path, t]));
    const h = setTimeout(async () => {
      try {
        const hits = await window.graphRepo.searchByVector(q, 25, 0.25);
        if (cancelled) return;
        const counts = new Map();
        for (const r of hits) {
          const parts = (r.domain_path || "").split(".");
          if (parts.length < 2) continue;
          const l2 = parts.slice(0, 2).join(".");
          if (!tilesByPath.has(l2)) continue;
          counts.set(l2, (counts.get(l2) || 0) + 1);
        }
        const top = Array.from(counts.entries())
          .sort((a, b) => b[1] - a[1])
          .slice(0, 3)
          .map(([p]) => tilesByPath.get(p));
        setLlmSuggestions(top);
      } catch (e) {
        if (!cancelled) {
          console.warn("[InterestModal] vector search failed", e);
          setLlmSuggestions([]);
        }
      } finally {
        if (!cancelled) setLlmLoading(false);
      }
    }, 500);
    return () => { cancelled = true; clearTimeout(h); };
  }, [query, filteredTiles, tiles]);

  function toggleTile(tile) {
    const id = tile.path;
    setSelectedIds(prev => {
      if (prev.includes(id)) return prev.filter(x => x !== id);
      if (prev.length >= MAX_PICKS) return prev;  // cap at 3
      return [...prev, id];
    });
    // If a tile is being added DURING an active search, record the query against
    // it so Stage 2 can re-rank its children by relevance to that query.
    if (!selectedIds.includes(id) && query.trim()) {
      const q = query.trim();
      setPickQueries(prev => ({ ...prev, [id]: q }));
    }
    if (selectedIds.includes(id)) {
      // Removing — clean up associated state
      setPickQueries(prev => {
        const next = { ...prev }; delete next[id]; return next;
      });
      setRankingMap(prev => {
        const next = { ...prev }; delete next[id]; return next;
      });
      setRefineMap(prev => {
        const next = { ...prev }; delete next[id]; return next;
      });
    }
  }

  function removePick(id) {
    setSelectedIds(prev => prev.filter(x => x !== id));
    setPickQueries(prev => { const n = { ...prev }; delete n[id]; return n; });
    setRankingMap(prev => { const n = { ...prev }; delete n[id]; return n; });
    setRefineMap(prev => { const n = { ...prev }; delete n[id]; return n; });
  }

  function clearAllPicks() {
    setSelectedIds([]);
    setPickQueries({});
    setRankingMap({});
    setRefineMap({});
  }

  // Note: reads from latest subcatMap state via setSubcatMap callback so we never
  // re-fetch a path that already has a result/loading entry.
  function ensureSubcats(tilePath) {
    setSubcatMap(prev => {
      const cur = prev[tilePath];
      if (Array.isArray(cur) || cur === "loading") return prev;  // already in-flight or done
      console.info("[InterestModal] fetching subcats for", tilePath);
      window.graphRepo.fetchSubcategories(tilePath)
        .then(arr => {
          console.info("[InterestModal] subcats for", tilePath, "→", arr.length, "items");
          setSubcatMap(p2 => ({ ...p2, [tilePath]: arr }));
        })
        .catch(e => {
          console.warn("[InterestModal] subcat load failed", tilePath, e);
          setSubcatMap(p2 => ({ ...p2, [tilePath]: "error" }));
        });
      return { ...prev, [tilePath]: "loading" };
    });
  }

  // Fire subcat fetch on every Stage-2 entry — covers the case where setStage(2) and
  // the forEach in goNext() didn't batch the way we expected.
  useEffectIM(() => {
    if (stage !== 2) return;
    selectedIds.forEach(id => ensureSubcats(id));
  }, [stage, selectedIds]);

  // For each pick with an associated search query, fetch a relevance ranking of its
  // layer-3 children: embed the query → match_public_nodes → keep hits whose
  // domain_path starts with the pick's path → aggregate by next layer → counts.
  function ensureRanking(tilePath, q) {
    setRankingMap(prev => {
      const cur = prev[tilePath];
      if (cur instanceof Map || cur === "loading") return prev;
      console.info("[InterestModal] ranking children of", tilePath, "by query:", q);
      window.graphRepo.searchByVector(q, 60, 0.2)
        .then(hits => {
          const parts = tilePath.split(".");
          const childDepth = parts.length + 1;
          const counts = new Map();
          for (const h of hits) {
            const pp = (h.domain_path || "").split(".");
            if (pp.length < childDepth) continue;
            let ok = true;
            for (let i = 0; i < parts.length; i++) {
              if (pp[i] !== parts[i]) { ok = false; break; }
            }
            if (!ok) continue;
            const child = pp.slice(0, childDepth).join(".");
            counts.set(child, (counts.get(child) || 0) + 1);
          }
          console.info("[InterestModal] ranking for", tilePath, "→", counts.size, "buckets");
          setRankingMap(p2 => ({ ...p2, [tilePath]: counts }));
        })
        .catch(e => {
          console.warn("[InterestModal] ranking failed", tilePath, e);
          setRankingMap(p2 => ({ ...p2, [tilePath]: null }));
        });
      return { ...prev, [tilePath]: "loading" };
    });
  }

  useEffectIM(() => {
    if (stage !== 2) return;
    for (const id of selectedIds) {
      const q = pickQueries[id];
      if (q) ensureRanking(id, q);
    }
  }, [stage, selectedIds, pickQueries]);

  function toggleRefine(tileId, subPath) {
    setRefineMap(prev => {
      const cur = prev[tileId] || [];
      if (cur.includes(subPath)) {
        return { ...prev, [tileId]: cur.filter(x => x !== subPath) };
      }
      if (cur.length >= MAX_REFINE_PATHS) return prev;
      return { ...prev, [tileId]: [...cur, subPath] };
    });
  }

  function goNext() {
    if (selectedIds.length === 0) return;
    console.info("[InterestModal] goNext → stage 2 with picks:", selectedIds);
    setStage(2);
  }

  function confirm() {
    if (!tiles) return;
    const picks = selectedIds.map((tileId, slot) => {
      const cl = cleanLabel(tileId);
      const refined = (refineMap[tileId] || []).slice(0, MAX_REFINE_PATHS);
      return {
        slot,
        tile_id: tileId,
        paths: refined.length ? refined : [tileId],
        label_en: cl.en,
        label_zh: cl.zh,
        query: pickQueries[tileId] || null,  // remember origin search for future re-rank
      };
    });
    onConfirm(picks);
  }

  const canNext    = selectedIds.length > 0;
  const canConfirm = selectedIds.length > 0;

  // ── Render ────────────────────────────────────────────────────────────────
  return (
    <div className="onboard-overlay">
      <div className="onboard-card" style={{ width: 660 }}>
        {/* Header */}
        <div className="onboard-head">
          <span className="eyebrow">
            {stage === 1 ? "STAGE 1 · INTERESTS" : "STAGE 2 · REFINE"}
          </span>
          <h2 className="onboard-title">
            {stage === 1 ? "选择你的领域" : "可选 · 收窄子方向"}
          </h2>
          <p className="onboard-sub">
            {stage === 1
              ? <>从下面挑 <strong>最多 3 个</strong> 领域。我们用它们从 187k 公共节点里挑出对应子图。每个领域会自动分配一个配色槽位。</>
              : <>每个选中的领域可选 <strong>至多 3 个 Layer-3 子方向</strong> 进一步收窄(可跳过)。</>}
          </p>
        </div>

        {/* Stage 1 — Search + Tile grid */}
        {stage === 1 && (
          <>
            {/* Picks row — always visible so user can drop old selections */}
            {selectedIds.length > 0 && (
              <div className="picks-row">
                <span className="picks-row-label">已选</span>
                {selectedIds.map((id, slot) => {
                  const cl = cleanLabel(id);
                  return (
                    <span key={id} className="pick-pill" title={id}>
                      <span className="swatch" style={{ background: COLOR_SLOTS[slot].swatch }} />
                      {cl.zh || cl.en}
                      {pickQueries[id] && (
                        <span className="pick-q-tag" title={`来自搜索 "${pickQueries[id]}"`}>✨</span>
                      )}
                      <button onClick={() => removePick(id)} title="移除">×</button>
                    </span>
                  );
                })}
                <button className="clear-all-btn" onClick={clearAllPicks}>清空全部</button>
              </div>
            )}

            {/* Search bar */}
            <div className="onboard-search">
              <Ic.Search />
              <input
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                placeholder="搜索领域(中英文均可,例如 计算机 / law / 物理)"
                autoFocus
              />
              {query && (
                <button className="search-back" onClick={() => setQuery("")} title="上一步">
                  ← 上一步
                </button>
              )}
            </div>

            {tilesErr ? (
              <div style={{ padding: 20, color: "var(--accent-warm)", fontSize: 12 }}>
                载入失败:{tilesErr}
              </div>
            ) : !tiles ? (
              <div style={{ padding: 20, color: "var(--text-dim)", fontSize: 12 }}>
                加载领域瓦片…
              </div>
            ) : (
              <>
                {/* Direct-match grid */}
                {filteredTiles && filteredTiles.length > 0 && (
                  <div className="onboard-grid" style={{ gridTemplateColumns: "1fr 1fr 1fr" }}>
                    {filteredTiles.map(t => renderTile(t, selectedIds, toggleTile))}
                  </div>
                )}

                {/* No direct match → LLM-suggested layer-2 tiles */}
                {filteredTiles && filteredTiles.length === 0 && (
                  <div className="ai-fallback">
                    <div className="ai-head">
                      <span className="dot" />
                      无直接匹配 · {llmLoading
                        ? "AI 正在按语义匹配…"
                        : (llmSuggestions && llmSuggestions.length > 0)
                          ? `AI 推荐 ${llmSuggestions.length} 个相关领域`
                          : (llmSuggestions && llmSuggestions.length === 0)
                            ? "未找到相近领域,试试别的关键词"
                            : "稍等…"}
                    </div>
                    {llmSuggestions && llmSuggestions.length > 0 && (
                      <div className="onboard-grid" style={{ gridTemplateColumns: "1fr 1fr 1fr" }}>
                        {llmSuggestions.map(t => renderTile(t, selectedIds, toggleTile))}
                      </div>
                    )}
                  </div>
                )}
              </>
            )}
            <div style={{ fontSize: 11, color: "var(--text-faint)", marginTop: 8, marginBottom: 4 }}>
              已选 {selectedIds.length} / {MAX_PICKS}
              {query && filteredTiles && filteredTiles.length > 0 &&
                ` · 已过滤到 ${filteredTiles.length} 个`}
            </div>
          </>
        )}

        {/* Stage 2 — Refinement */}
        {stage === 2 && tiles && (
          <div style={{ display: "grid", gap: 14, marginBottom: 12 }}>
            {selectedIds.length === 0 && (
              <div style={{ padding: 16, color: "var(--text-faint)", fontSize: 12 }}>
                还没有选中的领域。点击 ← 返回 重新选择。
              </div>
            )}
            {selectedIds.map((tileId, idx) => {
              const tile = tiles.find(t => t.path === tileId) || { path: tileId };
              const subs = subcatMap[tileId];
              const refined = refineMap[tileId] || [];
              const ranking = rankingMap[tileId];          // Map | "loading" | null | undefined
              const q = pickQueries[tileId];               // string | undefined
              // Sort subs: ranking score desc, then node_count desc.
              const sortedSubs = Array.isArray(subs)
                ? subs.slice().sort((a, b) => {
                    const ra = ranking instanceof Map ? (ranking.get(a.path) || 0) : 0;
                    const rb = ranking instanceof Map ? (ranking.get(b.path) || 0) : 0;
                    if (ra !== rb) return rb - ra;
                    return (b.node_count || 0) - (a.node_count || 0);
                  })
                : subs;
              const topRankedCount = ranking instanceof Map
                ? Array.from(ranking.values()).filter(v => v > 0).length
                : 0;
              return (
                <div key={tileId} className="refine-block">
                  <div className="refine-head">
                    <span
                      className="swatch"
                      style={{
                        background: COLOR_SLOTS[idx].swatch,
                        width: 12, height: 12, borderRadius: 3, display: "inline-block",
                        marginRight: 8, verticalAlign: "middle",
                      }}
                    />
                    <strong>{tileDisplayName(tile)}</strong>
                    <span style={{ marginLeft: 8, fontSize: 11, color: "var(--text-faint)", fontFamily: "var(--font-mono)" }}>
                      {tileId}
                    </span>
                    {q && (
                      <span style={{ marginLeft: 10, fontSize: 11, color: "var(--accent-2)" }}>
                        ✨ 按 "{q}" {ranking === "loading" ? "排序中…" : `重排 (命中 ${topRankedCount})`}
                      </span>
                    )}
                    <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-dim)" }}>
                      {refined.length === 0 ? "未收窄(全子树)" : `${refined.length} / ${MAX_REFINE_PATHS} 已选`}
                    </span>
                  </div>
                  {subs === "loading" && (
                    <div style={{ fontSize: 12, color: "var(--text-faint)", padding: "4px 0" }}>加载子方向…</div>
                  )}
                  {subs === "error" && (
                    <div style={{ fontSize: 12, color: "var(--accent-warm)", padding: "4px 0" }}>载入失败</div>
                  )}
                  {Array.isArray(subs) && subs.length === 0 && (
                    <div style={{ fontSize: 12, color: "var(--text-faint)", padding: "4px 0" }}>
                      这个领域下没有可下钻的 Layer-3 子方向
                    </div>
                  )}
                  {Array.isArray(sortedSubs) && sortedSubs.length > 0 && (
                    <div className="refine-chips">
                      {sortedSubs.map((s, i) => {
                        const on = refined.includes(s.path);
                        const cl = cleanLabel(s.path);
                        const rscore = ranking instanceof Map ? (ranking.get(s.path) || 0) : 0;
                        // Show "推荐" badge on top-3 ranked items (only if ranking present).
                        const isTop = rscore > 0 && i < 3 && ranking instanceof Map;
                        return (
                          <button
                            key={s.path}
                            className={"refine-chip" + (on ? " on" : "") + (isTop ? " top" : "")}
                            onClick={() => toggleRefine(tileId, s.path)}
                            title={s.path + (rscore ? ` · ${rscore} hits` : "")}
                            disabled={!on && refined.length >= MAX_REFINE_PATHS}
                          >
                            {isTop && <span className="rec-mark">★</span>}
                            {cl.zh || cl.en || s.path}
                            <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--text-faint)", marginLeft: 6 }}>
                              {s.node_count}
                            </span>
                          </button>
                        );
                      })}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}

        {/* Actions */}
        <div className="onboard-actions">
          {onCancel && stage === 1 && (
            <button className="btn btn-quiet" onClick={onCancel}>取消</button>
          )}
          {stage === 2 && (
            <button className="btn btn-quiet" onClick={() => setStage(1)} style={{ marginRight: "auto" }}>
              ← 返回
            </button>
          )}
          {stage === 1 ? (
            <button
              className={"btn btn-primary" + (canNext ? "" : " disabled")}
              disabled={!canNext}
              onClick={goNext}
            >下一步 →</button>
          ) : (
            <button
              className={"btn btn-primary" + (canConfirm ? "" : " disabled")}
              disabled={!canConfirm}
              onClick={confirm}
            >确认 ({selectedIds.length})</button>
          )}
        </div>
      </div>
    </div>
  );
}

window.InterestModal = InterestModal;
