Search: stop-words, full extraction modal, HTML-only generator.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 12:55:36 +03:00
parent 1c7874a31e
commit 4d66e44fd3

View File

@@ -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 @@
<div id="tab-search" class="panel">
<div class="toolbar">
<input type="text" class="search-box" id="search-input"
placeholder='Търси в извлечения и ключови думи; фраза в "кавички"'
placeholder='напр. „търся инфо за лимитни карти“ — служебните думи се прескачат'
oninput="doSearch()">
<div class="search-mode">
<label><input type="radio" name="search-mode" value="all" checked onchange="doSearch()"> всичко</label>
@@ -317,22 +329,32 @@
</div>
</div>
<div class="gen-panel">
<h3>ГЕНЕРИРАЙ ДОКУМЕНТ</h3>
<h3>ГЕНЕРИРАЙ HTML</h3>
<div class="format-btns">
<button class="format-btn" onclick="generateDoc('html')">
<span class="format-dot dot-html"></span> HTML файл (нов tab)
</button>
<button class="format-btn" onclick="generateDoc('docx')">
<span class="format-dot dot-word"></span> Word (.docx) — TODO
</button>
<button class="format-btn" onclick="generateDoc('pdf')">
<span class="format-dot dot-pdf"></span> PDF — TODO
<span class="format-dot dot-html"></span> Отвори HTML документ
</button>
</div>
</div>
</div>
</div>
<div class="modal-backdrop" id="section-modal" onclick="if(event.target===this)closeSection()">
<div class="modal" role="dialog" aria-modal="true">
<div class="modal-head">
<div>
<div class="code-badge" id="modal-code"></div>
<h2 id="modal-title" style="margin-top:8px"></h2>
</div>
<button class="btn-ghost" type="button" onclick="closeSection()">Затвори</button>
</div>
<div class="card-section-label">Ключови думи</div>
<div class="card-tags" id="modal-tags"></div>
<div class="card-section-label">Извлечение</div>
<div class="modal-body" id="modal-body">Зареждане…</div>
</div>
</div>
<div id="toast"></div>
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
@@ -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 => `
<div class="card ${selected.has(r.code)?'selected':''}" onclick="toggleSelect('${r.code}', this)">
<div class="card-header">
@@ -602,11 +648,43 @@ function doSearch() {
</div>
<div class="card-section-label">Извлечение</div>
<div class="card-text">${r.text_html || esc(r.text||'(няма текст — провери OUTPUT_DIR)')}</div>
<div class="card-actions">
<button type="button" class="btn-ghost" onclick="event.stopPropagation(); openSection('${r.code}')">Цяло извлечение</button>
</div>
<div class="card-footer">${esc(shortPath(r.source_file))} &nbsp;·&nbsp; ${r.char_count||0} знака</div>
</div>
`).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=>`<span class="tag">${esc(k.trim())}</span>`).join('')
|| '<span class="stats">—</span>';
document.getElementById('modal-body').innerHTML =
r.text_html || esc(r.text || '(празно)').replace(/\n/g, '<br>');
} 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);