303 lines
7.9 KiB
HTML
303 lines
7.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Basti Voice Chat</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
background: #0f0f14;
|
|
color: #e0e0e0;
|
|
height: 100dvh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
.header {
|
|
background: #1a1a2e;
|
|
padding: 12px 16px;
|
|
text-align: center;
|
|
border-bottom: 1px solid #2a2a3e;
|
|
font-size: 14px;
|
|
color: #888;
|
|
}
|
|
.header strong { color: #7c8aff; }
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.msg {
|
|
max-width: 85%;
|
|
padding: 12px 16px;
|
|
border-radius: 18px;
|
|
line-height: 1.5;
|
|
font-size: 15px;
|
|
word-wrap: break-word;
|
|
}
|
|
.msg.user {
|
|
align-self: flex-end;
|
|
background: #7c8aff;
|
|
color: #fff;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
.msg.assistant {
|
|
align-self: flex-start;
|
|
background: #2a2a3e;
|
|
color: #e0e0e0;
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
.msg.status {
|
|
align-self: center;
|
|
background: transparent;
|
|
color: #666;
|
|
font-size: 13px;
|
|
font-style: italic;
|
|
padding: 4px;
|
|
}
|
|
.bottom {
|
|
background: #1a1a2e;
|
|
padding: 20px 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
border-top: 1px solid #2a2a3e;
|
|
padding-bottom: max(20px, env(safe-area-inset-bottom));
|
|
}
|
|
#micBtn {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: #7c8aff;
|
|
color: white;
|
|
font-size: 36px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 20px rgba(124,138,255,0.3);
|
|
}
|
|
#micBtn:active { transform: scale(0.95); }
|
|
#micBtn.listening {
|
|
background: #ff4d6a;
|
|
animation: pulse 1.5s infinite;
|
|
box-shadow: 0 4px 30px rgba(255,77,106,0.5);
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(255,77,106,0.5); }
|
|
50% { box-shadow: 0 0 0 20px rgba(255,77,106,0); }
|
|
}
|
|
#micBtn.thinking { background: #ffa726; box-shadow: 0 4px 20px rgba(255,167,38,0.3); }
|
|
#statusText { font-size: 13px; color: #888; text-align: center; min-height: 18px; }
|
|
.settings-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
width: 100%;
|
|
max-width: 300px;
|
|
}
|
|
#textInput {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
border-radius: 20px;
|
|
border: 1px solid #2a2a3e;
|
|
background: #0f0f14;
|
|
color: #e0e0e0;
|
|
font-size: 14px;
|
|
outline: none;
|
|
}
|
|
#textInput:focus { border-color: #7c8aff; }
|
|
#sendBtn {
|
|
padding: 8px 20px;
|
|
border-radius: 20px;
|
|
border: none;
|
|
background: #2a2a3e;
|
|
color: #e0e0e0;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
#sendBtn:hover { background: #3a3a4e; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">🗣️ <strong>Basti</strong> Voice Chat</div>
|
|
|
|
<div id="messages">
|
|
<div class="msg assistant">Hey Daniel! Drück den 🎤-Button und sprich mit mir. Ich höre zu und antworte sofort.</div>
|
|
</div>
|
|
|
|
<div class="bottom">
|
|
<div id="statusText">Bereit</div>
|
|
<button id="micBtn" onclick="toggleMic()" title="Zum Sprechen drücken">🎤</button>
|
|
<div style="width:100%;max-width:300px;display:flex;gap:8px">
|
|
<input type="text" id="textInput" placeholder="Oder tippen..." onkeydown="if(event.key==='Enter')sendText()">
|
|
<button id="sendBtn" onclick="sendText()">Senden</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = '/api/chat';
|
|
let recognition = null;
|
|
let isListening = false;
|
|
let isThinking = false;
|
|
|
|
function initSpeech() {
|
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
if (!SpeechRecognition) return false;
|
|
recognition = new SpeechRecognition();
|
|
recognition.lang = 'de-DE';
|
|
recognition.interimResults = false;
|
|
recognition.continuous = false;
|
|
recognition.maxAlternatives = 1;
|
|
recognition.onresult = (e) => {
|
|
const text = e.results[0][0].transcript.trim();
|
|
if (text) { addMessage(text, 'user'); sendToBasti(text); }
|
|
};
|
|
recognition.onerror = (e) => {
|
|
console.log('Speech error:', e.error);
|
|
if (e.error === 'no-speech') {
|
|
setStatus('Nichts gehört — nochmal?');
|
|
} else if (e.error === 'not-allowed') {
|
|
setStatus('Mikrofon-Zugriff verweigert');
|
|
} else {
|
|
setStatus('Fehler: ' + e.error);
|
|
}
|
|
stopListening();
|
|
};
|
|
recognition.onend = () => {
|
|
if (isListening && !isThinking) {
|
|
// Restart if still listening (continuous mode workaround)
|
|
try { recognition.start(); } catch(e) {}
|
|
} else {
|
|
stopListening();
|
|
}
|
|
};
|
|
return true;
|
|
}
|
|
|
|
function toggleMic() {
|
|
if (isThinking) return;
|
|
if (isListening) { stopListening(); return; }
|
|
if (!recognition && !initSpeech()) {
|
|
setStatus('Speech API nicht verfügbar — bitte tippen');
|
|
return;
|
|
}
|
|
startListening();
|
|
}
|
|
|
|
function startListening() {
|
|
isListening = true;
|
|
document.getElementById('micBtn').classList.add('listening');
|
|
document.getElementById('micBtn').textContent = '🔴';
|
|
setStatus('Höre zu...');
|
|
try { recognition.start(); } catch(e) {}
|
|
}
|
|
|
|
function stopListening() {
|
|
isListening = false;
|
|
document.getElementById('micBtn').classList.remove('listening', 'thinking');
|
|
document.getElementById('micBtn').textContent = '🎤';
|
|
if (!isThinking) setStatus('Bereit');
|
|
try { recognition.stop(); } catch(e) {}
|
|
}
|
|
|
|
function setStatus(text) {
|
|
document.getElementById('statusText').textContent = text;
|
|
}
|
|
|
|
function addMessage(text, role) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg ' + role;
|
|
div.textContent = text;
|
|
if (role === 'status') div.className = 'msg status';
|
|
document.getElementById('messages').appendChild(div);
|
|
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
|
}
|
|
|
|
async function sendToBasti(text) {
|
|
isThinking = true;
|
|
stopListening();
|
|
document.getElementById('micBtn').classList.add('thinking');
|
|
document.getElementById('micBtn').textContent = '⏳';
|
|
setStatus('Basti denkt nach...');
|
|
|
|
try {
|
|
const resp = await fetch(API, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ text })
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.detail || err.error || 'API-Fehler');
|
|
}
|
|
|
|
const data = await resp.json();
|
|
const reply = data.response || data.text || JSON.stringify(data);
|
|
addMessage(reply, 'assistant');
|
|
speak(reply);
|
|
|
|
// Also save to session
|
|
fetch(API + '/save', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ user: text, assistant: reply })
|
|
}).catch(() => {});
|
|
|
|
} catch(e) {
|
|
addMessage('⚠️ Fehler: ' + e.message, 'status');
|
|
} finally {
|
|
isThinking = false;
|
|
document.getElementById('micBtn').classList.remove('thinking');
|
|
document.getElementById('micBtn').textContent = '🎤';
|
|
setStatus('Bereit');
|
|
}
|
|
}
|
|
|
|
function speak(text) {
|
|
if (!window.speechSynthesis) return;
|
|
// Cancel any ongoing speech
|
|
speechSynthesis.cancel();
|
|
const u = new SpeechSynthesisUtterance(text);
|
|
u.lang = 'en-US';
|
|
u.rate = 1.0;
|
|
u.pitch = 1.0;
|
|
// Try to use a good English voice
|
|
const voices = speechSynthesis.getVoices();
|
|
const preferred = voices.find(v => v.lang.startsWith('en') && v.name.includes('Google') || v.name.includes('Samantha') || v.name.includes('Daniel'));
|
|
if (preferred) u.voice = preferred;
|
|
speechSynthesis.speak(u);
|
|
}
|
|
|
|
function sendText() {
|
|
const input = document.getElementById('textInput');
|
|
const text = input.value.trim();
|
|
if (!text || isThinking) return;
|
|
input.value = '';
|
|
addMessage(text, 'user');
|
|
sendToBasti(text);
|
|
}
|
|
|
|
// Preload voices
|
|
if (window.speechSynthesis) {
|
|
speechSynthesis.getVoices();
|
|
speechSynthesis.onvoiceschanged = () => speechSynthesis.getVoices();
|
|
}
|
|
|
|
// Handle visibility changes to stop mic when switching apps
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden && isListening) stopListening();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|