Promote develop → main (2026-05-13 22:38 UTC) #25

Zusammengeführt
IntelSight_Admin hat 20 Commits von develop nach main 2026-05-14 00:38:19 +02:00 zusammengeführt
6 geänderte Dateien mit 2188 neuen und 2072 gelöschten Zeilen
Nur Änderungen aus Commit 892af55269 werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -449,8 +449,8 @@
<div class="modal-overlay" id="modal-sources" role="dialog" aria-modal="true" aria-labelledby="modal-sources-title"> <div class="modal-overlay" id="modal-sources" role="dialog" aria-modal="true" aria-labelledby="modal-sources-title">
<div class="modal modal-wide"> <div class="modal modal-wide">
<div class="modal-header"> <div class="modal-header">
<div class="modal-title" id="modal-sources-title">Quellenverwaltung</div> <div class="modal-title" id="modal-sources-title" data-i18n="sources_modal.title">Quellenverwaltung</div>
<button class="modal-close" onclick="closeModal('modal-sources')" aria-label="Schließen">&times;</button> <button class="modal-close" onclick="closeModal('modal-sources')" aria-label="Schließen" data-i18n-attr="aria-label:aria.close">&times;</button>
</div> </div>
<div class="modal-body sources-modal-body"> <div class="modal-body sources-modal-body">
<!-- Stats-Leiste --> <!-- Stats-Leiste -->
@@ -459,17 +459,17 @@
<!-- Toolbar --> <!-- Toolbar -->
<div class="sources-toolbar"> <div class="sources-toolbar">
<div class="sources-filters"> <div class="sources-filters">
<label for="sources-filter-type" class="sr-only">Quellentyp filtern</label> <label for="sources-filter-type" class="sr-only" data-i18n="sources_modal.filter.type">Quellentyp filtern</label>
<select id="sources-filter-type" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-type" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Typen</option> <option value="" data-i18n="sources_modal.filter.type_all">Alle Typen</option>
<option value="rss_feed">RSS-Feed</option> <option value="rss_feed">RSS-Feed</option>
<option value="web_source">Web-Quelle</option> <option value="web_source">Web-Quelle</option>
<option value="telegram_channel">Telegram</option> <option value="telegram_channel">Telegram</option>
<option value="excluded">Von mir ausgeschlossen</option> <option value="excluded">Von mir ausgeschlossen</option>
</select> </select>
<label for="sources-filter-category" class="sr-only">Kategorie filtern</label> <label for="sources-filter-category" class="sr-only" data-i18n="sources_modal.filter.category">Kategorie filtern</label>
<select id="sources-filter-category" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-category" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Kategorien</option> <option value="" data-i18n="sources_modal.filter.category_all">Alle Kategorien</option>
<option value="nachrichtenagentur">Nachrichtenagentur</option> <option value="nachrichtenagentur">Nachrichtenagentur</option>
<option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option> <option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option>
<option value="qualitaetszeitung">Qualitätszeitung</option> <option value="qualitaetszeitung">Qualitätszeitung</option>
@@ -481,9 +481,9 @@
<option value="boulevard">Boulevard</option> <option value="boulevard">Boulevard</option>
<option value="sonstige">Sonstige</option> <option value="sonstige">Sonstige</option>
</select> </select>
<label for="sources-filter-political" class="sr-only">Politische Ausrichtung filtern</label> <label for="sources-filter-political" class="sr-only" data-i18n="sources_modal.filter.political">Politische Ausrichtung filtern</label>
<select id="sources-filter-political" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-political" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Ausrichtungen</option> <option value="" data-i18n="sources_modal.filter.political_all">Alle Ausrichtungen</option>
<option value="links_extrem">Links (extrem)</option> <option value="links_extrem">Links (extrem)</option>
<option value="links">Links</option> <option value="links">Links</option>
<option value="mitte_links">Mitte-Links</option> <option value="mitte_links">Mitte-Links</option>
@@ -495,9 +495,9 @@
<option value="rechts_extrem">Rechts (extrem)</option> <option value="rechts_extrem">Rechts (extrem)</option>
<option value="na">Nicht eingeordnet</option> <option value="na">Nicht eingeordnet</option>
</select> </select>
<label for="sources-filter-mediatype" class="sr-only">Medientyp filtern</label> <label for="sources-filter-mediatype" class="sr-only" data-i18n="sources_modal.filter.mediatype">Medientyp filtern</label>
<select id="sources-filter-mediatype" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-mediatype" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Medientypen</option> <option value="" data-i18n="sources_modal.filter.mediatype_all">Alle Medientypen</option>
<option value="tageszeitung">Tageszeitung</option> <option value="tageszeitung">Tageszeitung</option>
<option value="wochenzeitung">Wochenzeitung</option> <option value="wochenzeitung">Wochenzeitung</option>
<option value="magazin">Magazin</option> <option value="magazin">Magazin</option>
@@ -519,9 +519,9 @@
<option value="fachmedium">Fachmedium</option> <option value="fachmedium">Fachmedium</option>
<option value="sonstige">Sonstige</option> <option value="sonstige">Sonstige</option>
</select> </select>
<label for="sources-filter-reliability" class="sr-only">Glaubwürdigkeit filtern</label> <label for="sources-filter-reliability" class="sr-only" data-i18n="sources_modal.filter.reliability">Glaubwürdigkeit filtern</label>
<select id="sources-filter-reliability" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-reliability" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Glaubwürdigkeiten</option> <option value="" data-i18n="sources_modal.filter.reliability_all">Alle Glaubwürdigkeiten</option>
<option value="sehr_hoch">Sehr hoch</option> <option value="sehr_hoch">Sehr hoch</option>
<option value="hoch">Hoch</option> <option value="hoch">Hoch</option>
<option value="gemischt">Gemischt</option> <option value="gemischt">Gemischt</option>
@@ -529,15 +529,15 @@
<option value="sehr_niedrig">Sehr niedrig</option> <option value="sehr_niedrig">Sehr niedrig</option>
<option value="na">Nicht eingeordnet</option> <option value="na">Nicht eingeordnet</option>
</select> </select>
<label for="sources-filter-extern" class="sr-only">Externe Reputation filtern</label> <label for="sources-filter-extern" class="sr-only" data-i18n="sources_modal.filter.extern">Externe Reputation filtern</label>
<select id="sources-filter-extern" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-extern" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Externe Reputation: alle</option> <option value="" data-i18n="sources_modal.filter.extern_all">Externe Reputation: alle</option>
<option value="ifcn">IFCN-Faktenchecker</option> <option value="ifcn">IFCN-Faktenchecker</option>
<option value="eu_disinfo">EU-Desinfo gelistet</option> <option value="eu_disinfo">EU-Desinfo gelistet</option>
</select> </select>
<label for="sources-filter-alignment" class="sr-only">Geopolitische Nähe filtern</label> <label for="sources-filter-alignment" class="sr-only" data-i18n="sources_modal.filter.alignment">Geopolitische Nähe filtern</label>
<select id="sources-filter-alignment" class="timeline-filter-select" onchange="App.filterSources()"> <select id="sources-filter-alignment" class="timeline-filter-select" onchange="App.filterSources()">
<option value="">Alle Nähen</option> <option value="" data-i18n="sources_modal.filter.alignment_all">Alle Nähen</option>
<option value="prorussisch">Prorussisch</option> <option value="prorussisch">Prorussisch</option>
<option value="proiranisch">Proiranisch</option> <option value="proiranisch">Proiranisch</option>
<option value="prowestlich">Prowestlich</option> <option value="prowestlich">Prowestlich</option>
@@ -551,11 +551,11 @@
<option value="neutral">Neutral</option> <option value="neutral">Neutral</option>
<option value="sonstige">Sonstige</option> <option value="sonstige">Sonstige</option>
</select> </select>
<label for="sources-search" class="sr-only">Quellen durchsuchen</label> <label for="sources-search" class="sr-only" data-i18n="sources_modal.search">Quellen durchsuchen</label>
<input type="text" id="sources-search" class="timeline-filter-input sources-search-input" placeholder="Suche..." oninput="App.filterSources()"> <input type="text" id="sources-search" class="timeline-filter-input sources-search-input" placeholder="Suche..." oninput="App.filterSources()" data-i18n-attr="placeholder:sources_modal.search_placeholder">
</div> </div>
<div class="sources-toolbar-actions"> <div class="sources-toolbar-actions">
<button class="btn btn-primary btn-small" onclick="App.toggleSourceForm()">+ Quelle</button> <button class="btn btn-primary btn-small" onclick="App.toggleSourceForm()" data-i18n="sources_modal.add_source">+ Quelle</button>
</div> </div>
</div> </div>
@@ -564,10 +564,10 @@
<div class="sources-add-form" id="sources-add-form" style="display:none;"> <div class="sources-add-form" id="sources-add-form" style="display:none;">
<div class="sources-form-row"> <div class="sources-form-row">
<div class="form-group flex-1"> <div class="form-group flex-1">
<label for="src-discover-url">URL oder Domain</label> <label for="src-discover-url" data-i18n="sources_modal.form.url_label">URL oder Domain</label>
<input type="text" id="src-discover-url" placeholder="z.B. netzpolitik.org oder t.me/kanalname"> <input type="text" id="src-discover-url" placeholder="z.B. netzpolitik.org oder t.me/kanalname" data-i18n-attr="placeholder:sources_modal.form.url_placeholder">
</div> </div>
<button class="btn btn-secondary btn-small" id="src-discover-btn" onclick="App.discoverSource()">Erkennen</button> <button class="btn btn-secondary btn-small" id="src-discover-btn" onclick="App.discoverSource()" data-i18n="sources_modal.form.discover">Erkennen</button>
</div> </div>
<!-- Ergebnis-Anzeige (nach Discovery) --> <!-- Ergebnis-Anzeige (nach Discovery) -->
@@ -575,10 +575,10 @@
<div class="sources-add-form-grid"> <div class="sources-add-form-grid">
<div class="form-group"> <div class="form-group">
<label for="src-name">Name</label> <label for="src-name">Name</label>
<input type="text" id="src-name" placeholder="Wird erkannt..."> <input type="text" id="src-name" placeholder="Wird erkannt..." data-i18n-attr="placeholder:sources_modal.form.name_placeholder">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="src-category">Kategorie</label> <label for="src-category" data-i18n="sources_modal.form.category">Kategorie</label>
<select id="src-category"> <select id="src-category">
<option value="nachrichtenagentur">Nachrichtenagentur</option> <option value="nachrichtenagentur">Nachrichtenagentur</option>
<option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option> <option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option>
@@ -597,7 +597,7 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Typ</label> <label data-i18n="sources_modal.form.type">Typ</label>
<input type="text" id="src-type-display" class="input-readonly" readonly> <input type="text" id="src-type-display" class="input-readonly" readonly>
<select id="src-type-select" style="display:none"> <select id="src-type-select" style="display:none">
<option value="rss_feed">RSS-Feed</option> <option value="rss_feed">RSS-Feed</option>
@@ -606,28 +606,28 @@
</select> </select>
</div> </div>
<div class="form-group" id="src-rss-url-group"> <div class="form-group" id="src-rss-url-group">
<label>RSS-Feed URL</label> <label data-i18n="sources_modal.form.rss_url">RSS-Feed URL</label>
<input type="text" id="src-rss-url" class="input-readonly" readonly> <input type="text" id="src-rss-url" class="input-readonly" readonly>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Domain</label> <label data-i18n="sources_modal.form.domain">Domain</label>
<input type="text" id="src-domain" class="input-readonly" readonly> <input type="text" id="src-domain" class="input-readonly" readonly>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="src-notes">Notizen</label> <label for="src-notes" data-i18n="sources_modal.form.notes">Notizen</label>
<input type="text" id="src-notes" placeholder="Optional"> <input type="text" id="src-notes" placeholder="Optional" data-i18n-attr="placeholder:sources_modal.form.notes_placeholder">
</div> </div>
</div> </div>
<div class="sources-discovery-actions"> <div class="sources-discovery-actions">
<button class="btn btn-primary btn-small" onclick="App.saveSource()">Speichern</button> <button class="btn btn-primary btn-small" onclick="App.saveSource()" data-i18n="common.save">Speichern</button>
<button class="btn btn-secondary btn-small" onclick="App.toggleSourceForm(false)">Abbrechen</button> <button class="btn btn-secondary btn-small" onclick="App.toggleSourceForm(false)" data-i18n="common.cancel">Abbrechen</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Quellen-Liste (gruppiert) --> <!-- Quellen-Liste (gruppiert) -->
<div class="sources-list" id="sources-list"> <div class="sources-list" id="sources-list">
<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;">Lade Quellen...</div> <div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;" data-i18n="sources_modal.list.loading">Lade Quellen...</div>
</div> </div>
</div> </div>
</div> </div>
@@ -683,26 +683,26 @@
</div> </div>
<!-- Chat-Assistent Widget --> <!-- Chat-Assistent Widget -->
<button class="chat-toggle-btn" id="chat-toggle-btn" title="Chat-Assistent" aria-label="Chat-Assistent oeffnen"> <button class="chat-toggle-btn" id="chat-toggle-btn" title="Chat-Assistent" aria-label="Chat-Assistent oeffnen" data-i18n-attr="title:chat.toggle_title,aria-label:chat.toggle_aria">
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.2L4 17.2V4h16v12z"/></svg> <svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.2L4 17.2V4h16v12z"/></svg>
</button> </button>
<div class="chat-window" id="chat-window"> <div class="chat-window" id="chat-window">
<div class="chat-header"> <div class="chat-header">
<span class="chat-header-title">AegisSight Assistent</span> <span class="chat-header-title" data-i18n="chat.title">AegisSight Assistent</span>
<div class="chat-header-actions"> <div class="chat-header-actions">
<button class="chat-header-btn chat-reset-btn" id="chat-reset-btn" title="Neuer Chat" aria-label="Neuen Chat starten" style="display:none"> <button class="chat-header-btn chat-reset-btn" id="chat-reset-btn" title="Neuer Chat" aria-label="Neuen Chat starten" style="display:none" data-i18n-attr="title:chat.new_title,aria-label:chat.new_aria">
<svg viewBox="0 0 24 24" width="15" height="15"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/></svg> <svg viewBox="0 0 24 24" width="15" height="15"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/></svg>
</button> </button>
<button class="chat-header-btn" id="chat-fullscreen-btn" title="Vollbild" aria-label="Vollbild umschalten"> <button class="chat-header-btn" id="chat-fullscreen-btn" title="Vollbild" aria-label="Vollbild umschalten" data-i18n-attr="title:chat.fullscreen_title,aria-label:chat.fullscreen_aria">
<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg> <svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>
</button> </button>
<button class="chat-header-btn chat-header-close" id="chat-close-btn" title="Schließen" aria-label="Chat schließen">&times;</button> <button class="chat-header-btn chat-header-close" id="chat-close-btn" title="Schließen" aria-label="Chat schließen" data-i18n-attr="title:chat.close_title,aria-label:chat.close_aria">&times;</button>
</div> </div>
</div> </div>
<div class="chat-messages" id="chat-messages"></div> <div class="chat-messages" id="chat-messages"></div>
<form class="chat-input-area" id="chat-form" autocomplete="off"> <form class="chat-input-area" id="chat-form" autocomplete="off">
<textarea id="chat-input" rows="1" placeholder="Frage stellen..." maxlength="2000"></textarea> <textarea id="chat-input" rows="1" placeholder="Frage stellen..." maxlength="2000" data-i18n-attr="placeholder:chat.input_placeholder"></textarea>
<button type="submit" class="chat-send-btn" title="Senden" aria-label="Nachricht senden"> <button type="submit" class="chat-send-btn" title="Senden" aria-label="Nachricht senden" data-i18n-attr="title:chat.send_title,aria-label:chat.send_aria">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button> </button>
</form> </form>
@@ -726,13 +726,13 @@
<script src="/static/js/i18n.js?v=20260513a"></script> <script src="/static/js/i18n.js?v=20260513a"></script>
<script src="/static/js/api.js?v=20260423a"></script> <script src="/static/js/api.js?v=20260423a"></script>
<script src="/static/js/ws.js?v=20260316b"></script> <script src="/static/js/ws.js?v=20260316b"></script>
<script src="/static/js/components.js?v=20260513d"></script> <script src="/static/js/components.js?v=20260514e"></script>
<script src="/static/js/layout.js?v=20260513f"></script> <script src="/static/js/layout.js?v=20260513f"></script>
<script src="/static/js/pipeline.js?v=20260513d"></script> <script src="/static/js/pipeline.js?v=20260513d"></script>
<script src="/static/js/app.js?v=20260514c"></script> <script src="/static/js/app.js?v=20260514e"></script>
<script src="/static/js/cluster-data.js?v=20260322f"></script> <script src="/static/js/cluster-data.js?v=20260322f"></script>
<script src="/static/js/tutorial.js?v=20260316z"></script> <script src="/static/js/tutorial.js?v=20260316z"></script>
<script src="/static/js/chat.js?v=20260422a"></script> <script src="/static/js/chat.js?v=20260514e"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();/* Tutorial.init() wird in App.init() nach Sprachwahl aufgerufen, damit es bei englischen Orgs unterdrueckt werden kann */});</script> <script>document.addEventListener("DOMContentLoaded",function(){Chat.init();/* Tutorial.init() wird in App.init() nach Sprachwahl aufgerufen, damit es bei englischen Orgs unterdrueckt werden kann */});</script>
<!-- Map Fullscreen Overlay --> <!-- Map Fullscreen Overlay -->
@@ -753,26 +753,26 @@
<div class="modal-overlay" id="modal-export" role="dialog" aria-modal="true"> <div class="modal-overlay" id="modal-export" role="dialog" aria-modal="true">
<div class="modal" style="max-width:420px;"> <div class="modal" style="max-width:420px;">
<div class="modal-header"> <div class="modal-header">
<h3>Bericht exportieren</h3> <h3 data-i18n="modal.export.title">Bericht exportieren</h3>
<button class="modal-close" onclick="closeModal('modal-export')">&times;</button> <button class="modal-close" onclick="closeModal('modal-export')" aria-label="Schließen" data-i18n-attr="aria-label:aria.close">&times;</button>
</div> </div>
<div class="modal-body" style="padding:20px;"> <div class="modal-body" style="padding:20px;">
<div style="margin-bottom:16px;"> <div style="margin-bottom:16px;">
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Bereiche</label> <label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;" data-i18n="export.sections">Bereiche</label>
<label class="export-radio"><input type="checkbox" name="export-section" value="zusammenfassung" checked><span>Zusammenfassung</span></label> <label class="export-radio"><input type="checkbox" name="export-section" value="zusammenfassung" checked><span data-i18n="export.section.summary">Zusammenfassung</span></label>
<label class="export-radio"><input type="checkbox" name="export-section" value="bericht" checked><span>Recherchebericht / Lagebild</span></label> <label class="export-radio"><input type="checkbox" name="export-section" value="bericht" checked><span data-i18n="export.section.report">Recherchebericht / Lagebild</span></label>
<label class="export-radio"><input type="checkbox" name="export-section" value="faktencheck" checked><span>Faktencheck</span></label> <label class="export-radio"><input type="checkbox" name="export-section" value="faktencheck" checked><span data-i18n="export.section.factcheck">Faktencheck</span></label>
<label class="export-radio"><input type="checkbox" name="export-section" value="quellen" checked><span>Quellen</span></label> <label class="export-radio"><input type="checkbox" name="export-section" value="quellen" checked><span data-i18n="export.section.sources">Quellen</span></label>
</div> </div>
<div style="margin-bottom:16px;"> <div style="margin-bottom:16px;">
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Format</label> <label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;" data-i18n="export.format">Format</label>
<label class="export-radio"><input type="radio" name="export-format" value="pdf" checked><span>PDF</span></label> <label class="export-radio"><input type="radio" name="export-format" value="pdf" checked><span>PDF</span></label>
<label class="export-radio"><input type="radio" name="export-format" value="docx"><span>Word (DOCX)</span></label> <label class="export-radio"><input type="radio" name="export-format" value="docx"><span data-i18n="export.format.docx">Word (DOCX)</span></label>
</div> </div>
</div> </div>
<div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);"> <div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);">
<button class="btn btn-secondary" onclick="closeModal('modal-export')">Abbrechen</button> <button class="btn btn-secondary" onclick="closeModal('modal-export')" data-i18n="common.cancel">Abbrechen</button>
<button class="btn btn-primary" id="export-submit-btn" onclick="App.submitExport()">Exportieren</button> <button class="btn btn-primary" id="export-submit-btn" onclick="App.submitExport()" data-i18n="export.submit">Exportieren</button>
</div> </div>
</div> </div>
</div> </div>

Datei anzeigen

@@ -201,5 +201,63 @@
"source.type.rss_feed": "RSS-Feed", "source.type.rss_feed": "RSS-Feed",
"source.type.telegram": "Telegram", "source.type.telegram": "Telegram",
"source.type.web": "Web-Quelle", "source.type.web": "Web-Quelle",
"modal.hint.sources_german_only": "Nur deutschsprachige Quellen (DE, AT, CH)" "modal.hint.sources_german_only": "Nur deutschsprachige Quellen (DE, AT, CH)",
"export.sections": "Bereiche",
"export.section.summary": "Zusammenfassung",
"export.section.report": "Recherchebericht / Lagebild",
"export.section.factcheck": "Faktencheck",
"export.section.sources": "Quellen",
"export.format": "Format",
"export.format.pdf": "PDF",
"export.format.docx": "Word (DOCX)",
"export.submit": "Exportieren",
"sources_modal.title": "Quellenverwaltung",
"sources_modal.stats.rss": "RSS-Feeds",
"sources_modal.stats.web": "Web-Quellen",
"sources_modal.stats.telegram": "Telegram",
"sources_modal.stats.excluded": "Ausgeschlossen",
"sources_modal.stats.articles": "Artikel gesamt",
"sources_modal.filter.type": "Quellentyp filtern",
"sources_modal.filter.type_all": "Alle Typen",
"sources_modal.filter.category": "Kategorie filtern",
"sources_modal.filter.category_all": "Alle Kategorien",
"sources_modal.filter.political": "Politische Ausrichtung filtern",
"sources_modal.filter.political_all": "Alle Ausrichtungen",
"sources_modal.filter.mediatype": "Medientyp filtern",
"sources_modal.filter.mediatype_all": "Alle Medientypen",
"sources_modal.filter.reliability": "Glaubwürdigkeit filtern",
"sources_modal.filter.reliability_all": "Alle Glaubwürdigkeiten",
"sources_modal.filter.extern": "Externe Reputation filtern",
"sources_modal.filter.extern_all": "Externe Reputation: alle",
"sources_modal.filter.alignment": "Geopolitische Nähe filtern",
"sources_modal.filter.alignment_all": "Alle Nähen",
"sources_modal.search": "Quellen durchsuchen",
"sources_modal.search_placeholder": "Suche...",
"sources_modal.add_source": "+ Quelle",
"sources_modal.form.url_label": "URL oder Domain",
"sources_modal.form.url_placeholder": "z.B. netzpolitik.org oder t.me/kanalname",
"sources_modal.form.discover": "Erkennen",
"sources_modal.form.name_placeholder": "Wird erkannt...",
"sources_modal.form.category": "Kategorie",
"sources_modal.form.type": "Typ",
"sources_modal.form.rss_url": "RSS-Feed URL",
"sources_modal.form.domain": "Domain",
"sources_modal.form.notes": "Notizen",
"sources_modal.form.notes_placeholder": "Optional",
"sources_modal.list.loading": "Lade Quellen...",
"sources_modal.excluded_badge": "Ausgeschlossen",
"chat.title": "AegisSight Assistent",
"chat.toggle_title": "Chat-Assistent",
"chat.toggle_aria": "Chat-Assistent öffnen",
"chat.new_title": "Neuer Chat",
"chat.new_aria": "Neuen Chat starten",
"chat.fullscreen_title": "Vollbild",
"chat.fullscreen_aria": "Vollbild umschalten",
"chat.close_title": "Schließen",
"chat.close_aria": "Chat schließen",
"chat.input_placeholder": "Frage stellen...",
"chat.send_title": "Senden",
"chat.send_aria": "Nachricht senden",
"chat.greeting": "Hallo! Ich bin der AegisSight Assistent. Stell mir gerne jede Frage rund um die Bedienung des Monitors, ich helfe dir weiter.",
"stats.articles_total": "Artikel gesamt"
} }

Datei anzeigen

@@ -201,5 +201,63 @@
"source.type.rss_feed": "RSS feed", "source.type.rss_feed": "RSS feed",
"source.type.telegram": "Telegram", "source.type.telegram": "Telegram",
"source.type.web": "Web source", "source.type.web": "Web source",
"modal.hint.sources_german_only": "Primary-language sources only" "modal.hint.sources_german_only": "Primary-language sources only",
"export.sections": "Sections",
"export.section.summary": "Summary",
"export.section.report": "Research report / Briefing",
"export.section.factcheck": "Fact check",
"export.section.sources": "Sources",
"export.format": "Format",
"export.format.pdf": "PDF",
"export.format.docx": "Word (DOCX)",
"export.submit": "Export",
"sources_modal.title": "Source management",
"sources_modal.stats.rss": "RSS feeds",
"sources_modal.stats.web": "Web sources",
"sources_modal.stats.telegram": "Telegram",
"sources_modal.stats.excluded": "Excluded",
"sources_modal.stats.articles": "Articles total",
"sources_modal.filter.type": "Filter by source type",
"sources_modal.filter.type_all": "All types",
"sources_modal.filter.category": "Filter by category",
"sources_modal.filter.category_all": "All categories",
"sources_modal.filter.political": "Filter by political orientation",
"sources_modal.filter.political_all": "All orientations",
"sources_modal.filter.mediatype": "Filter by media type",
"sources_modal.filter.mediatype_all": "All media types",
"sources_modal.filter.reliability": "Filter by reliability",
"sources_modal.filter.reliability_all": "All reliabilities",
"sources_modal.filter.extern": "Filter by external reputation",
"sources_modal.filter.extern_all": "External reputation: any",
"sources_modal.filter.alignment": "Filter by geopolitical alignment",
"sources_modal.filter.alignment_all": "All alignments",
"sources_modal.search": "Search sources",
"sources_modal.search_placeholder": "Search...",
"sources_modal.add_source": "+ Source",
"sources_modal.form.url_label": "URL or domain",
"sources_modal.form.url_placeholder": "e.g. example.com or t.me/channel",
"sources_modal.form.discover": "Detect",
"sources_modal.form.name_placeholder": "Detecting...",
"sources_modal.form.category": "Category",
"sources_modal.form.type": "Type",
"sources_modal.form.rss_url": "RSS feed URL",
"sources_modal.form.domain": "Domain",
"sources_modal.form.notes": "Notes",
"sources_modal.form.notes_placeholder": "Optional",
"sources_modal.list.loading": "Loading sources...",
"sources_modal.excluded_badge": "Excluded",
"chat.title": "AegisSight Assistant",
"chat.toggle_title": "Chat assistant",
"chat.toggle_aria": "Open chat assistant",
"chat.new_title": "New chat",
"chat.new_aria": "Start new chat",
"chat.fullscreen_title": "Fullscreen",
"chat.fullscreen_aria": "Toggle fullscreen",
"chat.close_title": "Close",
"chat.close_aria": "Close chat",
"chat.input_placeholder": "Ask a question...",
"chat.send_title": "Send",
"chat.send_aria": "Send message",
"chat.greeting": "Hi! I'm the AegisSight Assistant. Ask me anything about how to use the monitor and I'll guide you through.",
"stats.articles_total": "Articles total"
} }

Datei anzeigen

@@ -2778,10 +2778,10 @@ async handleRefresh() {
const excluded = this._myExclusions.length; const excluded = this._myExclusions.length;
bar.innerHTML = ` bar.innerHTML = `
<span class="sources-stat-item"><span class="sources-stat-value">${rss.count}</span> RSS-Feeds</span> <span class="sources-stat-item"><span class="sources-stat-value">${rss.count}</span> ${(typeof T === 'function' ? T('sources_modal.stats.rss', 'RSS-Feeds') : 'RSS-Feeds')}</span>
<span class="sources-stat-item"><span class="sources-stat-value">${web.count}</span> Web-Quellen</span> <span class="sources-stat-item"><span class="sources-stat-value">${web.count}</span> ${(typeof T === 'function' ? T('sources_modal.stats.web', 'Web-Quellen') : 'Web-Quellen')}</span>
<span class="sources-stat-item"><span class="sources-stat-value">${tg.count}</span> Telegram</span> <span class="sources-stat-item"><span class="sources-stat-value">${tg.count}</span> Telegram</span>
<span class="sources-stat-item"><span class="sources-stat-value">${excluded}</span> Ausgeschlossen</span> <span class="sources-stat-item"><span class="sources-stat-value">${excluded}</span> ${(typeof T === 'function' ? T('sources_modal.stats.excluded', 'Ausgeschlossen') : 'Ausgeschlossen')}</span>
<span class="sources-stat-item"><span class="sources-stat-value">${stats.total_articles}</span> Artikel gesamt</span> <span class="sources-stat-item"><span class="sources-stat-value">${stats.total_articles}</span> Artikel gesamt</span>
`; `;
}, },

Datei anzeigen

@@ -1,352 +1,352 @@
/** /**
* AegisSight Chat-Assistent Widget. * AegisSight Chat-Assistent Widget.
*/ */
const Chat = { const Chat = {
_conversationId: null, _conversationId: null,
_isOpen: false, _isOpen: false,
_isLoading: false, _isLoading: false,
_hasGreeted: false, _hasGreeted: false,
_tutorialHintDismissed: false, _tutorialHintDismissed: false,
_isFullscreen: false, _isFullscreen: false,
init() { init() {
const btn = document.getElementById('chat-toggle-btn'); const btn = document.getElementById('chat-toggle-btn');
const closeBtn = document.getElementById('chat-close-btn'); const closeBtn = document.getElementById('chat-close-btn');
const form = document.getElementById('chat-form'); const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input'); const input = document.getElementById('chat-input');
if (!btn || !form) return; if (!btn || !form) return;
btn.addEventListener('click', () => this.toggle()); btn.addEventListener('click', () => this.toggle());
closeBtn.addEventListener('click', () => this.close()); closeBtn.addEventListener('click', () => this.close());
const resetBtn = document.getElementById('chat-reset-btn'); const resetBtn = document.getElementById('chat-reset-btn');
if (resetBtn) resetBtn.addEventListener('click', () => this.reset()); if (resetBtn) resetBtn.addEventListener('click', () => this.reset());
const fsBtn = document.getElementById('chat-fullscreen-btn'); const fsBtn = document.getElementById('chat-fullscreen-btn');
if (fsBtn) fsBtn.addEventListener('click', () => this.toggleFullscreen()); if (fsBtn) fsBtn.addEventListener('click', () => this.toggleFullscreen());
form.addEventListener('submit', (e) => { form.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
this.send(); this.send();
}); });
// Enter sendet, Shift+Enter für Zeilenumbruch // Enter sendet, Shift+Enter für Zeilenumbruch
input.addEventListener('keydown', (e) => { input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
this.send(); this.send();
} }
}); });
// Auto-resize textarea // Auto-resize textarea
input.addEventListener('input', () => { input.addEventListener('input', () => {
input.style.height = 'auto'; input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px'; input.style.height = Math.min(input.scrollHeight, 120) + 'px';
}); });
}, },
toggle() { toggle() {
if (this._isOpen) { if (this._isOpen) {
this.close(); this.close();
} else { } else {
this.open(); this.open();
} }
}, },
open() { open() {
const win = document.getElementById('chat-window'); const win = document.getElementById('chat-window');
const btn = document.getElementById('chat-toggle-btn'); const btn = document.getElementById('chat-toggle-btn');
if (!win) return; if (!win) return;
win.classList.add('open'); win.classList.add('open');
btn.classList.add('active'); btn.classList.add('active');
this._isOpen = true; this._isOpen = true;
if (!this._hasGreeted) { if (!this._hasGreeted) {
this._hasGreeted = true; this._hasGreeted = true;
this.addMessage('assistant', 'Hallo! Ich bin der AegisSight Assistent. Stell mir gerne jede Frage rund um die Bedienung des Monitors, ich helfe dir weiter.'); this.addMessage('assistant', (typeof T === 'function' ? T('chat.greeting', 'Hallo! Ich bin der AegisSight Assistent. Stell mir gerne jede Frage rund um die Bedienung des Monitors, ich helfe dir weiter.') : 'Hallo! Ich bin der AegisSight Assistent. Stell mir gerne jede Frage rund um die Bedienung des Monitors, ich helfe dir weiter.'));
} }
// Tutorial-Hinweis temporaer deaktiviert (Ueberarbeitung) - reaktivieren durch Entfernen der Kommentarzeichen: // Tutorial-Hinweis temporaer deaktiviert (Ueberarbeitung) - reaktivieren durch Entfernen der Kommentarzeichen:
// if (typeof Tutorial !== 'undefined' && !this._tutorialHintDismissed) { // if (typeof Tutorial !== 'undefined' && !this._tutorialHintDismissed) {
// var oldHint = document.getElementById('chat-tutorial-hint'); // var oldHint = document.getElementById('chat-tutorial-hint');
// if (oldHint) oldHint.remove(); // if (oldHint) oldHint.remove();
// this._showTutorialHint(); // this._showTutorialHint();
// } // }
// Focus auf Input // Focus auf Input
setTimeout(() => { setTimeout(() => {
const input = document.getElementById('chat-input'); const input = document.getElementById('chat-input');
if (input) input.focus(); if (input) input.focus();
}, 200); }, 200);
}, },
close() { close() {
const win = document.getElementById('chat-window'); const win = document.getElementById('chat-window');
const btn = document.getElementById('chat-toggle-btn'); const btn = document.getElementById('chat-toggle-btn');
if (!win) return; if (!win) return;
win.classList.remove('open'); win.classList.remove('open');
win.classList.remove('fullscreen'); win.classList.remove('fullscreen');
btn.classList.remove('active'); btn.classList.remove('active');
this._isOpen = false; this._isOpen = false;
this._isFullscreen = false; this._isFullscreen = false;
const fsBtn = document.getElementById('chat-fullscreen-btn'); const fsBtn = document.getElementById('chat-fullscreen-btn');
if (fsBtn) { if (fsBtn) {
fsBtn.title = 'Vollbild'; fsBtn.title = 'Vollbild';
fsBtn.innerHTML = '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>'; fsBtn.innerHTML = '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>';
} }
}, },
reset() { reset() {
this._conversationId = null; this._conversationId = null;
this._hasGreeted = false; this._hasGreeted = false;
this._isLoading = false; this._isLoading = false;
const container = document.getElementById('chat-messages'); const container = document.getElementById('chat-messages');
if (container) container.innerHTML = ''; if (container) container.innerHTML = '';
this._updateResetBtn(); this._updateResetBtn();
this.open(); this.open();
}, },
toggleFullscreen() { toggleFullscreen() {
const win = document.getElementById('chat-window'); const win = document.getElementById('chat-window');
const btn = document.getElementById('chat-fullscreen-btn'); const btn = document.getElementById('chat-fullscreen-btn');
if (!win) return; if (!win) return;
this._isFullscreen = !this._isFullscreen; this._isFullscreen = !this._isFullscreen;
win.classList.toggle('fullscreen', this._isFullscreen); win.classList.toggle('fullscreen', this._isFullscreen);
if (btn) { if (btn) {
btn.title = this._isFullscreen ? 'Vollbild beenden' : 'Vollbild'; btn.title = this._isFullscreen ? 'Vollbild beenden' : 'Vollbild';
btn.innerHTML = this._isFullscreen btn.innerHTML = this._isFullscreen
? '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" fill="currentColor"/></svg>' ? '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" fill="currentColor"/></svg>'
: '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>'; : '<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>';
} }
}, },
_updateResetBtn() { _updateResetBtn() {
const btn = document.getElementById('chat-reset-btn'); const btn = document.getElementById('chat-reset-btn');
if (btn) btn.style.display = this._conversationId ? '' : 'none'; if (btn) btn.style.display = this._conversationId ? '' : 'none';
}, },
async send() { async send() {
const input = document.getElementById('chat-input'); const input = document.getElementById('chat-input');
const text = (input.value || '').trim(); const text = (input.value || '').trim();
if (!text || this._isLoading) return; if (!text || this._isLoading) return;
input.value = ''; input.value = '';
input.style.height = 'auto'; input.style.height = 'auto';
this.addMessage('user', text); this.addMessage('user', text);
this._showTyping(); this._showTyping();
this._isLoading = true; this._isLoading = true;
// Tutorial-Keywords temporaer deaktiviert (Ueberarbeitung) - reaktivieren durch Entfernen der Kommentarzeichen: // Tutorial-Keywords temporaer deaktiviert (Ueberarbeitung) - reaktivieren durch Entfernen der Kommentarzeichen:
// var lowerText = text.toLowerCase(); // var lowerText = text.toLowerCase();
// if (lowerText === 'rundgang' || lowerText === 'tutorial' || lowerText === 'tour' || lowerText === 'f\u00fchrung') { // if (lowerText === 'rundgang' || lowerText === 'tutorial' || lowerText === 'tour' || lowerText === 'f\u00fchrung') {
// this._hideTyping(); // this._hideTyping();
// this._isLoading = false; // this._isLoading = false;
// this.close(); // this.close();
// if (typeof Tutorial !== 'undefined') Tutorial.start(); // if (typeof Tutorial !== 'undefined') Tutorial.start();
// return; // return;
// } // }
try { try {
const body = { const body = {
message: text, message: text,
conversation_id: this._conversationId, conversation_id: this._conversationId,
}; };
// Aktuelle Lage mitschicken falls geoeffnet // Aktuelle Lage mitschicken falls geoeffnet
const incidentId = this._getIncidentContext(); const incidentId = this._getIncidentContext();
if (incidentId) { if (incidentId) {
body.incident_id = incidentId; body.incident_id = incidentId;
} }
const data = await this._request(body); const data = await this._request(body);
this._conversationId = data.conversation_id; this._conversationId = data.conversation_id;
this._updateResetBtn(); this._updateResetBtn();
this._hideTyping(); this._hideTyping();
this.addMessage('assistant', data.reply); this.addMessage('assistant', data.reply);
this._highlightUI(data.reply); this._highlightUI(data.reply);
} catch (err) { } catch (err) {
this._hideTyping(); this._hideTyping();
const msg = err.detail || err.message || 'Etwas ist schiefgelaufen. Bitte versuche es erneut.'; const msg = err.detail || err.message || 'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
this.addMessage('assistant', msg); this.addMessage('assistant', msg);
} finally { } finally {
this._isLoading = false; this._isLoading = false;
} }
}, },
addMessage(role, text) { addMessage(role, text) {
const container = document.getElementById('chat-messages'); const container = document.getElementById('chat-messages');
if (!container) return; if (!container) return;
const bubble = document.createElement('div'); const bubble = document.createElement('div');
bubble.className = 'chat-message ' + role; bubble.className = 'chat-message ' + role;
// Einfache Formatierung: Zeilenumbrueche und Fettschrift // Einfache Formatierung: Zeilenumbrueche und Fettschrift
const formatted = text const formatted = text
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>'); .replace(/\n/g, '<br>');
bubble.innerHTML = '<div class="chat-bubble">' + formatted + '</div>'; bubble.innerHTML = '<div class="chat-bubble">' + formatted + '</div>';
container.appendChild(bubble); container.appendChild(bubble);
// User-Nachrichten: nach unten scrollen. Antworten: zum Anfang der Antwort scrollen. // User-Nachrichten: nach unten scrollen. Antworten: zum Anfang der Antwort scrollen.
if (role === 'user') { if (role === 'user') {
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} else { } else {
bubble.scrollIntoView({ behavior: 'smooth', block: 'start' }); bubble.scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
}, },
_showTyping() { _showTyping() {
const container = document.getElementById('chat-messages'); const container = document.getElementById('chat-messages');
if (!container) return; if (!container) return;
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'chat-message assistant chat-typing-msg'; el.className = 'chat-message assistant chat-typing-msg';
el.innerHTML = '<div class="chat-bubble chat-typing"><span></span><span></span><span></span></div>'; el.innerHTML = '<div class="chat-bubble chat-typing"><span></span><span></span><span></span></div>';
container.appendChild(el); container.appendChild(el);
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
}, },
_hideTyping() { _hideTyping() {
const el = document.querySelector('.chat-typing-msg'); const el = document.querySelector('.chat-typing-msg');
if (el) el.remove(); if (el) el.remove();
}, },
_getIncidentContext() { _getIncidentContext() {
if (typeof App !== 'undefined' && App.currentIncidentId) { if (typeof App !== 'undefined' && App.currentIncidentId) {
return App.currentIncidentId; return App.currentIncidentId;
} }
return null; return null;
}, },
async _request(body) { async _request(body) {
const token = localStorage.getItem('osint_token'); const token = localStorage.getItem('osint_token');
const resp = await fetch('/api/chat', { const resp = await fetch('/api/chat', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': token ? 'Bearer ' + token : '', 'Authorization': token ? 'Bearer ' + token : '',
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
if (!resp.ok) { if (!resp.ok) {
const data = await resp.json().catch(() => ({})); const data = await resp.json().catch(() => ({}));
throw data; throw data;
} }
return await resp.json(); return await resp.json();
}, },
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// UI-Highlight: Bedienelemente im Dashboard hervorheben wenn im Chat erwaehnt // UI-Highlight: Bedienelemente im Dashboard hervorheben wenn im Chat erwaehnt
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
_UI_HIGHLIGHTS: [ _UI_HIGHLIGHTS: [
{ keywords: ['neue lage', 'lage erstellen', 'lage anlegen', 'recherche erstellen', 'neuen fall'], selector: '#new-incident-btn' }, { keywords: ['neue lage', 'lage erstellen', 'lage anlegen', 'recherche erstellen', 'neuen fall'], selector: '#new-incident-btn' },
{ keywords: ['theme wechseln', 'theme-umschalter', 'farbschema', 'helles design', 'dunkles design', 'hell- und dunkel', 'hellem und dunklem', 'dark mode', 'light mode'], selector: '#theme-toggle' }, { keywords: ['theme wechseln', 'theme-umschalter', 'farbschema', 'helles design', 'dunkles design', 'hell- und dunkel', 'hellem und dunklem', 'dark mode', 'light mode'], selector: '#theme-toggle' },
{ keywords: ['barrierefreiheit', 'accessibility', 'hoher kontrast', 'focus-anzeige', 'groessere schrift', 'animationen aus'], selector: '#a11y-btn' }, { keywords: ['barrierefreiheit', 'accessibility', 'hoher kontrast', 'focus-anzeige', 'groessere schrift', 'animationen aus'], selector: '#a11y-btn' },
{ keywords: ['abmelden', 'logout', 'ausloggen', 'abmeldung'], selector: '#logout-btn' }, { keywords: ['abmelden', 'logout', 'ausloggen', 'abmeldung'], selector: '#logout-btn' },
{ keywords: ['benachrichtigung', 'glocken-symbol', 'abonnieren', 'abonniert'], selector: '#notification-btn' }, { keywords: ['benachrichtigung', 'glocken-symbol', 'abonnieren', 'abonniert'], selector: '#notification-btn' },
{ keywords: ['aktualisieren', 'refresh starten'], selector: '#refresh-btn' }, { keywords: ['aktualisieren', 'refresh starten'], selector: '#refresh-btn' },
{ keywords: ['exportieren', 'export-button', 'lagebericht exportieren'], selector: 'button[onclick*="toggleExportDropdown"]' }, { keywords: ['exportieren', 'export-button', 'lagebericht exportieren'], selector: 'button[onclick*="toggleExportDropdown"]' },
{ keywords: ['faktencheck', 'factcheck'], selector: '[gs-id="factcheck"]' }, { keywords: ['faktencheck', 'factcheck'], selector: '[gs-id="factcheck"]' },
{ keywords: ['kartenansicht', 'karte angezeigt', 'interaktive karte', 'geoparsing'], selector: '[gs-id="map"]' }, { keywords: ['kartenansicht', 'karte angezeigt', 'interaktive karte', 'geoparsing'], selector: '[gs-id="map"]' },
{ keywords: ['quellen verwalten', 'quellenverwaltung', 'quelleneinstellung', 'quellenausschluss', 'quellen-einstellung'], selector: 'button[onclick*="openSourceManagement"]' }, { keywords: ['quellen verwalten', 'quellenverwaltung', 'quelleneinstellung', 'quellenausschluss', 'quellen-einstellung'], selector: 'button[onclick*="openSourceManagement"]' },
{ keywords: ['sichtbarkeit', 'privat oder oeffentlich', 'lage privat'], selector: '#incident-settings-btn' }, { keywords: ['sichtbarkeit', 'privat oder oeffentlich', 'lage privat'], selector: '#incident-settings-btn' },
{ keywords: ['eigene lagen', 'nur eigene'], selector: '.sidebar-filter-btn[data-filter="mine"]' }, { keywords: ['eigene lagen', 'nur eigene'], selector: '.sidebar-filter-btn[data-filter="mine"]' },
{ keywords: ['alle lagen anzeigen'], selector: '.sidebar-filter-btn[data-filter="all"]' }, { keywords: ['alle lagen anzeigen'], selector: '.sidebar-filter-btn[data-filter="all"]' },
{ keywords: ['feedback senden', 'feedback geben', 'rueckmeldung'], selector: 'button[onclick*="openFeedback"]' }, { keywords: ['feedback senden', 'feedback geben', 'rueckmeldung'], selector: 'button[onclick*="openFeedback"]' },
{ keywords: ['lage loeschen', 'lage entfernen', 'fall loeschen'], selector: '#delete-incident-btn' }, { keywords: ['lage loeschen', 'lage entfernen', 'fall loeschen'], selector: '#delete-incident-btn' },
], ],
_highlightUI(text) { _highlightUI(text) {
if (!text) return; if (!text) return;
var lower = text.toLowerCase(); var lower = text.toLowerCase();
var highlighted = new Set(); var highlighted = new Set();
for (var i = 0; i < this._UI_HIGHLIGHTS.length; i++) { for (var i = 0; i < this._UI_HIGHLIGHTS.length; i++) {
var entry = this._UI_HIGHLIGHTS[i]; var entry = this._UI_HIGHLIGHTS[i];
for (var k = 0; k < entry.keywords.length; k++) { for (var k = 0; k < entry.keywords.length; k++) {
var kw = entry.keywords[k]; var kw = entry.keywords[k];
if (lower.indexOf(kw) !== -1) { if (lower.indexOf(kw) !== -1) {
var selectors = entry.selector.split(','); var selectors = entry.selector.split(',');
for (var s = 0; s < selectors.length; s++) { for (var s = 0; s < selectors.length; s++) {
var sel = selectors[s].trim(); var sel = selectors[s].trim();
if (highlighted.has(sel)) continue; if (highlighted.has(sel)) continue;
var el = document.querySelector(sel); var el = document.querySelector(sel);
if (el) { if (el) {
highlighted.add(sel); highlighted.add(sel);
el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.scrollIntoView({ behavior: 'smooth', block: 'center' });
(function(element) { (function(element) {
setTimeout(function() { setTimeout(function() {
element.classList.add('chat-ui-highlight'); element.classList.add('chat-ui-highlight');
}, 400); }, 400);
setTimeout(function() { setTimeout(function() {
element.classList.remove('chat-ui-highlight'); element.classList.remove('chat-ui-highlight');
}, 4400); }, 4400);
})(el); })(el);
} }
} }
break; break;
} }
} }
} }
}, },
async _showTutorialHint() { async _showTutorialHint() {
var container = document.getElementById('chat-messages'); var container = document.getElementById('chat-messages');
if (!container) return; if (!container) return;
// API-State laden (Fallback: Standard-Hint) // API-State laden (Fallback: Standard-Hint)
var state = null; var state = null;
try { state = await API.getTutorialState(); } catch(e) {} try { state = await API.getTutorialState(); } catch(e) {}
var hint = document.createElement('div'); var hint = document.createElement('div');
hint.className = 'chat-tutorial-hint'; hint.className = 'chat-tutorial-hint';
hint.id = 'chat-tutorial-hint'; hint.id = 'chat-tutorial-hint';
var textDiv = document.createElement('div'); var textDiv = document.createElement('div');
textDiv.className = 'chat-tutorial-hint-text'; textDiv.className = 'chat-tutorial-hint-text';
textDiv.style.cursor = 'pointer'; textDiv.style.cursor = 'pointer';
if (state && !state.completed && state.current_step !== null && state.current_step > 0) { if (state && !state.completed && state.current_step !== null && state.current_step > 0) {
// Mittendrin abgebrochen // Mittendrin abgebrochen
var totalSteps = (typeof Tutorial !== 'undefined') ? Tutorial._steps.length : 32; var totalSteps = (typeof Tutorial !== 'undefined') ? Tutorial._steps.length : 32;
textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bei Schritt ' + (state.current_step + 1) + '/' + totalSteps + ' unterbrochen. Klicken Sie hier, um fortzusetzen.'; textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bei Schritt ' + (state.current_step + 1) + '/' + totalSteps + ' unterbrochen. Klicken Sie hier, um fortzusetzen.';
textDiv.addEventListener('click', function() { textDiv.addEventListener('click', function() {
Chat.close(); Chat.close();
Chat._tutorialHintDismissed = true; Chat._tutorialHintDismissed = true;
if (typeof Tutorial !== 'undefined') Tutorial.start(); if (typeof Tutorial !== 'undefined') Tutorial.start();
}); });
} else if (state && state.completed) { } else if (state && state.completed) {
// Bereits abgeschlossen // Bereits abgeschlossen
textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bereits abgeschlossen. <span style="text-decoration:underline;">Erneut starten?</span>'; textDiv.innerHTML = '<strong>Tipp:</strong> Sie haben den Rundgang bereits abgeschlossen. <span style="text-decoration:underline;">Erneut starten?</span>';
textDiv.addEventListener('click', async function() { textDiv.addEventListener('click', async function() {
Chat.close(); Chat.close();
Chat._tutorialHintDismissed = true; Chat._tutorialHintDismissed = true;
try { await API.resetTutorialState(); } catch(e) {} try { await API.resetTutorialState(); } catch(e) {}
if (typeof Tutorial !== 'undefined') Tutorial.start(true); if (typeof Tutorial !== 'undefined') Tutorial.start(true);
}); });
} else { } else {
// Nie gestartet // Nie gestartet
textDiv.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt für Schritt alle Funktionen des Monitors. Klicken Sie hier, um ihn zu starten.'; textDiv.innerHTML = '<strong>Tipp:</strong> Kennen Sie schon den interaktiven Rundgang? Er zeigt Ihnen Schritt für Schritt alle Funktionen des Monitors. Klicken Sie hier, um ihn zu starten.';
textDiv.addEventListener('click', function() { textDiv.addEventListener('click', function() {
Chat.close(); Chat.close();
Chat._tutorialHintDismissed = true; Chat._tutorialHintDismissed = true;
if (typeof Tutorial !== 'undefined') Tutorial.start(); if (typeof Tutorial !== 'undefined') Tutorial.start();
}); });
} }
var closeBtn = document.createElement('button'); var closeBtn = document.createElement('button');
closeBtn.className = 'chat-tutorial-hint-close'; closeBtn.className = 'chat-tutorial-hint-close';
closeBtn.title = 'Schließen'; closeBtn.title = 'Schließen';
closeBtn.innerHTML = '&times;'; closeBtn.innerHTML = '&times;';
closeBtn.addEventListener('click', function(e) { closeBtn.addEventListener('click', function(e) {
e.stopPropagation(); e.stopPropagation();
hint.remove(); hint.remove();
Chat._tutorialHintDismissed = true; Chat._tutorialHintDismissed = true;
}); });
hint.appendChild(textDiv); hint.appendChild(textDiv);
hint.appendChild(closeBtn); hint.appendChild(closeBtn);
container.appendChild(hint); container.appendChild(hint);
}, },
}; };

Datei-Diff unterdrückt, da er zu groß ist Diff laden