Promote develop → main (2026-05-03 13:47 UTC) #16
@@ -95,10 +95,15 @@ WICHTIG:
|
|||||||
- Wenn der Artikel schon auf {lang_label} ist (z.B. source_lang="{output_lang}"),
|
- Wenn der Artikel schon auf {lang_label} ist (z.B. source_lang="{output_lang}"),
|
||||||
kopiere headline und content unveraendert.
|
kopiere headline und content unveraendert.
|
||||||
|
|
||||||
Antworte AUSSCHLIESSLICH als JSON-Array - eine Liste von Objekten in der Form:
|
Antworte AUSSCHLIESSLICH mit einem flachen JSON-Array (kein Wrapper-Objekt!).
|
||||||
[{{"id": <int>, "headline_de": "<uebersetzter Titel>", "content_de": "<uebersetzter Text>"}}, ...]
|
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:
|
ARTIKEL:
|
||||||
{json.dumps(items, ensure_ascii=False, indent=2)}
|
{json.dumps(items, ensure_ascii=False, indent=2)}
|
||||||
@@ -134,6 +139,19 @@ def _parse_response(text: str) -> list[dict]:
|
|||||||
else:
|
else:
|
||||||
data = _extract_complete_objects(text)
|
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):
|
if not isinstance(data, list):
|
||||||
raise ValueError(f"Translator-Antwort ist kein Array: {type(data).__name__}")
|
raise ValueError(f"Translator-Antwort ist kein Array: {type(data).__name__}")
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async def require_writable_license(
|
|||||||
if lic.get("read_only"):
|
if lic.get("read_only"):
|
||||||
reason = lic.get("read_only_reason") or "expired"
|
reason = lic.get("read_only_reason") or "expired"
|
||||||
if reason == "budget_exceeded":
|
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":
|
elif reason == "expired":
|
||||||
detail = "Lizenz abgelaufen. Nur Lesezugriff moeglich."
|
detail = "Lizenz abgelaufen. Nur Lesezugriff moeglich."
|
||||||
elif reason == "no_license":
|
elif reason == "no_license":
|
||||||
|
|||||||
@@ -549,6 +549,31 @@ a:hover {
|
|||||||
font-weight: 500;
|
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 {
|
.header-license-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -72,6 +72,11 @@
|
|||||||
<span class="credits-percent" id="credits-percent"></span>
|
<span class="credits-percent" id="credits-percent"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="credits-divider"></div>
|
||||||
|
<button class="header-dropdown-action" type="button" onclick="AIDisclaimer && AIDisclaimer.show()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||||
|
<span>Über KI-Inhalte</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-license-warning" id="header-license-warning"></div>
|
<div class="header-license-warning" id="header-license-warning"></div>
|
||||||
@@ -738,5 +743,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/update-system.js"></script>
|
<script src="/static/js/update-system.js"></script>
|
||||||
|
<script src="/static/js/ai-disclaimer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
195
src/static/js/ai-disclaimer.js
Normale Datei
195
src/static/js/ai-disclaimer.js
Normale Datei
@@ -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: '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" '
|
||||||
|
+ 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
|
||||||
|
+ 'stroke-linecap="round" stroke-linejoin="round">'
|
||||||
|
+ '<circle cx="12" cy="12" r="10"/>'
|
||||||
|
+ '<path d="M12 16v-4"/><path d="M12 8h.01"/></svg>'
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = '<strong>KI-Modelle können Fehler machen</strong> '
|
||||||
|
+ '(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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -523,7 +523,7 @@ const App = {
|
|||||||
let text = 'Nur Lesezugriff';
|
let text = 'Nur Lesezugriff';
|
||||||
const reason = user.read_only_reason;
|
const reason = user.read_only_reason;
|
||||||
if (reason === 'budget_exceeded') {
|
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') {
|
} else if (reason === 'expired') {
|
||||||
text = 'Lizenz abgelaufen – nur Lesezugriff';
|
text = 'Lizenz abgelaufen – nur Lesezugriff';
|
||||||
} else if (reason === 'no_license') {
|
} else if (reason === 'no_license') {
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren