Upload via GUI (290 Dateien)
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user