const state = { mode: "auto", analysis: null, contacts: [], recognition: null, listening: false, selectedContactId: null, }; 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"), // Tab Elements tabButtons: document.querySelectorAll(".tab-btn"), tabViews: document.querySelectorAll(".tab-view"), tabBtnContactInfo: document.querySelector("#tabBtnContactInfo"), // Contact Info Elements contactInfoPlaceholder: document.querySelector("#contact-info-placeholder"), contactInfoContent: document.querySelector("#contact-info-content"), contactDetailsAvatar: document.querySelector("#contactDetailsAvatar"), contactDetailsName: document.querySelector("#contactDetailsName"), contactDetailsDesc: document.querySelector("#contactDetailsDesc"), contactDetailsEmail: document.querySelector("#contactDetailsEmail"), contactDetailsPhone: document.querySelector("#contactDetailsPhone"), contactDetailsAddress: document.querySelector("#contactDetailsAddress"), // Create Contact Elements createContactForm: document.querySelector("#createContactForm"), newContactFirstName: document.querySelector("#newContactFirstName"), newContactLastName: document.querySelector("#newContactLastName"), newContactEmail: document.querySelector("#newContactEmail"), newContactPhone: document.querySelector("#newContactPhone"), newContactAddress: document.querySelector("#newContactAddress"), newContactDesc: document.querySelector("#newContactDesc"), }; // Mode Buttons 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)); }); }); // Tab Switch Listeners elements.tabButtons.forEach((btn) => { btn.addEventListener("click", () => { const tabName = btn.dataset.tab; elements.tabButtons.forEach((b) => b.classList.toggle("is-active", b === btn)); elements.tabViews.forEach((v) => v.classList.toggle("is-active", v.id === `view-${tabName}`)); if (tabName === "contact-info") { loadSelectedContactDetails(); } }); }); // Dictate Control Listeners 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); // Select Contact Listener elements.contactSelect.addEventListener("change", () => { const val = elements.contactSelect.value; state.selectedContactId = val ? Number(val) : null; if (state.selectedContactId) { loadSelectedContactDetails(); } else { hideContactDetails(); } }); // Create Contact Form Listener elements.createContactForm.addEventListener("submit", async (e) => { e.preventDefault(); const contactData = { first_name: elements.newContactFirstName.value.trim(), last_name: elements.newContactLastName.value.trim(), email: elements.newContactEmail.value.trim(), phone: elements.newContactPhone.value.trim(), address: elements.newContactAddress.value.trim(), description: elements.newContactDesc.value.trim(), }; try { log("Erstelle neuen Kontakt in Monica..."); const res = await api("/api/contacts/create", { method: "POST", body: JSON.stringify(contactData), }); log(`Kontakt ${res.data.name} wurde erfolgreich erstellt!`); elements.createContactForm.reset(); // Reload contacts and auto-select new one await loadContacts(); elements.contactSelect.value = res.data.id; state.selectedContactId = res.data.id; elements.contactSearch.value = res.data.name; // Switch to dictate and load details elements.tabButtons[0].click(); loadSelectedContactDetails(); } catch (error) { log(`Kontakt konnte nicht erstellt werden: ${error.message}`, "error"); } }); await boot(); async function boot() { await checkHealth(); await loadContacts(); setupSpeechRecognition(); if (window.lucide) { window.lucide.createIcons(); } } 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; elements.writeToMonica.checked = 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); let selectedId = ""; 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; selectedId = contact.id; } elements.contactSelect.append(option); } if (selectedId) { state.selectedContactId = Number(selectedId); } } async function loadSelectedContactDetails() { if (!state.selectedContactId) { hideContactDetails(); return; } try { const details = await api(`/api/contacts/${state.selectedContactId}`); elements.contactDetailsName.textContent = details.name; elements.contactDetailsDesc.textContent = details.description || "Keine Beschreibung"; elements.contactDetailsEmail.textContent = details.email || "-"; elements.contactDetailsPhone.textContent = details.phone || "-"; elements.contactDetailsAddress.textContent = details.address || "-"; const avatarLetter = details.first_name ? details.first_name.slice(0, 1).toUpperCase() : "?"; elements.contactDetailsAvatar.textContent = avatarLetter; elements.contactInfoPlaceholder.hidden = true; elements.contactInfoContent.removeAttribute("hidden"); elements.contactInfoContent.style.display = "block"; if (window.lucide) { window.lucide.createIcons(); } } catch (error) { log(`Kontaktdetails konnten nicht geladen werden: ${error.message}`, "error"); } } function hideContactDetails() { elements.contactInfoPlaceholder.hidden = false; elements.contactInfoContent.setAttribute("hidden", "true"); elements.contactInfoContent.style.display = "none"; } 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, contacts: state.contacts }), }); state.analysis = analysis; await renderAnalysis(analysis); log("Ich habe daraus einen Vorschlag gemacht. Bitte kurz prüfen."); } catch (error) { log(error.message, "error"); } finally { elements.analyzeButton.disabled = false; } } async 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; if (inferredContact && inferredContact.id) { let match = state.contacts.find(c => c.id === inferredContact.id); if (!match) { state.contacts.push({ id: inferredContact.id, name: inferredContact.name, description: inferredContact.description || "" }); renderContacts(); } elements.contactSearch.value = ""; elements.contactSelect.value = inferredContact.id; state.selectedContactId = inferredContact.id; await loadSelectedContactDetails(); } else { elements.contactSelect.value = ""; state.selectedContactId = null; hideContactDetails(); } 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.innerHTML = ' 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.innerHTML = ' 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); }; }