@product7/product7-js 0.3.8 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@product7/product7-js",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "JavaScript SDK for integrating Product7 feedback widgets into any website",
5
5
  "main": "dist/product7-js.js",
6
6
  "module": "src/index.js",
@@ -89,8 +89,9 @@ export class APIService extends BaseAPIService {
89
89
  status: true,
90
90
  data: {
91
91
  contact_id: 'mock_contact_' + Date.now(),
92
- email: metadata.email,
92
+ email: metadata.email || '',
93
93
  name: metadata.name || '',
94
+ avatar: metadata.avatar || '',
94
95
  is_new: true,
95
96
  },
96
97
  };
@@ -98,21 +99,22 @@ export class APIService extends BaseAPIService {
98
99
  return mockResponse;
99
100
  }
100
101
 
102
+ const payload = {};
103
+
104
+ if (metadata.email) payload.email = metadata.email;
105
+ if (metadata.user_id) payload.user_id = metadata.user_id;
106
+ if (metadata.name) payload.name = metadata.name;
107
+ if (metadata.avatar) payload.avatar = metadata.avatar;
108
+ if (metadata.attributes) payload.attributes = metadata.attributes;
109
+ if (metadata.company) payload.company = metadata.company;
110
+
101
111
  const response = await this._makeRequest('/widget/identify', {
102
112
  method: 'POST',
103
113
  headers: {
104
114
  'Content-Type': 'application/json',
105
115
  Authorization: `Bearer ${this.sessionToken}`,
106
116
  },
107
- body: JSON.stringify({
108
- // user_id: metadata.user_id || null,
109
- email: metadata.email || '',
110
- // name: metadata.name || '',
111
- // phone: metadata.phone || '',
112
- // company: metadata.company || '',
113
- // avatar_url: metadata.avatar_url || '',
114
- // metadata: metadata.custom_fields || {},
115
- }),
117
+ body: JSON.stringify(payload),
116
118
  });
117
119
 
118
120
  if (response?.status && response?.data) {
@@ -48,7 +48,7 @@ export const designTokens = `
48
48
  --msg-bg-hover: #F9FAFB;
49
49
  --msg-bg-input: rgba(255, 255, 255, 0.7);
50
50
  --msg-bg-bubble-own: #F3F4F6;
51
- --msg-bg-bubble-received: rgba(21, 94, 239, 0.06);
51
+ --msg-bg-bubble-received: #155EEF;
52
52
  --msg-bg-header-gradient: linear-gradient(180deg, #e0e7ff 0%, #f0f4ff 35%, #FFFFFF 65%);
53
53
  --msg-bg-header-glow1: radial-gradient(circle, rgba(21, 94, 239, 0.08) 0%, transparent 70%);
54
54
  --msg-bg-header-glow2: radial-gradient(circle, rgba(139, 92, 246, 0.05) 0%, transparent 70%);
@@ -129,13 +129,13 @@ export const messengerComponentsStyles = `
129
129
 
130
130
  .messenger-message-own .messenger-message-bubble {
131
131
  background: var(--msg-bg-bubble-own);
132
- color: var(--msg-text);
132
+ color: #111827;
133
133
  border-bottom-right-radius: 0.25rem;
134
134
  }
135
135
 
136
136
  .messenger-message-received .messenger-message-bubble {
137
137
  background: var(--msg-bg-bubble-received);
138
- color: var(--msg-text);
138
+ color: #ffffff;
139
139
  border-bottom-left-radius: 0.25rem;
140
140
  }
141
141
 
@@ -205,7 +205,7 @@ export const messengerCoreStyles = `
205
205
  --msg-bg-hover: #232930;
206
206
  --msg-bg-input: #1a1e24;
207
207
  --msg-bg-bubble-own: #1e2330;
208
- --msg-bg-bubble-received: rgba(21, 94, 239, 0.12);
208
+ --msg-bg-bubble-received: #1D4ED8;
209
209
  --msg-bg-header-gradient: linear-gradient(180deg, #1a1e2e 0%, #141720 50%, #0f1317 100%);
210
210
  --msg-bg-header-glow1: radial-gradient(circle, rgba(21, 94, 239, 0.07) 0%, transparent 70%);
211
211
  --msg-bg-header-glow2: radial-gradient(circle, rgba(139, 92, 246, 0.05) 0%, transparent 70%);
@@ -0,0 +1,141 @@
1
+ /**
2
+ * NotificationSound — plays a soft ping when a new agent message arrives.
3
+ *
4
+ * Default: the bundled notification-sound.mp3 (inlined as base64 at build time).
5
+ * Falls back to a Web Audio API synthetic chime if the file was not bundled.
6
+ * Override: pass `soundUrl` (MP3/OGG/WAV URL or base64 data URI) to use a different file.
7
+ */
8
+ import BUNDLED_SOUND from 'virtual:notification-sound';
9
+
10
+ export class NotificationSound {
11
+ constructor({ soundUrl = BUNDLED_SOUND, volume = 0.4 } = {}) {
12
+ this._soundUrl = soundUrl;
13
+ this._volume = Math.min(1, Math.max(0, volume));
14
+ this._enabled = true;
15
+ this._audioCtx = null;
16
+ this._audioBuffer = null;
17
+ this._loadPromise = null;
18
+
19
+ if (soundUrl) {
20
+ this._loadPromise = this._loadAudioFile(soundUrl);
21
+ }
22
+ }
23
+
24
+ setEnabled(enabled) {
25
+ this._enabled = Boolean(enabled);
26
+ }
27
+
28
+ setVolume(volume) {
29
+ this._volume = Math.min(1, Math.max(0, volume));
30
+ }
31
+
32
+ async play() {
33
+ if (!this._enabled) return;
34
+ if (typeof window === 'undefined') return;
35
+
36
+ try {
37
+ if (this._soundUrl) {
38
+ await this._playFile();
39
+ } else {
40
+ this._playSynthetic();
41
+ }
42
+ } catch {
43
+ /* autoplay policy or context suspended — silent fail */
44
+ }
45
+ }
46
+
47
+ // ── Private ────────────────────────────────────────────────────────────────
48
+
49
+ _getAudioContext() {
50
+ if (!this._audioCtx || this._audioCtx.state === 'closed') {
51
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
52
+ if (!AudioContext) return null;
53
+ this._audioCtx = new AudioContext();
54
+ }
55
+ return this._audioCtx;
56
+ }
57
+
58
+ /**
59
+ * Synthetic two-tone chime via Web Audio API — no file required.
60
+ * • First tone: 880 Hz, 0–80 ms
61
+ * • Second tone: 1100 Hz, 60–160 ms
62
+ * Sounds like a gentle message ping.
63
+ */
64
+ _playSynthetic() {
65
+ const ctx = this._getAudioContext();
66
+ if (!ctx) return;
67
+
68
+ if (ctx.state === 'suspended') {
69
+ ctx.resume().catch(() => {});
70
+ }
71
+
72
+ const now = ctx.currentTime;
73
+ const vol = this._volume;
74
+
75
+ const tones = [
76
+ { freq: 880, start: 0, end: 0.08 },
77
+ { freq: 1100, start: 0.06, end: 0.16 },
78
+ ];
79
+
80
+ tones.forEach(({ freq, start, end }) => {
81
+ const osc = ctx.createOscillator();
82
+ const gain = ctx.createGain();
83
+
84
+ osc.type = 'sine';
85
+ osc.frequency.value = freq;
86
+
87
+ gain.gain.setValueAtTime(0, now + start);
88
+ gain.gain.linearRampToValueAtTime(vol, now + start + 0.01);
89
+ gain.gain.linearRampToValueAtTime(0, now + end);
90
+
91
+ osc.connect(gain);
92
+ gain.connect(ctx.destination);
93
+
94
+ osc.start(now + start);
95
+ osc.stop(now + end + 0.01);
96
+ });
97
+ }
98
+
99
+ async _loadAudioFile(url) {
100
+ try {
101
+ const ctx = this._getAudioContext();
102
+ if (!ctx) return;
103
+
104
+ const response = await fetch(url);
105
+ const arrayBuffer = await response.arrayBuffer();
106
+ this._audioBuffer = await ctx.decodeAudioData(arrayBuffer);
107
+ } catch {
108
+ /* failed to load — fall back to synthetic */
109
+ this._soundUrl = null;
110
+ }
111
+ }
112
+
113
+ async _playFile() {
114
+ if (this._loadPromise) {
115
+ await this._loadPromise;
116
+ this._loadPromise = null;
117
+ }
118
+
119
+ if (!this._audioBuffer) {
120
+ this._playSynthetic();
121
+ return;
122
+ }
123
+
124
+ const ctx = this._getAudioContext();
125
+ if (!ctx) return;
126
+
127
+ if (ctx.state === 'suspended') {
128
+ await ctx.resume();
129
+ }
130
+
131
+ const source = ctx.createBufferSource();
132
+ const gain = ctx.createGain();
133
+
134
+ source.buffer = this._audioBuffer;
135
+ gain.gain.value = this._volume;
136
+
137
+ source.connect(gain);
138
+ gain.connect(ctx.destination);
139
+ source.start();
140
+ }
141
+ }
@@ -1,3 +1,4 @@
1
+ import { NotificationSound } from '../utils/NotificationSound.js';
1
2
  import { WebSocketService } from '../core/WebSocketService.js';
2
3
  import {
3
4
  applyMessengerCustomStyles,
@@ -94,6 +95,19 @@ export class MessengerWidget extends BaseWidget {
94
95
  this._wsUnsubscribers = [];
95
96
  this._feedbackWidget = null;
96
97
 
98
+ const notificationSoundOption = options.notificationSound !== undefined
99
+ ? options.notificationSound
100
+ : true;
101
+
102
+ this._notificationSound = new NotificationSound({
103
+ soundUrl: typeof notificationSoundOption === 'string' ? notificationSoundOption : null,
104
+ volume: options.notificationVolume ?? 0.4,
105
+ });
106
+
107
+ if (notificationSoundOption === false) {
108
+ this._notificationSound.setEnabled(false);
109
+ }
110
+
97
111
  this._handleOpenChange = this._handleOpenChange.bind(this);
98
112
  this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
99
113
  this._handleTypingStarted = this._handleTypingStarted.bind(this);
@@ -418,6 +432,18 @@ export class MessengerWidget extends BaseWidget {
418
432
  optimisticMatchWindowMs: 30000,
419
433
  });
420
434
 
435
+ const isOwnMessage = message.sender_type === 'customer';
436
+
437
+ if (!isOwnMessage) {
438
+ const panelOpenOnThisConversation =
439
+ this.messengerState.isOpen &&
440
+ this.messengerState.activeConversationId === conversation_id;
441
+
442
+ if (!panelOpenOnThisConversation) {
443
+ this._notificationSound.play();
444
+ }
445
+ }
446
+
421
447
  if (
422
448
  !this.messengerState.isOpen ||
423
449
  this.messengerState.activeConversationId !== conversation_id
@@ -295,9 +295,25 @@ export class ChatView {
295
295
  `;
296
296
  }
297
297
 
298
+ // Render automated/bot messages as a proper received chat bubble
299
+ const teamName = this.state.teamName || 'Support';
300
+ const logoUrl = this.options.logoUrl;
301
+ const avatarHtml = logoUrl
302
+ ? `<div class="sdk-avatar sdk-avatar-sm"><img src="${this._escapeHtml(logoUrl)}" alt="${this._escapeHtml(teamName)}" /></div>`
303
+ : `<div class="sdk-avatar sdk-avatar-sm">${teamName.charAt(0).toUpperCase()}</div>`;
304
+ const timeStr = this._formatMessageTime(message.timestamp);
305
+
298
306
  return `
299
- <div class="messenger-message-system">
300
- <span class="messenger-message-system-text">${this._escapeHtml(content)}</span>
307
+ <div class="messenger-message messenger-message-received">
308
+ <div class="messenger-message-row">
309
+ <div class="messenger-message-avatar">${avatarHtml}</div>
310
+ <div class="messenger-message-wrapper">
311
+ <div class="messenger-message-bubble">
312
+ <div class="messenger-message-content">${this._formatMessageContent(content)}</div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ ${timeStr ? `<div class="messenger-message-meta"><span>${timeStr}</span></div>` : ''}
301
317
  </div>
302
318
  `;
303
319
  }