Files
agentik-desktop/sketches/003-three-pane-command/index.html
T

346 lines
20 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>Вариант 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>