// Search results — read q from hash, group hits by kind, sidebar // filters for kinds and space_id. import { api } from '../api.js'; import { el, mount, clear } from '../dom.js'; import { navigate } from '../router.js'; const KINDS = ['page', 'ref', 'source_doc', 'message']; const KIND_LABEL = { page: 'Pages', ref: 'References', source_doc: 'Source docs', message: 'Messages' }; const ROUTE_BY_KIND = { page: id => '#/page/' + id, ref: id => '#/ref/' + id, source_doc: () => null, message: () => null }; function hitRow(h) { const route = ROUTE_BY_KIND[h.kind]; const target = route ? route(h.id) : null; return el('div', { class: 'search-hit' }, target ? el('a', { href: target }, h.title_or_snippet || '(untitled)') : el('span', {}, h.title_or_snippet || '(untitled)'), el('div', { class: 'hit-meta' }, 'rank ' + (h.rank || 0).toFixed(3) + (h.space_id ? ' · space ' + h.space_id.slice(0, 8) : ' · no space') ) ); } async function runSearch(resultsEl, statusEl, q, opts) { if (!q) { clear(resultsEl); mount(statusEl, el('span', { class: 'muted' }, 'Type a query above to start.')); return; } mount(statusEl, el('span', { class: 'muted' }, 'Searching …')); const url = new URL('/api/search', location.origin); url.searchParams.set('q', q); if (opts.space_id) url.searchParams.set('space_id', opts.space_id); if (opts.kinds.length && opts.kinds.length < KINDS.length) url.searchParams.set('kinds', opts.kinds.join(',')); url.searchParams.set('limit', '100'); let hits = []; try { hits = await api.get(url.pathname + url.search); } catch (e) { clear(resultsEl); mount(statusEl, el('span', { class: 'muted' }, 'Error: ' + e.message)); return; } mount(statusEl, el('span', { class: 'muted' }, hits.length + ' hits')); const byKind = new Map(); for (const h of hits) { if (!byKind.has(h.kind)) byKind.set(h.kind, []); byKind.get(h.kind).push(h); } clear(resultsEl); if (!hits.length) { resultsEl.appendChild(el('p', { class: 'muted' }, 'No matches.')); return; } for (const k of KINDS) { const items = byKind.get(k); if (!items?.length) continue; resultsEl.appendChild(el('div', { class: 'search-group' }, el('div', { class: 'group-h' }, KIND_LABEL[k] + ' (' + items.length + ')'), items.map(hitRow) )); } } export async function render(main, ctx) { const q = ctx.query.q || ''; let spaces = []; try { spaces = await api.get('/api/spaces'); } catch { /* */ } const queryInput = el('input', { type: 'text', placeholder: 'Search …', value: q, style: { width: '100%' } }); const opts = { kinds: [...KINDS], space_id: '' }; const kindCheckboxes = KINDS.map(k => { const cb = el('input', { type: 'checkbox', checked: true, onchange: () => { opts.kinds = KINDS.filter((kk, i) => kindCheckboxes[i].querySelector('input').checked); runSearch(resultsEl, statusEl, queryInput.value, opts); } }); return el('label', { style: { display: 'block', cursor: 'pointer', padding: '2px 0' } }, cb, ' ', KIND_LABEL[k] ); }); const spaceSelect = el('select', { style: { width: '100%' }, onchange: () => { opts.space_id = spaceSelect.value; runSearch(resultsEl, statusEl, queryInput.value, opts); } }); spaceSelect.appendChild(el('option', { value: '' }, '(all spaces)')); for (const s of spaces) spaceSelect.appendChild(el('option', { value: s.id }, s.name)); const statusEl = el('div', { class: 'muted', style: { marginBottom: '10px', fontSize: '12px' } }); const resultsEl = el('div'); queryInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const v = queryInput.value.trim(); navigate('/search?q=' + encodeURIComponent(v)); runSearch(resultsEl, statusEl, v, opts); } }); mount(main, el('h1', { class: 'view-h1' }, 'Search'), el('p', { class: 'view-sub muted' }, 'Full-text across pages, references, source docs, and messages.'), el('div', { class: 'row' }, el('div', { class: 'card', style: { flex: '0 0 240px' } }, el('h3', {}, 'Filters'), el('div', { class: 'muted', style: { fontSize: '11px', marginBottom: '4px' } }, 'Kinds'), kindCheckboxes, el('div', { class: 'muted', style: { fontSize: '11px', margin: '10px 0 4px' } }, 'Space'), spaceSelect ), el('div', { class: 'card' }, el('h3', {}, 'Query'), queryInput, statusEl, resultsEl ) ) ); runSearch(resultsEl, statusEl, q, opts); }