Files
agentik-desktop/sketches/001-sidebar-utility/index.html
T

475 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Вариант 1 — Sidebar / Utility</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; --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);
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; }
.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 {
margin: 10px 16px 8px; padding: 8px 12px; border-radius: 9px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13px; width: calc(100% - 32px); outline: none;
}
.search:focus { border-color: var(--accent); }
.search::placeholder { color: var(--dim); }
/* лента папок — как в мобильном клиенте */
.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; }
.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; }
.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.g4 { background: #B79AE0; }
.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 { flex: 1; display: flex; flex-direction: column; }
.chat-head {
position: relative;
height: 58px; flex: 0 0 58px; border-bottom: 1px solid var(--line); display: flex; align-items: center;
gap: 8px; 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: #63C88A; }
/* кнопка «назад» — нужна только в узком окне */
.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); }
/* кольцо израсходованного контекста */
.tok-btn {
display: flex; align-items: center; gap: 8px; background: transparent; border: none;
cursor: pointer; color: var(--dim); padding: 4px 6px; border-radius: 9px; font-size: 11.5px;
}
.tok-btn:hover { background: var(--panel); color: var(--fg); }
.tok-ring { position: relative; width: 27px; height: 27px; flex: 0 0 27px; }
.tok-ring svg { transform: rotate(-90deg); display: block; }
.tok-ring .pct {
position: absolute; inset: 0; display: grid; place-items: center;
font-size: 8.5px; font-weight: 700; color: var(--fg);
}
/* развёрнутая карточка состояния */
.tok-panel {
display: none; position: absolute; top: 62px; right: 16px; width: 300px; min-width: 300px;
flex: 0 0 auto; z-index: 40;
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
box-shadow: 0 16px 48px rgba(0,0,0,.55); padding: 14px 16px;
}
.tok-panel.on { display: block; }
.tok-panel h4 { font-size: 10.5px; letter-spacing: 1.1px; text-transform: uppercase;
color: var(--dim); margin-bottom: 11px; font-weight: 700; }
.tok-row { display: flex; justify-content: space-between; font-size: 12.5px; margin-bottom: 8px; }
.tok-row span:first-child { color: var(--dim); }
.tok-row span:last-child { font-weight: 600; }
.tok-bar { height: 6px; border-radius: 3px; background: var(--panel2); overflow: hidden; margin: 3px 0 12px; }
.tok-bar i { display: block; height: 100%; background: var(--accent); }
.tok-sep { height: 1px; background: var(--line); margin: 12px 0; }
.tok-kv { font-size: 11.5px; color: var(--dim); display: flex; justify-content: space-between; margin-bottom: 6px; }
.tok-kv b { color: var(--fg); font-weight: 600; }
.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); cursor: pointer;
}
.think b { color: var(--accent); font-weight: 600; }
.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; }
.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;
}
.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); }
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;
}
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: none; align-items: center; gap: 12px; margin-bottom: 10px;
background: var(--bg); border: 1px solid var(--err); border-radius: 11px; padding: 9px 13px; }
.rec-panel.on { display: flex; }
.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: none; align-items: center; gap: 8px; font-size: 11.5px; color: var(--dim);
margin-bottom: 8px; padding-left: 2px;
}
.asr-note.on { display: flex; }
.asr-note .live { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
/* ---------- узкое окно: либо список, либо чат ---------- */
.backdrop {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 90;
}
.backdrop.on { display: block; }
@media (max-width: 880px) {
body { padding: 0; }
.window { width: 100vw; height: 100vh; border-radius: 0; border: none; transform: none !important; }
.window.mode-chat .sidebar { display: none; }
.window.mode-list .main { display: none; }
.sidebar { width: 100%; flex: 1 1 auto; border-right: none; }
.back-btn { display: grid; }
}
/* окно целиком влезает на любой экран — не режем картинку */
@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">
<!-- ================= СПИСОК ЧАТОВ ================= -->
<aside class="sidebar">
<div class="sb-head">
<div class="sb-title">
<span>Диалоги</span>
<button class="icon-btn" 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="chatList">
<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>
<div class="chat-row">
<div class="avatar g4">ПО</div>
<div class="chat-mid">
<div class="chat-name"><span>Помощь по Solana</span><span class="chat-time">вчера</span></div>
<div class="chat-prev">Девайс-нет стартовал</div>
</div>
</div>
</div>
</aside>
<!-- ================= ЧАТ ================= -->
<main class="main">
<header class="chat-head">
<button class="back-btn" id="backBtn" title="Назад к диалогам">←</button>
<div class="avatar">АГ</div>
<div class="head-meta">
<div class="head-name">agentik · рабочий</div>
<div class="head-sub"><span class="dot">●</span> на связи</div>
</div>
<button class="tok-btn" id="tokBtn" title="Израсходовано контекста — нажми для подробностей">
<span class="tok-ring">
<svg width="27" height="27">
<circle cx="13.5" cy="13.5" r="11" fill="none" stroke="#232B36" stroke-width="2.5"></circle>
<circle cx="13.5" cy="13.5" r="11" fill="none" stroke="#6AB2F2" stroke-width="2.5"
stroke-linecap="round" stroke-dasharray="69.1" stroke-dashoffset="45.6"></circle>
</svg>
<span class="pct">34%</span>
</span>
<span>34.3k / 100k</span>
</button>
<button class="icon-btn" title="Параметры диалога">⋯</button>
<!-- разворачивается по нажатию на кольцо -->
<div class="tok-panel" id="tokPanel">
<h4>Контекст диалога</h4>
<div class="tok-row"><span>Занято сейчас</span><span>34 300</span></div>
<div class="tok-bar"><i style="width:34%"></i></div>
<div class="tok-row"><span>Всего доступно</span><span>100 000</span></div>
<div class="tok-row"><span>Осталось</span><span>65 700</span></div>
<div class="tok-sep"></div>
<h4>За диалог целиком</h4>
<div class="tok-kv"><span>Отправлено в модель</span><b>412 800</b></div>
<div class="tok-kv"><span>Получено от модели</span><b>58 240</b></div>
<div class="tok-kv"><span>Ходов</span><b>18</b></div>
<div class="tok-sep"></div>
<div class="tok-kv"><span>Модель</span><b>codding-big</b></div>
</div>
</header>
<div class="messages" id="messages">
<div class="msg user">Проверь, почему падает CI в agentik<span class="meta">14:18</span></div>
<div class="think" onclick="this.querySelector('.rest').style.display = this.querySelector('.rest').style.display==='none'?'inline':'none'">
<b>Думает:</b> 42 прогона, красных 40. Смотрю имена тестов… <span class="rest">похоже на проблему кодировки при создании файлов классов.</span>
</div>
<div class="tool"><span class="gear">⚙</span> <code>grep -rnP 'fun `[^`]*[^\x00-\x7F]' --include='*.kt'</code></div>
<div class="msg bot">Нашёл причину: в именах тестов стоит длинное тире «—», а сборка работает в ASCII-кодировке. Отсюда падение компилятора. Починил, CI зелёный.<span class="meta">14:22</span></div>
<div class="msg user">Отлично. Сделай релиз «3»<span class="meta">14:23</span></div>
<div class="think" onclick="this.querySelector('.rest').style.display = this.querySelector('.rest').style.display==='none'?'inline':'none'">
<b>Думает:</b> создаю тег, запускаю публикацию… <span class="rest">проверю, что в Nexus появилась версия 3.</span>
</div>
</div>
<!-- ===== панель ввода ===== -->
<div class="composer">
<div class="attach-strip" id="chips"></div>
<div class="asr-note" id="asrNote">
<span class="live"></span> распознаю: <span id="asrText" style="color:var(--fg)"></span>
</div>
<div class="rec-panel" id="recPanel">
<button class="mini cancel" onclick="stopRec(true)">✕</button>
<div class="wave" id="wave"></div>
<span class="rec-time" id="recTime">0:00</span>
<div class="rec-actions">
<button class="mini ok" onclick="stopRec(false)">Остановить и отправить</button>
</div>
</div>
<div class="input-row" id="inputRow">
<div class="input-wrap">
<textarea id="ta" rows="1" placeholder="Написать агенту…"></textarea>
</div>
<button class="rec-btn" id="recBtn" title="Записать голосовое (Ctrl+R)">🎙</button>
<button class="send-btn" id="sendBtn" disabled>Отправить</button>
</div>
<div class="hint">
<span id="statusHint"></span>
</div>
</div>
</main>
</div>
<script>
const ta = document.getElementById('ta');
const sendBtn = document.getElementById('sendBtn');
const recBtn = document.getElementById('recBtn');
const recPanel = document.getElementById('recPanel');
const inputRow = document.getElementById('inputRow');
const messages = document.getElementById('messages');
const asrNote = document.getElementById('asrNote');
const asrText = document.getElementById('asrText');
const statusHint = document.getElementById('statusHint');
// волна из 28 столбиков
document.getElementById('wave').innerHTML = Array.from({length: 28}, () => '<i></i>').join('');
ta.addEventListener('input', () => {
ta.style.height = '22px';
ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
sendBtn.disabled = ta.value.trim() === '';
});
function send() {
const t = ta.value.trim(); if (!t) return;
addMsg(t, 'user');
ta.value = ''; ta.style.height = '22px'; sendBtn.disabled = true;
statusHint.textContent = 'агент думает…';
setTimeout(() => {
addMsg('Принято. Выполняю: «' + t + '» — покажу ход в этом же диалоге.', 'bot');
statusHint.textContent = 'готов';
}, 900);
}
function addMsg(text, who) {
const d = document.createElement('div');
d.className = 'msg ' + who;
const now = new Date();
d.innerHTML = text + '<span class="meta">' + now.getHours() + ':' + String(now.getMinutes()).padStart(2,'0') + '</span>';
messages.appendChild(d); messages.scrollTop = messages.scrollHeight;
}
sendBtn.onclick = send;
ta.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
// ---------- запись ----------
let recTimer = null, recSec = 0;
const WORDS = ['проверь','логи','сборки','за','вчерашний','день','и','сделай','вывод'];
function startRec() {
recSec = 0; recPanel.classList.add('on'); recBtn.classList.add('recording');
inputRow.style.opacity = '.45'; statusHint.textContent = 'запись…';
asrNote.classList.add('on'); asrText.textContent = '';
let i = 0;
recTimer = setInterval(() => {
recSec++;
document.getElementById('recTime').textContent = '0:' + String(recSec).padStart(2,'0');
if (i < WORDS.length && recSec % 1 === 0) { // живая расшифровка
asrText.textContent = WORDS.slice(0, ++i).join(' ') + (i < WORDS.length ? ' …' : '');
}
}, 1000);
}
function stopRec(cancel) {
clearInterval(recTimer);
recPanel.classList.remove('on'); recBtn.classList.remove('recording');
inputRow.style.opacity = '1'; asrNote.classList.remove('on');
if (cancel) { statusHint.textContent = 'запись отменена'; return; }
if (recSec < 1) { statusHint.textContent = 'слишком коротко'; return; }
ta.value = WORDS.join(' ') + '.'; // распознанный текст попадает в поле
ta.dispatchEvent(new Event('input'));
statusHint.textContent = 'распознано, ' + recSec + ' с — проверь и отправь';
ta.focus();
}
recBtn.onclick = () => recPanel.classList.contains('on') ? stopRec(false) : startRec();
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key.toLowerCase() === 'r') { e.preventDefault(); recBtn.click(); }
});
// ---------- широкое / узкое окно ----------
const win = document.querySelector('.window');
const NARROW = 880;
function applyMode() {
if (innerWidth > NARROW) { // широкое: видно и то, и другое
win.classList.remove('mode-chat', 'mode-list');
return;
}
// узкое: показываем что-то одно, по умолчанию список
if (!win.classList.contains('mode-chat') && !win.classList.contains('mode-list')) {
win.classList.add('mode-list');
}
}
document.querySelectorAll('.chat-row').forEach(r => r.addEventListener('click', () => {
document.querySelectorAll('.chat-row').forEach(x => x.classList.remove('active'));
r.classList.add('active');
if (innerWidth <= NARROW) win.classList.remove('mode-list'), win.classList.add('mode-chat');
}));
document.getElementById('backBtn').onclick = () => win.classList.remove('mode-chat'), win.classList.add('mode-list');
addEventListener('resize', applyMode);
applyMode();
// ---------- кольцо токенов: свёрнуто -> развёрнуто ----------
const tokPanel = document.getElementById('tokPanel');
document.getElementById('tokBtn').onclick = e => { e.stopPropagation(); tokPanel.classList.toggle('on'); };
document.addEventListener('click', e => {
if (tokPanel.classList.contains('on') && !tokPanel.contains(e.target)) tokPanel.classList.remove('on');
});
</script>
</body>
</html>