2091 lines
190 KiB
HTML
2091 lines
190 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>KT AI – прототип</title>
|
||
<!-- ПРЕФИКС РАЗВЁРТЫВАНИЯ. Статика кита адресуется от корня ПРИЛОЖЕНИЯ, а не
|
||
от корня сайта: под префиксом (/cons, /dkks) файлы лежат за ним, и путь
|
||
«/design-system/…» уводит запрос в корень домена — 404, страница без
|
||
стилей и мёртвый орб. Префикс — одно свойство развёртывания, поэтому и
|
||
место у него одно: атрибут data-kt-ai-base на <html>. Папка статики —
|
||
data-kt-ai-static (по умолчанию /design-system, как было).
|
||
|
||
<html lang="ru" data-theme="light" data-kt-ai-base="/cons">
|
||
|
||
Теги пишет скрипт, а не разметка: адрес уже разобранного <link> изменить
|
||
нельзя — браузер начинает грузить его сразу, и под префиксом это был бы
|
||
гарантированный 404 перед правильным запросом. -->
|
||
<script data-kt-ai-base-resolver>
|
||
(function () {
|
||
var корень = document.documentElement;
|
||
function норм(значение) {
|
||
значение = (значение || "").trim();
|
||
if (!значение || значение === "/") return "";
|
||
if (значение.charAt(0) !== "/") значение = "/" + значение;
|
||
return значение.replace(/\/+$/, "");
|
||
}
|
||
var база = норм(корень.getAttribute("data-kt-ai-base"));
|
||
var папка = норм(корень.getAttribute("data-kt-ai-static")) || "/design-system";
|
||
window.ktAiBase = база;
|
||
window.ktAiAsset = function (имя) {
|
||
return /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(имя) ? имя : база + папка + "/" + имя;
|
||
};
|
||
})();
|
||
</script>
|
||
<script data-kt-ai-boot="css">
|
||
["kt-ai-tokens.css", "kt-ai-components.css"].forEach(function (файл) {
|
||
var ссылка = document.createElement("link");
|
||
ссылка.rel = "stylesheet";
|
||
ссылка.href = window.ktAiAsset(файл);
|
||
document.head.appendChild(ссылка);
|
||
});
|
||
</script>
|
||
<!-- ============================================================
|
||
KT AI App Shell – boilerplate типового enterprise SaaS.
|
||
Гидрируется из JSON-конфига (тег script#kt-app-config в body) –
|
||
его генерирует дизайнер-LLM AI Interviewer по brief владельца.
|
||
Кликабельность встроена: поиск, фильтр статусов, drawer по клику
|
||
на ряд, контрольное действие с подтверждением, bulk-select,
|
||
AI-панель с подсказками, переключатель темы, ⌘K – фокус поиска.
|
||
Схема конфига – см. демо ниже (все ключи опциональны, кроме
|
||
product/table). Канон: ../docs/DESIGN.md · ../docs/PRODUCT_CONTRACT.md · product.schema.json
|
||
============================================================ -->
|
||
<style>
|
||
@layer product {
|
||
*, *::before, *::after { box-sizing: border-box; }
|
||
body { margin: 0; }
|
||
.shell-topbar-burger { display: none; }
|
||
@media (max-width: 831.98px) { .shell-topbar-burger { display: inline-flex; } }
|
||
/* Sidebar: верх – лого + имя решения + toggle; низ – сервисные ряды (Attio-паттерн) */
|
||
.sb-top { display: flex; align-items: center; gap: var(--kt-ai-space-3); padding: 2px 4px 12px; }
|
||
.sb-top img { width: 28px; height: 28px; border-radius: var(--kt-ai-radius-xl); flex-shrink: 0; }
|
||
.sb-top .nm { font-weight: var(--kt-ai-weight-semibold); font-size: var(--kt-ai-text-sm); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
||
.sb-bottom { margin-top: auto; display: flex; flex-direction: column; gap: 2px; }
|
||
.sb-bottom .badge { margin-left: auto; font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-2xs); background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); border-radius: var(--kt-ai-radius-full); padding: 0 6px; }
|
||
/* Низ сайдбара: тур-карточка + утилиты (Помощь, Отзыв) — паттерн ElevenLabs */
|
||
.sb-bottom { margin-top: auto; display: flex; flex-direction: column; gap: 2px; padding-top: var(--kt-ai-space-4); }
|
||
.sb-bottom button { background: none; border: none; font: inherit; color: inherit; text-align: left; cursor: pointer; width: 100%; }
|
||
.sb-bottom .sb-tour { display: flex; align-items: flex-start; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-3); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); background: var(--kt-ai-bg-elevated); transition: background var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); }
|
||
.sb-bottom .sb-tour:hover { background: var(--kt-ai-bg-soft); }
|
||
.sb-tour .ic { display: grid; place-items: center; width: 28px; height: 28px; flex-shrink: 0; border-radius: var(--kt-ai-radius-lg); background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); }
|
||
.sb-tour .tx { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
||
.sb-tour .tx strong { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg); }
|
||
.sb-tour .tx span { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
|
||
/* Политика рейла ДС (kt-ai-components.css, @media max-width 1023.98px) сюда
|
||
не дотягивается: компоненты лежат в @layer, а этот блок — беcслойный, и
|
||
беcслойное правило побеждает слоёное независимо от специфичности. Поэтому
|
||
свою часть рейла app-shell закрывает сам: в 52px от блока тура остаётся
|
||
иконка, подпись убирается. */
|
||
/* Диапазон задан С ДВУХ сторон: ниже 832px сайдбар — выдвижная панель во всю
|
||
ширину, и место под подпись там есть. Без нижней границы блок доставал до
|
||
телефона и оставлял от карточки тура одну иконку в панели на 232px — тот же
|
||
дефект, что был в политике рейла самой ДС (закрыт в 6.4.0). */
|
||
@media (min-width: 832px) and (max-width: 1023.98px) {
|
||
.sb-bottom .sb-tour { justify-content: center; padding: var(--kt-ai-space-3); border: 0; background: none; }
|
||
.sb-tour .tx { display: none; }
|
||
}
|
||
.sb-util[data-on="true"] { color: var(--kt-ai-primary); background: var(--kt-ai-primary-subtle); }
|
||
.kt-ai-shell[data-rail="true"] .sb-tour { justify-content: center; padding: var(--kt-ai-space-3); }
|
||
.kt-ai-shell[data-rail="true"] .sb-tour .tx { display: none; }
|
||
/* Онбординг-карусель (первый вход) */
|
||
.onb-card { position: relative; width: 420px; max-width: calc(100vw - 32px); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-3xl); box-shadow: var(--kt-ai-shadow-lg); padding: var(--kt-ai-space-8); text-align: center; }
|
||
.onb-x { position: absolute; top: var(--kt-ai-space-4); right: var(--kt-ai-space-4); }
|
||
.onb-ic { width: 56px; height: 56px; margin: 0 auto var(--kt-ai-space-6); display: grid; place-items: center; border-radius: var(--kt-ai-radius-2xl); background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); }
|
||
.onb-ic .kt-icon { width: 26px; height: 26px; }
|
||
.onb-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
|
||
.onb-text { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); line-height: var(--kt-ai-leading-normal); margin-bottom: var(--kt-ai-space-7); }
|
||
.onb-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--kt-ai-space-5); }
|
||
.onb-dots { display: flex; gap: 6px; }
|
||
.onb-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--kt-ai-border-strong); transition: background var(--kt-ai-dur-fast), width var(--kt-ai-dur-fast); }
|
||
.onb-dots span.on { background: var(--kt-ai-primary); width: 18px; border-radius: 3px; }
|
||
.onb-btns { display: flex; gap: var(--kt-ai-space-3); }
|
||
.menu-up .kt-ai-menu { top: auto; bottom: calc(100% + 4px); right: auto; left: 0; }
|
||
/* Ручной rail-режим (toggle в sb-top) */
|
||
.kt-ai-shell[data-rail="true"] { grid-template-columns: var(--kt-ai-sidebar-collapsed-width) minmax(0, 1fr); }
|
||
.kt-ai-shell[data-rail="true"] .nav-label, .kt-ai-shell[data-rail="true"] .kt-ai-nav-item .count,
|
||
.kt-ai-shell[data-rail="true"] .sb-top .nm, .kt-ai-shell[data-rail="true"] .sb-bottom .badge { display: none; }
|
||
.kt-ai-shell[data-rail="true"] .kt-ai-nav-item { justify-content: center; padding: 0; }
|
||
.kt-ai-shell[data-rail="true"] .sb-top { flex-direction: column; gap: var(--kt-ai-space-3); padding-left: 0; padding-right: 0; }
|
||
/* Төре-орб: кнопка ассистента в топбаре рядом с поиском */
|
||
.topbar-ai {
|
||
display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); flex-shrink: 0;
|
||
height: 32px; padding: 0 var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-full);
|
||
border: 1px solid var(--kt-ai-border-strong); background: var(--kt-ai-bg-elevated); color: var(--kt-ai-fg);
|
||
cursor: pointer; font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium);
|
||
transition: background var(--kt-ai-dur-fast);
|
||
}
|
||
.topbar-ai:hover { background: var(--kt-ai-bg-hover); }
|
||
.topbar-ai .orb { width: 16px; height: 16px; border-radius: 50%; background: var(--kt-ai-ai-orb); flex-shrink: 0; }
|
||
@media (max-width: 639.98px) { .topbar-ai { padding: 0; width: 32px; justify-content: center; } .topbar-ai .ai-label { display: none; } }
|
||
/* Топбар: заголовок страницы слева */
|
||
.topbar-title { display: flex; align-items: baseline; gap: var(--kt-ai-space-4); min-width: 0; }
|
||
.topbar-title .t { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.topbar-title .sub { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); white-space: nowrap; }
|
||
@media (max-width: 639.98px) { .topbar-title .sub { display: none; } }
|
||
/* :not([data-compact]) — чтобы потолок шапки не перебивал компактный вариант:
|
||
у него своя ширина, и она меньше. */
|
||
.kt-ai-topbar .kt-ai-search-field:not([data-compact="true"]) { max-width: 240px; }
|
||
@media (max-width: 767.98px) { .kt-ai-topbar .kt-ai-search-field:not([data-compact="true"]) { max-width: 176px; } }
|
||
/* Рабочая область: больше воздуха сверху (как в React-ките) */
|
||
#view { width: 100%; max-width: var(--kt-ai-content-table); margin-inline: auto; padding: var(--kt-ai-stack-region) var(--kt-ai-stack-region) var(--kt-ai-space-11); }
|
||
@media (max-width: 639.98px) { #view { padding: var(--kt-ai-stack-group) var(--kt-ai-space-6) var(--kt-ai-space-10); } }
|
||
/* Hero-заголовок страницы: лого + крупное имя продукта + подпись (паритет с React KTAIShell) */
|
||
.page-hero { display: flex; align-items: flex-start; gap: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-stack-group); }
|
||
.page-hero .ph-text { flex: 1; min-width: 0; }
|
||
.page-hero h1 { margin: 0; font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-semibold); letter-spacing: -.01em; line-height: 1.15; }
|
||
.page-hero .sub { margin-top: var(--kt-ai-space-3); font-size: var(--kt-ai-text-md); color: var(--kt-ai-fg-muted); }
|
||
.page-hero .ph-action { flex-shrink: 0; }
|
||
@media (max-width: 639.98px) { .page-hero { flex-direction: column; gap: var(--kt-ai-space-5); } .page-hero h1 { font-size: var(--kt-ai-text-2xl); } }
|
||
.topbar-crumb { display: flex; align-items: center; gap: var(--kt-ai-space-3); min-width: 0; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
|
||
.topbar-crumb span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.shell-panel-toggle { display: inline-flex; }
|
||
@media (max-width: 831.98px) { .shell-panel-toggle { display: none; } }
|
||
/* Кнопка создания – вверху sidebar (под брендом), на всю ширину */
|
||
.sb-new { margin: 0 4px var(--kt-ai-space-5); }
|
||
.sb-new .kt-ai-btn { width: 100%; justify-content: center; }
|
||
.kt-ai-shell[data-rail="true"] .sb-new .kt-ai-btn { padding: 0; }
|
||
/* Сетка сервисных рядов внизу: единый слот 20px под иконку/аватар – подписи на одной оси */
|
||
.sb-bottom .kt-ai-nav-item > .kt-icon { flex: 0 0 20px; }
|
||
.sb-bottom .kt-ai-avatar { width: 20px; height: 20px; font-size: 8px; flex: 0 0 20px; }
|
||
.sb-bottom > div:last-child { padding: var(--kt-ai-space-3) var(--kt-ai-space-4) 0; }
|
||
/* Мастер создания: степпер + загрузка */
|
||
.case-steps { margin-bottom: var(--kt-ai-space-6); overflow-x: auto; padding-bottom: 2px; }
|
||
.case-file { margin-top: var(--kt-ai-space-4); }
|
||
.menu-user { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); }
|
||
.menu-user .nm { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); }
|
||
.menu-user .role { font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); }
|
||
.notif-menu { min-width: 300px; max-width: 340px; padding: 0; }
|
||
.notif-menu .head { padding: var(--kt-ai-space-4) var(--kt-ai-space-5); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-semibold); border-bottom: 1px solid var(--kt-ai-divider); }
|
||
.guide-steps { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); margin: var(--kt-ai-space-5) 0; }
|
||
.guide-steps .gs { display: flex; gap: var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); align-items: flex-start; }
|
||
.guide-steps .gs .n { width: 20px; height: 20px; border-radius: 50%; background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); font-size: var(--kt-ai-text-2xs); font-weight: var(--kt-ai-weight-bold); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; }
|
||
#view > .kt-ai-kpi-strip { margin-bottom: var(--kt-ai-stack-block); }
|
||
.toolbar-row { display: flex; align-items: center; gap: var(--kt-ai-space-4); flex-wrap: wrap; padding: 0 0 var(--kt-ai-stack-block); }
|
||
.toolbar-row .spacer { flex: 1; }
|
||
.drawer-head { display: flex; align-items: flex-start; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
|
||
.drawer-head .t { flex: 1; }
|
||
.drawer-flags { display: flex; flex-direction: column; gap: var(--kt-ai-space-3); margin: var(--kt-ai-space-6) 0; }
|
||
.drawer-actions { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-8); }
|
||
/* v4.0 (Michelle): в рабочей области пара решения – инлайн, primary справа; стек – только в боковом drawer и на узких экранах */
|
||
.kt-ai-card .drawer-actions { flex-direction: row-reverse; justify-content: flex-start; }
|
||
.kt-ai-card .drawer-actions .kt-ai-btn { width: auto; }
|
||
@media (max-width: 639.98px) { .kt-ai-card .drawer-actions { flex-direction: column; } }
|
||
.ai-panel { display: flex; flex-direction: column; }
|
||
.ai-panel .kt-ai-chat { flex: 1; overflow-y: auto; padding-bottom: var(--kt-ai-space-6); }
|
||
.ai-panel-foot { margin-top: auto; display: flex; flex-direction: column; gap: var(--kt-ai-space-4); }
|
||
.section-stub { margin-top: var(--kt-ai-space-10); }
|
||
.proto-label { margin-left: auto; }
|
||
td .kt-ai-checkbox input, th .kt-ai-checkbox input { margin: 0; }
|
||
/* колонка уверенности агента: мини-метр + процент, тон по уровню */
|
||
.conf-cell { display: inline-flex; align-items: center; gap: var(--kt-ai-space-3); }
|
||
.conf-bar { width: 46px; height: 6px; border-radius: var(--kt-ai-radius-full); background: var(--kt-ai-bg-sunken); overflow: hidden; flex-shrink: 0; }
|
||
.conf-bar > span { display: block; height: 100%; border-radius: var(--kt-ai-radius-full); }
|
||
.conf-cell[data-tone="ok"] .conf-bar > span { background: var(--kt-ai-status-ok-fg); }
|
||
.conf-cell[data-tone="warn"] .conf-bar > span { background: var(--kt-ai-status-warn-fg); }
|
||
.conf-cell[data-tone="risk"] .conf-bar > span { background: var(--kt-ai-status-risk-fg); }
|
||
.conf-pct { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
|
||
.topbar-dot { position: absolute; top: 4px; right: 4px; width: 7px; height: 7px; border-radius: 50%; background: var(--kt-ai-primary); }
|
||
.kt-ai-topbar .kt-ai-menu { right: 0; left: auto; top: calc(100% + 6px); }
|
||
/* Карточка кейса: разделы «что сделал агент» / «решение человека» (ElevenLabs detail) */
|
||
.drawer-sec { padding: var(--kt-ai-space-6) 0; border-top: 1px solid var(--kt-ai-divider); }
|
||
.drawer-sec:first-of-type { border-top: none; padding-top: 0; }
|
||
.drawer-sec.decision { border-top: 2px solid var(--kt-ai-border-strong); }
|
||
.agent-summary { margin: var(--kt-ai-space-4) 0; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); line-height: var(--kt-ai-leading-normal); }
|
||
.agent-signal { display: flex; gap: var(--kt-ai-space-4); align-items: flex-start; padding: var(--kt-ai-space-3) 0; font-size: var(--kt-ai-text-sm); }
|
||
.agent-signal svg { color: var(--kt-ai-fg-faint); flex-shrink: 0; margin-top: 2px; }
|
||
.rec-box { display: flex; flex-direction: column; background: var(--kt-ai-primary-subtle); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-5); font-size: var(--kt-ai-text-sm); margin-top: var(--kt-ai-space-4); }
|
||
.cell-title { font-weight: var(--kt-ai-weight-medium); }
|
||
.cell-sub { font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.row-act { text-align: right; }
|
||
.row-kebab { opacity: 0; transition: opacity var(--kt-ai-dur-fast); }
|
||
.kt-ai-table tbody tr:hover .row-kebab { opacity: 1; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app">
|
||
|
||
<script id="kt-app-config" type="application/json">
|
||
{
|
||
"product": { "name": "Договоры аренды", "purpose": "Кабинет юриста · ОЦО ЮС", "entity": "договор" },
|
||
"user": { "name": "Нургуль Турлыбекова", "role": "Руководитель службы правового обеспечения" },
|
||
"notifications": [
|
||
{ "text": "Агент подготовил 2 новых проекта договора", "time": "5 мин", "unread": true },
|
||
{ "text": "Филиал Жезказган прислал кадастровые данные", "time": "1 ч", "unread": true },
|
||
{ "text": "Договор АР-2026-0609 согласован в Лотусе", "time": "вчера", "unread": false }
|
||
],
|
||
"steps": { "items": ["Заявка", "Проект агента", "Проверка юриста", "Согласование"], "current": 3 },
|
||
"nav": [
|
||
{ "label": "Очередь проверки" },
|
||
{ "label": "Реестр договоров" },
|
||
{ "label": "Контрагентские версии" },
|
||
{ "label": "Отчёты" }
|
||
],
|
||
"kpis": [
|
||
{ "value": "80", "label": "договоров / мес" },
|
||
{ "value": "20–30 мин", "label": "проверка вместо 3–4 ч" },
|
||
{ "value": "4", "label": "юриста в службе" }
|
||
],
|
||
"scenario": {
|
||
"headline": "Очередь проверки",
|
||
"control": "Подтвердить и отправить на согласование",
|
||
"reject": "Вернуть на доработку",
|
||
"confirmText": "Договор уйдёт в Лотус на круг согласования.",
|
||
"approveTo": "approved",
|
||
"rejectTo": "waiting"
|
||
},
|
||
"statuses": {
|
||
"ready": { "label": "готов к проверке", "tone": "info" },
|
||
"flags": { "label": "есть флаги", "tone": "warn" },
|
||
"waiting": { "label": "ждёт данных", "tone": "risk" },
|
||
"approved": { "label": "подтверждён", "tone": "ok" }
|
||
},
|
||
"table": {
|
||
"columns": [
|
||
{ "key": "obj", "label": "Объект аренды", "type": "text" },
|
||
{ "key": "branch", "label": "Филиал-инициатор" },
|
||
{ "key": "sum", "label": "Сумма, ₸/мес", "type": "num" },
|
||
{ "key": "date", "label": "Заявка", "type": "date" },
|
||
{ "key": "status", "label": "Статус", "type": "status" }
|
||
],
|
||
"rows": [
|
||
{ "obj": "Помещение, пр. Абая 31", "branch": "Алматы", "sum": "1 240 000", "date": "10.06.2026", "status": "ready",
|
||
"_flags": ["Тариф подтверждён по SAP", "Полномочия подписанта проверены", "80% текста – типовой шаблон"] },
|
||
{ "obj": "Офис, ул. Сатпаева 18", "branch": "Астана", "sum": "2 180 000", "date": "09.06.2026", "status": "flags",
|
||
"_flags": ["Тариф в заявке ниже актуального тарифа SAP – подставлен тариф SAP", "Срок аренды 11 мес – нетиповой"] },
|
||
{ "obj": "Площадка, ул. Алашахана 2", "branch": "Жезказган", "sum": "640 000", "date": "09.06.2026", "status": "waiting",
|
||
"_flags": ["Нет кадастровых данных объекта – запрос отправлен инициатору"] },
|
||
{ "obj": "Помещение, ул. Гоголя 39", "branch": "Алматы", "sum": "1 870 000", "date": "08.06.2026", "status": "ready",
|
||
"_flags": ["Проверки пройдены, отклонений нет"] },
|
||
{ "obj": "Склад, ул. Театральная 4", "branch": "Караганда", "sum": "980 000", "date": "07.06.2026", "status": "flags",
|
||
"_flags": ["Контрагентская версия: 3 отклонения от шаблона – п. 4.2, 6.1, 9.3"] },
|
||
{ "obj": "Офис, ул. Айтеке би 12", "branch": "Кызылорда", "sum": "450 000", "date": "06.06.2026", "status": "approved",
|
||
"_flags": [] },
|
||
{ "obj": "АТС, ул. Ауэзова 141", "branch": "Шымкент", "sum": "1 320 000", "date": "05.06.2026", "status": "ready",
|
||
"_flags": ["Тариф подтверждён по SAP"] },
|
||
{ "obj": "Помещение, ул. Желтоксан 2","branch": "Тараз", "sum": "720 000", "date": "04.06.2026", "status": "approved",
|
||
"_flags": [] }
|
||
]
|
||
},
|
||
"drawer": {
|
||
"titleKey": "obj",
|
||
"fields": [
|
||
{ "key": "branch", "label": "Филиал-инициатор" },
|
||
{ "key": "sum", "label": "Сумма по тарифу SAP" },
|
||
{ "key": "date", "label": "Дата заявки (Лотус)" }
|
||
],
|
||
"flagsLabel": "Проверки агента"
|
||
},
|
||
"ai": {
|
||
"intro": "Проект договора готов: данные заявки из Лотуса, тариф из SAP, проверка по ГК РК и внутренним документам пройдена. Спросите про любой флаг.",
|
||
"suggestions": ["Почему флаг по тарифу?", "Что с кадастровыми данными?", "Покажи отклонения контрагента"],
|
||
"answers": {
|
||
"Почему флаг по тарифу?": "Тариф в заявке филиала ниже актуального тарифа SAP. По правилу «тариф всегда из SAP» я подставил актуальное значение и пометил расхождение флагом.",
|
||
"Что с кадастровыми данными?": "В заявке филиала нет кадастровых данных объекта. Я отправил запрос инициатору в Лотусе; договор остаётся в статусе «ждёт данных».",
|
||
"Покажи отклонения контрагента": "Контрагентская версия отличается от шаблона в трёх пунктах: 4.2 (срок оплаты 15 → 30 дней), 6.1 (ответственность сторон), 9.3 (порядок расторжения). Полный diff – в карточке договора."
|
||
},
|
||
"fallback": "В кликабельном прототипе ответы ассистента – заглушки. В рабочей версии здесь отвечает ИИ-агент по данным договора."
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- embedded KT AI icon sprite (Lucide, monochrome) -->
|
||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
|
||
<symbol id="home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/>
|
||
<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||
</symbol>
|
||
<symbol id="chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/>
|
||
</symbol>
|
||
<symbol id="send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/>
|
||
<path d="m21.854 2.147-10.94 10.939"/>
|
||
</symbol>
|
||
<symbol id="feed" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 11a9 9 0 0 1 9 9"/>
|
||
<path d="M4 4a16 16 0 0 1 16 16"/>
|
||
<circle cx="5" cy="19" r="1"/>
|
||
</symbol>
|
||
<symbol id="lots" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<circle cx="12" cy="12" r="1"/>
|
||
</symbol>
|
||
<symbol id="source" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
||
<path d="M3 5V19A9 3 0 0 0 21 19V5"/>
|
||
<path d="M3 12A9 3 0 0 0 21 12"/>
|
||
</symbol>
|
||
<symbol id="catalog" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 7v14"/>
|
||
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>
|
||
</symbol>
|
||
<symbol id="decisions" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M13 5h8"/>
|
||
<path d="M13 12h8"/>
|
||
<path d="M13 19h8"/>
|
||
<path d="m3 17 2 2 4-4"/>
|
||
<path d="m3 7 2 2 4-4"/>
|
||
</symbol>
|
||
<symbol id="risk" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>
|
||
<path d="M12 8v4"/>
|
||
<path d="M12 16h.01"/>
|
||
</symbol>
|
||
<symbol id="recommender" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<circle cx="12" cy="12" r="6"/>
|
||
<circle cx="12" cy="12" r="2"/>
|
||
</symbol>
|
||
<symbol id="compare" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="18" cy="18" r="3"/>
|
||
<circle cx="6" cy="6" r="3"/>
|
||
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
|
||
<path d="M11 18H8a2 2 0 0 1-2-2V9"/>
|
||
</symbol>
|
||
<symbol id="collections" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>
|
||
</symbol>
|
||
<symbol id="search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m21 21-4.34-4.34"/>
|
||
<circle cx="11" cy="11" r="8"/>
|
||
</symbol>
|
||
<symbol id="settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M10 5H3"/>
|
||
<path d="M12 19H3"/>
|
||
<path d="M14 3v4"/>
|
||
<path d="M16 17v4"/>
|
||
<path d="M21 12h-9"/>
|
||
<path d="M21 19h-5"/>
|
||
<path d="M21 5h-7"/>
|
||
<path d="M8 10v4"/>
|
||
<path d="M8 12H3"/>
|
||
</symbol>
|
||
<symbol id="chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 3v16a2 2 0 0 0 2 2h16"/>
|
||
<path d="M7 16h8"/>
|
||
<path d="M7 11h12"/>
|
||
<path d="M7 6h3"/>
|
||
</symbol>
|
||
<symbol id="table" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/>
|
||
</symbol>
|
||
<symbol id="sort" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m21 16-4 4-4-4"/>
|
||
<path d="M17 20V4"/>
|
||
<path d="m3 8 4-4 4 4"/>
|
||
<path d="M7 4v16"/>
|
||
</symbol>
|
||
<symbol id="filter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"/>
|
||
</symbol>
|
||
<symbol id="download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 15V3"/>
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||
<path d="m7 10 5 5 5-5"/>
|
||
</symbol>
|
||
<symbol id="chevronRight" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m9 18 6-6-6-6"/>
|
||
</symbol>
|
||
<symbol id="external" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M15 3h6v6"/>
|
||
<path d="M10 14 21 3"/>
|
||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||
</symbol>
|
||
<symbol id="help" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||
<path d="M12 17h.01"/>
|
||
</symbol>
|
||
<symbol id="history" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/>
|
||
<path d="M12 7v5l4 2"/>
|
||
</symbol>
|
||
<symbol id="info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M12 16v-4"/>
|
||
<path d="M12 8h.01"/>
|
||
</symbol>
|
||
<symbol id="users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
|
||
<path d="M16 3.128a4 4 0 0 1 0 7.744"/>
|
||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
|
||
<circle cx="9" cy="7" r="4"/>
|
||
</symbol>
|
||
<symbol id="file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>
|
||
<path d="M14 2v5a1 1 0 0 0 1 1h5"/>
|
||
<path d="M10 9H8"/>
|
||
<path d="M16 13H8"/>
|
||
<path d="M16 17H8"/>
|
||
</symbol>
|
||
<symbol id="tool" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>
|
||
</symbol>
|
||
<symbol id="model" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 20v2"/>
|
||
<path d="M12 2v2"/>
|
||
<path d="M17 20v2"/>
|
||
<path d="M17 2v2"/>
|
||
<path d="M2 12h2"/>
|
||
<path d="M2 17h2"/>
|
||
<path d="M2 7h2"/>
|
||
<path d="M20 12h2"/>
|
||
<path d="M20 17h2"/>
|
||
<path d="M20 7h2"/>
|
||
<path d="M7 20v2"/>
|
||
<path d="M7 2v2"/>
|
||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||
<rect x="8" y="8" width="8" height="8" rx="1"/>
|
||
</symbol>
|
||
<symbol id="dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="7" height="9" x="3" y="3" rx="1"/>
|
||
<rect width="7" height="5" x="14" y="3" rx="1"/>
|
||
<rect width="7" height="9" x="14" y="12" rx="1"/>
|
||
<rect width="7" height="5" x="3" y="16" rx="1"/>
|
||
</symbol>
|
||
<symbol id="columns" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="18" height="18" x="3" y="3" rx="2"/>
|
||
<path d="M9 3v18"/>
|
||
<path d="M15 3v18"/>
|
||
</symbol>
|
||
<symbol id="more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="1"/>
|
||
<circle cx="19" cy="12" r="1"/>
|
||
<circle cx="5" cy="12" r="1"/>
|
||
</symbol>
|
||
<symbol id="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M20 6 9 17l-5-5"/>
|
||
</symbol>
|
||
<symbol id="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M18 6 6 18"/>
|
||
<path d="m6 6 12 12"/>
|
||
</symbol>
|
||
<symbol id="alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
|
||
<path d="M12 9v4"/>
|
||
<path d="M12 17h.01"/>
|
||
</symbol>
|
||
<symbol id="clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M12 6v6l4 2"/>
|
||
</symbol>
|
||
<symbol id="arrowRight" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M5 12h14"/>
|
||
<path d="m12 5 7 7-7 7"/>
|
||
</symbol>
|
||
<symbol id="panelLeftClose" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="18" height="18" x="3" y="3" rx="2"/>
|
||
<path d="M9 3v18"/>
|
||
<path d="m16 15-3-3 3-3"/>
|
||
</symbol>
|
||
<symbol id="panelLeft" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="18" height="18" x="3" y="3" rx="2"/>
|
||
<path d="M9 3v18"/>
|
||
</symbol>
|
||
<symbol id="trophy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"/>
|
||
<path d="M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"/>
|
||
<path d="M18 9h1.5a1 1 0 0 0 0-5H18"/>
|
||
<path d="M4 22h16"/>
|
||
<path d="M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"/>
|
||
<path d="M6 9H4.5a1 1 0 0 1 0-5H6"/>
|
||
</symbol>
|
||
<symbol id="pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/>
|
||
<circle cx="12" cy="10" r="3"/>
|
||
</symbol>
|
||
<symbol id="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="4"/>
|
||
<path d="M12 2v2"/>
|
||
<path d="M12 20v2"/>
|
||
<path d="m4.93 4.93 1.41 1.41"/>
|
||
<path d="m17.66 17.66 1.41 1.41"/>
|
||
<path d="M2 12h2"/>
|
||
<path d="M20 12h2"/>
|
||
<path d="m6.34 17.66-1.41 1.41"/>
|
||
<path d="m19.07 4.93-1.41 1.41"/>
|
||
</symbol>
|
||
<symbol id="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/>
|
||
</symbol>
|
||
<symbol id="menu" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></symbol>
|
||
<symbol id="book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></symbol>
|
||
<symbol id="rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></symbol>
|
||
<symbol id="sparkles" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/></symbol>
|
||
<symbol id="play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"/></symbol>
|
||
<symbol id="lightbulb" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></symbol>
|
||
<symbol id="plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></symbol>
|
||
<symbol id="minus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></symbol>
|
||
<symbol id="chevronDown" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
|
||
<symbol id="chevronLeft" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></symbol>
|
||
<symbol id="arrowLeft" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></symbol>
|
||
<symbol id="arrowUpDown" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/></symbol>
|
||
<symbol id="eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></symbol>
|
||
<symbol id="calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></symbol>
|
||
<symbol id="link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></symbol>
|
||
<symbol id="copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></symbol>
|
||
<symbol id="trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></symbol>
|
||
<symbol id="pencil" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></symbol>
|
||
<symbol id="user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
|
||
<symbol id="bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></symbol>
|
||
<symbol id="refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></symbol>
|
||
<symbol id="logout" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></symbol>
|
||
<symbol id="lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
|
||
<symbol id="mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></symbol>
|
||
<symbol id="megaphone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></symbol>
|
||
<symbol id="archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="20" height="5" x="2" y="3" rx="1" />
|
||
<path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" />
|
||
<path d="M10 12h4" />
|
||
</symbol>
|
||
<symbol id="arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M5 12h14" />
|
||
<path d="m12 5 7 7-7 7" />
|
||
</symbol>
|
||
<symbol id="building" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M10 12h4" />
|
||
<path d="M10 8h4" />
|
||
<path d="M14 21v-3a2 2 0 0 0-4 0v3" />
|
||
<path d="M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2" />
|
||
<path d="M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16" />
|
||
</symbol>
|
||
<symbol id="chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m9 18 6-6-6-6" />
|
||
</symbol>
|
||
<symbol id="alert-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<line x1="12" x2="12" y1="8" y2="12" />
|
||
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||
</symbol>
|
||
<symbol id="check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<path d="m9 12 2 2 4-4" />
|
||
</symbol>
|
||
<symbol id="clipboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
|
||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
|
||
<path d="M12 11h4" />
|
||
<path d="M12 16h4" />
|
||
<path d="M8 11h.01" />
|
||
<path d="M8 16h.01" />
|
||
</symbol>
|
||
<symbol id="sync" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 13v8l-4-4" />
|
||
<path d="m12 21 4-4" />
|
||
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />
|
||
</symbol>
|
||
<symbol id="database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||
<path d="M3 5V19A9 3 0 0 0 21 19V5" />
|
||
<path d="M3 12A9 3 0 0 0 21 12" />
|
||
</symbol>
|
||
<symbol id="spreadsheet" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||
<path d="M8 13h2" />
|
||
<path d="M14 13h2" />
|
||
<path d="M8 17h2" />
|
||
<path d="M14 17h2" />
|
||
</symbol>
|
||
<symbol id="merge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="18" cy="18" r="3" />
|
||
<circle cx="6" cy="6" r="3" />
|
||
<path d="M6 21V9a9 9 0 0 0 9 9" />
|
||
</symbol>
|
||
<symbol id="inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||
</symbol>
|
||
<symbol id="layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z" />
|
||
<path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" />
|
||
<path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" />
|
||
</symbol>
|
||
<symbol id="checklist" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M13 5h8" />
|
||
<path d="M13 12h8" />
|
||
<path d="M13 19h8" />
|
||
<path d="m3 17 2 2 4-4" />
|
||
<path d="m3 7 2 2 4-4" />
|
||
</symbol>
|
||
<symbol id="package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
||
<path d="M12 22V12" />
|
||
<polyline points="3.29 7 12 12 20.71 7" />
|
||
<path d="m7.5 4.27 9 5.15" />
|
||
</symbol>
|
||
<symbol id="receipt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 17V7" />
|
||
<path d="M16 8h-6a2 2 0 0 0 0 4h4a2 2 0 0 1 0 4H8" />
|
||
<path d="M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z" />
|
||
</symbol>
|
||
<symbol id="scale" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 3v18" />
|
||
<path d="m19 8 3 8a5 5 0 0 1-6 0zV7" />
|
||
<path d="M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1" />
|
||
<path d="m5 8 3 8a5 5 0 0 1-6 0zV7" />
|
||
<path d="M7 21h10" />
|
||
</symbol>
|
||
<symbol id="split" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M16 3h5v5" />
|
||
<path d="M8 3H3v5" />
|
||
<path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3" />
|
||
<path d="m15 9 6-6" />
|
||
</symbol>
|
||
<symbol id="warning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||
<path d="M12 9v4" />
|
||
<path d="M12 17h.01" />
|
||
</symbol>
|
||
<symbol id="upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 3v12" />
|
||
<path d="m17 8-5-5-5 5" />
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||
</symbol>
|
||
<symbol id="circleNumber0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 10v4a2 2 0 1 0 4 0v-4a2 2 0 1 0 -4 0"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 10l2 -2v8"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 8h3a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h3"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 9a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 8v3a1 1 0 0 0 1 1h3"/><path d="M14 8v8"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 15a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-3v-4h4"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M14 9a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v6a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-3"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 8h4l-2 8"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M12 12h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1"/></g>
|
||
</symbol>
|
||
<symbol id="circleNumber9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.575" stroke-linecap="round" stroke-linejoin="round">
|
||
<g transform="translate(12 12) scale(1.11111) translate(-12 -12)"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M10 15a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-6a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v2a1 1 0 0 0 1 1h3"/></g>
|
||
</symbol>
|
||
</svg>
|
||
|
||
<div class="kt-ai-shell">
|
||
<aside class="kt-ai-sidebar" id="sidebar" style="display:flex;flex-direction:column;padding:14px 10px;gap:2px">
|
||
<div class="sb-top">
|
||
<img src="data:image/png;base64,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" alt="KT AI">
|
||
<span class="nm nav-label" id="brandName">…</span>
|
||
</div>
|
||
<nav id="nav" aria-label="Разделы" style="display:flex;flex-direction:column;gap:2px"></nav>
|
||
<div class="sb-bottom">
|
||
<button class="sb-tour" onclick="S.onboarding(true)" title="Как это работает">
|
||
<span class="ic"><svg class="kt-icon"><use href="#sparkles"></use></svg></span>
|
||
<span class="tx"><strong>Как это работает</strong><span class="nav-label">Тур по прототипу</span></span>
|
||
</button>
|
||
<button class="kt-ai-nav-item sb-util" onclick="S.guide()" title="Помощь и инструкция">
|
||
<svg class="kt-icon"><use href="#help"></use></svg><span class="nav-label">Помощь</span>
|
||
</button>
|
||
<button class="kt-ai-nav-item sb-util" data-kt-feedback-trigger title="Режим обратной связи">
|
||
<svg class="kt-icon"><use href="#megaphone"></use></svg><span class="nav-label" data-ktfb-label>Отзыв</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="kt-ai-main" style="min-height:100dvh;display:flex;flex-direction:column">
|
||
<header class="kt-ai-topbar">
|
||
<button class="kt-ai-icon-button shell-topbar-burger" aria-label="Меню" onclick="S.toggleSidebar()">
|
||
<svg class="kt-icon" viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
||
</button>
|
||
<button class="kt-ai-icon-button shell-panel-toggle" aria-label="Свернуть меню" title="Свернуть меню" onclick="S.toggleRail()">
|
||
<svg class="kt-icon" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg>
|
||
</button>
|
||
<nav class="topbar-crumb"><span id="pageTitle">…</span><span id="pageSub" style="display:none"></span></nav>
|
||
<div class="spacer"></div>
|
||
<!-- id="topSearch", а НЕ "search": в документе выше вшит спрайт, и символ
|
||
лупы в нём назван ровно так же. getElementById возвращал символ,
|
||
поэтому поле шапки было мёртвым — ни фильтра, ни фокуса по сочетанию,
|
||
ни синхронизации значения. Гейт 20 держит id-пространство разведённым. -->
|
||
<div class="kt-ai-search-field" id="searchField"><input class="kt-ai-search" id="topSearch" placeholder="Поиск…"><kbd class="kt-ai-kbd" id="searchKbd" hidden></kbd></div>
|
||
<button class="topbar-ai" id="aiFab" aria-label="Ассистент Төре" title="Ассистент Төре" onclick="S.toggleAi()"><canvas class="kt-ai-orb" data-state="idle" style="width:20px;height:20px"></canvas></button>
|
||
<button class="kt-ai-theme-toggle" id="themeToggle" data-mode="light" aria-label="Переключить тему">
|
||
<svg class="tic tic-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
|
||
<svg class="tic tic-moon" viewBox="0 0 24 24"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
||
<span class="thumb"><svg class="ic ic-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg><svg class="ic ic-moon" viewBox="0 0 24 24"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg></span>
|
||
</button>
|
||
<button class="kt-ai-icon-button" id="actBtn" aria-label="Активность" title="Активность" onclick="S.toggleActivity()" style="display:none"><svg class="kt-icon"><use href="#history"></use></svg></button>
|
||
<div class="kt-ai-menu-anchor">
|
||
<button class="kt-ai-icon-button" aria-label="Уведомления" onclick="S.toggleMenu('notifMenu')" style="position:relative">
|
||
<svg class="kt-icon" viewBox="0 0 24 24"><path d="M18 9a6 6 0 1 0-12 0c0 7-2 7-2 9h16c0-2-2-2-2-9M10 21a2.2 2.2 0 0 0 4 0"/></svg>
|
||
<span class="topbar-dot" id="notifBadge" style="display:none"></span>
|
||
</button>
|
||
<div class="kt-ai-menu notif-menu" id="notifMenu" role="menu"><div class="head">Уведомления</div><div id="notifList"></div></div>
|
||
</div>
|
||
<div class="kt-ai-menu-anchor">
|
||
<button class="kt-ai-icon-button" aria-label="Профиль" onclick="S.toggleMenu('profileMenu')">
|
||
<span class="kt-ai-avatar" id="avatarBtn" style="width:24px;height:24px;font-size:9px">??</span>
|
||
</button>
|
||
<div class="kt-ai-menu" id="profileMenu" role="menu">
|
||
<div class="menu-user"><div class="nm" id="userName">Пользователь</div><div class="role" id="userRole"></div></div>
|
||
<div class="sep"></div>
|
||
<button role="menuitem" class="item" onclick="S.guide()"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M4 19V5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2Zm0 0a2 2 0 0 0 2 2h13"/></svg>Как пользоваться прототипом</button>
|
||
<button role="menuitem" class="item" onclick="S.stub('Профиль')"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3.6-6 8-6s8 2 8 6"/></svg>Профиль</button>
|
||
<button role="menuitem" class="item" onclick="S.stub('Настройки')"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3"/></svg>Настройки</button>
|
||
<div class="sep"></div>
|
||
<button role="menuitem" class="item" data-danger="true" onclick="S.stub('Выход')"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>Выйти</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="view"></div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Drawer деталей записи -->
|
||
<div class="kt-ai-drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" tabindex="-1" style="display:none"></div>
|
||
<div class="kt-ai-drawer" id="actPanel" role="dialog" aria-modal="false" aria-label="Активность" tabindex="-1" style="display:none"></div>
|
||
<div class="kt-ai-menu" id="rowMenu" role="menu" style="position:fixed;z-index:40"></div>
|
||
|
||
<!-- AI-панель -->
|
||
<div class="kt-ai-drawer ai-panel" id="aiPanel" role="dialog" aria-modal="false" aria-label="Ассистент Төре" tabindex="-1" style="display:none"></div>
|
||
|
||
<!-- Modal подтверждения контрольного действия -->
|
||
<div class="kt-ai-modal-overlay" id="modal" style="display:none" onclick="if(event.target===this)S.closeModal()"></div>
|
||
<div class="kt-ai-modal-overlay" id="onb" style="display:none" onclick="if(event.target===this)S.onbClose()"></div>
|
||
|
||
|
||
<div class="kt-ai-toast-stack" id="toasts" role="status" aria-live="polite" aria-atomic="false"></div>
|
||
|
||
<script>
|
||
"use strict";
|
||
/* KT AI App Shell runtime: гидрация конфига + типовые взаимодействия. */
|
||
const CFG = JSON.parse(document.getElementById("kt-app-config").textContent);
|
||
// морфология казахских/русских строк хрупкая → продукт может задать createLabel явно
|
||
const createLabel = (CFG.product && CFG.product.createLabel) || ("Новый " + ((CFG.product && CFG.product.entity) || "объект"));
|
||
const LOGO = (document.querySelector(".sb-top img") || {}).src || "";
|
||
const S = {};
|
||
let state = { navIndex: 0, query: "", statusFilter: "", selected: new Set(), openRow: -1 };
|
||
|
||
const $ = id => document.getElementById(id);
|
||
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||
// Ключ статуса или колонки, уезжающий в inline-обработчик: onclick="S.sort('…')".
|
||
// esc() тут не помощник — он готовит текст для РАЗМЕТКИ, а внутри атрибута
|
||
// лежит ещё и JS-строка. Ключ с апострофом ломал бы обработчик молча: кнопка
|
||
// есть, нажатие не делает ничего. Ключи приходят из контракта продукта, схема
|
||
// их вид не ограничивает.
|
||
const escJs = s => esc(String(s ?? "").replace(/\\/g, "\\\\").replace(/'/g, "\\'"));
|
||
const statuses = CFG.statuses || {};
|
||
const rows = (CFG.table && CFG.table.rows || []).map((r, i) => Object.assign({ _i: i }, r));
|
||
const cols = CFG.table && CFG.table.columns || [];
|
||
const statusCol = (cols.find(c => c.type === "status") || {}).key;
|
||
const workflowStatusKeys = statusCol ? Array.from(new Set(rows.map(r => r[statusCol]).filter(v => v != null && v !== "").map(String))) : [];
|
||
const filterKeys = (Array.isArray(CFG.filterKeys) && CFG.filterKeys.length ? CFG.filterKeys.map(String) : workflowStatusKeys).filter(k => statuses[k]).slice(0, 4);
|
||
|
||
function statusTag(key) {
|
||
const st = statuses[key] || { label: key, tone: "info" };
|
||
return '<span class="kt-ai-status-pill" data-status="' + esc(st.tone) + '">' + esc(st.label) + "</span>";
|
||
}
|
||
function confCell(v) {
|
||
const n = parseInt(String(v == null ? "" : v).replace(/[^\d]/g, ""), 10);
|
||
const pct = isNaN(n) ? 0 : Math.max(0, Math.min(100, n));
|
||
const tone = pct >= 80 ? "ok" : pct >= 60 ? "warn" : "risk";
|
||
return '<span class="conf-cell" data-tone="' + tone + '"><span class="conf-bar"><span style="width:' + pct + '%"></span></span>' +
|
||
'<span class="conf-pct">' + esc(v == null || v === "" ? "—" : v) + "</span></span>";
|
||
}
|
||
function cellHtml(row, col) {
|
||
const v = row[col.key];
|
||
if (col.type === "status") return statusTag(v);
|
||
if (col.type === "confidence") return confCell(v);
|
||
if (col.type === "num") return '<span class="kt-ai-num">' + esc(v) + "</span>";
|
||
if (col.type === "date") { const m = String(v).match(/(\d{4})-(\d{2})-(\d{2})/); return '<span class="kt-ai-num" style="font-size:var(--kt-ai-text-xs)">' + esc(m ? m[3] + "." + m[2] : v) + "</span>"; }
|
||
// двухстрочная entity-ячейка (ElevenLabs): значение + muted-подзаголовок из col.sub
|
||
if (col.sub && row[col.sub] != null && row[col.sub] !== "") return '<div class="cell-title">' + esc(v) + '</div><div class="cell-sub">' + esc(row[col.sub]) + "</div>";
|
||
return esc(v);
|
||
}
|
||
|
||
// Универсальный диапазон дат: фильтрует строки (→ таблица И график) по dateKey графика.
|
||
function parseDate(s) { const m = String(s == null ? "" : s).match(/(\d{4})-(\d{2})-(\d{2})/); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; }
|
||
function dateBounds() {
|
||
const key = (CFG.chart || {}).dateKey;
|
||
if (!key) return null;
|
||
const ds = rows.map(r => parseDate(r[key])).filter(Boolean);
|
||
if (!ds.length) return null;
|
||
const end = new Date(Math.max(...ds.map(d => d.getTime())));
|
||
const days = state.dateRange == null ? 30 : state.dateRange;
|
||
if (!days) return { start: new Date(Math.min(...ds.map(d => d.getTime()))), end, days: 0, key };
|
||
const start = new Date(end); start.setDate(start.getDate() - (days - 1));
|
||
return { start, end, days, key };
|
||
}
|
||
function visibleRows() {
|
||
const q = state.query.toLowerCase();
|
||
const db = dateBounds();
|
||
let out = rows.filter(r => {
|
||
if (db) { const d = parseDate(r[db.key]); if (d && (d < db.start || d > db.end)) return false; }
|
||
if (state.statusFilter && r[statusCol] !== state.statusFilter) return false;
|
||
if (!q) return true;
|
||
return cols.some(c => String(r[c.key] ?? "").toLowerCase().includes(q));
|
||
});
|
||
const s = state.sort;
|
||
if (s && s.key) {
|
||
const col = cols.find(c => c.key === s.key), num = col && col.type === "num";
|
||
out = out.slice().sort((a, b) => {
|
||
let av = a[s.key], bv = b[s.key];
|
||
if (num) { av = parseFloat(String(av).replace(/[^\d.-]/g, "")) || 0; bv = parseFloat(String(bv).replace(/[^\d.-]/g, "")) || 0; }
|
||
else { av = String(av ?? "").toLowerCase(); bv = String(bv ?? "").toLowerCase(); }
|
||
return av < bv ? (s.dir === "asc" ? -1 : 1) : av > bv ? (s.dir === "asc" ? 1 : -1) : 0;
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
S.sort = key => {
|
||
const s = state.sort;
|
||
state.sort = (s && s.key === key) ? (s.dir === "asc" ? { key, dir: "desc" } : null) : { key, dir: "asc" };
|
||
state.page = 1;
|
||
render();
|
||
};
|
||
S.page = n => { state.page = n; render(); };
|
||
S.setRange = d => { state.dateRange = d; state.page = 1; render(); };
|
||
|
||
/* ---------- Топбар: меню, профиль, уведомления, гид ---------- */
|
||
S.closeMenus = () => document.querySelectorAll(".kt-ai-menu[data-open='true']").forEach(m => { m.dataset.open = "false"; });
|
||
S.toggleMenu = id => {
|
||
const m = $(id), was = m.dataset.open === "true";
|
||
S.closeMenus();
|
||
if (!was) m.dataset.open = "true";
|
||
if (id === "notifMenu" && !was) $("notifBadge").style.display = "none";
|
||
};
|
||
S.toggleRail = () => { const sh = document.querySelector(".kt-ai-shell"); sh.dataset.rail = sh.dataset.rail === "true" ? "false" : "true"; };
|
||
S.stub = label => { S.closeMenus(); toast(label + " – раздел-заглушка прототипа", ""); };
|
||
S.guide = () => {
|
||
S.closeMenus();
|
||
const sc = CFG.scenario || {};
|
||
$("modal").innerHTML = '<div class="kt-ai-modal"><div class="modal-title">Как пользоваться прототипом</div>' +
|
||
'<div class="kt-ai-meta" style="margin-bottom:4px">' + esc(CFG.product.purpose || CFG.product.name) + ". Кликабелен один сквозной сценарий:</div>" +
|
||
'<div class="guide-steps">' +
|
||
'<div class="gs"><span class="n">1</span><span>Откройте запись в «' + esc(sc.headline || "списке") + '» – детали и проверки агента справа</span></div>' +
|
||
'<div class="gs"><span class="n">2</span><span>Изучите флаги агента и спросите ассистента (кнопка вверху) про любой из них</span></div>' +
|
||
'<div class="gs"><span class="n">3</span><span>Контрольное действие – «' + esc(sc.control || "Подтвердить") + '»: решение всегда за человеком</span></div>' +
|
||
"</div>" +
|
||
'<div class="kt-ai-meta">Данные – правдоподобные примеры по brief; разделы вне сценария – заглушки.</div>' +
|
||
'<div class="modal-actions"><button class="kt-ai-btn" data-variant="primary" onclick="S.closeModal()">Понятно</button></div></div>';
|
||
$("modal").style.display = "flex";
|
||
};
|
||
// Онбординг-карусель: 2-3 слайда при первом входе; повтор — из тур-карточки в сайдбаре. Контракт может задать свой CFG.onboarding.
|
||
const ONB = (CFG.onboarding && CFG.onboarding.length) ? CFG.onboarding : [
|
||
{ icon: "sparkles", title: CFG.product.name, text: CFG.product.purpose || "Рабочее пространство KT AI." },
|
||
{ icon: "eye", title: "Как пользоваться", text: "Откройте запись в списке — справа появится карточка с разбором и рекомендацией. Решение принимаете вы." },
|
||
{ icon: "megaphone", title: "Оставьте отзыв", text: "Внизу слева — «Отзыв»: включите режим, кликайте по элементам экрана и оставляйте заметки, затем экспортируйте их в markdown для аналитика." }
|
||
];
|
||
let onbIdx = 0;
|
||
const onbKey = "kt-onb:" + (CFG.product.name || "");
|
||
S.onboarding = (force) => {
|
||
if (!force) { try { if (localStorage.getItem(onbKey)) return; } catch (e) {} }
|
||
onbIdx = 0; S.onbRender(); $("onb").style.display = "flex";
|
||
};
|
||
S.onbRender = () => {
|
||
const s = ONB[onbIdx], last = onbIdx === ONB.length - 1;
|
||
$("onb").innerHTML = '<div class="onb-card" role="dialog" aria-modal="true" aria-label="Знакомство с прототипом">' +
|
||
'<button class="kt-ai-icon-button onb-x" aria-label="Закрыть" onclick="S.onbClose()"><svg class="kt-icon"><use href="#x"></use></svg></button>' +
|
||
'<div class="onb-ic"><svg class="kt-icon"><use href="#' + esc(s.icon || "sparkles") + '"></use></svg></div>' +
|
||
'<div class="onb-title">' + esc(s.title) + "</div>" +
|
||
'<div class="onb-text">' + esc(s.text) + "</div>" +
|
||
'<div class="onb-foot"><div class="onb-dots">' + ONB.map((_, i) => '<span class="' + (i === onbIdx ? "on" : "") + '"></span>').join("") + "</div>" +
|
||
'<div class="onb-btns">' + (onbIdx > 0 ? '<button class="kt-ai-btn" onclick="S.onbGo(-1)">Назад</button>' : "") +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="' + (last ? "S.onbClose()" : "S.onbGo(1)") + '">' + (last ? "Понятно" : "Далее") + "</button></div></div></div>";
|
||
};
|
||
S.onbGo = (d) => { onbIdx = Math.max(0, Math.min(ONB.length - 1, onbIdx + d)); S.onbRender(); };
|
||
S.onbClose = () => { $("onb").style.display = "none"; try { localStorage.setItem(onbKey, "1"); } catch (e) {} };
|
||
/* Автопоказ онбординга выключен: модалка поверх рабочего экрана — преграда,
|
||
а не помощь. Тур открывается карточкой «Как это работает» в сайдбаре. */
|
||
S.maybeOnboarding = () => {};
|
||
// Сочетание называется так, как оно нажимается на этой машине. «⌘K» на
|
||
// Windows и Linux — подпись не от той клавиатуры: обработчик слушает и Ctrl,
|
||
// а в поле написано другое.
|
||
function сочетаниеПоиска() {
|
||
const эппл = typeof navigator !== "undefined" &&
|
||
/Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent || "");
|
||
return эппл ? "⌘K" : "Ctrl K";
|
||
}
|
||
// Поиск в шапке: подсказка сочетания — элемент, а не текст в placeholder.
|
||
// Раньше «⌘K» стояло прямо в placeholder, и убрать его продукт не мог никак:
|
||
// на проде поле занимало место, которого в шапке нет (заявка DS-011).
|
||
function renderSearch() {
|
||
const с = CFG.search || {};
|
||
const поле = $("searchField"), ввод = $("topSearch"), бейдж = $("searchKbd");
|
||
if (!поле) return;
|
||
ввод.placeholder = с.placeholder || "Поиск…";
|
||
поле.setAttribute("data-compact", с.compact === true);
|
||
// Бейдж показывается только там, где сочетание действительно работает:
|
||
// подпись сочетания, которое выключено, — обещание (правило 9d).
|
||
const есть = с.shortcut !== false && с.compact !== true;
|
||
бейдж.hidden = !есть;
|
||
бейдж.textContent = есть ? сочетаниеПоиска() : "";
|
||
поле.setAttribute("data-shortcut", есть);
|
||
// В компактном виде о сочетании не написано, но оно работает — назовём его
|
||
// там, где место есть.
|
||
ввод.title = с.shortcut === false ? "" : "Поиск (" + сочетаниеПоиска() + ")";
|
||
}
|
||
|
||
function renderTop() {
|
||
renderSearch();
|
||
$("brandName").textContent = CFG.product.name;
|
||
$("actBtn").style.display = (CFG.activity && CFG.activity.length) ? "inline-flex" : "none";
|
||
const u = CFG.user || {};
|
||
const name = u.name || "Пользователь";
|
||
$("userName").textContent = name;
|
||
$("userRole").textContent = u.role || "";
|
||
$("avatarBtn").textContent = name.split(/\s+/).map(w => w[0]).slice(0, 2).join("").toUpperCase();
|
||
const open = rows.filter(r => statusCol && (statuses[r[statusCol]] || {}).tone !== "ok").length;
|
||
const notifs = (Array.isArray(CFG.notifications) && CFG.notifications.length ? CFG.notifications
|
||
: [{ text: open + " записей ждут решения", time: "сейчас", unread: true }]).filter(n => n && n.text);
|
||
$("notifList").innerHTML = notifs.map(n =>
|
||
'<div class="kt-ai-notif-item" data-unread="' + (n.unread !== false) + '">' + esc(n.text) +
|
||
'<span class="time">' + esc(n.time || "") + "</span></div>").join("");
|
||
const unread = notifs.filter(n => n.unread !== false).length;
|
||
$("notifBadge").style.display = unread ? "" : "none";
|
||
}
|
||
/* Степпер этапов (для мастера создания). config.steps = {items:[...]} */
|
||
function stepperHtml(cur) {
|
||
const st = CFG.steps;
|
||
if (!st || !Array.isArray(st.items) || !st.items.length) return "";
|
||
return '<div class="kt-ai-stepper case-steps" style="flex-wrap:nowrap">' + st.items.map((x, i) => {
|
||
const state = i + 1 < cur ? "done" : (i + 1 === cur ? "current" : "");
|
||
return '<div class="kt-ai-step-item"' + (state ? ' data-state="' + state + '"' : "") + '><span class="n">' +
|
||
(i + 1 < cur ? "✓" : (i + 1)) + "</span>" + esc(x) + "</div>" +
|
||
(i < st.items.length - 1 ? '<span class="kt-ai-step-sep"></span>' : "");
|
||
}).join("") + "</div>";
|
||
}
|
||
|
||
/* Мастер создания: этап 1 – заявка + документы (dropzone из ДС) */
|
||
S.newCase = () => {
|
||
S.closeMenus();
|
||
const entity = CFG.product.entity || "запись";
|
||
$("modal").innerHTML = '<div class="kt-ai-modal"><div class="modal-title">' +
|
||
esc(createLabel) + "</div>" + stepperHtml(1) +
|
||
'<div class="kt-ai-dropzone" id="caseDz" ondragover="event.preventDefault();this.dataset.drag=\'true\'" ondragleave="this.dataset.drag=\'\'" ondrop="event.preventDefault();this.dataset.drag=\'\';S.caseAttach()" onclick="S.caseAttach()">' +
|
||
'<svg class="kt-icon kt-icon-lg" viewBox="0 0 24 24"><path d="M12 16V4m0 0l-4 4m4-4l4 4"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/></svg>' +
|
||
'Перетащите заявку или документы<br><span class="kt-ai-meta">заявка из Лотуса, регламент, шаблон – агент извлечёт данные</span></div>' +
|
||
'<div id="caseFiles"></div>' +
|
||
'<div class="modal-actions"><button class="kt-ai-btn" onclick="S.closeModal()">Отмена</button>' +
|
||
'<button class="kt-ai-btn" data-variant="primary" id="caseSubmit" disabled onclick="S.caseCreate()">Создать черновик</button></div></div>';
|
||
$("modal").style.display = "flex";
|
||
};
|
||
S.caseAttach = () => {
|
||
if ($("caseFiles").children.length) return;
|
||
$("caseFiles").innerHTML = '<div class="kt-ai-file-row case-file">' +
|
||
'<svg class="kt-icon" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>' +
|
||
"Заявка_" + esc(CFG.product.entity || "документ") + '.docx <span class="size">96 КБ</span></div>';
|
||
$("caseSubmit").disabled = false;
|
||
};
|
||
S.caseCreate = () => {
|
||
S.closeModal();
|
||
toast("Черновик создан – агент готовит " + esc(CFG.product.entity || "запись"), "ok");
|
||
const b = $("notifBadge");
|
||
b.style.display = ""; b.textContent = (parseInt(b.textContent, 10) || 0) + 1;
|
||
$("notifList").insertAdjacentHTML("afterbegin",
|
||
'<div class="kt-ai-notif-item" data-unread="true">Новый ' + esc(CFG.product.entity || "объект") + ': агент извлекает данные из заявки<span class="time">сейчас</span></div>');
|
||
};
|
||
|
||
/* Орб рисует kt-ai-orb.js — один файл на оба рантайма.
|
||
Здесь лежала вторая копия анимации, целиком мёртвая: она искала $("orbCanvas"),
|
||
а такого id в разметке нет — у канваса шапки только класс. Копия не выполнялась
|
||
ни разу, но ехала в каждый прототип и расходилась с настоящим орбом (свои
|
||
константы, своя палитра, свой цикл кадров). kt-ai-orb.js монтирует все
|
||
canvas.kt-ai-orb сам, на DOMContentLoaded. */
|
||
document.addEventListener("click", e => { if (!e.target.closest(".kt-ai-menu-anchor")) S.closeMenus(); });
|
||
|
||
// Заголовок группы (`nav[].group`) — НЕ пункт: он не кликается и в нумерации
|
||
// не участвует. Переход идёт по индексу пункта (S.nav(i)), поэтому индекс
|
||
// берётся из исходного массива, а заголовок только вставляется перед пунктом,
|
||
// у которого группа сменилась. Вставить заголовок отдельным пунктом было
|
||
// нельзя — он сдвинул бы нумерацию, и state.navIndex перестал бы совпадать
|
||
// с разделом.
|
||
function renderNav() {
|
||
document.title = CFG.product.name + " · прототип KT AI";
|
||
let группа = null;
|
||
$("nav").innerHTML = (CFG.nav || [{ label: "Главная" }]).map((n, i) => {
|
||
let head = "";
|
||
if (n.group && n.group !== группа) {
|
||
// data-rail="keep" — чтобы в полосе значков заголовок не исчез вместе со
|
||
// своим разделителем: структурное правило рейла снимает всё без иконки.
|
||
head = '<div class="kt-ai-nav-group" data-rail="keep"><span class="nav-label">' + esc(n.group) + "</span></div>";
|
||
}
|
||
if (n.group) группа = n.group; else группа = null;
|
||
return head +
|
||
'<div class="kt-ai-nav-item" role="link" tabindex="0" data-active="' + (i === state.navIndex) + '"' + (i === state.navIndex ? ' aria-current="page"' : "") + ' onclick="S.nav(' + i + ')" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();S.nav(' + i + ')}">' +
|
||
'<svg class="kt-icon" aria-hidden="true"><use href="#' + esc(n.icon || "lots") + '"></use></svg>' +
|
||
'<span class="nav-label">' + esc(n.label) + "</span>" +
|
||
(n.count != null ? '<span class="count">' + esc(n.count) + "</span>" : "") +
|
||
"</div>";
|
||
}).join("") +
|
||
// v4.0 (Michelle): одноразделный прототип – сайдбар не пустует, тихо объясняет агента
|
||
(((CFG.nav || []).length <= 1 && CFG.scenario && CFG.scenario.confirmText)
|
||
? '<div style="margin:var(--kt-ai-space-6) var(--kt-ai-space-4) 0;padding:var(--kt-ai-space-4) var(--kt-ai-space-5);border:1px solid var(--kt-ai-border);border-radius:var(--kt-ai-radius-lg);background:var(--kt-ai-bg-elevated)">' +
|
||
'<div class="kt-ai-label" style="margin-bottom:4px">Контроль человека</div>' +
|
||
'<div class="kt-ai-meta" style="line-height:1.5">' + esc(CFG.scenario.confirmText) + "</div></div>"
|
||
: "");
|
||
}
|
||
|
||
// Сводный bar-chart над таблицей (видео Kole Jain: «data drives the form»).
|
||
// chart.groupBy=<колонка> → бары считаются из фильтрованных строк и адаптируются к выбранному табу.
|
||
function renderChart(ch, rowsForChart) {
|
||
if (!ch) return "";
|
||
let data = ch.bars || [], total = ch.total, cat = false;
|
||
if (ch.bucket === "day" && ch.dateKey) {
|
||
// раскат по дням за выбранный диапазон; rowsForChart уже отфильтрованы (диапазон+таб+поиск)
|
||
const db = dateBounds(), buckets = {};
|
||
if (db) for (let d = new Date(db.start); d <= db.end; d.setDate(d.getDate() + 1)) buckets[d.toISOString().slice(0, 10)] = 0;
|
||
(rowsForChart || []).forEach(r => { const d = parseDate(r[ch.dateKey]); if (d) { const k = d.toISOString().slice(0, 10); if (k in buckets) buckets[k]++; } });
|
||
const keys = Object.keys(buckets).sort();
|
||
data = keys.map(k => ({ label: String(+k.slice(8, 10)), value: buckets[k] }));
|
||
total = String((rowsForChart || []).length);
|
||
} else if (ch.groupBy) {
|
||
const g = {};
|
||
(rowsForChart || []).forEach(r => { const v = r[ch.groupBy]; const k = (v == null || v === "") ? "—" : String(v); g[k] = (g[k] || 0) + 1; });
|
||
data = Object.keys(g).map(k => ({ label: k, value: g[k] }));
|
||
total = String((rowsForChart || []).length);
|
||
cat = true;
|
||
}
|
||
if (!data.length) return "";
|
||
const colorVar = ({ blue: "--kt-ai-chart-blue", green: "--kt-ai-chart-green", orange: "--kt-ai-chart-orange", salmon: "--kt-ai-chart-salmon", pink: "--kt-ai-chart-pink", primary: "--kt-ai-primary" })[ch.color] || "--kt-ai-chart-blue";
|
||
const max = Math.max(1, ...data.map(b => +b.value || 0));
|
||
const bars = data.map(b => '<div class="cc-bar"' + tipAttr(b) + '><i style="height:' + Math.round((+b.value || 0) / max * 100) + '%"></i></div>').join("");
|
||
let axis;
|
||
if (cat || data.length <= 8) {
|
||
axis = '<div class="cc-axis cc-axis-cat">' + data.map(b => '<span title="' + esc(b.label || "") + '">' + esc(b.label || "") + "</span>").join("") + "</div>";
|
||
} else {
|
||
const n = data.length, step = Math.max(1, Math.ceil(n / 7));
|
||
axis = '<div class="cc-axis">' + data.map((b, i) => (i % step === 0 || i === n - 1) ? "<span>" + esc(b.label || "") + "</span>" : "").filter(Boolean).join("") + "</div>";
|
||
}
|
||
return '<section class="kt-ai-chart-card" style="--cc-color:var(' + colorVar + ')">' +
|
||
'<div class="cc-head"><div><div class="cc-title">' + esc(ch.title || "") + "</div>" +
|
||
(total ? '<div class="cc-total"><span class="num">' + esc(total) + "</span>" + (ch.unit ? '<span class="unit">' + esc(ch.unit) + "</span>" : "") + "</div>" : "") + "</div>" +
|
||
(ch.link ? '<button class="cc-link" type="button" onclick="toast(\'' + esc(ch.link) + " – заглушка прототипа','')\">" + esc(ch.link) + ' →</button>' : "") + "</div>" +
|
||
'<div class="cc-bars">' + bars + "</div>" + axis + "</section>";
|
||
}
|
||
|
||
function renderMain() {
|
||
const sc = CFG.scenario || {};
|
||
const segs = ['<button class="seg" data-active="' + (state.statusFilter === "") + '" onclick="S.filter(\'\')">Все</button>']
|
||
.concat(filterKeys.map(k =>
|
||
'<button class="seg" data-active="' + (state.statusFilter === k) + '" onclick="S.filter(\'' + escJs(k) + '\')">' + esc(statuses[k].label) + "</button>"));
|
||
const vis = visibleRows();
|
||
const pageSize = (CFG.table && CFG.table.pageSize) || 12;
|
||
const pageCount = Math.max(1, Math.ceil(vis.length / pageSize));
|
||
if (!state.page || state.page > pageCount) state.page = 1;
|
||
const pageRows = vis.slice((state.page - 1) * pageSize, state.page * pageSize);
|
||
const selVisible = vis.filter(r => state.selected.has(r._i)).length;
|
||
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
let html = "";
|
||
|
||
// системный баннер «что-почему-что делать» + действие (не статичный текст)
|
||
if (CFG.banner && CFG.banner.text) {
|
||
const b = CFG.banner;
|
||
const act = b.action ? '<button class="kt-ai-btn" data-size="sm" onclick="' +
|
||
(b.actionNav != null ? "S.nav(" + b.actionNav + ")" : "S.bannerAction()") + '">' + esc(b.action) + " →</button>" : "";
|
||
html += '<div class="kt-ai-banner" data-tone="' + esc(b.tone || "info") +
|
||
'" style="margin-bottom:var(--kt-ai-stack-group);display:flex;align-items:center;gap:var(--kt-ai-space-5)">' +
|
||
'<span style="flex:1;min-width:0">' + esc(b.text) + "</span>" + act + "</div>";
|
||
}
|
||
|
||
if (CFG.kpis && CFG.kpis.length) {
|
||
html += '<div class="kt-ai-kpi-strip" data-cards="true">' + CFG.kpis.map(k => kpiHtml(k)).join("") + "</div>";
|
||
}
|
||
|
||
// Табы НАД графиком — график адаптируется к выбранному табу (раскат по фильтрованным строкам)
|
||
// Универсальный фильтр диапазона дат (если у графика есть dateKey) — фильтрует и таблицу, и график
|
||
const curRange = state.dateRange == null ? 30 : state.dateRange;
|
||
const rangeCtl = (CFG.chart && CFG.chart.dateKey) ? '<div class="kt-ai-segmented range-seg">' +
|
||
[[7, "7 дней"], [30, "30 дней"], [90, "90 дней"], [0, "Всё"]].map(o => '<button class="seg" data-active="' + (curRange === o[0]) + '" onclick="S.setRange(' + o[0] + ')">' + o[1] + "</button>").join("") + "</div>" : "";
|
||
html += '<div class="toolbar-row"><div class="kt-ai-segmented">' + segs.join("") + '</div><div class="spacer"></div>' +
|
||
rangeCtl + '<span class="kt-ai-meta">' + vis.length + " из " + rows.length + "</span></div>";
|
||
|
||
html += renderChart(CFG.chart, vis);
|
||
|
||
html += '<div class="kt-ai-table-wrap" data-framed="true">' +
|
||
'<div class="kt-ai-table-bulkbar" data-open="' + (selVisible > 0) + '"><span class="count">' + selVisible + ' выбрано</span>' +
|
||
'<button class="kt-ai-btn" data-size="sm" onclick="S.bulkApprove()">' + esc(sc.control || "Подтвердить") + "</button>" +
|
||
'<button class="kt-ai-btn" data-size="sm" data-variant="ghost" onclick="S.clearSel()">Снять выбор</button></div>';
|
||
|
||
if (!vis.length) {
|
||
// первый запуск (данных ещё нет) отличаем от «фильтр ничего не нашёл»
|
||
const zeroData = rows.length === 0;
|
||
const icon = zeroData
|
||
? '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M9 16h6"/>'
|
||
: '<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>';
|
||
html += '<div class="kt-ai-empty"><div class="icon-spot"><svg class="kt-icon kt-icon-lg" viewBox="0 0 24 24">' + icon + '</svg></div>' +
|
||
'<span class="title">' + (zeroData ? "Пока нет записей" : "Ничего не найдено") + '</span>' +
|
||
'<span>' + (zeroData ? "Здесь появятся объекты, как только агент их обработает" : "Измените поиск или фильтр статуса") + '</span></div></div>';
|
||
} else {
|
||
// Ширина колонки = по содержимому (из ВСЕХ строк → стабильно при фильтрах), с клампом по типу. column.width — явный override.
|
||
const colw = c => {
|
||
if (c.width) return c.width;
|
||
if (c.type === "num") return "92px";
|
||
if (c.type === "date") return "116px";
|
||
if (c.type === "status") {
|
||
const keys = (c.values && c.values.length) ? c.values : Object.keys(statuses);
|
||
const m = Math.max(7, ...keys.map(k => String((statuses[k] || {}).label || k).length));
|
||
return Math.min(180, Math.round(m * 7.2 + 42)) + "px";
|
||
}
|
||
let m = String(c.label || "").length;
|
||
for (const r of rows) {
|
||
m = Math.max(m, String(r[c.key] == null ? "" : r[c.key]).length);
|
||
if (c.sub) m = Math.max(m, String(r[c.sub] == null ? "" : r[c.sub]).length);
|
||
}
|
||
return Math.min(300, Math.max(88, Math.round(m * 7 + 28))) + "px";
|
||
};
|
||
const kebab = '<svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="12" cy="19" r="1.4"/></svg>';
|
||
html += '<table class="kt-ai-table" style="table-layout:fixed">' +
|
||
'<colgroup><col style="width:40px">' + cols.map(c => "<col" + (colw(c) ? ' style="width:' + colw(c) + '"' : "") + ">").join("") + '<col style="width:48px"></colgroup>' +
|
||
'<thead><tr><th scope="col"><span class="kt-ai-sr-only">Выбор</span></th>' +
|
||
cols.map(c => {
|
||
const a = c.align || (c.type === "num" ? "right" : "left"); // выравнивание: num по умолчанию справа, column.align — override
|
||
const align = a !== "left" ? ' style="text-align:' + a + '"' : "";
|
||
if (!c.sortable) return '<th scope="col"' + align + ">" + esc(c.label) + "</th>";
|
||
const on = state.sort && state.sort.key === c.key, dir = on ? state.sort.dir : "";
|
||
return '<th scope="col"' + align + (on ? ' aria-sort="' + (dir === "asc" ? "ascending" : "descending") + '"' : "") +
|
||
'><button class="th-sort" type="button" onclick="S.sort(\'' + escJs(c.key) + '\')">' + esc(c.label) +
|
||
'<svg class="kt-icon th-sort-ic" data-dir="' + dir + '"><use href="#' + (on ? "chevronDown" : "arrowUpDown") + '"></use></svg></button></th>';
|
||
}).join("") + '<th scope="col"><span class="kt-ai-sr-only">Действия</span></th>' +
|
||
"</tr></thead><tbody>" +
|
||
pageRows.map(r => '<tr data-dim="' + (statusCol && statuses[r[statusCol]] && statuses[r[statusCol]].dim ? "true" : "false") + '" onclick="S.open(' + r._i + ')" style="cursor:pointer">' +
|
||
'<td class="cell-select" onclick="event.stopPropagation()"><label class="kt-ai-checkbox"><input type="checkbox" ' + (state.selected.has(r._i) ? "checked" : "") + ' onchange="S.sel(' + r._i + ',this.checked)"></label></td>' +
|
||
cols.map(c => { const a = c.align || (c.type === "num" ? "right" : "left"); return '<td data-label="' + esc(c.label) + '"' + (c.type === "num" ? ' class="num"' : "") + (a !== "left" ? ' style="text-align:' + a + '"' : "") + ">" + cellHtml(r, c) + "</td>"; }).join("") +
|
||
'<td class="row-act" onclick="event.stopPropagation()"><button class="kt-ai-icon-button row-kebab" aria-label="Действия" onclick="S.rowMenu(' + r._i + ',event)">' + kebab + "</button></td>" +
|
||
"</tr>").join("") +
|
||
"</tbody></table></div>";
|
||
if (pageCount > 1) {
|
||
let pages = "";
|
||
for (let p = 1; p <= pageCount; p++) pages += '<button class="page-btn" data-active="' + (p === state.page) + '" onclick="S.page(' + p + ')">' + p + "</button>";
|
||
const from = (state.page - 1) * pageSize + 1, to = Math.min(state.page * pageSize, vis.length);
|
||
html += '<div class="table-foot"><span class="kt-ai-meta">' + from + "–" + to + " из " + vis.length + "</span>" +
|
||
'<div class="kt-ai-pagination"><button class="page-btn" ' + (state.page === 1 ? "disabled" : "") + ' aria-label="Назад" onclick="S.page(' + (state.page - 1) + ')">‹</button>' +
|
||
pages + '<button class="page-btn" ' + (state.page === pageCount ? "disabled" : "") + ' aria-label="Вперёд" onclick="S.page(' + (state.page + 1) + ')">›</button></div></div>';
|
||
}
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function renderStub(label) {
|
||
$("pageTitle").textContent = label;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
return '<div class="kt-ai-card section-stub" style="margin-top:0"><div class="kt-ai-empty">' +
|
||
'<div class="icon-spot"><svg class="kt-icon kt-icon-lg" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18"/></svg></div>' +
|
||
'<span class="title">Раздел обозначен в концепте</span>' +
|
||
'<span>Детализирован будет после подтверждения сценария «' + esc((CFG.scenario || {}).headline || "") + '»</span>' +
|
||
'<button class="kt-ai-btn" data-size="sm" onclick="S.nav(0)">К главному сценарию</button></div></div>';
|
||
}
|
||
|
||
// Документ-артефакт (что продукт ПРОИЗВОДИТ): карточка с иконкой, статусом и кнопкой «Скачать».
|
||
function docCard(dd, objName) {
|
||
const fmt = dd.format ? String(dd.format).toUpperCase() : "";
|
||
const sub = [objName, dd.status, fmt].filter(Boolean).map(esc).join(" · ");
|
||
const dlName = (dd.label + (objName ? " — " + objName : "")).replace(/['"]/g, "");
|
||
return '<div class="kt-ai-doc"><span class="doc-ic"><svg class="kt-icon"><use href="#file"></use></svg></span>' +
|
||
'<span class="doc-meta"><span class="doc-name">' + esc(dd.label) + "</span>" +
|
||
(sub ? '<span class="doc-sub">' + sub + "</span>" : "") + "</span>" +
|
||
'<button class="kt-ai-btn" data-size="sm" onclick="S.downloadDoc(\'' + esc(dlName) + '\')"><svg class="kt-icon kt-icon-sm"><use href="#download"></use></svg>Скачать</button></div>';
|
||
}
|
||
// Секция-коллекция документов одного типа по всем записям (nav.view="documents", nav.docType).
|
||
function renderDocs(navItem) {
|
||
const docType = navItem.docType;
|
||
const titleKey = (CFG.drawer && CFG.drawer.titleKey) || (cols[0] && cols[0].key);
|
||
const items = [];
|
||
rows.forEach(r => (Array.isArray(r._docs) ? r._docs : []).forEach(dd => { if (!docType || dd.type === docType) items.push({ dd, name: r[titleKey] }); }));
|
||
if (!items.length) return '<div class="kt-ai-card" style="margin-top:0"><div class="kt-ai-empty">' +
|
||
'<div class="icon-spot"><svg class="kt-icon kt-icon-lg"><use href="#file"></use></svg></div>' +
|
||
'<span class="title">Документов пока нет</span><span>Появятся, когда агент сформирует их по предложениям</span></div></div>';
|
||
return '<div class="kt-ai-doc-list">' + items.map(it => docCard(it.dd, it.name)).join("") + "</div>";
|
||
}
|
||
S.downloadDoc = name => {
|
||
try {
|
||
const blob = new Blob(["KT AI — прототип\n\n" + name + "\n\nВ продукте здесь будет сформированный документ.\n"], { type: "text/plain;charset=utf-8" });
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = name.replace(/[^0-9A-Za-zА-Яа-яЁё]+/g, "_").slice(0, 60) + ".txt";
|
||
document.body.appendChild(a); a.click(); a.remove();
|
||
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
||
} catch (e) {}
|
||
toast("Скачивание: " + name, "ok");
|
||
};
|
||
|
||
// Архетип экрана из реестра archetypes/registry.json (открытый набор).
|
||
// Дефолт queue. Если архетип неизвестен/не реализован — подбираем безопасную форму
|
||
// по наличию данных, а не ломаем экран: есть секции → custom, есть таблица → queue, иначе → custom.
|
||
|
||
// document: документ с правками агента по фрагментам (принять/отклонить каждый флаг)
|
||
function renderDocument() {
|
||
const doc = CFG.document || {}, sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || doc.title || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
if (!state.fragState) state.fragState = {};
|
||
const frs = Array.isArray(doc.fragments) ? doc.fragments : [];
|
||
const edits = frs.filter(f => f.op && f.op !== "keep").length, flags = frs.filter(f => f.flag).length;
|
||
let h = '<div style="max-width:820px;margin:0 auto;display:flex;flex-direction:column;gap:var(--kt-ai-stack-group)">';
|
||
h += bannerHtml();
|
||
h += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)"><span class="kt-ai-label">Что сделал агент</span>' +
|
||
'<div style="margin-top:var(--kt-ai-space-3)">' + esc(doc.source ? "Собрал документ: " + doc.source + ". " : "") +
|
||
"Правок: " + edits + ", пометок риска: " + flags + ". Проверьте выделенные фрагменты.</div></div>";
|
||
if (!frs.length) return h + '<div class="kt-ai-empty"><span class="title">Документ пока пуст</span><span>Здесь появится текст с правками агента</span></div></div>';
|
||
h += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-8)"><div class="kt-ai-h2" style="margin-bottom:var(--kt-ai-space-6)">' + esc(doc.title || "Документ") + "</div>";
|
||
h += frs.map((f, i) => {
|
||
const st = state.fragState[i];
|
||
let b = '<div style="margin-bottom:var(--kt-ai-space-6);border-left:2px solid ' + (f.flag ? "var(--kt-ai-status-warn-fg)" : "var(--kt-ai-border)") + ';padding-left:var(--kt-ai-space-5)">';
|
||
if (f.heading) b += '<div class="kt-ai-h3" style="margin-bottom:var(--kt-ai-space-2)">' + esc(f.heading) + "</div>";
|
||
if (f.op === "edit" && f.was) b += '<div class="kt-ai-diff" style="margin-bottom:var(--kt-ai-space-2)"><span class="line" data-op="del">' + esc(f.was) + '</span><span class="line" data-op="add">' + esc(f.text) + "</span></div>";
|
||
else if (f.op === "add") b += '<div class="kt-ai-diff" style="margin-bottom:var(--kt-ai-space-2)"><span class="line" data-op="add">' + esc(f.text) + "</span></div>";
|
||
else if (f.op === "del") b += '<div class="kt-ai-diff" style="margin-bottom:var(--kt-ai-space-2)"><span class="line" data-op="del">' + esc(f.text) + "</span></div>";
|
||
else b += "<div>" + esc(f.text) + "</div>";
|
||
if (f.flag) {
|
||
b += '<div class="kt-ai-banner" data-tone="warn" style="margin-top:var(--kt-ai-space-2)">' + esc(f.flag) + "</div>";
|
||
b += '<div style="display:flex;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-2)">' +
|
||
(st === "ok" ? '<span class="kt-ai-status-pill" data-status="ok">Принято</span>'
|
||
: st === "no" ? '<span class="kt-ai-status-pill" data-status="risk">Отклонено</span>'
|
||
: '<button class="kt-ai-btn" data-size="sm" data-variant="primary" onclick="S.fragOk(' + i + ')">Принять правку</button>' +
|
||
'<button class="kt-ai-btn" data-size="sm" onclick="S.fragNo(' + i + ')">Отклонить</button>') + "</div>";
|
||
}
|
||
return b + "</div>";
|
||
}).join("");
|
||
h += "</div>";
|
||
h += '<div style="display:flex;gap:var(--kt-ai-space-4)">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.customAction()">' + esc(sc.control || "Подтвердить документ") + "</button>" +
|
||
(sc.reject ? '<button class="kt-ai-btn" onclick="S.customAction()">' + esc(sc.reject) + "</button>" : "") + "</div>";
|
||
return h + "</div>";
|
||
}
|
||
S.fragOk = i => { state.fragState[i] = "ok"; render(); toast("Правка принята", "ok"); };
|
||
S.fragNo = i => { state.fragState[i] = "no"; render(); toast("Правка отклонена — фрагмент вернётся к исходному", "warn"); };
|
||
|
||
// copilot: чат слева + живой артефакт-результат справа
|
||
function renderCopilot() {
|
||
const ai = CFG.ai || {}, art = CFG.artifact || {}, sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
let chat = '<div class="kt-ai-chat" id="copChat" data-fill="true" role="log" aria-live="polite" aria-label="Диалог с агентом">' + msgHtml("assistant", ai.intro || "Здравствуйте! Опишите, что подготовить.") + "</div>";
|
||
let comp = "";
|
||
if (ai.suggestions && ai.suggestions.length)
|
||
comp += '<div class="kt-ai-suggestions" style="margin:var(--kt-ai-space-3) 0">' +
|
||
ai.suggestions.map(s => '<button class="kt-ai-suggestion" onclick="S.aiAsk(this.textContent,\'copChat\')">' + esc(s) + "</button>").join("") + "</div>";
|
||
comp += '<div class="kt-ai-prompt-bar"><textarea rows="1" id="copInput" aria-label="Сообщение агенту" placeholder="Попросить агента…" onkeydown="if(event.key===\'Enter\'&&!event.shiftKey){event.preventDefault();S.aiAsk(this.value,\'copChat\');this.value=\'\'}"></textarea>' +
|
||
'<button class="kt-ai-prompt-send" aria-label="Отправить" onclick="var i=document.getElementById(\'copInput\');S.aiAsk(i.value,\'copChat\');i.value=\'\'"><svg class="kt-icon"><use href="#send"></use></svg></button></div>';
|
||
const typeLabel = { document: "Документ", slides: "Слайды", report: "Отчёт", table: "Таблица", letter: "Письмо" }[art.type] || "Результат";
|
||
let right = '<div class="kt-ai-card" style="padding:var(--kt-ai-space-6);display:flex;flex-direction:column;gap:var(--kt-ai-space-4);min-width:0">' +
|
||
'<div style="display:flex;justify-content:space-between;align-items:center;gap:var(--kt-ai-space-3)">' +
|
||
'<span class="kt-ai-label">' + esc(typeLabel) + (art.version ? " · " + esc(art.version) : "") + "</span>" +
|
||
'<span class="kt-ai-status-pill" data-status="ok">Черновик готов</span></div>' +
|
||
'<div class="kt-ai-h3">' + esc(art.title || "") + "</div>" +
|
||
'<div style="white-space:pre-wrap;font-size:var(--kt-ai-text-sm);line-height:1.6;max-height:420px;overflow:auto;border:1px solid var(--kt-ai-border);border-radius:var(--kt-ai-radius-lg);padding:var(--kt-ai-space-5);background:var(--kt-ai-bg-soft)">' + esc(art.preview || "") + "</div>" +
|
||
'<div style="display:flex;gap:var(--kt-ai-space-3)">' +
|
||
(Array.isArray(art.actions) && art.actions.length ? art.actions : [sc.control || "Скачать"]).slice(0, 3).map((a, j) =>
|
||
'<button class="kt-ai-btn" data-size="sm"' + (j === 0 ? ' data-variant="primary"' : "") + ' onclick="S.customAction()">' + esc(a) + "</button>").join("") + "</div></div>";
|
||
let h = '<div class="kt-ai-cols" style="grid-template-columns:minmax(320px,1fr) minmax(320px,1.1fr);align-items:stretch">' +
|
||
'<div style="display:flex;flex-direction:column;min-height:420px">' + bannerHtml("var(--kt-ai-space-4)") +
|
||
chat + '<div style="margin-top:auto">' + comp + "</div></div>" + right + "</div>";
|
||
return h;
|
||
}
|
||
|
||
// wizard: пошаговая заявка с автозаполнением агентом
|
||
function renderWizard() {
|
||
const wz = CFG.wizard || {}, sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
const steps = Array.isArray(wz.steps) ? wz.steps : [];
|
||
if (state.wizStep == null) state.wizStep = Math.min(Math.max((wz.current || 1) - 1, 0), steps.length - 1);
|
||
const cur = state.wizStep, stp = steps[cur] || { title: "", fields: [] };
|
||
// референс ElevenLabs (publishing flow): вертикальный степпер слева с описаниями, контент справа
|
||
let h = '<div class="kt-ai-cols" style="max-width:860px;margin:0 auto;grid-template-columns:230px minmax(0,1fr);gap:var(--kt-ai-space-9)">';
|
||
h += '<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-5)">' + steps.map((x, i) => {
|
||
const st = i < cur ? "done" : (i === cur ? "current" : "");
|
||
return '<div class="kt-ai-step-item"' + (st ? ' data-state="' + st + '"' : "") + ' style="align-items:flex-start"><span class="n" style="flex-shrink:0">' + (i < cur ? "✓" : i + 1) + "</span>" +
|
||
'<span><span style="display:block;font-size:var(--kt-ai-text-sm)">' + esc(x.title) + "</span>" +
|
||
(x.desc ? '<span style="display:block;margin-top:2px;font-size:var(--kt-ai-text-2xs);color:var(--kt-ai-fg-faint);line-height:1.4">' + esc(x.desc) + "</span>" : "") + "</span></div>";
|
||
}).join("") + "</div>";
|
||
h += '<div style="display:flex;flex-direction:column;gap:var(--kt-ai-stack-group)">';
|
||
h += bannerHtml();
|
||
h += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-7);display:flex;flex-direction:column;gap:var(--kt-ai-space-5)">' +
|
||
'<div class="kt-ai-h3">' + esc(stp.title) + "</div>" +
|
||
(stp.fields || []).map(f =>
|
||
'<label class="kt-ai-field"><span class="cap" style="display:flex;gap:var(--kt-ai-space-2);align-items:center">' + esc(f.label) +
|
||
(f.prefill ? '<span class="kt-ai-chip" data-ai="true">заполнил агент</span>' : "") + "</span>" +
|
||
'<input class="kt-ai-input" value="' + esc(String(f.value == null ? "" : f.value)) + '">' +
|
||
(f.hint ? '<span class="hint">' + esc(f.hint) + "</span>" : "") + "</label>").join("") + "</div>";
|
||
h += '<div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-4)">' +
|
||
'<button class="kt-ai-btn"' + (cur === 0 ? " disabled" : "") + ' onclick="S.wizPrev()">‹ Назад</button>' +
|
||
(cur < steps.length - 1
|
||
? '<button class="kt-ai-btn" data-variant="primary" onclick="S.wizNext()">Далее</button>'
|
||
: '<button class="kt-ai-btn" data-variant="primary" onclick="S.customAction()">' + esc(sc.control || "Отправить заявку") + "</button>") + "</div>";
|
||
h += "</div>";
|
||
return h + "</div>";
|
||
}
|
||
S.wizPrev = () => { if (state.wizStep > 0) { state.wizStep--; render(); } };
|
||
S.wizNext = () => { state.wizStep++; render(); };
|
||
|
||
|
||
// kanban: доска статусов – колонки из CFG.statuses, карточки = строки table.rows (клик открывает drawer)
|
||
function renderKanban() {
|
||
const d = CFG.drawer || {};
|
||
const titleKey = d.titleKey || (cols[0] || {}).key, subKey = (cols[1] || {}).key;
|
||
const colKeys = Object.keys(statuses);
|
||
let h = renderHead();
|
||
h += '<div style="display:flex;gap:var(--kt-ai-space-4);align-items:flex-start;overflow-x:auto;padding-bottom:var(--kt-ai-space-4);margin-top:var(--kt-ai-space-6)">';
|
||
h += colKeys.map(k => {
|
||
const st = statuses[k] || { label: k, tone: "info" };
|
||
const cards = rows.filter(r => statusCol && String(r[statusCol]) === k);
|
||
return '<div style="flex:1 1 0;min-width:230px;background:var(--kt-ai-bg-soft);border:1px solid var(--kt-ai-border);border-radius:var(--kt-ai-radius-xl);padding:var(--kt-ai-space-4)">' +
|
||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--kt-ai-space-4)">' + statusTag(k) +
|
||
'<span class="kt-ai-meta">' + cards.length + "</span></div>" +
|
||
(cards.length ? cards.map(r => {
|
||
const flags = r._flags || [];
|
||
return '<div class="kt-ai-card kt-ai-hover-lift" role="button" tabindex="0" onclick="S.open(' + r._i + ')" onkeydown="if(event.key===\'Enter\'){S.open(' + r._i + ')}" style="cursor:pointer;padding:var(--kt-ai-space-4);margin-bottom:var(--kt-ai-space-3)">' +
|
||
'<div style="font-size:var(--kt-ai-text-sm);font-weight:500">' + esc(r[titleKey]) + "</div>" +
|
||
(subKey && r[subKey] != null ? '<div class="kt-ai-meta" style="margin-top:2px">' + esc(r[subKey]) + "</div>" : "") +
|
||
(flags.length ? '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-3);display:flex;align-items:baseline;gap:5px;color:var(--kt-ai-status-warn-fg)">' +
|
||
'<svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24" style="flex-shrink:0;align-self:center"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>' +
|
||
'<span class="kt-ai-truncate" style="min-width:0">' + esc(flags[0]) + "</span>" + (flags.length > 1 ? '<span style="flex-shrink:0">+' + (flags.length - 1) + "</span>" : "") + "</div>" : "") +
|
||
"</div>";
|
||
}).join("") : '<div class="kt-ai-meta" style="padding:var(--kt-ai-space-4);text-align:center">Пусто</div>') + "</div>";
|
||
}).join("");
|
||
return h + "</div>";
|
||
}
|
||
|
||
// inbox: список + постоянная панель чтения (обращения, письма) – выбранная запись читается справа
|
||
function renderInbox() {
|
||
const sc = CFG.scenario || {}, d = CFG.drawer || {}, ib = CFG.inbox || {};
|
||
const titleKey = ib.titleKey || d.titleKey || (cols[0] || {}).key;
|
||
if (state.inboxIdx == null || !rows[state.inboxIdx]) state.inboxIdx = (attentionFirst()[0] || rows[0] || {})._i || 0;
|
||
const cur = rows[state.inboxIdx];
|
||
let h = renderHead();
|
||
if (!cur) return h + '<div class="kt-ai-empty"><span class="title">Пока нет записей</span></div>';
|
||
h += '<div class="kt-ai-cols" style="grid-template-columns:300px minmax(0,1fr);margin-top:var(--kt-ai-space-6)">';
|
||
h += '<div style="border:1px solid var(--kt-ai-border);border-radius:var(--kt-ai-radius-xl);overflow:hidden">' +
|
||
rows.map(r => '<div class="kt-ai-row-hover" role="button" tabindex="0" onclick="S.inboxSel(' + r._i + ')" onkeydown="if(event.key===\'Enter\'){S.inboxSel(' + r._i + ')}" style="cursor:pointer;border-radius:0;padding:var(--kt-ai-space-4) var(--kt-ai-space-5);border-bottom:1px solid var(--kt-ai-border)' +
|
||
(r._i === cur._i ? ";background:var(--kt-ai-bg-active)" : "") + '">' +
|
||
'<div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-3);align-items:baseline">' +
|
||
'<span class="kt-ai-truncate" style="font-size:var(--kt-ai-text-sm);font-weight:500">' + esc(r[titleKey]) + "</span>" +
|
||
(ib.timeKey && r[ib.timeKey] != null ? '<span class="kt-ai-meta" style="flex-shrink:0;font-size:var(--kt-ai-text-2xs)">' + esc(r[ib.timeKey]) + "</span>" : "") + "</div>" +
|
||
'<div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-3);margin-top:2px;align-items:center">' +
|
||
(ib.subKey && r[ib.subKey] != null ? '<span class="kt-ai-meta kt-ai-truncate">' + esc(r[ib.subKey]) + "</span>" : "<span></span>") +
|
||
(statusCol ? statusTag(r[statusCol]) : "") + "</div></div>").join("") + "</div>";
|
||
const flags = cur._flags || [];
|
||
h += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)">' +
|
||
'<div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-4);align-items:flex-start"><div class="kt-ai-h2">' + esc(cur[titleKey]) + "</div>" + (statusCol ? statusTag(cur[statusCol]) : "") + "</div>" +
|
||
'<div style="margin:var(--kt-ai-space-4) 0 var(--kt-ai-space-5)">' +
|
||
(d.fields && d.fields.length ? d.fields.map(f => '<div class="kt-ai-prop"><span class="k">' + esc(f.label) + '</span><span class="v">' + esc(fmtVal(cur[f.key]) ?? "–") + "</span></div>").join("") : "") + "</div>" +
|
||
(ib.bodyKey && cur[ib.bodyKey] != null ? '<div style="white-space:pre-wrap;font-size:var(--kt-ai-text-sm);line-height:1.65;border:1px solid var(--kt-ai-border);border-radius:var(--kt-ai-radius-lg);padding:var(--kt-ai-space-5);background:var(--kt-ai-bg-soft)">' + esc(cur[ib.bodyKey]) + "</div>" : "") +
|
||
(flags.length ? '<div class="kt-ai-label" style="margin-top:var(--kt-ai-space-5)">' + esc(d.flagsLabel || "Разбор агента") + "</div>" +
|
||
flags.map(f => '<div class="kt-ai-banner" data-tone="info" style="margin-top:var(--kt-ai-space-2)">' + esc(f) + "</div>").join("") : "") +
|
||
'<div class="drawer-actions"><button class="kt-ai-btn" data-variant="primary" onclick="state.openRow=' + cur._i + ';S.askApprove()">' + esc(sc.control || "Подтвердить") + "</button>" +
|
||
(sc.reject ? '<button class="kt-ai-btn" onclick="state.openRow=' + cur._i + ';S.reject()">' + esc(sc.reject) + "</button>" : "") + "</div></div>";
|
||
return h + "</div>";
|
||
}
|
||
S.inboxSel = i => { state.inboxIdx = i; render(); };
|
||
|
||
// analytics: аналитический разбор – сводка агента, чарты (bar/line/donut на чистом SVG/CSS), выводы с тонами
|
||
|
||
// переиспользуемый line-чарт (analytics и dashboard): полилиния + область + сетка + подписи оси
|
||
function lineChartSvg(pts, o) {
|
||
const W = o.W || 300, H = o.H || 120, color = o.color || "var(--kt-ai-chart-blue)";
|
||
const n = pts.length, x0 = 8, y0 = 12, w = W - 16, hh = H - 36;
|
||
const vals = pts.map(p => +p.value || 0);
|
||
const max = Math.max(1, ...vals), min = Math.min(...vals), span = (max - min) || 1;
|
||
const px = i => x0 + (n > 1 ? i * (w / (n - 1)) : w / 2), py = v => y0 + hh - ((v - min) / span) * hh;
|
||
const line = pts.map((p, i) => (i ? "L" : "M") + px(i).toFixed(1) + " " + py(+p.value || 0).toFixed(1)).join(" ");
|
||
const area = line + " L" + px(n - 1).toFixed(1) + " " + (y0 + hh) + " L" + px(0).toFixed(1) + " " + (y0 + hh) + " Z";
|
||
return '<svg viewBox="0 0 ' + W + " " + H + '" style="width:100%;height:auto;display:block" role="img" aria-label="' + esc(o.title || "") + '">' +
|
||
[0, .5, 1].map(f => '<line x1="' + x0 + '" y1="' + (y0 + hh * f).toFixed(1) + '" x2="' + (x0 + w) + '" y2="' + (y0 + hh * f).toFixed(1) + '" stroke="var(--kt-ai-border)" stroke-width="1" stroke-dasharray="2 3"/>').join("") +
|
||
'<path d="' + area + '" fill="' + color + '" opacity="0.10"/>' +
|
||
'<path d="' + line + '" fill="none" stroke="' + color + '" stroke-width="2" stroke-linejoin="round"/>' +
|
||
pts.map((p, i) => '<circle cx="' + px(i).toFixed(1) + '" cy="' + py(+p.value || 0).toFixed(1) + '" r="' + (W > 400 ? 3 : 2.6) + '" fill="' + color + '"' + tipAttr(p) + "></circle>").join("") +
|
||
pts.map((p, i) => { const step = Math.max(1, Math.ceil(n / (W > 400 ? 12 : 7))); if (i % step !== 0 && i !== n - 1) return ""; const anchor = i === 0 ? "start" : i === n - 1 ? "end" : "middle"; return '<text x="' + px(i).toFixed(1) + '" y="' + (H - 6) + '" text-anchor="' + anchor + '" font-size="' + (W > 400 ? 11 : 9) + '" fill="var(--kt-ai-fg-faint)">' + esc(p.label) + "</text>"; }).join("") +
|
||
'<text x="' + (W > 400 ? x0 : x0 + w) + '" y="' + (y0 - 3) + '"' + (W > 400 ? "" : ' text-anchor="end"') + ' font-size="' + (W > 400 ? 11 : 9) + '" fill="var(--kt-ai-fg-faint)">' + esc(String(max)) + (o.unit ? " " + esc(o.unit) : "") + "</text></svg>";
|
||
}
|
||
const ANALYTICS_PALETTE = ["--kt-ai-chart-blue", "--kt-ai-chart-green", "--kt-ai-chart-orange", "--kt-ai-chart-salmon", "--kt-ai-chart-pink"];
|
||
function tipAttr(b) {
|
||
// Подсказка марки. hint приходит от продукта ГОТОВЫМ — ничего не считаем и не
|
||
// форматируем: сервер и браузер округляют по-разному, и разошедшийся формат
|
||
// это разошедшееся число. Без hint показываем то же, что показывал title.
|
||
var v = b && b.hint ? JSON.stringify(b.hint) : ((b && b.label ? b.label + ": " : "") + (b ? b.value : ""));
|
||
return ' data-kt-tip="' + esc(v) + '" tabindex="0"';
|
||
}
|
||
|
||
function analyticsChartCard(ch, idx) {
|
||
const pts = Array.isArray(ch.points) ? ch.points.filter(p => p && p.label != null) : [];
|
||
const vals = pts.map(p => +p.value || 0);
|
||
const max = Math.max(1, ...vals);
|
||
const color = "var(" + (ANALYTICS_PALETTE[idx % ANALYTICS_PALETTE.length]) + ")";
|
||
const toneColor = t => toneFg(t, color);
|
||
let body = "";
|
||
if (ch.type === "line") {
|
||
body = lineChartSvg(pts, { color: color, unit: ch.unit, title: ch.title });
|
||
} else if (ch.type === "donut") {
|
||
const total = vals.reduce((a, b) => a + b, 0) || 1;
|
||
let off = 25, segs = "";
|
||
pts.forEach((p, i) => {
|
||
const share = (+p.value || 0) / total * 100;
|
||
const c = p.tone ? toneColor(p.tone) : "var(" + ANALYTICS_PALETTE[i % ANALYTICS_PALETTE.length] + ")";
|
||
segs += '<circle r="15.9155" cx="21" cy="21" fill="none" stroke="' + c + '" stroke-width="5.5" stroke-dasharray="' + share.toFixed(2) + " " + (100 - share).toFixed(2) + '" stroke-dashoffset="' + off.toFixed(2) + '"' + tipAttr(p) + "></circle>";
|
||
off -= share;
|
||
});
|
||
body = '<div style="display:flex;gap:var(--kt-ai-space-6);align-items:center">' +
|
||
'<svg viewBox="0 0 42 42" style="width:118px;height:118px;flex-shrink:0" role="img" aria-label="' + esc(ch.title) + '">' + segs +
|
||
'<text x="21" y="21.5" text-anchor="middle" dominant-baseline="middle" font-size="8" font-weight="600" fill="var(--kt-ai-fg)">' + esc(ch.total != null ? String(ch.total) : String(total)) + "</text></svg>" +
|
||
'<div style="flex:1;min-width:0">' + pts.map((p, i) => {
|
||
const c = p.tone ? toneColor(p.tone) : "var(" + ANALYTICS_PALETTE[i % ANALYTICS_PALETTE.length] + ")";
|
||
return '<div style="display:flex;align-items:center;gap:var(--kt-ai-space-3);padding:3px 0;font-size:var(--kt-ai-text-xs)">' +
|
||
'<span style="width:9px;height:9px;border-radius:3px;background:' + c + ';flex-shrink:0"></span>' +
|
||
'<span style="flex:1;min-width:0;line-height:1.3">' + esc(p.label) + "</span>" +
|
||
'<span class="kt-ai-num" style="font-weight:600">' + esc(p.value) + "</span>" +
|
||
'<span class="kt-ai-meta" style="width:38px;text-align:right">' + Math.round((+p.value || 0) / total * 100) + "%</span></div>";
|
||
}).join("") + "</div></div>";
|
||
} else { // bar (горизонтальные полосы: подпись | полоса | значение – читаемее для категорий)
|
||
body = pts.map(p => {
|
||
const c = p.tone ? toneColor(p.tone) : color;
|
||
return '<div class="kt-ai-row-hover" style="display:grid;grid-template-columns:minmax(90px,32%) 1fr 56px;gap:var(--kt-ai-space-3);align-items:center;padding:3px 0;font-size:var(--kt-ai-text-xs)">' +
|
||
'<span style="line-height:1.3">' + esc(p.label) + "</span>" +
|
||
'<span style="height:8px;border-radius:4px;background:var(--kt-ai-bg-soft);overflow:hidden;display:block"><span style="display:block;height:100%;border-radius:4px;width:' + Math.round((+p.value || 0) / max * 100) + "%;background:" + c + '"></span></span>' +
|
||
'<span class="kt-ai-num" style="text-align:right;font-weight:600">' + esc(p.value) + (ch.unit ? ' <span class="kt-ai-meta" style="font-weight:400">' + esc(ch.unit) + "</span>" : "") + "</span></div>";
|
||
}).join("");
|
||
}
|
||
return '<section class="kt-ai-chart-card" style="margin-bottom:0"><div class="cc-head"><div><div class="cc-title">' + esc(ch.title || "") + "</div>" +
|
||
(ch.total != null && ch.type !== "donut" ? '<div class="cc-total"><span class="num">' + esc(ch.total) + "</span>" + (ch.unit ? '<span class="unit">' + esc(ch.unit) + "</span>" : "") + "</div>" : "") + "</div></div>" +
|
||
body + (ch.note ? '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-4)">' + esc(ch.note) + "</div>" : "") + "</section>";
|
||
}
|
||
function renderAnalytics() {
|
||
const an = CFG.analytics || {}, sc = CFG.scenario || {};
|
||
const charts = Array.isArray(an.charts) ? an.charts : [];
|
||
const finds = Array.isArray(an.findings) ? an.findings : [];
|
||
let h = renderHead();
|
||
if (an.summary) h += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-6);margin-top:var(--kt-ai-space-6)"><div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-4);align-items:baseline"><span class="kt-ai-label">Что увидел агент</span>' +
|
||
(an.period ? '<span class="kt-ai-meta">' + esc(an.period) + "</span>" : "") + "</div>" +
|
||
'<div style="margin-top:var(--kt-ai-space-3);line-height:1.6">' + esc(an.summary) + "</div></div>";
|
||
if (charts.length) h += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--kt-ai-space-6);margin-top:var(--kt-ai-space-6)">' +
|
||
charts.map((ch, i) => analyticsChartCard(ch, i)).join("") + "</div>";
|
||
if (finds.length) {
|
||
h += '<div class="kt-ai-h3" style="margin:var(--kt-ai-space-8) 0 var(--kt-ai-space-4)">Выводы агента</div>';
|
||
h += finds.map(f => '<div class="kt-ai-banner" data-tone="' + esc(f.tone || "info") + '" style="margin-bottom:var(--kt-ai-space-3);display:flex;gap:var(--kt-ai-space-4);align-items:baseline">' +
|
||
'<span style="flex:1;min-width:0"><strong>' + esc(f.title) + "</strong>" + (f.detail ? " — " + esc(f.detail) : "") + "</span>" +
|
||
(f.value != null ? '<span class="kt-ai-num" style="font-weight:600;flex-shrink:0">' + esc(f.value) + "</span>" : "") + "</div>").join("");
|
||
}
|
||
if (sc.control) h += '<div style="margin-top:var(--kt-ai-space-6)"><button class="kt-ai-btn" data-variant="primary" onclick="S.customAction()">' + esc(sc.control) + "</button></div>";
|
||
return h;
|
||
}
|
||
|
||
// calendar: расписание по датам – события сгруппированы по дням (окна работ, дежурства, сроки)
|
||
const CAL_MONTHS = ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"];
|
||
const CAL_DAYS = ["вс", "пн", "вт", "ср", "чт", "пт", "сб"];
|
||
function renderCalendar() {
|
||
const cal = CFG.calendar || {};
|
||
const evs = (Array.isArray(cal.events) ? cal.events : []).filter(e => e && e.date && e.title).slice().sort((a, b) => String(a.date + (a.time || "")).localeCompare(String(b.date + (b.time || ""))));
|
||
const byDay = {};
|
||
evs.forEach(e => { (byDay[e.date] = byDay[e.date] || []).push(e); });
|
||
let h = renderHead();
|
||
h += '<div style="display:flex;justify-content:space-between;align-items:baseline;margin:var(--kt-ai-space-6) 0 var(--kt-ai-space-4)"><div class="kt-ai-h3">' + esc(cal.title || "Расписание") + "</div>" +
|
||
(cal.range ? '<span class="kt-ai-meta">' + esc(cal.range) + "</span>" : "") + "</div>";
|
||
h += Object.keys(byDay).sort().map(dk => {
|
||
const m = dk.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||
const dt = m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
|
||
const head = dt ? (+m[3]) + " " + CAL_MONTHS[dt.getMonth()] + ", " + CAL_DAYS[dt.getDay()] : dk;
|
||
return '<div style="margin-bottom:var(--kt-ai-space-5)"><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-3)">' + esc(head) + "</div>" +
|
||
byDay[dk].map(e => {
|
||
const tone = e.tone || "info";
|
||
const edge = toneFg(tone, "var(--kt-ai-border-strong)");
|
||
return '<div class="kt-ai-card kt-ai-hover-lift" style="padding:var(--kt-ai-space-4) var(--kt-ai-space-5);margin-bottom:var(--kt-ai-space-3);border-left:3px solid ' + edge + ';display:flex;gap:var(--kt-ai-space-5);align-items:baseline">' +
|
||
'<span class="kt-ai-num" style="flex-shrink:0;width:88px;font-size:var(--kt-ai-text-xs)">' + esc(e.time || "весь день") + "</span>" +
|
||
'<span style="flex:1;min-width:0"><span style="font-size:var(--kt-ai-text-sm);font-weight:500">' + esc(e.title) + "</span>" +
|
||
(e.note ? '<span class="kt-ai-meta" style="display:block;margin-top:2px">' + esc(e.note) + "</span>" : "") + "</span>" +
|
||
(e.badge ? '<span class="kt-ai-status-pill" data-status="' + esc(tone) + '">' + esc(e.badge) + "</span>" : "") + "</div>";
|
||
}).join("") + "</div>";
|
||
}).join("");
|
||
if (!evs.length) h += '<div class="kt-ai-empty"><span class="title">Событий пока нет</span></div>';
|
||
return h;
|
||
}
|
||
|
||
// map: схема-карта участка – точки (0-100 по обеим осям) со статусами, линии-связи; НЕ геокарта с тайлами
|
||
function renderMap() {
|
||
const mp = CFG.map || {};
|
||
const pts = (Array.isArray(mp.points) ? mp.points : []).filter(p => p && p.label != null);
|
||
const toneOf = p => (p.status && statuses[p.status] ? statuses[p.status].tone : p.tone) || "info";
|
||
const fillOf = t => toneFg(t, "var(--kt-ai-fg-muted)");
|
||
const px = v => 40 + (Math.max(0, Math.min(100, +v || 0)) / 100) * 920; // viewBox 1000x600: подписи рендерятся ~13px
|
||
const py = v => 60 + (Math.max(0, Math.min(100, +v || 0)) / 100) * 480;
|
||
let svg = "";
|
||
(Array.isArray(mp.lines) ? mp.lines : []).forEach(l => {
|
||
const a = pts[l[0]], b = pts[l[1]];
|
||
if (a && b) svg += '<line x1="' + px(a.x).toFixed(1) + '" y1="' + py(a.y).toFixed(1) + '" x2="' + px(b.x).toFixed(1) + '" y2="' + py(b.y).toFixed(1) + '" stroke="var(--kt-ai-border-strong)" stroke-width="4"/>';
|
||
});
|
||
pts.forEach(p => {
|
||
const t = toneOf(p);
|
||
const tx = Math.max(110, Math.min(890, px(p.x))); // подпись не вылезает за край схемы
|
||
svg += '<circle data-map-pt="true" cx="' + px(p.x).toFixed(1) + '" cy="' + py(p.y).toFixed(1) + '" r="9" fill="' + fillOf(t) + '"' + (t === "risk" ? ' stroke="var(--kt-ai-status-risk-fg)" stroke-opacity="0.35" stroke-width="8"' : "") + "><title>" + esc(p.label + (p.note ? " — " + p.note : "")) + "</title></circle>" +
|
||
'<text x="' + tx.toFixed(1) + '" y="' + (py(p.y) - 18).toFixed(1) + '" text-anchor="middle" font-size="12" fill="var(--kt-ai-fg-muted)">' + esc(p.label) + "</text>";
|
||
});
|
||
const att = pts.filter(p => /warn|risk/.test(toneOf(p)));
|
||
let h = renderHead();
|
||
h += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-6);margin-top:var(--kt-ai-space-6)">' +
|
||
(mp.title ? '<div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-4)">' + esc(mp.title) + "</div>" : "") +
|
||
'<svg viewBox="0 0 1000 600" style="width:100%;height:auto;display:block;background-image:radial-gradient(var(--kt-ai-border) 0.5px,transparent 0.5px);background-size:8px 8px;border-radius:var(--kt-ai-radius-lg)" role="img" aria-label="' + esc(mp.title || "Схема участка") + '">' + svg + "</svg>" +
|
||
'<div style="display:flex;gap:var(--kt-ai-space-5);margin-top:var(--kt-ai-space-4);flex-wrap:wrap">' +
|
||
[["ok", "в норме"], ["warn", "внимание"], ["risk", "проблема"]].map(l => '<span class="kt-ai-meta" style="display:flex;align-items:center;gap:5px"><span style="width:8px;height:8px;border-radius:50%;background:' + fillOf(l[0]) + '"></span>' + l[1] + "</span>").join("") + "</div></div>";
|
||
if (att.length) {
|
||
h += '<div class="kt-ai-h3" style="margin:var(--kt-ai-space-7) 0 var(--kt-ai-space-4)">Требует внимания</div>';
|
||
h += att.map(p => '<div class="kt-ai-banner" data-tone="' + (toneOf(p) === "risk" ? "risk" : "warn") + '" style="margin-bottom:var(--kt-ai-space-3)"><strong>' + esc(p.label) + "</strong>" + (p.note ? " — " + esc(p.note) : "") + "</div>").join("");
|
||
}
|
||
return h;
|
||
}
|
||
|
||
// timeline: хронология дела/инцидента – вертикальная лента событий с акторами и тонами
|
||
function renderTimeline() {
|
||
const tl = CFG.timeline || {};
|
||
const evs = (Array.isArray(tl.events) ? tl.events : []).filter(e => e && e.title);
|
||
const dotOf = t => toneFg(t, "var(--kt-ai-fg-faint)");
|
||
let h = renderHead();
|
||
if (tl.title) h += '<div class="kt-ai-h3" style="margin:var(--kt-ai-space-6) 0 var(--kt-ai-space-4)">' + esc(tl.title) + "</div>";
|
||
h += '<div style="max-width:720px;margin-top:var(--kt-ai-space-5)">' + evs.map((e, i) => {
|
||
const last = i === evs.length - 1;
|
||
return '<div style="display:grid;grid-template-columns:96px 20px minmax(0,1fr);gap:var(--kt-ai-space-3)">' +
|
||
'<span class="kt-ai-num" style="font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted);text-align:right;padding-top:2px">' + esc(e.time || "") + "</span>" +
|
||
'<span style="display:flex;flex-direction:column;align-items:center"><span style="width:10px;height:10px;border-radius:50%;background:' + dotOf(e.tone) + ";margin-top:4px;flex-shrink:0" +
|
||
(e.current ? ";box-shadow:0 0 0 3px var(--kt-ai-bg-active)" : "") + '"></span>' +
|
||
(last ? "" : '<span style="flex:1;width:1px;background:var(--kt-ai-border);margin-top:3px"></span>') + "</span>" +
|
||
'<div style="padding-bottom:' + (last ? "0" : "var(--kt-ai-space-6)") + '">' +
|
||
'<div style="display:flex;gap:var(--kt-ai-space-3);align-items:baseline;flex-wrap:wrap"><span style="font-size:var(--kt-ai-text-sm);font-weight:' + (e.current ? "600" : "500") + '">' + esc(e.title) + "</span>" +
|
||
(e.actor ? '<span class="kt-ai-chip"' + (/агент/i.test(e.actor) ? ' data-ai="true"' : "") + ">" + esc(e.actor) + "</span>" : "") + "</div>" +
|
||
(e.text ? '<div class="kt-ai-meta" style="margin-top:3px;line-height:1.55">' + esc(e.text) + "</div>" : "") + "</div></div>";
|
||
}).join("") + "</div>";
|
||
if (!evs.length) h += '<div class="kt-ai-empty"><span class="title">Событий пока нет</span></div>';
|
||
return h;
|
||
}
|
||
|
||
function mainByLayout() {
|
||
const lay = (CFG.archetype || CFG.layout || "queue");
|
||
if (lay === "queue") return renderMain();
|
||
if (lay === "cockpit") return renderCockpit();
|
||
if (lay === "dashboard") return renderDashboard();
|
||
if (lay === "compare") return renderCompare();
|
||
if (lay === "conversational") return renderConversational();
|
||
if (lay === "custom") return renderCustom();
|
||
if (lay === "document") return renderDocument();
|
||
if (lay === "copilot") return renderCopilot();
|
||
if (lay === "wizard") return renderWizard();
|
||
if (lay === "kanban") return renderKanban();
|
||
if (lay === "inbox") return renderInbox();
|
||
if (lay === "analytics") return renderAnalytics();
|
||
if (lay === "calendar") return renderCalendar();
|
||
if (lay === "map") return renderMap();
|
||
if (lay === "timeline") return renderTimeline();
|
||
// неизвестный / planned архетип → не падаем, выбираем форму под данные
|
||
if (Array.isArray(CFG.sections) && CFG.sections.length) return renderCustom();
|
||
if (CFG.table && CFG.table.rows && CFG.table.rows.length) return renderMain();
|
||
return renderCustom();
|
||
}
|
||
function attentionFirst() {
|
||
const att = rows.filter(r => statusCol && statuses[r[statusCol]] && /warn|risk/.test(statuses[r[statusCol]].tone || ""));
|
||
return att.length ? att : rows;
|
||
}
|
||
function plainTable(list) {
|
||
if (!list.length) return '<div class="kt-ai-empty"><span class="title">Пока нет записей</span><span>Здесь появятся объекты, как только агент их обработает</span></div>';
|
||
return '<div class="kt-ai-table-wrap"><table class="kt-ai-table"><thead><tr>' +
|
||
cols.map(c => "<th" + (c.type === "num" ? ' style="text-align:right"' : "") + ">" + esc(c.label) + "</th>").join("") +
|
||
'</tr></thead><tbody>' +
|
||
list.map(r => '<tr onclick="S.open(' + r._i + ')" style="cursor:pointer">' +
|
||
cols.map(c => "<td" + (c.type === "num" ? ' class="num"' : "") + ">" + cellHtml(r, c) + "</td>").join("") + "</tr>").join("") +
|
||
'</tbody></table></div>';
|
||
}
|
||
const TONE_FG = { ok: "var(--kt-ai-status-ok-fg)", warn: "var(--kt-ai-status-warn-fg)", risk: "var(--kt-ai-status-risk-fg)" };
|
||
function toneFg(t, dflt) { return TONE_FG[t] || dflt; }
|
||
function bannerHtml(mb) {
|
||
if (!(CFG.banner && CFG.banner.text)) return "";
|
||
return '<div class="kt-ai-banner" data-tone="' + esc(CFG.banner.tone || "info") + '"' + (mb ? ' style="margin-bottom:' + mb + '"' : "") + '>' + esc(CFG.banner.text) + "</div>";
|
||
}
|
||
function fmtVal(v) {
|
||
// ISO-дата не должна протекать в интерфейс: 2026-07-21 -> 21.07.2026
|
||
const m = String(v == null ? "" : v).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
return m ? m[3] + "." + m[2] + "." + m[1] : v;
|
||
}
|
||
// hint — «из чего считано» (40 знаков: знаменатель, доля, период).
|
||
// note — «чем это число НЕ является»: оговорка о поле-источнике. Потолка у неё
|
||
// нет и она не прячется под клик: полосу плиток видят все, и оговорка обязана
|
||
// стоять там же, где число, иначе её читают не все (см. .kt-ai-kpi .note).
|
||
function kpiHtml(k, extra) {
|
||
const long = String(k.value == null ? "" : k.value).length > 12;
|
||
return '<div class="kt-ai-kpi"' + (extra || "") + '><span class="value"' + (long ? ' data-long="true"' : "") + '>' + esc(k.value) + '</span><span class="label">' + esc(k.label) + "</span>" + (k.hint ? '<span class="hint">' + esc(k.hint) + "</span>" : "") + (k.note ? '<span class="note">' + esc(k.note) + "</span>" : "") + "</div>";
|
||
}
|
||
|
||
// Вывод раздела: последний блок экрана, после таблицы. Заголовок необязателен,
|
||
// абзацы разделяются пустой строкой — вывод бывает длиннее одной мысли.
|
||
function conclusionHtml() {
|
||
const c = CFG.conclusion;
|
||
if (!c || !c.text) return "";
|
||
const абзацы = String(c.text).split(/\n{2,}/).map(t => t.trim()).filter(Boolean);
|
||
return '<section class="kt-ai-conclusion">' +
|
||
(c.title ? '<span class="title">' + esc(c.title) + "</span>" : "") +
|
||
абзацы.map(t => '<span class="text">' + esc(t) + "</span>").join("") + "</section>";
|
||
}
|
||
function renderHead() {
|
||
const sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
let h = "";
|
||
h += bannerHtml("var(--kt-ai-stack-group)");
|
||
if (CFG.kpis && CFG.kpis.length) h += '<div class="kt-ai-kpi-strip" data-cards="true">' + CFG.kpis.map(k => kpiHtml(k)).join("") + "</div>";
|
||
return h;
|
||
}
|
||
// dashboard: метрики наверху + список «требует внимания» (управление состоянием процесса)
|
||
function renderDashboard() {
|
||
const att = rows.filter(r => statusCol && statuses[r[statusCol]] && /warn|risk/.test(statuses[r[statusCol]].tone || ""));
|
||
const db = CFG.dashboard || {};
|
||
const series = Array.isArray(db.series) ? db.series.filter(s => s && Array.isArray(s.points) && s.points.length >= 2) : [];
|
||
if (!series.length) {
|
||
// простой дашборд (без блока dashboard): KPI + список внимания
|
||
return renderHead() +
|
||
'<div class="kt-ai-h3" style="margin:var(--kt-ai-space-8) 0 var(--kt-ai-space-4)">' + (att.length ? "Требует внимания" : "Записи") + "</div>" +
|
||
plainTable(att.length ? att : rows);
|
||
}
|
||
// референс ElevenLabs dashboard: KPI-карточки = табы, выбранная метрика управляет большим чартом
|
||
const sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
let h = "";
|
||
if (CFG.banner && CFG.banner.text) {
|
||
const b = CFG.banner;
|
||
const act = b.action ? '<button class="kt-ai-btn" data-size="sm" onclick="' + (b.actionNav != null ? "S.nav(" + b.actionNav + ")" : "S.bannerAction()") + '">' + esc(b.action) + " →</button>" : "";
|
||
h += '<div class="kt-ai-banner" data-tone="' + esc(b.tone || "info") + '" style="margin-bottom:var(--kt-ai-stack-group);display:flex;align-items:center;gap:var(--kt-ai-space-5)"><span style="flex:1;min-width:0">' + esc(b.text) + "</span>" + act + "</div>";
|
||
}
|
||
// KPI-табы: series[i].kpi — индекс метрики в CFG.kpis (по умолчанию i)
|
||
const kpis = Array.isArray(CFG.kpis) ? CFG.kpis : [];
|
||
const kpiToSeries = {};
|
||
series.forEach((s, si) => { const ki = s.kpi != null ? +s.kpi : si; if (kpiToSeries[ki] == null && kpis[ki]) kpiToSeries[ki] = si; });
|
||
if (state.dashKpi == null || kpiToSeries[state.dashKpi] == null) state.dashKpi = +Object.keys(kpiToSeries)[0];
|
||
h += '<div class="kt-ai-kpi-strip" data-cards="true">' + kpis.map((k, ki) => {
|
||
const clickable = kpiToSeries[ki] != null, active = clickable && ki === state.dashKpi;
|
||
return kpiHtml(k, clickable ? ' data-tab="true" data-active="' + active + '" role="tab" aria-selected="' + active + '" tabindex="0" onclick="S.dashKpi(' + ki + ')" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();S.dashKpi(' + ki + ')}"' : "");
|
||
}).join("") + "</div>";
|
||
const cur = series[kpiToSeries[state.dashKpi]];
|
||
// без дубля цифры: значение уже на выбранной KPI-карточке (референс ElevenLabs)
|
||
h += '<section class="kt-ai-chart-card"><div class="cc-head"><div><div class="cc-title">' + esc(cur.title || (kpis[state.dashKpi] || {}).label || "") + "</div></div>" +
|
||
(db.range ? '<span class="kt-ai-meta">' + esc(db.range) + "</span>" : "") + "</div>" +
|
||
lineChartSvg(cur.points, { W: 1160, H: 290, color: "var(--kt-ai-chart-blue)", unit: cur.unit, title: cur.title }) +
|
||
(cur.note ? '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-4)">' + esc(cur.note) + "</div>" : "") + "</section>";
|
||
// нижняя сетка разборов: cards (мини-карточки статистики) / share (доли с процент-барами) / top (топ-N с баром за подписью)
|
||
const brs = Array.isArray(db.breakdowns) ? db.breakdowns : [];
|
||
if (brs.length) h += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:var(--kt-ai-space-6)">' + brs.map(br => {
|
||
const items = Array.isArray(br.items) ? br.items : [];
|
||
let body = "";
|
||
if (br.type === "cards") {
|
||
body = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--kt-ai-space-4)">' + items.map(it =>
|
||
'<div style="border:1px solid var(--kt-ai-border);border-radius:var(--kt-ai-radius-lg);padding:var(--kt-ai-space-4)">' +
|
||
'<div style="font-size:var(--kt-ai-text-sm);font-weight:500;margin-bottom:var(--kt-ai-space-3)">' + esc(it.title) + "</div>" +
|
||
(Array.isArray(it.stats) ? it.stats : []).map(st => '<div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-3);padding:2px 0;font-size:var(--kt-ai-text-xs)"><span class="kt-ai-meta">' + esc(st.label) + '</span><span class="kt-ai-num">' + esc(st.value) + "</span></div>").join("") + "</div>").join("") + "</div>";
|
||
} else if (br.type === "share") {
|
||
const total = items.reduce((a, b) => a + (+b.value || 0), 0) || 1;
|
||
body = items.map(it => {
|
||
const pct = Math.round((+it.value || 0) / total * 100);
|
||
return '<div class="kt-ai-row-hover" style="display:grid;grid-template-columns:minmax(90px,38%) 1fr 44px;gap:var(--kt-ai-space-3);align-items:center;padding:4px 0;font-size:var(--kt-ai-text-xs)">' +
|
||
'<span style="line-height:1.3">' + esc(it.label) + "</span>" +
|
||
'<span style="height:7px;border-radius:4px;background:var(--kt-ai-bg-soft);overflow:hidden;display:block"><span style="display:block;height:100%;border-radius:4px;width:' + pct + '%;background:var(--kt-ai-primary)"></span></span>' +
|
||
'<span class="kt-ai-num" style="text-align:right">' + pct + "%</span></div>";
|
||
}).join("");
|
||
} else { // top: бар-трек за подписью, значение справа (референс «Top called paths»)
|
||
const max = Math.max(1, ...items.map(it => +it.value || 0));
|
||
body = items.map(it =>
|
||
'<div class="kt-ai-row-hover" style="display:flex;align-items:center;gap:var(--kt-ai-space-3);padding:3px 0;font-size:var(--kt-ai-text-xs)">' +
|
||
'<span style="flex:1;position:relative;border-radius:var(--kt-ai-radius-sm);overflow:hidden;padding:4px 8px;background:var(--kt-ai-bg)">' +
|
||
'<span style="position:absolute;inset:0;width:' + Math.round((+it.value || 0) / max * 100) + '%;background:var(--kt-ai-bg-soft);border-radius:var(--kt-ai-radius-sm)"></span>' +
|
||
'<span style="position:relative">' + esc(it.label) + "</span></span>" +
|
||
'<span class="kt-ai-num" style="width:56px;text-align:right">' + esc(it.value) + (br.unit ? ' <span class="kt-ai-meta">' + esc(br.unit) + "</span>" : "") + "</span></div>").join("");
|
||
}
|
||
return '<section class="kt-ai-chart-card" style="margin-bottom:0"><div class="cc-head"><div class="cc-title">' + esc(br.title) + "</div></div>" + body +
|
||
(br.note ? '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-4)">' + esc(br.note) + "</div>" : "") + "</section>";
|
||
}).join("") + "</div>";
|
||
if (att.length) {
|
||
const regIdx = (CFG.nav || []).findIndex(n => n && n.view === "registry");
|
||
h += '<div style="display:flex;justify-content:space-between;align-items:baseline;margin:var(--kt-ai-space-8) 0 var(--kt-ai-space-4)"><div class="kt-ai-h3">Требует внимания</div>' +
|
||
(regIdx > 0 ? '<button class="cc-link" type="button" onclick="S.nav(' + regIdx + ')">' + esc((CFG.nav[regIdx] || {}).label || "Все записи") + " →</button>" : "") + "</div>" +
|
||
plainTable(att.slice(0, 5));
|
||
if (att.length > 5 && regIdx > 0) h += '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-3)">Ещё ' + (att.length - 5) + ' — в разделе «' + esc((CFG.nav[regIdx] || {}).label) + "»</div>";
|
||
}
|
||
return h;
|
||
}
|
||
S.dashKpi = ki => { state.dashKpi = ki; render(); };
|
||
// cockpit: один фокусный объект с сигналами и решением + компактная очередь сбоку (мониторинг/исключения)
|
||
function renderCockpit() {
|
||
const sc = CFG.scenario || {}, d = CFG.drawer || {};
|
||
const focal = attentionFirst()[0] || rows[0];
|
||
let html = renderHead();
|
||
if (!focal) return html + '<div class="kt-ai-empty"><span class="title">Пока нет объектов</span></div>';
|
||
const flags = focal._flags || [];
|
||
html += '<div style="display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:var(--kt-ai-space-6);margin-top:var(--kt-ai-space-6)">';
|
||
html += '<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)"><div class="kt-ai-h2">' + esc(focal[d.titleKey || cols[0].key]) + "</div>" +
|
||
'<div style="margin:6px 0 var(--kt-ai-space-5)">' + (statusCol ? statusTag(focal[statusCol]) : "") + "</div>" +
|
||
(d.fields && d.fields.length ? d.fields : cols).map(f => '<div class="kt-ai-prop"><span class="k">' + esc(f.label) + '</span><span class="v">' + esc(fmtVal(focal[f.key]) ?? "–") + "</span></div>").join("") +
|
||
(flags.length ? '<div class="kt-ai-label" style="margin-top:var(--kt-ai-space-5)">' + esc(d.flagsLabel || "Проверки агента") + "</div>" +
|
||
flags.map(f => '<div class="kt-ai-banner" data-tone="info" style="margin-top:var(--kt-ai-space-2)">' + esc(f) + "</div>").join("") : "") +
|
||
'<div class="drawer-actions"><button class="kt-ai-btn" data-variant="primary" onclick="state.openRow=' + focal._i + ';S.askApprove()">' + esc(sc.control || "Подтвердить") + "</button>" +
|
||
(sc.reject ? '<button class="kt-ai-btn" onclick="state.openRow=' + focal._i + ';S.reject()">' + esc(sc.reject) + "</button>" : "") +
|
||
"</div></div>";
|
||
html += '<div><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-3)">Очередь</div>' +
|
||
rows.map(r => '<div class="kt-ai-nav-item" onclick="S.open(' + r._i + ')" style="cursor:pointer;justify-content:space-between;gap:var(--kt-ai-space-3)"><span class="kt-ai-truncate">' + esc(r[d.titleKey || cols[0].key]) + "</span>" + (statusCol ? statusTag(r[statusCol]) : "") + "</div>").join("") + "</div>";
|
||
return html + "</div>";
|
||
}
|
||
|
||
// compare: сверка двух источников по парам полей, расхождения подсвечены (аудит/финансы/договоры)
|
||
function renderCompare() {
|
||
const sc = CFG.scenario || {}, d = CFG.drawer || {}, cmp = CFG.compare || {};
|
||
const pairs = Array.isArray(cmp.pairs) ? cmp.pairs : [];
|
||
if (!pairs.length) return renderMain(); // защита: нет пар → надёжная очередь
|
||
const focal = rows[state.focusRow] || attentionFirst()[0] || rows[0];
|
||
let html = renderHead();
|
||
if (!focal) return html + '<div class="kt-ai-empty"><span class="title">Пока нет объектов для сверки</span></div>';
|
||
const mism = p => String(focal[p.a] ?? "") !== String(focal[p.b] ?? "");
|
||
// в узких панелях сверки поле стопкой (метка над значением) – фикс. ширина .kt-ai-prop тут ломает перенос
|
||
const panel = (label, side) => '<div class="kt-ai-card" style="padding:var(--kt-ai-space-6)"><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-4)">' + esc(label) + "</div>" +
|
||
pairs.map(p => '<div style="padding:6px 8px;border-radius:var(--kt-ai-radius-md);margin-bottom:2px' + (mism(p) ? ';background:var(--kt-ai-status-warn-bg)' : '') + '">' +
|
||
'<div class="kt-ai-meta" style="font-size:var(--kt-ai-text-2xs)">' + esc(p.label) + '</div>' +
|
||
'<div style="font-size:var(--kt-ai-text-sm);margin-top:1px">' + esc(fmtVal(focal[side === "a" ? p.a : p.b]) ?? "–") + '</div></div>').join("") + "</div>";
|
||
const flags = focal._flags || [];
|
||
html += '<div style="display:grid;grid-template-columns:230px minmax(0,1fr);gap:var(--kt-ai-space-6);margin-top:var(--kt-ai-space-6)">';
|
||
html += '<div><div class="kt-ai-label" style="margin-bottom:var(--kt-ai-space-3)">Объекты сверки</div>' +
|
||
rows.map(r => '<div class="kt-ai-nav-item" onclick="S.compareFocus(' + r._i + ')" style="cursor:pointer;justify-content:space-between;gap:var(--kt-ai-space-3)' + (r._i === focal._i ? ";background:var(--kt-ai-bg-active)" : "") + '"><span class="kt-ai-truncate">' + esc(r[d.titleKey || cols[0].key]) + "</span>" + (statusCol ? statusTag(r[statusCol]) : "") + "</div>").join("") + "</div>";
|
||
html += '<div><div class="kt-ai-h2" style="margin-bottom:var(--kt-ai-space-4)">' + esc(focal[d.titleKey || cols[0].key]) + "</div>" +
|
||
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--kt-ai-space-4)">' + panel(cmp.aLabel || "Источник A", "a") + panel(cmp.bLabel || "Источник B", "b") + "</div>" +
|
||
(flags.length ? '<div class="kt-ai-label" style="margin-top:var(--kt-ai-space-5)">' + esc(d.flagsLabel || "Расхождения") + "</div>" + flags.map(f => '<div class="kt-ai-banner" data-tone="warn" style="margin-top:var(--kt-ai-space-2)">' + esc(f) + "</div>").join("") : "") +
|
||
'<div class="drawer-actions"><button class="kt-ai-btn" data-variant="primary" onclick="state.openRow=' + focal._i + ';S.askApprove()">' + esc(sc.control || "Подтвердить") + "</button>" +
|
||
(sc.reject ? '<button class="kt-ai-btn" onclick="state.openRow=' + focal._i + ';S.reject()">' + esc(sc.reject) + "</button>" : "") +
|
||
"</div></div>";
|
||
return html + "</div>";
|
||
}
|
||
|
||
// conversational: чат как главная рабочая область (референс ElevenLabs Conversational AI).
|
||
// Орб-аватар ассистента, ответ без пузыря, подсказки, prompt-bar снизу. Переиспользует S.aiAsk.
|
||
function renderConversational() {
|
||
const ai = CFG.ai || {}, sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
let h = '<div style="max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:var(--kt-ai-space-6);height:calc(100dvh - 106px);min-height:420px">';
|
||
h += bannerHtml();
|
||
// лента (пустая и скрытая до первого вопроса)
|
||
h += '<div class="kt-ai-chat" id="convChat" data-fill="true" role="log" aria-live="polite" aria-label="Диалог с агентом" style="display:none"></div>';
|
||
// подсказки + композер: единый док, который переезжает из hero-центра вниз
|
||
let dock = '<div id="convDock" style="display:flex;flex-direction:column;gap:var(--kt-ai-space-3);width:100%;max-width:680px;margin:0 auto">';
|
||
if (ai.suggestions && ai.suggestions.length)
|
||
dock += '<div class="kt-ai-suggestions" style="justify-content:center">' +
|
||
ai.suggestions.map(s => '<button class="kt-ai-suggestion" onclick="S.aiAsk(this.textContent,\'convChat\')">' + esc(s) + "</button>").join("") + "</div>";
|
||
dock += '<div class="kt-ai-prompt-bar"><textarea rows="1" id="convInput" aria-label="Сообщение агенту" placeholder="Спросить агента…" onkeydown="if(event.key===\'Enter\'&&!event.shiftKey){event.preventDefault();S.aiAsk(this.value,\'convChat\');this.value=\'\'}"></textarea>' +
|
||
'<button class="kt-ai-prompt-send" aria-label="Отправить" onclick="var i=document.getElementById(\'convInput\');S.aiAsk(i.value,\'convChat\');i.value=\'\'"><svg class="kt-icon"><use href="#send"></use></svg></button></div>' +
|
||
'<span class="kt-ai-gen-label" style="text-align:center">Сгенерировано ИИ</span></div>';
|
||
// hero: приветствие без пузыря, всё одной центрированной группой (паттерн пустого чата)
|
||
h += '<div id="convHero" style="flex:1 1 auto;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--kt-ai-space-7)">' +
|
||
'<span aria-hidden="true" style="width:44px;height:44px;border-radius:50%;background:var(--kt-ai-ai-orb)"></span>' +
|
||
'<div style="max-width:560px;text-align:center;font-size:var(--kt-ai-text-md);line-height:1.65">' + esc(ai.intro || "Здравствуйте! Чем могу помочь?") + "</div>" +
|
||
dock + "</div>";
|
||
return h + "</div>";
|
||
}
|
||
|
||
// custom: запасной архетип для экранов, не подходящих под другие (референс ElevenLabs settings-detail).
|
||
// Свободная компоновка из секций-карточек по контракту CFG.sections. Никогда не форсит таблицу.
|
||
function renderCustom() {
|
||
const sc = CFG.scenario || {};
|
||
$("pageTitle").textContent = sc.headline || CFG.product.name;
|
||
$("pageSub").textContent = CFG.product.purpose || "";
|
||
const sections = Array.isArray(CFG.sections) ? CFG.sections : [];
|
||
// Ширину называет ПРОДУКТ, а не подбор типов секций. Мера для чтения
|
||
// сплошного текста (720px) разумна, но секция `fields` — не сплошной текст, а
|
||
// пары «подпись — значение»: таблица критериев с доказательствами на 720px
|
||
// читается хуже, чем на общей ширине, и экран выглядел съехавшим к центру,
|
||
// потому что заголовок оставался во всю ширину. Раньше продукт добивался
|
||
// нужной ширины, подбирая типы секций, — это подбор, а не решение.
|
||
// "width": "full" — общая ширина (по умолчанию для любой не-текстовой секции)
|
||
// "width": "reading" — мера чтения, ~720px
|
||
const автоШирина = sections.some(s => s && ((s.type && s.type !== "text") || (s.fields && s.fields.length))) ? "full" : "reading";
|
||
const wide = (CFG.width || автоШирина) === "full";
|
||
let h = '<div style="max-width:' + (wide ? "1080px" : "720px") + ';margin:0 auto">' + renderHead();
|
||
if (!sections.length) {
|
||
h += '<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-6)"><div class="kt-ai-empty">' +
|
||
'<div class="icon-spot"><svg class="kt-icon kt-icon-lg" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18"/></svg></div>' +
|
||
'<span class="title">Экран ещё не наполнен</span><span>Опишите разделы в contract.sections — они появятся карточками здесь</span></div></div>';
|
||
} else {
|
||
// композиция: каждая секция — блок из примитивов системы; half-секции встают в две колонки
|
||
const block = s => {
|
||
const head = '<div class="kt-ai-h3">' + esc(s.title || "") + "</div>" +
|
||
(s.description ? '<div class="kt-ai-meta" style="margin-top:2px;margin-bottom:var(--kt-ai-space-4)">' + esc(s.description) + "</div>" : '<div style="margin-bottom:var(--kt-ai-space-4)"></div>');
|
||
if (s.type === "kpis") return head + (CFG.kpis && CFG.kpis.length ? '<div class="kt-ai-kpi-strip" data-cards="true">' + CFG.kpis.map(k => kpiHtml(k)).join("") + "</div>" : "");
|
||
if (s.type === "chart" && s.chart) return head + analyticsChartCard(s.chart, 0);
|
||
if (s.type === "table") return head + plainTable(rows);
|
||
if (s.type === "chat") { const ai = CFG.ai || {}; return head + '<div class="kt-ai-chat" data-scroll="true" id="secChat">' + msgHtml("assistant", ai.intro || "Чем помочь?") + "</div>" +
|
||
(ai.suggestions && ai.suggestions.length ? '<div class="kt-ai-suggestions" style="margin-top:var(--kt-ai-space-3)">' + ai.suggestions.map(x => '<button class="kt-ai-suggestion" onclick="S.aiAsk(this.textContent,\'secChat\')">' + esc(x) + "</button>").join("") + "</div>" : ""); }
|
||
if (s.type === "cards") return head + '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:var(--kt-ai-space-4)">' +
|
||
(s.items || []).map(it => '<div class="kt-ai-card kt-ai-hover-lift" role="button" tabindex="0" onclick="toast(\'' + esc(it.title) + ' – заглушка прототипа\',\'\')" onkeydown="if(event.key===\'Enter\')this.click()" style="cursor:pointer;padding:var(--kt-ai-space-5)">' +
|
||
'<div style="display:flex;justify-content:space-between;gap:var(--kt-ai-space-3);align-items:baseline"><span style="font-size:var(--kt-ai-text-sm);font-weight:500">' + esc(it.title) + "</span>" +
|
||
(it.badge ? '<span class="kt-ai-status-pill" data-status="' + esc(it.tone || "info") + '">' + esc(it.badge) + "</span>" : "") + "</div>" +
|
||
(it.text ? '<div class="kt-ai-meta" style="margin-top:var(--kt-ai-space-2);line-height:1.45">' + esc(it.text) + "</div>" : "") + "</div>").join("") + "</div>";
|
||
return head +
|
||
(s.body ? '<div style="font-size:var(--kt-ai-text-sm);white-space:pre-wrap;line-height:var(--kt-ai-leading-normal)">' + esc(s.body) + "</div>" : "") +
|
||
(Array.isArray(s.fields) && s.fields.length ? s.fields.map(f => '<div class="kt-ai-prop"><span class="k">' + esc(f.label) + '</span><span class="v">' + esc(f.value ?? "–") + "</span></div>").join("") : "");
|
||
};
|
||
h += '<div class="kt-ai-cols" style="grid-template-columns:1fr 1fr;margin-top:var(--kt-ai-space-5)">' +
|
||
sections.map(s => '<div class="kt-ai-card" style="padding:var(--kt-ai-space-7)' + (s.width === "half" ? "" : ";grid-column:1 / -1") + '">' + block(s) + "</div>").join("") + "</div>";
|
||
}
|
||
if (sc.control) h += '<div style="margin-top:var(--kt-ai-space-6);display:flex;gap:var(--kt-ai-space-4)">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.customAction()">' + esc(sc.control) + "</button>" +
|
||
(sc.reject ? '<button class="kt-ai-btn" onclick="S.customAction()">' + esc(sc.reject) + "</button>" : "") + "</div>";
|
||
return h + "</div>";
|
||
}
|
||
S.customAction = () => toast(((CFG.scenario || {}).control || "Готово") + " – выполнено", "ok");
|
||
|
||
// Hero (ElevenLabs): имя РАЗДЕЛА слева + подпись, главное действие справа. Лого — только в sidebar.
|
||
function pageHero(title, sub, action) {
|
||
return '<header class="page-hero"><div class="ph-text"><h1>' + esc(title) + "</h1>" +
|
||
(sub ? '<div class="sub">' + esc(sub) + "</div>" : "") + "</div>" +
|
||
(action ? '<div class="ph-action">' + action + "</div>" : "") + "</header>";
|
||
}
|
||
function render() {
|
||
const main = state.navIndex === 0;
|
||
const sc = CFG.scenario || {};
|
||
const archetype = CFG.archetype || CFG.layout || "queue";
|
||
const chatFill = main && archetype === "conversational"; // чат сам владеет областью (без page-hero, как в React fillBody)
|
||
const navItem = (CFG.nav || [])[state.navIndex] || {};
|
||
const isDocs = !main && navItem.view === "documents"; // секция-коллекция документов (с возможностью скачать)
|
||
const isRegistry = !main && navItem.view === "registry" && CFG.table && (CFG.table.rows || []).length; // полный реестр (главная — только обзор)
|
||
const title = main ? (sc.headline || CFG.product.name) : navItem.label;
|
||
const sub = (main || isDocs || isRegistry) ? (CFG.product.purpose || "") : "";
|
||
const action = (main || isRegistry) ? '<button class="kt-ai-btn" data-variant="primary" onclick="S.newCase()"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>' + esc(createLabel) + "</button>" : "";
|
||
const showHero = (main && !chatFill) || isDocs || isRegistry;
|
||
const body = main ? mainByLayout() : (isRegistry ? renderMain() : (isDocs ? renderDocs(navItem) : renderStub(navItem.label)));
|
||
// Вывод — последний блок экрана: он читается после данных, а не вместо них.
|
||
// Место одно на все архетипы, поэтому дописывается здесь, а не в каждом теле.
|
||
// Чат владеет всей областью сам (chatFill) — там дописывать некуда.
|
||
const вывод = (main && !chatFill) || isRegistry ? conclusionHtml() : "";
|
||
$("view").innerHTML = (showHero ? pageHero(title, sub, (main || isRegistry) ? action : "") : "") + body + вывод;
|
||
$("pageTitle").textContent = title; $("pageSub").textContent = sub;
|
||
const top = $("topSearch"); if (top && top.value !== state.query) top.value = state.query; // топбар-поиск держит state.query
|
||
}
|
||
S.compareFocus = i => { state.focusRow = i; render(); };
|
||
// загрузочное состояние (ДС: лоадеры на всём, что дольше 300мс) – показываем один раз при гидрации
|
||
function renderSkeleton() {
|
||
return '<div style="margin-top:var(--kt-ai-space-8)">' +
|
||
'<div class="kt-ai-skeleton" style="height:28px;width:240px;margin-bottom:var(--kt-ai-space-6)"></div>' +
|
||
'<div class="kt-ai-table-wrap">' +
|
||
Array.from({ length: 6 }).map(() => '<div class="kt-ai-skeleton" style="height:44px;margin:var(--kt-ai-space-2) 0"></div>').join("") +
|
||
"</div></div>";
|
||
}
|
||
|
||
/* ---------- Drawer записи ---------- */
|
||
// Карточка кейса: что сделал агент (анализ + признаки) + что решает человек (рекомендация, изменяемое действие)
|
||
S.open = i => {
|
||
const r = rows[i]; if (!r) return;
|
||
state.openRow = i;
|
||
const d = CFG.drawer || {}; const sc = CFG.scenario || {};
|
||
const flags = r._flags || [];
|
||
const recs = flags.filter(f => /^рекоменд/i.test(f));
|
||
const signals = flags.filter(f => !/^рекоменд/i.test(f));
|
||
const recText = recs.length ? recs[0].replace(/^Рекомендация:\s*/i, "") : "";
|
||
const acts = Array.isArray(sc.actions) ? sc.actions : [];
|
||
const preIdx = Math.max(0, acts.findIndex(a => recText && recText.toLowerCase().startsWith(a.toLowerCase().split(/[\s(/]/)[0])));
|
||
const fields = (d.fields || []).map(f => '<div class="kt-ai-prop"><span class="k">' + esc(f.label) + '</span><span class="v">' + esc(fmtVal(r[f.key]) ?? "–") + "</span></div>").join("");
|
||
|
||
let h = '<div class="drawer-head"><div class="t"><div class="kt-ai-h2" id="drawerTitle">' + esc(r[d.titleKey || cols[0].key]) + "</div>" +
|
||
'<div style="margin-top:6px">' + (statusCol ? statusTag(r[statusCol]) : "") + "</div></div>" +
|
||
'<button class="kt-ai-icon-button" aria-label="Закрыть" onclick="S.close()"><svg class="kt-icon" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button></div>';
|
||
|
||
h += '<div class="drawer-sec">' +
|
||
(sc.aiSummary ? '<div class="kt-ai-gen-label">Что сделал агент</div><p class="agent-summary">' + esc(sc.aiSummary) + "</p>" : "") +
|
||
fields + "</div>";
|
||
|
||
const docs = Array.isArray(r._docs) ? r._docs : [];
|
||
if (docs.length) h += '<div class="drawer-sec"><div class="kt-ai-label">Документы</div><div class="kt-ai-doc-list" style="margin-top:var(--kt-ai-space-3)">' +
|
||
docs.map(dd => docCard(dd, "")).join("") + "</div></div>";
|
||
|
||
if (signals.length) h += '<div class="drawer-sec"><div class="kt-ai-label">' + esc(d.flagsLabel || "Признаки риска") + "</div>" +
|
||
signals.map(f => '<div class="agent-signal"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg><span>' + esc(f) + "</span></div>").join("") + "</div>";
|
||
|
||
h += '<div class="drawer-sec decision"><div class="kt-ai-label">Решение</div>';
|
||
if (recText) h += '<div class="rec-box"><span class="kt-ai-gen-label" style="margin-bottom:6px">Рекомендация агента</span>' + esc(recText) + "</div>";
|
||
if (acts.length) h += '<div class="kt-ai-field" style="margin-top:var(--kt-ai-space-5)"><label for="recSelect">Действие — можно изменить</label>' +
|
||
'<select class="kt-ai-select" id="recSelect">' + acts.map((a, idx) => "<option" + (idx === preIdx ? " selected" : "") + ">" + esc(a) + "</option>").join("") + "</select></div>";
|
||
h += '<div class="drawer-actions">' +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.askApprove()">' + esc(sc.control || "Подтвердить") + "</button>" +
|
||
(sc.reject ? '<button class="kt-ai-btn" onclick="S.reject()">' + esc(sc.reject) + "</button>" : "") +
|
||
"</div></div>";
|
||
|
||
const dr = $("drawer");
|
||
dr.innerHTML = h;
|
||
dr.style.display = "block";
|
||
state._drawerReturn = document.activeElement;
|
||
dr.onkeydown = e => { if (e.key === "Escape") { e.stopPropagation(); S.close(); } };
|
||
dr.focus();
|
||
};
|
||
S.bannerAction = () => toast("Открываю связанный пакет работ", "");
|
||
S.close = () => { $("drawer").style.display = "none"; state.openRow = -1; const p = state._drawerReturn; if (p && p.focus) p.focus(); };
|
||
// Row-actions (ElevenLabs): kebab на hover ряда → быстрые действия
|
||
S.rowMenu = (i, e) => {
|
||
e.stopPropagation();
|
||
const sc = CFG.scenario || {};
|
||
const m = $("rowMenu");
|
||
m.innerHTML =
|
||
'<button role="menuitem" class="item" onclick="S.closeMenus();S.open(' + i + ')"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M15 3h6v6M10 14L21 3M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>Открыть</button>' +
|
||
(sc.control ? '<button role="menuitem" class="item" onclick="S.closeMenus();S.quickApprove(' + i + ')"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>' + esc(sc.control) + '</button>' : '') +
|
||
(sc.reject ? '<button role="menuitem" class="item" data-danger="true" onclick="S.closeMenus();S.quickReject(' + i + ')"><svg class="kt-icon kt-icon-sm" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>' + esc(sc.reject) + '</button>' : '');
|
||
const b = e.target.closest("button").getBoundingClientRect();
|
||
S.closeMenus();
|
||
m.style.top = (b.bottom + 4) + "px";
|
||
m.style.left = Math.max(8, Math.min(b.right - 200, window.innerWidth - 220)) + "px";
|
||
m.dataset.open = "true";
|
||
};
|
||
S.quickApprove = i => { const sc = CFG.scenario || {}; const r = rows[i]; if (r && statusCol && sc.approveTo) r[statusCol] = sc.approveTo; render(); toast((sc.control || "Подтверждено") + " – выполнено", "ok"); };
|
||
S.quickReject = i => { const sc = CFG.scenario || {}; const r = rows[i]; if (r && statusCol && sc.rejectTo) r[statusCol] = sc.rejectTo; render(); toast(sc.reject || "Возвращено", "warn"); };
|
||
|
||
/* ---------- Контрольное действие (подтверждение модалкой) ---------- */
|
||
S.askApprove = () => {
|
||
const sc = CFG.scenario || {};
|
||
$("modal").innerHTML = '<div class="kt-ai-modal" role="dialog" aria-modal="true" aria-label="Подтверждение действия"><div class="modal-title">' + esc(sc.control || "Подтвердить") + "?</div>" +
|
||
'<div class="kt-ai-meta">' + esc(sc.confirmText || "Действие будет зафиксировано.") + "</div>" +
|
||
'<div class="modal-actions"><button class="kt-ai-btn" onclick="S.closeModal()">Отмена</button>' +
|
||
'<button class="kt-ai-btn" data-variant="primary" onclick="S.approve()">' + esc(sc.control || "Подтвердить") + "</button></div></div>";
|
||
$("modal").style.display = "flex";
|
||
};
|
||
S.closeModal = () => { $("modal").style.display = "none"; };
|
||
S.approve = () => {
|
||
const sc = CFG.scenario || {};
|
||
const r = rows[state.openRow];
|
||
const sel = $("recSelect");
|
||
const act = sel ? sel.value : "";
|
||
if (r && statusCol && sc.approveTo) r[statusCol] = sc.approveTo;
|
||
S.closeModal(); S.close(); render();
|
||
toast(act ? ("Действие назначено: " + act) : ((sc.control || "Подтверждено") + " – выполнено"), "ok");
|
||
};
|
||
S.reject = () => {
|
||
const sc = CFG.scenario || {};
|
||
const r = rows[state.openRow];
|
||
if (r && statusCol && sc.rejectTo) r[statusCol] = sc.rejectTo;
|
||
S.close(); render();
|
||
toast(sc.reject || "Возвращено", "warn");
|
||
};
|
||
S.bulkApprove = () => {
|
||
const sc = CFG.scenario || {};
|
||
rows.forEach(r => { if (state.selected.has(r._i) && statusCol && sc.approveTo) r[statusCol] = sc.approveTo; });
|
||
const n = state.selected.size; state.selected.clear(); render();
|
||
toast(n + " записей: " + (sc.control || "подтверждено"), "ok");
|
||
};
|
||
|
||
/* ---------- Список ---------- */
|
||
S.nav = i => { state.navIndex = i; renderNav(); render(); };
|
||
S.filter = k => { state.statusFilter = k; state.page = 1; render(); };
|
||
S.sel = (i, on) => { if (on) state.selected.add(i); else state.selected.delete(i); render(); };
|
||
S.clearSel = () => { state.selected.clear(); render(); };
|
||
S.toggleSidebar = () => { const sb = $("sidebar"); sb.dataset.open = sb.dataset.open === "true" ? "false" : "true"; };
|
||
|
||
/* ---------- AI-панель ---------- */
|
||
let aiOpen = false;
|
||
S.toggleAi = () => { aiOpen = !aiOpen; $("aiPanel").style.display = aiOpen ? "flex" : "none"; $("aiFab").style.display = aiOpen ? "none" : "flex"; if (aiOpen) renderAi(); };
|
||
// Правая панель «Активность» — лента действий агента/истории решений (референс из видео Kole Jain)
|
||
S.toggleActivity = () => {
|
||
const p = $("actPanel");
|
||
if (p.style.display !== "none") { p.style.display = "none"; return; }
|
||
const tone = t => t === "info" ? "var(--kt-ai-status-info-fg)" : toneFg(t, "var(--kt-ai-fg-faint)");
|
||
const items = CFG.activity || [];
|
||
p.innerHTML = '<div class="act-head"><strong>Активность</strong><button class="kt-ai-icon-button" aria-label="Закрыть" onclick="S.toggleActivity()"><svg class="kt-icon"><use href="#x"></use></svg></button></div>' +
|
||
'<div class="kt-ai-activity">' + items.map(a =>
|
||
'<div class="act-item"><span class="act-dot" style="background:' + tone(a.tone) + '"></span>' +
|
||
'<div class="act-title">' + esc(a.title || "") + "</div>" +
|
||
'<div class="act-time">' + esc([a.time, a.ago].filter(Boolean).join(" · ")) + "</div>" +
|
||
((a.who || (a.tags && a.tags.length) || a.summary || a.link) ?
|
||
'<div class="act-card">' +
|
||
(a.who ? '<div class="act-row"><span class="k">Кто</span><span class="v">' + esc(a.who) + "</span></div>" : "") +
|
||
(a.tags && a.tags.length ? '<div class="act-row"><span class="k">Данные</span><span class="v">' + a.tags.map(t => '<span class="kt-ai-chip">' + esc(t) + "</span>").join("") + "</span></div>" : "") +
|
||
(a.summary ? '<div class="act-summary">' + esc(a.summary) + "</div>" : "") +
|
||
(a.link ? '<button class="kt-ai-btn" data-size="sm" data-variant="ghost" onclick="toast(\'' + esc(a.link) + " – заглушка','')\">" + esc(a.link) + ' →</button>' : "") +
|
||
"</div>" : "") +
|
||
"</div>").join("") + "</div>";
|
||
p.style.display = "block";
|
||
};
|
||
function renderAi() {
|
||
const ai = CFG.ai || {};
|
||
$("aiPanel").innerHTML =
|
||
'<div class="drawer-head"><div class="t kt-ai-h2">Ассистент</div>' +
|
||
'<button class="kt-ai-icon-button" aria-label="Закрыть" onclick="S.toggleAi()"><svg class="kt-icon" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button></div>' +
|
||
'<div class="kt-ai-chat" id="aiChat">' + msgHtml("assistant", ai.intro || "Чем помочь?") + "</div>" +
|
||
'<div class="ai-panel-foot">' +
|
||
'<div class="kt-ai-suggestions">' + (ai.suggestions || []).map(s => '<button class="kt-ai-suggestion" onclick="S.aiAsk(this.textContent)">' + esc(s) + "</button>").join("") + "</div>" +
|
||
'<div class="kt-ai-prompt-bar"><textarea rows="1" id="aiInput" placeholder="Спросить…" onkeydown="if(event.key===\'Enter\'&&!event.shiftKey){event.preventDefault();S.aiAsk(this.value);this.value=\'\'}"></textarea>' +
|
||
'<button class="kt-ai-prompt-send" aria-label="Отправить" onclick="var i=document.getElementById(\'aiInput\');S.aiAsk(i.value);i.value=\'\'"><svg class="kt-icon" viewBox="0 0 24 24"><path d="M12 19V5m0 0l-6 6m6-6l6 6"/></svg></button></div>' +
|
||
'<span class="kt-ai-gen-label">Сгенерировано ИИ</span></div>';
|
||
}
|
||
function msgHtml(role, text, streaming) {
|
||
if (role === "user") return '<div class="kt-ai-msg" data-role="user"><div class="body">' + esc(text) + "</div></div>";
|
||
return '<div class="kt-ai-msg" data-role="assistant"' + (streaming ? ' data-streaming="true"' : "") + ' style="align-items:flex-start">' +
|
||
'<span class="msg-orb" aria-hidden="true"></span><div class="body">' + esc(text) + "</div></div>";
|
||
}
|
||
S.aiAsk = (q, chatId) => {
|
||
q = (q || "").trim(); if (!q) return;
|
||
const ai = CFG.ai || {};
|
||
const chat = $(chatId || "aiChat");
|
||
if (!chat) return;
|
||
const hero = document.getElementById("convHero");
|
||
if (hero && chatId === "convChat") { // первый вопрос: hero складывается – интро становится первым сообщением, док уезжает вниз
|
||
chat.style.display = "";
|
||
chat.insertAdjacentHTML("beforeend", msgHtml("assistant", (CFG.ai || {}).intro || "Здравствуйте! Чем могу помочь?"));
|
||
const dock = document.getElementById("convDock");
|
||
if (dock) hero.parentElement.appendChild(dock);
|
||
hero.remove();
|
||
}
|
||
chat.insertAdjacentHTML("beforeend", msgHtml("user", q));
|
||
const a = (ai.answers || {})[q] || ai.fallback || "Ответ-заглушка прототипа.";
|
||
chat.insertAdjacentHTML("beforeend", msgHtml("assistant", "", true));
|
||
const msg = chat.lastElementChild, body = msg.querySelector(".body");
|
||
let i = 0;
|
||
const t = setInterval(() => {
|
||
body.textContent = a.slice(0, i += 4);
|
||
chat.scrollTop = chat.scrollHeight;
|
||
if (i >= a.length) { clearInterval(t); msg.dataset.streaming = ""; }
|
||
}, 16);
|
||
chat.scrollTop = chat.scrollHeight;
|
||
};
|
||
|
||
/* ---------- Топбар ---------- */
|
||
function toast(text, tone) {
|
||
const d = document.createElement("div");
|
||
d.className = "kt-ai-toast"; d.dataset.tone = tone || "";
|
||
d.textContent = text;
|
||
$("toasts").appendChild(d);
|
||
setTimeout(() => d.remove(), 3500);
|
||
}
|
||
$("topSearch").addEventListener("input", e => { state.query = e.target.value; state.page = 1; render(); });
|
||
$("themeToggle").onclick = () => {
|
||
const next = document.documentElement.dataset.theme === "light" ? "dark" : "light";
|
||
document.documentElement.dataset.theme = next;
|
||
$("themeToggle").dataset.mode = next;
|
||
};
|
||
document.addEventListener("keydown", e => {
|
||
if (e.key === "Escape") { S.close(); S.closeModal(); S.closeMenus(); if (aiOpen) S.toggleAi(); }
|
||
// Сочетание и его подпись включаются вместе: выключил подпись — выключено и
|
||
// сочетание, иначе в продукте живёт незаявленная горячая клавиша.
|
||
if ((CFG.search || {}).shortcut !== false &&
|
||
(e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); $("topSearch").focus(); }
|
||
});
|
||
|
||
renderTop(); renderNav();
|
||
$("view").innerHTML = renderSkeleton();
|
||
setTimeout(render, 400);
|
||
setTimeout(S.maybeOnboarding, 900);
|
||
</script>
|
||
<!-- Режим обратной связи прототипа (DoD #6): владелец кликает по элементу и оставляет заметку → экспорт в markdown для БА. Самодостаточен, грузится тихо. -->
|
||
<script data-kt-ai-boot="js">
|
||
// async = false — вставленный скриптом <script> иначе выполняется в порядке
|
||
// загрузки, а не вставки. Файлы независимы, но порядок в прототипе должен быть
|
||
// воспроизводимым: иначе «иногда работает» отлаживают в продукте, а не здесь.
|
||
["kt-ai-orb.js", "kt-ai-chart-tip.js", "kt-ai-composer.js", "kt-ai-feedback.js"].forEach(function (файл) {
|
||
var тег = document.createElement("script");
|
||
тег.src = window.ktAiAsset(файл);
|
||
тег.async = false;
|
||
document.body.appendChild(тег);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|