OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

assistant.js

7 KB
/**
 * OpenNIT Website-Assistent (Bubble + Inline-Shortcode).
 *
 * Spricht ausschließlich über admin-ajax (Action: opennit_assistant_ask) mit
 * WordPress; der OpenNIT-Token bleibt server-seitig. Vanilla JS, keine Abhängigkeiten.
 */
(function () {
    'use strict';

    var CFG = window.OpenNITAssistant || {};
    if (!CFG.ajaxUrl) { return; }

    function esc(s) {
        return String(s == null ? '' : s)
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    // Minimales Markdown: **fett**, `code`, Zeilenumbrüche, - Listen
    function format(text) {
        var html = esc(text)
            .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
            .replace(/`([^`]+)`/g, '<code>$1</code>');
        html = html.replace(/((?:^- .+(?:\n|$))+)/gm, function (block) {
            var items = block.trim().replace(/^- (.+)$/gm, '<li>$1</li>');
            return '<ul>' + items + '</ul>';
        });
        return html.replace(/\n/g, '<br>');
    }

    function buildPanel(root, mode) {
        var panel = document.createElement('div');
        panel.className = 'opennit-asst-panel';
        panel.innerHTML =
            '<div class="opennit-asst-header">' + esc(CFG.title || 'IT-Assistent') +
                (mode === 'bubble' ? '<button type="button" class="opennit-asst-close" aria-label="Schließen">&times;</button>' : '') +
            '</div>' +
            '<div class="opennit-asst-messages"></div>' +
            '<form class="opennit-asst-input">' +
                '<input type="text" maxlength="1000" placeholder="Frage eingeben…" aria-label="Frage" autocomplete="off">' +
                '<button type="submit" aria-label="Senden">&#10148;</button>' +
            '</form>';
        root.appendChild(panel);

        var messages = panel.querySelector('.opennit-asst-messages');
        var form     = panel.querySelector('.opennit-asst-input');
        var input    = form.querySelector('input');
        var sendBtn  = form.querySelector('button');
        var busy     = false;
        // Konversationsverlauf (ohne Begrüßung) für Mehr-Runden-Dialoge.
        var history  = [];

        if (CFG.welcome) { append('assistant', CFG.welcome); }

        function append(role, text) {
            var el = document.createElement('div');
            el.className = 'opennit-asst-msg opennit-asst-msg-' + role;
            el.innerHTML = '<div class="opennit-asst-bubble">' +
                (role === 'assistant' ? format(text) : esc(text)) + '</div>';
            messages.appendChild(el);
            messages.scrollTop = messages.scrollHeight;
            return el;
        }

        // Animierter „tippt…"-Indikator (drei pulsierende Punkte).
        function appendTyping() {
            var el = document.createElement('div');
            el.className = 'opennit-asst-msg opennit-asst-msg-assistant';
            el.innerHTML = '<div class="opennit-asst-bubble opennit-asst-typing" aria-label="Assistent schreibt">' +
                '<span></span><span></span><span></span></div>';
            messages.appendChild(el);
            messages.scrollTop = messages.scrollHeight;
            return el;
        }

        function send(text) {
            if (busy || !text.trim()) { return; }
            busy = true; sendBtn.disabled = true;
            append('user', text);
            input.value = '';
            var typing = appendTyping();

            var fd = new FormData();
            fd.append('action', 'opennit_assistant_ask');
            fd.append('nonce', CFG.nonce);
            fd.append('message', text);
            fd.append('history', JSON.stringify(history.slice(-12)));

            fetch(CFG.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' })
                .then(function (r) { return r.json(); })
                .then(function (res) {
                    typing.remove();
                    if (res && res.success) {
                        var answer = res.data.answer || '(keine Antwort)';
                        append('assistant', answer);
                        history.push({ role: 'user', content: text });
                        history.push({ role: 'assistant', content: answer });
                    } else {
                        append('assistant', (res && res.data) ? String(res.data) : 'Es ist ein Fehler aufgetreten.');
                    }
                })
                .catch(function () {
                    typing.remove();
                    append('assistant', 'Verbindungsfehler. Bitte später erneut versuchen.');
                })
                .finally(function () { busy = false; sendBtn.disabled = false; input.focus(); });
        }

        form.addEventListener('submit', function (e) { e.preventDefault(); send(input.value); });
        return panel;
    }

    function initInline(root) {
        if (root.dataset.init) { return; }
        root.dataset.init = '1';
        buildPanel(root, 'inline');
    }

    function initBubble(root) {
        if (root.dataset.init) { return; }
        root.dataset.init = '1';

        var btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'opennit-asst-fab';
        btn.setAttribute('aria-label', CFG.title || 'IT-Assistent');
        // Eigenes Icon aus dem OpenNIT-Backend, sonst Standard-Sprechblase.
        if (CFG.icon) {
            var img = document.createElement('img');
            img.className = 'opennit-asst-fab-icon';
            img.src = CFG.icon;
            img.alt = '';
            btn.appendChild(img);
        } else {
            btn.innerHTML = '<span>&#128172;</span>';
        }
        root.appendChild(btn);

        var wrap = document.createElement('div');
        wrap.className = 'opennit-asst-bubble-wrap';
        wrap.style.display = 'none';
        root.appendChild(wrap);

        // Panel wird erst beim ersten Öffnen gebaut. So bleibt der FAB-Klick
        // garantiert funktionsfähig, auch falls der Panel-Aufbau scheitert.
        var panel = null;
        var open  = false;

        function toggle(state) {
            open = (state === undefined) ? !open : state;
            if (open && !panel) {
                try {
                    panel = buildPanel(wrap, 'bubble');
                    var close = panel.querySelector('.opennit-asst-close');
                    if (close) { close.addEventListener('click', function () { toggle(false); }); }
                } catch (e) {
                    if (window.console) { console.error('OpenNIT-Assistent: Panel konnte nicht geladen werden.', e); }
                }
            }
            wrap.style.display = open ? 'block' : 'none';
            btn.classList.toggle('opennit-asst-fab-open', open);
            if (open && panel) { var i = panel.querySelector('input'); if (i) { i.focus(); } }
        }

        // Handler ZUERST anhängen – vor jeglichem Panel-Aufbau.
        btn.addEventListener('click', function () { toggle(); });
    }

    function boot() {
        document.querySelectorAll('[data-opennit-assistant="inline"]').forEach(initInline);
        document.querySelectorAll('[data-opennit-assistant="bubble"]').forEach(initBubble);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', boot);
    } else {
        boot();
    }
})();