feat(ui+docs): читаемые бейджи реплик + раздел документации

Бейджи в Песочнице:
- Каждый бейдж теперь с русским префиксом-меткой (ветка/шаг/роутер предложил/решение/тип ответа)
- Тег «многошаговая» на бейдже ветки при is_state_machine или наличии step_code
- Шаг: сначала русское название, потом код в скобках (Повод и специалист (qualify))
- get_thread_detail обогащает старые meta: подтягивает step_name и is_state_machine из БД

Документация:
- «Удержание в ветке» — пошаговый разбор sticky-механизма, явно что не второй роутер
- Новая карточка «Боковой вопрос (soft_insertion)» — откуда берётся, счётчик, nudge
- Блок под схемой «Что происходит на каждой реплике»: почему бейджи на ответах ассистента

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-25 21:08:27 +05:00
parent 85c3ec0222
commit 45832e2b37
3 changed files with 132 additions and 46 deletions
+77 -43
View File
@@ -225,52 +225,66 @@
white-space: pre-wrap;
}
.msg.assistant { background: var(--bot-bg); align-self: flex-start; }
.msg-intent {
display: inline-block;
background: var(--chip-bg);
color: var(--accent);
padding: 1px 7px;
/* Строка бейджей под сообщением ассистента */
.msg-badge {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
margin-right: 5px;
line-height: 1.5;
white-space: nowrap;
}
.msg-badge .badge-label {
font-weight: 400;
opacity: 0.65;
margin-right: 2px;
}
.msg-badge .badge-val {
font-weight: 600;
font-family: var(--mono);
margin-right: 6px;
}
.msg-badge .badge-sub {
font-weight: 400;
font-family: var(--mono);
opacity: 0.7;
margin-left: 2px;
}
/* Ветка */
.msg-intent {
background: var(--chip-bg);
color: var(--accent);
}
.msg-intent .badge-sm-tag {
font-weight: 400;
font-size: 9px;
opacity: 0.75;
margin-left: 4px;
background: rgba(99,102,241,0.12);
padding: 0 4px;
border-radius: 6px;
font-family: sans-serif;
}
/* Шаг */
.msg-step {
display: inline-block;
background: #eef2ff;
color: #3730a3;
padding: 1px 7px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
font-family: var(--mono);
margin-right: 6px;
}
/* Роутер предложил */
.msg-router {
display: inline-block;
color: var(--muted);
font-size: 10px;
margin-right: 6px;
}
.msg-router code {
background: #fafbfd;
border: 1px solid var(--border);
color: var(--muted);
padding: 0 4px;
border-radius: 4px;
font-family: var(--mono);
font-size: 10px;
background: #f3f4f6;
color: #4b5563;
border: 1px solid #e5e7eb;
}
.msg-router .badge-val { font-family: var(--mono); }
/* События */
.msg-event {
display: inline-block;
padding: 1px 7px;
border-radius: 10px;
font-size: 10px;
font-weight: 500;
margin-right: 4px;
cursor: help;
}
.msg-event .badge-label { opacity: 0.6; }
.msg-event.sticky { background: #dbeafe; color: #1e40af; }
.msg-event.hard_handoff { background: #ffedd5; color: #9a3412; }
.msg-event.soft_insertion{ background: #fef3c7; color: #78350f; }
@@ -310,7 +324,7 @@
padding-left: 10px;
color: var(--muted);
}
.msg-meta { font-size: 10px; color: var(--muted); margin-top: 4px; }
.msg-meta { font-size: 10px; color: var(--muted); margin-top: 6px; display: flex; flex-wrap: wrap; align-items: center; gap: 3px; }
.chat-empty {
margin: auto;
color: var(--muted);
@@ -695,28 +709,48 @@ function startNewThread() {
}
const EVENT_LABELS = {
sticky: { text: "удержались", title: "роутер предлагал другую ветку, ветка осталась в сценарии" },
hard_handoff: { text: "переключение", title: "ветка сама выдала [INTENT_CHANGE] и передала диалог другой" },
soft_insertion: { text: "боковой вопрос", title: "ответ вне шага: модель ответила на побочный вопрос, не двигая сценарий" },
resumed: { text: "возврат", title: "восстановили отложенный сценарий со всеми слотами" },
routing_loop: { text: "защита от петли", title: "сработала защита: автоматический перевод на оператора" },
validation_blocked: { text: "прыжок отклонён", title: "валидатор не разрешил переход в указанный шаг" },
sticky: { label: "решение:", text: "удержались в ветке", title: "роутер предлагал другую ветку — модель осталась в текущем сценарии" },
hard_handoff: { label: "решение:", text: "переключили ветку", title: "ветка выдала [INTENT_CHANGE] и передала диалог другой ветке" },
soft_insertion: { label: "тип ответа:", text: "боковой вопрос", title: "модель ответила на побочный вопрос, не продвигая сценарий (шаг не изменился)" },
resumed: { label: "решение:", text: "восстановили сценарий",title: "вернулись в ранее приостановленный сценарий со всеми слотами" },
routing_loop: { label: "защита:", text: "петля роутера", title: "сработала защита от петли: диалог автоматически передан оператору" },
validation_blocked:{ label: "валидатор:", text: "переход отклонён", title: "валидатор guard заблокировал переход на запрошенный шаг" },
};
function renderAssistantBadges(intentCode, intentName, meta) {
const intent = intentCode ? `<span class="msg-intent" title="${esc(intentName || intentCode)}">${esc(intentCode)}</span>` : "";
// Ветка
const displayName = (intentName && intentName !== intentCode) ? intentName : intentCode;
const smTag = (meta && (meta.is_state_machine || meta.step_code))
? `<span class="badge-sm-tag">многошаговая</span>` : "";
const codeHint = (intentName && intentName !== intentCode)
? `<span class="badge-sub">(${esc(intentCode)})</span>` : "";
const intent = intentCode
? `<span class="msg-badge msg-intent" title="Ветка: ${esc(intentName || intentCode)}"><span class="badge-label">ветка:</span><span class="badge-val">${esc(displayName)}</span>${codeHint}${smTag}</span>`
: "";
if (!meta) return intent;
// Шаг state machine
const stepDisplay = meta.step_name || meta.step_code;
const stepSub = meta.step_name && meta.step_code
? `<span class="badge-sub">(${esc(meta.step_code)})</span>` : "";
const stepBadge = meta.step_code
? `<span class="msg-step" title="шаг state machine">${esc(meta.step_code)}</span>`
? `<span class="msg-badge msg-step" title="Текущий шаг сценария"><span class="badge-label">шаг:</span><span class="badge-val">${esc(stepDisplay)}</span>${stepSub}</span>`
: "";
const router = (meta.router_intent_code && meta.router_intent_code !== meta.served_intent_code)
? `<span class="msg-router">роутер: <code>${esc(meta.router_intent_code)}</code></span>`
// Роутер предложил другую ветку
const routerDiffers = meta.router_intent_code && meta.router_intent_code !== meta.served_intent_code;
const router = routerDiffers
? `<span class="msg-badge msg-router" title="Роутер классифицировал реплику в другую ветку, но модель осталась здесь"><span class="badge-label">роутер предложил:</span><span class="badge-val">${esc(meta.router_intent_code)}</span></span>`
: "";
// События
const events = (meta.events || []).map(e => {
const cfg = EVENT_LABELS[e];
if (!cfg) return "";
return `<span class="msg-event ${esc(e)}" title="${esc(cfg.title)}">${esc(cfg.text)}</span>`;
return `<span class="msg-badge msg-event ${esc(e)}" title="${esc(cfg.title)}"><span class="badge-label">${esc(cfg.label)}</span><span class="badge-val">${esc(cfg.text)}</span></span>`;
}).join("");
return intent + stepBadge + router + events;
}