@helllo-ai/agent-chat-widget 0.1.6

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.
@@ -0,0 +1,350 @@
1
+ ;(function () {
2
+ if (typeof window === 'undefined') return
3
+ if (window.AgentChatWidget) return
4
+
5
+ const VERSION = '0.1.0-prod'
6
+
7
+ function domainAllowed(hostname, allowedList) {
8
+ if (!Array.isArray(allowedList) || allowedList.length === 0) return true
9
+ const host = (hostname || '').toLowerCase()
10
+ return allowedList.some((d) => {
11
+ const domain = (d || '').toLowerCase().trim()
12
+ if (!domain) return false
13
+ return host === domain || host.endsWith('.' + domain)
14
+ })
15
+ }
16
+
17
+ function createStyles(primary, background) {
18
+ const safePrimary = primary || '#0f172a'
19
+ const safeBg = background || '#ffffff'
20
+ return `
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; }
24
+ .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; }
26
+ .acw-header { background: ${safePrimary}; color: #fff; padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; }
27
+ .acw-title { font-weight: 600; font-size: 14px; }
28
+ .acw-close { background: transparent; border: none; color: #fff; cursor: pointer; font-size: 16px; }
29
+ .acw-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
30
+ .acw-msg { padding: 10px 12px; border-radius: 10px; max-width: 90%; font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
31
+ .acw-msg-user { margin-left: auto; background: ${safePrimary}; color: #fff; }
32
+ .acw-msg-bot { margin-right: auto; background: rgba(0,0,0,0.05); color: #111; }
33
+ .acw-input { border-top: 1px solid rgba(0,0,0,0.08); padding: 10px; display: flex; gap: 8px; }
34
+ .acw-input input { flex: 1; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.12); border-radius: 8px; font-size: 14px; }
35
+ .acw-input button { background: ${safePrimary}; color: #fff; border: none; border-radius: 8px; padding: 0 14px; cursor: pointer; font-weight: 600; }
36
+ .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); }
37
+ .acw-dot { width: 8px; height: 8px; border-radius: 999px; box-shadow: 0 0 0 6px rgba(0,0,0,0.04); }
38
+ .acw-dot.connected { background: #22c55e; box-shadow: 0 0 0 6px rgba(34,197,94,0.18); }
39
+ .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
40
+ .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
+ .acw-disconnect:hover { background: #eef2f7; }
42
+ @media (max-width: 480px) {
43
+ .acw-panel { width: calc(100vw - 24px); height: 70vh; }
44
+ }
45
+ `
46
+ }
47
+
48
+ function createWidget(config) {
49
+ const {
50
+ agentId,
51
+ embedKey,
52
+ primaryColor,
53
+ backgroundColor,
54
+ allowedDomains = [],
55
+ apiBaseUrl,
56
+ voiceServiceUrl,
57
+ wsUrl,
58
+ title = 'Chat with us',
59
+ greeting = null,
60
+ } = config
61
+
62
+ if (!agentId) {
63
+ console.warn('[AgentChatWidget] Missing data-agent-id')
64
+ return
65
+ }
66
+ if (!embedKey) {
67
+ console.warn('[AgentChatWidget] Missing data-embed-key')
68
+ return
69
+ }
70
+
71
+ const hostOk = domainAllowed(window.location.hostname, allowedDomains)
72
+ if (!hostOk) {
73
+ console.warn('[AgentChatWidget] Host not in allowed domains, widget will not load')
74
+ return
75
+ }
76
+
77
+ const container = document.createElement('div')
78
+ container.className = 'acw-container'
79
+ const shadow = container.attachShadow({ mode: 'open' })
80
+ const style = document.createElement('style')
81
+ style.textContent = createStyles(primaryColor, backgroundColor)
82
+ shadow.appendChild(style)
83
+
84
+ const panel = document.createElement('div')
85
+ panel.className = 'acw-panel'
86
+
87
+ const launcher = document.createElement('button')
88
+ launcher.className = 'acw-launcher'
89
+ launcher.innerHTML = '<span>Chat</span>'
90
+
91
+ const header = document.createElement('div')
92
+ header.className = 'acw-header'
93
+ const titleEl = document.createElement('div')
94
+ titleEl.className = 'acw-title'
95
+ titleEl.textContent = title
96
+ const headerActions = document.createElement('div')
97
+ headerActions.style.display = 'flex'
98
+ headerActions.style.alignItems = 'center'
99
+ headerActions.style.gap = '8px'
100
+
101
+ const minimizeBtn = document.createElement('button')
102
+ minimizeBtn.className = 'acw-close'
103
+ minimizeBtn.textContent = '–'
104
+ const closeBtn = document.createElement('button')
105
+ closeBtn.className = 'acw-close'
106
+ closeBtn.textContent = '×'
107
+
108
+ headerActions.appendChild(minimizeBtn)
109
+ headerActions.appendChild(closeBtn)
110
+ header.appendChild(titleEl)
111
+ header.appendChild(headerActions)
112
+
113
+ const statusEl = document.createElement('div')
114
+ statusEl.className = 'acw-status'
115
+ const statusDot = document.createElement('div')
116
+ statusDot.className = 'acw-dot disconnected'
117
+ const statusText = document.createElement('span')
118
+ statusText.textContent = 'Disconnected'
119
+ const disconnectBtn = document.createElement('button')
120
+ disconnectBtn.className = 'acw-disconnect'
121
+ disconnectBtn.textContent = 'Connect'
122
+ statusEl.appendChild(statusDot)
123
+ statusEl.appendChild(statusText)
124
+ statusEl.appendChild(disconnectBtn)
125
+
126
+ const messages = document.createElement('div')
127
+ messages.className = 'acw-messages'
128
+
129
+ const inputWrap = document.createElement('div')
130
+ inputWrap.className = 'acw-input'
131
+ const input = document.createElement('input')
132
+ input.type = 'text'
133
+ input.placeholder = 'Type a message...'
134
+ const sendBtn = document.createElement('button')
135
+ sendBtn.textContent = 'Send'
136
+ inputWrap.appendChild(input)
137
+ inputWrap.appendChild(sendBtn)
138
+
139
+ panel.appendChild(header)
140
+ panel.appendChild(statusEl)
141
+ panel.appendChild(messages)
142
+ panel.appendChild(inputWrap)
143
+ shadow.appendChild(panel)
144
+ shadow.appendChild(launcher)
145
+ document.body.appendChild(container)
146
+
147
+ function appendMessage(text, role) {
148
+ const msg = document.createElement('div')
149
+ msg.className = 'acw-msg ' + (role === 'user' ? 'acw-msg-user' : 'acw-msg-bot')
150
+ msg.textContent = text
151
+ messages.appendChild(msg)
152
+ messages.scrollTop = messages.scrollHeight
153
+ }
154
+
155
+ // No default greeting; only show messages after connection
156
+
157
+ let ws = null
158
+ let connected = false
159
+ let connecting = false
160
+
161
+ const resolvedWsUrl = (() => {
162
+ 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')
169
+ const params = new URLSearchParams()
170
+ params.set('embed_key', embedKey)
171
+ params.set('host', window.location.hostname)
172
+ return `${wsBase}/api/v1/agent-voice/agents/${agentId}/text-chat?${params.toString()}`
173
+ })()
174
+
175
+ // Log resolved URL once for debugging
176
+ try {
177
+ console.info('[AgentChatWidget-Prod] Resolved WS URL:', resolvedWsUrl)
178
+ } catch (_) {}
179
+
180
+ function updateStatus(text, isConnected) {
181
+ statusText.textContent = text
182
+ statusDot.classList.remove('connected', 'disconnected')
183
+ statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
184
+
185
+ // Update button text and handler based on connection status
186
+ disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
187
+ disconnectBtn.onclick = isConnected ? disconnect : connect
188
+ }
189
+
190
+ function connect() {
191
+ if (connected || connecting) return
192
+ connecting = true
193
+ const url = resolvedWsUrl
194
+ try {
195
+ ws = new WebSocket(url)
196
+ } catch (err) {
197
+ connecting = false
198
+ updateStatus('Connection error', false)
199
+ console.error('[AgentChatWidget-Prod] WS error', err)
200
+ return
201
+ }
202
+
203
+ ws.onopen = () => {
204
+ connected = true
205
+ connecting = false
206
+ updateStatus('Connected', true)
207
+ }
208
+
209
+ ws.onmessage = (event) => {
210
+ try {
211
+ const data = JSON.parse(event.data)
212
+ const text = data.text || data.content || data.message || ''
213
+ const role = data.role || 'assistant'
214
+ if (text) appendMessage(text, role === 'user' ? 'user' : 'bot')
215
+ } catch (e) {
216
+ // Fallback to raw text
217
+ if (event.data) appendMessage(String(event.data), 'bot')
218
+ }
219
+ }
220
+
221
+ ws.onerror = (e) => {
222
+ console.error('[AgentChatWidget-Prod] WS error', e)
223
+ updateStatus('Connection error', false)
224
+ appendMessage('Connection error. Check WS service and URL.', 'bot')
225
+ }
226
+
227
+ ws.onclose = (ev) => {
228
+ connected = false
229
+ connecting = false
230
+ const reason = ev && (ev.reason || ev.code)
231
+ updateStatus('Disconnected', false)
232
+ if (!ev.wasClean) {
233
+ appendMessage(`Connection closed (${reason || 'unclean'})`, 'bot')
234
+ }
235
+ }
236
+ }
237
+
238
+ function disconnect() {
239
+ if (ws) {
240
+ ws.close()
241
+ ws = null
242
+ }
243
+ connected = false
244
+ connecting = false
245
+ updateStatus('Disconnected', false)
246
+ }
247
+
248
+ function sendMessage() {
249
+ const text = input.value.trim()
250
+ if (!text) return
251
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
252
+ connect()
253
+ setTimeout(sendMessage, 200)
254
+ return
255
+ }
256
+ appendMessage(text, 'user')
257
+ ws.send(JSON.stringify({ type: 'user_message', text, role: 'user', embed_key: embedKey }))
258
+ input.value = ''
259
+ }
260
+
261
+ launcher.onclick = () => {
262
+ const isOpen = panel.style.display === 'flex'
263
+ if (isOpen) {
264
+ panel.style.display = 'none'
265
+ disconnect()
266
+ } else {
267
+ panel.style.display = 'flex'
268
+ connect()
269
+ }
270
+ }
271
+
272
+ closeBtn.onclick = () => {
273
+ panel.style.display = 'none'
274
+ disconnect()
275
+ }
276
+
277
+ minimizeBtn.onclick = () => {
278
+ panel.style.display = 'none'
279
+ disconnect()
280
+ }
281
+
282
+ disconnectBtn.onclick = () => {
283
+ disconnect()
284
+ }
285
+ sendBtn.onclick = sendMessage
286
+ input.addEventListener('keydown', (e) => {
287
+ if (e.key === 'Enter') {
288
+ e.preventDefault()
289
+ sendMessage()
290
+ }
291
+ })
292
+
293
+ return {
294
+ destroy() {
295
+ disconnect()
296
+ container.remove()
297
+ },
298
+ open() {
299
+ panel.style.display = 'flex'
300
+ connect()
301
+ },
302
+ close() {
303
+ panel.style.display = 'none'
304
+ disconnect()
305
+ },
306
+ }
307
+ }
308
+
309
+ function buildConfig(userConfig) {
310
+ const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
311
+ const ds = (scriptEl && scriptEl.dataset) || {}
312
+ const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
313
+ return {
314
+ agentId: userConfig.agentId || ds.agentId,
315
+ embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
316
+ primaryColor: userConfig.primaryColor || ds.primaryColor || ds.primary_color,
317
+ backgroundColor: userConfig.backgroundColor || ds.backgroundColor || ds.background_color,
318
+ allowedDomains: userConfig.allowedDomains || allowed,
319
+ apiBaseUrl: userConfig.apiBaseUrl || ds.apiBaseUrl || ds.api_base_url,
320
+ voiceServiceUrl: userConfig.voiceServiceUrl || ds.voiceServiceUrl || ds.voice_service_url,
321
+ wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
322
+ title: userConfig.title || ds.title,
323
+ greeting: userConfig.greeting || ds.greeting,
324
+ }
325
+ }
326
+
327
+ let instance = null
328
+
329
+ function init(userConfig = {}) {
330
+ if (instance) return instance
331
+ const cfg = buildConfig(userConfig)
332
+ instance = createWidget(cfg)
333
+ return instance
334
+ }
335
+
336
+ function destroy() {
337
+ if (instance && typeof instance.destroy === 'function') {
338
+ instance.destroy()
339
+ instance = null
340
+ }
341
+ }
342
+
343
+ window.AgentChatWidget = { init, destroy, version: VERSION }
344
+
345
+ // Auto-init if script tag has data-agent-id
346
+ const autoScript = document.currentScript || document.querySelector('script[data-agent-id]')
347
+ if (autoScript && autoScript.dataset && autoScript.dataset.autoInit !== 'false') {
348
+ init()
349
+ }
350
+ })()
@@ -0,0 +1,350 @@
1
+ ;(function () {
2
+ if (typeof window === 'undefined') return
3
+ if (window.AgentChatWidget) return
4
+
5
+ const VERSION = '0.1.0-staging'
6
+
7
+ function domainAllowed(hostname, allowedList) {
8
+ if (!Array.isArray(allowedList) || allowedList.length === 0) return true
9
+ const host = (hostname || '').toLowerCase()
10
+ return allowedList.some((d) => {
11
+ const domain = (d || '').toLowerCase().trim()
12
+ if (!domain) return false
13
+ return host === domain || host.endsWith('.' + domain)
14
+ })
15
+ }
16
+
17
+ function createStyles(primary, background) {
18
+ const safePrimary = primary || '#0f172a'
19
+ const safeBg = background || '#ffffff'
20
+ return `
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; }
24
+ .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; }
26
+ .acw-header { background: ${safePrimary}; color: #fff; padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; }
27
+ .acw-title { font-weight: 600; font-size: 14px; }
28
+ .acw-close { background: transparent; border: none; color: #fff; cursor: pointer; font-size: 16px; }
29
+ .acw-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
30
+ .acw-msg { padding: 10px 12px; border-radius: 10px; max-width: 90%; font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
31
+ .acw-msg-user { margin-left: auto; background: ${safePrimary}; color: #fff; }
32
+ .acw-msg-bot { margin-right: auto; background: rgba(0,0,0,0.05); color: #111; }
33
+ .acw-input { border-top: 1px solid rgba(0,0,0,0.08); padding: 10px; display: flex; gap: 8px; }
34
+ .acw-input input { flex: 1; padding: 10px 12px; border: 1px solid rgba(0,0,0,0.12); border-radius: 8px; font-size: 14px; }
35
+ .acw-input button { background: ${safePrimary}; color: #fff; border: none; border-radius: 8px; padding: 0 14px; cursor: pointer; font-weight: 600; }
36
+ .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); }
37
+ .acw-dot { width: 8px; height: 8px; border-radius: 999px; box-shadow: 0 0 0 6px rgba(0,0,0,0.04); }
38
+ .acw-dot.connected { background: #22c55e; box-shadow: 0 0 0 6px rgba(34,197,94,0.18); }
39
+ .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
40
+ .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
+ .acw-disconnect:hover { background: #eef2f7; }
42
+ @media (max-width: 480px) {
43
+ .acw-panel { width: calc(100vw - 24px); height: 70vh; }
44
+ }
45
+ `
46
+ }
47
+
48
+ function createWidget(config) {
49
+ const {
50
+ agentId,
51
+ embedKey,
52
+ primaryColor,
53
+ backgroundColor,
54
+ allowedDomains = [],
55
+ apiBaseUrl,
56
+ voiceServiceUrl,
57
+ wsUrl,
58
+ title = 'Chat with us (Staging)',
59
+ greeting = null,
60
+ } = config
61
+
62
+ if (!agentId) {
63
+ console.warn('[AgentChatWidget] Missing data-agent-id')
64
+ return
65
+ }
66
+ if (!embedKey) {
67
+ console.warn('[AgentChatWidget] Missing data-embed-key')
68
+ return
69
+ }
70
+
71
+ const hostOk = domainAllowed(window.location.hostname, allowedDomains)
72
+ if (!hostOk) {
73
+ console.warn('[AgentChatWidget] Host not in allowed domains, widget will not load')
74
+ return
75
+ }
76
+
77
+ const container = document.createElement('div')
78
+ container.className = 'acw-container'
79
+ const shadow = container.attachShadow({ mode: 'open' })
80
+ const style = document.createElement('style')
81
+ style.textContent = createStyles(primaryColor, backgroundColor)
82
+ shadow.appendChild(style)
83
+
84
+ const panel = document.createElement('div')
85
+ panel.className = 'acw-panel'
86
+
87
+ const launcher = document.createElement('button')
88
+ launcher.className = 'acw-launcher'
89
+ launcher.innerHTML = '<span>Chat (Staging)</span>'
90
+
91
+ const header = document.createElement('div')
92
+ header.className = 'acw-header'
93
+ const titleEl = document.createElement('div')
94
+ titleEl.className = 'acw-title'
95
+ titleEl.textContent = title
96
+ const headerActions = document.createElement('div')
97
+ headerActions.style.display = 'flex'
98
+ headerActions.style.alignItems = 'center'
99
+ headerActions.style.gap = '8px'
100
+
101
+ const minimizeBtn = document.createElement('button')
102
+ minimizeBtn.className = 'acw-close'
103
+ minimizeBtn.textContent = '–'
104
+ const closeBtn = document.createElement('button')
105
+ closeBtn.className = 'acw-close'
106
+ closeBtn.textContent = '×'
107
+
108
+ headerActions.appendChild(minimizeBtn)
109
+ headerActions.appendChild(closeBtn)
110
+ header.appendChild(titleEl)
111
+ header.appendChild(headerActions)
112
+
113
+ const statusEl = document.createElement('div')
114
+ statusEl.className = 'acw-status'
115
+ const statusDot = document.createElement('div')
116
+ statusDot.className = 'acw-dot disconnected'
117
+ const statusText = document.createElement('span')
118
+ statusText.textContent = 'Disconnected (Staging)'
119
+ const disconnectBtn = document.createElement('button')
120
+ disconnectBtn.className = 'acw-disconnect'
121
+ disconnectBtn.textContent = 'Connect'
122
+ statusEl.appendChild(statusDot)
123
+ statusEl.appendChild(statusText)
124
+ statusEl.appendChild(disconnectBtn)
125
+
126
+ const messages = document.createElement('div')
127
+ messages.className = 'acw-messages'
128
+
129
+ const inputWrap = document.createElement('div')
130
+ inputWrap.className = 'acw-input'
131
+ const input = document.createElement('input')
132
+ input.type = 'text'
133
+ input.placeholder = 'Type a message...'
134
+ const sendBtn = document.createElement('button')
135
+ sendBtn.textContent = 'Send'
136
+ inputWrap.appendChild(input)
137
+ inputWrap.appendChild(sendBtn)
138
+
139
+ panel.appendChild(header)
140
+ panel.appendChild(statusEl)
141
+ panel.appendChild(messages)
142
+ panel.appendChild(inputWrap)
143
+ shadow.appendChild(panel)
144
+ shadow.appendChild(launcher)
145
+ document.body.appendChild(container)
146
+
147
+ function appendMessage(text, role) {
148
+ const msg = document.createElement('div')
149
+ msg.className = 'acw-msg ' + (role === 'user' ? 'acw-msg-user' : 'acw-msg-bot')
150
+ msg.textContent = text
151
+ messages.appendChild(msg)
152
+ messages.scrollTop = messages.scrollHeight
153
+ }
154
+
155
+ // No default greeting; only show messages after connection
156
+
157
+ let ws = null
158
+ let connected = false
159
+ let connecting = false
160
+
161
+ const resolvedWsUrl = (() => {
162
+ 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')
169
+ const params = new URLSearchParams()
170
+ params.set('embed_key', embedKey)
171
+ params.set('host', window.location.hostname)
172
+ return `${wsBase}/api/v1/agent-voice/agents/${agentId}/text-chat?${params.toString()}`
173
+ })()
174
+
175
+ // Log resolved URL once for debugging
176
+ try {
177
+ console.info('[AgentChatWidget-Staging] Resolved WS URL:', resolvedWsUrl)
178
+ } catch (_) {}
179
+
180
+ function updateStatus(text, isConnected) {
181
+ statusText.textContent = text + ' (Staging)'
182
+ statusDot.classList.remove('connected', 'disconnected')
183
+ statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
184
+
185
+ // Update button text and handler based on connection status
186
+ disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
187
+ disconnectBtn.onclick = isConnected ? disconnect : connect
188
+ }
189
+
190
+ function connect() {
191
+ if (connected || connecting) return
192
+ connecting = true
193
+ const url = resolvedWsUrl
194
+ try {
195
+ ws = new WebSocket(url)
196
+ } catch (err) {
197
+ connecting = false
198
+ updateStatus('Connection error', false)
199
+ console.error('[AgentChatWidget-Staging] WS error', err)
200
+ return
201
+ }
202
+
203
+ ws.onopen = () => {
204
+ connected = true
205
+ connecting = false
206
+ updateStatus('Connected', true)
207
+ }
208
+
209
+ ws.onmessage = (event) => {
210
+ try {
211
+ const data = JSON.parse(event.data)
212
+ const text = data.text || data.content || data.message || ''
213
+ const role = data.role || 'assistant'
214
+ if (text) appendMessage(text, role === 'user' ? 'user' : 'bot')
215
+ } catch (e) {
216
+ // Fallback to raw text
217
+ if (event.data) appendMessage(String(event.data), 'bot')
218
+ }
219
+ }
220
+
221
+ ws.onerror = (e) => {
222
+ console.error('[AgentChatWidget-Staging] WS error', e)
223
+ updateStatus('Connection error', false)
224
+ appendMessage('Connection error. Check WS service and URL.', 'bot')
225
+ }
226
+
227
+ ws.onclose = (ev) => {
228
+ connected = false
229
+ connecting = false
230
+ const reason = ev && (ev.reason || ev.code)
231
+ updateStatus('Disconnected', false)
232
+ if (!ev.wasClean) {
233
+ appendMessage(`Connection closed (${reason || 'unclean'})`, 'bot')
234
+ }
235
+ }
236
+ }
237
+
238
+ function disconnect() {
239
+ if (ws) {
240
+ ws.close()
241
+ ws = null
242
+ }
243
+ connected = false
244
+ connecting = false
245
+ updateStatus('Disconnected', false)
246
+ }
247
+
248
+ function sendMessage() {
249
+ const text = input.value.trim()
250
+ if (!text) return
251
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
252
+ connect()
253
+ setTimeout(sendMessage, 200)
254
+ return
255
+ }
256
+ appendMessage(text, 'user')
257
+ ws.send(JSON.stringify({ type: 'user_message', text, role: 'user', embed_key: embedKey }))
258
+ input.value = ''
259
+ }
260
+
261
+ launcher.onclick = () => {
262
+ const isOpen = panel.style.display === 'flex'
263
+ if (isOpen) {
264
+ panel.style.display = 'none'
265
+ disconnect()
266
+ } else {
267
+ panel.style.display = 'flex'
268
+ connect()
269
+ }
270
+ }
271
+
272
+ closeBtn.onclick = () => {
273
+ panel.style.display = 'none'
274
+ disconnect()
275
+ }
276
+
277
+ minimizeBtn.onclick = () => {
278
+ panel.style.display = 'none'
279
+ disconnect()
280
+ }
281
+
282
+ disconnectBtn.onclick = () => {
283
+ disconnect()
284
+ }
285
+ sendBtn.onclick = sendMessage
286
+ input.addEventListener('keydown', (e) => {
287
+ if (e.key === 'Enter') {
288
+ e.preventDefault()
289
+ sendMessage()
290
+ }
291
+ })
292
+
293
+ return {
294
+ destroy() {
295
+ disconnect()
296
+ container.remove()
297
+ },
298
+ open() {
299
+ panel.style.display = 'flex'
300
+ connect()
301
+ },
302
+ close() {
303
+ panel.style.display = 'none'
304
+ disconnect()
305
+ },
306
+ }
307
+ }
308
+
309
+ function buildConfig(userConfig) {
310
+ const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
311
+ const ds = (scriptEl && scriptEl.dataset) || {}
312
+ const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
313
+ return {
314
+ agentId: userConfig.agentId || ds.agentId,
315
+ embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
316
+ primaryColor: userConfig.primaryColor || ds.primaryColor || ds.primary_color,
317
+ backgroundColor: userConfig.backgroundColor || ds.backgroundColor || ds.background_color,
318
+ allowedDomains: userConfig.allowedDomains || allowed,
319
+ apiBaseUrl: userConfig.apiBaseUrl || ds.apiBaseUrl || ds.api_base_url,
320
+ voiceServiceUrl: userConfig.voiceServiceUrl || ds.voiceServiceUrl || ds.voice_service_url,
321
+ wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
322
+ title: userConfig.title || ds.title,
323
+ greeting: userConfig.greeting || ds.greeting,
324
+ }
325
+ }
326
+
327
+ let instance = null
328
+
329
+ function init(userConfig = {}) {
330
+ if (instance) return instance
331
+ const cfg = buildConfig(userConfig)
332
+ instance = createWidget(cfg)
333
+ return instance
334
+ }
335
+
336
+ function destroy() {
337
+ if (instance && typeof instance.destroy === 'function') {
338
+ instance.destroy()
339
+ instance = null
340
+ }
341
+ }
342
+
343
+ window.AgentChatWidget = { init, destroy, version: VERSION }
344
+
345
+ // Auto-init if script tag has data-agent-id
346
+ const autoScript = document.currentScript || document.querySelector('script[data-agent-id]')
347
+ if (autoScript && autoScript.dataset && autoScript.dataset.autoInit !== 'false') {
348
+ init()
349
+ }
350
+ })()