@scalemule/chat 0.0.5 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,20 @@
1
- "use strict";var ScaleMuleSupportWidget=(()=>{var p=class{constructor(){this.listeners=new Map}on(s,e){return this.listeners.has(s)||this.listeners.set(s,new Set),this.listeners.get(s).add(e),()=>this.off(s,e)}off(s,e){this.listeners.get(s)?.delete(e)}once(s,e){let t=(n=>{this.off(s,t),e(n)});return this.on(s,t)}emit(s,...[e]){let t=this.listeners.get(s);if(t)for(let n of t)try{n(e)}catch(i){console.error(`[ScaleMuleChat] Error in ${String(s)} listener:`,i)}}removeAllListeners(s){s?this.listeners.delete(s):this.listeners.clear()}};var b="https://api.scalemule.com";var d=class{constructor(s,e){this.cache=new Map;this.maxMessages=s??200,this.maxConversations=e??50}getMessages(s){return this.cache.get(s)??[]}setMessages(s,e){this.cache.set(s,e.slice(0,this.maxMessages)),this.evictOldConversations()}addMessage(s,e){let t=this.cache.get(s)??[];t.some(n=>n.id===e.id)||(t.push(e),t.sort((n,i)=>n.created_at.localeCompare(i.created_at)),t.length>this.maxMessages&&t.splice(0,t.length-this.maxMessages),this.cache.set(s,t))}updateMessage(s,e){let t=this.cache.get(s);if(!t)return;let n=t.findIndex(i=>i.id===e.id);n>=0&&(t[n]=e)}removeMessage(s,e){let t=this.cache.get(s);if(!t)return;let n=t.findIndex(i=>i.id===e);n>=0&&t.splice(n,1)}clear(s){s?this.cache.delete(s):this.cache.clear()}evictOldConversations(){for(;this.cache.size>this.maxConversations;){let s=this.cache.keys().next().value;s&&this.cache.delete(s)}}};var y="scalemule_chat_offline_queue",h=class{constructor(s=!0){this.queue=[];this.enabled=s,this.enabled&&this.load()}enqueue(s,e,t="text",n){this.enabled&&(this.queue.push({conversationId:s,content:e,message_type:t,attachments:n,timestamp:Date.now()}),this.save())}drain(){let s=[...this.queue];return this.queue=[],this.save(),s}get size(){return this.queue.length}save(){try{typeof localStorage<"u"&&localStorage.setItem(y,JSON.stringify(this.queue))}catch{}}load(){try{if(typeof localStorage<"u"){let s=localStorage.getItem(y);s&&(this.queue=JSON.parse(s))}}catch{this.queue=[]}}};var u=class{constructor(s){this.baseUrl=s.baseUrl.replace(/\/$/,""),this.apiKey=s.apiKey,this.getToken=s.getToken,this.timeout=s.timeout??1e4}async get(s){return this.request("GET",s)}async post(s,e){return this.request("POST",s,e)}async patch(s,e){return this.request("PATCH",s,e)}async del(s){return this.request("DELETE",s)}async request(s,e,t){let n={"Content-Type":"application/json"};if(this.apiKey&&(n["x-api-key"]=this.apiKey),this.getToken){let o=await this.getToken();o&&(n.Authorization=`Bearer ${o}`)}let i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let o=await fetch(`${this.baseUrl}${e}`,{method:s,headers:n,body:t?JSON.stringify(t):void 0,signal:i.signal,credentials:"include"});if(clearTimeout(a),o.status===204)return{data:null,error:null};let r=await o.json().catch(()=>null);return o.ok?{data:r?.data!==void 0?r.data:r,error:null}:{data:null,error:{code:r?.error?.code??r?.code??"unknown",message:r?.error?.message??r?.message??o.statusText,status:o.status,details:r?.error?.details??r?.details}}}catch(o){return clearTimeout(a),{data:null,error:{code:"network_error",message:o instanceof Error?o.message:"Network error",status:0}}}}};var m=class extends p{constructor(e){super();this.ws=null;this.status="disconnected";this.subscriptions=new Set;this.presenceChannels=new Map;this.reconnectAttempt=0;this.reconnectTimer=null;this.heartbeatTimer=null;this.config=e,this.maxRetries=e.reconnect?.maxRetries??1/0,this.baseDelay=e.reconnect?.baseDelay??1e3,this.maxDelay=e.reconnect?.maxDelay??3e4;let t=e.baseUrl.replace(/\/$/,"");this.ticketUrl=`${t}/v1/realtime/ws/ticket`;let n=e.wsUrl?e.wsUrl.replace(/\/$/,""):t;this.wsBaseUrl=n.replace(/^http/,"ws")+"/v1/realtime/ws"}getStatus(){return this.status}async connect(){if(!(this.status==="connected"||this.status==="connecting")){this.setStatus("connecting");try{let e=await this.obtainTicket();if(!e){this.setStatus("disconnected"),this.emit("error",{message:"Failed to obtain WS ticket"});return}let t=`${this.wsBaseUrl}?ticket=${encodeURIComponent(e)}`;this.ws=new WebSocket(t),this.ws.onopen=()=>{this.setStatus("connected"),this.reconnectAttempt=0,this.startHeartbeat(),this.resubscribeAll()},this.ws.onmessage=n=>{this.handleMessage(n.data)},this.ws.onclose=()=>{this.stopHeartbeat(),this.status!=="disconnected"&&this.scheduleReconnect()},this.ws.onerror=()=>{}}catch{this.setStatus("disconnected"),this.scheduleReconnect()}}}disconnect(){this.setStatus("disconnected"),this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null)}subscribe(e){return this.subscriptions.add(e),this.status==="connected"?this.send({type:"subscribe",channel:e}):this.status==="disconnected"&&this.connect(),()=>this.unsubscribe(e)}unsubscribe(e){this.subscriptions.delete(e),this.status==="connected"&&this.send({type:"unsubscribe",channel:e})}publish(e,t){this.status==="connected"&&this.send({type:"publish",channel:e,data:t})}joinPresence(e,t){this.presenceChannels.set(e,t),this.status==="connected"&&this.send({type:"presence_join",channel:e,user_data:t??{}})}leavePresence(e){this.presenceChannels.delete(e),this.status==="connected"&&this.send({type:"presence_leave",channel:e})}async obtainTicket(){let e={"Content-Type":"application/json"};if(this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.getToken){let t=await this.config.getToken();t&&(e.Authorization=`Bearer ${t}`)}try{let t=await fetch(this.ticketUrl,{method:"POST",headers:e,credentials:"include"});if(!t.ok)return null;let n=await t.json();return n.ticket??n.data?.ticket??null}catch{return null}}send(e){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}handleMessage(e){if(e!=="pong")try{let t=JSON.parse(e);switch(t.type){case"auth_success":break;case"subscribed":break;case"message":this.emit("message",{channel:t.channel,data:t.data});break;case"presence_state":this.emit("presence:state",{channel:t.channel,members:t.members??[]});break;case"presence_join":this.emit("presence:join",{channel:t.channel,user:t.user});break;case"presence_leave":this.emit("presence:leave",{channel:t.channel,userId:t.user_id});break;case"presence_update":this.emit("presence:update",{channel:t.channel,userId:t.user_id,status:t.status,userData:t.user_data});break;case"error":this.emit("error",{message:t.message??"Unknown error"});break;case"token_expiring":this.reconnectAttempt=0,this.scheduleReconnect();break}}catch{}}resubscribeAll(){for(let e of this.subscriptions)this.send({type:"subscribe",channel:e});for(let[e,t]of this.presenceChannels)this.send({type:"presence_join",channel:e,user_data:t??{}})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.ws?.readyState===WebSocket.OPEN&&this.ws.send("ping")},3e4)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.reconnectAttempt>=this.maxRetries){this.setStatus("disconnected");return}this.setStatus("reconnecting"),this.emit("reconnecting",{attempt:this.reconnectAttempt+1});let e=Math.min(this.baseDelay*Math.pow(2,this.reconnectAttempt)+Math.random()*this.baseDelay*.3,this.maxDelay);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempt++,this.connect()},e)}setStatus(e){this.status!==e&&(this.status=e,this.emit("status",e))}};var g=class extends p{constructor(e){super();this.conversationSubs=new Map;this.conversationTypes=new Map;let t=e.apiBaseUrl??b;this.http=new u({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0)}),this.ws=new m({baseUrl:t,wsUrl:e.wsUrl,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0),reconnect:e.reconnect}),this.cache=new d(e.messageCache?.maxMessages,e.messageCache?.maxConversations),this.offlineQueue=new h(e.offlineQueue??!0),this.ws.on("status",n=>{switch(n){case"connected":this.emit("connected"),this.flushOfflineQueue();break;case"disconnected":this.emit("disconnected");break}}),this.ws.on("reconnecting",n=>{this.emit("reconnecting",n)}),this.ws.on("message",({channel:n,data:i})=>{this.handleRealtimeMessage(n,i)}),this.ws.on("presence:state",({channel:n,members:i})=>{let a=n.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:state",{conversationId:a,members:i})}),this.ws.on("presence:join",({channel:n,user:i})=>{let a=n.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:join",{userId:i.user_id,conversationId:a,userData:i.user_data})}),this.ws.on("presence:leave",({channel:n,userId:i})=>{let a=n.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:leave",{userId:i,conversationId:a})}),this.ws.on("presence:update",({channel:n,userId:i,status:a,userData:o})=>{let r=n.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:update",{userId:i,conversationId:r,status:a,userData:o})}),this.ws.on("error",({message:n})=>{this.emit("error",{code:"ws_error",message:n})})}get status(){return this.ws.getStatus()}connect(){this.ws.connect()}disconnect(){for(let e of this.conversationSubs.values())e();this.conversationSubs.clear(),this.ws.disconnect()}async createConversation(e){let t={...e,conversation_type:e.conversation_type??"direct"},n=await this.http.post("/v1/chat/conversations",t);return n.data&&this.trackConversationType(n.data),n}async listConversations(e){let t=new URLSearchParams;e?.page&&t.set("page",String(e.page)),e?.per_page&&t.set("per_page",String(e.per_page)),e?.conversation_type&&t.set("conversation_type",e.conversation_type);let n=t.toString(),i=await this.http.get(`/v1/chat/conversations${n?"?"+n:""}`);return i.data&&i.data.forEach(a=>this.trackConversationType(a)),i}async getConversation(e){let t=await this.http.get(`/v1/chat/conversations/${e}`);return t.data&&this.trackConversationType(t.data),t}trackConversationType(e){e.conversation_type!=="direct"&&e.conversation_type!=="group"&&this.conversationTypes.set(e.id,e.conversation_type)}async sendMessage(e,t){let n=await this.http.post(`/v1/chat/conversations/${e}/messages`,{content:t.content,message_type:t.message_type??"text",attachments:t.attachments});return n.data?this.cache.addMessage(e,n.data):n.error?.status===0&&this.offlineQueue.enqueue(e,t.content,t.message_type??"text"),n}async getMessages(e,t){let n=new URLSearchParams;t?.limit&&n.set("limit",String(t.limit)),t?.before&&n.set("before",t.before),t?.after&&n.set("after",t.after);let i=n.toString(),a=await this.http.get(`/v1/chat/conversations/${e}/messages${i?"?"+i:""}`);return a.data?.messages&&(t?.after||(a.data.messages=a.data.messages.slice().reverse()),!t?.before&&!t?.after&&this.cache.setMessages(e,a.data.messages)),a}async editMessage(e,t){return this.http.patch(`/v1/chat/messages/${e}`,{content:t})}async deleteMessage(e){return this.http.del(`/v1/chat/messages/${e}`)}getCachedMessages(e){return this.cache.getMessages(e)}async addReaction(e,t){return this.http.post(`/v1/chat/messages/${e}/reactions`,{emoji:t})}async removeReaction(e,t){return this.http.del(`/v1/chat/messages/${e}/reactions/${encodeURIComponent(t)}`)}async getUnreadTotal(){return this.http.get("/v1/chat/conversations/unread-total")}async sendTyping(e,t=!0){await this.http.post(`/v1/chat/conversations/${e}/typing`,{is_typing:t})}async markRead(e){await this.http.post(`/v1/chat/conversations/${e}/read`)}async getReadStatus(e){return this.http.get(`/v1/chat/conversations/${e}/read-status`)}async addParticipant(e,t){return this.http.post(`/v1/chat/conversations/${e}/participants`,{user_id:t})}async removeParticipant(e,t){return this.http.del(`/v1/chat/conversations/${e}/participants/${t}`)}joinPresence(e,t){let n=this.channelName(e);this.ws.joinPresence(n,t)}leavePresence(e){let t=this.channelName(e);this.ws.leavePresence(t)}updatePresence(e,t,n){let i=this.channelName(e);this.ws.send({type:"presence_update",channel:i,status:t,user_data:n})}async getChannelSettings(e){return this.http.get(`/v1/chat/channels/${e}/settings`)}setConversationType(e,t){this.conversationTypes.set(e,t)}async findChannelBySessionId(e){let t=await this.http.get(`/v1/chat/channels/by-session?linked_session_id=${encodeURIComponent(e)}`);return t.error?.status===404?null:(t.data&&this.conversationTypes.set(t.data.id,t.data.channel_type),t.data)}async joinChannel(e){return this.http.post(`/v1/chat/channels/${e}/join`,{})}async createEphemeralChannel(e){let t=await this.http.post("/v1/chat/channels/ephemeral",e);return t.data&&this.conversationTypes.set(t.data.id,"ephemeral"),t}async createLargeRoom(e){let t=await this.http.post("/v1/chat/channels/large-room",e);return t.data&&this.conversationTypes.set(t.data.id,"large_room"),t}async getSubscriberCount(e){return(await this.http.get(`/v1/chat/conversations/${e}/subscriber-count`)).data?.count??0}subscribeToConversation(e){if(this.conversationSubs.has(e))return this.conversationSubs.get(e);let t=this.channelName(e),n=this.ws.subscribe(t);return this.conversationSubs.set(e,n),()=>{this.conversationSubs.delete(e),n()}}channelName(e){switch(this.conversationTypes.get(e)){case"large_room":return`conversation:lr:${e}`;case"broadcast":return`conversation:bc:${e}`;case"support":return`conversation:support:${e}`;default:return`conversation:${e}`}}destroy(){this.disconnect(),this.cache.clear(),this.removeAllListeners()}handleRealtimeMessage(e,t){if(e.startsWith("conversation:")){this.handleConversationMessage(e,t);return}if(e.startsWith("private:")){this.handlePrivateMessage(t);return}}handlePrivateMessage(e){let t=e;if(!t)return;let n=t.event??t.type,i=t.data??t;switch(n){case"new_message":{let a=i.conversation_id,o=i.id??i.message_id,r=i.sender_id,l=i.content??"";a&&this.emit("inbox:update",{conversationId:a,messageId:o,senderId:r,preview:l});break}case"support:new_conversation":{let a=i.conversation_id,o=i.visitor_name;a&&this.emit("support:new",{conversationId:a,visitorName:o});break}case"support:assigned":{let a=i.conversation_id,o=i.visitor_name,r=i.visitor_email;a&&this.emit("support:assigned",{conversationId:a,visitorName:o,visitorEmail:r});break}}}handleConversationMessage(e,t){let n=e.replace(/^conversation:(?:lr:|bc:|support:)?/,""),i=t;if(!i)return;let a=i.event??i.type,o=i.data??i;switch(a){case"new_message":{let r=o;this.cache.addMessage(n,r),this.emit("message",{message:r,conversationId:n});break}case"message_edited":{let r=o;this.cache.updateMessage(n,r),this.emit("message:updated",{message:r,conversationId:n});break}case"message_deleted":{let r=o.message_id??o.id;r&&(this.cache.removeMessage(n,r),this.emit("message:deleted",{messageId:r,conversationId:n}));break}case"user_typing":{let r=o.user_id;r&&this.emit("typing",{userId:r,conversationId:n});break}case"user_stopped_typing":{let r=o.user_id;r&&this.emit("typing:stop",{userId:r,conversationId:n});break}case"typing_batch":{let r=o.users??[];for(let l of r)this.emit("typing",{userId:l,conversationId:n});break}case"messages_read":{let r=o.user_id,l=o.last_read_at;r&&l&&this.emit("read",{userId:r,conversationId:n,lastReadAt:l});break}case"room_upgraded":{if(o.new_type==="large_room"){this.conversationTypes.set(n,"large_room");let l=this.conversationSubs.get(n);l&&(l(),this.conversationSubs.delete(n),this.subscribeToConversation(n)),this.emit("room_upgraded",{conversationId:n,newType:"large_room"})}break}}}async flushOfflineQueue(){let e=this.offlineQueue.drain();for(let t of e)await this.sendMessage(t.conversationId,{content:t.content,message_type:t.message_type})}};var P="sm_support_",v=class{constructor(s){this.chatClient=null;this.refreshToken=null;this.accessToken=null;this.tokenExpiresAt=0;this.userId=null;this.apiKey=s.apiKey,this.apiBaseUrl=s.apiBaseUrl??"https://api.scalemule.com",this.storageKey=P+s.apiKey.substring(0,8);let e=this.loadState();e?(this.anonymousId=e.anonymous_id,this.refreshToken=e.refresh_token,this.userId=e.user_id):this.anonymousId=crypto.randomUUID()}async initVisitorSession(s){if(this.visitorName=s?.name,this.visitorEmail=s?.email,this.refreshToken)try{await this.refreshAccessToken(),this.initChatClient();return}catch{this.refreshToken=null}let e=await fetch(`${this.apiBaseUrl}/v1/auth/visitor-session`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({anonymous_id:this.anonymousId,name:s?.name,email:s?.email,page_url:typeof location<"u"?location.href:void 0})});if(!e.ok){let i=await e.text();throw new Error(`Visitor session failed: ${e.status} ${i}`)}let n=(await e.json()).data;this.accessToken=n.access_token,this.refreshToken=n.refresh_token,this.userId=n.user_id,this.tokenExpiresAt=Date.now()+n.expires_in*1e3,this.saveState(),this.initChatClient()}async startConversation(s,e){if(!this.accessToken)throw new Error("Call initVisitorSession() first");let t=await fetch(`${this.apiBaseUrl}/v1/chat/support/conversations`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey,Authorization:`Bearer ${this.accessToken}`},body:JSON.stringify({message:s,name:this.visitorName,email:this.visitorEmail,page_url:e?.page_url??(typeof location<"u"?location.href:void 0),user_agent:typeof navigator<"u"?navigator.userAgent:void 0})});if(!t.ok){let a=await t.text();throw new Error(`Create support conversation failed: ${t.status} ${a}`)}let i=(await t.json()).data;return this.chatClient&&this.chatClient.conversationTypes?.set(i.conversation_id,"support"),i}async getActiveConversation(){if(!this.accessToken)return null;let s=await fetch(`${this.apiBaseUrl}/v1/chat/support/conversations/mine`,{headers:{"x-api-key":this.apiKey,Authorization:`Bearer ${this.accessToken}`}});if(!s.ok)return null;let n=(await s.json()).data.find(i=>i.status==="active"||i.status==="waiting");return n&&this.chatClient&&this.chatClient.conversationTypes?.set(n.conversation_id,"support"),n??null}get chat(){if(!this.chatClient)throw new Error("Call initVisitorSession() first");return this.chatClient}get isInitialized(){return this.chatClient!==null}get visitorUserId(){return this.userId}destroy(){this.chatClient?.destroy(),this.chatClient=null}initChatClient(){this.chatClient&&this.chatClient.destroy();let s={apiKey:this.apiKey,apiBaseUrl:this.apiBaseUrl,getToken:async()=>{if(!this.accessToken)return null;if(Date.now()>this.tokenExpiresAt-6e4)try{await this.refreshAccessToken()}catch{}return this.accessToken}};this.chatClient=new g(s)}async refreshAccessToken(){if(!this.refreshToken)throw new Error("No refresh token");let s=await fetch(`${this.apiBaseUrl}/v1/auth/token/refresh`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({refresh_token:this.refreshToken})});if(!s.ok)throw new Error(`Token refresh failed: ${s.status}`);let t=(await s.json()).data;this.accessToken=t.access_token,this.tokenExpiresAt=Date.now()+t.expires_in*1e3}loadState(){try{let s=localStorage.getItem(this.storageKey);return s?JSON.parse(s):null}catch{return null}}saveState(){try{localStorage.setItem(this.storageKey,JSON.stringify({anonymous_id:this.anonymousId,refresh_token:this.refreshToken,user_id:this.userId}))}catch{}}};var w=`
1
+ "use strict";var ScaleMuleSupportWidget=(()=>{var g=class{constructor(){this.listeners=new Map}on(s,e){return this.listeners.has(s)||this.listeners.set(s,new Set),this.listeners.get(s).add(e),()=>this.off(s,e)}off(s,e){this.listeners.get(s)?.delete(e)}once(s,e){let t=(i=>{this.off(s,t),e(i)});return this.on(s,t)}emit(s,...[e]){let t=this.listeners.get(s);if(t)for(let i of t)try{i(e)}catch(n){console.error(`[ScaleMuleChat] Error in ${String(s)} listener:`,n)}}removeAllListeners(s){s?this.listeners.delete(s):this.listeners.clear()}};var b=class extends g{constructor(e,t){super();this.typingTimers=new Map;this.unsubscribers=[];this.client=e,this.conversationId=t,this.state={conversationId:t,messages:[],readStatuses:[],typingUsers:[],members:[],hasMore:!1,isLoading:!0,error:null}}getState(){return this.state}async init(e={}){let t=e.realtime??!0,i=e.presence??t;this.bindEvents(),t&&this.client.connect();try{await this.client.getConversation(this.conversationId);let[n,r]=await Promise.all([this.client.getMessages(this.conversationId),this.client.getReadStatus(this.conversationId)]);return this.state={...this.state,messages:n.data?.messages??[],readStatuses:r.data?.statuses??[],hasMore:n.data?.has_more??!1,isLoading:!1,error:n.error?.message??r.error?.message??null},t&&this.unsubscribers.push(this.client.subscribeToConversation(this.conversationId)),i&&this.client.joinPresence(this.conversationId),this.emit("state",this.state),this.emit("ready",this.state),this.state}catch(n){let r=n instanceof Error?n.message:"Failed to initialize chat controller";throw this.state={...this.state,isLoading:!1,error:r},this.emit("state",this.state),this.emit("error",{message:r}),n}}async loadMore(){let e=this.state.messages[0]?.id;if(!e)return;let t=await this.client.getMessages(this.conversationId,{before:e});t.data?.messages?.length&&(this.state={...this.state,messages:[...t.data.messages,...this.state.messages],hasMore:t.data.has_more??!1},this.emit("state",this.state))}async sendMessage(e,t=[]){let i=t.length>0?t.every(r=>r.mime_type.startsWith("image/"))&&!e?"image":"file":"text",n=await this.client.sendMessage(this.conversationId,{content:e,attachments:t,message_type:i});if(n.error)throw new Error(n.error.message)}stageOptimisticMessage(e){let t=this.client.stageOptimisticMessage(this.conversationId,e);return this.patchState({messages:[...this.client.getCachedMessages(this.conversationId)]}),t}async uploadAttachment(e,t,i){return this.client.uploadAttachment(e,t,i)}async refreshAttachmentUrl(e,t){return this.client.refreshAttachmentUrl(e,t)}async addReaction(e,t){let i=await this.client.addReaction(e,t);if(i.error)throw new Error(i.error.message)}async removeReaction(e,t){let i=await this.client.removeReaction(e,t);if(i.error)throw new Error(i.error.message)}async reportMessage(e,t,i){return this.client.reportMessage(e,t,i)}async muteConversation(e){return this.client.muteConversation(this.conversationId,e)}async unmuteConversation(){return this.client.unmuteConversation(this.conversationId)}async markRead(){await this.client.markRead(this.conversationId)}async refreshReadStatus(){let e=await this.client.getReadStatus(this.conversationId);if(e.data?.statuses)return this.patchState({readStatuses:e.data.statuses}),e.data.statuses;if(e.error)throw new Error(e.error.message);return this.state.readStatuses}sendTyping(e=!0){this.client.sendTyping(this.conversationId,e)}destroy(){this.client.leavePresence(this.conversationId);for(let e of this.typingTimers.values())clearTimeout(e);this.typingTimers.clear();for(let e of this.unsubscribers)e();this.unsubscribers=[],this.removeAllListeners()}bindEvents(){this.unsubscribers.length||(this.unsubscribers.push(this.client.on("message",({conversationId:e})=>{e===this.conversationId&&this.patchState({messages:[...this.client.getCachedMessages(this.conversationId)]})})),this.unsubscribers.push(this.client.on("message:updated",({conversationId:e})=>{e===this.conversationId&&this.patchState({messages:[...this.client.getCachedMessages(this.conversationId)]})})),this.unsubscribers.push(this.client.on("message:deleted",({conversationId:e})=>{e===this.conversationId&&this.patchState({messages:[...this.client.getCachedMessages(this.conversationId)]})})),this.unsubscribers.push(this.client.on("reaction",({conversationId:e})=>{e===this.conversationId&&this.patchState({messages:[...this.client.getCachedMessages(this.conversationId)]})})),this.unsubscribers.push(this.client.on("typing",({conversationId:e,userId:t})=>{if(e!==this.conversationId)return;this.patchState({typingUsers:this.state.typingUsers.includes(t)?this.state.typingUsers:[...this.state.typingUsers,t]});let i=this.typingTimers.get(t);i&&clearTimeout(i),this.typingTimers.set(t,setTimeout(()=>{this.patchState({typingUsers:this.state.typingUsers.filter(n=>n!==t)}),this.typingTimers.delete(t)},3e3))})),this.unsubscribers.push(this.client.on("typing:stop",({conversationId:e,userId:t})=>{if(e!==this.conversationId)return;let i=this.typingTimers.get(t);i&&(clearTimeout(i),this.typingTimers.delete(t)),this.patchState({typingUsers:this.state.typingUsers.filter(n=>n!==t)})})),this.unsubscribers.push(this.client.on("read",({conversationId:e,userId:t,lastReadAt:i})=>{if(e!==this.conversationId)return;let n=[...this.state.readStatuses],r=n.findIndex(a=>a.user_id===t);r>=0?n[r]={...n[r],last_read_at:i}:n.push({user_id:t,last_read_at:i}),this.patchState({readStatuses:n})})),this.unsubscribers.push(this.client.on("presence:state",({conversationId:e,members:t})=>{e===this.conversationId&&this.patchState({members:t.map(i=>({userId:i.user_id,status:i.status??"online",userData:i.user_data}))})})),this.unsubscribers.push(this.client.on("presence:join",({conversationId:e,userId:t,userData:i})=>{e===this.conversationId&&(this.state.members.some(n=>n.userId===t)||this.patchState({members:[...this.state.members,{userId:t,status:"online",userData:i}]}))})),this.unsubscribers.push(this.client.on("presence:leave",({conversationId:e,userId:t})=>{e===this.conversationId&&this.patchState({members:this.state.members.filter(i=>i.userId!==t)})})),this.unsubscribers.push(this.client.on("presence:update",({conversationId:e,userId:t,status:i,userData:n})=>{e===this.conversationId&&this.patchState({members:this.state.members.map(r=>r.userId===t?{...r,status:i,userData:n}:r)})})))}patchState(e){this.state={...this.state,...e,error:e.error!==void 0?e.error:this.state.error},this.emit("state",this.state)}};var M="https://api.scalemule.com";var y=class{constructor(s,e){this.cache=new Map;this.maxMessages=s??200,this.maxConversations=e??50}getMessages(s){return this.cache.get(s)??[]}getMessage(s,e){return this.getMessages(s).find(t=>t.id===e)}setMessages(s,e){this.cache.set(s,e.slice(0,this.maxMessages)),this.evictOldConversations()}addMessage(s,e){let t=this.cache.get(s)??[];t.some(i=>i.id===e.id)||(t.push(e),t.sort((i,n)=>i.created_at.localeCompare(n.created_at)),t.length>this.maxMessages&&t.splice(0,t.length-this.maxMessages),this.cache.set(s,t))}upsertMessage(s,e){if(this.getMessage(s,e.id)){this.updateMessage(s,e);return}this.addMessage(s,e)}updateMessage(s,e){let t=this.cache.get(s);if(!t)return;let i=t.findIndex(n=>n.id===e.id);i>=0&&(t[i]=e)}reconcileOptimisticMessage(s,e){let t=this.cache.get(s);if(!t)return e;let i=(e.attachments??[]).map(r=>r.file_id).sort(),n=t.findIndex(r=>{if(!r.id.startsWith("pending-")||r.sender_id!==e.sender_id||r.content!==e.content)return!1;let a=(r.attachments??[]).map(o=>o.file_id).sort();return a.length!==i.length?!1:a.every((o,l)=>o===i[l])});return n>=0&&(t[n]=e),e}removeMessage(s,e){let t=this.cache.get(s);if(!t)return;let i=t.findIndex(n=>n.id===e);i>=0&&t.splice(i,1)}clear(s){s?this.cache.delete(s):this.cache.clear()}evictOldConversations(){for(;this.cache.size>this.maxConversations;){let s=this.cache.keys().next().value;s&&this.cache.delete(s)}}};var A="scalemule_chat_offline_queue",_=class{constructor(s=!0){this.queue=[];this.enabled=s,this.enabled&&this.load()}enqueue(s,e,t="text",i){this.enabled&&(this.queue.push({conversationId:s,content:e,message_type:t,attachments:i,timestamp:Date.now()}),this.save())}drain(){let s=[...this.queue];return this.queue=[],this.save(),s}get size(){return this.queue.length}save(){try{typeof localStorage<"u"&&localStorage.setItem(A,JSON.stringify(this.queue))}catch{}}load(){try{if(typeof localStorage<"u"){let s=localStorage.getItem(A);s&&(this.queue=JSON.parse(s))}}catch{this.queue=[]}}};var w=class{constructor(s){this.baseUrl=s.baseUrl.replace(/\/$/,""),this.apiKey=s.apiKey,this.getToken=s.getToken,this.timeout=s.timeout??1e4}async get(s){return this.request("GET",s)}async post(s,e){return this.request("POST",s,e)}async patch(s,e){return this.request("PATCH",s,e)}async del(s){return this.request("DELETE",s)}async request(s,e,t){let i={"Content-Type":"application/json"};if(this.apiKey&&(i["x-api-key"]=this.apiKey),this.getToken){let a=await this.getToken();a&&(i.Authorization=`Bearer ${a}`)}let n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let a=await fetch(`${this.baseUrl}${e}`,{method:s,headers:i,body:t?JSON.stringify(t):void 0,signal:n.signal});if(clearTimeout(r),a.status===204)return{data:null,error:null};let o=await a.json().catch(()=>null);return a.ok?{data:o?.data!==void 0?o.data:o,error:null}:{data:null,error:{code:o?.error?.code??o?.code??"unknown",message:o?.error?.message??o?.message??a.statusText,status:a.status,details:o?.error?.details??o?.details}}}catch(a){return clearTimeout(r),{data:null,error:{code:"network_error",message:a instanceof Error?a.message:"Network error",status:0}}}}};var x=class extends g{constructor(e){super();this.ws=null;this.status="disconnected";this.subscriptions=new Set;this.presenceChannels=new Map;this.reconnectAttempt=0;this.reconnectTimer=null;this.heartbeatTimer=null;this.config=e,this.maxRetries=e.reconnect?.maxRetries??1/0,this.baseDelay=e.reconnect?.baseDelay??1e3,this.maxDelay=e.reconnect?.maxDelay??3e4;let t=e.baseUrl.replace(/\/$/,"");this.ticketUrl=`${t}/v1/realtime/ws/ticket`;let i=e.wsUrl?e.wsUrl.replace(/\/$/,""):t;this.wsBaseUrl=i.replace(/^http/,"ws")+"/v1/realtime/ws"}getStatus(){return this.status}async connect(){if(!(this.status==="connected"||this.status==="connecting")){this.setStatus("connecting");try{let e=await this.obtainTicket();if(!e){this.setStatus("disconnected"),this.emit("error",{message:"Failed to obtain WS ticket"});return}let t=`${this.wsBaseUrl}?ticket=${encodeURIComponent(e)}`;this.ws=new WebSocket(t),this.ws.onopen=()=>{this.setStatus("connected"),this.reconnectAttempt=0,this.startHeartbeat(),this.resubscribeAll()},this.ws.onmessage=i=>{this.handleMessage(i.data)},this.ws.onclose=()=>{this.stopHeartbeat(),this.status!=="disconnected"&&this.scheduleReconnect()},this.ws.onerror=()=>{}}catch{this.setStatus("disconnected"),this.scheduleReconnect()}}}disconnect(){this.setStatus("disconnected"),this.stopHeartbeat(),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.onclose=null,this.ws.close(),this.ws=null)}subscribe(e){return this.subscriptions.add(e),this.status==="connected"?this.send({type:"subscribe",channel:e}):this.status==="disconnected"&&this.connect(),()=>this.unsubscribe(e)}unsubscribe(e){this.subscriptions.delete(e),this.status==="connected"&&this.send({type:"unsubscribe",channel:e})}publish(e,t){this.status==="connected"&&this.send({type:"publish",channel:e,data:t})}joinPresence(e,t){this.presenceChannels.set(e,t),this.status==="connected"&&this.send({type:"presence_join",channel:e,user_data:t??{}})}leavePresence(e){this.presenceChannels.delete(e),this.status==="connected"&&this.send({type:"presence_leave",channel:e})}async obtainTicket(){let e={"Content-Type":"application/json"};if(this.config.apiKey&&(e["x-api-key"]=this.config.apiKey),this.config.getToken){let t=await this.config.getToken();t&&(e.Authorization=`Bearer ${t}`)}try{let t=await fetch(this.ticketUrl,{method:"POST",headers:e});if(!t.ok)return null;let i=await t.json();return i.ticket??i.data?.ticket??null}catch{return null}}send(e){this.ws?.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}handleMessage(e){if(e!=="pong")try{let t=JSON.parse(e);switch(t.type){case"auth_success":break;case"subscribed":break;case"message":this.emit("message",{channel:t.channel,data:t.data});break;case"presence_state":this.emit("presence:state",{channel:t.channel,members:t.members??[]});break;case"presence_join":this.emit("presence:join",{channel:t.channel,user:t.user});break;case"presence_leave":this.emit("presence:leave",{channel:t.channel,userId:t.user_id});break;case"presence_update":this.emit("presence:update",{channel:t.channel,userId:t.user_id,status:t.status,userData:t.user_data});break;case"error":this.emit("error",{message:t.message??"Unknown error"});break;case"token_expiring":this.reconnectAttempt=0,this.scheduleReconnect();break}}catch{}}resubscribeAll(){for(let e of this.subscriptions)this.send({type:"subscribe",channel:e});for(let[e,t]of this.presenceChannels)this.send({type:"presence_join",channel:e,user_data:t??{}})}startHeartbeat(){this.heartbeatTimer=setInterval(()=>{this.ws?.readyState===WebSocket.OPEN&&this.ws.send("ping")},3e4)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.reconnectAttempt>=this.maxRetries){this.setStatus("disconnected");return}this.setStatus("reconnecting"),this.emit("reconnecting",{attempt:this.reconnectAttempt+1});let e=Math.min(this.baseDelay*Math.pow(2,this.reconnectAttempt)+Math.random()*this.baseDelay*.3,this.maxDelay);this.reconnectTimer=setTimeout(()=>{this.reconnectAttempt++,this.connect()},e)}setStatus(e){this.status!==e&&(this.status=e,this.emit("status",e))}};var Z=[0,1e3,3e3],R=45e3,ee=new Set([0,408,429,500,502,503,504]);function te(c){return new Promise(s=>setTimeout(s,c))}function se(c,s){return s==="aborted"?!1:ee.has(c)||s==="upload_stalled"}function ie(c,s,e,t){return new Promise(i=>{if(typeof XMLHttpRequest>"u"){i({data:null,error:{code:"unsupported_environment",message:"XMLHttpRequest is not available in this environment",status:0}});return}let n=new XMLHttpRequest,r=!1,a=null,o=0,l=s.size,h=p=>{r||(r=!0,a&&(clearTimeout(a),a=null),i(p))},f=()=>{a&&clearTimeout(a),a=setTimeout(()=>{n.abort(),h({data:null,error:{code:"upload_stalled",message:`Upload stalled (no progress for ${R/1e3}s)`,status:0,details:{bytes_sent:o,total_bytes:l}}})},R)};if(t){if(t.aborted){h({data:null,error:{code:"aborted",message:"Upload aborted",status:0}});return}t.addEventListener("abort",()=>{n.abort(),h({data:null,error:{code:"aborted",message:"Upload aborted",status:0}})},{once:!0})}n.upload.addEventListener("progress",p=>{f(),o=p.loaded,l=p.total||l,p.lengthComputable&&e?.(Math.round(p.loaded/p.total*100))}),n.addEventListener("load",()=>{if(n.status>=200&&n.status<300){e?.(100),h({data:null,error:null});return}h({data:null,error:{code:"upload_error",message:`S3 upload failed: ${n.status}`,status:n.status,details:{bytes_sent:o,total_bytes:l}}})}),n.addEventListener("error",()=>{h({data:null,error:{code:"upload_error",message:"S3 upload failed",status:n.status||0,details:{bytes_sent:o,total_bytes:l}}})}),n.addEventListener("abort",()=>{r||h({data:null,error:{code:"aborted",message:"Upload aborted",status:0}})}),n.open("PUT",c,!0),s.type&&n.setRequestHeader("Content-Type",s.type),f(),n.send(s)})}async function I(c,s,e,t){let i=null;for(let[n,r]of Z.entries()){r>0&&await te(r);let a=await ie(c,s,e,t);if(!a.error)return a;if(i={...a.error,details:{...a.error.details,attempt:n+1}},!se(a.error.status,a.error.code))break}return{data:null,error:i??{code:"upload_error",message:"Upload failed",status:0}}}var S=class extends g{constructor(e){super();this.conversationSubs=new Map;this.conversationTypes=new Map;let t=e.apiBaseUrl??M;this.currentUserId=e.userId,this.http=new w({baseUrl:t,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0)}),this.ws=new x({baseUrl:t,wsUrl:e.wsUrl,apiKey:e.apiKey,getToken:e.getToken??(e.sessionToken?()=>Promise.resolve(e.sessionToken):void 0),reconnect:e.reconnect}),this.cache=new y(e.messageCache?.maxMessages,e.messageCache?.maxConversations),this.offlineQueue=new _(e.offlineQueue??!0),this.ws.on("status",i=>{switch(i){case"connected":this.emit("connected"),this.flushOfflineQueue();break;case"disconnected":this.emit("disconnected");break}}),this.ws.on("reconnecting",i=>{this.emit("reconnecting",i)}),this.ws.on("message",({channel:i,data:n})=>{this.handleRealtimeMessage(i,n)}),this.ws.on("presence:state",({channel:i,members:n})=>{let r=i.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:state",{conversationId:r,members:n})}),this.ws.on("presence:join",({channel:i,user:n})=>{let r=i.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:join",{userId:n.user_id,conversationId:r,userData:n.user_data})}),this.ws.on("presence:leave",({channel:i,userId:n})=>{let r=i.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:leave",{userId:n,conversationId:r})}),this.ws.on("presence:update",({channel:i,userId:n,status:r,userData:a})=>{let o=i.replace(/^conversation:(?:lr:|bc:|support:)?/,"");this.emit("presence:update",{userId:n,conversationId:o,status:r,userData:a})}),this.ws.on("error",({message:i})=>{this.emit("error",{code:"ws_error",message:i})})}get status(){return this.ws.getStatus()}get userId(){return this.currentUserId}connect(){this.ws.connect()}disconnect(){for(let e of this.conversationSubs.values())e();this.conversationSubs.clear(),this.ws.disconnect()}async createConversation(e){let t={...e,conversation_type:e.conversation_type??"direct"},i=await this.http.post("/v1/chat/conversations",t);return i.data&&this.trackConversationType(i.data),i}async listConversations(e){let t=new URLSearchParams;e?.page&&t.set("page",String(e.page)),e?.per_page&&t.set("per_page",String(e.per_page)),e?.conversation_type&&t.set("conversation_type",e.conversation_type);let i=t.toString(),n=await this.http.get(`/v1/chat/conversations${i?"?"+i:""}`);return n.data&&n.data.forEach(r=>this.trackConversationType(r)),n}async getConversation(e){let t=await this.http.get(`/v1/chat/conversations/${e}`);return t.data&&this.trackConversationType(t.data),t}trackConversationType(e){e.conversation_type!=="direct"&&e.conversation_type!=="group"&&this.conversationTypes.set(e.id,e.conversation_type)}async sendMessage(e,t){let i=await this.http.post(`/v1/chat/conversations/${e}/messages`,{content:t.content,message_type:t.message_type??"text",attachments:t.attachments});if(i.data){let n=this.cache.reconcileOptimisticMessage(e,i.data);this.cache.upsertMessage(e,n)}else i.error?.status===0&&this.offlineQueue.enqueue(e,t.content,t.message_type??"text",t.attachments);return i}async getMessages(e,t){let i=new URLSearchParams;t?.limit&&i.set("limit",String(t.limit)),t?.before&&i.set("before",t.before),t?.after&&i.set("after",t.after);let n=i.toString(),r=await this.http.get(`/v1/chat/conversations/${e}/messages${n?"?"+n:""}`);return r.data?.messages&&(t?.after||(r.data.messages=r.data.messages.slice().reverse()),!t?.before&&!t?.after&&this.cache.setMessages(e,r.data.messages)),r}async editMessage(e,t){return this.http.patch(`/v1/chat/messages/${e}`,{content:t})}async deleteMessage(e){return this.http.del(`/v1/chat/messages/${e}`)}async uploadAttachment(e,t,i){let n=typeof File<"u"&&e instanceof File?e.name:"attachment",r=e.type||"application/octet-stream",a=await this.http.post("/v1/storage/signed-url/upload",{filename:n,content_type:r,size_bytes:e.size,is_public:!1,metadata:{source:"chat_sdk"}});if(a.error||!a.data)return{data:null,error:a.error};let o=await I(a.data.upload_url,e,t,i);if(o.error)return{data:null,error:o.error};let l=await this.http.post("/v1/storage/signed-url/complete",{file_id:a.data.file_id,completion_token:a.data.completion_token});return l.error||!l.data?{data:null,error:l.error}:{data:{file_id:l.data.file_id,file_name:l.data.filename,file_size:l.data.size_bytes,mime_type:l.data.content_type,presigned_url:l.data.url},error:null}}async refreshAttachmentUrl(e,t){return this.http.get(`/v1/chat/messages/${e}/attachment/${t}/url`)}getCachedMessages(e){return this.cache.getMessages(e)}stageOptimisticMessage(e,t){return this.cache.upsertMessage(e,t),t}async addReaction(e,t){return this.http.post(`/v1/chat/messages/${e}/reactions`,{emoji:t})}async removeReaction(e,t){return this.http.del(`/v1/chat/messages/${e}/reactions/${encodeURIComponent(t)}`)}async reportMessage(e,t,i){return this.http.post(`/v1/chat/messages/${e}/report`,{reason:t,description:i})}async muteConversation(e,t){return this.http.post(`/v1/chat/conversations/${e}/mute`,{muted_until:t})}async unmuteConversation(e){return this.http.del(`/v1/chat/conversations/${e}/mute`)}async getUnreadTotal(){return this.http.get("/v1/chat/conversations/unread-total")}async sendTyping(e,t=!0){await this.http.post(`/v1/chat/conversations/${e}/typing`,{is_typing:t})}async markRead(e){await this.http.post(`/v1/chat/conversations/${e}/read`)}async getReadStatus(e){return this.http.get(`/v1/chat/conversations/${e}/read-status`)}async addParticipant(e,t){return this.http.post(`/v1/chat/conversations/${e}/participants`,{user_id:t})}async removeParticipant(e,t){return this.http.del(`/v1/chat/conversations/${e}/participants/${t}`)}joinPresence(e,t){let i=this.channelName(e);this.ws.joinPresence(i,t)}leavePresence(e){let t=this.channelName(e);this.ws.leavePresence(t)}updatePresence(e,t,i){let n=this.channelName(e);this.ws.send({type:"presence_update",channel:n,status:t,user_data:i})}async getChannelSettings(e){return this.http.get(`/v1/chat/channels/${e}/settings`)}setConversationType(e,t){this.conversationTypes.set(e,t)}async findChannelBySessionId(e){let t=await this.http.get(`/v1/chat/channels/by-session?linked_session_id=${encodeURIComponent(e)}`);return t.error?.status===404?null:(t.data&&this.conversationTypes.set(t.data.id,t.data.channel_type),t.data)}async joinChannel(e){return this.http.post(`/v1/chat/channels/${e}/join`,{})}async createEphemeralChannel(e){let t=await this.http.post("/v1/chat/channels/ephemeral",e);return t.data&&this.conversationTypes.set(t.data.id,"ephemeral"),t}async createLargeRoom(e){let t=await this.http.post("/v1/chat/channels/large-room",e);return t.data&&this.conversationTypes.set(t.data.id,"large_room"),t}async getSubscriberCount(e){return(await this.http.get(`/v1/chat/conversations/${e}/subscriber-count`)).data?.count??0}subscribeToConversation(e){if(this.conversationSubs.has(e))return this.conversationSubs.get(e);let t=this.channelName(e),i=this.ws.subscribe(t);return this.conversationSubs.set(e,i),()=>{this.conversationSubs.delete(e),i()}}channelName(e){switch(this.conversationTypes.get(e)){case"large_room":return`conversation:lr:${e}`;case"broadcast":return`conversation:bc:${e}`;case"support":return`conversation:support:${e}`;default:return`conversation:${e}`}}destroy(){this.disconnect(),this.cache.clear(),this.removeAllListeners()}handleRealtimeMessage(e,t){if(e.startsWith("conversation:")){this.handleConversationMessage(e,t);return}if(e.startsWith("private:")){this.handlePrivateMessage(t);return}}handlePrivateMessage(e){let t=e;if(!t)return;let i=t.event??t.type,n=t.data??t;switch(i){case"new_message":{let r=n.conversation_id,a=n.id??n.message_id,o=n.sender_id,l=n.content??"";r&&this.emit("inbox:update",{conversationId:r,messageId:a,senderId:o,preview:l});break}case"support:new_conversation":{let r=n.conversation_id,a=n.visitor_name;r&&this.emit("support:new",{conversationId:r,visitorName:a});break}case"support:assigned":{let r=n.conversation_id,a=n.visitor_name,o=n.visitor_email;r&&this.emit("support:assigned",{conversationId:r,visitorName:a,visitorEmail:o});break}}}normalizeMessage(e){return{id:e.id??e.message_id??"",content:e.content??"",message_type:e.message_type??"text",sender_id:e.sender_id??e.sender_user_id??"",sender_type:e.sender_type,sender_agent_model:e.sender_agent_model,attachments:e.attachments,reactions:e.reactions,is_edited:!!(e.is_edited??!1),created_at:e.created_at??e.updated_at??e.timestamp??new Date().toISOString()}}buildEditedMessage(e,t){let i=t.message_id??t.id;if(!i)return null;let n=this.cache.getMessage(e,i);return{id:n?.id??i,content:t.content??t.new_content??n?.content??"",message_type:n?.message_type??"text",sender_id:n?.sender_id??"",sender_type:n?.sender_type,sender_agent_model:n?.sender_agent_model,attachments:n?.attachments,reactions:n?.reactions,is_edited:!0,created_at:n?.created_at??t.updated_at??t.timestamp??new Date().toISOString()}}applyReactionEvent(e,t){let i={id:`${t.message_id}:${t.user_id}:${t.emoji}`,message_id:t.message_id,user_id:t.user_id,emoji:t.emoji,action:t.action,timestamp:t.timestamp},n=this.cache.getMessage(e,t.message_id);if(!n)return i;let r=[...n.reactions??[]],a=r.findIndex(o=>o.emoji===t.emoji);if(t.action==="added")if(a>=0){let o=r[a];if(!o.user_ids.includes(t.user_id)){let l=[...o.user_ids,t.user_id];r[a]={...o,user_ids:l,count:l.length}}}else r.push({emoji:t.emoji,count:1,user_ids:[t.user_id]});else if(a>=0){let o=r[a],l=o.user_ids.filter(h=>h!==t.user_id);l.length===0?r.splice(a,1):r[a]={...o,user_ids:l,count:l.length}}return this.cache.updateMessage(e,{...n,reactions:r}),i}handleConversationMessage(e,t){let i=e.replace(/^conversation:(?:lr:|bc:|support:)?/,""),n=t;if(!n)return;let r=n.event??n.type,a=n.data??n;switch(r){case"new_message":{let o=this.normalizeMessage(a),l=this.cache.reconcileOptimisticMessage(i,o);this.cache.upsertMessage(i,l),this.emit("message",{message:l,conversationId:i});break}case"message_edited":{let o=a,l=this.buildEditedMessage(i,o);l&&(this.cache.upsertMessage(i,l),this.emit("message:updated",{message:l,conversationId:i,update:o}));break}case"reaction":{let o=a;if(!o.message_id||!o.user_id||!o.emoji)break;let l=this.applyReactionEvent(i,o);this.emit("reaction",{reaction:l,conversationId:i,action:o.action});break}case"message_deleted":{let o=a.message_id??a.id;o&&(this.cache.removeMessage(i,o),this.emit("message:deleted",{messageId:o,conversationId:i}));break}case"user_typing":{let o=a.user_id;o&&this.emit("typing",{userId:o,conversationId:i});break}case"user_stopped_typing":{let o=a.user_id;o&&this.emit("typing:stop",{userId:o,conversationId:i});break}case"typing_batch":{let o=a.users??[];for(let l of o)this.emit("typing",{userId:l,conversationId:i});break}case"messages_read":{let o=a.user_id,l=a.last_read_at;o&&l&&this.emit("read",{userId:o,conversationId:i,lastReadAt:l});break}case"room_upgraded":{if(a.new_type==="large_room"){this.conversationTypes.set(i,"large_room");let l=this.conversationSubs.get(i);l&&(l(),this.conversationSubs.delete(i),this.subscribeToConversation(i)),this.emit("room_upgraded",{conversationId:i,newType:"large_room"})}break}}}async flushOfflineQueue(){let e=this.offlineQueue.drain();for(let t of e)await this.sendMessage(t.conversationId,{content:t.content,message_type:t.message_type,attachments:t.attachments})}};var ne="sm_support_",C=class{constructor(s){this.chatClient=null;this.refreshToken=null;this.accessToken=null;this.tokenExpiresAt=0;this.userId=null;this.apiKey=s.apiKey,this.apiBaseUrl=s.apiBaseUrl??"https://api.scalemule.com",this.wsUrl=s.wsUrl,this.storageKey=ne+s.apiKey.substring(0,8);let e=this.loadState();e?(this.anonymousId=e.anonymous_id,this.refreshToken=e.refresh_token,this.userId=e.user_id):this.anonymousId=crypto.randomUUID()}async initVisitorSession(s){if(this.visitorName=s?.name,this.visitorEmail=s?.email,this.refreshToken)try{await this.refreshAccessToken(),this.initChatClient();return}catch{this.refreshToken=null}let e=await fetch(`${this.apiBaseUrl}/v1/auth/visitor-session`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({anonymous_id:this.anonymousId,name:s?.name,email:s?.email,page_url:typeof location<"u"?location.href:void 0})});if(!e.ok){let n=await e.text();throw new Error(`Visitor session failed: ${e.status} ${n}`)}let i=(await e.json()).data;this.accessToken=i.access_token,this.refreshToken=i.refresh_token,this.userId=i.user_id,this.tokenExpiresAt=Date.now()+i.expires_in*1e3,this.saveState(),this.initChatClient()}async startConversation(s,e){if(!this.accessToken)throw new Error("Call initVisitorSession() first");let t=await fetch(`${this.apiBaseUrl}/v1/chat/support/conversations`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey,Authorization:`Bearer ${this.accessToken}`},body:JSON.stringify({message:s,name:this.visitorName,email:this.visitorEmail,page_url:e?.page_url??(typeof location<"u"?location.href:void 0),user_agent:typeof navigator<"u"?navigator.userAgent:void 0,attachments:e?.attachments,metadata:e?.metadata})});if(!t.ok){let r=await t.text();throw new Error(`Create support conversation failed: ${t.status} ${r}`)}let n=(await t.json()).data;return this.chatClient&&this.chatClient.conversationTypes?.set(n.conversation_id,"support"),n}async getActiveConversation(){if(!this.accessToken)return null;let s=await fetch(`${this.apiBaseUrl}/v1/chat/support/conversations/mine`,{headers:{"x-api-key":this.apiKey,Authorization:`Bearer ${this.accessToken}`}});if(!s.ok)return null;let i=(await s.json()).data.find(n=>n.status==="active"||n.status==="waiting");return i&&this.chatClient&&this.chatClient.conversationTypes?.set(i.conversation_id,"support"),i??null}async getWidgetConfig(){let s=await fetch(`${this.apiBaseUrl}/v1/chat/support/widget/config`,{headers:{"x-api-key":this.apiKey}});if(!s.ok){let t=await s.text();throw new Error(`Get widget config failed: ${s.status} ${t}`)}return(await s.json()).data}get chat(){if(!this.chatClient)throw new Error("Call initVisitorSession() first");return this.chatClient}get isInitialized(){return this.chatClient!==null}connect(){this.chat.connect()}disconnect(){this.chat.disconnect()}get visitorUserId(){return this.userId}destroy(){this.chatClient?.destroy(),this.chatClient=null}initChatClient(){this.chatClient&&this.chatClient.destroy();let s={apiKey:this.apiKey,apiBaseUrl:this.apiBaseUrl,wsUrl:this.wsUrl,getToken:async()=>{if(!this.accessToken)return null;if(Date.now()>this.tokenExpiresAt-6e4)try{await this.refreshAccessToken()}catch{}return this.accessToken}};this.chatClient=new S(s)}async refreshAccessToken(){if(!this.refreshToken)throw new Error("No refresh token");let s=await fetch(`${this.apiBaseUrl}/v1/auth/token/refresh`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify({refresh_token:this.refreshToken})});if(!s.ok)throw new Error(`Token refresh failed: ${s.status}`);let t=(await s.json()).data;this.accessToken=t.access_token,this.tokenExpiresAt=Date.now()+t.expires_in*1e3}loadState(){try{let s=localStorage.getItem(this.storageKey);return s?JSON.parse(s):null}catch{return null}}saveState(){try{localStorage.setItem(this.storageKey,JSON.stringify({anonymous_id:this.anonymousId,refresh_token:this.refreshToken,user_id:this.userId}))}catch{}}};var P='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="28" height="28"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/></svg>',L='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><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>',U='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>',$='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>',E='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M16.5 6.5v9.25a4.75 4.75 0 1 1-9.5 0V5.75a3.25 3.25 0 1 1 6.5 0V14a1.75 1.75 0 1 1-3.5 0V6.5H8.5V14a3.25 3.25 0 1 0 6.5 0V5.75a4.75 4.75 0 1 0-9.5 0v10a6.25 6.25 0 1 0 12.5 0V6.5h-1.5Z"/></svg>',O='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12 22a10 10 0 1 1 10-10 10.01 10.01 0 0 1-10 10Zm0-18.5a8.5 8.5 0 1 0 8.5 8.5A8.51 8.51 0 0 0 12 3.5Zm-3 7a1.25 1.25 0 1 1 1.25-1.25A1.25 1.25 0 0 1 9 10.5Zm6 0a1.25 1.25 0 1 1 1.25-1.25A1.25 1.25 0 0 1 15 10.5Zm-3 6.25A5.22 5.22 0 0 1 7.58 14h1.71a3.5 3.5 0 0 0 5.42 0h1.71A5.22 5.22 0 0 1 12 16.75Z"/></svg>';var D=`
2
2
  :host {
3
3
  all: initial;
4
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
4
+ --sm-primary: #2563eb;
5
+ --sm-primary-hover: #1d4ed8;
6
+ --sm-primary-disabled: #93c5fd;
7
+ --sm-primary-text: #ffffff;
8
+ --sm-badge-bg: #ef4444;
9
+ --sm-bubble-left: auto;
10
+ --sm-bubble-right: 20px;
11
+ --sm-panel-left: auto;
12
+ --sm-panel-right: 20px;
13
+ --sm-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
14
+ font-family: var(--sm-font-family);
5
15
  font-size: 14px;
6
16
  line-height: 1.5;
7
- color: #1a1a1a;
17
+ color: #111827;
8
18
  }
9
19
 
10
20
  * {
@@ -13,69 +23,67 @@
13
23
  padding: 0;
14
24
  }
15
25
 
16
- /* ============ Bubble ============ */
17
-
18
26
  .sm-bubble {
19
27
  position: fixed;
20
28
  bottom: 20px;
21
- right: 20px;
29
+ left: var(--sm-bubble-left);
30
+ right: var(--sm-bubble-right);
22
31
  width: 60px;
23
32
  height: 60px;
24
- border-radius: 50%;
25
- background: #2563eb;
26
- color: #fff;
33
+ border-radius: 999px;
34
+ background: var(--sm-primary);
35
+ color: var(--sm-primary-text);
27
36
  border: none;
28
37
  cursor: pointer;
29
38
  display: flex;
30
39
  align-items: center;
31
40
  justify-content: center;
32
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
41
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.22);
33
42
  transition: transform 0.2s ease, box-shadow 0.2s ease;
34
43
  z-index: 999999;
35
44
  }
36
45
 
37
46
  .sm-bubble:hover {
38
- transform: scale(1.05);
39
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
47
+ transform: translateY(-1px) scale(1.03);
48
+ box-shadow: 0 18px 36px rgba(15, 23, 42, 0.28);
40
49
  }
41
50
 
42
51
  .sm-bubble .sm-badge {
43
52
  position: absolute;
44
53
  top: -4px;
45
54
  right: -4px;
46
- background: #ef4444;
47
- color: #fff;
48
- border-radius: 10px;
49
- font-size: 11px;
50
- font-weight: 600;
51
55
  min-width: 20px;
52
56
  height: 20px;
57
+ border-radius: 999px;
58
+ background: var(--sm-badge-bg);
59
+ color: #fff;
53
60
  display: flex;
54
61
  align-items: center;
55
62
  justify-content: center;
56
63
  padding: 0 6px;
64
+ font-size: 11px;
65
+ font-weight: 700;
57
66
  }
58
67
 
59
- /* ============ Panel ============ */
60
-
61
68
  .sm-panel {
62
69
  position: fixed;
63
70
  bottom: 90px;
64
- right: 20px;
71
+ left: var(--sm-panel-left);
72
+ right: var(--sm-panel-right);
65
73
  width: 380px;
66
- max-height: 560px;
67
- background: #fff;
68
- border-radius: 16px;
69
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
74
+ max-height: min(560px, calc(100vh - 110px));
75
+ background: #ffffff;
76
+ border-radius: 18px;
77
+ box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);
70
78
  display: flex;
71
79
  flex-direction: column;
72
80
  overflow: hidden;
73
81
  z-index: 999998;
74
- animation: sm-slide-up 0.25s ease;
82
+ animation: sm-slide-up 0.22s ease;
75
83
  }
76
84
 
77
85
  @keyframes sm-slide-up {
78
- from { opacity: 0; transform: translateY(16px); }
86
+ from { opacity: 0; transform: translateY(14px); }
79
87
  to { opacity: 1; transform: translateY(0); }
80
88
  }
81
89
 
@@ -83,64 +91,106 @@
83
91
  display: none;
84
92
  }
85
93
 
86
- /* ============ Header ============ */
87
-
88
94
  .sm-header {
89
- background: #2563eb;
90
- color: #fff;
91
- padding: 16px 20px;
95
+ background: var(--sm-primary);
96
+ color: var(--sm-primary-text);
97
+ padding: 16px 18px;
92
98
  display: flex;
93
- align-items: center;
99
+ align-items: flex-start;
94
100
  justify-content: space-between;
101
+ gap: 16px;
102
+ }
103
+
104
+ .sm-header-copy {
105
+ min-width: 0;
95
106
  }
96
107
 
97
108
  .sm-header-title {
98
109
  font-size: 16px;
99
- font-weight: 600;
110
+ font-weight: 700;
111
+ }
112
+
113
+ .sm-header-status {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 6px;
117
+ margin-top: 4px;
118
+ font-size: 12px;
119
+ opacity: 0.92;
120
+ }
121
+
122
+ .sm-status-dot {
123
+ width: 10px;
124
+ height: 10px;
125
+ border-radius: 999px;
126
+ background: rgba(255, 255, 255, 0.65);
127
+ }
128
+
129
+ .sm-status-dot-online {
130
+ background: #22c55e;
131
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
132
+ }
133
+
134
+ .sm-status-dot-away {
135
+ background: #cbd5e1;
100
136
  }
101
137
 
102
138
  .sm-header-subtitle {
103
139
  font-size: 12px;
104
- opacity: 0.8;
105
- margin-top: 2px;
140
+ opacity: 0.82;
141
+ margin-top: 6px;
142
+ max-width: 250px;
106
143
  }
107
144
 
108
145
  .sm-header-actions {
109
146
  display: flex;
110
- gap: 8px;
147
+ gap: 6px;
111
148
  }
112
149
 
113
150
  .sm-header-btn {
114
151
  background: none;
115
152
  border: none;
116
- color: #fff;
153
+ color: inherit;
117
154
  cursor: pointer;
118
- opacity: 0.8;
119
- padding: 4px;
120
- border-radius: 4px;
155
+ border-radius: 10px;
156
+ padding: 6px;
121
157
  display: flex;
122
158
  align-items: center;
159
+ justify-content: center;
160
+ opacity: 0.86;
123
161
  }
124
162
 
125
163
  .sm-header-btn:hover {
126
164
  opacity: 1;
127
- background: rgba(255, 255, 255, 0.1);
165
+ background: rgba(255, 255, 255, 0.12);
128
166
  }
129
167
 
130
- /* ============ Pre-chat form ============ */
168
+ .sm-error {
169
+ padding: 10px 14px;
170
+ background: #fef2f2;
171
+ border-bottom: 1px solid #fecaca;
172
+ color: #b91c1c;
173
+ font-size: 12px;
174
+ }
131
175
 
132
- .sm-prechat {
133
- padding: 24px 20px;
176
+ .sm-body {
134
177
  flex: 1;
178
+ min-height: 0;
135
179
  display: flex;
136
180
  flex-direction: column;
137
- gap: 16px;
181
+ }
182
+
183
+ .sm-prechat {
184
+ padding: 20px 18px 18px;
185
+ display: flex;
186
+ flex-direction: column;
187
+ gap: 14px;
138
188
  }
139
189
 
140
190
  .sm-prechat-title {
141
191
  font-size: 18px;
142
- font-weight: 600;
143
- color: #1a1a1a;
192
+ font-weight: 700;
193
+ color: #111827;
144
194
  }
145
195
 
146
196
  .sm-prechat-desc {
@@ -151,194 +201,414 @@
151
201
  .sm-field {
152
202
  display: flex;
153
203
  flex-direction: column;
154
- gap: 4px;
204
+ gap: 6px;
155
205
  }
156
206
 
157
207
  .sm-field label {
158
208
  font-size: 13px;
159
- font-weight: 500;
209
+ font-weight: 600;
160
210
  color: #374151;
161
211
  }
162
212
 
163
213
  .sm-field input,
164
- .sm-field textarea {
214
+ .sm-field textarea,
215
+ .sm-input {
165
216
  border: 1px solid #d1d5db;
166
- border-radius: 8px;
217
+ border-radius: 14px;
167
218
  padding: 10px 12px;
168
219
  font-size: 14px;
169
220
  font-family: inherit;
221
+ color: #111827;
170
222
  outline: none;
171
- transition: border-color 0.15s;
223
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
224
+ background: #fff;
172
225
  }
173
226
 
174
227
  .sm-field input:focus,
175
- .sm-field textarea:focus {
176
- border-color: #2563eb;
177
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
228
+ .sm-field textarea:focus,
229
+ .sm-input:focus {
230
+ border-color: var(--sm-primary);
231
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
178
232
  }
179
233
 
180
234
  .sm-field textarea {
181
235
  resize: vertical;
182
- min-height: 80px;
236
+ min-height: 92px;
183
237
  }
184
238
 
185
- .sm-submit-btn {
186
- background: #2563eb;
187
- color: #fff;
239
+ .sm-prechat-actions {
240
+ display: flex;
241
+ gap: 10px;
242
+ align-items: center;
243
+ }
244
+
245
+ .sm-submit-btn,
246
+ .sm-send-btn {
188
247
  border: none;
189
- border-radius: 8px;
190
- padding: 12px;
191
- font-size: 14px;
192
- font-weight: 600;
193
248
  cursor: pointer;
194
- transition: background 0.15s;
249
+ background: var(--sm-primary);
250
+ color: var(--sm-primary-text);
251
+ transition: background 0.15s ease, opacity 0.15s ease;
195
252
  }
196
253
 
197
- .sm-submit-btn:hover {
198
- background: #1d4ed8;
254
+ .sm-submit-btn:hover,
255
+ .sm-send-btn:hover {
256
+ background: var(--sm-primary-hover);
199
257
  }
200
258
 
201
- .sm-submit-btn:disabled {
202
- background: #93c5fd;
259
+ .sm-submit-btn:disabled,
260
+ .sm-send-btn:disabled {
261
+ background: var(--sm-primary-disabled);
203
262
  cursor: not-allowed;
204
263
  }
205
264
 
206
- /* ============ Messages ============ */
265
+ .sm-submit-btn {
266
+ flex: 1;
267
+ border-radius: 14px;
268
+ padding: 12px;
269
+ font-size: 14px;
270
+ font-weight: 700;
271
+ }
272
+
273
+ .sm-chat-shell {
274
+ flex: 1;
275
+ min-height: 0;
276
+ display: flex;
277
+ flex-direction: column;
278
+ background: #f8fafc;
279
+ }
280
+
281
+ .sm-chat-shell.sm-dragging {
282
+ background: rgba(37, 99, 235, 0.05);
283
+ }
207
284
 
208
285
  .sm-messages {
209
286
  flex: 1;
287
+ min-height: 0;
210
288
  overflow-y: auto;
211
- padding: 16px 20px;
289
+ padding: 16px;
212
290
  display: flex;
213
291
  flex-direction: column;
214
292
  gap: 12px;
215
- min-height: 200px;
216
- max-height: 360px;
293
+ }
294
+
295
+ .sm-date-divider {
296
+ align-self: center;
297
+ font-size: 11px;
298
+ font-weight: 600;
299
+ color: #6b7280;
300
+ background: rgba(148, 163, 184, 0.16);
301
+ padding: 4px 10px;
302
+ border-radius: 999px;
303
+ }
304
+
305
+ .sm-unread-divider {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 12px;
309
+ font-size: 11px;
310
+ font-weight: 700;
311
+ color: var(--sm-primary);
312
+ text-transform: uppercase;
313
+ letter-spacing: 0.04em;
314
+ }
315
+
316
+ .sm-unread-divider::before,
317
+ .sm-unread-divider::after {
318
+ content: '';
319
+ flex: 1;
320
+ height: 1px;
321
+ background: rgba(37, 99, 235, 0.24);
217
322
  }
218
323
 
219
324
  .sm-msg {
220
325
  display: flex;
221
326
  flex-direction: column;
222
- max-width: 80%;
327
+ gap: 6px;
328
+ max-width: 84%;
223
329
  }
224
330
 
225
- .sm-msg.sm-msg-visitor {
331
+ .sm-msg-visitor {
226
332
  align-self: flex-end;
227
333
  }
228
334
 
229
- .sm-msg.sm-msg-rep {
335
+ .sm-msg-rep {
230
336
  align-self: flex-start;
231
337
  }
232
338
 
233
- .sm-msg.sm-msg-system {
339
+ .sm-msg-system {
234
340
  align-self: center;
235
- max-width: 90%;
341
+ max-width: 92%;
342
+ }
343
+
344
+ .sm-msg-row {
345
+ display: flex;
346
+ align-items: flex-end;
347
+ gap: 8px;
236
348
  }
237
349
 
238
350
  .sm-msg-bubble {
239
- padding: 10px 14px;
240
- border-radius: 12px;
241
- font-size: 14px;
351
+ padding: 10px 12px;
352
+ border-radius: 16px;
242
353
  line-height: 1.4;
243
354
  word-break: break-word;
355
+ background: #f3f4f6;
356
+ color: #111827;
357
+ }
358
+
359
+ .sm-msg-content {
360
+ white-space: pre-wrap;
244
361
  }
245
362
 
246
363
  .sm-msg-visitor .sm-msg-bubble {
247
- background: #2563eb;
248
- color: #fff;
249
- border-bottom-right-radius: 4px;
364
+ background: var(--sm-primary);
365
+ color: var(--sm-primary-text);
366
+ border-bottom-right-radius: 6px;
250
367
  }
251
368
 
252
369
  .sm-msg-rep .sm-msg-bubble {
253
- background: #f3f4f6;
254
- color: #1a1a1a;
255
- border-bottom-left-radius: 4px;
370
+ border-bottom-left-radius: 6px;
256
371
  }
257
372
 
258
373
  .sm-msg-system .sm-msg-bubble {
259
374
  background: transparent;
260
- color: #9ca3af;
375
+ color: #94a3b8;
261
376
  font-size: 12px;
262
377
  text-align: center;
263
- padding: 4px 8px;
378
+ padding: 2px 8px;
379
+ }
380
+
381
+ .sm-msg-action {
382
+ border: 1px solid #e5e7eb;
383
+ background: #ffffff;
384
+ color: #6b7280;
385
+ border-radius: 999px;
386
+ width: 28px;
387
+ height: 28px;
388
+ display: inline-flex;
389
+ align-items: center;
390
+ justify-content: center;
391
+ cursor: pointer;
392
+ flex-shrink: 0;
393
+ }
394
+
395
+ .sm-msg-action:hover {
396
+ color: var(--sm-primary);
397
+ border-color: rgba(37, 99, 235, 0.24);
264
398
  }
265
399
 
266
400
  .sm-msg-time {
267
401
  font-size: 11px;
268
- color: #9ca3af;
269
- margin-top: 4px;
402
+ color: #94a3b8;
270
403
  }
271
404
 
272
405
  .sm-msg-visitor .sm-msg-time {
273
406
  text-align: right;
274
407
  }
275
408
 
409
+ .sm-reactions {
410
+ display: flex;
411
+ flex-wrap: wrap;
412
+ gap: 6px;
413
+ }
414
+
415
+ .sm-reaction-badge {
416
+ border: 1px solid #e5e7eb;
417
+ background: #ffffff;
418
+ color: #374151;
419
+ border-radius: 999px;
420
+ padding: 4px 8px;
421
+ font-size: 12px;
422
+ cursor: pointer;
423
+ }
424
+
425
+ .sm-reaction-badge-active {
426
+ border-color: rgba(37, 99, 235, 0.32);
427
+ background: rgba(37, 99, 235, 0.08);
428
+ color: var(--sm-primary);
429
+ }
430
+
431
+ .sm-reaction-picker {
432
+ display: inline-flex;
433
+ gap: 6px;
434
+ padding: 6px;
435
+ border-radius: 999px;
436
+ border: 1px solid #e5e7eb;
437
+ background: #ffffff;
438
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
439
+ }
440
+
441
+ .sm-reaction-picker-btn {
442
+ border: none;
443
+ background: transparent;
444
+ cursor: pointer;
445
+ border-radius: 999px;
446
+ width: 30px;
447
+ height: 30px;
448
+ display: inline-flex;
449
+ align-items: center;
450
+ justify-content: center;
451
+ font-size: 17px;
452
+ }
453
+
454
+ .sm-attachment {
455
+ display: block;
456
+ margin-top: 8px;
457
+ border-radius: 12px;
458
+ max-width: 100%;
459
+ }
460
+
461
+ .sm-attachment-image,
462
+ .sm-attachment-video {
463
+ max-width: 260px;
464
+ }
465
+
466
+ .sm-attachment-audio {
467
+ width: 100%;
468
+ }
469
+
470
+ .sm-attachment-link {
471
+ color: inherit;
472
+ text-decoration: underline;
473
+ font-size: 13px;
474
+ }
475
+
276
476
  .sm-typing {
477
+ min-height: 22px;
478
+ padding: 0 16px 10px;
479
+ font-size: 12px;
480
+ color: #6b7280;
481
+ }
482
+
483
+ .sm-typing-indicator {
484
+ display: inline-flex;
485
+ align-items: center;
486
+ gap: 6px;
487
+ }
488
+
489
+ .sm-typing-indicator span:nth-child(-n+3) {
490
+ width: 6px;
491
+ height: 6px;
492
+ border-radius: 999px;
493
+ background: #94a3b8;
494
+ animation: sm-bounce 1.2s infinite ease-in-out;
495
+ }
496
+
497
+ .sm-typing-indicator span:nth-child(2) {
498
+ animation-delay: 0.15s;
499
+ }
500
+
501
+ .sm-typing-indicator span:nth-child(3) {
502
+ animation-delay: 0.3s;
503
+ }
504
+
505
+ @keyframes sm-bounce {
506
+ 0%, 80%, 100% { transform: scale(0.85); opacity: 0.5; }
507
+ 40% { transform: scale(1); opacity: 1; }
508
+ }
509
+
510
+ .sm-upload-list {
511
+ display: flex;
512
+ flex-wrap: wrap;
513
+ gap: 8px;
514
+ padding: 0 16px 10px;
515
+ }
516
+
517
+ .sm-upload-chip {
518
+ display: inline-flex;
519
+ align-items: center;
520
+ gap: 8px;
521
+ max-width: 100%;
522
+ border-radius: 999px;
523
+ border: 1px solid #e5e7eb;
524
+ background: #ffffff;
525
+ padding: 6px 10px;
277
526
  font-size: 12px;
278
- color: #9ca3af;
279
- padding: 0 4px;
280
- min-height: 18px;
527
+ color: #374151;
528
+ }
529
+
530
+ .sm-upload-chip-error {
531
+ border-color: #fecaca;
532
+ background: #fef2f2;
533
+ color: #b91c1c;
534
+ }
535
+
536
+ .sm-upload-name {
537
+ max-width: 140px;
538
+ overflow: hidden;
539
+ text-overflow: ellipsis;
540
+ white-space: nowrap;
281
541
  }
282
542
 
283
- /* ============ Input ============ */
543
+ .sm-upload-progress {
544
+ color: #6b7280;
545
+ }
546
+
547
+ .sm-upload-chip-error .sm-upload-progress {
548
+ color: inherit;
549
+ }
550
+
551
+ .sm-upload-remove {
552
+ border: none;
553
+ background: transparent;
554
+ color: inherit;
555
+ cursor: pointer;
556
+ font-size: 14px;
557
+ line-height: 1;
558
+ }
284
559
 
285
560
  .sm-input-area {
286
- border-top: 1px solid #e5e7eb;
287
- padding: 12px 16px;
288
561
  display: flex;
289
562
  align-items: flex-end;
290
- gap: 8px;
563
+ gap: 10px;
564
+ padding: 12px 16px 16px;
565
+ border-top: 1px solid #e5e7eb;
566
+ background: #ffffff;
291
567
  }
292
568
 
293
569
  .sm-input {
294
570
  flex: 1;
295
- border: 1px solid #d1d5db;
296
- border-radius: 20px;
297
- padding: 10px 16px;
298
- font-size: 14px;
299
- font-family: inherit;
300
- outline: none;
301
- resize: none;
571
+ min-height: 44px;
302
572
  max-height: 100px;
573
+ resize: none;
303
574
  overflow-y: auto;
304
575
  line-height: 1.4;
305
576
  }
306
577
 
307
- .sm-input:focus {
308
- border-color: #2563eb;
309
- }
310
-
311
- .sm-send-btn {
312
- background: #2563eb;
313
- color: #fff;
314
- border: none;
315
- border-radius: 50%;
316
- width: 36px;
317
- height: 36px;
318
- display: flex;
578
+ .sm-attach-btn {
579
+ width: 42px;
580
+ height: 42px;
581
+ border-radius: 14px;
582
+ border: 1px solid #d1d5db;
583
+ background: #ffffff;
584
+ color: #374151;
585
+ cursor: pointer;
586
+ display: inline-flex;
319
587
  align-items: center;
320
588
  justify-content: center;
321
- cursor: pointer;
322
589
  flex-shrink: 0;
323
- transition: background 0.15s;
324
590
  }
325
591
 
326
- .sm-send-btn:hover {
327
- background: #1d4ed8;
592
+ .sm-attach-btn:hover {
593
+ border-color: rgba(37, 99, 235, 0.28);
594
+ color: var(--sm-primary);
328
595
  }
329
596
 
330
- .sm-send-btn:disabled {
331
- background: #93c5fd;
332
- cursor: not-allowed;
597
+ .sm-send-btn {
598
+ width: 42px;
599
+ height: 42px;
600
+ border-radius: 14px;
601
+ display: inline-flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ flex-shrink: 0;
333
605
  }
334
606
 
335
- /* ============ Footer ============ */
336
-
337
607
  .sm-footer {
338
608
  text-align: center;
339
609
  padding: 8px;
340
610
  font-size: 11px;
341
- color: #9ca3af;
611
+ color: #94a3b8;
342
612
  }
343
613
 
344
614
  .sm-footer a {
@@ -350,54 +620,112 @@
350
620
  text-decoration: underline;
351
621
  }
352
622
 
353
- /* ============ Responsive ============ */
354
-
355
623
  @media (max-width: 440px) {
356
624
  .sm-panel {
357
- bottom: 0;
358
- right: 0;
359
625
  left: 0;
626
+ right: 0;
627
+ bottom: 0;
360
628
  width: 100%;
361
629
  max-height: 100vh;
362
- border-radius: 16px 16px 0 0;
630
+ border-radius: 18px 18px 0 0;
363
631
  }
364
632
  }
365
- `;var x='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="28" height="28"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/></svg>',C='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><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>',_='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>',T='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>';var U="sm_widget_";function S(c,s){try{localStorage.setItem(U+c,s)}catch{}}var f=class{constructor(s,e){this.conversation=null;this.panelEl=null;this.messagesEl=null;this.typingEl=null;this.isOpen=!1;this.unreadCount=0;this.eventCleanups=[];this.eventsSubscribed=!1;this.client=new v({apiKey:s,apiBaseUrl:e}),this.root=document.createElement("div"),this.root.id="scalemule-support-widget",this.shadow=this.root.attachShadow({mode:"closed"});let t=document.createElement("style");t.textContent=w,this.shadow.appendChild(t),this.renderBubble(),document.body.appendChild(this.root)}renderBubble(){let s=document.createElement("button");s.className="sm-bubble",s.innerHTML=x,s.setAttribute("aria-label","Open support chat"),s.addEventListener("click",()=>this.toggle()),this.shadow.appendChild(s)}renderPanel(){if(this.panelEl)return;let s=document.createElement("div");s.className="sm-panel",s.innerHTML=`
633
+ `;var re="sm_widget_";function N(c,s){try{localStorage.setItem(re+c,s)}catch{}}var oe=["\u{1F44D}","\u2764\uFE0F","\u{1F602}","\u{1F389}","\u{1F62E}","\u{1F440}"],m={title:"Support",subtitle:"We typically reply within a few minutes",primary_color:"#2563eb",position:"right",pre_chat_fields:[{key:"name",label:"Name",type:"text",required:!0},{key:"email",label:"Email",type:"email",required:!1}],business_hours:{},realtime_enabled:!1,welcome_message:"Hi! How can we help?",offline_message:"We're currently offline. Leave a message!",reps_online:!1,online_count:0};function d(c){return String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function T(c=""){return{conversationId:c,messages:[],readStatuses:[],typingUsers:[],members:[],hasMore:!1,isLoading:!1,error:null}}function le(c){try{return new Date(c).toLocaleTimeString(void 0,{hour:"numeric",minute:"2-digit"})}catch{return""}}function ce(c){try{return new Date(c).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}catch{return""}}function de(c,s){let e=new Date(c),t=new Date(s);return e.getFullYear()===t.getFullYear()&&e.getMonth()===t.getMonth()&&e.getDate()===t.getDate()}function F(c,s){let e=c.replace("#","");if(!/^[0-9a-fA-F]{6}$/.test(e))return c;let t=Number.parseInt(e,16),i=t>>16&255,n=t>>8&255,r=t&255,a=o=>Math.max(0,Math.min(255,Math.round(o+s*255))).toString(16).padStart(2,"0");return`#${a(i)}${a(n)}${a(r)}`}function H(c,s){return s.length?!c&&s.every(e=>e.mime_type.startsWith("image/"))?"image":"file":"text"}function he(c,s){return s?c.find(e=>e.user_id===s)?.last_read_at??null:null}function B(c,s,e){return{id:`pending-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,sender_id:c,sender_type:"human",content:s,message_type:H(s,e),attachments:e,reactions:[],is_edited:!1,created_at:new Date().toISOString()}}function pe(c,s){let e=d(s.file_name),t=d(s.presigned_url),i=d(s.file_id);return s.presigned_url?s.mime_type.startsWith("image/")?`<img class="sm-attachment sm-attachment-image" src="${t}" alt="${e}" loading="lazy" />`:s.mime_type.startsWith("video/")?`<video class="sm-attachment sm-attachment-video" src="${t}" controls preload="metadata"></video>`:s.mime_type.startsWith("audio/")?`<audio class="sm-attachment sm-attachment-audio" src="${t}" controls preload="metadata"></audio>`:`<a class="sm-attachment sm-attachment-link" href="${t}" target="_blank" rel="noreferrer">${e}</a>`:`<div class="sm-attachment sm-attachment-link" data-file-id="${i}" data-message-id="${d(c)}">${e}</div>`}var k=class{constructor(s,e,t={}){this.config=m;this.configLoaded=!1;this.conversation=null;this.controller=null;this.runtimeCleanups=[];this.runtimeState=T();this.panelEl=null;this.bodyEl=null;this.messagesEl=null;this.typingEl=null;this.uploadListEl=null;this.inputEl=null;this.fileInputEl=null;this.chatShellEl=null;this.errorBannerEl=null;this.isOpen=!1;this.unreadCount=0;this.unreadMarkerTimestamp=null;this.shouldScrollToUnread=!1;this.fallbackNotice=null;this.activeErrorMessage=null;this.realtimeFallbackActive=!1;this.pendingAttachments=[];this.openReactionMessageId=null;this.pollInterval=null;this.typingStopTimer=null;this.realtimeStatusCleanups=[];this.client=new C({apiKey:s,apiBaseUrl:e}),this.options=t,this.root=document.createElement("div"),this.root.id="scalemule-support-widget",this.shadow=this.root.attachShadow({mode:"closed"}),this.applyConfigTheme();let i=document.createElement("style");i.textContent=D,this.shadow.appendChild(i),this.renderBubble(),document.body.appendChild(this.root)}applyConfigTheme(){let s=this.options.color??this.config.primary_color??m.primary_color,e=this.options.position??this.config.position??m.position,t=F(s,-.12),i=F(s,.22);this.root.style.setProperty("--sm-primary",s),this.root.style.setProperty("--sm-primary-hover",t),this.root.style.setProperty("--sm-primary-disabled",i),this.root.style.setProperty("--sm-position",e),this.root.style.setProperty("--sm-bubble-left",e==="left"?"20px":"auto"),this.root.style.setProperty("--sm-bubble-right",e==="right"?"20px":"auto"),this.root.style.setProperty("--sm-panel-left",e==="left"?"20px":"auto"),this.root.style.setProperty("--sm-panel-right",e==="right"?"20px":"auto")}async ensureConfigLoaded(){if(!this.configLoaded){try{let s=await this.client.getWidgetConfig();this.config={...m,...s,primary_color:this.options.color??s.primary_color??m.primary_color,position:this.options.position??s.position??m.position}}catch{this.config={...m,primary_color:this.options.color??m.primary_color,position:this.options.position??m.position}}this.configLoaded=!0,this.applyConfigTheme()}}renderBubble(){let s=document.createElement("button");s.className="sm-bubble",s.innerHTML=P,s.setAttribute("aria-label","Open support chat"),s.addEventListener("click",()=>{this.toggle()}),this.shadow.appendChild(s)}renderPanel(){if(this.panelEl)return;let s=document.createElement("div");s.className="sm-panel sm-hidden",s.innerHTML=`
366
634
  <div class="sm-header">
367
- <div>
368
- <div class="sm-header-title">Support</div>
369
- <div class="sm-header-subtitle">We typically reply within a few minutes</div>
635
+ <div class="sm-header-copy">
636
+ <div class="sm-header-title"></div>
637
+ <div class="sm-header-status">
638
+ <span class="sm-status-dot"></span>
639
+ <span class="sm-status-label"></span>
640
+ </div>
641
+ <div class="sm-header-subtitle"></div>
370
642
  </div>
371
643
  <div class="sm-header-actions">
372
- <button class="sm-header-btn sm-minimize-btn" aria-label="Minimize">${T}</button>
373
- <button class="sm-header-btn sm-close-btn" aria-label="Close">${C}</button>
644
+ <button class="sm-header-btn sm-minimize-btn" aria-label="Minimize">${$}</button>
645
+ <button class="sm-header-btn sm-close-btn" aria-label="Close">${L}</button>
374
646
  </div>
375
647
  </div>
648
+ <div class="sm-error" hidden></div>
376
649
  <div class="sm-body"></div>
377
650
  <div class="sm-footer">Powered by <a href="https://scalemule.com" target="_blank" rel="noopener">ScaleMule</a></div>
378
- `,s.querySelector(".sm-minimize-btn").addEventListener("click",()=>this.minimize()),s.querySelector(".sm-close-btn").addEventListener("click",()=>this.minimize()),this.panelEl=s,this.shadow.appendChild(s)}renderPreChatForm(){let s=this.panelEl.querySelector(".sm-body");s.innerHTML=`
651
+ `,s.querySelector(".sm-minimize-btn").addEventListener("click",()=>this.minimize()),s.querySelector(".sm-close-btn").addEventListener("click",()=>this.minimize()),this.panelEl=s,this.bodyEl=s.querySelector(".sm-body"),this.errorBannerEl=s.querySelector(".sm-error"),this.shadow.appendChild(s),this.updateHeader()}updateHeader(){if(!this.panelEl)return;let s=this.panelEl.querySelector(".sm-header-title"),e=this.panelEl.querySelector(".sm-header-subtitle"),t=this.panelEl.querySelector(".sm-status-dot"),i=this.panelEl.querySelector(".sm-status-label");if(!s||!e||!t||!i)return;let n=this.client.visitorUserId,a=this.runtimeState.members.filter(h=>h.userId!==n).map(h=>h.status),o=this.config.reps_online,l=this.config.reps_online?"Online":"Away";a.length&&(a.some(h=>h==="online")?(o=!0,l="Online"):(a.some(h=>h==="away"||h==="dnd"),o=!1,l="Away")),s.textContent=this.config.title,e.textContent=o?this.config.subtitle:this.config.offline_message,i.textContent=o?l:"We'll respond soon",t.className=`sm-status-dot ${o?"sm-status-dot-online":"sm-status-dot-away"}`}renderError(s){this.activeErrorMessage=s,this.syncErrorBanner()}setFallbackNotice(s){this.fallbackNotice=s,this.syncErrorBanner()}syncErrorBanner(){if(!this.errorBannerEl)return;let s=this.activeErrorMessage??this.fallbackNotice;if(!s){this.errorBannerEl.hidden=!0,this.errorBannerEl.textContent="";return}this.errorBannerEl.hidden=!1,this.errorBannerEl.textContent=s}getConfiguredPreChatFields(){return this.config.pre_chat_fields?.length?this.config.pre_chat_fields.filter(s=>s.key!=="message"):m.pre_chat_fields}renderPreChatForm(){if(!this.bodyEl)return;let s=this.getConfiguredPreChatFields().map(e=>{let t=`sm-prechat-${e.key}`,i=e.required?"required":"",n=`${d(e.label)}${e.required?" *":""}`,r=`id="${t}" data-prechat-key="${d(e.key)}" placeholder="${d(e.label)}" ${i}`,a=e.type==="textarea"?`<textarea ${r}></textarea>`:`<input type="${d(e.type||"text")}" ${r} />`;return`
652
+ <div class="sm-field">
653
+ <label for="${t}">${n}</label>
654
+ ${a}
655
+ </div>
656
+ `}).join("");this.bodyEl.dataset.view="prechat",this.bodyEl.innerHTML=`
379
657
  <div class="sm-prechat">
380
658
  <div class="sm-prechat-title">Start a conversation</div>
381
- <div class="sm-prechat-desc">We're here to help. Fill in your details and we'll get back to you.</div>
382
- <div class="sm-field">
383
- <label for="sm-name">Name *</label>
384
- <input type="text" id="sm-name" placeholder="Your name" required />
385
- </div>
659
+ <div class="sm-prechat-desc">${d(this.config.reps_online?this.config.welcome_message:this.config.offline_message)}</div>
660
+ ${s}
386
661
  <div class="sm-field">
387
- <label for="sm-email">Email</label>
388
- <input type="email" id="sm-email" placeholder="you@example.com" />
662
+ <label for="sm-prechat-message">Message *</label>
663
+ <textarea id="sm-prechat-message" placeholder="How can we help?"></textarea>
389
664
  </div>
390
- <div class="sm-field">
391
- <label for="sm-message">Message *</label>
392
- <textarea id="sm-message" placeholder="How can we help?"></textarea>
665
+ <div class="sm-upload-list" id="sm-prechat-uploads"></div>
666
+ <div class="sm-prechat-actions">
667
+ <input type="file" id="sm-prechat-file-input" hidden multiple accept="image/*,video/*,audio/*" />
668
+ <button class="sm-attach-btn sm-prechat-attach" type="button" aria-label="Attach files">${E}</button>
669
+ <button class="sm-submit-btn" id="sm-start-btn">Start Chat</button>
393
670
  </div>
394
- <button class="sm-submit-btn" id="sm-start-btn">Start Chat</button>
395
- </div>
396
- `,s.querySelector("#sm-start-btn").addEventListener("click",()=>this.handleStartChat())}renderChatView(){let s=this.panelEl.querySelector(".sm-body");s.innerHTML=`
397
- <div class="sm-messages" id="sm-messages"></div>
398
- <div class="sm-typing" id="sm-typing"></div>
399
- <div class="sm-input-area">
400
- <textarea class="sm-input" id="sm-input" placeholder="Type a message..." rows="1"></textarea>
401
- <button class="sm-send-btn" id="sm-send-btn">${_}</button>
402
671
  </div>
403
- `,this.messagesEl=s.querySelector("#sm-messages"),this.typingEl=s.querySelector("#sm-typing");let e=s.querySelector("#sm-input");s.querySelector("#sm-send-btn").addEventListener("click",()=>this.handleSendMessage(e)),e.addEventListener("keydown",n=>{n.key==="Enter"&&!n.shiftKey&&(n.preventDefault(),this.handleSendMessage(e))}),e.addEventListener("input",()=>{e.style.height="auto",e.style.height=Math.min(e.scrollHeight,100)+"px"})}async toggle(){if(this.isOpen){this.minimize();return}this.isOpen=!0,this.unreadCount=0,this.updateBadge(),this.renderPanel(),this.panelEl&&this.panelEl.classList.remove("sm-hidden");try{await this.client.initVisitorSession()}catch{}let s=await this.client.getActiveConversation();s?(this.conversation=s,this.renderChatView(),await this.loadMessages(),this.subscribeToEvents()):this.renderPreChatForm()}minimize(){this.isOpen=!1,this.panelEl&&this.panelEl.classList.add("sm-hidden")}async handleStartChat(){let s=this.shadow.querySelector("#sm-name"),e=this.shadow.querySelector("#sm-email"),t=this.shadow.querySelector("#sm-message"),n=this.shadow.querySelector("#sm-start-btn"),i=s.value.trim(),a=t.value.trim();if(!(!i||!a)){n.disabled=!0,n.textContent="Connecting...";try{await this.client.initVisitorSession({name:i,email:e.value.trim()||void 0}),this.conversation=await this.client.startConversation(a,{page_url:location.href}),this.renderChatView(),this.appendMessage({id:"",sender_id:this.client.visitorUserId??"",sender_type:"human",content:a,message_type:"text",is_edited:!1,created_at:new Date().toISOString()}),this.subscribeToEvents(),S("conversation_id",this.conversation.conversation_id)}catch{n.disabled=!1,n.textContent="Start Chat"}}}async handleSendMessage(s){let e=s.value.trim();if(!(!e||!this.conversation)){s.value="",s.style.height="auto",this.appendMessage({id:"pending-"+Date.now(),sender_id:this.client.visitorUserId??"",sender_type:"human",content:e,message_type:"text",is_edited:!1,created_at:new Date().toISOString()});try{await this.client.chat.sendMessage(this.conversation.conversation_id,{content:e})}catch{}}}subscribeToEvents(){if(!this.conversation||this.eventsSubscribed)return;this.eventsSubscribed=!0;let s=this.client.chat,e=this.conversation.conversation_id;s.connect(),s.subscribeToConversation(e);let t=s.on("message",({message:a,conversationId:o})=>{o===e&&a.sender_id!==this.client.visitorUserId&&(this.appendMessage(a),this.isOpen||(this.unreadCount++,this.updateBadge()))}),n=s.on("typing",({conversationId:a})=>{a!==e||!this.typingEl||(this.typingEl.textContent="Support is typing...",setTimeout(()=>{this.typingEl&&(this.typingEl.textContent="")},3e3))}),i=s.on("typing:stop",({conversationId:a})=>{a!==e||!this.typingEl||(this.typingEl.textContent="")});this.eventCleanups.push(t,n,i)}appendMessage(s){if(!this.messagesEl)return;let e=s.sender_id===this.client.visitorUserId,t=s.sender_type==="system"||s.message_type==="system",n=document.createElement("div");n.className=`sm-msg ${t?"sm-msg-system":e?"sm-msg-visitor":"sm-msg-rep"}`;let i=document.createElement("div");if(i.className="sm-msg-bubble",i.textContent=s.content,n.appendChild(i),!t){let a=document.createElement("div");a.className="sm-msg-time",a.textContent=this.formatTime(s.created_at),n.appendChild(a)}this.messagesEl.appendChild(n),this.messagesEl.scrollTop=this.messagesEl.scrollHeight}async loadMessages(){if(!(!this.conversation||!this.messagesEl))try{let s=await this.client.chat.getMessages(this.conversation.conversation_id,{limit:50});if(s.data){this.messagesEl.innerHTML="";for(let e of s.data.messages)this.appendMessage(e)}}catch{}}updateBadge(){let s=this.shadow.querySelector(".sm-bubble");if(!s)return;let e=s.querySelector(".sm-badge");this.unreadCount>0?(e||(e=document.createElement("span"),e.className="sm-badge",s.appendChild(e)),e.textContent=String(this.unreadCount)):e&&e.remove()}formatTime(s){try{return new Date(s).toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})}catch{return""}}};(function(){let s=document.querySelectorAll("script[data-api-key]"),e=s[s.length-1];if(!e){console.warn("[ScaleMule] Support widget: missing data-api-key on script tag");return}let t=e.getAttribute("data-api-key");if(!t){console.warn("[ScaleMule] Support widget: data-api-key is empty");return}let n=e.getAttribute("data-api-url")||void 0;document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{new f(t,n)}):new f(t,n)})();})();
672
+ `,this.uploadListEl=this.bodyEl.querySelector("#sm-prechat-uploads"),this.fileInputEl=this.bodyEl.querySelector("#sm-prechat-file-input"),this.bodyEl.querySelector("#sm-start-btn")?.addEventListener("click",()=>{this.handleStartChat()}),this.bodyEl.querySelector(".sm-prechat-attach")?.addEventListener("click",()=>{this.fileInputEl?.click()}),this.fileInputEl?.addEventListener("change",()=>{this.fileInputEl?.files&&(this.handleFilesSelected(Array.from(this.fileInputEl.files)),this.fileInputEl.value="")}),this.renderPendingAttachments()}ensureChatView(){this.bodyEl&&(this.bodyEl.dataset.view!=="chat"&&(this.bodyEl.dataset.view="chat",this.bodyEl.innerHTML=`
673
+ <div class="sm-chat-shell" id="sm-chat-shell">
674
+ <div class="sm-messages" id="sm-messages"></div>
675
+ <div class="sm-typing" id="sm-typing"></div>
676
+ <div class="sm-upload-list" id="sm-upload-list"></div>
677
+ <div class="sm-input-area">
678
+ <input type="file" id="sm-file-input" hidden multiple accept="image/*,video/*,audio/*" />
679
+ <button class="sm-attach-btn" id="sm-attach-btn" type="button" aria-label="Attach files">${E}</button>
680
+ <textarea class="sm-input" id="sm-input" placeholder="Type a message..." rows="1"></textarea>
681
+ <button class="sm-send-btn" id="sm-send-btn" type="button" aria-label="Send message">${U}</button>
682
+ </div>
683
+ </div>
684
+ `,this.chatShellEl=this.bodyEl.querySelector("#sm-chat-shell"),this.messagesEl=this.bodyEl.querySelector("#sm-messages"),this.typingEl=this.bodyEl.querySelector("#sm-typing"),this.uploadListEl=this.bodyEl.querySelector("#sm-upload-list"),this.inputEl=this.bodyEl.querySelector("#sm-input"),this.fileInputEl=this.bodyEl.querySelector("#sm-file-input"),this.bodyEl.querySelector("#sm-send-btn")?.addEventListener("click",()=>{this.handleSendMessage()}),this.bodyEl.querySelector("#sm-attach-btn")?.addEventListener("click",()=>{this.fileInputEl?.click()}),this.fileInputEl?.addEventListener("change",()=>{this.fileInputEl?.files&&(this.handleFilesSelected(Array.from(this.fileInputEl.files)),this.fileInputEl.value="")}),this.inputEl?.addEventListener("keydown",s=>{s.key==="Enter"&&!s.shiftKey&&(s.preventDefault(),this.handleSendMessage())}),this.inputEl?.addEventListener("input",()=>{this.inputEl&&(this.inputEl.style.height="auto",this.inputEl.style.height=`${Math.min(this.inputEl.scrollHeight,100)}px`,this.sendTypingPulse())}),this.messagesEl?.addEventListener("click",s=>{let e=s.target.closest("[data-action]");if(!e)return;let t=e.dataset.action,i=e.dataset.messageId;if(i){if(t==="toggle-picker"){this.openReactionMessageId=this.openReactionMessageId===i?null:i,this.renderMessages();return}if(t==="set-reaction"){let n=e.dataset.emoji;if(!n)return;this.openReactionMessageId=null,this.handleReaction(i,n,e.dataset.reacted==="true")}}}),this.messagesEl?.addEventListener("scroll",()=>{this.clearUnreadMarkerIfViewed()}),this.chatShellEl?.addEventListener("dragover",s=>{s.preventDefault(),this.chatShellEl?.classList.add("sm-dragging")}),this.chatShellEl?.addEventListener("dragleave",s=>{let e=s.relatedTarget;(!e||!this.chatShellEl?.contains(e))&&this.chatShellEl?.classList.remove("sm-dragging")}),this.chatShellEl?.addEventListener("drop",s=>{s.preventDefault(),this.chatShellEl?.classList.remove("sm-dragging"),this.handleFilesSelected(Array.from(s.dataTransfer?.files??[]))})),this.renderMessages(),this.renderTypingIndicator(),this.renderPendingAttachments())}renderTypingIndicator(){if(!this.typingEl)return;let s=this.client.visitorUserId,e=this.runtimeState.typingUsers.filter(t=>t!==s);this.typingEl.innerHTML=e.length?'<div class="sm-typing-indicator"><span></span><span></span><span></span><span>Agent is typing...</span></div>':""}renderPendingAttachments(){if(this.uploadListEl){if(!this.pendingAttachments.length){this.uploadListEl.innerHTML="";return}this.uploadListEl.innerHTML=this.pendingAttachments.map(s=>`
685
+ <div class="sm-upload-chip ${s.error?"sm-upload-chip-error":""}">
686
+ <span class="sm-upload-name">${d(s.fileName)}</span>
687
+ <span class="sm-upload-progress">${d(s.error??`${s.progress}%`)}</span>
688
+ <button class="sm-upload-remove" type="button" data-pending-id="${d(s.id)}" aria-label="Remove attachment">\xD7</button>
689
+ </div>
690
+ `).join(""),this.uploadListEl.querySelectorAll(".sm-upload-remove").forEach(s=>{s.addEventListener("click",()=>{let e=s.dataset.pendingId;e&&(this.pendingAttachments=this.pendingAttachments.filter(t=>t.id!==e),this.renderPendingAttachments())})})}}renderMessages(){if(!this.messagesEl)return;let s=this.client.visitorUserId,e=this.runtimeState.messages,t=!1;if(this.messagesEl.innerHTML=e.map((i,n)=>{let r=e[n-1],a=!r||!de(r.created_at,i.created_at),o=i.sender_id===s,l=i.sender_type==="system"||i.message_type==="system",h=(i.attachments??[]).map(u=>pe(i.id,u)).join(""),f=!t&&!o&&!l&&this.unreadMarkerTimestamp!==null&&new Date(i.created_at).getTime()>new Date(this.unreadMarkerTimestamp).getTime();f&&(t=!0);let p=(i.reactions??[]).map(u=>{let v=!!(s&&u.user_ids.includes(s));return`
691
+ <button
692
+ type="button"
693
+ class="sm-reaction-badge ${v?"sm-reaction-badge-active":""}"
694
+ data-action="set-reaction"
695
+ data-message-id="${d(i.id)}"
696
+ data-emoji="${d(u.emoji)}"
697
+ data-reacted="${v?"true":"false"}"
698
+ >
699
+ ${d(u.emoji)} ${u.count}
700
+ </button>
701
+ `}).join(""),z=this.openReactionMessageId===i.id?`
702
+ <div class="sm-reaction-picker">
703
+ ${oe.map(u=>`
704
+ <button
705
+ type="button"
706
+ class="sm-reaction-picker-btn"
707
+ data-action="set-reaction"
708
+ data-message-id="${d(i.id)}"
709
+ data-emoji="${d(u)}"
710
+ data-reacted="${i.reactions?.some(v=>v.emoji===u&&!!(s&&v.user_ids.includes(s)))?"true":"false"}"
711
+ >
712
+ ${d(u)}
713
+ </button>
714
+ `).join("")}
715
+ </div>
716
+ `:"",q=a?`<div class="sm-date-divider">${d(ce(i.created_at))}</div>`:"",W=f?'<div class="sm-unread-divider" id="sm-unread-divider"><span>New messages</span></div>':"",j=!l&&!o?`<button type="button" class="sm-msg-action" data-action="toggle-picker" data-message-id="${d(i.id)}" aria-label="Add reaction">${O}</button>`:"";return`
717
+ ${q}
718
+ ${W}
719
+ <div class="sm-msg ${l?"sm-msg-system":o?"sm-msg-visitor":"sm-msg-rep"}">
720
+ <div class="sm-msg-row">
721
+ <div class="sm-msg-bubble">
722
+ ${i.content?`<div class="sm-msg-content">${d(i.content)}</div>`:""}
723
+ ${h}
724
+ </div>
725
+ ${j}
726
+ </div>
727
+ ${l?"":`<div class="sm-msg-time">${d(le(i.created_at))}${i.is_edited?" \xB7 edited":""}</div>`}
728
+ ${p?`<div class="sm-reactions">${p}</div>`:""}
729
+ ${z}
730
+ </div>
731
+ `}).join(""),this.shouldScrollToUnread){let i=this.messagesEl.querySelector("#sm-unread-divider");i?i.scrollIntoView({block:"center"}):this.messagesEl.scrollTop=this.messagesEl.scrollHeight,this.shouldScrollToUnread=!1}else this.messagesEl.scrollTop=this.messagesEl.scrollHeight}captureUnreadMarkerFromState(){let s=he(this.runtimeState.readStatuses,this.client.visitorUserId);if(!s){this.unreadMarkerTimestamp=null;return}let e=this.runtimeState.messages.some(t=>t.sender_id!==this.client.visitorUserId&&new Date(t.created_at).getTime()>new Date(s).getTime());this.unreadMarkerTimestamp=e?s:null,this.shouldScrollToUnread=e}clearUnreadMarkerIfViewed(){if(!this.unreadMarkerTimestamp||!this.messagesEl)return;let s=this.messagesEl.querySelector("#sm-unread-divider");s&&this.messagesEl.scrollTop>=s.offsetTop-24&&(this.unreadMarkerTimestamp=null,this.renderMessages())}async initializeVisitorSession(){this.client.isInitialized||await this.client.initVisitorSession()}async toggle(){if(this.isOpen){this.minimize();return}await this.ensureConfigLoaded(),this.renderPanel(),this.isOpen=!0,this.unreadCount=0,this.updateBadge(),this.panelEl?.classList.remove("sm-hidden");try{await this.initializeVisitorSession()}catch{this.renderError("Unable to initialize support chat")}let s=this.conversation??await this.client.getActiveConversation();s?(this.conversation=s,await this.ensureConversationRuntime(!0),this.renderChatView(),await this.markConversationRead()):(this.renderPreChatForm(),this.renderError(null))}minimize(){this.isOpen=!1,this.panelEl?.classList.add("sm-hidden")}async ensureConversationRuntime(s){if(this.conversation){if(this.renderError(null),this.config.realtime_enabled){await this.ensureRealtimeRuntime(s);return}this.realtimeFallbackActive=!1,this.cleanupRealtimeStatusWatchers(),this.setFallbackNotice(null),this.cleanupRealtimeRuntime(),await this.loadPollingSnapshot(s),this.startPolling()}}cleanupRealtimeRuntime(){this.controller&&(this.controller.destroy(),this.controller=null);for(let s of this.runtimeCleanups)s();this.runtimeCleanups=[]}cleanupRealtimeStatusWatchers(){for(let s of this.realtimeStatusCleanups)s();this.realtimeStatusCleanups=[]}ensureRealtimeStatusWatchers(){this.realtimeStatusCleanups.length||(this.realtimeStatusCleanups.push(this.client.chat.on("reconnecting",()=>{!this.config.realtime_enabled||!this.conversation||this.realtimeFallbackActive||this.setFallbackNotice("Realtime reconnecting\u2026")})),this.realtimeStatusCleanups.push(this.client.chat.on("disconnected",()=>{!this.config.realtime_enabled||!this.conversation||this.enterRealtimeFallback()})),this.realtimeStatusCleanups.push(this.client.chat.on("connected",()=>{if(!(!this.config.realtime_enabled||!this.conversation)){if(this.realtimeFallbackActive){this.restoreRealtimeRuntime();return}this.setFallbackNotice(null)}})))}async ensureRealtimeRuntime(s){if(this.conversation){if(this.ensureRealtimeStatusWatchers(),!this.controller||this.runtimeState.conversationId!==this.conversation.conversation_id){this.cleanupRealtimeRuntime(),this.stopPolling(),this.runtimeState=T(this.conversation.conversation_id),this.realtimeFallbackActive=!1,this.setFallbackNotice(null),this.controller=new b(this.client.chat,this.conversation.conversation_id),this.runtimeCleanups.push(this.controller.on("state",t=>{this.applyRuntimeState(t,!1)})),this.runtimeCleanups.push(this.controller.on("error",({message:t})=>{this.renderError(t)}));let e=await this.controller.init({realtime:!0,presence:!0});this.applyRuntimeState(e,s);return}s&&this.captureUnreadMarkerFromState()}}async enterRealtimeFallback(s="Realtime connection lost. Falling back to polling."){if(this.conversation){this.realtimeFallbackActive=!0,this.setFallbackNotice(s),this.cleanupRealtimeRuntime();try{await this.loadPollingSnapshot(!1),this.startPolling(),this.isOpen&&await this.markConversationRead()}catch(e){this.renderError(e instanceof Error?e.message:"Failed to refresh chat state")}}}async restoreRealtimeRuntime(){if(!(!this.conversation||!this.config.realtime_enabled))try{this.realtimeFallbackActive=!1,this.setFallbackNotice(null),await this.ensureRealtimeRuntime(!1)}catch{this.realtimeFallbackActive=!0,this.setFallbackNotice("Realtime reconnect failed. Staying on polling fallback."),this.startPolling()}}applyRuntimeState(s,e){let t=new Set(this.runtimeState.messages.map(n=>n.id)),i=s.messages.filter(n=>!t.has(n.id)&&n.sender_id!==this.client.visitorUserId&&n.sender_type!=="system").length;this.runtimeState=s,e&&this.captureUnreadMarkerFromState(),!this.isOpen&&i>0&&(this.unreadCount+=i,this.updateBadge()),this.updateHeader(),this.renderError(s.error),this.conversation&&this.panelEl&&this.bodyEl?.dataset.view==="chat"&&this.renderChatView(),this.isOpen&&i>0&&this.markConversationRead()}async loadPollingSnapshot(s){if(!this.conversation)return;let[e,t]=await Promise.all([this.client.chat.getMessages(this.conversation.conversation_id,{limit:50}),this.client.chat.getReadStatus(this.conversation.conversation_id)]),i={conversationId:this.conversation.conversation_id,messages:e.data?.messages??this.runtimeState.messages,readStatuses:t.data?.statuses??this.runtimeState.readStatuses,typingUsers:[],members:[],hasMore:e.data?.has_more??!1,isLoading:!1,error:e.error?.message??t.error?.message??null};this.applyRuntimeState(i,s)}startPolling(){this.pollInterval||!this.conversation||(this.pollInterval=setInterval(()=>{this.pollForUpdates()},3e4))}stopPolling(){this.pollInterval&&(clearInterval(this.pollInterval),this.pollInterval=null)}async pollForUpdates(){!this.conversation||this.config.realtime_enabled&&!this.realtimeFallbackActive||(await this.loadPollingSnapshot(!1),this.isOpen&&await this.markConversationRead())}getReadyAttachments(){return this.pendingAttachments.filter(s=>s.attachment).map(s=>s.attachment)}async handleFilesSelected(s){if(s.length){try{await this.initializeVisitorSession()}catch{this.renderError("Unable to prepare attachment upload");return}for(let e of s){let t=`${e.name}:${e.size}:${Date.now()}:${Math.random().toString(36).slice(2)}`;this.pendingAttachments=[...this.pendingAttachments,{id:t,fileName:e.name,progress:0}],this.renderPendingAttachments();let i=await this.client.chat.uploadAttachment(e,n=>{this.pendingAttachments=this.pendingAttachments.map(r=>r.id===t?{...r,progress:n}:r),this.renderPendingAttachments()});this.pendingAttachments=this.pendingAttachments.map(n=>n.id!==t?n:i.data?{...n,progress:100,attachment:i.data}:{...n,error:i.error?.message??"Upload failed"}),this.renderPendingAttachments()}}}collectPreChatValues(){let s={},e,t,i=!0;for(let n of this.getConfiguredPreChatFields()){let r=this.bodyEl?.querySelector(`[data-prechat-key="${n.key}"]`),a=r?.value.trim()??"";if(n.required&&!a){i=!1,r?.focus();break}a&&(n.key==="name"?e=a:n.key==="email"?t=a:s[n.key]=a)}return{name:e,email:t,metadata:s,valid:i}}async handleStartChat(){let s=this.bodyEl?.querySelector("#sm-start-btn"),t=this.bodyEl?.querySelector("#sm-prechat-message")?.value.trim()??"",i=this.getReadyAttachments(),{name:n,email:r,metadata:a,valid:o}=this.collectPreChatValues();if(!o||!t&&!i.length||!s){this.renderError("Please complete the required fields before starting the chat.");return}s.disabled=!0,s.textContent="Connecting...";try{await this.client.initVisitorSession({name:n,email:r}),this.conversation=await this.client.startConversation(t,{page_url:typeof location<"u"?location.href:void 0,attachments:i,metadata:a}),N("conversation_id",this.conversation.conversation_id),this.pendingAttachments=[],this.runtimeState={...T(this.conversation.conversation_id),messages:[B(this.client.visitorUserId??"visitor",t,i)]},this.unreadMarkerTimestamp=null,this.shouldScrollToUnread=!1,await this.ensureConversationRuntime(!1),this.renderChatView(),await this.markConversationRead()}catch(l){this.renderError(l instanceof Error?l.message:"Failed to start chat"),s.disabled=!1,s.textContent="Start Chat"}}async handleSendMessage(){if(!this.conversation)return;let s=this.inputEl?.value.trim()??"",e=this.getReadyAttachments();if(!s&&!e.length)return;this.inputEl.value="",this.inputEl.style.height="auto",this.pendingAttachments=[],this.renderPendingAttachments();let t=B(this.client.visitorUserId??"visitor",s,e);this.controller?this.controller.stageOptimisticMessage(t):(this.client.chat.stageOptimisticMessage(this.conversation.conversation_id,t),this.runtimeState={...this.runtimeState,messages:[...this.client.chat.getCachedMessages(this.conversation.conversation_id)]},this.renderChatView());try{if(this.controller)await this.controller.sendMessage(s,e);else{let i=await this.client.chat.sendMessage(this.conversation.conversation_id,{content:s,attachments:e,message_type:H(s,e)});if(i.error)throw new Error(i.error.message);await this.loadPollingSnapshot(!1)}await this.markConversationRead()}catch(i){this.renderError(i instanceof Error?i.message:"Failed to send message")}}async handleReaction(s,e,t){try{t?this.controller?await this.controller.removeReaction(s,e):(await this.client.chat.removeReaction(s,e),await this.loadPollingSnapshot(!1)):this.controller?await this.controller.addReaction(s,e):(await this.client.chat.addReaction(s,e),await this.loadPollingSnapshot(!1))}catch(i){this.renderError(i instanceof Error?i.message:"Failed to update reaction")}}sendTypingPulse(){!this.controller||!this.config.realtime_enabled||(this.controller.sendTyping(!0),this.typingStopTimer&&clearTimeout(this.typingStopTimer),this.typingStopTimer=setTimeout(()=>{this.controller?.sendTyping(!1)},2500))}async markConversationRead(){if(this.conversation){this.unreadCount=0,this.updateBadge();try{this.controller?(await this.controller.markRead(),await this.controller.refreshReadStatus()):await this.client.chat.markRead(this.conversation.conversation_id)}catch{}}}renderChatView(){this.ensureChatView(),this.updateHeader(),this.renderError(this.runtimeState.error)}updateBadge(){let s=this.shadow.querySelector(".sm-bubble");if(!s)return;let e=s.querySelector(".sm-badge");this.unreadCount>0?(e||(e=document.createElement("span"),e.className="sm-badge",s.appendChild(e)),e.textContent=String(this.unreadCount)):e&&e.remove()}destroy(){this.stopPolling(),this.typingStopTimer&&(clearTimeout(this.typingStopTimer),this.typingStopTimer=null),this.cleanupRealtimeStatusWatchers(),this.cleanupRealtimeRuntime(),this.client.destroy(),this.panelEl?.remove(),this.root.remove()}};(function(){let s=document.querySelectorAll("script[data-api-key]"),e=s[s.length-1];if(!e){console.warn("[ScaleMule] Support widget: missing data-api-key on script tag");return}let t=e.getAttribute("data-api-key");if(!t){console.warn("[ScaleMule] Support widget: data-api-key is empty");return}let i=e.getAttribute("data-api-url")||void 0,n=e.getAttribute("data-color")||void 0,r=e.getAttribute("data-position"),a=r==="left"||r==="right"?r:void 0,o=()=>{new k(t,i,{color:n,position:a})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",o):o()})();})();