v1.4 — день/ночь в Управление, кнопка подтверждения, комбо текст+микрофон
This commit is contained in:
+44
-21
@@ -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
@@ -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); }
|
||||||
|
|||||||
Reference in New Issue
Block a user