feat: Vue 3 label tab — complete card-stack UI with ASMR bucket UX #1

Open
pyr0ball wants to merge 0 commits from feat/vue-label-tab into main
Owner

Summary

Full implementation of the Avocet Vue 3 label tab, replacing the previous Streamlit UI with a purpose-built card-stack labeling interface.

Backend

  • FastAPI skeleton with JSONL helpers (_read_jsonl, _write_jsonl, _append_jsonl)
  • GET /api/queue — paginated batch fetch from staging JSONL
  • POST /api/label, /api/skip, /api/discard — write results + undo support
  • GET /api/config/labels — dynamic label config from label_tool.yaml
  • SPA serving with Cache-Control: no-cache on / (prevents stale index.html after rebuild)
  • Bind to 0.0.0.0 for LAN access
  • Email body HTML stripping via stdlib HTMLParser (no deps)

Frontend (Vue 3 + Pinia + UnoCSS + VueUse)

  • EmailCard — subject, from/date, body preview with expand/collapse
  • EmailCardStack — swipe gestures (VueUse useSwipe), depth shadows, dismiss/skip animations
  • LabelBucketGrid — numpad layout; picks up a card → buttons transform into drop buckets (the ASMR payoff); spring expansion + glow on hover/drop
  • UndoToast — 5-second countdown, undo button, accessible
  • useLabelKeyboard — 1-9 hotkeys, h to skip, S/D/U shortcuts, ? for help
  • useMotion — rich-animation toggle (localStorage cf-avocet-rich-motion)
  • useHapticsnavigator.vibrate() wrapper with in-app toggle
  • useEasterEgg — Konami code → hacker mode (terminal green), hired confetti, century mark, clean sweep, midnight labeler, cursor trail
  • Pinia store — queue buffer, totalRemaining, lastAction for undo, easter egg counters
  • CircuitForge base theme + Avocet Slate Teal / Russet brand colors; dark mode; self-hosted fonts

Bug Fixes

  • Queue drain: handleSkip/handleDiscard now call fetchBatch() when buffer drops below 3
  • Label schema synced across API, _LABEL_META, Vue UI, and classifier_adapters.py (10 labels: new_lead + hired replacing unrelated)
  • Queue schema normalized after IMAP fetch format changed
  • start-api polls port readiness instead of blind sleep

manage.sh

  • start-api — build Vue SPA + start FastAPI on port 8503
  • stop-api / open-api

Test Plan

  • ./manage.sh start-api — SPA builds, server starts on 8503
  • Cards render and swipe correctly
  • Bucket expansion fires when card is picked up
  • Labels POST correctly, queue drains and refills
  • Undo recovers last action
  • Keyboard shortcuts trigger labels
  • Konami code activates hacker mode
  • Hired confetti fires on hired label
  • ./manage.sh test — all pytest tests pass
## Summary Full implementation of the Avocet Vue 3 label tab, replacing the previous Streamlit UI with a purpose-built card-stack labeling interface. ### Backend - FastAPI skeleton with JSONL helpers (`_read_jsonl`, `_write_jsonl`, `_append_jsonl`) - `GET /api/queue` — paginated batch fetch from staging JSONL - `POST /api/label`, `/api/skip`, `/api/discard` — write results + undo support - `GET /api/config/labels` — dynamic label config from `label_tool.yaml` - SPA serving with `Cache-Control: no-cache` on `/` (prevents stale index.html after rebuild) - Bind to `0.0.0.0` for LAN access - Email body HTML stripping via stdlib `HTMLParser` (no deps) ### Frontend (Vue 3 + Pinia + UnoCSS + VueUse) - **EmailCard** — subject, from/date, body preview with expand/collapse - **EmailCardStack** — swipe gestures (VueUse `useSwipe`), depth shadows, dismiss/skip animations - **LabelBucketGrid** — numpad layout; picks up a card → buttons **transform into drop buckets** (the ASMR payoff); spring expansion + glow on hover/drop - **UndoToast** — 5-second countdown, undo button, accessible - **useLabelKeyboard** — 1-9 hotkeys, `h` to skip, `S`/`D`/`U` shortcuts, `?` for help - **useMotion** — rich-animation toggle (localStorage `cf-avocet-rich-motion`) - **useHaptics** — `navigator.vibrate()` wrapper with in-app toggle - **useEasterEgg** — Konami code → hacker mode (terminal green), hired confetti, century mark, clean sweep, midnight labeler, cursor trail - **Pinia store** — queue buffer, `totalRemaining`, `lastAction` for undo, easter egg counters - CircuitForge base theme + Avocet Slate Teal / Russet brand colors; dark mode; self-hosted fonts ### Bug Fixes - Queue drain: `handleSkip`/`handleDiscard` now call `fetchBatch()` when buffer drops below 3 - Label schema synced across API, `_LABEL_META`, Vue UI, and `classifier_adapters.py` (10 labels: `new_lead` + `hired` replacing `unrelated`) - Queue schema normalized after IMAP fetch format changed - `start-api` polls port readiness instead of blind sleep ### manage.sh - `start-api` — build Vue SPA + start FastAPI on port 8503 - `stop-api` / `open-api` ## Test Plan - [ ] `./manage.sh start-api` — SPA builds, server starts on 8503 - [ ] Cards render and swipe correctly - [ ] Bucket expansion fires when card is picked up - [ ] Labels POST correctly, queue drains and refills - [ ] Undo recovers last action - [ ] Keyboard shortcuts trigger labels - [ ] Konami code activates hacker mode - [ ] Hired confetti fires on `hired` label - [ ] `./manage.sh test` — all pytest tests pass
pyr0ball added 28 commits 2026-03-04 09:24:08 -08:00
- useApiFetch: typed fetch wrapper with network/http error discrimination
- useMotion: reactive localStorage override for rich-animation toggle, respects OS prefers-reduced-motion
- useHaptics: label/discard/skip/undo vibration patterns, gated on rich mode
- useKonamiCode + useHackerMode: 10-key Konami sequence → hacker theme, persisted in localStorage
- test-setup.ts: jsdom matchMedia stub so useMotion imports cleanly in Vitest
- smoke.test.ts: import smoke tests for all 4 composables (12 tests, all passing)
Implements Task 13: LabelView.vue wires together the label store, API
fetch, card stack, bucket grid, keyboard shortcuts, haptics, motion
preference, and three easter egg badges (on-a-roll, speed round, fifty
deep). App.vue updated to mount LabelView and restore hacker-mode theme
on load. 3 new LabelView tests; all 48 tests pass, build clean.
- Add _item_id() (content hash) + _normalize() to map legacy JSONL fields
  (from_addr/account/no-id) to Vue schema (from/source/id)
- All mutating endpoints now look up by _normalize(x)[id] — handles both
  stored-id (test fixtures) and content-hash (real data) transparently
- Change uvicorn bind from 127.0.0.1 to 0.0.0.0 so LAN clients can connect
Two bugs fixed:

1. Blank white page after vue SPA rebuild: browsers cached old index.html
   referencing old asset hashes. Assets are deleted on rebuild, causing
   404s for JS/CSS -> blank page. Fix: serve index.html with
   Cache-Control: no-cache so browsers always fetch fresh HTML.
   Hashed assets (/assets/chunk-abc123.js) remain cacheable forever.

2. Queue draining to empty on skip/discard: handleSkip and handleDiscard
   never refilled the local queue buffer. After enough skips, store.current
   went null and the empty state showed (blank-looking). Fix: both handlers
   now call fetchBatch() when queue drops below 3, matching handleLabel.

Also: sync classifier_adapters LABELS to match current 10-label schema
(new_lead + hired, remove unrelated).

48 Python tests pass, 48 frontend tests pass.
pyr0ball added 18 commits 2026-03-05 13:46:09 -08:00
useLabelKeyboard now accepts labels as Label[] | (() => Label[]).
The keymap is rebuilt on every keypress from the getter result instead of
being captured once at construction time — so keys 1–9 now fire correctly
after the async /api/config/labels fetch completes.

LabelView passes () => labels.value so the reactive ref is read lazily.

New test: 'evaluates labels getter on each keypress' covers the async-load
scenario (empty list → no match; push a label → key fires).
pyr0ball added 1 commit 2026-03-05 14:55:20 -08:00
pyr0ball added 2 commits 2026-03-05 15:14:33 -08:00
pyr0ball added 34 commits 2026-03-15 21:16:12 -07:00
TDD: 8 tests written first (red), then composable implemented (green).
Adapts to Anime.js v4 API: 2-arg animate(), object-param spring(),
utils.set() for instant drag-position updates without cache desync.
Replace CSS keyframe dismiss classes and inline cardStyle/deltaX/deltaY
with useCardAnimation composable — pickup/setDragPosition/snapBack/animateDismiss
are now called from pointer event handlers and a dismissType watcher.
Implements load_and_prepare_data (JSONL ingestion with class filtering),
compute_class_weights (inverse-frequency, div-by-zero safe), compute_metrics_for_trainer
(macro F1 + accuracy), and WeightedTrainer.compute_loss (**kwargs-safe for
Transformers 4.38+ num_items_in_batch). All 12 tests pass.
- load_and_prepare_data() now accepts Path | list[Path]; single-Path callers unchanged
- Dedup by MD5(subject + body[:100]); last file/row wins (lets later runs correct labels)
- Prints summary line when duplicates are dropped
- Added _EmailDataset (TorchDataset wrapper), run_finetune(), and argparse CLI
- run_finetune() saves model + tokenizer + training_info.json with score_files provenance
- Stratified split guard: val set size clamped to at least n_classes (handles tiny example data)
- 3 new unit tests (merge, last-write-wins dedup, single-Path compat) + 1 integration test
- All 16 tests pass (15 unit + 1 integration)
- deberta-small: batch_size 16→8 + grad_accum 1→2 (same effective batch),
  gradient_checkpointing=True (fp16 stays off: DeBERTa v3 disentangled
  attention overflows fp16 at the gather step)
- api: _best_cuda_device() picks highest free-VRAM GPU via nvidia-smi;
  sets CUDA_VISIBLE_DEVICES in subprocess env to prevent DataParallel
  replication across both GPUs; adds PYTORCH_ALLOC_CONF=expandable_segments
- SSE log now reports which GPU was selected
Reentrant gradient checkpointing (the default) conflicts with Accelerate's
gradient accumulation context manager -- causes 'backward through graph a
second time' on the first training step. use_reentrant=False uses the
non-reentrant autograd hook path which is compatible with Accelerate >= 0.27.
Adds POST /api/benchmark/cancel and POST /api/finetune/cancel endpoints
that terminate the running subprocess (kill on 3s timeout), and updates
the run generators to emit a cancelled SSE event instead of error when
the job was intentionally stopped.
- except clause in cancel_benchmark/cancel_finetune narrowed from Exception
  to _subprocess.TimeoutExpired (C1)
- _cancelled_jobs.discard() called after registering new proc to prevent
  a stale flag from a prior run masking errors (I2)
- local `import subprocess` removed from run_benchmark and
  run_finetune_endpoint; all Popen calls updated to _subprocess.Popen (I1)
- test patch targets updated from subprocess.Popen to app.api._subprocess.Popen;
  cancelled-event tests updated to set flag in proc.wait() side-effect so
  the discard-on-new-run logic is exercised correctly
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/vue-label-tab:feat/vue-label-tab
git checkout feat/vue-label-tab

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout main
git merge --no-ff feat/vue-label-tab
git checkout feat/vue-label-tab
git rebase main
git checkout main
git merge --ff-only feat/vue-label-tab
git checkout feat/vue-label-tab
git rebase main
git checkout main
git merge --no-ff feat/vue-label-tab
git checkout main
git merge --squash feat/vue-label-tab
git checkout main
git merge --ff-only feat/vue-label-tab
git checkout main
git merge feat/vue-label-tab
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Circuit-Forge/avocet#1
No description provided.