feat: Corrections tab — SFT candidate import, review, and JSONL export #15

Merged
pyr0ball merged 99 commits from feat/sft-corrections into main 2026-04-08 22:19:01 -07:00
4 changed files with 22 additions and 5 deletions
Showing only changes of commit 3788254abd - Show all commits

View file

@ -209,5 +209,16 @@ def get_labels():
# Static SPA — MUST be last (catches all unmatched paths) # Static SPA — MUST be last (catches all unmatched paths)
_DIST = _ROOT / "web" / "dist" _DIST = _ROOT / "web" / "dist"
if _DIST.exists(): if _DIST.exists():
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
# Serve index.html with no-cache so browsers always fetch fresh HTML after rebuilds.
# Hashed assets (/assets/index-abc123.js) can be cached forever — they change names
# when content changes (standard Vite cache-busting strategy).
_NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache"}
@app.get("/")
def get_spa_root():
return FileResponse(str(_DIST / "index.html"), headers=_NO_CACHE)
app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="spa") app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="spa")

View file

@ -27,8 +27,9 @@ LABELS: list[str] = [
"survey_received", "survey_received",
"neutral", "neutral",
"event_rescheduled", "event_rescheduled",
"unrelated",
"digest", "digest",
"new_lead",
"hired",
] ]
# Natural-language descriptions used by the RerankerAdapter. # Natural-language descriptions used by the RerankerAdapter.
@ -40,8 +41,9 @@ LABEL_DESCRIPTIONS: dict[str, str] = {
"survey_received": "invitation to complete a culture-fit survey or assessment", "survey_received": "invitation to complete a culture-fit survey or assessment",
"neutral": "automated ATS confirmation such as application received", "neutral": "automated ATS confirmation such as application received",
"event_rescheduled": "an interview or scheduled event moved to a new time", "event_rescheduled": "an interview or scheduled event moved to a new time",
"unrelated": "non-job-search email unrelated to any application or recruiter",
"digest": "job digest or multi-listing email with multiple job postings", "digest": "job digest or multi-listing email with multiple job postings",
"new_lead": "unsolicited recruiter outreach or cold contact about a new opportunity",
"hired": "job offer accepted, onboarding logistics, welcome email, or start date confirmation",
} }
# Lazy import shims — allow tests to patch without requiring the libs installed. # Lazy import shims — allow tests to patch without requiring the libs installed.

View file

@ -2,14 +2,16 @@
import pytest import pytest
def test_labels_constant_has_nine_items(): def test_labels_constant_has_ten_items():
from scripts.classifier_adapters import LABELS from scripts.classifier_adapters import LABELS
assert len(LABELS) == 9 assert len(LABELS) == 10
assert "interview_scheduled" in LABELS assert "interview_scheduled" in LABELS
assert "neutral" in LABELS assert "neutral" in LABELS
assert "event_rescheduled" in LABELS assert "event_rescheduled" in LABELS
assert "unrelated" in LABELS
assert "digest" in LABELS assert "digest" in LABELS
assert "new_lead" in LABELS
assert "hired" in LABELS
assert "unrelated" not in LABELS
def test_compute_metrics_perfect_predictions(): def test_compute_metrics_perfect_predictions():

View file

@ -211,6 +211,7 @@ async function handleSkip() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: item.id }), body: JSON.stringify({ id: item.id }),
}) })
if (store.queue.length < 3) await fetchBatch()
} }
async function handleDiscard() { async function handleDiscard() {
@ -228,6 +229,7 @@ async function handleDiscard() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: item.id }), body: JSON.stringify({ id: item.id }),
}) })
if (store.queue.length < 3) await fetchBatch()
} }
async function handleUndo() { async function handleUndo() {