v1.4 — день/ночь в Управление, кнопка подтверждения, комбо текст+микрофон

This commit is contained in:
2026-06-02 06:09:45 +03:00
parent 8138406ce3
commit 6374b20ea8
2 changed files with 52 additions and 31 deletions
+44 -21
View File
@@ -26,18 +26,20 @@
<div class="page"> <div class="page">
<div class="main"> <div class="main">
<div class="card"> <div class="card">
<div class="card-header">Управление</div> <div class="card-header">
Управление
<span style="float:right;cursor:pointer;" id="btnTheme">🌙</span>
</div>
<div class="card-body"> <div class="card-body">
<div style="display:flex;gap:8px;margin-bottom:8px;"> <div style="display:flex;gap:8px;margin-bottom:8px;">
<span class="tag">🤖 GPT-OSS-120B</span> <span class="tag">🤖 GPT-OSS-120B</span>
<span class="tag">🎤 Whisper-v3-Turbo</span> <span class="tag">🎤 Whisper-v3-Turbo</span>
</div> </div>
<canvas id="meter" width="400" height="60" style="width:100%;border-radius:6px;background:var(--grey-light);margin-bottom:8px;"></canvas> <canvas id="meter" width="400" height="60" style="width:100%;border-radius:6px;background:var(--grey-light);margin-bottom:8px;"></canvas>
<div style="display:flex;gap:.5rem;align-items:center;"> <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
<button id="btnRecord" class="btn btn-primary">🎙 Записать</button> <button id="btnRecord" class="btn btn-primary">🎙 Записать</button>
<button id="btnPlay" class="btn" disabled>▶ Прослушать</button> <button id="btnPlay" class="btn" disabled>▶ Прослушать</button>
<button id="btnReset" class="btn">🗑 Сброс</button> <button id="btnReset" class="btn">🗑 Сброс</button>
<button id="btnTheme" class="btn">🌙</button>
</div> </div>
<div id="status" class="status"></div> <div id="status" class="status"></div>
</div> </div>
@@ -47,7 +49,7 @@
<div class="card-header">Диалог</div> <div class="card-header">Диалог</div>
<div class="card-body"> <div class="card-body">
<div style="display:flex;gap:.5rem;margin-bottom:10px;"> <div style="display:flex;gap:.5rem;margin-bottom:10px;">
<input id="txtInput" type="text" placeholder="Введите текст или используйте микрофон..." style="flex:1;padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;outline:none;background:var(--card);color:var(--text);"> <input id="txtInput" type="text" placeholder="Текст + микрофон — что набрали и что сказали — всё в одном запросе к LLM" style="flex:1;padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;outline:none;background:var(--card);color:var(--text);">
<button id="btnSend" class="btn" style="font-size:13px;"></button> <button id="btnSend" class="btn" style="font-size:13px;"></button>
</div> </div>
<div id="chat" style="min-height:200px;max-height:340px;overflow-y:auto;"></div> <div id="chat" style="min-height:200px;max-height:340px;overflow-y:auto;"></div>
@@ -64,7 +66,7 @@
</div> </div>
<script> <script>
const VERSION = 'v1.3'; const VERSION = 'v1.4';
document.getElementById('ver').textContent = VERSION; document.getElementById('ver').textContent = VERSION;
const btnRecord = document.getElementById('btnRecord'); const btnRecord = document.getElementById('btnRecord');
@@ -76,13 +78,15 @@ const meter = document.getElementById('meter');
const meterCtx = meter.getContext('2d'); const meterCtx = meter.getContext('2d');
const txtInput = document.getElementById('txtInput'); const txtInput = document.getElementById('txtInput');
const btnSend = document.getElementById('btnSend'); const btnSend = document.getElementById('btnSend');
const btnTheme = document.getElementById('btnTheme');
let mediaRecorder, chunks = [], userBlob = null; let mediaRecorder, chunks = [], userBlob = null;
let isRecording = false, micAnalyser = null, micAnimId = null; let isRecording = false, micAnalyser = null, micAnimId = null;
let audioContext = null, chatHistory = []; let audioContext = null, chatHistory = [];
let pendingText = null, pendingSendBtn = null;
// Светлая тема по умолчанию // Светлая тема по умолчанию
if (localStorage.getItem('say_dark') === '1') document.body.classList.add('dark'); if (localStorage.getItem('say_dark') === '1') { document.body.classList.add('dark'); btnTheme.textContent = '☀️'; }
let history = JSON.parse(localStorage.getItem('say_history') || '[]'); let history = JSON.parse(localStorage.getItem('say_history') || '[]');
@@ -123,17 +127,21 @@ function fmt(s) {
renderHistory(); renderHistory();
// ===== Отправка текста ===== // ===== Отправка =====
async function sendText(text) { async function doSend() {
let text = '';
if (txtInput.value.trim()) text += txtInput.value.trim() + ' ';
if (pendingText) text += pendingText;
if (!text.trim()) return; if (!text.trim()) return;
txtInput.value = ''; txtInput.value = '';
chat.innerHTML += '<div class="msg user">⌨ ' + fmt(text) + '</div>'; if (pendingSendBtn) { pendingSendBtn.remove(); pendingSendBtn = null; pendingText = null; }
chat.innerHTML += '<div class="msg user">🗣 ' + fmt(text.trim()) + '</div>';
chat.scrollTop = chat.scrollHeight; chat.scrollTop = chat.scrollHeight;
await askLLM(text); await askLLM(text.trim());
} }
btnSend.onclick = () => sendText(txtInput.value); btnSend.onclick = doSend;
txtInput.onkeydown = e => { if (e.key === 'Enter') sendText(txtInput.value); }; txtInput.onkeydown = e => { if (e.key === 'Enter') doSend(); };
// ===== Запись ===== // ===== Запись =====
btnRecord.onclick = async () => { btnRecord.onclick = async () => {
@@ -167,7 +175,7 @@ async function startRecording() {
if (audioContext) audioContext.close(); if (audioContext) audioContext.close();
userBlob = new Blob(chunks, { type: 'audio/webm' }); userBlob = new Blob(chunks, { type: 'audio/webm' });
btnPlay.disabled = false; btnPlay.disabled = false;
await transcribeAndChat(); await transcribe();
}; };
mediaRecorder.start(); mediaRecorder.start();
} }
@@ -200,8 +208,8 @@ btnPlay.onclick = () => {
a.play(); setStatus('🔊 Ваша запись...'); a.play(); setStatus('🔊 Ваша запись...');
}; };
// ===== Транскрибация → LLM ===== // ===== Транскрибация → подтверждение → LLM =====
async function transcribeAndChat() { async function transcribe() {
let tStart = Date.now(); let tStart = Date.now();
setStatus('🎤 Транскрибация... 0с'); setStatus('🎤 Транскрибация... 0с');
const tTimer = setInterval(() => setStatus('🎤 Транскрибация... ' + ((Date.now() - tStart) / 1000).toFixed(0) + 'с'), 200); const tTimer = setInterval(() => setStatus('🎤 Транскрибация... ' + ((Date.now() - tStart) / 1000).toFixed(0) + 'с'), 200);
@@ -214,9 +222,25 @@ async function transcribeAndChat() {
const text = tData.text || '(не распознано)'; const text = tData.text || '(не распознано)';
const tElapsed = ((Date.now() - tStart) / 1000).toFixed(1); const tElapsed = ((Date.now() - tStart) / 1000).toFixed(1);
setStatus('✅ Транскрибация: ' + tElapsed + 'с'); setStatus('✅ Транскрибация: ' + tElapsed + 'с');
chat.innerHTML += '<div class="msg user">🗣 ' + fmt(text) + '</div>';
// Показываем распознанный текст + кнопку подтверждения
const msgDiv = document.createElement('div');
msgDiv.className = 'msg user';
msgDiv.innerHTML = '🗣 ' + fmt(text);
const btn = document.createElement('button');
btn.className = 'btn btn-primary';
btn.textContent = '✓ Отправить';
btn.style.marginTop = '8px'; btn.style.fontSize = '.8rem';
btn.onclick = () => {
pendingText = text;
pendingSendBtn = btn;
btn.textContent = '✓ Отмечено';
btn.style.background = '#16a34a'; btn.style.borderColor = '#16a34a';
doSend();
};
msgDiv.appendChild(btn);
chat.appendChild(msgDiv);
chat.scrollTop = chat.scrollHeight; chat.scrollTop = chat.scrollHeight;
await askLLM(text);
} catch (err) { } catch (err) {
clearInterval(tTimer); clearInterval(tTimer);
setStatus('⛔ Ошибка: ' + err.message); setStatus('⛔ Ошибка: ' + err.message);
@@ -264,12 +288,11 @@ async function askLLM(text) {
} }
// Тема // Тема
document.getElementById('btnTheme').onclick = () => { btnTheme.onclick = () => {
const dark = !document.body.classList.toggle('dark'); const dark = !document.body.classList.toggle('dark');
document.getElementById('btnTheme').textContent = dark ? '☀️' : '🌙'; btnTheme.textContent = dark ? '☀️' : '🌙';
localStorage.setItem('say_dark', dark ? '1' : '0'); localStorage.setItem('say_dark', dark ? '1' : '0');
}; };
document.getElementById('btnTheme').textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
document.getElementById('btnReset').onclick = () => { document.getElementById('btnReset').onclick = () => {
chatHistory = []; chat.innerHTML = ''; setStatus(''); chatHistory = []; chat.innerHTML = ''; setStatus('');
+8 -10
View File
@@ -1,8 +1,7 @@
/* === Nubes Design — точь-в-точь как ipwhitelist === */
:root { :root {
--bg: #f5f5f5; --card: #fff; --text: #1a1a1a; --muted: #6b7280; --bg: #f5f5f5; --card: #fff; --text: #1a1a1a; --muted: #6b7280;
--border: #d1d5db; --grey-light: #f3f4f6; --border: #d1d5db; --grey-light: #f3f4f6;
--blue: #2563eb; --blue-h: #1d4ed8; --blue: #2563eb; --blue-h: #1d4ed8; --green: #16a34a; --green-h: #15803d;
} }
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -22,7 +21,6 @@ body {
.main { flex: 1; min-width: 0; } .main { flex: 1; min-width: 0; }
.side { width: 272px; flex-shrink: 0; } .side { width: 272px; flex-shrink: 0; }
/* Cards */
.card { .card {
background: var(--card); border: 1px solid var(--border); background: var(--card); border: 1px solid var(--border);
border-radius: 12px; box-shadow: 0 1px 2px rgba(0,0,0,.04); border-radius: 12px; box-shadow: 0 1px 2px rgba(0,0,0,.04);
@@ -36,12 +34,12 @@ body {
.status { font-size: .85rem; color: var(--muted); margin-top: .5rem; min-height: 1.2rem; } .status { font-size: .85rem; color: var(--muted); margin-top: .5rem; min-height: 1.2rem; }
/* Buttons */
.btn { .btn {
display: inline-flex; align-items: center; gap: .35rem; display: inline-flex; align-items: center; gap: .35rem;
padding: .5rem 1rem; border: 1px solid var(--border); padding: .5rem 1rem; border: 1px solid var(--border);
border-radius: 6px; font-size: .85rem; font-weight: 500; border-radius: 6px; font-size: .85rem; font-weight: 500;
background: #fff; cursor: pointer; transition: background .15s; background: #fff; color: var(--text); cursor: pointer;
transition: background .15s, opacity .15s;
white-space: nowrap; font-family: inherit; white-space: nowrap; font-family: inherit;
} }
.btn:hover { background: var(--grey-light); } .btn:hover { background: var(--grey-light); }
@@ -63,16 +61,16 @@ body {
/* History */ /* History */
.hitem { padding: 6px 8px; border-radius: 4px; cursor: pointer; margin-bottom: 3px; font-size: .8rem; color: var(--muted); } .hitem { padding: 6px 8px; border-radius: 4px; cursor: pointer; margin-bottom: 3px; font-size: .8rem; color: var(--muted); }
.hitem:hover { background: var(--grey-light); } .hitem:hover { background: var(--grey-light); }
.hq { display: block; }
/* ===== ТЁМНАЯ ===== */ /* DARK */
body.dark { body.dark {
--bg: #0f0f1a; --card: #18182a; --text: #e0e0e0; --muted: #888; --bg: #0f0f1a; --card: #18182a; --text: #e0e0e0; --muted: #888;
--border: #2a2a40; --grey-light: #1e1e30; --border: #2a2a40; --grey-light: #1e1e30;
} }
body.dark header { background: var(--card) !important; border-color: var(--border) !important; } body.dark header { background: var(--card) !important; border-color: var(--border) !important; color: var(--muted) !important; }
body.dark .msg.user { background: #0a1e2e; border-color: #1e3a5f; color: #93c5fd; } body.dark header b { color: var(--text) !important; }
body.dark .msg.bot { background: var(--grey-light); border-color: var(--border); color: #d0d0f0; } body.dark .msg.user { background: #0a1e2e; border: 1px solid #1e3a5f; color: #93c5fd; }
body.dark .msg.bot { background: var(--grey-light); border: 1px solid var(--border); color: #d0d0f0; }
body.dark .msg b { color: #fff; } body.dark .msg b { color: #fff; }
body.dark .msg code { background: #2a2a44; color: #f8c291; } body.dark .msg code { background: #2a2a44; color: #f8c291; }
body.dark .btn { background: var(--card); color: var(--text); } body.dark .btn { background: var(--card); color: var(--text); }