@product7/feedback-sdk 1.2.4 → 1.2.6

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.
@@ -10,6 +10,7 @@ import { ChatView } from './messenger/views/ChatView.js';
10
10
  import { ConversationsView } from './messenger/views/ConversationsView.js';
11
11
  import { HelpView } from './messenger/views/HelpView.js';
12
12
  import { HomeView } from './messenger/views/HomeView.js';
13
+ import { WebSocketService } from '../core/WebSocketService.js';
13
14
 
14
15
  export class MessengerWidget extends BaseWidget {
15
16
  constructor(options) {
@@ -44,9 +45,14 @@ export class MessengerWidget extends BaseWidget {
44
45
 
45
46
  this.launcher = null;
46
47
  this.panel = null;
48
+ this.wsService = null;
49
+ this._wsUnsubscribers = [];
47
50
 
48
51
  // Bind methods
49
52
  this._handleOpenChange = this._handleOpenChange.bind(this);
53
+ this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
54
+ this._handleTypingStarted = this._handleTypingStarted.bind(this);
55
+ this._handleTypingStopped = this._handleTypingStopped.bind(this);
50
56
  }
51
57
 
52
58
  _render() {
@@ -62,16 +68,23 @@ export class MessengerWidget extends BaseWidget {
62
68
  });
63
69
  container.appendChild(this.launcher.render());
64
70
 
65
- // Create panel
71
+ // Create panel with all callbacks
66
72
  this.panel = new MessengerPanel(this.messengerState, {
67
73
  position: this.messengerOptions.position,
68
74
  theme: this.messengerOptions.theme,
69
75
  primaryColor: this.messengerOptions.primaryColor,
70
76
  logoUrl: this.messengerOptions.logoUrl,
71
77
  featuredContent: this.messengerOptions.featuredContent,
78
+ // Chat callbacks
72
79
  onSendMessage:
73
80
  this.messengerOptions.onSendMessage ||
74
81
  this._handleSendMessage.bind(this),
82
+ onStartConversation: this._handleStartConversation.bind(this),
83
+ onTyping: this.sendTypingIndicator.bind(this),
84
+ // Conversation list callbacks
85
+ onSelectConversation: this._handleSelectConversation.bind(this),
86
+ onStartNewConversation: this._handleNewConversationClick.bind(this),
87
+ // Article/changelog callbacks
75
88
  onArticleClick: this.messengerOptions.onArticleClick,
76
89
  onChangelogClick: this.messengerOptions.onChangelogClick,
77
90
  });
@@ -109,32 +122,181 @@ export class MessengerWidget extends BaseWidget {
109
122
  }
110
123
  }
111
124
 
125
+ /**
126
+ * Handle starting a new conversation
127
+ */
128
+ async _handleStartConversation(messageContent) {
129
+ try {
130
+ await this.startNewConversation(messageContent);
131
+ } catch (error) {
132
+ console.error('[MessengerWidget] Failed to start conversation:', error);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Handle selecting a conversation from the list
138
+ */
139
+ async _handleSelectConversation(conversationId) {
140
+ try {
141
+ await this.fetchMessages(conversationId);
142
+ } catch (error) {
143
+ console.error('[MessengerWidget] Failed to fetch messages:', error);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Handle clicking "new conversation" button
149
+ */
150
+ _handleNewConversationClick() {
151
+ // View is already changed by ConversationsView
152
+ // This is for any additional setup needed
153
+ }
154
+
112
155
  async _handleSendMessage(conversationId, message) {
113
- // Default message handler - can be overridden
156
+ // Emit event for external listeners
114
157
  this.sdk.eventBus.emit('messenger:messageSent', {
115
158
  widget: this,
116
159
  conversationId,
117
160
  message,
118
161
  });
119
162
 
120
- // In mock mode, simulate a response
121
- if (this.apiService?.mock) {
122
- setTimeout(() => {
123
- const response = {
124
- id: 'msg_' + Date.now(),
125
- content: "Thanks for your message! We'll get back to you soon.",
126
- isOwn: false,
127
- timestamp: new Date().toISOString(),
128
- sender: {
129
- name: 'Support Team',
130
- avatarUrl: null,
131
- },
132
- };
133
- this.messengerState.addMessage(conversationId, response);
134
- }, 1500);
163
+ try {
164
+ // Send message through API
165
+ const response = await this.apiService.sendMessage(conversationId, {
166
+ content: message.content,
167
+ });
168
+
169
+ if (response.status && response.data) {
170
+ // Update the message ID with server-assigned ID
171
+ // Message is already added to state optimistically in ChatView
172
+ console.log('[MessengerWidget] Message sent:', response.data.id);
173
+ }
174
+
175
+ // In mock mode, simulate an agent response after a delay
176
+ if (this.apiService?.mock) {
177
+ setTimeout(() => {
178
+ const mockResponse = {
179
+ id: 'msg_' + Date.now(),
180
+ content: "Thanks for your message! We'll get back to you soon.",
181
+ isOwn: false,
182
+ timestamp: new Date().toISOString(),
183
+ sender: {
184
+ name: 'Support Team',
185
+ avatarUrl: null,
186
+ },
187
+ };
188
+ this.messengerState.addMessage(conversationId, mockResponse);
189
+ }, 1500);
190
+ }
191
+ } catch (error) {
192
+ console.error('[MessengerWidget] Failed to send message:', error);
193
+ // Could add error handling UI here
135
194
  }
136
195
  }
137
196
 
197
+ /**
198
+ * Handle incoming WebSocket message
199
+ */
200
+ _handleWebSocketMessage(data) {
201
+ const { conversation_id, message } = data;
202
+
203
+ // Transform message to local format
204
+ const localMessage = {
205
+ id: message.id,
206
+ content: message.content,
207
+ isOwn: message.sender_type === 'customer',
208
+ timestamp: message.created_at,
209
+ sender: {
210
+ name: message.sender_name || 'Support',
211
+ avatarUrl: message.sender_avatar || null,
212
+ },
213
+ };
214
+
215
+ // Add message to state
216
+ this.messengerState.addMessage(conversation_id, localMessage);
217
+
218
+ // Update unread count if panel is closed or viewing different conversation
219
+ if (!this.messengerState.isOpen || this.messengerState.activeConversationId !== conversation_id) {
220
+ this._updateUnreadCount();
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Handle typing started event
226
+ */
227
+ _handleTypingStarted(data) {
228
+ if (data.is_agent) {
229
+ this.messengerState._notify('typingStarted', {
230
+ conversationId: data.conversation_id,
231
+ userName: data.user_name,
232
+ });
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Handle typing stopped event
238
+ */
239
+ _handleTypingStopped(data) {
240
+ this.messengerState._notify('typingStopped', {
241
+ conversationId: data.conversation_id,
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Update unread count from API
247
+ */
248
+ async _updateUnreadCount() {
249
+ try {
250
+ const response = await this.apiService.getUnreadCount();
251
+ if (response.status && response.data) {
252
+ this.messengerState.unreadCount = response.data.unread_count || 0;
253
+ this.messengerState._notify('unreadCountChange', { count: this.messengerState.unreadCount });
254
+ }
255
+ } catch (error) {
256
+ console.error('[MessengerWidget] Failed to get unread count:', error);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Initialize WebSocket connection
262
+ */
263
+ _initWebSocket() {
264
+ if (this.wsService) {
265
+ this.wsService.disconnect();
266
+ }
267
+
268
+ this.wsService = new WebSocketService({
269
+ baseURL: this.apiService.baseURL,
270
+ workspace: this.apiService.workspace,
271
+ sessionToken: this.apiService.sessionToken,
272
+ mock: this.apiService.mock,
273
+ });
274
+
275
+ // Subscribe to WebSocket events
276
+ this._wsUnsubscribers.push(
277
+ this.wsService.on('message', this._handleWebSocketMessage)
278
+ );
279
+ this._wsUnsubscribers.push(
280
+ this.wsService.on('typing_started', this._handleTypingStarted)
281
+ );
282
+ this._wsUnsubscribers.push(
283
+ this.wsService.on('typing_stopped', this._handleTypingStopped)
284
+ );
285
+ this._wsUnsubscribers.push(
286
+ this.wsService.on('connected', () => {
287
+ console.log('[MessengerWidget] WebSocket connected');
288
+ })
289
+ );
290
+ this._wsUnsubscribers.push(
291
+ this.wsService.on('disconnected', () => {
292
+ console.log('[MessengerWidget] WebSocket disconnected');
293
+ })
294
+ );
295
+
296
+ // Connect
297
+ this.wsService.connect();
298
+ }
299
+
138
300
  /**
139
301
  * Open the messenger panel
140
302
  */
@@ -256,82 +418,159 @@ export class MessengerWidget extends BaseWidget {
256
418
  }
257
419
 
258
420
  async _fetchConversations() {
259
- // Mock data for now
260
- if (this.apiService?.mock) {
261
- return [
262
- {
263
- id: 'conv_1',
264
- title: 'Chat with Sarah',
265
- participants: [{ name: 'Sarah', avatarUrl: null }],
266
- lastMessage: "Sarah: Hi 👋 I'm Sarah. How can I help today?",
267
- lastMessageTime: new Date(Date.now() - 49 * 60 * 1000).toISOString(),
268
- unread: 1,
269
- },
270
- {
271
- id: 'conv_2',
272
- title: 'Chat with Tom',
273
- participants: [{ name: 'Tom', avatarUrl: null }],
274
- lastMessage: 'Tom: The feature will be available next week.',
275
- lastMessageTime: new Date(
276
- Date.now() - 6 * 24 * 60 * 60 * 1000
277
- ).toISOString(),
278
- unread: 0,
279
- },
280
- ];
421
+ try {
422
+ const response = await this.apiService.getConversations();
423
+ if (response.status && response.data) {
424
+ // Transform API response to local format
425
+ return response.data.map((conv) => ({
426
+ id: conv.id,
427
+ title: conv.subject || `Chat with ${conv.assigned_user?.name || 'Support'}`,
428
+ participants: conv.assigned_user
429
+ ? [{ name: conv.assigned_user.name, avatarUrl: conv.assigned_user.avatar }]
430
+ : [{ name: 'Support', avatarUrl: null }],
431
+ lastMessage: conv.preview || conv.snippet || '',
432
+ lastMessageTime: conv.last_message_at,
433
+ unread: conv.unread || 0,
434
+ status: conv.status,
435
+ }));
436
+ }
437
+ return [];
438
+ } catch (error) {
439
+ console.error('[MessengerWidget] Failed to fetch conversations:', error);
440
+ return [];
281
441
  }
282
-
283
- // TODO: Implement API call
284
- return [];
285
442
  }
286
443
 
287
444
  async _fetchHelpArticles() {
288
- // Mock data for collections
289
- if (this.apiService?.mock) {
290
- return [
291
- {
292
- id: 'collection_1',
293
- title: 'Product Overview',
294
- description: 'See how your AI-first customer service solution works.',
295
- articleCount: 24,
296
- url: '#',
297
- },
298
- {
299
- id: 'collection_2',
300
- title: 'Getting Started',
301
- description:
302
- 'Everything you need to know to get started with Product7.',
303
- articleCount: 30,
304
- url: '#',
305
- },
306
- {
307
- id: 'collection_3',
308
- title: 'AI Agent',
309
- description:
310
- 'Resolving customer questions instantly and accurately—from live chat to email.',
311
- articleCount: 82,
312
- url: '#',
313
- },
314
- {
315
- id: 'collection_4',
316
- title: 'Channels',
317
- description:
318
- 'Enabling the channels you use to communicate with customers, all from the Inbox.',
319
- articleCount: 45,
320
- url: '#',
321
- },
322
- {
323
- id: 'collection_5',
324
- title: 'Billing & Payments',
325
- description:
326
- 'Manage your subscription, invoices, and payment methods.',
327
- articleCount: 12,
328
- url: '#',
329
- },
330
- ];
445
+ try {
446
+ const response = await this.apiService.getHelpCollections();
447
+ if (response.status && response.data) {
448
+ // Transform API response to local format
449
+ return response.data.map((collection) => ({
450
+ id: collection.id,
451
+ title: collection.title || collection.name,
452
+ description: collection.description || '',
453
+ articleCount: collection.article_count || collection.articleCount || 0,
454
+ icon: collection.icon || 'ph-book-open',
455
+ url: collection.url || `#/help/${collection.slug || collection.id}`,
456
+ }));
457
+ }
458
+ return [];
459
+ } catch (error) {
460
+ console.error('[MessengerWidget] Failed to fetch help articles:', error);
461
+ return [];
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Fetch messages for a conversation
467
+ */
468
+ async fetchMessages(conversationId) {
469
+ try {
470
+ const response = await this.apiService.getConversation(conversationId);
471
+ if (response.status && response.data) {
472
+ const messages = (response.data.messages || []).map((msg) => ({
473
+ id: msg.id,
474
+ content: msg.content,
475
+ isOwn: msg.sender_type === 'customer',
476
+ timestamp: msg.created_at,
477
+ sender: {
478
+ name: msg.sender_name || (msg.sender_type === 'customer' ? 'You' : 'Support'),
479
+ avatarUrl: msg.sender_avatar || null,
480
+ },
481
+ }));
482
+ this.messengerState.setMessages(conversationId, messages);
483
+
484
+ // Mark as read
485
+ await this.apiService.markConversationAsRead(conversationId);
486
+ this.messengerState.markAsRead(conversationId);
487
+
488
+ return messages;
489
+ }
490
+ return [];
491
+ } catch (error) {
492
+ console.error('[MessengerWidget] Failed to fetch messages:', error);
493
+ return [];
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Start a new conversation
499
+ */
500
+ async startNewConversation(message, subject = '') {
501
+ try {
502
+ const response = await this.apiService.startConversation({
503
+ message,
504
+ subject,
505
+ });
506
+
507
+ if (response.status && response.data) {
508
+ const conv = response.data;
509
+ const newConversation = {
510
+ id: conv.id,
511
+ title: conv.subject || 'New conversation',
512
+ participants: [{ name: 'Support', avatarUrl: null }],
513
+ lastMessage: message,
514
+ lastMessageTime: conv.created_at || new Date().toISOString(),
515
+ unread: 0,
516
+ status: 'open',
517
+ };
518
+
519
+ // Add to state
520
+ this.messengerState.addConversation(newConversation);
521
+
522
+ // Set initial message in messages cache
523
+ this.messengerState.setMessages(conv.id, [
524
+ {
525
+ id: 'msg_' + Date.now(),
526
+ content: message,
527
+ isOwn: true,
528
+ timestamp: new Date().toISOString(),
529
+ },
530
+ ]);
531
+
532
+ // Navigate to chat
533
+ this.messengerState.setActiveConversation(conv.id);
534
+ this.messengerState.setView('chat');
535
+
536
+ return conv;
537
+ }
538
+ return null;
539
+ } catch (error) {
540
+ console.error('[MessengerWidget] Failed to start conversation:', error);
541
+ return null;
331
542
  }
543
+ }
332
544
 
333
- // TODO: Implement API call
334
- return [];
545
+ /**
546
+ * Send typing indicator
547
+ */
548
+ async sendTypingIndicator(conversationId, isTyping) {
549
+ try {
550
+ await this.apiService.sendTypingIndicator(conversationId, isTyping);
551
+ } catch (error) {
552
+ // Silently fail - typing indicators are not critical
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Check agent availability
558
+ */
559
+ async checkAgentAvailability() {
560
+ try {
561
+ const response = await this.apiService.checkAgentsOnline();
562
+ if (response.status && response.data) {
563
+ this.messengerState.agentsOnline = response.data.agents_online;
564
+ this.messengerState.onlineCount = response.data.online_count || 0;
565
+ this.messengerState.responseTime = response.data.response_time || '';
566
+ this.messengerState._notify('availabilityUpdate', response.data);
567
+ return response.data;
568
+ }
569
+ return { agents_online: false, online_count: 0 };
570
+ } catch (error) {
571
+ console.error('[MessengerWidget] Failed to check availability:', error);
572
+ return { agents_online: false, online_count: 0 };
573
+ }
335
574
  }
336
575
 
337
576
  async _fetchChangelog() {
@@ -414,19 +653,64 @@ export class MessengerWidget extends BaseWidget {
414
653
  };
415
654
  }
416
655
 
417
- // TODO: Implement API call for changelogs
418
- return { homeItems: [], changelogItems: [] };
656
+ // Fetch changelogs from API
657
+ const response = await this.apiService.getChangelogs({ limit: 20 });
658
+ const changelogs = response.data || [];
659
+
660
+ // Map API response to expected format
661
+ const mappedItems = changelogs.map((item) => ({
662
+ id: item.id,
663
+ title: item.title,
664
+ description: item.excerpt || item.description || '',
665
+ tags: item.labels ? item.labels.map((label) => label.name) : [],
666
+ coverImage: item.cover_image || null,
667
+ coverText: null,
668
+ publishedAt: item.published_at,
669
+ url: item.slug ? `/changelog/${item.slug}` : '#',
670
+ }));
671
+
672
+ return {
673
+ homeItems: mappedItems.slice(0, 3),
674
+ changelogItems: mappedItems,
675
+ };
419
676
  }
420
677
 
421
678
  onMount() {
422
679
  // Load initial data after mounting
423
680
  this.loadInitialData();
681
+
682
+ // Initialize WebSocket for real-time updates
683
+ if (this.apiService?.sessionToken) {
684
+ this._initWebSocket();
685
+ }
686
+
687
+ // Check agent availability
688
+ this.checkAgentAvailability();
689
+
690
+ // Periodically check availability (every 60 seconds)
691
+ this._availabilityInterval = setInterval(() => {
692
+ this.checkAgentAvailability();
693
+ }, 60000);
424
694
  }
425
695
 
426
696
  onDestroy() {
427
697
  if (this._stateUnsubscribe) {
428
698
  this._stateUnsubscribe();
429
699
  }
700
+
701
+ // Clean up WebSocket
702
+ if (this.wsService) {
703
+ this.wsService.disconnect();
704
+ }
705
+
706
+ // Clean up WebSocket event listeners
707
+ this._wsUnsubscribers.forEach((unsub) => unsub());
708
+ this._wsUnsubscribers = [];
709
+
710
+ // Clean up availability interval
711
+ if (this._availabilityInterval) {
712
+ clearInterval(this._availabilityInterval);
713
+ }
430
714
  }
431
715
 
432
716
  destroy() {
@@ -436,6 +720,7 @@ export class MessengerWidget extends BaseWidget {
436
720
  if (this.panel) {
437
721
  this.panel.destroy();
438
722
  }
723
+ this.onDestroy();
439
724
  super.destroy();
440
725
  }
441
726
  }
@@ -1,5 +1,6 @@
1
1
  import { SDKError } from '../utils/errors.js';
2
2
  import { ButtonWidget } from './ButtonWidget.js';
3
+ import { ChangelogWidget } from './ChangelogWidget.js';
3
4
  import { InlineWidget } from './InlineWidget.js';
4
5
  import { MessengerWidget } from './MessengerWidget.js';
5
6
  import { SurveyWidget } from './SurveyWidget.js';
@@ -12,6 +13,7 @@ export class WidgetFactory {
12
13
  ['inline', InlineWidget],
13
14
  ['survey', SurveyWidget],
14
15
  ['messenger', MessengerWidget],
16
+ ['changelog', ChangelogWidget],
15
17
  ]);
16
18
 
17
19
  static register(type, WidgetClass) {
@@ -32,6 +32,18 @@ export class MessengerState {
32
32
  this.enableHelp = options.enableHelp !== false;
33
33
  this.enableChangelog = options.enableChangelog !== false;
34
34
 
35
+ // Agent availability
36
+ this.agentsOnline = false;
37
+ this.onlineCount = 0;
38
+ this.responseTime = 'Usually replies within a few minutes';
39
+
40
+ // Typing indicators
41
+ this.typingUsers = {}; // { conversationId: { userName, timestamp } }
42
+
43
+ // Loading states
44
+ this.isLoading = false;
45
+ this.isLoadingMessages = false;
46
+
35
47
  // Listeners
36
48
  this._listeners = new Set();
37
49
  }
@@ -108,7 +108,7 @@ export class NavigationTabs {
108
108
  }
109
109
 
110
110
  _getHomeIcon() {
111
- return `<i class="ph-duotone ph-house-simple" style="font-size: 24px;"></i>`;
111
+ return `<i class="ph-duotone ph-house" style="font-size: 24px;"></i>`;
112
112
  }
113
113
 
114
114
  _getMessagesIcon() {