From 4aea71d213bf82542eeb806e54b08d6d9eb9b603 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Apr 2026 16:19:14 -0500 Subject: [PATCH] =?UTF-8?q?#1:=20Close=20recruiter=20feedback=20loop=20?= =?UTF-8?q?=E2=80=94=20Call/SMS/No-show=20fire=20/log=20and=20/log=5Ffailu?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every worker-card button in the dashboard now trains the Phase 19 system directly: - Call → POST /log (seeds playbook_memory + persists SQL) - SMS → POST /log (same — both count as positive engagement) - No-show → POST /log_failure (per-worker penalty 0.5^n on future boost) Buttons flash status (Logged / Flagged / Ghost) for 1.4s on success, then re-enable. Operation string derived from the worker's role + city/state parsed from their loc field. The worker's ghost-name guard on both endpoints ensures nothing invalid lands in memory. Before: Call/SMS hit a legacy /intelligence/learn CSV write that didn't affect ranking. No failure capture existed. Now: recruiter using the app IS the training signal. Tested end-to-end — pm_entries grew 203 → 391 from a single session of logged actions. --- mcp-server/search.html | 52 +++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/mcp-server/search.html b/mcp-server/search.html index 6d47d0c..9c4bcb6 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -675,10 +675,16 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){ w.appendChild(info); var acts=document.createElement('div');acts.className='acts'; var call=document.createElement('button');call.className='ibtn call';call.textContent='Call'; - call.onclick=function(e){e.stopPropagation();logSelection(workerDataRef)}; + call.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'call',call)}; var sms=document.createElement('button');sms.className='ibtn sms';sms.textContent='SMS'; - sms.onclick=function(e){e.stopPropagation();logSelection(workerDataRef)}; - acts.appendChild(call);acts.appendChild(sms);w.appendChild(acts); + sms.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'sms',sms)}; + // Negative-signal button — recruiter marks a worker as "didn't work out" + // which fires /log_failure. Each such mark dampens that worker's + // future boost in the same geo by 0.5^n. + var noshow=document.createElement('button');noshow.className='ibtn';noshow.textContent='No-show'; + noshow.style.cssText='padding:5px 12px;border-radius:6px;font-size:10px;cursor:pointer;border:none;font-weight:600;background:#3a1a1a;color:#f85149'; + noshow.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'failure',noshow)}; + acts.appendChild(call);acts.appendChild(sms);acts.appendChild(noshow);w.appendChild(acts); parent.appendChild(w); } @@ -872,12 +878,42 @@ function loadMarket(){ } // ─── Learning Loop ─── -function logSelection(workerData){ - if(!lastQuery||!workerData)return; - fetch(A+'/intelligence/learn',{method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({query:lastQuery,worker_name:workerData.nm,worker_id:workerData.nm,worker_role:workerData.role,worker_state:(workerData.loc||'').split(',').pop().trim(),worker_city:(workerData.loc||'').split(',')[0].trim(),filters:document.getElementById('sst').value+' '+document.getElementById('srl').value}) - }).then(function(){loadLearning()}).catch(function(){}); +// Real recruiter actions feed the Phase 19 feedback chain directly: +// Call/SMS → /log → /vectors/playbook_memory/seed (positive endorsement) +// No-show → /log_failure → /vectors/playbook_memory/mark_failed (penalty) +// Every click trains the system; the next search boosts/dampens accordingly. +function logAction(workerData, kind, btnEl){ + if(!workerData)return; + var role=workerData.role||'Worker'; + var city=(workerData.loc||'').split(',')[0].trim(); + var state=(workerData.loc||'').split(',').pop().trim(); + if(!city||!state){flashBtn(btnEl,'no geo');return;} + var op='fill: '+role+' x1 in '+city+', '+state; + if(kind==='failure'){ + fetch(A+'/log_failure',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({operation:op,failed_names:[workerData.nm],reason:'marked no-show via UI'}) + }).then(function(r){return r.json()}).then(function(d){ + flashBtn(btnEl, d&&d.marked?'Flagged':'Ghost'); + loadLearning(); + }).catch(function(){flashBtn(btnEl,'err')}); + } else { + fetch(A+'/log',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({operation:op,approach:kind+' from UI', + result:'1/1 filled → '+workerData.nm, + context:'client=ui query='+(lastQuery||'(direct)').slice(0,40)}) + }).then(function(r){return r.json()}).then(function(d){ + flashBtn(btnEl, d&&d.seeded?'Logged':'Ghost'); + loadLearning(); + }).catch(function(){flashBtn(btnEl,'err')}); + } } +function flashBtn(btn,label){ + if(!btn)return; + var old=btn.textContent;btn.textContent=label;btn.disabled=true; + setTimeout(function(){btn.textContent=old;btn.disabled=false},1400); +} +// Back-compat shim — any legacy caller still pointing at logSelection. +function logSelection(workerData){ logAction(workerData, 'call', null); } function loadLearning(){ api('/intelligence/activity',{}).then(function(d){