const DATASET_KEY = "humanlabeling_dataset";
const AUTOSAVE_DELAY = 1500;
let _dataset = [];
let _activeRowId = null;
let _autosaveTimer = null;
let _currentReferences = [];
let _currentVideoUrl = null;
let _currentTitle = "";
let _currentPage = 1;
const PAGE_SIZE = 10;
// ── Init ──────────────────────────────────────────────────────────────────────
window.addEventListener("DOMContentLoaded", () => {
_loadDataset();
document.getElementById("transcriptArea").addEventListener("input", () => {
updateCharCount();
_scheduleAutosave();
});
document.getElementById("refInput").addEventListener("keydown", (e) => {
if (e.key === "Enter") addReference();
});
});
// ── References ────────────────────────────────────────────────────────────────
function addReference() {
const input = document.getElementById("refInput");
const val = input.value.trim();
if (!val) return;
if (!_currentReferences.includes(val)) {
_currentReferences.push(val);
_renderRefPills();
_scheduleAutosave();
}
input.value = "";
input.focus();
}
function removeReference(idx) {
_currentReferences.splice(idx, 1);
_renderRefPills();
_scheduleAutosave();
}
function editReference(idx) {
const current = _currentReferences[idx];
const container = document.getElementById("refPills");
const pill = container.querySelectorAll(".ref-pill")[idx];
if (!pill) return;
const input = document.createElement("input");
input.className = "ref-pill-edit";
input.value = current;
pill.innerHTML = "";
pill.appendChild(input);
input.focus();
input.select();
function commit() {
const val = input.value.trim();
if (val && val !== current) {
_currentReferences[idx] = val;
_scheduleAutosave();
}
_renderRefPills();
}
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); commit(); }
if (e.key === "Escape") _renderRefPills();
});
input.addEventListener("blur", commit);
}
function _renderRefPills() {
const container = document.getElementById("refPills");
if (_currentReferences.length === 0) {
container.innerHTML = "";
return;
}
container.innerHTML = _currentReferences
.map(
(ref, i) => `
${_esc(ref)}
`,
)
.join("");
}
// ── Video preview ─────────────────────────────────────────────────────────────
function loadVideoPreview(url) {
const container = document.getElementById("videoContainer");
const ytMatch = url.match(/(?:v=|youtu\.be\/|shorts\/)([A-Za-z0-9_-]{11})/);
if (!ytMatch) {
container.innerHTML = `
Không phải YouTube URL
`;
return;
}
container.innerHTML = "";
const iframe = document.createElement("iframe");
iframe.width = "100%";
iframe.height = "100%";
iframe.style.border = "none";
iframe.allow =
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture";
iframe.allowFullscreen = true;
iframe.src = `https://www.youtube.com/embed/${ytMatch[1]}`;
container.appendChild(iframe);
}
// ── Transcript ────────────────────────────────────────────────────────────────
function setTranscript(text) {
document.getElementById("transcriptArea").value = text || "";
updateCharCount();
}
function updateCharCount() {
const text = document.getElementById("transcriptArea").value;
const el = document.getElementById("charCount");
if (text.length > 0) {
const words = text.trim().split(/\s+/).length;
el.textContent = `${words} từ · ${text.length} ký tự`;
} else {
el.textContent = "";
}
}
async function copyText() {
const text = document.getElementById("transcriptArea").value;
if (!text) return;
try {
await navigator.clipboard.writeText(text);
const btn = document.getElementById("copyBtn");
btn.innerHTML = ' Đã copy!';
setTimeout(() => {
btn.innerHTML = ' Copy';
}, 1500);
} catch (_) {}
}
const _INSTRUCTION_TEMPLATE = `Bạn là một trợ lý AI chuyên nghiệp về biên tập nội dung pháp luật. Nhiệm vụ của bạn là sửa lỗi chính tả và định dạng lại văn bản phụ đề (transcript) của video dựa trên các quy tắc nghiêm ngặt sau:
### 1. QUY TẮC NGUYÊN VĂN (BẮT BUỘC)
- GIỮ NGUYÊN VĂN TỪNG CÂU TỪNG CHỮ của bản phụ đề được cung cấp.
- KHÔNG TÓM TẮT, không cắt xén, không tự ý viết lại câu theo ý mình, không chuyển thành dạng danh sách bài viết (trừ khi văn bản gốc đang liệt kê điều khoản 1, 2, 3...).
### 2. QUY TẮC SỬA LỖI & CHUẨN HÓA KHÁCH QUAN
- Sửa các lỗi chính tả do công cụ nhận diện giọng nói tự động (AI speech-to-text) nhận diện sai từ đồng âm (Ví dụ: "chữa thuế" -> "chịu thuế", "bài hát xã hội" -> "bảo hiểm xã hội").
- Nếu trong phụ đề có số liệu bị nhận diện sai lệch so với thực tế phát âm của video (Ví dụ: nghe nhầm 135 thành 155), hãy đính chính lại cho đúng với âm thanh trong video.
- Thêm dấu câu đầy đủ (chấm, phẩy, hai chấm, viết hoa đầu câu) để ngắt câu mạch lạc, rõ nghĩa.
- Giữ nguyên các đoạn quảng cáo chèn giữa video của kênh (nếu có trong phụ đề).
### 3. BỔ SUNG THÔNG TIN VĂN BẢN PHÁP LUẬT
- Khi trong phụ đề nhắc đến một văn bản pháp luật (Nghị quyết, Nghị định, Thông tư...) nhưng chỉ có số mà thiếu ký hiệu hoặc năm ban hành, hãy tra cứu nhanh để bổ sung đầy đủ ký hiệu chuẩn pháp lý (Ví dụ: "Nghị quyết 278 ngày 13 tháng 9" -> "Nghị quyết số 278/NQ-CP ngày 13 tháng 9 năm 2025").
### 4. ĐỊNH DẠNG ĐẦU RA
- Xuất kết quả duy nhất dưới dạng một khối mã Markdown (Code block) chứa văn bản hoàn chỉnh để người dùng dễ sao chép.
---
### THÔNG TIN CẦN XỬ LÝ:
- URL Video: {{URL}}
- Nội dung phụ đề gốc:
{{SCRIPT}}`;
async function generateInstruction() {
const script = document.getElementById("transcriptArea").value.trim();
const url = _currentVideoUrl || "";
if (!script && !url) return;
const prompt = _INSTRUCTION_TEMPLATE
.replace("{{URL}}", url || "[Chèn link video vào đây]")
.replace("{{SCRIPT}}", script || "[Dán đoạn phụ đề cần sửa vào đây]");
try {
await navigator.clipboard.writeText(prompt);
const btn = document.getElementById("genInstrBtn");
btn.innerHTML = ' Đã copy!';
setTimeout(() => {
btn.innerHTML = ' Gen Instruction';
}, 2000);
} catch (_) {}
}
// ── Status bar ────────────────────────────────────────────────────────────────
function setStatus(type, text) {
const bar = document.getElementById("statusBar");
bar.className = `status-bar ${type}`;
document.getElementById("statusText").textContent = text;
}
// ── Dataset ───────────────────────────────────────────────────────────────────
function _loadDataset() {
try {
_dataset = JSON.parse(localStorage.getItem(DATASET_KEY) || "[]");
} catch (_) {
_dataset = [];
}
_renderDataset();
}
function _saveDataset() {
localStorage.setItem(DATASET_KEY, JSON.stringify(_dataset));
_showAutosaved();
}
function _scheduleAutosave() {
if (!_activeRowId) return;
clearTimeout(_autosaveTimer);
document.getElementById("autosaveIndicator").textContent = "lưu...";
document.getElementById("autosaveIndicator").className = "autosave-indicator";
_autosaveTimer = setTimeout(() => {
const entry = _dataset.find((e) => e.id === _activeRowId);
if (entry) {
entry.script = document.getElementById("transcriptArea").value;
entry.references = [..._currentReferences];
_saveDataset();
_renderDataset();
}
}, AUTOSAVE_DELAY);
}
function _showAutosaved() {
const el = document.getElementById("autosaveIndicator");
el.textContent = "đã lưu";
el.className = "autosave-indicator saved";
setTimeout(() => {
el.textContent = "";
}, 2000);
}
function saveCurrentToDataset() {
if (!_activeRowId) return;
const script = document.getElementById("transcriptArea").value.trim();
const entry = _dataset.find((e) => e.id === _activeRowId);
if (!entry) return;
entry.script = script;
entry.references = [..._currentReferences];
entry.status = "verified";
_saveDataset();
_renderDataset();
setStatus("done", `Đã verify: ${_esc(entry.title)}`);
// Auto-advance to next unverified entry
const currentIdx = _dataset.findIndex((e) => e.id === _activeRowId);
const next = _dataset.slice(currentIdx + 1).find((e) => (e.status || "none") !== "verified");
if (next) selectRow(next.id);
}
function selectRow(id) {
const entry = _dataset.find((e) => e.id === id);
if (!entry) return;
_activeRowId = id;
_currentVideoUrl = entry.url;
_currentTitle = entry.title;
_currentReferences = [...(entry.references || [])];
setTranscript(entry.script);
_renderRefPills();
loadVideoPreview(entry.url);
document.getElementById("videoMeta").textContent = entry.title || entry.url;
_renderDataset();
}
function deleteRow(id) {
_dataset = _dataset.filter((e) => e.id !== id);
if (_activeRowId === id) {
_activeRowId = null;
setTranscript("");
_currentReferences = [];
_renderRefPills();
document.getElementById("videoMeta").textContent = "";
document.getElementById("videoContainer").innerHTML = `
Chọn một video từ dataset để xem
`;
}
_saveDataset();
_renderDataset();
}
function clearDataset() {
if (!_dataset.length) return;
if (!confirm(`Xoá tất cả ${_dataset.length} video khỏi dataset?`)) return;
_dataset = [];
_activeRowId = null;
_saveDataset();
_renderDataset();
}
function _renderDataset() {
const tbody = document.getElementById("datasetBody");
const count = document.getElementById("datasetCount");
const verified = _dataset.filter((e) => (e.status || "none") === "verified").length;
count.textContent = _dataset.length
? `${_dataset.length} video · ${verified} verified`
: "";
if (_dataset.length === 0) {
tbody.innerHTML =
'