Modules: - Chain: daemon RPC client (DaemonRpc singleton, cached queries) - Explorer: block browser, tx viewer, alias directory, search, stats API - Names: .lthn TLD registrar portal (availability check, lookup, directory) - Trade: scaffold (DEX frontend + API) - Pool: scaffold (mining pool dashboard) Replaces 5 Node.js containers (5.9GB) with one FrankenPHP app. Built on CorePHP framework pattern from host.uk.com. Co-Authored-By: Charon <charon@lethean.io>
768 lines
28 KiB
JavaScript
768 lines
28 KiB
JavaScript
/**
|
|
* SupportHost Chat Widget
|
|
*
|
|
* Embed this widget on any website to enable live chat with SupportHost.
|
|
*
|
|
* Usage:
|
|
* <script src="https://host.uk.com/js/support-widget.js"></script>
|
|
* <script>
|
|
* SupportWidget.init({ token: 'YOUR_WIDGET_TOKEN' });
|
|
* </script>
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
const SupportWidget = {
|
|
config: {
|
|
token: null,
|
|
baseUrl: null,
|
|
color: '#6366f1',
|
|
position: 'right',
|
|
greeting: 'Hello! How can we help you today?',
|
|
placeholder: 'Type a message...',
|
|
requireEmail: false,
|
|
},
|
|
state: {
|
|
isOpen: false,
|
|
isInitialised: false,
|
|
isLoading: false,
|
|
contactId: null,
|
|
visitorId: null,
|
|
conversationId: null,
|
|
messages: [],
|
|
widgetConfig: null,
|
|
},
|
|
container: null,
|
|
|
|
/**
|
|
* Initialise the widget.
|
|
*/
|
|
init(userConfig) {
|
|
if (this.state.isInitialised) {
|
|
console.warn('SupportWidget already initialised');
|
|
return;
|
|
}
|
|
|
|
if (!userConfig.token) {
|
|
console.error('SupportWidget: token is required');
|
|
return;
|
|
}
|
|
|
|
this.config.token = userConfig.token;
|
|
this.config.baseUrl = userConfig.baseUrl || this.detectBaseUrl();
|
|
|
|
// Generate or restore visitor ID
|
|
this.state.visitorId = this.getVisitorId();
|
|
|
|
this.createWidget();
|
|
this.bindEvents();
|
|
this.initWithServer();
|
|
},
|
|
|
|
/**
|
|
* Detect base URL from script src.
|
|
*/
|
|
detectBaseUrl() {
|
|
const scripts = document.getElementsByTagName('script');
|
|
for (let i = 0; i < scripts.length; i++) {
|
|
const src = scripts[i].src;
|
|
if (src.includes('support-widget.js')) {
|
|
const url = new URL(src);
|
|
return url.origin;
|
|
}
|
|
}
|
|
return window.location.origin;
|
|
},
|
|
|
|
/**
|
|
* Get or create visitor ID.
|
|
*/
|
|
getVisitorId() {
|
|
const key = 'support_widget_visitor_id';
|
|
let visitorId = localStorage.getItem(key);
|
|
if (!visitorId) {
|
|
visitorId = 'v_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
localStorage.setItem(key, visitorId);
|
|
}
|
|
return visitorId;
|
|
},
|
|
|
|
/**
|
|
* Create an element with attributes.
|
|
*/
|
|
createElement(tag, attrs, children) {
|
|
const el = document.createElement(tag);
|
|
if (attrs) {
|
|
Object.keys(attrs).forEach(key => {
|
|
if (key === 'className') {
|
|
el.className = attrs[key];
|
|
} else if (key === 'textContent') {
|
|
el.textContent = attrs[key];
|
|
} else {
|
|
el.setAttribute(key, attrs[key]);
|
|
}
|
|
});
|
|
}
|
|
if (children) {
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
el.appendChild(document.createTextNode(child));
|
|
} else if (child) {
|
|
el.appendChild(child);
|
|
}
|
|
});
|
|
}
|
|
return el;
|
|
},
|
|
|
|
/**
|
|
* Create SVG element.
|
|
*/
|
|
createSvg(path, size) {
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('width', size);
|
|
svg.setAttribute('height', size);
|
|
svg.setAttribute('viewBox', '0 0 24 24');
|
|
svg.setAttribute('fill', 'currentColor');
|
|
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
pathEl.setAttribute('d', path);
|
|
svg.appendChild(pathEl);
|
|
return svg;
|
|
},
|
|
|
|
/**
|
|
* Create widget DOM elements using safe DOM methods.
|
|
*/
|
|
createWidget() {
|
|
const widget = this.createElement('div', { id: 'support-widget' });
|
|
|
|
// Toggle button
|
|
const toggle = this.createElement('button', {
|
|
id: 'support-widget-toggle',
|
|
'aria-label': 'Open support chat'
|
|
});
|
|
toggle.appendChild(this.createSvg('M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z', '24'));
|
|
widget.appendChild(toggle);
|
|
|
|
// Panel
|
|
const panel = this.createElement('div', { id: 'support-widget-panel', className: 'sw-hidden' });
|
|
|
|
// Header
|
|
const header = this.createElement('div', { id: 'support-widget-header' });
|
|
header.appendChild(this.createElement('span', { id: 'support-widget-title', textContent: 'Support' }));
|
|
const closeBtn = this.createElement('button', {
|
|
id: 'support-widget-close',
|
|
'aria-label': 'Close chat',
|
|
textContent: '\u00D7'
|
|
});
|
|
header.appendChild(closeBtn);
|
|
panel.appendChild(header);
|
|
|
|
// Messages container
|
|
panel.appendChild(this.createElement('div', { id: 'support-widget-messages' }));
|
|
|
|
// Pre-chat form
|
|
const prechat = this.createElement('div', { id: 'support-widget-prechat', className: 'sw-hidden' });
|
|
prechat.appendChild(this.createElement('p', { id: 'support-widget-greeting' }));
|
|
const prechatForm = this.createElement('form', { id: 'support-widget-prechat-form' });
|
|
prechatForm.appendChild(this.createElement('input', {
|
|
type: 'email',
|
|
id: 'support-widget-email',
|
|
placeholder: 'Your email (optional)',
|
|
autocomplete: 'email'
|
|
}));
|
|
prechatForm.appendChild(this.createElement('input', {
|
|
type: 'text',
|
|
id: 'support-widget-name',
|
|
placeholder: 'Your name (optional)',
|
|
autocomplete: 'name'
|
|
}));
|
|
const textarea = this.createElement('textarea', {
|
|
id: 'support-widget-initial-message',
|
|
placeholder: 'How can we help?',
|
|
required: 'required',
|
|
rows: '3'
|
|
});
|
|
prechatForm.appendChild(textarea);
|
|
prechatForm.appendChild(this.createElement('button', {
|
|
type: 'submit',
|
|
id: 'support-widget-start-btn',
|
|
textContent: 'Start Chat'
|
|
}));
|
|
prechat.appendChild(prechatForm);
|
|
panel.appendChild(prechat);
|
|
|
|
// Chat form
|
|
const chatForm = this.createElement('form', { id: 'support-widget-form', className: 'sw-hidden' });
|
|
chatForm.appendChild(this.createElement('input', {
|
|
type: 'text',
|
|
id: 'support-widget-input',
|
|
placeholder: 'Type a message...',
|
|
autocomplete: 'off'
|
|
}));
|
|
const sendBtn = this.createElement('button', { type: 'submit', 'aria-label': 'Send message' });
|
|
sendBtn.appendChild(this.createSvg('M2.01 21L23 12 2.01 3 2 10l15 2-15 2z', '20'));
|
|
chatForm.appendChild(sendBtn);
|
|
panel.appendChild(chatForm);
|
|
|
|
// Loading
|
|
const loading = this.createElement('div', { id: 'support-widget-loading', className: 'sw-hidden' });
|
|
loading.appendChild(this.createElement('span', { textContent: 'Connecting...' }));
|
|
panel.appendChild(loading);
|
|
|
|
widget.appendChild(panel);
|
|
document.body.appendChild(widget);
|
|
this.container = widget;
|
|
this.injectStyles();
|
|
},
|
|
|
|
/**
|
|
* Inject widget styles.
|
|
*/
|
|
injectStyles() {
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#support-widget {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
${this.config.position === 'left' ? 'left' : 'right'}: 20px;
|
|
z-index: 999999;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
#support-widget * {
|
|
box-sizing: border-box;
|
|
}
|
|
#support-widget-toggle {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
background: ${this.config.color};
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
#support-widget-toggle:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
}
|
|
#support-widget-panel {
|
|
position: absolute;
|
|
bottom: 70px;
|
|
${this.config.position === 'left' ? 'left' : 'right'}: 0;
|
|
width: 360px;
|
|
max-width: calc(100vw - 40px);
|
|
height: 500px;
|
|
max-height: calc(100vh - 100px);
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
#support-widget-panel.sw-hidden {
|
|
display: none;
|
|
}
|
|
#support-widget-header {
|
|
padding: 16px 20px;
|
|
background: ${this.config.color};
|
|
color: white;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
#support-widget-title {
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
#support-widget-close {
|
|
background: none;
|
|
border: none;
|
|
color: white;
|
|
font-size: 28px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
opacity: 0.8;
|
|
transition: opacity 0.2s;
|
|
}
|
|
#support-widget-close:hover {
|
|
opacity: 1;
|
|
}
|
|
#support-widget-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
background: #f9fafb;
|
|
}
|
|
#support-widget-prechat {
|
|
flex: 1;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
#support-widget-prechat.sw-hidden {
|
|
display: none;
|
|
}
|
|
#support-widget-greeting {
|
|
margin: 0 0 16px 0;
|
|
color: #374151;
|
|
}
|
|
#support-widget-prechat-form input,
|
|
#support-widget-prechat-form textarea {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
}
|
|
#support-widget-prechat-form textarea {
|
|
resize: none;
|
|
}
|
|
#support-widget-prechat-form input:focus,
|
|
#support-widget-prechat-form textarea:focus {
|
|
outline: none;
|
|
border-color: ${this.config.color};
|
|
box-shadow: 0 0 0 3px ${this.config.color}20;
|
|
}
|
|
#support-widget-start-btn {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: ${this.config.color};
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
}
|
|
#support-widget-start-btn:hover {
|
|
opacity: 0.9;
|
|
}
|
|
#support-widget-start-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
#support-widget-form {
|
|
display: flex;
|
|
padding: 12px;
|
|
border-top: 1px solid #e5e7eb;
|
|
background: white;
|
|
flex-shrink: 0;
|
|
}
|
|
#support-widget-form.sw-hidden {
|
|
display: none;
|
|
}
|
|
#support-widget-input {
|
|
flex: 1;
|
|
padding: 10px 14px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 24px;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
margin-right: 8px;
|
|
}
|
|
#support-widget-input:focus {
|
|
outline: none;
|
|
border-color: ${this.config.color};
|
|
}
|
|
#support-widget-form button {
|
|
width: 40px;
|
|
height: 40px;
|
|
padding: 0;
|
|
background: ${this.config.color};
|
|
color: white;
|
|
border: none;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: opacity 0.2s;
|
|
flex-shrink: 0;
|
|
}
|
|
#support-widget-form button:hover {
|
|
opacity: 0.9;
|
|
}
|
|
#support-widget-loading {
|
|
padding: 20px;
|
|
text-align: center;
|
|
color: #6b7280;
|
|
}
|
|
#support-widget-loading.sw-hidden {
|
|
display: none;
|
|
}
|
|
.sw-message {
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.sw-message.sw-customer {
|
|
align-items: flex-end;
|
|
}
|
|
.sw-message.sw-agent {
|
|
align-items: flex-start;
|
|
}
|
|
.sw-message-bubble {
|
|
display: inline-block;
|
|
padding: 10px 14px;
|
|
border-radius: 16px;
|
|
max-width: 85%;
|
|
word-wrap: break-word;
|
|
}
|
|
.sw-customer .sw-message-bubble {
|
|
background: ${this.config.color};
|
|
color: white;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
.sw-agent .sw-message-bubble {
|
|
background: white;
|
|
color: #1f2937;
|
|
border-bottom-left-radius: 4px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
.sw-message-meta {
|
|
font-size: 11px;
|
|
color: #9ca3af;
|
|
margin-top: 4px;
|
|
}
|
|
.sw-agent-name {
|
|
font-weight: 500;
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
},
|
|
|
|
/**
|
|
* Bind event listeners.
|
|
*/
|
|
bindEvents() {
|
|
document.getElementById('support-widget-toggle').addEventListener('click', () => this.toggle());
|
|
document.getElementById('support-widget-close').addEventListener('click', () => this.close());
|
|
document.getElementById('support-widget-prechat-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.startChat();
|
|
});
|
|
document.getElementById('support-widget-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.sendMessage();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initialise with server.
|
|
*/
|
|
async initWithServer() {
|
|
this.showLoading(true);
|
|
|
|
try {
|
|
const response = await fetch(`${this.config.baseUrl}/api/support/chat/init`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
token: this.config.token,
|
|
visitor_id: this.state.visitorId,
|
|
page_url: window.location.href,
|
|
page_title: document.title,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
console.error('SupportWidget init failed:', data.message || data.error);
|
|
this.showError(data.message || 'Failed to connect');
|
|
return;
|
|
}
|
|
|
|
this.state.contactId = data.contact.id;
|
|
this.state.widgetConfig = data.widget;
|
|
this.state.isInitialised = true;
|
|
|
|
// Apply server configuration
|
|
if (data.widget.name) {
|
|
document.getElementById('support-widget-title').textContent = data.widget.name;
|
|
}
|
|
if (data.widget.greeting) {
|
|
document.getElementById('support-widget-greeting').textContent = data.widget.greeting;
|
|
}
|
|
if (data.widget.placeholder) {
|
|
document.getElementById('support-widget-input').placeholder = data.widget.placeholder;
|
|
}
|
|
if (data.widget.require_email) {
|
|
const emailInput = document.getElementById('support-widget-email');
|
|
emailInput.required = true;
|
|
emailInput.placeholder = 'Your email (required)';
|
|
}
|
|
|
|
// Check for active conversation
|
|
if (data.active_conversation) {
|
|
this.state.conversationId = data.active_conversation.id;
|
|
await this.loadHistory();
|
|
this.showChatView();
|
|
} else {
|
|
this.showPrechatView();
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('SupportWidget init error:', err);
|
|
this.showError('Failed to connect');
|
|
}
|
|
|
|
this.showLoading(false);
|
|
},
|
|
|
|
/**
|
|
* Toggle widget open/closed.
|
|
*/
|
|
toggle() {
|
|
this.state.isOpen = !this.state.isOpen;
|
|
document.getElementById('support-widget-panel').classList.toggle('sw-hidden', !this.state.isOpen);
|
|
},
|
|
|
|
/**
|
|
* Close widget.
|
|
*/
|
|
close() {
|
|
this.state.isOpen = false;
|
|
document.getElementById('support-widget-panel').classList.add('sw-hidden');
|
|
},
|
|
|
|
/**
|
|
* Show loading state.
|
|
*/
|
|
showLoading(show) {
|
|
document.getElementById('support-widget-loading').classList.toggle('sw-hidden', !show);
|
|
document.getElementById('support-widget-messages').classList.toggle('sw-hidden', show);
|
|
document.getElementById('support-widget-prechat').classList.add('sw-hidden');
|
|
document.getElementById('support-widget-form').classList.add('sw-hidden');
|
|
},
|
|
|
|
/**
|
|
* Show pre-chat form.
|
|
*/
|
|
showPrechatView() {
|
|
document.getElementById('support-widget-loading').classList.add('sw-hidden');
|
|
document.getElementById('support-widget-messages').classList.add('sw-hidden');
|
|
document.getElementById('support-widget-prechat').classList.remove('sw-hidden');
|
|
document.getElementById('support-widget-form').classList.add('sw-hidden');
|
|
},
|
|
|
|
/**
|
|
* Show chat view.
|
|
*/
|
|
showChatView() {
|
|
document.getElementById('support-widget-loading').classList.add('sw-hidden');
|
|
document.getElementById('support-widget-messages').classList.remove('sw-hidden');
|
|
document.getElementById('support-widget-prechat').classList.add('sw-hidden');
|
|
document.getElementById('support-widget-form').classList.remove('sw-hidden');
|
|
},
|
|
|
|
/**
|
|
* Show error message.
|
|
*/
|
|
showError(message) {
|
|
const loading = document.getElementById('support-widget-loading');
|
|
// Clear existing content safely
|
|
while (loading.firstChild) {
|
|
loading.removeChild(loading.firstChild);
|
|
}
|
|
const span = document.createElement('span');
|
|
span.style.color = '#ef4444';
|
|
span.textContent = message;
|
|
loading.appendChild(span);
|
|
loading.classList.remove('sw-hidden');
|
|
},
|
|
|
|
/**
|
|
* Start a new chat.
|
|
*/
|
|
async startChat() {
|
|
const email = document.getElementById('support-widget-email').value.trim();
|
|
const name = document.getElementById('support-widget-name').value.trim();
|
|
const message = document.getElementById('support-widget-initial-message').value.trim();
|
|
|
|
if (!message) return;
|
|
|
|
const btn = document.getElementById('support-widget-start-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Starting...';
|
|
|
|
try {
|
|
const response = await fetch(`${this.config.baseUrl}/api/support/chat/start`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
token: this.config.token,
|
|
contact_id: this.state.contactId,
|
|
email: email || null,
|
|
name: name || null,
|
|
message: message,
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
alert(data.message || 'Failed to start chat');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Start Chat';
|
|
return;
|
|
}
|
|
|
|
this.state.conversationId = data.conversation.id;
|
|
this.addMessage(message, 'customer');
|
|
this.showChatView();
|
|
|
|
// Clear form
|
|
document.getElementById('support-widget-email').value = '';
|
|
document.getElementById('support-widget-name').value = '';
|
|
document.getElementById('support-widget-initial-message').value = '';
|
|
|
|
} catch (err) {
|
|
console.error('Start chat error:', err);
|
|
alert('Failed to start chat');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Start Chat';
|
|
},
|
|
|
|
/**
|
|
* Send a message.
|
|
*/
|
|
async sendMessage() {
|
|
const input = document.getElementById('support-widget-input');
|
|
const message = input.value.trim();
|
|
|
|
if (!message || !this.state.conversationId) return;
|
|
|
|
this.addMessage(message, 'customer');
|
|
input.value = '';
|
|
|
|
try {
|
|
const response = await fetch(`${this.config.baseUrl}/api/support/chat/message`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
token: this.config.token,
|
|
contact_id: this.state.contactId,
|
|
conversation_id: this.state.conversationId,
|
|
message: message,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('Send message failed');
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Send message error:', err);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load chat history.
|
|
*/
|
|
async loadHistory() {
|
|
try {
|
|
const response = await fetch(
|
|
`${this.config.baseUrl}/api/support/chat/history?` +
|
|
`token=${encodeURIComponent(this.config.token)}` +
|
|
`&contact_id=${this.state.contactId}` +
|
|
`&conversation_id=${this.state.conversationId}`,
|
|
{
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.messages) {
|
|
const container = document.getElementById('support-widget-messages');
|
|
// Clear existing messages safely
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
data.messages.forEach(msg => {
|
|
this.addMessage(msg.body, msg.type, msg.agent_name, msg.created_at);
|
|
});
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('Load history error:', err);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add message to chat using safe DOM methods.
|
|
*/
|
|
addMessage(text, type, agentName, timestamp) {
|
|
const container = document.getElementById('support-widget-messages');
|
|
const div = document.createElement('div');
|
|
div.className = `sw-message sw-${type}`;
|
|
|
|
// Agent name (if agent message)
|
|
if (type === 'agent' && agentName) {
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.className = 'sw-agent-name';
|
|
nameSpan.textContent = agentName;
|
|
div.appendChild(nameSpan);
|
|
}
|
|
|
|
// Message bubble
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'sw-message-bubble';
|
|
bubble.textContent = text;
|
|
div.appendChild(bubble);
|
|
|
|
// Timestamp
|
|
if (timestamp) {
|
|
const meta = document.createElement('span');
|
|
meta.className = 'sw-message-meta';
|
|
meta.textContent = this.formatTime(timestamp);
|
|
div.appendChild(meta);
|
|
}
|
|
|
|
container.appendChild(div);
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
this.state.messages.push({ text, type, timestamp });
|
|
},
|
|
|
|
/**
|
|
* Format timestamp.
|
|
*/
|
|
formatTime(isoString) {
|
|
const date = new Date(isoString);
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
},
|
|
};
|
|
|
|
// Expose globally
|
|
window.SupportWidget = SupportWidget;
|
|
})();
|