this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add boolean search and result counts to label review

- Web app search supports `-term` exclusion (e.g. `integral -dif`)
- search-labels CLI gains `--exclude` flag (repeatable)
- /items returns total count; UI shows it next to search box and in load-more button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+58 -11
+29 -7
src/review_app.py
··· 166 166 params["split"] = split 167 167 168 168 if search: 169 - clauses.append("(i.typst LIKE :search OR e.typst_edited LIKE :search)") 170 - params["search"] = f"%{search}%" 169 + for k, term in enumerate(search.split()): 170 + if term.startswith("-") and len(term) > 1: 171 + word = term[1:] 172 + clauses.append( 173 + f"(i.typst NOT LIKE :excl{k} AND " 174 + f"(e.typst_edited IS NULL OR e.typst_edited NOT LIKE :excl{k}))" 175 + ) 176 + params[f"excl{k}"] = f"%{word}%" 177 + else: 178 + clauses.append( 179 + f"(i.typst LIKE :incl{k} OR e.typst_edited LIKE :incl{k})" 180 + ) 181 + params[f"incl{k}"] = f"%{term}%" 171 182 172 183 if filter == "pending": 173 184 clauses.append("r.image_path IS NULL") ··· 180 191 # "all" -- no extra clause 181 192 182 193 where = " AND ".join(clauses) 194 + # COUNT uses same clauses but without the cursor (i.id > 0) 195 + count_clauses = ["i.id > 0"] + clauses[1:] 196 + count_where = " AND ".join(count_clauses) 197 + 198 + joins = """ 199 + FROM items i 200 + LEFT JOIN reviews r ON i.image_path = r.image_path 201 + LEFT JOIN flags f ON i.image_path = f.image_path 202 + LEFT JOIN edits e ON i.image_path = e.image_path 203 + """ 204 + 205 + total = db.execute( 206 + f"SELECT COUNT(*) {joins} WHERE {count_where}", params 207 + ).fetchone()[0] 183 208 184 209 rows = db.execute(f""" 185 210 SELECT i.id, ··· 189 214 r.image_path IS NOT NULL AS reviewed, 190 215 f.image_path IS NOT NULL AS flagged, 191 216 e.image_path IS NOT NULL AS edited 192 - FROM items i 193 - LEFT JOIN reviews r ON i.image_path = r.image_path 194 - LEFT JOIN flags f ON i.image_path = f.image_path 195 - LEFT JOIN edits e ON i.image_path = e.image_path 217 + {joins} 196 218 WHERE {where} 197 219 ORDER BY i.id 198 220 LIMIT :limit ··· 200 222 201 223 items = [dict(r) for r in rows] 202 224 next_cursor = items[-1]["id"] if len(items) == limit else None 203 - return {"items": items, "next_cursor": next_cursor} 225 + return {"items": items, "next_cursor": next_cursor, "total": total} 204 226 205 227 # ── Image serving ───────────────────────────────────────────────────── 206 228
+7 -3
src/search_labels.py
··· 32 32 return re.compile(escaped if raw else r"\b" + escaped + r"\b") 33 33 34 34 35 - def search(pattern: str, splits: list[str], raw: bool = False) -> None: 35 + def search(pattern: str, splits: list[str], raw: bool = False, 36 + exclude: list[str] | None = None) -> None: 36 37 rx = _compile(pattern, raw) 38 + excl_rxs = [_compile(e, raw) for e in (exclude or [])] 37 39 total = 0 38 40 for split_name in splits: 39 41 manifest = DATA_ROOT / split_name / "manifest.jsonl" ··· 45 47 continue 46 48 r = json.loads(line) 47 49 t = r.get("typst", "") 48 - if rx.search(t): 50 + if rx.search(t) and not any(e.search(t) for e in excl_rxs): 49 51 hits.append(t) 50 52 if hits: 51 53 print(f"\n── {split_name} ({len(hits)} hits) ──") ··· 109 111 help="Replace matched pattern with this string") 110 112 parser.add_argument("--dry-run", action="store_true", 111 113 help="With --replace: show changes without writing") 114 + parser.add_argument("--exclude", metavar="STR", action="append", default=[], 115 + help="Exclude labels matching this pattern (repeatable)") 112 116 parser.add_argument("--split", metavar="NAME", 113 117 help="Restrict to one split") 114 118 args = parser.parse_args() ··· 118 122 if args.replace is not None: 119 123 replace(args.pattern, args.replace, splits, dry_run=args.dry_run, raw=args.raw) 120 124 else: 121 - search(args.pattern, splits, raw=args.raw) 125 + search(args.pattern, splits, raw=args.raw, exclude=args.exclude) 122 126 123 127 124 128 if __name__ == "__main__":
+22 -1
src/static/review.html
··· 120 120 <input id="search-inp" type="text" placeholder="search labels…" 121 121 style="background:#222;color:#ccc;border:1px solid #444;padding:0.3rem 0.6rem; 122 122 font-family:monospace;font-size:0.82rem;border-radius:3px;width:18rem;"> 123 + <span id="result-count" style="font-size:0.78rem;color:#777;"></span> 123 124 <span id="stats"></span> 124 125 </div> 125 126 ··· 134 135 split: '', 135 136 filter: 'pending', 136 137 search: '', 138 + total: null, 137 139 items: [], // each item: {id, split, typst, typst_orig, reviewed, flagged, edited, _editing, _draft} 138 140 nextCursor: null, 139 141 loading: false, ··· 214 216 async reload() { 215 217 state.items = []; 216 218 state.nextCursor = null; 219 + state.total = null; 217 220 state.loading = true; 218 221 _renderList(); 219 222 const data = await _fetchPage(0); 220 223 state.items = data.items.map(_ext); 221 224 state.nextCursor = data.next_cursor; 225 + state.total = data.total; 222 226 state.loading = false; 223 227 _renderList(); 228 + _setResultCount(); 224 229 _refreshStats(); 225 230 }, 226 231 ··· 395 400 const btn = document.getElementById('load-more'); 396 401 btn.hidden = state.nextCursor === null && !state.loading; 397 402 btn.disabled = state.loading; 398 - btn.textContent = state.loading ? 'Loading...' : 'Load more'; 403 + if (state.loading) { 404 + btn.textContent = 'Loading...'; 405 + } else { 406 + const shown = state.items.length; 407 + const total = state.total; 408 + btn.textContent = total != null 409 + ? `Load more (${shown.toLocaleString()} / ${total.toLocaleString()} shown)` 410 + : 'Load more'; 411 + } 412 + } 413 + 414 + function _setResultCount() { 415 + const el = document.getElementById('result-count'); 416 + if (!el) return; 417 + el.textContent = state.total != null && state.search 418 + ? `${state.total.toLocaleString()} results` 419 + : ''; 399 420 } 400 421 401 422 function _renderList() {