@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.
- package/README.md +182 -0
- package/agent-chat.latest.js +345 -0
- package/agent-chat.prod.js +350 -0
- package/agent-chat.staging.js +350 -0
- package/agent-chat.v0.1.0.js +345 -0
- package/agent-chat.v0.1.js +345 -0
- package/agent-chat.v0.js +345 -0
- package/build.js +88 -0
- package/package.json +38 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
;(function () {
|
|
2
|
+
if (typeof window === 'undefined') return
|
|
3
|
+
if (window.AgentChatWidget) return
|
|
4
|
+
|
|
5
|
+
const VERSION = '0.1.0'
|
|
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
|
+
const baseCandidate = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
|
|
164
|
+
const voiceBase = (baseCandidate || '').replace(/\/$/, '')
|
|
165
|
+
const wsBase = voiceBase.replace(/^http/, 'ws')
|
|
166
|
+
const params = new URLSearchParams()
|
|
167
|
+
params.set('embed_key', embedKey)
|
|
168
|
+
params.set('host', window.location.hostname)
|
|
169
|
+
return `${wsBase}/api/v1/agent-voice/agents/${agentId}/text-chat?${params.toString()}`
|
|
170
|
+
})()
|
|
171
|
+
|
|
172
|
+
// Log resolved URL once for debugging
|
|
173
|
+
try {
|
|
174
|
+
console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
|
|
175
|
+
} catch (_) {}
|
|
176
|
+
|
|
177
|
+
function updateStatus(text, isConnected) {
|
|
178
|
+
statusText.textContent = text
|
|
179
|
+
statusDot.classList.remove('connected', 'disconnected')
|
|
180
|
+
statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
|
|
181
|
+
|
|
182
|
+
// Update button text and handler based on connection status
|
|
183
|
+
disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
|
|
184
|
+
disconnectBtn.onclick = isConnected ? disconnect : connect
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function connect() {
|
|
188
|
+
if (connected || connecting) return
|
|
189
|
+
connecting = true
|
|
190
|
+
const url = resolvedWsUrl
|
|
191
|
+
try {
|
|
192
|
+
ws = new WebSocket(url)
|
|
193
|
+
} catch (err) {
|
|
194
|
+
connecting = false
|
|
195
|
+
updateStatus('Connection error', false)
|
|
196
|
+
console.error('[AgentChatWidget] WS error', err)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ws.onopen = () => {
|
|
201
|
+
connected = true
|
|
202
|
+
connecting = false
|
|
203
|
+
updateStatus('Connected', true)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ws.onmessage = (event) => {
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(event.data)
|
|
209
|
+
const text = data.text || data.content || data.message || ''
|
|
210
|
+
const role = data.role || 'assistant'
|
|
211
|
+
if (text) appendMessage(text, role === 'user' ? 'user' : 'bot')
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Fallback to raw text
|
|
214
|
+
if (event.data) appendMessage(String(event.data), 'bot')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
ws.onerror = (e) => {
|
|
219
|
+
console.error('[AgentChatWidget] WS error', e)
|
|
220
|
+
updateStatus('Connection error', false)
|
|
221
|
+
appendMessage('Connection error. Check WS service and URL.', 'bot')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ws.onclose = (ev) => {
|
|
225
|
+
connected = false
|
|
226
|
+
connecting = false
|
|
227
|
+
const reason = ev && (ev.reason || ev.code)
|
|
228
|
+
updateStatus('Disconnected', false)
|
|
229
|
+
if (!ev.wasClean) {
|
|
230
|
+
appendMessage(`Connection closed (${reason || 'unclean'})`, 'bot')
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function disconnect() {
|
|
236
|
+
if (ws) {
|
|
237
|
+
ws.close()
|
|
238
|
+
ws = null
|
|
239
|
+
}
|
|
240
|
+
connected = false
|
|
241
|
+
connecting = false
|
|
242
|
+
updateStatus('Disconnected', false)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sendMessage() {
|
|
246
|
+
const text = input.value.trim()
|
|
247
|
+
if (!text) return
|
|
248
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
249
|
+
connect()
|
|
250
|
+
setTimeout(sendMessage, 200)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
appendMessage(text, 'user')
|
|
254
|
+
ws.send(JSON.stringify({ type: 'user_message', text, role: 'user', embed_key: embedKey }))
|
|
255
|
+
input.value = ''
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
launcher.onclick = () => {
|
|
259
|
+
const isOpen = panel.style.display === 'flex'
|
|
260
|
+
if (isOpen) {
|
|
261
|
+
panel.style.display = 'none'
|
|
262
|
+
disconnect()
|
|
263
|
+
} else {
|
|
264
|
+
panel.style.display = 'flex'
|
|
265
|
+
connect()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
closeBtn.onclick = () => {
|
|
270
|
+
panel.style.display = 'none'
|
|
271
|
+
disconnect()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
minimizeBtn.onclick = () => {
|
|
275
|
+
panel.style.display = 'none'
|
|
276
|
+
disconnect()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
sendBtn.onclick = sendMessage
|
|
280
|
+
input.addEventListener('keydown', (e) => {
|
|
281
|
+
if (e.key === 'Enter') {
|
|
282
|
+
e.preventDefault()
|
|
283
|
+
sendMessage()
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
destroy() {
|
|
289
|
+
disconnect()
|
|
290
|
+
container.remove()
|
|
291
|
+
},
|
|
292
|
+
open() {
|
|
293
|
+
panel.style.display = 'flex'
|
|
294
|
+
connect()
|
|
295
|
+
},
|
|
296
|
+
close() {
|
|
297
|
+
panel.style.display = 'none'
|
|
298
|
+
disconnect()
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildConfig(userConfig) {
|
|
304
|
+
const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
|
|
305
|
+
const ds = (scriptEl && scriptEl.dataset) || {}
|
|
306
|
+
const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
|
|
307
|
+
return {
|
|
308
|
+
agentId: userConfig.agentId || ds.agentId,
|
|
309
|
+
embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
|
|
310
|
+
primaryColor: userConfig.primaryColor || ds.primaryColor || ds.primary_color,
|
|
311
|
+
backgroundColor: userConfig.backgroundColor || ds.backgroundColor || ds.background_color,
|
|
312
|
+
allowedDomains: userConfig.allowedDomains || allowed,
|
|
313
|
+
apiBaseUrl: userConfig.apiBaseUrl || ds.apiBaseUrl || ds.api_base_url,
|
|
314
|
+
voiceServiceUrl: userConfig.voiceServiceUrl || ds.voiceServiceUrl || ds.voice_service_url,
|
|
315
|
+
wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
|
|
316
|
+
title: userConfig.title || ds.title,
|
|
317
|
+
greeting: userConfig.greeting || ds.greeting,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let instance = null
|
|
322
|
+
|
|
323
|
+
function init(userConfig = {}) {
|
|
324
|
+
if (instance) return instance
|
|
325
|
+
const cfg = buildConfig(userConfig)
|
|
326
|
+
instance = createWidget(cfg)
|
|
327
|
+
return instance
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function destroy() {
|
|
331
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
332
|
+
instance.destroy()
|
|
333
|
+
instance = null
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
window.AgentChatWidget = { init, destroy, version: VERSION }
|
|
338
|
+
|
|
339
|
+
// Auto-init if script tag has data-agent-id
|
|
340
|
+
const autoScript = document.currentScript || document.querySelector('script[data-agent-id]')
|
|
341
|
+
if (autoScript && autoScript.dataset && autoScript.dataset.autoInit !== 'false') {
|
|
342
|
+
init()
|
|
343
|
+
}
|
|
344
|
+
})()
|
|
345
|
+
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
;(function () {
|
|
2
|
+
if (typeof window === 'undefined') return
|
|
3
|
+
if (window.AgentChatWidget) return
|
|
4
|
+
|
|
5
|
+
const VERSION = '0.1.0'
|
|
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
|
+
const baseCandidate = voiceServiceUrl || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
|
|
164
|
+
const voiceBase = (baseCandidate || '').replace(/\/$/, '')
|
|
165
|
+
const wsBase = voiceBase.replace(/^http/, 'ws')
|
|
166
|
+
const params = new URLSearchParams()
|
|
167
|
+
params.set('embed_key', embedKey)
|
|
168
|
+
params.set('host', window.location.hostname)
|
|
169
|
+
return `${wsBase}/api/v1/agent-voice/agents/${agentId}/text-chat?${params.toString()}`
|
|
170
|
+
})()
|
|
171
|
+
|
|
172
|
+
// Log resolved URL once for debugging
|
|
173
|
+
try {
|
|
174
|
+
console.info('[AgentChatWidget] Resolved WS URL:', resolvedWsUrl)
|
|
175
|
+
} catch (_) {}
|
|
176
|
+
|
|
177
|
+
function updateStatus(text, isConnected) {
|
|
178
|
+
statusText.textContent = text
|
|
179
|
+
statusDot.classList.remove('connected', 'disconnected')
|
|
180
|
+
statusDot.classList.add(isConnected ? 'connected' : 'disconnected')
|
|
181
|
+
|
|
182
|
+
// Update button text and handler based on connection status
|
|
183
|
+
disconnectBtn.textContent = isConnected ? 'Disconnect' : 'Connect'
|
|
184
|
+
disconnectBtn.onclick = isConnected ? disconnect : connect
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function connect() {
|
|
188
|
+
if (connected || connecting) return
|
|
189
|
+
connecting = true
|
|
190
|
+
const url = resolvedWsUrl
|
|
191
|
+
try {
|
|
192
|
+
ws = new WebSocket(url)
|
|
193
|
+
} catch (err) {
|
|
194
|
+
connecting = false
|
|
195
|
+
updateStatus('Connection error', false)
|
|
196
|
+
console.error('[AgentChatWidget] WS error', err)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ws.onopen = () => {
|
|
201
|
+
connected = true
|
|
202
|
+
connecting = false
|
|
203
|
+
updateStatus('Connected', true)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ws.onmessage = (event) => {
|
|
207
|
+
try {
|
|
208
|
+
const data = JSON.parse(event.data)
|
|
209
|
+
const text = data.text || data.content || data.message || ''
|
|
210
|
+
const role = data.role || 'assistant'
|
|
211
|
+
if (text) appendMessage(text, role === 'user' ? 'user' : 'bot')
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Fallback to raw text
|
|
214
|
+
if (event.data) appendMessage(String(event.data), 'bot')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
ws.onerror = (e) => {
|
|
219
|
+
console.error('[AgentChatWidget] WS error', e)
|
|
220
|
+
updateStatus('Connection error', false)
|
|
221
|
+
appendMessage('Connection error. Check WS service and URL.', 'bot')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ws.onclose = (ev) => {
|
|
225
|
+
connected = false
|
|
226
|
+
connecting = false
|
|
227
|
+
const reason = ev && (ev.reason || ev.code)
|
|
228
|
+
updateStatus('Disconnected', false)
|
|
229
|
+
if (!ev.wasClean) {
|
|
230
|
+
appendMessage(`Connection closed (${reason || 'unclean'})`, 'bot')
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function disconnect() {
|
|
236
|
+
if (ws) {
|
|
237
|
+
ws.close()
|
|
238
|
+
ws = null
|
|
239
|
+
}
|
|
240
|
+
connected = false
|
|
241
|
+
connecting = false
|
|
242
|
+
updateStatus('Disconnected', false)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sendMessage() {
|
|
246
|
+
const text = input.value.trim()
|
|
247
|
+
if (!text) return
|
|
248
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
249
|
+
connect()
|
|
250
|
+
setTimeout(sendMessage, 200)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
appendMessage(text, 'user')
|
|
254
|
+
ws.send(JSON.stringify({ type: 'user_message', text, role: 'user', embed_key: embedKey }))
|
|
255
|
+
input.value = ''
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
launcher.onclick = () => {
|
|
259
|
+
const isOpen = panel.style.display === 'flex'
|
|
260
|
+
if (isOpen) {
|
|
261
|
+
panel.style.display = 'none'
|
|
262
|
+
disconnect()
|
|
263
|
+
} else {
|
|
264
|
+
panel.style.display = 'flex'
|
|
265
|
+
connect()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
closeBtn.onclick = () => {
|
|
270
|
+
panel.style.display = 'none'
|
|
271
|
+
disconnect()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
minimizeBtn.onclick = () => {
|
|
275
|
+
panel.style.display = 'none'
|
|
276
|
+
disconnect()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
sendBtn.onclick = sendMessage
|
|
280
|
+
input.addEventListener('keydown', (e) => {
|
|
281
|
+
if (e.key === 'Enter') {
|
|
282
|
+
e.preventDefault()
|
|
283
|
+
sendMessage()
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
destroy() {
|
|
289
|
+
disconnect()
|
|
290
|
+
container.remove()
|
|
291
|
+
},
|
|
292
|
+
open() {
|
|
293
|
+
panel.style.display = 'flex'
|
|
294
|
+
connect()
|
|
295
|
+
},
|
|
296
|
+
close() {
|
|
297
|
+
panel.style.display = 'none'
|
|
298
|
+
disconnect()
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildConfig(userConfig) {
|
|
304
|
+
const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
|
|
305
|
+
const ds = (scriptEl && scriptEl.dataset) || {}
|
|
306
|
+
const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
|
|
307
|
+
return {
|
|
308
|
+
agentId: userConfig.agentId || ds.agentId,
|
|
309
|
+
embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
|
|
310
|
+
primaryColor: userConfig.primaryColor || ds.primaryColor || ds.primary_color,
|
|
311
|
+
backgroundColor: userConfig.backgroundColor || ds.backgroundColor || ds.background_color,
|
|
312
|
+
allowedDomains: userConfig.allowedDomains || allowed,
|
|
313
|
+
apiBaseUrl: userConfig.apiBaseUrl || ds.apiBaseUrl || ds.api_base_url,
|
|
314
|
+
voiceServiceUrl: userConfig.voiceServiceUrl || ds.voiceServiceUrl || ds.voice_service_url,
|
|
315
|
+
wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
|
|
316
|
+
title: userConfig.title || ds.title,
|
|
317
|
+
greeting: userConfig.greeting || ds.greeting,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let instance = null
|
|
322
|
+
|
|
323
|
+
function init(userConfig = {}) {
|
|
324
|
+
if (instance) return instance
|
|
325
|
+
const cfg = buildConfig(userConfig)
|
|
326
|
+
instance = createWidget(cfg)
|
|
327
|
+
return instance
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function destroy() {
|
|
331
|
+
if (instance && typeof instance.destroy === 'function') {
|
|
332
|
+
instance.destroy()
|
|
333
|
+
instance = null
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
window.AgentChatWidget = { init, destroy, version: VERSION }
|
|
338
|
+
|
|
339
|
+
// Auto-init if script tag has data-agent-id
|
|
340
|
+
const autoScript = document.currentScript || document.querySelector('script[data-agent-id]')
|
|
341
|
+
if (autoScript && autoScript.dataset && autoScript.dataset.autoInit !== 'false') {
|
|
342
|
+
init()
|
|
343
|
+
}
|
|
344
|
+
})()
|
|
345
|
+
|