abullard1 commited on
Commit
1aa3bac
·
verified ·
1 Parent(s): a1a1880

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +22 -5
  2. app.js +399 -0
  3. config.js +39 -0
  4. hub-2.15.0.bundle.mjs +0 -0
  5. index.html +23 -17
  6. style.css +43 -28
README.md CHANGED
@@ -1,10 +1,27 @@
1
  ---
2
- title: Audit Conversion
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: purple
6
  sdk: static
 
7
  pinned: false
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
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 &amp; 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
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
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; }