diff --git a/src/agents/translator.py b/src/agents/translator.py index f877f8c..511e523 100644 --- a/src/agents/translator.py +++ b/src/agents/translator.py @@ -95,10 +95,15 @@ WICHTIG: - Wenn der Artikel schon auf {lang_label} ist (z.B. source_lang="{output_lang}"), kopiere headline und content unveraendert. -Antworte AUSSCHLIESSLICH als JSON-Array - eine Liste von Objekten in der Form: -[{{"id": , "headline_de": "", "content_de": ""}}, ...] +Antworte AUSSCHLIESSLICH mit einem flachen JSON-Array (kein Wrapper-Objekt!). +Format genau so: +[ + {{"id": 1, "headline_de": "Titel auf Deutsch", "content_de": "Inhalt auf Deutsch"}}, + {{"id": 2, "headline_de": "...", "content_de": "..."}} +] -Keine Einleitung, keine Erklaerung, nur das JSON-Array. +NICHT erlaubt: {{"translations": [...]}} oder {{"items": [...]}} oder Markdown-Codefences. +Nur das Array, ohne Einleitung, ohne Erklaerung. ARTIKEL: {json.dumps(items, ensure_ascii=False, indent=2)} @@ -134,6 +139,19 @@ def _parse_response(text: str) -> list[dict]: else: data = _extract_complete_objects(text) + # Claude wraps das Array gelegentlich in {"translations": [...]} oder {"items": [...]} + if isinstance(data, dict): + for key in ("translations", "items", "results", "data"): + if isinstance(data.get(key), list): + data = data[key] + break + else: + # Einzelnes Objekt? Dann als Liste mit einem Element behandeln + if "id" in data: + data = [data] + else: + raise ValueError(f"Translator-Antwort: Dict ohne erwarteten Array-Key (keys={list(data.keys())[:5]})") + if not isinstance(data, list): raise ValueError(f"Translator-Antwort ist kein Array: {type(data).__name__}") diff --git a/src/middleware/license_check.py b/src/middleware/license_check.py index 553e75e..f54fe34 100644 --- a/src/middleware/license_check.py +++ b/src/middleware/license_check.py @@ -47,7 +47,7 @@ async def require_writable_license( if lic.get("read_only"): reason = lic.get("read_only_reason") or "expired" if reason == "budget_exceeded": - detail = "Token-Budget aufgebraucht. Bitte Verwaltung kontaktieren." + detail = "Token-Budget aufgebraucht. Für Aufstockung oder Upgrade bitte info@aegis-sight.de kontaktieren." elif reason == "expired": detail = "Lizenz abgelaufen. Nur Lesezugriff moeglich." elif reason == "no_license": diff --git a/src/static/css/style.css b/src/static/css/style.css index bcd177f..f232fac 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -549,6 +549,31 @@ a:hover { font-weight: 500; } +.header-dropdown-action { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + background: transparent; + border: 0; + padding: 8px 12px; + color: var(--text-secondary); + font-size: 12px; + font-family: inherit; + cursor: pointer; + border-radius: 6px; + text-align: left; + transition: background 0.15s ease, color 0.15s ease; +} +.header-dropdown-action:hover { + background: var(--bg-hover, rgba(255, 255, 255, 0.04)); + color: var(--text-primary); +} +.header-dropdown-action svg { + flex-shrink: 0; + color: var(--accent); +} + .header-license-badge { display: inline-block; font-size: 10px; diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 2b4921a..26b9b52 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -72,6 +72,11 @@ +
+
@@ -738,5 +743,6 @@ + diff --git a/src/static/js/ai-disclaimer.js b/src/static/js/ai-disclaimer.js new file mode 100644 index 0000000..d7e07e3 --- /dev/null +++ b/src/static/js/ai-disclaimer.js @@ -0,0 +1,195 @@ +/** + * AI-Hallucination-Disclaimer fuer den AegisSight Monitor. + * + * Zeigt: + * 1) Beim ersten Besuch (oder bei neuem v-Bump) ein Modal mit Hinweisen + * zur Fehlbarkeit von KI-Modellen. + * 2) Im Header-User-Dropdown immer einen Eintrag "Ueber KI-Inhalte", + * ueber den der User das Modal jederzeit erneut oeffnen kann. + * + * Persistenz: + * localStorage 'aegis_ai_disclaimer_seen' -> Versionsstring (z.B. "v1"). + * Wenn die Version sich aendert (Wortlaut-Update), erscheint das Modal + * beim naechsten Login erneut. + */ +(function () { + 'use strict'; + + const STORAGE_KEY = 'aegis_ai_disclaimer_seen'; + const CURRENT_VERSION = 'v1'; + + // ---- DOM-Helpers (analog zu update-system.js) ---- + function el(tag, attrs, ...children) { + const e = document.createElement(tag); + for (const k in (attrs || {})) { + if (k === 'class') e.className = attrs[k]; + else if (k === 'html') e.innerHTML = attrs[k]; + else if (k.startsWith('on')) e.addEventListener(k.slice(2), attrs[k]); + else e.setAttribute(k, attrs[k]); + } + for (const c of children) { + if (c == null) continue; + e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); + } + return e; + } + + function injectStyles() { + if (document.getElementById('aegis-aidisc-styles')) return; + const css = ` + #aegis-aidisc-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 99998; + backdrop-filter: blur(3px); + display: flex; align-items: center; justify-content: center; padding: 24px; + animation: aegis-aidisc-fade 0.25s ease; + } + @keyframes aegis-aidisc-fade { from { opacity: 0; } to { opacity: 1; } } + #aegis-aidisc-modal { + background: var(--bg-card); + color: var(--text-primary); + border-radius: 14px; + border: 1px solid var(--border); + box-shadow: 0 24px 80px rgba(0,0,0,0.4); + font-family: 'Inter', -apple-system, sans-serif; + max-width: 580px; width: 100%; max-height: 85vh; overflow: hidden; + display: flex; flex-direction: column; + } + #aegis-aidisc-modal header { + padding: 22px 28px 18px; border-bottom: 1px solid var(--border); + display: flex; align-items: center; gap: 12px; + } + #aegis-aidisc-modal header svg { color: var(--accent); flex-shrink: 0; } + #aegis-aidisc-modal h2 { margin: 0; color: var(--accent); font-size: 1.25rem; font-weight: 700; } + #aegis-aidisc-modal .body { padding: 18px 28px; overflow-y: auto; line-height: 1.55; } + #aegis-aidisc-modal .body p { margin: 0 0 12px; color: var(--text-primary); font-size: 0.94rem; } + #aegis-aidisc-modal .body strong { color: var(--accent); } + #aegis-aidisc-modal .body ul { margin: 8px 0 14px; padding-left: 22px; } + #aegis-aidisc-modal .body li { margin-bottom: 6px; color: var(--text-secondary); font-size: 0.92rem; } + #aegis-aidisc-modal .footnote { + margin-top: 10px; padding-top: 12px; border-top: 1px solid var(--border); + color: var(--text-tertiary); font-size: 0.82rem; + } + #aegis-aidisc-modal footer { + padding: 14px 28px 20px; border-top: 1px solid var(--border); + display: flex; justify-content: flex-end; gap: 10px; + } + #aegis-aidisc-modal footer button { + background: var(--accent); color: #fff; border: 0; padding: 10px 22px; + border-radius: 6px; font: inherit; font-size: 0.92rem; font-weight: 600; + cursor: pointer; + } + #aegis-aidisc-modal footer button:hover { background: var(--accent-hover); } + #aegis-aidisc-modal footer button.secondary { + background: transparent; color: var(--text-secondary); border: 1px solid var(--border); + } + #aegis-aidisc-modal footer button.secondary:hover { + background: var(--bg-hover, rgba(255,255,255,0.04)); color: var(--text-primary); + }`; + document.head.appendChild(el('style', { id: 'aegis-aidisc-styles', html: css })); + } + + // ---- Modal-Aufbau ---- + function buildModal(opts) { + const isFromUser = !!(opts && opts.fromUserAction); + + // Lucide info-Icon (gleiches Pattern wie .info-icon im Repo) + const headerIcon = el('span', { + html: '' + + '' + + '' + }); + + const body = el('div', { class: 'body' }); + body.appendChild(el('p', null, + 'Der AegisSight Monitor nutzt Künstliche Intelligenz (Claude von Anthropic) ' + + 'zur Analyse, Übersetzung und Zusammenfassung von Nachrichten.')); + + const warn = el('p'); + warn.innerHTML = 'KI-Modelle können Fehler machen ' + + '(sogenannte „Halluzinationen"): erfundene Details, falsche Verbindungen oder ' + + 'ungenaue Zusammenfassungen sind möglich, auch wenn der Text plausibel klingt.'; + body.appendChild(warn); + + body.appendChild(el('p', null, 'Wir empfehlen daher:')); + body.appendChild(el('ul', null, + el('li', null, 'Wichtige Informationen mit den verlinkten Quellen verifizieren'), + el('li', null, 'Bei kritischen Entscheidungen die Originalartikel prüfen'), + el('li', null, 'Faktenchecks als Hinweis verstehen, nicht als endgültige Wahrheit') + )); + + body.appendChild(el('p', { class: 'footnote' }, + 'Diesen Hinweis findest du jederzeit wieder im Menü oben rechts unter „Über KI-Inhalte".')); + + const closeAndStore = () => { + try { localStorage.setItem(STORAGE_KEY, CURRENT_VERSION); } catch (e) {} + overlay.remove(); + document.removeEventListener('keydown', escHandler); + }; + const closeOnly = () => { + overlay.remove(); + document.removeEventListener('keydown', escHandler); + }; + + const footer = el('footer', null); + if (!isFromUser) { + footer.appendChild(el('button', { class: 'secondary', onclick: closeOnly }, 'Später nochmal')); + } + footer.appendChild(el('button', { onclick: closeAndStore }, 'Verstanden')); + + const overlay = el('div', { id: 'aegis-aidisc-overlay' }, + el('div', { id: 'aegis-aidisc-modal' }, + el('header', null, headerIcon, el('h2', null, 'Hinweis zu KI-generierten Inhalten')), + body, + footer + ) + ); + + function escHandler(ev) { + if (ev.key === 'Escape' && document.getElementById('aegis-aidisc-overlay')) { + // ESC = wie "Verstanden" beim erstmaligen Anzeigen, sonst nur schliessen + if (isFromUser) closeOnly(); else closeAndStore(); + } + } + overlay.addEventListener('click', (ev) => { + if (ev.target === overlay) { + if (isFromUser) closeOnly(); else closeAndStore(); + } + }); + document.addEventListener('keydown', escHandler); + + return overlay; + } + + function show(opts) { + if (document.getElementById('aegis-aidisc-overlay')) return; + injectStyles(); + document.body.appendChild(buildModal(opts)); + } + + function init() { + // Nur auf der Dashboard-Seite zeigen, nicht auf der Login-Seite + if (!document.body || document.body.classList.contains('login-page')) return; + + injectStyles(); + let seenVersion = ''; + try { seenVersion = localStorage.getItem(STORAGE_KEY) || ''; } catch (e) {} + if (seenVersion !== CURRENT_VERSION) { + // Etwas verzoegern, damit Hauptdashboard sichtbar ist bevor Modal kommt + setTimeout(() => show({ fromUserAction: false }), 600); + } + } + + // Globaler Zugriff zum manuellen Oeffnen aus dem Header-Dropdown + window.AIDisclaimer = { + show: () => show({ fromUserAction: true }), + VERSION: CURRENT_VERSION, + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/src/static/js/app.js b/src/static/js/app.js index 13f2d64..1dd2e7b 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -523,7 +523,7 @@ const App = { let text = 'Nur Lesezugriff'; const reason = user.read_only_reason; if (reason === 'budget_exceeded') { - text = 'Token-Budget aufgebraucht – nur Lesezugriff. Bitte Verwaltung kontaktieren.'; + text = 'Token-Budget aufgebraucht – nur Lesezugriff. Für Aufstockung oder Upgrade bitte info@aegis-sight.de kontaktieren.'; } else if (reason === 'expired') { text = 'Lizenz abgelaufen – nur Lesezugriff'; } else if (reason === 'no_license') {