@scalemule/chat 0.0.3 → 0.0.5

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.
@@ -0,0 +1,403 @@
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=`
2
+ :host {
3
+ all: initial;
4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
5
+ font-size: 14px;
6
+ line-height: 1.5;
7
+ color: #1a1a1a;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ /* ============ Bubble ============ */
17
+
18
+ .sm-bubble {
19
+ position: fixed;
20
+ bottom: 20px;
21
+ right: 20px;
22
+ width: 60px;
23
+ height: 60px;
24
+ border-radius: 50%;
25
+ background: #2563eb;
26
+ color: #fff;
27
+ border: none;
28
+ cursor: pointer;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
33
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
34
+ z-index: 999999;
35
+ }
36
+
37
+ .sm-bubble:hover {
38
+ transform: scale(1.05);
39
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
40
+ }
41
+
42
+ .sm-bubble .sm-badge {
43
+ position: absolute;
44
+ top: -4px;
45
+ right: -4px;
46
+ background: #ef4444;
47
+ color: #fff;
48
+ border-radius: 10px;
49
+ font-size: 11px;
50
+ font-weight: 600;
51
+ min-width: 20px;
52
+ height: 20px;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ padding: 0 6px;
57
+ }
58
+
59
+ /* ============ Panel ============ */
60
+
61
+ .sm-panel {
62
+ position: fixed;
63
+ bottom: 90px;
64
+ right: 20px;
65
+ 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);
70
+ display: flex;
71
+ flex-direction: column;
72
+ overflow: hidden;
73
+ z-index: 999998;
74
+ animation: sm-slide-up 0.25s ease;
75
+ }
76
+
77
+ @keyframes sm-slide-up {
78
+ from { opacity: 0; transform: translateY(16px); }
79
+ to { opacity: 1; transform: translateY(0); }
80
+ }
81
+
82
+ .sm-panel.sm-hidden {
83
+ display: none;
84
+ }
85
+
86
+ /* ============ Header ============ */
87
+
88
+ .sm-header {
89
+ background: #2563eb;
90
+ color: #fff;
91
+ padding: 16px 20px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ }
96
+
97
+ .sm-header-title {
98
+ font-size: 16px;
99
+ font-weight: 600;
100
+ }
101
+
102
+ .sm-header-subtitle {
103
+ font-size: 12px;
104
+ opacity: 0.8;
105
+ margin-top: 2px;
106
+ }
107
+
108
+ .sm-header-actions {
109
+ display: flex;
110
+ gap: 8px;
111
+ }
112
+
113
+ .sm-header-btn {
114
+ background: none;
115
+ border: none;
116
+ color: #fff;
117
+ cursor: pointer;
118
+ opacity: 0.8;
119
+ padding: 4px;
120
+ border-radius: 4px;
121
+ display: flex;
122
+ align-items: center;
123
+ }
124
+
125
+ .sm-header-btn:hover {
126
+ opacity: 1;
127
+ background: rgba(255, 255, 255, 0.1);
128
+ }
129
+
130
+ /* ============ Pre-chat form ============ */
131
+
132
+ .sm-prechat {
133
+ padding: 24px 20px;
134
+ flex: 1;
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 16px;
138
+ }
139
+
140
+ .sm-prechat-title {
141
+ font-size: 18px;
142
+ font-weight: 600;
143
+ color: #1a1a1a;
144
+ }
145
+
146
+ .sm-prechat-desc {
147
+ font-size: 13px;
148
+ color: #6b7280;
149
+ }
150
+
151
+ .sm-field {
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 4px;
155
+ }
156
+
157
+ .sm-field label {
158
+ font-size: 13px;
159
+ font-weight: 500;
160
+ color: #374151;
161
+ }
162
+
163
+ .sm-field input,
164
+ .sm-field textarea {
165
+ border: 1px solid #d1d5db;
166
+ border-radius: 8px;
167
+ padding: 10px 12px;
168
+ font-size: 14px;
169
+ font-family: inherit;
170
+ outline: none;
171
+ transition: border-color 0.15s;
172
+ }
173
+
174
+ .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);
178
+ }
179
+
180
+ .sm-field textarea {
181
+ resize: vertical;
182
+ min-height: 80px;
183
+ }
184
+
185
+ .sm-submit-btn {
186
+ background: #2563eb;
187
+ color: #fff;
188
+ border: none;
189
+ border-radius: 8px;
190
+ padding: 12px;
191
+ font-size: 14px;
192
+ font-weight: 600;
193
+ cursor: pointer;
194
+ transition: background 0.15s;
195
+ }
196
+
197
+ .sm-submit-btn:hover {
198
+ background: #1d4ed8;
199
+ }
200
+
201
+ .sm-submit-btn:disabled {
202
+ background: #93c5fd;
203
+ cursor: not-allowed;
204
+ }
205
+
206
+ /* ============ Messages ============ */
207
+
208
+ .sm-messages {
209
+ flex: 1;
210
+ overflow-y: auto;
211
+ padding: 16px 20px;
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: 12px;
215
+ min-height: 200px;
216
+ max-height: 360px;
217
+ }
218
+
219
+ .sm-msg {
220
+ display: flex;
221
+ flex-direction: column;
222
+ max-width: 80%;
223
+ }
224
+
225
+ .sm-msg.sm-msg-visitor {
226
+ align-self: flex-end;
227
+ }
228
+
229
+ .sm-msg.sm-msg-rep {
230
+ align-self: flex-start;
231
+ }
232
+
233
+ .sm-msg.sm-msg-system {
234
+ align-self: center;
235
+ max-width: 90%;
236
+ }
237
+
238
+ .sm-msg-bubble {
239
+ padding: 10px 14px;
240
+ border-radius: 12px;
241
+ font-size: 14px;
242
+ line-height: 1.4;
243
+ word-break: break-word;
244
+ }
245
+
246
+ .sm-msg-visitor .sm-msg-bubble {
247
+ background: #2563eb;
248
+ color: #fff;
249
+ border-bottom-right-radius: 4px;
250
+ }
251
+
252
+ .sm-msg-rep .sm-msg-bubble {
253
+ background: #f3f4f6;
254
+ color: #1a1a1a;
255
+ border-bottom-left-radius: 4px;
256
+ }
257
+
258
+ .sm-msg-system .sm-msg-bubble {
259
+ background: transparent;
260
+ color: #9ca3af;
261
+ font-size: 12px;
262
+ text-align: center;
263
+ padding: 4px 8px;
264
+ }
265
+
266
+ .sm-msg-time {
267
+ font-size: 11px;
268
+ color: #9ca3af;
269
+ margin-top: 4px;
270
+ }
271
+
272
+ .sm-msg-visitor .sm-msg-time {
273
+ text-align: right;
274
+ }
275
+
276
+ .sm-typing {
277
+ font-size: 12px;
278
+ color: #9ca3af;
279
+ padding: 0 4px;
280
+ min-height: 18px;
281
+ }
282
+
283
+ /* ============ Input ============ */
284
+
285
+ .sm-input-area {
286
+ border-top: 1px solid #e5e7eb;
287
+ padding: 12px 16px;
288
+ display: flex;
289
+ align-items: flex-end;
290
+ gap: 8px;
291
+ }
292
+
293
+ .sm-input {
294
+ 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;
302
+ max-height: 100px;
303
+ overflow-y: auto;
304
+ line-height: 1.4;
305
+ }
306
+
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;
319
+ align-items: center;
320
+ justify-content: center;
321
+ cursor: pointer;
322
+ flex-shrink: 0;
323
+ transition: background 0.15s;
324
+ }
325
+
326
+ .sm-send-btn:hover {
327
+ background: #1d4ed8;
328
+ }
329
+
330
+ .sm-send-btn:disabled {
331
+ background: #93c5fd;
332
+ cursor: not-allowed;
333
+ }
334
+
335
+ /* ============ Footer ============ */
336
+
337
+ .sm-footer {
338
+ text-align: center;
339
+ padding: 8px;
340
+ font-size: 11px;
341
+ color: #9ca3af;
342
+ }
343
+
344
+ .sm-footer a {
345
+ color: #6b7280;
346
+ text-decoration: none;
347
+ }
348
+
349
+ .sm-footer a:hover {
350
+ text-decoration: underline;
351
+ }
352
+
353
+ /* ============ Responsive ============ */
354
+
355
+ @media (max-width: 440px) {
356
+ .sm-panel {
357
+ bottom: 0;
358
+ right: 0;
359
+ left: 0;
360
+ width: 100%;
361
+ max-height: 100vh;
362
+ border-radius: 16px 16px 0 0;
363
+ }
364
+ }
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=`
366
+ <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>
370
+ </div>
371
+ <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>
374
+ </div>
375
+ </div>
376
+ <div class="sm-body"></div>
377
+ <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=`
379
+ <div class="sm-prechat">
380
+ <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>
386
+ <div class="sm-field">
387
+ <label for="sm-email">Email</label>
388
+ <input type="email" id="sm-email" placeholder="you@example.com" />
389
+ </div>
390
+ <div class="sm-field">
391
+ <label for="sm-message">Message *</label>
392
+ <textarea id="sm-message" placeholder="How can we help?"></textarea>
393
+ </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
+ </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)})();})();
@@ -2,6 +2,7 @@ interface ChatConfig {
2
2
  apiKey?: string;
3
3
  embedToken?: string;
4
4
  apiBaseUrl?: string;
5
+ wsUrl?: string;
5
6
  applicationId?: string;
6
7
  userId?: string;
7
8
  sessionToken?: string;
@@ -30,7 +31,7 @@ interface ApiError {
30
31
  }
31
32
  interface Conversation {
32
33
  id: string;
33
- conversation_type: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room';
34
+ conversation_type: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room' | 'support';
34
35
  name?: string;
35
36
  created_by?: string;
36
37
  participant_count?: number;
@@ -182,6 +183,15 @@ interface ChatEventMap {
182
183
  senderId: string;
183
184
  preview: string;
184
185
  };
186
+ 'support:new': {
187
+ conversationId: string;
188
+ visitorName?: string;
189
+ };
190
+ 'support:assigned': {
191
+ conversationId: string;
192
+ visitorName?: string;
193
+ visitorEmail?: string;
194
+ };
185
195
  'error': {
186
196
  code: string;
187
197
  message: string;
@@ -196,7 +206,7 @@ interface SendMessageOptions {
196
206
  interface ListConversationsOptions {
197
207
  page?: number;
198
208
  per_page?: number;
199
- conversation_type?: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room';
209
+ conversation_type?: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room' | 'support';
200
210
  }
201
211
  interface UnreadTotalResponse {
202
212
  unread_conversations: number;
@@ -219,4 +229,4 @@ interface MessagesResponse {
219
229
  newest_id?: string;
220
230
  }
221
231
 
222
- export type { ApiError as A, ChannelSettings as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReactionSummary as R, SendMessageOptions as S, UnreadTotalResponse as U, ApiResponse as a, Attachment as b, ChannelWithSettings as c, ChatConfig as d, ChatEventMap as e, ChatMessage as f, ChatReaction as g, ConnectionStatus as h, Conversation as i, CreateConversationOptions as j, CreateEphemeralChannelOptions as k, CreateLargeRoomOptions as l, PresenceMember as m, ReadStatus as n };
232
+ export type { ApiResponse as A, ChatConfig as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReactionSummary as R, SendMessageOptions as S, UnreadTotalResponse as U, ChatMessage as a, ConnectionStatus as b, Conversation as c, ApiError as d, Attachment as e, ChannelSettings as f, ChannelWithSettings as g, ChatEventMap as h, ChatReaction as i, CreateConversationOptions as j, CreateEphemeralChannelOptions as k, CreateLargeRoomOptions as l, PresenceMember as m, ReadStatus as n };
@@ -2,6 +2,7 @@ interface ChatConfig {
2
2
  apiKey?: string;
3
3
  embedToken?: string;
4
4
  apiBaseUrl?: string;
5
+ wsUrl?: string;
5
6
  applicationId?: string;
6
7
  userId?: string;
7
8
  sessionToken?: string;
@@ -30,7 +31,7 @@ interface ApiError {
30
31
  }
31
32
  interface Conversation {
32
33
  id: string;
33
- conversation_type: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room';
34
+ conversation_type: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room' | 'support';
34
35
  name?: string;
35
36
  created_by?: string;
36
37
  participant_count?: number;
@@ -182,6 +183,15 @@ interface ChatEventMap {
182
183
  senderId: string;
183
184
  preview: string;
184
185
  };
186
+ 'support:new': {
187
+ conversationId: string;
188
+ visitorName?: string;
189
+ };
190
+ 'support:assigned': {
191
+ conversationId: string;
192
+ visitorName?: string;
193
+ visitorEmail?: string;
194
+ };
185
195
  'error': {
186
196
  code: string;
187
197
  message: string;
@@ -196,7 +206,7 @@ interface SendMessageOptions {
196
206
  interface ListConversationsOptions {
197
207
  page?: number;
198
208
  per_page?: number;
199
- conversation_type?: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room';
209
+ conversation_type?: 'direct' | 'group' | 'broadcast' | 'ephemeral' | 'large_room' | 'support';
200
210
  }
201
211
  interface UnreadTotalResponse {
202
212
  unread_conversations: number;
@@ -219,4 +229,4 @@ interface MessagesResponse {
219
229
  newest_id?: string;
220
230
  }
221
231
 
222
- export type { ApiError as A, ChannelSettings as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReactionSummary as R, SendMessageOptions as S, UnreadTotalResponse as U, ApiResponse as a, Attachment as b, ChannelWithSettings as c, ChatConfig as d, ChatEventMap as e, ChatMessage as f, ChatReaction as g, ConnectionStatus as h, Conversation as i, CreateConversationOptions as j, CreateEphemeralChannelOptions as k, CreateLargeRoomOptions as l, PresenceMember as m, ReadStatus as n };
232
+ export type { ApiResponse as A, ChatConfig as C, GetMessagesOptions as G, ListConversationsOptions as L, MessagesResponse as M, Participant as P, ReactionSummary as R, SendMessageOptions as S, UnreadTotalResponse as U, ChatMessage as a, ConnectionStatus as b, Conversation as c, ApiError as d, Attachment as e, ChannelSettings as f, ChannelWithSettings as g, ChatEventMap as h, ChatReaction as i, CreateConversationOptions as j, CreateEphemeralChannelOptions as k, CreateLargeRoomOptions as l, PresenceMember as m, ReadStatus as n };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/chat",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "ScaleMule standalone chat SDK — real-time messaging, presence, typing indicators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",