@modochats/widget 0.1.0

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.
Files changed (195) hide show
  1. package/.github/workflows/build-and-publish.yml +174 -0
  2. package/.vscode/settings.json +3 -0
  3. package/.yarn/install-state.gz +0 -0
  4. package/README.md +163 -0
  5. package/cdn-dist/README.md +42 -0
  6. package/cdn-dist/modo-web-component.js +1344 -0
  7. package/cdn-dist/modo-web-component.min.js +1 -0
  8. package/cdn-dist/package.json +27 -0
  9. package/dist/src/app.d.ts +29 -0
  10. package/dist/src/app.js +1 -0
  11. package/dist/src/app.js.map +1 -0
  12. package/dist/src/constants/index.d.ts +9 -0
  13. package/dist/src/constants/index.js +1 -0
  14. package/dist/src/constants/index.js.map +1 -0
  15. package/dist/src/constants/regex.d.ts +2 -0
  16. package/dist/src/constants/regex.js +1 -0
  17. package/dist/src/constants/regex.js.map +1 -0
  18. package/dist/src/index.d.ts +9 -0
  19. package/dist/src/index.js +1 -0
  20. package/dist/src/index.js.map +1 -0
  21. package/dist/src/models/chatbot.d.ts +23 -0
  22. package/dist/src/models/chatbot.js +1 -0
  23. package/dist/src/models/chatbot.js.map +1 -0
  24. package/dist/src/models/conversation.d.ts +22 -0
  25. package/dist/src/models/conversation.js +1 -0
  26. package/dist/src/models/conversation.js.map +1 -0
  27. package/dist/src/models/customer-data.d.ts +31 -0
  28. package/dist/src/models/customer-data.js +1 -0
  29. package/dist/src/models/customer-data.js.map +1 -0
  30. package/dist/src/models/message-utils.d.ts +12 -0
  31. package/dist/src/models/message-utils.js +1 -0
  32. package/dist/src/models/message-utils.js.map +1 -0
  33. package/dist/src/services/chat/conversation.d.ts +22 -0
  34. package/dist/src/services/chat/conversation.js +1 -0
  35. package/dist/src/services/chat/conversation.js.map +1 -0
  36. package/dist/src/services/chat/message-utils.d.ts +12 -0
  37. package/dist/src/services/chat/message-utils.js +1 -0
  38. package/dist/src/services/chat/message-utils.js.map +1 -0
  39. package/dist/src/services/chat/model.d.ts +27 -0
  40. package/dist/src/services/chat/model.js +1 -0
  41. package/dist/src/services/chat/model.js.map +1 -0
  42. package/dist/src/services/chatbot/chatbot.d.ts +23 -0
  43. package/dist/src/services/chatbot/chatbot.js +1 -0
  44. package/dist/src/services/chatbot/chatbot.js.map +1 -0
  45. package/dist/src/services/checker.d.ts +3 -0
  46. package/dist/src/services/checker.js +1 -0
  47. package/dist/src/services/checker.js.map +1 -0
  48. package/dist/src/services/listeners/adders.d.ts +3 -0
  49. package/dist/src/services/listeners/adders.js +1 -0
  50. package/dist/src/services/listeners/adders.js.map +1 -0
  51. package/dist/src/services/listeners/fn.d.ts +3 -0
  52. package/dist/src/services/listeners/fn.js +1 -0
  53. package/dist/src/services/listeners/fn.js.map +1 -0
  54. package/dist/src/services/socket/utils.d.ts +2 -0
  55. package/dist/src/services/socket/utils.js +1 -0
  56. package/dist/src/services/socket/utils.js.map +1 -0
  57. package/dist/src/services/ui/fn.d.ts +13 -0
  58. package/dist/src/services/ui/fn.js +1 -0
  59. package/dist/src/services/ui/fn.js.map +1 -0
  60. package/dist/src/services/ui/html.d.ts +3 -0
  61. package/dist/src/services/ui/html.js +1 -0
  62. package/dist/src/services/ui/html.js.map +1 -0
  63. package/dist/src/services/user/customer-data.d.ts +31 -0
  64. package/dist/src/services/user/customer-data.js +1 -0
  65. package/dist/src/services/user/customer-data.js.map +1 -0
  66. package/dist/src/services/voice-chat/model.d.ts +12 -0
  67. package/dist/src/services/voice-chat/model.js +1 -0
  68. package/dist/src/services/voice-chat/model.js.map +1 -0
  69. package/dist/src/services/voice-chat/utils.d.ts +9 -0
  70. package/dist/src/services/voice-chat/utils.js +1 -0
  71. package/dist/src/services/voice-chat/utils.js.map +1 -0
  72. package/dist/src/tools/fetch.d.ts +2 -0
  73. package/dist/src/tools/fetch.js +1 -0
  74. package/dist/src/tools/fetch.js.map +1 -0
  75. package/dist/src/types/app.d.ts +17 -0
  76. package/dist/src/types/app.js.map +1 -0
  77. package/dist/src/types/conversation.d.ts +14 -0
  78. package/dist/src/types/conversation.js +1 -0
  79. package/dist/src/types/conversation.js.map +1 -0
  80. package/dist/src/types/socket.d.ts +6 -0
  81. package/dist/src/types/socket.js +1 -0
  82. package/dist/src/types/socket.js.map +1 -0
  83. package/dist/src/types/window.d.ts +9 -0
  84. package/dist/src/types/window.js +1 -0
  85. package/dist/src/types/window.js.map +1 -0
  86. package/dist/src/utils/audio.d.ts +3 -0
  87. package/dist/src/utils/audio.js +1 -0
  88. package/dist/src/utils/audio.js.map +1 -0
  89. package/dist/src/utils/browser.d.ts +2 -0
  90. package/dist/src/utils/browser.js +1 -0
  91. package/dist/src/utils/browser.js.map +1 -0
  92. package/dist/src/utils/fetch.d.ts +18 -0
  93. package/dist/src/utils/fetch.js +1 -0
  94. package/dist/src/utils/fetch.js.map +1 -0
  95. package/dist/src/utils/uuid.d.ts +6 -0
  96. package/dist/src/utils/uuid.js +1 -0
  97. package/dist/src/utils/uuid.js.map +1 -0
  98. package/dist/types/src/app.d.ts +30 -0
  99. package/dist/types/src/app.d.ts.map +1 -0
  100. package/dist/types/src/constants/index.d.ts +10 -0
  101. package/dist/types/src/constants/index.d.ts.map +1 -0
  102. package/dist/types/src/constants/regex.d.ts +3 -0
  103. package/dist/types/src/constants/regex.d.ts.map +1 -0
  104. package/dist/types/src/index.d.ts +10 -0
  105. package/dist/types/src/index.d.ts.map +1 -0
  106. package/dist/types/src/models/chatbot.d.ts +24 -0
  107. package/dist/types/src/models/chatbot.d.ts.map +1 -0
  108. package/dist/types/src/models/conversation.d.ts +23 -0
  109. package/dist/types/src/models/conversation.d.ts.map +1 -0
  110. package/dist/types/src/models/customer-data.d.ts +32 -0
  111. package/dist/types/src/models/customer-data.d.ts.map +1 -0
  112. package/dist/types/src/models/message-utils.d.ts +13 -0
  113. package/dist/types/src/models/message-utils.d.ts.map +1 -0
  114. package/dist/types/src/services/chat/conversation.d.ts +23 -0
  115. package/dist/types/src/services/chat/conversation.d.ts.map +1 -0
  116. package/dist/types/src/services/chat/message-utils.d.ts +13 -0
  117. package/dist/types/src/services/chat/message-utils.d.ts.map +1 -0
  118. package/dist/types/src/services/chat/model.d.ts +28 -0
  119. package/dist/types/src/services/chat/model.d.ts.map +1 -0
  120. package/dist/types/src/services/chatbot/chatbot.d.ts +24 -0
  121. package/dist/types/src/services/chatbot/chatbot.d.ts.map +1 -0
  122. package/dist/types/src/services/checker.d.ts +4 -0
  123. package/dist/types/src/services/checker.d.ts.map +1 -0
  124. package/dist/types/src/services/listeners/adders.d.ts +4 -0
  125. package/dist/types/src/services/listeners/adders.d.ts.map +1 -0
  126. package/dist/types/src/services/listeners/fn.d.ts +4 -0
  127. package/dist/types/src/services/listeners/fn.d.ts.map +1 -0
  128. package/dist/types/src/services/socket/utils.d.ts +3 -0
  129. package/dist/types/src/services/socket/utils.d.ts.map +1 -0
  130. package/dist/types/src/services/ui/fn.d.ts +14 -0
  131. package/dist/types/src/services/ui/fn.d.ts.map +1 -0
  132. package/dist/types/src/services/ui/html.d.ts +4 -0
  133. package/dist/types/src/services/ui/html.d.ts.map +1 -0
  134. package/dist/types/src/services/user/customer-data.d.ts +32 -0
  135. package/dist/types/src/services/user/customer-data.d.ts.map +1 -0
  136. package/dist/types/src/services/voice-chat/model.d.ts +13 -0
  137. package/dist/types/src/services/voice-chat/model.d.ts.map +1 -0
  138. package/dist/types/src/services/voice-chat/utils.d.ts +10 -0
  139. package/dist/types/src/services/voice-chat/utils.d.ts.map +1 -0
  140. package/dist/types/src/tools/fetch.d.ts +3 -0
  141. package/dist/types/src/tools/fetch.d.ts.map +1 -0
  142. package/dist/types/src/types/app.d.ts +18 -0
  143. package/dist/types/src/types/app.d.ts.map +1 -0
  144. package/dist/types/src/types/conversation.d.ts +15 -0
  145. package/dist/types/src/types/conversation.d.ts.map +1 -0
  146. package/dist/types/src/types/socket.d.ts +7 -0
  147. package/dist/types/src/types/socket.d.ts.map +1 -0
  148. package/dist/types/src/types/window.d.ts +10 -0
  149. package/dist/types/src/types/window.d.ts.map +1 -0
  150. package/dist/types/src/utils/audio.d.ts +4 -0
  151. package/dist/types/src/utils/audio.d.ts.map +1 -0
  152. package/dist/types/src/utils/browser.d.ts +3 -0
  153. package/dist/types/src/utils/browser.d.ts.map +1 -0
  154. package/dist/types/src/utils/fetch.d.ts +19 -0
  155. package/dist/types/src/utils/fetch.d.ts.map +1 -0
  156. package/dist/types/src/utils/uuid.d.ts +7 -0
  157. package/dist/types/src/utils/uuid.d.ts.map +1 -0
  158. package/package.json +76 -0
  159. package/rollup.config.js +18 -0
  160. package/rollup.dev.config.js +22 -0
  161. package/scripts/create-umd-bundle.js +213 -0
  162. package/scripts/terser-minify.js +112 -0
  163. package/src/app.ts +117 -0
  164. package/src/constants/index.ts +21 -0
  165. package/src/constants/regex.ts +2 -0
  166. package/src/index.ts +16 -0
  167. package/src/services/chat/conversation.ts +135 -0
  168. package/src/services/chat/message-utils.ts +221 -0
  169. package/src/services/chat/model.ts +139 -0
  170. package/src/services/chatbot/chatbot.ts +66 -0
  171. package/src/services/checker.ts +10 -0
  172. package/src/services/listeners/adders.ts +178 -0
  173. package/src/services/listeners/fn.ts +77 -0
  174. package/src/services/socket/utils.ts +9 -0
  175. package/src/services/ui/fn.ts +254 -0
  176. package/src/services/ui/html.ts +192 -0
  177. package/src/services/user/customer-data.ts +78 -0
  178. package/src/services/voice-chat/model.ts +79 -0
  179. package/src/services/voice-chat/utils.ts +137 -0
  180. package/src/tools/fetch.ts +7 -0
  181. package/src/types/app.ts +17 -0
  182. package/src/types/conversation.ts +14 -0
  183. package/src/types/socket.ts +7 -0
  184. package/src/types/window.ts +12 -0
  185. package/src/utils/audio.ts +67 -0
  186. package/src/utils/browser.ts +4 -0
  187. package/src/utils/fetch.ts +98 -0
  188. package/src/utils/uuid.ts +13 -0
  189. package/temp/audio/new-message.mp3 +0 -0
  190. package/temp/audio/on-hold.mp3 +0 -0
  191. package/temp/audio-processor.js +261 -0
  192. package/temp/css/index.css +2283 -0
  193. package/temp/dev.html +87 -0
  194. package/temp/index.html +16 -0
  195. package/tsconfig.json +119 -0
package/src/app.ts ADDED
@@ -0,0 +1,117 @@
1
+ import {WidgetOptions} from "./types/app.js";
2
+ import {Chatbot} from "./services/chatbot/chatbot.js";
3
+ import {fetchChatbot} from "./utils/fetch.js";
4
+ import {checkIfHostIsAllowed} from "./services/checker.js";
5
+ import {createChatContainer} from "./services/ui/html.js";
6
+ import {CustomerData} from "./services/user/customer-data.js";
7
+ import {loadStarters, updateChatToggleImage, updateChatTitle, applyModoOptions, loadCss} from "./services/ui/fn.js";
8
+ import {VERSION} from "./constants/index.js";
9
+ import {VoiceChat} from "./services/voice-chat/model.js";
10
+ import {Chat} from "./services/chat/model.js";
11
+
12
+ class Widget {
13
+ container?: HTMLDivElement;
14
+ publicKey: string;
15
+ chatbot?: Chatbot;
16
+ customerData: CustomerData;
17
+ chat: Chat;
18
+
19
+ options: Partial<WidgetOptions> = {};
20
+ openedCount: number = 0;
21
+ version: string;
22
+ isInitialized: boolean = false;
23
+ isOpen: boolean = false;
24
+ voiceChat?: VoiceChat;
25
+
26
+ constructor(publicKey: string, options?: Partial<WidgetOptions>) {
27
+ this.publicKey = publicKey;
28
+ this.customerData = new CustomerData(this, options?.userData);
29
+ this.chat = new Chat();
30
+ this.version = VERSION;
31
+ this.options = {
32
+ position: options?.position || "right",
33
+ theme: options?.theme,
34
+ primaryColor: options?.primaryColor,
35
+ title: options?.title || "",
36
+ userData: options?.userData,
37
+ foregroundColor: options?.foregroundColor,
38
+ fullScreen: typeof options?.fullScreen === "boolean" ? options?.fullScreen : false
39
+ };
40
+ if (options?.autoInit) this.init();
41
+ }
42
+ async init() {
43
+ if (this.isInitialized) throw new Error("Widget already initialized");
44
+ const chatbotRes = await fetchChatbot(this.publicKey);
45
+ this.chatbot = new Chatbot(chatbotRes);
46
+ this.options = {
47
+ ...this.options,
48
+ theme: this.options?.theme || this.chatbot?.uiConfig?.theme || "dark",
49
+ primaryColor: this.options?.primaryColor || this.chatbot?.uiConfig?.primaryColor || "#667eea",
50
+ foregroundColor: this.options?.foregroundColor || this.chatbot?.uiConfig?.foregroundColor || "#fff"
51
+ };
52
+ if (checkIfHostIsAllowed(this)) {
53
+ await loadCss();
54
+ window.getMWidget = () => this;
55
+ createChatContainer(this);
56
+ applyModoOptions();
57
+ loadStarters();
58
+ updateChatToggleImage();
59
+ updateChatTitle();
60
+
61
+ this.isInitialized = true;
62
+ this.chatbot.showTooltip();
63
+
64
+ // In fullscreen mode, automatically open the chat
65
+ if (this.options.fullScreen) {
66
+ // Ensure chat body is visible in fullscreen mode
67
+ const chatBody = this.container?.querySelector(".mw-chat-body");
68
+ if (chatBody) {
69
+ chatBody.classList.remove("mw-hidden");
70
+ chatBody.classList.add("mw-active");
71
+ }
72
+ try {
73
+ this.chat.initInstance();
74
+ } finally {
75
+ this.onOpen();
76
+ }
77
+ } else this.chat.initInstance();
78
+ } else throw new Error("host not allowed");
79
+ }
80
+ async onOpen() {
81
+ this.isOpen = true;
82
+ this.openedCount++;
83
+
84
+ // Hide tooltip when chat is opened
85
+ this.conversation?.hideTooltip();
86
+ this.chatbot?.hideTooltip();
87
+ this.conversation?.markAsRead();
88
+ this.conversation?.scrollToBottom();
89
+ if (this.openedCount === 1) {
90
+ if (this.chat.conversationD) {
91
+ await this.conversation?.loadMessages();
92
+ await this.chat?.socket?.connect();
93
+ }
94
+ if (this.chatbot?.voiceChat) this.voiceChat = new VoiceChat();
95
+ await this.customerData.fetchUpdate();
96
+ }
97
+ }
98
+ onClose() {
99
+ this.isOpen = false;
100
+ }
101
+
102
+ /**
103
+ * Update user data with new values
104
+ * @param newUserData - Object containing new user data to merge
105
+ */
106
+ async updateUserData(newUserData: Record<string, any>) {
107
+ await this.customerData.updateUserData(newUserData);
108
+ }
109
+ get conversation() {
110
+ return this.chat.conversation;
111
+ }
112
+ }
113
+
114
+ window.ModoChat = Widget;
115
+ window.ModoWidget = Widget;
116
+
117
+ export type {Widget};
@@ -0,0 +1,21 @@
1
+ const getEnvironment = () => {
2
+ // Check for browser global variable
3
+ if (typeof window !== "undefined" && (window as any).ENVIRONMENT) {
4
+ return (window as any).ENVIRONMENT;
5
+ }
6
+
7
+ // Check for NODE_ENV in build process
8
+ if (typeof process !== "undefined" && process.env?.NODE_ENV) {
9
+ return process.env.NODE_ENV.toUpperCase();
10
+ }
11
+
12
+ return "PROD"; // Default to production
13
+ };
14
+ const isDev = getEnvironment() === "DEV";
15
+ const isProd = getEnvironment() === "PROD";
16
+ const BASE_API_URL = isDev ? "https://dev-api.modochats.com" : "https://api.modochats.com";
17
+ const BASE_WEBSOCKET_URL = isDev ? "wss://dev-api.modochats.com/ws" : "wss://api.modochats.com/ws";
18
+ const VERSION = "0.51";
19
+ const BASE_STORAGE_URL = "https://modochats.s3.ir-thr-at1.arvanstorage.ir";
20
+ const NEW_MESSAGE_AUDIO_URL = `${BASE_STORAGE_URL}/new-message.mp3`;
21
+ export {BASE_API_URL, BASE_WEBSOCKET_URL, VERSION, isDev, isProd, getEnvironment, NEW_MESSAGE_AUDIO_URL, BASE_STORAGE_URL};
@@ -0,0 +1,2 @@
1
+ const PhoneNumberRegex = /^(\+98|0)?9\d{9}$/;
2
+ export {PhoneNumberRegex};
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Main exports
2
+ export {Widget as ModoChat} from "./app.js";
3
+ export type {WidgetOptions, FetchPaginationRes} from "./types/app.js";
4
+
5
+ // Models
6
+ export {Chatbot} from "./services/chatbot/chatbot.js";
7
+ export {Conversation} from "./services/chat/conversation.js";
8
+ export {CustomerData} from "./services/user/customer-data.js";
9
+
10
+ // Services
11
+ export {VoiceChat} from "./services/voice-chat/model.js";
12
+ export {Chat} from "./services/chat/model.js";
13
+
14
+ // Types
15
+ export type {ConversationStatus, MessageType} from "./types/conversation.js";
16
+ export type {SocketMessage} from "./types/socket.js";
@@ -0,0 +1,135 @@
1
+ import {NEW_MESSAGE_AUDIO_URL} from "#src/constants/index.js";
2
+ import {setConversationType, switchToConversationLayout, switchToStarterLayout} from "#src/services/ui/fn.js";
3
+ import {playAudio, preloadAudio} from "#src/utils/audio.js";
4
+ import {initMessageElement, showMessageTooltip} from "./message-utils.js";
5
+ import {ConversationMessage} from "@modochats/chat-client";
6
+
7
+ class Conversation {
8
+ constructor() {}
9
+ // d = data
10
+ get d() {
11
+ return window.getMWidget?.().chat?.conversationD;
12
+ }
13
+ addMessage(message: ConversationMessage, options?: {incoming: boolean}) {
14
+ const widget = window.getMWidget?.();
15
+ initMessageElement(message);
16
+ if (options?.incoming) {
17
+ if (widget?.isOpen) this.markAsRead();
18
+ else {
19
+ this.addBadge();
20
+ showMessageTooltip(message);
21
+ playAudio(NEW_MESSAGE_AUDIO_URL).catch(console.warn);
22
+ }
23
+ }
24
+ this.scrollToBottom();
25
+ }
26
+ clearContainerEl() {
27
+ const chatMessagesContainer = document.querySelector(".mw-chat-messages-con");
28
+ if (chatMessagesContainer) {
29
+ chatMessagesContainer.innerHTML = "";
30
+ }
31
+ }
32
+ addSystemMessage(message: string) {
33
+ const chatMessagesContainer = document.querySelector(".mw-chat-messages-con");
34
+ if (chatMessagesContainer) {
35
+ const systemMessageElement = document.createElement("div");
36
+ systemMessageElement.className = "mw-system-message";
37
+ systemMessageElement.innerHTML = `
38
+ <div class="mw-system-message-content">
39
+ ${message}
40
+ </div>
41
+ `;
42
+ chatMessagesContainer.appendChild(systemMessageElement);
43
+ this.scrollToBottom();
44
+ }
45
+ }
46
+
47
+ scrollToBottom() {
48
+ const chatMessagesContainer = document.querySelector(".mw-chat-messages-con");
49
+ if (chatMessagesContainer) {
50
+ chatMessagesContainer.scrollTop = chatMessagesContainer.scrollHeight;
51
+ }
52
+ }
53
+
54
+ clear() {
55
+ this.d?.clear();
56
+ this.d!.messages = [];
57
+ const widget = window.getMWidget?.();
58
+ localStorage.removeItem(`modo-chat:${widget?.publicKey}-conversation-uuid`);
59
+ this.clearContainerEl();
60
+ switchToStarterLayout();
61
+ }
62
+
63
+ onInit() {
64
+ switchToConversationLayout();
65
+ preloadAudio("./audio/new-message.mp3").catch(console.warn);
66
+ if (this?.d?.status) setConversationType(this.d?.status!);
67
+ }
68
+
69
+ setStatus() {
70
+ setConversationType(this.d?.status!);
71
+ }
72
+
73
+ async loadMessages() {
74
+ const widget = window.getMWidget?.();
75
+ const messages = await this.d?.loadMessages();
76
+ const chatMessagesContainer = widget?.container?.querySelector(".mw-chat-messages-con");
77
+ if (chatMessagesContainer) chatMessagesContainer.innerHTML = "";
78
+ for (const message of messages || []) this.addMessage(message);
79
+ }
80
+
81
+ addBadge() {
82
+ const widget = window.getMWidget?.();
83
+ if (!widget?.isOpen && this.unreadCount > 0 && widget) {
84
+ const badge = widget.container?.querySelector(".mw-badge");
85
+ const badgeText = widget.container?.querySelector(".mw-badge-text");
86
+
87
+ if (badge && badgeText) {
88
+ // Show the badge
89
+ badge.classList.remove("mw-hidden");
90
+
91
+ // Update badge text
92
+ const displayCount = this.unreadCount > 99 ? "99+" : this.unreadCount.toString();
93
+ badgeText.textContent = displayCount;
94
+
95
+ // Add plus class for 99+ counts
96
+ if (this.unreadCount > 99) {
97
+ badge.classList.add("mw-badge-plus");
98
+ } else {
99
+ badge.classList.remove("mw-badge-plus");
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ get unreadCount() {
106
+ return this.d?.unreadCount || 0;
107
+ }
108
+
109
+ hideBadge() {
110
+ const widget = window.getMWidget?.();
111
+ const badge = widget?.container?.querySelector(".mw-badge");
112
+ if (badge) {
113
+ badge.classList.add("mw-hidden");
114
+ }
115
+ }
116
+
117
+ hideTooltip() {
118
+ const widget = window.getMWidget?.();
119
+ const tooltip = widget?.container?.querySelector(".mw-toggle-tooltip");
120
+ if (tooltip) {
121
+ tooltip.classList.add("mw-hidden");
122
+ }
123
+ }
124
+
125
+ async markAsRead() {
126
+ const widget = window.getMWidget?.();
127
+ await this.d?.markAsRead();
128
+ this.hideBadge();
129
+ }
130
+
131
+ get containerElement(): HTMLDivElement | null {
132
+ return document.querySelector(".mw-chat-messages-con");
133
+ }
134
+ }
135
+ export {Conversation};
@@ -0,0 +1,221 @@
1
+ import {ConversationMessage} from "@modochats/chat-client";
2
+ import {marked} from "marked";
3
+
4
+ type MessageRelatedUtil<T extends Array<any> = [], R = void> = (message: ConversationMessage, ...args: T) => R;
5
+
6
+ const initMessageElement: MessageRelatedUtil = message => {
7
+ const element = document.createElement("div");
8
+ const widget = window.getMWidget?.();
9
+ element.id = `message-${message.id}`;
10
+ // Format time from createdAt
11
+ const messageTime = new Date(message.createdAt);
12
+ const timeString = messageTime.toLocaleTimeString("fa-IR", {
13
+ hour: "2-digit",
14
+ minute: "2-digit",
15
+ hour12: false
16
+ });
17
+
18
+ // Build the HTML with replied-to message preview if exists
19
+ let repliedToHtml = "";
20
+ if (message.repliedTo) {
21
+ const repliedContent = message.repliedTo.content.length > 40 ? message.repliedTo.content.substring(0, 40) + "..." : message.repliedTo.content;
22
+ repliedToHtml = `
23
+ <div class="mw-replied-to-preview" data-reply-message-id="${message.repliedTo.id}">
24
+ <div class="mw-replied-to-header">
25
+ <span class="mw-replied-to-sender">${message.repliedTo.type === "USER" ? "شما" : "پشتیبان"}</span>
26
+ </div>
27
+ <div class="mw-replied-to-content">${repliedContent}</div>
28
+ </div>
29
+ `;
30
+ }
31
+
32
+ // Build file preview HTML if file exists
33
+ let filePreviewHtml = "";
34
+ if (message.fileSrc) {
35
+ const displayFileName = message.fileSrc.length > 20 ? message.fileSrc.substring(0, 17) + "..." : message.fileSrc;
36
+ filePreviewHtml = `
37
+ <a href="${message.fileSrc}" target="_blank" rel="noopener noreferrer" class="mw-file-preview" title="دانلود فایل">
38
+ <div class="mw-file-preview-icon">
39
+ ${'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-8-6m4 18H6V4h7v5h5v11z"/></svg>'}
40
+ </div>
41
+ <div class="mw-file-preview-info">
42
+ <div class="mw-file-preview-name">${displayFileName}</div>
43
+ <div class="mw-file-preview-type">${"file"}</div>
44
+ </div>
45
+ </a>
46
+ `;
47
+ }
48
+
49
+ element.innerHTML = `
50
+ <div class="mw-chat-message ${message.type === "USER" ? "mw-chat-message-user" : "mw-chat-message-supporter"}">
51
+ ${repliedToHtml}
52
+ ${filePreviewHtml}
53
+ <div class="mw-message-content">${marked.parse(message.content) as string}</div>
54
+ </div>
55
+ <div class="mw-message-footer">
56
+ ${
57
+ message.type !== "USER"
58
+ ? `
59
+ <div class="mw-message-feedback">
60
+ <button class="mw-feedback-btn mw-feedback-dislike" data-message-id="${message.id}" title="مفید نبود">
61
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none"><path fill="currentColor" d="m15 14l-.986.164A1 1 0 0 1 15 13zM4 14v1a1 1 0 0 1-1-1zm16.522-2.392l.98-.196zM6 3h11.36v2H6zm12.56 12H15v-2h3.56zm-2.573-1.164l.805 4.835L14.82 19l-.806-4.836zM14.82 21h-.214v-2h.214zm-3.543-1.781l-2.515-3.774l1.664-1.11l2.516 3.774zM7.93 15H4v-2h3.93zM3 14V6h2v8zm17.302-8.588l1.2 6l-1.96.392l-1.2-6zM8.762 15.445A1 1 0 0 0 7.93 15v-2a3 3 0 0 1 2.496 1.336zm8.03 3.226A2 2 0 0 1 14.82 21v-2zM18.56 13a1 1 0 0 0 .981-1.196l1.961-.392A3 3 0 0 1 18.561 15zm-1.2-10a3 3 0 0 1 2.942 2.412l-1.96.392A1 1 0 0 0 17.36 5zm-2.754 18a4 4 0 0 1-3.328-1.781l1.664-1.11a2 2 0 0 0 1.664.891zM6 5a1 1 0 0 0-1 1H3a3 3 0 0 1 3-3z"/><path stroke="currentColor" stroke-width="2" d="M8 14V4"/></g></svg>
62
+ </button>
63
+ <button class="mw-feedback-btn mw-feedback-like" data-message-id="${message.id}" title="مفید بود">
64
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none"><path fill="currentColor" d="m15 10l-.74-.123a.75.75 0 0 0 .74.873zM4 10v-.75a.75.75 0 0 0-.75.75zm16.522 2.392l.735.147zM6 20.75h11.36v-1.5H6zm12.56-11.5H15v1.5h3.56zm-2.82.873l.806-4.835l-1.48-.247l-.806 4.836zm-.92-6.873h-.214v1.5h.213zm-3.335 1.67L8.97 8.693l1.248.832l2.515-3.773zM7.93 9.25H4v1.5h3.93zM3.25 10v8h1.5v-8zm16.807 8.54l1.2-6l-1.47-.295l-1.2 6zM8.97 8.692a1.25 1.25 0 0 1-1.04.557v1.5c.92 0 1.778-.46 2.288-1.225zm7.576-3.405A1.75 1.75 0 0 0 14.82 3.25v1.5a.25.25 0 0 1 .246.291zm2.014 5.462c.79 0 1.38.722 1.226 1.495l1.471.294A2.75 2.75 0 0 0 18.56 9.25zm-1.2 10a2.75 2.75 0 0 0 2.697-2.21l-1.47-.295a1.25 1.25 0 0 1-1.227 1.005zm-2.754-17.5a3.75 3.75 0 0 0-3.12 1.67l1.247.832a2.25 2.25 0 0 1 1.873-1.002zM6 19.25c-.69 0-1.25-.56-1.25-1.25h-1.5A2.75 2.75 0 0 0 6 20.75z"/><path stroke="currentColor" stroke-width="1.5" d="M8 10v10"/></g></svg>
65
+ </button>
66
+ </div>
67
+ `
68
+ : ""
69
+ }
70
+ <div class="mw-message-time">${timeString}</div>
71
+ </div>
72
+ `;
73
+
74
+ element.className = `mw-message-wrapper ${message.type === "USER" ? "mw-message-wrapper-user" : "mw-message-wrapper-supporter"}`;
75
+ widget?.chat?.conversation.containerElement?.appendChild(element);
76
+
77
+ addMessageElementListeners(message);
78
+
79
+ // Add feedback event listeners for non-user messages
80
+ if (message.type !== "USER") {
81
+ addMessageFeedbackListeners(message);
82
+ }
83
+
84
+ // Add clicked listener for replied-to preview
85
+ if (message.repliedTo) {
86
+ addMessageRepliedToListener(message);
87
+ }
88
+ };
89
+
90
+ const addMessageElementListeners: MessageRelatedUtil = message => {
91
+ const widget = window.getMWidget?.();
92
+ const element = getMessageElement(message);
93
+
94
+ element?.addEventListener("dblclick", () => {
95
+ widget?.chat.replyMaster.setReply(message);
96
+ });
97
+ };
98
+
99
+ const addMessageFeedbackListeners: MessageRelatedUtil = message => {
100
+ const element = getMessageElement(message);
101
+ const likeBtn = element?.querySelector(".mw-feedback-like") as HTMLButtonElement;
102
+ const dislikeBtn = element?.querySelector(".mw-feedback-dislike") as HTMLButtonElement;
103
+
104
+ if (likeBtn) {
105
+ likeBtn.addEventListener("click", () => {
106
+ sendMessageFeedBack(message, true);
107
+ });
108
+ }
109
+
110
+ if (dislikeBtn) {
111
+ dislikeBtn.addEventListener("click", () => {
112
+ sendMessageFeedBack(message, false);
113
+ });
114
+ }
115
+ };
116
+
117
+ const sendMessageFeedBack: MessageRelatedUtil<[liked: boolean]> = async (message, liked) => {
118
+ const element = getMessageElement(message);
119
+ if (message.hasFeedback) return; // Prevent multiple feedback submissions
120
+ disableMessageFeedbackButtons(message);
121
+ try {
122
+ await message.sendFeedBack(liked);
123
+ const likeBtn = element?.querySelector(".mw-feedback-like") as HTMLButtonElement;
124
+ const dislikeBtn = element?.querySelector(".mw-feedback-dislike") as HTMLButtonElement;
125
+
126
+ if (liked && likeBtn) {
127
+ likeBtn.classList.add("mw-feedback-active");
128
+ } else if (!liked && dislikeBtn) {
129
+ dislikeBtn.classList.add("mw-feedback-active");
130
+ }
131
+ } catch {
132
+ enableMessageFeedbackButtons(message);
133
+ }
134
+ };
135
+
136
+ const disableMessageFeedbackButtons: MessageRelatedUtil = message => {
137
+ const element = getMessageElement(message);
138
+ const likeBtn = element?.querySelector(".mw-feedback-like") as HTMLButtonElement;
139
+ const dislikeBtn = element?.querySelector(".mw-feedback-dislike") as HTMLButtonElement;
140
+
141
+ if (likeBtn) {
142
+ likeBtn.disabled = true;
143
+ likeBtn.classList.add("mw-feedback-disabled");
144
+ }
145
+
146
+ if (dislikeBtn) {
147
+ dislikeBtn.disabled = true;
148
+ dislikeBtn.classList.add("mw-feedback-disabled");
149
+ }
150
+ };
151
+
152
+ const addMessageRepliedToListener: MessageRelatedUtil = message => {
153
+ const element = getMessageElement(message);
154
+
155
+ const repliedToEl = getMessageElement(message.repliedTo! || {});
156
+ const repliedToPreview = element?.querySelector(".mw-replied-to-preview") as HTMLDivElement;
157
+ if (repliedToPreview && repliedToEl) {
158
+ repliedToPreview.addEventListener("click", () => {
159
+ // Scroll to the replied message
160
+ repliedToEl?.scrollIntoView({behavior: "smooth", block: "center"});
161
+
162
+ // Add highlight effect
163
+ repliedToEl?.classList.add("mw-message-highlight");
164
+ setTimeout(() => {
165
+ repliedToEl?.classList.remove("mw-message-highlight");
166
+ }, 2000);
167
+ });
168
+ }
169
+ };
170
+
171
+ const getMessageElement: MessageRelatedUtil<[], HTMLDivElement | undefined | null> = message => {
172
+ const widget = window.getMWidget?.();
173
+ return widget?.chat.conversation.containerElement?.querySelector(`#message-${message.id}`);
174
+ };
175
+
176
+ const showMessageTooltip: MessageRelatedUtil = message => {
177
+ const widget = window.getMWidget?.();
178
+ const tooltip = widget?.container?.querySelector(".mw-toggle-tooltip");
179
+ const tooltipText = widget?.container?.querySelector(".mw-toggle-tooltip-text");
180
+ if (tooltip && tooltipText) {
181
+ // Show the tooltip
182
+ tooltip.classList.remove("mw-hidden");
183
+
184
+ // Update tooltip text with message preview
185
+ const preview = message.content.length > 50 ? message.content.substring(0, 50) + "..." : message.content;
186
+ tooltipText.textContent = preview;
187
+
188
+ // Auto-hide after 3 seconds
189
+ setTimeout(() => {
190
+ tooltip.classList.add("mw-hidden");
191
+ }, 3000);
192
+ }
193
+ };
194
+
195
+ const enableMessageFeedbackButtons: MessageRelatedUtil = message => {
196
+ const element = getMessageElement(message);
197
+ const likeBtn = element?.querySelector(".mw-feedback-like") as HTMLButtonElement;
198
+ const dislikeBtn = element?.querySelector(".mw-feedback-dislike") as HTMLButtonElement;
199
+
200
+ if (likeBtn) {
201
+ likeBtn.disabled = false;
202
+ likeBtn.classList.remove("mw-feedback-disabled");
203
+ }
204
+
205
+ if (dislikeBtn) {
206
+ dislikeBtn.disabled = false;
207
+ dislikeBtn.classList.remove("mw-feedback-disabled");
208
+ }
209
+ };
210
+
211
+ export {
212
+ initMessageElement,
213
+ addMessageElementListeners,
214
+ addMessageFeedbackListeners,
215
+ sendMessageFeedBack,
216
+ disableMessageFeedbackButtons,
217
+ addMessageRepliedToListener,
218
+ getMessageElement,
219
+ showMessageTooltip,
220
+ enableMessageFeedbackButtons
221
+ };
@@ -0,0 +1,139 @@
1
+ import {Conversation} from "#src/models/conversation.js";
2
+ import {ChatClient, ConversationMessage, EventType} from "@modochats/chat-client";
3
+ import {onSocketConnectionUpdate} from "../socket/utils.js";
4
+
5
+ class Chat {
6
+ private instance?: ChatClient;
7
+ fileMaster: CFileMaster;
8
+ replyMaster: CReplyMaster;
9
+ conversation: Conversation;
10
+ constructor() {
11
+ this.fileMaster = new CFileMaster();
12
+ this.replyMaster = new CReplyMaster();
13
+ this.conversation = new Conversation();
14
+ }
15
+ initInstance() {
16
+ const widget = window.getMWidget?.();
17
+ const savedUUid = localStorage.getItem(`modo-chat:${widget?.chatbot?.uuid}-conversation-uuid`);
18
+ this.instance = new ChatClient({
19
+ chatbotUuid: widget?.chatbot?.uuid as string,
20
+ userData: {uuid: widget?.customerData.uniqueId as string, phoneNumber: widget?.customerData.phoneNumber},
21
+ conversationUUid: savedUUid || undefined
22
+ });
23
+ this.instance.on(EventType.CONVERSATION_SYSTEM_MESSAGE, ev => {
24
+ this.conversation.addSystemMessage(ev.message);
25
+ });
26
+ this.instance.on(EventType.CONVERSATION_MESSAGE, ev => {
27
+ this.conversation.addMessage(ev.message, {incoming: ev.incoming});
28
+ });
29
+ this.instance.on(EventType.SOCKET_CONNECTED, ev => {
30
+ onSocketConnectionUpdate(true);
31
+ });
32
+ this.instance.on(EventType.SOCKET_DISCONNECTED, ev => {
33
+ onSocketConnectionUpdate(false);
34
+ });
35
+ this.instance.on(EventType.CONVERSATION_LOAD, ev => {
36
+ this.conversation.clearContainerEl();
37
+ this.conversation.setStatus();
38
+ this.conversation.onInit();
39
+
40
+ localStorage.setItem(`modo-chat:${widget?.chatbot?.uuid}-conversation-uuid`, this.instance?.conversation?.uuid as string);
41
+ });
42
+ this.instance.on(EventType.CONVERSATION_MESSAGES_CLEAR, () => {
43
+ this.conversation.clearContainerEl();
44
+ });
45
+ }
46
+ get socket() {
47
+ return this.instance?.socket;
48
+ }
49
+ // conversation data , since the instance only stores and handles data ^^
50
+ get conversationD() {
51
+ return this.instance?.conversation;
52
+ }
53
+ clear() {
54
+ this.conversation.clear();
55
+ this.instance?.clearConversation();
56
+ }
57
+
58
+ sendMessage(...args: Parameters<ChatClient["sendMessage"]>) {
59
+ return this.instance?.sendMessage(...args);
60
+ }
61
+ }
62
+ class CFileMaster {
63
+ file?: File;
64
+
65
+ clearFile() {
66
+ this.file = undefined;
67
+ this.toggleUiState();
68
+ }
69
+ setFile(file: File) {
70
+ this.file = file;
71
+ this.toggleUiState();
72
+ }
73
+ toggleUiState() {
74
+ const container = window.getMWidget?.().container;
75
+ const fileUploadBtn = container?.querySelector(".mw-file-upload-btn") as HTMLButtonElement;
76
+ const fileInput = container?.querySelector(".mw-file-input") as HTMLInputElement;
77
+ const uploadIcon = container?.querySelector(".mw-file-upload-icon") as SVGElement;
78
+ const removeIcon = container?.querySelector(".mw-file-remove-icon") as SVGElement;
79
+
80
+ if (this.file) {
81
+ uploadIcon.classList.add("mw-hidden");
82
+ removeIcon.classList.remove("mw-hidden");
83
+ fileUploadBtn.classList.add("mw-file-uploaded");
84
+ } else {
85
+ fileInput.value = "";
86
+ uploadIcon.classList.remove("mw-hidden");
87
+ removeIcon.classList.add("mw-hidden");
88
+ fileUploadBtn.classList.remove("mw-file-uploaded");
89
+ }
90
+ }
91
+ }
92
+
93
+ class CReplyMaster {
94
+ replyingTo?: ConversationMessage;
95
+
96
+ setReply(message: ConversationMessage) {
97
+ this.replyingTo = message;
98
+ this.updateReplyUI();
99
+ }
100
+
101
+ clearReply() {
102
+ this.replyingTo = undefined;
103
+
104
+ this.updateReplyUI();
105
+ }
106
+
107
+ private updateReplyUI() {
108
+ const container = window.getMWidget?.().container;
109
+ const replyPreview = container?.querySelector(".mw-reply-preview") as HTMLDivElement;
110
+ const replyText = container?.querySelector(".mw-reply-preview-text") as HTMLElement;
111
+ const chatMessagesContainer = container?.querySelector(".mw-chat-messages-con") as HTMLDivElement;
112
+
113
+ if (this.replyingTo) {
114
+ // Show reply preview
115
+ if (replyPreview && replyText) {
116
+ // Truncate content to 50 chars
117
+ const content = this.replyingTo.content.length > 50 ? this.replyingTo.content.substring(0, 50) + "..." : this.replyingTo.content;
118
+ replyText.textContent = content;
119
+ replyPreview.classList.remove("mw-hidden");
120
+
121
+ // Add reply active class to messages container
122
+ if (chatMessagesContainer) {
123
+ chatMessagesContainer.classList.add("mw-reply-active");
124
+ }
125
+ }
126
+ } else {
127
+ // Hide reply preview
128
+ if (replyPreview) {
129
+ replyPreview.classList.add("mw-hidden");
130
+
131
+ // Remove reply active class from messages container
132
+ if (chatMessagesContainer) {
133
+ chatMessagesContainer.classList.remove("mw-reply-active");
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ export {Chat};