docs+ui: страница «Документация», единый стиль заголовков, перевод на оператора

Добавлена /docs.html — обзор мультиагентной системы для оператора. Все
термины в формате «русский (english)», жирным: ветка (intent), маршрутизатор
(router), пошаговый сценарий (state machine), шаг (step), допустимые
переходы (allowed_next), слоты (slots), условия выхода (exit conditions),
переключение ветки (hard handoff), удержание в ветке (sticky state machine),
структурированный ответ (structured output), отложенный сценарий
(suspended/resume), защита от петли (routing loop guard), состояние диалога
(thread state). Плюс пошаговая схема обработки реплики и резюме защитных
механизмов. Ссылка «Документация» добавлена в шапку всех страниц.

Унификация заголовков под стиль «Версии» в правом сайдбаре Настроек: убран
uppercase, переход на 13px / var(--fg) / font-weight 600 / зажатый
letter-spacing. Применилось к .col-head во всех колонках, .field label в
редакторе, .section-header в списке веток, заголовкам столбцов на странице
Отладки и заголовкам секций RAG-результата. Бейджи (АКТИВНАЯ, система)
оставлены прежними — это статусные метки, не заголовки.

Переименование ветки escalate_human для согласованности с русским UI:
«Эскалация на оператора» → «Перевод на оператора», описание тоже. Точечная
миграция при старте (intent_service.migrate_intent_copy) обновляет
существующие записи в БД, только если поле в точности совпадает со старым
значением — операторские правки не затираются.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-25 16:41:58 +05:00
parent 932b488bcb
commit 3c71372ec8
6 changed files with 510 additions and 68 deletions
+374
View File
@@ -0,0 +1,374 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Agent for Patients — Документация</title>
<style>
:root {
--bg: #f5f6f8;
--panel: #ffffff;
--border: #e1e4ea;
--muted: #6b7280;
--fg: #111827;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--ok: #16a34a;
--warn: #d97706;
--err: #dc2626;
--chip-bg: #eef2ff;
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--fg);
font-size: 14px;
line-height: 1.6;
display: flex;
flex-direction: column;
}
header {
background: var(--panel);
border-bottom: 1px solid var(--border);
padding: 14px 24px;
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
header h1 { margin: 0; font-size: 16px; font-weight: 600; }
.nav { display: flex; gap: 4px; }
.nav-link {
text-decoration: none;
color: var(--muted);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.nav-link:hover { background: var(--chip-bg); color: var(--fg); }
.nav-link.active { background: var(--accent); color: #fff; }
main {
flex: 1;
overflow-y: auto;
padding: 32px 24px 80px 24px;
}
article {
max-width: 820px;
margin: 0 auto;
}
article h1 { font-size: 26px; font-weight: 700; margin: 0 0 6px 0; letter-spacing: -0.02em; }
article .lead {
color: var(--muted);
font-size: 15px;
margin: 0 0 32px 0;
}
article h2 {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
margin: 36px 0 12px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--border);
}
article h3 {
font-size: 15px;
font-weight: 600;
margin: 20px 0 8px 0;
}
article p { margin: 0 0 12px 0; }
article ul, article ol { margin: 0 0 12px 0; padding-left: 22px; }
article li { margin: 4px 0; }
article code {
background: var(--chip-bg);
color: var(--accent);
padding: 1px 6px;
border-radius: 4px;
font-family: var(--mono);
font-size: 12.5px;
}
article pre {
background: #fafbfd;
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
overflow-x: auto;
font-family: var(--mono);
font-size: 12px;
line-height: 1.55;
margin: 8px 0 16px 0;
}
article pre code { background: none; color: var(--fg); padding: 0; font-size: 12px; }
/* Карточка термина */
.term-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
margin-bottom: 10px;
}
.term-card .term-head {
font-size: 15px;
margin-bottom: 6px;
}
.term-card .term-head strong { font-weight: 700; }
.term-card .term-en {
color: var(--muted);
font-weight: 400;
}
.term-card .term-body {
font-size: 13.5px;
color: var(--fg);
}
/* Схема пошагово */
.flow-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 18px;
margin: 12px 0 18px 0;
}
.flow-step {
display: grid;
grid-template-columns: 32px 1fr;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--border);
}
.flow-step:last-child { border-bottom: none; }
.flow-num {
background: var(--accent);
color: #fff;
width: 24px;
height: 24px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.flow-step b { font-size: 14px; }
.flow-step .flow-text { font-size: 13.5px; color: var(--fg); }
.flow-step .flow-text .muted { color: var(--muted); font-size: 13px; }
/* TOC */
.toc {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 18px;
margin-bottom: 24px;
font-size: 13.5px;
}
.toc strong { display: block; margin-bottom: 6px; font-size: 13px; }
.toc ol { margin: 0; padding-left: 22px; }
.toc a { color: var(--accent); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
.callout {
background: #fffbeb;
border: 1px solid #fde68a;
color: #78350f;
border-radius: 8px;
padding: 10px 14px;
font-size: 13.5px;
margin: 10px 0 16px 0;
}
</style>
</head>
<body>
<header>
<h1>Chat Agent for Patients</h1>
<nav class="nav">
<a href="/" class="nav-link">Отладка</a>
<a href="/sandbox.html" class="nav-link">Песочница</a>
<a href="/settings.html" class="nav-link">Настройки</a>
<a href="/docs.html" class="nav-link active">Документация</a>
</nav>
</header>
<main>
<article>
<h1>Как работает мультиагентная система</h1>
<p class="lead">Здесь объясняется, что такое <b>ветка (intent)</b>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</p>
<div class="toc">
<strong>Содержание</strong>
<ol>
<li><a href="#why">Зачем это всё</a></li>
<li><a href="#terms">Главные термины</a></li>
<li><a href="#flow">Что происходит на каждой реплике</a></li>
<li><a href="#guards">Защитные механизмы</a></li>
<li><a href="#where">Где что настраивается</a></li>
</ol>
</div>
<h2 id="why">Зачем это всё</h2>
<p>На пилоте у нас был «один большой системный промпт» — модель пыталась одновременно записывать на приём, отвечать на вопросы по ценам и эскалировать острые случаи. По мере усложнения скрипта запись начинала «плыть»: модель забывала шаги, путала ветки, перескакивала через мини-интервью.</p>
<p>Мы перешли на <b>графовую архитектуру (graph-based routing)</b>: реплика пациента сначала идёт в <b>маршрутизатор (router)</b>, который определяет тему, а потом — в <b>ветку (intent)</b>, отвечающую только за свой узкий сценарий. У сложных веток внутри есть собственный <b>пошаговый сценарий (state machine)</b>.</p>
<h2 id="terms">Главные термины</h2>
<div class="term-card">
<div class="term-head"><strong>Ветка</strong> <span class="term-en">(intent)</span></div>
<div class="term-body">Изолированный «под-агент» с собственным системным промптом. Отвечает за одну тему: запись, перенос, цены, медицинский вопрос, общая справка, перевод на оператора. У каждой ветки — свой код (<code>new_booking</code>, <code>price_question</code> и т. п.) и активная версия настроек.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Маршрутизатор</strong> <span class="term-en">(router)</span></div>
<div class="term-body">Системная ветка <code>_router</code>: отдельный, дешёвый вызов модели, который по последней реплике пациента возвращает один из кодов веток. Не отвечает пациенту напрямую — только классифицирует. Вызывается на КАЖДОЙ реплике, не один раз при входе.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Пошаговый сценарий</strong> <span class="term-en">(state machine)</span></div>
<div class="term-body">Внутренний граф шагов внутри ветки. Сейчас есть только у <code>new_booking</code>: 6 шагов от приветствия до подтверждения записи. Модель на каждой реплике видит, на каком шаге сейчас, и какие <b>слоты</b> уже собраны.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Шаг</strong> <span class="term-en">(step)</span></div>
<div class="term-body">Узел пошагового сценария. У каждого шага свой код (<code>intro</code>, <code>qualify</code>, <code>present</code>, <code>offer_time</code>, <code>book</code>, <code>close</code>), свой кусок промпта и список <b>допустимых переходов</b>.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Допустимые переходы</strong> <span class="term-en">(allowed_next)</span></div>
<div class="term-body">Список кодов шагов, в которые можно перейти с текущего. Например, с <code>qualify</code> разрешено в <code>qualify</code> (остаться) или <code>present</code> (двигаться вперёд), но не в <code>close</code>. Если модель попытается перепрыгнуть через шаг — <b>валидатор переходов (transition validator)</b> отклонит запрос, мы останемся на шаге.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Слоты</strong> <span class="term-en">(slots)</span></div>
<div class="term-body">JSON-словарь данных, которые ветка собирает по ходу разговора. Для записи это <code>name</code>, <code>reason</code>, <code>specialist</code>, <code>preferred_time</code>, <code>confirmed</code>. Модель на каждой реплике их видит и обновляет — старые не переспрашиваются.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Условия выхода</strong> <span class="term-en">(exit conditions)</span></div>
<div class="term-body">Список ситуаций, когда ветка должна вместо обычного ответа выдать служебный сигнал <code>[INTENT_CHANGE: &lt;код_ветки&gt;]</code> и передать диалог другой ветке. Например, если пациент в записи упомянул хирургию — ветка <code>new_booking</code> сама вернёт <code>[INTENT_CHANGE: escalate_human]</code>.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Переключение ветки</strong> <span class="term-en">(hard handoff)</span></div>
<div class="term-body">Полная смена ветки внутри одного диалога с обнулением шага и слотов. Бывает в двух случаях: ветка сама выдала <code>[INTENT_CHANGE]</code> или маршрутизатор предложил другую ветку, которая не имеет пошагового сценария.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Удержание в ветке</strong> <span class="term-en">(sticky state machine)</span></div>
<div class="term-body">Защита от ложных переключений: если диалог идёт по пошаговому сценарию (например, <code>new_booking · qualify</code>) и маршрутизатор на короткой реплике («Алексей», «болит ухо») предлагает другую ветку — мы НЕ сбрасываем состояние, а передаём модели подсказку «маршрутизатор думает X, но ты в Y». Модель сама решает: остаться в сценарии (заполнить слот) или явно выйти через <code>[INTENT_CHANGE]</code>.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Структурированный ответ ветки</strong> <span class="term-en">(structured output)</span></div>
<div class="term-body">Каждая sm-ветка возвращает не только текст пациенту, но и служебный JSON-блок в хвосте ответа:
<pre><code>STATE_JSON: {"state_after": "qualify", "slots_updated": {"name": "Алексей"}}</code></pre>
Парсер вырезает этот блок (пациент его не видит), валидатор проверяет легальность <code>state_after</code>, обновляет состояние диалога.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Отложенный сценарий</strong> <span class="term-en">(suspended intent / resume)</span></div>
<div class="term-body">Если из пошаговой ветки произошёл переход в другую (например, посреди записи спросили про цены), её состояние (текущий шаг и собранные слоты) запоминается в полях <code>suspended_intent</code>, <code>resumable_step_code</code>, <code>resumable_slots</code>. Когда маршрутизатор увидит, что пациент возвращается к исходной теме («ладно, продолжаем запись»), мы автоматически восстановим шаг и слоты.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Защита от петли</strong> <span class="term-en">(routing loop guard)</span></div>
<div class="term-body">Счётчик <code>handoff_count</code> в состоянии диалога считает все переключения ветки. При превышении 3 переключений за диалог следующее переключение блокируется: диалог автоматически уходит в <code>escalate_human</code> с шаблонным ответом «Уточню детали с администратором клиники, свяжемся с вами в течение ближайшего часа». Это страховка от циклов вроде «запись ↔ цены ↔ запись ↔ цены».</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Состояние диалога</strong> <span class="term-en">(thread state)</span></div>
<div class="term-body">Запись в БД (одна на диалог), хранящая текущую ветку, текущий шаг (если есть), собранные слоты, <code>handoff_count</code> и поля отложенного сценария. Видно в Песочнице справа, в блоке «Состояние диалога».</div>
</div>
<h2 id="flow">Что происходит на каждой реплике</h2>
<div class="flow-card">
<div class="flow-step">
<div class="flow-num">1</div>
<div class="flow-text">
<b>Маршрутизатор классифицирует реплику.</b>
<div class="muted">Отдельный вызов модели с короткой системой и историей. Возвращает один код ветки.</div>
</div>
</div>
<div class="flow-step">
<div class="flow-num">2</div>
<div class="flow-text">
<b>Проверяем отложенный сценарий.</b>
<div class="muted">Если в состоянии диалога есть <code>suspended_intent</code> и маршрутизатор вернул именно его — восстанавливаем шаг и слоты, очищаем поля, обнуляем счётчик переключений.</div>
</div>
</div>
<div class="flow-step">
<div class="flow-num">3</div>
<div class="flow-text">
<b>Применяем удержание в ветке.</b>
<div class="muted">Если диалог уже идёт по sm-ветке и маршрутизатор предлагает другую — состояние не сбрасываем, в системный промпт ветки добавляется блок <code>[ПОДСКАЗКА РОУТЕРА]</code>.</div>
</div>
</div>
<div class="flow-step">
<div class="flow-num">4</div>
<div class="flow-text">
<b>Если переключение всё-таки происходит — инкрементим счётчик.</b>
<div class="muted">При превышении 3 — авто-перевод на оператора, без вызова модели.</div>
</div>
</div>
<div class="flow-step">
<div class="flow-num">5</div>
<div class="flow-text">
<b>Собираем системный промпт ветки.</b>
<div class="muted">Базовый промпт + промпт текущего шага (если есть) + блок текущего состояния (шаг, слоты, подсказка). Зовём модель.</div>
</div>
</div>
<div class="flow-step">
<div class="flow-num">6</div>
<div class="flow-text">
<b>Парсим ответ.</b>
<div class="muted">Если есть <code>[INTENT_CHANGE]</code> — переключаемся в новую ветку (если из sm-ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть <code>STATE_JSON:</code> — валидируем переход, обновляем шаг и сливаем слоты.</div>
</div>
</div>
<div class="flow-step">
<div class="flow-num">7</div>
<div class="flow-text">
<b>Сохраняем сообщение пациента, ответ модели и обновлённое состояние диалога.</b>
<div class="muted">Всё одной транзакцией. Если что-то упадёт — откатываем целиком, «диалог-призрак» в списке не появится.</div>
</div>
</div>
</div>
<h2 id="guards">Защитные механизмы</h2>
<ul>
<li><b>Удержание в ветке</b> защищает от ложного сброса сценария на коротких репликах вроде «Алексей» или «болит ухо».</li>
<li><b>Валидатор переходов</b> блокирует «прыжки через шаг» — модель не сможет уйти из <code>intro</code> сразу в <code>book</code>.</li>
<li><b>Защита от петли</b> ограничивает число переключений ветки за диалог. После 3-го — авто-перевод на оператора.</li>
<li><b>Отложенный сценарий</b> возвращает прерванный сценарий с теми же слотами и шагом — пациент не должен повторять имя или повод.</li>
<li><b>Ретрай LLM</b>: и маршрутизатор, и ветка делают один повтор при сетевом сбое DeepSeek. При полном падении — откат транзакции и понятный ответ «модель временно недоступна».</li>
</ul>
<h2 id="where">Где что настраивается</h2>
<ul>
<li><a href="/settings.html">Настройки</a> — список веток, активные версии промптов, поля «Системный промпт», «Правила», «Условия выхода». Для веток с пошаговым сценарием — вкладка «Шаги» с редактором каждого шага и его допустимых переходов.</li>
<li><a href="/sandbox.html">Песочница</a> — живые диалоги от лица пациента. В правой панели видны: состояние диалога (ветка, шаг, слоты, счётчик переключений, отложенный сценарий), решение маршрутизатора, RAG-фрагменты и собранный системный промпт.</li>
<li><a href="/">Отладка</a> — база знаний (загрузка / переразметка документов), одиночные тестовые вопросы без памяти диалога.</li>
</ul>
<div class="callout">
Документ описывает текущее состояние после Спринта 6a. Перевод на оператора с указанием причины (acute_pain / surgery / routing_loop / …), сводка для оператора и умный маршрутизатор, видящий состояние диалога — в Спринте 6b.
</div>
</article>
</main>
</body>
</html>
+7 -8
View File
@@ -125,10 +125,9 @@
}
th {
font-weight: 600;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--fg);
font-size: 13px;
letter-spacing: -0.01em;
}
tr:last-child td { border-bottom: none; }
tr.doc-row { cursor: pointer; }
@@ -250,11 +249,10 @@
}
.col h3 {
margin: 0 0 10px 0;
font-size: 12px;
font-size: 13px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--fg);
letter-spacing: -0.01em;
}
.chunk {
border: 1px solid var(--border);
@@ -343,6 +341,7 @@
<a href="/" class="nav-link active">Отладка</a>
<a href="/sandbox.html" class="nav-link">Песочница</a>
<a href="/settings.html" class="nav-link">Настройки</a>
<a href="/docs.html" class="nav-link">Документация</a>
</nav>
<span class="status"><span class="dot" id="dot"></span><span id="status-text">проверяю…</span></span>
<span class="stats" id="stats"></span>
+55 -39
View File
@@ -93,14 +93,25 @@
flex-direction: column;
min-height: 0;
}
.col-panel:last-child { border-right: none; border-left: 1px solid var(--border); }
.col-panel:last-child {
border-right: none;
border-left: 1px solid var(--border);
background: var(--bg);
}
/* Правая панель — стек карточек на сером фоне */
.col-panel:last-child .col-body {
padding: 14px 14px 18px 14px;
display: flex;
flex-direction: column;
gap: 12px;
}
.col-head {
padding: 12px 16px;
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
font-size: 13px;
color: var(--fg);
font-weight: 600;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: 8px;
@@ -300,53 +311,56 @@
.chat-input button:hover { background: var(--accent-hover); }
.chat-input button:disabled { background: var(--muted); cursor: not-allowed; }
/* Правая панель — отладка */
.debug-section { padding: 14px 16px; border-bottom: 1px solid var(--border); }
/* Правая панель — карточки */
.debug-section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 14px;
}
.debug-section h3 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
font-size: 13px;
color: var(--fg);
margin: 0 0 10px 0;
font-weight: 600;
letter-spacing: -0.01em;
}
/* Сворачиваемая секция (details/summary) */
/* Сворачиваемая секция (details/summary) с тем же видом, что и обычная карточка */
.debug-section.collapsible > summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 10px 0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
gap: 8px;
margin: 0;
font-size: 13px;
color: var(--fg);
font-weight: 600;
letter-spacing: -0.01em;
}
.debug-section.collapsible[open] > summary { margin: 0 0 10px 0; }
.debug-section.collapsible > summary::-webkit-details-marker { display: none; }
.debug-section.collapsible > summary::before {
content: "";
display: inline-block;
transition: transform 0.15s;
font-size: 10px;
color: var(--muted);
}
.debug-section.collapsible[open] > summary::before { transform: rotate(90deg); }
.debug-section.collapsible > summary:hover { color: var(--fg); }
.debug-section.collapsible > summary .summary-count {
.debug-section.collapsible > summary::after {
content: "";
margin-left: auto;
font-size: 14px;
line-height: 1;
color: var(--muted);
transition: transform 0.15s;
}
.debug-section.collapsible[open] > summary::after { transform: rotate(180deg); }
.debug-section.collapsible > summary:hover { color: var(--accent); }
.debug-section.collapsible > summary .summary-count {
background: var(--chip-bg);
color: var(--accent);
padding: 1px 7px;
padding: 1px 8px;
border-radius: 10px;
font-size: 10px;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
font-weight: 500;
}
.chunk-card {
background: var(--panel);
background: #fafbfd;
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 8px;
@@ -408,7 +422,7 @@
overflow-y: auto;
}
.prompt-box {
background: var(--panel);
background: #fafbfd;
color: var(--fg);
border: 1px solid var(--border);
padding: 10px 12px;
@@ -464,6 +478,7 @@
<a href="/" class="nav-link">Отладка</a>
<a href="/sandbox.html" class="nav-link active">Песочница</a>
<a href="/settings.html" class="nav-link">Настройки</a>
<a href="/docs.html" class="nav-link">Документация</a>
</nav>
<span class="status" style="margin-left:auto;"><span class="dot" id="dot"></span><span id="status-text">проверяю…</span></span>
</header>
@@ -494,7 +509,6 @@
</section>
<aside class="col-panel">
<div class="col-head">Отладка ответа</div>
<div class="col-body">
<div class="debug-section">
<h3>Состояние диалога</h3>
@@ -511,10 +525,12 @@
</summary>
<div id="debug-chunks"><div class="mini">— пока пусто —</div></div>
</details>
<div class="debug-section">
<h3>Собранный промпт</h3>
<details class="debug-section collapsible" id="debug-prompt-section">
<summary>
<span>Собранный промпт</span>
</summary>
<div id="debug-prompt"><div class="mini">— пока пусто —</div></div>
</div>
</details>
</div>
</aside>
+37 -20
View File
@@ -72,15 +72,30 @@
flex-direction: column;
min-height: 0;
}
.col-panel:last-child { border-right: none; border-left: 1px solid var(--border); }
.col-head {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
.col-panel:last-child {
border-right: none;
border-left: 1px solid var(--border);
background: var(--bg);
}
/* Правая колонка Настроек — серый фон, заголовок без рамки и стек карточек */
.col-panel:last-child > .col-head {
border-bottom: none;
padding: 16px 14px 8px 14px;
}
.col-panel:last-child > .col-head #versions-intent {
color: var(--muted);
font-weight: normal;
}
.col-panel:last-child > .col-body {
padding: 0 14px 18px 14px;
}
.col-head {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border);
font-size: 13px;
color: var(--fg);
font-weight: 600;
letter-spacing: -0.01em;
}
.col-body {
flex: 1;
@@ -91,9 +106,7 @@
/* Список веток */
.section-header {
padding: 10px 16px 6px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
color: var(--muted);
font-weight: 600;
background: #fafbfd;
@@ -182,10 +195,11 @@
.field { margin-bottom: 14px; position: relative; }
.field label {
display: block;
font-size: 12px;
font-weight: 500;
color: var(--muted);
margin-bottom: 4px;
font-size: 13px;
font-weight: 600;
color: var(--fg);
letter-spacing: -0.01em;
margin-bottom: 6px;
}
.field label.with-hint {
display: flex;
@@ -313,16 +327,18 @@
}
/* Версии */
.versions { padding: 10px; }
.versions {
display: flex;
flex-direction: column;
gap: 10px;
}
.version-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
background: #fafbfd;
border-radius: 10px;
padding: 12px 14px;
}
.version-card.active {
background: #fff;
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent);
}
@@ -486,6 +502,7 @@
<a href="/" class="nav-link">Отладка</a>
<a href="/sandbox.html" class="nav-link">Песочница</a>
<a href="/settings.html" class="nav-link active">Настройки</a>
<a href="/docs.html" class="nav-link">Документация</a>
</nav>
<span class="stats" id="stats"></span>
</header>