@helllo-ai/agent-chat-widget 0.1.6 → 0.1.7
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/package.json +5 -2
- package/agent-chat.v0.1.0.js +0 -345
- package/agent-chat.v0.1.js +0 -345
- package/agent-chat.v0.js +0 -345
- package/build.js +0 -88
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helllo-ai/agent-chat-widget",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Bot Swarm Agent Chat Widget - Embeddable chat widget for AI agents",
|
|
5
5
|
"main": "agent-chat.latest.js",
|
|
6
6
|
"files": [
|
|
7
|
-
"
|
|
7
|
+
"agent-chat.staging.js",
|
|
8
|
+
"agent-chat.prod.js",
|
|
9
|
+
"agent-chat.latest.js",
|
|
10
|
+
"README.md"
|
|
8
11
|
],
|
|
9
12
|
"keywords": [
|
|
10
13
|
"chat-widget",
|
package/agent-chat.v0.1.0.js
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
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
|
-
|
package/agent-chat.v0.1.js
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
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
|
-
|
package/agent-chat.v0.js
DELETED
|
@@ -1,345 +0,0 @@
|
|
|
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
|
-
|
package/build.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build script for agent-chat-widget package
|
|
5
|
-
*
|
|
6
|
-
* This script syncs the widget files from the frontend/public directory
|
|
7
|
-
* and prepares them for npm publishing.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* node build.js # Build latest/development version
|
|
11
|
-
* node build.js --staging # Build staging version
|
|
12
|
-
* node build.js --prod # Build production version
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
|
|
18
|
-
const sourceDir = path.join(__dirname, '../../../frontend/public');
|
|
19
|
-
const targetDir = __dirname;
|
|
20
|
-
|
|
21
|
-
// Parse command line arguments
|
|
22
|
-
const args = process.argv.slice(2);
|
|
23
|
-
const isStaging = args.includes('--staging');
|
|
24
|
-
const isProd = args.includes('--prod');
|
|
25
|
-
|
|
26
|
-
// Determine which files to sync based on build type
|
|
27
|
-
let widgetFiles;
|
|
28
|
-
if (isProd) {
|
|
29
|
-
console.log('🏗️ Building production version...');
|
|
30
|
-
widgetFiles = [
|
|
31
|
-
'agent-chat.prod.js',
|
|
32
|
-
'agent-chat.v0.1.0.js',
|
|
33
|
-
'agent-chat.v0.1.js',
|
|
34
|
-
'agent-chat.v0.js'
|
|
35
|
-
];
|
|
36
|
-
} else if (isStaging) {
|
|
37
|
-
console.log('🏗️ Building staging version...');
|
|
38
|
-
widgetFiles = [
|
|
39
|
-
'agent-chat.staging.js',
|
|
40
|
-
'agent-chat.v0.1.0.js',
|
|
41
|
-
'agent-chat.v0.1.js',
|
|
42
|
-
'agent-chat.v0.js'
|
|
43
|
-
];
|
|
44
|
-
} else {
|
|
45
|
-
console.log('🏗️ Building development version...');
|
|
46
|
-
widgetFiles = [
|
|
47
|
-
'agent-chat.latest.js',
|
|
48
|
-
'agent-chat.v0.1.0.js',
|
|
49
|
-
'agent-chat.v0.1.js',
|
|
50
|
-
'agent-chat.v0.js'
|
|
51
|
-
];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.log('🔄 Syncing widget files from frontend/public...');
|
|
55
|
-
|
|
56
|
-
let syncedCount = 0;
|
|
57
|
-
let errorCount = 0;
|
|
58
|
-
|
|
59
|
-
widgetFiles.forEach(filename => {
|
|
60
|
-
const sourcePath = path.join(sourceDir, filename);
|
|
61
|
-
const targetPath = path.join(targetDir, filename);
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
if (fs.existsSync(sourcePath)) {
|
|
65
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
66
|
-
console.log(`✅ Synced: ${filename}`);
|
|
67
|
-
syncedCount++;
|
|
68
|
-
} else {
|
|
69
|
-
console.log(`⚠️ Source file not found: ${filename}`);
|
|
70
|
-
errorCount++;
|
|
71
|
-
}
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error(`❌ Error syncing ${filename}:`, error.message);
|
|
74
|
-
errorCount++;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
console.log(`\n📊 Sync complete: ${syncedCount} files synced, ${errorCount} errors`);
|
|
79
|
-
|
|
80
|
-
if (errorCount > 0) {
|
|
81
|
-
console.log('\n💡 Make sure to run the widget versioning script first:');
|
|
82
|
-
console.log(' cd frontend && npm run version:widget');
|
|
83
|
-
process.exit(1);
|
|
84
|
-
} else {
|
|
85
|
-
const buildType = isProd ? 'production' : isStaging ? 'staging' : 'development';
|
|
86
|
-
const publishCmd = isStaging ? 'npm run publish:staging' : 'npm run publish:public';
|
|
87
|
-
console.log(`\n🎉 ${buildType} build ready! Run: ${publishCmd}`);
|
|
88
|
-
}
|