updated avatars and voice mode
All checks were successful
Deploy FluentGerman.ai / deploy (push) Successful in 51s

This commit is contained in:
2026-02-16 20:59:39 +01:00
parent 3890f0479f
commit bce4124974
5 changed files with 227 additions and 137 deletions

View File

@@ -2,21 +2,15 @@
// Configure marked for safe rendering
marked.setOptions({
breaks: true, // Convert \n to <br>
gfm: true, // GitHub-flavored markdown
breaks: true,
gfm: true,
});
/**
* Render markdown text to sanitized HTML.
*/
function renderMarkdown(text) {
const raw = marked.parse(text);
return DOMPurify.sanitize(raw);
}
/**
* Format a date as a friendly relative time string.
*/
function relativeTime(dateStr) {
if (!dateStr) return null;
const date = new Date(dateStr);
@@ -34,6 +28,9 @@ function relativeTime(dateStr) {
return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
// Avatar — DiceBear "avataaars" style for friendly illustrated headshots
const AVATAR_SEEDS = ['Felix', 'Lena', 'Hans', 'Sophie', 'Klaus', 'Marta', 'Otto', 'Emma'];
document.addEventListener('DOMContentLoaded', async () => {
if (!requireAuth()) return;
@@ -41,27 +38,31 @@ document.addEventListener('DOMContentLoaded', async () => {
const displayName = user?.username || 'User';
document.getElementById('user-name').textContent = displayName;
// Random avatar from DiceBear
const avatarImg = document.getElementById('avatar-img');
const seed = AVATAR_SEEDS[Math.floor(Math.random() * AVATAR_SEEDS.length)];
avatarImg.src = `https://api.dicebear.com/9.x/avataaars/svg?seed=${seed}&backgroundColor=b6e3f4,c0aede,d1d4f9`;
const messagesEl = document.getElementById('chat-messages');
const inputEl = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const voiceBtn = document.getElementById('voice-btn');
const micBtn = document.getElementById('mic-btn');
const voiceToggle = document.getElementById('voice-toggle-input');
let history = [];
let voiceModeOn = false;
// ── Personalised welcome ──────────────────────────────────────────
const greetingEl = document.getElementById('welcome-greeting');
const subtitleEl = document.getElementById('welcome-subtitle');
const metaEl = document.getElementById('welcome-meta');
// Immediately show the username we already have from localStorage
greetingEl.textContent = `Hallo, ${displayName}! 👋`;
// Fetch dashboard info for latest instruction data
try {
const resp = await api('/chat/dashboard');
if (resp?.ok) {
const data = await resp.json();
// Use server username (authoritative)
greetingEl.textContent = `Hallo, ${data.username}! 👋`;
if (data.latest_instruction_at) {
@@ -74,20 +75,33 @@ document.addEventListener('DOMContentLoaded', async () => {
}
}
} catch (e) {
console.warn('[Chat] Could not fetch dashboard info:', e);
console.warn('[Chat] Could not fetch dashboard:', e);
}
// ── Voice ─────────────────────────────────────────────────────────
const voice = new VoiceManager();
await voice.init();
// Disable mic button if no STT method is available
if (voice.isDisabled) {
voiceBtn.disabled = true;
voiceBtn.title = 'Voice input requires Chrome or Edge (with HTTPS)';
voiceBtn.style.opacity = '0.35';
voiceBtn.style.cursor = 'not-allowed';
}
// Voice toggle handler
voiceToggle.addEventListener('change', () => {
voiceModeOn = voiceToggle.checked;
if (voiceModeOn) {
if (voice.isDisabled) {
showToast('Voice requires Chrome or Edge (HTTPS).', 'error');
voiceToggle.checked = false;
voiceModeOn = false;
return;
}
micBtn.classList.remove('hidden');
inputEl.placeholder = 'Voice mode ON — click the mic to speak...';
} else {
micBtn.classList.add('hidden');
inputEl.placeholder = 'Type your message...';
// Stop any active recording
if (voice.isRecording) voice.stopRecording();
}
});
voice.onResult = (text) => {
inputEl.value = text;
@@ -96,11 +110,10 @@ document.addEventListener('DOMContentLoaded', async () => {
};
voice.onStateChange = (recording) => {
voiceBtn.classList.toggle('recording', recording);
voiceBtn.textContent = recording ? '⏹' : '🎤';
micBtn.classList.toggle('recording', recording);
};
voiceBtn.addEventListener('click', () => voice.toggleRecording());
micBtn.addEventListener('click', () => voice.toggleRecording());
// ── Chat ──────────────────────────────────────────────────────────
function appendMessage(role, content) {
@@ -122,7 +135,6 @@ document.addEventListener('DOMContentLoaded', async () => {
const text = inputEl.value.trim();
if (!text) return;
// Capture whether this was a voice input BEFORE clearing
const wasVoice = voice.lastInputWasVoice;
voice.lastInputWasVoice = false;
@@ -132,7 +144,6 @@ document.addEventListener('DOMContentLoaded', async () => {
appendMessage('user', text);
history.push({ role: 'user', content: text });
// Create assistant message placeholder
const assistantEl = appendMessage('assistant', '');
let fullResponse = '';
@@ -166,7 +177,6 @@ document.addEventListener('DOMContentLoaded', async () => {
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;
}
@@ -183,8 +193,8 @@ document.addEventListener('DOMContentLoaded', async () => {
if (fullResponse) {
history.push({ role: 'assistant', content: fullResponse });
// Auto-speak response if the user used voice input
if (wasVoice) {
// Auto-speak if voice mode is ON (regardless of input method)
if (voiceModeOn) {
await voice.speak(fullResponse);
}
}