demo: P2 — staffer-language routes (zip, headcount, name, late-triage, ingest log)
Built from a playwright run as three personas:
Maria — "8 production workers near 60607 by next Friday, prior-fill at this client"
Devon — "what came in last night?"
Aisha — "Marcus running late site 4422"
Each one previously fell through to smart_search and returned irrelevant
results (geo wrong, headcount ignored, no triage, no temporal). Now:
A. Zip code → city/state lookup. Chicago zips (606xx, 607xx, 608xx)
resolve to {city: Chicago, state: IL}; 13 metro prefixes covered.
Maria's "near 60607" now returns Chicago workers, not Dayton/Green Bay.
B. Headcount parser. "8 production workers" / "12 forklift operators" /
"5 welders" set top_k 1..200, capped 5..25 for SQL+vector LIMIT.
Allows 0-2 role words between the count and the worker noun so
"8 production workers" matches as well as "8 workers".
C. Bare-name profile lookup. Single short capitalized phrase
("Marcus" / "Sarah Lopez") triggers a profile route. Per-token LIKE
AND-joined so "Marcus Rivera" matches "Marcus L. Rivera" without
hardcoding middle initials.
E. Late-worker / no-show triage. Pattern: <Name> (running late|late|
no show|sick|out today|called out|can't make it) — pulls profile +
reliability + responsiveness + recent calls, sources 5 same-role
same-geo backfills sorted by responsiveness, drafts a client SMS
the coordinator can copy. Front-end renders triage card + Copy SMS
button + green backfill list.
F. Contractor name preview anchor. The PROJECT INDEX preview line on
each permit card now wraps contact_1_name and contact_2_name in
anchors to /contractor?name=... — clicking a contractor finally
navigates instead of doing nothing. Click handler stops propagation
so the details element doesn't toggle.
D. Temporal "what came in" route. last night / today / past N hours /
recent — surfaces datasets from the catalog whose updated_at is
within the window, samples one row per dataset to detect worker-
shape, groups by role for worker tables. Schema-agnostic — drop
any dataset and it shows up. Currently sparse because no fresh
ingest has happened today; will populate as ingest runs.
Server: /intelligence/chat smart_search route accepts structured
state/role from the search-form dropdowns (P1 from prior commit) and
now ALSO honors b.state, b.role, q.match for headcount + zip + name +
triage patterns BEFORE falling through to NL parsing.
Front-end: doSearch dispatches on response.type and renders triage,
profile, ingest_log, and miss states with type-specific UI. All DOM
construction uses textContent / appendChild — no innerHTML, no XSS.
Verified end-to-end via playwright drive of devop.live/lakehouse:
Maria → 8 Chicago Production Workers (60685, 60662, 60634)
tags: "headcount: 8 · zip 60607 → Chicago, IL · ..."
Aisha → Marcus V. Campbell card + draft SMS + 5 Quincy IL backfills
"I'm dispatching Scott B. Cooper (96% reliability) to cover."
Devon → ingest_log surfaces successful_playbooks_live (last 1h)
Marcus → 5 profiles (Adams Louisville KY, Jenkins Green Bay WI, ...)
Screenshots: /tmp/persona_v2/{01_maria,02_aisha,03_devon,04_marcus}.png
Restart sequence after these edits: pkill -9 -f "mcp-server/index.ts" ;
cd /home/profit/lakehouse ; bun run mcp-server/index.ts. The bun on
:3700 is not systemd-managed (pre-existing convention).
This commit is contained in:
+152
-1
@@ -1609,10 +1609,29 @@ function loadLiveContracts(){
|
||||
var ebLabel=document.createElement('span');ebLabel.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px';
|
||||
ebLabel.textContent='PROJECT INDEX — Build Signals';
|
||||
var ebTags=document.createElement('span');ebTags.style.cssText='color:#e6edf3;font-size:11px;flex:1;font-weight:500';
|
||||
// Contractor names link to the full profile page. Without anchors,
|
||||
// clicking the preview did nothing — the only working contractor
|
||||
// link was inside the lazy-loaded entity brief, which a coordinator
|
||||
// wouldn't reach without first expanding the details.
|
||||
var preview=[];
|
||||
function contactLink(n){
|
||||
var a=document.createElement('a');
|
||||
a.href='/contractor?name='+encodeURIComponent(n);
|
||||
a.target='_blank';a.rel='noopener';
|
||||
a.style.cssText='color:inherit;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
||||
a.title='Open full contractor profile';
|
||||
a.textContent=n;
|
||||
a.addEventListener('click',function(e){e.stopPropagation()}); // don't toggle the details
|
||||
return a;
|
||||
}
|
||||
if(p.contact_1_name) preview.push(p.contact_1_name);
|
||||
if(p.contact_2_name && p.contact_2_name!==p.contact_1_name) preview.push(p.contact_2_name);
|
||||
ebTags.textContent=preview.join(' · ');
|
||||
if(preview.length){
|
||||
preview.forEach(function(n,i){
|
||||
if(i>0) ebTags.appendChild(document.createTextNode(' · '));
|
||||
ebTags.appendChild(contactLink(n));
|
||||
});
|
||||
}
|
||||
var ebMeta=document.createElement('span');ebMeta.style.cssText='color:#545d68;font-size:10px';
|
||||
ebMeta.textContent='click → fetch OSHA + ILSOS';
|
||||
ebSum.appendChild(ebCaret);ebSum.appendChild(ebLabel);ebSum.appendChild(ebTags);ebSum.appendChild(ebMeta);
|
||||
@@ -2270,6 +2289,130 @@ function pw(text){
|
||||
rel:rr?parseFloat(rr[1]):0,avail:av?parseFloat(av[1]):0,arch:ar?ar[1]:'',hasM:!!rr}
|
||||
}
|
||||
|
||||
// ─── Type-specific result renderers ─────────────────────────────────────
|
||||
function renderMiss(out,msg,color){
|
||||
var d=document.createElement('div');
|
||||
d.style.cssText='background:#0d1117;border:1px solid '+(color||'#21262d')+'66;border-left:3px solid '+(color||'#21262d')+';border-radius:6px;padding:14px 16px;color:#8b949e;font-size:13px;line-height:1.5';
|
||||
d.textContent=msg;
|
||||
out.appendChild(d);
|
||||
}
|
||||
function workerLine(w){
|
||||
var bits=[];
|
||||
if(w.role) bits.push(w.role);
|
||||
if(w.city||w.state) bits.push((w.city||'')+(w.city&&w.state?', ':'')+(w.state||''));
|
||||
if(w.zip) bits.push('ZIP '+w.zip);
|
||||
return bits.join(' · ');
|
||||
}
|
||||
function appendStat(parent,label,val){
|
||||
var s=document.createElement('span');
|
||||
var l=document.createElement('span');l.textContent=label+': ';
|
||||
var b=document.createElement('b');b.style.color='#e6edf3';b.textContent=val;
|
||||
s.appendChild(l);s.appendChild(b);
|
||||
parent.appendChild(s);
|
||||
}
|
||||
function renderTriage(out,d){
|
||||
var w=d.worker, bf=d.backfills||[];
|
||||
var card=document.createElement('div');
|
||||
card.style.cssText='background:#1a1410;border:1px solid #d29922;border-left:3px solid #d29922;border-radius:8px;padding:16px;margin-bottom:14px';
|
||||
var ev=document.createElement('div');
|
||||
ev.style.cssText='font-size:11px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:6px';
|
||||
ev.textContent='⚠ TRIAGE — '+(d.event||'event').toUpperCase();
|
||||
card.appendChild(ev);
|
||||
var hdr=document.createElement('div');
|
||||
hdr.style.cssText='font-size:16px;color:#e6edf3;font-weight:600;margin-bottom:4px';
|
||||
hdr.textContent=w.name;
|
||||
card.appendChild(hdr);
|
||||
var line=document.createElement('div');
|
||||
line.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
|
||||
line.textContent=workerLine(w);
|
||||
card.appendChild(line);
|
||||
var stats=document.createElement('div');
|
||||
stats.style.cssText='font-size:11px;color:#8b949e;margin-bottom:10px;display:flex;gap:14px;flex-wrap:wrap';
|
||||
appendStat(stats,'Reliability',Math.round((w.rel||0)*100)+'%');
|
||||
appendStat(stats,'Responsiveness',Math.round((w.resp||0)*100)+'%');
|
||||
appendStat(stats,'Availability',Math.round((w.avail||0)*100)+'%');
|
||||
if(w.archetype) appendStat(stats,'Archetype',w.archetype);
|
||||
if(w.recent_calls!=null) appendStat(stats,'Prior calls',w.recent_calls);
|
||||
card.appendChild(stats);
|
||||
var smsLabel=document.createElement('div');
|
||||
smsLabel.style.cssText='font-size:10px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:4px';
|
||||
smsLabel.textContent='DRAFT SMS — TO CLIENT';
|
||||
card.appendChild(smsLabel);
|
||||
var smsBox=document.createElement('div');
|
||||
smsBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:10px 12px;font-family:ui-monospace,monospace;font-size:12px;color:#e6edf3;line-height:1.5;white-space:pre-wrap';
|
||||
smsBox.textContent=d.draft_sms||'';
|
||||
card.appendChild(smsBox);
|
||||
var copyBtn=document.createElement('button');
|
||||
copyBtn.style.cssText='margin-top:8px;background:#1f6feb;border:none;color:#fff;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer';
|
||||
copyBtn.textContent='Copy SMS';
|
||||
copyBtn.onclick=function(){
|
||||
if(navigator.clipboard) navigator.clipboard.writeText(d.draft_sms||'');
|
||||
copyBtn.textContent='Copied ✓';
|
||||
setTimeout(function(){copyBtn.textContent='Copy SMS'},1500);
|
||||
};
|
||||
card.appendChild(copyBtn);
|
||||
out.appendChild(card);
|
||||
if(bf.length){
|
||||
var bfHdr=document.createElement('div');
|
||||
bfHdr.style.cssText='font-size:11px;color:#3fb950;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin:8px 0 8px';
|
||||
bfHdr.textContent='✓ BACKFILLS READY — '+bf.length+' local '+(w.role||'workers')+' available, sorted by responsiveness';
|
||||
out.appendChild(bfHdr);
|
||||
bf.forEach(function(c,i){
|
||||
addWorkerInsight(out,c.name,workerLine(c),
|
||||
'Reliability '+Math.round((c.rel||0)*100)+'% · Responds '+Math.round((c.resp||0)*100)+'% · Available '+Math.round((c.avail||0)*100)+'%'+(c.archetype?' · '+c.archetype:''),
|
||||
i,'#3fb950',c);
|
||||
});
|
||||
}else{
|
||||
var bfNone=document.createElement('div');
|
||||
bfNone.style.cssText='background:#1a1010;border:1px solid #f85149;border-radius:6px;padding:10px 14px;color:#fca5a5;font-size:12px';
|
||||
bfNone.textContent='No same-role workers available locally. Widen the search — try a neighboring city or relax availability threshold.';
|
||||
out.appendChild(bfNone);
|
||||
}
|
||||
}
|
||||
function renderProfiles(out,d){
|
||||
var hdr=document.createElement('div');
|
||||
hdr.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
|
||||
hdr.textContent=d.summary;
|
||||
out.appendChild(hdr);
|
||||
(d.profiles||[]).forEach(function(w,i){
|
||||
addWorkerInsight(out,w.name,workerLine(w),
|
||||
'Reliability '+Math.round((w.rel||0)*100)+'%'+(w.resp?' · Responds '+Math.round(w.resp*100)+'%':'')+(w.archetype?' · '+w.archetype:''),
|
||||
i,null,w);
|
||||
});
|
||||
}
|
||||
function renderIngestLog(out,d){
|
||||
var hdr=document.createElement('div');
|
||||
hdr.style.cssText='font-size:12px;color:#e6edf3;margin-bottom:10px;padding:10px 12px;background:#0d2818;border:1px solid #2ea04340;border-left:3px solid #3fb950;border-radius:6px';
|
||||
hdr.textContent=d.summary;
|
||||
out.appendChild(hdr);
|
||||
(d.datasets||[]).forEach(function(ds){
|
||||
var card=document.createElement('div');
|
||||
card.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:12px 14px;margin-bottom:8px';
|
||||
var top=document.createElement('div');
|
||||
top.style.cssText='display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px';
|
||||
var nm=document.createElement('span');
|
||||
nm.style.cssText='font-size:13px;color:#e6edf3;font-weight:600';
|
||||
nm.textContent=ds.name;
|
||||
var ago=document.createElement('span');
|
||||
ago.style.cssText='font-size:11px;color:#545d68';
|
||||
ago.textContent=(ds.hours_ago||0)+'h ago · '+(ds.row_count||0).toLocaleString()+' rows';
|
||||
top.appendChild(nm);top.appendChild(ago);
|
||||
card.appendChild(top);
|
||||
if(ds.looks_like_workers && ds.role_breakdown && ds.role_breakdown.length){
|
||||
var rb=document.createElement('div');
|
||||
rb.style.cssText='font-size:11px;color:#8b949e;display:flex;gap:10px;flex-wrap:wrap;margin-top:4px';
|
||||
ds.role_breakdown.forEach(function(r){
|
||||
var pill=document.createElement('span');
|
||||
pill.style.cssText='background:#161b22;border:1px solid #21262d;padding:2px 8px;border-radius:9px';
|
||||
pill.textContent=(r.role||'?')+' · '+r.cnt;
|
||||
rb.appendChild(pill);
|
||||
});
|
||||
card.appendChild(rb);
|
||||
}
|
||||
out.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function doSearch(){
|
||||
var q=document.getElementById('sq').value.trim();if(!q)return;
|
||||
lastQuery=q;
|
||||
@@ -2285,6 +2428,14 @@ function doSearch(){
|
||||
body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined})
|
||||
}).then(function(r){return r.json()}).then(function(d){
|
||||
out.textContent='';
|
||||
// Type-specific renderers — added 2026-04-27 for the persona-driven
|
||||
// routes (triage / profile / ingest_log). Default falls through to
|
||||
// the smart_search renderer below.
|
||||
if(d.type==='triage' && d.worker){return renderTriage(out,d)}
|
||||
if(d.type==='triage_miss'){return renderMiss(out,d.summary,'#f85149')}
|
||||
if(d.type==='profile' && d.profiles && d.profiles.length){return renderProfiles(out,d)}
|
||||
if(d.type==='profile_miss'){return renderMiss(out,d.summary,'#d29922')}
|
||||
if(d.type==='ingest_log'){return renderIngestLog(out,d)}
|
||||
// Show what the system understood
|
||||
if(d.understood&&d.understood.length){
|
||||
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px';
|
||||
|
||||
Reference in New Issue
Block a user