343 lines
17 KiB
HTML
343 lines
17 KiB
HTML
<!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>
|