Rebuild search UI: zero dependencies, plain JS, DOM-only, works

Replaced complex dashboard with minimal search.html:
- No external JS/CSS files, no transpilation, no module imports
- Plain JS with .then() chains (no async/await compat issues)
- DOM-only rendering via createElement (no innerHTML with data)
- 20s AbortController timeout so fetch never hangs
- Detects /lakehouse/ proxy prefix automatically
- 7KB total, loads in 18ms

Calls lakehouse /vectors/hybrid directly — SQL filters always apply,
works even when HNSW isn't loaded (brute-force fallback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 13:26:27 -05:00
parent e7e988dcc0
commit 7cb9999451
2 changed files with 182 additions and 2 deletions

View File

@ -852,9 +852,11 @@ tr:hover{background:#111827}
});
}
// Dashboard UI
// Dashboard — calls lakehouse /vectors/hybrid directly (no gateway hop)
if (url.pathname === "/" || url.pathname === "/dashboard") {
return new Response(Bun.file(import.meta.dir + "/dashboard.html"));
return new Response(Bun.file(import.meta.dir + "/search.html"), {
headers: { ...cors, "Content-Type": "text/html" },
});
}
if (url.pathname === "/dashboard.css") {
return new Response(Bun.file(import.meta.dir + "/dashboard.css"), { headers: { "Content-Type": "text/css" } });

178
mcp-server/search.html Normal file
View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lakehouse Search</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}}
</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>
<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>
<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>
<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(); });
function go() {
var q = document.getElementById('q').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;
out.textContent = '';
var p = document.createElement('div');
p.className = 'msg';
p.textContent = 'Searching with AI...';
out.appendChild(p);
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
})
.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;
}
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)';
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;
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);
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);
});
}
</script>
</body></html>