OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

settings.php

48 KB
<?php $this->layout('layouts/admin', [
    'page_title' => 'KI-Integration – Einstellungen',
    'current_admin_page' => 'ai-chat'
]); ?>

<div class="settings-page">
    <?= $this->partial('ai-chat-tabs', ['active' => 'integration']) ?>

    <form action="/admin/settings/ai-chat" method="POST" class="form" id="aiChatSettingsForm">
        <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">

        <!-- Aktivierung & Funktionen -->
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#129302; KI-Integration') ?></span>
            </div>
            <div class="detail-card-body">
                <p class="text-muted" style="margin-top:0;">
                    <?= $this->te('Jedes Modul kann unabh&auml;ngig aktiviert werden. Alle Module nutzen den konfigurierten KI-Anbieter &amp; API-Key.') ?>
                </p>

                <!-- Genereller Datenschutzhinweis (Daten-Egress an externen LLM) -->
                <div class="ai-privacy-box" style="margin:16px 0;">
                    <span class="ai-privacy-box-icon">&#9888;&#65039;</span>
                    <div>
                        <strong><?= $this->te('Datenschutzhinweis &ndash; welche Daten an die KI gehen') ?></strong>
                        <p style="margin:8px 0 6px;">
                            <?= $this->t('Bei aktivierter KI werden je nach Modul u.&nbsp;a. folgende Inhalte an den konfigurierten KI-Anbieter &uuml;bertragen: Dokumentations-/Seiteninhalte, Ticket-Texte, Namen, E-Mail-Adressen, Telefonnummern, IP-/Hostnamen, sowie <strong>Inventar-/Seriennummern, Standorte und Kostendaten</strong>. Personenbezogene Freitext-Daten werden &uuml;ber den Anonymisierungsdienst maskiert, soweit erkannt; <strong>Inventar-/Serien-/Standort-/Kostenangaben werden bewusst <u>nicht</u> maskiert</strong>, damit die KI fachlich sinnvoll antworten kann.') ?>
                        </p>
                        <p style="margin:6px 0 0;">
                            <?= $this->t('<strong>Empfehlung:</strong> Betreiben Sie die KI <strong>lokal/selbst-gehostet</strong> (z.&nbsp;B. Ollama, LM&nbsp;Studio &ndash; eigene Endpoint-URL eintragen) oder schlie&szlig;en Sie mit dem Cloud-Anbieter einen <strong>Auftragsverarbeitungsvertrag (AVV/DPA)</strong> ab. Aktivieren Sie nur die Module, deren Daten-Weitergabe Sie bewusst akzeptieren.') ?>
                        </p>
                    </div>
                </div>

                <!-- Feature-Zeilen -->
                <div class="ai-features-list">

                    <!-- Feature 0: KI-Chat -->
                    <div class="ai-feature-row">
                        <span class="ai-feature-icon-col">&#129302;</span>
                        <div class="ai-feature-body">
                            <div class="ai-feature-top">
                                <div class="ai-feature-meta">
                                    <?= $this->t('<span class="ai-feature-name">KI-Chat</span> <span class="ai-feature-subdesc">Chat-Assistent f&uuml;r alle angemeldeten Benutzer – sensible Daten werden vor der &Uuml;bertragung automatisch anonymisiert</span>') ?>
                                </div>
                                <label class="toggle-label ai-feature-toggle-wrap">
                                    <input type="checkbox" name="chat_enabled" value="1"
                                           <?= !empty($chat_enabled) ? 'checked' : '' ?>>
                                    <span class="toggle-switch"></span>
                                </label>
                            </div>
                        </div>
                    </div>

                    <!-- Feature 1: WYSIWYG-Textverbesserung -->
                    <div class="ai-feature-row">
                        <span class="ai-feature-icon-col">&#9999;&#65039;</span>
                        <div class="ai-feature-body">
                            <div class="ai-feature-top">
                                <div class="ai-feature-meta">
                                    <?= $this->t('<span class="ai-feature-name">Textverbesserung im WYSIWYG-Editor</span> <span class="ai-feature-subdesc">KI-Button zum Umformulieren von markierten Texten direkt im Editor</span>') ?>
                                </div>
                                <label class="toggle-label ai-feature-toggle-wrap">
                                    <input type="checkbox" name="editor_improve_enabled" value="1"
                                           <?= !empty($editor_improve_enabled) ? 'checked' : '' ?>>
                                    <span class="toggle-switch"></span>
                                </label>
                            </div>
                            <div class="ai-privacy-box">
                                <span class="ai-privacy-box-icon">&#9888;&#65039;</span>
                                <div><?= $this->t('<strong>Datenschutzhinweis:</strong> Texte aus dem Editor werden <em>nicht anonymisiert</em> an die KI-API &uuml;bertragen. Empfiehlt sich ausschlie&szlig;lich in Kombination mit einer <strong>selbst-gehosteten KI</strong> (z.&nbsp;B. Ollama).') ?></div>
                            </div>
                        </div>
                    </div>

                    <!-- Feature 2: Topics-Editor KI-Assistent -->
                    <div class="ai-feature-row">
                        <span class="ai-feature-icon-col">&#10024;</span>
                        <div class="ai-feature-body">
                            <div class="ai-feature-top">
                                <div class="ai-feature-meta">
                                    <span class="ai-feature-name"><?= $this->te('KI-Assistent im Seiten-Editor') ?> <em><?= $this->te('(Topics)') ?></em></span>
                                    <span class="ai-feature-subdesc"><?= $this->te('Inhalt per Prompt im TinyMCE-Editor erstellen, bearbeiten oder erg&auml;nzen') ?></span>
                                </div>
                                <label class="toggle-label ai-feature-toggle-wrap">
                                    <input type="checkbox" name="topics_ai_enabled" value="1"
                                           <?= !empty($topics_ai_enabled) ? 'checked' : '' ?>>
                                    <span class="toggle-switch"></span>
                                </label>
                            </div>
                            <div class="ai-privacy-box">
                                <span class="ai-privacy-box-icon">&#9888;&#65039;</span>
                                <div><?= $this->t('<strong>Datenschutzhinweis:</strong> Der gesamte Editor-Inhalt (HTML) wird <em>nicht anonymisiert</em> an die KI-API &uuml;bertragen. Empfiehlt sich ausschlie&szlig;lich in Kombination mit einer <strong>selbst-gehosteten KI</strong> (z.&nbsp;B. Ollama).') ?></div>
                            </div>
                        </div>
                    </div>

                    <!-- Feature 3: Ticket KI-Antwortvorschlag -->
                    <div class="ai-feature-row">
                        <span class="ai-feature-icon-col">&#127881;</span>
                        <div class="ai-feature-body">
                            <div class="ai-feature-top">
                                <div class="ai-feature-meta">
                                    <?= $this->t('<span class="ai-feature-name"> KI-Antwortvorschlag im Ticket-Editor <span class="badge">Experimentell</span> </span> <span class="ai-feature-subdesc">KI analysiert den Ticket-Inhalt und schl&auml;gt eine passende Antwort vor</span>') ?>
                                </div>
                                <label class="toggle-label ai-feature-toggle-wrap">
                                    <input type="checkbox" name="ticket_ai_suggestions" value="1"
                                           <?= !empty($ticket_ai_suggestions) ? 'checked' : '' ?>>
                                    <span class="toggle-switch"></span>
                                </label>
                            </div>
                            <div class="ai-privacy-box">
                                <span class="ai-privacy-box-icon">&#9888;&#65039;</span>
                                <div><?= $this->t('<strong>Datenschutzhinweis:</strong> Ticket-Inhalte werden <em>unverschl&uuml;sselt und nicht anonymisiert</em> an die KI-API &uuml;bertragen. Empfiehlt sich ausschlie&szlig;lich in Kombination mit einer <strong>selbst-gehosteten KI</strong> (z.&nbsp;B. Ollama).') ?></div>
                            </div>
                            <div class="ai-ticket-context">
                                <div class="form-group">
                                    <label for="ticket_suggestion_context"><?= $this->te('Unternehmens-Kontext f&uuml;r KI-Antwortvorschl&auml;ge') ?></label>
                                    <textarea id="ticket_suggestion_context" name="ticket_suggestion_context"
                                        rows="5" class="form-control"
                                        placeholder="<?= $this->te('Hier Informationen eintragen, die die KI kennen soll – z. B.:&#10;- Taskmanager ist für Benutzer gesperrt&#10;- Passwortrichtlinie: mind. 12 Zeichen, Sonderzeichen, Großbuchstaben&#10;- Fernwartung läuft über AnyDesk, ID bitte erfragen&#10;- VPN-Zugänge werden nur durch die IT-Abteilung eingerichtet') ?>"
                                        ><?= htmlspecialchars($ticket_suggestion_context ?? '', ENT_QUOTES) ?></textarea>
                                    <small class="text-muted" style="margin-top:0.3rem;display:block">
                                        <?= $this->te('Dieser Text wird als zus&auml;tzlicher Kontext in jeden KI-Antwortvorschlag eingebunden – z.&nbsp;B. gesperrte Tools, Passwortrichtlinien, interne Prozesse. Einfach stichpunktartig, kein Markdown.') ?>
                                    </small>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- Feature 4: Ticket-Stimmungsanalyse -->
                    <div class="ai-feature-row ai-feature-row--last">
                        <span class="ai-feature-icon-col">&#128578;</span>
                        <div class="ai-feature-body">
                            <div class="ai-feature-top">
                                <div class="ai-feature-meta">
                                    <?= $this->t('<span class="ai-feature-name"> Stimmungsanalyse in der Ticketliste <span class="badge">Experimentell</span> </span> <span class="ai-feature-subdesc">KI bewertet die Stimmung der ersten Ticket-Nachricht und zeigt sie als Smiley in der Ticketliste</span>') ?>
                                </div>
                                <label class="toggle-label ai-feature-toggle-wrap">
                                    <input type="checkbox" name="ticket_sentiment_enabled" value="1"
                                           <?= !empty($ticket_sentiment_enabled) ? 'checked' : '' ?>>
                                    <span class="toggle-switch"></span>
                                </label>
                            </div>
                            <div class="ai-privacy-box">
                                <span class="ai-privacy-box-icon">&#9888;&#65039;</span>
                                <div><?= $this->t('<strong>Datenschutzhinweis:</strong> Zur Analyse wird der Anfang der ersten Ticket-Nachricht an die KI-API &uuml;bertragen (Namen/E-Mail-Adressen werden je nach Datenschutz-Modus anonymisiert). Empfiehlt sich besonders in Kombination mit einer <strong>selbst-gehosteten KI</strong> (z.&nbsp;B. Ollama). Die Analyse l&auml;uft im Hintergrund &uuml;ber den Scheduler.') ?></div>
                            </div>
                        </div>
                    </div>

                </div><!-- /.ai-features-list -->
            </div>
        </div>

        <!-- Anbieter & API-Keys -->
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#128272; KI-Anbieter &amp; API-Keys') ?></span>
            </div>
            <div class="detail-card-body">
                <p class="text-muted" style="margin-top:0;"><?= $this->te('API-Keys werden verschl&uuml;sselt gespeichert (AES-256-GCM).') ?></p>

                <div class="form-group">
                    <label for="ai_provider"><?= $this->te('KI-Anbieter') ?></label>
                    <select id="ai_provider" name="ai_provider" class="form-control" onchange="updateModelList()">
                        <?php foreach ($providers as $key => $label): ?>
                            <option value="<?= $this->e($key) ?>"
                                    <?= ($ai_config['provider'] ?? 'openai') === $key ? 'selected' : '' ?>>
                                <?= $this->e($label) ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>

                <!-- OpenAI -->
                <div id="openai-section" class="provider-section" style="display:none">
                    <div class="form-group">
                        <label><?= $this->te('OpenAI API-Key') ?></label>
                        <?php if ($openai_key_set): ?>
                            <div class="api-key-set">
                                <span class="api-key-indicator"><?= $this->te('&#10003; API-Key hinterlegt') ?></span>
                                <label class="toggle-label" style="margin-top:0.5rem">
                                    <input type="checkbox" name="remove_openai_key" value="1">
                                    <span class="toggle-switch"></span>
                                    <?= $this->te('API-Key entfernen') ?>
                                </label>
                            </div>
                        <?php endif; ?>
                        <input type="password" name="openai_api_key" class="form-control"
                               placeholder="<?= $openai_key_set ? $this->te('Neuen API-Key eingeben zum &Auml;ndern') : 'sk-...' ?>"
                               autocomplete="new-password">
                        <small class="text-muted" style="display:block">
                            <?= $this->te('API-Key von') ?> <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener">platform.openai.com</a>
                        </small>
                    </div>
                </div>

                <!-- Anthropic -->
                <div id="anthropic-section" class="provider-section" style="display:none">
                    <div class="form-group">
                        <label><?= $this->te('Anthropic API-Key') ?></label>
                        <?php if ($anthropic_key_set): ?>
                            <div class="api-key-set">
                                <span class="api-key-indicator"><?= $this->te('&#10003; API-Key hinterlegt') ?></span>
                                <label class="toggle-label" style="margin-top:0.5rem">
                                    <input type="checkbox" name="remove_anthropic_key" value="1">
                                    <span class="toggle-switch"></span>
                                    <?= $this->te('API-Key entfernen') ?>
                                </label>
                            </div>
                        <?php endif; ?>
                        <input type="password" name="anthropic_api_key" class="form-control"
                               placeholder="<?= $anthropic_key_set ? 'Neuen API-Key eingeben zum &Auml;ndern' : 'sk-ant-...' ?>"
                               autocomplete="new-password">
                        <small class="text-muted" style="display:block">
                            <?= $this->te('API-Key von') ?> <a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener">console.anthropic.com</a>
                        </small>
                    </div>
                </div>

                <!-- DeepSeek -->
                <div id="deepseek-section" class="provider-section" style="display:none">
                    <div class="form-group">
                        <label><?= $this->te('DeepSeek API-Key') ?></label>
                        <?php if ($deepseek_key_set): ?>
                            <div class="api-key-set">
                                <span class="api-key-indicator"><?= $this->te('&#10003; API-Key hinterlegt') ?></span>
                                <label class="toggle-label" style="margin-top:0.5rem">
                                    <input type="checkbox" name="remove_deepseek_key" value="1">
                                    <span class="toggle-switch"></span>
                                    <?= $this->te('API-Key entfernen') ?>
                                </label>
                            </div>
                        <?php endif; ?>
                        <input type="password" name="deepseek_api_key" class="form-control"
                               placeholder="<?= $deepseek_key_set ? 'Neuen API-Key eingeben zum &Auml;ndern' : 'sk-...' ?>"
                               autocomplete="new-password">
                        <small class="text-muted" style="display:block">
                            <?= $this->te('API-Key von') ?> <a href="https://platform.deepseek.com/api_keys" target="_blank" rel="noopener">platform.deepseek.com</a>
                        </small>
                    </div>
                </div>

                <!-- Self-hosted / OpenAI-kompatibel -->
                <div id="openai_compatible-section" class="provider-section" style="display:none">
                    <div class="form-group">
                        <label for="custom_base_url"><?= $this->te('Server-URL') ?></label>
                        <input type="url" id="custom_base_url" name="custom_base_url" class="form-control"
                               value="<?= $this->e($ai_config['custom_base_url'] ?? '') ?>"
                               placeholder="http://localhost:11434">
                        <small class="text-muted" style="display:block">
                            <?= $this->t('Basis-URL des lokalen Servers. <code>/v1/chat/completions</code> wird automatisch angeh&auml;ngt.<br> Beispiele: Ollama <code>http://localhost:11434</code> &nbsp;|&nbsp; LM Studio <code>http://localhost:1234</code> &nbsp;|&nbsp; vLLM <code>http://localhost:8000</code>') ?>
                        </small>
                    </div>
                    <div class="form-group">
                        <label><?= $this->te('API-Key (optional)') ?></label>
                        <?php if ($custom_key_set): ?>
                            <div class="api-key-set">
                                <span class="api-key-indicator"><?= $this->te('&#10003; API-Key hinterlegt') ?></span>
                                <label class="toggle-label" style="margin-top:0.5rem">
                                    <input type="checkbox" name="remove_custom_key" value="1">
                                    <span class="toggle-switch"></span>
                                    <?= $this->te('API-Key entfernen') ?>
                                </label>
                            </div>
                        <?php endif; ?>
                        <input type="password" name="custom_api_key" class="form-control"
                               placeholder="<?= $custom_key_set ? 'Neuen Key eingeben zum &Auml;ndern' : 'Leer lassen wenn kein Key ben&ouml;tigt' ?>"
                               autocomplete="new-password">
                        <small class="text-muted" style="display:block"><?= $this->te('Bei lokalen Servern ohne Authentifizierung leer lassen.') ?></small>
                    </div>
                    <div class="form-group">
                        <label class="toggle-label">
                            <input type="checkbox" name="disable_tools" value="1"
                                   <?= !empty($ai_config['disable_tools']) ? 'checked' : '' ?>>
                            <span class="toggle-switch"></span>
                            <?= $this->te('Tool-Calling deaktivieren') ?>
                        </label>
                        <small class="text-muted" style="display:block;margin-top:0.3rem">
                            <?= $this->te('Aktivieren wenn das Modell kein Function-Calling unterst&uuml;tzt (z.B. reine Text-Modelle). Der KI-Assistent beantwortet dann Fragen ohne Datenbankzugriff.') ?>
                        </small>
                    </div>
                </div>
            </div>
        </div>

        <!-- Datenschutz -->
        <?php $privacyMode = $ai_config['privacy_mode'] ?? 'auto'; ?>
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#128737; Datenschutz-Modus') ?></span>
            </div>
            <div class="detail-card-body">
                <p class="text-muted" style="margin-top:0;"><?= $this->te('Steuert, ob Daten vor dem Senden an die KI anonymisiert werden.') ?></p>
                <div class="form-group">
                    <label for="privacy_mode"><?= $this->te('Anonymisierung') ?></label>
                    <select id="privacy_mode" name="privacy_mode" class="form-control">
                        <option value="auto" <?= $privacyMode === 'auto' ? 'selected' : '' ?>><?= $this->te('Automatisch (empfohlen)') ?></option>
                        <option value="on"   <?= $privacyMode === 'on'   ? 'selected' : '' ?>><?= $this->te('Immer an') ?></option>
                        <option value="off"  <?= $privacyMode === 'off'  ? 'selected' : '' ?>><?= $this->te('Immer aus') ?></option>
                    </select>
                    <small class="text-muted" style="display:block;margin-top:0.4rem">
                        <?= $this->t('<strong>Automatisch:</strong> bei Cloud-Anbietern (OpenAI, Anthropic, DeepSeek) <em>an</em> &ndash; Namen, IPs, Hosts und Firmen werden durch Platzhalter ersetzt, bevor sie das Haus verlassen. Bei selbst-gehosteten Modellen <em>aus</em>, da die Daten lokal bleiben.<br> <strong>Immer an</strong> maximiert den Datenschutz, senkt aber die Antwortqualit&auml;t (das Modell sieht nur Platzhalter). <strong>Immer aus</strong> liefert die besten Antworten &ndash; bei Cloud-Anbietern werden dann Klartext-Daten &uuml;bertragen.<br> Passw&ouml;rter, API-Keys und Vault-Eintr&auml;ge werden in <em>jedem</em> Modus niemals an die KI gesendet.') ?>
                    </small>
                </div>
                <div class="form-group">
                    <label class="toggle-label">
                        <input type="checkbox" name="experimental_actions" value="1"
                               <?= !empty($ai_config['experimental_actions']) ? 'checked' : '' ?>>
                        <span class="toggle-switch"></span>
                        <?= $this->te('Experimentelle Aktionen erlauben') ?>
                    </label>
                    <small class="text-muted" style="display:block;margin-top:0.3rem">
                        <?= $this->te('Erlaubt der KI, auch riskantere Aktionen vorzuschlagen (Doku-Seiten anlegen/&auml;ndern, VM-Status, Lagerbestand, Lizenzen, Asset-&Uuml;bergabe). Jede Aktion muss weiterhin manuell best&auml;tigt werden und erfordert die passende Berechtigung.') ?>
                    </small>
                </div>
                <div class="form-group">
                    <label for="monthly_cost_cap_eur"><?= $this->te('Budget pro Nutzer / Monat ({cur})', ['cur' => $this->currency()]) ?></label>
                    <input type="number" step="0.01" min="0" id="monthly_cost_cap_eur" name="monthly_cost_cap_eur"
                           class="form-control" value="<?= htmlspecialchars((string)($ai_config['monthly_cost_cap_eur'] ?? '')) ?>"
                           placeholder="<?= $this->te('0 = unbegrenzt') ?>">
                    <small class="text-muted" style="display:block"><?= $this->te('Ist das Budget eines Nutzers im laufenden Monat aufgebraucht, werden weitere Chat-Anfragen blockiert (nur Cloud-Anbieter, selbst-gehostet = kostenlos).') ?></small>
                </div>
                <div class="form-group">
                    <label for="monthly_cost_cap_total_eur"><?= $this->te('Gesamt-Budget / Monat ({cur})', ['cur' => $this->currency()]) ?></label>
                    <input type="number" step="0.01" min="0" id="monthly_cost_cap_total_eur" name="monthly_cost_cap_total_eur"
                           class="form-control" value="<?= htmlspecialchars((string)($ai_config['monthly_cost_cap_total_eur'] ?? '')) ?>"
                           placeholder="<?= $this->te('0 = unbegrenzt') ?>">
                    <small class="text-muted" style="display:block"><?= $this->te('Globales Limit &uuml;ber alle Nutzer. 0 = unbegrenzt.') ?></small>
                </div>
                <div class="form-group">
                    <label for="ai_usd_rate"><?= $this->te('Umrechnungskurs USD → {cur}', ['cur' => $this->currency()]) ?></label>
                    <input type="text" inputmode="decimal" id="ai_usd_rate" name="ai_usd_rate"
                           class="form-control" value="<?= $this->e((string)($usd_kurs ?? '')) ?>">
                    <small class="text-muted" style="display:block"><?= $this->te('Die Preislisten der Anbieter stehen in USD; die Kostenschätzung rechnet damit in die Währung der Instanz um.') ?></small>
                </div>
                <div class="form-group">
                    <label for="context_window"><?= $this->te('Kontextfenster-Override (Tokens)') ?></label>
                    <input type="number" step="1" min="0" id="context_window" name="context_window"
                           class="form-control" value="<?= htmlspecialchars((string)($ai_config['context_window'] ?? '')) ?>"
                           placeholder="<?= $this->te('0 = automatisch') ?>">
                    <small class="text-muted" style="display:block"><?= $this->te('Nur n&ouml;tig, wenn das automatische Limit nicht passt (z.B. ein kleines lokales Modell mit 4096 Tokens).') ?></small>
                </div>
            </div>
        </div>

        <!-- Semantische Suche -->
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#129518; Semantische Doku-Suche (experimentell)') ?></span>
            </div>
            <div class="detail-card-body">
                <p class="text-muted" style="margin-top:0;"><?= $this->te('Findet Doku-Seiten nach Bedeutung statt nur nach Stichwort (Embeddings).') ?></p>
                <div class="form-group">
                    <label class="toggle-label">
                        <input type="checkbox" name="semantic_search" value="1"
                               <?= !empty($ai_config['semantic_search']) ? 'checked' : '' ?>>
                        <span class="toggle-switch"></span>
                        <?= $this->te('Semantische Suche aktivieren') ?>
                    </label>
                    <small class="text-muted" style="display:block;margin-top:0.3rem">
                        <?= $this->t('Erg&auml;nzt die Volltextsuche um &auml;hnlichkeitsbasierte Treffer. Erfordert ein Embedding-Modell (OpenAI oder ein lokales Modell wie <code>nomic-embed-text</code>) und eine einmalige Indizierung. Anthropic/DeepSeek bieten keine Embeddings.') ?>
                    </small>
                </div>
                <div class="form-group">
                    <label for="embedding_model"><?= $this->te('Embedding-Modell') ?></label>
                    <input type="text" id="embedding_model" name="embedding_model" class="form-control"
                           value="<?= htmlspecialchars((string)($ai_config['embedding_model'] ?? '')) ?>"
                           placeholder="<?= $this->te('z.B. text-embedding-3-small (OpenAI) oder nomic-embed-text (lokal)') ?>">
                </div>
                <div class="form-group">
                    <button type="button" class="btn btn-secondary" id="aiReindexBtn"><?= $this->te('Seiten jetzt indizieren') ?></button>
                    <span id="aiReindexStatus" style="margin-left:0.6rem;color:var(--gray-600)"></span>
                </div>
            </div>
        </div>

        <!-- API-Auto: Web-Suche -->
        <!-- Nur sinnvoll, wenn API-Auto- UND KI-Modul aktiv sind; die Funktion
             wirkt zudem nur mit Anthropic (Provider-Abhängigkeit per JS). -->
        <?php if (($modules['api_auto'] ?? false) && ($modules['ai_chat'] ?? false)): ?>
        <div class="detail-card" id="apiAutoWebSearchCard" style="display:none">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#127760; API-Auto: Web-Suche') ?></span>
            </div>
            <div class="detail-card-body">
                <p class="text-muted" style="margin-top:0;"><?= $this->te('Recherchiert die API-Dokumentation im Web, wenn beim Modul „API-Auto" keine Beschreibung hochgeladen wurde.') ?></p>
                <div class="form-group">
                    <label class="toggle-label">
                        <input type="checkbox" name="api_auto_web_search" value="1"
                               <?= ($ai_config['api_auto_web_search'] ?? true) ? 'checked' : '' ?>>
                        <span class="toggle-switch"></span>
                        <?= $this->te('Web-Suche im Wissens-Fallback aktivieren') ?>
                    </label>
                    <small class="text-muted" style="display:block;margin-top:0.3rem">
                        <?= $this->t('Gibst du beim Anlegen einer API-Auto-Verbindung nur einen Namen/eine URL (ohne Beschreibung) an, sucht die KI die offizielle API-Dokumentation im Web. <strong>Nur mit Anthropic (Claude) wirksam</strong> &ndash; bei OpenAI, DeepSeek oder lokalen Modellen wird automatisch der reine Wissens-Modus genutzt. Verursacht zus&auml;tzliche Anthropic-Kosten.') ?>
                    </small>
                </div>
            </div>
        </div>
        <script>
        (function () {
            // Karte nur einblenden, wenn als Provider Anthropic gewählt ist –
            // reagiert live auf die Provider-Auswahl (noch vor dem Speichern).
            var card = document.getElementById('apiAutoWebSearchCard');
            var sel  = document.getElementById('ai_provider');
            if (!card || !sel) { return; }
            function sync() { card.style.display = (sel.value === 'anthropic') ? '' : 'none'; }
            sel.addEventListener('change', sync);
            sync();
        })();
        </script>
        <?php endif; ?>
        <script>
        (function () {
            var btn = document.getElementById('aiReindexBtn');
            var out = document.getElementById('aiReindexStatus');
            if (!btn) return;
            var csrf = (document.querySelector('input[name="_csrf"]') || {}).value || '';
            var total = 0;
            function run() {
                var fd = new FormData(); fd.append('_csrf', csrf);
                fetch('/admin/settings/ai-chat/reindex', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
                    .then(function (r) { return r.json(); })
                    .then(function (d) {
                        if (d.error) { out.textContent = 'Fehler: ' + d.error; btn.disabled = false; return; }
                        total += (d.indexed || 0);
                        out.textContent = total + ' indiziert, ' + (d.remaining || 0) + ' verbleibend …';
                        if (d.remaining > 0 && (d.indexed || 0) > 0) { run(); }
                        else { out.textContent = 'Fertig: ' + total + ' Seiten indiziert.'; btn.disabled = false; }
                    })
                    .catch(function () { out.textContent = 'Netzwerkfehler.'; btn.disabled = false; });
            }
            btn.addEventListener('click', function () { btn.disabled = true; total = 0; out.textContent = 'Indiziere …'; run(); });
        })();
        </script>

        <!-- Website-Bot (WordPress) -->
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#127760; Website-Bot (WordPress)') ?></span>
            </div>
            <div class="detail-card-body">
                <p class="text-muted" style="margin-top:0;"><?= $this->te('Ein eng begrenzter Assistent f&uuml;r die &ouml;ffentliche Website (Bubble &amp; Shortcode im WordPress-Plugin).') ?></p>
                <div class="form-group">
                    <label class="toggle-label">
                        <input type="checkbox" name="bot_enabled" value="1"
                               <?= !empty($ai_config['bot_enabled']) ? 'checked' : '' ?>>
                        <span class="toggle-switch"></span>
                        <?= $this->te('Website-Bot aktivieren') ?>
                    </label>
                    <small class="text-muted" style="display:block;margin-top:0.3rem">
                        <?= $this->t('Beantwortet nur <strong>einfache allgemeine PC-/IT-Fragen</strong> und Fragen zur <strong>freigegebenen Dokumentation</strong> (Themen-Whitelist unter <a href="/admin/api">API &amp; WordPress</a>). Keine internen Daten, keine Aktionen. Das WordPress-Plugin ben&ouml;tigt einen Token mit Scope <code>assistant</code>.') ?>
                    </small>
                </div>
                <div class="form-group">
                    <label for="bot_intro"><?= $this->te('Begr&uuml;&szlig;ung / Zusatzkontext (optional)') ?></label>
                    <textarea id="bot_intro" name="bot_intro" class="form-control" rows="2"
                              placeholder="<?= $this->te('z.B. Firmenname, Tonalit&auml;t, Hinweis auf das IT-Team …') ?>"><?= htmlspecialchars((string)($ai_config['bot_intro'] ?? '')) ?></textarea>
                </div>
                <div class="form-group">
                    <label for="bot_max_tokens"><?= $this->te('Maximale Antwortl&auml;nge (Tokens)') ?></label>
                    <input type="number" step="1" min="128" max="1500" id="bot_max_tokens" name="bot_max_tokens"
                           class="form-control" value="<?= htmlspecialchars((string)($ai_config['bot_max_tokens'] ?? 700)) ?>">
                </div>
            </div>
        </div>

        <!-- Modell & Limits -->
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#9881; Modell &amp; Limits') ?></span>
            </div>
            <div class="detail-card-body">
                <div class="form-row-2">
                    <div class="form-group">
                        <label for="ai_model"><?= $this->te('Modell') ?></label>
                        <!-- Dropdown für OpenAI / Anthropic -->
                        <select id="ai_model" name="ai_model" class="form-control">
                            <?php foreach ($models as $providerKey => $providerModels): ?>
                                <?php foreach ($providerModels as $modelId => $modelLabel): ?>
                                    <option value="<?= $this->e($modelId) ?>"
                                            data-provider="<?= $this->e($providerKey) ?>"
                                            <?= ($ai_config['model'] ?? '') === $modelId ? 'selected' : '' ?>>
                                        <?= $this->e($modelLabel) ?>
                                    </option>
                                <?php endforeach; ?>
                            <?php endforeach; ?>
                        </select>
                        <!-- Freitext für self-hosted -->
                        <input type="text" id="ai_model_custom" name="ai_model_custom" class="form-control"
                               style="display:none"
                               value="<?= $this->e(($ai_config['provider'] ?? '') === 'openai_compatible' ? ($ai_config['model'] ?? '') : '') ?>"
                               placeholder="<?= $this->te('z.B. llama3, mistral, phi3, gemma2 …') ?>">
                        <small class="text-muted" id="ai_model_custom_hint" style="display:none">
                            <?= $this->te('Modellname genau wie vom Server erwartet (Gro&szlig;-/Kleinschreibung beachten).') ?>
                        </small>
                    </div>
                    <div class="form-group">
                        <label><?= $this->te('Max. Token pro Funktion') ?></label>
                        <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.75rem;margin-top:0.25rem">
                            <div>
                                <label for="ai_chat_max_tokens" style="font-size:0.85rem;font-weight:500;margin-bottom:0.25rem;display:block"><?= $this->te('&#128172; Chat') ?></label>
                                <input type="number" id="ai_chat_max_tokens" name="ai_chat_max_tokens" class="form-control"
                                       value="<?= (int)($ai_config['chat_max_tokens'] ?? 1024) ?>"
                                       min="256" max="4096" step="256">
                                <small class="text-muted"><?= $this->te('Max. 4096') ?></small>
                            </div>
                            <div>
                                <label for="ai_improve_max_tokens" style="font-size:0.85rem;font-weight:500;margin-bottom:0.25rem;display:block"><?= $this->te('&#9999;&#65039; Textverbesserung') ?></label>
                                <input type="number" id="ai_improve_max_tokens" name="ai_improve_max_tokens" class="form-control"
                                       value="<?= (int)($ai_config['improve_max_tokens'] ?? 2048) ?>"
                                       min="256" max="4096" step="256">
                                <small class="text-muted"><?= $this->te('Max. 4096') ?></small>
                            </div>
                            <div>
                                <label for="ai_page_content_max_tokens" style="font-size:0.85rem;font-weight:500;margin-bottom:0.25rem;display:block"><?= $this->te('&#128196; Seiten-Generator') ?></label>
                                <input type="number" id="ai_page_content_max_tokens" name="ai_page_content_max_tokens" class="form-control"
                                       value="<?= (int)($ai_config['page_content_max_tokens'] ?? 4096) ?>"
                                       min="256" max="8192" step="256">
                                <small class="text-muted"><?= $this->te('Max. 8192') ?></small>
                            </div>
                        </div>
                        <small class="text-muted" style="margin-top:0.5rem;display:block"><?= $this->te('H&ouml;here Werte = l&auml;ngere Antworten &amp; h&ouml;here API-Kosten. Chat niedrig halten verhindert Timeouts.') ?></small>
                        <!-- Globaler Fallback-Wert (wird aus dem h&ouml;chsten der drei Werte berechnet) -->
                        <input type="hidden" id="ai_max_tokens" name="ai_max_tokens"
                               value="<?= (int)($ai_config['max_tokens'] ?? 1024) ?>">
                    </div>
                </div>

                <!-- Verbindungstest -->
                <div class="form-group" style="margin-top:1rem">
                    <button type="button" class="btn btn-secondary" id="testConnectionBtn">
                        <?= $this->te('&#128202; Verbindung testen') ?>
                    </button>
                    <span id="testResult" style="margin-left:1rem;font-weight:500"></span>
                </div>
            </div>
        </div>

        <!-- Datenschutz-Hinweis -->
        <div class="detail-card">
            <div class="detail-card-header">
                <span class="detail-card-title"><?= $this->te('&#128274; Datenschutz &amp; Anonymisierung') ?></span>
            </div>
            <div class="detail-card-body">
                <p><?= $this->t('Folgende Daten werden <strong>automatisch anonymisiert</strong>, bevor sie die externe KI-API erreichen:') ?></p>
                <ul style="margin:0.5rem 0 0 1.5rem;line-height:1.8">
                    <li><?= $this->t('Namen von Benutzern und Ansprechpartnern → <code>[PERSON_X]</code>') ?></li>
                    <li><?= $this->t('E-Mail-Adressen → <code>[EMAIL_X]</code>') ?></li>
                    <li><?= $this->t('IP-Adressen und Netzwerkadressen → <code>[IP_X]</code>, <code>[SUBNET_X]</code>') ?></li>
                    <li><?= $this->t('Server- und Hostnamen → <code>[HOST_X]</code>') ?></li>
                    <li><?= $this->t('Firmennamen → <code>[COMPANY_X]</code>') ?></li>
                    <li><?= $this->t('Vertragsbezeichnungen → <code>[CONTRACT_X]</code>') ?></li>
                </ul>
                <p style="margin-top:0.75rem"><?= $this->t('<strong>Niemals</strong> &uuml;bertragen werden') ?>:</p>
                <ul style="margin:0.5rem 0 0 1.5rem;line-height:1.8">
                    <li><?= $this->te('Vault-Eintr&auml;ge (Passw&ouml;rter, Zugangsdaten)') ?></li>
                    <li><?= $this->te('API-Keys, Zertifikate, kryptografische Schl&uuml;ssel') ?></li>
                    <li><?= $this->te('Audit-Log-Rohdaten') ?></li>
                    <li><?= $this->te('Session-Daten') ?></li>
                </ul>
                <p class="text-muted" style="margin-top:0.75rem">
                    <?= $this->te('Die KI-Antworten werden auf dem Server de-anonymisiert und erst dann dem Benutzer angezeigt. Die KI selbst sieht zu keinem Zeitpunkt echte Klarnamen, IPs oder sensible Bezeichnungen.') ?>
                </p>
            </div>
        </div>

        <div class="form-actions">
            <button type="submit" class="btn btn-primary"><?= $this->te('&#128190; Einstellungen speichern') ?></button>
        </div>
    </form>
</div>

<script>
// Modell-Daten
const allModels = <?= json_encode($models) ?>;
const currentProvider = <?= json_encode($ai_config['provider'] ?? 'openai') ?>;
const currentModel    = <?= json_encode($ai_config['model'] ?? '') ?>;

function updateModelList() {
    const provider    = document.getElementById('ai_provider').value;
    const modelSel    = document.getElementById('ai_model');
    const modelCustom = document.getElementById('ai_model_custom');
    const modelHint   = document.getElementById('ai_model_custom_hint');
    const provModels  = allModels[provider] || {};

    if (provider === 'openai_compatible') {
        // Freitext-Eingabe. Das Dropdown wird deaktiviert, damit es seinen
        // Wert nicht mitsendet und den Freitext serverseitig überschreibt.
        modelSel.style.display    = 'none';
        modelSel.disabled         = true;
        modelCustom.style.display = '';
        modelHint.style.display   = '';
    } else {
        modelSel.style.display    = '';
        modelSel.disabled         = false;
        modelCustom.style.display = 'none';
        modelHint.style.display   = 'none';

        // Dropdown befüllen
        modelSel.innerHTML = '';
        for (const [id, label] of Object.entries(provModels)) {
            const opt = document.createElement('option');
            opt.value       = id;
            opt.textContent = label;
            if (id === currentModel) opt.selected = true;
            modelSel.appendChild(opt);
        }
        if (modelSel.options.length > 0 && !modelSel.value) {
            modelSel.options[0].selected = true;
        }
    }

    // Provider-Sections ein-/ausblenden
    document.querySelectorAll('.provider-section').forEach(s => s.style.display = 'none');
    const section = document.getElementById(provider + '-section');
    if (section) section.style.display = '';
}

// Initial beim Laden
document.addEventListener('DOMContentLoaded', function() {
    updateModelList();

    // Verbindungstest
    document.getElementById('testConnectionBtn').addEventListener('click', function() {
        const btn    = this;
        const result = document.getElementById('testResult');
        btn.disabled = true;
        result.textContent = 'Teste...';
        result.style.color = '#666';

        fetch('/admin/settings/ai-chat/test', {
            headers: {'X-Requested-With': 'XMLHttpRequest'}
        })
        .then(r => r.json())
        .then(data => {
            if (data.success) {
                result.textContent = '✓ ' + (data.reply || 'Verbindung erfolgreich');
                result.style.color = '#28a745';
            } else {
                result.textContent = '✗ ' + (data.error || 'Fehler');
                result.style.color = '#dc3545';
            }
        })
        .catch(() => {
            result.textContent = '✗ Netzwerkfehler';
            result.style.color = '#dc3545';
        })
        .finally(() => { btn.disabled = false; });
    });
});
</script>

<style>
/* ─── Feature-Liste (modul-spezifisch: zeilige Toggle-Liste) ─────────────── */
.ai-features-list { border-top: 1px solid var(--border, var(--gray-200)); margin-top: 1rem; }

.ai-feature-row {
    display: flex;
    gap: 1rem;
    padding: 1.1rem 0;
    border-bottom: 1px solid var(--border, var(--gray-200));
    transition: background .15s;
}
.ai-feature-row--last { border-bottom: none; }

.ai-feature-icon-col {
    font-size: 1.55rem;
    line-height: 1;
    flex-shrink: 0;
    width: 2rem;
    text-align: center;
    padding-top: .15rem;
}
.ai-feature-body { flex: 1; min-width: 0; }

.ai-feature-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}
.ai-feature-meta { flex: 1; }

.ai-feature-name {
    display: block;
    font-weight: 600;
    font-size: .95rem;
    color: var(--text-primary, var(--gray-900));
    line-height: 1.3;
}
.ai-feature-subdesc {
    display: block;
    font-size: .8rem;
    color: var(--text-muted, var(--gray-500));
    margin-top: .2rem;
    line-height: 1.4;
}
.ai-feature-toggle-wrap {
    flex-shrink: 0;
    margin: .1rem 0 0 !important;
}

/* ─── Datenschutz-Box (Warn-Callout, modul-spezifisch) ──────────────────── */
.ai-privacy-box {
    display: flex;
    gap: .6rem;
    background: var(--amber-50);
    border: 1px solid var(--amber-300);
    border-radius: 6px;
    padding: .5rem .75rem;
    font-size: .8rem;
    color: var(--amber-800);
    line-height: 1.55;
    margin-top: .65rem;
}
.ai-privacy-box-icon { flex-shrink: 0; line-height: 1.55; }

/* ─── Ticket-Kontext-Block ──────────────────────────────────────────────── */
.ai-ticket-context {
    margin-top: .9rem;
    padding-top: .9rem;
    border-top: 1px dashed var(--border, var(--gray-200));
}

/* iOS-Style Toggle-Switches (Feature-An/Aus) – keine zentrale Komponente */
.toggle-label {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}
.toggle-label input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}
.toggle-switch {
    position: relative;
    display: inline-block;
    width: 44px;
    height: 24px;
    flex-shrink: 0;
    background: var(--slate-300);
    border-radius: 9999px;
    transition: background .2s ease;
}
.toggle-switch::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    background: var(--color-on-accent);
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
    transition: transform .2s ease;
}
.toggle-label input[type="checkbox"]:checked + .toggle-switch {
    background: var(--color-primary, var(--color-primary));
}
.toggle-label input[type="checkbox"]:checked + .toggle-switch::after {
    transform: translateX(20px);
}
.toggle-label input[type="checkbox"]:focus-visible + .toggle-switch {
    box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), .35);
}
.toggle-label input[type="checkbox"]:disabled + .toggle-switch {
    opacity: .5;
    cursor: not-allowed;
}

/* API-Key-Status-Anzeige */
.api-key-set {
    display: flex;
    flex-direction: column;
    gap: .25rem;
}
.api-key-indicator {
    font-size: .85rem;
    font-weight: 600;
    color: var(--green-700);
}
</style>