Quelltext des aktuellen Standes
/**
* 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, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"');
}
// 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">×</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">➤</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>💬</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();
}
})();