@ticketping/chat-widget
Version:
Customer support chat widget for Ticketping - Intercom-like experience with real-time messaging
3,099 lines • 114 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
/*!
* Ticketping Chat Widget v1.0.0
* (c) 2024 Ticketping
* Licensed under MIT
*/
function createDOMElement(tag, attributes = {}) {
const element = document.createElement(tag);
if (!attributes) {
return element;
}
Object.entries(attributes).forEach(([key, value]) => {
if (value === null || value === void 0) {
return;
}
if (key === "className") {
element.className = value;
} else if (key === "textContent") {
element.textContent = value;
} else if (key === "innerHTML") {
element.innerHTML = value;
} else if (key.startsWith("on") && typeof value === "function") {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
} else if (typeof value === "boolean") {
if (value) {
element.setAttribute(key, key);
element[key] = true;
} else {
element[key] = false;
}
} else {
element.setAttribute(key, value);
}
});
return element;
}
const DEFAULT_CONFIG = {
// Required
appId: null,
teamSlug: null,
teamLogoIcon: null,
// API Configuration
apiBase: "https://api.ticketping.com",
wsBase: "wss://ws.ticketping.com",
// Authentication
userJWT: null,
enableSecureMode: true,
// Widget Appearance
position: "bottom-right",
// 'bottom-right' | 'bottom-left'
theme: "default",
// 'default' | 'dark' | 'light'
zIndex: 999999,
// Widget Behavior
autoOpen: false,
enableTypingIndicators: true,
enableFileUpload: true,
enableEmojis: true,
// File Upload
maxFileSize: 10 * 1024 * 1024,
// 10MB
allowedFileTypes: [
"image/jpeg",
"image/png",
"image/gif",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"text/plain"
],
// Messages
maxMessageLength: 5e3,
enableMarkdown: false,
enableLinkPreviews: true,
// Conversations
showConversationHistory: true,
maxConversationsStored: 50,
autoDeleteAfterDays: 30,
// Notifications
enableSoundNotifications: true,
enableBrowserNotifications: false,
// Analytics
analytics: true,
trackUserInteractions: true,
// Localization
locale: "en",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
// Development
debug: false,
logLevel: "error",
// Custom Labels
labels: {
// Header
headerTitle: "Ticketping",
headerSubtitle: "We're here to help!",
// Tabs
homeTab: "Home",
messagesTab: "Messages",
// Home Tab
welcomeTitle: "👋 Welcome!",
welcomeMessage: "Hi there! How can we help you today?",
startConversationButton: "Send us a message",
// Messages
messageInputPlaceholder: "Type your message...",
sendButton: "Send",
attachButton: "Attach file",
// Status Messages
typingIndicator: "Support is typing...",
agentOnline: "We're online and ready to help!",
agentOffline: "We'll get back to you soon!",
connectionLost: "Connection lost. Trying to reconnect...",
// Errors
fileTooLarge: "File size exceeds maximum limit",
fileTypeNotAllowed: "File type not supported",
messageTooLong: "Message is too long",
sendError: "Failed to send message. Please try again.",
loadError: "Failed to load conversation",
// Empty States
noConversations: "No conversations yet",
noConversationsDescription: "Start a conversation to get help from our support team.",
// Help Articles
helpArticlesTitle: "Popular articles",
helpArticles: [
{
title: "Getting started guide",
url: "/help/getting-started",
icon: "article"
},
{
title: "How to create tickets",
url: "/help/create-tickets",
icon: "check"
},
{
title: "Best practices",
url: "/help/best-practices",
icon: "star"
}
]
},
// Custom CSS
customCSS: null,
// Callbacks
onReady: null,
onOpen: null,
onClose: null,
onMessageSent: null,
onMessageReceived: null,
onConversationStarted: null,
onError: null
};
const API_ENDPOINTS = {
auth: "/api/v1/jwt/auth/",
newChatSession: "/api/v1/chat-session/create/",
conversations: "/api/v1/chat-sessions/",
messages: "/messages",
fileUpload: "/api/v1/chat-session/file-upload/",
analytics: "/analytics",
teamWidgetSettings: "/api/v1/team/widget/"
};
const WEBSOCKET_EVENTS = {
// Client -> Server
AUTH: "auth",
TYPING_START: "typing_start",
TYPING_STOP: "typing_stop",
JOIN_CONVERSATION: "join_conversation",
LEAVE_CONVERSATION: "leave_conversation",
FILE_ATTACHMENT: "file_attachment",
MARK_READ: "mark_read",
// Server -> Client
SERVER_STATE: "server_session_state",
SERVER_MESSAGE_HISTORY: "server_message_history",
SERVER_MESSAGE: "server_message",
SERVER_AGENT_STATUS: "server_agent_status",
SERVER_AGENT_JOINED: "server_agent_joined",
SERVER_AGENT_LEFT: "server_agent_left",
SERVER_CONVERSATION_UPDATED: "server_conversation_updated",
SERVER_AUTH_SUCCESS: "server_auth_success",
SERVER_AUTH_FAILED: "server_auth_failed",
SERVER_ANONYMOUS_AUTH_SUCCESS: "server_anonymous_auth_success",
SERVER_ANONYMOUS_AUTH_FAILED: "server_anonymous_auth_failed",
PONG: "pong",
ERROR: "error"
};
const STORAGE_KEYS = {
CONVERSATIONS: "ticketping_conversations",
USER_DATA: "ticketping_user",
SETTINGS: "ticketping_settings",
DEVICE_ID: "ticketping_device_id",
UNREAD_CONVERSATIONS: "ticketping_unread_conversations"
};
const CSS_CLASSES = {
BUBBLE: "ticketping-chat-bubble",
WINDOW: "ticketping-chat-window",
OPEN: "open"
};
class ChatBubble {
constructor(container, options = {}) {
if (!container) {
console.error("ChatBubble: Container is required");
this.container = null;
this.element = null;
this.notificationBadge = null;
this.isOpen = false;
this.options = __spreadValues({
onClick: () => {
},
showNotificationBadge: false,
notificationCount: 0
}, options);
return;
}
this.container = container;
this.options = __spreadValues({
onClick: () => {
},
showNotificationBadge: false,
notificationCount: 0
}, options);
this.element = null;
this.notificationBadge = null;
this.isOpen = false;
this.render();
this.attachEventListeners();
}
render() {
if (!this.container) {
return;
}
this.element = createDOMElement("button", {
className: `${CSS_CLASSES.BUBBLE}`,
"aria-label": "Open chat",
"aria-expanded": "false",
role: "button",
tabindex: "0"
});
const iconSvg = this.createChatIcon();
this.element.appendChild(iconSvg);
if (this.options.showNotificationBadge) {
this.createNotificationBadge();
}
this.container.appendChild(this.element);
}
createChatIcon() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 18 18");
svg.setAttribute("width", "32");
svg.setAttribute("height", "32");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M14.25,2.25H3.75c-1.105,0-2,.896-2,2v7c0,1.104,.895,2,2,2h2v3l3.75-3h4.75c1.105,0,2-.896,2-2V4.25c0-1.104-.895-2-2-2Z");
path.setAttribute("fill", "none");
path.setAttribute("stroke", this.options.iconColor || "#ffffff");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("stroke-width", "1.5");
svg.appendChild(path);
return svg;
}
createCloseIcon() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "24");
svg.setAttribute("height", "24");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z");
svg.appendChild(path);
return svg;
}
createNotificationBadge() {
this.notificationBadge = createDOMElement("div", {
className: "notification-badge"
});
this.element.appendChild(this.notificationBadge);
}
attachEventListeners() {
this.element.addEventListener("click", () => {
this.handleClick();
});
this.element.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.handleClick();
}
});
}
handleClick() {
this.options.onClick();
if (this.notificationBadge) {
this.hideNotificationBadge();
}
}
setOpen(isOpen) {
this.isOpen = isOpen;
if (!this.element) {
return;
}
if (isOpen) {
this.element.classList.add(CSS_CLASSES.OPEN);
this.element.setAttribute("aria-expanded", "true");
this.element.setAttribute("aria-label", "Close chat");
const svg = this.element.querySelector("svg");
if (svg) {
this.element.removeChild(svg);
this.element.appendChild(this.createCloseIcon());
}
} else {
this.element.classList.remove(CSS_CLASSES.OPEN);
this.element.setAttribute("aria-expanded", "false");
this.element.setAttribute("aria-label", "Open chat");
const svg = this.element.querySelector("svg");
if (svg) {
this.element.removeChild(svg);
this.element.appendChild(this.createChatIcon());
}
}
}
showNotificationBadge(count = 1) {
if (!this.element) {
return;
}
if (!this.notificationBadge) {
this.createNotificationBadge();
}
this.options.notificationCount = count;
if (count > 0) {
this.notificationBadge.style.display = "flex";
} else {
this.notificationBadge.style.display = "none";
}
}
hideNotificationBadge() {
if (this.notificationBadge) {
this.notificationBadge.style.display = "none";
this.options.notificationCount = 0;
}
}
updateNotificationCount(count) {
this.options.notificationCount = count;
if (count > 0) {
this.showNotificationBadge(count);
} else {
this.hideNotificationBadge();
}
}
setTheme(theme) {
if (this.element) {
this.element.setAttribute("data-theme", theme);
}
}
setPosition(position) {
if (this.element) {
this.element.setAttribute("data-position", position);
}
}
bounce() {
if (!this.element) {
return;
}
this.element.style.animation = "tp-bounce 0.6s ease-in-out";
setTimeout(() => {
this.element.style.animation = "";
}, 600);
}
hide() {
if (this.element) {
this.element.style.display = "none";
}
}
show() {
if (this.element) {
this.element.style.display = "";
}
}
// Accessibility
setAriaLabel(label) {
this.element.setAttribute("aria-label", label);
}
focus() {
if (this.element) {
this.element.focus();
}
}
blur() {
if (this.element) {
this.element.blur();
}
}
// State management
disable() {
if (this.element) {
this.element.disabled = true;
this.element.setAttribute("aria-disabled", "true");
}
}
enable() {
if (this.element) {
this.element.disabled = false;
this.element.setAttribute("aria-disabled", "false");
}
}
// Cleanup
destroy() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
}
// Alias methods for test compatibility
showNotification(count = 1) {
return this.showNotificationBadge(count);
}
hideNotification() {
return this.hideNotificationBadge();
}
}
class ChatWindow {
constructor(container, options = {}) {
this.container = container;
this.options = __spreadValues({
onClose: () => {
},
onTabSwitch: () => {
},
onSendMessage: () => {
},
onFileUpload: () => {
},
onConversationSelect: () => {
},
onBackButtonClick: () => {
},
teamLogoIcon: null,
teamSettings: null
}, options);
this.element = null;
this.activeTab = "home";
this.conversations = [];
this.unreadConversations = /* @__PURE__ */ new Set();
this.currentMessages = [];
this.isUploading = false;
this.render();
this.attachEventListeners();
}
render() {
this.element = createDOMElement("div", {
className: CSS_CLASSES.WINDOW
});
this.element.innerHTML = `
<div class="ticketping-chat-content">
<div class="ticketping-tab-content active" id="homeTab">
<div class="ticketping-chat-header">
<div class="ticketping-workspace-logo">
${this.getLogoHtml()}
</div>
<button class="ticketping-close-btn" aria-label="Close chat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
<div class="ticketping-home-container">
<div class="ticketping-home-content">
<h4>${this.getWelcomeTitle()}</h4>
<p>${this.getWelcomeMessage()}</p>
</div>
<div class="ticketping-actions-container">
<div class="ticketping-recent-conversation" id="recentConversationSection" style="display: none;">
<div class="ticketping-recent-conversation-header">
<span class="ticketping-recent-conversation-title">Recent Conversation</span>
</div>
<div class="ticketping-recent-conversation-item" id="recentConversationItem">
<div class="ticketping-recent-conversation-preview"></div>
<div class="ticketping-recent-conversation-time"></div>
</div>
</div>
<button class="ticketping-start-conversation-btn">
<div class="ticketping-start-conversation-btn-content">
<span class="ticketping-start-conversation-btn-text">Send us a message</span>
<span class="ticketping-start-conversation-btn-subtext">${this.getResponseTimeText()}</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 18"><path d="M16.345,1.654c-.344-.344-.845-.463-1.305-.315L2.117,5.493c-.491,.158-.831,.574-.887,1.087-.056,.512,.187,.992,.632,1.251l4.576,2.669,3.953-3.954c.293-.293,.768-.293,1.061,0s.293,.768,0,1.061l-3.954,3.954,2.669,4.576c.235,.402,.65,.639,1.107,.639,.048,0,.097-.003,.146-.008,.512-.056,.929-.396,1.086-.886L16.661,2.96h0c.148-.463,.027-.963-.316-1.306Z" fill="currentColor" fill-opacity="0.6"></path></svg>
</button>
</div>
</div>
<div class="ticketping-plug">
<a href="https://ticketping.com" target="_blank" rel="noopener noreferrer">
<span>Powered by</span> <svg width="65.499" height="13" viewBox="0 0 65.499 13" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Ticketping" style="margin-top: 1px;"><path d="M35.072 9.717q-0.6 0 -0.96 -0.36 -0.347 -0.36 -0.347 -0.96V4.304h-1.813V3.144h1.813V0.971h1.374v2.173h1.96v1.16h-1.96V8.157q0 0.4 0.374 0.4h1.374V9.717zm-6.992 0.186q-1 0 -1.746 -0.413 -0.746 -0.427 -1.173 -1.186 -0.413 -0.773 -0.413 -1.786v-0.16q0 -1.026 0.413 -1.786 0.413 -0.773 1.147 -1.186 0.746 -0.427 1.72 -0.427 0.947 0 1.653 0.427 0.719 0.413 1.12 1.16t0.4 1.746v0.52h-5.054q0.027 0.867 0.573 1.387 0.56 0.506 1.387 0.506 0.773 0 1.16 -0.347 0.4 -0.347 0.613 -0.801l1.133 0.587q-0.186 0.374 -0.547 0.786 -0.347 0.413 -0.92 0.693t-1.467 0.281m-1.921 -4.148h3.64q-0.053 -0.746 -0.534 -1.16 -0.48 -0.427 -1.253 -0.427t-1.266 0.427q-0.48 0.413 -0.587 1.16M18.099 9.717V0.385h1.374v5.32h0.213l2.586 -2.56h1.826l-3.319 3.187 3.427 3.386H22.392l-2.707 -2.733h-0.213v2.733zm-4.837 0.186q-0.947 0 -1.72 -0.4 -0.759 -0.4 -1.213 -1.16 -0.44 -0.759 -0.44 -1.826v-0.173q0 -1.067 0.44 -1.813 0.453 -0.759 1.213 -1.16 0.773 -0.413 1.72 -0.413t1.613 0.347 1.067 0.92q0.413 0.573 0.534 1.266l-1.334 0.281q-0.067 -0.44 -0.281 -0.801t-0.6 -0.573 -0.973 -0.213q-0.573 0 -1.04 0.266 -0.453 0.253 -0.719 0.746 -0.266 0.48 -0.266 1.173v0.12q0 0.693 0.266 1.186t0.719 0.746q0.467 0.253 1.04 0.253 0.867 0 1.321 -0.44 0.453 -0.453 0.573 -1.147l1.334 0.307q-0.16 0.68 -0.573 1.253 -0.4 0.573 -1.067 0.92 -0.666 0.334 -1.613 0.334m-6.464 -0.186V3.144h1.374V9.717zm0.693 -7.466q-0.4 0 -0.68 -0.253 -0.266 -0.266 -0.266 -0.68T6.812 0.65q0.281 -0.266 0.68 -0.266 0.413 0 0.68 0.266 0.266 0.253 0.266 0.666t-0.266 0.68q-0.266 0.253 -0.68 0.253M3.158 9.719q-0.6 0 -0.96 -0.36 -0.347 -0.36 -0.347 -0.96v-4.095H0.038V3.144h1.813V0.971h1.374v2.173h1.96v1.16H3.225V8.157q0 0.4 0.374 0.4H4.972V9.717zm58.588 -6.721q0.768 0 1.356 0.312 0.6 0.3 0.936 0.756v-0.961h1.38v6.72q0 0.912 -0.385 1.621 -0.385 0.719 -1.116 1.128 -0.719 0.407 -1.728 0.407 -1.343 0 -2.232 -0.636 -0.888 -0.624 -1.009 -1.704h1.356q0.156 0.516 0.66 0.828 0.516 0.324 1.224 0.324 0.828 0 1.331 -0.504 0.516 -0.504 0.516 -1.465V8.72q-0.348 0.468 -0.948 0.792 -0.588 0.312 -1.343 0.312 -0.863 0 -1.584 -0.432 -0.707 -0.444 -1.128 -1.224 -0.407 -0.792 -0.407 -1.789t0.407 -1.764q0.42 -0.768 1.128 -1.187 0.719 -0.432 1.584 -0.432m2.292 3.408q0 -0.685 -0.288 -1.187 -0.276 -0.504 -0.732 -0.768t-0.984 -0.264 -0.984 0.264q-0.456 0.251 -0.744 0.756 -0.276 0.492 -0.276 1.175t0.276 1.2q0.288 0.516 0.744 0.792 0.468 0.264 0.984 0.264 0.529 0 0.984 -0.264t0.732 -0.768q0.288 -0.516 0.288 -1.2m-9.654 -3.406q0.78 0 1.392 0.324 0.624 0.324 0.972 0.96t0.348 1.536v3.9h-1.356V6.021q0 -0.888 -0.444 -1.356 -0.444 -0.48 -1.212 -0.48t-1.224 0.48q-0.444 0.468 -0.444 1.356V9.717h-1.368V3.105h1.368v0.756q0.336 -0.407 0.853 -0.636 0.529 -0.229 1.116 -0.229m-13.743 1.081q0.348 -0.456 0.948 -0.768t1.356 -0.312q0.863 0 1.572 0.432 0.719 0.42 1.128 1.187t0.407 1.764 -0.407 1.789q-0.407 0.78 -1.128 1.224 -0.707 0.432 -1.572 0.432 -0.756 0 -1.343 -0.3 -0.588 -0.312 -0.96 -0.768v4.104h-1.368V3.105h1.368zm4.02 2.304q0 -0.685 -0.288 -1.175 -0.276 -0.504 -0.744 -0.756 -0.456 -0.264 -0.984 -0.264 -0.516 0 -0.984 0.264 -0.456 0.264 -0.744 0.768 -0.276 0.504 -0.276 1.187t0.276 1.2q0.288 0.504 0.744 0.768 0.468 0.264 0.984 0.264 0.529 0 0.984 -0.264 0.468 -0.276 0.744 -0.792 0.288 -0.516 0.288 -1.2m2.981 3.337V3.144h1.374V9.717zm0.693 -7.467q-0.4 0 -0.68 -0.253 -0.266 -0.266 -0.266 -0.68t0.266 -0.666q0.281 -0.266 0.68 -0.266 0.413 0 0.68 0.266 0.266 0.253 0.266 0.666t-0.266 0.68q-0.266 0.253 -0.68 0.253" fill="currentColor"/></svg>
</a>
</div>
</div>
<div class="ticketping-tab-content" id="messagesTab">
<div class="ticketping-messages-header">
<button class="ticketping-back-btn" id="tpBackBtn" aria-label="Go back">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"></path></svg>
</button>
<div class="ticketping-tab-heading">
<span>Messages</span>
<span class="ticketping-status-dot ${this.getAvailabilityStatusClass()}"></span>
</div>
<button class="ticketping-close-btn-2" aria-label="Close chat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
<div class="ticketping-messages-content">
<div class="ticketping-conversation-container">
<div class="ticketping-conversation-list ticketping-thin-scrollbar" id="conversationList">
<!-- Conversations will be populated here -->
</div>
<div class="ticketping-send-a-message-container" id="sendMessageBtnContainer">
<button class="ticketping-send-message-btn">
<span class="ticketping-send-message-btn-text">Send us a message</span>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="16px" height="16px" viewBox="0 0 18 18"><path d="M16.345,1.654c-.344-.344-.845-.463-1.305-.315L2.117,5.493c-.491,.158-.831,.574-.887,1.087-.056,.512,.187,.992,.632,1.251l4.576,2.669,3.953-3.954c.293-.293,.768-.293,1.061,0s.293,.768,0,1.061l-3.954,3.954,2.669,4.576c.235,.402,.65,.639,1.107,.639,.048,0,.097-.003,.146-.008,.512-.056,.929-.396,1.086-.886L16.661,2.96h0c.148-.463,.027-.963-.316-1.306Z" fill="currentColor"></path></svg>
</button>
</div>
</div>
<div class="active-conversation" id="activeConversation" style="display: none;">
<div class="ticketping-loading-state" id="loadingState" style="display: none;">
<div class="ticketping-loading-content">
<div class="ticketping-loading-spinner">
<div class="tp-loading-spinner-child"></div>
</div>
<div class="ticketping-loading-text">
<p>Starting conversation...</p>
<p class="ticketping-loading-subtext">Connecting you with support</p>
</div>
</div>
</div>
<div class="ticketping-messages-list ticketping-thin-scrollbar" id="messagesList">
<!-- Messages will be populated here -->
</div>
<div class="typing-indicator" id="typingIndicator">
Support is typing
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="ticketping-availability-bar" id="availabilityBar">
${this.getAvailabilityHtml()}
</div>
<div class="ticketping-message-input-container">
<div class="ticketping-message-input-wrapper">
<textarea
id="messageInput"
class="ticketping-message-input"
placeholder="Type your message..."
rows="1"
></textarea>
<div class="ticketping-input-actions">
<div class="ticketping-file-input-container">
<input type="file" class="ticketping-file-input" accept="image/*,.pdf,.doc,.docx">
<button class="ticketping-file-btn">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="16px" height="16px" viewBox="0 0 18 18"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 1.75C14.5 1.33579 14.1642 1 13.75 1C13.3358 1 13 1.33579 13 1.75V3.5H11.25C10.8358 3.5 10.5 3.83579 10.5 4.25C10.5 4.66421 10.8358 5 11.25 5H13V6.75C13 7.16421 13.3358 7.5 13.75 7.5C14.1642 7.5 14.5 7.16421 14.5 6.75V5H16.25C16.6642 5 17 4.66421 17 4.25C17 3.83579 16.6642 3.5 16.25 3.5H14.5V1.75Z" fill="currentColor"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4.75C2 2.67879 3.67879 1 5.75 1C7.82121 1 9.5 2.67879 9.5 4.75V11.75C9.5 12.9922 8.49221 14 7.25 14C6.00779 14 5 12.9922 5 11.75V5C5 4.58579 5.33579 4.25 5.75 4.25C6.16421 4.25 6.5 4.58579 6.5 5V11.75C6.5 12.1638 6.83621 12.5 7.25 12.5C7.66379 12.5 8 12.1638 8 11.75V4.75C8 3.50721 6.99279 2.5 5.75 2.5C4.50721 2.5 3.5 3.50721 3.5 4.75V11.75C3.5 13.8208 5.17921 15.5 7.25 15.5C9.32079 15.5 11 13.8208 11 11.75V8.75C11 8.33579 11.3358 8 11.75 8C12.1642 8 12.5 8.33579 12.5 8.75V11.75C12.5 14.6492 10.1492 17 7.25 17C4.35079 17 2 14.6492 2 11.75V4.75Z" fill="currentColor" fill-opacity="0.4" data-color="color-2"></path></svg>
</button>
</div>
<button class="ticketping-send-btn" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256"><path d="M208.49,120.49a12,12,0,0,1-17,0L140,69V216a12,12,0,0,1-24,0V69L64.49,120.49a12,12,0,0,1-17-17l72-72a12,12,0,0,1,17,0l72,72A12,12,0,0,1,208.49,120.49Z"></path></svg>
</button>
</div>
</div>
<div class="ticketping-uploading-overlay" style="display: none;">
<div class="ticketping-uploading-content">
<div class="ticketping-spinner">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#e5e7eb" stroke-width="2"/>
<path d="M12 2a10 10 0 0 1 10 10" stroke="#3b82f6" stroke-width="2" stroke-linecap="round">
<animateTransform attributeName="transform" attributeType="XML" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/>
</path>
</svg>
</div>
<span class="ticketping-uploading-text">Uploading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ticketping-chat-tabs" id="ticketpingChatTabs">
<button class="ticketping-tab active" data-tab="home">
<div class="tab-icon">
<svg class="icon-inactive" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 18"><path d="M14.855 5.95L9.605 1.96C9.247 1.688 8.752 1.688 8.395 1.96L3.145 5.95C2.896 6.139 2.75 6.434 2.75 6.747V14.251C2.75 15.356 3.645 16.251 4.75 16.251H7.25V12.251C7.25 11.699 7.698 11.251 8.25 11.251H9.75C10.302 11.251 10.75 11.699 10.75 12.251V16.251H13.25C14.355 16.251 15.25 15.356 15.25 14.251V6.746C15.25 6.433 15.104 6.14 14.855 5.95Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"></path></svg>
<svg class="icon-active" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 18"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.0591 1.36312C9.4333 0.886573 8.56694 0.887449 7.94127 1.36281L2.69155 5.3526C2.2559 5.68346 2 6.19867 2 6.746V14.25C2 15.7692 3.23079 17 4.75 17H13.25C14.7692 17 16 15.7692 16 14.25V6.746C16 6.20008 15.7448 5.68398 15.3088 5.35288L10.0591 1.36312Z" fill="currentColor" fill-opacity="0.4" data-color="color-2"></path>
<path d="M11.5 13.5V17H6.5V13.5C6.5 12.1193 7.61929 11 9 11C10.3807 11 11.5 12.1193 11.5 13.5Z" fill="currentColor"></path></svg>
</div>
<span>Home</span>
</button>
<button class="ticketping-tab" data-tab="messages">
<div class="tab-icon">
<div class="ticketping-tab-unread-dot" id="messagesTabUnreadDot" style="display: none;"></div>
<svg class="icon-inactive" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 18"><path d="M14.25,2.25H3.75c-1.105,0-2,.896-2,2v7c0,1.104,.895,2,2,2h2v3l3.75-3h4.75c1.105,0,2-.896,2-2V4.25c0-1.104-.895-2-2-2Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><line x1="5" y1="6.25" x2="13" y2="6.25" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" data-color="color-2"></line><line x1="5" y1="9.25" x2="10.25" y2="9.25" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" data-color="color-2"></line></svg>
<svg class="icon-active" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 18"><path d="M3.75 1.5C2.23054 1.5 1 2.73203 1 4.25V11.25C1 12.768 2.23054 14 3.75 14H5V16.25C5 16.5383 5.16526 16.8011 5.42511 16.926C5.68496 17.0509 5.99339 17.0158 6.21852 16.8357L9.76309 14H14.25C15.7695 14 17 12.768 17 11.25V4.25C17 2.73203 15.7695 1.5 14.25 1.5H3.75Z" fill="currentColor" fill-opacity="0.4" data-color="color-2"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.25 6.25C4.25 5.83579 4.58579 5.5 5 5.5H13C13.4142 5.5 13.75 5.83579 13.75 6.25C13.75 6.66421 13.4142 7 13 7H5C4.58579 7 4.25 6.66421 4.25 6.25Z" fill="currentColor"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.25 9.25C4.25 8.83579 4.58579 8.5 5 8.5H10.25C10.6642 8.5 11 8.83579 11 9.25C11 9.66421 10.6642 10 10.25 10H5C4.58579 10 4.25 9.66421 4.25 9.25Z" fill="currentColor"></path></svg>
</div>
<span>Messages</span>
</button>
</div>
`;
this.container.appendChild(this.element);
}
attachEventListeners() {
this.element.querySelector(".ticketping-close-btn").addEventListener("click", () => {
this.options.onClose();
});
this.element.querySelector(".ticketping-close-btn-2").addEventListener("click", () => {
this.options.onClose();
});
this.element.querySelector("#tpBackBtn").addEventListener("click", () => {
this.showConversationList();
this.options.onBackButtonClick();
});
this.element.querySelectorAll(".ticketping-tab").forEach((tab) => {
tab.addEventListener("click", () => {
this.switchTab(tab.dataset.tab);
});
});
this.element.querySelector(".ticketping-start-conversation-btn").addEventListener("click", () => {
this.options.onConversationSelect("new");
setTimeout(() => {
this.element.querySelector("#messageInput").focus();
}, 50);
});
this.element.querySelector(".ticketping-send-message-btn").addEventListener("click", () => {
this.options.onConversationSelect("new");
setTimeout(() => {
this.element.querySelector("#messageInput").focus();
}, 50);
});
const recentConversationItem = this.element.querySelector("#recentConversationItem");
if (recentConversationItem) {
recentConversationItem.addEventListener("click", () => {
const sessionId = recentConversationItem.dataset.conversationId;
if (sessionId) {
this.options.onConversationSelect(sessionId);
}
});
}
const messageInput = this.element.querySelector("#messageInput");
const sendBtn = this.element.querySelector(".ticketping-send-btn");
messageInput.addEventListener("input", () => {
this.handleInputChange(messageInput, sendBtn);
});
messageInput.addEventListener("keypress", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.sendMessage(messageInput, sendBtn);
}
});
sendBtn.addEventListener("click", () => {
this.sendMessage(messageInput, sendBtn);
});
const fileBtn = this.element.querySelector(".ticketping-file-btn");
const fileInput = this.element.querySelector(".ticketping-file-input");
fileBtn.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", () => this.handleFileUpload(fileInput));
}
show() {
this.element.classList.add(CSS_CLASSES.OPEN);
}
hide() {
this.element.classList.remove(CSS_CLASSES.OPEN);
}
switchTab(tabName) {
this.activeTab = tabName;
this.element.querySelectorAll(".ticketping-tab").forEach((tab) => {
tab.classList.toggle("active", tab.dataset.tab === tabName);
});
this.element.querySelectorAll(".ticketping-tab-content").forEach((content) => {
content.classList.toggle("active", content.id === `${tabName}Tab`);
});
if (tabName === "messages") {
this.element.querySelector("#conversationList").classList.add("show");
this.element.querySelector("#sendMessageBtnContainer").classList.add("show");
} else {
this.element.querySelector("#conversationList").classList.remove("show");
this.element.querySelector("#sendMessageBtnContainer").classList.remove("show");
}
this.options.onTabSwitch(tabName);
}
setConversations(conversations, unreadConversations = /* @__PURE__ */ new Set()) {
this.conversations = conversations;
this.unreadConversations = unreadConversations instanceof Set ? unreadConversations : new Set(unreadConversations);
this.renderConversationList();
this.updateRecentConversation();
}
renderConversationList() {
const listElement = this.element.querySelector("#conversationList");
if (this.conversations.length === 0) {
this.showEmptyState();
return;
}
listElement.innerHTML = "";
const sortedConversations = [...this.conversations].sort((a, b) => {
const dateA = new Date(a.modified || a.created);
const dateB = new Date(b.modified || b.created);
return dateB - dateA;
});
sortedConversations.forEach((conversation) => {
var _a;
const isUnread = this.unreadConversations.has(conversation.sessionId);
const item = createDOMElement("div", {
className: `ticketping-conversation-item${isUnread ? " unread" : ""}`,
"data-conversation": conversation.sessionId
});
const lastMessage = (_a = conversation["messages"]) == null ? void 0 : _a[conversation["messages"].length - 1];
const snippet = lastMessage ? lastMessage.messageText.substring(0, 50) + "..." : "";
item.innerHTML = `
${isUnread ? '<div class="ticketping-unread-dot"></div>' : ""}
<div class="ticketping-conversation-preview${isUnread ? " unread" : ""}">${conversation.summary || snippet || "Support Chat"}</div>
<div class="ticketping-conversation-time">${this.formatDateTime(conversation.modified || conversation.created)}</div>
`;
item.addEventListener("click", () => {
this.options.onConversationSelect(conversation.sessionId);
});
listElement.appendChild(item);
});
}
showEmptyState() {
const listElement = this.element.querySelector("#conversationList");
listElement.innerHTML = `
<div class="empty-state">
<div class="empty-state-content">
<p>No conversations yet</p>
<p class="empty-state-subtext">Send us a message to get help</p>
</div>
</div>
`;
}
showConversationItem() {
this.element.querySelector("#conversationList").classList.remove("show");
this.element.querySelector("#activeConversation").style.display = "flex";
this.element.querySelector("#tpBackBtn").classList.add("show");
this.element.querySelector("#sendMessageBtnContainer").classList.remove("show");
this.element.querySelector("#ticketpingChatTabs").style.display = "none";
this.hideLoadingState();
setTimeout(() => {
this.element.querySelector("#messageInput").focus();
}, 50);
}
showConversationList() {
this.element.querySelector("#conversationList").classList.add("show");
this.element.querySelector("#activeConversation").style.display = "none";
this.element.querySelector("#tpBackBtn").classList.remove("show");
this.element.querySelector("#sendMessageBtnContainer").classList.add("show");
this.element.querySelector("#ticketpingChatTabs").style.display = "flex";
this.clearMessages();
this.hideLoadingState();
}
showLoadingState() {
this.element.querySelector("#conversationList").classList.remove("show");
this.element.querySelector("#activeConversation").style.display = "flex";
this.element.querySelector("#loadingState").style.display = "flex";
this.element.querySelector("#messagesList").style.display = "none";
this.element.querySelector("#typingIndicator").style.display = "none";
this.element.querySelector(".ticketping-message-input-container").style.display = "none";
this.element.querySelector("#tpBackBtn").classList.add("show");
this.element.querySelector("#sendMessageBtnContainer").classList.remove("show");
this.element.querySelector("#ticketpingChatTabs").style.display = "none";
}
hideLoadingState() {
this.element.querySelector("#loadingState").style.display = "none";
this.element.querySelector("#messagesList").style.display = "block";
this.element.querySelector("#typingIndicator").style.display = "none";
this.element.querySelector(".ticketping-message-input-container").style.display = "block";
}
addMessage(message) {
const messagesList = this.element.querySelector("#messagesList");
const previousMessage = this.currentMessages[this.currentMessages.length - 1];
this.currentMessages.push(message);
const processedMessage = this.processNewMessageForGrouping(message, previousMessage);
if (processedMessage.updatePrevious && previousMessage) {
const previousMessageElements = messagesList.querySelectorAll(".ticketping-message");
const lastPreviousElement = previousMessageElements[previousMessageElements.length - 1];
if (lastPreviousElement) {
this.updateMessageElementForGrouping(lastPreviousElement, previousMessage, false);
}
}
if (processedMessage.showDateSeparator) {
const dateSeparator = this.createDateSeparatorElement(processedMessage.created);
messagesList.appendChild(dateSeparator);
}
const messageElement = this.createMessageElement(processedMessage);
messagesList.appendChild(messageElement);
this.scrollToBottom();
}
setMessages(messages) {
const messagesList = this.element.querySelector("#messagesList");
messagesList.innerHTML = "";
this.currentMessages = [...messages];
const processedMessages = this.processMessagesForGrouping(messages);
processedMessages.forEach((message) => {
if (message.showDateSeparator) {
const dateSeparator = this.createDateSeparatorElement(message.created);
messagesList.appendChild(dateSeparator);
}
const messageElement = this.createMessageElement(message);
messagesList.appendChild(messageElement);
});
this.scrollToBottom();
}
clearMessages() {
const messagesList = this.element.querySelector("#messagesList");
messagesList.innerHTML = "";
this.currentMessages = [];
}
createMessageElement(message) {
let cssClasses = `ticketping-message ${message.sender.toLowerCase()}`;
if (message.isGrouped) {
cssClasses += " grouped";
}
if (message.isFirstInGroup) {
cssClasses += " first-in-group";
}
if (message.isLastInGroup) {
cssClasses += " last-in-group";
}
const element = createDOMElement("div", {
className: cssClasses
});
const hasAttachment = message.filename && message.filepath;
const showTimestamp = message.showTimestamp !== false;
if (hasAttachment) {
const messageContent = message.messageHtml ? message.messageHtml : this.escapeHtml(message.messageText || "");
const attachmentHtml = this.createAttachmentHtml(message.filename, message.filepath);
element.innerHTML = `
<div class="ticketping-message-bubble">
${messageContent}
${attachmentHtml}
</div>
${showTimestamp ? `<div class="ticketping-message-time">${this.formatTime(message.created)}</div>` : ""}
`;
} else {
element.innerHTML = `
<div class="ticketping-message-bubble">${message.messageHtml ? message.messageHtml : this.escapeHtml(message.messageText)}</div>
${showTimestamp ? `<div class="ticketping-message-time">${this.formatTime(message.created)}</div>` : ""}
`;
}
return element;
}
createDateSeparatorElement(date) {
const element = createDOMElement("div", {
className: "ticketping-date-separator"
});
element.innerHTML = `
<div class="ticketping-date-separator-line"></div>
<div class="ticketping-date-separator-text">${this.formatDateSeparator(date)}</div>
<div class="ticketping-date-separator-line"></div>
`;
return element;
}
processMessagesForGrouping(messages) {
if (!messages || messages.length === 0) {
return messages;
}
if (messages.length === 1) {
return [__spreadProps(__spreadValues({}, messages[0]), { showTimestamp: true })];
}
const TIME_THRESHOLD = 5 * 60 * 1e3;
const processedMessages = messages.map((message) => __spreadProps(__spreadValues({}, message), { showTimestamp: false, showDateSeparator: false }));
const groups = [];
let currentGroup = { start: 0, end: 0, sender: processedMessages[0].sender };
for (let i = 1; i < processedMessages.length; i++) {
const current = processedMessages[i];
const previous = processedMessages[i - 1];
const currentDate = this.getDateInUserTimezone(current.created);
const previousDate = this.getDateInUserTimezone(previous.created);
if (currentDate !== previousDate) {
processedMessages[i].showDateSeparator = true;
}
const currentTime = new Date(current.created).getTime();
const previousTime = new Date(previous.created).getTime();
const timeGap = currentTime - previousTime;
const senderChanged = current.sender !== previous.sender;
const timeGapTooLarge = timeGap > TIME_THRESHOLD;
if (senderChanged || timeGapTooLarge) {
currentGroup.end = i - 1;
groups.push(currentGroup);
currentGroup = { start: i, end: i, sender: current.sender };
} else {
currentGroup.end = i;
}
}
groups.push(currentGroup);
groups.forEach((group) => {
processedMessages[group.end].showTimestamp = true;
if (group.start === group.end) {
processedMessages[group.start].showTimestamp = true;
} else {
for (let i = group.start; i <= group.end; i++) {
processedMessages[i].isGrouped = true;
processedMessages[i].isFirstInGroup = i === group.start;
processedMessages[i].isLastInGroup = i === group.end;
}
}
});
processedMessages[0].showTimestamp = true;
return processedMessages;
}
processNewMessageForGrouping(newMessage, previousMessage) {
const TIME_THRESHOLD = 5 * 60 * 1e3;
const processedMessage = __spreadProps(__spreadValues({}, newMessage), {
showTimestamp: true,
// Default to showing timestamp
showDateSeparator: false,
isGrouped: false,
isFirstInGroup: false,
isLastInGroup: false,
updatePrevious: false
});
if (!previousMessage) {
return processedMessage;
}
const currentDate = this.getDateInUserTimezone(newMessage.created);
const previousDate = this.getDateInUserTimezone(previousMessage.created);
if (currentDate !== previousDate) {
processedMessage.showDateSeparator = true;
}
const currentTime = new Date(newMessage.created).getTime();
const previousTime = new Date(previousMessage.created).getTime();
const timeGap = currentTime - previousTime;
const senderChanged = newMessage.sender !== previousMessage.sender;
const timeGapTooLarge = timeGap > TIME_THRESHOLD;
if (!senderChanged && !timeGapTooLarge) {
processedMessage.isGrouped = true;
processedMessage.isLastInGroup = true;
processedMessage.showTimestamp = true;
processedMessage.updatePrevious = true;
}
return processedMessage;
}
updateMessageElementForGrouping(messageElement, message, showTimestamp) {
messageElement.classList.add("grouped");
messageElement.classList.remove("last-in-group");
const timeElement = messageElement.querySelector(".ticketping-message-time");
if (timeElement) {
timeElement.style.display = showTimestamp ? "block" : "none";
}
}
handleInputChange(input, sendBtn) {
const hasText = input.value.trim().length > 0;
sendBtn.disabled = !hasText;
this.toggleFileInputButton(!hasText);
this.autoResizeTextarea(input);
}
sendMessage(input, sendBtn) {
const text = input.value.trim();
if (!text) {
return;
}
this.options.onSendMessage({ text });
input.value = "";
sendBtn.disabled = true;
this.toggleFileInputButton(true);
this.autoResizeTextarea(input);
}
toggleFileInputButton(show) {
const fileInputContainer = this.element.querySelector(".ticketping-file-input-container");
if (fileInputContainer) {
fileInputContainer.classList.toggle("tp-hidden", !show);
}
}
handleFileUpload(fileInput) {
return __async(this, null, function* () {
const file = fileInput.files[0];
if (file) {
this.setUploadingState(true);
yield this.options.onFileUpload(file);
fileInput.value = "";
this.finishFileUpload();
}
});
}
setUploadingState(isUploading) {
this.isUploading = isUploading;
const uploadingOverlay = this.element.querySelector(".ticketping-uploading-overlay");
if (isUploading) {
uploadingOverlay.style.display = "flex";
} else {
uploadingOverlay.style.display = "none";
}
}
finishFileUpload() {
this.setUploadingState(false);
}
showTypingIndicator(show = true) {
const indicator = this.element.querySelector("#typingIndicator");
indicator.classList.toggle("show", show);
if (show) {
this.scrollToBottom();
}
}
autoResizeTextarea(textarea) {
textarea.style.height = "auto";
textarea.style.height = Math.min(textarea.scrollHeight, 100) + "px";
}
scrollToBottom() {
const messagesList = this.element.querySelector("#messagesList");
setTimeout(() => {
messagesList.scrollTop = messagesList.scrollHeight;
}, 100);
}
formatTime(date) {
return new Date(date).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
}
formatDateTime(date) {
return new Date(date).toLocaleString([], { month: "long", day: "numeric", hour: "numeric", minute: "2-digit" });
}
getDateInUserTimezone(date) {
return new Date(date).toLocaleDateString();
}
formatDateSeparator(date) {
const messageDate = new Date(date);
const today = /* @__PURE__ */ new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const messageDateStr = this.getDateInUserTimezone(date);
const todayStr = this.getDateInUserTimezone(today);
const yesterdayStr = this.getDateInUserTimezone(yesterday);
if (messageDateStr === todayStr) {
return "Today";
} else if (messageDateStr === yesterdayStr) {
return "Yesterday";
} else {
return messageDate.toLocaleDateString([], {
weekday: "long",
month: "long",
day: "numeric",
year: messageDate.getFullYear() !== today.getFullYear() ? "numeric" : void 0
});
}
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
getLogoHtml() {
const teamLogoIcon = this.options.teamLogoIcon;
if (teamLogoIcon) {
return `<img src="${teamLogoIcon}" alt="logo">`;
}
return '<svg width="40" height="40" viewBox="0 0 1.2 1.2" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.1 0.25a0.15 0.15 0 1 1 -0.3 0 0.15 0.15 0 0 1 0.3 0" fill="#4CB782"/><path opacity=".5" d="M0.762 0.127A0.5 0.5 0 0 0 0.6 0.1C0.324 0.1 0.1 0.324 0.1 0.6c0 0.08 0.019 0.156 0.052 0.223 0.009 0.018 0.012 0.038 0.007 0.057l-0.03 0.111a0.065 0.065 0 0 0 0.08 0.08l0.111 -0.03a0.082 0.082 0 0 1 0.057 0.007A0.498 0.498 0 0 0 0.6 1.1c0.276 0 0.5 -0.224 0.5 -0.5 0 -0.057 -0.009 -0.111 -0.027 -0.162a0.225 0.225 0 0 1 -0.312 -0.312" fill="#1C274C"/></svg>';
}
getWelcomeTitle() {
return "Hi there 👋";
}
getWelcomeMessage() {
const teamSettings = this.options.teamSettings;
if (teamSettings == null ? void 0 : teamSettings.widgetWelcomeMessage) {
return this.escapeHtml(teamSettings.widgetWelcomeMessage);
}
return "How can we help you?";
}
getAvailabilityHtml() {
const teamSettings = this.options.teamSettings;
if (!teamSettings) {
return "";
}
const { isAvailable, nextAvailable, workHoursDisplay } = teamSettings;
if (isAvailable) {
return "";
}
const moonIcon = '<svg class="ticketping-availability-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 18 18"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.54419 1.47446C8.70875 1.73227 8.70028 2.06417 8.52278 2.31324C7.88003 3.21522 7.5 4.31129 7.5 5.49999C7.5 8.53778 9.96222 11 13 11C14.0509 11 15.029 10.7009 15.8667 10.1868C16.1275 10.0267 16.4594 10.0412 16.7053 10.2233C16.9513 10.4054 17.0619 10.7186 16.9848 11.0148C16.0904 14.4535 12.9735 17 9.25 17C4.83179 17 1.25 13.4182 1.25 8.99999C1.25 5.08453 4.06262 1.83365 7.77437 1.14073C8.07502 1.0846 8.37963 1.21666 8.54419 1.47446Z" fill="currentColor"></path></svg>';
const subtitle = `<div class="ticketping-availability-subtitle">Leave a message and we'll pick it up first thing.</div>`;
if (nextAvailable) {
return `
<div class="ticketping-availability ticketping-availability--offline">
${moonIcon}
<div class="ticketping-availability-content">
<div class="ticketping-availability-title">Offline right now — back ${nextAvailable.dayName} at ${nextAvailable.timeFormatted} ${nextAvailable.timezoneAbbr}</div>
${subtitle}
</div>
</div>
`;
}
if (workHoursDisplay == null ? void 0 : workHoursDisplay.full) {
return `
<div class="ticketping-availability ticketping-availability--offline">
${moonIcon}
<div class="ticketping-availability-content">
<div class="ticketping-availability-title">Offline right now — Hours: ${this.escapeHtml(workHoursDisplay.full)}</div>
${subtitle}
</div>
</div>
`;
}
return `
<div class="ticketping-availability ticketping-availability--offline">
${moonIcon}
<div class="ticketping-availability-content">
<div class="ticketping-availability-title">Offline right now</div>
${subtitle}
</div>
</div>
`;
}
getAvailabilityStatusClass() {
const teamSettings = this.options.teamSettings;
if (!teamSettings) {
return "online";
}
return teamSettings.isAvailable ? "online" : "offline";
}
getResponseTimeText() {
const teamSettings = this.options.teamSettings;
if (!teamSettings) {
return "Typically replies within minutes";
}
if (teamSettings.isAvailable) {
return "Typically replies within minutes";
}
if (teamSettings.nextAvailable) {
return `We'll respond on ${teamSettings.nextAvailable.dayName} at ${teamSettings.nextAvailable.timeFormatted}`;
}
return "Typically replies within minutes";
}
isImageFile(filename) {
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
const extension = filename.toLowerCase().substring(filename.lastIndexOf("."));
return imageExtensions.includes(extension);
}
createAttachmentHtml(filename, filepath) {
const escapedFilename = this.escapeHtml(filename);
const isImage = this.isImageFile(filename);
if (isImage) {
return `
<div class="ticketping-message-attachment">
<div class="ticketping-attachment-image">
<a href="${filepath}" target="_blank" rel="noopener noreferrer">
<div class="ticketping-image-container">
<div class="ticketping-image-placeholder"></div>
<img src="${filepath}" alt="${escapedFilename}" class="ticketping-attachment-img" onload="this.parentElement.classList.add('loaded')" onerror="this.parentElement.classList.add('error')" />
</div>
</a>
</div>
</div>
`;
} else {
return `
<div class="ticketping-message-attachment">
<div class="ticketping-attachment-info">
<div class="ticketping-attachment-name">
<a href="${filepath}" target="_blank" rel="noopener noreferrer" style="color: #3B82F6; text-decoration: underline;">
${escapedFilename}
</a>
</div>
</div>
</div>
`;
}
}
updateAgentStatus(status) {
const headerSubtext = this.element.querySelector(".ticketping-chat-header-content p");
if (status === "online") {
headerSubtext.textContent = "We're online and ready to help!";
} else {
headerSubtext.textContent = "We'll get back to you soon!";
}
}
updateRecentConversation() {
var _a;
const recentConversationSection = this.element.querySelector("#recentConversationSection");
const recentConversationItem = this.element.querySelector("#recentConversationItem");
if (!recentConversationSection || !recentConversationItem) {
return;
}
if (this.conversations.length === 0) {
recentConversationSection.style.display = "none";
return;
}
const sortedConversations = [...this.conversations].sort((a, b) => {
const dateA = new Date(a.modified || a.created);
const dateB = new Date(b.modified || b.created);
return dateB - dateA;
});
const recentConversation = sortedConversations[0];
const isUnread = this.unreadConversations.has(recentConversation.sessionId);
const lastMessage = (_a = recentConversation["messages"]) == null ? void 0 : _a[recentConversation["messages"].length - 1];
const snippet = lastMessage ? lastMessage.messageText.substring(0, 60) + "..." : "";
const preview = recentConversation.summary || snippet || "Support Chat";
const previewElement = recentConversationItem.querySelector(".ticketping-recent-conversation-preview");
previewElement.textContent = preview;
if (isUnread) {
recentConversationItem.classList.add("unread");
previewElement.classList.add("unread");
if (!recentConversationItem.querySelector(".ticketping-unread-dot")) {
const dot = createDOMElement("div", { className: "ticketping-unread-dot" });
recentConversationItem.insertBefore(dot, recentConversationItem.firstChild);
}
} else {
recentConversationItem.classList.remove("unread");
previewElement.classList.remove("unread");
const existingDot = recentConversationItem.querySelector(".ticketping-unread-dot");
if (existingDot) {
existingDot.remove();
}
}
recentConversationItem.querySelector(".ticketping-recent-conversation-time").textContent = this.formatDateTime(recentConversation.modified || recentConversation.created);
recentConversationItem.dataset.conversationId = recentConversation.sessionId;
recentConversationSection.style.display = "block";
}
/**
* Show or hide the unread dot on the Messages tab
*/
setMessagesTabUnread(hasUnread) {
const dot = this.element.querySelector("#messagesTabUnreadDot");
if (dot) {
dot.style.display = hasUnread ? "block" : "none";
}
}
showError(message) {
console.error(message);
}
destroy() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
}
}
class WebSocketService {
constructor(wsUrl, token = null, options = {}) {
this.wsUrl = wsUrl;
this.token = token;
this.isAnonymous = !token;
this.options = __spreadValues({
onSessionState: () => {
},
onMessage: () => {
},
onFileAttachment: () => {
},
onMessageHistory: () => {
},
onTyping: () => {
},
onStatusChange: () => {
},
onError: () => {
},
onConnect: () => {
},
onDisconnect: () => {
},
reconnectAttempts: 5,
reconnectDelay: 1e3,
heartbeatInterval: 3e4
}, options);
this.ws = null;
this.isConnected = false;
this.reconnectCount = 0;
this.heartbeatTimer = null;
this.reconnectTimer = null;
this.typingTimer = null;
this.connect();
}
connect() {
try {
this.ws = new WebSocket(this.wsUrl);
this.attachEventListeners();
} catch (error) {
console.error("WebSocket connection failed:", error);
this.options.onError(error);
this.scheduleReconnect();
}
}
attachEventListeners() {
this.ws.onopen = (event) => {
console.log("WebSocket connected");
this.isConnected = true;
this.reconnectCount = 0;
this.authenticate();
this.startHeartbeat();
this.options.onConnect(event);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
this.options.onError(error);
}
};
this.ws.onclose = (event) => {
console.log("WebSocket disconnected:", event.code, event.reason);
this.isConnected = false;
this.stopHeartbeat();
this.options.onDisconnect(event);
if (event.code !== 1e3 && this.reconnectCount < this.options.reconnectAttempts) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
this.options.onError(error);
};
}
authenticate() {
if (this.isAnonymous) {
this.send({
type: WEBSOCKET_EVENTS.AUTH,
anonymous: true
});
} else {
this.send({
type: WEBSOCKET_EVENTS.AUTH,
token: this.token
});
}
}
handleMessage(data) {
switch (data.type) {
case WEBSOCKET_EVENTS.SERVER_STATE:
this.options.onSessionState(data);
break;
case WEBSOCKET_EVENTS.SERVER_MESSAGE:
this.options.onMessage(data);
break;
case WEBSOCKET_EVENTS.FILE_ATTACHMENT:
this.options.onFileAttachment(data);
break;
case WEBSOCKET_EVENTS.SERVER_MESSAGE_HISTORY:
this.options.onMessageHistory(data);
break;
case WEBSOCKET_EVENTS.SERVER_TYPING:
this.options.onTyping(data);
break;
case WEBSOCKET_EVENTS.SERVER_AGENT_STATUS:
this.options.onStatusChange(data);
break;
case WEBSOCKET_EVENTS.SERVER_AGENT_JOINED:
this.options.onStatusChange(__spreadValues({ type: "agent_joined" }, data));
break;
case WEBSOCKET_EVENTS.SERVER_AGENT_LEFT:
this.options.onStatusChange(__spreadValues({ type: "agent_left" }, data));
break;
case WEBSOCKET_EVENTS.SERVER_CONVERSATION_UPDATED:
this.options.onMessage(data);
break;
case WEBSOCKET_EVENTS.SERVER_AUTH_SUCCESS:
console.log("WebSocket authentication successful");
break;
case WEBSOCKET_EVENTS.SERVER_AUTH_FAILED:
console.error("WebSocket authentication failed");
this.options.onError(new Error("Authentication failed"));
this.disconnect();
break;
case WEBSOCKET_EVENTS.SERVER_ANONYMOUS_AUTH_SUCCESS:
console.log("Anonymous WebSocket authentication successful");
break;
case WEBSOCKET_EVENTS.SERVER_ANONYMOUS_AUTH_FAILED:
console.error("Anonymous WebSocket authentication failed");
this.options.onError(new Error("Anonymous authentication failed"));
this.disconnect();
break;
case WEBSOCKET_EVENTS.PONG:
break;
case WEBSOCKET_EVENTS.ERROR:
console.error("WebSocket error:", data.message);
this.options.onError(new Error(data.message));
break;
default:
console.warn("Unknown WebSocket message type:", data.type);
}
}
send(data) {
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
return true;
} catch (error) {
console.error("Failed to send WebSocket message:", error);
this.options.onError(error);
return false;
}
} else {
console.warn("WebSocket not connected, message queued");
return false;
}
}
sendMessage(message) {
return this.send(__spreadValues(__spreadValues({}, message), message.file && { file: message.file }));
}
sendTypingStart(conversationId) {
if (this.typingTimer) {
clearTimeout(this.typingTimer);
}
this.send({
type: WEBSOCKET_EVENTS.TYPING_START,
conversationId
});
this.typingTimer = setTimeout(() => {
this.sendTypingStop(conversationId);
}, 3e3);
}
sendTypingStop(conversationId) {
if (this.typingTimer) {
clearTimeout(this.typingTimer);
this.typingTimer = null;
}
this.send({
type: WEBSOCKET_EVENTS.TYPING_STOP,
conversationId
});
}
joinConversation(conversationId) {
return this.send({
type: WEBSOCKET_EVENTS.JOIN_CONVERSATION,
conversationId
});
}
leaveConversation(conversationId) {
return this.send({
type: WEBSOCKET_EVENTS.LEAVE_CONVERSATION,
conversationId
});
}
markRead(sessionId) {
return this.send({
type: WEBSOCKET_EVENTS.MARK_READ,
sessionId
});
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.send({ type: "ping" });
}
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
scheduleReconnect() {
if (this.reconnectCount >= this.options.reconnectAttempts) {
console.error("Max reconnection attempts reached");
this.options.onError(new Error("Max reconnection attempts reached"));
return;
}
this.reconnectCount++;
const delay = this.options.reconnectDelay * Math.pow(2, this.reconnectCount - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectCount})`);
this.reconnectTimer = setTimeout(() => {
console.log(`Reconnection attempt ${this.reconnectCount}`);
this.connect();
}, delay);
}
reconnect() {
this.disconnect();
this.reconnectCount = 0;
this.connect();
}
disconnect() {
this.isConnected = false;
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.typingTimer) {
clearTimeout(this.typingTimer);
this.typingTimer = null;
}
if (this.ws) {
this.ws.close(1e3, "Client disconnect");
this.ws = null;
}
}
// Utility methods
getConnectionState() {
if (!this.ws) {
return "DISCONNECTED";
}
switch (this.ws.readyState) {
case WebSocket.CONNECTING:
return "CONNECTING";
case WebSocket.OPEN:
return "CONNECTED";
case WebSocket.CLOSING:
return "CLOSING";
case WebSocket.CLOSED:
return "DISCONNECTED";
default:
return "UNKNOWN";
}
}
isAnonymousUser() {
return this.isAnonymous;
}
isWsConnected() {
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
}
// Event subscription for external components
on(eventType, callback) {
switch (eventType) {
case "message":
this.options.onMessage = callback;
break;
case "typing":
this.options.onTyping = callback;
break;
case "status":
this.options.onStatusChange = callback;
break;
case "error":
this.options.onError = callback;
break;
case "connect":
this.options.onConnect = callback;
break;
case "disconnect":
this.options.onDisconnect = callback;
break;
default:
console.warn("Unknown event type:", eventType);
}
}
// Cleanup
destroy() {
this.disconnect();
}
}
class NotificationWS {
constructor(wsUrl, options = {}) {
this.wsUrl = wsUrl;
this.options = __spreadValues({
onUnreadCount: () => {
},
onConnect: () => {
},
onDisconnect: () => {
},
onError: () => {
},
reconnectAttempts: 10,
reconnectDelay: 2e3,
heartbeatInterval: 3e4
}, options);
this.ws = null;
this.isConnected = false;
this.reconnectCount = 0;
this.heartbeatTimer = null;
this.reconnectTimer = null;
this.shouldReconnect = true;
this.connect();
}
connect() {
if (!this.shouldReconnect) {
return;
}
try {
this.ws = new WebSocket(this.wsUrl);
this.attachEventListeners();
} catch (error) {
console.error("Notification WebSocket connection failed:", error);
this.options.onError(error);
this.scheduleReconnect();
}
}
attachEventListeners() {
this.ws.onopen = () => {
console.log("Notification WebSocket connected");
this.isConnected = true;
this.reconnectCount = 0;
this.startHeartbeat();
this.requestUnreadCount();
this.options.onConnect();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error("Failed to parse notification message:", error);
}
};
this.ws.onclose = (event) => {
console.log("Notification WebSocket disconnected:", event.code, event.reason);
this.isConnected = false;
this.stopHeartbeat();
this.options.onDisconnect(event);
console.log(event);
const isNetworkError = event.code !== 1e3 && event.code !== 1006;
if (this.shouldReconnect && isNetworkError) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error("Notification WebSocket error:", error);
this.options.onError(error);
};
}
handleMessage(data) {
switch (data.type) {
case "unread_count":
this.options.onUnreadCount(data.count);
break;
case "pong":
break;
default:
console.log("Unknown notification message type:", data.type);
}
}
send(data) {
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
return true;
} catch (error) {
console.error("Failed to send notification message:", error);
return false;
}
}
return false;
}
requestUnreadCount() {
return this.send({ type: "get_unread_count" });
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.send({ type: "ping" });
}
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
scheduleReconnect() {
if (!this.shouldReconnect) {
return;
}
if (this.reconnectCount >= this.options.reconnectAttempts) {
console.error("Notification WebSocket: Max reconnection attempts reached");
return;
}
this.reconnectCount++;
const delay = this.options.reconnectDelay * Math.pow(1.5, this.reconnectCount - 1);
console.log(`Notification WebSocket: Reconnecting in ${delay}ms (attempt ${this.reconnectCount})`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, delay);
}
disconnect() {
this.shouldReconnect = false;
this.isConnected = false;
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close(1e3, "Client disconnect");
this.ws = null;
}
}
isWsConnected() {
return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
}
destroy() {
this.disconnect();
}
}
const TP_CHAT_JWT_COOKIE_KEY = "ticketping_chat_jwt";
class ApiService {
constructor(config) {
this.config = config;
this.baseURL = config.apiBase;
}
// Cookie utility methods
setCookie(name, value, hours) {
const date = /* @__PURE__ */ new Date();
date.setTime(date.getTime() + hours * 60 * 60 * 1e3);
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${value};${expires};path=/;SameSite=Strict`;
}
getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === " ") {
c = c.substring(1, c.length);
}
if (c.indexOf(nameEQ) === 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
}
deleteCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;SameSite=Strict`;
}
request(_0) {
return __async(this, arguments, function* (endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = __spreadValues({
"Content-Type": "application/json"
}, options.headers);
if (this.config.appId) {
headers["x-tpwidget-id"] = this.config.appId;
}
const requestOptions = __spreadValues({
method: "GET",
headers
}, options);
try {
const response = yield fetch(url, requestOptions);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return yield response.json();
} else {
return yield response.text();
}
} catch (error) {
console.error(`API request failed: ${endpoint}`, error);
if (this.isAuthError(error)) {
this.clearChatToken();
}
throw error;
}
});
}
// Authentication
getChatToken() {
return __async(this, null, function* () {
const cachedJWT = this.getCookie(TP_CHAT_JWT_COOKIE_KEY);
if (cachedJWT) {
try {
const payload = JSON.parse(atob(cachedJWT.split(".")[1]));
const currentTime = Math.floor(Date.now() / 1e3);
if (payload.exp && payload.exp > currentTime) {
return {
chatJWT: cachedJWT
};
}
} catch (error) {
console.log("error", error);
console.warn("Invalid cached JWT format, fetching new token");
this.deleteCookie(TP_CHAT_JWT_COOKIE_KEY);
}
}
const params = new URLSearchParams({
jwt: this.config.userJWT,
team: this.config.teamSlug
});
const response = yield this.request(`${API_ENDPOINTS.auth}?${params}`, {
method: "GET"
});
if (response.jwt) {
this.setCookie(TP_CHAT_JWT_COOKIE_KEY, response.jwt, 168);
}
return {
chatJWT: response.jwt
};
});
}
// Clear cached chat token (useful when token becomes invalid)
clearChatToken() {
this.deleteCookie(TP_CHAT_JWT_COOKIE_KEY);
}
createChatSession() {
return __async(this, null, function* () {
const headers = {
"Content-Type": "application/json"
};
const { chatJWT } = yield this.getChatToken();
return yield this.request(API_ENDPOINTS.newChatSession, {
method: "POST",
headers,
body: JSON.stringify({
appId: this.config.appId,
team: this.config.teamSlug,
jwt: chatJWT
})
});
});
}
// Conversations
getConversations(limit = 50, offset = 0) {
return __async(this, null, function* () {
const { chatJWT } = yield this.getChatToken();
const headers = {
"Authorization": `Bearer ${chatJWT}`,
"Content-Type": "application/json"
};
const params = new URLSearchParams({
limit: limit.toString(),
offset: offset.toString()
});
return yield this.request(`${API_ENDPOINTS.conversations}?${params}`, { headers });
});
}
// Messages
getMessages(conversationId, limit = 50, before = null) {
return __async(this, null, function* () {
const params = new URLSearchParams({
conversationId,
limit: limit.toString()
});
if (before) {
params.append("before", before);
}
return yield this.request(`${API_ENDPOINTS.messages}?${params}`);
});
}
sendMessage(message) {
return __async(this, null, function* () {
return yield this.request(API_ENDPOINTS.messages, {
method: "POST",
body: JSON.stringify(__spreadValues({
conversationId: message.conversationId,
text: message.text,
type: message.type || "user",
timestamp: message.timestamp,
messageId: message.id
}, message.file && { file: message.file }))
});
});
}
markAsRead(conversationId, messageId) {
return __async(this, null, function* () {
return yield this.request(`${API_ENDPOINTS.messages}/${messageId}/read`, {
method: "POST",
body: JSON.stringify({ conversationId })
});
});
}
// File upload
uploadFile(file, sessionId) {
return __async(this, null, function* () {
const formData = new FormData();
formData.append("file", file);
const headers = {};
if (this.config.userJWT) {
const { chatJWT } = yield this.getChatToken();
if (chatJWT) {
headers["Authorization"] = `Bearer ${chatJWT}`;
}
}
if (this.config.appId) {
headers["x-tpwidget-id"] = this.config.appId;
}
try {
const response = yield fetch(`${this.baseURL}${API_ENDPOINTS.fileUpload}${this.config.teamSlug}/${sessionId}/`, {
method: "POST",
headers,
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result = yield response.json();
return result.url;
} catch (error) {
console.error("File upload failed:", error);
throw error;
}
});
}
// Analytics
track(_0) {
return __async(this, arguments, function* (event, data = {}) {
return;
});
}
updateUser(updates) {
return __async(this, null, function* () {
return yield this.request("/users/me", {
method: "PATCH",
body: JSON.stringify(updates)
});
});
}
// Agent status
getAgentStatus() {
return __async(this, null, function* () {
return yield this.request("/agents/status");
});
}
// Help articles (if integrated)
getHelpArticles(query = "", limit = 5) {
return __async(this, null, function* () {
const params = new URLSearchParams({
q: query,
limit: limit.toString(),
appId: this.config.appId
});
return yield this.request(`/help/articles?${params}`);
});
}
searchHelpArticles(query) {
return __async(this, null, function* () {
return yield this.request("/help/search", {
method: "POST",
body: JSON.stringify({
query,
appId: this.config.appId
})
});
});
}
// Utility methods
setToken(token) {
this.token = token;
}
setAppId(appId) {
this.config.appId = appId;
}
// Error handling helpers
isNetworkError(error) {
return error instanceof TypeError && error.message.includes("fetch");
}
isAuthError(error) {
return error.message.includes("401") || error.message.includes("Unauthorized");
}
isRateLimitError(error) {
return error.message.includes("429") || error.message.includes("Too Many Requests");
}
// Retry mechanism for failed requests
requestWithRetry(_0) {
return __async(this, arguments, function* (endpoint, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return yield this.request(endpoint, options);
} catch (error) {
lastError = error;
if (this.isAuthError(error) || error.message.includes("4") && !this.isRateLimitError(error)) {
throw error;
}
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1e3;
yield new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
});
}
// Batch operations
batchRequest(requests) {
return __async(this, null, function* () {
return yield this.request("/batch", {
method: "POST",
body: JSON.stringify({ requests })
});
});
}
// Pagination helper
getPaginatedData(_0) {
return __async(this, arguments, function* (endpoint, options = {}) {
const {
limit = 50,
maxItems = 200,
onPage = () => {
}
} = options;
const results = [];
let offset = 0;
let hasMore = true;
while (hasMore && results.length < maxItems) {
const params = new URLSearchParams({
limit: limit.toString(),
offset: offset.toString()
});
const response = yield this.request(`${endpoint}?${params}`);
const items = response.data || response.items || response;
results.push(...items);
onPage(items, offset);
hasMore = items.length === limit;
offset += limit;
}
return results.slice(0, maxItems);
});
}
// Health check
healthCheck() {
return __async(this, null, function* () {
try {
yield this.request("/health");
return true;
} catch (error) {
console.warn("Health check failed:", error.message);
return false;
}
});
}
// Team widget settings
getTeamWidgetSettings() {
return __async(this, null, function* () {
return yield this.request(`${API_ENDPOINTS.teamWidgetSettings}${this.config.teamSlug}/`);
});
}
// Convenience methods for HTTP requests
get(_0) {
return __async(this, arguments, function* (endpoint, options = {}) {
return yield this.request(endpoint, __spreadValues({
method: "GET"
}, options));
});
}
post(_0) {
return __async(this, arguments, function* (endpoint, data = null, options = {}) {
const requestOptions = __spreadValues({
method: "POST"
}, options);
if (data) {
requestOptions.body = JSON.stringify(data);
}
return yield this.request(endpoint, requestOptions);
});
}
}
class StorageService {
constructor() {
this.isAvailable = this.checkStorageAvailability();
this.deviceId = this.getOrCreateDeviceId();
}
checkStorageAvailability() {
try {
const test = "__storage_test__";
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (error) {
console.warn("localStorage not available, using memory storage:", error.message);
this.memoryStorage = /* @__PURE__ */ new Map();
return false;
}
}
getOrCreateDeviceId() {
let deviceId = this.getItem(STORAGE_KEYS.DEVICE_ID);
if (!deviceId) {
deviceId = this.generateUUID();
this.setItem(STORAGE_KEYS.DEVICE_ID, deviceId);
}
return deviceId;
}
/**
* Generate a UUID v4 using crypto.randomUUID() with fallback for older browsers
*/
generateUUID() {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
return crypto.randomUUID();
}
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
return "10000000-1000-4000-8000-100000000000".replace(
/[018]/g,
(c) => (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
);
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : r & 3 | 8;
return v.toString(16);
});
}
getDeviceId() {
return this.deviceId;
}
setItem(key, value) {
try {
const serializedValue = JSON.stringify(value);
if (this.isAvailable) {
localStorage.setItem(key, serializedValue);
} else {
this.memoryStorage.set(key, serializedValue);
}
} catch (error) {
console.warn("Failed to store item:", key, error);
}
}
getItem(key) {
try {
let serializedValue;
if (this.isAvailable) {
serializedValue = localStorage.getItem(key);
} else {
serializedValue = this.memoryStorage.get(key);
}
return serializedValue ? JSON.parse(serializedValue) : null;
} catch (error) {
console.warn("Failed to retrieve item:", key, error);
return null;
}
}
removeItem(key) {
try {
if (this.isAvailable) {
localStorage.removeItem(key);
} else {
this.memoryStorage.delete(key);
}
} catch (error) {
console.warn("Failed to remove item:", key, error);
}
}
clear() {
try {
if (this.isAvailable) {
Object.values(STORAGE_KEYS).forEach((key) => {
localStorage.removeItem(key);
});
} else {
this.memoryStorage.clear();
}
} catch (error) {
console.warn("Failed to clear storage:", error);
}
}
// Conversation management
saveConversation(conversation) {
const conversations = this.getConversations();
const existingIndex = conversations.findIndex((c) => c.sessionId === conversation.sessionId);
if (existingIndex >= 0) {
conversations[existingIndex] = conversation;
} else {
conversations.push(conversation);
}
const maxConversations = 50;
if (conversations.length > maxConversations) {
conversations.sort((a, b) => new Date(b.modified || b.created) - new Date(a.modified || a.created));
conversations.splice(maxConversations);
}
this.setItem(STORAGE_KEYS.CONVERSATIONS, conversations);
}
getConversations() {
return this.getItem(STORAGE_KEYS.CONVERSATIONS) || [];
}
clearConversations() {
this.removeItem(STORAGE_KEYS.CONVERSATIONS);
}
deleteConversation(conversationId) {
const conversations = this.getConversations();
const filtered = conversations.filter((c) => c.sessionId !== conversationId);
this.setItem(STORAGE_KEYS.CONVERSATIONS, filtered);
}
// Unread conversations management
saveUnreadConversations(unreadSessionIds) {
this.setItem(STORAGE_KEYS.UNREAD_CONVERSATIONS, unreadSessionIds);
}
getUnreadConversations() {
return this.getItem(STORAGE_KEYS.UNREAD_CONVERSATIONS) || [];
}
clearUnreadConversations() {
this.removeItem(STORAGE_KEYS.UNREAD_CONVERSATIONS);
}
// User management
setUser(userData) {
this.setItem(STORAGE_KEYS.USER_DATA, __spreadProps(__spreadValues({}, userData), {
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
}));
}
getUser() {
return this.getItem(STORAGE_KEYS.USER_DATA);
}
clearUser() {
this.removeItem(STORAGE_KEYS.USER_DATA);
}
// Settings management
saveSettings(settings) {
const currentSettings = this.getSettings();
this.setItem(STORAGE_KEYS.SETTINGS, __spreadValues(__spreadValues({}, currentSettings), settings));
}
setSettings(settings) {
return this.saveSettings(settings);
}
getSettings() {
return this.getItem(STORAGE_KEYS.SETTINGS) || {};
}
getSetting(key, defaultValue = null) {
const settings = this.getSettings();
return settings[key] !== void 0 ? settings[key] : defaultValue;
}
setSetting(key, value) {
const settings = this.getSettings();
settings[key] = value;
this.setItem(STORAGE_KEYS.SETTINGS, settings);
}
// Data cleanup
cleanupOldData(maxAge = 30) {
const conversations = this.getConversations();
const cutoffDate = /* @__PURE__ */ new Date();
cutoffDate.setDate(cutoffDate.getDate() - maxAge);
const filteredConversations = conversations.filter((conversation) => {
const conversationDate = new Date(conversation.modified || conversation.created);
return conversationDate > cutoffDate;
});
if (filteredConversations.length !== conversations.length) {
this.setItem(STORAGE_KEYS.CONVERSATIONS, filteredConversations);
console.log(`Cleaned up ${conversations.length - filteredConversations.length} old conversations`);
}
}
// Export/Import functionality
exportData() {
return {
conversations: this.getConversations(),
user: this.getUser(),
settings: this.getSettings(),
deviceId: this.deviceId,
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
};
}
importData(data) {
try {
if (data.conversations) {
this.setItem(STORAGE_KEYS.CONVERSATIONS, data.conversations);
}
if (data.user) {
this.setItem(STORAGE_KEYS.USER_DATA, data.user);
}
if (data.settings) {
this.setItem(STORAGE_KEYS.SETTINGS, data.settings);
}
return true;
} catch (error) {
console.error("Failed to import data:", error);
return false;
}
}
// Storage size management
getStorageSize() {
if (!this.isAvailable) {
return 0;
}
let total = 0;
Object.values(STORAGE_KEYS).forEach((key) => {
const item = localStorage.getItem(key);
if (item) {
total += item.length;
}
});
return total;
}
getStorageSizeHuman() {
const bytes = this.getStorageSize();
if (bytes === 0) {
return "0 B";
}
const k = 1024;
const sizes = ["B", "KB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
// Migration support
migrateData(fromVersion, toVersion) {
console.log(`Migrating storage from version ${fromVersion} to ${toVersion}`);
if (fromVersion === "1.0.0" && toVersion === "1.1.0") {
const conversations = this.getConversations();
const migratedConversations = conversations.map((conv) => __spreadProps(__spreadValues({}, conv), {
version: "1.1.0"
// Add new fields or transform existing ones
}));
this.setItem(STORAGE_KEYS.CONVERSATIONS, migratedConversations);
}
}
// Privacy compliance
clearPersonalData() {
this.clearUser();
const conversations = this.getConversations();
const anonymizedConversations = conversations.map((conv) => __spreadProps(__spreadValues({}, conv), {
messages: conv.messages.map((msg) => __spreadProps(__spreadValues({}, msg), {
// Remove personally identifiable information
text: msg.type === "user" ? "[User message removed]" : msg.text
}))
}));
this.setItem(STORAGE_KEYS.CONVERSATIONS, anonymizedConversations);
}
// Utility methods
hasData() {
return this.getConversations().length > 0 || this.getUser() !== null;
}
getDataSummary() {
return {
conversationCount: this.getConversations().length,
hasUser: this.getUser() !== null,
storageSize: this.getStorageSizeHuman(),
deviceId: this.deviceId
};
}
}
function validateConfig(config) {
const errors = [];
if (!config) {
errors.push("Configuration is required");
return {
isValid: false,
errors
};
}
if (!config.appId) {
errors.push("appId is required");
} else if (typeof config.appId !== "string") {
errors.push("appId must be a string");
}
if (!config.teamSlug) {
errors.push("teamSlug is required");
} else if (typeof config.teamSlug !== "string") {
errors.push("teamSlug must be a string");
}
if (config.apiBase !== void 0 && typeof config.apiBase !== "string") {
errors.push("apiBase must be a string");
}
if (config.wsUrl !== void 0 && typeof config.wsUrl !== "string") {
errors.push("wsUrl must be a string");
}
if (config.userJWT !== void 0 && typeof config.userJWT !== "string") {
errors.push("userJWT must be a string");
}
if (config.autoStart !== void 0 && typeof config.autoStart !== "boolean") {
errors.push("autoStart must be a boolean");
}
if (config.position !== void 0) {
const validPositions = ["bottom-right", "bottom-left", "top-right", "top-left"];
if (typeof config.position !== "string" || !validPositions.includes(config.position)) {
errors.push(`position must be one of: ${validPositions.join(", ")}`);
}
}
if (config.theme !== void 0 && typeof config.theme !== "object") {
const validThemes = ["default", "dark", "light"];
if (typeof config.theme === "string" && !validThemes.includes(config.theme)) {
errors.push(`theme must be one of: ${validThemes.join(", ")}`);
}
}
if (config.maxFileSize !== void 0) {
if (typeof config.maxFileSize !== "number") {
errors.push("maxFileSize must be a number");
} else if (config.maxFileSize > 10 * 1024 * 1024) {
errors.push("maxFileSize cannot exceed 10MB");
}
}
if (config.allowedFileTypes !== void 0 && !Array.isArray(config.allowedFileTypes)) {
errors.push("allowedFileTypes must be an array");
}
return {
isValid: errors.length === 0,
errors
};
}
class TicketpingChat {
constructor(config = {}) {
this.config = __spreadValues(__spreadValues({}, DEFAULT_CONFIG), config);
this.isInitialized = false;
this.isOpen = false;
this.currentUser = null;
this.api = new ApiService(this.config);
this.storage = new StorageService();
this.ws = null;
this.notifiWs = null;
this.chatBubble = null;
this.chatWindow = null;
this.widgetContainer = null;
this.conversations = /* @__PURE__ */ new Map();
this.isChatSessionActive = false;
this.currentChatSession = null;
this.unreadCount = 0;
this.unreadConversations = /* @__PURE__ */ new Set();
this.teamSettings = null;
this.init();
}
init() {
return __async(this, null, function* () {
if (this.isInitialized) {
console.warn("TicketpingChat already initialized");
return;
}
try {
const validation = validateConfig(this.config);
if (!validation.isValid) {
throw new Error(`Invalid configuration: ${validation.errors.join(", ")}`);
}
yield this.fetchTeamSettings();
this.createWidgetContainer();
this.applyWidgetPosition();
this.chatBubble = new ChatBubble(this.widgetContainer, {
onClick: () => this.toggle(),
iconColor: this.getIconColor()
});
if (!this.shouldShowBubble()) {
this.chatBubble.hide();
this.track("widget_bubble_hidden_by_settings");
}
this.chatWindow = new ChatWindow(this.widgetContainer, {
onClose: () => this.close(),
onTabSwitch: (tab) => this.handleTabSwitch(tab),
onSendMessage: (message) => this.sendMessage(message),
onFileUpload: (file) => this.handleFileUpload(file),
onConversationSelect: (sessionId) => this.loadConversation(sessionId),
onBackButtonClick: () => this.backToList(),
teamLogoIcon: this.getTeamLogoIcon(),
teamSettings: this.teamSettings
});
yield this.loadStoredConversations();
this.initNotificationWebSocket();
this.isInitialized = true;
this.track("widget_initialized", {
appId: this.config.appId,
version: "1.0.0"
});
} catch (error) {
console.error("Failed to initialize TicketpingChat:", error);
this.track("widget_init_error", { error: error.message });
}
});
}
/**
* Fetch team widget settings from server
*/
fetchTeamSettings() {
return __async(this, null, function* () {
var _a, _b;
try {
this.teamSettings = yield this.api.getTeamWidgetSettings();
this.track("team_settings_loaded", {
teamSlug: (_a = this.teamSettings) == null ? void 0 : _a.teamSlug,
isAvailable: (_b = this.teamSettings) == null ? void 0 : _b.isAvailable
});
} catch (error) {
console.warn("Failed to fetch team settings, using defaults:", error);
this.teamSettings = null;
}
});
}
/**
* Get team logo icon from team settings or config
*/
getTeamLogoIcon() {
var _a;
return this.config.teamLogoIcon || ((_a = this.teamSettings) == null ? void 0 : _a.logoUrl) || null;
}
/**
* Check if widget bubble should be shown based on team settings
*/
shouldShowBubble() {
if (!this.teamSettings || this.teamSettings.widgetBubbleVisible === void 0) {
return true;
}
return this.teamSettings.widgetBubbleVisible;
}
/**
* Apply widget position from team settings
*/
applyWidgetPosition() {
var _a;
if (!this.widgetContainer) {
return;
}
const position = ((_a = this.teamSettings) == null ? void 0 : _a.widgetPosition) || this.config.position || "bottom-right";
const normalizedPosition = position === "left" ? "bottom-left" : "bottom-right";
this.widgetContainer.classList.remove("position-bottom-left", "position-bottom-right");
if (normalizedPosition === "bottom-left") {
this.widgetContainer.classList.add("position-bottom-left");
}
}
backToList() {
this.isChatSessionActive = false;
this.currentChatSession = null;
if (this.ws) {
this.ws.disconnect();
}
this.chatWindow.setConversations(
Array.from(this.conversations.values()),
this.unreadConversations
);
this.track("back_to_list");
}
startConversation() {
return __async(this, null, function* () {
const result = yield this.api.createChatSession();
this.currentChatSession = result.sessionId;
yield this.initWsConversation(result.sessionId);
this.isChatSessionActive = true;
});
}
initWsConversation(sessionId) {
return __async(this, null, function* () {
try {
if (this.config.userJWT) {
yield this.initAuthWsConversation(sessionId);
} else {
yield this.initAnonWsConversation(sessionId);
}
} catch (error) {
console.warn("WebSocket initialization failed:", error);
this.track("websocket_init_error", { error: error.message });
throw error;
}
});
}
initAnonWsConversation(sessionId) {
return __async(this, null, function* () {
return new Promise((resolve, reject) => {
try {
let wsUrl = `${this.config.wsBase}/ws/chat/${this.config.teamSlug}/`;
if (sessionId) {
wsUrl = `${this.config.wsBase}/ws/chat/${this.config.teamSlug}/${sessionId}/`;
}
this.ws = new WebSocketService(wsUrl, null, {
onSessionState: (data) => {
this.handleSessionState(data);
resolve();
},
onMessage: (message) => this.handleWebSocketMessage(message),
onFileAttachment: (data) => this.handleWebSocketMessage(data),
onMessageHistory: (data) => this.handleMessageHistory(data),
onTyping: (data) => this.handleTypingIndicator(data),
onStatusChange: (status) => this.handleAgentStatus(status),
onError: (error) => {
this.handleWebSocketError(error);
reject(error);
}
});
setTimeout(() => {
if (this.ws && !this.ws.isWsConnected()) {
resolve();
}
}, 5e3);
} catch (error) {
console.warn("WebSocket initialization failed:", error);
this.track("websocket_init_error", { error: error.message });
reject(error);
}
});
});
}
initAuthWsConversation(sessionId) {
return __async(this, null, function* () {
return new Promise((resolve, reject) => __async(this, null, function* () {
try {
const { chatJWT } = yield this.api.getChatToken();
let wsUrl = `${this.config.wsBase}/ws/chat/${this.config.teamSlug}/?jwt=${chatJWT}`;
if (sessionId) {
wsUrl = `${this.config.wsBase}/ws/chat/${this.config.teamSlug}/${sessionId}/?jwt=${chatJWT}`;
}
this.ws = new WebSocketService(wsUrl, chatJWT, {
onSessionState: (data) => {
this.handleSessionState(data);
resolve();
},
onMessage: (message) => this.handleWebSocketMessage(message),
onFileAttachment: (data) => this.handleWebSocketMessage(data),
onMessageHistory: (data) => this.handleMessageHistory(data),
onTyping: (data) => this.handleTypingIndicator(data),
onStatusChange: (status) => this.handleAgentStatus(status),
onError: (error) => {
this.handleWebSocketError(error);
reject(error);
}
});
setTimeout(() => {
if (this.ws && !this.ws.isWsConnected()) {
resolve();
}
}, 5e3);
} catch (error) {
console.warn("WebSocket auth initialization failed:", error);
this.track("websocket_auth_init_error", { error: error.message });
reject(error);
}
}));
});
}
/**
* Initialize persistent notification WebSocket for real-time unread count updates
*/
initNotificationWebSocket() {
if (this.notifiWs) {
this.notifiWs.disconnect();
}
const wsUrl = `${this.config.wsBase}/ws/customer-notifs/${this.config.teamSlug}/`;
this.notifiWs = new NotificationWS(wsUrl, {
onUnreadCount: (count) => this.handleUnreadCountUpdate(count),
onConnect: () => {
console.log("Notification WebSocket connected");
this.track("notification_ws_connected");
},
onDisconnect: () => {
console.log("Notification WebSocket disconnected");
},
onError: (error) => {
console.warn("Notification WebSocket error:", error);
}
});
}
/**
* Handle unread count updates from notification WebSocket
*/
handleUnreadCountUpdate(count) {
this.unreadCount = count;
if (count > 0 && !this.isOpen) {
this.chatBubble.showNotificationBadge(count);
} else {
this.chatBubble.hideNotificationBadge();
}
this.chatWindow.setMessagesTabUnread(count > 0);
this.refreshConversations();
this.track("unread_count_updated", { count });
}
/**
* Refresh conversations from server to get updated unread state
*/
refreshConversations() {
return __async(this, null, function* () {
try {
if (this.config.userJWT) {
const serverConversations = yield this.api.getConversations();
if (serverConversations && serverConversations.results) {
this.unreadConversations.clear();
serverConversations.results.forEach((conv) => {
const existing = this.conversations.get(conv.sessionId);
const messages = (existing == null ? void 0 : existing.messages) || conv.messages || [];
this.conversations.set(conv.sessionId, __spreadProps(__spreadValues({}, conv), { messages }));
if (conv.hasUnread) {
this.unreadConversations.add(conv.sessionId);
}
});
this.chatWindow.setConversations(
Array.from(this.conversations.values()),
this.unreadConversations
);
}
}
} catch (error) {
console.warn("Failed to refresh conversations:", error);
}
});
}
createWidgetContainer() {
const existing = document.querySelector(".ticketping-widget");
if (existing) {
existing.remove();
}
this.widgetContainer = createDOMElement("div", {
className: "ticketping-widget",
"data-version": "1.0.0"
});
this.applyCustomTheme();
document.body.appendChild(this.widgetContainer);
}
applyCustomTheme() {
if (!this.config.theme) {
return;
}
const theme = this.config.theme;
const root = document.documentElement;
const themeMap = {
primaryColor: "--tp-primary-color",
primaryButtonText: "--tp-primary-button-text",
primaryHover: "--tp-primary-hover",
textPrimary: "--tp-text-primary",
textSecondary: "--tp-text-secondary",
textMuted: "--tp-text-muted",
background: "--tp-background",
backgroundSecondary: "--tp-background-secondary",
backgroundTertiary: "--tp-background-tertiary",
border: "--tp-border",
borderLight: "--tp-border-light",
notificationBg: "--tp-notification-bg",
successColor: "--tp-success-color",
offlineColor: "--tp-offline-color",
errorBg: "--tp-error-bg",
errorText: "--tp-error-text",
errorBorder: "--tp-error-border",
shadowLight: "--tp-shadow-light",
shadowMedium: "--tp-shadow-medium",
shadowDark: "--tp-shadow-dark",
iconColor: "--tp-icon-color"
};
Object.entries(themeMap).forEach(([themeKey, cssVar]) => {
if (theme[themeKey]) {
root.style.setProperty(cssVar, theme[themeKey]);
}
});
}
getIconColor() {
if (this.config.theme && typeof this.config.theme === "object") {
return this.config.theme.iconColor;
}
return null;
}
// Public API methods
open() {
if (!this.isInitialized) {
return;
}
this.isOpen = true;
this.chatBubble.setOpen(true);
this.chatWindow.show();
this.clearUnreadNotification();
this.track("widget_opened");
}
clearUnreadNotification() {
this.unreadCount = 0;
this.chatBubble.hideNotificationBadge();
}
markConversationAsRead(sessionId) {
if (this.unreadConversations.has(sessionId)) {
this.unreadConversations.delete(sessionId);
this.chatWindow.setConversations(
Array.from(this.conversations.values()),
this.unreadConversations
);
if (this.ws && this.ws.isWsConnected()) {
this.ws.markRead(sessionId);
}
}
}
close() {
if (!this.isInitialized) {
return;
}
if (this.ws) {
this.ws.disconnect();
}
this.isOpen = false;
this.chatBubble.setOpen(false);
this.chatWindow.hide();
this.track("widget_closed");
}
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
identify(userData) {
return __async(this, null, function* () {
this.currentUser = userData;
this.storage.setUser(userData);
if (userData.userJWT && userData.userJWT !== "your-actual-jwt-token-here") {
this.config.userJWT = userData.userJWT;
try {
yield this.loadAuthenticatedUserData();
yield this.reinitializeWebSocketIfNeeded();
this.track("user_identified", {
userId: userData.id || userData.email || "unknown",
hasJWT: !!userData.userJWT
});
} catch (error) {
console.warn("Failed to initialize authenticated user features:", error);
this.track("user_identify_error", { error: error.message });
}
}
});
}
sendMessage(messageData) {
return __async(this, null, function* () {
console.log("sendMessage", messageData);
if (!this.currentChatSession) {
throw new Error("No conversation started!");
}
const message = __spreadValues({
sessionId: this.currentChatSession,
type: "user_message",
sender: "USER",
messageText: messageData.text,
created: (/* @__PURE__ */ new Date()).toISOString()
}, messageData);
console.log("message", message);
this.addMessageToConversation(this.currentChatSession, message);
this.chatWindow.addMessage(message);
if (this.ws && this.ws.isWsConnected()) {
this.ws.sendMessage(message);
} else {
yield this.api.sendMessage(message);
}
this.track("message_sent", {
chatSessionId: this.currentChatSession,
messageType: message.type,
hasAttachment: !!message.file
});
});
}
handleFileUpload(file) {
return __async(this, null, function* () {
try {
if (file.size > this.config.maxFileSize) {
throw new Error(`File size exceeds ${this.config.maxFileSize / 1024 / 1024}MB limit`);
}
yield this.api.uploadFile(file, this.currentChatSession);
} catch (error) {
console.error("File upload failed:", error);
this.chatWindow.showError("Failed to upload file: " + error.message);
this.track("file_upload_error", { error: error.message });
}
});
}
handleSessionState(data) {
console.log("handleSessionState", data);
this.currentChatSession = data.sessionId;
this.conversations.set(data.sessionId, {
sessionId: data.sessionId,
messages: [],
created: data.created
});
this.chatWindow.showConversationItem(this.currentChatSession);
}
handleWebSocketMessage(data) {
this.addMessageToConversation(data.sessionId, data);
if (data.sessionId === this.currentChatSession) {
this.chatWindow.addMessage(data);
}
if (data.sender !== "USER") {
const isViewingThisConversation = this.isOpen && data.sessionId === this.currentChatSession;
if (!isViewingThisConversation) {
this.unreadConversations.add(data.sessionId);
this.chatWindow.setConversations(
Array.from(this.conversations.values()),
this.unreadConversations
);
}
}
}
handleMessageHistory(data) {
console.log("handleMessageHistory", data);
console.log("this.conversations", this.conversations);
this.conversations.set(data.sessionId, {
sessionId: data.sessionId,
messages: data.messages,
created: data.created
});
this.chatWindow.setMessages(data.messages);
}
handleTabSwitch(tab) {
this.track("tab_switched", { tab });
}
loadConversation(chatSessionId) {
return __async(this, null, function* () {
try {
if (!chatSessionId || chatSessionId === "new") {
this.chatWindow.switchTab("messages");
this.chatWindow.showLoadingState();
yield this.startConversation();
this.chatWindow.showConversationItem();
return;
}
this.chatWindow.switchTab("messages");
this.chatWindow.showLoadingState();
yield this.initWsConversation(chatSessionId);
this.isChatSessionActive = true;
this.markConversationAsRead(chatSessionId);
this.chatWindow.showConversationItem();
this.track("conversation_loaded", { chatSessionId });
} catch (error) {
console.error("Failed to load conversation:", error);
this.chatWindow.hideLoadingState();
this.chatWindow.showError("Failed to load conversation");
}
});
}
loadStoredConversations() {
return __async(this, null, function* () {
try {
const stored = this.storage.getConversations();
stored.forEach((conv) => this.conversations.set(conv.sessionId, conv));
if (this.config.userJWT) {
const serverConversations = yield this.api.getConversations();
serverConversations["results"].forEach((conv) => {
this.conversations.set(conv.sessionId, conv);
this.storage.saveConversation(conv);
if (conv.hasUnread) {
this.unreadConversations.add(conv.sessionId);
}
});
}
this.chatWindow.setConversations(
Array.from(this.conversations.values()),
this.unreadConversations
);
} catch (error) {
console.warn("Failed to load conversations:", error);
}
});
}
/**
* Load authenticated user data including conversations and user preferences
*/
loadAuthenticatedUserData() {
return __async(this, null, function* () {
if (!this.config.userJWT) {
return;
}
try {
const serverConversations = yield this.api.getConversations();
if (serverConversations && serverConversations.results) {
this.conversations.clear();
this.unreadConversations.clear();
serverConversations.results.forEach((conv) => {
this.conversations.set(conv.sessionId, conv);
this.storage.saveConversation(conv);
if (conv.hasUnread) {
this.unreadConversations.add(conv.sessionId);
}
});
if (this.chatWindow) {
this.chatWindow.setConversations(
Array.from(this.conversations.values()),
this.unreadConversations
);
}
console.log(`Loaded ${serverConversations.results.length} conversations for authenticated user`);
}
} catch (error) {
console.warn("Failed to load authenticated user data:", error);
yield this.loadStoredConversations();
throw error;
}
});
}
/**
* Reinitialize WebSocket connection with authentication if needed
*/
reinitializeWebSocketIfNeeded() {
return __async(this, null, function* () {
if (!this.isChatSessionActive || !this.currentChatSession) {
return;
}
try {
console.log("Reinitializing WebSocket with authentication...");
if (this.ws) {
this.ws.disconnect();
this.ws = null;
}
yield this.initWsConversation(this.currentChatSession);
console.log("WebSocket reinitialized with authentication successfully");
this.track("websocket_reinitialized", {
sessionId: this.currentChatSession,
authenticated: true
});
} catch (error) {
console.error("Failed to reinitialize WebSocket:", error);
this.track("websocket_reinit_error", {
error: error.message,
sessionId: this.currentChatSession
});
}
});
}
// Utility methods
addMessageToConversation(chatSessionId, message) {
const conversation = this.conversations.get(chatSessionId);
console.log("addMessageToConversation", conversation, chatSessionId, message);
if (conversation) {
conversation.messages.push(message);
conversation.modified = /* @__PURE__ */ new Date();
this.storage.saveConversation(conversation);
}
}
track(event, data = {}) {
if (this.config.analytics && true) {
this.api.track(event, __spreadProps(__spreadValues({}, data), {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
appId: this.config.appId,
version: "1.0.0"
}));
}
}
handleWebSocketError(error) {
console.warn("WebSocket error:", error);
this.track("websocket_error", { error: error.message });
}
handleAgentStatus(status) {
this.chatWindow.updateAgentStatus(status);
}
handleAgentJoined(data) {
this.chatWindow.showAgentJoined(data.agent);
}
handleTypingIndicator(data) {
if (data.sessionId === this.currentChatSession) {
this.chatWindow.showTypingIndicator(data.typing);
}
}
// Cleanup
destroy() {
if (this.ws) {
this.ws.disconnect();
}
if (this.notifiWs) {
this.notifiWs.disconnect();
}
if (this.widgetContainer) {
this.widgetContainer.remove();
}
this.isInitialized = false;
this.isChatSessionActive = false;
this.currentChatSession = null;
this.currentUser = null;
this.track("widget_destroyed");
}
}
window.TicketpingChat = {
instance: null,
init(config = {}) {
if (this.instance) {
console.warn("TicketpingChat already initialized");
return this.instance;
}
this.instance = new TicketpingChat(config);
return this.instance;
},
identify(userData) {
if (this.instance) {
return this.instance.identify(userData);
}
},
open() {
if (this.instance) {
this.instance.open();
}
},
close() {
if (this.instance) {
this.instance.close();
}
},
startConversation() {
if (this.instance) {
return this.instance.startConversation();
}
},
destroy() {
if (this.instance) {
this.instance.destroy();
this.instance = null;
}
},
// Version info
version: "1.0.0"
};
if (window.ticketpingConfig) {
window.TicketpingChat.init(window.ticketpingConfig);
}
export {
TicketpingChat as default
};
//# sourceMappingURL=widget.esm.js.map