@helllo-ai/agent-chat-widget 0.1.18 → 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,11 +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
202
275
  let firstMessageReceived = false
276
+ let conversationStarted = false
277
+ let formShownBeforeConnect = false
278
+ let pendingCustomerName = null
279
+ let pendingCustomerPhone = null
280
+ let unreadCount = 0
203
281
 
204
282
  if (captureCustomerInfo) {
205
283
  customerForm = document.createElement('div')
@@ -244,7 +322,9 @@
244
322
  skipBtn.className = 'skip-btn'
245
323
  skipBtn.textContent = 'Skip'
246
324
  formActions.appendChild(submitBtn)
247
- formActions.appendChild(skipBtn)
325
+ if (allowSkipCustomerInfo) {
326
+ formActions.appendChild(skipBtn)
327
+ }
248
328
 
249
329
  customerForm.appendChild(formTitle)
250
330
  customerForm.appendChild(formDesc)
@@ -261,20 +341,29 @@
261
341
  return
262
342
  }
263
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
+
264
355
  if (!sessionId) {
265
356
  console.error('[AgentChatWidget] No session ID available')
266
357
  alert('Session not established. Please wait for connection.')
267
358
  return
268
359
  }
269
360
 
270
- // Ensure WebSocket is connected
271
361
  if (!ws || ws.readyState !== WebSocket.OPEN) {
272
362
  alert('WebSocket not connected. Please wait for connection.')
273
363
  return
274
364
  }
275
365
 
276
366
  try {
277
- // Send customer info via WebSocket
278
367
  ws.send(JSON.stringify({
279
368
  type: 'customer_info',
280
369
  session_id: sessionId,
@@ -282,8 +371,6 @@
282
371
  customer_name: name,
283
372
  embed_key: embedKey
284
373
  }))
285
-
286
- // Mark as submitted and show chat interface
287
374
  customerInfoSubmitted = true
288
375
  customerForm.style.display = 'none'
289
376
  messages.style.display = 'flex'
@@ -295,6 +382,13 @@
295
382
  }
296
383
 
297
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
+ }
298
392
  customerInfoSubmitted = true
299
393
  customerForm.style.display = 'none'
300
394
  messages.style.display = 'flex'
@@ -302,7 +396,7 @@
302
396
  }
303
397
 
304
398
  submitBtn.onclick = submitCustomerInfo
305
- skipBtn.onclick = skipCustomerInfo
399
+ if (allowSkipCustomerInfo) skipBtn.onclick = skipCustomerInfo
306
400
 
307
401
  // Allow Enter key to submit
308
402
  nameInput.addEventListener('keydown', (e) => {
@@ -323,18 +417,59 @@
323
417
 
324
418
  panel.appendChild(header)
325
419
  panel.appendChild(statusEl)
420
+ panel.appendChild(startCtaEl)
326
421
  panel.appendChild(messages)
327
422
  panel.appendChild(inputWrap)
423
+ panel.appendChild(footerEl)
328
424
  shadow.appendChild(panel)
329
425
  shadow.appendChild(launcher)
330
426
  document.body.appendChild(container)
331
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
+
332
463
  function appendMessage(text, role) {
333
464
  const msg = document.createElement('div')
334
465
  msg.className = 'acw-msg ' + (role === 'user' ? 'acw-msg-user' : 'acw-msg-bot')
335
466
  msg.textContent = text
336
467
  messages.appendChild(msg)
337
468
  messages.scrollTop = messages.scrollHeight
469
+ if (role === 'bot' && panel.style.display !== 'flex') {
470
+ unreadCount++
471
+ updateUnreadBadge()
472
+ }
338
473
  }
339
474
 
340
475
  // No default greeting; only show messages after connection
@@ -381,26 +516,34 @@
381
516
  console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
382
517
  } catch (_) {}
383
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
+
384
525
  function updateStatus(text, isConnected) {
385
526
  statusText.textContent = text
386
527
  statusDot.classList.remove('connected', 'disconnected')
387
528
  statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
388
-
389
- // Update button text and handler based on connection status
390
- disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
391
- disconnectBtn.onclick = isConnected ? disconnect : connect
529
+ setInputEnabled(isConnected)
392
530
  }
393
531
 
394
532
  function connect() {
395
533
  if (connected || connecting) return
396
534
  connecting = true
397
- firstMessageReceived = false // Reset when connecting
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)
398
540
  const url = resolvedWsUrl
399
541
  try {
400
542
  ws = new WebSocket(url)
401
543
  } catch (err) {
402
544
  connecting = false
403
- updateStatus('Connection error', false)
545
+ updateStatus('Disconnected', false)
546
+ appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
404
547
  console.error('[AgentChatWidget] WS error', err)
405
548
  return
406
549
  }
@@ -409,11 +552,8 @@
409
552
  connected = true
410
553
  connecting = false
411
554
  updateStatus('Connected', true)
412
-
413
- // Session ID will be received from connection_established message
414
- // Don't show customer form yet - wait for first message
415
- messages.style.display = 'flex'
416
- inputWrap.style.display = 'flex'
555
+ // If captureCustomerInfo, connection_established will show form or chat
556
+ if (!captureCustomerInfo) showChat()
417
557
  }
418
558
 
419
559
  ws.onmessage = (event) => {
@@ -422,42 +562,55 @@
422
562
 
423
563
  // Handle connection_established message
424
564
  if (data.type === 'connection_established') {
425
- // Extract session_id from connection message
426
- if (data.session_id) {
427
- sessionId = data.session_id
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
428
588
  }
429
589
 
430
590
  // Display welcome message if provided
431
591
  if (data.welcome_message) {
432
592
  appendMessage(data.welcome_message, 'bot')
433
-
434
- // Show customer info form after welcome message
435
593
  if (!firstMessageReceived && captureCustomerInfo && customerForm && !customerInfoSubmitted) {
436
594
  firstMessageReceived = true
437
- // Small delay to let the message render first
438
595
  setTimeout(() => {
439
- customerForm.style.display = 'flex'
440
- messages.style.display = 'none'
441
- inputWrap.style.display = 'none'
442
- // Focus first input
596
+ showFormOnly()
443
597
  const nameInput = customerForm.querySelector('#acw-customer-name')
444
598
  if (nameInput) setTimeout(() => nameInput.focus(), 100)
445
599
  }, 300)
446
600
  }
447
601
  } else {
448
- // No welcome message, but still show form if needed
449
602
  if (!firstMessageReceived && captureCustomerInfo && customerForm && !customerInfoSubmitted) {
450
603
  firstMessageReceived = true
451
604
  setTimeout(() => {
452
- customerForm.style.display = 'flex'
453
- messages.style.display = 'none'
454
- inputWrap.style.display = 'none'
605
+ showFormOnly()
455
606
  const nameInput = customerForm.querySelector('#acw-customer-name')
456
607
  if (nameInput) setTimeout(() => nameInput.focus(), 100)
457
608
  }, 100)
458
609
  }
459
610
  }
460
- return // Don't process as regular message
611
+ if (captureCustomerInfo && !customerInfoSubmitted && customerForm) return
612
+ showChat()
613
+ return
461
614
  }
462
615
 
463
616
  // Handle regular chat messages
@@ -476,17 +629,16 @@
476
629
 
477
630
  ws.onerror = (e) => {
478
631
  console.error('[AgentChatWidget] WS error', e)
479
- updateStatus('Connection error', false)
480
- 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')
481
634
  }
482
635
 
483
636
  ws.onclose = (ev) => {
484
637
  connected = false
485
638
  connecting = false
486
- const reason = ev && (ev.reason || ev.code)
487
639
  updateStatus('Disconnected', false)
488
640
  if (!ev.wasClean) {
489
- appendMessage(`Connection closed (${reason || 'unclean'})`, 'bot')
641
+ appendMessage('Failed to connect. Please try again or check your connection.', 'bot')
490
642
  }
491
643
  }
492
644
  }
@@ -502,6 +654,10 @@
502
654
  sessionId = null
503
655
  customerInfoSubmitted = false
504
656
  firstMessageReceived = false
657
+ conversationStarted = false
658
+ formShownBeforeConnect = false
659
+ pendingCustomerName = null
660
+ pendingCustomerPhone = null
505
661
  if (customerForm) {
506
662
  customerForm.style.display = 'none'
507
663
  const nameInput = customerForm.querySelector('#acw-customer-name')
@@ -509,6 +665,7 @@
509
665
  if (nameInput) nameInput.value = ''
510
666
  if (phoneInput) phoneInput.value = ''
511
667
  }
668
+ showStartCta()
512
669
  }
513
670
 
514
671
  function sendMessage() {
@@ -521,8 +678,11 @@
521
678
  }
522
679
 
523
680
  if (!ws || ws.readyState !== WebSocket.OPEN) {
524
- connect()
525
- 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
+ }
526
686
  return
527
687
  }
528
688
  appendMessage(text, 'user')
@@ -530,6 +690,19 @@
530
690
  input.value = ''
531
691
  }
532
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
+
533
706
  launcher.onclick = () => {
534
707
  const isOpen = panel.style.display === 'flex'
535
708
  if (isOpen) {
@@ -537,7 +710,9 @@
537
710
  disconnect()
538
711
  } else {
539
712
  panel.style.display = 'flex'
540
- connect()
713
+ unreadCount = 0
714
+ updateUnreadBadge()
715
+ if (!connected && !connecting && !conversationStarted) showStartCta()
541
716
  }
542
717
  }
543
718
 
@@ -566,7 +741,7 @@
566
741
  },
567
742
  open() {
568
743
  panel.style.display = 'flex'
569
- connect()
744
+ // Connection only via explicit Connect button
570
745
  },
571
746
  close() {
572
747
  panel.style.display = 'none'
@@ -582,9 +757,14 @@
582
757
  const captureCustomerInfo = userConfig.captureCustomerInfo !== undefined
583
758
  ? userConfig.captureCustomerInfo
584
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'
585
763
  const position = userConfig.position || ds.position || ds.launcherPosition || 'bottom-right'
586
764
  const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
587
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'
588
768
  return {
589
769
  agentId: userConfig.agentId || ds.agentId,
590
770
  embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
@@ -597,7 +777,11 @@
597
777
  title: userConfig.title || ds.title,
598
778
  greeting: userConfig.greeting || ds.greeting,
599
779
  captureCustomerInfo: captureCustomerInfo,
780
+ allowSkipCustomerInfo: allowSkipCustomerInfo,
600
781
  position: validPosition,
782
+ iconUrl: userConfig.iconUrl || userConfig.icon_url || ds.iconUrl || ds.icon_url,
783
+ showIcon: showIcon,
784
+ launcherText: launcherText,
601
785
  }
602
786
  }
603
787
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helllo-ai/agent-chat-widget",
3
- "version": "0.1.18",
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": [