Restore search UI: keyword cloud, mode filters, labeled extractions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 10:44:25 +03:00
parent 7f86c4e7c2
commit a31c8d678c

View File

@@ -142,6 +142,20 @@
.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 {
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); } .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; } .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); } .card.selected .check-icon { background: var(--accent2); border-color: var(--accent2); }
@@ -272,11 +286,19 @@
<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='Няколко думи разделени с интервал (AND); за фраза - "в кавички"' placeholder='Търси в извлечения и ключови думи; фраза в "кавички"'
oninput="doSearch()"> oninput="doSearch()">
<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="keywords" onchange="doSearch()"> само ключови</label>
<label><input type="radio" name="search-mode" value="text" onchange="doSearch()"> само извлечения</label>
</div>
<span class="stats" id="search-stats"></span> <span class="stats" id="search-stats"></span>
<button class="btn-primary" onclick="addSelectedToGenerator()">Добави избраните → Генератор</button> <button class="btn-primary" onclick="addSelectedToGenerator()">Добави избраните → Генератор</button>
</div> </div>
<div class="card-section-label" style="margin-top:0">Ключови думи — клик за филтър</div>
<div class="kw-cloud" id="kw-cloud"></div>
<div class="card-section-label">Извлечения (резултати)</div>
<div class="results-grid" id="search-results"></div> <div class="results-grid" id="search-results"></div>
</div> </div>
@@ -318,6 +340,7 @@ const ALL = {{ sections_json | safe }};
document.getElementById('total-count').textContent = ALL.length + ' секции'; document.getElementById('total-count').textContent = ALL.length + ' секции';
renderEditor(ALL); renderEditor(ALL);
renderKwCloud();
doSearch(); doSearch();
renderGenerator(); renderGenerator();
@@ -465,6 +488,49 @@ async function saveOne(inp) {
// ── SEARCH ──────────────────────────── // ── SEARCH ────────────────────────────
const selected = new Set(); const selected = new Set();
let genOrder = []; 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 = '<span class="stats">Няма ключови думи в данните</span>';
return;
}
host.innerHTML = kws.map(([k, n]) =>
`<span class="tag ${activeKw.has(k.toLowerCase()) ? 'active' : ''}"
data-kw="${esc(k)}" title="${n} секции">${esc(k)}</span>`
).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) { function tokenizeQuery(q) {
const tokens = []; const tokens = [];
@@ -477,15 +543,27 @@ function tokenizeQuery(q) {
return tokens; return tokens;
} }
function searchMode() {
const el = document.querySelector('input[name="search-mode"]:checked');
return el ? el.value : 'all';
}
function doSearch() { function doSearch() {
const q = document.getElementById('search-input').value.trim().toLowerCase(); const q = document.getElementById('search-input').value.trim().toLowerCase();
const tokens = q ? tokenizeQuery(q) : []; const tokens = q ? tokenizeQuery(q) : [];
const mode = searchMode();
const results = tokens.length === 0 ? ALL : ALL.filter(r => { 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)); return tokens.every(t => hay.includes(t));
}); });
document.getElementById('search-stats').textContent = 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 => ` 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">
@@ -495,11 +573,13 @@ function doSearch() {
</div> </div>
<div class="check-icon"></div> <div class="check-icon"></div>
</div> </div>
<div class="card-section-label">Ключови думи</div>
<div class="card-tags"> <div class="card-tags">
${(r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`<span class="tag">${esc(k.trim())}</span>`).join('')} ${(r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`<span class="tag">${esc(k.trim())}</span>`).join('') || '<span class="stats">—</span>'}
</div> </div>
<div class="card-text">${r.text_html || esc(r.text||'(няма preview)')}</div> <div class="card-section-label">Извлечение</div>
<div class="card-footer">${esc(shortPath(r.source_file))} &nbsp;·&nbsp; ${r.char_count} знака</div> <div class="card-text">${r.text_html || esc(r.text||'(няма текст — провери OUTPUT_DIR)')}</div>
<div class="card-footer">${esc(shortPath(r.source_file))} &nbsp;·&nbsp; ${r.char_count||0} знака</div>
</div> </div>
`).join(''); `).join('');
} }