257 lines
9.4 KiB
HTML
257 lines
9.4 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🏔 База знаний: Сноубайки</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: #0f1117; color: #e0e0e0; min-height: 100vh; }
|
||
|
||
.header { background: linear-gradient(135deg, #1a2a4a, #2d4a7a);
|
||
padding: 20px 24px; border-bottom: 1px solid #2a3a5a; }
|
||
.header h1 { font-size: 1.5rem; color: #fff; }
|
||
.header .meta { font-size: 0.8rem; color: #8a9ab0; margin-top: 4px; }
|
||
|
||
.layout { display: flex; height: calc(100vh - 70px); }
|
||
|
||
.sidebar { width: 260px; min-width: 260px; background: #161b2e;
|
||
border-right: 1px solid #2a3a5a; overflow-y: auto;
|
||
display: flex; flex-direction: column; }
|
||
|
||
.search-box { padding: 12px; border-bottom: 1px solid #2a3a5a; }
|
||
.search-box input { width: 100%; background: #0f1117; border: 1px solid #2a3a5a;
|
||
color: #e0e0e0; padding: 8px 12px; border-radius: 6px;
|
||
font-size: 0.85rem; outline: none; }
|
||
.search-box input:focus { border-color: #4a7aff; }
|
||
|
||
.nav { padding: 8px 0; flex: 1; }
|
||
.nav-item { padding: 8px 16px; cursor: pointer; font-size: 0.85rem;
|
||
color: #8a9ab0; border-left: 3px solid transparent;
|
||
transition: all 0.15s; white-space: nowrap; overflow: hidden;
|
||
text-overflow: ellipsis; }
|
||
.nav-item:hover { background: #1f2940; color: #c0d0e0; }
|
||
.nav-item.active { background: #1f2940; color: #4a9aff;
|
||
border-left-color: #4a9aff; }
|
||
|
||
.content { flex: 1; overflow-y: auto; padding: 32px 40px; max-width: 900px; }
|
||
|
||
.status-box { background: #1a2a1a; border: 1px solid #2a4a2a;
|
||
border-radius: 8px; padding: 16px 20px; margin-bottom: 24px; }
|
||
.status-box.loading { background: #1a1a2a; border-color: #2a2a4a; }
|
||
.status-box.error { background: #2a1a1a; border-color: #4a2a2a; }
|
||
|
||
/* Markdown стили */
|
||
.md h1 { font-size: 1.8rem; color: #fff; margin: 0 0 24px; border-bottom: 1px solid #2a3a5a; padding-bottom: 12px; }
|
||
.md h2 { font-size: 1.3rem; color: #6ab0ff; margin: 32px 0 12px; }
|
||
.md h3 { font-size: 1.1rem; color: #a0c0e0; margin: 20px 0 8px; }
|
||
.md p { line-height: 1.7; margin-bottom: 12px; color: #c0d0e0; }
|
||
.md ul, .md ol { margin: 8px 0 12px 20px; }
|
||
.md li { line-height: 1.7; color: #c0d0e0; margin-bottom: 4px; }
|
||
.md strong { color: #fff; }
|
||
.md em { color: #a0b0c0; }
|
||
.md code { background: #1f2940; padding: 2px 6px; border-radius: 4px;
|
||
font-family: monospace; font-size: 0.85em; color: #7ab0ff; }
|
||
.md blockquote { border-left: 3px solid #4a7aff; padding: 8px 16px;
|
||
background: #161b2e; margin: 12px 0; color: #a0b0c0; }
|
||
.md table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
||
.md th { background: #1f2940; padding: 8px 12px; text-align: left;
|
||
color: #8ab0d0; font-size: 0.85rem; }
|
||
.md td { padding: 8px 12px; border-bottom: 1px solid #1f2940; font-size: 0.9rem; }
|
||
.md tr:hover td { background: #161b2e; }
|
||
|
||
.highlight { background: #3a4a1a; border-radius: 2px; }
|
||
|
||
#search-results { padding: 0 8px; }
|
||
.search-result { padding: 10px 8px; border-bottom: 1px solid #2a3a5a;
|
||
cursor: pointer; font-size: 0.82rem; color: #8a9ab0; }
|
||
.search-result:hover { background: #1f2940; color: #c0d0e0; }
|
||
.search-result .sr-title { color: #6ab0ff; font-weight: 600; margin-bottom: 4px; }
|
||
|
||
::-webkit-scrollbar { width: 6px; }
|
||
::-webkit-scrollbar-track { background: #0f1117; }
|
||
::-webkit-scrollbar-thumb { background: #2a3a5a; border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<h1>🏔 База знаний: Сноубайки</h1>
|
||
<div class="meta" id="meta-info">Загрузка...</div>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
<div class="sidebar">
|
||
<div class="search-box">
|
||
<input type="text" id="search-input" placeholder="🔍 Поиск по базе..." oninput="onSearch(this.value)">
|
||
</div>
|
||
<div class="nav" id="nav-items"></div>
|
||
<div id="search-results" style="display:none"></div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div id="content-area">
|
||
<div class="status-box loading">
|
||
⏳ Загрузка базы знаний...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let fullMarkdown = '';
|
||
let sections = [];
|
||
let searchTimeout = null;
|
||
|
||
async function loadKB() {
|
||
try {
|
||
const resp = await fetch('/snowbike/knowledge_base.md?t=' + Date.now());
|
||
if (!resp.ok) {
|
||
if (resp.status === 404) {
|
||
document.getElementById('content-area').innerHTML = `
|
||
<div class="status-box loading">
|
||
⏳ <strong>Анализ ещё выполняется</strong><br><br>
|
||
База знаний генерируется. Страница обновится автоматически.<br>
|
||
<small style="color:#6a8a9a">Обновление через 30 секунд...</small>
|
||
</div>`;
|
||
document.getElementById('meta-info').textContent = 'Анализ выполняется...';
|
||
setTimeout(loadKB, 30000);
|
||
return;
|
||
}
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
fullMarkdown = await resp.text();
|
||
parseAndRender();
|
||
|
||
} catch(e) {
|
||
document.getElementById('content-area').innerHTML = `
|
||
<div class="status-box error">⚠️ Ошибка загрузки: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function parseAndRender() {
|
||
// Парсим разделы по ## заголовкам
|
||
sections = [];
|
||
const lines = fullMarkdown.split('\n');
|
||
let current = null;
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('## ')) {
|
||
if (current) sections.push(current);
|
||
current = { title: line.replace('## ', '').trim(), lines: [line], anchor: slugify(line) };
|
||
} else if (line.startsWith('# ') && !current) {
|
||
sections.push({ title: line.replace('# ', '').trim(), lines: [line], anchor: 'top', isTitle: true });
|
||
} else if (current) {
|
||
current.lines.push(line);
|
||
}
|
||
}
|
||
if (current) sections.push(current);
|
||
|
||
// Мета-инфо из комментария
|
||
const metaMatch = fullMarkdown.match(/<!-- Сгенерировано: (.+?) -->/);
|
||
const factsMatch = fullMarkdown.match(/<!-- .+?(\d+) фактов/);
|
||
if (metaMatch) {
|
||
document.getElementById('meta-info').textContent =
|
||
`Сгенерировано: ${metaMatch[1]} · ${factsMatch ? factsMatch[1] + ' фактов' : ''}`;
|
||
}
|
||
|
||
// Навигация
|
||
const nav = document.getElementById('nav-items');
|
||
nav.innerHTML = '';
|
||
sections.forEach((s, i) => {
|
||
if (s.isTitle) return;
|
||
const el = document.createElement('div');
|
||
el.className = 'nav-item' + (i === 1 ? ' active' : '');
|
||
el.textContent = s.title;
|
||
el.onclick = () => showSection(i);
|
||
nav.appendChild(el);
|
||
});
|
||
|
||
// Показываем первый раздел
|
||
showSection(1);
|
||
}
|
||
|
||
function showSection(idx) {
|
||
const section = sections[idx];
|
||
if (!section) return;
|
||
|
||
// Активный пункт меню
|
||
document.querySelectorAll('.nav-item').forEach((el, i) => {
|
||
el.classList.toggle('active', i === idx - 1);
|
||
});
|
||
|
||
const html = marked.parse(section.lines.join('\n'));
|
||
document.getElementById('content-area').innerHTML = `<div class="md">${html}</div>`;
|
||
document.getElementById('search-results').style.display = 'none';
|
||
document.getElementById('nav-items').style.display = '';
|
||
}
|
||
|
||
function slugify(text) {
|
||
return text.toLowerCase().replace(/[^a-zа-я0-9]+/g, '-');
|
||
}
|
||
|
||
function onSearch(query) {
|
||
clearTimeout(searchTimeout);
|
||
if (!query.trim()) {
|
||
document.getElementById('search-results').style.display = 'none';
|
||
document.getElementById('nav-items').style.display = '';
|
||
return;
|
||
}
|
||
searchTimeout = setTimeout(() => doSearch(query), 200);
|
||
}
|
||
|
||
function doSearch(query) {
|
||
const q = query.toLowerCase();
|
||
const results = [];
|
||
|
||
for (const section of sections) {
|
||
if (section.isTitle) continue;
|
||
const text = section.lines.join('\n').toLowerCase();
|
||
if (!text.includes(q)) continue;
|
||
|
||
// Находим контекст вокруг совпадения
|
||
const lines = section.lines;
|
||
const matches = [];
|
||
for (const line of lines) {
|
||
if (line.toLowerCase().includes(q) && line.trim() && !line.startsWith('#')) {
|
||
const excerpt = line.trim().replace(new RegExp(query, 'gi'), m => `<mark>${m}</mark>`);
|
||
matches.push(excerpt);
|
||
if (matches.length >= 3) break;
|
||
}
|
||
}
|
||
if (matches.length) results.push({ title: section.title, matches });
|
||
}
|
||
|
||
const nav = document.getElementById('nav-items');
|
||
const sr = document.getElementById('search-results');
|
||
|
||
if (results.length === 0) {
|
||
sr.innerHTML = '<div class="search-result" style="color:#6a8a9a">Ничего не найдено</div>';
|
||
} else {
|
||
sr.innerHTML = results.map(r => `
|
||
<div class="search-result" onclick="showSectionByTitle('${r.title}')">
|
||
<div class="sr-title">${r.title}</div>
|
||
${r.matches.map(m => `<div>${m}</div>`).join('')}
|
||
</div>`).join('');
|
||
}
|
||
|
||
nav.style.display = 'none';
|
||
sr.style.display = '';
|
||
|
||
// Показываем первый результат
|
||
if (results.length > 0) showSectionByTitle(results[0].title);
|
||
}
|
||
|
||
function showSectionByTitle(title) {
|
||
const idx = sections.findIndex(s => s.title === title);
|
||
if (idx >= 0) showSection(idx);
|
||
}
|
||
|
||
loadKB();
|
||
</script>
|
||
</body>
|
||
</html>
|