Search: stop-words, full extraction modal, HTML-only generator.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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))} · ${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);
|
||||
|
||||
Reference in New Issue
Block a user