/** * Netzwerkanalyse-Erweiterungen für App-Objekt. * Wird nach app.js geladen und erweitert App um Netzwerk-Funktionalität. */ // State-Erweiterung App.networkAnalyses = []; App.currentNetworkId = null; App._networkGenerating = new Set(); /** * Netzwerkanalysen laden und Sidebar rendern. */ App.loadNetworkAnalyses = async function() { try { this.networkAnalyses = await API.listNetworkAnalyses(); } catch (e) { console.warn('Netzwerkanalysen laden fehlgeschlagen:', e); this.networkAnalyses = []; } this.renderNetworkSidebar(); }; /** * Netzwerkanalysen-Sektion in der Sidebar rendern. */ App.renderNetworkSidebar = function() { var container = document.getElementById('network-analyses-list'); if (!container) return; var countEl = document.getElementById('count-network-analyses'); if (countEl) countEl.textContent = '(' + this.networkAnalyses.length + ')'; if (this.networkAnalyses.length === 0) { container.innerHTML = '
Keine Netzwerkanalysen
'; return; } var self = this; container.innerHTML = this.networkAnalyses.map(function(na) { var isActive = na.id === self.currentNetworkId; var statusClass = na.status === 'generating' ? 'generating' : (na.status === 'error' ? 'error' : 'ready'); var countText = na.status === 'ready' ? (na.entity_count + ' / ' + na.relation_count) : na.status === 'generating' ? '...' : ''; return ''; }).join(''); }; /** * Netzwerkanalyse auswählen und anzeigen. */ App.selectNetworkAnalysis = async function(id) { this.currentNetworkId = id; this.currentIncidentId = null; localStorage.removeItem('selectedIncidentId'); localStorage.setItem('selectedNetworkId', id); // Views umschalten document.getElementById('empty-state').style.display = 'none'; document.getElementById('incident-view').style.display = 'none'; document.getElementById('network-view').style.display = 'flex'; // Sidebar aktualisieren this.renderSidebar(); this.renderNetworkSidebar(); // Analyse laden try { var analysis = await API.getNetworkAnalysis(id); this._renderNetworkHeader(analysis); if (analysis.status === 'ready') { this._hideNetworkProgress(); var graphData = await API.getNetworkGraph(id); NetworkGraph.init('network-graph-area', graphData); this._setupNetworkFilters(graphData); // Update-Check try { var updateCheck = await API.checkNetworkUpdate(id); var badge = document.getElementById('network-update-badge'); if (badge) badge.style.display = updateCheck.has_update ? 'inline-flex' : 'none'; } catch (e) { /* ignorieren */ } } else if (analysis.status === 'generating') { this._showNetworkProgress('entity_extraction', 0); } else if (analysis.status === 'error') { this._hideNetworkProgress(); var graphArea = document.getElementById('network-graph-area'); if (graphArea) graphArea.innerHTML = '
Fehler bei der Generierung. Versuche es erneut.
'; } } catch (err) { UI.showToast('Fehler beim Laden der Netzwerkanalyse: ' + err.message, 'error'); } }; /** * Netzwerkanalyse-Header rendern. */ App._renderNetworkHeader = function(analysis) { var el; el = document.getElementById('network-title'); if (el) el.textContent = analysis.name; el = document.getElementById('network-entity-count'); if (el) el.textContent = analysis.entity_count + ' Entitäten'; el = document.getElementById('network-relation-count'); if (el) el.textContent = analysis.relation_count + ' Beziehungen'; el = document.getElementById('network-incident-list-text'); if (el) el.textContent = (analysis.incident_titles || []).join(', ') || '-'; el = document.getElementById('network-last-generated'); if (el) { if (analysis.last_generated_at) { var d = parseUTC(analysis.last_generated_at) || new Date(analysis.last_generated_at); el.textContent = 'Generiert: ' + d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE }) + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }); } else { el.textContent = ''; } } }; /** * Filter-Controls in der Netzwerk-Sidebar aufsetzen. */ App._setupNetworkFilters = function(graphData) { // Typ-Filter-Buttons aktivieren var types = new Set(); (graphData.entities || []).forEach(function(e) { types.add(e.entity_type); }); var filterContainer = document.getElementById('network-type-filter-container'); if (filterContainer) { var allTypes = ['person', 'organisation', 'location', 'event', 'military']; var typeLabels = { person: 'Person', organisation: 'Organisation', location: 'Ort', event: 'Ereignis', military: 'Militär' }; filterContainer.innerHTML = allTypes.map(function(t) { var hasEntities = types.has(t); return ''; }).join(''); } // Gewicht-Slider var slider = document.getElementById('network-weight-slider'); if (slider) { slider.value = 1; slider.oninput = function() { var label = document.getElementById('network-weight-value'); if (label) label.textContent = this.value; NetworkGraph.filterByWeight(parseInt(this.value)); }; } // Suche var searchInput = document.getElementById('network-search'); if (searchInput) { searchInput.value = ''; var timer = null; searchInput.oninput = function() { clearTimeout(timer); var val = this.value; timer = setTimeout(function() { NetworkGraph.search(val); }, 250); }; } }; /** * Typ-Filter toggle. */ App.toggleNetworkTypeFilter = function(btn) { btn.classList.toggle('active'); var activeTypes = []; document.querySelectorAll('.network-type-filter.active').forEach(function(b) { activeTypes.push(b.dataset.type); }); NetworkGraph.filterByType(new Set(activeTypes)); }; /** * Progress-Bar anzeigen. */ App._showNetworkProgress = function(phase, progress) { var bar = document.getElementById('network-progress-bar'); if (bar) bar.style.display = 'block'; var steps = ['entity_extraction', 'relationship_extraction', 'correction']; var stepEls = document.querySelectorAll('.network-progress-step'); var connectorEls = document.querySelectorAll('.network-progress-connector'); var phaseIndex = steps.indexOf(phase); stepEls.forEach(function(el, i) { el.classList.remove('active', 'done'); if (i < phaseIndex) el.classList.add('done'); else if (i === phaseIndex) el.classList.add('active'); }); connectorEls.forEach(function(el, i) { el.classList.remove('done'); if (i < phaseIndex) el.classList.add('done'); }); var fill = document.getElementById('network-progress-fill'); if (fill) { var pct = ((phaseIndex / steps.length) * 100) + (progress || 0) * (100 / steps.length) / 100; fill.style.width = Math.min(100, pct) + '%'; } var label = document.getElementById('network-progress-label'); if (label) { var labels = { entity_extraction: 'Entitäten werden extrahiert...', relationship_extraction: 'Beziehungen werden analysiert...', correction: 'Korrekturen werden angewendet...' }; label.textContent = labels[phase] || 'Wird verarbeitet...'; } }; App._hideNetworkProgress = function() { var bar = document.getElementById('network-progress-bar'); if (bar) bar.style.display = 'none'; }; /** * Modal: Neue Netzwerkanalyse öffnen. */ App.openNetworkModal = async function() { var list = document.getElementById('network-incident-options'); if (list) list.innerHTML = '
Lade Lagen...
'; openModal('modal-network-new'); // Lagen laden try { var incidents = await API.listIncidents(); // Sortierung: zuerst Live (adhoc) alphabetisch, dann Analyse (research) alphabetisch incidents.sort(function(a, b) { var typeA = (a.type === 'research') ? 1 : 0; var typeB = (b.type === 'research') ? 1 : 0; if (typeA !== typeB) return typeA - typeB; return (a.title || '').localeCompare(b.title || '', 'de'); }); if (list) { list.innerHTML = incidents.map(function(inc) { var typeLabel = inc.type === 'research' ? 'Analyse' : 'Live'; return ''; }).join(''); } } catch (e) { if (list) list.innerHTML = '
Fehler beim Laden der Lagen
'; } // Name-Feld leeren var nameField = document.getElementById('network-name'); if (nameField) nameField.value = ''; // Suchfeld leeren var searchField = document.getElementById('network-incident-search'); if (searchField) { searchField.value = ''; searchField.oninput = function() { var term = this.value.toLowerCase(); document.querySelectorAll('.network-incident-option').forEach(function(opt) { var text = opt.textContent.toLowerCase(); opt.style.display = text.includes(term) ? '' : 'none'; }); }; } }; /** * Netzwerkanalyse erstellen. */ App.submitNetworkAnalysis = async function(e) { if (e) e.preventDefault(); var name = (document.getElementById('network-name').value || '').trim(); if (!name) { UI.showToast('Bitte einen Namen eingeben.', 'warning'); return; } var incidentIds = []; document.querySelectorAll('.network-incident-cb:checked').forEach(function(cb) { incidentIds.push(parseInt(cb.value)); }); if (incidentIds.length === 0) { UI.showToast('Bitte mindestens eine Lage auswählen.', 'warning'); return; } var btn = document.getElementById('network-submit-btn'); if (btn) btn.disabled = true; try { var result = await API.createNetworkAnalysis({ name: name, incident_ids: incidentIds }); closeModal('modal-network-new'); await this.loadNetworkAnalyses(); await this.selectNetworkAnalysis(result.id); UI.showToast('Netzwerkanalyse gestartet.', 'success'); } catch (err) { UI.showToast('Fehler: ' + err.message, 'error'); } finally { if (btn) btn.disabled = false; } }; /** * Netzwerkanalyse neu generieren. */ App.regenerateNetwork = async function() { if (!this.currentNetworkId) return; if (!await confirmDialog('Netzwerkanalyse neu generieren? Bestehende Daten werden überschrieben.')) return; try { await API.regenerateNetwork(this.currentNetworkId); this._showNetworkProgress('entity_extraction', 0); await this.loadNetworkAnalyses(); UI.showToast('Neugenerierung gestartet.', 'success'); } catch (err) { UI.showToast('Fehler: ' + err.message, 'error'); } }; /** * Netzwerkanalyse löschen. */ App.deleteNetworkAnalysis = async function() { if (!this.currentNetworkId) return; if (!await confirmDialog('Netzwerkanalyse wirklich löschen? Alle Daten gehen verloren.')) return; try { await API.deleteNetworkAnalysis(this.currentNetworkId); this.currentNetworkId = null; localStorage.removeItem('selectedNetworkId'); NetworkGraph.destroy(); document.getElementById('network-view').style.display = 'none'; document.getElementById('empty-state').style.display = 'flex'; await this.loadNetworkAnalyses(); UI.showToast('Netzwerkanalyse gelöscht.', 'success'); } catch (err) { UI.showToast('Fehler: ' + err.message, 'error'); } }; /** * Netzwerkanalyse exportieren. */ App.exportNetwork = async function(format) { if (!this.currentNetworkId) return; if (format === 'png') { NetworkGraph.exportPNG(); return; } try { var resp = await API.exportNetworkAnalysis(this.currentNetworkId, format); if (!resp.ok) throw new Error('Export fehlgeschlagen'); var blob = await resp.blob(); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'netzwerk-' + this.currentNetworkId + '.' + format; a.click(); URL.revokeObjectURL(url); } catch (err) { UI.showToast('Export fehlgeschlagen: ' + err.message, 'error'); } }; /** * WebSocket-Handler für Netzwerk-Events. */ App._handleNetworkStatus = function(msg) { if (msg.analysis_id === this.currentNetworkId) { this._showNetworkProgress(msg.phase, msg.progress || 0); } }; App._handleNetworkComplete = async function(msg) { this._networkGenerating.delete(msg.analysis_id); if (msg.analysis_id === this.currentNetworkId) { this._hideNetworkProgress(); // Graph neu laden try { var graphData = await API.getNetworkGraph(msg.analysis_id); NetworkGraph.init('network-graph-area', graphData); this._setupNetworkFilters(graphData); var analysis = await API.getNetworkAnalysis(msg.analysis_id); this._renderNetworkHeader(analysis); } catch (e) { console.error('Graph nach Generierung laden fehlgeschlagen:', e); } UI.showToast('Netzwerkanalyse fertig: ' + (msg.entity_count || 0) + ' Entitäten, ' + (msg.relation_count || 0) + ' Beziehungen', 'success'); } await this.loadNetworkAnalyses(); }; App._handleNetworkError = function(msg) { this._networkGenerating.delete(msg.analysis_id); if (msg.analysis_id === this.currentNetworkId) { this._hideNetworkProgress(); var graphArea = document.getElementById('network-graph-area'); if (graphArea) graphArea.innerHTML = '
Fehler: ' + _escHtml(msg.error || 'Unbekannter Fehler') + '
'; } UI.showToast('Netzwerkanalyse fehlgeschlagen: ' + (msg.error || 'Unbekannter Fehler'), 'error'); this.loadNetworkAnalyses(); }; /** * Cluster isolieren (nur verbundene Knoten zeigen). */ App.isolateNetworkCluster = function() { if (NetworkGraph._selectedNode) { NetworkGraph.isolateCluster(NetworkGraph._selectedNode.id); } }; /** * Graph-Ansicht zurücksetzen. */ App.resetNetworkView = function() { NetworkGraph.resetView(); // Typ-Filter zurücksetzen document.querySelectorAll('.network-type-filter').forEach(function(btn) { if (!btn.disabled) btn.classList.add('active'); }); var slider = document.getElementById('network-weight-slider'); if (slider) { slider.value = 1; var lbl = document.getElementById('network-weight-value'); if (lbl) lbl.textContent = '1'; } var search = document.getElementById('network-search'); if (search) search.value = ''; }; // HTML-Escape Hilfsfunktion (falls nicht global verfügbar) function _escHtml(text) { if (typeof UI !== 'undefined' && UI.escape) return UI.escape(text); var d = document.createElement('div'); d.textContent = text || ''; return d.innerHTML; }