(() => { const API_URL = 'https://fb.srv.medeba-media.de/api'; const PROVIDER_MODELS = { gemini: [ { value: '', label: 'Standard (gemini-2.0-flash-exp)' }, { value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash' }, { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' } ], claude: [ { value: '', label: 'Standard (claude-3-5-haiku)' }, { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku (schnell)' }, { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet (beste Qualität)' }, { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' } ], openai: [ { value: '', label: 'Standard (gpt-3.5-turbo)' }, { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (günstig)' }, { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, { value: 'gpt-4o', label: 'GPT-4o' }, { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' } ] }; const PROVIDER_INFO = { gemini: { name: 'Google Gemini', apiKeyLink: 'https://aistudio.google.com/app/apikey', apiKeyHelp: 'API-Schlüssel erforderlich. Erstelle ihn im Google AI Studio.' }, claude: { name: 'Anthropic Claude', apiKeyLink: 'https://console.anthropic.com/settings/keys', apiKeyHelp: 'API-Schlüssel erforderlich. Erstelle ihn in der Anthropic Console.' }, openai: { name: 'OpenAI', apiKeyLink: 'https://platform.openai.com/api-keys', apiKeyHelp: 'Für lokale OpenAI-kompatible Server (z.B. Ollama) kannst du den Schlüssel leer lassen.' } }; let credentials = []; let currentSettings = null; function apiFetch(url, options = {}) { return fetch(url, {...options, credentials: 'include'}); } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 24px; right: 24px; background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'}; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; z-index: 999999; max-width: 350px; animation: slideIn 0.3s ease-out; `; toast.textContent = message; if (!document.getElementById('settings-toast-styles')) { const style = document.createElement('style'); style.id = 'settings-toast-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; document.head.appendChild(style); } document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => toast.remove(), 300); }, 3000); } function showError(msg) { showToast(msg, 'error'); } function showSuccess(msg) { showToast(msg, 'success'); } async function loadCredentials() { const res = await apiFetch(`${API_URL}/ai-credentials`); if (!res.ok) throw new Error('Failed to load credentials'); credentials = await res.json(); renderCredentials(); updateActiveCredentialSelect(); } async function loadSettings() { const res = await apiFetch(`${API_URL}/ai-settings`); if (!res.ok) throw new Error('Failed to load settings'); currentSettings = await res.json(); document.getElementById('aiEnabled').checked = currentSettings.enabled === 1; document.getElementById('activeCredential').value = currentSettings.active_credential_id || ''; document.getElementById('aiPromptPrefix').value = currentSettings.prompt_prefix || 'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n'; } function shorten(text, maxLength = 80) { if (typeof text !== 'string') { return ''; } if (text.length <= maxLength) { return text; } return `${text.slice(0, maxLength - 3)}...`; } function escapeHtmlAttr(text) { return escapeHtml(text || '').replace(/"/g, '"'); } function formatTimeLabel(iso) { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } function formatRelativePast(iso) { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; const diffMs = Date.now() - date.getTime(); if (diffMs < 0) return 'gerade eben'; const diffMinutes = Math.round(diffMs / 60000); if (diffMinutes <= 1) return 'gerade eben'; if (diffMinutes < 60) return `vor ${diffMinutes} Min`; const diffHours = Math.round(diffMinutes / 60); if (diffHours < 24) return `vor ${diffHours} Std`; const diffDays = Math.round(diffHours / 24); if (diffDays === 1) return 'gestern'; if (diffDays < 7) return `vor ${diffDays} Tagen`; return date.toLocaleDateString('de-DE'); } function formatRelativeFuture(iso) { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; const diffMs = date.getTime() - Date.now(); if (diffMs <= 0) return 'gleich'; const diffMinutes = Math.round(diffMs / 60000); if (diffMinutes < 1) return 'gleich'; if (diffMinutes < 60) return `in ${diffMinutes} Min`; const diffHours = Math.round(diffMinutes / 60); if (diffHours < 24) return `in ${diffHours} Std`; return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } function buildCredentialBadges(credential) { const badges = []; if (!credential.is_active) { badges.push({ label: 'Deaktiviert', className: 'status-badge--muted', title: 'Dieser Login ist derzeit deaktiviert' }); } else if (credential.auto_disabled) { const untilText = credential.auto_disabled_until ? formatRelativeFuture(credential.auto_disabled_until) : 'läuft'; const untilTime = credential.auto_disabled_until ? formatTimeLabel(credential.auto_disabled_until) : ''; const reason = credential.auto_disabled_reason ? credential.auto_disabled_reason.replace(/^AUTO:/, '').trim() : 'Automatisch deaktiviert'; badges.push({ label: `Cooldown ${untilText}${untilTime ? ` (${untilTime})` : ''}`.trim(), className: 'status-badge--warning', title: reason || 'Automatisch deaktiviert' }); } else { badges.push({ label: 'Aktiv', className: 'status-badge--success', title: 'Login ist aktiv' }); } if (credential.last_error_message) { badges.push({ label: `Fehler ${formatRelativePast(credential.last_error_at)}`.trim(), className: 'status-badge--danger', title: credential.last_error_message }); } if (credential.usage_24h_count) { const resetHint = credential.usage_24h_reset_at ? `Reset ${formatRelativeFuture(credential.usage_24h_reset_at)}` : '24h Nutzung'; badges.push({ label: `24h: ${credential.usage_24h_count}`, className: 'status-badge--info', title: resetHint }); } if (credential.last_rate_limit_remaining) { badges.push({ label: `Limit: ${credential.last_rate_limit_remaining}`, className: 'status-badge--neutral', title: 'Letzter „rate limit remaining“-Wert' }); } return badges; } function buildCredentialMetaLines(credential) { const lines = []; if (credential.last_success_at) { lines.push(`Zuletzt erfolgreich: ${formatRelativePast(credential.last_success_at)}`); } if (!credential.last_success_at && credential.last_used_at) { lines.push(`Zuletzt genutzt: ${formatRelativePast(credential.last_used_at)}`); } if (credential.rate_limit_reset_at && !credential.auto_disabled) { lines.push(`Limit-Reset ${formatRelativeFuture(credential.rate_limit_reset_at)}`); } if (credential.latest_event && credential.latest_event.type) { const typeLabel = credential.latest_event.type.replace(/_/g, ' '); const eventTime = credential.latest_event.created_at ? formatRelativePast(credential.latest_event.created_at) : ''; const message = credential.latest_event.message ? shorten(credential.latest_event.message, 90) : ''; const parts = [`Letztes Event (${typeLabel})`]; if (eventTime) parts.push(eventTime); if (message) parts.push(`– ${message}`); lines.push(parts.join(' ')); } return lines; } function renderCredentials() { const list = document.getElementById('credentialsList'); if (!credentials.length) { list.innerHTML = '

Noch keine Anmeldedaten gespeichert

'; return; } list.innerHTML = credentials.map((c, index) => { const providerName = escapeHtml(PROVIDER_INFO[c.provider]?.name || c.provider); const modelLabel = c.model ? ` · ${escapeHtml(c.model)}` : ''; const endpointLabel = c.base_url ? ` · ${escapeHtml(c.base_url)}` : ''; const badges = buildCredentialBadges(c); const badgesHtml = badges.length ? `
${badges.map(badge => `${escapeHtml(badge.label)}`).join('')}
` : ''; const metaLines = buildCredentialMetaLines(c); const metaHtml = metaLines.length ? `
${metaLines.map(line => `
${escapeHtml(line)}
`).join('')}
` : ''; return `
⋮⋮
`; }).join(''); // Add drag and drop event listeners setupDragAndDrop(); } function updateActiveCredentialSelect() { const select = document.getElementById('activeCredential'); select.innerHTML = '' + credentials.map(c => ``).join(''); if (currentSettings?.active_credential_id) { select.value = currentSettings.active_credential_id; } } function updateModelOptions(provider) { const modelInput = document.getElementById('credentialModel'); const modelList = document.getElementById('credentialModelOptions'); const apiKeyInput = document.getElementById('credentialApiKey'); const baseUrlGroup = document.getElementById('credentialBaseUrlGroup'); const baseUrlHelp = document.getElementById('credentialBaseUrlHelp'); const baseUrlInput = document.getElementById('credentialBaseUrl'); const info = PROVIDER_INFO[provider]; const models = PROVIDER_MODELS[provider] || []; if (modelList) { modelList.innerHTML = models.map(m => ``).join(''); } if (modelInput) { const firstSuggestion = models.find(m => m.value)?.value; modelInput.placeholder = firstSuggestion ? `z.B. ${firstSuggestion}` : 'Modell-ID (z.B. llama3.1)'; } const help = document.getElementById('credentialApiKeyHelp'); if (help) { if (info) { const parts = []; if (info.apiKeyHelp) { parts.push(info.apiKeyHelp); } if (info.apiKeyLink) { parts.push(`API-Schlüssel erstellen`); } help.innerHTML = parts.join(' '); } else { help.textContent = ''; } } if (apiKeyInput) { if (provider === 'openai') { apiKeyInput.placeholder = 'sk-... oder leer für lokale Server'; } else { apiKeyInput.placeholder = 'API-Schlüssel'; } } if (baseUrlGroup && baseUrlHelp) { if (provider === 'openai') { baseUrlGroup.style.display = 'block'; baseUrlHelp.textContent = 'Leer lassen für die offizielle OpenAI-API. Für lokale OpenAI/Ollama-Server gib die Basis-URL an, z.B. http://localhost:11434/v1'; if (baseUrlInput) { baseUrlInput.placeholder = 'https://api.openai.com/v1 oder http://localhost:11434/v1'; } } else { baseUrlGroup.style.display = 'none'; baseUrlHelp.textContent = ''; if (baseUrlInput) { baseUrlInput.placeholder = ''; } } } const modelHelp = document.getElementById('credentialModelHelp'); if (modelHelp) { modelHelp.textContent = 'Trage die Modell-ID ein. Du kannst einen Vorschlag auswählen oder einen eigenen Wert eingeben.'; } } function openCredentialModal(credential = null) { const modal = document.getElementById('credentialModal'); const form = document.getElementById('credentialForm'); const apiKeyInput = document.getElementById('credentialApiKey'); const baseUrlInput = document.getElementById('credentialBaseUrl'); if (credential) { document.getElementById('credentialModalTitle').textContent = 'Anmeldedaten bearbeiten'; document.getElementById('credentialId').value = credential.id; document.getElementById('credentialName').value = credential.name; document.getElementById('credentialProvider').value = credential.provider; updateModelOptions(credential.provider); document.getElementById('credentialModel').value = credential.model || ''; if (baseUrlInput) { baseUrlInput.value = credential.base_url || ''; } if (apiKeyInput) { apiKeyInput.value = ''; apiKeyInput.placeholder = credential.provider === 'openai' ? 'Leer lassen, um den bestehenden Schlüssel zu behalten' : 'Leer lassen, um den bestehenden Schlüssel zu behalten'; } } else { document.getElementById('credentialModalTitle').textContent = 'Anmeldedaten hinzufügen'; form.reset(); updateModelOptions('gemini'); document.getElementById('credentialId').value = ''; if (apiKeyInput) { apiKeyInput.value = ''; apiKeyInput.placeholder = 'API-Schlüssel'; } if (baseUrlInput) { baseUrlInput.value = ''; } } modal.removeAttribute('hidden'); } function closeCredentialModal() { document.getElementById('credentialModal').setAttribute('hidden', ''); } async function saveCredential(e) { e.preventDefault(); try { const id = document.getElementById('credentialId').value; const name = document.getElementById('credentialName').value.trim(); const provider = document.getElementById('credentialProvider').value; const apiKey = document.getElementById('credentialApiKey').value.trim(); const model = document.getElementById('credentialModel').value.trim(); const baseUrlRaw = document.getElementById('credentialBaseUrl')?.value.trim() || ''; if (!name) { throw new Error('Bitte einen Namen angeben'); } const data = { name, provider, model: model || null, base_url: provider === 'openai' ? baseUrlRaw : '' }; if (!id) { if (!apiKey && !(provider === 'openai' && baseUrlRaw)) { throw new Error('API-Schlüssel ist erforderlich (oder Basis-URL für lokale OpenAI-kompatible Server angeben)'); } data.api_key = apiKey; } else if (apiKey) { data.api_key = apiKey; } const url = id ? `${API_URL}/ai-credentials/${id}` : `${API_URL}/ai-credentials`; const method = id ? 'PUT' : 'POST'; const res = await apiFetch(url, { method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern der Anmeldedaten'); } await loadCredentials(); closeCredentialModal(); showSuccess('✅ Anmeldedaten erfolgreich gespeichert'); } catch (err) { showError('❌ ' + err.message); } } async function editCredential(id) { const cred = credentials.find(c => c.id === id); if (!cred) { showError('Anmeldedaten nicht gefunden'); return; } openCredentialModal(cred); } async function toggleCredentialActive(id, isActive) { try { const res = await apiFetch(`${API_URL}/ai-credentials/${id}`, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ is_active: isActive ? 1 : 0 }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Aktualisieren'); } await loadCredentials(); showSuccess(`✅ Login ${isActive ? 'aktiviert' : 'deaktiviert'}`); } catch (err) { showError('❌ ' + err.message); await loadCredentials(); // Reload to reset checkbox } } async function deleteCredential(id) { if (!confirm('Wirklich löschen?')) return; const res = await apiFetch(`${API_URL}/ai-credentials/${id}`, {method: 'DELETE'}); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to delete'); } await loadCredentials(); showSuccess('Anmeldedaten gelöscht'); } // Make function globally accessible window.toggleCredentialActive = toggleCredentialActive; // ============================================================================ // DRAG AND DROP // ============================================================================ let draggedElement = null; function setupDragAndDrop() { const items = document.querySelectorAll('.credential-item'); items.forEach(item => { item.addEventListener('dragstart', handleDragStart); item.addEventListener('dragover', handleDragOver); item.addEventListener('drop', handleDrop); item.addEventListener('dragend', handleDragEnd); item.addEventListener('dragenter', handleDragEnter); item.addEventListener('dragleave', handleDragLeave); }); } function handleDragStart(e) { draggedElement = this; this.style.opacity = '0.4'; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', this.innerHTML); } function handleDragOver(e) { if (e.preventDefault) { e.preventDefault(); } e.dataTransfer.dropEffect = 'move'; return false; } function handleDragEnter(e) { if (this !== draggedElement) { this.classList.add('drag-over'); } } function handleDragLeave(e) { this.classList.remove('drag-over'); } function handleDrop(e) { if (e.stopPropagation) { e.stopPropagation(); } if (draggedElement !== this) { // Get the container const container = this.parentNode; const allItems = [...container.querySelectorAll('.credential-item')]; // Get indices const draggedIndex = allItems.indexOf(draggedElement); const targetIndex = allItems.indexOf(this); // Reorder in DOM if (draggedIndex < targetIndex) { this.parentNode.insertBefore(draggedElement, this.nextSibling); } else { this.parentNode.insertBefore(draggedElement, this); } // Update backend saveCredentialOrder(); } this.classList.remove('drag-over'); return false; } function handleDragEnd(e) { this.style.opacity = '1'; // Remove all drag-over classes document.querySelectorAll('.credential-item').forEach(item => { item.classList.remove('drag-over'); }); } async function saveCredentialOrder() { try { const items = document.querySelectorAll('.credential-item'); const order = Array.from(items).map(item => parseInt(item.dataset.credentialId)); const res = await apiFetch(`${API_URL}/ai-credentials/reorder`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ order }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern der Reihenfolge'); } credentials = await res.json(); showSuccess('✅ Reihenfolge gespeichert'); } catch (err) { showError('❌ ' + err.message); await loadCredentials(); // Reload to restore original order } } async function saveSettings(e) { e.preventDefault(); try { const data = { enabled: document.getElementById('aiEnabled').checked, active_credential_id: parseInt(document.getElementById('activeCredential').value) || null, prompt_prefix: document.getElementById('aiPromptPrefix').value }; const res = await apiFetch(`${API_URL}/ai-settings`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern der Einstellungen'); } currentSettings = await res.json(); showSuccess('✅ Einstellungen erfolgreich gespeichert'); } catch (err) { showError('❌ ' + err.message); } } async function testComment() { const modal = document.getElementById('testModal'); modal.removeAttribute('hidden'); document.getElementById('testResult').style.display = 'none'; document.getElementById('testError').style.display = 'none'; // Load last test data from localStorage const lastTest = localStorage.getItem('lastTestComment'); if (lastTest) { try { const data = JSON.parse(lastTest); document.getElementById('testPostText').value = data.postText || ''; document.getElementById('testProfileNumber').value = data.profileNumber || '1'; } catch (e) { console.error('Failed to load last test comment:', e); } } } async function generateTest() { const text = document.getElementById('testPostText').value; const profileNumber = parseInt(document.getElementById('testProfileNumber').value); if (!text) return; document.getElementById('testLoading').style.display = 'block'; document.getElementById('testResult').style.display = 'none'; document.getElementById('testError').style.display = 'none'; try { const res = await apiFetch(`${API_URL}/ai/generate-comment`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({postText: text, profileNumber}) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed'); } const data = await res.json(); document.getElementById('testComment').textContent = data.comment; document.getElementById('testResult').style.display = 'block'; // Save test data to localStorage localStorage.setItem('lastTestComment', JSON.stringify({ postText: text, profileNumber: profileNumber, comment: data.comment, timestamp: new Date().toISOString() })); } catch (err) { document.getElementById('testError').textContent = err.message; document.getElementById('testError').style.display = 'block'; } finally { document.getElementById('testLoading').style.display = 'none'; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ============================================================================ // PROFILE FRIENDS // ============================================================================ let profileFriends = {}; async function loadProfileFriends() { const list = document.getElementById('profileFriendsList'); list.innerHTML = ''; for (let i = 1; i <= 5; i++) { const res = await apiFetch(`${API_URL}/profile-friends/${i}`); const data = await res.json(); profileFriends[i] = data.friend_names || ''; const div = document.createElement('div'); div.className = 'form-group'; div.innerHTML = `

Kommagetrennte Liste von Freundesnamen für Profil ${i}

`; list.appendChild(div); document.getElementById(`friends${i}`).addEventListener('blur', async (e) => { const newValue = e.target.value.trim(); if (newValue !== profileFriends[i]) { await saveFriends(i, newValue); } }); } } async function saveFriends(profileNumber, friendNames) { try { const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ friend_names: friendNames }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern'); } profileFriends[profileNumber] = friendNames; showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`); } catch (err) { showError('❌ ' + err.message); } } // Event listeners document.getElementById('addCredentialBtn').addEventListener('click', () => openCredentialModal()); document.getElementById('credentialModalClose').addEventListener('click', closeCredentialModal); document.getElementById('credentialCancelBtn').addEventListener('click', closeCredentialModal); document.getElementById('credentialForm').addEventListener('submit', saveCredential); document.getElementById('credentialProvider').addEventListener('change', e => updateModelOptions(e.target.value)); document.getElementById('aiSettingsForm').addEventListener('submit', (e) => { e.preventDefault(); saveSettings(e); }); document.getElementById('saveAllBtn').addEventListener('click', (e) => { e.preventDefault(); saveSettings(e); }); document.getElementById('testBtn').addEventListener('click', testComment); document.getElementById('testModalClose').addEventListener('click', () => document.getElementById('testModal').setAttribute('hidden', '')); document.getElementById('generateTestComment').addEventListener('click', generateTest); // Initialize Promise.all([loadCredentials(), loadSettings(), loadProfileFriends()]).catch(err => showError(err.message)); })();