@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.
@@ -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-disconnect { margin-left: auto; background: #f8fafc; color: #0f172a; border: 1px solid rgba(0,0,0,0.08); border-radius: 6px; padding: 6px 8px; font-size: 12px; cursor: pointer; }
49
- .acw-disconnect:hover { background: #eef2f7; }
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
- launcher.innerHTML = '<span>Chat</span>'
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(titleEl)
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
- statusEl.appendChild(disconnectBtn)
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
- messages.className = 'acw-messages'
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
- const baseCandidate = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
208
- const voiceBase = (baseCandidate || '').replace(/\/$/, '')
209
- const wsBase = voiceBase.replace(/^http/, 'ws')
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('Connection error', false)
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) appendMessage(text, role === 'user' ? 'user' : 'bot')
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) appendMessage(String(event.data), 'bot')
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('Connection error', false)
265
- appendMessage('Connection error. Check WS service and URL.', 'bot')
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(`Connection closed (${reason || 'unclean'})`, 'bot')
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
- connect()
294
- setTimeout(sendMessage, 200)
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
- connect()
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
- connect()
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