Files
wiki/tasks/snowbike-kb/viewer/index.html
2026-04-12 21:55:33 +03:00

257 lines
9.4 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="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>