Files
deskhub/DeskHub-Webseite/includes/assistant_widget.php
T
2026-10-23 17:22:17 +00:00

208 lines
14 KiB
PHP

<?php
/**
* includes/assistant_widget.php: Chat-Fenster des digitalen Assistenten im Kundenportal (wird von footer_customer.php eingebunden,
* nur wenn assistant_active() wahr ist). Alle Texte werden per textContent eingesetzt, nie als HTML.
*/
$__as = assistant_texts();
?>
<style>
#as-bubble{position:fixed;bottom:20px;right:20px;width:56px;height:56px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.5rem;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.25);z-index:9990;border:0}
#as-panel{display:none;position:fixed;bottom:88px;right:20px;width:360px;max-width:calc(100vw - 32px);height:min(540px,calc(100vh - 120px));background:#fff;color:#1f2937;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.28);z-index:9991;flex-direction:column;overflow:hidden;font-size:.88rem}
#as-panel.open{display:flex}
#as-head{background:var(--primary);color:#fff;padding:12px 14px;display:flex;align-items:center;gap:8px;flex-shrink:0}
#as-head strong{font-size:.92rem}
#as-close{margin-left:auto;cursor:pointer;font-size:1.3rem;line-height:1;opacity:.9;background:none;border:0;color:#fff}
#as-msgs{flex:1;overflow-y:auto;padding:12px;background:#f7f8fa;display:flex;flex-direction:column;gap:8px}
.as-row{max-width:88%;padding:8px 11px;border-radius:12px;white-space:pre-wrap;word-wrap:break-word;line-height:1.4}
.as-bot{align-self:flex-start;background:#fff;border:1px solid #e5e7eb}
.as-user{align-self:flex-end;background:var(--primary);color:#fff}
.as-src{font-size:.72rem;color:#6b7280;margin-top:4px}
.as-btns{align-self:flex-start;display:flex;flex-wrap:wrap;gap:6px;max-width:95%}
.as-btns button{border:1px solid var(--primary);background:#fff;color:var(--primary);border-radius:16px;padding:5px 11px;font-size:.8rem;cursor:pointer}
.as-btns button.as-main{background:var(--primary);color:#fff}
#as-form{display:flex;gap:8px;padding:10px 12px;border-top:1px solid #eee;flex-shrink:0;background:#fff}
#as-input{flex:1;padding:9px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:.88rem}
#as-send{padding:8px 14px}
#as-note{display:none;background:#fffbeb;border-bottom:1px solid #fde68a;color:#92400e;font-size:.76rem;padding:6px 12px}
</style>
<button type="button" id="as-bubble" aria-label="<?= e($__as['name']) ?> öffnen" title="<?= e($__as['name']) ?>">💬</button>
<div id="as-panel" role="dialog" aria-label="<?= e($__as['name']) ?>">
<div id="as-head"><span style="font-size:1.1rem">🤖</span><strong id="as-title"></strong><button type="button" id="as-close" aria-label="Schließen">&times;</button></div>
<div id="as-note">Ein Mitarbeiter ist jetzt erreichbar. Laden Sie die Seite neu, um den Live-Chat zu nutzen.</div>
<div id="as-msgs" aria-live="polite"></div>
<form id="as-form" autocomplete="off">
<input type="text" id="as-hp" name="hp_website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">
<input type="text" id="as-input" maxlength="300" placeholder="Ihre Frage …" aria-label="Ihre Nachricht">
<button type="submit" class="btn" id="as-send">Senden</button>
</form>
</div>
<script>
(function () {
var CSRF = <?= json_encode(csrf_token()) ?>;
var $ = function (id) { return document.getElementById(id); };
var panel = $('as-panel'), msgs = $('as-msgs'), input = $('as-input'), form = $('as-form'), sendBtn = $('as-send');
var cfg = null, started = false, busy = false;
var mode = 'ask'; // 'ask' = Fragen stellen, 'ticket' = Angaben für ein Ticket sammeln
var step = '', data = {}, lastQuestion = '';
function api(action, fields) {
var fd = new FormData();
fd.append('action', action); fd.append('csrf_token', CSRF);
Object.keys(fields || {}).forEach(function (k) { fd.append(k, fields[k]); });
return fetch('/assistant.php', { method: 'POST', body: fd, credentials: 'same-origin' })
.then(function (r) { return r.json().then(function (j) { j.__status = r.status; return j; }); });
}
function scroll() { msgs.scrollTop = msgs.scrollHeight; }
// Text mit anklickbaren https-Adressen, ohne HTML einzusetzen
function fill(el, text) {
var re = /(https?:\/\/[^\s<>"']+)/g, last = 0, m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) el.appendChild(document.createTextNode(text.slice(last, m.index)));
var a = document.createElement('a'); a.href = m[1]; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = m[1]; a.style.color = 'inherit'; a.style.textDecoration = 'underline';
el.appendChild(a); last = m.index + m[1].length;
}
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
}
function say(text, who, extra) {
var row = document.createElement('div'); row.className = 'as-row ' + (who === 'user' ? 'as-user' : 'as-bot');
fill(row, text);
if (extra && extra.source) { var s = document.createElement('div'); s.className = 'as-src'; s.textContent = extra.source; row.appendChild(s); }
if (extra && extra.link) { var l = document.createElement('a'); l.href = extra.link; l.target = '_blank'; l.rel = 'noopener'; l.textContent = 'Ganzen Artikel lesen'; l.style.cssText = 'display:block;margin-top:6px;font-size:.78rem'; row.appendChild(l); }
msgs.appendChild(row); scroll(); return row;
}
function buttons(list) {
var box = document.createElement('div'); box.className = 'as-btns';
list.forEach(function (b) {
var el = document.createElement('button'); el.type = 'button'; el.textContent = b.label; if (b.main) el.className = 'as-main';
el.addEventListener('click', function () { box.remove(); if (b.user !== false) say(b.label, 'user'); b.fn(); });
box.appendChild(el);
});
msgs.appendChild(box); scroll();
}
function setBusy(b) { busy = b; sendBtn.disabled = b; input.disabled = b; if (!b) input.focus(); }
// ── Fragen ────────────────────────────────────────────────────────────────
function ask(text) {
lastQuestion = text; setBusy(true);
api('ask', { message: text }).then(function (r) {
setBusy(false);
if (r.error) { say(r.error, 'bot'); if (r.csrfExpired || r.active === false) input.disabled = true; return; }
if (r.type === 'ticket') { startTicket(); return; }
if (r.type === 'smalltalk') { say(r.text, 'bot'); return; }
if (r.type === 'answer') {
say(r.text, 'bot', { source: (r.source === 'kb' ? 'Aus der Wissensdatenbank: ' : '') + (r.source === 'kb' ? r.title : ''), link: r.link });
(r.others || []).forEach(function (o) { var row = say('Das könnte auch helfen: ' + o.title, 'bot'); var a = document.createElement('a'); a.href = o.link; a.target = '_blank'; a.rel = 'noopener'; a.textContent = ' Öffnen'; row.appendChild(a); });
var lid = r.logId || 0;
function feedback(ok) { if (lid) api('feedback', { logId: lid, helpful: ok ? 1 : 0 }).catch(function () {}); }
buttons([
{ label: 'Das hat geholfen', fn: function () { feedback(true); say('Schön, dass ich helfen konnte! Haben Sie noch eine Frage?', 'bot'); } },
{ label: 'Ticket erstellen', main: true, fn: function () { feedback(false); startTicket(); } }
]);
return;
}
say(r.text || (cfg && cfg.noAnswer), 'bot');
buttons([
{ label: 'Ja, Ticket erstellen', main: true, fn: startTicket },
{ label: 'Nein, andere Frage', fn: function () { say('Gern. Was möchten Sie wissen?', 'bot'); } }
]);
}).catch(function () { setBusy(false); say('Das hat leider nicht geklappt. Bitte versuchen Sie es noch einmal.', 'bot'); });
}
// ── Ticket: Angaben Schritt für Schritt abfragen ──────────────────────────
function startTicket() {
mode = 'ticket'; data = {};
if (cfg.helpTopics && cfg.helpTopics.length) {
step = 'topic'; say('Worum geht es in Ihrer Anfrage?', 'bot');
var list = cfg.helpTopics.slice(0, 8).map(function (t) { return { label: t.name, fn: function () { data.helpTopicId = t.id; askName(); } }; });
list.push({ label: 'Etwas anderes', fn: function () { askName(); } });
buttons(list);
} else { askName(); }
}
function askName() {
step = 'name';
if (cfg.user && cfg.user.name) { data.name = cfg.user.name; if (cfg.user.email) { data.email = cfg.user.email; askSubject(); return; } askEmail(); return; }
say('Wie heißen Sie?', 'bot'); input.placeholder = 'Ihr Name'; input.focus();
}
function askEmail() { step = 'email'; say('Unter welcher E-Mail-Adresse erreichen wir Sie? Dorthin schicken wir die Antwort.', 'bot'); input.placeholder = 'name@beispiel.de'; input.focus(); }
function askSubject() {
step = 'subject'; say('Wie lautet der Betreff? (Kurz in einem Satz)', 'bot'); input.placeholder = 'Betreff';
if (lastQuestion) buttons([{ label: 'Meine Frage übernehmen', user: false, fn: function () { data.subject = lastQuestion.slice(0, 120); say(data.subject, 'user'); askMessage(); } }]);
input.focus();
}
function askMessage() {
step = 'message'; say('Bitte beschreiben Sie Ihr Anliegen so genau wie möglich.', 'bot'); input.placeholder = 'Ihre Beschreibung';
if (lastQuestion) buttons([{ label: 'Meine Frage als Beschreibung', user: false, fn: function () { data.message = lastQuestion; say(data.message, 'user'); confirmTicket(); } }]);
input.focus();
}
function confirmTicket() {
step = 'confirm'; input.placeholder = '';
say('Das habe ich notiert:\nName: ' + data.name + '\nE-Mail: ' + data.email + '\nBetreff: ' + data.subject + '\nBeschreibung: ' + data.message + '\n\nSoll ich das Ticket so erstellen?', 'bot');
confirmButtons();
}
function cancelTicket() { mode = 'ask'; step = ''; input.placeholder = 'Ihre Frage …'; say('Alles klar, ich habe nichts erstellt. Wie kann ich sonst helfen?', 'bot'); }
function confirmButtons() {
buttons([
{ label: 'Ticket erstellen', main: true, fn: sendTicket },
{ label: 'Abbrechen', fn: cancelTicket }
]);
}
function sendTicket() {
setBusy(true);
var f = { name: data.name, email: data.email, subject: data.subject, message: data.message, hp_website: $('as-hp').value };
if (data.helpTopicId) f.helpTopicId = data.helpTopicId;
api('ticket', f).then(function (r) {
setBusy(false);
if (r.ok) {
mode = 'ask'; step = ''; input.placeholder = 'Ihre Frage …';
say('Ihr Ticket wurde erstellt. Die Nummer lautet ' + r.number + '. Wir melden uns per E-Mail bei Ihnen, eine Bestätigung ist unterwegs.', 'bot');
return;
}
say(r.error || 'Das Ticket konnte nicht erstellt werden.', 'bot');
if (r.__status === 422) { buttons([{ label: 'Angaben ändern', main: true, fn: function () { startTicket(); } }, { label: 'Abbrechen', fn: function () { mode = 'ask'; step = ''; input.placeholder = 'Ihre Frage …'; } }]); }
}).catch(function () { setBusy(false); say('Das hat leider nicht geklappt. Bitte versuchen Sie es noch einmal.', 'bot'); });
}
form.addEventListener('submit', function (e) {
e.preventDefault();
var text = input.value.trim(); if (!text || busy) return;
input.value = '';
Array.prototype.forEach.call(msgs.querySelectorAll('.as-btns'), function (b) { b.remove(); }); // offene Vorschläge entfallen, wenn der Kunde selbst tippt
say(text, 'user');
if (mode === 'ask') { ask(text); return; }
if (step === 'name') { data.name = text.slice(0, 120); askEmail(); }
else if (step === 'email') {
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(text)) { say('Das sieht nicht wie eine E-Mail-Adresse aus. Bitte noch einmal, zum Beispiel name@beispiel.de.', 'bot'); return; }
data.email = text; askSubject();
}
else if (step === 'subject') { data.subject = text.slice(0, 200); askMessage(); }
else if (step === 'message') { data.message = text; confirmTicket(); }
else if (step === 'confirm') {
// auch getippte Antworten gelten (ja / ticket erstellen / nein / abbrechen)
var t = text.toLowerCase().replace(/[^a-zäöüß ]/g, '').trim();
if (/^(ja|jap|jo|ok|okay|klar|gerne|bitte|passt|stimmt|richtig|yes|ticket erstellen|erstellen|absenden|senden|abschicken|bestätigen)\b/.test(t)) { sendTicket(); }
else if (/^(nein|nee|no|abbrechen|stopp|stop|doch nicht|abbruch)\b/.test(t)) { cancelTicket(); }
else { say('Bitte antworten Sie mit „Ja“ oder „Nein“, oder wählen Sie eine Schaltfläche.', 'bot'); confirmButtons(); }
}
});
function open() {
panel.classList.add('open');
if (started) { input.focus(); return; }
started = true;
api('status').then(function (r) {
if (!r.active) { say('Der Assistent ist gerade nicht verfügbar.', 'bot'); input.disabled = true; return; }
cfg = r; $('as-title').textContent = r.name; say(r.greeting, 'bot');
buttons([{ label: 'Ticket erstellen', fn: startTicket }]);
input.focus();
}).catch(function () { say('Der Assistent ist gerade nicht erreichbar.', 'bot'); });
}
$('as-title').textContent = <?= json_encode($__as['name'], JSON_UNESCAPED_UNICODE) ?>;
$('as-bubble').addEventListener('click', function () { if (panel.classList.contains('open')) panel.classList.remove('open'); else open(); });
$('as-close').addEventListener('click', function () { panel.classList.remove('open'); });
// Wird zwischendurch ein Mitarbeiter erreichbar, einen Hinweis zeigen (der Live-Chat erscheint nach dem Neuladen)
setInterval(function () {
api('status').then(function (r) { if (r && r.active === false) $('as-note').style.display = 'block'; }).catch(function () {});
}, 60000);
})();
</script>