@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,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
+
package/build.js ADDED
@@ -0,0 +1,88 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@helllo-ai/agent-chat-widget",
3
+ "version": "0.1.6",
4
+ "description": "Bot Swarm Agent Chat Widget - Embeddable chat widget for AI agents",
5
+ "main": "agent-chat.latest.js",
6
+ "files": [
7
+ "*.js"
8
+ ],
9
+ "keywords": [
10
+ "chat-widget",
11
+ "ai-agent",
12
+ "chatbot",
13
+ "widget",
14
+ "embeddable"
15
+ ],
16
+ "author": "Bot Swarm",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/helllo-ai/agent-chat-widget.git"
21
+ },
22
+ "homepage": "https://helllo.ai",
23
+ "bugs": {
24
+ "url": "https://github.com/bot-swarm/agent-chat-widget/issues"
25
+ },
26
+ "scripts": {
27
+ "build": "node build.js",
28
+ "build:staging": "node build.js --staging",
29
+ "build:prod": "node build.js --prod",
30
+ "version:patch": "npm version patch",
31
+ "version:minor": "npm version minor",
32
+ "version:major": "npm version major",
33
+ "publish:public": "npm publish --access public",
34
+ "publish:staging": "npm publish --tag staging --access public"
35
+ },
36
+ "unpkg": "agent-chat.latest.js",
37
+ "jsdelivr": "agent-chat.latest.js"
38
+ }