@helllo-ai/agent-chat-widget 0.1.11 → 0.1.13

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.
@@ -19,10 +19,20 @@
19
19
  const safeBg = background || '#ffffff'
20
20
  return `
21
21
  :host, .acw * { box-sizing: border-box; }
22
- .acw-container { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000; font-family: Inter, system-ui, -apple-system, sans-serif; }
23
- .acw-launcher { background: ${safePrimary}; color: #fff; border: none; border-radius: 999px; padding: 10px 14px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.18); font-size: 14px; }
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%); }
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; }
25
- .acw-panel { position: absolute; right: 0; bottom: 56px; 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; }
27
+ .acw-launcher.vertical { flex-direction: column; padding: 14px 10px; min-width: 48px; height: auto; }
28
+ .acw-launcher.vertical span { writing-mode: sideways-lr; text-orientation: mixed; letter-spacing: 0.5px; }
29
+ .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
+ .acw-panel.bottom-right { right: 0; bottom: 56px; }
31
+ .acw-panel.middle-right { right: 56px; top: 50%; transform: translateY(-50%); }
32
+ .acw-panel.top-right { right: 0; top: 56px; }
33
+ .acw-panel.bottom-left { left: 0; bottom: 56px; }
34
+ .acw-panel.middle-left { left: 56px; top: 50%; transform: translateY(-50%); }
35
+ .acw-panel.top-left { left: 0; top: 56px; }
26
36
  .acw-header { background: ${safePrimary}; color: #fff; padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; }
27
37
  .acw-title { font-weight: 600; font-size: 14px; }
28
38
  .acw-close { background: transparent; border: none; color: #fff; cursor: pointer; font-size: 16px; }
@@ -39,6 +49,18 @@
39
49
  .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
40
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; }
41
51
  .acw-disconnect:hover { background: #eef2f7; }
52
+ .acw-customer-form { position: absolute; inset: 0; background: ${safeBg}; z-index: 10; display: flex; flex-direction: column; padding: 20px; }
53
+ .acw-customer-form h3 { margin: 0 0 12px; font-size: 16px; font-weight: 600; color: #0f172a; }
54
+ .acw-customer-form p { margin: 0 0 16px; font-size: 13px; color: #64748b; }
55
+ .acw-customer-form .field { margin-bottom: 16px; }
56
+ .acw-customer-form label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: #475569; }
57
+ .acw-customer-form input { width: 100%; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.12); border-radius: 8px; font-size: 14px; }
58
+ .acw-customer-form .actions { display: flex; gap: 8px; margin-top: auto; }
59
+ .acw-customer-form button { flex: 1; padding: 10px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
60
+ .acw-customer-form .submit-btn { background: ${safePrimary}; color: #fff; }
61
+ .acw-customer-form .submit-btn:hover { opacity: 0.95; }
62
+ .acw-customer-form .skip-btn { background: #f8fafc; color: #64748b; border: 1px solid rgba(0,0,0,0.08); }
63
+ .acw-customer-form .skip-btn:hover { background: #eef2f7; }
42
64
  @media (max-width: 480px) {
43
65
  .acw-panel { width: calc(100vw - 24px); height: 70vh; }
44
66
  }
@@ -57,14 +79,22 @@
57
79
  wsUrl,
58
80
  title = 'Chat with us',
59
81
  greeting = null,
82
+ captureCustomerInfo = false,
83
+ position = 'bottom-right',
60
84
  } = config
61
85
 
86
+ // Valid positions
87
+ const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
88
+ const finalPosition = validPositions.includes(position) ? position : 'bottom-right'
89
+ const isVertical = finalPosition === 'middle-right' || finalPosition === 'middle-left'
90
+ const isLeft = finalPosition.includes('left')
91
+
62
92
  if (!agentId) {
63
- console.warn('[AgentChatWidget] Missing data-agent-id')
93
+ console.warn('[AgentChatWidget] Missing agentId. Provide it via init({ agentId: "..." }) or data-agent-id attribute')
64
94
  return
65
95
  }
66
96
  if (!embedKey) {
67
- console.warn('[AgentChatWidget] Missing data-embed-key')
97
+ console.warn('[AgentChatWidget] Missing embedKey. Provide it via init({ embedKey: "..." }) or data-embed-key attribute')
68
98
  return
69
99
  }
70
100
 
@@ -75,17 +105,46 @@
75
105
  }
76
106
 
77
107
  const container = document.createElement('div')
78
- container.className = 'acw-container'
108
+ container.className = `acw-container ${finalPosition}`
109
+
110
+ // Apply position styles directly to container (since it's in regular DOM, not shadow)
111
+ container.style.position = 'fixed'
112
+ container.style.zIndex = '2147483000'
113
+ container.style.fontFamily = 'Inter, system-ui, -apple-system, sans-serif'
114
+
115
+ // Set container position based on finalPosition
116
+ if (finalPosition === 'bottom-right') {
117
+ container.style.right = '16px'
118
+ container.style.bottom = '16px'
119
+ } else if (finalPosition === 'middle-right') {
120
+ container.style.right = '16px'
121
+ container.style.top = '50%'
122
+ container.style.transform = 'translateY(-50%)'
123
+ } else if (finalPosition === 'top-right') {
124
+ container.style.right = '16px'
125
+ container.style.top = '16px'
126
+ } else if (finalPosition === 'bottom-left') {
127
+ container.style.left = '16px'
128
+ container.style.bottom = '16px'
129
+ } else if (finalPosition === 'middle-left') {
130
+ container.style.left = '16px'
131
+ container.style.top = '50%'
132
+ container.style.transform = 'translateY(-50%)'
133
+ } else if (finalPosition === 'top-left') {
134
+ container.style.left = '16px'
135
+ container.style.top = '16px'
136
+ }
137
+
79
138
  const shadow = container.attachShadow({ mode: 'open' })
80
139
  const style = document.createElement('style')
81
140
  style.textContent = createStyles(primaryColor, backgroundColor)
82
141
  shadow.appendChild(style)
83
142
 
84
143
  const panel = document.createElement('div')
85
- panel.className = 'acw-panel'
144
+ panel.className = `acw-panel ${finalPosition}`
86
145
 
87
146
  const launcher = document.createElement('button')
88
- launcher.className = 'acw-launcher'
147
+ launcher.className = isVertical ? 'acw-launcher vertical' : 'acw-launcher'
89
148
  launcher.innerHTML = '<span>Chat</span>'
90
149
 
91
150
  const header = document.createElement('div')
@@ -136,6 +195,142 @@
136
195
  inputWrap.appendChild(input)
137
196
  inputWrap.appendChild(sendBtn)
138
197
 
198
+ // Customer info form (if enabled)
199
+ let customerForm = null
200
+ let sessionId = null
201
+ let customerInfoSubmitted = false
202
+
203
+ if (captureCustomerInfo) {
204
+ customerForm = document.createElement('div')
205
+ customerForm.className = 'acw-customer-form'
206
+ customerForm.style.display = 'none'
207
+
208
+ const formTitle = document.createElement('h3')
209
+ formTitle.textContent = 'Welcome! Please share your details'
210
+ const formDesc = document.createElement('p')
211
+ formDesc.textContent = 'We need a few details to get started'
212
+
213
+ const nameField = document.createElement('div')
214
+ nameField.className = 'field'
215
+ const nameLabel = document.createElement('label')
216
+ nameLabel.textContent = 'Your Name'
217
+ nameLabel.setAttribute('for', 'acw-customer-name')
218
+ const nameInput = document.createElement('input')
219
+ nameInput.id = 'acw-customer-name'
220
+ nameInput.type = 'text'
221
+ nameInput.placeholder = 'Enter your name'
222
+ nameField.appendChild(nameLabel)
223
+ nameField.appendChild(nameInput)
224
+
225
+ const phoneField = document.createElement('div')
226
+ phoneField.className = 'field'
227
+ const phoneLabel = document.createElement('label')
228
+ phoneLabel.textContent = 'Phone Number'
229
+ phoneLabel.setAttribute('for', 'acw-customer-phone')
230
+ const phoneInput = document.createElement('input')
231
+ phoneInput.id = 'acw-customer-phone'
232
+ phoneInput.type = 'tel'
233
+ phoneInput.placeholder = 'Enter your phone number'
234
+ phoneField.appendChild(phoneLabel)
235
+ phoneField.appendChild(phoneInput)
236
+
237
+ const formActions = document.createElement('div')
238
+ formActions.className = 'actions'
239
+ const submitBtn = document.createElement('button')
240
+ submitBtn.className = 'submit-btn'
241
+ submitBtn.textContent = 'Start Chat'
242
+ const skipBtn = document.createElement('button')
243
+ skipBtn.className = 'skip-btn'
244
+ skipBtn.textContent = 'Skip'
245
+ formActions.appendChild(submitBtn)
246
+ formActions.appendChild(skipBtn)
247
+
248
+ customerForm.appendChild(formTitle)
249
+ customerForm.appendChild(formDesc)
250
+ customerForm.appendChild(nameField)
251
+ customerForm.appendChild(phoneField)
252
+ customerForm.appendChild(formActions)
253
+
254
+ async function submitCustomerInfo() {
255
+ const name = nameInput.value.trim()
256
+ const phone = phoneInput.value.trim()
257
+
258
+ if (!name || !phone) {
259
+ alert('Please fill in both name and phone number')
260
+ return
261
+ }
262
+
263
+ if (!sessionId) {
264
+ console.error('[AgentChatWidget] No session ID available')
265
+ return
266
+ }
267
+
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')
275
+ }
276
+ const endpoint = `${apiBase}/api/v1/agents/text-chat/customer-info`
277
+
278
+ 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
+ }
301
+ } catch (error) {
302
+ console.error('[AgentChatWidget] Error submitting customer info:', error)
303
+ alert('Error submitting information. Please try again.')
304
+ }
305
+ }
306
+
307
+ function skipCustomerInfo() {
308
+ customerInfoSubmitted = true
309
+ customerForm.style.display = 'none'
310
+ messages.style.display = 'flex'
311
+ inputWrap.style.display = 'flex'
312
+ }
313
+
314
+ submitBtn.onclick = submitCustomerInfo
315
+ skipBtn.onclick = skipCustomerInfo
316
+
317
+ // Allow Enter key to submit
318
+ nameInput.addEventListener('keydown', (e) => {
319
+ if (e.key === 'Enter') {
320
+ e.preventDefault()
321
+ phoneInput.focus()
322
+ }
323
+ })
324
+ phoneInput.addEventListener('keydown', (e) => {
325
+ if (e.key === 'Enter') {
326
+ e.preventDefault()
327
+ submitCustomerInfo()
328
+ }
329
+ })
330
+
331
+ panel.appendChild(customerForm)
332
+ }
333
+
139
334
  panel.appendChild(header)
140
335
  panel.appendChild(statusEl)
141
336
  panel.appendChild(messages)
@@ -159,13 +354,32 @@
159
354
  let connecting = false
160
355
 
161
356
  const resolvedWsUrl = (() => {
357
+ // Explicit wsUrl takes highest priority
162
358
  if (wsUrl) return wsUrl
163
- // Use production URLs by default for prod widget
164
- const defaultVoiceServiceUrl = voiceServiceUrl || 'wss://talk2ai-prod.helllo.ai'
165
- const defaultApiBaseUrl = apiBaseUrl || 'https://api-prod.helllo.ai'
166
- const baseCandidate = defaultVoiceServiceUrl || defaultApiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
167
- const voiceBase = (baseCandidate || '').replace(/\/$/, '')
168
- const wsBase = voiceBase.replace(/^http/, 'ws')
359
+
360
+ // Determine environment-specific WebSocket URL based on VERSION
361
+ let wsBase
362
+ if (VERSION.includes('-staging')) {
363
+ wsBase = 'wss://talk2ai-staging.helllo.ai'
364
+ } else if (VERSION.includes('-prod')) {
365
+ wsBase = 'wss://talk2ai-prod.helllo.ai'
366
+ } else {
367
+ // For dev/latest, use existing fallback logic
368
+ const baseCandidate = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
369
+ const voiceBase = (baseCandidate || '').replace(/\/$/, '')
370
+ // Handle both http/https and ws/wss URLs
371
+ if (voiceBase.startsWith('http://')) {
372
+ wsBase = voiceBase.replace(/^http/, 'ws')
373
+ } else if (voiceBase.startsWith('https://')) {
374
+ wsBase = voiceBase.replace(/^https/, 'wss')
375
+ } else if (voiceBase.startsWith('ws://') || voiceBase.startsWith('wss://')) {
376
+ wsBase = voiceBase // Already a WebSocket URL
377
+ } else {
378
+ // Default to ws if no protocol
379
+ wsBase = 'ws://' + voiceBase
380
+ }
381
+ }
382
+
169
383
  const params = new URLSearchParams()
170
384
  params.set('embed_key', embedKey)
171
385
  params.set('host', window.location.hostname)
@@ -174,7 +388,7 @@
174
388
 
175
389
  // Log resolved URL once for debugging
176
390
  try {
177
- console.info('[AgentChatWidget-Prod] Resolved WS URL:', resolvedWsUrl)
391
+ console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
178
392
  } catch (_) {}
179
393
 
180
394
  function updateStatus(text, isConnected) {
@@ -196,7 +410,7 @@
196
410
  } catch (err) {
197
411
  connecting = false
198
412
  updateStatus('Connection error', false)
199
- console.error('[AgentChatWidget-Prod] WS error', err)
413
+ console.error('[AgentChatWidget] WS error', err)
200
414
  return
201
415
  }
202
416
 
@@ -204,6 +418,22 @@
204
418
  connected = true
205
419
  connecting = false
206
420
  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
+ }
207
437
  }
208
438
 
209
439
  ws.onmessage = (event) => {
@@ -219,7 +449,7 @@
219
449
  }
220
450
 
221
451
  ws.onerror = (e) => {
222
- console.error('[AgentChatWidget-Prod] WS error', e)
452
+ console.error('[AgentChatWidget] WS error', e)
223
453
  updateStatus('Connection error', false)
224
454
  appendMessage('Connection error. Check WS service and URL.', 'bot')
225
455
  }
@@ -243,11 +473,26 @@
243
473
  connected = false
244
474
  connecting = false
245
475
  updateStatus('Disconnected', false)
476
+ sessionId = null
477
+ customerInfoSubmitted = false
478
+ if (customerForm) {
479
+ customerForm.style.display = 'none'
480
+ const nameInput = customerForm.querySelector('#acw-customer-name')
481
+ const phoneInput = customerForm.querySelector('#acw-customer-phone')
482
+ if (nameInput) nameInput.value = ''
483
+ if (phoneInput) phoneInput.value = ''
484
+ }
246
485
  }
247
486
 
248
487
  function sendMessage() {
249
488
  const text = input.value.trim()
250
489
  if (!text) return
490
+
491
+ // Don't allow sending messages if customer info form is required and not submitted
492
+ if (captureCustomerInfo && !customerInfoSubmitted) {
493
+ return
494
+ }
495
+
251
496
  if (!ws || ws.readyState !== WebSocket.OPEN) {
252
497
  connect()
253
498
  setTimeout(sendMessage, 200)
@@ -279,9 +524,6 @@
279
524
  disconnect()
280
525
  }
281
526
 
282
- disconnectBtn.onclick = () => {
283
- disconnect()
284
- }
285
527
  sendBtn.onclick = sendMessage
286
528
  input.addEventListener('keydown', (e) => {
287
529
  if (e.key === 'Enter') {
@@ -310,6 +552,12 @@
310
552
  const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
311
553
  const ds = (scriptEl && scriptEl.dataset) || {}
312
554
  const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
555
+ const captureCustomerInfo = userConfig.captureCustomerInfo !== undefined
556
+ ? userConfig.captureCustomerInfo
557
+ : ds.captureCustomerInfo === 'true' || ds.capture_customer_info === 'true'
558
+ const position = userConfig.position || ds.position || ds.launcherPosition || 'bottom-right'
559
+ const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
560
+ const validPosition = validPositions.includes(position) ? position : 'bottom-right'
313
561
  return {
314
562
  agentId: userConfig.agentId || ds.agentId,
315
563
  embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
@@ -321,6 +569,8 @@
321
569
  wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
322
570
  title: userConfig.title || ds.title,
323
571
  greeting: userConfig.greeting || ds.greeting,
572
+ captureCustomerInfo: captureCustomerInfo,
573
+ position: validPosition,
324
574
  }
325
575
  }
326
576
 
@@ -348,3 +598,4 @@
348
598
  init()
349
599
  }
350
600
  })()
601
+
@@ -19,10 +19,20 @@
19
19
  const safeBg = background || '#ffffff'
20
20
  return `
21
21
  :host, .acw * { box-sizing: border-box; }
22
- .acw-container { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000; font-family: Inter, system-ui, -apple-system, sans-serif; }
23
- .acw-launcher { background: ${safePrimary}; color: #fff; border: none; border-radius: 999px; padding: 10px 14px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.18); font-size: 14px; }
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%); }
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; }
25
- .acw-panel { position: absolute; right: 0; bottom: 56px; 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; }
27
+ .acw-launcher.vertical { flex-direction: column; padding: 14px 10px; min-width: 48px; height: auto; }
28
+ .acw-launcher.vertical span { writing-mode: sideways-lr; text-orientation: mixed; letter-spacing: 0.5px; }
29
+ .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
+ .acw-panel.bottom-right { right: 0; bottom: 56px; }
31
+ .acw-panel.middle-right { right: 56px; top: 50%; transform: translateY(-50%); }
32
+ .acw-panel.top-right { right: 0; top: 56px; }
33
+ .acw-panel.bottom-left { left: 0; bottom: 56px; }
34
+ .acw-panel.middle-left { left: 56px; top: 50%; transform: translateY(-50%); }
35
+ .acw-panel.top-left { left: 0; top: 56px; }
26
36
  .acw-header { background: ${safePrimary}; color: #fff; padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; }
27
37
  .acw-title { font-weight: 600; font-size: 14px; }
28
38
  .acw-close { background: transparent; border: none; color: #fff; cursor: pointer; font-size: 16px; }
@@ -39,6 +49,18 @@
39
49
  .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
40
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; }
41
51
  .acw-disconnect:hover { background: #eef2f7; }
52
+ .acw-customer-form { position: absolute; inset: 0; background: ${safeBg}; z-index: 10; display: flex; flex-direction: column; padding: 20px; }
53
+ .acw-customer-form h3 { margin: 0 0 12px; font-size: 16px; font-weight: 600; color: #0f172a; }
54
+ .acw-customer-form p { margin: 0 0 16px; font-size: 13px; color: #64748b; }
55
+ .acw-customer-form .field { margin-bottom: 16px; }
56
+ .acw-customer-form label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: #475569; }
57
+ .acw-customer-form input { width: 100%; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.12); border-radius: 8px; font-size: 14px; }
58
+ .acw-customer-form .actions { display: flex; gap: 8px; margin-top: auto; }
59
+ .acw-customer-form button { flex: 1; padding: 10px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
60
+ .acw-customer-form .submit-btn { background: ${safePrimary}; color: #fff; }
61
+ .acw-customer-form .submit-btn:hover { opacity: 0.95; }
62
+ .acw-customer-form .skip-btn { background: #f8fafc; color: #64748b; border: 1px solid rgba(0,0,0,0.08); }
63
+ .acw-customer-form .skip-btn:hover { background: #eef2f7; }
42
64
  @media (max-width: 480px) {
43
65
  .acw-panel { width: calc(100vw - 24px); height: 70vh; }
44
66
  }
@@ -55,16 +77,24 @@
55
77
  apiBaseUrl,
56
78
  voiceServiceUrl,
57
79
  wsUrl,
58
- title = 'Chat with us (Staging)',
80
+ title = 'Chat with us',
59
81
  greeting = null,
82
+ captureCustomerInfo = false,
83
+ position = 'bottom-right',
60
84
  } = config
61
85
 
86
+ // Valid positions
87
+ const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
88
+ const finalPosition = validPositions.includes(position) ? position : 'bottom-right'
89
+ const isVertical = finalPosition === 'middle-right' || finalPosition === 'middle-left'
90
+ const isLeft = finalPosition.includes('left')
91
+
62
92
  if (!agentId) {
63
- console.warn('[AgentChatWidget] Missing data-agent-id')
93
+ console.warn('[AgentChatWidget] Missing agentId. Provide it via init({ agentId: "..." }) or data-agent-id attribute')
64
94
  return
65
95
  }
66
96
  if (!embedKey) {
67
- console.warn('[AgentChatWidget] Missing data-embed-key')
97
+ console.warn('[AgentChatWidget] Missing embedKey. Provide it via init({ embedKey: "..." }) or data-embed-key attribute')
68
98
  return
69
99
  }
70
100
 
@@ -75,18 +105,47 @@
75
105
  }
76
106
 
77
107
  const container = document.createElement('div')
78
- container.className = 'acw-container'
108
+ container.className = `acw-container ${finalPosition}`
109
+
110
+ // Apply position styles directly to container (since it's in regular DOM, not shadow)
111
+ container.style.position = 'fixed'
112
+ container.style.zIndex = '2147483000'
113
+ container.style.fontFamily = 'Inter, system-ui, -apple-system, sans-serif'
114
+
115
+ // Set container position based on finalPosition
116
+ if (finalPosition === 'bottom-right') {
117
+ container.style.right = '16px'
118
+ container.style.bottom = '16px'
119
+ } else if (finalPosition === 'middle-right') {
120
+ container.style.right = '16px'
121
+ container.style.top = '50%'
122
+ container.style.transform = 'translateY(-50%)'
123
+ } else if (finalPosition === 'top-right') {
124
+ container.style.right = '16px'
125
+ container.style.top = '16px'
126
+ } else if (finalPosition === 'bottom-left') {
127
+ container.style.left = '16px'
128
+ container.style.bottom = '16px'
129
+ } else if (finalPosition === 'middle-left') {
130
+ container.style.left = '16px'
131
+ container.style.top = '50%'
132
+ container.style.transform = 'translateY(-50%)'
133
+ } else if (finalPosition === 'top-left') {
134
+ container.style.left = '16px'
135
+ container.style.top = '16px'
136
+ }
137
+
79
138
  const shadow = container.attachShadow({ mode: 'open' })
80
139
  const style = document.createElement('style')
81
140
  style.textContent = createStyles(primaryColor, backgroundColor)
82
141
  shadow.appendChild(style)
83
142
 
84
143
  const panel = document.createElement('div')
85
- panel.className = 'acw-panel'
144
+ panel.className = `acw-panel ${finalPosition}`
86
145
 
87
146
  const launcher = document.createElement('button')
88
- launcher.className = 'acw-launcher'
89
- launcher.innerHTML = '<span>Chat (Staging)</span>'
147
+ launcher.className = isVertical ? 'acw-launcher vertical' : 'acw-launcher'
148
+ launcher.innerHTML = '<span>Chat</span>'
90
149
 
91
150
  const header = document.createElement('div')
92
151
  header.className = 'acw-header'
@@ -115,7 +174,7 @@
115
174
  const statusDot = document.createElement('div')
116
175
  statusDot.className = 'acw-dot disconnected'
117
176
  const statusText = document.createElement('span')
118
- statusText.textContent = 'Disconnected (Staging)'
177
+ statusText.textContent = 'Disconnected'
119
178
  const disconnectBtn = document.createElement('button')
120
179
  disconnectBtn.className = 'acw-disconnect'
121
180
  disconnectBtn.textContent = 'Connect'
@@ -136,6 +195,142 @@
136
195
  inputWrap.appendChild(input)
137
196
  inputWrap.appendChild(sendBtn)
138
197
 
198
+ // Customer info form (if enabled)
199
+ let customerForm = null
200
+ let sessionId = null
201
+ let customerInfoSubmitted = false
202
+
203
+ if (captureCustomerInfo) {
204
+ customerForm = document.createElement('div')
205
+ customerForm.className = 'acw-customer-form'
206
+ customerForm.style.display = 'none'
207
+
208
+ const formTitle = document.createElement('h3')
209
+ formTitle.textContent = 'Welcome! Please share your details'
210
+ const formDesc = document.createElement('p')
211
+ formDesc.textContent = 'We need a few details to get started'
212
+
213
+ const nameField = document.createElement('div')
214
+ nameField.className = 'field'
215
+ const nameLabel = document.createElement('label')
216
+ nameLabel.textContent = 'Your Name'
217
+ nameLabel.setAttribute('for', 'acw-customer-name')
218
+ const nameInput = document.createElement('input')
219
+ nameInput.id = 'acw-customer-name'
220
+ nameInput.type = 'text'
221
+ nameInput.placeholder = 'Enter your name'
222
+ nameField.appendChild(nameLabel)
223
+ nameField.appendChild(nameInput)
224
+
225
+ const phoneField = document.createElement('div')
226
+ phoneField.className = 'field'
227
+ const phoneLabel = document.createElement('label')
228
+ phoneLabel.textContent = 'Phone Number'
229
+ phoneLabel.setAttribute('for', 'acw-customer-phone')
230
+ const phoneInput = document.createElement('input')
231
+ phoneInput.id = 'acw-customer-phone'
232
+ phoneInput.type = 'tel'
233
+ phoneInput.placeholder = 'Enter your phone number'
234
+ phoneField.appendChild(phoneLabel)
235
+ phoneField.appendChild(phoneInput)
236
+
237
+ const formActions = document.createElement('div')
238
+ formActions.className = 'actions'
239
+ const submitBtn = document.createElement('button')
240
+ submitBtn.className = 'submit-btn'
241
+ submitBtn.textContent = 'Start Chat'
242
+ const skipBtn = document.createElement('button')
243
+ skipBtn.className = 'skip-btn'
244
+ skipBtn.textContent = 'Skip'
245
+ formActions.appendChild(submitBtn)
246
+ formActions.appendChild(skipBtn)
247
+
248
+ customerForm.appendChild(formTitle)
249
+ customerForm.appendChild(formDesc)
250
+ customerForm.appendChild(nameField)
251
+ customerForm.appendChild(phoneField)
252
+ customerForm.appendChild(formActions)
253
+
254
+ async function submitCustomerInfo() {
255
+ const name = nameInput.value.trim()
256
+ const phone = phoneInput.value.trim()
257
+
258
+ if (!name || !phone) {
259
+ alert('Please fill in both name and phone number')
260
+ return
261
+ }
262
+
263
+ if (!sessionId) {
264
+ console.error('[AgentChatWidget] No session ID available')
265
+ return
266
+ }
267
+
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')
275
+ }
276
+ const endpoint = `${apiBase}/api/v1/agents/text-chat/customer-info`
277
+
278
+ 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
+ }
301
+ } catch (error) {
302
+ console.error('[AgentChatWidget] Error submitting customer info:', error)
303
+ alert('Error submitting information. Please try again.')
304
+ }
305
+ }
306
+
307
+ function skipCustomerInfo() {
308
+ customerInfoSubmitted = true
309
+ customerForm.style.display = 'none'
310
+ messages.style.display = 'flex'
311
+ inputWrap.style.display = 'flex'
312
+ }
313
+
314
+ submitBtn.onclick = submitCustomerInfo
315
+ skipBtn.onclick = skipCustomerInfo
316
+
317
+ // Allow Enter key to submit
318
+ nameInput.addEventListener('keydown', (e) => {
319
+ if (e.key === 'Enter') {
320
+ e.preventDefault()
321
+ phoneInput.focus()
322
+ }
323
+ })
324
+ phoneInput.addEventListener('keydown', (e) => {
325
+ if (e.key === 'Enter') {
326
+ e.preventDefault()
327
+ submitCustomerInfo()
328
+ }
329
+ })
330
+
331
+ panel.appendChild(customerForm)
332
+ }
333
+
139
334
  panel.appendChild(header)
140
335
  panel.appendChild(statusEl)
141
336
  panel.appendChild(messages)
@@ -159,13 +354,32 @@
159
354
  let connecting = false
160
355
 
161
356
  const resolvedWsUrl = (() => {
357
+ // Explicit wsUrl takes highest priority
162
358
  if (wsUrl) return wsUrl
163
- // Use staging URLs by default for staging widget
164
- const defaultVoiceServiceUrl = voiceServiceUrl || 'wss://talk2ai-staging.helllo.ai'
165
- const defaultApiBaseUrl = apiBaseUrl || 'https://api-staging.helllo.ai'
166
- const baseCandidate = defaultVoiceServiceUrl || defaultApiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
167
- const voiceBase = (baseCandidate || '').replace(/\/$/, '')
168
- const wsBase = voiceBase.replace(/^http/, 'ws')
359
+
360
+ // Determine environment-specific WebSocket URL based on VERSION
361
+ let wsBase
362
+ if (VERSION.includes('-staging')) {
363
+ wsBase = 'wss://talk2ai-staging.helllo.ai'
364
+ } else if (VERSION.includes('-prod')) {
365
+ wsBase = 'wss://talk2ai-prod.helllo.ai'
366
+ } else {
367
+ // For dev/latest, use existing fallback logic
368
+ const baseCandidate = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
369
+ const voiceBase = (baseCandidate || '').replace(/\/$/, '')
370
+ // Handle both http/https and ws/wss URLs
371
+ if (voiceBase.startsWith('http://')) {
372
+ wsBase = voiceBase.replace(/^http/, 'ws')
373
+ } else if (voiceBase.startsWith('https://')) {
374
+ wsBase = voiceBase.replace(/^https/, 'wss')
375
+ } else if (voiceBase.startsWith('ws://') || voiceBase.startsWith('wss://')) {
376
+ wsBase = voiceBase // Already a WebSocket URL
377
+ } else {
378
+ // Default to ws if no protocol
379
+ wsBase = 'ws://' + voiceBase
380
+ }
381
+ }
382
+
169
383
  const params = new URLSearchParams()
170
384
  params.set('embed_key', embedKey)
171
385
  params.set('host', window.location.hostname)
@@ -174,11 +388,11 @@
174
388
 
175
389
  // Log resolved URL once for debugging
176
390
  try {
177
- console.info('[AgentChatWidget-Staging] Resolved WS URL:', resolvedWsUrl)
391
+ console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
178
392
  } catch (_) {}
179
393
 
180
394
  function updateStatus(text, isConnected) {
181
- statusText.textContent = text + ' (Staging)'
395
+ statusText.textContent = text
182
396
  statusDot.classList.remove('connected', 'disconnected')
183
397
  statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
184
398
 
@@ -196,7 +410,7 @@
196
410
  } catch (err) {
197
411
  connecting = false
198
412
  updateStatus('Connection error', false)
199
- console.error('[AgentChatWidget-Staging] WS error', err)
413
+ console.error('[AgentChatWidget] WS error', err)
200
414
  return
201
415
  }
202
416
 
@@ -204,6 +418,22 @@
204
418
  connected = true
205
419
  connecting = false
206
420
  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
+ }
207
437
  }
208
438
 
209
439
  ws.onmessage = (event) => {
@@ -219,7 +449,7 @@
219
449
  }
220
450
 
221
451
  ws.onerror = (e) => {
222
- console.error('[AgentChatWidget-Staging] WS error', e)
452
+ console.error('[AgentChatWidget] WS error', e)
223
453
  updateStatus('Connection error', false)
224
454
  appendMessage('Connection error. Check WS service and URL.', 'bot')
225
455
  }
@@ -243,11 +473,26 @@
243
473
  connected = false
244
474
  connecting = false
245
475
  updateStatus('Disconnected', false)
476
+ sessionId = null
477
+ customerInfoSubmitted = false
478
+ if (customerForm) {
479
+ customerForm.style.display = 'none'
480
+ const nameInput = customerForm.querySelector('#acw-customer-name')
481
+ const phoneInput = customerForm.querySelector('#acw-customer-phone')
482
+ if (nameInput) nameInput.value = ''
483
+ if (phoneInput) phoneInput.value = ''
484
+ }
246
485
  }
247
486
 
248
487
  function sendMessage() {
249
488
  const text = input.value.trim()
250
489
  if (!text) return
490
+
491
+ // Don't allow sending messages if customer info form is required and not submitted
492
+ if (captureCustomerInfo && !customerInfoSubmitted) {
493
+ return
494
+ }
495
+
251
496
  if (!ws || ws.readyState !== WebSocket.OPEN) {
252
497
  connect()
253
498
  setTimeout(sendMessage, 200)
@@ -279,9 +524,6 @@
279
524
  disconnect()
280
525
  }
281
526
 
282
- disconnectBtn.onclick = () => {
283
- disconnect()
284
- }
285
527
  sendBtn.onclick = sendMessage
286
528
  input.addEventListener('keydown', (e) => {
287
529
  if (e.key === 'Enter') {
@@ -310,6 +552,12 @@
310
552
  const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
311
553
  const ds = (scriptEl && scriptEl.dataset) || {}
312
554
  const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
555
+ const captureCustomerInfo = userConfig.captureCustomerInfo !== undefined
556
+ ? userConfig.captureCustomerInfo
557
+ : ds.captureCustomerInfo === 'true' || ds.capture_customer_info === 'true'
558
+ const position = userConfig.position || ds.position || ds.launcherPosition || 'bottom-right'
559
+ const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
560
+ const validPosition = validPositions.includes(position) ? position : 'bottom-right'
313
561
  return {
314
562
  agentId: userConfig.agentId || ds.agentId,
315
563
  embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
@@ -321,6 +569,8 @@
321
569
  wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
322
570
  title: userConfig.title || ds.title,
323
571
  greeting: userConfig.greeting || ds.greeting,
572
+ captureCustomerInfo: captureCustomerInfo,
573
+ position: validPosition,
324
574
  }
325
575
  }
326
576
 
@@ -348,3 +598,4 @@
348
598
  init()
349
599
  }
350
600
  })()
601
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helllo-ai/agent-chat-widget",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Bot Swarm Agent Chat Widget - Embeddable chat widget for AI agents",
5
5
  "main": "agent-chat.latest.js",
6
6
  "files": [
@@ -26,16 +26,6 @@
26
26
  "bugs": {
27
27
  "url": "https://github.com/bot-swarm/agent-chat-widget/issues"
28
28
  },
29
- "scripts": {
30
- "build": "node build.js",
31
- "build:staging": "node build.js --staging",
32
- "build:prod": "node build.js --prod",
33
- "version:patch": "npm version patch",
34
- "version:minor": "npm version minor",
35
- "version:major": "npm version major",
36
- "publish:public": "npm publish --access public",
37
- "publish:staging": "npm publish --tag staging --access public"
38
- },
39
29
  "unpkg": "agent-chat.latest.js",
40
30
  "jsdelivr": "agent-chat.latest.js"
41
31
  }