Fix dashboard: always use hybrid (no HNSW dependency), 15s timeout, error display

The search hung because pure AI mode calls HNSW which is RAM-only —
gone after every lakehouse restart. Now ALL AI/hybrid searches go
through the /search endpoint which uses brute-force when HNSW isn't
loaded. Added 15s AbortController timeout so fetch never hangs.
Added window.onerror handler to show JS errors on page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 13:23:29 -05:00
parent 5c93338f40
commit e7e988dcc0

View File

@ -104,6 +104,11 @@ body{font-family:'Inter','SF Pro',system-ui,sans-serif;background:#0a0a0f;color:
</div> </div>
<script> <script>
window.onerror = function(msg, url, line) {
document.body.insertAdjacentHTML('afterbegin',
'<div style="background:#7f1d1d;color:#fca5a5;padding:12px;font-size:12px">JS Error: ' + msg + ' (line ' + line + ')</div>');
};
const base = window.location.pathname.replace(/\/+$/, ''); const base = window.location.pathname.replace(/\/+$/, '');
const GW = window.location.origin + base; const GW = window.location.origin + base;
let mode = 'ai'; let mode = 'ai';
@ -139,9 +144,10 @@ async function doSearch() {
const t0 = Date.now(); const t0 = Date.now();
// If ANY filter is set, always use hybrid — filters only work through SQL // ALWAYS use hybrid for AI modes — it handles filters AND works even
const hasFilters = state || role || rel > 0.5; // when HNSW isn't loaded (falls back to brute-force). Pure HNSW mode
const effectiveMode = (mode === 'crm') ? 'crm' : (hasFilters ? 'hybrid' : mode); // hangs when the RAM index isn't built.
const effectiveMode = (mode === 'crm') ? 'crm' : 'hybrid';
try { try {
let data; let data;
@ -159,33 +165,29 @@ async function doSearch() {
data = await r.json(); data = await r.json();
renderCRMResults(data, query, Date.now() - t0); renderCRMResults(data, query, Date.now() - t0);
} else if (effectiveMode === 'ai') {
// Pure AI vector search — no filters, just meaning
const r = await fetch(GW + '/api/vectors/hnsw/search', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({index_name: 'workers_500k_v1', query: query, top_k: 10})
});
data = await r.json();
renderAIResults(data, query, Date.now() - t0);
} else { } else {
// Hybrid — SQL filters enforce structure, AI ranks by relevance // Hybrid — SQL filters enforce structure, AI ranks by relevance
// Always works — uses brute-force when HNSW isn't loaded
let filter = "CAST(reliability AS DOUBLE) >= " + rel; let filter = "CAST(reliability AS DOUBLE) >= " + rel;
if (state) filter += " AND state = '" + state + "'"; if (state) filter += " AND state = '" + state + "'";
if (role) filter += " AND role = '" + role + "'"; if (role) filter += " AND role = '" + role + "'";
const controller = new AbortController();
const timeout = setTimeout(function() { controller.abort(); }, 15000);
const r = await fetch(GW + '/search', { const r = await fetch(GW + '/search', {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ body: JSON.stringify({
question: query, index_name: 'workers_500k_v1', question: query, index_name: 'workers_500k_v1',
sql_filter: filter, dataset: 'workers_500k', sql_filter: filter, dataset: 'workers_500k',
id_column: 'worker_id', top_k: 10, generate: false id_column: 'worker_id', top_k: 10, generate: false
}) }),
signal: controller.signal
}); });
clearTimeout(timeout);
data = await r.json(); data = await r.json();
renderHybridResults(data, query, Date.now() - t0); renderHybridResults(data, query, Date.now() - t0);
} }
} catch (e) { } catch (e) {
document.getElementById('results').innerHTML = '<div class="empty">Error: ' + e.message + '</div>'; document.getElementById('results').innerHTML = '<div class="empty">Search error: ' + e.message + '<br><br>If filters were set, try Hybrid mode. If the search is slow, the AI index may be loading — try again in 10 seconds.</div>';
} }
btn.disabled = false; btn.disabled = false;