| | |
| | |
| |
|
| | 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>`; |
| | } |
| | } |
| |
|