154 lines
4.8 KiB
JavaScript
154 lines
4.8 KiB
JavaScript
/* FluentGerman.ai — Chat interface logic */
|
|
|
|
// Configure marked for safe rendering
|
|
marked.setOptions({
|
|
breaks: true, // Convert \n to <br>
|
|
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);
|
|
});
|