@helllo-ai/agent-chat-widget 0.1.17 → 0.1.21

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
 
@@ -24,8 +24,14 @@
24
24
  .acw-container.middle-right { right: 16px; top: 50%; transform: translateY(-50%); }
25
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; }
26
26
  .acw-launcher:hover { opacity: 0.95; }
27
+ .acw-launcher img { width: 24px; height: 24px; object-fit: contain; }
27
28
  .acw-launcher.vertical { flex-direction: column; padding: 14px 10px; min-width: 48px; height: auto; }
28
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; }
29
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; }
30
36
  .acw-panel.bottom-right { right: 0; bottom: 56px; }
31
37
  .acw-panel.middle-right { right: 56px; top: 50%; transform: translateY(-50%); }
@@ -42,13 +48,15 @@
42
48
  .acw-msg-bot { margin-right: auto; background: rgba(0,0,0,0.05); color: #111; }
43
49
  .acw-input { border-top: 1px solid rgba(0,0,0,0.08); padding: 10px; display: flex; gap: 8px; }
44
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; }
45
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; }
46
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); }
47
57
  .acw-dot { width: 8px; height: 8px; border-radius: 999px; box-shadow: 0 0 0 6px rgba(0,0,0,0.04); }
48
58
  .acw-dot.connected { background: #22c55e; box-shadow: 0 0 0 6px rgba(34,197,94,0.18); }
49
59
  .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
50
- .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; }
51
- .acw-disconnect:hover { background: #eef2f7; }
52
60
  .acw-customer-form { position: absolute; inset: 0; background: ${safeBg}; z-index: 10; display: flex; flex-direction: column; padding: 20px; }
53
61
  .acw-customer-form h3 { margin: 0 0 12px; font-size: 16px; font-weight: 600; color: #0f172a; }
54
62
  .acw-customer-form p { margin: 0 0 16px; font-size: 13px; color: #64748b; }
@@ -61,6 +69,9 @@
61
69
  .acw-customer-form .submit-btn:hover { opacity: 0.95; }
62
70
  .acw-customer-form .skip-btn { background: #f8fafc; color: #64748b; border: 1px solid rgba(0,0,0,0.08); }
63
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; }
64
75
  @media (max-width: 480px) {
65
76
  .acw-panel { width: calc(100vw - 24px); height: 70vh; }
66
77
  }
@@ -80,9 +91,29 @@
80
91
  title = 'Chat with us',
81
92
  greeting = null,
82
93
  captureCustomerInfo = false,
94
+ allowSkipCustomerInfo = true,
83
95
  position = 'bottom-right',
96
+ iconUrl = null,
97
+ showIcon = true,
98
+ launcherText = 'Chat',
84
99
  } = config
85
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
+
86
117
  // Valid positions
87
118
  const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
88
119
  const finalPosition = validPositions.includes(position) ? position : 'bottom-right'
@@ -145,13 +176,42 @@
145
176
 
146
177
  const launcher = document.createElement('button')
147
178
  launcher.className = isVertical ? 'acw-launcher vertical' : 'acw-launcher'
148
- 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
+ }
149
192
 
150
193
  const header = document.createElement('div')
151
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'
152
206
  const titleEl = document.createElement('div')
153
207
  titleEl.className = 'acw-title'
154
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)
155
215
  const headerActions = document.createElement('div')
156
216
  headerActions.style.display = 'flex'
157
217
  headerActions.style.alignItems = 'center'
@@ -166,7 +226,7 @@
166
226
 
167
227
  headerActions.appendChild(minimizeBtn)
168
228
  headerActions.appendChild(closeBtn)
169
- header.appendChild(titleEl)
229
+ header.appendChild(headerLeft)
170
230
  header.appendChild(headerActions)
171
231
 
172
232
  const statusEl = document.createElement('div')
@@ -175,15 +235,17 @@
175
235
  statusDot.className = 'acw-dot disconnected'
176
236
  const statusText = document.createElement('span')
177
237
  statusText.textContent = 'Disconnected'
178
- const disconnectBtn = document.createElement('button')
179
- disconnectBtn.className = 'acw-disconnect'
180
- disconnectBtn.textContent = 'Connect'
181
238
  statusEl.appendChild(statusDot)
182
239
  statusEl.appendChild(statusText)
183
- 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)
184
246
 
185
247
  const messages = document.createElement('div')
186
- messages.className = 'acw-messages'
248
+ messages.className = 'acw-messages'
187
249
 
188
250
  const inputWrap = document.createElement('div')
189
251
  inputWrap.className = 'acw-input'
@@ -195,10 +257,27 @@
195
257
  inputWrap.appendChild(input)
196
258
  inputWrap.appendChild(sendBtn)
197
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
+
198
271
  // Customer info form (if enabled)
199
272
  let customerForm = null
200
273
  let sessionId = null
201
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
202
281
 
203
282
  if (captureCustomerInfo) {
204
283
  customerForm = document.createElement('div')
@@ -243,7 +322,9 @@
243
322
  skipBtn.className = 'skip-btn'
244
323
  skipBtn.textContent = 'Skip'
245
324
  formActions.appendChild(submitBtn)
246
- formActions.appendChild(skipBtn)
325
+ if (allowSkipCustomerInfo) {
326
+ formActions.appendChild(skipBtn)
327
+ }
247
328
 
248
329
  customerForm.appendChild(formTitle)
249
330
  customerForm.appendChild(formDesc)
@@ -251,7 +332,7 @@
251
332
  customerForm.appendChild(phoneField)
252
333
  customerForm.appendChild(formActions)
253
334
 
254
- async function submitCustomerInfo() {
335
+ function submitCustomerInfo() {
255
336
  const name = nameInput.value.trim()
256
337
  const phone = phoneInput.value.trim()
257
338
 
@@ -260,51 +341,54 @@
260
341
  return
261
342
  }
262
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
+
263
355
  if (!sessionId) {
264
356
  console.error('[AgentChatWidget] No session ID available')
357
+ alert('Session not established. Please wait for connection.')
265
358
  return
266
359
  }
267
360
 
268
- const baseUrl = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
269
- let apiBase = (baseUrl || '').replace(/\/$/, '')
270
- // Convert ws:// to http:// for HTTP requests
271
- if (apiBase.startsWith('ws://')) {
272
- apiBase = apiBase.replace(/^ws/, 'http')
273
- } else if (apiBase.startsWith('wss://')) {
274
- apiBase = apiBase.replace(/^wss/, 'https')
361
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
362
+ alert('WebSocket not connected. Please wait for connection.')
363
+ return
275
364
  }
276
- const endpoint = `${apiBase}/api/v1/agents/text-chat/customer-info`
277
365
 
278
366
  try {
279
- const response = await fetch(endpoint, {
280
- method: 'POST',
281
- headers: {
282
- 'Content-Type': 'application/json',
283
- },
284
- body: JSON.stringify({
285
- session_id: sessionId,
286
- phone_number: phone,
287
- customer_name: name,
288
- }),
289
- })
290
-
291
- if (response.ok) {
292
- customerInfoSubmitted = true
293
- customerForm.style.display = 'none'
294
- messages.style.display = 'flex'
295
- inputWrap.style.display = 'flex'
296
- } else {
297
- const errorText = await response.text()
298
- console.error('[AgentChatWidget] Failed to submit customer info:', errorText)
299
- alert('Failed to submit information. Please try again.')
300
- }
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'
301
378
  } catch (error) {
302
- console.error('[AgentChatWidget] Error submitting customer info:', error)
379
+ console.error('[AgentChatWidget] Error sending customer info:', error)
303
380
  alert('Error submitting information. Please try again.')
304
381
  }
305
382
  }
306
383
 
307
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
+ }
308
392
  customerInfoSubmitted = true
309
393
  customerForm.style.display = 'none'
310
394
  messages.style.display = 'flex'
@@ -312,7 +396,7 @@
312
396
  }
313
397
 
314
398
  submitBtn.onclick = submitCustomerInfo
315
- skipBtn.onclick = skipCustomerInfo
399
+ if (allowSkipCustomerInfo) skipBtn.onclick = skipCustomerInfo
316
400
 
317
401
  // Allow Enter key to submit
318
402
  nameInput.addEventListener('keydown', (e) => {
@@ -333,18 +417,59 @@
333
417
 
334
418
  panel.appendChild(header)
335
419
  panel.appendChild(statusEl)
420
+ panel.appendChild(startCtaEl)
336
421
  panel.appendChild(messages)
337
422
  panel.appendChild(inputWrap)
423
+ panel.appendChild(footerEl)
338
424
  shadow.appendChild(panel)
339
425
  shadow.appendChild(launcher)
340
426
  document.body.appendChild(container)
341
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
+
342
463
  function appendMessage(text, role) {
343
464
  const msg = document.createElement('div')
344
465
  msg.className = 'acw-msg ' + (role === 'user' ? 'acw-msg-user' : 'acw-msg-bot')
345
466
  msg.textContent = text
346
467
  messages.appendChild(msg)
347
468
  messages.scrollTop = messages.scrollHeight
469
+ if (role === 'bot' && panel.style.display !== 'flex') {
470
+ unreadCount++
471
+ updateUnreadBadge()
472
+ }
348
473
  }
349
474
 
350
475
  // No default greeting; only show messages after connection
@@ -391,25 +516,34 @@
391
516
  console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
392
517
  } catch (_) {}
393
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
+
394
525
  function updateStatus(text, isConnected) {
395
526
  statusText.textContent = text
396
527
  statusDot.classList.remove('connected', 'disconnected')
397
528
  statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
398
-
399
- // Update button text and handler based on connection status
400
- disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
401
- disconnectBtn.onclick = isConnected ? disconnect : connect
529
+ setInputEnabled(isConnected)
402
530
  }
403
531
 
404
532
  function connect() {
405
533
  if (connected || connecting) return
406
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)
407
540
  const url = resolvedWsUrl
408
541
  try {
409
542
  ws = new WebSocket(url)
410
543
  } catch (err) {
411
544
  connecting = false
412
- updateStatus('Connection error', false)
545
+ updateStatus('Disconnected', false)
546
+ appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
413
547
  console.error('[AgentChatWidget] WS error', err)
414
548
  return
415
549
  }
@@ -418,49 +552,93 @@
418
552
  connected = true
419
553
  connecting = false
420
554
  updateStatus('Connected', true)
421
-
422
- // Generate session ID when websocket connects
423
- sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
424
-
425
- // Show customer info form if enabled and not yet submitted
426
- if (captureCustomerInfo && customerForm && !customerInfoSubmitted) {
427
- customerForm.style.display = 'flex'
428
- messages.style.display = 'none'
429
- inputWrap.style.display = 'none'
430
- // Focus first input
431
- const nameInput = customerForm.querySelector('#acw-customer-name')
432
- if (nameInput) setTimeout(() => nameInput.focus(), 100)
433
- } else {
434
- messages.style.display = 'flex'
435
- inputWrap.style.display = 'flex'
436
- }
555
+ // If captureCustomerInfo, connection_established will show form or chat
556
+ if (!captureCustomerInfo) showChat()
437
557
  }
438
558
 
439
559
  ws.onmessage = (event) => {
440
560
  try {
441
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
442
617
  const text = data.text || data.content || data.message || ''
443
618
  const role = data.role || 'assistant'
444
- if (text) appendMessage(text, role === 'user' ? 'user' : 'bot')
619
+ if (text) {
620
+ appendMessage(text, role === 'user' ? 'user' : 'bot')
621
+ }
445
622
  } catch (e) {
446
623
  // Fallback to raw text
447
- if (event.data) appendMessage(String(event.data), 'bot')
624
+ if (event.data) {
625
+ appendMessage(String(event.data), 'bot')
626
+ }
448
627
  }
449
628
  }
450
629
 
451
630
  ws.onerror = (e) => {
452
631
  console.error('[AgentChatWidget] WS error', e)
453
- updateStatus('Connection error', false)
454
- 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')
455
634
  }
456
635
 
457
636
  ws.onclose = (ev) => {
458
637
  connected = false
459
638
  connecting = false
460
- const reason = ev && (ev.reason || ev.code)
461
639
  updateStatus('Disconnected', false)
462
640
  if (!ev.wasClean) {
463
- appendMessage(`Connection closed (${reason || 'unclean'})`, 'bot')
641
+ appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
464
642
  }
465
643
  }
466
644
  }
@@ -475,6 +653,11 @@
475
653
  updateStatus('Disconnected', false)
476
654
  sessionId = null
477
655
  customerInfoSubmitted = false
656
+ firstMessageReceived = false
657
+ conversationStarted = false
658
+ formShownBeforeConnect = false
659
+ pendingCustomerName = null
660
+ pendingCustomerPhone = null
478
661
  if (customerForm) {
479
662
  customerForm.style.display = 'none'
480
663
  const nameInput = customerForm.querySelector('#acw-customer-name')
@@ -482,6 +665,7 @@
482
665
  if (nameInput) nameInput.value = ''
483
666
  if (phoneInput) phoneInput.value = ''
484
667
  }
668
+ showStartCta()
485
669
  }
486
670
 
487
671
  function sendMessage() {
@@ -494,8 +678,11 @@
494
678
  }
495
679
 
496
680
  if (!ws || ws.readyState !== WebSocket.OPEN) {
497
- connect()
498
- 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
+ }
499
686
  return
500
687
  }
501
688
  appendMessage(text, 'user')
@@ -503,6 +690,19 @@
503
690
  input.value = ''
504
691
  }
505
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
+
506
706
  launcher.onclick = () => {
507
707
  const isOpen = panel.style.display === 'flex'
508
708
  if (isOpen) {
@@ -510,7 +710,9 @@
510
710
  disconnect()
511
711
  } else {
512
712
  panel.style.display = 'flex'
513
- connect()
713
+ unreadCount = 0
714
+ updateUnreadBadge()
715
+ if (!connected && !connecting && !conversationStarted) showStartCta()
514
716
  }
515
717
  }
516
718
 
@@ -539,7 +741,7 @@
539
741
  },
540
742
  open() {
541
743
  panel.style.display = 'flex'
542
- connect()
744
+ // Connection only via explicit Connect button
543
745
  },
544
746
  close() {
545
747
  panel.style.display = 'none'
@@ -555,9 +757,14 @@
555
757
  const captureCustomerInfo = userConfig.captureCustomerInfo !== undefined
556
758
  ? userConfig.captureCustomerInfo
557
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'
558
763
  const position = userConfig.position || ds.position || ds.launcherPosition || 'bottom-right'
559
764
  const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
560
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'
561
768
  return {
562
769
  agentId: userConfig.agentId || ds.agentId,
563
770
  embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
@@ -570,7 +777,11 @@
570
777
  title: userConfig.title || ds.title,
571
778
  greeting: userConfig.greeting || ds.greeting,
572
779
  captureCustomerInfo: captureCustomerInfo,
780
+ allowSkipCustomerInfo: allowSkipCustomerInfo,
573
781
  position: validPosition,
782
+ iconUrl: userConfig.iconUrl || userConfig.icon_url || ds.iconUrl || ds.icon_url,
783
+ showIcon: showIcon,
784
+ launcherText: launcherText,
574
785
  }
575
786
  }
576
787
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helllo-ai/agent-chat-widget",
3
- "version": "0.1.17",
3
+ "version": "0.1.21",
4
4
  "description": "Bot Swarm Agent Chat Widget - Embeddable chat widget for AI agents",
5
5
  "main": "agent-chat.latest.js",
6
6
  "files": [