Add chat widget script to contact, index, and pricing pages
This commit is contained in:
parent
cc601663b9
commit
21722c70fb
4 changed files with 379 additions and 0 deletions
376
chat-widget.js
Normal file
376
chat-widget.js
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
/* ============================================================
|
||||
Stackless — floating chat widget
|
||||
Connects to the n8n chat workflow at:
|
||||
https://n8nmac.apphub.co.za/webhook/34f4684f-a603-4207-a8a9-75746a345463
|
||||
Self-contained: injects DOM + scoped styles on first load.
|
||||
Survives the workflow returning 5xx — surfaces a friendly
|
||||
fallback with a "message a human" path instead of crashing.
|
||||
============================================================ */
|
||||
(function () {
|
||||
if (window.__stacklessChatLoaded) return;
|
||||
window.__stacklessChatLoaded = true;
|
||||
|
||||
var WEBHOOK = 'https://n8nmac.apphub.co.za/webhook/34f4684f-a603-4207-a8a9-75746a345463';
|
||||
var BRAND = { teal: '#1DB897', coral: '#F76857', ink: '#212A40', inkDeep: '#1C2439', paper: '#F4F2EC', surface: '#FFFFFF', border: '#E5E2D8', muted: '#6E7387' };
|
||||
var STORE_KEY = 'stackless:chat:session';
|
||||
var POS_KEY = 'stackless:chat:open';
|
||||
var HISTORY_KEY = 'stackless:chat:history';
|
||||
|
||||
// ---- session id (persists per browser) ----
|
||||
function getSession() {
|
||||
try {
|
||||
var s = localStorage.getItem(STORE_KEY);
|
||||
if (!s) {
|
||||
s = 'sess-' + (crypto && crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2, 10));
|
||||
localStorage.setItem(STORE_KEY, s);
|
||||
}
|
||||
return s;
|
||||
} catch (_) { return 'sess-anon'; }
|
||||
}
|
||||
|
||||
// ---- styles (scoped under .cw-*) ----
|
||||
var css = ''
|
||||
+ '.cw-root{position:fixed;right:20px;bottom:20px;z-index:1000;font-family:Inter,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;color:' + BRAND.ink + ';}'
|
||||
+ '.cw-launcher{width:58px;height:58px;border-radius:50%;background:' + BRAND.ink + ';border:0;box-shadow:0 12px 30px -10px rgba(28,36,57,.45),0 0 0 1px rgba(255,255,255,.04) inset;cursor:pointer;display:grid;place-items:center;transition:transform .2s ease,background .2s ease,box-shadow .2s ease;color:#fff;}'
|
||||
+ '.cw-launcher:hover{transform:translateY(-2px);box-shadow:0 18px 38px -10px rgba(28,36,57,.55);}'
|
||||
+ '.cw-launcher:focus-visible{outline:2px solid ' + BRAND.teal + ';outline-offset:3px;}'
|
||||
+ '.cw-launcher svg{width:24px;height:24px;}'
|
||||
+ '.cw-launcher .cw-pulse{position:absolute;width:10px;height:10px;border-radius:50%;background:' + BRAND.coral + ';top:6px;right:6px;box-shadow:0 0 0 0 rgba(247,104,87,.6);animation:cw-pulse 2s infinite;}'
|
||||
+ '@keyframes cw-pulse{0%{box-shadow:0 0 0 0 rgba(247,104,87,.55);}70%{box-shadow:0 0 0 12px rgba(247,104,87,0);}100%{box-shadow:0 0 0 0 rgba(247,104,87,0);}}'
|
||||
+ '.cw-panel{position:fixed;right:20px;bottom:92px;width:min(380px,calc(100vw - 32px));height:min(560px,calc(100vh - 120px));max-height:calc(100vh - 120px);background:' + BRAND.surface + ';border:1px solid ' + BRAND.border + ';border-radius:20px;box-shadow:0 30px 80px -30px rgba(28,36,57,.35);display:flex;flex-direction:column;overflow:hidden;opacity:0;transform:translateY(12px) scale(.98);transform-origin:bottom right;pointer-events:none;transition:opacity .2s ease,transform .2s ease;}'
|
||||
+ '.cw-panel.is-open{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;}'
|
||||
+ '.cw-head{display:flex;align-items:center;gap:12px;padding:14px 16px;background:' + BRAND.ink + ';color:#fff;}'
|
||||
+ '.cw-avatar{width:34px;height:34px;border-radius:10px;background:' + BRAND.surface + ';display:grid;place-items:center;flex:none;}'
|
||||
+ '.cw-avatar svg{width:22px;height:22px;}'
|
||||
+ '.cw-meta{flex:1;min-width:0;}'
|
||||
+ '.cw-title{font:600 14.5px/1.2 "Inter Tight",-apple-system,system-ui,sans-serif;letter-spacing:-.01em;}'
|
||||
+ '.cw-status{display:flex;align-items:center;gap:6px;margin-top:3px;font:500 11px/1 "JetBrains Mono",ui-monospace,Menlo,monospace;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,.7);}'
|
||||
+ '.cw-dot{width:7px;height:7px;border-radius:50%;background:' + BRAND.teal + ';box-shadow:0 0 0 3px rgba(29,184,151,.25);}'
|
||||
+ '.cw-head-btn{background:transparent;border:0;color:rgba(255,255,255,.7);cursor:pointer;width:32px;height:32px;border-radius:8px;display:grid;place-items:center;transition:background .15s,color .15s;}'
|
||||
+ '.cw-head-btn:hover{background:rgba(255,255,255,.1);color:#fff;}'
|
||||
+ '.cw-head-btn svg{width:16px;height:16px;}'
|
||||
+ '.cw-messages{flex:1;overflow-y:auto;padding:18px 16px;background:' + BRAND.paper + ';display:flex;flex-direction:column;gap:10px;scrollbar-width:thin;scrollbar-color:' + BRAND.border + ' transparent;}'
|
||||
+ '.cw-messages::-webkit-scrollbar{width:6px;}'
|
||||
+ '.cw-messages::-webkit-scrollbar-thumb{background:' + BRAND.border + ';border-radius:3px;}'
|
||||
+ '.cw-msg{max-width:84%;padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap;animation:cw-in .2s ease;}'
|
||||
+ '@keyframes cw-in{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:translateY(0);}}'
|
||||
+ '.cw-msg--bot{background:' + BRAND.surface + ';border:1px solid ' + BRAND.border + ';color:' + BRAND.ink + ';border-bottom-left-radius:4px;align-self:flex-start;}'
|
||||
+ '.cw-msg--user{background:' + BRAND.ink + ';color:#fff;border-bottom-right-radius:4px;align-self:flex-end;}'
|
||||
+ '.cw-msg--err{background:#fff5f3;border:1px solid #f7c8bf;color:#a8341f;border-bottom-left-radius:4px;align-self:flex-start;}'
|
||||
+ '.cw-msg a{color:' + BRAND.teal + ';text-decoration:underline;text-underline-offset:2px;}'
|
||||
+ '.cw-msg--user a{color:#a4f0d6;}'
|
||||
+ '.cw-typing{align-self:flex-start;background:' + BRAND.surface + ';border:1px solid ' + BRAND.border + ';border-radius:16px;border-bottom-left-radius:4px;padding:12px 16px;display:flex;gap:4px;}'
|
||||
+ '.cw-typing span{width:7px;height:7px;border-radius:50%;background:' + BRAND.muted + ';animation:cw-typing 1.2s infinite;}'
|
||||
+ '.cw-typing span:nth-child(2){animation-delay:.15s;}'
|
||||
+ '.cw-typing span:nth-child(3){animation-delay:.3s;}'
|
||||
+ '@keyframes cw-typing{0%,60%,100%{opacity:.3;transform:translateY(0);}30%{opacity:1;transform:translateY(-3px);}}'
|
||||
+ '.cw-quick{padding:0 16px 12px;background:' + BRAND.paper + ';display:flex;flex-wrap:wrap;gap:6px;}'
|
||||
+ '.cw-quick button{background:' + BRAND.surface + ';border:1px solid ' + BRAND.border + ';border-radius:999px;padding:7px 12px;font:500 12.5px/1 Inter,system-ui,sans-serif;color:' + BRAND.ink + ';cursor:pointer;transition:background .15s,border-color .15s,color .15s;}'
|
||||
+ '.cw-quick button:hover{background:' + BRAND.ink + ';color:#fff;border-color:' + BRAND.ink + ';}'
|
||||
+ '.cw-form{display:flex;gap:8px;align-items:flex-end;padding:12px 14px;border-top:1px solid ' + BRAND.border + ';background:' + BRAND.surface + ';}'
|
||||
+ '.cw-form textarea{flex:1;resize:none;border:1px solid ' + BRAND.border + ';border-radius:14px;padding:10px 12px;font:400 14px/1.5 Inter,system-ui,sans-serif;color:' + BRAND.ink + ';background:' + BRAND.surface + ';outline:none;min-height:42px;max-height:120px;transition:border-color .15s,box-shadow .15s;}'
|
||||
+ '.cw-form textarea:focus{border-color:' + BRAND.teal + ';box-shadow:0 0 0 3px rgba(29,184,151,.15);}'
|
||||
+ '.cw-form textarea::placeholder{color:' + BRAND.muted + ';}'
|
||||
+ '.cw-send{width:42px;height:42px;border-radius:12px;background:' + BRAND.coral + ';color:#fff;border:0;cursor:pointer;display:grid;place-items:center;flex:none;transition:background .15s,transform .12s;}'
|
||||
+ '.cw-send:hover{background:#e25645;}'
|
||||
+ '.cw-send:active{transform:translateY(1px);}'
|
||||
+ '.cw-send:disabled{opacity:.5;cursor:not-allowed;}'
|
||||
+ '.cw-send svg{width:18px;height:18px;}'
|
||||
+ '.cw-foot{padding:8px 16px 12px;background:' + BRAND.surface + ';border-top:1px solid ' + BRAND.border + ';font:500 11px/1.4 "JetBrains Mono",ui-monospace,Menlo,monospace;letter-spacing:.04em;color:' + BRAND.muted + ';text-align:center;}'
|
||||
+ '.cw-foot a{color:' + BRAND.ink + ';text-decoration:underline;text-underline-offset:2px;}'
|
||||
+ '@media (max-width:520px){'
|
||||
+ '.cw-root{right:12px;bottom:12px;}'
|
||||
+ '.cw-panel{right:12px;left:12px;bottom:84px;width:auto;height:calc(100vh - 100px);border-radius:18px;}'
|
||||
+ '.cw-launcher{width:54px;height:54px;}'
|
||||
+ '}'
|
||||
+ '@media (prefers-reduced-motion:reduce){'
|
||||
+ '.cw-launcher,.cw-panel,.cw-msg{transition:none;animation:none;}'
|
||||
+ '.cw-pulse{animation:none;}'
|
||||
+ '}';
|
||||
var style = document.createElement('style');
|
||||
style.setAttribute('data-cw', 'stackless');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// ---- icons (inline, currentColor) ----
|
||||
var ICON_CHAT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>';
|
||||
var ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
||||
var ICON_SEND = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
|
||||
var ICON_RESET = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>';
|
||||
// brand mark: three slanted capsules + coral dot (matches the logo)
|
||||
var ICON_BRAND = '<svg viewBox="0 0 32 32" fill="none" aria-hidden="true">'
|
||||
+ '<rect x="6.5" y="14.5" width="13" height="4" rx="2" transform="rotate(-22 13 16.5)" fill="#AEDED3"/>'
|
||||
+ '<rect x="9" y="13" width="13" height="4" rx="2" transform="rotate(-22 15.5 15)" fill="#5FBEC2"/>'
|
||||
+ '<rect x="11.5" y="11.5" width="13" height="4" rx="2" transform="rotate(-22 18 13.5)" fill="#172E58"/>'
|
||||
+ '<circle cx="22.5" cy="20" r="2.2" fill="#F17B61"/>'
|
||||
+ '</svg>';
|
||||
|
||||
// ---- DOM scaffold ----
|
||||
var root = document.createElement('div');
|
||||
root.className = 'cw-root';
|
||||
root.innerHTML = ''
|
||||
+ '<button type="button" class="cw-launcher" aria-label="Open chat with stackless" aria-expanded="false">'
|
||||
+ ICON_CHAT
|
||||
+ '<span class="cw-pulse" aria-hidden="true"></span>'
|
||||
+ '</button>'
|
||||
+ '<section class="cw-panel" role="dialog" aria-label="Stackless chat" aria-hidden="true">'
|
||||
+ '<header class="cw-head">'
|
||||
+ '<div class="cw-avatar">' + ICON_BRAND + '</div>'
|
||||
+ '<div class="cw-meta">'
|
||||
+ '<div class="cw-title">stackless assistant</div>'
|
||||
+ '<div class="cw-status"><span class="cw-dot"></span> online · replies in seconds</div>'
|
||||
+ '</div>'
|
||||
+ '<button type="button" class="cw-head-btn cw-reset" aria-label="Reset conversation" title="Reset">' + ICON_RESET + '</button>'
|
||||
+ '<button type="button" class="cw-head-btn cw-close" aria-label="Close chat">' + ICON_CLOSE + '</button>'
|
||||
+ '</header>'
|
||||
+ '<div class="cw-messages" role="log" aria-live="polite" aria-relevant="additions"></div>'
|
||||
+ '<div class="cw-quick" aria-label="Suggested questions"></div>'
|
||||
+ '<form class="cw-form" novalidate>'
|
||||
+ '<textarea class="cw-input" rows="1" placeholder="Ask about websites, AI agents, pricing…" aria-label="Your message"></textarea>'
|
||||
+ '<button type="submit" class="cw-send" aria-label="Send">' + ICON_SEND + '</button>'
|
||||
+ '</form>'
|
||||
+ '<div class="cw-foot">powered by <a href="https://stackless.co.za">stackless</a> · <a href="contact.html">talk to a human</a></div>'
|
||||
+ '</section>';
|
||||
document.body.appendChild(root);
|
||||
|
||||
var launcher = root.querySelector('.cw-launcher');
|
||||
var panel = root.querySelector('.cw-panel');
|
||||
var closeBtn = root.querySelector('.cw-close');
|
||||
var resetBtn = root.querySelector('.cw-reset');
|
||||
var messages = root.querySelector('.cw-messages');
|
||||
var quick = root.querySelector('.cw-quick');
|
||||
var form = root.querySelector('.cw-form');
|
||||
var input = root.querySelector('.cw-input');
|
||||
var sendBtn = root.querySelector('.cw-send');
|
||||
|
||||
var QUICK_PROMPTS = [
|
||||
'What can you build?',
|
||||
'How much does a website cost?',
|
||||
'What is Rent-an-AI-Agent?',
|
||||
'How long does a project take?'
|
||||
];
|
||||
|
||||
// ---- state ----
|
||||
var history = [];
|
||||
try { history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]') || []; } catch (_) { history = []; }
|
||||
var isOpen = false;
|
||||
var inFlight = false;
|
||||
|
||||
function saveHistory() {
|
||||
try { localStorage.setItem(HISTORY_KEY, JSON.stringify(history.slice(-50))); } catch (_) {}
|
||||
}
|
||||
|
||||
function renderQuick() {
|
||||
quick.innerHTML = '';
|
||||
QUICK_PROMPTS.forEach(function (q) {
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.textContent = q;
|
||||
b.addEventListener('click', function () {
|
||||
if (inFlight) return;
|
||||
input.value = q;
|
||||
autoSize();
|
||||
form.requestSubmit();
|
||||
});
|
||||
quick.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function escape(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||||
});
|
||||
}
|
||||
// Allow http(s) links inside bot messages; everything else stays escaped.
|
||||
function linkify(escaped) {
|
||||
return escaped.replace(/(https?:\/\/[^\s<]+)/g, function (url) {
|
||||
return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
|
||||
});
|
||||
}
|
||||
|
||||
function addMessage(role, text, opts) {
|
||||
opts = opts || {};
|
||||
var div = document.createElement('div');
|
||||
div.className = 'cw-msg cw-msg--' + role;
|
||||
div.innerHTML = linkify(escape(text));
|
||||
messages.appendChild(div);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
if (role === 'user' || role === 'bot') {
|
||||
history.push({ role: role, text: text, t: Date.now() });
|
||||
saveHistory();
|
||||
}
|
||||
return div;
|
||||
}
|
||||
|
||||
function addError(text) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'cw-msg cw-msg--err';
|
||||
div.innerHTML = linkify(escape(text));
|
||||
messages.appendChild(div);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
return div;
|
||||
}
|
||||
|
||||
function showTyping() {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'cw-typing';
|
||||
el.setAttribute('aria-label', 'Assistant is typing');
|
||||
el.innerHTML = '<span></span><span></span><span></span>';
|
||||
messages.appendChild(el);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
messages.innerHTML = '';
|
||||
if (!history.length) {
|
||||
addMessage('bot', "hey — i'm the stackless assistant. ask me what we build, what it costs, or how it works.");
|
||||
return;
|
||||
}
|
||||
history.forEach(function (m) { addMessage(m.role, m.text); });
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
panel.classList.add('is-open');
|
||||
panel.setAttribute('aria-hidden', 'false');
|
||||
launcher.setAttribute('aria-expanded', 'true');
|
||||
launcher.style.transform = 'scale(.9)';
|
||||
setTimeout(function () { input.focus(); }, 200);
|
||||
try { localStorage.setItem(POS_KEY, '1'); } catch (_) {}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!isOpen) return;
|
||||
isOpen = false;
|
||||
panel.classList.remove('is-open');
|
||||
panel.setAttribute('aria-hidden', 'true');
|
||||
launcher.setAttribute('aria-expanded', 'false');
|
||||
launcher.style.transform = '';
|
||||
try { localStorage.setItem(POS_KEY, '0'); } catch (_) {}
|
||||
}
|
||||
|
||||
function toggle() { isOpen ? close() : open(); }
|
||||
|
||||
function reset() {
|
||||
history = [];
|
||||
try { localStorage.removeItem(HISTORY_KEY); } catch (_) {}
|
||||
try {
|
||||
var s = localStorage.getItem(STORE_KEY);
|
||||
localStorage.removeItem(STORE_KEY);
|
||||
if (s) localStorage.setItem(STORE_KEY, 'sess-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8));
|
||||
} catch (_) {}
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
// ---- webhook call ----
|
||||
function extractReply(data) {
|
||||
if (data == null) return '';
|
||||
if (typeof data === 'string') return data;
|
||||
if (Array.isArray(data)) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var v = extractReply(data[i]);
|
||||
if (v) return v;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
var keys = ['output', 'text', 'response', 'message', 'reply', 'answer', 'content', 'result'];
|
||||
for (var k = 0; k < keys.length; k++) {
|
||||
if (typeof data[keys[k]] === 'string' && data[keys[k]].trim()) return data[keys[k]];
|
||||
}
|
||||
// Sometimes n8n wraps it: { data: { output: "..." } } or { json: { output: "..." } }
|
||||
if (data.data) { var d = extractReply(data.data); if (d) return d; }
|
||||
if (data.json) { var j = extractReply(data.json); if (j) return j; }
|
||||
if (typeof data.message === 'object') { var m = extractReply(data.message); if (m) return m; }
|
||||
return '';
|
||||
}
|
||||
|
||||
function callWebhook(text) {
|
||||
var ctrl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||||
var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, 25000);
|
||||
return fetch(WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ chatInput: text, sessionId: getSession(), message: text }),
|
||||
signal: ctrl ? ctrl.signal : undefined
|
||||
})
|
||||
.then(function (res) {
|
||||
clearTimeout(timer);
|
||||
var ct = (res.headers.get('content-type') || '').toLowerCase();
|
||||
var parse = ct.indexOf('application/json') >= 0 ? res.json() : res.text();
|
||||
return parse.then(function (body) { return { ok: res.ok, status: res.status, body: body }; });
|
||||
});
|
||||
}
|
||||
|
||||
function send(text) {
|
||||
text = (text || '').trim();
|
||||
if (!text || inFlight) return;
|
||||
inFlight = true;
|
||||
sendBtn.disabled = true;
|
||||
addMessage('user', text);
|
||||
var typing = showTyping();
|
||||
callWebhook(text)
|
||||
.then(function (r) {
|
||||
typing.remove();
|
||||
if (!r.ok) {
|
||||
var detail = (r.body && (r.body.message || r.body.error)) ? ' (' + (r.body.message || r.body.error) + ')' : '';
|
||||
addError("our assistant is having a moment — try again, or tap 'talk to a human' below" + detail + '.');
|
||||
return;
|
||||
}
|
||||
var reply = extractReply(r.body);
|
||||
if (!reply) {
|
||||
addError("didn't catch a reply from the assistant. try again, or message us directly on contact.html.");
|
||||
return;
|
||||
}
|
||||
addMessage('bot', reply);
|
||||
})
|
||||
.catch(function (err) {
|
||||
typing.remove();
|
||||
var msg = (err && err.name === 'AbortError')
|
||||
? "that took longer than expected. try again, or message us on contact.html."
|
||||
: "couldn't reach the assistant. check your connection, or message us on contact.html.";
|
||||
addError(msg);
|
||||
})
|
||||
.then(function () {
|
||||
inFlight = false;
|
||||
sendBtn.disabled = false;
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- input behaviour ----
|
||||
function autoSize() {
|
||||
input.style.height = 'auto';
|
||||
input.style.height = Math.min(120, input.scrollHeight) + 'px';
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var v = input.value;
|
||||
input.value = '';
|
||||
autoSize();
|
||||
send(v);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
input.addEventListener('input', autoSize);
|
||||
|
||||
launcher.addEventListener('click', toggle);
|
||||
closeBtn.addEventListener('click', close);
|
||||
resetBtn.addEventListener('click', function () { reset(); input.focus(); });
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && isOpen) close();
|
||||
});
|
||||
|
||||
// ---- boot ----
|
||||
renderHistory();
|
||||
renderQuick();
|
||||
try { if (localStorage.getItem(POS_KEY) === '1') open(); } catch (_) {}
|
||||
})();
|
||||
|
|
@ -577,5 +577,6 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<script src="chat-widget.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1115,5 +1115,6 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script src="chat-widget.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -632,5 +632,6 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script src="chat-widget.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in a new issue