v1.4 — день/ночь в Управление, кнопка подтверждения, комбо текст+микрофон
This commit is contained in:
+44
-21
@@ -26,18 +26,20 @@
|
||||
<div class="page">
|
||||
<div class="main">
|
||||
<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 style="display:flex;gap:8px;margin-bottom:8px;">
|
||||
<span class="tag">🤖 GPT-OSS-120B</span>
|
||||
<span class="tag">🎤 Whisper-v3-Turbo</span>
|
||||
</div>
|
||||
<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="btnPlay" class="btn" disabled>▶ Прослушать</button>
|
||||
<button id="btnReset" class="btn">🗑 Сброс</button>
|
||||
<button id="btnTheme" class="btn">🌙</button>
|
||||
</div>
|
||||
<div id="status" class="status"></div>
|
||||
</div>
|
||||
@@ -47,7 +49,7 @@
|
||||
<div class="card-header">Диалог</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
</div>
|
||||
<div id="chat" style="min-height:200px;max-height:340px;overflow-y:auto;"></div>
|
||||
@@ -64,7 +66,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const VERSION = 'v1.3';
|
||||
const VERSION = 'v1.4';
|
||||
document.getElementById('ver').textContent = VERSION;
|
||||
|
||||
const btnRecord = document.getElementById('btnRecord');
|
||||
@@ -76,13 +78,15 @@ const meter = document.getElementById('meter');
|
||||
const meterCtx = meter.getContext('2d');
|
||||
const txtInput = document.getElementById('txtInput');
|
||||
const btnSend = document.getElementById('btnSend');
|
||||
const btnTheme = document.getElementById('btnTheme');
|
||||
|
||||
let mediaRecorder, chunks = [], userBlob = null;
|
||||
let isRecording = false, micAnalyser = null, micAnimId = null;
|
||||
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') || '[]');
|
||||
|
||||
@@ -123,17 +127,21 @@ function fmt(s) {
|
||||
|
||||
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;
|
||||
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;
|
||||
await askLLM(text);
|
||||
await askLLM(text.trim());
|
||||
}
|
||||
|
||||
btnSend.onclick = () => sendText(txtInput.value);
|
||||
txtInput.onkeydown = e => { if (e.key === 'Enter') sendText(txtInput.value); };
|
||||
btnSend.onclick = doSend;
|
||||
txtInput.onkeydown = e => { if (e.key === 'Enter') doSend(); };
|
||||
|
||||
// ===== Запись =====
|
||||
btnRecord.onclick = async () => {
|
||||
@@ -167,7 +175,7 @@ async function startRecording() {
|
||||
if (audioContext) audioContext.close();
|
||||
userBlob = new Blob(chunks, { type: 'audio/webm' });
|
||||
btnPlay.disabled = false;
|
||||
await transcribeAndChat();
|
||||
await transcribe();
|
||||
};
|
||||
mediaRecorder.start();
|
||||
}
|
||||
@@ -200,8 +208,8 @@ btnPlay.onclick = () => {
|
||||
a.play(); setStatus('🔊 Ваша запись...');
|
||||
};
|
||||
|
||||
// ===== Транскрибация → LLM =====
|
||||
async function transcribeAndChat() {
|
||||
// ===== Транскрибация → подтверждение → LLM =====
|
||||
async function transcribe() {
|
||||
let tStart = Date.now();
|
||||
setStatus('🎤 Транскрибация... 0с');
|
||||
const tTimer = setInterval(() => setStatus('🎤 Транскрибация... ' + ((Date.now() - tStart) / 1000).toFixed(0) + 'с'), 200);
|
||||
@@ -214,9 +222,25 @@ async function transcribeAndChat() {
|
||||
const text = tData.text || '(не распознано)';
|
||||
const tElapsed = ((Date.now() - tStart) / 1000).toFixed(1);
|
||||
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;
|
||||
await askLLM(text);
|
||||
} catch (err) {
|
||||
clearInterval(tTimer);
|
||||
setStatus('⛔ Ошибка: ' + err.message);
|
||||
@@ -264,12 +288,11 @@ async function askLLM(text) {
|
||||
}
|
||||
|
||||
// Тема
|
||||
document.getElementById('btnTheme').onclick = () => {
|
||||
btnTheme.onclick = () => {
|
||||
const dark = !document.body.classList.toggle('dark');
|
||||
document.getElementById('btnTheme').textContent = dark ? '☀️' : '🌙';
|
||||
btnTheme.textContent = dark ? '☀️' : '🌙';
|
||||
localStorage.setItem('say_dark', dark ? '1' : '0');
|
||||
};
|
||||
document.getElementById('btnTheme').textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
|
||||
|
||||
document.getElementById('btnReset').onclick = () => {
|
||||
chatHistory = []; chat.innerHTML = ''; setStatus('');
|
||||
|
||||
+8
-10
@@ -1,8 +1,7 @@
|
||||
/* === Nubes Design — точь-в-точь как ipwhitelist === */
|
||||
:root {
|
||||
--bg: #f5f5f5; --card: #fff; --text: #1a1a1a; --muted: #6b7280;
|
||||
--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; }
|
||||
@@ -22,7 +21,6 @@ body {
|
||||
.main { flex: 1; min-width: 0; }
|
||||
.side { width: 272px; flex-shrink: 0; }
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
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; }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
padding: .5rem 1rem; border: 1px solid var(--border);
|
||||
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;
|
||||
}
|
||||
.btn:hover { background: var(--grey-light); }
|
||||
@@ -63,16 +61,16 @@ body {
|
||||
/* History */
|
||||
.hitem { padding: 6px 8px; border-radius: 4px; cursor: pointer; margin-bottom: 3px; font-size: .8rem; color: var(--muted); }
|
||||
.hitem:hover { background: var(--grey-light); }
|
||||
.hq { display: block; }
|
||||
|
||||
/* ===== ТЁМНАЯ ===== */
|
||||
/* DARK */
|
||||
body.dark {
|
||||
--bg: #0f0f1a; --card: #18182a; --text: #e0e0e0; --muted: #888;
|
||||
--border: #2a2a40; --grey-light: #1e1e30;
|
||||
}
|
||||
body.dark header { background: var(--card) !important; border-color: var(--border) !important; }
|
||||
body.dark .msg.user { background: #0a1e2e; border-color: #1e3a5f; color: #93c5fd; }
|
||||
body.dark .msg.bot { background: var(--grey-light); border-color: var(--border); color: #d0d0f0; }
|
||||
body.dark header { background: var(--card) !important; border-color: var(--border) !important; color: var(--muted) !important; }
|
||||
body.dark header b { color: var(--text) !important; }
|
||||
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 code { background: #2a2a44; color: #f8c291; }
|
||||
body.dark .btn { background: var(--card); color: var(--text); }
|
||||
|
||||
Reference in New Issue
Block a user