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-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 { 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-text img { max-width: 100%; max-height: 200px; display: block; margin: 6px 0; border-radius: 4px; border: 1px solid var(--border); }
|
||||||
.card-section-label {
|
.card-actions { display: flex; gap: 8px; margin-top: 10px; }
|
||||||
font-family: var(--mono); font-size: 10px; color: var(--muted);
|
.card-actions button { font-size: 11px; padding: 5px 10px; }
|
||||||
letter-spacing: .06em; text-transform: uppercase; margin: 10px 0 6px;
|
.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 {
|
.kw-cloud {
|
||||||
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px;
|
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px;
|
||||||
padding: 12px 14px; background: var(--bg2); border: 1px solid var(--border);
|
padding: 12px 14px; background: var(--bg2); border: 1px solid var(--border);
|
||||||
@@ -286,7 +298,7 @@
|
|||||||
<div id="tab-search" class="panel">
|
<div id="tab-search" class="panel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<input type="text" class="search-box" id="search-input"
|
<input type="text" class="search-box" id="search-input"
|
||||||
placeholder='Търси в извлечения и ключови думи; фраза в "кавички"'
|
placeholder='напр. „търся инфо за лимитни карти“ — служебните думи се прескачат'
|
||||||
oninput="doSearch()">
|
oninput="doSearch()">
|
||||||
<div class="search-mode">
|
<div class="search-mode">
|
||||||
<label><input type="radio" name="search-mode" value="all" checked onchange="doSearch()"> всичко</label>
|
<label><input type="radio" name="search-mode" value="all" checked onchange="doSearch()"> всичко</label>
|
||||||
@@ -317,22 +329,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gen-panel">
|
<div class="gen-panel">
|
||||||
<h3>ГЕНЕРИРАЙ ДОКУМЕНТ</h3>
|
<h3>ГЕНЕРИРАЙ HTML</h3>
|
||||||
<div class="format-btns">
|
<div class="format-btns">
|
||||||
<button class="format-btn" onclick="generateDoc('html')">
|
<button class="format-btn" onclick="generateDoc('html')">
|
||||||
<span class="format-dot dot-html"></span> HTML файл (нов tab)
|
<span class="format-dot dot-html"></span> Отвори HTML документ
|
||||||
</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
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
|
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
|
||||||
@@ -556,12 +578,32 @@ function toggleKw(ev, kw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tokenizeQuery(q) {
|
function tokenizeQuery(q) {
|
||||||
|
// Паразитни / служебни думи — прескачат се при търсене
|
||||||
|
const STOP = new Set([
|
||||||
|
'аз','ти','той','тя','то','ние','вие','те','ме','ми','му','го','я','ги','ни','ви','се','си',
|
||||||
|
'съм','си','е','сме','сте','са','бях','беше','бяха','ще','да','не','ли','дали',
|
||||||
|
'и','или','но','а','нито','че','като','ако','защото','понеже',
|
||||||
|
'за','от','на','в','във','с','със','по','към','до','при','след','преди','без','над','под','през','около','между',
|
||||||
|
'този','тази','това','тези','онзи','онази','онова','онези','един','една','едно','едни',
|
||||||
|
'как','кой','коя','кое','кои','къде','кога','защо','колко','какъв','каква','какво','какви',
|
||||||
|
'инфо','информация','търся','търсим','търсите','искам','искаме','искате','моля','моляте',
|
||||||
|
'покажи','покажете','кажи','кажете','дай','дайте','намери','намерете','имам','имаме','има','няма',
|
||||||
|
'може','могат','трябва','също','още','вече','само','много','малко','добре','така','тук','там',
|
||||||
|
'сега','тогава','пак','отново','примерно','например','относно','във','връзка','относно',
|
||||||
|
'please','info','information','about','for','the','a','an','i','am','looking','find','show','get',
|
||||||
|
]);
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
const re = /"([^"]+)"|(\S+)/g;
|
const re = /"([^"]+)"|(\S+)/g;
|
||||||
let m;
|
let m;
|
||||||
while ((m = re.exec(q)) !== null) {
|
while ((m = re.exec(q)) !== null) {
|
||||||
const t = (m[1] || m[2] || '').toLowerCase();
|
let t = (m[1] || m[2] || '').toLowerCase();
|
||||||
if (t) tokens.push(t);
|
// махаме пунктуация в края/началото: "карти:," -> "карти"
|
||||||
|
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;
|
return tokens;
|
||||||
}
|
}
|
||||||
@@ -572,7 +614,8 @@ function searchMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doSearch() {
|
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 tokens = q ? tokenizeQuery(q) : [];
|
||||||
const mode = searchMode();
|
const mode = searchMode();
|
||||||
const results = tokens.length === 0 ? ALL : ALL.filter(r => {
|
const results = tokens.length === 0 ? ALL : ALL.filter(r => {
|
||||||
@@ -585,8 +628,11 @@ function doSearch() {
|
|||||||
else hay = kw + ' ' + title + ' ' + text;
|
else hay = kw + ' ' + title + ' ' + text;
|
||||||
return tokens.every(t => hay.includes(t));
|
return tokens.every(t => hay.includes(t));
|
||||||
});
|
});
|
||||||
|
const skippedNote = (raw && tokens.length === 0)
|
||||||
|
? ' · само паразитни думи — показани всички'
|
||||||
|
: (tokens.length ? ' · филтър: ' + tokens.join(', ') : '');
|
||||||
document.getElementById('search-stats').textContent =
|
document.getElementById('search-stats').textContent =
|
||||||
results.length + ' извлечения' + (tokens.length ? ' · филтър: ' + tokens.length + ' термина' : '');
|
results.length + ' извлечения' + skippedNote;
|
||||||
document.getElementById('search-results').innerHTML = results.map(r => `
|
document.getElementById('search-results').innerHTML = results.map(r => `
|
||||||
<div class="card ${selected.has(r.code)?'selected':''}" onclick="toggleSelect('${r.code}', this)">
|
<div class="card ${selected.has(r.code)?'selected':''}" onclick="toggleSelect('${r.code}', this)">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -602,11 +648,43 @@ function doSearch() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-section-label">Извлечение</div>
|
<div class="card-section-label">Извлечение</div>
|
||||||
<div class="card-text">${r.text_html || esc(r.text||'(няма текст — провери OUTPUT_DIR)')}</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 class="card-footer">${esc(shortPath(r.source_file))} · ${r.char_count||0} знака</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).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) {
|
function toggleSelect(code, el) {
|
||||||
if (selected.has(code)) {
|
if (selected.has(code)) {
|
||||||
selected.delete(code);
|
selected.delete(code);
|
||||||
|
|||||||
Reference in New Issue
Block a user