import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; // Bảng tổ hợp ẢO HOÁ (chỉ vẽ dòng đang thấy) + bộ lọc bằng NÚT nối chuỗi — // mirror tool video_cook (ui/combo_model.py + ui/tab2_output.py). // Cột: STT | "tên + tên + tên" | [🎵 Source] | ✓ // Lọc: Đặt lại / Chọn hết / Bỏ hết / Lọc ngẫu nhiên % / Lọc tương đồng % // + "Ưu tiên khúc khác hoàn toàn". Tất cả chạy trên tập ĐANG CHỌN. // Trạng thái tick giữ trong Uint8Array (chịu được vài trăm nghìn dòng, thao tác // hàng loạt chỉ vài ms). Lọc tương đồng đẩy sang Web Worker để UI không đơ. const ROW_H = 30; const VIEW_H = 384; const RANK_BLOCK = 8192; const JOIN = " + "; const ComboTable = forwardRef(function ComboTable( { sequences, clipNames, nSeg, sources = [], maxRender = 10000, onCount }, ref) { const n = sequences.length; const checkedRef = useRef(new Uint8Array(0)); const rankRef = useRef(null); // prefix-sum số dòng chọn / block const workerRef = useRef(null); const [checkedCount, setCheckedCount] = useState(0); const [, force] = useState(0); const bump = () => force((x) => x + 1); const [scrollTop, setScrollTop] = useState(0); const [randPct, setRandPct] = useState(50); const [simPct, setSimPct] = useState(50); const [prio, setPrio] = useState([]); // bool theo từng khúc const [filtering, setFiltering] = useState(null); // {msg,pct} | null const [status, setStatus] = useState(""); // Nạp bộ dữ liệu mới -> tick hết, dọn mọi thứ. useEffect(() => { const buf = new Uint8Array(n); buf.fill(1); checkedRef.current = buf; rankRef.current = null; setCheckedCount(n); setScrollTop(0); setPrio(Array.from({ length: nSeg }, () => false)); setStatus(""); bump(); // eslint-disable-next-line }, [sequences, clipNames, nSeg]); // Worker lọc tương đồng (tạo 1 lần). useEffect(() => { const w = new Worker(new URL("../workers/simFilter.js", import.meta.url), { type: "module" }); workerRef.current = w; return () => { try { w.terminate(); } catch { /* */ } }; }, []); useEffect(() => { onCount && onCount(checkedCount); }, [checkedCount, onCount]); useImperativeHandle(ref, () => ({ getSelected() { const c = checkedRef.current; const out = []; for (let i = 0; i < c.length; i++) if (c[i]) out.push(sequences[i]); return out; }, selectedCount: () => checkedCount, }), [sequences, checkedCount]); // ── thao tác tick ────────────────────────────────────────────────────── const invalidate = () => { rankRef.current = null; }; const recount = () => { let s = 0; const c = checkedRef.current; for (let i = 0; i < c.length; i++) s += c[i]; setCheckedCount(s); }; const toggle = (i) => { const c = checkedRef.current; c[i] ^= 1; setCheckedCount((v) => v + (c[i] ? 1 : -1)); invalidate(); bump(); }; const setAll = (v) => { checkedRef.current.fill(v ? 1 : 0); setCheckedCount(v ? n : 0); invalidate(); bump(); setStatus(v ? `↺ Đã chọn tất cả ${n} tổ hợp` : "Đã bỏ chọn hết"); }; const setRows = (rows) => { const buf = new Uint8Array(n); for (const r of rows) if (r >= 0 && r < n) buf[r] = 1; checkedRef.current = buf; invalidate(); bump(); recount(); }; const checkedRows = () => { const c = checkedRef.current; const out = []; for (let i = 0; i < c.length; i++) if (c[i]) out.push(i); return out; }; // ── cột 🎵 Source: xoay vòng theo THỨ HẠNG trong các dòng ĐANG CHỌN ─────── const buildRank = () => { const c = checkedRef.current; const blocks = Math.ceil(n / RANK_BLOCK) + 1; const idx = new Int32Array(blocks); let total = 0; for (let b = 0; b < blocks - 1; b++) { idx[b] = total; const end = Math.min((b + 1) * RANK_BLOCK, n); for (let i = b * RANK_BLOCK; i < end; i++) total += c[i]; } idx[blocks - 1] = total; rankRef.current = idx; return idx; }; const sourceFor = (row) => { if (!sources.length) return null; const idx = rankRef.current || buildRank(); const block = (row / RANK_BLOCK) | 0; let rank = idx[block]; const c = checkedRef.current; for (let i = block * RANK_BLOCK; i < row; i++) rank += c[i]; return sources[rank % sources.length]; }; // ── bộ lọc ─────────────────────────────────────────────────────────────── const reset = () => setAll(true); const randomFilter = () => { const pool = checkedRows(); if (!pool.length) { setStatus("Chưa chọn tổ hợp nào để lọc."); return; } const keepCount = Math.max(1, Math.round(pool.length * randPct / 100)); // Fisher–Yates một phần -> lấy keepCount phần tử ngẫu nhiên, không lệch. for (let i = 0; i < keepCount; i++) { const j = i + Math.floor(Math.random() * (pool.length - i)); const t = pool[i]; pool[i] = pool[j]; pool[j] = t; } setRows(pool.slice(0, keepCount)); setStatus(`🎲 Giữ ${keepCount}/${pool.length} ngẫu nhiên (${randPct}% của tập đang chọn)`); }; const similarityFilter = () => { const pool = checkedRows(); if (!pool.length) { setStatus("Chưa chọn tổ hợp nào để lọc."); return; } const maxSame = Math.floor(simPct / 100 * nSeg + 1e-9); const priorityCols = prio.map((p, i) => (p ? i : -1)).filter((i) => i >= 0); // dựng mảng phẳng các hàng đang chọn -> chuyển thẳng sang worker (transferable) const flat = new Int32Array(pool.length * nSeg); for (let k = 0; k < pool.length; k++) { const seq = sequences[pool[k]]; const base = k * nSeg; for (let j = 0; j < nSeg; j++) flat[base + j] = seq[j]; } setFiltering({ msg: `🔍 Đang lọc ${pool.length} tổ hợp…`, pct: 0 }); const w = workerRef.current; w.onmessage = (e) => { const d = e.data; if (d.type === "progress") { setFiltering({ msg: `🔍 Đang lọc tương đồng… ${Math.round(d.done / d.total * 100)}%`, pct: Math.round(d.done / d.total * 100) }); return; } // done const keep = d.keep; const keptRows = []; for (let k = 0; k < pool.length; k++) if (keep[k]) keptRows.push(pool[k]); setRows(keptRows); setFiltering(null); setStatus(`🔍 Xong! Còn ${keptRows.length}/${pool.length} (loại ${pool.length - keptRows.length} tổ hợp tương đồng)`); }; w.postMessage({ flat, nSeg, maxSame, priorityCols }, [flat.buffer]); }; // ── ảo hoá ──────────────────────────────────────────────────────────────── const start = Math.max(0, ((scrollTop / ROW_H) | 0) - 6); const visN = Math.ceil(VIEW_H / ROW_H) + 12; const end = Math.min(n, start + visN); const rows = []; const c = checkedRef.current; for (let i = start; i < end; i++) { const on = !!c[i]; const names = sequences[i].map((id) => clipNames[id] || String(id)).join(JOIN); const src = sourceFor(i); rows.push(
toggle(i)} style={{ display: "flex", alignItems: "center", height: ROW_H, cursor: "pointer", background: i % 2 ? "var(--white)" : "var(--cream)", opacity: on ? 1 : 0.4, borderBottom: "1px solid var(--line)", fontSize: 12 }}>
{i + 1}
clipNames[id] || String(id)).join("\n")}>{names}
{src != null && (
🎵 {src}
)}
); } const over = checkedCount > maxRender; return (
{/* thanh công cụ */}
Ngẫu nhiên setRandPct(Math.max(1, Math.min(99, +e.target.value || 1)))} style={{ width: 60 }} /> Tương đồng setSimPct(Math.max(0, Math.min(100, +e.target.value || 0)))} style={{ width: 60 }} /> Đã chọn: {checkedCount.toLocaleString()} / {n.toLocaleString()}
{/* ưu tiên khúc khác hoàn toàn */} {nSeg > 0 && (
Ưu tiên khác hoàn toàn ở khúc: {prio.map((p, i) => ( ))}
)} {/* tiêu đề cột */}
STT
Tổ hợp video
{sources.length > 0 &&
🎵 Source
}
{/* danh sách ảo hoá */}
setScrollTop(e.currentTarget.scrollTop)} style={{ height: VIEW_H, overflow: "auto", border: "1px solid var(--line)", borderTop: "none" }}>
{rows}
{filtering && (
{filtering.msg}
)} {!filtering && status && (
{status}
)} {over && (

⚠ Đang chọn {checkedCount.toLocaleString()} biến thể — chỉ render tối đa {maxRender.toLocaleString()} video/lần, phần dư sẽ bị bỏ.

)}
); }); export default ComboTable;