/* FluentGerman.ai — Chat interface logic */ // Configure marked for safe rendering marked.setOptions({ breaks: true, // Convert \n to
gfm: true, // GitHub-flavored markdown }); /** * Render markdown text to sanitized HTML. */ function renderMarkdown(text) { const raw = marked.parse(text); return DOMPurify.sanitize(raw); } document.addEventListener('DOMContentLoaded', async () => { if (!requireAuth()) return; const user = getUser(); document.getElementById('user-name').textContent = user?.username || 'User'; const messagesEl = document.getElementById('chat-messages'); const inputEl = document.getElementById('chat-input'); const sendBtn = document.getElementById('send-btn'); const voiceBtn = document.getElementById('voice-btn'); let history = []; // Init voice const voice = new VoiceManager(); await voice.init(); voice.onResult = (text) => { inputEl.value = text; voice.lastInputWasVoice = true; sendMessage(); }; voice.onStateChange = (recording) => { voiceBtn.classList.toggle('recording', recording); voiceBtn.textContent = recording ? '⏹' : '🎤'; }; voiceBtn.addEventListener('click', () => voice.toggleRecording()); // Chat function appendMessage(role, content) { const div = document.createElement('div'); div.className = `message message-${role}`; if (role === 'assistant') { div.innerHTML = renderMarkdown(content); } else { div.textContent = content; } messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; return div; } async function sendMessage() { const text = inputEl.value.trim(); if (!text) return; // Capture whether this was a voice input BEFORE clearing const wasVoice = voice.lastInputWasVoice; voice.lastInputWasVoice = false; inputEl.value = ''; sendBtn.disabled = true; appendMessage('user', text); history.push({ role: 'user', content: text }); // Create assistant message placeholder const assistantEl = appendMessage('assistant', ''); let fullResponse = ''; try { const response = await api('/chat/', { method: 'POST', body: JSON.stringify({ message: text, history: history.slice(-20) }), }); if (!response?.ok) { const errData = await response?.json().catch(() => ({})); throw new Error(errData.detail || `Chat failed (${response?.status})`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); 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; // Live-render markdown as tokens stream in 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 }); // Auto-speak response if the user used voice input if (wasVoice) { await voice.speak(fullResponse); } } } catch (e) { assistantEl.textContent = 'Sorry, something went wrong. Please try again.'; showToast(e.message, 'error'); console.error('[Chat] Error:', e); } sendBtn.disabled = false; inputEl.focus(); } sendBtn.addEventListener('click', sendMessage); inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Logout document.getElementById('logout-btn').addEventListener('click', logout); });