@ticketping/chat-widget
Version:
Customer support chat widget for Ticketping - Intercom-like experience with real-time messaging
2,417 lines • 86.2 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'
primaryColor: "#667eea",
borderRadius: "12px",
zIndex: 999999,
// Widget Behavior
autoOpen: false,
showPulseAnimation: true,
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"
};
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",
// 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"
};
const CSS_CLASSES = {
BUBBLE: "ticketping-chat-bubble",
WINDOW: "ticketping-chat-window",
OPEN: "open",
PULSE: "pulse"
};
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: () => {
},
onAnimationComplete: () => {
},
showPulseAnimation: true,
showNotificationBadge: false,
notificationCount: 0
}, options);
return;
}
this.container = container;
this.options = __spreadValues({
onClick: () => {
},
onAnimationComplete: () => {
},
showPulseAnimation: true,
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} ${this.options.showPulseAnimation ? CSS_CLASSES.PULSE : ""}`,
"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 256 256");
svg.setAttribute("width", "32");
svg.setAttribute("height", "32");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M146.476 232.21L152.258 222.442L138.489 214.291L132.706 224.061L146.476 232.21ZM103.742 222.442L109.524 232.21L123.293 224.061L117.511 214.291L103.742 222.442ZM132.706 224.061C130.651 227.534 125.349 227.534 123.293 224.061L109.524 232.21C117.776 246.151 138.224 246.151 146.476 232.21L132.706 224.061ZM112 29.3333H144V13.3333H112V29.3333ZM226.667 112V122.667H242.667V112H226.667ZM29.3335 122.667V112H13.3335V122.667H29.3335ZM13.3335 122.667C13.3335 134.982 13.3291 144.62 13.8595 152.393C14.3948 160.238 15.5013 166.767 18.0022 172.804L32.7843 166.682C31.2254 162.918 30.3021 158.334 29.8225 151.303C29.3379 144.202 29.3335 135.201 29.3335 122.667H13.3335ZM83.2268 194.577C69.8355 194.346 62.8186 193.494 57.3185 191.216L51.1956 205.998C59.7819 209.554 69.5615 210.343 82.9512 210.574L83.2268 194.577ZM18.0022 172.804C24.2272 187.832 36.1672 199.773 51.1956 205.998L57.3185 191.216C46.2105 186.614 37.3853 177.79 32.7843 166.682L18.0022 172.804ZM226.667 122.667C226.667 135.201 226.663 144.202 226.178 151.303C225.698 158.334 224.775 162.918 223.216 166.682L237.998 172.804C240.499 166.767 241.605 160.238 242.141 152.393C242.671 144.62 242.667 134.982 242.667 122.667H226.667ZM173.049 210.574C186.439 210.343 196.219 209.554 204.804 205.998L198.682 191.216C193.182 193.494 186.164 194.346 172.773 194.577L173.049 210.574ZM223.216 166.682C218.615 177.79 209.79 186.614 198.682 191.216L204.804 205.998C219.833 199.773 231.773 187.832 237.998 172.804L223.216 166.682ZM144 29.3333C161.613 29.3333 174.261 29.3417 184.127 30.2798C193.874 31.2065 200.076 32.9838 205.02 36.0136L213.38 22.3714C205.499 17.5421 196.559 15.3897 185.642 14.3517C174.843 13.3249 161.304 13.3333 144 13.3333V29.3333ZM242.667 112C242.667 94.6966 242.675 81.1573 241.648 70.3585C240.61 59.4406 238.458 50.5008 233.629 42.62L219.986 50.98C223.017 55.9244 224.794 62.1263 225.721 71.8729C226.658 81.7386 226.667 94.3867 226.667 112H242.667ZM205.02 36.0136C211.12 39.7517 216.249 44.8802 219.986 50.98L233.629 42.62C228.572 34.3673 221.633 27.4287 213.38 22.3714L205.02 36.0136ZM112 13.3333C94.6968 13.3333 81.1575 13.3249 70.3587 14.3517C59.4408 15.3897 50.501 17.5421 42.6202 22.3714L50.9802 36.0136C55.9245 32.9838 62.1265 31.2065 71.8731 30.2798C81.7388 29.3417 94.3869 29.3333 112 29.3333V13.3333ZM29.3335 112C29.3335 94.3867 29.3419 81.7386 30.28 71.8729C31.2067 62.1263 32.984 55.9244 36.0138 50.98L22.3716 42.62C17.5422 50.5008 15.3899 59.4406 14.3518 70.3585C13.3251 81.1573 13.3335 94.6966 13.3335 112H29.3335ZM42.6202 22.3714C34.3675 27.4287 27.4289 34.3673 22.3716 42.62L36.0138 50.98C39.7518 44.8802 44.8804 39.7517 50.9802 36.0136L42.6202 22.3714ZM117.511 214.291C115.345 210.632 113.444 207.404 111.596 204.867C109.648 202.196 107.416 199.791 104.319 197.989L96.2745 211.821C96.7802 212.114 97.4692 212.651 98.6652 214.291C99.9598 216.068 101.423 218.523 103.742 222.442L117.511 214.291ZM82.9512 210.574C87.6348 210.655 90.6014 210.715 92.8629 210.964C94.9761 211.199 95.7951 211.541 96.2745 211.821L104.319 197.989C101.196 196.173 97.9464 195.429 94.6238 195.061C91.4496 194.71 87.6135 194.653 83.2268 194.577L82.9512 210.574ZM152.258 222.442C154.577 218.523 156.04 216.068 157.335 214.291C158.53 212.651 159.219 212.114 159.725 211.821L151.681 197.989C148.585 199.791 146.351 202.196 144.404 204.867C142.556 207.404 140.655 210.632 138.489 214.291L152.258 222.442ZM172.773 194.577C168.386 194.653 164.551 194.71 161.376 195.061C158.053 195.429 154.804 196.173 151.681 197.989L159.725 211.821C160.205 211.541 161.024 211.199 163.137 210.964C165.399 210.715 168.365 210.655 173.049 210.574L172.773 194.577Z");
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",
textContent: this.options.notificationCount > 99 ? "99+" : this.options.notificationCount.toString()
});
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();
}
});
if (this.options.showPulseAnimation) {
setTimeout(() => {
this.removePulse();
this.options.onAnimationComplete();
}, 1e4);
}
}
handleClick() {
this.options.onClick();
this.removePulse();
if (this.notificationBadge) {
this.hideNotificationBadge();
}
}
setOpen(isOpen) {
this.isOpen = isOpen;
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());
}
}
}
removePulse() {
this.element.classList.remove(CSS_CLASSES.PULSE);
}
showNotificationBadge(count = 1) {
if (!this.notificationBadge) {
this.createNotificationBadge();
}
this.options.notificationCount = count;
this.notificationBadge.textContent = count > 99 ? "99+" : count.toString();
if (count > 0) {
this.notificationBadge.style.display = "flex";
this.notificationBadge.style.animation = "tp-pulse 1s ease-in-out 3";
} 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) {
this.element.setAttribute("data-theme", theme);
}
setPosition(position) {
this.element.setAttribute("data-position", position);
}
// Animation methods
pulse() {
this.element.classList.add(CSS_CLASSES.PULSE);
}
stopPulse() {
this.element.classList.remove(CSS_CLASSES.PULSE);
}
bounce() {
this.element.style.animation = "tp-bounce 0.6s ease-in-out";
setTimeout(() => {
this.element.style.animation = "";
}, 600);
}
// Accessibility
setAriaLabel(label) {
this.element.setAttribute("aria-label", label);
}
focus() {
this.element.focus();
}
blur() {
this.element.blur();
}
// State management
disable() {
this.element.disabled = true;
this.element.setAttribute("aria-disabled", "true");
}
enable() {
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
}, options);
this.element = null;
this.activeTab = "home";
this.conversations = [];
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>Hi there 👋</h4>
<p>How can we help you?</p>
</div>
<div class="ticketping-actions-container">
<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">Typically respond within minutes</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256"><path d="M231.4,44.34s0,.1,0,.15l-58.2,191.94a15.88,15.88,0,0,1-14,11.51q-.69.06-1.38.06a15.86,15.86,0,0,1-14.42-9.15L107,164.15a4,4,0,0,1,.77-4.58l57.92-57.92a8,8,0,0,0-11.31-11.31L96.43,148.26a4,4,0,0,1-4.58.77L17.08,112.64a16,16,0,0,1,2.49-29.8l191.94-58.2.15,0A16,16,0,0,1,231.4,44.34Z"></path></svg>
</button>
</div>
</div>
<div class="ticketping-plug">
<a href="https://ticketping.com" target="_blank" rel="noopener noreferrer">
Powered by Ticketping
</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>
</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" 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" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M231.4,44.34s0,.1,0,.15l-58.2,191.94a15.88,15.88,0,0,1-14,11.51q-.69.06-1.38.06a15.86,15.86,0,0,1-14.42-9.15L107,164.15a4,4,0,0,1,.77-4.58l57.92-57.92a8,8,0,0,0-11.31-11.31L96.43,148.26a4,4,0,0,1-4.58.77L17.08,112.64a16,16,0,0,1,2.49-29.8l191.94-58.2.15,0A16,16,0,0,1,231.4,44.34Z"></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" 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-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" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M209.66,122.34a8,8,0,0,1,0,11.32l-82.05,82a56,56,0,0,1-79.2-79.21L147.67,35.73a40,40,0,1,1,56.61,56.55L105,193A24,24,0,1,1,71,159L154.3,74.38A8,8,0,1,1,165.7,85.6L82.39,170.31a8,8,0,1,0,11.27,11.36L192.93,81A24,24,0,1,0,159,47L59.76,147.68a40,40,0,1,0,56.53,56.62l82.06-82A8,8,0,0,1,209.66,122.34Z"></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>
</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" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M219.31,108.68l-80-80a16,16,0,0,0-22.62,0l-80,80A15.87,15.87,0,0,0,32,120v96a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V160h32v56a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V120A15.87,15.87,0,0,0,219.31,108.68ZM208,208H160V152a8,8,0,0,0-8-8H104a8,8,0,0,0-8,8v56H48V120l80-80,80,80Z"></path></svg>
<svg class="icon-active" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,120v96a8,8,0,0,1-8,8H160a8,8,0,0,1-8-8V164a4,4,0,0,0-4-4H108a4,4,0,0,0-4,4v52a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V120a16,16,0,0,1,4.69-11.31l80-80a16,16,0,0,1,22.62,0l80,80A16,16,0,0,1,224,120Z"></path></svg>
</div>
<span>Home</span>
</button>
<button class="ticketping-tab" data-tab="messages">
<div class="tab-icon">
<svg class="icon-inactive" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.85,15.85,0,0,0,9.24,14.5A16.13,16.13,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H80a8,8,0,0,0-5.23,1.95L40,224V64H216ZM88,112a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,112Zm0,32a8,8,0,0,1,8-8h64a8,8,0,1,1,0,16H96A8,8,0,0,1,88,144Z"></path></svg>
<svg class="icon-active" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.84,15.84,0,0,0,9.25,14.5A16.05,16.05,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM160,152H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Zm0-32H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Z"></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 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) {
this.conversations = conversations;
this.renderConversationList();
}
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 item = createDOMElement("div", {
className: "ticketping-conversation-item",
"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 = `
<div class="ticketping-conversation-preview">${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 messageElement = this.createMessageElement(message);
messagesList.appendChild(messageElement);
this.scrollToBottom();
}
setMessages(messages) {
const messagesList = this.element.querySelector("#messagesList");
messagesList.innerHTML = "";
messages.forEach((message) => {
const messageElement = this.createMessageElement(message);
messagesList.appendChild(messageElement);
});
this.scrollToBottom();
}
clearMessages() {
const messagesList = this.element.querySelector("#messagesList");
messagesList.innerHTML = "";
}
createMessageElement(message) {
const element = createDOMElement("div", {
className: `ticketping-message ${message.sender.toLowerCase()}`
});
const hasAttachment = message.filename && message.filepath;
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>
<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>
<div class="ticketping-message-time">${this.formatTime(message.created)}</div>
`;
}
return element;
}
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) {
const file = fileInput.files[0];
if (file) {
this.options.onFileUpload(file);
fileInput.value = "";
}
}
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" });
}
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>';
}
createAttachmentHtml(filename, filepath) {
const escapedFilename = this.escapeHtml(filename);
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: inherit; text-decoration: none;">
${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!";
}
}
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
});
}
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();
}
}
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"
};
return yield this.request(API_ENDPOINTS.newChatSession, {
method: "POST",
headers,
body: JSON.stringify({
appId: this.config.appId,
team: this.config.teamSlug,
jwt: this.config.userJWT
})
});
});
}
// 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 });
});
}
getConversation(conversationId) {
return __async(this, null, function* () {
return yield this.request(`${API_ENDPOINTS.conversations}/${conversationId}`);
});
}
createConversation() {
return __async(this, arguments, function* (data = {}) {
return yield this.request(API_ENDPOINTS.conversations, {
method: "POST",
body: JSON.stringify(__spreadValues({
appId: this.config.appId
}, data))
});
});
}
updateConversation(conversationId, updates) {
return __async(this, null, function* () {
return yield this.request(`${API_ENDPOINTS.conversations}/${conversationId}`, {
method: "PATCH",
body: JSON.stringify(updates)
});
});
}
// 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;
}
});
}
// 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 = `device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.setItem(STORAGE_KEYS.DEVICE_ID, deviceId);
}
return deviceId;
}
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);
}
getConversation(conversationId) {
const conversations = this.getConversations();
return conversations.find((c) => c.sessionId === conversationId);
}
deleteConversation(conversationId) {
const conversations = this.getConversations();
const filtered = conversations.filter((c) => c.sessionId !== conversationId);
this.setItem(STORAGE_KEYS.CONVERSATIONS, filtered);
}
// 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.showPulseAnimation !== void 0 && typeof config.showPulseAnimation !== "boolean") {
errors.push("showPulseAnimation must be a boolean");
}
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.chatBubble = null;
this.chatWindow = null;
this.widgetContainer = null;
this.conversations = /* @__PURE__ */ new Map();
this.isChatSessionActive = false;
this.currentChatSession = 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(", ")}`);
}
this.createWidgetContainer();
this.chatBubble = new ChatBubble(this.widgetContainer, {
showPulseAnimation: this.config.showPulseAnimation,
onClick: () => this.toggle(),
onAnimationComplete: () => this.removePulse()
});
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.config.teamLogoIcon
});
yield this.loadStoredConversations();
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 });
}
});
}
backToList() {
this.isChatSessionActive = false;
this.currentChatSession = null;
if (this.ws) {
this.ws.disconnect();
}
this.chatWindow.setConversations(Array.from(this.conversations.values()));
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);
}
}));
});
}
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",
primaryButtonBg: "--tp-primary-button-bg",
primaryButtonText: "--tp-primary-button-text",
primaryHover: "--tp-primary-hover",
textPrimary: "--tp-text-primary",
textSecondary: "--tp-text-secondary",
textMuted: "--tp-text-muted",
textWhite: "--tp-text-white",
background: "--tp-background",
backgroundSecondary: "--tp-background-secondary",
backgroundTertiary: "--tp-background-tertiary",
border: "--tp-border",
borderLight: "--tp-border-light",
borderCard: "--tp-border-card",
notificationBg: "--tp-notification-bg",
successColor: "--tp-success-color",
offlineColor: "--tp-offline-color",
errorBg: "--tp-error-bg",
errorText: "--tp-error-text",
errorBorder: "--tp-error-border",
pulseColor: "--tp-pulse-color",
shadowLight: "--tp-shadow-light",
shadowMedium: "--tp-shadow-medium",
shadowDark: "--tp-shadow-dark",
overlayLight: "--tp-overlay-light"
};
Object.entries(themeMap).forEach(([themeKey, cssVar]) => {
if (theme[themeKey]) {
root.style.setProperty(cssVar, theme[themeKey]);
if (themeKey === "pulseColor") {
const color = theme[themeKey];
if (color.startsWith("#")) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
root.style.setProperty("--tp-pulse-color-70", `rgba(${r}, ${g}, ${b}, 0.7)`);
root.style.setProperty("--tp-pulse-color-0", `rgba(${r}, ${g}, ${b}, 0)`);
}
}
}
});
}
// Public API methods
open() {
if (!this.isInitialized) {
return;
}
this.isOpen = true;
this.chatBubble.setOpen(true);
this.chatWindow.show();
this.track("widget_opened");
}
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);
}
}
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.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);
});
}
this.chatWindow.setConversations(Array.from(this.conversations.values()));
} 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();
serverConversations.results.forEach((conv) => {
this.conversations.set(conv.sessionId, conv);
this.storage.saveConversation(conv);
});
if (this.chatWindow) {
this.chatWindow.setConversations(Array.from(this.conversations.values()));
}
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);
}
}
updateConversation(chatSessionId, updates) {
const conversation = this.conversations.get(chatSessionId);
if (conversation) {
Object.assign(conversation, updates);
this.storage.saveConversation(conversation);
}
}
removePulse() {
setTimeout(() => {
this.chatBubble.removePulse();
}, 1e4);
}
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.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