Spaces:
Running
Running
| /* JobTracker classifier, in-browser. | |
| * | |
| * Faithful port of the 3-layer hybrid (backend/jobtracker/classifier): | |
| * 1. rules — same 201 regexes, same scoring (strong +3 ×2-subject, | |
| * weak +1, negative −5), same margin→confidence tiers, | |
| * same ATS-domain boost. Accept ≥0.9. | |
| * 2. embeddings — cosine vs a synthetic example bank embedded with the | |
| * fine-tuned body ("query: " prefix). Accept ≥0.85. | |
| * 3. setfit — LogisticRegression head over the same embedding | |
| * (no prefix). Accept ≥0.70, else needs_review. | |
| * The embedding body is the fine-tuned e5-small, dynamic-int8 ONNX, | |
| * verified output-identical to the Python pipeline (6/6 suite). | |
| */ | |
| import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.2'; | |
| env.allowRemoteModels = false; | |
| env.allowLocalModels = true; | |
| env.localModelPath = './'; | |
| const $ = (id) => document.getElementById(id); | |
| const state = $('state'), dot = $('dot'), go = $('go'); | |
| const prog = $('prog'), progbar = $('progbar'); | |
| let extractor = null, rules = null, head = null, examples = null; | |
| /* ---------- layer 1: rules ---------- */ | |
| function compileRules(raw) { | |
| const cats = {}; | |
| for (const [cat, g] of Object.entries(raw.categories)) { | |
| cats[cat] = { | |
| strong: g.strong.map((p) => new RegExp(p, 'i')), | |
| weak: g.weak.map((p) => new RegExp(p, 'i')), | |
| negative: g.negative.map((p) => new RegExp(p, 'i')), | |
| }; | |
| } | |
| return { cats, ats: raw.ats_domains }; | |
| } | |
| function rulesClassify(subject, body, sender) { | |
| const scores = {}; | |
| let isAts = false; | |
| if (sender && sender.includes('@')) { | |
| const dom = sender.toLowerCase().split('@').pop(); | |
| isAts = rules.ats.some((a) => dom.includes(a)); | |
| } | |
| for (const [cat, g] of Object.entries(rules.cats)) { | |
| let s = 0; | |
| for (const re of g.strong) { if (re.test(subject)) s += 6; else if (re.test(body)) s += 3; } | |
| for (const re of g.weak) { if (re.test(subject)) s += 2; else if (re.test(body)) s += 1; } | |
| for (const re of g.negative) { if (re.test(subject) || re.test(body)) s -= 5; } | |
| scores[cat] = s; | |
| } | |
| const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); | |
| const [winner, ws] = sorted[0]; | |
| const runner = sorted[1] ? sorted[1][1] : 0; | |
| if (ws <= 0) return { category: 'other', confidence: 0.5 }; | |
| const margin = ws - runner; | |
| let conf = 0.6; | |
| if (ws >= 10 && margin >= 5) conf = 0.95; | |
| else if (ws >= 6 && margin >= 3) conf = 0.9; | |
| else if (ws >= 4 && margin >= 2) conf = 0.8; | |
| else if (ws >= 2 && margin >= 1) conf = 0.7; | |
| if (isAts && ['applied', 'rejection', 'interview', 'offer'].includes(winner)) | |
| conf = Math.min(conf + 0.05, 0.95); | |
| return { category: winner, confidence: conf }; | |
| } | |
| /* ---------- embedding ---------- */ | |
| async function embed(text) { | |
| const out = await extractor(text, { pooling: 'mean', normalize: true }); | |
| return Array.from(out.data); | |
| } | |
| const cosine = (a, b) => a.reduce((s, v, i) => s + v * b[i], 0); // both normalized | |
| /* ---------- layer 2: similarity ---------- */ | |
| function simClassify(vec) { | |
| let best = null, bestSim = -1; | |
| for (const ex of examples) { | |
| const s = cosine(vec, ex.vec); | |
| if (s > bestSim) { bestSim = s; best = ex.label; } | |
| } | |
| return { category: best, confidence: bestSim }; | |
| } | |
| /* ---------- layer 3: setfit head ---------- */ | |
| function headClassify(vec) { | |
| const logits = head.coef.map((row, i) => | |
| row.reduce((s, w, j) => s + w * vec[j], 0) + head.intercept[i]); | |
| const m = Math.max(...logits); | |
| const exps = logits.map((l) => Math.exp(l - m)); | |
| const Z = exps.reduce((a, b) => a + b, 0); | |
| const probs = exps.map((e) => e / Z); | |
| const k = probs.indexOf(Math.max(...probs)); | |
| return { category: head.classes[k], confidence: probs[k] }; | |
| } | |
| /* ---------- UI ---------- */ | |
| const EXAMPLES = [ | |
| ['Interview availability — SWE, Platform', "Hi Ayush, thanks for applying. We'd like to schedule a 45-minute technical interview next week. Could you share your availability?"], | |
| ['Your application was received', 'Thank you for applying to the Backend Engineer role. Our team is reviewing applications and will reach out if there is a match.'], | |
| ['Update on your application', "After careful consideration, we've decided to move forward with other candidates. We appreciate the time you invested."], | |
| ['Congratulations — offer details inside', "We're thrilled to extend an offer for the ML Engineer position. Compensation and start date are in the attached letter."], | |
| ['Next step: online assessment', 'Please complete the coding assessment linked below within 5 days. It should take about 90 minutes.'], | |
| ['Your weekly job digest', '12 new jobs recommended for you based on your profile. View all jobs or manage your preferences.'], | |
| ]; | |
| function renderTrace(rows) { | |
| $('trace').innerHTML = rows.map((r) => | |
| `<div class="trow" data-state="${r.state}"> | |
| <b>${r.layer}</b><span class="st">${r.note}</span> | |
| <span class="ms">${r.ms !== null ? r.ms.toFixed(1) + 'ms' : '—'}</span> | |
| </div>`).join(''); | |
| } | |
| function renderVerdict(category, confidence, needsReview, source) { | |
| $('verdict').innerHTML = | |
| `<b>${category.replace(/_/g, ' ')}</b> | |
| <small>${(confidence * 100).toFixed(1)}% · answered by ${source}</small> | |
| ${needsReview ? '<small class="review">below 0.85 — production queues this for human review</small>' : ''}`; | |
| } | |
| async function classify() { | |
| const subject = $('subject').value.trim(); | |
| const body = $('body').value.trim(); | |
| if (!subject && !body) return; | |
| go.disabled = true; | |
| const trace = []; | |
| const t0 = performance.now(); | |
| // layer 1 — rules | |
| const r1 = rulesClassify(subject, body, null); | |
| const t1 = performance.now(); | |
| if (r1.confidence >= 0.9) { | |
| trace.push({ layer: 'rules', state: 'answered', note: `${r1.category} @ ${(r1.confidence * 100).toFixed(0)}% — regex answered`, ms: t1 - t0 }); | |
| trace.push({ layer: 'embeddings', state: 'skipped', note: 'never ran', ms: null }); | |
| trace.push({ layer: 'setfit', state: 'skipped', note: 'never ran', ms: null }); | |
| renderVerdict(r1.category, r1.confidence, false, 'rules'); | |
| renderTrace(trace); showTotal(t1 - t0); go.disabled = false; return; | |
| } | |
| trace.push({ layer: 'rules', state: 'passed', note: `top ${r1.category} @ ${(r1.confidence * 100).toFixed(0)}% — not confident enough`, ms: t1 - t0 }); | |
| const text = `${subject} ${body}`.trim(); | |
| // layer 2 — similarity (query-prefixed embedding) | |
| const tq0 = performance.now(); | |
| const qvec = await embed('query: ' + text); | |
| const r2 = simClassify(qvec); | |
| const tq1 = performance.now(); | |
| if (r2.confidence >= 0.85) { | |
| trace.push({ layer: 'embeddings', state: 'answered', note: `${r2.category} @ ${(r2.confidence * 100).toFixed(0)}% cosine`, ms: tq1 - tq0 }); | |
| trace.push({ layer: 'setfit', state: 'skipped', note: 'never ran', ms: null }); | |
| renderVerdict(r2.category, r2.confidence, false, 'embeddings'); | |
| renderTrace(trace); showTotal(tq1 - t0); go.disabled = false; return; | |
| } | |
| trace.push({ layer: 'embeddings', state: 'passed', note: `best ${r2.category} @ ${(r2.confidence * 100).toFixed(0)}% — below 0.85`, ms: tq1 - tq0 }); | |
| // layer 3 — setfit head (unprefixed embedding) | |
| const ts0 = performance.now(); | |
| const svec = await embed(text); | |
| const r3 = headClassify(svec); | |
| const ts1 = performance.now(); | |
| const needsReview = r3.confidence < 0.85; | |
| const category = r3.confidence >= 0.7 ? r3.category : 'needs_review'; | |
| trace.push({ layer: 'setfit', state: 'answered', note: `${r3.category} @ ${(r3.confidence * 100).toFixed(0)}% softmax`, ms: ts1 - ts0 }); | |
| renderVerdict(category, r3.confidence, needsReview, 'setfit'); | |
| renderTrace(trace); showTotal(ts1 - t0); go.disabled = false; | |
| } | |
| function showTotal(ms) { | |
| $('timing').textContent = `total ${ms.toFixed(1)}ms · in this tab`; | |
| } | |
| async function boot() { | |
| renderTrace([ | |
| { layer: 'rules', state: 'passed', note: '201 patterns, ready', ms: null }, | |
| { layer: 'embeddings', state: 'passed', note: 'awaiting model', ms: null }, | |
| { layer: 'setfit', state: 'passed', note: 'awaiting model', ms: null }, | |
| ]); | |
| $('examples').innerHTML = EXAMPLES.map(([s], i) => | |
| `<button class="ghost" data-i="${i}">${s.slice(0, 34)}…</button>`).join(''); | |
| $('examples').addEventListener('click', (e) => { | |
| const i = e.target?.dataset?.i; | |
| if (i === undefined) return; | |
| $('subject').value = EXAMPLES[i][0]; | |
| $('body').value = EXAMPLES[i][1]; | |
| if (!go.disabled) classify(); | |
| }); | |
| go.addEventListener('click', classify); | |
| const [rulesRaw, headRaw, exRaw] = await Promise.all([ | |
| fetch('./rules.json').then((r) => r.json()), | |
| fetch('./head.json').then((r) => r.json()), | |
| fetch('./examples.json').then((r) => r.json()), | |
| ]); | |
| rules = compileRules(rulesRaw); head = headRaw; examples = exRaw; | |
| prog.hidden = false; | |
| extractor = await pipeline('feature-extraction', 'model', { | |
| dtype: 'fp32', | |
| progress_callback: (p) => { | |
| if (p.status === 'progress' && p.total) | |
| progbar.style.width = `${Math.round((p.loaded / p.total) * 100)}%`; | |
| }, | |
| }); | |
| prog.hidden = true; | |
| await embed('warmup'); // JIT + session warm | |
| state.textContent = 'local · ready'; | |
| dot.setAttribute('data-on', ''); | |
| go.disabled = false; | |
| } | |
| boot().catch((err) => { | |
| state.textContent = 'failed to load'; | |
| console.error(err); | |
| }); | |