improved feedback for voice mode
All checks were successful
Deploy FluentGerman.ai / deploy (push) Successful in 48s
All checks were successful
Deploy FluentGerman.ai / deploy (push) Successful in 48s
This commit is contained in:
@@ -120,6 +120,18 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
micBtn.classList.toggle('recording', recording);
|
||||
};
|
||||
|
||||
// Show "Transcribing..." state
|
||||
voice.onProcessing = (processing) => {
|
||||
if (processing) {
|
||||
inputEl.placeholder = 'Transcribing...';
|
||||
inputEl.disabled = true;
|
||||
} else {
|
||||
inputEl.placeholder = voiceModeOn ? 'Voice mode ON — click the mic to speak...' : 'Type your message...';
|
||||
inputEl.disabled = false;
|
||||
inputEl.focus();
|
||||
}
|
||||
};
|
||||
|
||||
micBtn.addEventListener('click', () => voice.toggleRecording());
|
||||
|
||||
// ── Chat ──────────────────────────────────────────────────────────
|
||||
@@ -128,7 +140,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
div.className = `message message-${role}`;
|
||||
|
||||
if (role === 'assistant') {
|
||||
div.innerHTML = renderMarkdown(content);
|
||||
// content might be empty initially for thinking state
|
||||
if (content === 'Thinking...') {
|
||||
div.innerHTML = '<span class="thinking-dots">Thinking<span>.</span><span>.</span><span>.</span></span>';
|
||||
div.classList.add('message-thinking');
|
||||
} else {
|
||||
div.innerHTML = renderMarkdown(content);
|
||||
}
|
||||
} else {
|
||||
div.textContent = content;
|
||||
}
|
||||
@@ -168,41 +186,80 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// Special handling for Voice Mode: Buffer text, wait for TTS, then show & play
|
||||
if (voiceModeOn) {
|
||||
// Show thinking state
|
||||
assistantEl.innerHTML = '<span class="thinking-dots">Thinking<span>.</span><span>.</span><span>.</span></span>';
|
||||
assistantEl.classList.add('message-thinking');
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') break;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.token) {
|
||||
fullResponse += parsed.token;
|
||||
assistantEl.innerHTML = renderMarkdown(fullResponse);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
if (parsed.error) {
|
||||
showToast(parsed.error, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
// skip unparseable chunks
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') break;
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.token) fullResponse += parsed.token;
|
||||
if (parsed.error) showToast(parsed.error, 'error');
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fullResponse) {
|
||||
history.push({ role: 'assistant', content: fullResponse });
|
||||
// Text complete. Now fetch audio.
|
||||
if (fullResponse) {
|
||||
history.push({ role: 'assistant', content: fullResponse });
|
||||
|
||||
// Auto-speak if voice mode is ON (regardless of input method)
|
||||
if (voiceModeOn) {
|
||||
await voice.speak(fullResponse);
|
||||
// Keep "Thinking..." until audio is ready or failed
|
||||
const audioUrl = await voice.fetchAudio(fullResponse);
|
||||
|
||||
// Visual update: Remove thinking, show text
|
||||
assistantEl.classList.remove('message-thinking');
|
||||
assistantEl.innerHTML = renderMarkdown(fullResponse);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
|
||||
if (audioUrl) {
|
||||
await voice.playAudio(audioUrl);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Normal Text Mode: Stream directly to UI
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') break;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.token) {
|
||||
fullResponse += parsed.token;
|
||||
assistantEl.innerHTML = renderMarkdown(fullResponse);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
if (parsed.error) {
|
||||
showToast(parsed.error, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
// skip unparseable chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fullResponse) {
|
||||
history.push({ role: 'assistant', content: fullResponse });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user