UNPKG

@ticketping/chat-widget

Version:

Customer support chat widget for Ticketping - Intercom-like experience with real-time messaging

1,194 lines (1,187 loc) 93.3 kB
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" }; 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" }; 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 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(); } }); } handleClick() { this.options.onClick(); 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()); } } } 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); } 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.currentMessages = []; 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"> <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">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 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" 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 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-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 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) { this.conversations = conversations; 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 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 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) { 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" }); } 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>'; } 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!"; } } 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 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"; recentConversationItem.querySelector(".ticketping-recent-conversation-preview").textContent = preview; recentConversationItem.querySelector(".ticketping-recent-conversation-time").textContent = this.formatDateTime(recentConversation.modified || recentConversation.created); recentConversationItem.dataset.conversationId = recentConversation.sessionId; recentConversationSection.style.display = "block"; } 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 authentica