Макеты — один sketches/index.html; approved/ удалён
- sketches/index.html: все экраны одним документом в порядке 1·… (14–19 — бывшие approved) - approved/ (4 файла групп + 3 макета + README) удалён как дубль - стили вынесены в sketches/style.css - README, THEMES, BORROW-FROM-ASSISTENT, MARKDOWN-SOURCE, MIC-ASR-SEARCH приведены к фактическому состоянию
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
# Вариант 2 — Rail / Voice-first
|
||||
|
||||
## Суть
|
||||
Голос — главное действие, а не дополнение к клавиатуре. Огромная кнопка микрофона
|
||||
слева в панели ввода, живая расшифровка на экране, пока говоришь.
|
||||
|
||||
## Ключевые решения
|
||||
- **Раскладка:** рейка иконок (76) + диалоги (296) + чат. Рейка — вход в память агента
|
||||
и инструменты, это не просто мессенджер.
|
||||
- **Голос:** кнопка 58 px, при записи краснеет и пульсирует. Над полем раскрывается
|
||||
панель: волна, таймер и **текст, который появляется по мере речи**.
|
||||
- **Три выхода из записи:** «Отправить» / «В поле ввода» (поправить руками) / «Отменить».
|
||||
Голосовое сообщение в ленте рисуется с волной и кнопкой проигрывания.
|
||||
- **Горячие клавиши:** Ctrl+R — запись, удержание пробела — говорить, Esc — отмена.
|
||||
|
||||
## Что хорошо
|
||||
- Голосом пользоваться реально удобно: видно, что тебя услышали, до отправки.
|
||||
- Ошибку в тексте видно до отправки — можно поправить.
|
||||
|
||||
## Что слабо
|
||||
- Больше элементов на экране, надо разбираться.
|
||||
- «Пробел на удержание» конфликтует с обычным набором текста — нужна аккуратность.
|
||||
|
||||
## Кому
|
||||
Тому, кто в основном диктует, а не печатает.
|
||||
@@ -1,342 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Вариант 2 — Rail / Voice-first</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0E1418; --panel: #141E24; --panel2: #1B2830; --line: #22323C;
|
||||
--fg: #E8EEF2; --dim: #7A93A3; --accent: #4FD1C5; --hot: #FF6B6B;
|
||||
--bot-bubble: #1B2830; --user-bubble: #1F4B54;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #070B0D; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
.window {
|
||||
width: 1240px; height: 750px; background: var(--bg); border-radius: 14px; display: flex;
|
||||
overflow: hidden; border: 1px solid var(--line); box-shadow: 0 24px 80px rgba(0,0,0,.65);
|
||||
}
|
||||
|
||||
/* ---- узкая колонка-рейка: чаты иконками ---- */
|
||||
.rail {
|
||||
width: 76px; flex: 0 0 76px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; align-items: center; padding: 12px 0; gap: 10px;
|
||||
}
|
||||
.rail-btn {
|
||||
width: 46px; height: 46px; border-radius: 13px; border: none; background: var(--panel2);
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 17px;
|
||||
position: relative; transition: .15s;
|
||||
}
|
||||
.rail-btn:hover { color: var(--fg); }
|
||||
.rail-btn.active { background: var(--accent); color: #07201D; }
|
||||
.rail-btn .dot-badge {
|
||||
position: absolute; top: -3px; right: -3px; background: var(--accent); color: #07201D;
|
||||
font-size: 10px; font-weight: 800; border-radius: 8px; padding: 1px 5px; min-width: 16px;
|
||||
}
|
||||
.rail-btn .label {
|
||||
position: absolute; bottom: -14px; font-size: 9px; color: var(--dim); white-space: nowrap;
|
||||
}
|
||||
.rail-sep { width: 28px; height: 1px; background: var(--line); margin: 8px 0; }
|
||||
.rail-spacer { flex: 1; }
|
||||
|
||||
/* ---- средний столбец: список диалогов ---- */
|
||||
.list { width: 296px; flex: 0 0 296px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; }
|
||||
.l-head { padding: 16px 18px 12px; }
|
||||
.l-head h1 { font-size: 16.5px; font-weight: 650; letter-spacing: .2px; }
|
||||
.l-head p { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.search {
|
||||
margin: 0 18px 10px; padding: 9px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13px; outline: none;
|
||||
}
|
||||
.search:focus { border-color: var(--accent); }
|
||||
.search::placeholder { color: var(--dim); }
|
||||
.rows { flex: 1; overflow-y: auto; padding: 0 10px 12px; }
|
||||
.row {
|
||||
display: flex; gap: 11px; padding: 10px 11px; border-radius: 11px; cursor: pointer;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
.row:hover { background: var(--panel2); }
|
||||
.row.active { background: var(--panel2); border-left-color: var(--accent); }
|
||||
.row .av {
|
||||
width: 38px; height: 38px; flex: 0 0 38px; border-radius: 11px; display: grid; place-items: center;
|
||||
font-size: 13px; font-weight: 700; background: rgba(79,209,197,.16); color: var(--accent);
|
||||
}
|
||||
.row .mid { flex: 1; min-width: 0; }
|
||||
.row .nm { font-size: 13.5px; font-weight: 550; display: flex; justify-content: space-between; }
|
||||
.row .tm { font-size: 10.5px; color: var(--dim); font-weight: 400; }
|
||||
.row .pv { font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
|
||||
/* ---- основная зона чата ---- */
|
||||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.m-head { height: 60px; flex: 0 0 60px; display: flex; align-items: center; gap: 12px;
|
||||
padding: 0 22px; border-bottom: 1px solid var(--line); }
|
||||
.m-head .t { font-size: 14.5px; font-weight: 600; }
|
||||
.m-head .s { font-size: 11.5px; color: var(--dim); }
|
||||
.ghost { margin-left: auto; display: flex; gap: 6px; }
|
||||
.ghost button { background: transparent; border: 1px solid var(--line); color: var(--dim);
|
||||
border-radius: 8px; padding: 5px 11px; font-size: 11.5px; cursor: pointer; }
|
||||
.ghost button:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
.thread { flex: 1; overflow-y: auto; padding: 22px 26px; display: flex; flex-direction: column; gap: 13px; }
|
||||
.b { max-width: 76%; padding: 10px 14px; border-radius: 14px; font-size: 13.5px; }
|
||||
.b.u { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.b.a { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
.b .tm { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
|
||||
|
||||
/* голосовое сообщение — с волной прямо в пузыре */
|
||||
.voice-b {
|
||||
align-self: flex-end; background: var(--user-bubble); border-radius: 14px 14px 4px 14px;
|
||||
padding: 10px 14px; display: flex; align-items: center; gap: 11px; min-width: 250px;
|
||||
}
|
||||
.play { width: 30px; height: 30px; border-radius: 50%; background: var(--accent); color: #07201D;
|
||||
border: none; display: grid; place-items: center; cursor: pointer; font-size: 12px; }
|
||||
.vwave { flex: 1; display: flex; align-items: center; gap: 2px; height: 24px; }
|
||||
.vwave i { flex: 1; background: rgba(232,238,242,.5); border-radius: 2px; }
|
||||
.vdur { font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---- КОМПОЗЕР: голос — главный ---- */
|
||||
.composer { border-top: 1px solid var(--line); background: var(--panel); padding: 16px 22px 14px; }
|
||||
|
||||
.mic-zone { display: flex; align-items: center; gap: 16px; }
|
||||
.mic-big {
|
||||
width: 58px; height: 58px; flex: 0 0 58px; border-radius: 50%; border: none; cursor: pointer;
|
||||
background: var(--accent); color: #07201D; font-size: 23px; display: grid; place-items: center;
|
||||
transition: .2s;
|
||||
}
|
||||
.mic-big:hover { transform: scale(1.05); }
|
||||
.mic-big.on { background: var(--hot); color: #fff; animation: ring 1.5s infinite; }
|
||||
@keyframes ring { 0%,100% { box-shadow: 0 0 0 0 rgba(255,107,107,.6); } 50% { box-shadow: 0 0 0 13px rgba(255,107,107,0); } }
|
||||
|
||||
.mic-side { flex: 1; min-width: 0; }
|
||||
.mic-hint { font-size: 12px; color: var(--dim); margin-bottom: 7px; }
|
||||
.mic-hint b { color: var(--fg); font-weight: 550; }
|
||||
.type-row { display: flex; align-items: center; gap: 9px; }
|
||||
.type-row textarea {
|
||||
flex: 1; background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 10px 13px;
|
||||
color: var(--fg); font-family: inherit; font-size: 13.5px; outline: none; resize: none;
|
||||
height: 40px; max-height: 110px; line-height: 1.4;
|
||||
}
|
||||
.type-row textarea:focus { border-color: var(--accent); }
|
||||
.type-row textarea::placeholder { color: var(--dim); }
|
||||
.go { height: 40px; padding: 0 17px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--dim); font-size: 12.5px; cursor: pointer; }
|
||||
.go.active { background: var(--accent); border-color: var(--accent); color: #07201D; font-weight: 650; }
|
||||
|
||||
/* расшифровка в реальном времени */
|
||||
.live-asr { margin-top: 11px; display: none; background: var(--bg); border: 1px solid var(--hot);
|
||||
border-radius: 11px; padding: 11px 14px; }
|
||||
.live-asr.on { display: block; }
|
||||
.live-asr .top { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; font-size: 11.5px; color: var(--hot); font-weight: 600; }
|
||||
.blink { width: 7px; height: 7px; border-radius: 50%; background: var(--hot); animation: bl .9s infinite; }
|
||||
@keyframes bl { 50% { opacity: .15; } }
|
||||
.big-wave { display: flex; align-items: center; gap: 3px; height: 30px; margin-bottom: 8px; }
|
||||
.big-wave i { width: 3px; background: var(--accent); border-radius: 2px; height: 25%; animation: bw .8s infinite ease-in-out; }
|
||||
.big-wave i:nth-child(2n) { animation-delay: .1s; } .big-wave i:nth-child(3n) { animation-delay: .2s; }
|
||||
@keyframes bw { 0%,100% { height: 18%; } 50% { height: 100%; } }
|
||||
.asr-line { font-size: 14px; color: var(--fg); min-height: 20px; }
|
||||
.asr-line .cursor { display: inline-block; width: 2px; height: 15px; background: var(--accent);
|
||||
vertical-align: middle; animation: bl .8s infinite; }
|
||||
.live-actions { display: flex; gap: 8px; margin-top: 11px; }
|
||||
.live-actions button { border-radius: 9px; padding: 7px 14px; font-size: 12.5px; cursor: pointer; border: 1px solid var(--line); background: transparent; color: var(--dim); }
|
||||
.live-actions .prim { background: var(--accent); border-color: var(--accent); color: #07201D; font-weight: 620; }
|
||||
.live-actions .dang { color: var(--hot); }
|
||||
.foot-note { font-size: 10.5px; color: var(--dim); margin-top: 9px; }
|
||||
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
|
||||
|
||||
/* окно целиком влезает на любой экран — не режем картинку */
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
|
||||
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
|
||||
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window">
|
||||
<!-- ============ РЕЙКА ============ -->
|
||||
<nav class="rail">
|
||||
<button class="rail-btn active" title="Диалоги">💬<span class="dot-badge">3</span></button>
|
||||
<button class="rail-btn" title="Память агента">🧠</button>
|
||||
<button class="rail-btn" title="Инструменты">⚙</button>
|
||||
<div class="rail-sep"></div>
|
||||
<button class="rail-btn" title="Новый диалог">+</button>
|
||||
<div class="rail-spacer"></div>
|
||||
<button class="rail-btn" title="Настройки">⚙︎</button>
|
||||
</nav>
|
||||
|
||||
<!-- ============ СПИСОК ДИАЛОГОВ ============ -->
|
||||
<section class="list">
|
||||
<div class="l-head">
|
||||
<h1>Диалоги</h1>
|
||||
<p>локальный агент · 3 непрочитанных</p>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск…">
|
||||
<div class="rows">
|
||||
<div class="row active">
|
||||
<div class="av">АГ</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>agentik</span><span class="tm">14:22</span></div>
|
||||
<div class="pv">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">ОЧ</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Отчёт за квартал</span><span class="tm">12:05</span></div>
|
||||
<div class="pv">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">РЗ</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Разбор логов</span><span class="tm">вчера</span></div>
|
||||
<div class="pv">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">ПО</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Solana-бэкенд</span><span class="tm">вчера</span></div>
|
||||
<div class="pv">Девайс-нет стартовал</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="av">КН</div>
|
||||
<div class="mid">
|
||||
<div class="nm"><span>Книги</span><span class="tm">пн</span></div>
|
||||
<div class="pv">Разложил по полкам</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ ЧАТ ============ -->
|
||||
<main class="main">
|
||||
<header class="m-head">
|
||||
<div>
|
||||
<div class="t">agentik</div>
|
||||
<div class="s">на связи · codding-big</div>
|
||||
</div>
|
||||
<div class="ghost">
|
||||
<button>Стоп</button>
|
||||
<button>Очистить</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="thread" id="thread">
|
||||
<div class="b u">Посмотри логи за вчера<span class="tm">14:10</span></div>
|
||||
<div class="b a">Проверил: 3 ошибки 502 около 04:20, все — обрыв соединения с upstream. Виновник — таймаут прокси. Поправить?<span class="tm">14:12</span></div>
|
||||
|
||||
<div class="voice-b">
|
||||
<button class="play" onclick="togglePlay(this)">▶</button>
|
||||
<div class="vwave" id="pastWave"></div>
|
||||
<span class="vdur">0:07</span>
|
||||
</div>
|
||||
|
||||
<div class="b a">Понял: увеличить таймаут до 60 секунд и перезапустить прокси. Сделал — прокси поднят заново, ошибок за последний час нет.<span class="tm">14:20</span></div>
|
||||
</div>
|
||||
|
||||
<div class="composer">
|
||||
<div class="live-asr" id="liveAsr">
|
||||
<div class="top"><span class="blink"></span> ЗАПИСЬ · <span id="liveTime">0:00</span></div>
|
||||
<div class="big-wave" id="bigWave"></div>
|
||||
<div class="asr-line"><span id="asrLine"></span><span class="cursor"></span></div>
|
||||
<div class="live-actions">
|
||||
<button class="prim" onclick="stopRec(false)">Отправить</button>
|
||||
<button onclick="insertAsText()">В поле ввода</button>
|
||||
<button class="dang" onclick="stopRec(true)">Отменить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mic-zone">
|
||||
<button class="mic-big" id="micBtn" title="Запись (Ctrl+R, удержание пробела)">🎙</button>
|
||||
<div class="mic-side">
|
||||
<div class="mic-hint"><b>Говорите</b> — текст появится в поле ниже. Можно править перед отправкой.</div>
|
||||
<div class="type-row">
|
||||
<textarea id="ta" placeholder="Или напишите руками…"></textarea>
|
||||
<button class="go" id="goBtn" disabled>Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="foot-note">
|
||||
<kbd>Ctrl</kbd>+<kbd>R</kbd> — начать/стоп · <kbd>Пробел</kbd> (удержание) — говорить · <kbd>Enter</kbd> — отправить · <kbd>Esc</kbd> — отменить запись
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ta = document.getElementById('ta'), goBtn = document.getElementById('goBtn');
|
||||
const micBtn = document.getElementById('micBtn'), liveAsr = document.getElementById('liveAsr');
|
||||
const thread = document.getElementById('thread');
|
||||
|
||||
document.getElementById('pastWave').innerHTML =
|
||||
Array.from({length: 34}, () => '<i style="height:' + (18 + Math.random()*80) + '%"></i>').join('');
|
||||
document.getElementById('bigWave').innerHTML = Array.from({length: 40}, () => '<i></i>').join('');
|
||||
|
||||
ta.addEventListener('input', () => {
|
||||
goBtn.disabled = ta.value.trim() === '';
|
||||
goBtn.classList.toggle('active', ta.value.trim() !== '');
|
||||
ta.style.height = '40px'; ta.style.height = Math.min(ta.scrollHeight, 110) + 'px';
|
||||
});
|
||||
|
||||
function send() {
|
||||
const t = ta.value.trim(); if (!t) return;
|
||||
add(t, 'u'); ta.value = ''; ta.style.height = '40px';
|
||||
goBtn.disabled = true; goBtn.classList.remove('active');
|
||||
setTimeout(() => add('Выполняю: «' + t + '».', 'a'), 800);
|
||||
}
|
||||
function add(text, who) {
|
||||
const d = document.createElement('div'); d.className = 'b ' + who;
|
||||
const n = new Date();
|
||||
d.innerHTML = text + '<span class="tm">' + n.getHours() + ':' + String(n.getMinutes()).padStart(2,'0') + '</span>';
|
||||
thread.appendChild(d); thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
goBtn.onclick = send;
|
||||
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
|
||||
|
||||
// ---- запись с живой расшифровкой ----
|
||||
const PHRASE = 'посмотри логи за вчерашний день и скажи что там с ошибками';
|
||||
let tmr = null, sec = 0, wordIdx = 0;
|
||||
function startRec() {
|
||||
sec = 0; wordIdx = 0;
|
||||
liveAsr.classList.add('on'); micBtn.classList.add('on');
|
||||
document.getElementById('asrLine').textContent = '';
|
||||
tmr = setInterval(() => {
|
||||
sec++;
|
||||
document.getElementById('liveTime').textContent = '0:' + String(sec).padStart(2,'0');
|
||||
const words = PHRASE.split(' ');
|
||||
if (wordIdx < words.length) {
|
||||
document.getElementById('asrLine').textContent = words.slice(0, ++wordIdx).join(' ');
|
||||
}
|
||||
}, 450);
|
||||
}
|
||||
function stopRec(cancel) {
|
||||
clearInterval(tmr);
|
||||
liveAsr.classList.remove('on'); micBtn.classList.remove('on');
|
||||
if (cancel) return;
|
||||
const text = document.getElementById('asrLine').textContent;
|
||||
if (!text) return;
|
||||
if (sec >= 2) { // короткие — сразу в чат, длинные — в поле
|
||||
add(text, 'u');
|
||||
setTimeout(() => add('Записал голосом, разбираю…', 'a'), 700);
|
||||
}
|
||||
}
|
||||
function insertAsText() {
|
||||
ta.value = (ta.value ? ta.value + ' ' : '') + document.getElementById('asrLine').textContent;
|
||||
ta.dispatchEvent(new Event('input')); ta.focus(); stopRec(true);
|
||||
}
|
||||
function togglePlay(btn) { btn.textContent = btn.textContent === '▶' ? '❚❚' : '▶'; }
|
||||
micBtn.onclick = () => liveAsr.classList.contains('on') ? stopRec(false) : startRec();
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); micBtn.click(); }
|
||||
if (e.key === 'Escape' && liveAsr.classList.contains('on')) stopRec(true);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,25 +0,0 @@
|
||||
# Вариант 3 — Three-pane / Command center
|
||||
|
||||
## Суть
|
||||
Три панели: диалоги, чат и **состояние агента**. Работа агента видна не как текст
|
||||
в переписке, а как отдельные раскрывающиеся шаги.
|
||||
|
||||
## Ключевые решения
|
||||
- **Раскладка:** 268 + центр + 306. Третья панель — модель, контекст, инструменты,
|
||||
память агента, файлы сессии.
|
||||
- **Шаг агента** — сворачиваемый блок: что делал, сколько заняло, детали по клику.
|
||||
Это то, чем агент отличается от собеседника, и здесь оно вынесено в структуру.
|
||||
- **Кнопка записи:** компактная рядом с «Отправить»; при записи снизу появляется
|
||||
строка: квадрат «стоп», индикатор уровня, текущая расшифровка, таймер.
|
||||
- **Короткая диктовка** (от 2 с) уходит сразу, текст остаётся в поле.
|
||||
|
||||
## Что хорошо
|
||||
- Видно, что агент делает прямо сейчас и сколько это стоит.
|
||||
- Удобно на широком экране, ничего не надо прятать.
|
||||
|
||||
## Что слабо
|
||||
- На узком экране (ноутбук 1366) третью панель придётся убирать.
|
||||
- Настройки сессии — не то, на что хочется смотреть постоянно.
|
||||
|
||||
## Кому
|
||||
Тому, кто держит агента включённым весь день и следит за расходом.
|
||||
@@ -1,345 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Вариант 3 — Three-pane / Command center</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0F1115; --panel: #161A21; --panel2: #1E242D; --panel3: #252C37; --line: #2A323D;
|
||||
--fg: #ECEFF3; --dim: #808FA0; --accent: #7C8CF8; --ok: #4ADE80; --warn: #FBBF24; --hot: #F87171;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #06080A; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 22px;
|
||||
}
|
||||
.win { width: 1360px; height: 780px; background: var(--bg); border-radius: 13px; overflow: hidden;
|
||||
border: 1px solid var(--line); display: flex; flex-direction: column; box-shadow: 0 26px 90px rgba(0,0,0,.7); }
|
||||
|
||||
/* ---- строка меню-заголовка ---- */
|
||||
.titlebar { height: 42px; flex: 0 0 42px; display: flex; align-items: center; gap: 12px;
|
||||
padding: 0 16px; background: var(--panel); border-bottom: 1px solid var(--line); }
|
||||
.lights { display: flex; gap: 7px; }
|
||||
.lights i { width: 11px; height: 11px; border-radius: 50%; }
|
||||
.lights i:nth-child(1) { background: #FF5F57; } .lights i:nth-child(2) { background: #FEBC2E; }
|
||||
.lights i:nth-child(3) { background: #28C840; }
|
||||
.tb-title { font-size: 12.5px; color: var(--dim); }
|
||||
.tb-title b { color: var(--fg); font-weight: 550; }
|
||||
.tb-right { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--dim); }
|
||||
.pill { background: var(--panel2); border: 1px solid var(--line); border-radius: 20px; padding: 3px 11px;
|
||||
display: flex; align-items: center; gap: 6px; }
|
||||
.pill .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
|
||||
.body { flex: 1; display: flex; min-height: 0; }
|
||||
|
||||
/* ========== ПАНЕЛЬ 1: чаты ========== */
|
||||
.p1 { width: 268px; flex: 0 0 268px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; }
|
||||
.p-head { padding: 14px 15px 9px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.p-head h2 { font-size: 11px; letter-spacing: 1.3px; text-transform: uppercase; color: var(--dim); font-weight: 650; }
|
||||
.p-head button { background: var(--accent); border: none; color: #fff; border-radius: 7px;
|
||||
width: 24px; height: 24px; cursor: pointer; font-size: 15px; line-height: 1; }
|
||||
.f-tabs { display: flex; gap: 4px; padding: 0 12px 10px; flex-wrap: wrap; }
|
||||
.f-tab { font-size: 11px; padding: 4px 9px; border-radius: 7px; color: var(--dim); background: var(--panel2);
|
||||
cursor: pointer; border: 1px solid transparent; }
|
||||
.f-tab.on { color: var(--fg); border-color: var(--accent); background: rgba(124,140,248,.14); }
|
||||
.q { margin: 0 12px 9px; padding: 7px 11px; border-radius: 8px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 12.5px; outline: none; }
|
||||
.q:focus { border-color: var(--accent); } .q::placeholder { color: var(--dim); }
|
||||
.cf { flex: 1; overflow-y: auto; padding: 0 8px 12px; }
|
||||
.c { display: flex; gap: 10px; padding: 9px 10px; border-radius: 9px; cursor: pointer; }
|
||||
.c:hover { background: var(--panel2); }
|
||||
.c.on { background: var(--panel2); box-shadow: inset 2px 0 0 var(--accent); }
|
||||
.c .a { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 9px; display: grid; place-items: center;
|
||||
font-size: 11.5px; font-weight: 700; background: var(--panel3); color: var(--accent); }
|
||||
.c .m { flex: 1; min-width: 0; }
|
||||
.c .n { font-size: 13px; font-weight: 550; display: flex; justify-content: space-between; gap: 6px; }
|
||||
.c .n span.t { font-size: 10px; color: var(--dim); font-weight: 400; }
|
||||
.c .p { font-size: 11.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
|
||||
.c.new .n { color: var(--accent); }
|
||||
|
||||
/* ========== ПАНЕЛЬ 2: диалог ========== */
|
||||
.p2 { flex: 1; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--line); }
|
||||
.p2h { height: 54px; flex: 0 0 54px; display: flex; align-items: center; gap: 11px; padding: 0 18px;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.p2h .nm { font-size: 14px; font-weight: 600; }
|
||||
.p2h .sub { font-size: 11px; color: var(--dim); }
|
||||
.p2h .acts { margin-left: auto; display: flex; gap: 6px; }
|
||||
.p2h .acts button { background: var(--panel2); border: 1px solid var(--line); color: var(--dim);
|
||||
border-radius: 7px; padding: 5px 11px; font-size: 11.5px; cursor: pointer; }
|
||||
.p2h .acts button:hover { color: var(--fg); }
|
||||
|
||||
.flow { flex: 1; overflow-y: auto; padding: 18px 22px; display: flex; flex-direction: column; gap: 11px; }
|
||||
.u { align-self: flex-end; max-width: 78%; background: #2A3350; padding: 9px 13px; border-radius: 12px 12px 3px 12px;
|
||||
font-size: 13.5px; }
|
||||
.a { align-self: flex-start; max-width: 78%; background: var(--panel2); padding: 9px 13px;
|
||||
border-radius: 12px 12px 12px 3px; font-size: 13.5px; }
|
||||
.tm { font-size: 10px; color: var(--dim); margin-top: 4px; }
|
||||
|
||||
/* свёрнутый шаг работы агента — то, что отличает агента от чата */
|
||||
.step { align-self: flex-start; width: 100%; max-width: 92%; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
||||
.step-h { display: flex; align-items: center; gap: 9px; padding: 8px 12px; cursor: pointer; font-size: 12.5px; }
|
||||
.step-h:hover { background: var(--panel2); }
|
||||
.step-h .ico { color: var(--accent); }
|
||||
.step-h .nm { color: var(--fg); font-weight: 550; }
|
||||
.step-h .stat { margin-left: auto; font-size: 10.5px; color: var(--ok); }
|
||||
.step-h .chev { color: var(--dim); font-size: 10px; }
|
||||
.step-b { display: none; padding: 0 12px 11px 34px; font-size: 12px; color: var(--dim); }
|
||||
.step.open .step-b { display: block; }
|
||||
.step-b code { color: #A8B6C8; background: var(--bg); border: 1px solid var(--line); border-radius: 5px;
|
||||
padding: 1px 6px; font-size: 11px; }
|
||||
|
||||
/* ========== ПАНЕЛЬ 3: состояние агента ========== */
|
||||
.p3 { width: 306px; flex: 0 0 306px; background: var(--panel); display: flex; flex-direction: column; }
|
||||
.sec { border-bottom: 1px solid var(--line); padding: 13px 15px; }
|
||||
.sec h3 { font-size: 10.5px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--dim);
|
||||
font-weight: 650; margin-bottom: 10px; display: flex; align-items: center; gap: 7px; }
|
||||
.kv { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 7px; }
|
||||
.kv span:first-child { color: var(--dim); }
|
||||
.kv span:last-child { font-weight: 550; }
|
||||
.bar { height: 5px; border-radius: 3px; background: var(--panel3); overflow: hidden; margin-top: 4px; }
|
||||
.bar i { display: block; height: 100%; background: var(--accent); }
|
||||
.tool-chip { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px 0; }
|
||||
.tool-chip .s { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.tool-chip .s.run { background: var(--warn); animation: p 1s infinite; }
|
||||
.tool-chip .s.ok { background: var(--ok); }
|
||||
@keyframes p { 50% { opacity: .3; } }
|
||||
.mem { font-size: 11.5px; color: var(--dim); padding: 7px 9px; background: var(--panel2);
|
||||
border-radius: 8px; margin-bottom: 6px; line-height: 1.4; }
|
||||
.mem b { color: var(--fg); font-weight: 550; display: block; font-size: 11.5px; margin-bottom: 2px; }
|
||||
|
||||
/* ========== КОМПОЗЕР (низ панели 2) ========== */
|
||||
.comp { border-top: 1px solid var(--line); background: var(--panel); padding: 13px 18px 14px; }
|
||||
.rec-state { display: none; align-items: center; gap: 12px; background: var(--bg); border: 1px solid var(--hot);
|
||||
border-radius: 10px; padding: 10px 13px; margin-bottom: 10px; }
|
||||
.rec-state.on { display: flex; }
|
||||
.stop-rec { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 50%; background: var(--hot);
|
||||
border: none; color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 12px; }
|
||||
.sq { width: 11px; height: 11px; background: #fff; border-radius: 2px; }
|
||||
.meter { flex: 1; display: flex; align-items: center; gap: 3px; height: 26px; }
|
||||
.meter i { width: 3px; background: var(--hot); border-radius: 2px; height: 25%; animation: mn .9s infinite ease-in-out; }
|
||||
.meter i:nth-child(2n) { animation-delay: .12s; } .meter i:nth-child(3n) { animation-delay: .24s; }
|
||||
@keyframes mn { 0%,100% { height: 20%; } 50% { height: 100%; } }
|
||||
.rt { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--hot); font-weight: 600; }
|
||||
.rec-state .live-txt { font-size: 11.5px; color: var(--dim); max-width: 260px; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.crow { display: flex; align-items: flex-end; gap: 8px; }
|
||||
.cbox { flex: 1; display: flex; align-items: flex-end; background: var(--bg); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 9px 12px; }
|
||||
.cbox:focus-within { border-color: var(--accent); }
|
||||
.cbox textarea { flex: 1; background: transparent; border: none; outline: none; color: var(--fg);
|
||||
font-family: inherit; font-size: 13.5px; resize: none; height: 21px; max-height: 110px; line-height: 1.45; }
|
||||
.cbox textarea::placeholder { color: var(--dim); }
|
||||
.ic { width: 36px; height: 36px; border-radius: 9px; border: 1px solid var(--line); background: var(--bg);
|
||||
color: var(--dim); cursor: pointer; display: grid; place-items: center; font-size: 15px; flex: 0 0 36px; }
|
||||
.ic:hover { color: var(--fg); }
|
||||
.ic.rec { border-color: var(--accent); color: var(--accent); }
|
||||
.ic.rec.live { background: var(--hot); border-color: var(--hot); color: #fff; animation: rp 1.4s infinite; }
|
||||
@keyframes rp { 0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,.5); } 50% { box-shadow: 0 0 0 9px rgba(248,113,113,0); } }
|
||||
.send { height: 36px; padding: 0 16px; border-radius: 9px; border: none; background: var(--accent);
|
||||
color: #fff; font-size: 12.5px; font-weight: 600; cursor: pointer; }
|
||||
.send:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
|
||||
.keys { font-size: 10.5px; color: var(--dim); margin-top: 9px; display: flex; gap: 14px; }
|
||||
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; font-size: 10px; }
|
||||
|
||||
/* окно целиком влезает на любой экран — не режем картинку */
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window, .win { transform: scale(.78); } }
|
||||
@media (max-height: 700px) { .window, .win { transform: scale(.66); } }
|
||||
@media (max-height: 600px) { .window, .win { transform: scale(.56); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="win">
|
||||
<div class="titlebar">
|
||||
<div class="lights"><i></i><i></i><i></i></div>
|
||||
<div class="tb-title">agentik-desktop — <b>agentik · рабочий</b></div>
|
||||
<div class="tb-right">
|
||||
<span class="pill"><span class="d"></span> агент на связи</span>
|
||||
<span>Память: 128 заметок</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<!-- ======== ПАНЕЛЬ 1 ======== -->
|
||||
<aside class="p1">
|
||||
<div class="p-head"><h2>Диалоги</h2><button title="Новый">+</button></div>
|
||||
<div class="f-tabs">
|
||||
<div class="f-tab on">Все</div><div class="f-tab">Работа</div>
|
||||
<div class="f-tab">Инфра</div><div class="f-tab">Дом</div><div class="f-tab">+</div>
|
||||
</div>
|
||||
<input class="q" placeholder="Поиск диалога…">
|
||||
<div class="cf">
|
||||
<div class="c on"><div class="a">АГ</div><div class="m">
|
||||
<div class="n"><span>agentik · рабочий</span><span class="t">14:22</span></div>
|
||||
<div class="p">Собрал отчёт, жду правок</div></div></div>
|
||||
<div class="c new"><div class="a">ОЧ</div><div class="m">
|
||||
<div class="n"><span>Отчёт за квартал</span><span class="t">12:05</span></div>
|
||||
<div class="p">Файл лежит на диске</div></div></div>
|
||||
<div class="c"><div class="a">РЗ</div><div class="m">
|
||||
<div class="n"><span>Разбор логов nginx</span><span class="t">вчера</span></div>
|
||||
<div class="p">Нашёл 3 ошибки 502</div></div></div>
|
||||
<div class="c"><div class="a">ПО</div><div class="m">
|
||||
<div class="n"><span>Solana-бэкенд</span><span class="t">вчера</span></div>
|
||||
<div class="p">Девайс-нет стартовал</div></div></div>
|
||||
<div class="c"><div class="a">КН</div><div class="m">
|
||||
<div class="n"><span>Книги</span><span class="t">пн</span></div>
|
||||
<div class="p">Разложил по полкам</div></div></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ======== ПАНЕЛЬ 2 ======== -->
|
||||
<section class="p2">
|
||||
<div class="p2h">
|
||||
<div>
|
||||
<div class="nm">agentik · рабочий</div>
|
||||
<div class="sub">codding-big · контекст 34%</div>
|
||||
</div>
|
||||
<div class="acts"><button>Стоп</button><button>Очистить</button></div>
|
||||
</div>
|
||||
|
||||
<div class="flow" id="flow">
|
||||
<div class="u">Разберись, почему CI красный, и выпусти релиз 3<span class="tm">14:05</span></div>
|
||||
|
||||
<div class="step open" onclick="this.classList.toggle('open')">
|
||||
<div class="step-h"><span class="ico">🔍</span><span class="nm">Поиск причины</span>
|
||||
<span class="stat">готово · 4 с</span><span class="chev">▼</span></div>
|
||||
<div class="step-b">Нашёл: в именах тестов длинное тире, сборка идёт в ASCII.<br>
|
||||
<code>InvalidPathException: unmappable characters</code></div>
|
||||
</div>
|
||||
|
||||
<div class="step" onclick="this.classList.toggle('open')">
|
||||
<div class="step-h"><span class="ico">✎</span><span class="nm">Правка 16 имён тестов</span>
|
||||
<span class="stat">готово · 12 с</span><span class="chev">▼</span></div>
|
||||
<div class="step-b">Заменено на обычный дефис в 4 файлах. Ничего другого не тронуто.</div>
|
||||
</div>
|
||||
|
||||
<div class="step" onclick="this.classList.toggle('open')">
|
||||
<div class="step-h"><span class="ico">⚙</span><span class="nm">Сборка и проверка</span>
|
||||
<span class="stat">готово · 1 м 10 с</span><span class="chev">▼</span></div>
|
||||
<div class="step-b">14 модулей, тесты зелёные, jar-ы собраны.</div>
|
||||
</div>
|
||||
|
||||
<div class="a">Готово. CI зелёный, релиз «3» опубликован: 101 артефакт в Nexus.<span class="tm">14:22</span></div>
|
||||
</div>
|
||||
|
||||
<div class="comp">
|
||||
<div class="rec-state" id="recState">
|
||||
<button class="stop-rec" onclick="stopRec(false)"><span class="sq"></span></button>
|
||||
<div class="meter" id="meter"></div>
|
||||
<span class="live-txt" id="liveTxt">слушаю…</span>
|
||||
<span class="rt" id="rt">0:00</span>
|
||||
<button class="ic" onclick="stopRec(true)" title="Отменить">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="crow">
|
||||
<div class="cbox"><textarea id="ta" placeholder="Сообщение агенту…"></textarea></div>
|
||||
<button class="ic rec" id="recBtn" title="Голосовое (Ctrl+R)">🎙</button>
|
||||
<button class="send" id="sendBtn" disabled>Отправить</button>
|
||||
</div>
|
||||
<div class="keys">
|
||||
<span><kbd>Ctrl</kbd>+<kbd>R</kbd> запись</span>
|
||||
<span><kbd>Enter</kbd> отправить</span>
|
||||
<span><kbd>Shift</kbd>+<kbd>Enter</kbd> перенос</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======== ПАНЕЛЬ 3 ======== -->
|
||||
<aside class="p3">
|
||||
<div class="sec">
|
||||
<h3>Сессия</h3>
|
||||
<div class="kv"><span>Модель</span><span>codding-big</span></div>
|
||||
<div class="kv"><span>Контекст</span><span>34 %</span></div>
|
||||
<div class="bar"><i style="width:34%"></i></div>
|
||||
<div class="kv" style="margin-top:9px"><span>Ходов</span><span>18</span></div>
|
||||
<div class="kv"><span>Стоимость</span><span>0.42 ₽</span></div>
|
||||
</div>
|
||||
|
||||
<div class="sec">
|
||||
<h3>Инструменты</h3>
|
||||
<div class="tool-chip"><span class="s ok"></span> shell · <span style="color:var(--dim)">готов</span></div>
|
||||
<div class="tool-chip"><span class="s ok"></span> read_file · <span style="color:var(--dim)">готов</span></div>
|
||||
<div class="tool-chip"><span class="s run"></span> write_file · <span style="color:var(--dim)">работает</span></div>
|
||||
<div class="tool-chip"><span class="s ok"></span> memory · <span style="color:var(--dim)">готов</span></div>
|
||||
</div>
|
||||
|
||||
<div class="sec">
|
||||
<h3>Память агента</h3>
|
||||
<div class="mem"><b>Про CI agentik</b>длинное тире в именах тестов ломает сборку в ASCII</div>
|
||||
<div class="mem"><b>Про Nexus</b>публикация идёт через общий action devops/publish</div>
|
||||
</div>
|
||||
|
||||
<div class="sec" style="border-bottom:none">
|
||||
<h3>Файлы сессии</h3>
|
||||
<div class="kv"><span>standalone-3.jar</span><span style="color:var(--dim)">240 МБ</span></div>
|
||||
<div class="kv"><span>agentik-cli-3.jar</span><span style="color:var(--dim)">7.2 МБ</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ta = document.getElementById('ta'), sendBtn = document.getElementById('sendBtn');
|
||||
const flow = document.getElementById('flow');
|
||||
const recBtn = document.getElementById('recBtn'), recState = document.getElementById('recState');
|
||||
const liveTxt = document.getElementById('liveTxt');
|
||||
document.getElementById('meter').innerHTML = Array.from({length: 34}, () => '<i></i>').join('');
|
||||
|
||||
ta.addEventListener('input', () => {
|
||||
sendBtn.disabled = ta.value.trim() === '';
|
||||
ta.style.height = '21px'; ta.style.height = Math.min(ta.scrollHeight, 110) + 'px';
|
||||
});
|
||||
function send() {
|
||||
const t = ta.value.trim(); if (!t) return;
|
||||
add(t, 'u'); ta.value = ''; ta.style.height = '21px'; sendBtn.disabled = true;
|
||||
setTimeout(() => {
|
||||
const s = document.createElement('div'); s.className = 'step';
|
||||
s.onclick = () => s.classList.toggle('open');
|
||||
s.innerHTML = '<div class="step-h"><span class="ico">⚙</span><span class="nm">Выполняю</span>' +
|
||||
'<span class="stat">идёт…</span><span class="chev">▼</span></div>' +
|
||||
'<div class="step-b">Задача: ' + t + '</div>';
|
||||
flow.appendChild(s);
|
||||
setTimeout(() => {
|
||||
s.querySelector('.stat').textContent = 'готово · 3 с';
|
||||
s.querySelector('.stat').style.color = 'var(--ok)';
|
||||
s.classList.add('open');
|
||||
add('Сделано: ' + t, 'a');
|
||||
}, 1400);
|
||||
}, 300);
|
||||
}
|
||||
function add(text, who) {
|
||||
const d = document.createElement('div'); d.className = who;
|
||||
const n = new Date();
|
||||
d.innerHTML = text + '<span class="tm">' + n.getHours() + ':' + String(n.getMinutes()).padStart(2,'0') + '</span>';
|
||||
flow.appendChild(d); flow.scrollTop = flow.scrollHeight;
|
||||
}
|
||||
sendBtn.onclick = send;
|
||||
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
|
||||
|
||||
const P = 'разберись с красным си ай и выпусти новый релиз'.split(' ');
|
||||
let tm = null, sec = 0, wi = 0;
|
||||
function startRec() {
|
||||
sec = 0; wi = 0; recState.classList.add('on'); recBtn.classList.add('live');
|
||||
tm = setInterval(() => {
|
||||
sec++; document.getElementById('rt').textContent = '0:' + String(sec).padStart(2,'0');
|
||||
if (wi < P.length) liveTxt.textContent = P.slice(0, ++wi).join(' ');
|
||||
}, 500);
|
||||
}
|
||||
function stopRec(cancel) {
|
||||
clearInterval(tm); recState.classList.remove('on'); recBtn.classList.remove('live');
|
||||
if (cancel) return;
|
||||
const t = liveTxt.textContent;
|
||||
if (t && sec >= 2) { add(t, 'u'); setTimeout(() => add('Принято голосом, начинаю.', 'a'), 800); }
|
||||
}
|
||||
recBtn.onclick = () => recState.classList.contains('on') ? stopRec(false) : startRec();
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); recBtn.click(); }
|
||||
if (e.key === 'Escape' && recState.classList.contains('on')) stopRec(true);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,73 +0,0 @@
|
||||
# Вариант 4 — Настройки: агенты
|
||||
|
||||
> **МАКЕТ ОДОБРЕН 2026-09-19.** Одобренная версия — `approved/004-settings-agents.html`
|
||||
> (без панели-переключателя и без фона основного окна). Этот файл — черновик
|
||||
> со всеми состояниями и заметками.
|
||||
|
||||
## Суть
|
||||
Два окна друг над другом. Первое — настройки: список агентов и форма нового.
|
||||
Второе, поверх — **проверка связи**: нажал «Проверить связь» и она идёт
|
||||
в отдельном окне, а не строкой внутри формы.
|
||||
|
||||
## Окно 1: список агентов
|
||||
- У каждого агента: имя, адрес, состояние — «на связи» зелёным, «не отвечает» серым,
|
||||
ошибка красным. Видно сразу, а не спрятано в глубине.
|
||||
- Кнопки в строке: **проверить связь** (⇄), **изменить** (✎), **удалить** (🗑).
|
||||
Проверка доступна и для уже добавленного агента — из этого же списка.
|
||||
- Внизу пунктирная «+ Добавить агента».
|
||||
|
||||
## Окно 1, вид 2: форма нового агента
|
||||
- **Название** — с пояснением, что имя придумывает пользователь: сервер своего
|
||||
имени не сообщает (это из исходников, не догадка).
|
||||
- **Адрес сервера** — целиком, вместе с путём `/agentik`. Прямо в строке адреса
|
||||
кнопка **«Проверить связь»**.
|
||||
- **Чем отличать в списке: цвет или картинка.** Переключатель, выбирается что-то одно.
|
||||
- **Цвет** — простой выбор из пяти.
|
||||
- **Картинка** — файл с диска: круглый превью, «Выбрать файл…» и «Убрать».
|
||||
Картинка показывается там же, где был бы цвет: в списке агентов, в диалогах,
|
||||
в шапке чата.
|
||||
- ⚠️ **Картинку копировать себе, а не брать по исходному адресу.** Выбранный файл
|
||||
сохраняется внутрь клиента; дальше используется только эта копия. Если держать
|
||||
ссылку на исходный путь, значок пропадёт, когда файл удалят, переименуют или
|
||||
вынут флешку. То же для картинки по ссылке: скачать и положить себе.
|
||||
|
||||
## Окно 2: проверка связи (отдельное, поверх настроек)
|
||||
Три состояния, сменяются в одном окне:
|
||||
|
||||
1. **Идёт** — крутящееся кольцо, «Спрашиваю агента…», под ним адрес.
|
||||
2. **Отвечает** — зелёная галочка, «Агент отвечает». Подробности: ответ на запрос,
|
||||
адрес, число диалогов, отклик в миллисекундах. Подсказка «Адрес рабочий».
|
||||
Кнопка внизу — «Добавить агента» (доступна).
|
||||
3. **Не отвечает** — красный крестик. Подробности: нет ответа, отклик истёк.
|
||||
Подсказка: проверить адрес, что агент запущен, что порт открыт и путь `/agentik`
|
||||
указан целиком. Кнопка внизу заблокирована — добавить нельзя.
|
||||
|
||||
Внизу окна всегда есть **«Проверить снова»**. Закрытие окна возвращает к настройкам,
|
||||
а не выбрасывает из них.
|
||||
|
||||
## Почему проверка отдельным окном — лучше
|
||||
- Места больше: видно и результат, и подробности, и объяснение, что делать дальше.
|
||||
- Основную форму не дёргает: пока проверяешь — поля на месте, ничего не сбилось.
|
||||
- Одно и то же окно годится и для формы, и для уже добавленного агента.
|
||||
|
||||
## На чём основана проверка (не выдумка)
|
||||
У сервера агента есть ответ на `GET /agentik/health` (отдаёт «ok») и запрос
|
||||
списка диалогов. Оба безопасные — ничего не меняют. Адреса взяты из исходников.
|
||||
|
||||
⚠️ **Живого агента сейчас не поднято** — на порту 8080 отвечает чужой сервер (404).
|
||||
Поэтому на настоящем агенте проверка ещё не гонялась: в макете показаны все три
|
||||
состояния, но замеры времени и число диалогов — условные.
|
||||
|
||||
## Фон: папки вернулись
|
||||
В первом варианте над списком диалогов есть **папки** — «Все чаты», «Работа», «Дом»,
|
||||
«+». В прошлой версии этого макета я их потерял. Теперь на фоне видны и папки,
|
||||
и поиск, и значок непрочитанных — как в одобренном варианте 1.
|
||||
|
||||
## Плюс: список диалогов с двумя агентами
|
||||
Диалоги сгруппированы по агентам, у группы цветная точка, у каждой строки цветная
|
||||
полоска, цвет аватара совпадает с цветом агента из настроек.
|
||||
|
||||
## Что слабо / открытые вопросы
|
||||
- Одного цвета для различения может оказаться мало.
|
||||
- Где хранится список агентов — не решено.
|
||||
- Пароля/ключа доступа у библиотеки нет — поля для него нет.
|
||||
@@ -1,625 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Настройки: агенты</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
|
||||
}
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
/* ============ фон: обычный клиент ============ */
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* ── папки: как в выбранном варианте 1 ── */
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; align-items: flex-start; cursor: pointer; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
||||
.row-tail { flex: 0 0 auto; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700;
|
||||
border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.ic-btn { width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 16px; margin-left: auto; }
|
||||
.ic-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ ЗАТЕМНЕНИЕ (одно на оба окна) ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; display: none; }
|
||||
.scrim.on { display: block; }
|
||||
.scrim.top { background: rgba(0,0,0,.42); z-index: 200; }
|
||||
|
||||
/* ============ ОКНО НАСТРОЕК ============ */
|
||||
.layer { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; pointer-events: none; }
|
||||
.layer.on { display: flex; }
|
||||
.layer > * { pointer-events: auto; }
|
||||
/* порядок слоёв: настройки 150, проверка поверх 250 */
|
||||
#layerSettings { z-index: 150; }
|
||||
#layerCheck { z-index: 250; }
|
||||
|
||||
.dialog {
|
||||
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px; background: #17212B;
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
/* список агентов */
|
||||
.agent-card {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.agent-card:hover { border-color: var(--line); }
|
||||
.agent-card .avatar { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
|
||||
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
|
||||
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
|
||||
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
|
||||
/* форма */
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
.note code { background: var(--panel2); padding: 1px 4px; border-radius: 4px; font-size: 10.5px; }
|
||||
|
||||
/* адрес + кнопка проверки в одной строке */
|
||||
.addr-row { display: flex; gap: 8px; }
|
||||
.addr-row input { flex: 1; }
|
||||
|
||||
.colors { display: flex; gap: 9px; }
|
||||
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
|
||||
.swatch.on { border-color: var(--fg); }
|
||||
|
||||
/* цвет ИЛИ картинка */
|
||||
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
|
||||
.appearance-tabs .tab {
|
||||
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.pane { display: none; }
|
||||
.pane.on { display: block; }
|
||||
|
||||
.pic-row { display: flex; align-items: center; gap: 13px; }
|
||||
.pic-preview {
|
||||
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
|
||||
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
|
||||
font-size: 20px; overflow: hidden;
|
||||
}
|
||||
.pic-preview.filled { border-style: solid; border-color: var(--line); }
|
||||
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.avatar.pic { background: transparent; padding: 0; overflow: hidden; }
|
||||
.avatar.pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
/* кнопки */
|
||||
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
|
||||
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
|
||||
.btn:hover { border-color: var(--dim); }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
|
||||
.btn.prim:disabled { background: var(--panel2); border-color: var(--line); color: var(--dim); cursor: default; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* ============ ОКНО ПРОВЕРКИ СВЯЗИ ============ */
|
||||
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
|
||||
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
|
||||
@keyframes sp { to { transform: rotate(360deg); } }
|
||||
|
||||
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
|
||||
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-size: 19px; flex: 0 0 40px; }
|
||||
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
|
||||
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
|
||||
.probe-head.busy .big { background: transparent; padding: 0; }
|
||||
.probe-head .txt { flex: 1; }
|
||||
.probe-head .ttl { font-size: 14px; font-weight: 600; }
|
||||
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px; }
|
||||
|
||||
.probe-list { font-size: 12px; }
|
||||
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.probe-list div:last-child { border-bottom: none; }
|
||||
.probe-list span { color: var(--dim); }
|
||||
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; }
|
||||
|
||||
.hint-line { font-size: 11.5px; color: var(--dim); margin-top: 12px; padding: 9px 11px;
|
||||
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); }
|
||||
|
||||
/* переключатель состояний — только для показа */
|
||||
.switcher {
|
||||
position: fixed; top: 14px; left: 50%; transform: translateX(-50%); display: flex; gap: 7px;
|
||||
background: rgba(23,33,43,.96); border: 1px solid var(--line); border-radius: 11px; padding: 7px 9px; z-index: 300;
|
||||
}
|
||||
.switcher button { background: transparent; border: none; color: var(--dim); font-size: 12px;
|
||||
padding: 5px 11px; border-radius: 7px; cursor: pointer; font-family: inherit; }
|
||||
.switcher button.on { background: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
|
||||
/* ============ список диалогов с несколькими агентами ============ */
|
||||
.group-head { display: flex; align-items: center; gap: 8px; padding: 11px 10px 6px; }
|
||||
.group-head .dot { width: 9px; height: 9px; border-radius: 50%; }
|
||||
.group-head .nm { font-size: 10.5px; letter-spacing: 1.1px; text-transform: uppercase; color: var(--dim); font-weight: 700; }
|
||||
.group-head .cnt { font-size: 10.5px; color: var(--dim); margin-left: auto; }
|
||||
.accent-bar { width: 3px; border-radius: 2px; flex: 0 0 3px; align-self: stretch; }
|
||||
|
||||
@media (max-height: 820px) { body { padding: 8px; } .window { transform: scale(.8); } }
|
||||
@media (max-height: 700px) { .window { transform: scale(.68); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="switcher">
|
||||
<button data-view="list" class="on">Список агентов</button>
|
||||
<button data-view="add">Добавить агента</button>
|
||||
<button data-view="check" data-mode="ok">Проверка: отвечает</button>
|
||||
<button data-view="check" data-mode="err">Проверка: не отвечает</button>
|
||||
<button data-view="multi">Список диалогов</button>
|
||||
</div>
|
||||
|
||||
<div class="window">
|
||||
<!-- ===== фон: клиент с папками ===== -->
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="ic-btn" style="margin:0" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…">
|
||||
|
||||
<div class="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt">3</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">+</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list" id="chatArea"></div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="chat-head">
|
||||
<div class="avatar">АГ</div>
|
||||
<div>
|
||||
<div class="head-name">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
<button class="ic-btn" title="Настройки">⚙</button>
|
||||
</header>
|
||||
<div class="messages">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ===== затемнение под окном настроек ===== -->
|
||||
<div class="scrim on" id="scrim1"></div>
|
||||
|
||||
<!-- ===== ОКНО 1: настройки ===== -->
|
||||
<div class="layer on" id="layerSettings">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2 id="dTitle">Агенты</h2>
|
||||
<button class="x-btn" onclick="closeAll()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- ── вид 1: список агентов ── -->
|
||||
<div class="d-body" id="viewList">
|
||||
<div class="agent-card">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state"><span class="d"></span>на связи</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-card">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
</div>
|
||||
<div class="state err"><span class="d"></span>не отвечает</div>
|
||||
<div class="card-acts">
|
||||
<button class="mini-ic" title="Проверить связь">⇄</button>
|
||||
<button class="mini-ic" title="Изменить">✎</button>
|
||||
<button class="mini-ic del" title="Удалить">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn wide" onclick="show('add')">+ Добавить агента</button>
|
||||
</div>
|
||||
|
||||
<!-- ── вид 2: форма ── -->
|
||||
<div class="d-body" id="viewAdd" style="display:none">
|
||||
<div class="field">
|
||||
<label>Название</label>
|
||||
<input type="text" value="рабочий" placeholder="как вы его будете звать">
|
||||
<div class="note">Как называть этого агента в списке. Придумываете сами — сервер своего имени не сообщает.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Адрес сервера агента</label>
|
||||
<div class="addr-row">
|
||||
<input type="text" class="mono" value="http://192.168.88.135:8080/agentik"
|
||||
placeholder="http://хост:порт/agentik">
|
||||
<button class="btn sm" onclick="openCheck('form')">Проверить связь</button>
|
||||
</div>
|
||||
<div class="note">Адрес целиком, вместе с путём <code>/agentik</code>. Если агент
|
||||
на этой же машине — <code>http://localhost:8080/agentik</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Как отличать в списке</label>
|
||||
<div class="appearance-tabs">
|
||||
<div class="tab on" data-tab="color">Цвет</div>
|
||||
<div class="tab" data-tab="pic">Картинка</div>
|
||||
</div>
|
||||
|
||||
<!-- цвет -->
|
||||
<div class="pane on" id="paneColor">
|
||||
<div class="colors">
|
||||
<div class="swatch on" style="background:#6AB2F2"></div>
|
||||
<div class="swatch" style="background:#86C99A"></div>
|
||||
<div class="swatch" style="background:#C9A886"></div>
|
||||
<div class="swatch" style="background:#B79AE0"></div>
|
||||
<div class="swatch" style="background:#E28C8C"></div>
|
||||
</div>
|
||||
<div class="note">Цвет остаётся простым выбором: им помечаются диалоги агента
|
||||
в списке — полоска у строки и точка у группы.</div>
|
||||
</div>
|
||||
|
||||
<!-- картинка -->
|
||||
<div class="pane" id="panePic">
|
||||
<div class="pic-row">
|
||||
<div class="pic-preview" id="picPreview">🖼</div>
|
||||
<div class="pic-actions">
|
||||
<button class="btn sm" onclick="pickPic()">Выбрать файл…</button>
|
||||
<button class="btn sm" id="picClear" onclick="clearPic()" style="display:none">Убрать</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">Картинка копируется <b>внутрь клиента</b> — по исходному
|
||||
адресу не подтягивается. Иначе значок пропадёт, если файл удалят,
|
||||
флешку вынут или папку переименуют.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot" id="footList">
|
||||
<button class="btn prim" style="flex:1" onclick="closeAll()">Готово</button>
|
||||
</div>
|
||||
<div class="d-foot" id="footAdd" style="display:none">
|
||||
<button class="btn" style="flex:1" onclick="show('list')">Отмена</button>
|
||||
<button class="btn prim" style="flex:1">Добавить агента</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== затемнение под окном проверки ===== -->
|
||||
<div class="scrim top" id="scrim2"></div>
|
||||
|
||||
<!-- ===== ОКНО 2: проверка связи (поверх настроек) ===== -->
|
||||
<div class="layer" id="layerCheck">
|
||||
<div class="dialog small">
|
||||
<div class="d-head">
|
||||
<h2>Проверка связи</h2>
|
||||
<button class="x-btn" onclick="closeCheck()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="d-body">
|
||||
<div class="probe-head" id="probeHead">
|
||||
<div class="big" id="probeIcon"></div>
|
||||
<div class="txt">
|
||||
<div class="ttl" id="probeTitle">Спрашиваю агента…</div>
|
||||
<div class="sub" id="probeSub">http://192.168.88.135:8080/agentik</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="probe-list" id="probeList" style="display:none">
|
||||
<div><span>Ответ на запрос</span><b id="p1">—</b></div>
|
||||
<div><span>Отвечает по адресу</span><b id="p2">—</b></div>
|
||||
<div><span>Диалогов у агента</span><b id="p3">—</b></div>
|
||||
<div><span>Отклик</span><b id="p4">—</b></div>
|
||||
</div>
|
||||
|
||||
<div class="hint-line" id="probeHint" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1" id="probeAgain" onclick="runCheck()">Проверить снова</button>
|
||||
<button class="btn prim" style="flex:1" id="probeOk" onclick="closeCheck()" disabled>Добавить агента</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---------------- данные списка диалогов ----------------
|
||||
const CHATS_SINGLE = `
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g3">РЗ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Разбор логов nginx</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Нашёл 3 ошибки 502</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_MULTI = `
|
||||
<div class="group-head"><span class="dot" style="background:#6AB2F2"></span>
|
||||
<span class="nm">рабочий</span><span class="cnt">2 диалога</span></div>
|
||||
<div class="chat-row active">
|
||||
<div class="accent-bar" style="background:#6AB2F2"></div>
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>CI и релизы</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="accent-bar" style="background:#6AB2F2"></div>
|
||||
<div class="avatar">ОЧ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Отчёт за квартал</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-head"><span class="dot" style="background:#86C99A"></span>
|
||||
<span class="nm">домашний</span><span class="cnt">1 диалог</span></div>
|
||||
<div class="chat-row">
|
||||
<div class="accent-bar" style="background:#86C99A"></div>
|
||||
<div class="avatar g2">КН</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>Книги и полки</span><span class="chat-time">вчера</span></div>
|
||||
<div class="chat-prev">Разложил по авторам</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const chatArea = document.getElementById('chatArea');
|
||||
chatArea.innerHTML = CHATS_SINGLE;
|
||||
|
||||
// ---------------- окна ----------------
|
||||
function el(id){ return document.getElementById(id); }
|
||||
|
||||
function show(v) {
|
||||
const isList = (v === 'list');
|
||||
el('viewList').style.display = isList ? 'block' : 'none';
|
||||
el('viewAdd').style.display = isList ? 'none' : 'block';
|
||||
el('footList').style.display = isList ? 'flex' : 'none';
|
||||
el('footAdd').style.display = isList ? 'none' : 'flex';
|
||||
el('dTitle').textContent = isList ? 'Агенты' : 'Новый агент';
|
||||
openSettings();
|
||||
}
|
||||
|
||||
function openSettings(){
|
||||
el('scrim1').classList.add('on');
|
||||
el('layerSettings').classList.add('on');
|
||||
}
|
||||
// закрыть окно настроек (вместе с окном проверки, если оно открыто)
|
||||
function closeAll(){
|
||||
el('layerCheck').classList.remove('on');
|
||||
el('scrim2').classList.remove('on');
|
||||
el('layerSettings').classList.remove('on');
|
||||
el('scrim1').classList.remove('on');
|
||||
}
|
||||
function closeCheck(){
|
||||
el('layerCheck').classList.remove('on');
|
||||
el('scrim2').classList.remove('on');
|
||||
}
|
||||
|
||||
// ---------------- ОКНО ПРОВЕРКИ СВЯЗИ ----------------
|
||||
// отдельное окно поверх настроек: из формы нового агента или из списка
|
||||
function openCheck(from, addr) {
|
||||
el('scrim2').classList.add('on');
|
||||
el('layerCheck').classList.add('on');
|
||||
el('probeOk').textContent = (from === 'form') ? 'Добавить агента' : 'Готово';
|
||||
window.__checkAddr = addr || 'http://192.168.88.135:8080/agentik';
|
||||
runCheck();
|
||||
}
|
||||
|
||||
function runCheck() {
|
||||
const head = el('probeHead');
|
||||
const list = el('probeList');
|
||||
const hint = el('probeHint');
|
||||
const icon = el('probeIcon');
|
||||
const addr = window.__checkAddr || 'http://192.168.88.135:8080/agentik';
|
||||
|
||||
head.className = 'probe-head busy';
|
||||
icon.outerHTML = '<span class="spin" id="probeIcon"></span>';
|
||||
el('probeTitle').textContent = 'Спрашиваю агента…';
|
||||
el('probeSub').textContent = addr;
|
||||
list.style.display = 'none';
|
||||
hint.style.display = 'none';
|
||||
el('probeOk').disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
const sp = document.getElementById('probeIcon');
|
||||
sp.outerHTML = '<div class="big" id="probeIcon"></div>';
|
||||
const fail = (window.__checkMode === 'err');
|
||||
|
||||
if (fail) {
|
||||
el('probeHead').className = 'probe-head err';
|
||||
el('probeIcon').textContent = '✕';
|
||||
el('probeTitle').textContent = 'Агент не отвечает';
|
||||
el('probeSub').textContent = addr;
|
||||
el('p1').textContent = 'нет ответа'; el('p2').textContent = '—';
|
||||
el('p3').textContent = '—'; el('p4').textContent = '5000 мс (истекло)';
|
||||
list.style.display = 'block';
|
||||
hint.style.display = 'block';
|
||||
hint.textContent = 'Проверьте адрес и что агент запущен. Убедитесь, что порт открыт и путь /agentik указан целиком.';
|
||||
el('probeOk').disabled = true;
|
||||
} else {
|
||||
el('probeHead').className = 'probe-head ok';
|
||||
el('probeIcon').textContent = '✓';
|
||||
el('probeTitle').textContent = 'Агент отвечает';
|
||||
el('probeSub').textContent = addr;
|
||||
el('p1').textContent = 'ok'; el('p2').textContent = addr;
|
||||
el('p3').textContent = '4'; el('p4').textContent = '38 мс';
|
||||
list.style.display = 'block';
|
||||
hint.style.display = 'block';
|
||||
hint.textContent = 'Адрес рабочий. Можно добавлять этого агента.';
|
||||
el('probeOk').disabled = false;
|
||||
}
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
// ---------------- переключатель для показа ----------------
|
||||
document.querySelectorAll('.switcher button').forEach(b => b.onclick = () => {
|
||||
document.querySelectorAll('.switcher button').forEach(x => x.classList.remove('on'));
|
||||
b.classList.add('on');
|
||||
const v = b.dataset.view;
|
||||
|
||||
if (v === 'multi') {
|
||||
chatArea.innerHTML = CHATS_MULTI;
|
||||
closeAll();
|
||||
return;
|
||||
}
|
||||
chatArea.innerHTML = CHATS_SINGLE;
|
||||
if (v === 'list') { show('list'); closeCheck(); }
|
||||
if (v === 'add') { show('add'); closeCheck(); }
|
||||
if (v === 'check') {
|
||||
window.__checkMode = b.dataset.mode || 'ok';
|
||||
const okAddr = 'http://192.168.88.135:8080/agentik';
|
||||
const badAddr = 'http://192.168.76.199:8080/agentik';
|
||||
show(window.__checkMode === 'ok' ? 'add' : 'list');
|
||||
openCheck('form', window.__checkMode === 'ok' ? okAddr : badAddr);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.swatch').forEach(s => s.onclick = () => {
|
||||
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('on'));
|
||||
s.classList.add('on');
|
||||
});
|
||||
|
||||
// ---------------- как отличать: цвет ИЛИ картинка ----------------
|
||||
document.querySelectorAll('.appearance-tabs .tab').forEach(tab => tab.onclick = () => {
|
||||
document.querySelectorAll('.appearance-tabs .tab').forEach(x => x.classList.remove('on'));
|
||||
tab.classList.add('on');
|
||||
const pic = (tab.dataset.tab === 'pic');
|
||||
el('paneColor').classList.toggle('on', !pic);
|
||||
el('panePic').classList.toggle('on', pic);
|
||||
});
|
||||
|
||||
// выбор картинки: в настоящем клиенте — системный диалог выбора файла,
|
||||
// а выбранный файл КОПИРУЕТСЯ в папку клиента (не ссылка на исходный путь)
|
||||
function pickPic() {
|
||||
// для макета показываем готовый пример вместо файлового диалога
|
||||
setPic('data:image/svg+xml;utf8,' + encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54">
|
||||
<rect width="54" height="54" fill="#86C99A"/>
|
||||
<circle cx="27" cy="21" r="8" fill="#121218"/>
|
||||
<path d="M10 50 Q27 30 44 50 Z" fill="#121218"/>
|
||||
</svg>`));
|
||||
}
|
||||
|
||||
function setPic(src) {
|
||||
const p = el('picPreview');
|
||||
p.innerHTML = `<img src="${src}" alt="">`;
|
||||
p.classList.add('filled');
|
||||
el('picClear').style.display = '';
|
||||
}
|
||||
|
||||
function clearPic() {
|
||||
const p = el('picPreview');
|
||||
p.innerHTML = '🖼';
|
||||
p.classList.remove('filled');
|
||||
el('picClear').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,72 +0,0 @@
|
||||
# Вариант 5 — Новый диалог: выбор ассистента
|
||||
|
||||
> **МАКЕТ ОДОБРЕН 2026-09-19.** Одобренная версия — `approved/005-new-chat-picker.html`.
|
||||
> Этот файл — черновик со всеми состояниями.
|
||||
|
||||
## Суть
|
||||
|
||||
Окно, которое появляется при нажатии «+» (новый диалог), **когда ассистентов
|
||||
больше одного**. Спрашиваем, в каком ассистенте создать диалог.
|
||||
|
||||
**Когда окно показывается и когда нет:**
|
||||
|
||||
| Сколько ассистентов | Что происходит по «+» |
|
||||
|---|---|
|
||||
| **0** | Кнопки «+» **нет вообще** — создавать диалог не в ком |
|
||||
| **1** | Окно **не показывается**: диалог создаётся сразу в этом ассистенте |
|
||||
| **2 и больше** | Окно с выбором — этот макет |
|
||||
|
||||
## Состояния (переключатель сверху)
|
||||
|
||||
1. **Выбор: 2 ассистента** — основной вид. Сверху подсказка «В каком ассистенте
|
||||
создать диалог?», ниже список. Первый отмечен **по умолчанию** — чтобы создать
|
||||
диалог, достаточно одного нажатия.
|
||||
2. **Третий не отвечает** — выбран ассистент без связи. Окно **предупреждает**,
|
||||
но выбрать даёт: ассистент мог просто перезапускаться. Из списка не выкидываем.
|
||||
3. **Ассистент один** — окна нет, затемнения нет.
|
||||
4. **Ассистентов нет** — кнопки «+» нет, пустой список объясняет, что делать,
|
||||
и ведёт в настройки.
|
||||
5. **Диалог создан** — новый диалог первым в списке, шапка чата показывает
|
||||
выбранного ассистента (его цвет и имя).
|
||||
|
||||
## Что видно в списке выбора
|
||||
|
||||
- **Значок ассистента** — цвет или картинка, как настроено в `004-settings-agents`.
|
||||
- **Имя** и **адрес сервера** — чтобы различить похожие имена.
|
||||
- **Состояние связи**: «на связи» зелёным, «не отвечает» красным.
|
||||
- **Кружок отметки** справа.
|
||||
- У ассистента без связи предупреждение появляется **при выборе**, а не сразу:
|
||||
пока не выбран — шума нет.
|
||||
|
||||
## Решения
|
||||
|
||||
- **Выбран первый по умолчанию.** Открыл окно → сразу «Создать диалог» работает.
|
||||
Лишнее движение нужно только если нужен не первый.
|
||||
- **Ассистента без связи выбирать можно.** Запрещать нечестно: связь может
|
||||
пропасть на секунду, а диалог человек создаёт осознанно. Окно предупреждает
|
||||
словами, но не мешает.
|
||||
- **Кнопку «+» при нуле ассистентов не рисуем.** Нажимать нечего — диалог
|
||||
создавать не в ком.
|
||||
- **Окно маленькое (480 px) и без вкладок.** Одно действие — один выбор.
|
||||
Окно настроек (`004`) — 620 px, оно про управление; здесь просто выбор.
|
||||
|
||||
## Что НЕ делаем
|
||||
|
||||
- Не даём создавать диалог «без ассистента» — по библиотеке `client` диалог
|
||||
живёт **внутри** агента, отдельно его не создать.
|
||||
- Не показываем окно ради одного варианта — это лишнее нажатие.
|
||||
- Не прячем ассистентов без связи (см. решения выше).
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- Что делать, если **все** ассистенты не отвечают: показывать окно со всеми
|
||||
красными или сразу писать «никто не отвечает»? Сейчас — показываем список.
|
||||
- Нужен ли в этом окне шорткат «создать и сразу написать», то есть открывать
|
||||
диалог с фокусом в поле ввода. Пока просто создаём.
|
||||
- Нужна ли в окне ссылка «добавить нового ассистента» — сейчас её нет, за
|
||||
добавлением идём в настройки.
|
||||
|
||||
## Из чего собрано
|
||||
|
||||
Стиль и классы взяты из `004-settings-agents` — тема та же (`#121218`, `#17212B`,
|
||||
`#6AB2F2`), окно и затемнение устроены так же. Ничего нового не изобреталось.
|
||||
@@ -1,436 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Новый диалог — выбор ассистента</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 100vh; padding: 24px; gap: 16px;
|
||||
}
|
||||
|
||||
/* ============ переключатель состояний ============ */
|
||||
.switcher { display: flex; gap: 7px; flex-wrap: wrap; justify-content: center; max-width: 1180px; }
|
||||
.switcher button {
|
||||
padding: 7px 13px; border-radius: 9px; border: 1px solid var(--line); background: var(--panel);
|
||||
color: var(--dim); font-size: 12.5px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.switcher button:hover { color: var(--fg); }
|
||||
.switcher button.on { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
|
||||
/* ============ окно клиента ============ */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }
|
||||
.search {
|
||||
margin: 0 16px 10px; padding: 8px 11px; border-radius: 9px; border: 1px solid transparent;
|
||||
background: var(--panel2); color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
.folders { display: flex; gap: 6px; padding: 0 16px 10px; overflow-x: auto; }
|
||||
.folder {
|
||||
padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 2px 8px 12px; }
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; align-items: flex-start; cursor: pointer; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.avatar {
|
||||
width: 42px; height: 42px; border-radius: 50%; flex: 0 0 42px; display: grid; place-items: center;
|
||||
font-size: 15px; font-weight: 600; color: #0F1922; background: var(--accent);
|
||||
}
|
||||
.avatar.g2 { background: #86C99A; }
|
||||
.avatar.g3 { background: #C9A886; }
|
||||
/* аватар-картинка: клиент показывает СВОЮ копию файла */
|
||||
.avatar.pic {
|
||||
background: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E") center/cover;
|
||||
}
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
||||
.row-tail { flex: 0 0 auto; }
|
||||
.badge { background: var(--badge); color: #0F1922; font-size: 11px; font-weight: 700; border-radius: 10px; padding: 1px 7px; }
|
||||
|
||||
/* кнопка «+» рядом с заголовком. Состояние .hidden — при нуле ассистентов */
|
||||
.ic-btn { width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 16px; }
|
||||
.ic-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.add-btn { font-size: 19px; color: var(--accent); }
|
||||
.add-btn.hidden { display: none; }
|
||||
|
||||
/* пустой список */
|
||||
.empty { padding: 44px 26px; text-align: center; }
|
||||
.empty .big { font-size: 34px; opacity: .28; margin-bottom: 12px; }
|
||||
.empty .ttl { font-size: 14px; font-weight: 600; margin-bottom: 7px; }
|
||||
.empty .sub { font-size: 12.5px; color: var(--dim); line-height: 1.55; }
|
||||
.empty .link { color: var(--accent); cursor: pointer; text-decoration: none; border-bottom: 1px dashed var(--accent); }
|
||||
|
||||
.main { flex: 1; display: flex; flex-direction: column; }
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.chat-head .ic-btn { margin-left: auto; }
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
|
||||
/* ============ затемнение и окно ============ */
|
||||
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.62); z-index: 100; display: none; }
|
||||
.scrim.on { display: block; }
|
||||
|
||||
.layer { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; z-index: 150; pointer-events: none; }
|
||||
.layer.on { display: flex; }
|
||||
.layer > * { pointer-events: auto; }
|
||||
|
||||
.dialog {
|
||||
width: 480px; min-width: 480px; flex: 0 0 auto; max-height: 600px; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
|
||||
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 8px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
|
||||
|
||||
/* ── вариант выбора ассистента ── */
|
||||
.agent-opt {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; cursor: pointer;
|
||||
border: 1.5px solid transparent;
|
||||
}
|
||||
.agent-opt:hover { border-color: #33414F; }
|
||||
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
|
||||
.agent-opt .avatar { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
|
||||
.ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.ag-state.ok { color: var(--ok); }
|
||||
.ag-state.off { color: var(--dim); } .ag-state.off .d { background: var(--dim); }
|
||||
.ag-state.err { color: var(--err); } .ag-state.err .d { background: var(--err); }
|
||||
|
||||
/* кружок-галочка выбора */
|
||||
.pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%; border: 2px solid #3D4A58; }
|
||||
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
|
||||
.agent-opt.on .pick::after {
|
||||
content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 9px;
|
||||
border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg);
|
||||
}
|
||||
|
||||
/* предупреждение под списком */
|
||||
.warn {
|
||||
display: none; gap: 8px; align-items: flex-start; margin-top: 4px; padding: 10px 12px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA;
|
||||
}
|
||||
.warn.on { display: flex; }
|
||||
.warn .i { flex: 0 0 auto; }
|
||||
|
||||
/* кнопки */
|
||||
.btn {
|
||||
padding: 10px 16px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel2);
|
||||
color: var(--fg); font-size: 13.5px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.btn:hover { border-color: #3D4A58; }
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
.btn.prim:disabled { background: #253544; border-color: #253544; color: #5C6B7A; cursor: not-allowed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="switcher">
|
||||
<button data-view="pick" class="on">Выбор: 2 ассистента</button>
|
||||
<button data-view="offline">Третий не отвечает</button>
|
||||
<button data-view="one">Ассистент один</button>
|
||||
<button data-view="zero">Ассистентов нет</button>
|
||||
<button data-view="made">Диалог создан</button>
|
||||
</div>
|
||||
|
||||
<div class="window">
|
||||
<!-- ================= СПИСОК ДИАЛОГОВ ================= -->
|
||||
<aside class="sidebar">
|
||||
<div class="sb-head">
|
||||
<div class="sb-title">
|
||||
<span>Диалоги</span>
|
||||
<button class="ic-btn add-btn" id="newChatBtn" title="Новый диалог">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<input class="search" placeholder="Поиск диалога…" id="searchBox">
|
||||
|
||||
<div class="folders" id="folders">
|
||||
<div class="folder active">Все чаты <span class="cnt" id="cnt">2</span></div>
|
||||
<div class="folder">Работа</div>
|
||||
<div class="folder">Дом</div>
|
||||
<div class="folder">+</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-list" id="chatList"></div>
|
||||
</aside>
|
||||
|
||||
<!-- ================= ЧАТ ================= -->
|
||||
<main class="main" id="mainPane">
|
||||
<header class="chat-head">
|
||||
<div class="avatar" id="headAv">АГ</div>
|
||||
<div>
|
||||
<div class="head-name" id="headName">agentik · рабочий</div>
|
||||
<div class="head-sub"><span class="dot">●</span> на связи</div>
|
||||
</div>
|
||||
<button class="ic-btn" title="Настройки">⚙</button>
|
||||
</header>
|
||||
<div class="messages" id="msgs">
|
||||
<div class="msg user">Проверь, почему падает CI</div>
|
||||
<div class="msg bot">Нашёл: длинное тире в именах тестов ломает сборку. Починил, CI зелёный.</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="scrim" id="scrim"></div>
|
||||
|
||||
<!-- ================= ОКНО: НОВЫЙ ДИАЛОГ ================= -->
|
||||
<div class="layer" id="layerNew">
|
||||
<div class="dialog">
|
||||
<div class="d-head">
|
||||
<h2>Новый диалог</h2>
|
||||
<button class="x-btn" onclick="closeDialog()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="d-body">
|
||||
<div class="d-lead" id="lead">В каком ассистенте создать диалог?</div>
|
||||
|
||||
<div id="agentOpts">
|
||||
<div class="agent-opt on" data-id="a1" data-ok="1" data-tag="АГ">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="agent-opt" data-id="a2" data-ok="1" data-tag="ДМ">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
|
||||
<div class="agent-opt" data-id="a3" data-ok="0" data-tag="КА" data-pic="1">
|
||||
<div class="avatar pic"></div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">Кот-ассистент</div>
|
||||
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
|
||||
<div class="ag-state err"><span class="d"></span>не отвечает</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warn" id="warn">
|
||||
<span class="i">⚠</span>
|
||||
<span id="warnText">Ассистент сейчас не отвечает. Диалог создать можно, но
|
||||
сообщение не уйдёт, пока он не поднимется.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-foot">
|
||||
<button class="btn" style="flex:1" onclick="closeDialog()">Отмена</button>
|
||||
<button class="btn prim" style="flex:1" id="createBtn" disabled>Создать диалог</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const el = id => document.getElementById(id);
|
||||
|
||||
// ───────── данные состояний ─────────
|
||||
const AG_ALL = `
|
||||
<div class="agent-opt on" data-id="a1" data-ok="1">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">agentik · рабочий</div>
|
||||
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
<div class="agent-opt" data-id="a2" data-ok="1">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">домашний</div>
|
||||
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
|
||||
<div class="ag-state ok"><span class="d"></span>на связи</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>
|
||||
<div class="agent-opt" data-id="a3" data-ok="0">
|
||||
<div class="avatar pic"></div>
|
||||
<div class="ag-mid">
|
||||
<div class="ag-name">Кот-ассистент</div>
|
||||
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
|
||||
<div class="ag-state err"><span class="d"></span>не отвечает</div>
|
||||
</div>
|
||||
<div class="pick"></div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_TWO = `
|
||||
<div class="chat-row active">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>домашний</span><span class="chat-time">12:05</span></div>
|
||||
<div class="chat-prev">Файл лежит на диске</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_NEW = `
|
||||
<div class="chat-row active">
|
||||
<div class="avatar g2">ДМ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>домашний</span><span class="chat-time">сейчас</span></div>
|
||||
<div class="chat-prev">Новый диалог</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-row">
|
||||
<div class="avatar">АГ</div>
|
||||
<div class="chat-mid">
|
||||
<div class="chat-name"><span>agentik · рабочий</span><span class="chat-time">14:22</span></div>
|
||||
<div class="chat-prev">Собрал отчёт, жду правок</div>
|
||||
</div>
|
||||
<div class="row-tail"><span class="badge">2</span></div>
|
||||
</div>`;
|
||||
|
||||
const CHATS_EMPTY = `
|
||||
<div class="empty">
|
||||
<div class="big">💬</div>
|
||||
<div class="ttl">Диалогов пока нет</div>
|
||||
<div class="sub">Ассистенты ещё не добавлены.<br>
|
||||
Добавьте первого в <a class="link" onclick="alert('Откроется окно настроек — макет 004-settings-agents')">настройках</a>.</div>
|
||||
</div>`;
|
||||
|
||||
// ───────── управление окном ─────────
|
||||
function setAgents(html) { el('agentOpts').innerHTML = html; bindOpts(); }
|
||||
|
||||
function bindOpts() {
|
||||
document.querySelectorAll('.agent-opt').forEach(o => o.onclick = () => {
|
||||
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
|
||||
o.classList.add('on');
|
||||
el('createBtn').disabled = false;
|
||||
el('warn').classList.toggle('on', o.dataset.ok === '0');
|
||||
});
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
el('scrim').classList.add('on');
|
||||
el('layerNew').classList.add('on');
|
||||
el('createBtn').disabled = false;
|
||||
}
|
||||
function closeDialog() {
|
||||
el('scrim').classList.remove('on');
|
||||
el('layerNew').classList.remove('on');
|
||||
}
|
||||
el('newChatBtn').onclick = openDialog;
|
||||
|
||||
document.querySelectorAll('.switcher button').forEach(b => b.onclick = () => {
|
||||
document.querySelectorAll('.switcher button').forEach(x => x.classList.remove('on'));
|
||||
b.classList.add('on');
|
||||
render(b.dataset.view);
|
||||
});
|
||||
|
||||
function render(v) {
|
||||
closeDialog();
|
||||
el('warn').classList.remove('on');
|
||||
|
||||
if (v === 'pick') {
|
||||
el('chatList').innerHTML = CHATS_TWO;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
setAgents(AG_ALL);
|
||||
el('createBtn').disabled = false;
|
||||
openDialog();
|
||||
}
|
||||
|
||||
if (v === 'offline') {
|
||||
el('chatList').innerHTML = CHATS_TWO;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
setAgents(AG_ALL);
|
||||
const off = document.querySelector('.agent-opt[data-ok="0"]');
|
||||
document.querySelectorAll('.agent-opt').forEach(x => x.classList.remove('on'));
|
||||
off.classList.add('on');
|
||||
el('warn').classList.add('on');
|
||||
el('createBtn').disabled = false;
|
||||
openDialog();
|
||||
}
|
||||
|
||||
if (v === 'one') {
|
||||
el('chatList').innerHTML = CHATS_TWO;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (v === 'zero') {
|
||||
el('chatList').innerHTML = CHATS_EMPTY;
|
||||
el('folders').style.display = 'none'; el('searchBox').style.display = 'none';
|
||||
el('newChatBtn').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (v === 'made') {
|
||||
el('chatList').innerHTML = CHATS_NEW;
|
||||
el('folders').style.display = ''; el('searchBox').style.display = '';
|
||||
el('cnt').textContent = '2';
|
||||
el('newChatBtn').classList.remove('hidden');
|
||||
el('headAv').textContent = 'ДМ'; el('headAv').className = 'avatar g2';
|
||||
el('headName').textContent = 'домашний';
|
||||
}
|
||||
}
|
||||
|
||||
bindOpts();
|
||||
render('pick');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
# sketches — макеты основного окна
|
||||
|
||||
**Здесь рабочий черновик основного окна десктопного клиента.** Это не
|
||||
«утверждено» — `approved/` живёт отдельно. Правило: одобренное живёт только
|
||||
там, а в `sketches/` — то, что ещё сравниваем и допиливаем.
|
||||
|
||||
## Раскладка: rail / voice-first
|
||||
|
||||
Узкая рейка иконок слева (диалоги / память / инструменты / новый),
|
||||
посередине список диалогов, справа чат. Голосовое сообщение — главное
|
||||
действие: крупная акцентная кнопка микрофона в композере.
|
||||
|
||||
**Цветовая схема:** тёмная тема, общий набор токенов из `../THEMES.md`.
|
||||
Палитра согласована с Android-клиентом — расширения (`--panel2`, `--line`,
|
||||
`--badge`, `--ok`, `--user-bubble`, `--bot-bubble`, `--a1`..`--a5`) тут
|
||||
уже есть.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Состояние |
|
||||
|---|---|
|
||||
| `style.css` | Общий CSS для всех макетов |
|
||||
| `1-chats.html` | Слева — список диалогов с цветными полосками агентов, справа — пусто («выберите диалог») |
|
||||
| `2-chat.html` | Открытый диалог: сообщения, ход работы агента, композер с полем + микрофон + отправка |
|
||||
| `3-recording.html` | Идёт запись — в композере волна + таймер + ⏸ + ✕ + ➤, одна строка 46 px |
|
||||
| `4-empty.html` | Список пуст, ни одного ассистента не добавлено |
|
||||
| `5-new-chat.html` | Модалка по центру — выбор ассистента из добавленных |
|
||||
| `6-groups.html` | Модалка управления группами: список с действиями «изменить» / «удалить» |
|
||||
| `7-groups-add.html` | Создание группы: имя + пустой список диалогов + кнопка «+ Добавить диалог» |
|
||||
| `8-groups-edit.html` | Переименование + теги уже добавленных диалогов с `✕` + блок «ОПАСНОЕ» с удалением группы |
|
||||
| `9-groups-pick-chat.html` | Выбор диалога для добавления в группу: поиск + чекбоксы по диалогам |
|
||||
|
||||
## Как смотреть
|
||||
|
||||
Открыть файл в браузере — самодостаточный, без сборки и без зависимостей.
|
||||
+1426
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,610 @@
|
||||
/* === палитра тёмной темы (согласована с Android, см. ../THEMES.md) === */
|
||||
:root {
|
||||
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
|
||||
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
|
||||
--err: #EC3942; --ok: #63C88A;
|
||||
--user-bubble: #2B5278; --bot-bubble: #1C2733;
|
||||
--a1: #6AB2F2; --a2: #86C99A; --a3: #C9A886; --a4: #B79AE0; --a5: #E28C8C;
|
||||
--on-accent: #0F1922;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #0A0A0F; color: var(--fg); line-height: 1.45;
|
||||
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 18px;
|
||||
}
|
||||
.win {
|
||||
width: 1240px; height: 760px; background: var(--bg);
|
||||
border-radius: 14px; overflow: hidden; border: 1px solid #2A3340;
|
||||
display: flex; box-shadow: 0 28px 90px rgba(0,0,0,.7);
|
||||
}
|
||||
.cap {
|
||||
position: absolute; margin-top: -22px; font-size: 12px; color: var(--dim);
|
||||
}
|
||||
@media (max-height: 820px) { body { padding: 8px; } .win { transform: scale(.78); } }
|
||||
@media (max-height: 700px) { .win { transform: scale(.66); } }
|
||||
@media (max-height: 600px) { .win { transform: scale(.56); } }
|
||||
|
||||
/* === средняя колонка: список диалогов === */
|
||||
.list {
|
||||
width: 296px; flex: 0 0 296px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.l-head { padding: 14px 16px 10px; display: flex; align-items: center; gap: 8px; }
|
||||
.l-head h1 { font-size: 15.5px; font-weight: 650; letter-spacing: .2px; }
|
||||
.chips { display: flex; gap: 6px; padding: 0 14px 10px; overflow-x: auto; }
|
||||
.chip {
|
||||
padding: 6px 11px; border-radius: 16px; font-size: 12px; color: var(--dim);
|
||||
background: transparent; white-space: nowrap; border: 1px solid transparent;
|
||||
}
|
||||
.chip:hover { background: var(--panel2); }
|
||||
.chip.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.13); }
|
||||
.chip b { color: var(--badge); font-weight: 700; margin-left: 3px; }
|
||||
.chip-act {
|
||||
margin-left: auto; flex: 0 0 auto; padding: 6px 9px; border-radius: 11px;
|
||||
background: transparent; color: var(--dim); font-size: 13px; cursor: pointer;
|
||||
border: 1px solid transparent; display: inline-flex; align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.chip-act:hover { color: var(--fg); background: var(--panel2); }
|
||||
.chip-act svg { width: 14px; height: 14px; stroke: currentColor; fill: none;
|
||||
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.q {
|
||||
margin: 0 14px 10px; height: 36px; border-radius: 11px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--dim); font-size: 12.5px;
|
||||
display: flex; align-items: center; gap: 8px; padding: 0 12px;
|
||||
}
|
||||
.rows { flex: 1; overflow-y: auto; padding: 0 8px 12px; }
|
||||
.row {
|
||||
display: flex; gap: 11px; padding: 9px 10px; border-radius: 12px; cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.row:hover { background: var(--panel2); }
|
||||
.row.on { background: #233040; }
|
||||
.row .strip {
|
||||
position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px;
|
||||
background: var(--a1); border-radius: 3px;
|
||||
}
|
||||
.row .ava {
|
||||
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%;
|
||||
display: grid; place-items: center; font-size: 14px; font-weight: 700; color: var(--on-accent);
|
||||
}
|
||||
.row .mid { flex: 1; min-width: 0; }
|
||||
.row .l1 { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
|
||||
.row .l1 .n { font-size: 13.5px; font-weight: 550; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.row .l1 .t { font-size: 10.5px; color: var(--dim); flex: 0 0 auto; }
|
||||
.row .l2 { display: flex; justify-content: space-between; gap: 8px; align-items: center; margin-top: 2px; }
|
||||
.row .l2 .p { font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.bd { background: var(--badge); color: var(--on-accent); border-radius: 11px; font-size: 10.5px; font-weight: 700; padding: 1px 6px; flex: 0 0 auto; }
|
||||
|
||||
/* === основная зона чата === */
|
||||
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
||||
.m-head {
|
||||
height: 60px; flex: 0 0 60px; display: flex; align-items: center; gap: 11px;
|
||||
padding: 0 22px; border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.m-head .ava { width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; font-size: 14px; font-weight: 700; color: var(--on-accent); }
|
||||
.m-head .hd .t1 { font-size: 14.5px; font-weight: 600; }
|
||||
.m-head .hd .t2 { font-size: 11.5px; color: var(--dim); }
|
||||
.m-head .hd .t2.ok { color: var(--ok); }
|
||||
.m-head .hd .t2.bad { color: var(--err); }
|
||||
.m-head .ghost { margin-left: auto; display: flex; gap: 6px; }
|
||||
.m-head .ghost button {
|
||||
background: transparent; border: 1px solid var(--line); color: var(--dim);
|
||||
border-radius: 8px; padding: 5px 11px; font-size: 11.5px; cursor: pointer;
|
||||
}
|
||||
.m-head .ghost button:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
.thread { flex: 1; overflow-y: auto; padding: 20px 26px; display: flex; flex-direction: column; gap: 11px; }
|
||||
.b {
|
||||
max-width: 76%; padding: 9px 13px; border-radius: 14px; font-size: 13.5px;
|
||||
}
|
||||
.b.u { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 5px; }
|
||||
.b.a { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 5px; }
|
||||
.b b { font-weight: 650; }
|
||||
.b code { background: rgba(255,255,255,.08); border-radius: 4px; padding: 1px 5px; font-size: 12px; font-family: "DejaVu Sans Mono", monospace; }
|
||||
.b .tm { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
|
||||
.cur { display: inline-block; width: 7px; height: 13px; background: var(--accent); vertical-align: -2px; margin-left: 2px; }
|
||||
|
||||
/* ход работы агента — компактная раскрывающаяся строка */
|
||||
.act-line {
|
||||
align-self: flex-start; display: flex; align-items: center; gap: 9px;
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 7px 12px; font-size: 12.5px; color: var(--dim); max-width: 78%;
|
||||
}
|
||||
.act-line .g { color: var(--accent); }
|
||||
.act-line .nm { color: var(--fg); }
|
||||
.act-line .ch { margin-left: auto; font-size: 11px; }
|
||||
|
||||
/* === композер: voice-first — микрофон главный === */
|
||||
.composer { border-top: 1px solid var(--line); background: var(--panel); padding: 14px 22px 14px; }
|
||||
.rec-panel {
|
||||
display: flex; align-items: center; gap: 10px; background: var(--bg);
|
||||
border: 1px solid var(--err); border-radius: 13px; padding: 4px 6px 4px 13px; height: 46px;
|
||||
}
|
||||
.wave { display: flex; align-items: center; gap: 3px; height: 28px; flex: 1; min-width: 0; }
|
||||
.wave i { width: 3px; background: var(--err); border-radius: 2px; }
|
||||
.wave i:nth-child(1) { height: 30%; } .wave i:nth-child(2) { height: 70%; }
|
||||
.wave i:nth-child(3) { height: 45%; } .wave i:nth-child(4) { height: 100%; }
|
||||
.wave i:nth-child(5) { height: 55%; } .wave i:nth-child(6) { height: 80%; }
|
||||
.wave i:nth-child(7) { height: 35%; } .wave i:nth-child(8) { height: 65%; }
|
||||
.wave i:nth-child(9) { height: 25%; }
|
||||
.rec-time {
|
||||
font-variant-numeric: tabular-nums; font-size: 13px; color: var(--err);
|
||||
font-weight: 700; flex: 0 0 auto;
|
||||
}
|
||||
.rec-btn {
|
||||
width: 34px; height: 34px; flex: 0 0 34px; border-radius: 10px;
|
||||
background: transparent; border: none; color: var(--dim); font-size: 14px;
|
||||
display: grid; place-items: center; cursor: pointer;
|
||||
}
|
||||
.rec-btn:hover { color: var(--fg); background: var(--panel2); }
|
||||
.rec-btn.rec-send { background: var(--accent); color: var(--on-accent); font-size: 15px; }
|
||||
.rec-btn.rec-send:hover { background: #82BEF5; }
|
||||
|
||||
.composer-row { display: flex; align-items: center; gap: 10px; }
|
||||
.field {
|
||||
flex: 1; min-width: 0; height: 46px; border-radius: 13px; background: var(--bg);
|
||||
border: 1px solid var(--line); display: flex; align-items: center; padding: 0 14px;
|
||||
font-size: 13.5px; color: var(--dim);
|
||||
}
|
||||
.field.focus { border-color: var(--accent); }
|
||||
.mic {
|
||||
width: 52px; height: 52px; flex: 0 0 52px; border-radius: 50%;
|
||||
border: 2px solid var(--accent); background: var(--bg); color: var(--accent);
|
||||
font-size: 19px; display: grid; place-items: center; cursor: pointer;
|
||||
}
|
||||
.mic:hover { background: rgba(106,178,242,.13); }
|
||||
.mic.rec { background: var(--err); border-color: var(--err); color: #fff; }
|
||||
.send {
|
||||
width: 46px; height: 46px; flex: 0 0 46px; border-radius: 13px; border: none;
|
||||
background: var(--accent); color: var(--on-accent); font-size: 17px;
|
||||
display: grid; place-items: center; cursor: pointer;
|
||||
}
|
||||
.send.off { background: var(--panel2); color: var(--dim); cursor: default; }
|
||||
.rec-note { font-size: 11px; color: var(--dim); margin-top: 7px; text-align: center; }
|
||||
|
||||
/* стрим: вместо поля — широкая красная кнопка «Стоп» */
|
||||
.stop-wide {
|
||||
height: 50px; border-radius: 13px; border: 1px solid rgba(236,57,66,.45);
|
||||
background: rgba(236,57,66,.10); color: #F0A6AA; font-size: 13.5px; font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center; gap: 9px; cursor: pointer;
|
||||
}
|
||||
|
||||
/* === пустое состояние === */
|
||||
.empty {
|
||||
flex: 1; display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; text-align: center; padding: 30px 34px; gap: 14px;
|
||||
}
|
||||
.empty .big {
|
||||
width: 78px; height: 78px; border-radius: 22px; background: var(--panel);
|
||||
border: 1px solid var(--line); display: grid; place-items: center; font-size: 32px;
|
||||
color: var(--dim);
|
||||
}
|
||||
.empty h3 { font-size: 17px; font-weight: 600; }
|
||||
.empty p { font-size: 13px; color: var(--dim); line-height: 1.55; max-width: 340px; }
|
||||
.btn {
|
||||
border-radius: 12px; padding: 11px 18px; font-size: 13.5px; cursor: pointer;
|
||||
border: 1px solid var(--line); background: transparent; color: var(--fg); font-family: inherit;
|
||||
}
|
||||
.btn.prim { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 650; }
|
||||
|
||||
/* === модалка нового диалога === */
|
||||
.scrim {
|
||||
position: absolute; inset: 0; background: rgba(0,0,0,.6);
|
||||
display: flex; align-items: center; justify-content: center; padding: 22px;
|
||||
}
|
||||
.modal {
|
||||
width: 480px; max-height: 84%; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 16px; display: flex;
|
||||
flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.modal h2 { font-size: 16px; font-weight: 650; padding: 16px 20px 4px; }
|
||||
.modal .sub { font-size: 12.5px; color: var(--dim); padding: 0 20px 12px; }
|
||||
.modal .body { flex: 1; overflow-y: auto; padding: 4px 12px 8px; }
|
||||
.agent {
|
||||
display: flex; align-items: center; gap: 12px; padding: 13px 14px;
|
||||
border-radius: 12px; cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.agent:hover { background: var(--panel2); }
|
||||
.agent.on { border-color: var(--accent); background: rgba(106,178,242,.07); }
|
||||
.agent .sign { width: 38px; height: 38px; flex: 0 0 38px; border-radius: 50%; display: grid; place-items: center; font-size: 14px; font-weight: 700; color: var(--on-accent); }
|
||||
.agent .mid { flex: 1; min-width: 0; }
|
||||
.agent .nm { font-size: 14px; font-weight: 550; }
|
||||
.agent .ad { font-size: 11.5px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.agent .ok { color: var(--ok); font-size: 11.5px; }
|
||||
.agent .bad { color: var(--err); font-size: 11.5px; }
|
||||
.agent .radio { width: 22px; height: 22px; flex: 0 0 22px; border-radius: 50%; border: 2px solid var(--line); }
|
||||
.agent.on .radio { border-color: var(--accent); background: radial-gradient(circle, var(--accent) 0 6px, transparent 7px); }
|
||||
.modal .foot { padding: 12px 16px 16px; display: flex; gap: 10px; border-top: 1px solid var(--line); }
|
||||
.modal .foot .btn:first-child { flex: 1; }
|
||||
.modal .warn {
|
||||
margin: 0 14px 10px; padding: 10px 12px; border-radius: 11px;
|
||||
background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA;
|
||||
}
|
||||
|
||||
/* === модалка управления группами === */
|
||||
.modal.groups { width: 540px; }
|
||||
.modal.groups h2 { display: flex; align-items: center; gap: 10px; padding: 14px 20px 6px; }
|
||||
.modal.groups h2 .x { margin-left: auto; width: 28px; height: 28px; border-radius: 8px; border: none; background: transparent; color: var(--dim); cursor: pointer; font-size: 16px; }
|
||||
.modal.groups h2 .x:hover { background: var(--panel2); color: var(--fg); }
|
||||
.modal.groups .body { padding: 6px 14px 10px; }
|
||||
.grp {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 13px;
|
||||
border-radius: 11px; background: var(--panel2); margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.grp:hover { border-color: var(--line); }
|
||||
.grp .mid { flex: 1; min-width: 0; }
|
||||
.grp .nm { font-size: 14px; font-weight: 550; }
|
||||
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 2px; }
|
||||
.grp .acts { display: flex; gap: 4px; }
|
||||
.grp.sys { background: transparent; border: 1px dashed var(--line); }
|
||||
.grp .lock { font-size: 11px; color: var(--dim); }
|
||||
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 14px; display: inline-flex;
|
||||
align-items: center; justify-content: center; line-height: 1; }
|
||||
.mini-ic svg, .card-acts svg { width: 16px; height: 16px; stroke: currentColor; fill: none;
|
||||
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.mini-ic.del svg, .card-acts.del svg { fill: currentColor; stroke: none; }
|
||||
.mini-ic:hover { background: var(--panel); color: var(--fg); }
|
||||
.mini-ic.del:hover { color: var(--err); }
|
||||
.modal .info {
|
||||
display: flex; gap: 9px; align-items: flex-start; margin: 4px 14px 12px;
|
||||
padding: 10px 13px; border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
|
||||
font-size: 11.5px; color: var(--dim); line-height: 1.5;
|
||||
}
|
||||
.modal .danger-zone { margin: 14px 14px 0; padding-top: 14px; border-top: 1px solid var(--line); }
|
||||
.modal .danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 8px; }
|
||||
.modal .form-field { margin: 0 14px 14px; }
|
||||
.modal .form-field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.modal .form-field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.modal .form-field input:focus { border-color: var(--accent); }
|
||||
.modal .form-field .note { font-size: 11px; color: var(--dim); margin-top: 6px; line-height: 1.5; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
|
||||
|
||||
/* === теги диалогов в группе + кнопка «добавить диалог» === */
|
||||
.modal .tags { margin: 0 14px 12px; }
|
||||
.modal .tags .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
|
||||
.modal .tags .empty { font-size: 12px; color: var(--dim); padding: 9px 12px;
|
||||
background: var(--bg); border: 1px dashed var(--line); border-radius: 10px; }
|
||||
.modal .tags-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 9px; }
|
||||
.chat-tag {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 4px 8px 4px 4px; background: var(--bg);
|
||||
border: 1px solid var(--line); border-radius: 9px;
|
||||
}
|
||||
.chat-tag .ava-sm { width: 22px; height: 22px; flex: 0 0 22px; border-radius: 50%;
|
||||
display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--on-accent); }
|
||||
.chat-tag .nm { font-size: 12.5px; }
|
||||
.chat-tag .x { width: 18px; height: 18px; border-radius: 50%; border: none;
|
||||
background: transparent; color: var(--dim); cursor: pointer;
|
||||
font-size: 10px; display: grid; place-items: center; }
|
||||
.chat-tag .x:hover { color: var(--err); background: rgba(236,57,66,.12); }
|
||||
.add-chat-btn {
|
||||
display: flex; align-items: center; justify-content: center; gap: 7px;
|
||||
padding: 9px; width: 100%; cursor: pointer; font-size: 13px; font-family: inherit;
|
||||
background: transparent; color: var(--dim);
|
||||
border: 1px dashed var(--line); border-radius: 10px;
|
||||
}
|
||||
.add-chat-btn:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
/* === модалка выбора диалога для группы === */
|
||||
.modal.pick { width: 460px; }
|
||||
.modal.pick .q {
|
||||
margin: 4px 14px 8px; padding: 9px 12px; border-radius: 10px;
|
||||
border: 1px solid var(--line); background: var(--bg);
|
||||
color: var(--fg); font-size: 13px; outline: none; font-family: inherit;
|
||||
}
|
||||
.modal.pick .q:focus { border-color: var(--accent); }
|
||||
.modal.pick .q::placeholder { color: var(--dim); }
|
||||
.modal.pick .body { padding: 0 8px 8px; max-height: 360px; }
|
||||
.pick-row {
|
||||
display: flex; align-items: center; gap: 11px; padding: 9px 10px;
|
||||
border-radius: 10px; cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.pick-row:hover { background: var(--panel2); }
|
||||
.pick-row.in { opacity: .42; cursor: default; }
|
||||
.pick-row .ava-sm { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 50%;
|
||||
display: grid; place-items: center; font-size: 12px; font-weight: 700; color: var(--on-accent); }
|
||||
.pick-row .mid { flex: 1; min-width: 0; }
|
||||
.pick-row .nm { font-size: 13.5px; }
|
||||
.pick-row .ad { font-size: 11.5px; color: var(--dim); margin-top: 1px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.pick-row .check { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 6px;
|
||||
border: 1.5px solid var(--line); display: grid; place-items: center; color: transparent; font-size: 11px; }
|
||||
.pick-row .check.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||
|
||||
/* === 004 · окно агентов (список + форма + поповер проверки) === */
|
||||
.dialog {
|
||||
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px;
|
||||
background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
box-shadow: 0 26px 80px rgba(0,0,0,.7);
|
||||
}
|
||||
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
|
||||
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
|
||||
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
|
||||
color: var(--dim); cursor: pointer; font-size: 17px; }
|
||||
.x-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
|
||||
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
|
||||
|
||||
.agent-card {
|
||||
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
|
||||
}
|
||||
.agent-card:hover { border-color: var(--line); }
|
||||
.ag-mid { flex: 1; min-width: 0; }
|
||||
.ag-name { font-size: 14px; font-weight: 550; }
|
||||
.ag-desc { font-size: 11.5px; color: var(--fg); opacity: .7;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
|
||||
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
||||
|
||||
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
|
||||
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
|
||||
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
|
||||
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input {
|
||||
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
|
||||
}
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; line-height: 1.5; }
|
||||
.note code, .hint-line code { background: var(--panel2); padding: 1px 4px; border-radius: 4px;
|
||||
font-size: 10.5px; }
|
||||
.addr-row { display: flex; gap: 8px; }
|
||||
.addr-row input { flex: 1; }
|
||||
|
||||
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
|
||||
.appearance-tabs .tab {
|
||||
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent);
|
||||
background: rgba(106,178,242,.12); }
|
||||
.pane { display: none; } .pane.on { display: block; }
|
||||
.colors { display: flex; gap: 9px; }
|
||||
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
|
||||
border: 2px solid transparent; }
|
||||
.swatch.on { border-color: var(--fg); }
|
||||
.pic-row { display: flex; align-items: center; gap: 13px; }
|
||||
.pic-preview {
|
||||
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
|
||||
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
|
||||
font-size: 20px; overflow: hidden;
|
||||
}
|
||||
.pic-preview.filled { border-style: solid; }
|
||||
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
|
||||
.hint-line { font-size: 11.5px; color: var(--dim); margin: 2px 0 16px; padding: 9px 11px;
|
||||
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); line-height: 1.5; }
|
||||
|
||||
/* поповер проверки связи */
|
||||
.layer { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
|
||||
z-index: 250; }
|
||||
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
|
||||
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
|
||||
@keyframes sp { to { transform: rotate(360deg); } }
|
||||
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
|
||||
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
|
||||
font-size: 19px; flex: 0 0 40px; }
|
||||
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
|
||||
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
|
||||
.probe-head.busy .big { background: transparent; padding: 0; }
|
||||
.probe-head .txt { flex: 1; min-width: 0; }
|
||||
.probe-head .ttl { font-size: 14px; font-weight: 600; }
|
||||
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.probe-list { font-size: 12px; }
|
||||
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.probe-list div:last-child { border-bottom: none; }
|
||||
.probe-list span { color: var(--dim); }
|
||||
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 11.5px; }
|
||||
|
||||
/* === окно клиента .window + сайдбар .sidebar (вариант sidebar-utility) === */
|
||||
.window {
|
||||
width: 1180px; height: 740px; background: var(--bg); border-radius: 12px;
|
||||
display: flex; overflow: hidden; border: 1px solid var(--line); position: relative;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||
}
|
||||
.sidebar { width: 320px; background: var(--panel); border-right: 1px solid var(--line);
|
||||
display: flex; flex-direction: column; flex: 0 0 320px; }
|
||||
.sb-head { padding: 14px 16px 10px; }
|
||||
.sb-title { font-size: 17px; font-weight: 600; display: flex; align-items: center;
|
||||
justify-content: space-between; }
|
||||
.icon-btn { width: 30px; height: 30px; border-radius: 8px; border: none;
|
||||
background: transparent; color: var(--dim); cursor: pointer; display: grid; place-items: center;
|
||||
font-size: 17px; }
|
||||
.icon-btn:hover { background: var(--panel2); color: var(--fg); }
|
||||
.search { padding: 8px 12px; border-radius: 9px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13px; outline: none; font-family: inherit; }
|
||||
.search:focus { border-color: var(--accent); }
|
||||
.search::placeholder { color: var(--dim); }
|
||||
|
||||
/* лента папок (sidebar-utility) */
|
||||
.folders { display: flex; gap: 6px; padding: 6px 16px 10px; overflow-x: auto; }
|
||||
.folder { padding: 5px 11px; border-radius: 14px; font-size: 12px; color: var(--dim);
|
||||
background: var(--panel2); cursor: pointer; white-space: nowrap; border: 1px solid transparent; }
|
||||
.folder.active { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
|
||||
.folder .cnt { color: var(--badge); font-weight: 600; margin-left: 4px; }
|
||||
.folder.act { margin-left: auto; background: transparent; color: var(--dim);
|
||||
display: grid; place-items: center; padding: 5px 9px; font-size: 13px; }
|
||||
.folder.act svg { width: 14px; height: 14px; stroke: currentColor; fill: none;
|
||||
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.folder.act:hover { color: var(--fg); background: var(--panel2); }
|
||||
|
||||
.chat-list { flex: 1; overflow-y: auto; padding: 4px 8px 12px; }
|
||||
.chat-row { display: flex; gap: 11px; padding: 9px 10px; border-radius: 10px; cursor: pointer;
|
||||
align-items: flex-start; }
|
||||
.chat-row:hover { background: var(--panel2); }
|
||||
.chat-row.active { background: #233040; }
|
||||
.chat-mid { flex: 1; min-width: 0; }
|
||||
.chat-name { font-size: 14px; font-weight: 500; display: flex; justify-content: space-between; gap: 8px; }
|
||||
.chat-time { font-size: 11px; color: var(--dim); font-weight: 400; flex: 0 0 auto; }
|
||||
.chat-prev { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; margin-top: 2px; }
|
||||
.badge { background: var(--badge); color: #0F1922; border-radius: 10px; font-size: 11px;
|
||||
font-weight: 700; padding: 1px 6px; min-width: 19px; text-align: center; }
|
||||
.row-tail { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: 0 0 auto; }
|
||||
|
||||
/* правая часть .main — заголовок чата */
|
||||
.chat-head { height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line);
|
||||
display: flex; align-items: center; gap: 11px; padding: 0 16px; }
|
||||
.chat-head .avatar { width: 36px; height: 36px; flex: 0 0 36px; font-size: 13px; }
|
||||
.head-meta { flex: 1; }
|
||||
.head-name { font-size: 14.5px; font-weight: 600; }
|
||||
.head-sub { font-size: 11.5px; color: var(--dim); }
|
||||
.head-sub .dot { color: var(--ok); }
|
||||
.back-btn { display: none; width: 32px; height: 32px; flex: 0 0 32px; border-radius: 8px;
|
||||
border: none; background: transparent; color: var(--fg); cursor: pointer; font-size: 17px;
|
||||
place-items: center; }
|
||||
.back-btn:hover { background: var(--panel); }
|
||||
|
||||
/* сообщения */
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column;
|
||||
gap: 12px; }
|
||||
.msg { max-width: 74%; padding: 9px 13px; border-radius: 13px; font-size: 13.5px; }
|
||||
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
||||
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border-bottom-left-radius: 4px; }
|
||||
.msg .meta { font-size: 10.5px; color: var(--dim); margin-top: 5px; }
|
||||
.think { align-self: flex-start; max-width: 80%; border-left: 2px solid var(--accent);
|
||||
background: rgba(106,178,242,.07); padding: 8px 12px; border-radius: 0 10px 10px 0;
|
||||
font-size: 12.5px; color: var(--dim); }
|
||||
.think b { color: var(--accent); font-weight: 600; }
|
||||
.think .rest { display: none; }
|
||||
.tool { align-self: flex-start; max-width: 80%; background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 8px 12px; font-size: 12.5px;
|
||||
display: flex; align-items: center; gap: 9px; }
|
||||
.tool .gear { color: var(--accent); }
|
||||
.tool code { color: var(--dim); font-size: 11.5px; }
|
||||
|
||||
/* композер */
|
||||
.composer { border-top: 1px solid var(--line); padding: 12px 16px 14px; background: var(--panel); }
|
||||
.attach-strip { display: flex; gap: 8px; margin-bottom: 9px; }
|
||||
.attach-strip:empty { display: none; }
|
||||
.composer .chip { background: var(--panel2); border: 1px solid var(--line); border-radius: 8px;
|
||||
padding: 5px 9px; font-size: 11.5px; color: var(--dim); display: flex; align-items: center; gap: 7px; }
|
||||
.composer .chip .x { cursor: pointer; color: var(--err); font-weight: 700; }
|
||||
.input-row { display: flex; align-items: flex-end; gap: 9px; }
|
||||
.input-wrap { flex: 1; display: flex; align-items: flex-end; background: var(--bg);
|
||||
border: 1px solid var(--line); border-radius: 11px; padding: 9px 12px; gap: 9px; }
|
||||
.input-wrap:focus-within { border-color: var(--accent); }
|
||||
.input-wrap textarea { flex: 1; background: transparent; border: none; outline: none; resize: none;
|
||||
color: var(--fg); font-family: inherit; font-size: 13.5px; line-height: 1.45; max-height: 120px;
|
||||
height: 22px; }
|
||||
.input-wrap textarea::placeholder { color: var(--dim); }
|
||||
.rec-btn { width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--dim); cursor: pointer; display: grid; place-items: center;
|
||||
font-size: 17px; flex: 0 0 40px; transition: all .18s; }
|
||||
.rec-btn:hover { color: var(--fg); border-color: var(--dim); }
|
||||
.rec-btn.recording { background: var(--err); border-color: var(--err); color: #fff;
|
||||
animation: pulse 1.4s infinite; }
|
||||
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(236,57,66,.55); }
|
||||
50% { box-shadow: 0 0 0 9px rgba(236,57,66,0); } }
|
||||
.send-btn { height: 40px; padding: 0 18px; border-radius: 11px; border: none;
|
||||
background: var(--accent); color: #0F1922; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
.send-btn:disabled { background: var(--panel2); color: var(--dim); cursor: default; }
|
||||
.hint { font-size: 11px; color: var(--dim); margin-top: 8px; display: flex;
|
||||
justify-content: space-between; }
|
||||
kbd { background: var(--panel2); border: 1px solid var(--line); border-radius: 4px;
|
||||
padding: 1px 5px; font-size: 10.5px; }
|
||||
|
||||
/* запись — полоса над полем */
|
||||
.rec-panel { display: flex; align-items: center; gap: 12px; margin-bottom: 10px;
|
||||
background: var(--bg); border: 1px solid var(--err); border-radius: 11px; padding: 9px 13px; }
|
||||
.wave { display: flex; align-items: center; gap: 3px; height: 22px; flex: 1; }
|
||||
.wave i { width: 3px; background: var(--err); border-radius: 2px; height: 30%;
|
||||
animation: wv .9s infinite ease-in-out; }
|
||||
.wave i:nth-child(2n) { animation-delay: .12s; }
|
||||
.wave i:nth-child(3n) { animation-delay: .24s; }
|
||||
.wave i:nth-child(4n) { animation-delay: .36s; }
|
||||
@keyframes wv { 0%,100% { height: 22%; } 50% { height: 100%; } }
|
||||
.rec-time { font-variant-numeric: tabular-nums; font-size: 13px; color: var(--err); font-weight: 600; }
|
||||
.rec-actions { display: flex; gap: 8px; }
|
||||
.mini { border: 1px solid var(--line); background: var(--panel2); color: var(--fg);
|
||||
border-radius: 8px; padding: 5px 11px; font-size: 12px; cursor: pointer; }
|
||||
.mini.ok { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 600; }
|
||||
.mini.cancel { color: var(--err); }
|
||||
|
||||
.asr-note { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--dim);
|
||||
margin-bottom: 8px; padding-left: 2px; }
|
||||
.asr-note .live { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
||||
|
||||
/* === 005 · выбор ассистента (agent-opt) === */
|
||||
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
|
||||
.agent-opt { display: flex; align-items: center; gap: 12px; padding: 11px 12px;
|
||||
border-radius: 11px; background: var(--panel2); margin-bottom: 9px; cursor: pointer;
|
||||
border: 1.5px solid transparent; }
|
||||
.agent-opt:hover { border-color: #33414F; }
|
||||
.agent-opt.on { border-color: var(--accent); background: rgba(106,178,242,.10); }
|
||||
.agent-opt .pick { width: 20px; height: 20px; flex: 0 0 20px; border-radius: 50%;
|
||||
border: 2px solid #3D4A58; }
|
||||
.agent-opt.on .pick { border-color: var(--accent); background: var(--accent); position: relative; }
|
||||
.agent-opt.on .pick::after { content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px;
|
||||
height: 9px; border: solid #0F1922; border-width: 0 2px 2px 0; transform: rotate(42deg); }
|
||||
.agent-opt .ag-state { font-size: 11px; display: flex; align-items: center; gap: 5px; margin-top: 4px; }
|
||||
.agent-opt .ag-state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
||||
.agent-opt .ag-state.ok { color: var(--ok); }
|
||||
.agent-opt .ag-state.off { color: var(--dim); } .agent-opt .ag-state.off .d { background: var(--dim); }
|
||||
.agent-opt .ag-state.err { color: var(--err); } .agent-opt .ag-state.err .d { background: var(--err); }
|
||||
|
||||
/* === 006 · формы групп (approved-вариант) === */
|
||||
.d-lead { font-size: 12.5px; color: var(--dim); margin-bottom: 12px; }
|
||||
.warn { display: flex; gap: 9px; align-items: flex-start; margin-bottom: 15px; padding: 11px 13px;
|
||||
border-radius: 10px; background: rgba(236,57,66,.09); border: 1px solid rgba(236,57,66,.32);
|
||||
font-size: 12px; color: #F0A6AA; line-height: 1.5; }
|
||||
.warn .i { flex: 0 0 auto; font-size: 13px; }
|
||||
.info { display: flex; gap: 9px; align-items: flex-start; margin-top: 4px; padding: 11px 13px;
|
||||
border-radius: 10px; background: var(--bg); border: 1px solid var(--line);
|
||||
font-size: 11.5px; color: var(--dim); line-height: 1.5; }
|
||||
.danger-zone { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--line); }
|
||||
.danger-zone .lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--dim); font-weight: 700; margin-bottom: 9px; }
|
||||
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
|
||||
.btn.dang:hover { border-color: var(--err); background: rgba(236,57,66,.09); }
|
||||
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF; font-weight: 650; }
|
||||
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
border-style: dashed; color: var(--dim); }
|
||||
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
|
||||
.grp { display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
|
||||
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent; }
|
||||
.grp:hover { border-color: var(--line); }
|
||||
.grp .mid { flex: 1; min-width: 0; }
|
||||
.grp .nm { font-size: 14px; font-weight: 550; }
|
||||
.grp .sub { font-size: 11.5px; color: var(--dim); margin-top: 3px; }
|
||||
.grp .acts { display: flex; gap: 4px; flex: 0 0 auto; }
|
||||
.grp.sys { background: transparent; border: 1px dashed var(--line); }
|
||||
.grp.sys .nm { color: var(--fg); }
|
||||
.grp .lock { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
|
||||
.field { margin-bottom: 15px; }
|
||||
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
|
||||
.field input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit; }
|
||||
.field input:focus { border-color: var(--accent); }
|
||||
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
|
||||
Reference in New Issue
Block a user