From 6913c1e683a05d7f6480dabc4a758af3d2e7d8c4 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Fri, 27 Mar 2026 23:31:05 +0100 Subject: [PATCH] feat: Beschreibung generieren Button im Neuer-Fall-Modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KI-gestütztes Prompt Enhancement: Button generiert per Haiku aus dem Titel eine strukturierte Beschreibung. Unterscheidet zwischen Live-Monitoring (kompakte Vorfallsbeschreibung) und Recherche (strukturiertes Briefing mit Schwerpunkten und Suchbegriffen). - Neuer Endpoint POST /api/incidents/enhance-description - Button erscheint für beide Lage-Typen, aktiv ab 3 Zeichen Titel - Info-Hinweis wechselt je nach Typ mit Beispiel - Spinner-Animation während der Generierung --- src/models.py | 6 ++++ src/routers/incidents.py | 61 ++++++++++++++++++++++++++++++++++++++- src/static/css/style.css | 20 +++++++++++++ src/static/dashboard.html | 7 +++++ src/static/js/api.js | 4 +++ src/static/js/app.js | 45 +++++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 1 deletion(-) diff --git a/src/models.py b/src/models.py index 2766984..d162f11 100644 --- a/src/models.py +++ b/src/models.py @@ -68,6 +68,12 @@ class IncidentUpdate(BaseModel): visibility: Optional[str] = Field(default=None, pattern="^(public|private)$") +class DescriptionEnhanceRequest(BaseModel): + title: str = Field(min_length=3) + description: str | None = None + type: str = Field(default="adhoc", pattern="^(adhoc|research)$") + + class IncidentResponse(BaseModel): id: int title: str diff --git a/src/routers/incidents.py b/src/routers/incidents.py index 8b2d8b6..9e00790 100644 --- a/src/routers/incidents.py +++ b/src/routers/incidents.py @@ -1,7 +1,7 @@ """Incidents-Router: Lagen verwalten (Multi-Tenant).""" from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status from fastapi.responses import StreamingResponse -from models import IncidentCreate, IncidentUpdate, IncidentResponse, SubscriptionUpdate, SubscriptionResponse +from models import IncidentCreate, IncidentUpdate, IncidentResponse, SubscriptionUpdate, SubscriptionResponse, DescriptionEnhanceRequest from auth import get_current_user from middleware.license_check import require_writable_license from database import db_dependency, get_db @@ -155,6 +155,65 @@ async def get_refreshing_incidents( } +# --- Beschreibung generieren (Prompt Enhancement) --- + +ENHANCE_PROMPT_RESEARCH = """Du generierst ein strukturiertes Recherche-Briefing fuer ein OSINT-Lagemonitoring-System. +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ae, oe, ue, ss) und KEINE Umschreibungen. + +Titel: {title} +Vorhandener Kontext: {context} +Typ: Hintergrundrecherche + +Erstelle ein praezises Recherche-Briefing mit: +1. Vollstaendiger Name/Bezeichnung des Themas (inkl. Rechtsform bei Unternehmen, voller Name bei Personen) +2. Recherche-Schwerpunkte (5-8 thematische Punkte, z.B. Geschichte, Finanzen, Fuehrung, Kontroversen, Innovation) +3. Relevante Suchbegriffe (deutsch + englisch, inkl. Abkuerzungen und alternative Schreibweisen) + +Schreibe NUR das Briefing als Fliesstext mit Aufzaehlungen. Keine Erklaerungen davor oder danach.""" + +ENHANCE_PROMPT_ADHOC = """Du generierst eine praezise Vorfallsbeschreibung fuer ein OSINT-Lagemonitoring-System. +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ae, oe, ue, ss) und KEINE Umschreibungen. + +Titel: {title} +Vorhandener Kontext: {context} +Typ: Live-Monitoring (aktuelle Ereignisse) + +Erstelle eine knappe, informative Beschreibung mit: +1. Was ist passiert / worum geht es +2. Wo (geographischer Kontext) +3. Wer ist beteiligt (Akteure, Organisationen, Laender) +4. Wonach soll gesucht werden (aktuelle Entwicklungen, Reaktionen, Hintergruende) + +Schreibe NUR die Beschreibung als Fliesstext (3-5 Zeilen). Keine Erklaerungen davor oder danach.""" + +_enhance_logger = logging.getLogger("osint.enhance") + + +@router.post("/enhance-description") +async def enhance_description( + data: DescriptionEnhanceRequest, + current_user: dict = Depends(get_current_user), +): + """Generiert eine strukturierte Beschreibung per KI aus dem Titel.""" + from agents.claude_client import call_claude + from config import CLAUDE_MODEL_FAST + + template = ENHANCE_PROMPT_RESEARCH if data.type == "research" else ENHANCE_PROMPT_ADHOC + context = data.description.strip() if data.description and data.description.strip() else "Kein Kontext angegeben" + prompt = template.format(title=data.title.strip(), context=context) + + try: + result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST) + _enhance_logger.info( + f"Beschreibung generiert fuer \"{data.title[:50]}\": " + f"{usage.input_tokens}in/{usage.output_tokens}out" + ) + return {"description": result.strip()} + except Exception as e: + _enhance_logger.error(f"Beschreibung generieren fehlgeschlagen: {e}") + raise HTTPException(status_code=500, detail="Beschreibung konnte nicht generiert werden") + + @router.get("/{incident_id}", response_model=IncidentResponse) async def get_incident( incident_id: int, diff --git a/src/static/css/style.css b/src/static/css/style.css index 86d1c30..034f8da 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -2076,6 +2076,26 @@ a:hover { margin-top: var(--sp-xs); } +.description-enhance-row { + margin-top: var(--sp-md); + display: flex; + align-items: center; +} + +.spinner-inline { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid var(--border); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin-inline 0.8s linear infinite; +} + +@keyframes spin-inline { + to { transform: rotate(360deg); } +} + /* === Inline-Zitate === */ .citation { color: var(--accent); diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 741a033..d7348b5 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -335,6 +335,13 @@
+
+ +
+
Beschreibe den Vorfall möglichst genau: Was ist passiert? Wo? Wer ist beteiligt? Je präziser, desto bessere Ergebnisse.
diff --git a/src/static/js/api.js b/src/static/js/api.js index d7c418a..615fff0 100644 --- a/src/static/js/api.js +++ b/src/static/js/api.js @@ -70,6 +70,10 @@ const API = { return this._request('GET', `/incidents${query}`); }, + enhanceDescription(title, description, type) { + return this._request('POST', '/incidents/enhance-description', { title, description, type }); + }, + createIncident(data) { return this._request('POST', '/incidents', data); }, diff --git a/src/static/js/app.js b/src/static/js/app.js index c4c576f..8e4b49c 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -1610,6 +1610,31 @@ const App = { } }, +async generateDescription() { + const title = document.getElementById('inc-title').value.trim(); + const description = document.getElementById('inc-description').value.trim(); + const type = document.getElementById('inc-type').value; + const btn = document.getElementById('btn-enhance-description'); + const btnText = document.getElementById('enhance-btn-text'); + const spinner = document.getElementById('enhance-spinner'); + + if (title.length < 3 || !btn) return; + btn.disabled = true; + btnText.style.display = 'none'; + spinner.style.display = ''; + + try { + const result = await API.enhanceDescription(title, description || null, type); + document.getElementById('inc-description').value = result.description; + } catch (err) { + UI.showToast('Beschreibung konnte nicht generiert werden', 'error'); + } finally { + btnText.style.display = ''; + spinner.style.display = 'none'; + btn.disabled = title.length < 3; + } + }, + async handleRefresh() { if (!this.currentIncidentId) return; if (this._refreshingIncidents.has(this.currentIncidentId)) { @@ -3214,6 +3239,14 @@ function toggleTypeDefaults() { } else { hint.textContent = 'Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.'; } + + // Beschreibungs-Hinweis je nach Typ wechseln + const descHint = document.getElementById('description-hint'); + if (descHint) { + descHint.textContent = type === 'research' + ? 'Nenne das vollständige Thema, gewünschte Schwerpunkte und relevante URLs. Beispiel: "Muster GmbH \u2014 Fokus auf Führungspersonen, Kontroversen, Finanzkennzahlen"' + : 'Beschreibe den Vorfall möglichst genau: Was ist passiert? Wo? Wer ist beteiligt? Je präziser, desto bessere Ergebnisse.'; + } } // Tab-Fokus: Nur Tab-Badge (Titel-Counter) zurücksetzen, nicht alle Notifications @@ -3294,3 +3327,15 @@ document.addEventListener('click', (e) => { }); document.addEventListener('DOMContentLoaded', () => App.init()); + + +// Titel-Input: Button "Beschreibung generieren" aktivieren/deaktivieren +document.addEventListener('DOMContentLoaded', () => { + const titleInput = document.getElementById('inc-title'); + if (titleInput) { + titleInput.addEventListener('input', function() { + const btn = document.getElementById('btn-enhance-description'); + if (btn) btn.disabled = this.value.trim().length < 3; + }); + } +});