Spaces:
Sleeping
Sleeping
zofiasmolenasana commited on
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
- .gitignore +3 -12
- app.py +108 -0
- 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 & 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">← 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 & 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 |
|