Feat: Complete smart contact matching, CRM features integration, and premium glassmorphism UI
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 10s

This commit is contained in:
Kroonk
2026-05-22 18:04:28 +02:00
parent d812944d5a
commit 9f62eebed6
4 changed files with 791 additions and 115 deletions

View File

@@ -4,6 +4,7 @@ const state = {
contacts: [],
recognition: null,
listening: false,
selectedContactId: null,
};
const elements = {
@@ -29,8 +30,33 @@ const elements = {
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;
@@ -38,6 +64,20 @@ document.querySelectorAll("[data-mode]").forEach((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 = "";
@@ -53,12 +93,64 @@ 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() {
@@ -91,14 +183,57 @@ function renderContacts(selectedName = "") {
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() {
@@ -112,10 +247,14 @@ async function analyzeInput() {
try {
const analysis = await api("/api/analyze", {
method: "POST",
body: JSON.stringify({ text, mode: state.mode }),
body: JSON.stringify({
text,
mode: state.mode,
contacts: state.contacts
}),
});
state.analysis = analysis;
renderAnalysis(analysis);
await renderAnalysis(analysis);
log("Ich habe daraus einen Vorschlag gemacht. Bitte kurz prüfen.");
} catch (error) {
log(error.message, "error");
@@ -124,7 +263,7 @@ async function analyzeInput() {
}
}
function renderAnalysis(analysis) {
async function renderAnalysis(analysis) {
elements.review.hidden = false;
elements.typeSelect.value = analysis.type;
elements.summaryInput.value = analysis.summary;
@@ -135,10 +274,26 @@ function renderAnalysis(analysis) {
elements.journalPost.value = analysis.journal?.post || "";
elements.reminders.innerHTML = "";
const inferredContact = analysis.contact?.name || "";
if (inferredContact) {
elements.contactSearch.value = inferredContact;
loadContacts().then(() => renderContacts(inferredContact));
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 || []) {
@@ -246,7 +401,7 @@ function setupSpeechRecognition() {
recognition.addEventListener("end", () => {
state.listening = false;
elements.voiceButton.classList.remove("listening");
elements.voiceButton.textContent = "Aufnehmen";
elements.voiceButton.innerHTML = '<span class="mic-pulse-circle"></span> Aufnehmen';
});
recognition.addEventListener("error", (event) => {
@@ -266,7 +421,7 @@ function toggleSpeech() {
state.listening = true;
elements.voiceButton.classList.add("listening");
elements.voiceButton.textContent = "Stoppen";
elements.voiceButton.innerHTML = '<span class="mic-pulse-circle recording"></span> Stoppen';
state.recognition.start();
}