Files
BSN-Chatsystem/voice-chat/index.html
T

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>