// Florio Analytics — Labeling tab (ported from js/pages/labeling.js).
// Human-in-the-loop supervised data collection for the avatar k-NN pipeline.
// Shows one houseplant photo at a time, user picks the closest-matching avatar
// from a grid, the label is written to `avatar_labels` in Firestore. A Cloud
// Function (onLabelCreated) embeds the photo via Replicate CLIP and fills the
// `embedding` field. The 3D cluster viz is a PCA projection of those embeddings.
//
// Rebuilt in the new design kit, but ALL behaviour is preserved:
//   • avatar pick grid with live Rive previews + per-avatar count badges
//   • queue loading (Unsplash queue vs user-scan queue tabs)
//   • label submit (batch write), skip ("no_match") + not-houseplant ("rejected")
//   • upload from computer, progress strip, per-avatar balance bars
//   • Unsplash seeding (key prompted + cached in localStorage)
//   • purge non-Unsplash pending items
//   • re-embed (test 1 / re-embed all) via the reEmbedLabels Cloud Function
//   • 3D cluster visualization (Three.js + OrbitControls, custom PCA)
(function () {
  const { useState, useEffect, useRef, useCallback } = React;
  const e = React.createElement;

  // ─── Constants ─────────────────────────────────────────────────────────────

  // All 19 avatar ids. Append new avatars here to have them show up in the grid.
  const AVATAR_ORDER = [
    "monstera", "fern", "peace", "snake", "cactus",
    "palm", "bushy", "hanging", "orchid", "aloevera",
    "succulent", "bonsai", "anthurium", "prayer", "dracaena",
    "ficus", "banana", "clover", "coin",
  ];

  const DISPLAY_NAMES = {
    monstera: "Monstera", palm: "Palm", bushy: "Bushy", peace: "Peace Lily",
    cactus: "Cactus", succulent: "Succulent", aloevera: "Aloe Vera",
    snake: "Snake Plant", fern: "Fern", hanging: "Hanging", orchid: "Orchid",
    anthurium: "Anthurium", bonsai: "Bonsai", banana: "Banana",
    dracaena: "Dracaena", ficus: "Ficus", prayer: "Prayer Plant",
    clover: "Clover", coin: "Coin Plant",
  };

  // Seed queries per category — Unsplash search terms for INDOOR houseplant photos.
  const SEED_CATEGORY_QUERIES = {
    monstera:  ["monstera indoor plant", "monstera deliciosa indoor plant", "monstera adansonii indoor plant", "monstera potted indoor plant"],
    fern:      ["fern indoor plant", "boston fern indoor plant", "birds nest fern indoor plant", "maidenhair fern indoor plant"],
    peace:     ["peace lily indoor plant", "spathiphyllum indoor plant", "peace lily potted indoor plant"],
    snake:     ["snake plant indoor plant", "sansevieria indoor plant", "snake plant potted indoor plant"],
    cactus:    ["cactus indoor plant", "barrel cactus indoor plant", "cactus potted indoor plant", "euphorbia trigona indoor plant"],
    succulent: ["succulent indoor plant", "echeveria indoor plant", "haworthia indoor plant", "jade plant indoor plant"],
    aloevera:  ["aloe vera indoor plant", "aloe vera potted indoor plant"],
    palm:      ["parlor palm indoor plant", "areca palm indoor plant", "kentia palm indoor plant", "ponytail palm indoor plant"],
    orchid:    ["orchid indoor plant", "orchid phalaenopsis indoor plant", "orchid potted indoor plant"],
    hanging:   ["pothos indoor plant", "string of pearls indoor plant", "string of hearts indoor plant", "hoya indoor plant", "tradescantia zebrina indoor plant", "ivy indoor plant"],
    bushy:     ["philodendron indoor plant", "philodendron brasil indoor plant", "syngonium indoor plant", "spider plant indoor plant", "alocasia indoor plant", "bird of paradise indoor plant"],
    anthurium: ["anthurium indoor plant", "anthurium potted indoor plant", "anthurium houseplant indoor plant"],
    bonsai:    ["bonsai indoor plant", "bonsai tree indoor plant", "bonsai potted indoor plant"],
    banana:    ["banana indoor plant", "banana plant potted indoor plant"],
    dracaena:  ["dracaena indoor plant", "dracaena marginata indoor plant", "cordyline indoor plant"],
    ficus:     ["fiddle leaf fig indoor plant", "rubber plant ficus indoor plant", "ficus benjamina indoor plant"],
    prayer:    ["calathea indoor plant", "calathea orbifolia indoor plant", "maranta prayer plant indoor plant", "stromanthe indoor plant", "fittonia indoor plant"],
    clover:    ["oxalis indoor plant", "oxalis triangularis indoor plant"],
    coin:      ["pilea indoor plant", "pilea peperomioides indoor plant", "peperomia indoor plant"],
    indoor:    ["indoor plant", "houseplant on shelf", "potted plant living room", "indoor plant collection"],
  };

  const SEED_OPTIONS = [
    { group: "Houseplant categories", items: [
      ["monstera", "Monstera"], ["fern", "Ferns"], ["peace", "Peace Lily"], ["snake", "Snake Plant"],
      ["cactus", "Cactus"], ["succulent", "Succulents"], ["aloevera", "Aloe Vera"], ["palm", "Palms"],
      ["orchid", "Orchid"], ["hanging", "Hanging / Trailing"], ["bushy", "Bushy / Foliage"],
      ["anthurium", "Anthurium"], ["bonsai", "Bonsai"], ["banana", "Banana Plant"], ["dracaena", "Dracaena"],
      ["ficus", "Ficus"], ["prayer", "Prayer Plant"], ["clover", "Clover / Oxalis"], ["coin", "Coin Plant / Pilea"],
    ]},
    { group: "Broad", items: [["indoor", "Indoor plants"], ["all", "All houseplants"]] },
    { group: "Other", items: [["custom", "Custom search…"]] },
  ];

  const PHOTOS_PER_QUERY = 30;
  const FUNCTIONS_BASE = "https://us-central1-florio-a6f48.cloudfunctions.net";
  const UNSPLASH_KEY_STORAGE = "florio_unsplash_access_key";

  const AVATAR_COLORS = {
    monstera:  "#2d9a4e", fern:      "#5bb87a", peace:     "#a8b5c8",
    snake:     "#d4b83e", cactus:    "#c0392b", palm:      "#2980b9",
    bushy:     "#8e44ad", hanging:   "#16a085", orchid:    "#c2185b",
    aloevera:  "#7cb342", succulent: "#e67e22", bonsai:    "#6d4c41",
    anthurium: "#e53935", prayer:    "#00838f", dracaena:  "#546e7a",
    ficus:     "#388e3c", banana:    "#f9a825", clover:    "#00897b",
    coin:      "#9e9d24",
  };

  // ─── Rive previews (lazy import, cached at module scope) ─────────────────────

  let RiveLib = null;

  async function ensureRiveLib() {
    if (RiveLib) return RiveLib;
    try {
      const mod = await import("https://esm.sh/@rive-app/canvas@2.17.3");
      const Rive = mod.Rive || (mod.default && mod.default.Rive);
      const Layout = mod.Layout || (mod.default && mod.default.Layout);
      const Fit = mod.Fit || (mod.default && mod.default.Fit);
      const Alignment = mod.Alignment || (mod.default && mod.default.Alignment);
      RiveLib = { Rive, Layout, Fit, Alignment };
    } catch (err) {
      console.error("[Rive] runtime import FAILED:", err);
      RiveLib = { Rive: null };
    }
    return RiveLib;
  }

  // Mounts a Rive canvas (or a letter placeholder) for one avatar.
  function RivePreview({ id }) {
    const slotRef = useRef(null);
    useEffect(() => {
      let cancelled = false;
      let riveInstance = null;
      const name = DISPLAY_NAMES[id] || id;
      const placeholder = () => {
        if (slotRef.current) {
          slotRef.current.innerHTML =
            '<div style="width:100%;height:100%;display:grid;place-items:center;font-family:var(--font-sans);font-weight:700;font-size:22px;color:var(--text-muted);background:var(--surface-sunken);border-radius:10px;">' +
            (name[0] || "?") + "</div>";
        }
      };
      (async () => {
        const lib = await ensureRiveLib();
        if (cancelled || !slotRef.current) return;
        if (!lib.Rive) { placeholder(); return; }
        const canvas = document.createElement("canvas");
        canvas.width = 160; canvas.height = 160;
        canvas.style.width = "100%"; canvas.style.height = "100%";
        slotRef.current.innerHTML = "";
        slotRef.current.appendChild(canvas);
        const src = "../rive/" + id + "/" + id + "_4.riv";
        try {
          riveInstance = new lib.Rive({
            src, canvas, autoplay: true, stateMachines: "State Machine 1",
            layout: new lib.Layout({ fit: lib.Fit.Contain, alignment: lib.Alignment.Center }),
            onLoadError: () => placeholder(),
          });
        } catch (err) {
          console.error("[Rive] constructor threw for", id, err);
          placeholder();
        }
      })();
      return () => {
        cancelled = true;
        try { if (riveInstance) riveInstance.cleanup(); } catch (err) { /* noop */ }
      };
    }, [id]);
    return e("div", { ref: slotRef, style: { width: 64, height: 64, display: "grid", placeItems: "center" } });
  }

  // ─── Unsplash helpers ────────────────────────────────────────────────────────

  function getUnsplashKey() {
    let key = localStorage.getItem(UNSPLASH_KEY_STORAGE);
    if (!key) {
      key = prompt(
        "Unsplash Access Key required.\n\n" +
          "1. Sign up at https://unsplash.com/developers\n" +
          "2. Create an app, copy the Access Key (starts with a long alphanumeric string)\n" +
          "3. Paste it below — it stays in your browser only.",
      );
      if (key) { localStorage.setItem(UNSPLASH_KEY_STORAGE, key.trim()); return key.trim(); }
      return null;
    }
    return key;
  }

  async function searchUnsplash(query, perPage, accessKey) {
    const url =
      "https://api.unsplash.com/search/photos?query=" + encodeURIComponent(query) +
      "&per_page=" + perPage + "&orientation=portrait&content_filter=high";
    const res = await fetch(url, {
      headers: { Authorization: "Client-ID " + accessKey, "Accept-Version": "v1" },
    });
    if (!res.ok) {
      const body = await res.text();
      throw new Error("Unsplash " + res.status + ": " + body.slice(0, 120));
    }
    const json = await res.json();
    return (json.results || []).map((p) => ({
      id: p.id,
      photoUrl: p.urls.regular,
      photographer: (p.user && p.user.name) || null,
      photographerUrl: (p.user && p.user.links && p.user.links.html) || null,
    }));
  }

  function readFileAsDataUrl(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsDataURL(file);
    });
  }

  // ─── PCA via dual method (N×N gram matrix, since N << 768) ───────────────────

  function pcaProject3D(embeddings) {
    const N = embeddings.length;
    const D = embeddings[0].length;
    const mean = new Float64Array(D);
    for (const emb of embeddings) for (let d = 0; d < D; d++) mean[d] += emb[d];
    for (let d = 0; d < D; d++) mean[d] /= N;
    const centered = embeddings.map((emb) => {
      const c = new Float64Array(D);
      for (let d = 0; d < D; d++) c[d] = emb[d] - mean[d];
      return c;
    });
    const K = [];
    for (let i = 0; i < N; i++) K[i] = new Float64Array(N);
    for (let i = 0; i < N; i++) {
      for (let j = i; j < N; j++) {
        let dot = 0;
        for (let d = 0; d < D; d++) dot += centered[i][d] * centered[j][d];
        K[i][j] = dot; K[j][i] = dot;
      }
    }
    const { vectors, values } = topEigenvectors(K, 3);
    const result = [];
    for (let i = 0; i < N; i++) {
      result.push({
        x: Math.sqrt(Math.abs(values[0])) * vectors[0][i],
        y: Math.sqrt(Math.abs(values[1])) * vectors[1][i],
        z: Math.sqrt(Math.abs(values[2])) * vectors[2][i],
      });
    }
    return result;
  }

  function topEigenvectors(K, count) {
    const N = K.length;
    const vectors = [];
    const values = [];
    const mat = K.map((row) => Float64Array.from(row));
    for (let c = 0; c < count; c++) {
      let v = new Float64Array(N);
      for (let i = 0; i < N; i++) v[i] = Math.random() - 0.5;
      for (let iter = 0; iter < 300; iter++) {
        const next = new Float64Array(N);
        for (let i = 0; i < N; i++) {
          let s = 0;
          for (let j = 0; j < N; j++) s += mat[i][j] * v[j];
          next[i] = s;
        }
        let norm = 0;
        for (let i = 0; i < N; i++) norm += next[i] * next[i];
        norm = Math.sqrt(norm);
        if (norm < 1e-10) break;
        for (let i = 0; i < N; i++) next[i] /= norm;
        v = next;
      }
      let lambda = 0;
      for (let i = 0; i < N; i++) {
        let s = 0;
        for (let j = 0; j < N; j++) s += mat[i][j] * v[j];
        lambda += v[i] * s;
      }
      vectors.push(v);
      values.push(lambda);
      for (let i = 0; i < N; i++)
        for (let j = 0; j < N; j++)
          mat[i][j] -= lambda * v[i] * v[j];
    }
    return { vectors, values };
  }

  // ─── Three.js 3D scatter ──────────────────────────────────────────────────────

  let threeLib = null;
  async function loadThree() {
    if (threeLib) return threeLib;
    const [THREE, ctrl] = await Promise.all([
      import("https://esm.sh/three@0.164.1"),
      import("https://esm.sh/three@0.164.1/examples/jsm/controls/OrbitControls.js"),
    ]);
    threeLib = { THREE, OrbitControls: ctrl.OrbitControls };
    return threeLib;
  }

  function makeGlowTexture(color, size) {
    const canvas = document.createElement("canvas");
    canvas.width = size; canvas.height = size;
    const ctx = canvas.getContext("2d");
    const half = size / 2;
    const grad = ctx.createRadialGradient(half, half, 0, half, half, half);
    grad.addColorStop(0, color + "ff");
    grad.addColorStop(0.15, color + "cc");
    grad.addColorStop(0.4, color + "44");
    grad.addColorStop(0.7, color + "11");
    grad.addColorStop(1, color + "00");
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, size, size);
    const core = ctx.createRadialGradient(half, half, 0, half, half, half * 0.2);
    core.addColorStop(0, "#ffffff");
    core.addColorStop(1, color + "00");
    ctx.fillStyle = core;
    ctx.fillRect(0, 0, size, size);
    return canvas;
  }

  // Imperatively builds the Three.js scene into `container`. Returns a cleanup fn.
  async function renderScatter3D(container, tooltipEl, points, coords) {
    const { THREE, OrbitControls } = await loadThree();

    const W = container.clientWidth || 760;
    const H = 560;
    container.style.height = H + "px";

    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
    for (const c of coords) {
      if (c.x < minX) minX = c.x; if (c.x > maxX) maxX = c.x;
      if (c.y < minY) minY = c.y; if (c.y > maxY) maxY = c.y;
      if (c.z < minZ) minZ = c.z; if (c.z > maxZ) maxZ = c.z;
    }
    const rX = maxX - minX || 1, rY = maxY - minY || 1, rZ = maxZ - minZ || 1;
    const SCALE = 4;
    const norm = coords.map((c) => ({
      x: ((c.x - minX) / rX - 0.5) * SCALE,
      y: ((c.y - minY) / rY - 0.5) * SCALE,
      z: ((c.z - minZ) / rZ - 0.5) * SCALE,
    }));

    const scene = new THREE.Scene();
    scene.background = new THREE.Color("#0a0e1a");
    scene.fog = new THREE.FogExp2(0x0a0e1a, 0.04);

    const camera = new THREE.PerspectiveCamera(50, W / H, 0.1, 100);
    camera.position.set(SCALE * 0.9, SCALE * 0.7, SCALE * 1.1);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    renderer.setSize(W, H);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.2;
    container.prepend(renderer.domElement);
    renderer.domElement.style.borderRadius = "var(--radius-lg, 8px)";

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.minDistance = 1;
    controls.maxDistance = 20;
    controls.target.set(0, 0, 0);
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.4;

    const boxGeo = new THREE.BoxGeometry(SCALE, SCALE, SCALE);
    const boxEdges = new THREE.EdgesGeometry(boxGeo);
    scene.add(new THREE.LineSegments(boxEdges, new THREE.LineBasicMaterial({ color: 0x1a5fff, opacity: 0.18, transparent: true })));

    const gridMat = new THREE.LineBasicMaterial({ color: 0x1a5fff, opacity: 0.07, transparent: true });
    const half = SCALE / 2;
    const divisions = 8;
    const step = SCALE / divisions;
    for (let i = 1; i < divisions; i++) {
      const offset = -half + i * step;
      scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(offset, -half, -half), new THREE.Vector3(offset, half, -half)]), gridMat));
      scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(-half, offset, -half), new THREE.Vector3(half, offset, -half)]), gridMat));
      scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(offset, -half, -half), new THREE.Vector3(offset, -half, half)]), gridMat));
      scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(-half, -half, offset), new THREE.Vector3(half, -half, offset)]), gridMat));
    }

    const axisLen = SCALE * 0.55;
    const axisColors = [0x3388ff, 0x33aaff, 0x2266dd];
    for (let a = 0; a < 3; a++) {
      const dir = [[1, 0, 0], [0, 1, 0], [0, 0, 1]][a];
      const geo = new THREE.BufferGeometry().setFromPoints([
        new THREE.Vector3(-dir[0] * axisLen, -dir[1] * axisLen, -dir[2] * axisLen),
        new THREE.Vector3(dir[0] * axisLen, dir[1] * axisLen, dir[2] * axisLen),
      ]);
      scene.add(new THREE.Line(geo, new THREE.LineBasicMaterial({ color: axisColors[a], opacity: 0.3, transparent: true })));
    }

    const glowTextureCache = {};
    function getGlowTexture(hex) {
      if (!glowTextureCache[hex]) {
        const tex = new THREE.CanvasTexture(makeGlowTexture(hex, 128));
        tex.needsUpdate = true;
        glowTextureCache[hex] = tex;
      }
      return glowTextureCache[hex];
    }

    const sprites = [];
    for (let i = 0; i < norm.length; i++) {
      const color = AVATAR_COLORS[points[i].avatar] || "#999999";
      const mat = new THREE.SpriteMaterial({ map: getGlowTexture(color), transparent: true, opacity: 0.85, depthWrite: false, blending: THREE.AdditiveBlending });
      const sprite = new THREE.Sprite(mat);
      sprite.position.set(norm[i].x, norm[i].y, norm[i].z);
      sprite.scale.setScalar(0.28);
      sprite.userData = { index: i, baseScale: 0.28 };
      scene.add(sprite);
      sprites.push(sprite);
    }

    const coreGeo = new THREE.SphereGeometry(0.04, 8, 6);
    const meshes = [];
    for (let i = 0; i < norm.length; i++) {
      const color = AVATAR_COLORS[points[i].avatar] || "#999999";
      const mesh = new THREE.Mesh(coreGeo, new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.0 }));
      mesh.position.set(norm[i].x, norm[i].y, norm[i].z);
      mesh.userData = { index: i };
      scene.add(mesh);
      meshes.push(mesh);
    }

    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    let hoveredIdx = -1;

    const onMove = (ev) => {
      const rect = renderer.domElement.getBoundingClientRect();
      mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
      mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(mouse, camera);
      const hits = raycaster.intersectObjects(meshes);
      if (hits.length > 0) {
        const idx = hits[0].object.userData.index;
        if (idx !== hoveredIdx) {
          if (hoveredIdx >= 0) sprites[hoveredIdx].scale.setScalar(sprites[hoveredIdx].userData.baseScale);
          hoveredIdx = idx;
          sprites[idx].scale.setScalar(sprites[idx].userData.baseScale * 2);
          const pt = points[idx];
          const name = DISPLAY_NAMES[pt.avatar] || pt.avatar;
          tooltipEl.innerHTML =
            '<img src="' + pt.photoUrl + '" alt="" style="width:54px;height:54px;border-radius:8px;object-fit:cover;flex-shrink:0;">' +
            '<div><strong style="font-family:var(--font-sans);font-size:13px;color:#fff;">' + name + "</strong>" +
            '<div style="font-family:var(--font-sans);font-size:11px;color:rgba(255,255,255,0.7);margin-top:2px;">' + pt.species + "</div></div>";
          tooltipEl.style.display = "flex";
        }
        const tx = ev.clientX - rect.left + 14;
        const ty = ev.clientY - rect.top - 16;
        tooltipEl.style.left = Math.min(tx, rect.width - 250) + "px";
        tooltipEl.style.top = Math.max(ty, 8) + "px";
        renderer.domElement.style.cursor = "pointer";
      } else {
        if (hoveredIdx >= 0) { sprites[hoveredIdx].scale.setScalar(sprites[hoveredIdx].userData.baseScale); hoveredIdx = -1; }
        tooltipEl.style.display = "none";
        renderer.domElement.style.cursor = "grab";
      }
    };
    const onLeave = () => {
      if (hoveredIdx >= 0) { sprites[hoveredIdx].scale.setScalar(sprites[hoveredIdx].userData.baseScale); hoveredIdx = -1; }
      tooltipEl.style.display = "none";
    };
    renderer.domElement.addEventListener("mousemove", onMove);
    renderer.domElement.addEventListener("mouseleave", onLeave);

    let animId;
    const clock = new THREE.Clock();
    function animate() {
      animId = requestAnimationFrame(animate);
      controls.update();
      const t = clock.getElapsedTime();
      for (let i = 0; i < sprites.length; i++) {
        if (i === hoveredIdx) continue;
        const pulse = 1 + Math.sin(t * 1.5 + i * 0.7) * 0.06;
        sprites[i].scale.setScalar(sprites[i].userData.baseScale * pulse);
      }
      renderer.render(scene, camera);
    }
    animate();

    // Returns the legend filter applier + cleanup, so the React legend can drive it.
    return {
      sprites, meshes, points,
      cleanup: () => {
        cancelAnimationFrame(animId);
        renderer.domElement.removeEventListener("mousemove", onMove);
        renderer.domElement.removeEventListener("mouseleave", onLeave);
        try { renderer.dispose(); } catch (err) { /* noop */ }
        try { controls.dispose(); } catch (err) { /* noop */ }
      },
    };
  }

  // ─── Shared style helpers (match the other ported tabs) ──────────────────────

  const cardStyle = {
    background: "var(--surface-card)", border: "1px solid var(--border-subtle)",
    borderRadius: "var(--radius-2xl)", boxShadow: "var(--shadow-card)", padding: 22,
  };
  const secondaryBtn = (extra) => Object.assign({
    padding: "8px 14px", borderRadius: "var(--radius-md)", border: "1px solid var(--border-default)",
    background: "var(--surface-card)", color: "var(--text-secondary)", cursor: "pointer",
    fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: 13, whiteSpace: "nowrap",
  }, extra || {});

  function countTier(n) { return n < 20 ? "low" : n < 50 ? "ok" : "good"; }
  const TIER_COLOR = { low: "#E0584F", ok: "#E0A23F", good: "#3FA45B" };

  // ─── Main component ──────────────────────────────────────────────────────────

  function Labeling() {
    const PA = window.PA;
    const ChartCard = PA.ChartCard;

    const [ready, setReady] = useState(false);

    // Source tab: "unsplash" (default queue) or "user_scan" (user_scan items).
    const [activeSource, setActiveSource] = useState("unsplash");
    const activeSourceRef = useRef("unsplash");
    activeSourceRef.current = activeSource;

    const [currentItem, setCurrentItem] = useState(null);
    const currentItemRef = useRef(null);
    currentItemRef.current = currentItem;
    const [photoError, setPhotoError] = useState(false);
    const [photoLoading, setPhotoLoading] = useState(true);

    const [counts, setCounts] = useState({
      labeledCount: 0, queueTotal: 0,
      userScanLabeledCount: 0, userScanQueueTotal: 0,
      pendingUserScans: 0, perAvatar: {},
    });
    const countsRef = useRef(counts);
    countsRef.current = counts;

    const [progressMeta, setProgressMeta] = useState("Loading queue…");
    const [flashId, setFlashId] = useState(null);

    // Seed UI
    const [seedOpen, setSeedOpen] = useState(false);
    const [customQuery, setCustomQuery] = useState("");
    const [seedBusy, setSeedBusy] = useState(false);

    // Button transient labels
    const [purgeLabel, setPurgeLabel] = useState("Purge non-Unsplash");
    const [purgeBusy, setPurgeBusy] = useState(false);
    const [embedOneLabel, setEmbedOneLabel] = useState("Test embed 1");
    const [embedAllLabel, setEmbedAllLabel] = useState("Re-embed all");
    const [embedBusy, setEmbedBusy] = useState(false);

    const uploadRef = useRef(null);

    // ── Firestore helpers (lazy — window.FlorioDB available after FlorioReady) ──
    const FB = () => window.FlorioDB;

    // ── refreshProgress ──
    const refreshProgress = useCallback(async () => {
      const { db, collection, getDocs } = FB();
      try {
        const [labelsSnap, queueSnap] = await Promise.all([
          getDocs(collection(db, "avatar_labels")),
          getDocs(collection(db, "labeling_queue")),
        ]);
        let labeledCount = 0, userScanLabeledCount = 0;
        const perAvatar = {};
        labelsSnap.forEach((d) => {
          const data = d.data();
          if (data.skipped || data.notHouseplant) return;
          if (data.source === "user_scan" || data.source === "watering_photo") userScanLabeledCount += 1;
          else labeledCount += 1;
          if (data.chosenAvatar) perAvatar[data.chosenAvatar] = (perAvatar[data.chosenAvatar] || 0) + 1;
        });
        let pendingQueue = 0, pendingUserScans = 0;
        queueSnap.forEach((d) => {
          const data = d.data();
          if (data.status !== "pending") return;
          if (data.userScan === true) pendingUserScans += 1;
          else pendingQueue += 1;
        });
        setCounts({
          labeledCount, userScanLabeledCount,
          queueTotal: labeledCount + pendingQueue,
          userScanQueueTotal: userScanLabeledCount + pendingUserScans,
          pendingUserScans, perAvatar,
        });
      } catch (err) {
        console.error("[Labeling] refreshProgress failed:", err);
        setProgressMeta("Failed: " + err.message);
      }
    }, []);

    // ── loadNext ──
    const loadNext = useCallback(async () => {
      const { db, collection, query, where, limit, getDocs } = FB();
      const isUserScan = activeSourceRef.current === "user_scan";
      setPhotoError(false);
      setPhotoLoading(true);
      try {
        const filters = [where("status", "==", "pending")];
        if (isUserScan) filters.push(where("userScan", "==", true));
        const q = query(collection(db, "labeling_queue"), ...filters, limit(1));
        const snap = await getDocs(q);
        const match = snap.docs[0] || null;
        if (!match) {
          setCurrentItem(null);
          setPhotoLoading(false);
          return;
        }
        setCurrentItem(Object.assign({ id: match.id }, match.data(), { _source: activeSourceRef.current }));
      } catch (err) {
        console.error("[Labeling] loadNext failed:", err);
        setCurrentItem({ _error: err.message });
        setPhotoLoading(false);
      }
    }, []);

    // ── submitLabel ──
    const submitLabel = useCallback(async (avatarId) => {
      const item = currentItemRef.current;
      if (!item || !item.id) return;
      const { db, collection, doc, writeBatch, serverTimestamp } = FB();
      const user = window.FlorioDB.auth.currentUser;
      if (!user) { console.warn("[Labeling] no authed user"); return; }
      try {
        const batch = writeBatch(db);
        const labelRef = doc(collection(db, "avatar_labels"));
        const photoUrl = item.photoDataUrl || item.photoUrl;
        batch.set(labelRef, {
          labelId: labelRef.id, photoUrl, chosenAvatar: avatarId, labelerId: user.uid,
          species: item.species || null, source: item.source || "manual",
          sourceQueueItemId: item.id, skipped: false, notHouseplant: false,
          embedding: null, createdAt: serverTimestamp(),
        });
        const queueRef = doc(db, "labeling_queue", item.id);
        batch.set(queueRef, { status: "labeled", labeledAt: serverTimestamp() }, { merge: true });
        await batch.commit();

        const isUserScan = activeSourceRef.current === "user_scan";
        const prev = countsRef.current;
        setCounts(Object.assign({}, prev, {
          labeledCount: prev.labeledCount + (isUserScan ? 0 : 1),
          userScanLabeledCount: prev.userScanLabeledCount + (isUserScan ? 1 : 0),
          perAvatar: Object.assign({}, prev.perAvatar, { [avatarId]: (prev.perAvatar[avatarId] || 0) + 1 }),
        }));

        setFlashId(avatarId);
        setTimeout(() => setFlashId((f) => (f === avatarId ? null : f)), 350);
        await loadNext();
      } catch (err) {
        console.error("[Labeling] submitLabel failed:", err);
        alert("Save failed: " + err.message);
      }
    }, [loadNext]);

    // ── skipItem ──
    const skipItem = useCallback(async (reason) => {
      const item = currentItemRef.current;
      if (!item || !item.id) return;
      const { db, doc, setDoc, serverTimestamp } = FB();
      try {
        const status = reason === "not_houseplant" ? "rejected" : "no_match";
        const queueRef = doc(db, "labeling_queue", item.id);
        await setDoc(queueRef, { status, resolvedAt: serverTimestamp() }, { merge: true });
        await loadNext();
      } catch (err) {
        console.error("[Labeling] skip failed:", err);
      }
    }, [loadNext]);

    // ── handleUpload ──
    const handleUpload = useCallback(async (ev) => {
      const files = Array.from(ev.target.files || []);
      if (!files.length) return;
      const { db, collection, doc, setDoc, serverTimestamp } = FB();
      const origMeta = progressMeta;
      setProgressMeta("Uploading " + files.length + " photo" + (files.length > 1 ? "s" : "") + "…");
      let added = 0;
      for (const file of files) {
        try {
          const dataUrl = await readFileAsDataUrl(file);
          const ref = doc(collection(db, "labeling_queue"));
          await setDoc(ref, {
            queueId: ref.id, photoDataUrl: dataUrl,
            species: file.name.replace(/\.[^.]+$/, "").replace(/[-_]/g, " "),
            source: "manual_upload", status: "pending", createdAt: serverTimestamp(),
          });
          added += 1;
          setProgressMeta("Uploading… " + added + "/" + files.length);
        } catch (err) {
          console.error("[Labeling] upload failed for", file.name, err);
        }
      }
      setProgressMeta(added > 0 ? "Uploaded " + added + " photo" + (added > 1 ? "s" : "") : "Upload failed");
      ev.target.value = "";
      await refreshProgress();
      if (!currentItemRef.current || !currentItemRef.current.id) await loadNext();
      setTimeout(() => setProgressMeta(origMeta), 5000);
    }, [progressMeta, refreshProgress, loadNext]);

    // ── runSeed (Unsplash) ──
    const runSeed = useCallback(async (category, customQueries) => {
      const key = getUnsplashKey();
      if (!key) return;
      setSeedBusy(true);
      const { db, collection, doc, setDoc, getDocs, serverTimestamp } = FB();
      const queries = customQueries
        || (category === "all"
            ? Object.values(SEED_CATEGORY_QUERIES).flat()
            : SEED_CATEGORY_QUERIES[category] || [category + " houseplant indoor"]);
      const origMeta = progressMeta;
      setProgressMeta("Seeding…");
      try {
        const existingIds = new Set();
        const [queueSnap, labelsSnap] = await Promise.all([
          getDocs(collection(db, "labeling_queue")),
          getDocs(collection(db, "avatar_labels")),
        ]);
        queueSnap.forEach((d) => { const id = d.data().unsplashId; if (id) existingIds.add(id); });
        labelsSnap.forEach((d) => { const id = d.data().unsplashId; if (id) existingIds.add(id); });

        let added = 0, duplicates = 0, queryFailed = 0, rateLimited = false;
        for (let qi = 0; qi < queries.length; qi++) {
          const q = queries[qi];
          let photos;
          try {
            photos = await searchUnsplash(q, PHOTOS_PER_QUERY, key);
          } catch (err) {
            queryFailed += 1;
            if (String(err.message).includes("429") || String(err.message).includes("Rate Limit")) { rateLimited = true; break; }
            console.warn('[Labeling] search failed for "' + q + '":', err);
            continue;
          }
          for (const photo of photos) {
            if (existingIds.has(photo.id)) { duplicates += 1; continue; }
            try {
              const ref = doc(collection(db, "labeling_queue"));
              await setDoc(ref, {
                queueId: ref.id, photoUrl: photo.photoUrl, species: q, source: "unsplash",
                unsplashId: photo.id, photographer: photo.photographer, photographerUrl: photo.photographerUrl,
                status: "pending", createdAt: serverTimestamp(),
              });
              existingIds.add(photo.id);
              added += 1;
            } catch (err) {
              console.warn("[Labeling] write failed for", photo.photoUrl, err);
            }
          }
          setProgressMeta("Seeding… " + added + " added (" + (qi + 1) + "/" + queries.length + ")");
        }

        let result;
        if (added > 0) {
          result = "Seeded " + added + " photos";
          if (duplicates) result += " · " + duplicates + " duplicates skipped";
          if (rateLimited) result += " · rate-limited";
        } else if (rateLimited) result = "Rate limited — try again in an hour";
        else if (duplicates > 0) result = "All " + duplicates + " photos already exist";
        else result = "No photos returned (" + queryFailed + " failed)";
        setProgressMeta(result);

        await refreshProgress();
        if (!currentItemRef.current || !currentItemRef.current.id) await loadNext();
      } catch (err) {
        console.error("[Labeling] seed run failed:", err);
        if (String(err.message).includes("401") || String(err.message).includes("Unauthorized")) {
          localStorage.removeItem(UNSPLASH_KEY_STORAGE);
          setProgressMeta("Bad Unsplash key — try seeding again");
        } else {
          setProgressMeta("Seed failed");
        }
      } finally {
        setSeedBusy(false);
        setTimeout(() => setProgressMeta(origMeta), 8000);
      }
    }, [progressMeta, refreshProgress, loadNext]);

    const onSeedSelect = useCallback((val) => {
      setSeedOpen(false);
      if (val === "custom") { setSeedOpen("custom"); return; }
      if (val) runSeed(val);
    }, [runSeed]);

    const submitCustomSeed = useCallback(() => {
      const q = customQuery.trim();
      if (!q) return;
      runSeed("custom", [q + " indoor plant"]);
      setCustomQuery("");
      setSeedOpen(false);
    }, [customQuery, runSeed]);

    // ── purgeNonUnsplash ──
    const purgeNonUnsplash = useCallback(async () => {
      const { db, collection, query, where, getDocs, writeBatch } = FB();
      setPurgeBusy(true);
      setPurgeLabel("Scanning…");
      let snap;
      try {
        snap = await getDocs(query(collection(db, "labeling_queue"), where("status", "==", "pending")));
      } catch (err) {
        console.error("[Labeling] purge scan failed:", err);
        setPurgeLabel("Scan failed");
        setTimeout(() => { setPurgeBusy(false); setPurgeLabel("Purge non-Unsplash"); }, 4000);
        return;
      }
      const toDelete = [];
      snap.forEach((d) => { if (d.data().source !== "unsplash") toDelete.push(d.ref); });
      if (toDelete.length === 0) {
        setPurgeLabel("Nothing to purge");
        setTimeout(() => { setPurgeBusy(false); setPurgeLabel("Purge non-Unsplash"); }, 4000);
        return;
      }
      if (!confirm("Delete " + toDelete.length + " non-Unsplash pending item(s)?")) {
        setPurgeBusy(false); setPurgeLabel("Purge non-Unsplash");
        return;
      }
      setPurgeLabel("Deleting " + toDelete.length + "…");
      let deleted = 0;
      for (let i = 0; i < toDelete.length; i += 500) {
        const batch = writeBatch(db);
        toDelete.slice(i, i + 500).forEach((ref) => batch.delete(ref));
        await batch.commit();
        deleted += Math.min(500, toDelete.length - i);
        setPurgeLabel("Deleted " + deleted + " / " + toDelete.length + "…");
      }
      setPurgeLabel("Purged " + deleted + " items");
      await refreshProgress();
      if (!currentItemRef.current || !currentItemRef.current.id) await loadNext();
      setTimeout(() => { setPurgeBusy(false); setPurgeLabel("Purge non-Unsplash"); }, 5000);
    }, [refreshProgress, loadNext]);

    // ── runReEmbed ──
    const runReEmbed = useCallback(async (mode) => {
      const isOne = mode === "one";
      const baseLabel = isOne ? "Test embed 1" : "Re-embed all";
      const setLabel = isOne ? setEmbedOneLabel : setEmbedAllLabel;
      const user = window.FlorioDB.auth.currentUser;
      if (!user) { alert("Not signed in"); return; }
      if (!isOne && !confirm("Re-embed all labeled photos via CLIP? Skips Wikipedia URLs. May take a few minutes.")) return;
      setEmbedBusy(true);
      try {
        const token = await user.getIdToken();
        if (isOne) {
          setLabel("Embedding 1…");
          const res = await fetch(FUNCTIONS_BASE + "/reEmbedLabels?mode=one", { headers: { Authorization: "Bearer " + token } });
          const data = await res.json();
          if (!res.ok) throw new Error(data.error || "HTTP " + res.status);
          setLabel(data.ok + " ok, " + data.failed + " fail, " + data.skipped + " skip");
          if (data.ok > 0) refreshClusterRef.current && refreshClusterRef.current();
        } else {
          let totalOk = 0, totalFail = 0, totalSkip = 0;
          while (true) {
            setLabel("Re-embedding… " + totalOk + " done");
            const res = await fetch(FUNCTIONS_BASE + "/reEmbedLabels?mode=one", { headers: { Authorization: "Bearer " + token } });
            const data = await res.json();
            if (!res.ok) throw new Error(data.error || "HTTP " + res.status);
            totalOk += data.ok; totalFail += data.failed; totalSkip += data.skipped;
            if (data.ok === 0 && data.failed === 0) break;
          }
          setLabel("Done: " + totalOk + " ok, " + totalFail + " fail, " + totalSkip + " skip");
          if (totalOk > 0) refreshClusterRef.current && refreshClusterRef.current();
        }
      } catch (err) {
        console.error("[Labeling] re-embed failed:", err);
        setLabel("Failed: " + err.message);
      } finally {
        setTimeout(() => { setEmbedBusy(false); setLabel(baseLabel); }, 8000);
      }
    }, []);

    // ── Cluster viz ──
    const clusterCanvasRef = useRef(null);
    const clusterTooltipRef = useRef(null);
    const scene3DRef = useRef(null);
    const refreshClusterRef = useRef(null);
    const [clusterMeta, setClusterMeta] = useState("");
    const [clusterMsg, setClusterMsg] = useState("Loading embeddings…");
    const [clusterBusy, setClusterBusy] = useState(false);
    const [clusterLegend, setClusterLegend] = useState([]); // [{id,color,name}]
    const [clusterFilter, setClusterFilter] = useState(null);

    const renderClusterViz = useCallback(async () => {
      const { db, collection, getDocs } = FB();
      setClusterBusy(true);
      // tear down any previous scene
      if (scene3DRef.current) { try { scene3DRef.current.cleanup(); } catch (err) { /* noop */ } scene3DRef.current = null; }
      if (clusterCanvasRef.current) {
        // remove any leftover <canvas> the renderer prepended
        const old = clusterCanvasRef.current.querySelector("canvas");
        if (old) old.remove();
      }
      setClusterLegend([]);
      setClusterFilter(null);
      try {
        const snap = await getDocs(collection(db, "avatar_labels"));
        const withEmbedding = [];
        let pendingCount = 0;
        snap.forEach((d) => {
          const data = d.data();
          if (data.skipped || data.notHouseplant) return;
          if (data.embeddingVersion !== "v2") { pendingCount++; return; }
          const emb = data.embedding;
          if (Array.isArray(emb) && emb.length > 2) {
            withEmbedding.push({ embedding: emb, avatar: data.chosenAvatar, species: data.species || "unknown", photoUrl: data.photoUrl });
          } else {
            pendingCount++;
          }
        });
        if (withEmbedding.length < 3) {
          setClusterMeta("");
          setClusterMsg("Need at least 3 labels with embeddings. " + withEmbedding.length +
            " have embeddings" + (pendingCount > 0 ? ", " + pendingCount + " awaiting embedding" : "") +
            ". Make sure the Replicate API token is configured and the onLabelCreated function is deployed.");
          return;
        }
        setClusterMsg(null);
        setClusterMeta(withEmbedding.length + " plotted" + (pendingCount > 0 ? " · " + pendingCount + " pending" : ""));
        const coords = pcaProject3D(withEmbedding.map((p) => p.embedding));
        // build the scene after the message clears (next tick so the canvas div exists)
        await new Promise((r) => setTimeout(r, 0));
        if (!clusterCanvasRef.current) return;
        const handle = await renderScatter3D(clusterCanvasRef.current, clusterTooltipRef.current, withEmbedding, coords);
        scene3DRef.current = handle;
        const seen = new Set(withEmbedding.map((p) => p.avatar));
        setClusterLegend(AVATAR_ORDER.filter((id) => seen.has(id)).map((id) => ({ id, color: AVATAR_COLORS[id] || "#999", name: DISPLAY_NAMES[id] || id })));
      } catch (err) {
        console.error("[Cluster] render failed:", err);
        setClusterMsg("Failed to load: " + err.message);
      } finally {
        setClusterBusy(false);
      }
    }, []);
    refreshClusterRef.current = renderClusterViz;

    // Apply legend click-to-filter to the live Three.js sprites/meshes.
    const applyClusterFilter = useCallback((avatarId) => {
      setClusterFilter((cur) => {
        const next = cur === avatarId ? null : avatarId;
        const handle = scene3DRef.current;
        if (handle) {
          for (let i = 0; i < handle.sprites.length; i++) {
            const match = !next || handle.points[i].avatar === next;
            handle.sprites[i].visible = match;
            handle.meshes[i].visible = match;
          }
        }
        return next;
      });
    }, []);

    // ── Boot: wait for FlorioReady, then load everything ──
    useEffect(() => {
      let alive = true;
      (async () => {
        await window.FlorioReady;
        if (!alive) return;
        setReady(true);
        await refreshProgress();
        await loadNext();
        renderClusterViz();
      })();
      return () => {
        alive = false;
        if (scene3DRef.current) { try { scene3DRef.current.cleanup(); } catch (err) { /* noop */ } }
      };
    }, []); // eslint-disable-line

    // Reload the queue whenever the source tab changes (after initial boot).
    useEffect(() => {
      if (!ready) return;
      loadNext();
    }, [activeSource]); // eslint-disable-line

    // ─── Derived progress display ───────────────────────────────────────────────
    const isUserScan = activeSource === "user_scan";
    let progressCount, progressFillPct, derivedMeta;
    if (isUserScan) {
      progressCount = counts.userScanLabeledCount + " / " + counts.userScanQueueTotal + " labeled";
      progressFillPct = counts.userScanQueueTotal > 0 ? Math.min(100, (counts.userScanLabeledCount / counts.userScanQueueTotal) * 100) : 0;
      const pending = counts.userScanQueueTotal - counts.userScanLabeledCount;
      derivedMeta = pending > 0 ? pending + " user scans waiting" : "All user scans labeled";
    } else {
      const targetForBeta = 1000;
      progressCount = counts.labeledCount + " / " + counts.queueTotal + " labeled";
      progressFillPct = Math.min(100, (counts.labeledCount / targetForBeta) * 100);
      if (counts.labeledCount >= targetForBeta) derivedMeta = "Beta target reached";
      else if (counts.labeledCount >= 500) derivedMeta = "Usable quality · " + (targetForBeta - counts.labeledCount) + " more to beta";
      else if (counts.labeledCount >= 200) derivedMeta = "k-NN starts working · target " + targetForBeta;
      else derivedMeta = "Warming up · first ML threshold at 200";
    }
    // progressMeta holds transient messages (uploading/seeding); otherwise show the derived status.
    const showMeta = (progressMeta && progressMeta !== "Loading queue…") ? progressMeta : derivedMeta;
    const maxPerAvatar = Math.max(1, ...Object.values(counts.perAvatar));

    // ─── Photo panel content ──────────────────────────────────────────────────
    const imgSrc = currentItem && (currentItem.photoDataUrl || currentItem.photoUrl);
    let sourceLine = null;
    if (currentItem && currentItem.id) {
      if (currentItem.source === "unsplash" && currentItem.photographer) {
        sourceLine = e("span", null,
          e("span", null, currentItem.species || "unknown"), " · photo by ",
          currentItem.photographerUrl
            ? e("a", { href: currentItem.photographerUrl + "?utm_source=florio_labeling&utm_medium=referral", target: "_blank", rel: "noopener", style: { color: "var(--app-accent)" } }, currentItem.photographer)
            : currentItem.photographer,
          " on ", e("a", { href: "https://unsplash.com/?utm_source=florio_labeling&utm_medium=referral", target: "_blank", rel: "noopener", style: { color: "var(--app-accent)" } }, "Unsplash"));
      } else if (currentItem.source === "user_scan" || currentItem.source === "watering_photo") {
        const label = currentItem.source === "watering_photo" ? "watering photo" : "user scan";
        sourceLine = e("span", null,
          e("span", null, currentItem.species || "unknown"), " · " + label,
          currentItem.characterTypeId
            ? e("span", null, " · AI picked: ", e("strong", null, DISPLAY_NAMES[currentItem.characterTypeId] || currentItem.characterTypeId))
            : null);
      } else {
        sourceLine = (currentItem.species || "unknown") + " · source: " + (currentItem.source || "manual");
      }
    }
    const hasItem = !!(currentItem && currentItem.id);

    function EmptyPhoto({ title, hint }) {
      return e("div", { style: { minHeight: 360, display: "grid", placeItems: "center", textAlign: "center", padding: 24, color: "var(--text-muted)" } },
        e("div", null,
          e("p", { style: { fontFamily: "var(--font-sans)", fontSize: 15, fontWeight: 600, color: "var(--text-secondary)", margin: 0 } }, title),
          hint ? e("p", { style: { fontFamily: "var(--font-sans)", fontSize: 12, opacity: 0.7, marginTop: 6 } }, hint) : null));
    }

    let photoContent;
    if (currentItem && currentItem._error) {
      photoContent = e(EmptyPhoto, { title: "Failed to load queue.", hint: currentItem._error });
    } else if (!hasItem) {
      photoContent = e(EmptyPhoto, {
        title: "Queue empty.",
        hint: isUserScan
          ? "No user scan photos yet. They appear here when users scan a plant in the app."
          : "All pending photos have been labeled. Seed more to continue.",
      });
    } else if (photoError) {
      photoContent = e(EmptyPhoto, { title: "Image failed to load.", hint: "Skip this one." });
    } else {
      photoContent = e("img", {
        src: imgSrc, alt: currentItem.species || "plant",
        onLoad: () => setPhotoLoading(false),
        onError: () => { setPhotoError(true); setPhotoLoading(false); },
        style: { width: "100%", maxHeight: 460, objectFit: "contain", borderRadius: "var(--radius-lg)", display: "block", background: "var(--surface-sunken)" },
      });
    }

    // ─── Render ──────────────────────────────────────────────────────────────
    const eyebrow = (t) => e("div", { style: { fontFamily: "var(--font-mono-display)", fontSize: 10, letterSpacing: ".14em", textTransform: "uppercase", color: "var(--text-muted)", marginBottom: 6 } }, t);

    return e("div", { style: { display: "flex", flexDirection: "column", gap: 22 } },
      // ── Header actions row ──
      e("div", { style: { display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center", justifyContent: "flex-end" } },
        e("div", { style: { marginRight: "auto" } },
          eyebrow("Avatar k-NN training data"),
          e("div", { style: { fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text-muted)", maxWidth: 520 } },
            "Label photos with the closest-matching avatar to build the training set."),
        ),
        e("a", { href: "../gaps.html", style: secondaryBtn({ textDecoration: "none", display: "inline-flex", alignItems: "center" }) }, "View avatar gaps"),
        // Seed group (Unsplash only)
        !isUserScan && e("div", { style: { position: "relative" } },
          e("button", { onClick: () => setSeedOpen((o) => (o ? false : "menu")), disabled: seedBusy, style: secondaryBtn({ opacity: seedBusy ? 0.6 : 1 }) }, seedBusy ? "Seeding…" : "Seed plants…"),
          seedOpen === "menu" && e("div", { style: { position: "absolute", top: 42, right: 0, zIndex: 50, background: "var(--surface-card)", border: "1px solid var(--border-subtle)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-card)", padding: 8, width: 240, maxHeight: 400, overflowY: "auto" } },
            SEED_OPTIONS.map((grp) => e("div", { key: grp.group },
              e("div", { style: { fontFamily: "var(--font-mono-display)", fontSize: 9, letterSpacing: ".12em", textTransform: "uppercase", color: "var(--text-muted)", padding: "8px 8px 4px" } }, grp.group),
              grp.items.map(([val, lab]) => e("button", {
                key: val, onClick: () => onSeedSelect(val),
                style: { display: "block", width: "100%", textAlign: "left", padding: "7px 10px", borderRadius: "var(--radius-md)", border: "none", background: "transparent", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text-primary)" },
                onMouseEnter: (ev) => { ev.target.style.background = "var(--surface-sunken)"; },
                onMouseLeave: (ev) => { ev.target.style.background = "transparent"; },
              }, lab)),
            )),
          ),
          seedOpen === "custom" && e("div", { style: { position: "absolute", top: 42, right: 0, zIndex: 50, background: "var(--surface-card)", border: "1px solid var(--border-subtle)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-card)", padding: 12, width: 280, display: "flex", gap: 8 } },
            e("input", {
              type: "text", value: customQuery, autoFocus: true, placeholder: "Custom search…",
              onChange: (ev) => setCustomQuery(ev.target.value),
              onKeyDown: (ev) => { if (ev.key === "Enter") submitCustomSeed(); },
              style: { flex: 1, height: 38, borderRadius: "var(--radius-md)", border: "1px solid var(--border-default)", padding: "0 10px", fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text-primary)", background: "var(--surface-card)", outline: "none" },
            }),
            e("button", { onClick: submitCustomSeed, style: secondaryBtn({}) }, "Seed"),
          ),
        ),
        e("label", { htmlFor: "labeling-upload-input", style: secondaryBtn({ display: "inline-flex", alignItems: "center" }) }, "Upload photos"),
        e("input", { id: "labeling-upload-input", ref: uploadRef, type: "file", accept: "image/*", multiple: true, hidden: true, onChange: handleUpload }),
        !isUserScan && e("button", { onClick: purgeNonUnsplash, disabled: purgeBusy, style: secondaryBtn({ borderColor: "var(--negative, #E0584F)", color: "var(--negative, #E0584F)", opacity: purgeBusy ? 0.6 : 1 }) }, purgeLabel),
        e("button", { onClick: () => runReEmbed("one"), disabled: embedBusy, style: secondaryBtn({ opacity: embedBusy ? 0.6 : 1 }) }, embedOneLabel),
        e("button", { onClick: () => runReEmbed("all"), disabled: embedBusy, style: secondaryBtn({ opacity: embedBusy ? 0.6 : 1 }) }, embedAllLabel),
      ),

      // ── Source tabs ──
      e("div", { style: { display: "inline-flex", background: "var(--surface-sunken)", borderRadius: 999, padding: 3, gap: 2, alignSelf: "flex-start" } },
        [["unsplash", "Unsplash photos", null], ["user_scan", "User scans", counts.pendingUserScans]].map(([src, lab, badge]) =>
          e("button", {
            key: src, onClick: () => setActiveSource(src),
            style: {
              border: "none", cursor: "pointer", padding: "7px 15px", borderRadius: 999, display: "inline-flex", alignItems: "center", gap: 7,
              fontFamily: "var(--font-ui-alt, var(--font-sans))", fontWeight: 600, fontSize: 12.5,
              background: activeSource === src ? "var(--surface-card)" : "transparent",
              color: activeSource === src ? "var(--text-primary)" : "var(--text-muted)",
              boxShadow: activeSource === src ? "var(--shadow-sm)" : "none",
            },
          }, lab, badge != null ? e("span", { style: { background: "var(--app-accent)", color: "#fff", borderRadius: 999, fontSize: 10, fontWeight: 700, padding: "1px 7px", fontFamily: "var(--font-mono)" } }, badge) : null)),
      ),

      // ── Progress strip ──
      e("div", { style: Object.assign({}, cardStyle, { padding: 18, display: "flex", flexDirection: "column", gap: 12 }) },
        e("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 12, flexWrap: "wrap" } },
          e("span", { style: { fontFamily: "var(--font-sans)", fontWeight: 700, fontSize: 16, color: "var(--text-primary)" } }, progressCount),
          e("span", { style: { fontFamily: "var(--font-sans)", fontSize: 12.5, color: "var(--text-muted)" } }, showMeta),
        ),
        e("div", { style: { height: 10, background: "var(--surface-sunken)", borderRadius: 99, overflow: "hidden" } },
          e("div", { style: { width: progressFillPct + "%", height: "100%", borderRadius: 99, background: "var(--app-accent)", transition: "width var(--dur-fast, .2s)" } }),
        ),
        // Per-avatar balance bars
        e("div", { style: { display: "flex", alignItems: "flex-end", gap: 4, height: 70, paddingTop: 6 } },
          AVATAR_ORDER.map((id) => {
            const n = counts.perAvatar[id] || 0;
            const pct = (n / maxPerAvatar) * 100;
            const tier = countTier(n);
            const name = DISPLAY_NAMES[id] || id;
            return e("div", { key: id, title: name + ": " + n, style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 4, minWidth: 0 } },
              e("div", { style: { width: "100%", height: 40, display: "flex", alignItems: "flex-end" } },
                e("div", { style: { width: "100%", height: Math.max(6, pct) + "%", borderRadius: 4, background: TIER_COLOR[tier] } })),
              e("span", { style: { fontFamily: "var(--font-mono)", fontSize: 8.5, color: "var(--text-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "100%" } }, name.split(" ")[0]));
          }),
        ),
      ),

      // ── Main labeling area ──
      e("div", { style: { display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1.2fr)", gap: 22, alignItems: "start" } },
        // Left: current photo
        e("div", { style: Object.assign({}, cardStyle, { display: "flex", flexDirection: "column", gap: 14 }) },
          e("div", null, photoContent),
          e("div", { style: { display: "flex", gap: 10, flexWrap: "wrap" } },
            e("button", { onClick: () => skipItem("no_match"), disabled: !hasItem, style: secondaryBtn({ opacity: hasItem ? 1 : 0.5, cursor: hasItem ? "pointer" : "default" }) }, "No close matching avatars"),
            e("button", { onClick: () => skipItem("not_houseplant"), disabled: !hasItem, style: secondaryBtn({ opacity: hasItem ? 1 : 0.5, cursor: hasItem ? "pointer" : "default" }) }, "Not a houseplant"),
          ),
          sourceLine ? e("p", { style: { fontFamily: "var(--font-sans)", fontSize: 12.5, color: "var(--text-muted)", margin: 0 } }, sourceLine) : null,
        ),

        // Right: avatar pick grid
        e("div", { style: cardStyle },
          e("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 16, gap: 10, flexWrap: "wrap" } },
            e("span", { style: { fontFamily: "var(--font-sans)", fontWeight: 700, fontSize: 17, color: "var(--text-primary)" } }, "Pick the closest-matching avatar"),
            e("span", { style: { fontFamily: "var(--font-sans)", fontSize: 12, color: "var(--text-muted)" } }, "Click a card to label the photo"),
          ),
          e("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))", gap: 10 } },
            AVATAR_ORDER.map((id) => {
              const name = DISPLAY_NAMES[id] || id;
              const n = counts.perAvatar[id] || 0;
              const tier = countTier(n);
              const flashing = flashId === id;
              return e("button", {
                key: id, onClick: () => submitLabel(id),
                style: {
                  position: "relative", display: "flex", flexDirection: "column", alignItems: "center", gap: 6,
                  padding: "12px 8px 10px", borderRadius: "var(--radius-xl)",
                  border: "1px solid " + (flashing ? "var(--app-accent)" : "var(--border-subtle)"),
                  background: flashing ? "color-mix(in srgb, var(--app-accent) 14%, var(--surface-card))" : "var(--surface-card)",
                  cursor: hasItem ? "pointer" : "pointer", transition: "border-color var(--dur-fast,.2s), background var(--dur-fast,.2s), transform var(--dur-fast,.2s)",
                  transform: flashing ? "scale(0.96)" : "scale(1)",
                },
                onMouseEnter: (ev) => { if (!flashing) ev.currentTarget.style.borderColor = "var(--border-default)"; ev.currentTarget.style.background = "var(--surface-sunken)"; },
                onMouseLeave: (ev) => { if (!flashing) { ev.currentTarget.style.borderColor = "var(--border-subtle)"; ev.currentTarget.style.background = "var(--surface-card)"; } },
              },
                e(RivePreview, { id }),
                e("span", { style: { fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", textAlign: "center", lineHeight: 1.1 } }, name),
                e("span", { style: { position: "absolute", top: 8, right: 8, minWidth: 18, textAlign: "center", padding: "1px 5px", borderRadius: 99, fontFamily: "var(--font-mono)", fontSize: 10, fontWeight: 700, color: "#fff", background: TIER_COLOR[tier] } }, n),
              );
            }),
          ),
        ),
      ),

      // ── Cluster viz ──
      e(ChartCard, {
        title: "Label clusters (embedding space)", eyebrow: "PCA of CLIP embeddings",
        action: e("div", { style: { display: "flex", alignItems: "center", gap: 10 } },
          e("span", { style: { fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--text-muted)" } }, clusterMeta),
          e("button", { onClick: renderClusterViz, disabled: clusterBusy, style: secondaryBtn({ padding: "5px 12px", fontSize: 12, opacity: clusterBusy ? 0.6 : 1 }) }, clusterBusy ? "Loading…" : "Refresh"),
        ),
      },
        clusterMsg
          ? e("div", { style: { minHeight: 180, display: "grid", placeItems: "center", textAlign: "center", padding: 22 } },
              e("p", { style: { fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text-muted)", maxWidth: 520, margin: 0 } }, clusterMsg))
          : e("div", { style: { display: "flex", gap: 16, flexWrap: "wrap" } },
              e("div", { ref: clusterCanvasRef, style: { flex: 1, minWidth: 320, position: "relative", borderRadius: "var(--radius-lg)", overflow: "hidden", background: "#0a0e1a" } },
                e("div", { ref: clusterTooltipRef, style: { display: "none", position: "absolute", zIndex: 10, pointerEvents: "none", gap: 8, alignItems: "center", background: "rgba(10,14,26,0.92)", border: "1px solid rgba(255,255,255,0.12)", borderRadius: 8, padding: 8, boxShadow: "0 6px 18px rgba(0,0,0,0.4)" } })),
              e("div", { style: { width: 180, display: "flex", flexDirection: "column", gap: 8, paddingTop: 4 } },
                clusterLegend.map((it) => e("span", {
                  key: it.id, onClick: () => applyClusterFilter(it.id),
                  style: {
                    display: "flex", alignItems: "center", gap: 8, cursor: "pointer",
                    fontFamily: "var(--font-sans)", fontSize: 12.5, color: "var(--text-secondary)",
                    opacity: !clusterFilter || clusterFilter === it.id ? 1 : 0.25, transition: "opacity var(--dur-fast,.2s)",
                  },
                },
                  e("span", { style: { width: 10, height: 10, borderRadius: 3, background: it.color, boxShadow: "0 0 6px " + it.color, flexShrink: 0 } }),
                  it.name)),
              ),
            ),
      ),
    );
  }

  // Register under the Florio-owned namespace (see Shell.jsx NAV_TOOLS → navId 'labeling').
  (window.FlorioTabs = window.FlorioTabs || {}).labeling = Labeling;
})();
