From a31c8d678c52410fe1216d249fef5e148cd20006 Mon Sep 17 00:00:00 2001 From: sabo Date: Wed, 26 Aug 2026 10:44:25 +0300 Subject: [PATCH] Restore search UI: keyword cloud, mode filters, labeled extractions. Co-authored-by: Cursor --- webapp/templates/viewer.html | 92 +++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/webapp/templates/viewer.html b/webapp/templates/viewer.html index 0e665c8..55b8e88 100644 --- a/webapp/templates/viewer.html +++ b/webapp/templates/viewer.html @@ -142,6 +142,20 @@ .card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; } .card-text { font-size: 12px; color: var(--muted); line-height: 1.6; max-height: 280px; overflow: hidden; } .card-text img { max-width: 100%; max-height: 200px; display: block; margin: 6px 0; border-radius: 4px; border: 1px solid var(--border); } + .card-section-label { + font-family: var(--mono); font-size: 10px; color: var(--muted); + letter-spacing: .06em; text-transform: uppercase; margin: 10px 0 6px; + } + .kw-cloud { + display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; + padding: 12px 14px; background: var(--bg2); border: 1px solid var(--border); + border-radius: var(--radius-lg); max-height: 140px; overflow-y: auto; + } + .kw-cloud .tag { cursor: pointer; user-select: none; } + .kw-cloud .tag:hover { outline: 2px solid var(--accent); } + .kw-cloud .tag.active { background: var(--accent); color: #fff; } + .search-mode { display: flex; gap: 8px; align-items: center; font-size: 12px; color: var(--muted); } + .search-mode label { display: flex; align-items: center; gap: 4px; cursor: pointer; font-family: var(--mono); font-size: 11px; } .card-footer { margin-top: 8px; font-size: 11px; color: var(--muted); font-family: var(--mono); } .check-icon { width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--border); flex-shrink: 0; margin-top: 2px; transition: all .15s; } .card.selected .check-icon { background: var(--accent2); border-color: var(--accent2); } @@ -272,11 +286,19 @@ @@ -318,6 +340,7 @@ const ALL = {{ sections_json | safe }}; document.getElementById('total-count').textContent = ALL.length + ' секции'; renderEditor(ALL); +renderKwCloud(); doSearch(); renderGenerator(); @@ -465,6 +488,49 @@ async function saveOne(inp) { // ── SEARCH ──────────────────────────── const selected = new Set(); let genOrder = []; +const activeKw = new Set(); + +function allKeywords() { + const map = new Map(); + ALL.forEach(r => { + (r.keywords || '').split(',').map(k => k.trim()).filter(Boolean).forEach(k => { + map.set(k, (map.get(k) || 0) + 1); + }); + }); + return [...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'bg')); +} + +function renderKwCloud() { + const host = document.getElementById('kw-cloud'); + if (!host) return; + const kws = allKeywords(); + if (!kws.length) { + host.innerHTML = 'Няма ключови думи в данните'; + return; + } + host.innerHTML = kws.map(([k, n]) => + `${esc(k)}` + ).join(''); + host.onclick = (ev) => { + const t = ev.target.closest('.tag[data-kw]'); + if (!t) return; + toggleKw(ev, t.getAttribute('data-kw')); + }; +} + +function toggleKw(ev, kw) { + ev.stopPropagation(); + const key = kw.toLowerCase(); + if (activeKw.has(key)) activeKw.delete(key); + else activeKw.add(key); + // sync into search box as quoted tokens for clarity + const box = document.getElementById('search-input'); + const parts = [...activeKw].map(k => (/\s/.test(k) ? `"${k}"` : k)); + box.value = parts.join(' '); + renderKwCloud(); + doSearch(); +} function tokenizeQuery(q) { const tokens = []; @@ -477,15 +543,27 @@ function tokenizeQuery(q) { return tokens; } +function searchMode() { + const el = document.querySelector('input[name="search-mode"]:checked'); + return el ? el.value : 'all'; +} + function doSearch() { const q = document.getElementById('search-input').value.trim().toLowerCase(); const tokens = q ? tokenizeQuery(q) : []; + const mode = searchMode(); const results = tokens.length === 0 ? ALL : ALL.filter(r => { - const hay = ((r.keywords||'') + ' ' + (r.title||'') + ' ' + (r.text||'')).toLowerCase(); + const kw = (r.keywords || '').toLowerCase(); + const title = (r.title || '').toLowerCase(); + const text = (r.text || '').toLowerCase(); + let hay = ''; + if (mode === 'keywords') hay = kw; + else if (mode === 'text') hay = text + ' ' + title; + else hay = kw + ' ' + title + ' ' + text; return tokens.every(t => hay.includes(t)); }); document.getElementById('search-stats').textContent = - results.length + ' резултата' + (tokens.length > 1 ? ' (за ' + tokens.length + ' термина)' : ''); + results.length + ' извлечения' + (tokens.length ? ' · филтър: ' + tokens.length + ' термина' : ''); document.getElementById('search-results').innerHTML = results.map(r => `
@@ -495,11 +573,13 @@ function doSearch() {
+
- ${(r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`${esc(k.trim())}`).join('')} + ${(r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`${esc(k.trim())}`).join('') || '—'}
-
${r.text_html || esc(r.text||'(няма preview)')}
- + +
${r.text_html || esc(r.text||'(няма текст — провери OUTPUT_DIR)')}
+ `).join(''); }