Spaces:
Running
Running
Download ratings.js from ankanmbz/worldguide-comparison: direct link, hf CLI and curl.
- Browser
- Download file 8.88 kB
-
https://huggingface.co/spaces/ankanmbz/worldguide-comparison/resolve/main/ratings.js
- Command line
-
hf download hf://spaces/ankanmbz/worldguide-comparison/ratings.js
-
curl -L -o ratings.js https://huggingface.co/spaces/ankanmbz/worldguide-comparison/resolve/main/ratings.js
8.88 kB
| ; | |
| window.WorldGuideRatings = (() => { | |
| const STORAGE_KEY = "worldguide-anonymous-voter"; | |
| const LOCAL_SCORES_KEY = "worldguide-local-ratings"; | |
| const descriptions = ["Poor", "Fair", "Good", "Very good", "Excellent"]; | |
| const records = new Map(); | |
| const loads = new Map(); | |
| const versions = new Map(); | |
| const listeners = new Map(); | |
| let apiBase, voterId, mode, storageAvailable = true; | |
| const configured = fetch("./ratings-config.json") | |
| .then(response => { if (!response.ok) throw new Error("No ratings configuration"); return response.json(); }) | |
| .then(config => { | |
| if (config.mode === "local") { | |
| mode = "local"; | |
| document.getElementById("ratings-notice").textContent = "Scores are saved on this device only. Shared score collection is not connected yet."; | |
| return true; | |
| } | |
| if (config.mode === "supabase") { | |
| window.WorldGuideSupabaseRatings.connect(config); | |
| mode = "supabase"; | |
| document.getElementById("ratings-notice").textContent = "Your scores are saved anonymously to the shared ratings database. Select 1 (Poor) to 5 (Excellent)."; | |
| return true; | |
| } | |
| const url = new URL(config.apiBase); | |
| if (url.protocol !== "https:" || !url.hostname.endsWith(".hf.space")) throw new Error("Invalid ratings service"); | |
| apiBase = url.origin; | |
| mode = "remote"; | |
| document.getElementById("ratings-notice").textContent = "Rate overall performance from 1 (Poor) to 5 (Excellent). Scores are saved anonymously."; | |
| return true; | |
| }).catch(() => false); | |
| try { | |
| voterId = localStorage.getItem(STORAGE_KEY); | |
| if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(voterId || "")) { | |
| voterId = crypto.randomUUID(); | |
| localStorage.setItem(STORAGE_KEY, voterId); | |
| } | |
| } catch { | |
| voterId = crypto.randomUUID(); | |
| storageAvailable = false; | |
| } | |
| const keyFor = (sample, model) => `${sample}:${model}`; | |
| function emit(key) { | |
| for (const entry of listeners.get(key) || []) { | |
| if (entry.node.isConnected) entry.update(); | |
| } | |
| } | |
| async function request(path, data) { | |
| if (!(await configured)) throw new Error("Rating service is not connected yet."); | |
| let response; | |
| try { | |
| response = await fetch(apiBase + path, { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ voter_id: voterId, ...data }), | |
| signal: AbortSignal.timeout(45000), | |
| }); | |
| } catch { | |
| throw new Error("Couldn’t reach the rating service. Please retry."); | |
| } | |
| if (!response.ok) { | |
| if (response.status === 429) throw new Error("Please wait a minute, then try again."); | |
| throw new Error("Rating service is unavailable. Please retry shortly."); | |
| } | |
| return response.json(); | |
| } | |
| function loadSample(sample) { | |
| if (loads.has(sample)) return loads.get(sample); | |
| const startingVersions = new Map(versions); | |
| const pending = configured.then(connected => { | |
| if (!connected) throw new Error("Ratings not configured"); | |
| if (mode === "local") { | |
| let scores = {}; | |
| try { scores = JSON.parse(localStorage.getItem(LOCAL_SCORES_KEY) || "{}"); } catch { /* Keep this visit's scores. */ } | |
| const ratings = {}; | |
| for (const [key, value] of Object.entries(scores || {})) { | |
| if (key.startsWith(sample + ":")) ratings[key.slice(sample.length + 1)] = value.score; | |
| } | |
| return { ratings }; | |
| } | |
| if (mode === "supabase") return window.WorldGuideSupabaseRatings.read(sample); | |
| return request("/api/ratings/mine", { sample_id: sample }); | |
| }).then(data => { | |
| for (const [model, score] of Object.entries(data.ratings || {})) { | |
| if (!Number.isInteger(score) || score < 1 || score > 5) continue; | |
| const key = keyFor(sample, model); | |
| if ((versions.get(key) || 0) === (startingVersions.get(key) || 0)) { | |
| records.set(key, { score, state: mode === "local" ? "local" : "saved" }); | |
| emit(key); | |
| } | |
| } | |
| }).catch(() => { | |
| // Loading a previous rating must not invent a saved score or block a new vote. | |
| loads.delete(sample); | |
| }); | |
| loads.set(sample, pending); | |
| return pending; | |
| } | |
| async function save(sample, model, score) { | |
| const key = keyFor(sample, model); | |
| if (records.get(key)?.state === "saving") return; | |
| const previous = records.get(key)?.score; | |
| versions.set(key, (versions.get(key) || 0) + 1); | |
| records.set(key, { score: previous, pending: score, state: "saving" }); | |
| emit(key); | |
| try { | |
| if (!(await configured)) throw new Error("Rating service is not connected yet."); | |
| if (mode === "local") { | |
| let remembered = false; | |
| try { | |
| const stored = JSON.parse(localStorage.getItem(LOCAL_SCORES_KEY) || "{}"); | |
| stored[key] = { score, updatedAt: new Date().toISOString() }; | |
| localStorage.setItem(LOCAL_SCORES_KEY, JSON.stringify(stored)); | |
| remembered = true; | |
| } catch { storageAvailable = false; } | |
| records.set(key, { score, state: remembered ? "local" : "session" }); | |
| emit(key); | |
| return; | |
| } | |
| const result = mode === "supabase" ? await window.WorldGuideSupabaseRatings.save(sample, model, score) : | |
| await request("/api/ratings", { sample_id: sample, model_id: model, score }); | |
| if (result.saved !== true || result.score !== score) throw new Error("Score was not confirmed. Please retry."); | |
| records.set(key, { score, state: "saved" }); | |
| } catch (error) { | |
| records.set(key, { score: previous, pending: score, state: "error", message: error.message }); | |
| } | |
| emit(key); | |
| } | |
| function createPanel({ sampleId, modelId, label }) { | |
| const key = keyFor(sampleId, modelId); | |
| const panel = document.createElement("div"); | |
| panel.className = "rating-panel"; | |
| const header = document.createElement("div"); | |
| header.className = "rating-header"; | |
| const heading = document.createElement("span"); | |
| heading.className = "rating-label"; | |
| heading.textContent = "Rate this video"; | |
| const scale = document.createElement("span"); | |
| scale.className = "rating-scale"; | |
| scale.textContent = "1 Poor · 5 Excellent"; | |
| header.append(heading, scale); | |
| const group = document.createElement("div"); | |
| group.className = "rating-options"; | |
| group.setAttribute("role", "group"); | |
| group.setAttribute("aria-label", `Rate ${label} from 1 to 5`); | |
| const buttons = []; | |
| for (let score = 1; score <= 5; score++) { | |
| const button = document.createElement("button"); | |
| button.type = "button"; | |
| button.className = "rating-button"; | |
| button.textContent = String(score); | |
| button.setAttribute("aria-label", `${score} — ${descriptions[score - 1]}`); | |
| button.setAttribute("aria-pressed", "false"); | |
| button.title = descriptions[score - 1]; | |
| button.addEventListener("click", () => save(sampleId, modelId, score)); | |
| buttons.push(button); | |
| group.append(button); | |
| } | |
| const status = document.createElement("div"); | |
| status.className = "rating-status"; | |
| status.setAttribute("role", "status"); | |
| status.setAttribute("aria-live", "polite"); | |
| const message = document.createElement("span"); | |
| const retry = document.createElement("button"); | |
| retry.className = "text-button rating-retry"; | |
| retry.type = "button"; | |
| retry.textContent = "Retry"; | |
| retry.hidden = true; | |
| retry.addEventListener("click", () => save(sampleId, modelId, records.get(key).pending)); | |
| status.append(message, retry); | |
| panel.append(header, group, status); | |
| function update() { | |
| const record = records.get(key); | |
| const saving = record?.state === "saving"; | |
| buttons.forEach((button, index) => { | |
| button.disabled = saving; | |
| button.setAttribute("aria-pressed", String(record?.score === index + 1)); | |
| button.classList.toggle("pending", saving && record.pending === index + 1); | |
| }); | |
| panel.classList.toggle("rating-failed", record?.state === "error"); | |
| retry.hidden = record?.state !== "error"; | |
| message.textContent = saving ? "Saving your score…" : record?.state === "error" ? record.message : | |
| record?.state === "local" ? `Saved on this device · ${record.score}/5` : | |
| record?.state === "session" ? `For this visit only · ${record.score}/5` : | |
| record?.score ? `Saved · Your score: ${record.score}/5` : "Select a score. You can change it later."; | |
| if (!storageAvailable && record?.state === "saved") message.textContent += " Remembered for this visit only."; | |
| } | |
| const entries = (listeners.get(key) || []).filter(entry => entry.node.isConnected); | |
| entries.push({ node: panel, update }); | |
| listeners.set(key, entries); | |
| update(); | |
| loadSample(sampleId); | |
| return panel; | |
| } | |
| return { createPanel }; | |
| })(); | |