@product7/product7-js 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 (58) hide show
  1. package/README.md +1025 -0
  2. package/dist/README.md +1025 -0
  3. package/dist/product7-js.js +14658 -0
  4. package/dist/product7-js.js.map +1 -0
  5. package/dist/product7-js.min.js +2 -0
  6. package/dist/product7-js.min.js.map +1 -0
  7. package/package.json +114 -0
  8. package/src/api/mock-data/index.js +360 -0
  9. package/src/api/services/ChangelogService.js +28 -0
  10. package/src/api/services/FeedbackService.js +44 -0
  11. package/src/api/services/HelpService.js +50 -0
  12. package/src/api/services/MessengerService.js +279 -0
  13. package/src/api/services/SurveyService.js +127 -0
  14. package/src/api/utils/helpers.js +30 -0
  15. package/src/core/APIService.js +303 -0
  16. package/src/core/BaseAPIService.js +298 -0
  17. package/src/core/EventBus.js +54 -0
  18. package/src/core/Product7.js +812 -0
  19. package/src/core/WebSocketService.js +275 -0
  20. package/src/docs/api.md +226 -0
  21. package/src/docs/example.md +461 -0
  22. package/src/docs/framework-integrations.md +714 -0
  23. package/src/docs/installation.md +281 -0
  24. package/src/index.js +894 -0
  25. package/src/styles/base.js +50 -0
  26. package/src/styles/changelog.js +665 -0
  27. package/src/styles/components.js +553 -0
  28. package/src/styles/design-tokens.js +124 -0
  29. package/src/styles/feedback.js +325 -0
  30. package/src/styles/messenger-components.js +632 -0
  31. package/src/styles/messenger-core.js +233 -0
  32. package/src/styles/messenger-features.js +169 -0
  33. package/src/styles/messenger-views.js +877 -0
  34. package/src/styles/messenger.js +17 -0
  35. package/src/styles/messengerCustomStyles.js +114 -0
  36. package/src/styles/styles.js +26 -0
  37. package/src/styles/survey.js +894 -0
  38. package/src/utils/errors.js +142 -0
  39. package/src/utils/helpers.js +219 -0
  40. package/src/widgets/BaseWidget.js +548 -0
  41. package/src/widgets/ButtonWidget.js +104 -0
  42. package/src/widgets/ChangelogWidget.js +615 -0
  43. package/src/widgets/InlineWidget.js +148 -0
  44. package/src/widgets/MessengerWidget.js +979 -0
  45. package/src/widgets/SurveyWidget.js +1325 -0
  46. package/src/widgets/TabWidget.js +45 -0
  47. package/src/widgets/WidgetFactory.js +70 -0
  48. package/src/widgets/messenger/MessengerState.js +323 -0
  49. package/src/widgets/messenger/components/MessengerLauncher.js +124 -0
  50. package/src/widgets/messenger/components/MessengerPanel.js +111 -0
  51. package/src/widgets/messenger/components/NavigationTabs.js +130 -0
  52. package/src/widgets/messenger/views/ChangelogView.js +167 -0
  53. package/src/widgets/messenger/views/ChatView.js +592 -0
  54. package/src/widgets/messenger/views/ConversationsView.js +244 -0
  55. package/src/widgets/messenger/views/HelpView.js +239 -0
  56. package/src/widgets/messenger/views/HomeView.js +300 -0
  57. package/src/widgets/messenger/views/PreChatFormView.js +109 -0
  58. package/types/index.d.ts +341 -0
@@ -0,0 +1,979 @@
1
+ import { WebSocketService } from '../core/WebSocketService.js';
2
+ import {
3
+ applyMessengerCustomStyles,
4
+ removeMessengerCustomStyles,
5
+ } from '../styles/messengerCustomStyles.js';
6
+ import { BaseWidget } from './BaseWidget.js';
7
+ import { MessengerState } from './messenger/MessengerState.js';
8
+ import { MessengerLauncher } from './messenger/components/MessengerLauncher.js';
9
+ import { MessengerPanel } from './messenger/components/MessengerPanel.js';
10
+ import { ChangelogView } from './messenger/views/ChangelogView.js';
11
+ import { ChatView } from './messenger/views/ChatView.js';
12
+ import { ConversationsView } from './messenger/views/ConversationsView.js';
13
+ import { HelpView } from './messenger/views/HelpView.js';
14
+ import { HomeView } from './messenger/views/HomeView.js';
15
+ import { PreChatFormView } from './messenger/views/PreChatFormView.js';
16
+
17
+ export class MessengerWidget extends BaseWidget {
18
+ constructor(options) {
19
+ super({ ...options, type: 'messenger' });
20
+ const resolvedTheme = options.theme || 'light';
21
+ const hasExplicitTextColor = Object.prototype.hasOwnProperty.call(
22
+ options,
23
+ 'textColor'
24
+ );
25
+ const hasExplicitBackgroundColor = Object.prototype.hasOwnProperty.call(
26
+ options,
27
+ 'backgroundColor'
28
+ );
29
+ const defaultTextColor = resolvedTheme === 'dark' ? '#E2E8F0' : '#1d1d1f';
30
+ const defaultBackgroundColor =
31
+ resolvedTheme === 'dark' ? '#0F172A' : '#ffffff';
32
+
33
+ const resolvedEnableChangelog =
34
+ typeof options.enableChangelog === 'boolean'
35
+ ? options.enableChangelog
36
+ : options.enableNews !== false;
37
+
38
+ this.messengerOptions = {
39
+ position: options.position || 'right',
40
+ theme: resolvedTheme,
41
+ primaryColor: options.primaryColor || '#155EEF',
42
+ textColor: hasExplicitTextColor ? options.textColor : defaultTextColor,
43
+ backgroundColor: hasExplicitBackgroundColor
44
+ ? options.backgroundColor
45
+ : defaultBackgroundColor,
46
+ logoUrl: options.logoUrl || 'https://product7.io/p7logo.svg',
47
+ teamName: options.teamName || 'Support',
48
+ teamAvatars: options.teamAvatars || [],
49
+ greetingMessage: options.greetingMessage || 'Hi there 👋',
50
+ welcomeMessage: options.welcomeMessage || 'How can we help?',
51
+ enableHelp: options.enableHelp !== false,
52
+ enableChangelog: resolvedEnableChangelog,
53
+ autoLoadData: options.autoLoadData !== false,
54
+ initialView: options.initialView || 'home',
55
+ previewData: options.previewData || null,
56
+ featuredContent: options.featuredContent || null,
57
+ feedbackUrl: options.feedbackUrl || null,
58
+ changelogUrl: options.changelogUrl || null,
59
+ helpUrl: options.helpUrl || null,
60
+ roadmapUrl: options.roadmapUrl || null,
61
+ onSendMessage: options.onSendMessage || null,
62
+ onFeedbackClick: options.onFeedbackClick || null,
63
+ onArticleClick: options.onArticleClick || null,
64
+ onChangelogClick: options.onChangelogClick || null,
65
+ };
66
+
67
+ this.messengerState = new MessengerState({
68
+ teamName: this.messengerOptions.teamName,
69
+ teamAvatars: this.messengerOptions.teamAvatars,
70
+ greetingMessage: this.messengerOptions.greetingMessage,
71
+ welcomeMessage: this.messengerOptions.welcomeMessage,
72
+ enableHelp: this.messengerOptions.enableHelp,
73
+ enableChangelog: this.messengerOptions.enableChangelog,
74
+ metadata: this.sdk?.apiService?.getMetadata() || null,
75
+ urls: {
76
+ feedback: this.messengerOptions.feedbackUrl,
77
+ changelog: this.messengerOptions.changelogUrl,
78
+ help: this.messengerOptions.helpUrl,
79
+ roadmap: this.messengerOptions.roadmapUrl,
80
+ },
81
+ });
82
+
83
+ this.launcher = null;
84
+ this.panel = null;
85
+ this.wsService = null;
86
+ this._wsUnsubscribers = [];
87
+ this._feedbackWidget = null;
88
+
89
+ this._handleOpenChange = this._handleOpenChange.bind(this);
90
+ this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
91
+ this._handleTypingStarted = this._handleTypingStarted.bind(this);
92
+ this._handleTypingStopped = this._handleTypingStopped.bind(this);
93
+ this._handleConversationClosed = this._handleConversationClosed.bind(this);
94
+ }
95
+
96
+ _createInternalFeedbackWidget() {
97
+ try {
98
+ const widget = this.sdk.createWidget('button', {
99
+ trigger: false,
100
+ displayMode: 'modal',
101
+ boardName: this.sdk.config.boardName,
102
+ primaryColor: this.messengerOptions.primaryColor,
103
+ theme: this.messengerOptions.theme,
104
+ });
105
+ widget.mount();
106
+ return widget;
107
+ } catch (e) {
108
+ console.warn('[Messenger] Could not create internal feedback widget:', e);
109
+ return null;
110
+ }
111
+ }
112
+
113
+ _render() {
114
+ const container = document.createElement('div');
115
+ container.className = `messenger-widget theme-${this.messengerOptions.theme}`;
116
+ container.style.zIndex = '999999';
117
+
118
+ applyMessengerCustomStyles({
119
+ primaryColor: this.messengerOptions.primaryColor,
120
+ textColor: this.messengerOptions.textColor,
121
+ backgroundColor: this.messengerOptions.backgroundColor,
122
+ theme: this.messengerOptions.theme,
123
+ });
124
+
125
+ // Create internal feedback widget (no floating button) so the
126
+ // "Leave a feedback" button in the home view can open it inline.
127
+ if (!this.messengerOptions.onFeedbackClick) {
128
+ this._feedbackWidget = this._createInternalFeedbackWidget();
129
+ }
130
+
131
+ this.launcher = new MessengerLauncher(this.messengerState, {
132
+ position: this.messengerOptions.position,
133
+ primaryColor: this.messengerOptions.primaryColor,
134
+ });
135
+ container.appendChild(this.launcher.render());
136
+
137
+ this.panel = new MessengerPanel(this.messengerState, {
138
+ position: this.messengerOptions.position,
139
+ theme: this.messengerOptions.theme,
140
+ primaryColor: this.messengerOptions.primaryColor,
141
+ logoUrl: this.messengerOptions.logoUrl,
142
+ featuredContent: this.messengerOptions.featuredContent,
143
+ onSendMessage:
144
+ this.messengerOptions.onSendMessage ||
145
+ this._handleSendMessage.bind(this),
146
+ onStartConversation: this._handleStartConversation.bind(this),
147
+ onTyping: this.sendTypingIndicator.bind(this),
148
+ onSelectConversation: this._handleSelectConversation.bind(this),
149
+ onStartNewConversation: this._handleNewConversationClick.bind(this),
150
+ onIdentifyContact: this._handleIdentifyContact.bind(this),
151
+ onFeedbackClick:
152
+ this.messengerOptions.onFeedbackClick ||
153
+ (this._feedbackWidget ? () => this._feedbackWidget.open() : null),
154
+ onArticleClick: this.messengerOptions.onArticleClick,
155
+ onChangelogClick: this.messengerOptions.onChangelogClick,
156
+ });
157
+
158
+ this.panel.registerView('home', HomeView);
159
+ this.panel.registerView('messages', ConversationsView);
160
+ this.panel.registerView('chat', ChatView);
161
+ this.panel.registerView('prechat', PreChatFormView);
162
+ this.panel.registerView('help', HelpView);
163
+ this.panel.registerView('changelog', ChangelogView);
164
+
165
+ container.appendChild(this.panel.render());
166
+ this.panel.hide();
167
+
168
+ return container;
169
+ }
170
+
171
+ _attachEvents() {
172
+ this._stateUnsubscribe = this.messengerState.subscribe((type, data) => {
173
+ if (type === 'openChange') {
174
+ this._handleOpenChange(data.isOpen);
175
+ }
176
+ if (type === 'conversationChange') {
177
+ this._handleActiveConversationChange(
178
+ data.conversationId,
179
+ data.previousConversationId
180
+ );
181
+ }
182
+ });
183
+ }
184
+
185
+ _handleOpenChange(isOpen) {
186
+ if (isOpen) {
187
+ this.panel.show();
188
+ this.sdk.eventBus.emit('messenger:opened', { widget: this });
189
+ } else {
190
+ this.panel.hide();
191
+ this.messengerState.reset();
192
+ this.sdk.eventBus.emit('messenger:closed', { widget: this });
193
+ }
194
+ }
195
+
196
+ _handleActiveConversationChange(conversationId, previousConversationId) {
197
+ if (previousConversationId && this.wsService) {
198
+ this.wsService.send('conversation:unsubscribe', {
199
+ conversation_id: previousConversationId,
200
+ });
201
+ }
202
+ if (conversationId && this.wsService) {
203
+ this.wsService.send('conversation:subscribe', {
204
+ conversation_id: conversationId,
205
+ });
206
+ }
207
+ }
208
+
209
+ async _handleStartConversation(messageContent, pendingAttachments) {
210
+ try {
211
+ if (!this.messengerState.isIdentified) {
212
+ this.messengerState.pendingMessage = {
213
+ content: messageContent,
214
+ attachments: pendingAttachments,
215
+ };
216
+ this.messengerState.setView('prechat');
217
+ return null;
218
+ }
219
+
220
+ const openConversation = this.messengerState.conversations.find(
221
+ (c) => c.status === 'open'
222
+ );
223
+
224
+ if (openConversation) {
225
+ this.messengerState.setActiveConversation(openConversation.id);
226
+ await this._handleSendMessage(
227
+ openConversation.id,
228
+ { content: messageContent },
229
+ pendingAttachments
230
+ );
231
+ return openConversation;
232
+ }
233
+
234
+ return await this.startNewConversation(
235
+ messageContent,
236
+ '',
237
+ pendingAttachments
238
+ );
239
+ } catch (error) {
240
+ console.error('[MessengerWidget] Failed to start conversation:', error);
241
+ return null;
242
+ }
243
+ }
244
+
245
+ async _handleSelectConversation(conversationId) {
246
+ try {
247
+ await this.fetchMessages(conversationId);
248
+ } catch (error) {
249
+ console.error('[MessengerWidget] Failed to fetch messages:', error);
250
+ }
251
+ }
252
+
253
+ _handleNewConversationClick() {
254
+ const openConversation = this.messengerState.conversations.find(
255
+ (c) => c.status === 'open'
256
+ );
257
+
258
+ if (openConversation) {
259
+ this.messengerState.setActiveConversation(openConversation.id);
260
+ this.messengerState.setView('chat');
261
+ this._handleSelectConversation(openConversation.id);
262
+ } else {
263
+ this.messengerState.setActiveConversation(null);
264
+ this.messengerState.setView('chat');
265
+ }
266
+ }
267
+
268
+ async _handleIdentifyContact(contactData) {
269
+ try {
270
+ const response = await this.apiService.identifyContact({
271
+ name: contactData.name,
272
+ email: contactData.email,
273
+ });
274
+
275
+ if (response.status) {
276
+ console.log(
277
+ '[MessengerWidget] Contact identified:',
278
+ response.data.contact_id
279
+ );
280
+ this.messengerState.setIdentified(true, {
281
+ name: contactData.name,
282
+ email: contactData.email,
283
+ });
284
+ }
285
+
286
+ return response;
287
+ } catch (error) {
288
+ console.error('[MessengerWidget] Failed to identify contact:', error);
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ markAsIdentified(name, email) {
294
+ this.messengerState.setIdentified(true, { name, email });
295
+ console.log('[MessengerWidget] Marked as identified:', email);
296
+ }
297
+
298
+ async _handleUploadFile(base64Data, filename) {
299
+ try {
300
+ const response = await this.apiService.uploadFile(base64Data, filename);
301
+ if (response.status && response.url) {
302
+ return response.url;
303
+ }
304
+ throw new Error('Upload failed');
305
+ } catch (error) {
306
+ console.error('[MessengerWidget] Failed to upload file:', error);
307
+ throw error;
308
+ }
309
+ }
310
+
311
+ async _uploadPendingAttachments(pendingAttachments) {
312
+ if (!pendingAttachments || pendingAttachments.length === 0) return [];
313
+
314
+ const uploaded = [];
315
+ for (const att of pendingAttachments) {
316
+ try {
317
+ const cdnUrl = await this._handleUploadFile(att.preview, att.file.name);
318
+ uploaded.push({
319
+ url: cdnUrl,
320
+ type: att.type.startsWith('image') ? 'image' : 'file',
321
+ name: att.file.name,
322
+ });
323
+ } catch (err) {
324
+ console.error(
325
+ '[MessengerWidget] Skipping failed attachment upload:',
326
+ att.file.name,
327
+ err
328
+ );
329
+ }
330
+ }
331
+ return uploaded;
332
+ }
333
+
334
+ async _handleSendMessage(conversationId, message, pendingAttachments) {
335
+ this.sdk.eventBus.emit('messenger:messageSent', {
336
+ widget: this,
337
+ conversationId,
338
+ message,
339
+ });
340
+
341
+ try {
342
+ const uploadedAttachments =
343
+ await this._uploadPendingAttachments(pendingAttachments);
344
+
345
+ const response = await this.apiService.sendMessage(conversationId, {
346
+ content: message.content,
347
+ attachments: uploadedAttachments,
348
+ });
349
+
350
+ if (response.status && response.data) {
351
+ console.log('[MessengerWidget] Message sent:', response.data.id);
352
+ }
353
+
354
+ if (this.apiService?.mock) {
355
+ setTimeout(() => {
356
+ const mockResponse = {
357
+ id: 'msg_' + Date.now(),
358
+ content: "Thanks for your message! We'll get back to you soon.",
359
+ isOwn: false,
360
+ timestamp: new Date().toISOString(),
361
+ sender: {
362
+ name: 'Support Team',
363
+ avatarUrl: null,
364
+ },
365
+ };
366
+ this.messengerState.addMessage(conversationId, mockResponse);
367
+ }, 1500);
368
+ }
369
+ } catch (error) {
370
+ console.error('[MessengerWidget] Failed to send message:', error);
371
+ }
372
+ }
373
+
374
+ _handleWebSocketMessage(data) {
375
+ const { conversation_id, message } = data;
376
+
377
+ let attachments = [];
378
+ if (message.attachments) {
379
+ try {
380
+ attachments =
381
+ typeof message.attachments === 'string'
382
+ ? JSON.parse(message.attachments)
383
+ : message.attachments;
384
+ } catch (e) {
385
+ // ignore
386
+ }
387
+ }
388
+
389
+ const localMessage = {
390
+ id: message.id,
391
+ content: message.content,
392
+ isOwn: message.sender_type === 'customer',
393
+ timestamp: message.created_at,
394
+ attachments: attachments.length > 0 ? attachments : undefined,
395
+ sender: {
396
+ name: message.sender_name || 'Support',
397
+ avatarUrl: message.sender_avatar || null,
398
+ },
399
+ };
400
+
401
+ this.messengerState.upsertMessage(conversation_id, localMessage, {
402
+ reconcileOwnOptimistic: true,
403
+ optimisticMatchWindowMs: 30000,
404
+ });
405
+
406
+ if (
407
+ !this.messengerState.isOpen ||
408
+ this.messengerState.activeConversationId !== conversation_id
409
+ ) {
410
+ this._updateUnreadCount();
411
+ }
412
+ }
413
+
414
+ _handleTypingStarted(data) {
415
+ if (data.is_agent) {
416
+ this.messengerState._notify('typingStarted', {
417
+ conversationId: data.conversation_id,
418
+ userName: data.user_name,
419
+ });
420
+ }
421
+ }
422
+
423
+ _handleTypingStopped(data) {
424
+ this.messengerState._notify('typingStopped', {
425
+ conversationId: data.conversation_id,
426
+ });
427
+ }
428
+
429
+ _handleConversationClosed(data) {
430
+ const conversationId =
431
+ data?.conversation_id || data?.id || data?.conversation?.id;
432
+ if (!conversationId) return;
433
+
434
+ this.messengerState.updateConversation(conversationId, {
435
+ status: 'closed',
436
+ });
437
+ }
438
+
439
+ async _updateUnreadCount() {
440
+ try {
441
+ const response = await this.apiService.getUnreadCount();
442
+ if (response.status && response.data) {
443
+ this.messengerState.unreadCount = response.data.unread_count || 0;
444
+ this.messengerState._notify('unreadCountChange', {
445
+ count: this.messengerState.unreadCount,
446
+ });
447
+ }
448
+ } catch (error) {
449
+ console.error('[MessengerWidget] Failed to get unread count:', error);
450
+ }
451
+ }
452
+
453
+ _initWebSocket() {
454
+ if (this.wsService) {
455
+ this.wsService.disconnect();
456
+ }
457
+
458
+ this.wsService = new WebSocketService({
459
+ baseURL: this.apiService.baseURL,
460
+ workspace: this.apiService.workspace,
461
+ sessionToken: this.apiService.sessionToken,
462
+ mock: this.apiService.mock,
463
+ });
464
+
465
+ this._wsUnsubscribers.push(
466
+ this.wsService.on('message', this._handleWebSocketMessage)
467
+ );
468
+ this._wsUnsubscribers.push(
469
+ this.wsService.on('typing_started', this._handleTypingStarted)
470
+ );
471
+ this._wsUnsubscribers.push(
472
+ this.wsService.on('typing_stopped', this._handleTypingStopped)
473
+ );
474
+ this._wsUnsubscribers.push(
475
+ this.wsService.on('conversation_closed', this._handleConversationClosed)
476
+ );
477
+ this._wsUnsubscribers.push(
478
+ this.wsService.on('connected', () => {
479
+ console.log('[MessengerWidget] WebSocket connected');
480
+ if (this.messengerState.activeConversationId) {
481
+ this.wsService.send('conversation:subscribe', {
482
+ conversation_id: this.messengerState.activeConversationId,
483
+ });
484
+ }
485
+ })
486
+ );
487
+ this._wsUnsubscribers.push(
488
+ this.wsService.on('disconnected', () => {
489
+ console.log('[MessengerWidget] WebSocket disconnected');
490
+ })
491
+ );
492
+
493
+ this.wsService.connect();
494
+ }
495
+
496
+ open() {
497
+ this.messengerState.setOpen(true);
498
+ }
499
+
500
+ close() {
501
+ this.messengerState.setOpen(false);
502
+ }
503
+
504
+ toggle() {
505
+ this.messengerState.setOpen(!this.messengerState.isOpen);
506
+ }
507
+
508
+ navigateTo(view) {
509
+ this.messengerState.setView(view);
510
+ if (!this.messengerState.isOpen) {
511
+ this.open();
512
+ }
513
+ }
514
+
515
+ setConversations(conversations) {
516
+ this.messengerState.setConversations(conversations);
517
+ }
518
+
519
+ addMessage(conversationId, message) {
520
+ this.messengerState.addMessage(conversationId, message);
521
+ }
522
+
523
+ setHelpArticles(articles) {
524
+ this.messengerState.setHelpArticles(articles);
525
+ }
526
+
527
+ setHomeChangelogItems(items) {
528
+ this.messengerState.setHomeChangelogItems(items);
529
+ }
530
+
531
+ setChangelogItems(items) {
532
+ this.messengerState.setChangelogItems(items);
533
+ }
534
+
535
+ setUnreadCount(count) {
536
+ this.messengerState.unreadCount = count;
537
+ this.messengerState._notify('unreadCountChange', { count });
538
+ }
539
+
540
+ getState() {
541
+ return {
542
+ isOpen: this.messengerState.isOpen,
543
+ currentView: this.messengerState.currentView,
544
+ unreadCount: this.messengerState.unreadCount,
545
+ conversations: this.messengerState.conversations,
546
+ };
547
+ }
548
+
549
+ _applyPreviewData() {
550
+ const data = this.messengerOptions.previewData;
551
+ if (!data || typeof data !== 'object') {
552
+ return;
553
+ }
554
+
555
+ if (Array.isArray(data.conversations)) {
556
+ this.messengerState.setConversations(data.conversations);
557
+ }
558
+
559
+ if (Array.isArray(data.helpArticles)) {
560
+ this.messengerState.setHelpArticles(data.helpArticles);
561
+ }
562
+
563
+ if (Array.isArray(data.homeChangelogItems)) {
564
+ this.messengerState.setHomeChangelogItems(data.homeChangelogItems);
565
+ }
566
+
567
+ if (Array.isArray(data.changelogItems)) {
568
+ this.messengerState.setChangelogItems(data.changelogItems);
569
+ }
570
+
571
+ if (typeof data.unreadCount === 'number') {
572
+ this.setUnreadCount(data.unreadCount);
573
+ }
574
+
575
+ if (data.availability && typeof data.availability === 'object') {
576
+ const availability = data.availability;
577
+ this.messengerState.agentsOnline = Boolean(
578
+ availability.agentsOnline ?? availability.agents_online
579
+ );
580
+ this.messengerState.onlineCount =
581
+ availability.onlineCount ?? availability.online_count ?? 0;
582
+ this.messengerState.responseTime =
583
+ availability.responseTime ??
584
+ availability.response_time ??
585
+ this.messengerState.responseTime;
586
+ this.messengerState._notify('availabilityUpdate', availability);
587
+ }
588
+
589
+ if (typeof data.currentView === 'string') {
590
+ this.messengerState.setView(data.currentView);
591
+ }
592
+ }
593
+
594
+ async loadInitialData() {
595
+ try {
596
+ const conversations = await this._fetchConversations();
597
+ this.messengerState.setConversations(conversations);
598
+ } catch (error) {
599
+ console.error('[MessengerWidget] Failed to load conversations:', error);
600
+ }
601
+
602
+ if (this.messengerOptions.enableHelp) {
603
+ try {
604
+ const articles = await this._fetchHelpArticles();
605
+ this.messengerState.setHelpArticles(articles);
606
+ } catch (error) {
607
+ console.error('[MessengerWidget] Failed to load help articles:', error);
608
+ }
609
+ }
610
+
611
+ if (this.messengerOptions.enableChangelog) {
612
+ try {
613
+ const { homeItems, changelogItems } = await this._fetchChangelog();
614
+ this.messengerState.setHomeChangelogItems(homeItems);
615
+ this.messengerState.setChangelogItems(changelogItems);
616
+ } catch (error) {
617
+ console.error('[MessengerWidget] Failed to load changelog:', error);
618
+ }
619
+ }
620
+ }
621
+
622
+ async _fetchConversations() {
623
+ try {
624
+ const response = await this.apiService.getConversations();
625
+ if (response.status && response.data) {
626
+ return response.data.map((conv) => ({
627
+ id: conv.id,
628
+ title:
629
+ conv.subject ||
630
+ `Chat with ${conv.assigned_user?.name || 'Support'}`,
631
+ participants: conv.assigned_user
632
+ ? [
633
+ {
634
+ name: conv.assigned_user.name,
635
+ avatarUrl: conv.assigned_user.avatar,
636
+ },
637
+ ]
638
+ : [{ name: 'Support', avatarUrl: null }],
639
+ lastMessage: conv.preview || conv.snippet || '',
640
+ lastMessageTime: conv.last_message_at,
641
+ unread: conv.unread || 0,
642
+ status: conv.status,
643
+ }));
644
+ }
645
+ return [];
646
+ } catch (error) {
647
+ console.error('[MessengerWidget] Failed to fetch conversations:', error);
648
+ return [];
649
+ }
650
+ }
651
+
652
+ async _fetchHelpArticles() {
653
+ try {
654
+ const response = await this.apiService.getHelpCollections();
655
+ if (response.success && response.data) {
656
+ const collections = response.data.collections || response.data;
657
+ const helpBase = (this.messengerOptions.helpUrl || '').replace(
658
+ /\/$/,
659
+ ''
660
+ );
661
+
662
+ const sorted = [...collections].sort(
663
+ (a, b) => (a.order ?? 0) - (b.order ?? 0)
664
+ );
665
+
666
+ return sorted.map((collection) => ({
667
+ id: collection.id,
668
+ title: collection.title,
669
+ description: collection.description || '',
670
+ articleCount: collection.article_count || 0,
671
+ icon: collection.icon || null,
672
+ author: collection.author || null,
673
+ url:
674
+ collection.url_slug && helpBase
675
+ ? `${helpBase}/collections/${collection.url_slug}`
676
+ : helpBase || null,
677
+ }));
678
+ }
679
+ return [];
680
+ } catch (error) {
681
+ console.error('[MessengerWidget] Failed to fetch help articles:', error);
682
+ return [];
683
+ }
684
+ }
685
+
686
+ async fetchMessages(conversationId) {
687
+ try {
688
+ const response = await this.apiService.getConversation(conversationId);
689
+ if (response.status && response.data) {
690
+ const messages = (response.data.messages || []).map((msg) => {
691
+ let attachments;
692
+ if (msg.attachments) {
693
+ try {
694
+ attachments =
695
+ typeof msg.attachments === 'string'
696
+ ? JSON.parse(msg.attachments)
697
+ : msg.attachments;
698
+ } catch (e) {
699
+ // ignore
700
+ }
701
+ }
702
+ return {
703
+ id: msg.id,
704
+ content: msg.content,
705
+ isOwn: msg.sender_type === 'customer',
706
+ timestamp: msg.created_at,
707
+ attachments:
708
+ attachments && attachments.length > 0 ? attachments : undefined,
709
+ sender: {
710
+ name:
711
+ msg.sender_name ||
712
+ (msg.sender_type === 'customer' ? 'You' : 'Support'),
713
+ avatarUrl: msg.sender_avatar || null,
714
+ },
715
+ };
716
+ });
717
+ this.messengerState.setMessages(conversationId, messages);
718
+
719
+ await this.apiService.markConversationAsRead(conversationId);
720
+ this.messengerState.markAsRead(conversationId);
721
+
722
+ return messages;
723
+ }
724
+ return [];
725
+ } catch (error) {
726
+ console.error('[MessengerWidget] Failed to fetch messages:', error);
727
+ return [];
728
+ }
729
+ }
730
+
731
+ async startNewConversation(message, subject = '', pendingAttachments = []) {
732
+ try {
733
+ const uploadedAttachments =
734
+ await this._uploadPendingAttachments(pendingAttachments);
735
+
736
+ const response = await this.apiService.startConversation({
737
+ message,
738
+ subject,
739
+ attachments: uploadedAttachments,
740
+ });
741
+
742
+ if (response.status && response.data) {
743
+ const conv = response.data;
744
+ const newConversation = {
745
+ id: conv.id,
746
+ title: conv.subject || 'New conversation',
747
+ participants: [{ name: 'Support', avatarUrl: null }],
748
+ lastMessage: message,
749
+ lastMessageTime: conv.created_at || new Date().toISOString(),
750
+ unread: 0,
751
+ status: 'open',
752
+ };
753
+
754
+ this.messengerState.addConversation(newConversation);
755
+
756
+ this.messengerState.setMessages(conv.id, [
757
+ {
758
+ id: 'msg_' + Date.now(),
759
+ content: message,
760
+ isOwn: true,
761
+ timestamp: new Date().toISOString(),
762
+ },
763
+ ]);
764
+
765
+ this.messengerState.setActiveConversation(conv.id);
766
+ this.messengerState.setView('chat');
767
+
768
+ return conv;
769
+ }
770
+ return null;
771
+ } catch (error) {
772
+ console.error('[MessengerWidget] Failed to start conversation:', error);
773
+ return null;
774
+ }
775
+ }
776
+
777
+ async sendTypingIndicator(conversationId, isTyping) {
778
+ try {
779
+ await this.apiService.sendTypingIndicator(conversationId, isTyping);
780
+ } catch (error) {
781
+ // silent fail
782
+ }
783
+ }
784
+
785
+ async checkAgentAvailability() {
786
+ try {
787
+ const response = await this.apiService.checkAgentsOnline();
788
+ if (response.status && response.data) {
789
+ this.messengerState.agentsOnline = response.data.agents_online;
790
+ this.messengerState.onlineCount = response.data.online_count || 0;
791
+ this.messengerState.responseTime = response.data.response_time || '';
792
+
793
+ if (response.data.available_agents) {
794
+ this.messengerState.setTeamAvatarsFromAgents(
795
+ response.data.available_agents
796
+ );
797
+ }
798
+
799
+ this.messengerState._notify('availabilityUpdate', response.data);
800
+ return response.data;
801
+ }
802
+ return { agents_online: false, online_count: 0 };
803
+ } catch (error) {
804
+ console.error('[MessengerWidget] Failed to check availability:', error);
805
+ return { agents_online: false, online_count: 0 };
806
+ }
807
+ }
808
+
809
+ async _fetchChangelog() {
810
+ if (this.apiService?.mock) {
811
+ return {
812
+ homeItems: [
813
+ {
814
+ id: 'changelog_5',
815
+ title: 'New integrations available',
816
+ description:
817
+ 'Connect with more tools you love and streamline your workflow.',
818
+ labels: [{ name: 'Integration', color: '#dbeafe' }],
819
+ coverImage:
820
+ 'https://images.unsplash.com/photo-1674027444485-cec3da58eef4?w=500&auto=format&fit=crop&q=60',
821
+ publishedAt: new Date(
822
+ Date.now() - 14 * 24 * 60 * 60 * 1000
823
+ ).toISOString(),
824
+ url: '#',
825
+ },
826
+ {
827
+ id: 'changelog_2',
828
+ title: 'A new era of Insights has arrived',
829
+ description:
830
+ 'We announced Fin Insights, a groundbreaking, AI-powered product that gives you complete visibility into every customer conversation.',
831
+ labels: [
832
+ { name: 'New feature', color: '#dcfce7' },
833
+ { name: 'AI', color: '#f3e8ff' },
834
+ ],
835
+ coverImage:
836
+ 'https://images.unsplash.com/photo-1666875753105-c63a6f3bdc86?w=500&auto=format&fit=crop&q=60',
837
+ publishedAt: new Date(
838
+ Date.now() - 5 * 24 * 60 * 60 * 1000
839
+ ).toISOString(),
840
+ url: '#',
841
+ },
842
+ {
843
+ id: 'changelog_1',
844
+ title: 'The 2025 Customer Service Transformation Report',
845
+ description:
846
+ 'Learn how AI has transformed customer service from the ground up—rewriting its economics and reshaping expectations.',
847
+ labels: [{ name: 'Report', color: '#fef9c3' }],
848
+ coverImage:
849
+ 'https://images.unsplash.com/photo-1762330467475-a565d04e1808?w=500&auto=format&fit=crop&q=60',
850
+ publishedAt: new Date(
851
+ Date.now() - 2 * 24 * 60 * 60 * 1000
852
+ ).toISOString(),
853
+ url: '#',
854
+ },
855
+ ],
856
+ changelogItems: [
857
+ {
858
+ id: 'changelog_4',
859
+ title: 'Enhanced conversation analytics',
860
+ description:
861
+ 'Get deeper insights into your customer conversations with our new analytics dashboard.',
862
+ labels: [{ name: 'Analytics', color: '#fce7f3' }],
863
+ coverImage:
864
+ 'https://images.unsplash.com/photo-1523961131990-5ea7c61b2107?w=500&auto=format&fit=crop&q=60',
865
+ publishedAt: new Date(
866
+ Date.now() - 10 * 24 * 60 * 60 * 1000
867
+ ).toISOString(),
868
+ url: '#',
869
+ },
870
+ {
871
+ id: 'changelog_3',
872
+ title: 'Escalation guidance for complex issues',
873
+ description:
874
+ 'New AI-powered escalation guidance helps your team handle complex customer issues more effectively.',
875
+ labels: [
876
+ { name: 'New feature', color: '#dcfce7' },
877
+ { name: 'AI', color: '#f3e8ff' },
878
+ ],
879
+ coverImage:
880
+ 'https://images.unsplash.com/photo-1764773516703-b246ac2ad5ef?w=500&auto=format&fit=crop&q=60',
881
+ publishedAt: new Date(
882
+ Date.now() - 7 * 24 * 60 * 60 * 1000
883
+ ).toISOString(),
884
+ url: '#',
885
+ },
886
+ ],
887
+ };
888
+ }
889
+
890
+ try {
891
+ const response = await this.apiService.getChangelogs({ limit: 20 });
892
+
893
+ if (response.success && response.data) {
894
+ const changelogs = Array.isArray(response.data) ? response.data : [];
895
+ const changelogBase = (
896
+ this.messengerOptions.changelogUrl || ''
897
+ ).replace(/\/$/, '');
898
+
899
+ const mappedItems = changelogs.map((item) => ({
900
+ id: item.id,
901
+ title: item.title,
902
+ description: item.excerpt || item.description || '',
903
+ labels: item.labels || [],
904
+ coverImage: item.cover_image || null,
905
+ publishedAt: item.published_at,
906
+ url:
907
+ item.slug && changelogBase
908
+ ? `${changelogBase}/${item.slug}`
909
+ : changelogBase || null,
910
+ }));
911
+
912
+ return {
913
+ homeItems: mappedItems.slice(0, 3),
914
+ changelogItems: mappedItems,
915
+ };
916
+ }
917
+
918
+ return { homeItems: [], changelogItems: [] };
919
+ } catch (error) {
920
+ console.error('[MessengerWidget] Failed to fetch changelog:', error);
921
+ return { homeItems: [], changelogItems: [] };
922
+ }
923
+ }
924
+
925
+ async onMount() {
926
+ this._applyPreviewData();
927
+
928
+ if (this.messengerOptions.autoLoadData) {
929
+ this.loadInitialData();
930
+
931
+ if (this.apiService?.sessionToken) {
932
+ this._initWebSocket();
933
+ }
934
+
935
+ this.checkAgentAvailability();
936
+
937
+ this._availabilityInterval = setInterval(() => {
938
+ this.checkAgentAvailability();
939
+ }, 60000);
940
+ }
941
+
942
+ if (
943
+ this.messengerOptions.initialView &&
944
+ this.messengerOptions.initialView !== this.messengerState.currentView
945
+ ) {
946
+ this.messengerState.setView(this.messengerOptions.initialView);
947
+ }
948
+ }
949
+
950
+ onDestroy() {
951
+ if (this._stateUnsubscribe) {
952
+ this._stateUnsubscribe();
953
+ }
954
+
955
+ if (this.wsService) {
956
+ this.wsService.disconnect();
957
+ }
958
+
959
+ this._wsUnsubscribers.forEach((unsub) => unsub());
960
+ this._wsUnsubscribers = [];
961
+
962
+ if (this._availabilityInterval) {
963
+ clearInterval(this._availabilityInterval);
964
+ }
965
+ }
966
+
967
+ destroy() {
968
+ removeMessengerCustomStyles();
969
+
970
+ if (this.launcher) {
971
+ this.launcher.destroy();
972
+ }
973
+ if (this.panel) {
974
+ this.panel.destroy();
975
+ }
976
+ this.onDestroy();
977
+ super.destroy();
978
+ }
979
+ }