208 lines
14 KiB
PHP
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">×</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>
|