@product7/product7-js 0.3.9 → 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/dist/product7-js.js +190 -6
- package/dist/product7-js.js.map +1 -1
- package/dist/product7-js.min.js +1 -1
- package/dist/product7-js.min.js.map +1 -1
- package/package.json +1 -1
- package/src/styles/design-tokens.js +1 -1
- package/src/styles/messenger-components.js +2 -2
- package/src/styles/messenger-core.js +1 -1
- package/src/utils/NotificationSound.js +141 -0
- package/src/widgets/MessengerWidget.js +26 -0
- package/src/widgets/messenger/views/ChatView.js +18 -2
package/package.json
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
300
|
-
<
|
|
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
|
}
|