feat: Beschreibung generieren Button im Neuer-Fall-Modal
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
Dieser Commit ist enthalten in:
@@ -68,6 +68,12 @@ class IncidentUpdate(BaseModel):
|
|||||||
visibility: Optional[str] = Field(default=None, pattern="^(public|private)$")
|
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):
|
class IncidentResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
title: str
|
title: str
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Incidents-Router: Lagen verwalten (Multi-Tenant)."""
|
"""Incidents-Router: Lagen verwalten (Multi-Tenant)."""
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
||||||
from fastapi.responses import StreamingResponse
|
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 auth import get_current_user
|
||||||
from middleware.license_check import require_writable_license
|
from middleware.license_check import require_writable_license
|
||||||
from database import db_dependency, get_db
|
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)
|
@router.get("/{incident_id}", response_model=IncidentResponse)
|
||||||
async def get_incident(
|
async def get_incident(
|
||||||
incident_id: int,
|
incident_id: int,
|
||||||
|
|||||||
@@ -2076,6 +2076,26 @@ a:hover {
|
|||||||
margin-top: var(--sp-xs);
|
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 === */
|
/* === Inline-Zitate === */
|
||||||
.citation {
|
.citation {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|||||||
@@ -335,6 +335,13 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-description">Beschreibung / Kontext</label>
|
<label for="inc-description">Beschreibung / Kontext</label>
|
||||||
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)"></textarea>
|
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)"></textarea>
|
||||||
|
<div class="description-enhance-row">
|
||||||
|
<button type="button" class="btn btn-secondary btn-small" id="btn-enhance-description" onclick="App.generateDescription()" disabled>
|
||||||
|
<span id="enhance-btn-text">✦ Beschreibung generieren</span>
|
||||||
|
<span id="enhance-spinner" class="spinner-inline" style="display:none;"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint" id="description-hint">Beschreibe den Vorfall möglichst genau: Was ist passiert? Wo? Wer ist beteiligt? Je präziser, desto bessere Ergebnisse.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-type">Art der Lage</label>
|
<label for="inc-type">Art der Lage</label>
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ const API = {
|
|||||||
return this._request('GET', `/incidents${query}`);
|
return this._request('GET', `/incidents${query}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enhanceDescription(title, description, type) {
|
||||||
|
return this._request('POST', '/incidents/enhance-description', { title, description, type });
|
||||||
|
},
|
||||||
|
|
||||||
createIncident(data) {
|
createIncident(data) {
|
||||||
return this._request('POST', '/incidents', data);
|
return this._request('POST', '/incidents', data);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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() {
|
async handleRefresh() {
|
||||||
if (!this.currentIncidentId) return;
|
if (!this.currentIncidentId) return;
|
||||||
if (this._refreshingIncidents.has(this.currentIncidentId)) {
|
if (this._refreshingIncidents.has(this.currentIncidentId)) {
|
||||||
@@ -3214,6 +3239,14 @@ function toggleTypeDefaults() {
|
|||||||
} else {
|
} else {
|
||||||
hint.textContent = 'Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.';
|
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
|
// 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());
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren