From 4d66e44fd30fb248217771a73b2777e970d051ea Mon Sep 17 00:00:00 2001 From: sabo Date: Wed, 26 Aug 2026 12:55:36 +0300 Subject: [PATCH] Search: stop-words, full extraction modal, HTML-only generator. Co-authored-by: Cursor --- webapp/templates/viewer.html | 110 ++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/webapp/templates/viewer.html b/webapp/templates/viewer.html index d7d81aa..05ed87b 100644 --- a/webapp/templates/viewer.html +++ b/webapp/templates/viewer.html @@ -142,10 +142,22 @@ .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; + .card-actions { display: flex; gap: 8px; margin-top: 10px; } + .card-actions button { font-size: 11px; padding: 5px 10px; } + .modal-backdrop { + display: none; position: fixed; inset: 0; background: rgba(20,24,36,.45); + z-index: 200; align-items: center; justify-content: center; padding: 24px; } + .modal-backdrop.open { display: flex; } + .modal { + background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg); + width: min(900px, 100%); max-height: min(86vh, 900px); overflow: auto; + box-shadow: 0 16px 48px rgba(0,0,0,.18); padding: 20px 22px; + } + .modal-head { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 12px; } + .modal-head h2 { font-size: 16px; font-weight: 500; line-height: 1.35; } + .modal-body { font-size: 13px; line-height: 1.65; color: var(--text); } + .modal-body img { max-width: 100%; max-height: 320px; display: block; margin: 10px 0; border: 1px solid var(--border); border-radius: 6px; } .kw-cloud { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; padding: 12px 14px; background: var(--bg2); border: 1px solid var(--border); @@ -286,7 +298,7 @@ + +
@@ -556,12 +578,32 @@ function toggleKw(ev, kw) { } function tokenizeQuery(q) { + // Паразитни / служебни думи — прескачат се при търсене + const STOP = new Set([ + 'аз','ти','той','тя','то','ние','вие','те','ме','ми','му','го','я','ги','ни','ви','се','си', + 'съм','си','е','сме','сте','са','бях','беше','бяха','ще','да','не','ли','дали', + 'и','или','но','а','нито','че','като','ако','защото','понеже', + 'за','от','на','в','във','с','със','по','към','до','при','след','преди','без','над','под','през','около','между', + 'този','тази','това','тези','онзи','онази','онова','онези','един','една','едно','едни', + 'как','кой','коя','кое','кои','къде','кога','защо','колко','какъв','каква','какво','какви', + 'инфо','информация','търся','търсим','търсите','искам','искаме','искате','моля','моляте', + 'покажи','покажете','кажи','кажете','дай','дайте','намери','намерете','имам','имаме','има','няма', + 'може','могат','трябва','също','още','вече','само','много','малко','добре','така','тук','там', + 'сега','тогава','пак','отново','примерно','например','относно','във','връзка','относно', + 'please','info','information','about','for','the','a','an','i','am','looking','find','show','get', + ]); const tokens = []; const re = /"([^"]+)"|(\S+)/g; let m; while ((m = re.exec(q)) !== null) { - const t = (m[1] || m[2] || '').toLowerCase(); - if (t) tokens.push(t); + let t = (m[1] || m[2] || '').toLowerCase(); + // махаме пунктуация в края/началото: "карти:," -> "карти" + t = t.replace(/^[\s.,;:!?()\[\]{}"'«»]+|[\s.,;:!?()\[\]{}"'«»]+$/g, ''); + if (!t) continue; + // quoted phrases keep even if short; unquoted stop-words skip + const quoted = !!m[1]; + if (!quoted && (STOP.has(t) || t.length < 2)) continue; + tokens.push(t); } return tokens; } @@ -572,7 +614,8 @@ function searchMode() { } function doSearch() { - const q = document.getElementById('search-input').value.trim().toLowerCase(); + const raw = document.getElementById('search-input').value.trim(); + const q = raw.toLowerCase(); const tokens = q ? tokenizeQuery(q) : []; const mode = searchMode(); const results = tokens.length === 0 ? ALL : ALL.filter(r => { @@ -585,8 +628,11 @@ function doSearch() { else hay = kw + ' ' + title + ' ' + text; return tokens.every(t => hay.includes(t)); }); + const skippedNote = (raw && tokens.length === 0) + ? ' · само паразитни думи — показани всички' + : (tokens.length ? ' · филтър: ' + tokens.join(', ') : ''); document.getElementById('search-stats').textContent = - results.length + ' извлечения' + (tokens.length ? ' · филтър: ' + tokens.length + ' термина' : ''); + results.length + ' извлечения' + skippedNote; document.getElementById('search-results').innerHTML = results.map(r => `
@@ -602,11 +648,43 @@ function doSearch() {
${r.text_html || esc(r.text||'(няма текст — провери OUTPUT_DIR)')}
+
+ +
`).join(''); } +async function openSection(code) { + const modal = document.getElementById('section-modal'); + document.getElementById('modal-code').textContent = code; + document.getElementById('modal-title').textContent = 'Зареждане…'; + document.getElementById('modal-tags').innerHTML = ''; + document.getElementById('modal-body').textContent = 'Зареждане…'; + modal.classList.add('open'); + try { + const res = await fetch('/api/section/' + encodeURIComponent(code)); + if (!res.ok) throw new Error('HTTP ' + res.status); + const r = await res.json(); + document.getElementById('modal-title').textContent = r.title || code; + document.getElementById('modal-tags').innerHTML = + (r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`${esc(k.trim())}`).join('') + || '—'; + document.getElementById('modal-body').innerHTML = + r.text_html || esc(r.text || '(празно)').replace(/\n/g, '
'); + } catch (e) { + document.getElementById('modal-body').textContent = 'Грешка: ' + e.message; + } +} + +function closeSection() { + document.getElementById('section-modal').classList.remove('open'); +} +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeSection(); +}); + function toggleSelect(code, el) { if (selected.has(code)) { selected.delete(code);