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:
parent
e7e988dcc0
commit
7cb9999451
@ -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
178
mcp-server/search.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user