@helllo-ai/agent-chat-widget 0.1.18 → 0.1.23
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/agent-chat.latest.js +446 -27
- package/agent-chat.prod.js +233 -49
- package/agent-chat.staging.js +233 -49
- package/package.json +1 -1
package/agent-chat.latest.js
CHANGED
|
@@ -20,10 +20,18 @@
|
|
|
20
20
|
return `
|
|
21
21
|
:host, .acw * { box-sizing: border-box; }
|
|
22
22
|
.acw-container { position: fixed; z-index: 2147483000; font-family: Inter, system-ui, -apple-system, sans-serif; }
|
|
23
|
+
.acw-container.bottom-right { right: 16px; bottom: 16px; }
|
|
24
|
+
.acw-container.middle-right { right: 16px; top: 50%; transform: translateY(-50%); }
|
|
23
25
|
.acw-launcher { background: ${safePrimary}; color: #fff; border: none; border-radius: 999px; padding: 10px 14px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.18); font-size: 14px; transition: transform 0.2s; }
|
|
24
26
|
.acw-launcher:hover { opacity: 0.95; }
|
|
27
|
+
.acw-launcher img { width: 24px; height: 24px; object-fit: contain; }
|
|
25
28
|
.acw-launcher.vertical { flex-direction: column; padding: 14px 10px; min-width: 48px; height: auto; }
|
|
26
29
|
.acw-launcher.vertical span { writing-mode: sideways-lr; text-orientation: mixed; letter-spacing: 0.5px; }
|
|
30
|
+
.acw-header-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
31
|
+
.acw-header-icon { width: 28px; height: 28px; object-fit: contain; flex-shrink: 0; }
|
|
32
|
+
.acw-header-title-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
33
|
+
.acw-unread-badge { background: rgba(255,255,255,0.9); color: ${safePrimary}; font-size: 11px; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: inline-flex; align-items: center; justify-content: center; padding: 0 5px; }
|
|
34
|
+
.acw-unread-badge.hidden { display: none; }
|
|
27
35
|
.acw-panel { position: absolute; width: 360px; max-width: 90vw; height: 520px; max-height: 80vh; background: ${safeBg}; border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; box-shadow: 0 16px 40px rgba(0,0,0,0.22); display: none; flex-direction: column; overflow: hidden; }
|
|
28
36
|
.acw-panel.bottom-right { right: 0; bottom: 56px; }
|
|
29
37
|
.acw-panel.middle-right { right: 56px; top: 50%; transform: translateY(-50%); }
|
|
@@ -40,13 +48,30 @@
|
|
|
40
48
|
.acw-msg-bot { margin-right: auto; background: rgba(0,0,0,0.05); color: #111; }
|
|
41
49
|
.acw-input { border-top: 1px solid rgba(0,0,0,0.08); padding: 10px; display: flex; gap: 8px; }
|
|
42
50
|
.acw-input input { flex: 1; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.12); border-radius: 8px; font-size: 14px; }
|
|
51
|
+
.acw-input input:disabled, .acw-input button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
43
52
|
.acw-input button { background: ${safePrimary}; color: #fff; border: none; border-radius: 8px; padding: 0 14px; cursor: pointer; font-weight: 600; }
|
|
53
|
+
.acw-footer { font-size: 11px; color: #94a3b8; padding: 6px 12px 10px; text-align: center; border-top: 1px solid rgba(0,0,0,0.06); }
|
|
54
|
+
.acw-footer a { color: #64748b; text-decoration: none; }
|
|
55
|
+
.acw-footer a:hover { text-decoration: underline; }
|
|
44
56
|
.acw-status { font-size: 12px; color: #666; padding: 8px 12px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid rgba(0,0,0,0.06); }
|
|
45
57
|
.acw-dot { width: 8px; height: 8px; border-radius: 999px; box-shadow: 0 0 0 6px rgba(0,0,0,0.04); }
|
|
46
58
|
.acw-dot.connected { background: #22c55e; box-shadow: 0 0 0 6px rgba(34,197,94,0.18); }
|
|
47
59
|
.acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
|
|
48
|
-
.acw-
|
|
49
|
-
.acw-
|
|
60
|
+
.acw-customer-form { position: absolute; inset: 0; background: ${safeBg}; z-index: 10; display: flex; flex-direction: column; padding: 20px; }
|
|
61
|
+
.acw-customer-form h3 { margin: 0 0 12px; font-size: 16px; font-weight: 600; color: #0f172a; }
|
|
62
|
+
.acw-customer-form p { margin: 0 0 16px; font-size: 13px; color: #64748b; }
|
|
63
|
+
.acw-customer-form .field { margin-bottom: 16px; }
|
|
64
|
+
.acw-customer-form label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: #475569; }
|
|
65
|
+
.acw-customer-form input { width: 100%; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.12); border-radius: 8px; font-size: 14px; }
|
|
66
|
+
.acw-customer-form .actions { display: flex; gap: 8px; margin-top: auto; }
|
|
67
|
+
.acw-customer-form button { flex: 1; padding: 10px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
68
|
+
.acw-customer-form .submit-btn { background: ${safePrimary}; color: #fff; }
|
|
69
|
+
.acw-customer-form .submit-btn:hover { opacity: 0.95; }
|
|
70
|
+
.acw-customer-form .skip-btn { background: #f8fafc; color: #64748b; border: 1px solid rgba(0,0,0,0.08); }
|
|
71
|
+
.acw-customer-form .skip-btn:hover { background: #eef2f7; }
|
|
72
|
+
.acw-start-cta { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
|
73
|
+
.acw-start-cta button { background: ${safePrimary}; color: #fff; border: none; border-radius: 10px; padding: 14px 24px; font-size: 15px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.12); }
|
|
74
|
+
.acw-start-cta button:hover { opacity: 0.95; }
|
|
50
75
|
@media (max-width: 480px) {
|
|
51
76
|
.acw-panel { width: calc(100vw - 24px); height: 70vh; }
|
|
52
77
|
}
|
|
@@ -65,9 +90,30 @@
|
|
|
65
90
|
wsUrl,
|
|
66
91
|
title = 'Chat with us',
|
|
67
92
|
greeting = null,
|
|
93
|
+
captureCustomerInfo = false,
|
|
94
|
+
allowSkipCustomerInfo = true,
|
|
68
95
|
position = 'bottom-right',
|
|
96
|
+
iconUrl = null,
|
|
97
|
+
showIcon = true,
|
|
98
|
+
launcherText = 'Chat',
|
|
69
99
|
} = config
|
|
70
100
|
|
|
101
|
+
// Resolve icon: explicit iconUrl, then page favicon, then /favicon.ico (only used when showIcon is true)
|
|
102
|
+
const resolvedIconUrl = (() => {
|
|
103
|
+
if (!showIcon) return null
|
|
104
|
+
if (iconUrl && typeof iconUrl === 'string' && iconUrl.trim()) return iconUrl.trim()
|
|
105
|
+
try {
|
|
106
|
+
const link = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]')
|
|
107
|
+
if (link && link.href) return link.href
|
|
108
|
+
} catch (_) {}
|
|
109
|
+
try {
|
|
110
|
+
const origin = window.location.origin || ''
|
|
111
|
+
if (origin) return origin + '/favicon.ico'
|
|
112
|
+
} catch (_) {}
|
|
113
|
+
return null
|
|
114
|
+
})()
|
|
115
|
+
const launcherLabel = (typeof launcherText === 'string' && launcherText.trim()) ? launcherText.trim() : 'Chat'
|
|
116
|
+
|
|
71
117
|
// Valid positions
|
|
72
118
|
const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
|
|
73
119
|
const finalPosition = validPositions.includes(position) ? position : 'bottom-right'
|
|
@@ -130,13 +176,42 @@
|
|
|
130
176
|
|
|
131
177
|
const launcher = document.createElement('button')
|
|
132
178
|
launcher.className = isVertical ? 'acw-launcher vertical' : 'acw-launcher'
|
|
133
|
-
|
|
179
|
+
if (resolvedIconUrl) {
|
|
180
|
+
const launcherImg = document.createElement('img')
|
|
181
|
+
launcherImg.src = resolvedIconUrl
|
|
182
|
+
launcherImg.alt = launcherLabel
|
|
183
|
+
launcher.appendChild(launcherImg)
|
|
184
|
+
const launcherTextSpan = document.createElement('span')
|
|
185
|
+
launcherTextSpan.textContent = launcherLabel
|
|
186
|
+
launcher.appendChild(launcherTextSpan)
|
|
187
|
+
} else {
|
|
188
|
+
const span = document.createElement('span')
|
|
189
|
+
span.textContent = launcherLabel
|
|
190
|
+
launcher.appendChild(span)
|
|
191
|
+
}
|
|
134
192
|
|
|
135
193
|
const header = document.createElement('div')
|
|
136
194
|
header.className = 'acw-header'
|
|
195
|
+
const headerLeft = document.createElement('div')
|
|
196
|
+
headerLeft.className = 'acw-header-left'
|
|
197
|
+
if (resolvedIconUrl) {
|
|
198
|
+
const headerIcon = document.createElement('img')
|
|
199
|
+
headerIcon.className = 'acw-header-icon'
|
|
200
|
+
headerIcon.src = resolvedIconUrl
|
|
201
|
+
headerIcon.alt = ''
|
|
202
|
+
headerLeft.appendChild(headerIcon)
|
|
203
|
+
}
|
|
204
|
+
const titleWrap = document.createElement('div')
|
|
205
|
+
titleWrap.className = 'acw-header-title-wrap'
|
|
137
206
|
const titleEl = document.createElement('div')
|
|
138
207
|
titleEl.className = 'acw-title'
|
|
139
208
|
titleEl.textContent = title
|
|
209
|
+
const unreadBadge = document.createElement('span')
|
|
210
|
+
unreadBadge.className = 'acw-unread-badge hidden'
|
|
211
|
+
unreadBadge.textContent = '0'
|
|
212
|
+
titleWrap.appendChild(titleEl)
|
|
213
|
+
titleWrap.appendChild(unreadBadge)
|
|
214
|
+
headerLeft.appendChild(titleWrap)
|
|
140
215
|
const headerActions = document.createElement('div')
|
|
141
216
|
headerActions.style.display = 'flex'
|
|
142
217
|
headerActions.style.alignItems = 'center'
|
|
@@ -151,7 +226,7 @@
|
|
|
151
226
|
|
|
152
227
|
headerActions.appendChild(minimizeBtn)
|
|
153
228
|
headerActions.appendChild(closeBtn)
|
|
154
|
-
header.appendChild(
|
|
229
|
+
header.appendChild(headerLeft)
|
|
155
230
|
header.appendChild(headerActions)
|
|
156
231
|
|
|
157
232
|
const statusEl = document.createElement('div')
|
|
@@ -160,15 +235,17 @@
|
|
|
160
235
|
statusDot.className = 'acw-dot disconnected'
|
|
161
236
|
const statusText = document.createElement('span')
|
|
162
237
|
statusText.textContent = 'Disconnected'
|
|
163
|
-
const disconnectBtn = document.createElement('button')
|
|
164
|
-
disconnectBtn.className = 'acw-disconnect'
|
|
165
|
-
disconnectBtn.textContent = 'Connect'
|
|
166
238
|
statusEl.appendChild(statusDot)
|
|
167
239
|
statusEl.appendChild(statusText)
|
|
168
|
-
|
|
240
|
+
|
|
241
|
+
const startCtaEl = document.createElement('div')
|
|
242
|
+
startCtaEl.className = 'acw-start-cta'
|
|
243
|
+
const startConversationBtn = document.createElement('button')
|
|
244
|
+
startConversationBtn.textContent = 'Click to Start Conversation'
|
|
245
|
+
startCtaEl.appendChild(startConversationBtn)
|
|
169
246
|
|
|
170
247
|
const messages = document.createElement('div')
|
|
171
|
-
|
|
248
|
+
messages.className = 'acw-messages'
|
|
172
249
|
|
|
173
250
|
const inputWrap = document.createElement('div')
|
|
174
251
|
inputWrap.className = 'acw-input'
|
|
@@ -180,20 +257,219 @@
|
|
|
180
257
|
inputWrap.appendChild(input)
|
|
181
258
|
inputWrap.appendChild(sendBtn)
|
|
182
259
|
|
|
260
|
+
const footerEl = document.createElement('div')
|
|
261
|
+
footerEl.className = 'acw-footer'
|
|
262
|
+
footerEl.style.display = 'none'
|
|
263
|
+
const footerLink = document.createElement('a')
|
|
264
|
+
footerLink.href = 'https://helllo.ai'
|
|
265
|
+
footerLink.target = '_blank'
|
|
266
|
+
footerLink.rel = 'noopener noreferrer'
|
|
267
|
+
footerLink.textContent = 'helllo.ai'
|
|
268
|
+
footerEl.appendChild(document.createTextNode('Powered by '))
|
|
269
|
+
footerEl.appendChild(footerLink)
|
|
270
|
+
|
|
271
|
+
// Customer info form (if enabled)
|
|
272
|
+
let customerForm = null
|
|
273
|
+
let sessionId = null
|
|
274
|
+
let customerInfoSubmitted = false
|
|
275
|
+
let firstMessageReceived = false
|
|
276
|
+
let conversationStarted = false
|
|
277
|
+
let formShownBeforeConnect = false
|
|
278
|
+
let pendingCustomerName = null
|
|
279
|
+
let pendingCustomerPhone = null
|
|
280
|
+
let unreadCount = 0
|
|
281
|
+
|
|
282
|
+
if (captureCustomerInfo) {
|
|
283
|
+
customerForm = document.createElement('div')
|
|
284
|
+
customerForm.className = 'acw-customer-form'
|
|
285
|
+
customerForm.style.display = 'none'
|
|
286
|
+
|
|
287
|
+
const formTitle = document.createElement('h3')
|
|
288
|
+
formTitle.textContent = 'Welcome! Please share your details'
|
|
289
|
+
const formDesc = document.createElement('p')
|
|
290
|
+
formDesc.textContent = 'We need a few details to get started'
|
|
291
|
+
|
|
292
|
+
const nameField = document.createElement('div')
|
|
293
|
+
nameField.className = 'field'
|
|
294
|
+
const nameLabel = document.createElement('label')
|
|
295
|
+
nameLabel.textContent = 'Your Name'
|
|
296
|
+
nameLabel.setAttribute('for', 'acw-customer-name')
|
|
297
|
+
const nameInput = document.createElement('input')
|
|
298
|
+
nameInput.id = 'acw-customer-name'
|
|
299
|
+
nameInput.type = 'text'
|
|
300
|
+
nameInput.placeholder = 'Enter your name'
|
|
301
|
+
nameField.appendChild(nameLabel)
|
|
302
|
+
nameField.appendChild(nameInput)
|
|
303
|
+
|
|
304
|
+
const phoneField = document.createElement('div')
|
|
305
|
+
phoneField.className = 'field'
|
|
306
|
+
const phoneLabel = document.createElement('label')
|
|
307
|
+
phoneLabel.textContent = 'Phone Number'
|
|
308
|
+
phoneLabel.setAttribute('for', 'acw-customer-phone')
|
|
309
|
+
const phoneInput = document.createElement('input')
|
|
310
|
+
phoneInput.id = 'acw-customer-phone'
|
|
311
|
+
phoneInput.type = 'tel'
|
|
312
|
+
phoneInput.placeholder = 'Enter your phone number'
|
|
313
|
+
phoneField.appendChild(phoneLabel)
|
|
314
|
+
phoneField.appendChild(phoneInput)
|
|
315
|
+
|
|
316
|
+
const formActions = document.createElement('div')
|
|
317
|
+
formActions.className = 'actions'
|
|
318
|
+
const submitBtn = document.createElement('button')
|
|
319
|
+
submitBtn.className = 'submit-btn'
|
|
320
|
+
submitBtn.textContent = 'Start Chat'
|
|
321
|
+
const skipBtn = document.createElement('button')
|
|
322
|
+
skipBtn.className = 'skip-btn'
|
|
323
|
+
skipBtn.textContent = 'Skip'
|
|
324
|
+
formActions.appendChild(submitBtn)
|
|
325
|
+
if (allowSkipCustomerInfo) {
|
|
326
|
+
formActions.appendChild(skipBtn)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
customerForm.appendChild(formTitle)
|
|
330
|
+
customerForm.appendChild(formDesc)
|
|
331
|
+
customerForm.appendChild(nameField)
|
|
332
|
+
customerForm.appendChild(phoneField)
|
|
333
|
+
customerForm.appendChild(formActions)
|
|
334
|
+
|
|
335
|
+
function submitCustomerInfo() {
|
|
336
|
+
const name = nameInput.value.trim()
|
|
337
|
+
const phone = phoneInput.value.trim()
|
|
338
|
+
|
|
339
|
+
if (!name || !phone) {
|
|
340
|
+
alert('Please fill in both name and phone number')
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Form shown before connect: save and connect; customer_info sent in connection_established
|
|
345
|
+
if (formShownBeforeConnect) {
|
|
346
|
+
pendingCustomerName = name
|
|
347
|
+
pendingCustomerPhone = phone
|
|
348
|
+
customerForm.style.display = 'none'
|
|
349
|
+
messages.style.display = 'flex'
|
|
350
|
+
inputWrap.style.display = 'none'
|
|
351
|
+
connect()
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!sessionId) {
|
|
356
|
+
console.error('[AgentChatWidget] No session ID available')
|
|
357
|
+
alert('Session not established. Please wait for connection.')
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
362
|
+
alert('WebSocket not connected. Please wait for connection.')
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
ws.send(JSON.stringify({
|
|
368
|
+
type: 'customer_info',
|
|
369
|
+
session_id: sessionId,
|
|
370
|
+
phone_number: phone,
|
|
371
|
+
customer_name: name,
|
|
372
|
+
embed_key: embedKey
|
|
373
|
+
}))
|
|
374
|
+
customerInfoSubmitted = true
|
|
375
|
+
customerForm.style.display = 'none'
|
|
376
|
+
messages.style.display = 'flex'
|
|
377
|
+
inputWrap.style.display = 'flex'
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error('[AgentChatWidget] Error sending customer info:', error)
|
|
380
|
+
alert('Error submitting information. Please try again.')
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function skipCustomerInfo() {
|
|
385
|
+
if (formShownBeforeConnect) {
|
|
386
|
+
customerForm.style.display = 'none'
|
|
387
|
+
messages.style.display = 'flex'
|
|
388
|
+
inputWrap.style.display = 'none'
|
|
389
|
+
connect()
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
customerInfoSubmitted = true
|
|
393
|
+
customerForm.style.display = 'none'
|
|
394
|
+
messages.style.display = 'flex'
|
|
395
|
+
inputWrap.style.display = 'flex'
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
submitBtn.onclick = submitCustomerInfo
|
|
399
|
+
if (allowSkipCustomerInfo) skipBtn.onclick = skipCustomerInfo
|
|
400
|
+
|
|
401
|
+
// Allow Enter key to submit
|
|
402
|
+
nameInput.addEventListener('keydown', (e) => {
|
|
403
|
+
if (e.key === 'Enter') {
|
|
404
|
+
e.preventDefault()
|
|
405
|
+
phoneInput.focus()
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
phoneInput.addEventListener('keydown', (e) => {
|
|
409
|
+
if (e.key === 'Enter') {
|
|
410
|
+
e.preventDefault()
|
|
411
|
+
submitCustomerInfo()
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
panel.appendChild(customerForm)
|
|
416
|
+
}
|
|
417
|
+
|
|
183
418
|
panel.appendChild(header)
|
|
184
419
|
panel.appendChild(statusEl)
|
|
420
|
+
panel.appendChild(startCtaEl)
|
|
185
421
|
panel.appendChild(messages)
|
|
186
422
|
panel.appendChild(inputWrap)
|
|
423
|
+
panel.appendChild(footerEl)
|
|
187
424
|
shadow.appendChild(panel)
|
|
188
425
|
shadow.appendChild(launcher)
|
|
189
426
|
document.body.appendChild(container)
|
|
190
427
|
|
|
428
|
+
// Initial state: show start CTA, hide messages and input until user starts conversation
|
|
429
|
+
messages.style.display = 'none'
|
|
430
|
+
inputWrap.style.display = 'none'
|
|
431
|
+
|
|
432
|
+
function showStartCta() {
|
|
433
|
+
startCtaEl.style.display = 'flex'
|
|
434
|
+
messages.style.display = 'none'
|
|
435
|
+
inputWrap.style.display = 'none'
|
|
436
|
+
footerEl.style.display = 'none'
|
|
437
|
+
if (customerForm) customerForm.style.display = 'none'
|
|
438
|
+
}
|
|
439
|
+
function showFormOnly() {
|
|
440
|
+
startCtaEl.style.display = 'none'
|
|
441
|
+
messages.style.display = 'none'
|
|
442
|
+
inputWrap.style.display = 'none'
|
|
443
|
+
footerEl.style.display = 'none'
|
|
444
|
+
if (customerForm) customerForm.style.display = 'flex'
|
|
445
|
+
}
|
|
446
|
+
function showChat() {
|
|
447
|
+
startCtaEl.style.display = 'none'
|
|
448
|
+
if (customerForm) customerForm.style.display = 'none'
|
|
449
|
+
messages.style.display = 'flex'
|
|
450
|
+
inputWrap.style.display = 'flex'
|
|
451
|
+
footerEl.style.display = 'block'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function updateUnreadBadge() {
|
|
455
|
+
unreadBadge.textContent = String(unreadCount)
|
|
456
|
+
if (unreadCount > 0) {
|
|
457
|
+
unreadBadge.classList.remove('hidden')
|
|
458
|
+
} else {
|
|
459
|
+
unreadBadge.classList.add('hidden')
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
191
463
|
function appendMessage(text, role) {
|
|
192
464
|
const msg = document.createElement('div')
|
|
193
465
|
msg.className = 'acw-msg ' + (role === 'user' ? 'acw-msg-user' : 'acw-msg-bot')
|
|
194
466
|
msg.textContent = text
|
|
195
467
|
messages.appendChild(msg)
|
|
196
468
|
messages.scrollTop = messages.scrollHeight
|
|
469
|
+
if (role === 'bot' && panel.style.display !== 'flex') {
|
|
470
|
+
unreadCount++
|
|
471
|
+
updateUnreadBadge()
|
|
472
|
+
}
|
|
197
473
|
}
|
|
198
474
|
|
|
199
475
|
// No default greeting; only show messages after connection
|
|
@@ -203,10 +479,32 @@
|
|
|
203
479
|
let connecting = false
|
|
204
480
|
|
|
205
481
|
const resolvedWsUrl = (() => {
|
|
482
|
+
// Explicit wsUrl takes highest priority
|
|
206
483
|
if (wsUrl) return wsUrl
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
484
|
+
|
|
485
|
+
// Determine environment-specific WebSocket URL based on VERSION
|
|
486
|
+
let wsBase
|
|
487
|
+
if (VERSION.includes('-staging')) {
|
|
488
|
+
wsBase = 'wss://talk2ai-staging.helllo.ai'
|
|
489
|
+
} else if (VERSION.includes('-prod')) {
|
|
490
|
+
wsBase = 'wss://talk2ai-prod.helllo.ai'
|
|
491
|
+
} else {
|
|
492
|
+
// For dev/latest, use existing fallback logic
|
|
493
|
+
const baseCandidate = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
|
|
494
|
+
const voiceBase = (baseCandidate || '').replace(/\/$/, '')
|
|
495
|
+
// Handle both http/https and ws/wss URLs
|
|
496
|
+
if (voiceBase.startsWith('http://')) {
|
|
497
|
+
wsBase = voiceBase.replace(/^http/, 'ws')
|
|
498
|
+
} else if (voiceBase.startsWith('https://')) {
|
|
499
|
+
wsBase = voiceBase.replace(/^https/, 'wss')
|
|
500
|
+
} else if (voiceBase.startsWith('ws://') || voiceBase.startsWith('wss://')) {
|
|
501
|
+
wsBase = voiceBase // Already a WebSocket URL
|
|
502
|
+
} else {
|
|
503
|
+
// Default to ws if no protocol
|
|
504
|
+
wsBase = 'ws://' + voiceBase
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
210
508
|
const params = new URLSearchParams()
|
|
211
509
|
params.set('embed_key', embedKey)
|
|
212
510
|
params.set('host', window.location.hostname)
|
|
@@ -218,25 +516,34 @@
|
|
|
218
516
|
console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
|
|
219
517
|
} catch (_) {}
|
|
220
518
|
|
|
519
|
+
function setInputEnabled(enabled) {
|
|
520
|
+
input.disabled = !enabled
|
|
521
|
+
sendBtn.disabled = !enabled
|
|
522
|
+
input.placeholder = enabled ? 'Type a message...' : 'Connect to start chatting'
|
|
523
|
+
}
|
|
524
|
+
|
|
221
525
|
function updateStatus(text, isConnected) {
|
|
222
526
|
statusText.textContent = text
|
|
223
527
|
statusDot.classList.remove('connected', 'disconnected')
|
|
224
528
|
statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
|
|
225
|
-
|
|
226
|
-
// Update button text and handler based on connection status
|
|
227
|
-
disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
|
|
228
|
-
disconnectBtn.onclick = isConnected ? disconnect : connect
|
|
529
|
+
setInputEnabled(isConnected)
|
|
229
530
|
}
|
|
230
531
|
|
|
231
532
|
function connect() {
|
|
232
533
|
if (connected || connecting) return
|
|
233
534
|
connecting = true
|
|
535
|
+
firstMessageReceived = false
|
|
536
|
+
startCtaEl.style.display = 'none'
|
|
537
|
+
// Show chat UI immediately so the panel is not empty while connecting
|
|
538
|
+
showChat()
|
|
539
|
+
setInputEnabled(false)
|
|
234
540
|
const url = resolvedWsUrl
|
|
235
541
|
try {
|
|
236
542
|
ws = new WebSocket(url)
|
|
237
543
|
} catch (err) {
|
|
238
544
|
connecting = false
|
|
239
|
-
updateStatus('
|
|
545
|
+
updateStatus('Disconnected', false)
|
|
546
|
+
appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
|
|
240
547
|
console.error('[AgentChatWidget] WS error', err)
|
|
241
548
|
return
|
|
242
549
|
}
|
|
@@ -245,33 +552,93 @@
|
|
|
245
552
|
connected = true
|
|
246
553
|
connecting = false
|
|
247
554
|
updateStatus('Connected', true)
|
|
555
|
+
// If captureCustomerInfo, connection_established will show form or chat
|
|
556
|
+
if (!captureCustomerInfo) showChat()
|
|
248
557
|
}
|
|
249
558
|
|
|
250
559
|
ws.onmessage = (event) => {
|
|
251
560
|
try {
|
|
252
561
|
const data = JSON.parse(event.data)
|
|
562
|
+
|
|
563
|
+
// Handle connection_established message
|
|
564
|
+
if (data.type === 'connection_established') {
|
|
565
|
+
if (data.session_id) sessionId = data.session_id
|
|
566
|
+
|
|
567
|
+
// Form was shown before connect (Start Conversation → form → submit/skip): send customer_info if pending, then show chat
|
|
568
|
+
if (formShownBeforeConnect) {
|
|
569
|
+
if (pendingCustomerName && pendingCustomerPhone && ws && ws.readyState === WebSocket.OPEN) {
|
|
570
|
+
try {
|
|
571
|
+
ws.send(JSON.stringify({
|
|
572
|
+
type: 'customer_info',
|
|
573
|
+
session_id: sessionId,
|
|
574
|
+
phone_number: pendingCustomerPhone,
|
|
575
|
+
customer_name: pendingCustomerName,
|
|
576
|
+
embed_key: embedKey
|
|
577
|
+
}))
|
|
578
|
+
} catch (e) {
|
|
579
|
+
console.error('[AgentChatWidget] Error sending customer info:', e)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
pendingCustomerName = null
|
|
583
|
+
pendingCustomerPhone = null
|
|
584
|
+
customerInfoSubmitted = true
|
|
585
|
+
if (data.welcome_message) appendMessage(data.welcome_message, 'bot')
|
|
586
|
+
showChat()
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Display welcome message if provided
|
|
591
|
+
if (data.welcome_message) {
|
|
592
|
+
appendMessage(data.welcome_message, 'bot')
|
|
593
|
+
if (!firstMessageReceived && captureCustomerInfo && customerForm && !customerInfoSubmitted) {
|
|
594
|
+
firstMessageReceived = true
|
|
595
|
+
setTimeout(() => {
|
|
596
|
+
showFormOnly()
|
|
597
|
+
const nameInput = customerForm.querySelector('#acw-customer-name')
|
|
598
|
+
if (nameInput) setTimeout(() => nameInput.focus(), 100)
|
|
599
|
+
}, 300)
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
if (!firstMessageReceived && captureCustomerInfo && customerForm && !customerInfoSubmitted) {
|
|
603
|
+
firstMessageReceived = true
|
|
604
|
+
setTimeout(() => {
|
|
605
|
+
showFormOnly()
|
|
606
|
+
const nameInput = customerForm.querySelector('#acw-customer-name')
|
|
607
|
+
if (nameInput) setTimeout(() => nameInput.focus(), 100)
|
|
608
|
+
}, 100)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (captureCustomerInfo && !customerInfoSubmitted && customerForm) return
|
|
612
|
+
showChat()
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Handle regular chat messages
|
|
253
617
|
const text = data.text || data.content || data.message || ''
|
|
254
618
|
const role = data.role || 'assistant'
|
|
255
|
-
if (text)
|
|
619
|
+
if (text) {
|
|
620
|
+
appendMessage(text, role === 'user' ? 'user' : 'bot')
|
|
621
|
+
}
|
|
256
622
|
} catch (e) {
|
|
257
623
|
// Fallback to raw text
|
|
258
|
-
if (event.data)
|
|
624
|
+
if (event.data) {
|
|
625
|
+
appendMessage(String(event.data), 'bot')
|
|
626
|
+
}
|
|
259
627
|
}
|
|
260
628
|
}
|
|
261
629
|
|
|
262
630
|
ws.onerror = (e) => {
|
|
263
631
|
console.error('[AgentChatWidget] WS error', e)
|
|
264
|
-
updateStatus('
|
|
265
|
-
appendMessage('
|
|
632
|
+
updateStatus('Disconnected', false)
|
|
633
|
+
appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
|
|
266
634
|
}
|
|
267
635
|
|
|
268
636
|
ws.onclose = (ev) => {
|
|
269
637
|
connected = false
|
|
270
638
|
connecting = false
|
|
271
|
-
const reason = ev && (ev.reason || ev.code)
|
|
272
639
|
updateStatus('Disconnected', false)
|
|
273
640
|
if (!ev.wasClean) {
|
|
274
|
-
appendMessage(
|
|
641
|
+
appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
|
|
275
642
|
}
|
|
276
643
|
}
|
|
277
644
|
}
|
|
@@ -284,14 +651,38 @@
|
|
|
284
651
|
connected = false
|
|
285
652
|
connecting = false
|
|
286
653
|
updateStatus('Disconnected', false)
|
|
654
|
+
sessionId = null
|
|
655
|
+
customerInfoSubmitted = false
|
|
656
|
+
firstMessageReceived = false
|
|
657
|
+
conversationStarted = false
|
|
658
|
+
formShownBeforeConnect = false
|
|
659
|
+
pendingCustomerName = null
|
|
660
|
+
pendingCustomerPhone = null
|
|
661
|
+
if (customerForm) {
|
|
662
|
+
customerForm.style.display = 'none'
|
|
663
|
+
const nameInput = customerForm.querySelector('#acw-customer-name')
|
|
664
|
+
const phoneInput = customerForm.querySelector('#acw-customer-phone')
|
|
665
|
+
if (nameInput) nameInput.value = ''
|
|
666
|
+
if (phoneInput) phoneInput.value = ''
|
|
667
|
+
}
|
|
668
|
+
showStartCta()
|
|
287
669
|
}
|
|
288
670
|
|
|
289
671
|
function sendMessage() {
|
|
290
672
|
const text = input.value.trim()
|
|
291
673
|
if (!text) return
|
|
674
|
+
|
|
675
|
+
// Don't allow sending messages if customer info form is required and not submitted
|
|
676
|
+
if (captureCustomerInfo && !customerInfoSubmitted) {
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
292
680
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
293
|
-
|
|
294
|
-
|
|
681
|
+
if (connecting) {
|
|
682
|
+
appendMessage('Connecting... Please wait.', 'bot')
|
|
683
|
+
} else {
|
|
684
|
+
appendMessage('Please click "Click to Start Conversation" to start chatting.', 'bot')
|
|
685
|
+
}
|
|
295
686
|
return
|
|
296
687
|
}
|
|
297
688
|
appendMessage(text, 'user')
|
|
@@ -299,6 +690,19 @@
|
|
|
299
690
|
input.value = ''
|
|
300
691
|
}
|
|
301
692
|
|
|
693
|
+
startConversationBtn.onclick = () => {
|
|
694
|
+
conversationStarted = true
|
|
695
|
+
startCtaEl.style.display = 'none'
|
|
696
|
+
if (captureCustomerInfo && customerForm) {
|
|
697
|
+
formShownBeforeConnect = true
|
|
698
|
+
showFormOnly()
|
|
699
|
+
const nameInput = customerForm.querySelector('#acw-customer-name')
|
|
700
|
+
if (nameInput) setTimeout(() => nameInput.focus(), 100)
|
|
701
|
+
} else {
|
|
702
|
+
connect()
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
302
706
|
launcher.onclick = () => {
|
|
303
707
|
const isOpen = panel.style.display === 'flex'
|
|
304
708
|
if (isOpen) {
|
|
@@ -306,7 +710,9 @@
|
|
|
306
710
|
disconnect()
|
|
307
711
|
} else {
|
|
308
712
|
panel.style.display = 'flex'
|
|
309
|
-
|
|
713
|
+
unreadCount = 0
|
|
714
|
+
updateUnreadBadge()
|
|
715
|
+
if (!connected && !connecting && !conversationStarted) showStartCta()
|
|
310
716
|
}
|
|
311
717
|
}
|
|
312
718
|
|
|
@@ -335,7 +741,7 @@
|
|
|
335
741
|
},
|
|
336
742
|
open() {
|
|
337
743
|
panel.style.display = 'flex'
|
|
338
|
-
|
|
744
|
+
// Connection only via explicit Connect button
|
|
339
745
|
},
|
|
340
746
|
close() {
|
|
341
747
|
panel.style.display = 'none'
|
|
@@ -348,9 +754,17 @@
|
|
|
348
754
|
const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
|
|
349
755
|
const ds = (scriptEl && scriptEl.dataset) || {}
|
|
350
756
|
const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
|
|
757
|
+
const captureCustomerInfo = userConfig.captureCustomerInfo !== undefined
|
|
758
|
+
? userConfig.captureCustomerInfo
|
|
759
|
+
: ds.captureCustomerInfo === 'true' || ds.capture_customer_info === 'true'
|
|
760
|
+
const allowSkipCustomerInfo = userConfig.allowSkipCustomerInfo !== undefined
|
|
761
|
+
? userConfig.allowSkipCustomerInfo
|
|
762
|
+
: ds.allowSkipCustomerInfo !== 'false' && ds.allow_skip_customer_info !== 'false'
|
|
351
763
|
const position = userConfig.position || ds.position || ds.launcherPosition || 'bottom-right'
|
|
352
764
|
const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
|
|
353
765
|
const validPosition = validPositions.includes(position) ? position : 'bottom-right'
|
|
766
|
+
const showIcon = userConfig.showIcon !== undefined ? userConfig.showIcon : ds.showIcon !== 'false' && ds.show_icon !== 'false'
|
|
767
|
+
const launcherText = userConfig.launcherText ?? userConfig.launcher_text ?? ds.launcherText ?? ds.launcher_text ?? 'Chat'
|
|
354
768
|
return {
|
|
355
769
|
agentId: userConfig.agentId || ds.agentId,
|
|
356
770
|
embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
|
|
@@ -362,7 +776,12 @@
|
|
|
362
776
|
wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
|
|
363
777
|
title: userConfig.title || ds.title,
|
|
364
778
|
greeting: userConfig.greeting || ds.greeting,
|
|
779
|
+
captureCustomerInfo: captureCustomerInfo,
|
|
780
|
+
allowSkipCustomerInfo: allowSkipCustomerInfo,
|
|
365
781
|
position: validPosition,
|
|
782
|
+
iconUrl: userConfig.iconUrl || userConfig.icon_url || ds.iconUrl || ds.icon_url,
|
|
783
|
+
showIcon: showIcon,
|
|
784
|
+
launcherText: launcherText,
|
|
366
785
|
}
|
|
367
786
|
}
|
|
368
787
|
|