Dashboard: the staffer's actual workday, not a search box
Not a CRM search page. A staffing workstation: Top: Pipeline showing urgent/filling/total/filled at a glance Main: Contract cards sorted by urgency — each shows: - Client, role, headcount, start time - Pre-matched workers with names and AI fit scores - Call All / Send SMS / Find More action buttons - Unfilled contracts at top, filled at bottom - 'Find More' opens search pre-filled with that contract's role Right sidebar: - Alerts: erratic workers, expiring certs, system status - Recent communications: who confirmed, who's pending - Quick stats: total workers, reliable count, coverage The search is there but collapsed — it's a tool, not the focus. When they open the page, their day is already organized. This is what the CRM doesn't do: anticipate, pre-match, organize. The staffer's expertise is in relationships and judgment calls — this handles the data mining so they can focus on that. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7cb9999451
commit
05785b4628
@ -1,178 +1,369 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Lakehouse Search</title>
|
||||
<title>Staffing Co-Pilot</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:system-ui,sans-serif;background:#0a0a0f;color:#d4d4d8}
|
||||
.top{background:#111827;padding:20px;text-align:center;border-bottom:1px solid #1e293b}
|
||||
.top h1{color:#818cf8;font-size:22px;margin-bottom:4px}
|
||||
.top p{color:#64748b;font-size:13px}
|
||||
.box{max-width:800px;margin:20px auto;padding:0 16px}
|
||||
.row{display:flex;gap:8px;margin-bottom:12px}
|
||||
input[type=text]{flex:1;padding:14px;background:#111827;border:1px solid #334155;border-radius:8px;color:#e2e8f0;font-size:15px;outline:none}
|
||||
input[type=text]:focus{border-color:#818cf8}
|
||||
button{padding:14px 24px;background:#7c3aed;border:none;border-radius:8px;color:#fff;font-size:14px;font-weight:600;cursor:pointer}
|
||||
button:hover{background:#6d28d9}
|
||||
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
|
||||
select{padding:8px;background:#111827;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-size:12px}
|
||||
.examples{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px}
|
||||
.ex{padding:4px 12px;background:#1e293b;border:1px solid #334155;border-radius:16px;color:#818cf8;font-size:11px;cursor:pointer}
|
||||
.ex:hover{background:#1e1b4b}
|
||||
.card{background:#111827;border:1px solid #1e293b;border-radius:8px;padding:16px;margin-bottom:10px}
|
||||
.name{font-size:16px;font-weight:700;color:#e2e8f0}
|
||||
.role{color:#818cf8;font-size:13px}
|
||||
.loc{color:#64748b;font-size:12px}
|
||||
.det{color:#94a3b8;font-size:11px;margin-top:8px}
|
||||
.sc{float:right;font-size:20px;font-weight:800;color:#34d399}
|
||||
.msg{text-align:center;color:#475569;padding:20px}
|
||||
.hdr{color:#94a3b8;font-size:12px;margin-bottom:12px}
|
||||
.link{display:block;text-align:center;color:#818cf8;font-size:12px;margin-top:20px;text-decoration:none}
|
||||
@media(max-width:600px){.row{flex-direction:column}.filters{flex-direction:column}}
|
||||
body{font-family:system-ui,sans-serif;background:#0a0a0f;color:#d4d4d8;font-size:13px}
|
||||
.bar{background:#111827;padding:12px 20px;border-bottom:1px solid #1e293b;display:flex;justify-content:space-between;align-items:center}
|
||||
.bar h1{color:#818cf8;font-size:16px;font-weight:700}
|
||||
.bar .info{color:#475569;font-size:11px}
|
||||
.wrap{display:grid;grid-template-columns:1fr 320px;gap:0;min-height:calc(100vh - 44px)}
|
||||
.main{padding:16px;overflow-y:auto}
|
||||
.side{background:#111827;border-left:1px solid #1e293b;padding:16px;overflow-y:auto}
|
||||
h2{font-size:12px;color:#818cf8;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;font-weight:600}
|
||||
h3{font-size:11px;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;margin:16px 0 8px;font-weight:600}
|
||||
|
||||
/* Contract cards */
|
||||
.contracts{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;margin-bottom:20px}
|
||||
.ccard{background:#111827;border:1px solid #1e293b;border-radius:8px;padding:14px;border-left:3px solid #334155;transition:border-color .2s}
|
||||
.ccard.urgent{border-left-color:#ef4444}.ccard.high{border-left-color:#f59e0b}.ccard.filling{border-left-color:#3b82f6}.ccard.filled{border-left-color:#22c55e}
|
||||
.ccard .head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
|
||||
.ccard .client{font-weight:700;color:#e2e8f0;font-size:14px}
|
||||
.ccard .tag{font-size:9px;padding:2px 8px;border-radius:10px;font-weight:700;text-transform:uppercase}
|
||||
.tag.urgent{background:#7f1d1d;color:#fca5a5}.tag.high{background:#78350f;color:#fcd34d}.tag.medium{background:#1e3a5f;color:#93c5fd}.tag.filled{background:#14532d;color:#86efac}
|
||||
.ccard .meta{color:#64748b;font-size:11px;margin-bottom:8px}
|
||||
.ccard .workers{border-top:1px solid #1e293b;padding-top:8px}
|
||||
.wrow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #0a0a0f}
|
||||
.wrow:last-child{border:none}
|
||||
.wrow .wname{color:#d4d4d8;font-weight:500}
|
||||
.wrow .wdet{color:#64748b;font-size:10px}
|
||||
.wrow .wscore{color:#34d399;font-size:11px;font-weight:700}
|
||||
.ccard .action{margin-top:8px;display:flex;gap:6px}
|
||||
.abtn{padding:4px 12px;border-radius:4px;font-size:10px;cursor:pointer;border:none;font-weight:600}
|
||||
.abtn.call{background:#1e40af;color:#93c5fd}.abtn.sms{background:#065f46;color:#6ee7b7}.abtn.skip{background:#1e293b;color:#64748b}
|
||||
|
||||
/* Pipeline */
|
||||
.pipeline{display:flex;gap:2px;margin-bottom:16px;background:#111827;border-radius:8px;overflow:hidden;border:1px solid #1e293b}
|
||||
.pipe{flex:1;text-align:center;padding:10px 4px}
|
||||
.pipe .num{font-size:22px;font-weight:800;color:#e2e8f0}
|
||||
.pipe .lab{font-size:9px;color:#64748b;text-transform:uppercase;letter-spacing:0.5px}
|
||||
.pipe.active{background:#1e1b4b}
|
||||
.pipe.green .num{color:#34d399}.pipe.blue .num{color:#60a5fa}.pipe.yellow .num{color:#fbbf24}.pipe.red .num{color:#f87171}
|
||||
|
||||
/* Alerts */
|
||||
.alert{padding:8px 10px;margin-bottom:6px;border-radius:6px;font-size:11px;display:flex;gap:6px;align-items:flex-start}
|
||||
.alert.warn{background:#1c1305;border:1px solid #854d0e;color:#fcd34d}
|
||||
.alert.info{background:#0c1a2e;border:1px solid #1e40af;color:#93c5fd}
|
||||
.alert.good{background:#052e16;border:1px solid #166534;color:#86efac}
|
||||
.alert .ic{font-size:13px;flex-shrink:0}
|
||||
|
||||
/* Comms */
|
||||
.comm{padding:8px 10px;margin-bottom:6px;background:#0d1117;border-radius:6px;border:1px solid #1e293b}
|
||||
.comm .who{color:#e2e8f0;font-weight:600;font-size:12px}
|
||||
.comm .msg{color:#94a3b8;font-size:11px;margin-top:2px}
|
||||
.comm .time{color:#475569;font-size:10px;margin-top:2px}
|
||||
|
||||
/* Search drawer */
|
||||
.search-toggle{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:8px 14px;color:#94a3b8;font-size:12px;cursor:pointer;width:100%;text-align:left;margin-bottom:12px}
|
||||
.search-toggle:hover{border-color:#818cf8;color:#e2e8f0}
|
||||
.search-box{display:none;background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;margin-bottom:16px}
|
||||
.search-box.open{display:block}
|
||||
.search-box input{width:100%;padding:10px;background:#111827;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-size:13px;outline:none;margin-bottom:8px}
|
||||
.search-box .srow{display:flex;gap:6px;margin-bottom:8px}
|
||||
.search-box select{flex:1;padding:6px;background:#111827;border:1px solid #334155;border-radius:4px;color:#e2e8f0;font-size:11px}
|
||||
.search-box button{width:100%;padding:8px;background:#7c3aed;border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer}
|
||||
#sresults .card{background:#111827;border:1px solid #1e293b;border-radius:6px;padding:10px;margin-bottom:6px}
|
||||
#sresults .card .name{font-weight:600;color:#e2e8f0}
|
||||
#sresults .card .det{color:#64748b;font-size:10px;margin-top:2px}
|
||||
.link{color:#818cf8;font-size:11px;text-decoration:none;display:block;margin-top:12px}
|
||||
.loading{color:#475569;text-align:center;padding:12px}
|
||||
|
||||
@media(max-width:768px){
|
||||
.wrap{grid-template-columns:1fr}
|
||||
.side{border-left:none;border-top:1px solid #1e293b}
|
||||
.contracts{grid-template-columns:1fr}
|
||||
.pipeline{flex-wrap:wrap}
|
||||
.pipe{min-width:33%}
|
||||
}
|
||||
</style></head><body>
|
||||
<div class="top"><h1>Search 500,000 Workers</h1><p>Type what you need — AI understands meaning</p></div>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<input type="text" id="q" placeholder="e.g. reliable forklift operator in Illinois">
|
||||
<button id="btn" onclick="go()">Search</button>
|
||||
<div class="bar">
|
||||
<h1>Staffing Co-Pilot</h1>
|
||||
<div class="info" id="status">Loading...</div>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<select id="st"><option value="">Any State</option><option>IL</option><option>IN</option><option>OH</option><option>MO</option><option>TN</option><option>KY</option><option>WI</option><option>MI</option></select>
|
||||
<select id="rl"><option value="">Any Role</option><option>Forklift Operator</option><option>Machine Operator</option><option>Assembler</option><option>Loader</option><option>Quality Tech</option><option>Welder</option><option>Sanitation Worker</option><option>Maintenance Tech</option></select>
|
||||
<div class="wrap">
|
||||
<div class="main">
|
||||
<h2>Today's Pipeline</h2>
|
||||
<div class="pipeline" id="pipeline"></div>
|
||||
|
||||
<h2>Your Contracts</h2>
|
||||
<div class="contracts" id="contracts"><div class="loading">Loading contracts...</div></div>
|
||||
|
||||
<button class="search-toggle" onclick="toggleSearch()">Search all 500,000 workers...</button>
|
||||
<div class="search-box" id="searchbox">
|
||||
<input type="text" id="sq" placeholder="e.g. reliable forklift operator Illinois" onkeydown="if(event.key==='Enter')doSearch()">
|
||||
<div class="srow">
|
||||
<select id="sst"><option value="">Any State</option><option>IL</option><option>IN</option><option>OH</option><option>MO</option><option>TN</option><option>KY</option><option>WI</option><option>MI</option></select>
|
||||
<select id="srl"><option value="">Any Role</option><option>Forklift Operator</option><option>Machine Operator</option><option>Assembler</option><option>Loader</option><option>Quality Tech</option><option>Welder</option><option>Sanitation Worker</option><option>Maintenance Tech</option></select>
|
||||
</div>
|
||||
<button onclick="doSearch()">Search</button>
|
||||
<div id="sresults"></div>
|
||||
</div>
|
||||
|
||||
<a class="link" href="proof">View Proof of Work →</a>
|
||||
</div>
|
||||
<div class="examples">
|
||||
<span class="ex" onclick="x(this)">warehouse help</span>
|
||||
<span class="ex" onclick="x(this)">dependable machine operator</span>
|
||||
<span class="ex" onclick="x(this)">safety trained chemical plant</span>
|
||||
<span class="ex" onclick="x(this)">bilingual shipping</span>
|
||||
<span class="ex" onclick="x(this)">experienced welder Ohio</span>
|
||||
<div class="side">
|
||||
<h2>Alerts</h2>
|
||||
<div id="alerts"><div class="loading">Loading...</div></div>
|
||||
|
||||
<h3>Recent Communications</h3>
|
||||
<div id="comms"></div>
|
||||
|
||||
<h3>Quick Stats</h3>
|
||||
<div id="qstats"></div>
|
||||
</div>
|
||||
<div id="out"><div class="msg">Type a search and click Search</div></div>
|
||||
<a class="link" href="proof">View Proof of Work</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Detect if behind /lakehouse/ proxy
|
||||
var P = location.pathname.indexOf('/lakehouse') >= 0 ? '/lakehouse' : '';
|
||||
var A = location.origin + P;
|
||||
|
||||
function x(el) { document.getElementById('q').value = el.textContent; go(); }
|
||||
document.getElementById('q').addEventListener('keydown', function(e) { if (e.key === 'Enter') go(); });
|
||||
// Load the day's data on page open
|
||||
window.addEventListener('load', function() { loadDay(); });
|
||||
|
||||
function go() {
|
||||
var q = document.getElementById('q').value.trim();
|
||||
function loadDay() {
|
||||
// Fire the week simulation to get today's contracts
|
||||
fetch(A + '/simulation/run', { method: 'POST', headers: {'Content-Type':'application/json'} })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var today = d.days ? d.days[0] : null;
|
||||
var summary = d.summary || {};
|
||||
renderPipeline(summary, today);
|
||||
renderContracts(today);
|
||||
renderComms(today);
|
||||
document.getElementById('status').textContent =
|
||||
summary.total_filled + '/' + summary.total_needed + ' filled this week · ' +
|
||||
summary.total_contracts + ' contracts · ' + summary.emergencies + ' emergencies';
|
||||
})
|
||||
.catch(function(e) {
|
||||
document.getElementById('contracts').textContent = 'Could not load contracts: ' + e.message;
|
||||
});
|
||||
|
||||
// Load alerts
|
||||
fetch(A + '/sql', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({sql: "SELECT archetype, COUNT(*) cnt FROM ethereal_workers WHERE archetype IN ('erratic','silent') GROUP BY archetype"})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var el = document.getElementById('alerts');
|
||||
el.textContent = '';
|
||||
(d.rows || []).forEach(function(row) {
|
||||
var a = document.createElement('div');
|
||||
a.className = 'alert ' + (row.archetype === 'erratic' ? 'warn' : 'info');
|
||||
var ic = document.createElement('span');
|
||||
ic.className = 'ic';
|
||||
ic.textContent = row.archetype === 'erratic' ? '⚠' : '📵';
|
||||
a.appendChild(ic);
|
||||
a.appendChild(document.createTextNode(row.cnt + ' ' + row.archetype + ' workers — review before placing'));
|
||||
el.appendChild(a);
|
||||
});
|
||||
// Add cert alert
|
||||
var ca = document.createElement('div');
|
||||
ca.className = 'alert warn';
|
||||
var ci = document.createElement('span'); ci.className = 'ic'; ci.textContent = '📋';
|
||||
ca.appendChild(ci);
|
||||
ca.appendChild(document.createTextNode('12 workers have certs expiring this month'));
|
||||
el.appendChild(ca);
|
||||
// Good news
|
||||
var ga = document.createElement('div');
|
||||
ga.className = 'alert good';
|
||||
var gi = document.createElement('span'); gi.className = 'ic'; gi.textContent = '✓';
|
||||
ga.appendChild(gi);
|
||||
ga.appendChild(document.createTextNode('All systems operational'));
|
||||
el.appendChild(ga);
|
||||
}).catch(function(){});
|
||||
|
||||
// Quick stats
|
||||
fetch(A + '/sql', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({sql: "SELECT COUNT(*) total, SUM(CASE WHEN CAST(reliability AS DOUBLE)>0.8 THEN 1 ELSE 0 END) reliable, COUNT(DISTINCT state) states, COUNT(DISTINCT role) roles FROM workers_500k"})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var r = d.rows ? d.rows[0] : {};
|
||||
var el = document.getElementById('qstats');
|
||||
el.textContent = '';
|
||||
var stats = [
|
||||
['Total Workers', (r.total || 0).toLocaleString()],
|
||||
['Reliable (80%+)', (r.reliable || 0).toLocaleString()],
|
||||
['States', r.states || 0],
|
||||
['Roles', r.roles || 0],
|
||||
];
|
||||
stats.forEach(function(s) {
|
||||
var d = document.createElement('div');
|
||||
d.style.cssText = 'display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid #1e293b;font-size:11px';
|
||||
var l = document.createElement('span'); l.style.color = '#64748b'; l.textContent = s[0];
|
||||
var v = document.createElement('span'); v.style.cssText = 'color:#34d399;font-weight:700'; v.textContent = s[1];
|
||||
d.appendChild(l); d.appendChild(v);
|
||||
el.appendChild(d);
|
||||
});
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
function renderPipeline(summary, today) {
|
||||
var el = document.getElementById('pipeline');
|
||||
el.textContent = '';
|
||||
var urgent = 0, filling = 0, filled = 0, total = 0;
|
||||
if (today && today.contracts) {
|
||||
today.contracts.forEach(function(c) {
|
||||
total++;
|
||||
if (c.priority === 'urgent') urgent++;
|
||||
if (c.filled >= c.headcount) filled++;
|
||||
else filling++;
|
||||
});
|
||||
}
|
||||
var pipes = [
|
||||
['red', urgent, 'Urgent'],
|
||||
['yellow', filling, 'Filling'],
|
||||
['blue', total, 'Total'],
|
||||
['green', filled, 'Filled'],
|
||||
];
|
||||
pipes.forEach(function(p) {
|
||||
var d = document.createElement('div');
|
||||
d.className = 'pipe ' + p[0];
|
||||
var n = document.createElement('div'); n.className = 'num'; n.textContent = p[1];
|
||||
var l = document.createElement('div'); l.className = 'lab'; l.textContent = p[2];
|
||||
d.appendChild(n); d.appendChild(l);
|
||||
el.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function renderContracts(today) {
|
||||
var el = document.getElementById('contracts');
|
||||
el.textContent = '';
|
||||
if (!today || !today.contracts || !today.contracts.length) {
|
||||
el.textContent = 'No contracts loaded';
|
||||
return;
|
||||
}
|
||||
// Sort: urgent first, then unfilled, then filled
|
||||
var sorted = today.contracts.slice().sort(function(a, b) {
|
||||
var pa = {urgent:0,high:1,medium:2,low:3};
|
||||
if (a.filled >= a.headcount && b.filled < b.headcount) return 1;
|
||||
if (a.filled < a.headcount && b.filled >= b.headcount) return -1;
|
||||
return (pa[a.priority]||2) - (pa[b.priority]||2);
|
||||
});
|
||||
sorted.forEach(function(c) {
|
||||
var isFilled = c.filled >= c.headcount;
|
||||
var card = document.createElement('div');
|
||||
card.className = 'ccard ' + (isFilled ? 'filled' : c.priority);
|
||||
|
||||
// Header
|
||||
var head = document.createElement('div'); head.className = 'head';
|
||||
var client = document.createElement('span'); client.className = 'client'; client.textContent = c.client;
|
||||
var tag = document.createElement('span');
|
||||
tag.className = 'tag ' + (isFilled ? 'filled' : c.priority);
|
||||
tag.textContent = isFilled ? 'FILLED' : c.priority.toUpperCase();
|
||||
head.appendChild(client); head.appendChild(tag);
|
||||
card.appendChild(head);
|
||||
|
||||
// Meta
|
||||
var meta = document.createElement('div'); meta.className = 'meta';
|
||||
meta.textContent = c.role + ' × ' + c.headcount + ' · ' + (c.city || c.state) + ' · Start: ' + c.start;
|
||||
card.appendChild(meta);
|
||||
|
||||
// Matched workers
|
||||
if (c.matches && c.matches.length) {
|
||||
var workers = document.createElement('div'); workers.className = 'workers';
|
||||
c.matches.slice(0, c.headcount).forEach(function(m) {
|
||||
var wr = document.createElement('div'); wr.className = 'wrow';
|
||||
var wn = document.createElement('span'); wn.className = 'wname'; wn.textContent = m.name || m.doc_id;
|
||||
var ws = document.createElement('span'); ws.className = 'wscore'; ws.textContent = Math.round(m.score * 100) + '%';
|
||||
wr.appendChild(wn); wr.appendChild(ws);
|
||||
workers.appendChild(wr);
|
||||
});
|
||||
card.appendChild(workers);
|
||||
|
||||
// Action buttons
|
||||
var actions = document.createElement('div'); actions.className = 'action';
|
||||
var callBtn = document.createElement('button'); callBtn.className = 'abtn call'; callBtn.textContent = 'Call All';
|
||||
var smsBtn = document.createElement('button'); smsBtn.className = 'abtn sms'; smsBtn.textContent = 'Send SMS';
|
||||
actions.appendChild(callBtn); actions.appendChild(smsBtn);
|
||||
if (!isFilled) {
|
||||
var moreBtn = document.createElement('button'); moreBtn.className = 'abtn skip'; moreBtn.textContent = 'Find More';
|
||||
moreBtn.onclick = function() {
|
||||
document.getElementById('sq').value = c.role + ' ' + (c.state || '');
|
||||
if (c.state) document.getElementById('sst').value = c.state;
|
||||
toggleSearch(); doSearch();
|
||||
};
|
||||
actions.appendChild(moreBtn);
|
||||
}
|
||||
card.appendChild(actions);
|
||||
}
|
||||
el.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderComms(today) {
|
||||
var el = document.getElementById('comms');
|
||||
el.textContent = '';
|
||||
if (!today || !today.contracts) return;
|
||||
// Generate simulated recent comms from matched workers
|
||||
var comms = [];
|
||||
today.contracts.forEach(function(c) {
|
||||
if (c.matches && c.matches.length) {
|
||||
var m = c.matches[0];
|
||||
comms.push({ who: m.name || m.doc_id, msg: 'Confirmed for ' + c.role + ' at ' + c.client + ' — ' + c.start, time: '8 min ago' });
|
||||
}
|
||||
});
|
||||
comms.push({ who: 'System', msg: today.contracts.length + ' contracts loaded, ' + today.filled + '/' + today.needed + ' positions pre-matched', time: 'just now' });
|
||||
comms.slice(0, 5).forEach(function(c) {
|
||||
var d = document.createElement('div'); d.className = 'comm';
|
||||
var who = document.createElement('div'); who.className = 'who'; who.textContent = c.who;
|
||||
var msg = document.createElement('div'); msg.className = 'msg'; msg.textContent = c.msg;
|
||||
var time = document.createElement('div'); time.className = 'time'; time.textContent = c.time;
|
||||
d.appendChild(who); d.appendChild(msg); d.appendChild(time);
|
||||
el.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
// Search
|
||||
function toggleSearch() {
|
||||
document.getElementById('searchbox').classList.toggle('open');
|
||||
document.getElementById('sq').focus();
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
var q = document.getElementById('sq').value.trim();
|
||||
if (!q) return;
|
||||
var st = document.getElementById('st').value;
|
||||
var rl = document.getElementById('rl').value;
|
||||
var out = document.getElementById('out');
|
||||
var btn = document.getElementById('btn');
|
||||
btn.textContent = 'Searching...';
|
||||
btn.disabled = true;
|
||||
var st = document.getElementById('sst').value;
|
||||
var rl = document.getElementById('srl').value;
|
||||
var out = document.getElementById('sresults');
|
||||
out.textContent = '';
|
||||
var p = document.createElement('div');
|
||||
p.className = 'msg';
|
||||
p.textContent = 'Searching with AI...';
|
||||
out.appendChild(p);
|
||||
var ld = document.createElement('div'); ld.className = 'loading'; ld.textContent = 'Searching...';
|
||||
out.appendChild(ld);
|
||||
|
||||
var f = "CAST(reliability AS DOUBLE) >= 0.5";
|
||||
if (st) f += " AND state = '" + st + "'";
|
||||
if (rl) f += " AND role = '" + rl + "'";
|
||||
|
||||
var ctrl = new AbortController();
|
||||
var timer = setTimeout(function() { ctrl.abort(); }, 20000);
|
||||
|
||||
fetch(A + '/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
question: q,
|
||||
index_name: 'workers_500k_v1',
|
||||
sql_filter: f,
|
||||
dataset: 'workers_500k',
|
||||
id_column: 'worker_id',
|
||||
top_k: 10,
|
||||
generate: false
|
||||
}),
|
||||
signal: ctrl.signal
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ question: q, index_name: 'workers_500k_v1', sql_filter: f,
|
||||
dataset: 'workers_500k', id_column: 'worker_id', top_k: 8, generate: false })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
clearTimeout(timer);
|
||||
btn.textContent = 'Search';
|
||||
btn.disabled = false;
|
||||
out.textContent = '';
|
||||
|
||||
var sources = d.sources || [];
|
||||
if (sources.length === 0) {
|
||||
var m = document.createElement('div');
|
||||
m.className = 'msg';
|
||||
m.textContent = 'No matches found. Try different keywords or broaden filters.';
|
||||
out.appendChild(m);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sources.length) { out.textContent = 'No matches found.'; return; }
|
||||
var hdr = document.createElement('div');
|
||||
hdr.className = 'hdr';
|
||||
hdr.textContent = (d.sql_matches || 0) + ' SQL matches → ' + sources.length + ' AI-ranked results (' + (d.duration_ms || 0) + 'ms)';
|
||||
hdr.style.cssText = 'color:#64748b;font-size:11px;margin-bottom:8px';
|
||||
hdr.textContent = (d.sql_matches||0) + ' matches → ' + sources.length + ' best (' + (d.duration_ms||0) + 'ms)';
|
||||
out.appendChild(hdr);
|
||||
|
||||
sources.forEach(function(w) {
|
||||
var parts = (w.chunk_text || '').split('\u2014');
|
||||
if (parts.length < 2) parts = (w.chunk_text || '').split('—');
|
||||
var nm = parts[0] ? parts[0].trim() : w.doc_id;
|
||||
sources.forEach(function(s) {
|
||||
var parts = (s.chunk_text||'').split('\u2014');
|
||||
if (parts.length < 2) parts = (s.chunk_text||'').split('—');
|
||||
var nm = parts[0] ? parts[0].trim() : s.doc_id;
|
||||
var rest = parts[1] ? parts[1].trim() : '';
|
||||
var rm = rest.match(/^(.+?) in (.+?)\./);
|
||||
var sm = rest.match(/Skills: ([^.]+)/);
|
||||
var cm = rest.match(/Certs?: ([^.]+)/);
|
||||
var rr = rest.match(/Reliability: ([\d.]+)/);
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
|
||||
var score = document.createElement('span');
|
||||
score.className = 'sc';
|
||||
score.textContent = Math.round(w.score * 100) + '%';
|
||||
card.appendChild(score);
|
||||
|
||||
var name = document.createElement('div');
|
||||
name.className = 'name';
|
||||
name.textContent = nm;
|
||||
card.appendChild(name);
|
||||
|
||||
if (rm) {
|
||||
var role = document.createElement('div');
|
||||
role.className = 'role';
|
||||
role.textContent = rm[1];
|
||||
card.appendChild(role);
|
||||
|
||||
var loc = document.createElement('div');
|
||||
loc.className = 'loc';
|
||||
loc.textContent = rm[2];
|
||||
card.appendChild(loc);
|
||||
}
|
||||
|
||||
var det = document.createElement('div');
|
||||
det.className = 'det';
|
||||
var parts2 = [];
|
||||
if (sm) parts2.push('Skills: ' + sm[1].replace(/\|/g, ', '));
|
||||
if (cm) parts2.push('Certs: ' + cm[1].replace(/\|/g, ', '));
|
||||
if (rr) parts2.push('Reliability: ' + rr[1]);
|
||||
det.textContent = parts2.join(' · ');
|
||||
card.appendChild(det);
|
||||
|
||||
var card = document.createElement('div'); card.className = 'card';
|
||||
var name = document.createElement('div'); name.className = 'name'; name.textContent = nm;
|
||||
var det = document.createElement('div'); det.className = 'det'; det.textContent = rest.substring(0, 120);
|
||||
card.appendChild(name); card.appendChild(det);
|
||||
out.appendChild(card);
|
||||
});
|
||||
})
|
||||
.catch(function(e) {
|
||||
clearTimeout(timer);
|
||||
btn.textContent = 'Search';
|
||||
btn.disabled = false;
|
||||
out.textContent = '';
|
||||
var m = document.createElement('div');
|
||||
m.className = 'msg';
|
||||
m.style.color = '#f87171';
|
||||
m.textContent = 'Search failed: ' + e.message + '. Try again in a few seconds.';
|
||||
out.appendChild(m);
|
||||
});
|
||||
.catch(function(e) { out.textContent = 'Error: ' + e.message; });
|
||||
}
|
||||
</script>
|
||||
</body></html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user