const state = { mode: "auto", analysis: null, contacts: [], recognition: null, listening: false, }; const elements = { apiStatus: document.querySelector("#apiStatus"), inputText: document.querySelector("#inputText"), analyzeButton: document.querySelector("#analyzeButton"), clearButton: document.querySelector("#clearButton"), voiceButton: document.querySelector("#voiceButton"), review: document.querySelector("#review"), writeToMonica: document.querySelector("#writeToMonica"), contactSearch: document.querySelector("#contactSearch"), contactSelect: document.querySelector("#contactSelect"), typeSelect: document.querySelector("#typeSelect"), summaryInput: document.querySelector("#summaryInput"), noteEnabled: document.querySelector("#noteEnabled"), noteBody: document.querySelector("#noteBody"), journalEnabled: document.querySelector("#journalEnabled"), journalTitle: document.querySelector("#journalTitle"), journalPost: document.querySelector("#journalPost"), reminders: document.querySelector("#reminders"), reminderTemplate: document.querySelector("#reminderTemplate"), addReminder: document.querySelector("#addReminder"), saveButton: document.querySelector("#saveButton"), discardButton: document.querySelector("#discardButton"), messageLog: document.querySelector("#messageLog"), }; document.querySelectorAll("[data-mode]").forEach((button) => { button.addEventListener("click", () => { state.mode = button.dataset.mode; document.querySelectorAll("[data-mode]").forEach((item) => item.classList.toggle("active", item === button)); }); }); elements.analyzeButton.addEventListener("click", analyzeInput); elements.clearButton.addEventListener("click", () => { elements.inputText.value = ""; elements.review.hidden = true; log(""); }); elements.discardButton.addEventListener("click", () => { elements.review.hidden = true; state.analysis = null; }); elements.addReminder.addEventListener("click", () => addReminderRow()); elements.saveButton.addEventListener("click", saveEntry); elements.contactSearch.addEventListener("input", debounce(loadContacts, 240)); elements.voiceButton.addEventListener("click", toggleSpeech); await boot(); async function boot() { await checkHealth(); await loadContacts(); setupSpeechRecognition(); } async function checkHealth() { try { const health = await api("/api/health"); elements.apiStatus.textContent = health.monicaConfigured ? `Monica verbunden: ${health.monicaBaseUrl}` : "Demo-Modus: Monica Token fehlt"; elements.writeToMonica.disabled = !health.monicaConfigured; } catch (error) { elements.apiStatus.textContent = "Backend nicht erreichbar"; log(error.message, "error"); } } async function loadContacts() { const query = elements.contactSearch.value.trim(); try { const payload = await api(`/api/contacts${query ? `?query=${encodeURIComponent(query)}` : ""}`); state.contacts = payload.data || []; renderContacts(); } catch (error) { log(error.message, "error"); } } function renderContacts(selectedName = "") { elements.contactSelect.innerHTML = ""; const empty = new Option("Kein Kontakt ausgewählt", ""); elements.contactSelect.append(empty); for (const contact of state.contacts) { const option = new Option(contact.name, contact.id); option.dataset.name = contact.name; if (selectedName && contact.name.toLowerCase().includes(selectedName.toLowerCase())) { option.selected = true; } elements.contactSelect.append(option); } } async function analyzeInput() { const text = elements.inputText.value.trim(); if (!text) { log("Erst etwas diktieren oder eintippen."); return; } elements.analyzeButton.disabled = true; try { const analysis = await api("/api/analyze", { method: "POST", body: JSON.stringify({ text, mode: state.mode }), }); state.analysis = analysis; renderAnalysis(analysis); log("Ich habe daraus einen Vorschlag gemacht. Bitte kurz prüfen."); } catch (error) { log(error.message, "error"); } finally { elements.analyzeButton.disabled = false; } } function renderAnalysis(analysis) { elements.review.hidden = false; elements.typeSelect.value = analysis.type; elements.summaryInput.value = analysis.summary; elements.noteEnabled.checked = Boolean(analysis.note?.enabled); elements.noteBody.value = analysis.note?.body || ""; elements.journalEnabled.checked = Boolean(analysis.journal?.enabled); elements.journalTitle.value = analysis.journal?.title || ""; elements.journalPost.value = analysis.journal?.post || ""; elements.reminders.innerHTML = ""; const inferredContact = analysis.contact?.name || ""; if (inferredContact) { elements.contactSearch.value = inferredContact; loadContacts().then(() => renderContacts(inferredContact)); } for (const reminder of analysis.reminders || []) { addReminderRow(reminder); } } function addReminderRow(reminder = {}) { const fragment = elements.reminderTemplate.content.cloneNode(true); const row = fragment.querySelector(".reminder"); row.querySelector(".reminderEnabled").checked = reminder.enabled ?? true; row.querySelector(".reminderTitle").value = reminder.title || ""; row.querySelector(".reminderDate").value = reminder.initial_date || ""; row.querySelector(".reminderDescription").value = reminder.description || ""; row.querySelector(".removeReminder").addEventListener("click", () => row.remove()); elements.reminders.append(row); } async function saveEntry() { const selectedOption = elements.contactSelect.selectedOptions[0]; const entry = { writeToMonica: elements.writeToMonica.checked, type: elements.typeSelect.value, summary: elements.summaryInput.value.trim(), rawText: state.analysis?.rawText || elements.inputText.value, contact: selectedOption?.value ? { id: Number(selectedOption.value), name: selectedOption.dataset.name || selectedOption.textContent } : null, note: { enabled: elements.noteEnabled.checked, body: elements.noteBody.value.trim(), isFavorited: state.analysis?.note?.isFavorited || false, }, journal: { enabled: elements.journalEnabled.checked, title: elements.journalTitle.value.trim(), post: elements.journalPost.value.trim(), }, reminders: collectReminders(), }; elements.saveButton.disabled = true; try { const result = await api("/api/save", { method: "POST", body: JSON.stringify(entry), }); const monicaSummary = result.monicaResults?.length ? ` Monica: ${result.monicaResults.map((item) => `${item.type} ${item.ok ? "ok" : "Fehler"}`).join(", ")}.` : ""; log(`Gespeichert.${monicaSummary}`); } catch (error) { log(error.message, "error"); } finally { elements.saveButton.disabled = false; } } function collectReminders() { return [...elements.reminders.querySelectorAll(".reminder")].map((row) => ({ enabled: row.querySelector(".reminderEnabled").checked, title: row.querySelector(".reminderTitle").value.trim(), initial_date: row.querySelector(".reminderDate").value, description: row.querySelector(".reminderDescription").value.trim(), frequency_type: "one_time", frequency_number: 1, })); } function setupSpeechRecognition() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { elements.voiceButton.textContent = "Sprache nicht unterstützt"; elements.voiceButton.disabled = true; return; } const recognition = new SpeechRecognition(); recognition.lang = "de-DE"; recognition.continuous = true; recognition.interimResults = true; recognition.addEventListener("result", (event) => { let finalText = ""; let interimText = ""; for (let index = event.resultIndex; index < event.results.length; index += 1) { const transcript = event.results[index][0].transcript; if (event.results[index].isFinal) { finalText += transcript; } else { interimText += transcript; } } if (finalText) { elements.inputText.value = `${elements.inputText.value.trim()} ${finalText}`.trim(); } if (interimText) { log(`Höre: ${interimText}`); } }); recognition.addEventListener("end", () => { state.listening = false; elements.voiceButton.classList.remove("listening"); elements.voiceButton.textContent = "Aufnehmen"; }); recognition.addEventListener("error", (event) => { log(`Spracherkennung: ${event.error}`, "error"); }); state.recognition = recognition; } function toggleSpeech() { if (!state.recognition) return; if (state.listening) { state.recognition.stop(); return; } state.listening = true; elements.voiceButton.classList.add("listening"); elements.voiceButton.textContent = "Stoppen"; state.recognition.start(); } async function api(url, options = {}) { const response = await fetch(url, { ...options, headers: { "Content-Type": "application/json", ...(options.headers || {}), }, }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(payload.detail || payload.error || response.statusText); } return payload; } function log(message, level = "info") { elements.messageLog.textContent = message || ""; elements.messageLog.dataset.level = level; } function debounce(fn, delay) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }