@product7/product7-js 0.6.5 → 0.6.8
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 +370 -22
- 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/core/WebSocketService.js +69 -10
- package/src/styles/liveChat-components.js +159 -0
- package/src/styles/liveChat-core.js +4 -0
- package/src/widgets/LiveChatWidget.js +23 -0
- package/src/widgets/liveChat/components/LiveChatPanel.js +3 -2
- package/src/widgets/liveChat/views/FeedbackFormView.js +104 -0
- package/src/widgets/liveChat/views/HomeView.js +8 -10
package/package.json
CHANGED
|
@@ -11,11 +11,28 @@ export class WebSocketService {
|
|
|
11
11
|
|
|
12
12
|
this.ws = null;
|
|
13
13
|
this.reconnectAttempts = 0;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Reconnect indefinitely with capped exponential backoff. The
|
|
15
|
+
// previous 5-attempt hard cap meant ~31s of churn on a flaky network
|
|
16
|
+
// permanently killed the live-chat connection until page refresh —
|
|
17
|
+
// the dominant "messages don't arrive" symptom in production.
|
|
18
|
+
this.reconnectBaseDelay = 1000;
|
|
19
|
+
this.reconnectMaxDelay = 30_000;
|
|
16
20
|
this.pingInterval = null;
|
|
17
21
|
this.isConnected = false;
|
|
18
22
|
|
|
23
|
+
// Heartbeat watchdog. The browser's onclose/onerror only fires when
|
|
24
|
+
// it notices the TCP socket is dead — on stalled-but-not-closed
|
|
25
|
+
// connections (NAT timeouts, sleeping mobile, some intermediaries)
|
|
26
|
+
// that can take many minutes. We track the wall-clock time of the
|
|
27
|
+
// last received frame and force a reconnect if nothing arrives
|
|
28
|
+
// within the timeout. Server's protocol-level pings come every 54s
|
|
29
|
+
// (pkg/websocket/client.go pingPeriod) and our app-level pings/pongs
|
|
30
|
+
// every 30s, so 90s catches a dead pipe with one cycle of headroom.
|
|
31
|
+
this.lastFrameAt = 0;
|
|
32
|
+
this.heartbeatTimeoutMs = 90_000;
|
|
33
|
+
this.heartbeatCheckIntervalMs = 15_000;
|
|
34
|
+
this.heartbeatInterval = null;
|
|
35
|
+
|
|
19
36
|
// Event listeners
|
|
20
37
|
this._listeners = new Map();
|
|
21
38
|
|
|
@@ -39,6 +56,8 @@ export class WebSocketService {
|
|
|
39
56
|
return;
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
this._intentionallyClosed = false;
|
|
60
|
+
|
|
42
61
|
// Mock mode - simulate connection
|
|
43
62
|
if (this.mock) {
|
|
44
63
|
this.isConnected = true;
|
|
@@ -70,13 +89,15 @@ export class WebSocketService {
|
|
|
70
89
|
*/
|
|
71
90
|
disconnect() {
|
|
72
91
|
this.isConnected = false;
|
|
73
|
-
this.
|
|
92
|
+
this._intentionallyClosed = true; // Prevent reconnection
|
|
74
93
|
|
|
75
94
|
if (this.pingInterval) {
|
|
76
95
|
clearInterval(this.pingInterval);
|
|
77
96
|
this.pingInterval = null;
|
|
78
97
|
}
|
|
79
98
|
|
|
99
|
+
this._stopHeartbeat();
|
|
100
|
+
|
|
80
101
|
if (this.ws) {
|
|
81
102
|
this.ws.close();
|
|
82
103
|
this.ws = null;
|
|
@@ -88,6 +109,35 @@ export class WebSocketService {
|
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
_startHeartbeat() {
|
|
113
|
+
this._stopHeartbeat();
|
|
114
|
+
this.lastFrameAt = Date.now();
|
|
115
|
+
this.heartbeatInterval = setInterval(() => {
|
|
116
|
+
if (Date.now() - this.lastFrameAt > this.heartbeatTimeoutMs) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`[WebSocket] No frames in ${this.heartbeatTimeoutMs}ms, forcing reconnect`
|
|
119
|
+
);
|
|
120
|
+
// Closing the socket fires onclose, which schedules a
|
|
121
|
+
// reconnect through the normal path.
|
|
122
|
+
this._stopHeartbeat();
|
|
123
|
+
if (this.ws) {
|
|
124
|
+
try {
|
|
125
|
+
this.ws.close();
|
|
126
|
+
} catch (_) {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, this.heartbeatCheckIntervalMs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_stopHeartbeat() {
|
|
135
|
+
if (this.heartbeatInterval) {
|
|
136
|
+
clearInterval(this.heartbeatInterval);
|
|
137
|
+
this.heartbeatInterval = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
91
141
|
/**
|
|
92
142
|
* Subscribe to events
|
|
93
143
|
* @param {string} event - Event name
|
|
@@ -137,6 +187,8 @@ export class WebSocketService {
|
|
|
137
187
|
console.log('[WebSocket] Connected');
|
|
138
188
|
this.isConnected = true;
|
|
139
189
|
this.reconnectAttempts = 0;
|
|
190
|
+
this._intentionallyClosed = false;
|
|
191
|
+
this._startHeartbeat();
|
|
140
192
|
this._emit('connected', {});
|
|
141
193
|
|
|
142
194
|
// Start ping interval to keep connection alive
|
|
@@ -146,6 +198,9 @@ export class WebSocketService {
|
|
|
146
198
|
}
|
|
147
199
|
|
|
148
200
|
_onMessage(event) {
|
|
201
|
+
// Any inbound frame counts as a sign of life for the watchdog —
|
|
202
|
+
// including pongs, message:new, typing, etc.
|
|
203
|
+
this.lastFrameAt = Date.now();
|
|
149
204
|
try {
|
|
150
205
|
const data = JSON.parse(event.data);
|
|
151
206
|
const { type, payload } = data;
|
|
@@ -190,8 +245,13 @@ export class WebSocketService {
|
|
|
190
245
|
this.pingInterval = null;
|
|
191
246
|
}
|
|
192
247
|
|
|
248
|
+
this._stopHeartbeat();
|
|
249
|
+
|
|
193
250
|
this._emit('disconnected', { code: event.code, reason: event.reason });
|
|
194
|
-
|
|
251
|
+
// Skip reconnect if disconnect() was called intentionally.
|
|
252
|
+
if (!this._intentionallyClosed) {
|
|
253
|
+
this._scheduleReconnect();
|
|
254
|
+
}
|
|
195
255
|
}
|
|
196
256
|
|
|
197
257
|
_onError(error) {
|
|
@@ -200,14 +260,13 @@ export class WebSocketService {
|
|
|
200
260
|
}
|
|
201
261
|
|
|
202
262
|
_scheduleReconnect() {
|
|
203
|
-
if (this.
|
|
204
|
-
console.log('[WebSocket] Max reconnect attempts reached');
|
|
205
|
-
this._emit('reconnect_failed', {});
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
263
|
+
if (this._intentionallyClosed) return;
|
|
208
264
|
|
|
209
265
|
this.reconnectAttempts++;
|
|
210
|
-
const delay =
|
|
266
|
+
const delay = Math.min(
|
|
267
|
+
this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
268
|
+
this.reconnectMaxDelay
|
|
269
|
+
);
|
|
211
270
|
console.log(
|
|
212
271
|
`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
|
|
213
272
|
);
|
|
@@ -839,4 +839,163 @@
|
|
|
839
839
|
opacity: 0.7;
|
|
840
840
|
cursor: not-allowed;
|
|
841
841
|
}
|
|
842
|
+
|
|
843
|
+
/* ========================================
|
|
844
|
+
FEEDBACK FORM VIEW
|
|
845
|
+
======================================== */
|
|
846
|
+
|
|
847
|
+
.liveChat-feedback-view {
|
|
848
|
+
display: flex;
|
|
849
|
+
flex-direction: column;
|
|
850
|
+
height: 100%;
|
|
851
|
+
overflow: hidden;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.liveChat-feedback-header {
|
|
855
|
+
display: flex;
|
|
856
|
+
align-items: center;
|
|
857
|
+
gap: var(--spacing-3);
|
|
858
|
+
padding: var(--spacing-4) var(--spacing-4);
|
|
859
|
+
border-bottom: 1px solid var(--border-color);
|
|
860
|
+
flex-shrink: 0;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.liveChat-feedback-title {
|
|
864
|
+
font-size: var(--font-size-base);
|
|
865
|
+
font-weight: var(--font-weight-semibold);
|
|
866
|
+
color: var(--text-primary);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.liveChat-feedback-body {
|
|
870
|
+
display: flex;
|
|
871
|
+
flex-direction: column;
|
|
872
|
+
gap: var(--spacing-4);
|
|
873
|
+
padding: var(--spacing-5) var(--spacing-4);
|
|
874
|
+
flex: 1;
|
|
875
|
+
overflow-y: auto;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.liveChat-feedback-prompt {
|
|
879
|
+
margin: 0;
|
|
880
|
+
font-size: var(--font-size-sm);
|
|
881
|
+
color: var(--text-secondary);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
.liveChat-feedback-input {
|
|
886
|
+
width: 100%;
|
|
887
|
+
border: 1px solid var(--border-color);
|
|
888
|
+
border-radius: var(--radius-md);
|
|
889
|
+
padding: var(--spacing-3);
|
|
890
|
+
font-size: var(--font-size-sm);
|
|
891
|
+
font-family: inherit;
|
|
892
|
+
color: var(--text-primary);
|
|
893
|
+
background: var(--msg-bg);
|
|
894
|
+
box-sizing: border-box;
|
|
895
|
+
transition: border-color var(--transition-fast);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.liveChat-feedback-input::placeholder {
|
|
899
|
+
color: var(--msg-text-secondary);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.liveChat-feedback-input:focus {
|
|
903
|
+
outline: none;
|
|
904
|
+
border-color: var(--color-primary);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.liveChat-feedback-textarea {
|
|
908
|
+
width: 100%;
|
|
909
|
+
resize: none;
|
|
910
|
+
border: 1px solid var(--border-color);
|
|
911
|
+
border-radius: var(--radius-md);
|
|
912
|
+
padding: var(--spacing-3);
|
|
913
|
+
font-size: var(--font-size-sm);
|
|
914
|
+
font-family: inherit;
|
|
915
|
+
color: var(--text-primary);
|
|
916
|
+
background: var(--msg-bg);
|
|
917
|
+
box-sizing: border-box;
|
|
918
|
+
transition: border-color var(--transition-fast);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.liveChat-feedback-textarea::placeholder {
|
|
922
|
+
color: var(--msg-text-secondary);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.liveChat-feedback-textarea:focus {
|
|
926
|
+
outline: none;
|
|
927
|
+
border-color: var(--color-primary);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.liveChat-feedback-submit {
|
|
931
|
+
display: flex;
|
|
932
|
+
align-items: center;
|
|
933
|
+
justify-content: center;
|
|
934
|
+
padding: var(--spacing-3);
|
|
935
|
+
border-radius: var(--radius-md);
|
|
936
|
+
font-size: var(--font-size-sm);
|
|
937
|
+
font-weight: var(--font-weight-medium);
|
|
938
|
+
font-family: inherit;
|
|
939
|
+
cursor: pointer;
|
|
940
|
+
border: none;
|
|
941
|
+
background: var(--color-primary);
|
|
942
|
+
color: #ffffff;
|
|
943
|
+
transition: all var(--transition-fast);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.liveChat-feedback-submit:hover:not(:disabled) {
|
|
947
|
+
background: var(--color-primary-hover);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.liveChat-feedback-submit:disabled {
|
|
951
|
+
opacity: 0.5;
|
|
952
|
+
cursor: not-allowed;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.liveChat-feedback-thankyou {
|
|
956
|
+
display: flex;
|
|
957
|
+
flex-direction: column;
|
|
958
|
+
align-items: center;
|
|
959
|
+
justify-content: center;
|
|
960
|
+
flex: 1;
|
|
961
|
+
padding: var(--spacing-6) var(--spacing-4);
|
|
962
|
+
text-align: center;
|
|
963
|
+
gap: var(--spacing-3);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.liveChat-feedback-thankyou-emoji {
|
|
967
|
+
font-size: 48px;
|
|
968
|
+
line-height: 1;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.liveChat-feedback-thankyou h3 {
|
|
972
|
+
margin: 0;
|
|
973
|
+
font-size: var(--font-size-lg);
|
|
974
|
+
font-weight: var(--font-weight-semibold);
|
|
975
|
+
color: var(--text-primary);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.liveChat-feedback-thankyou p {
|
|
979
|
+
margin: 0;
|
|
980
|
+
font-size: var(--font-size-sm);
|
|
981
|
+
color: var(--text-secondary);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.liveChat-feedback-done-btn {
|
|
985
|
+
margin-top: var(--spacing-2);
|
|
986
|
+
padding: var(--spacing-2) var(--spacing-5);
|
|
987
|
+
border-radius: var(--radius-md);
|
|
988
|
+
font-size: var(--font-size-sm);
|
|
989
|
+
font-weight: var(--font-weight-medium);
|
|
990
|
+
font-family: inherit;
|
|
991
|
+
cursor: pointer;
|
|
992
|
+
border: 1px solid var(--border-color);
|
|
993
|
+
background: transparent;
|
|
994
|
+
color: var(--text-primary);
|
|
995
|
+
transition: all var(--transition-fast);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.liveChat-feedback-done-btn:hover {
|
|
999
|
+
background: var(--bg-secondary);
|
|
1000
|
+
}
|
|
842
1001
|
`;
|
|
@@ -13,6 +13,7 @@ import { ChatView } from './liveChat/views/ChatView.js';
|
|
|
13
13
|
import { ConversationsView } from './liveChat/views/ConversationsView.js';
|
|
14
14
|
import { HelpView } from './liveChat/views/HelpView.js';
|
|
15
15
|
import { HomeView } from './liveChat/views/HomeView.js';
|
|
16
|
+
import { FeedbackFormView } from './liveChat/views/FeedbackFormView.js';
|
|
16
17
|
import { PreChatFormView } from './liveChat/views/PreChatFormView.js';
|
|
17
18
|
|
|
18
19
|
export class LiveChatWidget extends BaseWidget {
|
|
@@ -208,6 +209,7 @@ export class LiveChatWidget extends BaseWidget {
|
|
|
208
209
|
this.panel.registerView('messages', ConversationsView);
|
|
209
210
|
this.panel.registerView('chat', ChatView);
|
|
210
211
|
this.panel.registerView('prechat', PreChatFormView);
|
|
212
|
+
this.panel.registerView('feedback', FeedbackFormView);
|
|
211
213
|
this.panel.registerView('help', HelpView);
|
|
212
214
|
this.panel.registerView('changelog', ChangelogView);
|
|
213
215
|
|
|
@@ -563,13 +565,34 @@ export class LiveChatWidget extends BaseWidget {
|
|
|
563
565
|
this._wsUnsubscribers.push(
|
|
564
566
|
this.wsService.on('conversation_closed', this._handleConversationClosed)
|
|
565
567
|
);
|
|
568
|
+
// Track first vs reconnect locally so we only backfill on reconnects.
|
|
569
|
+
// First connect is right after _initWebSocket() and the surrounding
|
|
570
|
+
// flow has already loaded fresh data — re-fetching would be wasted.
|
|
571
|
+
let wsHasConnectedBefore = false;
|
|
566
572
|
this._wsUnsubscribers.push(
|
|
567
573
|
this.wsService.on('connected', () => {
|
|
568
574
|
console.log('[LiveChatWidget] WebSocket connected');
|
|
575
|
+
const isReconnect = wsHasConnectedBefore;
|
|
576
|
+
wsHasConnectedBefore = true;
|
|
569
577
|
if (this.LiveChatState.activeConversationId) {
|
|
570
578
|
this.wsService.send('conversation:subscribe', {
|
|
571
579
|
conversation_id: this.LiveChatState.activeConversationId,
|
|
572
580
|
});
|
|
581
|
+
// On reconnect, refetch the active conversation's messages.
|
|
582
|
+
// The server doesn't replay events that fired during the WS
|
|
583
|
+
// disconnect, so anything broadcast in the gap is otherwise
|
|
584
|
+
// permanently lost from the customer's view until they
|
|
585
|
+
// refresh the page.
|
|
586
|
+
if (isReconnect) {
|
|
587
|
+
this.fetchMessages(this.LiveChatState.activeConversationId).catch(
|
|
588
|
+
(err) => {
|
|
589
|
+
console.error(
|
|
590
|
+
'[LiveChatWidget] Failed to backfill messages on reconnect:',
|
|
591
|
+
err
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
}
|
|
573
596
|
}
|
|
574
597
|
})
|
|
575
598
|
);
|
|
@@ -95,11 +95,12 @@ export class LiveChatPanel {
|
|
|
95
95
|
</div>`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Hide nav in chat and
|
|
98
|
+
// Hide nav in chat, prechat and feedback views
|
|
99
99
|
if (navContainer) {
|
|
100
100
|
const hideNav =
|
|
101
101
|
this.state.currentView === 'chat' ||
|
|
102
|
-
this.state.currentView === 'prechat'
|
|
102
|
+
this.state.currentView === 'prechat' ||
|
|
103
|
+
this.state.currentView === 'feedback';
|
|
103
104
|
navContainer.style.display = hideNav ? 'none' : '';
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export class FeedbackFormView {
|
|
2
|
+
constructor(state, options = {}) {
|
|
3
|
+
this.state = state;
|
|
4
|
+
this.options = options;
|
|
5
|
+
this.element = null;
|
|
6
|
+
this._isSubmitting = false;
|
|
7
|
+
this._selectedRating = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
render() {
|
|
11
|
+
this.element = document.createElement('div');
|
|
12
|
+
this.element.className = 'liveChat-view liveChat-feedback-view';
|
|
13
|
+
this._renderForm();
|
|
14
|
+
return this.element;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_renderForm() {
|
|
18
|
+
this.element.innerHTML = `
|
|
19
|
+
<div class="liveChat-feedback-header">
|
|
20
|
+
<button class="sdk-btn-icon liveChat-feedback-back-btn">
|
|
21
|
+
<iconify-icon icon="ph:arrow-left" width="20" height="20"></iconify-icon>
|
|
22
|
+
</button>
|
|
23
|
+
<span class="liveChat-feedback-title">Leave us feedback</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="liveChat-feedback-body">
|
|
26
|
+
<p class="liveChat-feedback-prompt">Share your thoughts with us. We read every message.</p>
|
|
27
|
+
<input
|
|
28
|
+
type="text"
|
|
29
|
+
class="liveChat-feedback-input"
|
|
30
|
+
placeholder="Title"
|
|
31
|
+
/>
|
|
32
|
+
<textarea
|
|
33
|
+
class="liveChat-feedback-textarea"
|
|
34
|
+
placeholder="Your feedback..."
|
|
35
|
+
rows="5"
|
|
36
|
+
></textarea>
|
|
37
|
+
<button class="liveChat-feedback-submit">Send feedback</button>
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
this._attachEvents();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_renderThankYou() {
|
|
44
|
+
this.element.innerHTML = `
|
|
45
|
+
<div class="liveChat-feedback-header">
|
|
46
|
+
<button class="sdk-btn-icon liveChat-feedback-back-btn">
|
|
47
|
+
<iconify-icon icon="ph:arrow-left" width="20" height="20"></iconify-icon>
|
|
48
|
+
</button>
|
|
49
|
+
<span class="liveChat-feedback-title">Leave us feedback</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="liveChat-feedback-thankyou">
|
|
52
|
+
<span class="liveChat-feedback-thankyou-emoji">🙏</span>
|
|
53
|
+
<h3>Thanks for your feedback!</h3>
|
|
54
|
+
<p>We appreciate you taking the time to share your thoughts.</p>
|
|
55
|
+
<button class="liveChat-feedback-done-btn">Done</button>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
this.element.querySelector('.liveChat-feedback-back-btn').addEventListener('click', () => {
|
|
59
|
+
this.state.setView('home');
|
|
60
|
+
});
|
|
61
|
+
this.element.querySelector('.liveChat-feedback-done-btn').addEventListener('click', () => {
|
|
62
|
+
this.state.setView('home');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_attachEvents() {
|
|
67
|
+
this.element.querySelector('.liveChat-feedback-back-btn').addEventListener('click', () => {
|
|
68
|
+
this.state.setView('home');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const submitBtn = this.element.querySelector('.liveChat-feedback-submit');
|
|
72
|
+
submitBtn.addEventListener('click', async () => {
|
|
73
|
+
if (this._isSubmitting) return;
|
|
74
|
+
const title = this.element.querySelector('.liveChat-feedback-input').value.trim();
|
|
75
|
+
const message = this.element.querySelector('.liveChat-feedback-textarea').value.trim();
|
|
76
|
+
await this._submit(title, message);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async _submit(title, message) {
|
|
81
|
+
this._isSubmitting = true;
|
|
82
|
+
const submitBtn = this.element.querySelector('.liveChat-feedback-submit');
|
|
83
|
+
if (submitBtn) {
|
|
84
|
+
submitBtn.disabled = true;
|
|
85
|
+
submitBtn.textContent = 'Sending...';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (this.options.onSubmitFeedback) {
|
|
90
|
+
await this.options.onSubmitFeedback({ title, message });
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.warn('[FeedbackFormView] Submit error:', e);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this._renderThankYou();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
destroy() {
|
|
100
|
+
if (this.element && this.element.parentNode) {
|
|
101
|
+
this.element.parentNode.removeChild(this.element);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -136,8 +136,11 @@
|
|
|
136
136
|
const sendIcon = `<iconify-icon icon="ph:paper-plane-right" width="20" height="20" style="flex-shrink: 0;"></iconify-icon>`;
|
|
137
137
|
const caretIcon = `<iconify-icon icon="ph:caret-right" width="20" height="20" style="flex-shrink: 0;"></iconify-icon>`;
|
|
138
138
|
|
|
139
|
-
const
|
|
140
|
-
|
|
139
|
+
const isUnavailable = this.state.businessHoursState === 'offline' || this.state.businessHoursState === 'away';
|
|
140
|
+
const buttonLabel = isUnavailable ? 'Leave us a message' : this.state.startButtonText;
|
|
141
|
+
const buttonSubtext = isUnavailable
|
|
142
|
+
? "We'll get back to you when we're back"
|
|
143
|
+
: (this.state.responseTime || 'We typically reply within a few minutes');
|
|
141
144
|
|
|
142
145
|
const recentCardHtml = openConversation
|
|
143
146
|
? this._renderRecentMessageCard(openConversation)
|
|
@@ -147,8 +150,8 @@
|
|
|
147
150
|
${recentCardHtml}
|
|
148
151
|
<button class="liveChat-home-message-btn">
|
|
149
152
|
<div class="liveChat-home-continue-info">
|
|
150
|
-
<span class="liveChat-home-continue-label">${
|
|
151
|
-
<span class="liveChat-home-message-subtext">${
|
|
153
|
+
<span class="liveChat-home-continue-label">${buttonLabel}</span>
|
|
154
|
+
<span class="liveChat-home-message-subtext">${buttonSubtext}</span>
|
|
152
155
|
</div>
|
|
153
156
|
${sendIcon}
|
|
154
157
|
</button>
|
|
@@ -312,12 +315,7 @@
|
|
|
312
315
|
const feedbackBtn = this.element.querySelector('.liveChat-feedback-btn');
|
|
313
316
|
if (feedbackBtn) {
|
|
314
317
|
feedbackBtn.addEventListener('click', () => {
|
|
315
|
-
|
|
316
|
-
this.state.setOpen(false);
|
|
317
|
-
this.options.onFeedbackClick();
|
|
318
|
-
} else if (this.state.urls?.feedback) {
|
|
319
|
-
window.open(this.state.urls.feedback, '_blank');
|
|
320
|
-
}
|
|
318
|
+
this.state.setView('feedback');
|
|
321
319
|
});
|
|
322
320
|
}
|
|
323
321
|
|