zofiasmolenasana commited on
Commit
0129ad7
Β·
unverified Β·
1 Parent(s): 2774e3f

Add junk label, clear-all button, and previous sheet navigation

Browse files

- Junk label (J key) for marking useless content discarded from chunks
- Clear All button to reset all labels on current sheet (with undo)
- Previous sheet navigation with history dropdown for re-editing
- Backend endpoints: /api/history, /api/load_labeled_sheet
- Updated .gitignore for deployment safety

Made-with: Cursor

Files changed (3) hide show
  1. .gitignore +3 -12
  2. app.py +108 -0
  3. static/index.html +144 -0
.gitignore CHANGED
@@ -1,19 +1,10 @@
1
  credentials/
 
2
  *.pyc
3
  __pycache__/
4
  .env
5
- data/labeled/
6
- data/tmp/
7
- data/raw/
8
- .DS_Store
9
- *.egg-info/
10
- dist/
11
- build/
12
  .sfdx/
 
13
  spreadsheet-labeller-*.json
 
14
  docs/
15
- *.pt
16
- data/experiments/
17
- data/models/
18
- data/splits/
19
- data/spreadsheet_gnn.pt
 
1
  credentials/
2
+ data/
3
  *.pyc
4
  __pycache__/
5
  .env
 
 
 
 
 
 
 
6
  .sfdx/
7
+ .cursor/
8
  spreadsheet-labeller-*.json
9
+ .DS_Store
10
  docs/
 
 
 
 
 
app.py CHANGED
@@ -318,6 +318,114 @@ def get_progress():
318
  return metadata_client.get_progress()
319
 
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  @app.post("/api/ingest")
322
  def ingest_data(
323
  source: str = Query("sheetpedia"),
 
318
  return metadata_client.get_progress()
319
 
320
 
321
+ @app.get("/api/history")
322
+ def get_labeling_history(
323
+ labeler: str = Query("anonymous"),
324
+ limit: int = Query(20),
325
+ _=Depends(_require_auth),
326
+ ):
327
+ """Return recently labeled sheets by this labeler, newest first."""
328
+ rows = metadata_client.get_all_rows()
329
+ labeled = [
330
+ {
331
+ "file_name": r["file_name"],
332
+ "drive_file_id": r["drive_file_id"],
333
+ "sheet_name": r["sheet_name"],
334
+ "timestamp": r["timestamp"],
335
+ "num_labeled_cells": r["num_labeled_cells"],
336
+ "meta_row_index": r["_row_index"],
337
+ }
338
+ for r in rows
339
+ if r["status"] == "labelled" and r["labeler"] == labeler
340
+ ]
341
+ labeled.sort(key=lambda r: r.get("timestamp", ""), reverse=True)
342
+ return {"sheets": labeled[:limit]}
343
+
344
+
345
+ @app.get("/api/load_labeled_sheet")
346
+ def load_labeled_sheet(
347
+ drive_file_id: str = Query(...),
348
+ sheet_name: str = Query(...),
349
+ meta_row_index: int = Query(...),
350
+ _=Depends(_require_auth),
351
+ ):
352
+ """Load a previously labeled sheet with its saved labels for re-editing."""
353
+ # Load the grid from xlsx
354
+ try:
355
+ local_path = str(config.TEMP_DIR / f"{drive_file_id}.xlsx")
356
+ if not Path(local_path).exists():
357
+ drive_client.download_file(drive_file_id, local_path)
358
+ grid = xlsx_client.fetch_xlsx_sheet(local_path, sheet_name)
359
+ except Exception as e:
360
+ logger.exception("Error reading sheet for re-edit: %s/%s", drive_file_id, sheet_name)
361
+ raise HTTPException(status_code=502, detail=f"Sheet read error: {e}")
362
+
363
+ grid["drive_file_id"] = drive_file_id
364
+ grid["meta_row_index"] = meta_row_index
365
+
366
+ # Find saved labels: try local file first, then Drive
367
+ saved_labels = _load_saved_labels(drive_file_id, sheet_name, grid["spreadsheet_id"])
368
+
369
+ return {
370
+ "sheet": grid,
371
+ "saved_labels": saved_labels,
372
+ }
373
+
374
+
375
+ def _load_saved_labels(
376
+ drive_file_id: str, sheet_name: str, spreadsheet_id: str,
377
+ ) -> list[dict] | None:
378
+ """Load previously saved labels from local JSON or Drive."""
379
+ safe_name = f"{spreadsheet_id}_{sheet_name}".replace("/", "_").replace(" ", "_")
380
+ local_path = config.LABELED_DIR / f"{safe_name}.json"
381
+
382
+ data = None
383
+ if local_path.exists():
384
+ with open(local_path) as f:
385
+ data = json.load(f)
386
+ else:
387
+ # Try finding it on Drive via metadata
388
+ rows = metadata_client.get_all_rows()
389
+ match = next(
390
+ (r for r in rows
391
+ if r["drive_file_id"] == drive_file_id
392
+ and r["sheet_name"] == sheet_name
393
+ and r["labels_file_id"]
394
+ and r["labels_file_id"] != "local-only"),
395
+ None,
396
+ )
397
+ if match:
398
+ try:
399
+ tmp = str(config.TEMP_DIR / f"labels_{safe_name}.json")
400
+ drive_client.download_file(match["labels_file_id"], tmp)
401
+ with open(tmp) as f:
402
+ data = json.load(f)
403
+ except Exception:
404
+ logger.warning("Could not download labels from Drive for %s", safe_name)
405
+
406
+ if not data or "cells" not in data:
407
+ return None
408
+
409
+ result = []
410
+ for c in data["cells"]:
411
+ label = c.get("label", "unlabeled")
412
+ if label == "unlabeled":
413
+ continue
414
+ entry = {
415
+ "row": c["row"],
416
+ "col": c["col"],
417
+ "label": label,
418
+ "table": c.get("table", 0),
419
+ }
420
+ target = c.get("comment_target")
421
+ if label == "comment" and target:
422
+ entry["comment_target_row"] = target["row"]
423
+ entry["comment_target_col"] = target["col"]
424
+ result.append(entry)
425
+
426
+ return result
427
+
428
+
429
  @app.post("/api/ingest")
430
  def ingest_data(
431
  source: str = Query("sheetpedia"),
static/index.html CHANGED
@@ -23,6 +23,26 @@
23
  border:2px solid #dc2626;border-radius:8px;cursor:pointer;transition:all .15s;white-space:nowrap}
24
  #clear-all-btn:hover{background:#dc2626;color:#fff}
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  /* Selector bars (table & level) */
27
  .selector-bar{display:flex;align-items:center;gap:6px;padding:0 24px;background:#fff;
28
  border-bottom:1px solid #eee;height:34px}
@@ -219,6 +239,10 @@
219
  <button data-lang="pl">PL</button>
220
  </div>
221
  <button id="help-btn" title="Labeling guide / Instrukcja">?</button>
 
 
 
 
222
  <button id="clear-all-btn" data-i18n="clear_all_btn">CLEAR ALL</button>
223
  <button id="save-btn" disabled data-i18n="save_btn">SAVE &amp; NEXT</button>
224
  </div>
@@ -271,8 +295,12 @@
271
  loading_sheet: "Loading first sheet\u2026",
272
  username_placeholder: "Your name",
273
  save_btn: "SAVE & NEXT",
 
274
  clear_all_btn: "CLEAR ALL",
275
  clear_all_confirm: "Remove all labels from this sheet? This can be undone with Ctrl+Z.",
 
 
 
276
  saving: "Saving\u2026",
277
  done: "Done!",
278
  all_done: "All sheets have been labeled!",
@@ -314,8 +342,12 @@
314
  loading_sheet: "\u0141adowanie pierwszego arkusza\u2026",
315
  username_placeholder: "Twoje imi\u0119",
316
  save_btn: "ZAPISZ I DALEJ",
 
317
  clear_all_btn: "WYCZYΕšΔ† WSZYSTKO",
318
  clear_all_confirm: "UsunΔ…Δ‡ wszystkie etykiety z tego arkusza? MoΕΌna cofnΔ…Δ‡ przez Ctrl+Z.",
 
 
 
319
  saving: "Zapisywanie\u2026",
320
  done: "Gotowe!",
321
  all_done: "Wszystkie arkusze zosta\u0142y oznaczone!",
@@ -1226,6 +1258,118 @@
1226
  drawCommentLinks();
1227
  });
1228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1229
  /* ── Save & Next ───────────────────────────────────────────────────── */
1230
  saveBtn.addEventListener("click", doSave);
1231
 
 
23
  border:2px solid #dc2626;border-radius:8px;cursor:pointer;transition:all .15s;white-space:nowrap}
24
  #clear-all-btn:hover{background:#dc2626;color:#fff}
25
 
26
+ /* Prev button & history dropdown */
27
+ .prev-wrapper{position:relative}
28
+ #prev-btn{padding:8px 16px;font-size:13px;font-weight:600;background:#fff;color:#555;
29
+ border:2px solid #ccc;border-radius:8px;cursor:pointer;transition:all .15s;white-space:nowrap}
30
+ #prev-btn:hover{border-color:#2563eb;color:#2563eb}
31
+ .history-popup{position:absolute;top:100%;left:0;margin-top:6px;background:#fff;
32
+ border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12);
33
+ z-index:200;min-width:340px;max-height:360px;overflow-y:auto;display:none}
34
+ .history-popup.open{display:block}
35
+ .history-popup .history-header{padding:10px 14px;font-size:13px;font-weight:600;color:#333;
36
+ border-bottom:1px solid #eee}
37
+ .history-popup .history-empty{padding:16px 14px;font-size:13px;color:#888;text-align:center}
38
+ .history-popup .history-item{padding:8px 14px;cursor:pointer;border-bottom:1px solid #f0f0f0;
39
+ transition:background .1s;display:flex;flex-direction:column;gap:2px}
40
+ .history-popup .history-item:hover{background:#f0f7ff}
41
+ .history-popup .history-item:last-child{border-bottom:none}
42
+ .history-popup .history-item .hi-name{font-size:13px;font-weight:500;color:#222;
43
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
44
+ .history-popup .history-item .hi-meta{font-size:11px;color:#888}
45
+
46
  /* Selector bars (table & level) */
47
  .selector-bar{display:flex;align-items:center;gap:6px;padding:0 24px;background:#fff;
48
  border-bottom:1px solid #eee;height:34px}
 
239
  <button data-lang="pl">PL</button>
240
  </div>
241
  <button id="help-btn" title="Labeling guide / Instrukcja">?</button>
242
+ <div class="prev-wrapper" id="prev-wrapper">
243
+ <button id="prev-btn" data-i18n="prev_btn">&larr; PREV</button>
244
+ <div class="history-popup" id="history-popup"></div>
245
+ </div>
246
  <button id="clear-all-btn" data-i18n="clear_all_btn">CLEAR ALL</button>
247
  <button id="save-btn" disabled data-i18n="save_btn">SAVE &amp; NEXT</button>
248
  </div>
 
295
  loading_sheet: "Loading first sheet\u2026",
296
  username_placeholder: "Your name",
297
  save_btn: "SAVE & NEXT",
298
+ prev_btn: "\u2190 PREV",
299
  clear_all_btn: "CLEAR ALL",
300
  clear_all_confirm: "Remove all labels from this sheet? This can be undone with Ctrl+Z.",
301
+ history_title: "Recently labeled sheets",
302
+ history_empty: "No labeled sheets yet.",
303
+ history_cells: "cells",
304
  saving: "Saving\u2026",
305
  done: "Done!",
306
  all_done: "All sheets have been labeled!",
 
342
  loading_sheet: "\u0141adowanie pierwszego arkusza\u2026",
343
  username_placeholder: "Twoje imi\u0119",
344
  save_btn: "ZAPISZ I DALEJ",
345
+ prev_btn: "\u2190 WRΓ“Δ†",
346
  clear_all_btn: "WYCZYΕšΔ† WSZYSTKO",
347
  clear_all_confirm: "UsunΔ…Δ‡ wszystkie etykiety z tego arkusza? MoΕΌna cofnΔ…Δ‡ przez Ctrl+Z.",
348
+ history_title: "Ostatnio oznaczone arkusze",
349
+ history_empty: "Brak oznaczonych arkuszy.",
350
+ history_cells: "komΓ³rek",
351
  saving: "Zapisywanie\u2026",
352
  done: "Gotowe!",
353
  all_done: "Wszystkie arkusze zosta\u0142y oznaczone!",
 
1258
  drawCommentLinks();
1259
  });
1260
 
1261
+ /* ── Previous sheet / history ──────────────────────────────────────── */
1262
+ const prevBtn = document.getElementById("prev-btn");
1263
+ const historyPopup = document.getElementById("history-popup");
1264
+
1265
+ prevBtn.addEventListener("click", async () => {
1266
+ if (historyPopup.classList.contains("open")) {
1267
+ historyPopup.classList.remove("open");
1268
+ return;
1269
+ }
1270
+ const username = usernameInput.value.trim() || "anonymous";
1271
+ historyPopup.innerHTML = `<div class="history-header">${t("history_title")}</div>
1272
+ <div class="history-empty">${t("loading")}</div>`;
1273
+ historyPopup.classList.add("open");
1274
+
1275
+ try {
1276
+ const resp = await fetch(`/api/history?labeler=${encodeURIComponent(username)}`);
1277
+ const data = await resp.json();
1278
+ const sheets = data.sheets || [];
1279
+ if (sheets.length === 0) {
1280
+ historyPopup.innerHTML = `<div class="history-header">${t("history_title")}</div>
1281
+ <div class="history-empty">${t("history_empty")}</div>`;
1282
+ return;
1283
+ }
1284
+ let html = `<div class="history-header">${t("history_title")}</div>`;
1285
+ for (const s of sheets) {
1286
+ const ts = s.timestamp ? new Date(s.timestamp).toLocaleString() : "";
1287
+ const cells = s.num_labeled_cells ? `${s.num_labeled_cells} ${t("history_cells")}` : "";
1288
+ const meta = [ts, cells].filter(Boolean).join(" Β· ");
1289
+ html += `<div class="history-item" data-dfid="${s.drive_file_id}"
1290
+ data-sn="${escHtml(s.sheet_name)}" data-mri="${s.meta_row_index}">
1291
+ <span class="hi-name">${escHtml(s.file_name)} / ${escHtml(s.sheet_name)}</span>
1292
+ <span class="hi-meta">${meta}</span>
1293
+ </div>`;
1294
+ }
1295
+ historyPopup.innerHTML = html;
1296
+
1297
+ historyPopup.querySelectorAll(".history-item").forEach(item => {
1298
+ item.addEventListener("click", () => {
1299
+ historyPopup.classList.remove("open");
1300
+ loadPreviousSheet(
1301
+ item.dataset.dfid,
1302
+ item.dataset.sn,
1303
+ parseInt(item.dataset.mri),
1304
+ );
1305
+ });
1306
+ });
1307
+ } catch (err) {
1308
+ historyPopup.innerHTML = `<div class="history-empty" style="color:#dc2626">${err.message}</div>`;
1309
+ }
1310
+ });
1311
+
1312
+ document.addEventListener("click", (e) => {
1313
+ if (!e.target.closest("#prev-wrapper")) historyPopup.classList.remove("open");
1314
+ });
1315
+
1316
+ async function loadPreviousSheet(driveFileId, sheetName, metaRowIndex) {
1317
+ saveBtn.disabled = true;
1318
+ tableWrap.innerHTML = `<p style="padding:40px;color:#888">${t("loading")}</p>`;
1319
+
1320
+ try {
1321
+ const url = `/api/load_labeled_sheet?drive_file_id=${encodeURIComponent(driveFileId)}`
1322
+ + `&sheet_name=${encodeURIComponent(sheetName)}`
1323
+ + `&meta_row_index=${metaRowIndex}`;
1324
+ const resp = await fetch(url);
1325
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
1326
+ const data = await resp.json();
1327
+
1328
+ currentSheet = data.sheet;
1329
+ cellLabels = {};
1330
+ commentLinks = {};
1331
+ exitLinkMode();
1332
+ selectedCells.clear();
1333
+ lastClickedKey = null;
1334
+ maxTableId = 1;
1335
+ maxLevel = 3;
1336
+ activeLevel = 1;
1337
+ clearUndoRedo();
1338
+
1339
+ if (data.saved_labels) {
1340
+ prefillFromSavedLabels(data.saved_labels);
1341
+ }
1342
+
1343
+ refreshDynamicCSS();
1344
+ renderTableBar();
1345
+ renderLevelBar();
1346
+ renderLegend();
1347
+ setActiveTable(1);
1348
+ sheetInfoEl.textContent = `${currentSheet.spreadsheet_id} / ${currentSheet.sheet_name}`;
1349
+ renderTable();
1350
+ saveBtn.disabled = false;
1351
+ } catch (err) {
1352
+ tableWrap.innerHTML = `<p style="padding:40px;color:red">${t("error_prefix")}${err.message}</p>`;
1353
+ }
1354
+ }
1355
+
1356
+ function prefillFromSavedLabels(labels) {
1357
+ if (!labels || !labels.length) return;
1358
+ for (const c of labels) {
1359
+ const key = `${c.row},${c.col}`;
1360
+ cellLabels[key] = { label: c.label, table: c.table || 0 };
1361
+ if (c.table > maxTableId) maxTableId = c.table;
1362
+ const m = c.label.match(/^(?:row_header|col_header)_(\d+)$/);
1363
+ if (m) {
1364
+ const lv = parseInt(m[1]);
1365
+ if (lv > maxLevel) maxLevel = lv;
1366
+ }
1367
+ if (c.label === "comment" && c.comment_target_row != null) {
1368
+ commentLinks[key] = `${c.comment_target_row},${c.comment_target_col}`;
1369
+ }
1370
+ }
1371
+ }
1372
+
1373
  /* ── Save & Next ───────────────────────────────────────────────────── */
1374
  saveBtn.addEventListener("click", doSave);
1375