Files
llm/tools/management-console/index.html
T

867 lines
46 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">
<title>LLM Control Console</title>
<style>
:root {
--bg: #eef0ed;
--surface: #fbfbf7;
--surface-2: #e1e6e1;
--ink: #202522;
--muted: #66706a;
--line: #cbd3cc;
--accent: #0f6b5f;
--accent-2: #8a5a12;
--ok: #146c43;
--warn: #9a5b00;
--bad: #ad2f2b;
--info: #315f85;
--radius: 8px;
--mono: "Cascadia Mono", "JetBrains Mono", Consolas, monospace;
--sans: "Aptos", "Segoe UI", Tahoma, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
letter-spacing: 0;
}
button, input, select { font: inherit; }
button {
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
min-height: 34px;
border-radius: 6px;
cursor: pointer;
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
}
button:hover { border-color: #8b978e; }
button:active { transform: translateY(1px); }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
button.warn { background: #f7ead3; border-color: #d2a456; color: #4f360d; }
input, select {
width: 100%;
min-height: 34px;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
color: var(--ink);
padding: 7px 9px;
}
.app {
min-height: 100vh;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
}
aside {
background: #dfe5df;
border-right: 1px solid var(--line);
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
}
.brand h1 { margin: 0; font-size: 18px; line-height: 1.1; }
.brand p { margin: 6px 0 0; color: var(--muted); font-size: 12px; line-height: 1.35; }
nav { display: grid; gap: 6px; }
nav button {
text-align: left;
padding: 8px 10px;
background: transparent;
}
nav button.active { background: var(--surface); border-color: #9da99f; font-weight: 700; }
.side-block {
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgba(251, 251, 247, 0.55);
padding: 10px;
display: grid;
gap: 8px;
font-size: 12px;
color: var(--muted);
}
main {
padding: 16px;
display: grid;
gap: 14px;
align-content: start;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--line);
padding-bottom: 12px;
}
.topbar h2 { margin: 0; font-size: 20px; }
.topbar .meta { color: var(--muted); font-size: 12px; font-family: var(--mono); overflow-wrap: anywhere; }
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
min-width: 0;
}
.panel-head {
min-height: 42px;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.panel-head h3 { margin: 0; font-size: 14px; }
.panel-body { padding: 12px; }
.metric {
display: grid;
gap: 4px;
min-height: 92px;
}
.metric .value { font-size: 28px; font-weight: 780; line-height: 1; }
.metric .label { color: var(--muted); font-size: 12px; }
.status {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 64px;
height: 24px;
padding: 0 8px;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12px;
color: var(--muted);
background: #fff;
white-space: nowrap;
}
.status.ok { color: var(--ok); border-color: rgba(20, 108, 67, .34); }
.status.failed, .status.error, .status.timeout { color: var(--bad); border-color: rgba(173, 47, 43, .34); }
.status.missing { color: var(--bad); border-color: rgba(173, 47, 43, .34); }
.status.warning, .status.stale { color: var(--warn); border-color: rgba(154, 91, 0, .34); }
.status.running, .status.queued { color: var(--info); border-color: rgba(49, 95, 133, .34); }
.badge {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
color: var(--muted);
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.badge.docs { color: var(--ok); border-color: rgba(20, 108, 67, .34); }
.badge.example { color: var(--warn); border-color: rgba(154, 91, 0, .38); background: #fff6e7; }
.warning-note {
margin: 0 0 10px;
border-left: 3px solid var(--warn);
background: #fff8eb;
padding: 8px 10px;
color: #5b3b08;
font-size: 13px;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: var(--muted); font-size: 11px; text-transform: uppercase; }
td.mono, .mono { font-family: var(--mono); font-size: 12px; }
.two { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(340px, .85fr); gap: 10px; }
.commands { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.stack {
display: grid;
gap: 10px;
}
.ask-row { display: grid; grid-template-columns: minmax(0, 1fr) 220px 110px; gap: 8px; margin-bottom: 10px; }
.filter-row { display: grid; grid-template-columns: 150px 230px minmax(180px, 1fr) 100px; gap: 8px; margin-bottom: 10px; }
.table-scroll { overflow: auto; max-height: 430px; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
.table-scroll table { min-width: 920px; }
.doc-title { font-weight: 700; max-width: 360px; overflow-wrap: anywhere; }
.doc-url { color: var(--muted); overflow-wrap: anywhere; }
.tiny-btn { min-height: 28px; padding: 3px 8px; }
.command-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
text-align: left;
padding: 8px 10px;
min-height: 42px;
}
.command-btn span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.command-btn small { color: var(--muted); font-size: 11px; }
pre {
margin: 0;
max-height: 360px;
overflow: auto;
background: #17201c;
color: #dfe9e2;
padding: 10px;
border-radius: 6px;
font: 12px/1.45 var(--mono);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.hidden { display: none; }
@media (max-width: 1080px) {
.app { grid-template-columns: 1fr; }
aside { position: static; }
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.two { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
main { padding: 10px; }
.grid, .commands { grid-template-columns: 1fr; }
.filter-row { grid-template-columns: 1fr; }
.topbar { align-items: flex-start; flex-direction: column; }
.toolbar { justify-content: flex-start; }
th:nth-child(4), td:nth-child(4) { display: none; }
}
</style>
</head>
<body>
<div class="app">
<aside>
<section class="brand">
<h1>LLM Control Console</h1>
<p>Модели, RAG, 1C adapter, проверки и переносимые артефакты.</p>
</section>
<nav id="nav">
<button data-view="overview" class="active">Обзор</button>
<button data-view="models">Модели</button>
<button data-view="rag">RAG и ИТС</button>
<button data-view="onec">1С</button>
<button data-view="checks">Проверки</button>
<button data-view="jobs">Журнал</button>
</nav>
<section class="side-block">
<div>Workspace</div>
<div class="mono" id="workspace">...</div>
</section>
</aside>
<main>
<section class="topbar">
<div>
<h2 id="title">Обзор</h2>
<div class="meta" id="generated">...</div>
</div>
<div class="toolbar">
<button id="refreshBtn">Обновить</button>
<button class="primary" data-command="artifact_manifest">Manifest</button>
<button data-command="artifact_check">Check</button>
</div>
</section>
<section id="view-overview" class="view"></section>
<section id="view-models" class="view hidden"></section>
<section id="view-rag" class="view hidden"></section>
<section id="view-onec" class="view hidden"></section>
<section id="view-checks" class="view hidden"></section>
<section id="view-jobs" class="view hidden"></section>
</main>
</div>
<script>
const state = {
data: null,
view: "overview",
selectedJob: null,
officialDocs: null,
officialDocText: null,
officialDocFilters: {status: "all", source_type: "all", q: ""}
};
const fmtBytes = (v) => {
const n = Number(v || 0);
if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB`;
if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${n} B`;
};
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
const status = (s) => `<span class="status ${esc(String(s || 'unknown').toLowerCase())}">${esc(s || "unknown")}</span>`;
const fileMeta = (f) => f?.exists ? `${fmtBytes(f.size_bytes)} · ${new Date(f.mtime).toLocaleString()}` : "нет файла";
const sourceBadge = (scope, type) => {
const t = String(type || "");
if (scope === "metadata" || scope === "all_with_examples" || t === "metadata") return `<span class="badge example">Пример</span>`;
if (t.startsWith("official_1c_its") || scope === "official_1c_docs") return `<span class="badge docs">Документация</span>`;
return `<span class="badge">${esc(t || "Источник")}</span>`;
};
async function api(path, options) {
const res = await fetch(path, options);
const data = await res.json();
if (!res.ok) throw new Error(data.error || res.statusText);
return data;
}
async function refresh() {
state.data = await api("/api/overview");
render();
}
async function runCommand(id) {
const data = await api("/api/run", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({command_id: id})
});
state.selectedJob = data.job.id;
await refresh();
pollJob(data.job.id);
}
async function fetchTargetItsDoc() {
const url = document.getElementById("itsTargetUrl").value.trim();
const target = document.getElementById("itsTargetResult");
if (!url) {
target.innerHTML = `<pre>Введите URL страницы ИТС.</pre>`;
return;
}
target.innerHTML = `<pre>Запускаю прицельную загрузку...</pre>`;
const data = await api("/api/official-docs/fetch-target", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({url, max_pages: 12, max_depth: 2})
});
state.selectedJob = data.job.id;
target.innerHTML = `<pre>Задание создано: ${esc(data.job.id)}</pre>`;
await refresh();
pollJob(data.job.id);
}
async function askRag() {
const question = document.getElementById("ragQuestion").value.trim();
const sourceType = document.getElementById("ragSourceType").value;
const platformVersion = document.getElementById("ragPlatformVersion")?.value.trim() || "";
const platformDocId = document.getElementById("ragPlatformDocId")?.value.trim() || "";
const target = document.getElementById("ragAnswer");
if (!question) {
target.innerHTML = `<pre>Введите вопрос.</pre>`;
return;
}
target.innerHTML = `<pre>Ищу контекст...</pre>`;
const data = await api("/api/rag/query", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({question, source_type: sourceType, platform_version: platformVersion, platform_doc_id: platformDocId, limit: 5})
});
const scopeWarning = ["metadata", "all_with_examples"].includes(data.source_scope)
? `<p class="warning-note">Выбран контур с примерами. Такой результат нельзя использовать как факт текущей базы.</p>`
: "";
target.innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Ответ</h3><span class="status ${data.result_count ? "ok" : "missing"}">${esc(data.result_count)} источн.</span></div>
<div class="panel-body">
${scopeWarning}
<p>${esc(data.answer)}</p>
<table><thead><tr><th>Score</th><th>Источник</th><th>Фрагмент</th></tr></thead><tbody>
${data.results.map(r => `<tr><td>${esc(r.score)}</td><td>${sourceBadge(data.source_scope, r.source_type)}<br>${esc(r.title)}<br><span class="mono">${esc([r.platform_version, r.platform_doc_id, r.doc_book].filter(Boolean).join(" · "))}</span><br><span class="mono">${esc(r.doc_coordinate || "")}</span><br><span class="mono">${esc(r.url || r.source_path)}</span></td><td>${esc(r.snippet)}</td></tr>`).join("")}
</tbody></table>
</div>
</div>`;
}
async function loadOfficialDocs() {
const statusValue = document.getElementById("itsDocStatus")?.value || state.officialDocFilters.status;
const sourceValue = document.getElementById("itsDocSource")?.value || state.officialDocFilters.source_type;
const queryValue = document.getElementById("itsDocQuery")?.value.trim() || "";
state.officialDocFilters = {status: statusValue, source_type: sourceValue, q: queryValue};
const params = new URLSearchParams({status: statusValue, source_type: sourceValue, q: queryValue, limit: "500"});
state.officialDocs = await api(`/api/official-docs/pages?${params.toString()}`);
state.officialDocText = null;
renderOfficialDocs();
}
async function showOfficialDocText(row) {
const params = new URLSearchParams();
if (row.normalized_file) params.set("normalized_file", row.normalized_file);
if (row.raw_file) params.set("raw_file", row.raw_file);
params.set("title", row.title || "");
params.set("source_type", row.source_type || "");
state.officialDocText = await api(`/api/official-docs/page-text?${params.toString()}`);
renderOfficialDocs();
}
function renderOfficialDocs() {
const target = document.getElementById("itsDocsTable");
if (!target) return;
const data = state.officialDocs;
if (!data) {
target.innerHTML = `
<div class="filter-row">
<select id="itsDocStatus">
<option value="all">Все статусы</option>
<option value="passed">Прошли</option>
<option value="skipped">Пропущены</option>
</select>
<select id="itsDocSource"><option value="all">Все источники</option></select>
<input id="itsDocQuery" value="" placeholder="Поиск по заголовку, URL, причине">
<button id="itsDocApply" class="primary">Показать</button>
</div>
<pre>Список страниц еще не загружен.</pre>`;
document.getElementById("itsDocApply").onclick = () => loadOfficialDocs().catch(err => {
target.innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
return;
}
const rows = data.rows || [];
const sourceOptions = ["all", ...(data.source_types || [])]
.map(value => `<option value="${esc(value)}" ${state.officialDocFilters.source_type === value ? "selected" : ""}>${esc(value === "all" ? "Все источники" : value)}</option>`)
.join("");
const table = rows.length ? `
<div class="table-scroll"><table><thead><tr><th>Статус</th><th>Страница</th><th>Причина</th><th>Качество</th><th></th></tr></thead><tbody>
${rows.map((row, index) => {
const q = row.quality || {};
return `<tr>
<td>${status(row.status === "passed" ? "ok" : "skipped")}<br><span class="mono">${esc(row.source_id || "")}</span></td>
<td><div class="doc-title">${esc(row.title || "")}</div><div class="doc-url">${esc(row.url || "")}</div><div class="mono">${esc(row.normalized_file || row.raw_file || "")}</div></td>
<td>${esc(row.skip_reason || "прошла нормализацию")}</td>
<td class="mono">строк ${esc(q.content_lines ?? "")}<br>текст ${esc(q.prose_lines ?? "")}<br>слов ${esc(q.word_count ?? "")}</td>
<td><button class="tiny-btn" data-its-doc-index="${index}">Текст</button></td>
</tr>`;
}).join("")}
</tbody></table></div>` : `<pre>По фильтру страниц нет.</pre>`;
const detail = state.officialDocText
? `<div class="panel" style="margin-top:10px"><div class="panel-head"><h3>Просмотр текста</h3><span class="badge">${esc(state.officialDocText.kind)}</span></div><div class="panel-body"><div class="mono" style="margin-bottom:8px">${esc(state.officialDocText.path)}</div><pre>${esc(state.officialDocText.text || "")}</pre></div></div>`
: "";
target.innerHTML = `
<div class="filter-row">
<select id="itsDocStatus">
<option value="all" ${state.officialDocFilters.status === "all" ? "selected" : ""}>Все статусы</option>
<option value="passed" ${state.officialDocFilters.status === "passed" ? "selected" : ""}>Прошли</option>
<option value="skipped" ${state.officialDocFilters.status === "skipped" ? "selected" : ""}>Пропущены</option>
</select>
<select id="itsDocSource">${sourceOptions}</select>
<input id="itsDocQuery" value="${esc(state.officialDocFilters.q)}" placeholder="Поиск по заголовку, URL, причине">
<button id="itsDocApply" class="primary">Показать</button>
</div>
<p class="mono">Прошло: ${esc(data.counts?.passed ?? 0)} · пропущено: ${esc(data.counts?.skipped ?? 0)} · в таблице: ${esc(data.counts?.filtered ?? 0)}</p>
${table}
${detail}`;
document.getElementById("itsDocApply").onclick = () => loadOfficialDocs().catch(err => {
target.innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
document.querySelectorAll("[data-its-doc-index]").forEach(btn => {
btn.onclick = () => showOfficialDocText(rows[Number(btn.dataset.itsDocIndex)]).catch(err => {
state.officialDocText = {kind: "error", path: "", text: err.stack || err.message};
renderOfficialDocs();
});
});
}
async function resolveFact() {
const target = document.getElementById("factAnswer");
const sourceKind = document.getElementById("factSourceKind").value;
const sourcePath = document.getElementById("factSourcePath").value.trim();
const path = document.getElementById("factPath").value.trim();
const view = document.getElementById("factView").value;
if (!path) {
target.innerHTML = `<pre>Введите путь факта.</pre>`;
return;
}
target.innerHTML = `<pre>Проверяю факт в источнике конфигурации...</pre>`;
const data = await api("/api/1c/fact", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({source_kind: sourceKind, source_path: sourcePath, path, view})
});
target.innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Проверка факта</h3>${status(data.exists ? "ok" : "missing")}</div>
<div class="panel-body">
<table><tbody>
<tr><th>Факт</th><td class="mono">${esc(path)}</td></tr>
<tr><th>Источник</th><td>${esc(data.source?.kind || "")}<br><span class="mono">${esc(data.source?.path || "")}</span></td></tr>
<tr><th>Объект</th><td>${esc(data.object?.kind_ru || data.object?.kind || "")}.${esc(data.object?.name || "")}</td></tr>
<tr><th>Статус</th><td>${esc(data.exists ? "подтвержден" : "не подтвержден")} · ${esc(data.confidence || "")} · ${esc(data.reason || "")}</td></tr>
<tr><th>Найдено</th><td><pre>${esc(JSON.stringify(data.match || data.nearest || null, null, 2))}</pre></td></tr>
</tbody></table>
</div>
</div>`;
}
async function routeOneCQuestion() {
const target = document.getElementById("routeAnswer");
const question = document.getElementById("routeQuestion").value.trim();
const sourcePath = document.getElementById("routeSourcePath").value.trim();
const view = document.getElementById("routeView").value;
if (!question) {
target.innerHTML = `<pre>Введите вопрос или задачу.</pre>`;
return;
}
target.innerHTML = `<pre>Определяю маршрут...</pre>`;
const data = await api("/api/1c/intake", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({question, source_path: sourcePath, view})
});
const route = data.route || {};
const decision = route.decision || {};
const checks = route.fact_checks || [];
const risks = route.source_risks || [];
const facts = data.facts || {};
const policy = data.answer_policy || {};
target.innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Маршрут</h3>${status(decision.route || "unknown")}</div>
<div class="panel-body">
${risks.length ? `<p class="warning-note">${esc(risks.map(r => r.message).join(" "))}</p>` : ""}
<table><tbody>
<tr><th>Документация</th><td>${esc(decision.needs_docs_rag ? "нужна" : "не нужна")} · ${esc(decision.safe_rag_scope || "")}</td></tr>
<tr><th>Текущая база</th><td>${esc(decision.needs_current_config ? "нужна" : "не нужна")}</td></tr>
<tr><th>Код</th><td>${status(policy.code_generation_allowed ? "ok" : "blocked")} ${esc(policy.must_check_current_config_before_code ? "проверка фактов обязательна" : "")}</td></tr>
<tr><th>Факты</th><td>${esc(facts.confirmed_count || 0)} подтвержд. · ${esc(facts.unresolved_count || 0)} не подтвержд.</td></tr>
<tr><th>Пути фактов</th><td class="mono">${esc((route.fact_paths || []).join(", ") || "нет")}</td></tr>
</tbody></table>
${checks.length ? `<table><thead><tr><th>Факт</th><th>Статус</th><th>Деталь</th></tr></thead><tbody>
${checks.map(row => {
const r = row.result || {};
const match = r.match || {};
return `<tr><td class="mono">${esc(row.path)}</td><td>${status(r.exists ? "ok" : "missing")}</td><td>${esc(r.exists ? `${match.name || ""} ${match.type || ""}` : (r.reason || row.reason || ""))}<br><span class="mono">${esc(JSON.stringify(match.origin || {}, null, 0))}</span></td></tr>`;
}).join("")}
</tbody></table>` : ""}
<pre>${esc(JSON.stringify(data.next_commands || [], null, 2))}</pre>
</div>
</div>`;
}
async function pollJob(id) {
const data = await api(`/api/jobs/${id}`);
const job = data.job;
if (!job) return;
state.selectedJob = id;
await refresh();
if (["queued", "running"].includes(job.status)) setTimeout(() => pollJob(id), 1200);
}
function setView(view) {
state.view = view;
document.querySelectorAll("nav button").forEach(btn => btn.classList.toggle("active", btn.dataset.view === view));
document.querySelectorAll(".view").forEach(el => el.classList.add("hidden"));
document.getElementById(`view-${view}`).classList.remove("hidden");
document.getElementById("title").textContent = {
overview: "Обзор", models: "Модели", rag: "RAG и ИТС", onec: "1С", checks: "Проверки", jobs: "Журнал"
}[view];
}
function renderMetrics(d) {
const a = d.artifacts.counts || {};
const m = d.models.counts || {};
return `
<div class="grid">
<div class="panel metric"><div class="panel-body"><div class="value">${a.artifacts || 0}</div><div class="label">групп артефактов</div></div></div>
<div class="panel metric"><div class="panel-body"><div class="value">${fmtBytes(a.total_size_bytes)}</div><div class="label">локальные модели и RAG</div></div></div>
<div class="panel metric"><div class="panel-body"><div class="value">${m.ok || 0}</div><div class="label">моделей OK</div></div></div>
<div class="panel metric"><div class="panel-body"><div class="value">${d.one_c.status || "..."}</div><div class="label">1C plugin</div></div></div>
</div>`;
}
function renderOverview(d) {
document.getElementById("view-overview").innerHTML = `
${renderMetrics(d)}
<div class="two">
<div class="panel">
<div class="panel-head"><h3>Артефакты</h3>${status((d.artifacts.file.exists && "ready") || "missing")}</div>
<div class="panel-body">${artifactTable(d.artifacts.artifacts.slice(0, 8))}</div>
</div>
<div class="panel">
<div class="panel-head"><h3>Последние задания</h3></div>
<div class="panel-body">${jobsList(d.jobs.slice(0, 6))}</div>
</div>
</div>`;
}
function artifactTable(items) {
return `<table><thead><tr><th>Группа</th><th>Файлы</th><th>Размер</th><th>Статус</th></tr></thead><tbody>${
items.map(a => `<tr><td>${esc(a.name)}</td><td>${a.files || 0}</td><td>${fmtBytes(a.size_bytes)}</td><td>${status(a.exists ? "ok" : "missing")}</td></tr>`).join("")
}</tbody></table>`;
}
function renderModels(d) {
const q6 = d.models.q6_download || {};
const q6Service = d.models.q6_service || {};
const rows = (d.models.models || []).map(m => {
const servedOnGpu = q6Service.available && m.served_model_name === q6Service.expected_model;
const statusText = servedOnGpu
? `${status("ok")}<br><span class="mono">served on GPU; local storage: ${esc(m.status || "")}${m.reason ? ` (${esc(m.reason)})` : ""}</span>`
: `${status(m.status)}<br><span class="mono">${esc(m.reason || "")}</span>`;
return `
<tr>
<td>
<div class="doc-title">${esc(m.id)}</div>
<span class="mono">${esc(m.served_model_name || "")}</span>
</td>
<td>${esc(m.runtime)}<br><span class="mono">${esc(m.quantization || "")}</span></td>
<td>${statusText}</td>
<td class="mono">${esc(m.storage_path)}${m.filename ? `<br>${esc(m.filename)}` : ""}</td>
</tr>`;
}).join("");
document.getElementById("view-models").innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Q6 1C service</h3>${status(q6Service.state || "unknown")}</div>
<div class="panel-body">
<div class="stack">
<div class="commands">
<button data-command="q6_service_start">Запустить сервис Q6</button>
<button data-command="q6_service_restart">Перезапустить Q6</button>
<button data-command="q6_service_status">Статус сервиса Q6</button>
<button data-command="q6_service_stop">Остановить Q6</button>
</div>
<table><tbody>
<tr><th>Endpoint</th><td class="mono">${esc(q6Service.base_url || "")}</td></tr>
<tr><th>OpenAI base</th><td class="mono">${esc(q6Service.openai_base_url || "")}</td></tr>
<tr><th>Model</th><td>${status(q6Service.available ? "ok" : "missing")} <span class="mono">${esc(q6Service.expected_model || "")}</span></td></tr>
<tr><th>Served</th><td class="mono">${esc((q6Service.models || []).join(", ") || "нет")}</td></tr>
<tr><th>Container</th><td class="mono">${esc(q6Service.container || "")}<br>${esc(q6Service.container_status || "")}</td></tr>
<tr><th>Error</th><td>${esc(q6Service.error || "")}</td></tr>
</tbody></table>
<div class="panel" style="border-style:dashed">
<div class="panel-head"><h3>OpenAI connection</h3><span class="badge">external agent</span></div>
<div class="panel-body">
<table><tbody>
<tr><th>OPENAI_BASE_URL</th><td class="mono">${esc(q6Service.openai_base_url || "")}</td></tr>
<tr><th>OPENAI_API_KEY</th><td class="mono">${esc(q6Service.openai_api_key_hint || "dummy")}</td></tr>
<tr><th>OPENAI_MODEL</th><td class="mono">${esc(q6Service.expected_model || "")}</td></tr>
</tbody></table>
<pre>OPENAI_BASE_URL=${esc(q6Service.openai_base_url || "")}
OPENAI_API_KEY=${esc(q6Service.openai_api_key_hint || "dummy")}
OPENAI_MODEL=${esc(q6Service.expected_model || "")}</pre>
</div>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-head"><h3>Q6 1C download</h3>${status(q6.state || "unknown")}</div>
<div class="panel-body">
<div class="commands">
<button data-command="q6_download_start">Скачать Q6</button>
<button data-command="q6_download_status">Статус загрузки Q6</button>
<button data-command="q6_download_stop">Остановить загрузку Q6</button>
<button data-command="model_storage">Проверить модели</button>
</div>
<table><tbody>
<tr><th>Progress</th><td>${esc(q6.percent ?? 0)}% · ${fmtBytes(q6.bytes || 0)} / ${fmtBytes(q6.expected_bytes || 0)}</td></tr>
<tr><th>Message</th><td>${esc(q6.message || "")}</td></tr>
<tr><th>File</th><td class="mono">${esc(q6.file || "")}</td></tr>
<tr><th>Log</th><td class="mono">${esc(q6.log || "")}</td></tr>
</tbody></table>
</div>
</div>
<div class="panel"><div class="panel-head"><h3>Model storage</h3>${status(d.models.status || "unknown")}</div>
<div class="panel-body"><p class="mono">${esc(fileMeta(d.models.file))}</p>
<table><thead><tr><th>Model / served</th><th>Runtime / quant</th><th>Status / reason</th><th>Path / file</th></tr></thead><tbody>${rows}</tbody></table></div></div>`;
}
function renderRag(d) {
const r = d.rag;
const p = r.official_progress || {};
const q = r.official_quality || {};
const sq = r.official_static_check || {};
const sc = r.official_start_coverage || {};
const pv = r.official_platform_versions || {};
const ac = r.official_access_check || {};
const qc = q.counts || {};
const sqc = sq.counts || {};
const scc = sc.counts || {};
const pvc = pv.counts || {};
const pvd = pv.defaults || {};
const latestActive = pvd.latest_active_8_3 || {};
document.getElementById("view-rag").innerHTML = `
<div class="two">
<div class="panel"><div class="panel-head"><h3>RAG corpus</h3></div><div class="panel-body">
<table><tbody>
<tr><th>Corpus</th><td>${esc(fileMeta(r.corpus))}</td></tr>
<tr><th>Index</th><td>${esc(fileMeta(r.index))}</td></tr>
<tr><th>Manifest</th><td>${esc(fileMeta(r.manifest))}</td></tr>
<tr><th>ITS starts</th><td>${esc(fileMeta(r.official_start_links))}</td></tr>
<tr><th>ITS platform versions</th><td>${esc(fileMeta(pv.file))} · версий ${esc(pvc.versions ?? 0)} · активных ${esc(pvc.active_versions ?? 0)} · default ${esc(latestActive.platform_version || "")}</td></tr>
<tr><th>ITS access</th><td>${status(ac.status === "ok" ? "ok" : "warning")} ${esc((ac.findings || []).join(", ") || "нет отчета")} · page ${esc(ac.page?.http_status || "")} · src ${esc(ac.src?.http_status || "")}</td></tr>
<tr><th>ITS start coverage</th><td>${status(sc.passed ? "ok" : "warning")} активных ${esc(scc.active_candidates ?? 0)}/${esc(scc.candidates ?? 0)} · источников ${esc(scc.sources ?? 0)}</td></tr>
<tr><th>ITS raw</th><td>${esc(fileMeta(r.official_raw))}</td></tr>
<tr><th>ITS progress</th><td>${status(p.status || "unknown")} ${esc(p.page_count || 0)} стр. · ошибок ${esc(p.error_count || 0)} · очередь ${esc(p.queue_size ?? "")} · resume ${esc(p.skipped_existing || 0)}/${esc(p.resumed_pages || 0)}</td></tr>
<tr><th>ITS current</th><td class="mono">${esc(p.current_url || "")}</td></tr>
<tr><th>ITS normalized</th><td>${esc(fileMeta(r.official_normalized))}</td></tr>
<tr><th>ITS media</th><td>${esc(fileMeta(r.official_media))}</td></tr>
<tr><th>ITS static</th><td>${esc(fileMeta(r.official_static))} ${r.official_static?.exists ? `<a class="badge" href="${esc(r.official_static_url)}" target="_blank">открыть</a>` : ""}</td></tr>
<tr><th>ITS static quality</th><td>${status(sq.passed ? "ok" : "warning")} ${esc(sqc.pages ?? 0)} стр. · media ${esc(sqc.media_files ?? 0)} · assets ${esc(sqc.asset_files ?? 0)} · внеш. img ${esc(sqc.external_image_pages ?? 0)} · внеш. css/js ${esc(sqc.external_asset_pages ?? 0)} · битых ссылок ${esc(sqc.broken_local_refs ?? 0)}</td></tr>
<tr><th>ITS quality</th><td>${status(q.passed ? "ok" : "warning")} ${esc(qc.normalized_pages ?? 0)} стр. · пропущено ${esc(qc.skipped_pages ?? 0)} · src ${esc(qc.content_src_pages ?? 0)}/${esc(qc.raw_pages ?? 0)} · img ${esc(qc.media_images ?? 0)} · предупреждений ${esc(qc.warnings ?? 0)}</td></tr>
</tbody></table>
</div></div>
<div class="panel"><div class="panel-head"><h3>Действия</h3></div><div class="panel-body">
<div class="ask-row">
<input id="itsTargetUrl" value="https://its.1c.ru/db/v8316doc#bookmark:dev:TI000000044" placeholder="URL страницы ИТС">
<button id="itsTargetFetchBtn" class="primary">Загрузить страницу ИТС</button>
</div>
<div id="itsTargetResult"><pre>Прицельная загрузка добавит страницу в RAG.</pre></div>
<div class="commands">
<button data-command="1c_rag_freshness">Проверить свежесть RAG</button>
<button data-command="1c_rag_source_governance">Проверить источники RAG</button>
<button data-command="official_docs_quality">Проверить качество ИТС</button>
<button data-command="official_docs_access">Проверить доступ ИТС</button>
<button data-command="official_docs_ingestion_logic">Проверить логику ИТС</button>
<button data-command="official_docs_discover_starts">Найти старты ИТС</button>
<button data-command="official_docs_start_coverage">Проверить старты ИТС</button>
<button data-command="official_docs_platform_versions">Проверить версии платформы</button>
<button data-command="official_docs_fetch_plan">План загрузки ИТС</button>
<button data-command="official_docs_download_media">Скачать медиа ИТС</button>
<button data-command="official_docs_static_site">Собрать статический ИТС</button>
<button data-command="official_docs_static_quality">Проверить статический ИТС</button>
<button data-command="official_docs_private">Проверить ИТС artifacts</button>
<button data-command="official_docs_reload_fresh">Перезагрузить ИТС 2000 стр.</button>
<button class="warn" data-command="its_cookie_prompt">Обновить cookie ИТС</button>
</div>
</div></div>
</div>
<div class="panel">
<div class="panel-head"><h3>Загруженные страницы ИТС</h3><span class="badge">контроль качества</span></div>
<div class="panel-body" id="itsDocsTable"><pre>Нажмите "Показать", чтобы загрузить список страниц.</pre></div>
</div>
<div class="panel">
<div class="panel-head"><h3>Вопрос по документации</h3></div>
<div class="panel-body">
<div class="ask-row">
<input id="ragQuestion" value="Что такое форма в 1С?" placeholder="Вопрос по документации 1С">
<select id="ragSourceType">
<option value="official_1c_docs">Документация: все</option>
<option value="official_1c_its_glossary">ИТС: глоссарий</option>
<option value="official_1c_its_platform_doc">ИТС: документация платформы</option>
<option value="official_1c_its_developer_guide">ИТС: руководство разработчика</option>
<option value="metadata">Примеры metadata</option>
<option value="all_with_examples">Все, включая примеры</option>
</select>
<input id="ragPlatformVersion" value="" placeholder="8.3.16">
<input id="ragPlatformDocId" value="" placeholder="v8316doc">
<button id="ragAskBtn" class="primary">Спросить</button>
</div>
<div id="ragAnswer"><pre>Ответ появится здесь.</pre></div>
</div>
</div>`;
}
function renderOneC(d) {
document.getElementById("view-onec").innerHTML = `
<div class="panel"><div class="panel-head"><h3>1C plugin</h3>${status(d.one_c.status || "unknown")}</div>
<div class="panel-body">
<div class="commands">
<button data-command="1c_plugin">Запустить healthcheck 1С</button>
<button data-command="1c_question_router">Проверить маршрутизатор</button>
<button data-command="1c_agent_intake">Проверить intake</button>
</div>
<p class="mono">${esc(fileMeta(d.one_c.file))}</p>
<table><tbody>
<tr><th>ID</th><td>${esc(d.one_c.manifest.id || "")}</td></tr>
<tr><th>Status</th><td>${esc(d.one_c.manifest.status || "")}</td></tr>
<tr><th>Failed</th><td>${esc((d.one_c.failed_checks || []).join(", ") || "нет")}</td></tr>
</tbody></table>
</div></div>
<div class="panel">
<div class="panel-head"><h3>Маршрут вопроса</h3></div>
<div class="panel-body">
<div class="ask-row">
<input id="routeQuestion" value="В примере RAG есть реквизит Артикул у справочника Номенклатура. Напиши код для текущей базы." placeholder="Вопрос или задача по 1С">
<select id="routeView">
<option value="effective">effective</option>
<option value="base">base</option>
</select>
<button id="routeAskBtn" class="primary">Разобрать</button>
</div>
<input id="routeSourcePath" value="reports/1c-sql/upo/unified-object-route-index.json" placeholder="Путь к route-index">
<div id="routeAnswer"><pre>Маршрут появится здесь.</pre></div>
</div>
</div>
<div class="panel">
<div class="panel-head"><h3>Проверка факта конфигурации</h3></div>
<div class="panel-body">
<div class="ask-row">
<select id="factSourceKind">
<option value="route_index">Текущий route-index</option>
<option value="metadata_snapshot">Явный metadata snapshot</option>
</select>
<input id="factSourcePath" value="reports/1c-sql/upo/unified-object-route-index.json" placeholder="Путь к источнику фактов">
</div>
<div class="ask-row">
<input id="factPath" value="Документ.ПриходнаяНакладная.ДатаСоздания" placeholder="Например: Справочник.Номенклатура.Цвет">
<select id="factView">
<option value="effective">effective</option>
<option value="base">base</option>
<option value="extension">extension</option>
</select>
<button id="factAskBtn" class="primary">Проверить</button>
</div>
<div id="factAnswer"><pre>Результат появится здесь.</pre></div>
</div>
</div>`;
}
function renderChecks(d) {
const grouped = {};
for (const cmd of d.commands) (grouped[cmd.category] ||= []).push(cmd);
document.getElementById("view-checks").innerHTML = Object.entries(grouped).map(([group, cmds]) => `
<div class="panel"><div class="panel-head"><h3>${esc(group)}</h3></div><div class="panel-body"><div class="commands">
${cmds.map(c => `<button class="command-btn" data-command="${esc(c.id)}"><span>${esc(c.label)}</span><small>${esc(c.id)}</small></button>`).join("")}
</div></div></div>
`).join("");
}
function jobsList(jobs) {
if (!jobs.length) return `<p class="mono">Заданий пока нет.</p>`;
return `<table><thead><tr><th>Задание</th><th>Статус</th><th>Время</th></tr></thead><tbody>${
jobs.map(j => `<tr><td>${esc(j.label)}</td><td>${status(j.status)}</td><td class="mono">${esc(j.finished_at || j.started_at || j.created_at)}</td></tr>
${j.id === state.selectedJob ? `<tr><td colspan="3"><pre>${esc((j.stdout || "") + (j.stderr ? "\\nERR:\\n" + j.stderr : ""))}</pre></td></tr>` : ""}`).join("")
}</tbody></table>`;
}
function renderJobs(d) {
document.getElementById("view-jobs").innerHTML = `<div class="panel"><div class="panel-head"><h3>Журнал выполнения</h3></div><div class="panel-body">${jobsList(d.jobs)}</div></div>`;
}
function render() {
const d = state.data;
if (!d) return;
document.getElementById("workspace").textContent = d.workspace;
document.getElementById("generated").textContent = d.generated_at;
renderOverview(d); renderModels(d); renderRag(d); renderOneC(d); renderChecks(d); renderJobs(d);
renderOfficialDocs();
setView(state.view);
document.querySelectorAll("[data-command]").forEach(btn => btn.onclick = () => runCommand(btn.dataset.command));
const askBtn = document.getElementById("ragAskBtn");
if (askBtn) askBtn.onclick = () => askRag().catch(err => {
document.getElementById("ragAnswer").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const factBtn = document.getElementById("factAskBtn");
if (factBtn) factBtn.onclick = () => resolveFact().catch(err => {
document.getElementById("factAnswer").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const routeBtn = document.getElementById("routeAskBtn");
if (routeBtn) routeBtn.onclick = () => routeOneCQuestion().catch(err => {
document.getElementById("routeAnswer").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const itsDocApply = document.getElementById("itsDocApply");
if (itsDocApply) itsDocApply.onclick = () => loadOfficialDocs().catch(err => {
document.getElementById("itsDocsTable").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const itsTargetFetchBtn = document.getElementById("itsTargetFetchBtn");
if (itsTargetFetchBtn) itsTargetFetchBtn.onclick = () => fetchTargetItsDoc().catch(err => {
document.getElementById("itsTargetResult").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
}
document.getElementById("refreshBtn").onclick = refresh;
document.querySelectorAll("nav button").forEach(btn => btn.onclick = () => { setView(btn.dataset.view); render(); });
refresh().catch(err => {
document.getElementById("view-overview").innerHTML = `<div class="panel"><div class="panel-body"><pre>${esc(err.stack || err.message)}</pre></div></div>`;
});
</script>
</body>
</html>