Files
rip-help-system/webapp/templates/viewer.html

750 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="bg">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RIP Help Viewer{% if prefix %} — {{ prefix }}{% endif %}</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f6f8;
--bg2: #ffffff;
--bg3: #eef0f4;
--border: #d8dce3;
--accent: #3a6fcf;
--accent2: #2ca36f;
--text: #1a1a1f;
--muted: #6a6a78;
--danger: #c84545;
--tag-bg: #e7efff;
--tag-text: #2c5cb8;
--mono: 'IBM Plex Mono', monospace;
--sans: 'IBM Plex Sans', sans-serif;
--radius: 6px;
--radius-lg: 12px;
}
body { background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 14px; min-height: 100vh; }
header {
display: flex; align-items: center; gap: 16px; padding: 14px 24px;
border-bottom: 1px solid #1f3a6f;
background: linear-gradient(90deg, #2d5fb0 0%, #3a6fcf 55%, #5589dd 100%);
color: #ffffff; box-shadow: 0 2px 6px rgba(0,0,0,.10);
position: sticky; top: 0; z-index: 100;
}
header h1 {
font-family: var(--mono); font-size: 16px; font-weight: 600; color: #ffffff;
letter-spacing: .05em; padding: 4px 12px;
background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.28);
border-radius: var(--radius);
}
header .sep { color: rgba(255,255,255,.35); }
header .pfx { font-family: var(--mono); font-size: 12px; color: rgba(255,255,255,.85); }
.gen-time { font-size: 11px; color: rgba(255,255,255,.80); margin-left: auto; font-family: var(--mono); }
.tabs { display: flex; gap: 2px; padding: 0 24px; background: var(--bg2); border-bottom: 1px solid var(--border); }
.tab {
padding: 10px 20px; font-size: 13px; font-family: var(--mono); color: var(--muted);
cursor: pointer; border-bottom: 2px solid transparent;
transition: color .15s, border-color .15s; user-select: none;
}
.tab.active { color: var(--accent); border-color: var(--accent); }
.tab:hover:not(.active) { color: var(--text); }
.panel { display: none; padding: 20px 24px; }
.panel.active { display: block; }
.toolbar { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; align-items: center; }
input[type=text], textarea {
background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius);
color: var(--text); font-family: var(--sans); font-size: 13px; padding: 7px 12px;
outline: none; transition: border-color .15s;
}
input[type=text]:focus, textarea:focus { border-color: var(--accent); }
.search-box { flex: 1; min-width: 220px; }
button {
padding: 7px 16px; border-radius: var(--radius); border: none;
font-family: var(--mono); font-size: 12px; cursor: pointer; transition: opacity .15s;
}
button:hover { opacity: .85; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-success { background: var(--accent2); color: #fff; }
.btn-danger { background: var(--danger); color: #fff; }
.btn-ghost { background: var(--bg3); color: var(--text); border: 1px solid var(--border); }
.stats { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.tbl-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); }
table { width: 100%; border-collapse: collapse; }
thead th {
background: var(--bg3); padding: 10px 12px; text-align: left;
font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--muted);
letter-spacing: .06em; text-transform: uppercase;
border-bottom: 1px solid var(--border); white-space: nowrap;
}
tbody tr { border-bottom: 1px solid var(--border); transition: background .1s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--bg3); }
td { padding: 8px 12px; vertical-align: top; }
.code-badge {
font-family: var(--mono); font-size: 11px; color: var(--accent);
background: rgba(58,111,207,.10); padding: 2px 7px; border-radius: 4px; white-space: nowrap;
}
.kw-cell { display: flex; flex-direction: column; gap: 6px; min-width: 280px; }
.kw-tags { display: flex; flex-wrap: wrap; gap: 4px; min-height: 18px; }
.tag {
font-family: var(--mono); font-size: 11px;
background: var(--tag-bg); color: var(--tag-text);
padding: 2px 8px; border-radius: 20px; white-space: nowrap;
}
.kw-edit-row { display: flex; gap: 6px; align-items: center; }
.kw-input {
flex: 1; min-width: 180px;
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
color: var(--text); padding: 5px 10px; font-size: 12px; font-family: var(--sans);
}
.kw-input:focus {
border-color: var(--accent); background: var(--bg2);
box-shadow: 0 0 0 2px rgba(58,111,207,.15); outline: none;
}
.kw-input.changed { border-color: var(--accent2); background: rgba(44,163,111,.05); }
.kw-input.saving { border-color: var(--accent); background: rgba(58,111,207,.05); }
.kw-input.saved { border-color: var(--accent2); background: rgba(44,163,111,.10); }
.kw-input.error { border-color: var(--danger); background: rgba(200,69,69,.05); }
.save-btn {
padding: 5px 12px; font-size: 12px;
background: var(--accent2); color: #fff;
border-radius: var(--radius); border: none; cursor: pointer;
display: none; font-family: var(--mono);
}
.save-btn.visible { display: inline-block; }
.save-btn:hover { opacity: .9; }
.src-file { font-size: 11px; color: var(--muted); font-family: var(--mono); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.title-cell { max-width: 220px; }
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
.card {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
padding: 14px 16px; transition: border-color .15s; cursor: pointer;
}
.card:hover { border-color: var(--accent); }
.card.selected { border-color: var(--accent2); background: rgba(44,163,111,.10); }
.card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
.card-title { font-weight: 500; font-size: 13px; line-height: 1.4; }
.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;
}
.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); }
.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); }
.gen-layout { display: grid; grid-template-columns: 1fr 280px; gap: 20px; }
.selected-list { display: flex; flex-direction: column; gap: 8px; }
.sel-item {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
padding: 10px 14px; display: flex; align-items: center; gap: 10px; cursor: grab;
}
.sel-item:active { cursor: grabbing; }
.sel-item.drag-over { border-color: var(--accent); background: rgba(58,111,207,.10); }
.drag-handle { color: var(--muted); font-size: 16px; user-select: none; }
.sel-item-info { flex: 1; }
.sel-item-title { font-size: 13px; font-weight: 500; }
.sel-item-code { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.remove-btn { background: none; border: none; color: var(--muted); font-size: 16px; cursor: pointer; padding: 0 4px; }
.remove-btn:hover { color: var(--danger); }
.gen-panel {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
padding: 20px; position: sticky; top: 80px;
}
.gen-panel h3 { font-family: var(--mono); font-size: 13px; color: var(--muted); margin-bottom: 16px; letter-spacing: .06em; }
.format-btns { display: flex; flex-direction: column; gap: 8px; }
.format-btn {
padding: 10px 16px; border-radius: var(--radius); border: 1px solid var(--border);
background: var(--bg3); color: var(--text); font-family: var(--mono); font-size: 12px;
cursor: pointer; text-align: left; transition: all .15s;
display: flex; align-items: center; gap: 10px;
}
.format-btn:hover { border-color: var(--accent); color: var(--accent); }
.format-dot { width: 8px; height: 8px; border-radius: 50%; }
.dot-word { background: #2b7fd4; }
.dot-html { background: #e0854a; }
.dot-pdf { background: #e05c5c; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
.empty-state .big { font-size: 40px; margin-bottom: 12px; }
#toast {
position: fixed; bottom: 24px; right: 24px;
background: var(--accent2); color: #fff;
padding: 10px 20px; border-radius: var(--radius);
font-family: var(--mono); font-size: 13px;
opacity: 0; transform: translateY(10px);
transition: all .3s; pointer-events: none; z-index: 999;
}
#toast.show { opacity: 1; transform: translateY(0); }
#toast.error { background: var(--danger); }
.sep { color: var(--border); margin: 0 4px; }
.home-wrap { display: flex; flex-direction: column; align-items: center; gap: 20px; padding: 24px; }
.home-wrap img {
max-width: min(920px, 100%); height: auto; border-radius: var(--radius-lg);
border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,.08);
}
.rescan-box {
width: min(640px, 100%);
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
padding: 16px 18px;
}
.rescan-box h3 {
font-family: var(--mono); font-size: 12px; color: var(--muted);
letter-spacing: .06em; text-transform: uppercase; margin-bottom: 12px;
}
.rescan-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; }
.rescan-box input[type=file] { font-size: 12px; }
.rescan-status { font-family: var(--mono); font-size: 12px; color: var(--muted); min-height: 1.2em; }
</style>
</head>
<body>
<header>
<h1>BG16RFPR001-1.001-0068</h1>
<span class="sep">|</span>
<span class="pfx" id="total-count"></span>
{% if prefix %}<span class="sep">|</span><span class="pfx">prefix={{ prefix }}</span>{% endif %}
<span class="gen-time">генериран: {{ generated }}</span>
</header>
<div class="tabs">
{% if home_url %}<div class="tab active" onclick="switchTab('home')">00 / Home</div>{% endif %}
<div class="tab {% if not home_url %}active{% endif %}" onclick="switchTab('editor')">01 / Редактор</div>
<div class="tab" onclick="switchTab('search')">02 / Търсене</div>
<div class="tab" onclick="switchTab('generator')">03 / Генератор</div>
</div>
{% if home_url %}
<div id="tab-home" class="panel active">
<div class="home-wrap">
<img src="{{ home_url }}" alt="Home">
<div class="rescan-box">
<h3>Rescan — качи help файлове</h3>
<div class="rescan-row">
<input type="file" id="rescan-file" accept=".zip,.docx,.doc,.html,.htm,.pdf,.txt">
<label class="stats"><input type="checkbox" id="rescan-force"> force</label>
<button class="btn-primary" id="rescan-btn" onclick="startRescan()">Старт</button>
</div>
<div class="rescan-status" id="rescan-status">ZIP или един файл → обработка на сървъра → БД + Output</div>
</div>
</div>
</div>
{% endif %}
<div id="tab-editor" class="panel {% if not home_url %}active{% endif %}">
<div class="toolbar">
<input type="text" class="search-box" id="editor-search" placeholder="Филтрирай по код, заглавие, ключова дума..." oninput="filterEditor()">
<span class="stats" id="editor-stats"></span>
</div>
<div class="tbl-wrap">
<table id="editor-table">
<thead>
<tr>
<th>Код</th>
<th>Заглавие</th>
<th>Ключови думи</th>
<th>Source файл</th>
<th>Обновен</th>
</tr>
</thead>
<tbody id="editor-body"></tbody>
</table>
</div>
</div>
<div id="tab-search" class="panel">
<div class="toolbar">
<input type="text" class="search-box" id="search-input"
placeholder='Търси в извлечения и ключови думи; фраза в "кавички"'
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>
<button class="btn-primary" onclick="addSelectedToGenerator()">Добави избраните → Генератор</button>
</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>
<div id="tab-generator" class="panel">
<div class="gen-layout">
<div>
<div class="toolbar">
<span class="stats" id="gen-stats">Няма избрани секции</span>
<button class="btn-ghost" onclick="clearGenerator()">Изчисти</button>
</div>
<div class="selected-list" id="selected-list">
<div class="empty-state">
<div class="big">⬡</div>
<div>Избери секции от таб Търсене</div>
</div>
</div>
</div>
<div class="gen-panel">
<h3>ГЕНЕРИРАЙ ДОКУМЕНТ</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
</button>
</div>
</div>
</div>
</div>
<div id="toast"></div>
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
<script>
let ALL = [];
try {
ALL = JSON.parse(document.getElementById('sections-data').textContent || '[]');
} catch (e) {
console.error('sections JSON parse failed', e);
ALL = [];
}
function esc(s) {
return String(s ?? '')
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function shortPath(p) {
if (!p) return '';
const parts = String(p).replace(/\\/g,'/').split('/');
return parts.slice(-2).join('/');
}
function toast(msg, isErr) {
const el = document.getElementById('toast');
if (!el) return;
el.textContent = msg;
el.classList.toggle('error', !!isErr);
el.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(() => el.classList.remove('show'), 2800);
}
function switchTab(name) {
const order = [{% if home_url %}'home',{% endif %}'editor','search','generator'];
document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', order[i] === name));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById('tab-' + name);
if (panel) panel.classList.add('active');
if (name === 'generator') renderGenerator();
if (name === 'search') doSearch();
}
async function startRescan() {
const inp = document.getElementById('rescan-file');
const st = document.getElementById('rescan-status');
const btn = document.getElementById('rescan-btn');
if (!inp.files || !inp.files[0]) {
st.textContent = 'Избери файл първо.';
return;
}
const fd = new FormData();
fd.append('file', inp.files[0]);
fd.append('prefix', {{ (prefix or 'RIP') | tojson }});
fd.append('force', document.getElementById('rescan-force').checked ? 'true' : 'false');
const token = localStorage.getItem('RESCAN_TOKEN') || '';
if (token) fd.append('token', token);
btn.disabled = true;
st.textContent = 'Качване...';
try {
const headers = {};
if (token) headers['X-Rescan-Token'] = token;
const res = await fetch('/api/rescan', { method: 'POST', body: fd, headers });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || ('HTTP ' + res.status));
st.textContent = 'Job ' + data.job_id + ' — обработка...';
await pollRescan(data.job_id);
} catch (e) {
st.textContent = 'Грешка: ' + e.message;
} finally {
btn.disabled = false;
}
}
async function pollRescan(jobId) {
const st = document.getElementById('rescan-status');
for (let i = 0; i < 180; i++) {
await new Promise(r => setTimeout(r, 2000));
const res = await fetch('/api/rescan/' + encodeURIComponent(jobId));
const data = await res.json();
st.textContent = data.status + (data.message ? ' — ' + data.message : '');
if (data.status === 'done') {
st.textContent = 'Готово. Презареждане...';
location.reload();
return;
}
if (data.status === 'error') {
st.textContent = 'Грешка: ' + (data.message || 'unknown');
return;
}
}
st.textContent = 'Timeout — провери /api/rescan/' + jobId;
}
// ── EDITOR ────────────────────────────
function renderEditor(rows) {
const tbody = document.getElementById('editor-body');
tbody.innerHTML = rows.map(r => `
<tr>
<td><span class="code-badge">${r.code}</span></td>
<td class="title-cell">${esc(r.title)}</td>
<td>
<div class="kw-cell">
<div class="kw-tags" id="tags-${r.code}">
${(r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`<span class="tag">${esc(k.trim())}</span>`).join('')}
</div>
<div class="kw-edit-row">
<input class="kw-input" data-code="${r.code}" value="${esc(r.keywords||'')}"
placeholder="ключови думи, разделени със запетая"
oninput="onKwChange(this)"
onkeydown="if(event.key==='Enter'){saveOne(this);event.preventDefault();}">
<button class="save-btn" id="sb-${r.code}"
onclick="saveOne(document.querySelector(\`[data-code='${r.code}']\`))">✓ Запази</button>
</div>
</div>
</td>
<td><span class="src-file" title="${esc(r.source_file)}">${esc(shortPath(r.source_file))}</span></td>
<td style="font-size:11px;color:var(--muted);font-family:var(--mono);white-space:nowrap">${r.updated_at}</td>
</tr>
`).join('');
document.getElementById('editor-stats').textContent = rows.length + ' реда';
}
function filterEditor() {
const q = document.getElementById('editor-search').value.toLowerCase();
const filtered = q ? ALL.filter(r =>
(r.code||'').toLowerCase().includes(q) ||
(r.title||'').toLowerCase().includes(q) ||
(r.keywords||'').toLowerCase().includes(q) ||
(r.source_file||'').toLowerCase().includes(q)
) : ALL;
renderEditor(filtered);
}
function onKwChange(inp) {
const code = inp.dataset.code;
inp.classList.add('changed');
inp.classList.remove('saved', 'error');
document.getElementById('sb-' + code).classList.add('visible');
const tagsHost = document.getElementById('tags-' + code);
if (tagsHost) {
tagsHost.innerHTML = inp.value.split(',')
.filter(k => k.trim())
.map(k => `<span class="tag">${esc(k.trim())}</span>`)
.join('');
}
}
async function saveOne(inp) {
const code = inp.dataset.code;
const keywords = inp.value;
const btn = document.getElementById('sb-' + code);
inp.classList.remove('changed', 'saved', 'error');
inp.classList.add('saving');
try {
const res = await fetch(`/api/keywords/${encodeURIComponent(code)}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({keywords})
});
if (!res.ok) throw new Error('HTTP ' + res.status);
inp.classList.remove('saving');
inp.classList.add('saved');
if (btn) btn.classList.remove('visible');
const row = ALL.find(r => r.code === code);
if (row) row.keywords = keywords;
toast(`Запазено: ${code}`);
setTimeout(() => inp.classList.remove('saved'), 1500);
} catch (e) {
inp.classList.remove('saving');
inp.classList.add('error');
toast(`Грешка: ${e.message}`, true);
}
}
// ── SEARCH ────────────────────────────
const selected = new Set();
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) {
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);
}
return tokens;
}
function searchMode() {
const el = document.querySelector('input[name="search-mode"]:checked');
return el ? el.value : 'all';
}
function doSearch() {
const q = document.getElementById('search-input').value.trim().toLowerCase();
const tokens = q ? tokenizeQuery(q) : [];
const mode = searchMode();
const results = tokens.length === 0 ? ALL : ALL.filter(r => {
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));
});
document.getElementById('search-stats').textContent =
results.length + ' извлечения' + (tokens.length ? ' · филтър: ' + tokens.length + ' термина' : '');
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">
<div>
<div class="code-badge" style="margin-bottom:6px">${r.code}</div>
<div class="card-title">${esc(r.title)}</div>
</div>
<div class="check-icon"></div>
</div>
<div class="card-section-label">Ключови думи</div>
<div class="card-tags">
${(r.keywords||'').split(',').filter(k=>k.trim()).map(k=>`<span class="tag">${esc(k.trim())}</span>`).join('') || '<span class="stats">—</span>'}
</div>
<div class="card-section-label">Извлечение</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>
`).join('');
}
function toggleSelect(code, el) {
if (selected.has(code)) {
selected.delete(code);
el.classList.remove('selected');
genOrder = genOrder.filter(c => c !== code);
} else {
selected.add(code);
el.classList.add('selected');
genOrder.push(code);
}
}
function addSelectedToGenerator() {
if (selected.size === 0) { toast('Избери поне една секция'); return; }
switchTab('generator');
}
// ── GENERATOR ─────────────────────────
function renderGenerator() {
const list = document.getElementById('selected-list');
const stats = document.getElementById('gen-stats');
if (genOrder.length === 0) {
list.innerHTML = '<div class="empty-state"><div class="big">⬡</div><div>Избери секции от таб Търсене</div></div>';
stats.textContent = 'Няма избрани секции';
return;
}
stats.textContent = genOrder.length + ' секции избрани';
list.innerHTML = genOrder.map((code, idx) => {
const r = ALL.find(x => x.code === code);
if (!r) return '';
return `
<div class="sel-item" draggable="true" data-idx="${idx}"
ondragstart="dragStart(event,${idx})"
ondragover="dragOver(event,${idx})"
ondrop="dragDrop(event,${idx})"
ondragleave="this.classList.remove('drag-over')">
<span class="drag-handle">⠿</span>
<div class="sel-item-info">
<div class="sel-item-title">${esc(r.title)}</div>
<div class="sel-item-code">${code}</div>
</div>
<button class="remove-btn" onclick="removeFromGen('${code}')">✕</button>
</div>`;
}).join('');
}
function removeFromGen(code) {
selected.delete(code);
genOrder = genOrder.filter(c => c !== code);
renderGenerator();
}
function clearGenerator() { selected.clear(); genOrder = []; renderGenerator(); }
let dragIdx = null;
function dragStart(e, idx) { dragIdx = idx; e.dataTransfer.effectAllowed = 'move'; }
function dragOver(e, idx) { e.preventDefault(); e.currentTarget.classList.add('drag-over'); }
function dragDrop(e, idx) {
e.preventDefault();
e.currentTarget.classList.remove('drag-over');
if (dragIdx === null || dragIdx === idx) return;
const moved = genOrder.splice(dragIdx, 1)[0];
genOrder.splice(idx, 0, moved);
dragIdx = null;
renderGenerator();
}
function generateDoc(fmt) {
if (genOrder.length === 0) { toast('Избери секции първо'); return; }
const sections = genOrder.map(code => ALL.find(r => r.code === code)).filter(Boolean);
if (fmt === 'html') {
const html = buildHtmlDoc(sections);
const blob = new Blob([html], {type: 'text/html;charset=utf-8'});
const url = URL.createObjectURL(blob);
const win = window.open(url, '_blank');
if (!win) {
toast('Браузърът блокира new tab — позволи pop-ups', true);
download('help_document.html', html, 'text/html');
} else {
toast('HTML документът е отворен в нов tab');
}
return;
}
toast('Word / PDF — TODO', true);
}
function buildHtmlDoc(sections) {
// Картинките са relative URLs — за самостоятелен файл клиента ги embed-ва не може
// лесно. За първа итерация: оставяме URLs както са (същият домейн ги сервира).
const body = sections.map(s => `
<section>
<h2>${esc(s.title)}</h2>
<p class="meta">${esc(s.code)} &nbsp;·&nbsp; ${esc(s.keywords||'')}</p>
<div class="content">${s.text_html || esc(s.text||'').replace(/\n/g,'<br>')}</div>
</section>
`).join('<hr>');
// Превръщаме относителни /images/... в абсолютни (HOST + path) за работа в нов tab
const base = location.protocol + '//' + location.host;
const bodyAbs = body.replace(/src="\/images\//g, `src="${base}/images/`);
return `<!DOCTYPE html><html lang="bg"><head><meta charset="UTF-8">
<title>Help документ</title>
<style>
body{font-family:Georgia,serif;max-width:860px;margin:40px auto;padding:0 20px;color:#222;line-height:1.7}
h1{font-size:24px;border-bottom:2px solid #333;padding-bottom:10px}
h2{font-size:18px;margin-top:0;color:#1a1a2e}
.meta{font-size:11px;color:#888;font-family:monospace;margin-bottom:12px}
.content{font-size:14px}
hr{border:none;border-top:1px solid #e0e0e0;margin:32px 0}
section{margin-bottom:8px}
img{max-width:100%;border:1px solid #eee;border-radius:4px;margin:8px 0}
</style></head><body>
<h1>Help документ</h1>
<p style="font-size:12px;color:#888">Генериран: ${new Date().toLocaleString('bg-BG')}</p>
<hr>
${bodyAbs}
</body></html>`;
}
function download(filename, content, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([content], {type: mime}));
a.download = filename;
a.click();
}
// ── INIT ─────────────────────────────
document.getElementById('total-count').textContent = ALL.length + ' секции';
try {
renderEditor(ALL);
renderKwCloud();
doSearch();
renderGenerator();
} catch (e) {
console.error('init failed', e);
toast('Грешка при зареждане на UI: ' + e.message, true);
}
</script>
</body>
</html>