Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,866 @@
|
||||
<!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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user