/* conversations.jsx — Flow 4 (data studio) + Flow 5 (readiness threshold) */

function FB({ fb }) {
  if (fb === "up") return <span className="badge badge-good" style={{ height: 20, padding: "0 7px" }}>{I("thumbUp", { style: { width: 11, height: 11 } })}</span>;
  if (fb === "down") return <span className="badge badge-bad" style={{ height: 20, padding: "0 7px" }}>{I("thumbDown", { style: { width: 11, height: 11 } })}</span>;
  return <span className="badge badge-neutral" style={{ height: 20, padding: "0 7px", color: "var(--ink-3)" }}>—</span>;
}

function ReadinessBar({ have, need, pending, onApproveAll, unlocked, onOpen }) {
  const pct = Math.min(100, (have / need) * 100);
  return (
    <div className="card card-pad fu" style={{ background: unlocked ? "var(--good-bg)" : "var(--surface)", borderColor: unlocked ? "var(--good-border)" : "var(--border)", marginBottom: 22 }}>
      <div className="between wrap" style={{ gap: 18 }}>
        <div className="grow" style={{ minWidth: 280 }}>
          <div className="between" style={{ gap: 14, marginBottom: 6 }}>
            <div className="row" style={{ gap: 9 }}>
              <span style={{ color: unlocked ? "var(--good)" : "var(--ink-2)", flex: "none" }}>{I(unlocked ? "checkCircle" : "target", { style: { width: 18, height: 18 } })}</span>
              <span className="h3" style={{ whiteSpace: "nowrap" }}>{unlocked ? "You've collected enough high-quality examples" : "Fine-tuning readiness"}</span>
            </div>
            <span className="tnum" style={{ fontSize: 13, fontWeight: 700, whiteSpace: "nowrap" }}>{have} / {need}</span>
          </div>
          <div className="caption" style={{ marginBottom: 11 }}>
            {unlocked ? "Your chatbot is ready to become a better model." : "Approve high-quality examples to unlock the Fine-Tuning Studio."}
          </div>
          <div className={cx("bar", unlocked && "good")}><i style={{ width: pct + "%" }} /></div>
        </div>
        <div className="row" style={{ gap: 10, flex: "none" }}>
          {unlocked ? (
            <button className="btn btn-good btn-lg" onClick={onOpen}>{I("layers")} Open Fine-Tuning Studio</button>
          ) : pending > 0 ? (
            <div className="row" style={{ gap: 12 }}>
              <div className="stack" style={{ alignItems: "flex-end" }}>
                <span style={{ fontSize: 13, fontWeight: 700, whiteSpace: "nowrap" }}>{pending} suggested</span>
                <span className="caption" style={{ whiteSpace: "nowrap" }}>found by AI review</span>
              </div>
              <button className="btn btn-primary" onClick={onApproveAll}>{I("spark")} Approve all {pending}</button>
            </div>
          ) : null}
        </div>
      </div>
    </div>
  );
}

function ConvDetail({ c, onClose, onUpdate, allLabels }) {
  const [answer, setAnswer] = React.useState(c.a);
  const [showLabels, setShowLabels] = React.useState(false);
  React.useEffect(() => { setAnswer(c.a); }, [c.id]);
  const aiNote = c.score == null ? "No AI review yet. Approve based on your own read, or generate QA to fill scores in."
    : c.score >= 85 ? "Accurate, well-grounded, and on-brand. Strong fine-tuning candidate."
    : c.score >= 70 ? "Mostly correct but a little terse or generic. Worth a light edit before approving."
    : "Weak answer — vague, off-tone, or should have escalated. Edit the ideal answer before using.";
  const tone = toneFor(c.score);

  return (
    <div className="card fu" style={{ display: "flex", flexDirection: "column", height: "calc(100vh - 230px)", minHeight: 480, overflow: "hidden" }}>
      <div className="card-hd between" style={{ flex: "none" }}>
        <div className="row" style={{ gap: 9 }}>
          <span className="mono caption">{c.id}</span>
          <FB fb={c.fb} />
          {c.escalate && <span className="badge badge-warn">{I("alert", { style: { width: 11, height: 11 } })} Escalated</span>}
        </div>
        <button className="icon-btn" onClick={onClose} style={{ width: 30, height: 30 }}>{I("x", { style: { width: 16, height: 16 } })}</button>
      </div>

      <div className="scroll-y" style={{ flex: 1, padding: "var(--pad-card)", display: "flex", flexDirection: "column", gap: 18 }}>
        {/* user message */}
        <div>
          <div className="eyebrow" style={{ marginBottom: 7 }}>Customer message</div>
          <div className="bubble user" style={{ maxWidth: "100%", alignSelf: "stretch", borderRadius: "var(--r-md)" }}>{c.q}</div>
        </div>

        {/* RAG context */}
        <div>
          <div className="eyebrow" style={{ marginBottom: 7 }}>Retrieved RAG context</div>
          <div className="row" style={{ gap: 9, padding: "10px 12px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: "var(--r-md)", alignItems: "flex-start" }}>
            <span style={{ color: "var(--bad)", flex: "none" }}>{I("doc", { style: { width: 15, height: 15, marginTop: 1 } })}</span>
            <span className="mono" style={{ fontSize: 12, wordBreak: "break-all", minWidth: 0, flex: 1 }}>{c.ctx}</span>
          </div>
        </div>

        {/* model response */}
        <div>
          <div className="eyebrow" style={{ marginBottom: 7 }}>Model response</div>
          <div className="bubble bot" style={{ maxWidth: "100%", borderRadius: "var(--r-md)" }}>{c.a}</div>
        </div>

        {/* AI review */}
        <div className="row" style={{ gap: 14, padding: "14px", background: "var(--surface-2)", borderRadius: "var(--r-md)", border: "1px solid var(--border)" }}>
          <Ring value={c.score || 0} tone={tone} size={56} />
          <div className="grow">
            <div className="row" style={{ gap: 7, marginBottom: 4 }}>
              <span style={{ color: "var(--accent)" }}>{I("spark", { style: { width: 15, height: 15 } })}</span>
              <span className="label">AI review</span>
            </div>
            <p style={{ fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5 }}>{aiNote}</p>
            {c.evaluation && (
              <div className="row wrap" style={{ gap: 7, marginTop: 9 }}>
                <span className="badge badge-neutral">{c.score == null ? "No AI score" : `Quality ${c.score}`}</span>
                <span className="badge badge-neutral">Halluc. risk {c.halluc}</span>
                <span className="badge badge-neutral">Brand voice {c.voice}</span>
              </div>
            )}
            {c.evaluation?.unsupportedClaims?.length > 0 && (
              <div className="caption" style={{ marginTop: 8 }}>
                Risk noted: {c.evaluation.unsupportedClaims[0]}
              </div>
            )}
          </div>
        </div>

        {/* human-edited ideal answer */}
        <div>
          <div className="between" style={{ marginBottom: 7 }}>
            <span className="eyebrow">Human-edited ideal answer</span>
            {answer !== c.a && <span className="badge badge-info">{I("edit", { style: { width: 11, height: 11 } })} Edited</span>}
          </div>
          <textarea className="textarea" value={answer} onChange={(e) => setAnswer(e.target.value)} style={{ minHeight: 120 }} />
          <div className="caption" style={{ marginTop: 6 }}>This is the answer the fine-tuned model will learn to produce.</div>
        </div>

        {/* labels */}
        <div>
          <div className="between" style={{ marginBottom: 7 }}>
            <span className="eyebrow">Labels</span>
            <button className="btn btn-ghost btn-sm" onClick={() => setShowLabels(s => !s)}>{I("plus", { style: { width: 13, height: 13 } })} Add</button>
          </div>
          <div className="row wrap" style={{ gap: 7 }}>
            {c.labels.map(id => <LabelTag key={id} id={id} onRemove={(lid) => onUpdate({ labels: c.labels.filter(x => x !== lid) })} />)}
            {c.labels.length === 0 && <span className="caption">No labels yet</span>}
          </div>
          {showLabels && (
            <div className="row wrap fu" style={{ gap: 6, marginTop: 9 }}>
              {allLabels.filter(l => !c.labels.includes(l.id)).map(l => (
                <button key={l.id} className="chip" onClick={() => { onUpdate({ labels: [...c.labels, l.id] }); }}>
                  <span className="dot" style={{ background: l.color }} /> {l.name}
                </button>
              ))}
            </div>
          )}
        </div>
      </div>

      {/* actions */}
      <div className="card-ft row" style={{ gap: 10, flex: "none" }}>
        <button className={cx("btn", c.ready ? "btn-secondary" : "btn-ghost")} onClick={() => onUpdate({ ready: !c.ready })} style={{ flex: 1 }}>
          {I(c.ready ? "checkCircle" : "flask", { style: { width: 15, height: 15 } })} {c.ready ? "Marked for fine-tuning" : "Good for fine-tuning?"}
        </button>
        <button className={cx("btn", c.approved ? "btn-good" : "btn-primary")} onClick={() => onUpdate({ approved: !c.approved, ready: true, a: answer })} style={{ flex: 1 }}>
          {c.approved ? <>{I("check")} Approved</> : <>{I("thumbUp")} Approve example</>}
        </button>
      </div>
    </div>
  );
}

function chatLogToConvo(log, i = 0) {
  const sources = Array.isArray(log.sources) ? log.sources : [];
  const firstSource = sources[0];
  const sourceLocation = firstSource && !/^chunk\s*\d*$/i.test(String(firstSource.page || "").trim()) ? firstSource.page : "";
  const evaluation = log.evaluation || log.analysis || null;
  const metrics = log.metrics || {};
  const quality = Number(metrics.quality ?? evaluation?.quality ?? log.quality ?? 0);
  const halluc = Number(metrics.halluc ?? metrics.hallucinationRisk ?? evaluation?.hallucinationRisk ?? evaluation?.halluc ?? 0);
  const voice = Number(metrics.voice ?? metrics.brandVoice ?? evaluation?.brandVoice ?? evaluation?.voice ?? 0);
  const score = Number.isFinite(quality) && quality > 0 ? Math.round(quality) : null;
  return {
    id: log.id || log.conversationId || `chat-${i + 1}`,
    q: log.question || log.q || "Untitled conversation",
    ctx: firstSource ? `${firstSource.doc}${sourceLocation ? ` - ${sourceLocation}` : ""}` : "No RAG source returned",
    labels: ["support", "product"],
    a: log.answer || log.a || "",
    fb: "none",
    score,
    halluc: Number.isFinite(halluc) ? Math.round(halluc) : 0,
    voice: Number.isFinite(voice) ? Math.round(voice) : 0,
    evaluation: evaluation ? {
      quality: score,
      halluc: Number.isFinite(halluc) ? Math.round(halluc) : 0,
      voice: Number.isFinite(voice) ? Math.round(voice) : 0,
      reason: evaluation.reason || "",
      unsupportedClaims: Array.isArray(evaluation.unsupportedClaims) ? evaluation.unsupportedClaims : [],
      supportedClaims: Array.isArray(evaluation.supportedClaims) ? evaluation.supportedClaims : [],
    } : null,
    ready: score >= 85,
    approved: Boolean(log.approved),
    edited: false,
    sourceCount: sources.length,
    createdAt: log.createdAt,
    createdAtMs: Date.parse(log.createdAt || "") || 0,
    autoqa: log.mode === "autoqa" || log.source === "knowledge-base",
    modelId: log.modelId || null,
    rejected: Boolean(log.rejected),
    mode: log.mode || "api",
  };
}

function formatGenerated(ms) {
  if (!ms) return "-";
  const d = new Date(ms);
  return `${d.toLocaleDateString(undefined, { month: "short", day: "numeric" })}, ${d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`;
}

function pageNumbers(total, current) {
  if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
  const uniq = [...new Set([1, current - 1, current, current + 1, total].filter(n => n >= 1 && n <= total))].sort((a, b) => a - b);
  const out = [];
  for (let i = 0; i < uniq.length; i++) {
    if (i > 0 && uniq[i] - uniq[i - 1] > 1) out.push("…");
    out.push(uniq[i]);
  }
  return out;
}

function mergeConvos(primary, fallback) {
  const seen = new Set();
  return [...primary, ...fallback].filter(row => {
    if (seen.has(row.id)) return false;
    seen.add(row.id);
    return true;
  });
}

function ConfirmDialog({ dialog, onClose }) {
  if (!dialog) return null;
  return (
    <div className="scrim" role="presentation">
      <div className="modal" role="dialog" aria-modal="true" style={{ maxWidth: 470 }}>
        <div className="card-pad stack" style={{ gap: 12 }}>
          <div className="row" style={{ gap: 12, alignItems: "flex-start" }}>
            <span style={{ width: 36, height: 36, borderRadius: "var(--r-sm)", background: dialog.danger ? "var(--bad-bg)" : "var(--primary-soft)", color: dialog.danger ? "var(--bad)" : "var(--ink)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}>
              {I(dialog.icon || (dialog.danger ? "alert" : "spark"), { style: { width: 18, height: 18 } })}
            </span>
            <div style={{ minWidth: 0 }}>
              <div className="h3">{dialog.title}</div>
              <div className="caption" style={{ marginTop: 4, whiteSpace: "pre-wrap", lineHeight: 1.55 }}>{dialog.message}</div>
            </div>
          </div>
        </div>
        <div className="row" style={{ justifyContent: "flex-end", gap: 10, padding: "14px 18px", borderTop: "1px solid var(--border)", background: "var(--surface-2)" }}>
          {dialog.cancelLabel !== null && <button className="btn btn-secondary" onClick={onClose}>{dialog.cancelLabel || "Cancel"}</button>}
          <button className="btn btn-primary" onClick={dialog.onConfirm}>{dialog.confirmLabel || "Confirm"}</button>
        </div>
      </div>
    </div>
  );
}

function ConversationsView({ ctx }) {
  const { user } = useFirebaseAuth();
  const { LABELS, THRESHOLD } = window.DEMO;
  const [autoqaSettings] = React.useState(() => window.ModelOSIntegrations.load());
  const activeAgent = ctx.activeAgent || autoqaSettings.activeAgent;
  const [scopeAllBots, setScopeAllBots] = React.useState(false);
  const [botNames, setBotNames] = React.useState({});
  const [rows, setRows] = React.useState(() => {
    return window.ModelOSIntegrations.loadLocalChatLogs().map(chatLogToConvo);
  });
  const [selId, setSelId] = React.useState(rows[0]?.id);
  const [filter, setFilter] = React.useState("all");
  const [q, setQ] = React.useState("");
  const [searchHot, setSearchHot] = React.useState(false);
  const [searchFocus, setSearchFocus] = React.useState(false);
  const [page, setPage] = React.useState(1);
  const [dataVersions, setDataVersions] = React.useState([]);
  const [versionsLoading, setVersionsLoading] = React.useState(false);
  const [versionDialog, setVersionDialog] = React.useState(null);
  const [viewVersionId, setViewVersionId] = React.useState(undefined); // undefined = not chosen, null = live
  const [sync, setSync] = React.useState({ type: "info", text: "Local cache loaded" });
  const [logsLoading, setLogsLoading] = React.useState(false);

  // Declared BEFORE the readiness computation below: the browser JSX
  // transform turns const into var, so using them earlier would silently
  // read undefined and scope the readiness bar to live data.
  const viewVersion = viewVersionId && viewVersionId !== "live" ? dataVersions.find(v => v.versionId === viewVersionId) : null;
  const versionMemberIds = viewVersion ? new Set(viewVersion.conversationIds || []) : null;

  // Version-scoped readiness: reflect the APPROVED examples in the current
  // scope — version members that are approved now, or live approvals when
  // viewing live data.
  const scopeRows = versionMemberIds ? rows.filter(r => versionMemberIds.has(r.id)) : rows;
  const approvedHere = scopeRows.filter(r => r.approved).length;
  const pending = viewVersion ? 0 : scopeRows.filter(r => r.ready && !r.approved).length;
  const total = viewVersionId === undefined && dataVersions.length > 0 ? null : approvedHere;
  const unlocked = total != null && total >= THRESHOLD.need;

  React.useEffect(() => { if (unlocked) ctx.setFtUnlocked(true); }, [unlocked]);

  const refreshLogs = async () => {
    const scopeModelId = scopeAllBots ? "" : (activeAgent?.modelId || "");
    const localRows = window.ModelOSIntegrations.loadLocalChatLogs()
      .map(chatLogToConvo)
      .filter(r => !scopeModelId || r.modelId === scopeModelId);
    setRows(localRows);
    setLogsLoading(true);
    setSync({ type: "info", text: scopeModelId ? "Loading bot conversations..." : "Checking Firestore..." });
    try {
      const remoteRows = (await window.ModelOSIntegrations.loadFirestoreChatLogs(3000, user))
        .map(chatLogToConvo)
        .filter(r => !scopeModelId || r.modelId === scopeModelId);
      const next = mergeConvos(remoteRows, localRows);
      setRows(next);
      if (!next.find(r => r.id === selId)) setSelId(next[0]?.id);
      setSync({ type: "success", text: next.length ? `Loaded ${next.length} ${scopeModelId ? "bot " : ""}chats (newest first)` : "Firestore connected; no chats yet" });
    } catch (err) {
      setSync({ type: "error", text: err?.message || "Could not load Firestore" });
    } finally {
      setLogsLoading(false);
    }
  };

  React.useEffect(() => { refreshLogs(); }, [user?.uid, activeAgent?.modelId, scopeAllBots]);

  const loadVersions = async () => {
    if (!user || !activeAgent?.modelId) { setDataVersions([]); return; }
    setVersionsLoading(true);
    try {
      setDataVersions(await window.ModelOSIntegrations.listDataVersions(activeAgent.modelId, user));
    } catch { setDataVersions([]); }
    finally { setVersionsLoading(false); }
  };
  React.useEffect(() => { loadVersions(); }, [user?.uid, activeAgent?.modelId]);

  // Default the table scope to the latest version once versions exist.
  React.useEffect(() => {
    if (viewVersionId === undefined && dataVersions.length) {
      const latest = dataVersions[dataVersions.length - 1];
      setViewVersionId(latest.versionId);
    }
  }, [dataVersions]);

  // Honor a jump request from the Fine-Tuning Studio: select the dataset
  // version the user clicked there, then consume the request.
  React.useEffect(() => {
    if (!user || !dataVersions.length) return;
    try {
      const jump = window.ModelOSUserStorage.load(user.uid, "studioJumpVersion", null);
      if (!jump || !jump.versionId) return;
      window.ModelOSUserStorage.save(user.uid, "studioJumpVersion", null);
      if (jump.modelId && activeAgent && activeAgent.modelId && jump.modelId !== activeAgent.modelId) return;
      if (dataVersions.some(v => v.versionId === jump.versionId)) setViewVersionId(jump.versionId);
    } catch {}
  }, [user?.uid, dataVersions, activeAgent?.modelId]);

  const [recommendations, setRecommendations] = React.useState([]);
  React.useEffect(() => {
    let cancelled = false;
    let pollTimer = null;
    let pollStop = null;
    if (!user || !viewVersion?.versionId) { setRecommendations([]); return undefined; }
    const fetchRecs = () => window.ModelOSIntegrations.listDataRecommendations(viewVersion.versionId, user).catch(() => []);
    // An eval entry without a matching recommendation means the feedback
    // LLM is still writing it - poll locally (no page refresh) until it lands.
    const hasPendingFeedback = recs => {
      const have = new Set((recs || []).map(r => r.evalJobId));
      return (viewVersion.evalHistory || []).some(e => e.evalJobId && !have.has(e.evalJobId));
    };
    fetchRecs().then(recs => {
      if (cancelled) return;
      setRecommendations(recs || []);
      if (hasPendingFeedback(recs || [])) {
        pollTimer = setInterval(async () => {
          const next = await fetchRecs();
          if (cancelled) return;
          setRecommendations(next || []);
          if (!hasPendingFeedback(next || [])) { clearInterval(pollTimer); pollStop && clearTimeout(pollStop); }
        }, 10000);
        pollStop = setTimeout(() => { clearInterval(pollTimer); }, 600000);
      }
    });
    return () => {
      cancelled = true;
      if (pollTimer) clearInterval(pollTimer);
      if (pollStop) clearTimeout(pollStop);
    };
  }, [user?.uid, viewVersion?.versionId, viewVersion?.evalHistory?.length]);

  // Flat incremental naming: v1, v2, v3… based on the highest existing
  // number, so duplicate or deleted names never cause collisions.
  const suggestVersionName = () => {
    const nums = dataVersions.map(v => parseInt(String(v.name || "").replace(/^v/, ""), 10)).filter(Number.isFinite);
    return `v${(nums.length ? Math.max(...nums) : 0) + 1}`;
  };
  const createVersion = async () => {
    if (!versionDialog || versionDialog.busy) return;
    setVersionDialog(prev => prev ? { ...prev, busy: true, error: null } : prev);
    try {
      const result = await window.ModelOSIntegrations.createDataVersion({
        modelId: activeAgent.modelId,
        name: versionDialog.name,
        parentVersionId: versionDialog.parentVersionId || null,
        inherit: Boolean(versionDialog.parentVersionId) && versionDialog.inherit,
      }, user);
      setVersionDialog(null);
      // Switch the table to the freshly created version right away, even
      // while it is still empty.
      if (result.version) setViewVersionId(result.version.versionId);
      loadVersions();
    } catch (err) {
      setVersionDialog(prev => prev ? { ...prev, busy: false, error: err.message || "Could not create the version." } : prev);
    }
  };
  const [improving, setImproving] = React.useState(null);
  const improveAbortRef = React.useRef(null);
  const [confirmDialog, setConfirmDialog] = React.useState(null);
  const startImproving = (rec) => {
    setConfirmDialog(null);
    if (improving || !viewVersion?.versionId || !user) return;
    const settings = window.ModelOSIntegrations.load();
    const controller = new AbortController();
    improveAbortRef.current = controller;
    setImproving({ recId: rec.recId, position: 0, total: 0, error: null });
    window.ModelOSIntegrations.applyRecommendation({
      versionId: viewVersion.versionId,
      recommendationId: rec.recId,
      llmProvider: settings.llmProvider,
      llmModel: settings.llmModel,
      llmApiKey: settings.llmApiKey,
    }, {
      signal: controller.signal,
      onEvent: (event, payload) => {
        if (event === 'start') setImproving(s => s ? { ...s, total: payload.total || 0, versionName: payload.versionName } : s);
        else if (event === 'progress') setImproving(s => s ? { ...s, position: payload.position || 0 } : s);
        else if (event === 'done') {
          setImproving(null);
          improveAbortRef.current = null;
          refreshLogs();
          loadVersions().then(() => setViewVersionId(payload.versionId)).catch(() => {});
        } else if (event === 'error') {
          setImproving(s => s ? { ...s, error: payload.message || 'Improvement failed.' } : s);
        }
      },
    }, user).catch(err => {
      if (err?.name === 'AbortError') {
        setImproving(null);
      } else {
        setImproving(s => s ? { ...s, error: err.message || 'Improvement failed.' } : s);
      }
    });
  };
  const stopImproving = () => { improveAbortRef.current?.abort(); };
  const applyRecommendationToDataset = (rec) => {
    if (improving || !viewVersion?.versionId || !user) return;
    const settings = window.ModelOSIntegrations.load();
    if (!window.ModelOSIntegrations.hasLlmApiKey(settings)) {
      setConfirmDialog({
        title: "API key required",
        message: "Add your LLM API key in the Knowledge base page before improving the dataset.",
        confirmLabel: "OK",
        cancelLabel: null,
        icon: "alert",
        danger: true,
        onConfirm: () => setConfirmDialog(null),
      });
      return;
    }
    setConfirmDialog({
      title: "Improve dataset",
      message: `Rewrite every answer in ${viewVersion.name} following this recommendation?\n\nA new version will be created automatically with the improved answers (originals stay untouched). With ~${viewVersion.count} examples this takes several minutes.`,
      confirmLabel: "Improve dataset",
      onConfirm: () => startImproving(rec),
    });
  };

  const [regenerating, setRegenerating] = React.useState(null);
  const regenerateFeedback = async (entry) => {
    console.log('[regenerateFeedback] called with entry:', entry);
    if (regenerating) { console.log('[regenerateFeedback] already regenerating, skipping'); return; }
    if (!entry.evalJobId) { console.log('[regenerateFeedback] no evalJobId, skipping'); return; }
    const settings = window.ModelOSIntegrations.load();
    console.log('[regenerateFeedback] settings:', { hasApiKey: !!settings.llmApiKey, llmModel: settings.llmModel });
    if (!settings.llmApiKey) {
      console.log('[regenerateFeedback] no API key, showing dialog');
      setConfirmDialog({
        title: "API key required",
        message: "Add your LLM API key in the Knowledge base page before regenerating feedback.",
        confirmLabel: "OK",
        cancelLabel: null,
        icon: "alert",
        danger: true,
        onConfirm: () => setConfirmDialog(null),
      });
      return;
    }
    setRegenerating(entry.evalJobId);
    try {
      // entry.jobId is the fine-tune job ID, entry.evalJobId is the eval job ID
      console.log('[regenerateFeedback] calling API with jobId:', entry.jobId, 'evalJobId:', entry.evalJobId);
      const result = await window.FineTuneApi.regenerateFeedback(entry.jobId || 'unknown', entry.evalJobId, {
        apiKey: settings.llmApiKey,
        judgeModel: settings.llmModel || 'gpt-4o-mini',
      });
      console.log('[regenerateFeedback] API call succeeded, result:', result);
      // Refresh recommendations after regeneration
      console.log('[regenerateFeedback] Fetching fresh recommendations for versionId:', viewVersion.versionId);
      const recs = await window.ModelOSIntegrations.listDataRecommendations(viewVersion.versionId, user);
      console.log('[regenerateFeedback] Fetched recommendations:', recs.map(r => ({ recId: r.recId, evalJobId: r.evalJobId, createdAt: r.createdAt, textPreview: (r.text || '').slice(0, 100) })));
      setRecommendations(recs || []);
    } catch (err) {
      console.error('[regenerateFeedback] API call failed:', err);
      setConfirmDialog({
        title: "Regeneration failed",
        message: err.message || "Could not regenerate feedback.",
        confirmLabel: "OK",
        cancelLabel: null,
        icon: "alert",
        danger: true,
        onConfirm: () => setConfirmDialog(null),
      });
    } finally {
      setRegenerating(null);
    }
  };

  const useVersionForFineTune = (version) => {
    // Training count = version members that are approved.
    const memberIds = new Set(version.conversationIds || []);
    const trainCount = rows.filter(r => memberIds.has(r.id) && r.approved && !r.rejected).length;
    // Nothing approved in this version -> carry nothing into the studio.
    if (!trainCount) {
      window.ModelOSUserStorage.save(user.uid, "selectedDataVersion", null);
      ctx.setView("finetune");
      return;
    }
    window.ModelOSUserStorage.save(user.uid, "selectedDataVersion", {
      versionId: version.versionId,
      name: version.name,
      count: trainCount,
      modelId: activeAgent?.modelId,
    });
    ctx.setView("finetune");
  };
  // Open the Fine-Tuning Studio carrying whatever version is selected in the table.
  const openFineTuneStudio = () => {
    if (viewVersion) { useVersionForFineTune(viewVersion); return; }
    window.ModelOSUserStorage.save(user.uid, "selectedDataVersion", null);
    ctx.setView("finetune");
  };
  const versionTree = (() => {
    const byParent = new Map();
    for (const v of dataVersions) {
      const key = v.parentVersionId || "";
      if (!byParent.has(key)) byParent.set(key, []);
      byParent.get(key).push(v);
    }
    const rows = [];
    const walk = (parentId, depth) => {
      for (const v of (byParent.get(parentId || "") || [])) {
        rows.push({ ...v, depth });
        walk(v.versionId, depth + 1);
      }
    };
    walk("", 0);
    return rows;
  })();

  React.useEffect(() => {
    if (!user) return undefined;
    let cancelled = false;
    window.ModelOSIntegrations.listAgents(user).then(agents => {
      if (cancelled) return;
      const names = {};
      for (const agent of agents || []) names[agent.modelId] = agent.name || "Bot";
      setBotNames(names);
    }).catch(() => {});
    return () => { cancelled = true; };
  }, [user?.uid]);

  const doRejectAll = (targets) => {
    setConfirmDialog(null);
    // Unapprove only: examples stay in the dataset and keep their version
    // membership — they are not deleted or marked rejected.
    setRows(rs => rs.map(r => (!versionMemberIds || versionMemberIds.has(r.id)) && r.approved ? { ...r, approved: false } : r));
    if (user) {
      targets.forEach(r => window.ModelOSIntegrations.updateConversationApproval(r.id, { approved: false }, user).catch(() => {}));
    }
  };
  const rejectAll = () => {
    const scopeRows = versionMemberIds ? rows.filter(r => versionMemberIds.has(r.id)) : rows;
    const targets = scopeRows.filter(r => r.approved);
    if (!targets.length) return;
    const label = versionMemberIds ? ` in ${viewVersion.name}` : "";
    setConfirmDialog({
      title: "Disapprove examples",
      message: `Disapprove ${targets.length} examples${label}?\n\nThey stay in this dataset and keep their version membership — only the approved flag is cleared.`,
      confirmLabel: `Disapprove ${targets.length}`,
      icon: "thumbDown",
      danger: true,
      onConfirm: () => doRejectAll(targets),
    });
  };

  const createFirstVersion = async () => {
    if (!user || !activeAgent?.modelId) return;
    try {
      const result = await window.ModelOSIntegrations.createDataVersion({ modelId: activeAgent.modelId, name: "v1", parentVersionId: null, inherit: false }, user);
      if (result.version) setViewVersionId(result.version.versionId);
      loadVersions();
    } catch (err) {
      setConfirmDialog({
        title: "Could not create v1",
        message: err.message || "Could not create v1.",
        confirmLabel: "OK",
        cancelLabel: null,
        icon: "alert",
        danger: true,
        onConfirm: () => setConfirmDialog(null),
      });
    }
  };

  const update = (id, patch) => {
    setRows(rs => rs.map(r => r.id === id ? { ...r, ...patch } : r));
    if (patch && (patch.approved !== undefined || patch.rejected !== undefined) && user) {
      window.ModelOSIntegrations.updateConversationApproval(id, patch, user).catch(() => {});
    }
    // Membership follows the selected version: approving adds, rejecting removes.
    if (viewVersion?.versionId && user) {
      if (patch && patch.approved === true) {
        window.ModelOSIntegrations.updateVersionMembers(viewVersion.versionId, { add: [id] }, user)
          .then(() => loadVersions()).catch(() => {});
      } else if (patch && patch.rejected === true) {
        window.ModelOSIntegrations.updateVersionMembers(viewVersion.versionId, { remove: [id] }, user)
          .then(() => loadVersions()).catch(() => {});
      }
    }
  };
  const approveAll = () => {
    // Approve everything in the current view scope (version members when a
    // version is selected), regardless of AI-suggested status.
    const targets = (versionMemberIds ? rows.filter(r => versionMemberIds.has(r.id)) : rows)
      .filter(r => !r.approved && !r.rejected);
    if (!targets.length) return;
    const ids = new Set(targets.map(r => r.id));
    setRows(rs => rs.map(r => ids.has(r.id) ? { ...r, approved: true, ready: true } : r));
    if (user) {
      targets.forEach(r => {
        window.ModelOSIntegrations.updateConversationApproval(r.id, { approved: true }, user).catch(() => {});
      });
    }
  };

  const tabRows = (() => {
    let base = rows.filter(r => !r.rejected);
    if (versionMemberIds) base = base.filter(r => versionMemberIds.has(r.id));
    return base;
  })();
  const filtered = tabRows.filter(r => {
    if (q && !r.q.toLowerCase().includes(q.toLowerCase())) return false;
    if (filter === "approved") return r.approved;
    if (filter === "needs") return !r.ready && !r.approved;
    if (filter !== "all") return r.labels.includes(filter);
    return true;
  });

  const [sort, setSort] = React.useState({ key: "created", dir: "desc" });
  const toggleSort = (key) => setSort(prev =>
    prev.key === key ? { key, dir: prev.dir === "asc" ? "desc" : "asc" } : { key, dir: key === "q" ? "asc" : "desc" });
  const sortArrow = (key) => sort.key === key ? (sort.dir === "asc" ? " \u2191" : " \u2193") : "";
  const sorted = [...filtered].sort((a, b) => {
    let cmp = 0;
    if (sort.key === "score") cmp = (a.score || 0) - (b.score || 0);
    else if (sort.key === "created") cmp = (a.createdAtMs || 0) - (b.createdAtMs || 0);
    else if (sort.key === "q") cmp = String(a.q).localeCompare(String(b.q));
    else if (sort.key === "halluc") cmp = (a.halluc || 0) - (b.halluc || 0);
    return sort.dir === "asc" ? cmp : -cmp;
  });

  const totalPages = Math.max(1, Math.ceil(sorted.length / 50));
  const safePage = Math.min(page, totalPages);
  const paged = sorted.slice((safePage - 1) * 50, safePage * 50);
  React.useEffect(() => { setPage(1); }, [filter, q, scopeAllBots, activeAgent?.modelId, viewVersionId]);

  const sel = rows.find(r => r.id === selId);

  const FILTERS = [
    { id: "all", name: "All", n: tabRows.length },
    { id: "approved", name: "Approved for training", n: tabRows.filter(r => r.approved).length },
    { id: "needs", name: "Needs review", n: tabRows.filter(r => !r.ready && !r.approved).length },
  ];

  const autoqa = window.useAutoQa({ modelId: activeAgent?.modelId, settings: autoqaSettings, authUser: user, versionId: viewVersion?.versionId });
  const [autoqaFiles, setAutoqaFiles] = React.useState(null);
  const [autoqaFilesLoading, setAutoqaFilesLoading] = React.useState(false);

  const openAutoqaFromStudio = async () => {
    if (!activeAgent?.modelId || !user) return;
    setAutoqaFiles(null);
    setAutoqaFilesLoading(true);
    autoqa.openPicker();
    try {
      const status = await window.ModelOSIntegrations.loadKnowledgeStatus(activeAgent.modelId, user);
      setAutoqaFiles((status?.files || []).filter(f => f.status === "ready" && f.chunkCount));
    } finally {
      setAutoqaFilesLoading(false);
    }
  };
  const closeAutoqaFromStudio = () => {
    const wasDone = autoqa.autoqa?.phase === "done";
    const madeVersionId = autoqa.autoqa?.summary?.versionId || null;
    autoqa.close();
    if (wasDone) {
      refreshLogs();
      loadVersions().then(() => { if (madeVersionId) setViewVersionId(madeVersionId); }).catch(() => {});
    }
  };

  return (
    <div className="page page-wide">
      {logsLoading && (
        <div style={{ position: "fixed", inset: 0, zIndex: 200, background: "rgba(250, 250, 252, 0.72)", backdropFilter: "blur(2px)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 14 }}>
          <span className="spin" style={{ display: "inline-flex", color: "var(--primary)" }}>{I("refresh", { style: { width: 36, height: 36 } })}</span>
          <div className="h3">Loading training data</div>
          <div className="caption">Fetching conversations from the database...</div>
        </div>
      )}
      <ConfirmDialog dialog={confirmDialog} onClose={() => setConfirmDialog(null)} />
      <window.AutoQaDialog state={autoqa.autoqa} files={autoqaFiles} filesLoading={autoqaFilesLoading} contextLabel={activeAgent?.modelId ? `Bot: ${activeAgent.name || activeAgent.modelId}` : ""}
        hasKey={window.ModelOSIntegrations.hasLlmApiKey(autoqaSettings)}
        providerLabel={autoqaSettings.llmProvider}
        onClose={closeAutoqaFromStudio} onStart={autoqa.start} onStop={autoqa.stop}
        onDownload={autoqa.downloadJsonl} onOpenStudio={closeAutoqaFromStudio}
        onQuestionsPerChunkChange={autoqa.setQuestionsPerChunk} onPickFile={autoqa.toggleFile} />
      <PageHead eyebrow="Improve"
        title="Training Data Studio"
        sub="Collect customer conversations, review model responses, edit ideal answers, and approve the best examples for fine-tuning.">
        <div className={cx("badge", sync.type === "error" ? "badge-bad" : sync.type === "success" ? "badge-good" : "badge-neutral")}>
          {I(sync.type === "error" ? "alert" : sync.type === "success" ? "checkCircle" : "clock", { style: { width: 13, height: 13 } })}
          {/* In version scope the raw chat count is misleading (training uses
              only this version's members) - derived at render so it is correct
              even when the version is selected after the initial load. */}
          {versionMemberIds && sync.type === "success" ? "Sync with database" : sync.text}
        </div>
        <button className="btn btn-secondary" disabled={!activeAgent?.modelId} onClick={openAutoqaFromStudio} title={activeAgent?.modelId ? "Generate question/answer pairs from a knowledge base document" : "Select or create a bot first"}>{I("spark")} Generate QA</button>
        <button className="btn btn-secondary" onClick={refreshLogs}>{I("refresh")} Sync chats</button>
      </PageHead>

      <ReadinessBar have={total} need={THRESHOLD.need} pending={pending} unlocked={unlocked}
        onApproveAll={approveAll} onOpen={openFineTuneStudio} />

      {versionDialog && (
        <div className="scrim" role="presentation">
          <div className="modal" role="dialog" aria-modal="true" style={{ maxWidth: 480 }}>
            <div className="card-hd between" style={{ gap: 12 }}>
              <div className="row" style={{ gap: 10 }}>
                <span style={{ width: 34, height: 34, borderRadius: "var(--r-sm)", background: "var(--primary-soft)", color: "var(--ink)", display: "flex", alignItems: "center", justifyContent: "center", flex: "none" }}>{I("layers", { style: { width: 17, height: 17 } })}</span>
                <div>
                  <div className="h3">New data version</div>
                  <div className="caption">Snapshot the training set as a version node.</div>
                </div>
              </div>
              <button className="icon-btn" onClick={() => !versionDialog.busy && setVersionDialog(null)} style={{ width: 32, height: 32 }}>{I("x", { style: { width: 15, height: 15 } })}</button>
            </div>
            <div className="card-pad stack" style={{ gap: 13 }}>
              <label>
                <span className="field-label">Parent version</span>
                <select className="input" value={versionDialog.parentVersionId || ""} onChange={(e) => setVersionDialog(d => ({ ...d, parentVersionId: e.target.value, name: suggestVersionName(e.target.value) }))}>
                  <option value="">None - snapshot the currently approved examples</option>
                  {dataVersions.map(v => <option key={v.versionId} value={v.versionId}>{v.name} ({v.count} examples)</option>)}
                </select>
              </label>
              <label>
                <span className="field-label">Version name</span>
                <input className="input" value={versionDialog.name} onChange={(e) => setVersionDialog(d => ({ ...d, name: e.target.value }))} />
              </label>
              {versionDialog.parentVersionId ? (
                <label className="row" style={{ gap: 8, cursor: "pointer" }}>
                  <input type="checkbox" checked={versionDialog.inherit} onChange={(e) => setVersionDialog(d => ({ ...d, inherit: e.target.checked }))} style={{ width: 15, height: 15 }} />
                  <span className="caption">Inherit parent's data (off = snapshot the current approved examples instead)</span>
                </label>
              ) : (
                <div className="caption">This version captures the currently approved examples ({approvedHere}).</div>
              )}
              {versionDialog.error && <div className="badge badge-bad" style={{ alignSelf: "flex-start" }}>{I("alert", { style: { width: 12, height: 12 } })} {versionDialog.error}</div>}
            </div>
            <div className="row" style={{ justifyContent: "flex-end", gap: 10, padding: "14px 18px", borderTop: "1px solid var(--border)", background: "var(--surface-2)" }}>
              <button className="btn btn-secondary" disabled={versionDialog.busy} onClick={() => setVersionDialog(null)}>Cancel</button>
              <button className="btn btn-primary" disabled={versionDialog.busy} onClick={createVersion}>{versionDialog.busy ? "Creating..." : "Create version"}</button>
            </div>
          </div>
        </div>
      )}

      <div style={{ display: "grid", gridTemplateColumns: "1fr 440px", gap: 20, alignItems: "start" }} className="cv-grid">
        {/* list */}
        <div className="stack fu fu-1" style={{ gap: 12 }}>
          {dataVersions.length === 0 && !versionsLoading ? (
            <div className="card card-pad row between" style={{ gap: 12 }}>
              <div className="row" style={{ gap: 10 }}>
                {I("layers", { style: { width: 18, height: 18, color: "var(--ink-3)" } })}
                <div>
                  <div style={{ fontSize: 13.5, fontWeight: 600 }}>No dataset versions yet</div>
                  <div className="caption">Create v1 to start collecting training data for this bot. Approvals and generated examples are added into the selected version.</div>
                </div>
              </div>
              <button className="btn btn-primary btn-sm" disabled={!activeAgent?.modelId || !user} onClick={() => createFirstVersion()}>
                {I("plus", { style: { width: 13, height: 13 } })} Create v1
              </button>
            </div>
          ) : (
            <div className="row between wrap" style={{ gap: 10 }}>
              <div className="row" style={{ gap: 10 }}>
                <select className="input" style={{ height: 36, fontSize: 13, minWidth: 220 }} value={viewVersionId ?? ""} onChange={(e) => setViewVersionId(e.target.value || null)} title="Which version the table shows - generated and approved rows are added into this version">
                  {dataVersions.map(v => (
                    <option key={v.versionId} value={v.versionId}>
                      {v.name} ({v.count} examples){v.evalSummary && v.evalSummary.avgJudgeScore != null ? ` · eval ${v.evalSummary.avgJudgeScore}` : ""}
                    </option>
                  ))}
                </select>
                <button className="btn btn-secondary btn-sm" disabled={!activeAgent?.modelId || !user} onClick={() => setVersionDialog({ name: suggestVersionName(""), parentVersionId: "", inherit: true, busy: false, error: null })}>
                  {I("plus", { style: { width: 13, height: 13 } })} New version
                </button>
              </div>
              <button className="btn btn-primary btn-sm" disabled={!viewVersion} onClick={() => viewVersion && useVersionForFineTune(viewVersion)}>
                {I("layers", { style: { width: 13, height: 13 } })} Fine-tune with this version
              </button>
            </div>
          )}
          {viewVersion && (
            <div className="card">
              <div className="card-hd between">
                <div className="row" style={{ gap: 8 }}>
                  <span className="h3">Evaluation feedback</span>
                  {viewVersion.evalSummary ? (
                    <span className="badge badge-info" title={`Eval of a model trained on ${viewVersion.name}`}>
                      {I("gauge", { style: { width: 11, height: 11 } })}
                      {viewVersion.evalSummary.avgJudgeScore != null ? ` avg judge ${viewVersion.evalSummary.avgJudgeScore}` : ` ${viewVersion.evalSummary.completed ?? 0} cases`}
                      {viewVersion.evalSummary.at ? ` · ${formatGenerated(Date.parse(viewVersion.evalSummary.at))}` : ""}
                    </span>
                  ) : <span className="badge badge-neutral">no evaluation yet</span>}
                </div>
                <span className="caption">Improvement recommendations from models trained on {viewVersion.name}</span>
              </div>
              <div className="stack">
                {(Array.isArray(viewVersion.evalHistory) ? viewVersion.evalHistory : [])
                  .slice()
                  .sort((a, b) => String(b.at || '').localeCompare(String(a.at || '')))
                  .map(entry => {
                    const rec = recommendations.find(r => r.evalJobId === entry.evalJobId);
                    return (
                      <div key={entry.evalJobId} style={{ padding: "10px 16px", borderBottom: "1px solid var(--border)" }}>
                        <div className="row between wrap" style={{ gap: 8 }}>
                          <div className="row" style={{ gap: 8 }}>
                            {I("gauge", { style: { width: 12, height: 12, color: "var(--info)" } })}
                            <b style={{ fontSize: 13 }}>{entry.jobName || "Fine-tuned model"}</b>
                            <span className="caption" style={{ opacity: .65 }}>{formatGenerated(Date.parse(entry.at || ""))}</span>
                          </div>
                          <div className="row" style={{ gap: 6 }}>
                            {entry.avgJudgeScore != null && <span className="badge badge-good tnum">judge {entry.avgJudgeScore}</span>}
                            <span className="badge badge-neutral tnum">{entry.completed ?? 0} cases</span>
                          </div>
                        </div>
                        {!rec && (
                          <div className="row" style={{ gap: 7, marginTop: 6 }}>
                            <span className="spin" style={{ display: "inline-flex", color: "var(--accent)" }}>{I("refresh", { style: { width: 12, height: 12 } })}</span>
                            <span className="caption">Generating evaluation feedback — it will appear here automatically when ready.</span>
                          </div>
                        )}
                        {rec && (
                          <div style={{ marginTop: 4 }}>
                            <details>
                              <summary className="caption" style={{ cursor: "pointer" }}>{I("spark", { style: { width: 11, height: 11, verticalAlign: "-2px", marginRight: 4 } })}Dataset improvement recommendation <span style={{ opacity: 0.5, fontSize: 10 }}>(generated: {rec.createdAt ? new Date(rec.createdAt).toLocaleString() : 'unknown'})</span></summary>
                              <div className="caption" style={{ whiteSpace: "pre-wrap", lineHeight: 1.55, marginTop: 6 }}>{rec.text}</div>
                            </details>
                            {improving && improving.recId === rec.recId ? (
                              <div className="row between" style={{ gap: 10, marginTop: 8 }}>
                                <span className="caption">
                                  {improving.error
                                    ? improving.error
                                    : improving.versionName
                                      ? `Improving into ${improving.versionName} - ${improving.position}/${improving.total || '...'} answers`
                                      : 'Starting...'}
                                </span>
                                {!improving.error && <button className="btn btn-ghost btn-sm" onClick={stopImproving}>Stop</button>}
                              </div>
                            ) : (
                              <div className="row" style={{ gap: 8, marginTop: 6 }}>
                                <button className="btn btn-secondary btn-sm" disabled={!!improving}
                                        onClick={() => applyRecommendationToDataset(rec)}
                                        title="Rewrite every answer in this version following the recommendation; creates a new version">
                                  {I("spark", { style: { width: 12, height: 12 } })} Improve dataset
                                </button>
                                <button className="btn btn-ghost btn-sm" disabled={!!regenerating}
                                        onClick={() => regenerateFeedback(entry)}
                                        title="Regenerate the feedback using the latest prompt template">
                                  {regenerating === entry.evalJobId
                                    ? <><span className="spin" style={{ display: "inline-flex" }}>{I("refresh", { style: { width: 12, height: 12 } })}</span> Regenerating...</>
                                    : <>{I("refresh", { style: { width: 12, height: 12 } })} Regenerate feedback</>}
                                </button>
                              </div>
                            )}
                          </div>
                        )}
                      </div>
                    );
                  })}
                {recommendations
                  .filter(rec => !(viewVersion.evalHistory || []).some(entry => entry.evalJobId === rec.evalJobId))
                  .map(rec => (
                    <div key={rec.recId} style={{ padding: "10px 16px", borderBottom: "1px solid var(--border)" }}>
                      <div className="row" style={{ gap: 8 }}>
                        {I("spark", { style: { width: 12, height: 12, color: "var(--accent)" } })}
                        <b style={{ fontSize: 13 }}>{rec.jobName || "Fine-tuned model"}</b>
                        <span className="caption" style={{ opacity: .65 }}>{formatGenerated(Date.parse(rec.createdAt || ""))}</span>
                      </div>
                      <details style={{ marginTop: 4 }}>
                        <summary className="caption" style={{ cursor: "pointer" }}>Dataset improvement recommendation</summary>
                        <div className="caption" style={{ whiteSpace: "pre-wrap", lineHeight: 1.55, marginTop: 6 }}>{rec.text}</div>
                      </details>
                      {improving && improving.recId === rec.recId ? (
                        <div className="row between" style={{ gap: 10, marginTop: 8 }}>
                          <span className="caption">{improving.error ? improving.error : (improving.versionName ? `Improving into ${improving.versionName} - ${improving.position}/${improving.total || '...'} answers` : 'Starting...')}</span>
                          {!improving.error && <button className="btn btn-ghost btn-sm" onClick={stopImproving}>Stop</button>}
                        </div>
                      ) : (
                        <button className="btn btn-secondary btn-sm" style={{ marginTop: 6 }} disabled={!!improving} onClick={() => applyRecommendationToDataset(rec)}>
                          {I("spark", { style: { width: 12, height: 12 } })} Improve dataset
                        </button>
                      )}
                    </div>
                  ))}
                {!(viewVersion.evalHistory || []).length && recommendations.length === 0 && (
                  <div className="caption" style={{ padding: "12px 16px" }}>
                    No evaluations yet. Fine-tune a model with this version and run an evaluation - results and an improvement recommendation will appear here automatically when it completes.
                  </div>
                )}
              </div>
            </div>
          )}
          <div className="between wrap" style={{ gap: 10 }}>
            <div className="row wrap" style={{ gap: 5, alignItems: "center", flex: 1 }}>
              {FILTERS.map(f => (
                <button key={f.id} className="chip" data-on={filter === f.id ? "1" : "0"} onClick={() => setFilter(f.id)} style={{ padding: "0 8px", fontSize: 12, gap: 4, height: 26 }}>
                  {f.name} <span style={{ opacity: .6 }}>{f.n}</span>
                </button>
              ))}
              <button className="btn btn-ghost btn-sm" style={{ color: "var(--good)" }} onClick={approveAll} title="Approve every example in the current view scope">
                {I("check", { style: { width: 13, height: 13 } })} Approve all
              </button>
              <button className="btn btn-ghost btn-sm" style={{ color: "var(--bad)" }} onClick={rejectAll} title="Disapprove every approved example in the current view scope">
                {I("thumbDown", { style: { width: 13, height: 13 } })} Disapprove all
              </button>
            </div>
            {/* Collapsed to an icon; expands LEFT on hover/focus as an
                overlay (layout untouched) with a darker raised background. */}
            <div style={{ position: "relative", flex: "none", height: 34, width: 34 }}>
              {(() => {
                const open = searchHot || searchFocus || q;
                return (
                  <div
                    style={{
                      position: "absolute", right: 0, top: 0, height: 34,
                      width: open ? 230 : 34,
                      transition: "width .18s ease",
                      background: open ? "var(--surface-2)" : "transparent",
                      border: `1px solid ${open ? "var(--border)" : "transparent"}`,
                      borderRadius: 9,
                      boxShadow: open ? "0 8px 24px rgba(0,0,0,.18)" : "none",
                      display: "flex", alignItems: "center",
                      overflow: "hidden", zIndex: 40,
                      cursor: "text",
                    }}
                    onMouseEnter={() => setSearchHot(true)}
                    onMouseLeave={() => setSearchHot(false)}
                  >
                    <span style={{ width: 34, display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-3)", flex: "none" }}>{I("search", { style: { width: 15, height: 15 } })}</span>
                    <input
                      style={{ height: 32, width: "100%", border: "none", outline: "none", background: "transparent", fontSize: 13, paddingRight: 10, opacity: open ? 1 : 0, transition: "opacity .15s ease" }}
                      placeholder="Search messages"
                      value={q}
                      onChange={e => setQ(e.target.value)}
                      onFocus={() => setSearchFocus(true)}
                      onBlur={() => setSearchFocus(false)}
                    />
                  </div>
                );
              })()}
            </div>
          </div>

          <div className="card" style={{ overflow: "hidden" }}>
            <table className="tbl" style={{ tableLayout: "fixed", width: "calc(100% - 16px)" }}>
              <thead><tr>
                <th style={{ width: "24%", cursor: "pointer", userSelect: "none" }} onClick={() => toggleSort("q")}>Customer message{sortArrow("q")}</th>
                <th style={{ width: 110 }}>Labels</th>
                <th style={{ width: 70, textAlign: "center" }}>Source</th>
                <th style={{ width: 92, textAlign: "center", cursor: "pointer", userSelect: "none" }} onClick={() => toggleSort("created")}>Generated{sortArrow("created")}</th>
                <th style={{ width: 56, textAlign: "center", cursor: "pointer", userSelect: "none" }} onClick={() => toggleSort("score")}>AI score{sortArrow("score")}</th>
                <th style={{ width: 86, textAlign: "center" }}>Status</th>
              </tr></thead>
              <tbody>
                {paged.map(r => (
                  <tr key={r.id} className="clickable" onClick={() => setSelId(r.id)} style={{ background: selId === r.id ? "var(--surface-2)" : "" }}>
                    <td>
                      <div className="row" style={{ gap: 8 }}>
                        {r.ready && !r.approved && <span title="AI-suggested" style={{ color: "var(--accent)", flex: "none" }}>{I("spark", { style: { width: 14, height: 14 } })}</span>}
                        <span style={{ fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1, minWidth: 0 }}>{r.q}</span>
                      </div>
                    </td>
                    <td><div className="row wrap" style={{ gap: 4 }}>{r.labels.slice(0, 2).map(id => <LabelTag key={id} id={id} />)}</div></td>
                    <td style={{ textAlign: "center" }}>
                      <div className="stack" style={{ gap: 3, alignItems: "center" }}>
                        {r.autoqa ? <span className="badge badge-accent" title="Generated from a knowledge base document">Auto QA</span> : <span className="badge badge-info">Live</span>}
                        {r.modelId && r.modelId !== activeAgent?.modelId && (
                          <span className="caption" style={{ opacity: .7 }} title={r.modelId}>{botNames[r.modelId] || "Other bot"}</span>
                        )}
                      </div>
                    </td>
                    <td style={{ textAlign: "center" }}><span className="caption tnum" title={r.createdAt ? new Date(r.createdAtMs || r.createdAt).toLocaleString() : ""}>{formatGenerated(r.createdAtMs)}</span></td>
                    <td style={{ textAlign: "center" }}>{r.score != null ? <span className="tnum" style={{ fontWeight: 700, color: TONE_VAR[toneFor(r.score)] }}>{r.score}</span> : <span className="caption">—</span>}</td>
                    <td style={{ textAlign: "center" }}>
                      <div className="stack" style={{ gap: 3, alignItems: "center" }}>
                        {r.rejected ? <span className="badge badge-bad">{I("thumbDown", { style: { width: 11, height: 11 } })} Rejected</span>
                          : r.approved ? <span className="badge badge-good">{I("check", { style: { width: 11, height: 11 } })} Approved</span>
                          : r.ready ? <span className="badge badge-accent">Suggested</span>
                          : <span className="badge badge-neutral">Review</span>}
                        {r.rejected && (
                          <button className="btn btn-ghost btn-sm" onClick={(e) => { e.stopPropagation(); update(r.id, { rejected: false }); }} title="Restore to the review queue">
                            {I("refresh", { style: { width: 11, height: 11 } })} Restore
                          </button>
                        )}
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
            {sorted.length === 0 && <Empty icon="search" title="No conversations" sub="Try a different filter or search term." />}
            {totalPages > 1 && (
              <div className="row wrap" style={{ justifyContent: "center", gap: 6, padding: "10px 12px", borderTop: "1px solid var(--border)" }}>
                <button className="chip" disabled={safePage <= 1} onClick={() => setPage(1)} title="First page">«</button>
                <button className="chip" disabled={safePage <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>Prev</button>
                {pageNumbers(totalPages, safePage).map((n, i) => n === "…" ? (
                  <span key={`e${i}`} className="caption" style={{ padding: "0 2px", alignSelf: "center" }}>…</span>
                ) : (
                  <button key={n} className="chip" data-on={n === safePage ? "1" : "0"} onClick={() => setPage(n)}>{n}</button>
                ))}
                <button className="chip" disabled={safePage >= totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>Next</button>
                <button className="chip" disabled={safePage >= totalPages} onClick={() => setPage(totalPages)} title="Last page">»</button>
                <span className="caption" style={{ alignSelf: "center", marginLeft: 6 }}>{sorted.length} rows - {totalPages} page{totalPages > 1 ? "s" : ""}</span>
              </div>
            )}
          </div>
        </div>

        {/* detail */}
        <div className="cv-detail fu fu-2" style={{ position: "sticky", top: 20 }}>
          {sel
            ? <ConvDetail c={sel} allLabels={LABELS} onClose={() => setSelId(null)} onUpdate={(p) => update(sel.id, p)} />
            : <div className="card" style={{ height: "calc(100vh - 230px)", minHeight: 480 }}><Empty icon="message" title="Select a conversation" sub="Pick a message from the list to review, edit, and approve it as training data." /></div>}
        </div>
      </div>

      <style>{`@media(max-width:1040px){.cv-grid{grid-template-columns:1fr !important}.cv-grid .cv-detail{position:static !important}}`}</style>
    </div>
  );
}
window.ConversationsView = ConversationsView;
