Files
agentik-desktop/sketches/002-rail-voice-first/index.html
T

343 lines
17 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>Вариант 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>