| |
| |
|
|
| let _viewerInitialized = false; |
|
|
| |
| const IMAGE_BASE_URL = "https://huggingface.co/datasets/launch/LudoBench/resolve/main/images"; |
|
|
| |
| |
| |
| async function loadManifest() { |
| const res = await fetch("manifest.json"); |
| if (!res.ok) throw new Error("Failed to load manifest: " + res.status); |
| const manifest = await res.json(); |
| return manifest.files; |
| } |
|
|
| |
| |
| |
| function buildFolderIndex(files) { |
| const set = new Set(); |
| for (const f of files) set.add(f.folder); |
| return Array.from(set).sort(); |
| } |
|
|
| function populateFolderSelect(folders) { |
| const sel = document.getElementById("folderSelect"); |
| sel.innerHTML = ""; |
| for (const f of folders) { |
| const opt = document.createElement("option"); |
| opt.textContent = f; |
| opt.value = f; |
| sel.appendChild(opt); |
| } |
| } |
|
|
| function filterFiles(files, folder) { |
| return files.filter(f => f.folder === folder); |
| } |
|
|
| function populateFileSelect(files) { |
| const sel = document.getElementById("fileSelect"); |
| sel.innerHTML = ""; |
| for (const f of files) { |
| const opt = document.createElement("option"); |
| opt.value = f.json_path; |
| opt.textContent = f.name; |
| sel.appendChild(opt); |
| } |
| } |
|
|
| |
| |
| |
| async function loadJson(path) { |
| const res = await fetch(path); |
| if (!res.ok) throw new Error("Error loading " + path); |
| return await res.json(); |
| } |
|
|
| |
| |
| |
| function normalizedAnswers(raw) { |
| return new Set( |
| String(raw || "") |
| .split(/,|\/|\bor\b/i) |
| .map(s => s.trim().toLowerCase()) |
| .filter(Boolean) |
| ); |
| } |
|
|
| function isNumber(s) { |
| return s !== "" && !Number.isNaN(Number(s)); |
| } |
|
|
| function renderQuestion(data) { |
| const card = document.getElementById("questionCard"); |
| card.innerHTML = ` |
| <h2>${data.Game} <span class="id-tag">(ID ${data.ID})</span></h2> |
| <p class="question-label">Question:</p> |
| <p class="question-text">${data.Question}</p> |
| `; |
|
|
| const info = document.getElementById("answerInfo"); |
| info.textContent = ""; |
| info.className = "answer-info"; |
| } |
|
|
| function attachAnswerLogic(data) { |
| const raw = String(data.Answer || "").trim(); |
| const accepted = normalizedAnswers(raw); |
|
|
| const input = document.getElementById("answerInput"); |
| const button = document.getElementById("checkButton"); |
| const info = document.getElementById("answerInfo"); |
|
|
| const sol = document.getElementById("solutionText"); |
| sol.innerHTML = `<strong>Expected:</strong> ${raw || "\u2014"}`; |
|
|
| button.onclick = () => { |
| const user = input.value.trim().toLowerCase(); |
| let ok = false; |
|
|
| if (accepted.has(user)) ok = true; |
| else if (isNumber(user)) { |
| for (const a of accepted) |
| if (isNumber(a) && Math.abs(Number(a) - Number(user)) < 1e-9) |
| ok = true; |
| } |
|
|
| info.textContent = ok ? "Correct!" : "Not quite. Try again."; |
| info.className = "answer-info " + (ok ? "correct" : "wrong"); |
| }; |
| } |
|
|
| |
| |
| |
| function renderImage(data) { |
| const container = document.getElementById("imageContainer"); |
| const multi = document.getElementById("multiImages"); |
|
|
| multi.innerHTML = ""; |
| container.classList.add("hidden"); |
|
|
| let urls = data.game_state_url; |
| if (!urls) return; |
| if (!Array.isArray(urls)) urls = [urls]; |
|
|
| const folder = data.Game.toLowerCase().replace(/\s+/g, "_"); |
|
|
| urls.forEach(url => { |
| const file = url.split("/").pop(); |
| const localPath = `${IMAGE_BASE_URL}/${folder}/${file}`; |
|
|
| const block = document.createElement("div"); |
| block.className = "multi-img-block"; |
|
|
| const spinner = document.createElement("div"); |
| spinner.className = "spinner"; |
| block.appendChild(spinner); |
|
|
| const img = document.createElement("img"); |
| img.style.display = "none"; |
| img.src = localPath; |
|
|
| img.onload = () => { |
| spinner.style.display = "none"; |
| img.style.display = "block"; |
| }; |
|
|
| img.onerror = () => { |
| spinner.style.display = "none"; |
| const err = document.createElement("div"); |
| err.textContent = "Failed to load " + localPath; |
| err.style.color = "#d44"; |
| block.appendChild(err); |
| }; |
|
|
| const link = document.createElement("a"); |
| link.href = localPath; |
| link.target = "_blank"; |
| link.rel = "noopener noreferrer"; |
| link.appendChild(img); |
| block.appendChild(link); |
|
|
| const caption = document.createElement("div"); |
| caption.className = "multi-img-caption"; |
| caption.textContent = file; |
| block.appendChild(caption); |
|
|
| const full = document.createElement("a"); |
| full.href = localPath; |
| full.target = "_blank"; |
| full.rel = "noopener noreferrer"; |
| full.className = "full-img-link"; |
| full.textContent = "View full image"; |
| block.appendChild(full); |
|
|
| multi.appendChild(block); |
| }); |
|
|
| container.classList.remove("hidden"); |
| } |
|
|
| |
| |
| |
| let GLOBAL_FILES = []; |
| let GLOBAL_CURRENT_FOLDER = ""; |
| let loadAndRenderRef = null; |
|
|
| function goRelative(offset) { |
| const fileSelect = document.getElementById("fileSelect"); |
| const options = Array.from(fileSelect.options); |
| if (options.length === 0) return; |
|
|
| const values = options.map(o => o.value); |
| const current = fileSelect.value; |
| let idx = values.indexOf(current); |
| if (idx === -1) return; |
|
|
| let next = idx + offset; |
| if (next < 0) next = values.length - 1; |
| if (next >= values.length) next = 0; |
|
|
| fileSelect.value = values[next]; |
| if (loadAndRenderRef) loadAndRenderRef(values[next]); |
| } |
|
|
| |
| |
| |
| async function initViewer() { |
| if (_viewerInitialized) return; |
| _viewerInitialized = true; |
|
|
| document.getElementById("prevBtn").onclick = () => goRelative(-1); |
| document.getElementById("nextBtn").onclick = () => goRelative(1); |
|
|
| const questionCard = document.getElementById("questionCard"); |
|
|
| try { |
| const files = await loadManifest(); |
| GLOBAL_FILES = files; |
|
|
| const folders = buildFolderIndex(files); |
| populateFolderSelect(folders); |
|
|
| const folderSel = document.getElementById("folderSelect"); |
| const fileSel = document.getElementById("fileSelect"); |
|
|
| async function loadAndRender(path) { |
| const data = await loadJson(path); |
| renderQuestion(data); |
| attachAnswerLogic(data); |
| renderImage(data); |
| } |
|
|
| loadAndRenderRef = loadAndRender; |
|
|
| function refresh() { |
| GLOBAL_CURRENT_FOLDER = folderSel.value; |
| const filtered = filterFiles(files, GLOBAL_CURRENT_FOLDER); |
| populateFileSelect(filtered); |
| if (filtered.length > 0) |
| loadAndRender(filtered[0].json_path); |
| } |
|
|
| folderSel.onchange = refresh; |
| fileSel.onchange = () => loadAndRender(fileSel.value); |
|
|
| folderSel.value = "kingdomino_tier1"; |
| refresh(); |
|
|
| } catch (err) { |
| console.error(err); |
| questionCard.innerHTML = `<p style="color:#f55;">Init error: ${err}</p>`; |
| } |
| } |
|
|