Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- README.md +22 -5
- app.js +399 -0
- config.js +39 -0
- hub-2.15.0.bundle.mjs +0 -0
- index.html +23 -17
- style.css +43 -28
README.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
| 1 |
---
|
| 2 |
-
title: Audit Conversion
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: LocalGate Audit — Conversion
|
| 3 |
+
emoji: ✅
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
+
license: apache-2.0
|
| 10 |
+
hf_oauth: true
|
| 11 |
+
hf_oauth_expiration_minutes: 1440
|
| 12 |
+
hf_oauth_scopes:
|
| 13 |
+
- read-repos
|
| 14 |
+
- contribute-repos
|
| 15 |
+
hf_oauth_authorized_org: localgate
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# LocalGate audit — conversion
|
| 19 |
+
|
| 20 |
+
Private static annotation Space. Use the **direct URL**
|
| 21 |
+
(https://localgate-audit-conversion.static.hf.space) — the embedded view links out.
|
| 22 |
+
Sign in with your HF account; judgments are committed to a private dataset in
|
| 23 |
+
**your own namespace** (`{you}/localgate-audit-results`) under your own token,
|
| 24 |
+
so every grade is attributed by commit author. Progress saves continuously and
|
| 25 |
+
resumes across sessions. Incident response: revoke a leaked token at
|
| 26 |
+
https://huggingface.co/settings/tokens and remove this app under Settings →
|
| 27 |
+
Connected Applications.
|
app.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* LocalGate audit engine — shared by both audit Spaces.
|
| 2 |
+
*
|
| 3 |
+
* Design contract (deliverables/AUDIT_SPACES_BUILD.md, FINAL SPEC):
|
| 4 |
+
* - Same-tab OAuth on the direct *.static.hf.space URL; embedded views link out.
|
| 5 |
+
* - Token lives in sessionStorage only; the hub library persists nothing itself.
|
| 6 |
+
* - Scopes are read-repos + contribute-repos: results go to a private dataset in
|
| 7 |
+
* the ANNOTATOR'S OWN namespace ({user}/localgate-audit-results), created on
|
| 8 |
+
* first login. Nobody can write anyone else's results at the permission layer.
|
| 9 |
+
* - Append-only event log: every judgment/revision is a new row; analysis takes
|
| 10 |
+
* newest-ts-per-key. Upload = whole-file rewrite of the user's own file.
|
| 11 |
+
* - All content rendered via textContent. No innerHTML anywhere in this file.
|
| 12 |
+
* - localStorage holds the event log per (task, user) — loss bound is zero;
|
| 13 |
+
* uploads every SAVE_EVERY judgments / on 's' / on tab-hide / at the end.
|
| 14 |
+
*/
|
| 15 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent, uploadFiles, downloadFile,
|
| 16 |
+
createRepo } from "./hub-2.15.0.bundle.mjs";
|
| 17 |
+
|
| 18 |
+
/* global CONFIG */
|
| 19 |
+
const ORG_ID = "6a7af86a89612db0d39b0b14"; // localgate — forces the org grant
|
| 20 |
+
const ITEMS_REPO = { type: "dataset", name: "localgate/audit-items" };
|
| 21 |
+
const SAVE_EVERY = 5;
|
| 22 |
+
|
| 23 |
+
const $ = (sel) => document.querySelector(sel);
|
| 24 |
+
const el = (tag, cls, text) => {
|
| 25 |
+
const node = document.createElement(tag);
|
| 26 |
+
if (cls) node.className = cls;
|
| 27 |
+
if (text !== undefined) node.textContent = text;
|
| 28 |
+
return node;
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
// ── rubric fingerprint (same discipline as convert.py's PROMPT_VERSION) ──────
|
| 32 |
+
async function rubricVersion() {
|
| 33 |
+
const data = new TextEncoder().encode(CONFIG.rubric + "\0" + JSON.stringify(CONFIG.fields));
|
| 34 |
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
| 35 |
+
return [...new Uint8Array(hash)].slice(0, 6).map((b) => b.toString(16).padStart(2, "0")).join("");
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// ── auth ─────────────────────────────────────────────────────────────────────
|
| 39 |
+
function storedAuth() {
|
| 40 |
+
try {
|
| 41 |
+
const raw = sessionStorage.getItem("oauth");
|
| 42 |
+
if (!raw) return null;
|
| 43 |
+
const auth = JSON.parse(raw);
|
| 44 |
+
if (new Date(auth.accessTokenExpiresAt) <= new Date()) return null;
|
| 45 |
+
return auth;
|
| 46 |
+
} catch { return null; }
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
async function ensureAuth() {
|
| 50 |
+
let auth = storedAuth();
|
| 51 |
+
if (auth) return auth;
|
| 52 |
+
const fresh = await oauthHandleRedirectIfPresent();
|
| 53 |
+
if (fresh) {
|
| 54 |
+
sessionStorage.setItem("oauth", JSON.stringify(fresh));
|
| 55 |
+
history.replaceState(null, "", location.pathname); // ?code is single-use; keep it out of history
|
| 56 |
+
return fresh;
|
| 57 |
+
}
|
| 58 |
+
return null;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
async function signIn() {
|
| 62 |
+
const url = await oauthLoginUrl(); // reads window.huggingface.variables in a Space
|
| 63 |
+
window.location.href = url + "&orgIds=" + ORG_ID; // no param passthrough in the helper; append
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// ── state ────────────────────────────────────────────────────────────────────
|
| 67 |
+
const state = {
|
| 68 |
+
auth: null, user: null, items: [], order: [], idx: 0,
|
| 69 |
+
events: [], // append-only, mirrored to localStorage
|
| 70 |
+
unsaved: 0, sessionId: crypto.randomUUID().slice(0, 8),
|
| 71 |
+
shownAt: 0, rubricVersion: "", resultsRepo: null, lastSummary: null,
|
| 72 |
+
breakShownAt: Date.now(),
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
const logKey = () => `audit:${CONFIG.task}:${state.user}`;
|
| 76 |
+
const latestByKey = (events) => {
|
| 77 |
+
const map = new Map();
|
| 78 |
+
for (const ev of events) map.set(ev.key, ev); // events are time-ordered
|
| 79 |
+
return map;
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
function loadLocalEvents() {
|
| 83 |
+
try { return JSON.parse(localStorage.getItem(logKey()) ?? "[]"); }
|
| 84 |
+
catch { return []; }
|
| 85 |
+
}
|
| 86 |
+
function persistLocal() {
|
| 87 |
+
localStorage.setItem(logKey(), JSON.stringify(state.events));
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Seeded per-user order (fatigue decorrelation) — mulberry32 over a string hash.
|
| 91 |
+
function seededOrder(n, seed) {
|
| 92 |
+
let h = 1779033703;
|
| 93 |
+
for (const ch of seed) { h = Math.imul(h ^ ch.charCodeAt(0), 3432918353); h = (h << 13) | (h >>> 19); }
|
| 94 |
+
const rand = () => {
|
| 95 |
+
h = Math.imul(h ^ (h >>> 16), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909);
|
| 96 |
+
return ((h ^= h >>> 16) >>> 0) / 4294967296;
|
| 97 |
+
};
|
| 98 |
+
const order = [...Array(n).keys()];
|
| 99 |
+
for (let i = n - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [order[i], order[j]] = [order[j], order[i]]; }
|
| 100 |
+
return order;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Warm-ups always come first, in file order; real items follow in seeded order.
|
| 104 |
+
function buildOrder() {
|
| 105 |
+
const warm = [], real = [];
|
| 106 |
+
state.items.forEach((item, i) => (item.warmup ? warm : real).push(i));
|
| 107 |
+
const shuffled = seededOrder(real.length, `${CONFIG.task}:${state.user}`).map((i) => real[i]);
|
| 108 |
+
state.order = [...warm, ...shuffled];
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function keyOf(item) { return String(item.id ?? item.key); }
|
| 112 |
+
|
| 113 |
+
function firstUngraded() {
|
| 114 |
+
const done = latestByKey(state.events);
|
| 115 |
+
for (let pos = 0; pos < state.order.length; pos++) {
|
| 116 |
+
const item = state.items[state.order[pos]];
|
| 117 |
+
const ev = done.get(keyOf(item));
|
| 118 |
+
if (!ev || !CONFIG.isComplete(ev)) return pos;
|
| 119 |
+
}
|
| 120 |
+
return state.order.length;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// ── results repo (own namespace; contribute-repos scope) ─────────────────────
|
| 124 |
+
async function ensureResultsRepo() {
|
| 125 |
+
const name = `${state.user}/localgate-audit-results`;
|
| 126 |
+
state.resultsRepo = { type: "dataset", name };
|
| 127 |
+
try {
|
| 128 |
+
await createRepo({ repo: state.resultsRepo, accessToken: state.auth.accessToken,
|
| 129 |
+
private: true });
|
| 130 |
+
} catch (err) {
|
| 131 |
+
// 409 = already exists (any prior session) — the expected steady state.
|
| 132 |
+
if (!/409|exist/i.test(String(err))) throw err;
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const remotePath = () => `${CONFIG.task}/${state.user}.jsonl`;
|
| 137 |
+
|
| 138 |
+
async function pullRemote() {
|
| 139 |
+
const blob = await downloadFile({ repo: state.resultsRepo, path: remotePath(),
|
| 140 |
+
accessToken: state.auth.accessToken });
|
| 141 |
+
if (blob === null) return []; // no file yet — first session
|
| 142 |
+
return (await blob.text()).split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
async function push() {
|
| 146 |
+
if (!state.events.length) return;
|
| 147 |
+
const jsonl = state.events.map((ev) => JSON.stringify(ev)).join("\n") + "\n";
|
| 148 |
+
await uploadFiles({
|
| 149 |
+
repo: state.resultsRepo, accessToken: state.auth.accessToken,
|
| 150 |
+
commitTitle: `${CONFIG.task}: ${state.user} — ${state.events.length} events`,
|
| 151 |
+
files: [{ path: remotePath(), content: new Blob([jsonl]) }],
|
| 152 |
+
});
|
| 153 |
+
state.unsaved = 0;
|
| 154 |
+
renderStatus();
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
async function pushGuarded() {
|
| 158 |
+
try { await push(); }
|
| 159 |
+
catch (err) {
|
| 160 |
+
banner(/403|401/.test(String(err))
|
| 161 |
+
? "Saved locally, but publishing failed (permissions). Nothing is lost — press s to retry after it's fixed."
|
| 162 |
+
: `Saved locally; publishing failed: ${String(err).slice(0, 120)} — press s to retry.`);
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// ── recording ────────────────────────────────────────────────────────────────
|
| 167 |
+
function record(item, values, unsure, note) {
|
| 168 |
+
const key = keyOf(item);
|
| 169 |
+
const prior = [...state.events].reverse().find((ev) => ev.key === key);
|
| 170 |
+
state.events.push({
|
| 171 |
+
key, ...values,
|
| 172 |
+
unsure: !!unsure, note: note || "",
|
| 173 |
+
user: state.user, ts: new Date().toISOString(),
|
| 174 |
+
order_index: state.idx, elapsed_ms: Date.now() - state.shownAt,
|
| 175 |
+
edit_count: prior ? (prior.edit_count ?? 0) + 1 : 0,
|
| 176 |
+
session_id: state.sessionId, rubric_version: state.rubricVersion,
|
| 177 |
+
client: `${CONFIG.task}@${CONFIG.build}`,
|
| 178 |
+
...(item.warmup ? { warmup: true } : {}),
|
| 179 |
+
});
|
| 180 |
+
persistLocal();
|
| 181 |
+
state.lastSummary = `${key} → ${CONFIG.summarize(values)}${unsure ? " (unsure)" : ""}`;
|
| 182 |
+
state.unsaved += 1;
|
| 183 |
+
if (state.unsaved >= SAVE_EVERY) pushGuarded();
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// ── UI ───────────────────────────────────────────────────────────────────────
|
| 187 |
+
function banner(text) {
|
| 188 |
+
const box = $("#banner");
|
| 189 |
+
box.textContent = text;
|
| 190 |
+
box.hidden = !text;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function renderStatus() {
|
| 194 |
+
const done = [...latestByKey(state.events).values()]
|
| 195 |
+
.filter((ev) => !ev.warmup && CONFIG.isComplete(ev)).length;
|
| 196 |
+
const total = state.items.filter((item) => !item.warmup).length;
|
| 197 |
+
$("#progress").textContent =
|
| 198 |
+
`${done}/${total} graded · ${state.unsaved} unsaved` +
|
| 199 |
+
(state.items[state.order[state.idx]]?.warmup ? " · WARM-UP" : "");
|
| 200 |
+
$("#bar-fill").style.width = `${(100 * done) / total}%`;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
function contentBlock(label, text, opts = {}) {
|
| 204 |
+
const wrap = el("section", "field" + (opts.scroll ? " response" : ""));
|
| 205 |
+
const head = el("b", null, label + (opts.count ? ` · ${text.length} chars` : ""));
|
| 206 |
+
wrap.append(head);
|
| 207 |
+
const body = el("div", "content");
|
| 208 |
+
body.textContent = text; // textContent ONLY — never innerHTML
|
| 209 |
+
wrap.append(body);
|
| 210 |
+
if (opts.scroll) wrap.append(el("div", "endmark", "· · · end of response · · ·"));
|
| 211 |
+
return wrap;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
function renderItem() {
|
| 215 |
+
const main = $("#main");
|
| 216 |
+
main.replaceChildren();
|
| 217 |
+
banner("");
|
| 218 |
+
|
| 219 |
+
if (state.idx >= state.order.length) {
|
| 220 |
+
main.append(el("h2", null, "All items graded — thank you!"));
|
| 221 |
+
main.append(el("p", null, "Final save in progress. You can close this tab once the counter reads 0 unsaved."));
|
| 222 |
+
pushGuarded();
|
| 223 |
+
renderStatus();
|
| 224 |
+
$("#controls").hidden = true;
|
| 225 |
+
return;
|
| 226 |
+
}
|
| 227 |
+
const item = state.items[state.order[state.idx]];
|
| 228 |
+
if (item.warmup) {
|
| 229 |
+
main.append(el("p", "warmup-note",
|
| 230 |
+
"Warm-up item — discussable with the others; real items start after these and must be graded independently."));
|
| 231 |
+
}
|
| 232 |
+
for (const block of CONFIG.blocks(item)) {
|
| 233 |
+
main.append(contentBlock(block.label, block.text, block));
|
| 234 |
+
}
|
| 235 |
+
state.shownAt = Date.now();
|
| 236 |
+
renderControls(item);
|
| 237 |
+
renderStatus();
|
| 238 |
+
if (Date.now() - state.breakShownAt > 45 * 60 * 1000) {
|
| 239 |
+
banner("You've been at this a while — good moment for a break. Everything up to the counter is saved.");
|
| 240 |
+
state.breakShownAt = Date.now();
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function renderControls(item) {
|
| 245 |
+
const controls = $("#controls");
|
| 246 |
+
controls.hidden = false;
|
| 247 |
+
controls.replaceChildren();
|
| 248 |
+
const current = latestByKey(state.events).get(keyOf(item));
|
| 249 |
+
const selection = { ...(current ? CONFIG.valuesOf(current) : {}) };
|
| 250 |
+
let unsure = current?.unsure ?? false;
|
| 251 |
+
|
| 252 |
+
const groups = [];
|
| 253 |
+
for (const field of CONFIG.fields) {
|
| 254 |
+
const group = el("div", "grp");
|
| 255 |
+
group.append(el("span", "lbl", field.label));
|
| 256 |
+
for (const [value, hotkey] of field.options) {
|
| 257 |
+
const button = el("button", null, value);
|
| 258 |
+
button.append(el("kbd", null, hotkey));
|
| 259 |
+
if (selection[field.name] === value) button.classList.add("sel");
|
| 260 |
+
button.addEventListener("click", () => choose(field.name, value));
|
| 261 |
+
group.append(button);
|
| 262 |
+
}
|
| 263 |
+
controls.append(group);
|
| 264 |
+
groups.push(group);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
const unsureBtn = el("button", "unsure" + (unsure ? " sel" : ""), "unsure");
|
| 268 |
+
unsureBtn.append(el("kbd", null, "u"));
|
| 269 |
+
unsureBtn.title = "marks this judgment as uncertain — it still counts, the flag is analysis metadata";
|
| 270 |
+
unsureBtn.addEventListener("click", () => { unsure = !unsure; unsureBtn.classList.toggle("sel", unsure); });
|
| 271 |
+
controls.append(unsureBtn);
|
| 272 |
+
|
| 273 |
+
const note = el("input", null);
|
| 274 |
+
note.id = "note"; note.placeholder = CONFIG.notePlaceholder;
|
| 275 |
+
note.value = current?.note ?? "";
|
| 276 |
+
controls.append(note);
|
| 277 |
+
|
| 278 |
+
const nav = el("div", "nav");
|
| 279 |
+
const prev = el("button", null, "← prev"); prev.append(el("kbd", null, "j"));
|
| 280 |
+
prev.addEventListener("click", () => move(-1));
|
| 281 |
+
const skip = el("button", null, "next →"); skip.append(el("kbd", null, "k"));
|
| 282 |
+
skip.addEventListener("click", () => move(1));
|
| 283 |
+
const save = el("button", null, "save"); save.append(el("kbd", null, "s"));
|
| 284 |
+
save.addEventListener("click", () => pushGuarded());
|
| 285 |
+
nav.append(prev, skip, save);
|
| 286 |
+
controls.append(nav);
|
| 287 |
+
|
| 288 |
+
if (state.lastSummary) controls.append(el("div", "last", `last: ${state.lastSummary} — j to revisit`));
|
| 289 |
+
|
| 290 |
+
function choose(name, value) {
|
| 291 |
+
selection[name] = value;
|
| 292 |
+
CONFIG.fields.forEach((field, fi) => {
|
| 293 |
+
groups[fi].querySelectorAll("button").forEach((button) => {
|
| 294 |
+
const label = button.childNodes[0].textContent;
|
| 295 |
+
button.classList.toggle("sel", selection[field.name] === label);
|
| 296 |
+
});
|
| 297 |
+
});
|
| 298 |
+
if (CONFIG.fields.every((field) => selection[field.name])) {
|
| 299 |
+
record(item, selection, unsure, note.value);
|
| 300 |
+
move(1);
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
controls._choose = choose; // for the keyboard handler
|
| 304 |
+
controls._toggleUnsure = () => unsureBtn.click();
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
function move(delta) {
|
| 308 |
+
state.idx = Math.max(0, Math.min(state.order.length, state.idx + delta));
|
| 309 |
+
renderItem();
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
document.addEventListener("keydown", (event) => {
|
| 313 |
+
if (event.target.tagName === "INPUT" || $("#controls").hidden) return;
|
| 314 |
+
const controls = $("#controls");
|
| 315 |
+
if (event.key === "j") { event.preventDefault(); move(-1); return; }
|
| 316 |
+
if (event.key === "k") { event.preventDefault(); move(1); return; }
|
| 317 |
+
if (event.key === "s") { event.preventDefault(); pushGuarded(); return; }
|
| 318 |
+
if (event.key === "u") { event.preventDefault(); controls._toggleUnsure?.(); return; }
|
| 319 |
+
if (event.key === "e") {
|
| 320 |
+
event.preventDefault();
|
| 321 |
+
const resp = document.querySelector(".response .content");
|
| 322 |
+
if (resp) resp.parentElement.scrollTop = resp.parentElement.scrollHeight;
|
| 323 |
+
return;
|
| 324 |
+
}
|
| 325 |
+
const binding = CONFIG.hotkeys[event.key];
|
| 326 |
+
if (binding) { event.preventDefault(); controls._choose?.(binding[0], binding[1]); }
|
| 327 |
+
});
|
| 328 |
+
|
| 329 |
+
document.addEventListener("visibilitychange", () => {
|
| 330 |
+
if (document.visibilityState === "hidden" && state.unsaved) pushGuarded(); // best-effort
|
| 331 |
+
});
|
| 332 |
+
|
| 333 |
+
// ── boot ─────────────────────────────────────────────────────────────────────
|
| 334 |
+
async function boot() {
|
| 335 |
+
$("#task-title").textContent = CONFIG.title;
|
| 336 |
+
state.rubricVersion = await rubricVersion();
|
| 337 |
+
$("#rubric-body").textContent = CONFIG.rubric;
|
| 338 |
+
|
| 339 |
+
if (window.self !== window.top) {
|
| 340 |
+
// Embedded in hf.co: OAuth storage is partitioned here — link out instead.
|
| 341 |
+
const main = $("#main");
|
| 342 |
+
main.replaceChildren(el("p", null, "Open this Space in its own tab to sign in:"));
|
| 343 |
+
const link = el("a", "open-out", location.href);
|
| 344 |
+
link.href = location.href; link.target = "_blank"; link.rel = "noopener";
|
| 345 |
+
main.append(link);
|
| 346 |
+
return;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
state.auth = await ensureAuth();
|
| 350 |
+
if (!state.auth) {
|
| 351 |
+
const main = $("#main");
|
| 352 |
+
main.replaceChildren(el("p", null, CONFIG.landing));
|
| 353 |
+
main.append(el("p", "independence", CONFIG.independence));
|
| 354 |
+
const button = el("button", "signin", "Sign in with Hugging Face");
|
| 355 |
+
button.addEventListener("click", signIn);
|
| 356 |
+
main.append(button);
|
| 357 |
+
return;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
const username = state.auth.userInfo.preferred_username;
|
| 361 |
+
const inOrg = (state.auth.userInfo.orgs ?? []).some((org) => org.preferred_username === "localgate");
|
| 362 |
+
if (!inOrg) {
|
| 363 |
+
$("#main").replaceChildren(el("p", null,
|
| 364 |
+
`@${username} is not on this study's annotator list — ask Samuel to add you to the localgate org.`));
|
| 365 |
+
return;
|
| 366 |
+
}
|
| 367 |
+
state.user = username;
|
| 368 |
+
$("#whoami").textContent = `@${username}`;
|
| 369 |
+
|
| 370 |
+
try {
|
| 371 |
+
const blob = await downloadFile({ repo: ITEMS_REPO, path: CONFIG.itemsPath,
|
| 372 |
+
accessToken: state.auth.accessToken });
|
| 373 |
+
if (blob === null) throw new Error("items file missing from localgate/audit-items");
|
| 374 |
+
state.items = JSON.parse(await blob.text());
|
| 375 |
+
} catch (err) {
|
| 376 |
+
$("#main").replaceChildren(el("p", null, `Could not load items: ${String(err).slice(0, 140)}`));
|
| 377 |
+
const retry = el("button", null, "retry");
|
| 378 |
+
retry.addEventListener("click", boot);
|
| 379 |
+
$("#main").append(retry);
|
| 380 |
+
return;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
await ensureResultsRepo();
|
| 384 |
+
// Merge remote history with local log: union by (key, ts) — both are append-only.
|
| 385 |
+
let remote = [];
|
| 386 |
+
try { remote = await pullRemote(); }
|
| 387 |
+
catch (err) { banner(`Could not read previous progress (${String(err).slice(0, 80)}) — grading continues; local log is intact.`); }
|
| 388 |
+
const seen = new Set(remote.map((ev) => ev.key + "|" + ev.ts));
|
| 389 |
+
state.events = [...remote,
|
| 390 |
+
...loadLocalEvents().filter((ev) => !seen.has(ev.key + "|" + ev.ts))]
|
| 391 |
+
.sort((a, b) => a.ts.localeCompare(b.ts));
|
| 392 |
+
persistLocal();
|
| 393 |
+
|
| 394 |
+
buildOrder();
|
| 395 |
+
state.idx = firstUngraded();
|
| 396 |
+
renderItem();
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
boot();
|
config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
window.CONFIG = {
|
| 2 |
+
task: "conversion",
|
| 3 |
+
title: "Conversion audit — 152 items",
|
| 4 |
+
itemsPath: "conversion_items.json",
|
| 5 |
+
build: "c7f424d",
|
| 6 |
+
landing: "You will judge whether each rewritten exam question still asks the same "
|
| 7 |
+
+ "question as the original, and whether it stands on its own without the answer "
|
| 8 |
+
+ "options. 152 items plus 3 discussable warm-ups; expect roughly 1.5–2 hours, "
|
| 9 |
+
+ "in as many sittings as you like — progress saves continuously.",
|
| 10 |
+
independence: "Grade independently: do not discuss real items with the other "
|
| 11 |
+
+ "annotators until everyone has finished.",
|
| 12 |
+
rubric: "SAME QUESTION — yes when the rewrite asks for exactly the fact the "
|
| 13 |
+
+ "original asked for; no when it broadens, narrows, or shifts what counts as a "
|
| 14 |
+
+ "correct answer.\n\nSELF-CONTAINED — yes when the rewrite is answerable "
|
| 15 |
+
+ "without ever seeing the answer options: it names its subject, includes any "
|
| 16 |
+
+ "data the options used to carry, and does not say things like 'which of the "
|
| 17 |
+
+ "following'. no when answering it would require the removed options or other "
|
| 18 |
+
+ "missing context.\n\nJudge the rewrite as a fresh reader would. The original "
|
| 19 |
+
+ "and its options are shown only so you can compare; the model that answers the "
|
| 20 |
+
+ "rewrite will never see them.\n\nThe optional note is for anything that made an "
|
| 21 |
+
+ "item hard to call — one clause is plenty. Press u to flag a judgment you "
|
| 22 |
+
+ "are unsure about (it still counts; the flag helps the disagreement analysis).",
|
| 23 |
+
fields: [
|
| 24 |
+
{ name: "same_question", label: "same question?", options: [["yes","1"],["no","2"]] },
|
| 25 |
+
{ name: "self_contained", label: "self-contained?", options: [["yes","3"],["no","4"]] },
|
| 26 |
+
],
|
| 27 |
+
hotkeys: { "1":["same_question","yes"], "2":["same_question","no"],
|
| 28 |
+
"3":["self_contained","yes"], "4":["self_contained","no"] },
|
| 29 |
+
notePlaceholder: "why (optional)",
|
| 30 |
+
isComplete: (ev) => !!(ev.same_question && ev.self_contained),
|
| 31 |
+
valuesOf: (ev) => ({ same_question: ev.same_question, self_contained: ev.self_contained }),
|
| 32 |
+
summarize: (v) => `same:${v.same_question} self:${v.self_contained}`,
|
| 33 |
+
blocks: (item) => [
|
| 34 |
+
{ label: "original (with options)",
|
| 35 |
+
text: item.original + "\n\n" + item.options.map((o,i)=>String.fromCharCode(65+i)+". "+o).join("\n") },
|
| 36 |
+
{ label: "reference answer", text: item.reference_answer },
|
| 37 |
+
{ label: "rewritten (open-ended)", text: item.rewritten, count: true },
|
| 38 |
+
],
|
| 39 |
+
};
|
hub-2.15.0.bundle.mjs
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
index.html
CHANGED
|
@@ -1,19 +1,25 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; connect-src https://huggingface.co https://*.hf.co; img-src 'self' data:; base-uri 'none'; form-action 'none'">
|
| 7 |
+
<title>LocalGate audit</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css?v=c7f424d">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<header>
|
| 12 |
+
<h1 id="task-title">Loading…</h1>
|
| 13 |
+
<span id="whoami"></span>
|
| 14 |
+
<span id="progress"></span>
|
| 15 |
+
</header>
|
| 16 |
+
<div id="bar"><div id="bar-fill"></div></div>
|
| 17 |
+
<div id="banner" hidden></div>
|
| 18 |
+
<main id="main"><p>Loading…</p></main>
|
| 19 |
+
<details id="rubric"><summary>Rubric & edge rules (keep open while grading)</summary>
|
| 20 |
+
<pre id="rubric-body"></pre></details>
|
| 21 |
+
<div id="controls" hidden></div>
|
| 22 |
+
<script src="config.js?v=c7f424d"></script>
|
| 23 |
+
<script type="module" src="app.js?v=c7f424d"></script>
|
| 24 |
+
</body>
|
| 25 |
</html>
|
style.css
CHANGED
|
@@ -1,28 +1,43 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
.
|
| 27 |
-
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root { --ink:#1a1a1a; --dim:#666; --paper:#fafaf7; --card:#fff; --line:#ddd;
|
| 2 |
+
--accent:#2c5f8a; --warn:#8a5a1a; }
|
| 3 |
+
* { box-sizing: border-box; }
|
| 4 |
+
body { margin:0; font:16px/1.55 Georgia, serif; color:var(--ink); background:var(--paper); }
|
| 5 |
+
header { display:flex; gap:1rem; align-items:baseline; padding:.6rem 1.1rem; flex-wrap:wrap;
|
| 6 |
+
border-bottom:2px solid var(--ink); background:var(--card);
|
| 7 |
+
font-family:ui-sans-serif,system-ui; }
|
| 8 |
+
header h1 { font-size:1rem; margin:0; }
|
| 9 |
+
#whoami, #progress { color:var(--dim); font-size:.85rem; }
|
| 10 |
+
#bar { height:5px; background:var(--line); } #bar-fill { height:100%; width:0; background:var(--accent); }
|
| 11 |
+
#banner { padding:.5rem 1.1rem; background:#fdf3e3; color:var(--warn);
|
| 12 |
+
font-family:ui-sans-serif,system-ui; font-size:.9rem; }
|
| 13 |
+
main { max-width:56rem; margin:1rem auto 11rem; padding:0 1rem; }
|
| 14 |
+
.field { background:var(--card); border:1px solid var(--line); padding:.7rem 1rem; margin:.7rem 0; }
|
| 15 |
+
.field b { display:block; font-family:ui-sans-serif,system-ui; font-size:.72rem;
|
| 16 |
+
text-transform:uppercase; letter-spacing:.08em; color:var(--dim); margin-bottom:.3rem; }
|
| 17 |
+
.field .content { white-space:pre-wrap; overflow-wrap:anywhere; }
|
| 18 |
+
.response { max-height:24rem; overflow-y:auto; border-width:2px; }
|
| 19 |
+
.endmark { text-align:center; color:var(--dim); font-size:.8rem; padding-top:.5rem;
|
| 20 |
+
font-family:ui-sans-serif,system-ui; }
|
| 21 |
+
.warmup-note { background:#eef4fa; border:1px solid var(--accent); padding:.5rem .8rem;
|
| 22 |
+
font-family:ui-sans-serif,system-ui; font-size:.9rem; }
|
| 23 |
+
.independence { color:var(--dim); font-style:italic; }
|
| 24 |
+
button.signin { font:inherit; font-size:1rem; padding:.6rem 1.4rem; cursor:pointer;
|
| 25 |
+
border:1.5px solid var(--ink); background:var(--ink); color:#fff; }
|
| 26 |
+
details#rubric { position:fixed; bottom:5.6rem; left:0; right:0; background:var(--card);
|
| 27 |
+
border-top:1px solid var(--line); padding:.4rem 1.1rem; max-height:40vh;
|
| 28 |
+
overflow-y:auto; font-family:ui-sans-serif,system-ui; font-size:.85rem; }
|
| 29 |
+
details#rubric summary { cursor:pointer; color:var(--accent); }
|
| 30 |
+
details#rubric pre { white-space:pre-wrap; font:inherit; }
|
| 31 |
+
#controls { position:fixed; bottom:0; left:0; right:0; background:var(--card);
|
| 32 |
+
border-top:2px solid var(--ink); padding:.55rem 1.1rem; display:flex;
|
| 33 |
+
gap:.6rem; align-items:center; flex-wrap:wrap; font-family:ui-sans-serif,system-ui; }
|
| 34 |
+
#controls .grp { display:flex; gap:.35rem; align-items:center; }
|
| 35 |
+
#controls .lbl { font-size:.78rem; color:var(--dim); }
|
| 36 |
+
#controls button { font:inherit; font-size:.9rem; padding:.4rem .8rem; cursor:pointer;
|
| 37 |
+
border:1.5px solid var(--line); background:var(--card); }
|
| 38 |
+
#controls button.sel { border-color:var(--ink); background:var(--ink); color:#fff; }
|
| 39 |
+
#controls button.unsure.sel { background:var(--warn); border-color:var(--warn); }
|
| 40 |
+
#controls kbd { font-size:.7rem; opacity:.55; margin-left:.3rem; }
|
| 41 |
+
#controls #note { font:inherit; font-size:.85rem; padding:.35rem; border:1px solid var(--line); width:13rem; }
|
| 42 |
+
#controls .nav { margin-left:auto; display:flex; gap:.35rem; }
|
| 43 |
+
#controls .last { width:100%; color:var(--dim); font-size:.78rem; }
|