vanilla-agent 1.21.0 → 1.22.0
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 +87 -0
- package/dist/index.cjs +24 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +247 -3
- package/dist/index.d.ts +247 -3
- package/dist/index.global.js +43 -43
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +23 -23
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +185 -0
- package/package.json +1 -1
- package/src/client.ts +215 -1
- package/src/components/message-bubble.ts +208 -4
- package/src/components/messages.ts +10 -3
- package/src/defaults.ts +15 -0
- package/src/index.ts +11 -3
- package/src/install.ts +69 -7
- package/src/session.ts +77 -1
- package/src/styles/widget.css +185 -0
- package/src/types.ts +184 -0
- package/src/ui.ts +27 -4
package/dist/install.global.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var SiteAgentInstaller=(()=>{(function(){"use strict";if(window.__siteAgentInstallerLoaded)return;window.__siteAgentInstallerLoaded=!0;let n=window.siteAgentConfig||{},
|
|
1
|
+
"use strict";var SiteAgentInstaller=(()=>{(function(){"use strict";if(window.__siteAgentInstallerLoaded)return;window.__siteAgentInstallerLoaded=!0;let c=(()=>{let e=document.currentScript;if(!e)return{};let t={},i=e.getAttribute("data-travrse-token");i&&(t.clientToken=i);let o=e.getAttribute("data-flow-id");o&&(t.flowId=o);let l=e.getAttribute("data-api-url");return l&&(t.apiUrl=l),t})(),n={...window.siteAgentConfig||{},...c},d=n.version||"latest",g=n.cdn||"jsdelivr",f=n.autoInit!==!1,u=()=>{if(n.cssUrl&&n.jsUrl)return{cssUrl:n.cssUrl,jsUrl:n.jsUrl};let t=`/npm/vanilla-agent@${d}/dist`;return g==="unpkg"?{cssUrl:`https://unpkg.com${t}/widget.css`,jsUrl:`https://unpkg.com${t}/index.global.js`}:{cssUrl:`https://cdn.jsdelivr.net${t}/widget.css`,jsUrl:`https://cdn.jsdelivr.net${t}/index.global.js`}},{cssUrl:r,jsUrl:s}=u(),w=()=>!!document.head.querySelector("link[data-vanilla-agent]")||!!document.head.querySelector('link[href*="widget.css"]'),p=()=>!!window.AgentWidget,A=()=>new Promise((e,t)=>{if(w()){e();return}let i=document.createElement("link");i.rel="stylesheet",i.href=r,i.setAttribute("data-vanilla-agent","true"),i.onload=()=>e(),i.onerror=()=>t(new Error(`Failed to load CSS from ${r}`)),document.head.appendChild(i)}),m=()=>new Promise((e,t)=>{if(p()){e();return}let i=document.createElement("script");i.src=s,i.async=!0,i.onload=()=>e(),i.onerror=()=>t(new Error(`Failed to load JS from ${s}`)),document.head.appendChild(i)}),C=()=>{if(!window.AgentWidget||!window.AgentWidget.initAgentWidget){console.warn("AgentWidget not available. Make sure the script loaded successfully.");return}let e=n.target||"body",t={...n.config};if(n.apiUrl&&!t.apiUrl&&(t.apiUrl=n.apiUrl),n.clientToken&&!t.clientToken&&(t.clientToken=n.clientToken),n.flowId&&!t.flowId&&(t.flowId=n.flowId),!(!(t.apiUrl||t.clientToken)&&Object.keys(t).length===0))try{window.AgentWidget.initAgentWidget({target:e,config:t})}catch(o){console.error("Failed to initialize AgentWidget:",o)}},a=async()=>{try{await A(),await m(),f&&(n.config||n.apiUrl||n.clientToken)&&setTimeout(C,0)}catch(e){console.error("Failed to install AgentWidget:",e)}};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",a):a()})();})();
|
|
2
2
|
//# sourceMappingURL=install.global.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/install.ts"],"sourcesContent":["/**\n * Standalone installer script for easy script tag installation\n * This script automatically loads CSS and JS, then initializes the widget\n * if configuration is provided via window.siteAgentConfig\n */\n\ninterface SiteAgentInstallConfig {\n version?: string;\n cdn?: \"unpkg\" | \"jsdelivr\";\n cssUrl?: string;\n jsUrl?: string;\n target?: string | HTMLElement;\n config?: any;\n autoInit?: boolean;\n}\n\ndeclare global {\n interface Window {\n siteAgentConfig?: SiteAgentInstallConfig;\n AgentWidget?: any;\n }\n}\n\n(function() {\n \"use strict\";\n\n // Prevent double installation\n if ((window as any).__siteAgentInstallerLoaded) {\n return;\n }\n (window as any).__siteAgentInstallerLoaded = true;\n\n const config: SiteAgentInstallConfig = window.siteAgentConfig || {};\n const version = config.version || \"latest\";\n const cdn = config.cdn || \"jsdelivr\";\n const autoInit = config.autoInit !== false; // Default to true\n\n // Determine CDN base URL\n const getCdnBase = () => {\n if (config.cssUrl && config.jsUrl) {\n return { cssUrl: config.cssUrl, jsUrl: config.jsUrl };\n }\n \n const packageName = \"vanilla-agent\";\n const basePath = `/npm/${packageName}@${version}/dist`;\n \n if (cdn === \"unpkg\") {\n return {\n cssUrl: `https://unpkg.com${basePath}/widget.css`,\n jsUrl: `https://unpkg.com${basePath}/index.global.js`\n };\n } else {\n return {\n cssUrl: `https://cdn.jsdelivr.net${basePath}/widget.css`,\n jsUrl: `https://cdn.jsdelivr.net${basePath}/index.global.js`\n };\n }\n };\n\n const { cssUrl, jsUrl } = getCdnBase();\n\n // Check if CSS is already loaded\n const isCssLoaded = () => {\n return !!document.head.querySelector('link[data-vanilla-agent]') ||\n !!document.head.querySelector(`link[href*=\"widget.css\"]`);\n };\n\n // Check if JS is already loaded\n const isJsLoaded = () => {\n return !!(window as any).AgentWidget;\n };\n\n // Load CSS\n const loadCSS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isCssLoaded()) {\n resolve();\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = cssUrl;\n link.setAttribute(\"data-vanilla-agent\", \"true\");\n link.onload = () => resolve();\n link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));\n document.head.appendChild(link);\n });\n };\n\n // Load JS\n const loadJS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isJsLoaded()) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = jsUrl;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load JS from ${jsUrl}`));\n document.head.appendChild(script);\n });\n };\n\n // Initialize widget\n const initWidget = () => {\n if (!window.AgentWidget || !window.AgentWidget.initAgentWidget) {\n console.warn(\"AgentWidget not available. Make sure the script loaded successfully.\");\n return;\n }\n\n const target = config.target || \"body\";\n // Merge
|
|
1
|
+
{"version":3,"sources":["../src/install.ts"],"sourcesContent":["/**\n * Standalone installer script for easy script tag installation\n * This script automatically loads CSS and JS, then initializes the widget\n * if configuration is provided via window.siteAgentConfig\n */\n\ninterface SiteAgentInstallConfig {\n version?: string;\n cdn?: \"unpkg\" | \"jsdelivr\";\n cssUrl?: string;\n jsUrl?: string;\n target?: string | HTMLElement;\n config?: any;\n autoInit?: boolean;\n // Client token mode options (can also be set via data attributes)\n clientToken?: string;\n flowId?: string;\n apiUrl?: string;\n}\n\ndeclare global {\n interface Window {\n siteAgentConfig?: SiteAgentInstallConfig;\n AgentWidget?: any;\n }\n}\n\n(function() {\n \"use strict\";\n\n // Prevent double installation\n if ((window as any).__siteAgentInstallerLoaded) {\n return;\n }\n (window as any).__siteAgentInstallerLoaded = true;\n\n /**\n * Read configuration from data attributes on the current script tag.\n * Supports: data-travrse-token, data-flow-id, data-api-url\n */\n const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {\n // Try to get the current script element\n const script = document.currentScript as HTMLScriptElement | null;\n if (!script) return {};\n\n const scriptConfig: Partial<SiteAgentInstallConfig> = {};\n\n // Client token from data attribute (primary method for client token mode)\n const token = script.getAttribute('data-travrse-token');\n if (token) {\n scriptConfig.clientToken = token;\n }\n\n // Optional flow ID\n const flowId = script.getAttribute('data-flow-id');\n if (flowId) {\n scriptConfig.flowId = flowId;\n }\n\n // Optional API URL override\n const apiUrl = script.getAttribute('data-api-url');\n if (apiUrl) {\n scriptConfig.apiUrl = apiUrl;\n }\n\n return scriptConfig;\n };\n\n // Get config from script attributes (must be called synchronously during script execution)\n const scriptConfig = getConfigFromScript();\n\n // Merge script attributes with window config (script attributes take precedence)\n const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};\n const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };\n \n const version = config.version || \"latest\";\n const cdn = config.cdn || \"jsdelivr\";\n const autoInit = config.autoInit !== false; // Default to true\n\n // Determine CDN base URL\n const getCdnBase = () => {\n if (config.cssUrl && config.jsUrl) {\n return { cssUrl: config.cssUrl, jsUrl: config.jsUrl };\n }\n \n const packageName = \"vanilla-agent\";\n const basePath = `/npm/${packageName}@${version}/dist`;\n \n if (cdn === \"unpkg\") {\n return {\n cssUrl: `https://unpkg.com${basePath}/widget.css`,\n jsUrl: `https://unpkg.com${basePath}/index.global.js`\n };\n } else {\n return {\n cssUrl: `https://cdn.jsdelivr.net${basePath}/widget.css`,\n jsUrl: `https://cdn.jsdelivr.net${basePath}/index.global.js`\n };\n }\n };\n\n const { cssUrl, jsUrl } = getCdnBase();\n\n // Check if CSS is already loaded\n const isCssLoaded = () => {\n return !!document.head.querySelector('link[data-vanilla-agent]') ||\n !!document.head.querySelector(`link[href*=\"widget.css\"]`);\n };\n\n // Check if JS is already loaded\n const isJsLoaded = () => {\n return !!(window as any).AgentWidget;\n };\n\n // Load CSS\n const loadCSS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isCssLoaded()) {\n resolve();\n return;\n }\n\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = cssUrl;\n link.setAttribute(\"data-vanilla-agent\", \"true\");\n link.onload = () => resolve();\n link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`));\n document.head.appendChild(link);\n });\n };\n\n // Load JS\n const loadJS = (): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (isJsLoaded()) {\n resolve();\n return;\n }\n\n const script = document.createElement(\"script\");\n script.src = jsUrl;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load JS from ${jsUrl}`));\n document.head.appendChild(script);\n });\n };\n\n // Initialize widget\n const initWidget = () => {\n if (!window.AgentWidget || !window.AgentWidget.initAgentWidget) {\n console.warn(\"AgentWidget not available. Make sure the script loaded successfully.\");\n return;\n }\n\n const target = config.target || \"body\";\n // Merge top-level config options into widget config\n const widgetConfig = { ...config.config };\n \n // Merge apiUrl from top-level config into widget config if present\n if (config.apiUrl && !widgetConfig.apiUrl) {\n widgetConfig.apiUrl = config.apiUrl;\n }\n \n // Merge clientToken from top-level config into widget config if present\n if (config.clientToken && !widgetConfig.clientToken) {\n widgetConfig.clientToken = config.clientToken;\n }\n \n // Merge flowId from top-level config into widget config if present\n if (config.flowId && !widgetConfig.flowId) {\n widgetConfig.flowId = config.flowId;\n }\n\n // Only initialize if we have either apiUrl OR clientToken (or other config)\n const hasApiConfig = widgetConfig.apiUrl || widgetConfig.clientToken;\n if (!hasApiConfig && Object.keys(widgetConfig).length === 0) {\n return;\n }\n\n try {\n window.AgentWidget.initAgentWidget({\n target,\n config: widgetConfig\n });\n } catch (error) {\n console.error(\"Failed to initialize AgentWidget:\", error);\n }\n };\n\n // Main installation flow\n const install = async () => {\n try {\n await loadCSS();\n await loadJS();\n \n // Auto-init if we have config OR apiUrl OR clientToken\n const shouldAutoInit = autoInit && (\n config.config || \n config.apiUrl || \n config.clientToken\n );\n \n if (shouldAutoInit) {\n // Wait a tick to ensure AgentWidget is fully initialized\n setTimeout(initWidget, 0);\n }\n } catch (error) {\n console.error(\"Failed to install AgentWidget:\", error);\n }\n };\n\n // Start installation\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", install);\n } else {\n install();\n }\n})();\n\n"],"mappings":"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","names":["scriptConfig","script","token","flowId","apiUrl","config","version","cdn","autoInit","getCdnBase","basePath","cssUrl","jsUrl","isCssLoaded","isJsLoaded","loadCSS","resolve","reject","link","loadJS","initWidget","target","widgetConfig","error","install"]}
|
package/dist/widget.css
CHANGED
|
@@ -1215,3 +1215,188 @@ form:focus-within textarea {
|
|
|
1215
1215
|
.vanilla-message-user-bubble code:not(pre code) {
|
|
1216
1216
|
background-color: rgba(255, 255, 255, 0.2);
|
|
1217
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
/* ============================================
|
|
1220
|
+
Message Action Buttons (Copy, Upvote, Downvote)
|
|
1221
|
+
============================================ */
|
|
1222
|
+
|
|
1223
|
+
/* Make message bubble position relative for overlay positioning */
|
|
1224
|
+
.vanilla-message-bubble {
|
|
1225
|
+
position: relative;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/* Fade-in animation for action buttons */
|
|
1229
|
+
@keyframes tvw-message-actions-fade-in {
|
|
1230
|
+
from {
|
|
1231
|
+
opacity: 0;
|
|
1232
|
+
}
|
|
1233
|
+
to {
|
|
1234
|
+
opacity: 1;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/* Base action bar styles */
|
|
1239
|
+
.tvw-message-actions {
|
|
1240
|
+
display: flex;
|
|
1241
|
+
align-items: center;
|
|
1242
|
+
gap: 0.25rem;
|
|
1243
|
+
margin-top: 0.5rem;
|
|
1244
|
+
padding-top: 0.5rem;
|
|
1245
|
+
border-top: 1px solid var(--cw-divider, #f1f5f9);
|
|
1246
|
+
/* Fade in when first shown (for "always" visibility) */
|
|
1247
|
+
animation: tvw-message-actions-fade-in 0.3s ease-out;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/* Action bar alignment */
|
|
1251
|
+
.tvw-message-actions-left {
|
|
1252
|
+
justify-content: flex-start;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.tvw-message-actions-center {
|
|
1256
|
+
justify-content: center;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.tvw-message-actions-right {
|
|
1260
|
+
justify-content: flex-end;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/* Hover visibility mode - overlay on desktop */
|
|
1264
|
+
@media (hover: hover) {
|
|
1265
|
+
.tvw-message-actions-hover {
|
|
1266
|
+
/* Hidden by default */
|
|
1267
|
+
opacity: 0;
|
|
1268
|
+
pointer-events: none;
|
|
1269
|
+
transition: opacity 0.15s ease-in-out;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/* Pill layout - compact floating pill */
|
|
1273
|
+
.tvw-message-actions-hover.tvw-message-actions-pill {
|
|
1274
|
+
position: absolute;
|
|
1275
|
+
bottom: 0.5rem;
|
|
1276
|
+
margin-top: 0;
|
|
1277
|
+
padding: 0.25rem;
|
|
1278
|
+
border-top: none;
|
|
1279
|
+
width: fit-content;
|
|
1280
|
+
background-color: var(--cw-surface, #ffffff);
|
|
1281
|
+
border: 1px solid var(--cw-divider, #f1f5f9);
|
|
1282
|
+
border-radius: var(--cw-radius-md, 0.75rem);
|
|
1283
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/* Pill layout - position based on alignment */
|
|
1287
|
+
.tvw-message-actions-hover.tvw-message-actions-pill.tvw-message-actions-left {
|
|
1288
|
+
left: 0.75rem;
|
|
1289
|
+
right: auto;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.tvw-message-actions-hover.tvw-message-actions-pill.tvw-message-actions-center {
|
|
1293
|
+
left: 50%;
|
|
1294
|
+
right: auto;
|
|
1295
|
+
transform: translateX(-50%);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.tvw-message-actions-hover.tvw-message-actions-pill.tvw-message-actions-right {
|
|
1299
|
+
right: 0.75rem;
|
|
1300
|
+
left: auto;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/* Row layout - full-width bar at bottom */
|
|
1304
|
+
.tvw-message-actions-hover.tvw-message-actions-row {
|
|
1305
|
+
position: absolute;
|
|
1306
|
+
bottom: 0;
|
|
1307
|
+
left: 0;
|
|
1308
|
+
right: 0;
|
|
1309
|
+
margin-top: 0;
|
|
1310
|
+
padding: 0.5rem 0.75rem;
|
|
1311
|
+
border-top: none;
|
|
1312
|
+
background: linear-gradient(
|
|
1313
|
+
to top,
|
|
1314
|
+
var(--cw-surface, #ffffff) 70%,
|
|
1315
|
+
transparent
|
|
1316
|
+
);
|
|
1317
|
+
border-radius: 0 0 var(--cw-radius-lg, 1.5rem) var(--cw-radius-lg, 1.5rem);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.vanilla-message-bubble:hover .tvw-message-actions-hover,
|
|
1321
|
+
.vanilla-message-bubble:focus-within .tvw-message-actions-hover {
|
|
1322
|
+
opacity: 1;
|
|
1323
|
+
pointer-events: auto;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/* On touch devices (no hover support), show inline and always visible */
|
|
1328
|
+
@media (hover: none) {
|
|
1329
|
+
.tvw-message-actions-hover {
|
|
1330
|
+
/* Keep normal flow positioning on mobile */
|
|
1331
|
+
position: static;
|
|
1332
|
+
opacity: 1;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/* Action button base styles */
|
|
1337
|
+
.tvw-message-action-btn {
|
|
1338
|
+
display: inline-flex;
|
|
1339
|
+
align-items: center;
|
|
1340
|
+
justify-content: center;
|
|
1341
|
+
width: 28px;
|
|
1342
|
+
height: 28px;
|
|
1343
|
+
padding: 0;
|
|
1344
|
+
border: none;
|
|
1345
|
+
border-radius: 6px;
|
|
1346
|
+
background-color: transparent;
|
|
1347
|
+
color: var(--cw-muted, #6b7280);
|
|
1348
|
+
cursor: pointer;
|
|
1349
|
+
transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.tvw-message-action-btn:hover {
|
|
1353
|
+
background-color: var(--cw-container, #f8fafc);
|
|
1354
|
+
color: var(--cw-primary, #111827);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.tvw-message-action-btn:active {
|
|
1358
|
+
transform: scale(0.95);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.tvw-message-action-btn:focus {
|
|
1362
|
+
outline: none;
|
|
1363
|
+
box-shadow: 0 0 0 2px var(--cw-accent, #1d4ed8);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.tvw-message-action-btn:focus:not(:focus-visible) {
|
|
1367
|
+
box-shadow: none;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.tvw-message-action-btn:focus-visible {
|
|
1371
|
+
box-shadow: 0 0 0 2px var(--cw-accent, #1d4ed8);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/* Active state (voted) */
|
|
1375
|
+
.tvw-message-action-btn.tvw-message-action-active {
|
|
1376
|
+
background-color: var(--cw-accent, #1d4ed8);
|
|
1377
|
+
color: #ffffff;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.tvw-message-action-btn.tvw-message-action-active:hover {
|
|
1381
|
+
background-color: var(--cw-accent, #1d4ed8);
|
|
1382
|
+
color: #ffffff;
|
|
1383
|
+
opacity: 0.9;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/* Success state (after copy) */
|
|
1387
|
+
.tvw-message-action-btn.tvw-message-action-success {
|
|
1388
|
+
background-color: #10b981;
|
|
1389
|
+
color: #ffffff;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.tvw-message-action-btn.tvw-message-action-success:hover {
|
|
1393
|
+
background-color: #10b981;
|
|
1394
|
+
color: #ffffff;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/* Icon styling within buttons */
|
|
1398
|
+
.tvw-message-action-btn svg {
|
|
1399
|
+
width: 14px;
|
|
1400
|
+
height: 14px;
|
|
1401
|
+
flex-shrink: 0;
|
|
1402
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
package/src/client.ts
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
AgentWidgetCustomFetch,
|
|
10
10
|
AgentWidgetSSEEventParser,
|
|
11
11
|
AgentWidgetHeadersFunction,
|
|
12
|
-
AgentWidgetSSEEventResult
|
|
12
|
+
AgentWidgetSSEEventResult,
|
|
13
|
+
ClientSession,
|
|
14
|
+
ClientInitResponse,
|
|
15
|
+
ClientChatRequest
|
|
13
16
|
} from "./types";
|
|
14
17
|
import {
|
|
15
18
|
extractTextFromJson,
|
|
@@ -27,6 +30,7 @@ type DispatchOptions = {
|
|
|
27
30
|
type SSEHandler = (event: AgentWidgetEvent) => void;
|
|
28
31
|
|
|
29
32
|
const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
|
|
33
|
+
const DEFAULT_CLIENT_API_BASE = "https://api.travrse.ai";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Maps parserType string to the corresponding parser factory function
|
|
@@ -55,6 +59,10 @@ export class AgentWidgetClient {
|
|
|
55
59
|
private readonly customFetch?: AgentWidgetCustomFetch;
|
|
56
60
|
private readonly parseSSEEvent?: AgentWidgetSSEEventParser;
|
|
57
61
|
private readonly getHeaders?: AgentWidgetHeadersFunction;
|
|
62
|
+
|
|
63
|
+
// Client token mode properties
|
|
64
|
+
private clientSession: ClientSession | null = null;
|
|
65
|
+
private sessionInitPromise: Promise<ClientSession> | null = null;
|
|
58
66
|
|
|
59
67
|
constructor(private config: AgentWidgetConfig = {}) {
|
|
60
68
|
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
@@ -72,7 +80,213 @@ export class AgentWidgetClient {
|
|
|
72
80
|
this.getHeaders = config.getHeaders;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Check if running in client token mode
|
|
85
|
+
*/
|
|
86
|
+
public isClientTokenMode(): boolean {
|
|
87
|
+
return !!this.config.clientToken;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the appropriate API URL based on mode
|
|
92
|
+
*/
|
|
93
|
+
private getClientApiUrl(endpoint: 'init' | 'chat'): string {
|
|
94
|
+
const baseUrl = this.config.apiUrl?.replace(/\/+$/, '').replace(/\/v1\/dispatch$/, '') || DEFAULT_CLIENT_API_BASE;
|
|
95
|
+
return endpoint === 'init'
|
|
96
|
+
? `${baseUrl}/v1/client/init`
|
|
97
|
+
: `${baseUrl}/v1/client/chat`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the current client session (if any)
|
|
102
|
+
*/
|
|
103
|
+
public getClientSession(): ClientSession | null {
|
|
104
|
+
return this.clientSession;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Initialize session for client token mode.
|
|
109
|
+
* Called automatically on first message if not already initialized.
|
|
110
|
+
*/
|
|
111
|
+
public async initSession(): Promise<ClientSession> {
|
|
112
|
+
if (!this.isClientTokenMode()) {
|
|
113
|
+
throw new Error('initSession() only available in client token mode');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return existing session if valid
|
|
117
|
+
if (this.clientSession && new Date() < this.clientSession.expiresAt) {
|
|
118
|
+
return this.clientSession;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Deduplicate concurrent init calls
|
|
122
|
+
if (this.sessionInitPromise) {
|
|
123
|
+
return this.sessionInitPromise;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.sessionInitPromise = this._doInitSession();
|
|
127
|
+
try {
|
|
128
|
+
const session = await this.sessionInitPromise;
|
|
129
|
+
this.clientSession = session;
|
|
130
|
+
this.config.onSessionInit?.(session);
|
|
131
|
+
return session;
|
|
132
|
+
} finally {
|
|
133
|
+
this.sessionInitPromise = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async _doInitSession(): Promise<ClientSession> {
|
|
138
|
+
const response = await fetch(this.getClientApiUrl('init'), {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
token: this.config.clientToken,
|
|
145
|
+
flow_id: this.config.flowId,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const error = await response.json().catch(() => ({ error: 'Session initialization failed' }));
|
|
151
|
+
if (response.status === 401) {
|
|
152
|
+
throw new Error(`Invalid client token: ${error.hint || error.error}`);
|
|
153
|
+
}
|
|
154
|
+
if (response.status === 403) {
|
|
155
|
+
throw new Error(`Origin not allowed: ${error.hint || error.error}`);
|
|
156
|
+
}
|
|
157
|
+
throw new Error(error.error || 'Failed to initialize session');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const data: ClientInitResponse = await response.json();
|
|
161
|
+
return {
|
|
162
|
+
sessionId: data.session_id,
|
|
163
|
+
expiresAt: new Date(data.expires_at),
|
|
164
|
+
flow: data.flow,
|
|
165
|
+
config: {
|
|
166
|
+
welcomeMessage: data.config.welcome_message,
|
|
167
|
+
placeholder: data.config.placeholder,
|
|
168
|
+
theme: data.config.theme,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear the current client session
|
|
175
|
+
*/
|
|
176
|
+
public clearClientSession(): void {
|
|
177
|
+
this.clientSession = null;
|
|
178
|
+
this.sessionInitPromise = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Send a message - handles both proxy and client token modes
|
|
183
|
+
*/
|
|
75
184
|
public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
|
|
185
|
+
if (this.isClientTokenMode()) {
|
|
186
|
+
return this.dispatchClientToken(options, onEvent);
|
|
187
|
+
}
|
|
188
|
+
return this.dispatchProxy(options, onEvent);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Client token mode dispatch
|
|
193
|
+
*/
|
|
194
|
+
private async dispatchClientToken(options: DispatchOptions, onEvent: SSEHandler) {
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
if (options.signal) {
|
|
197
|
+
options.signal.addEventListener("abort", () => controller.abort());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
onEvent({ type: "status", status: "connecting" });
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Ensure session is initialized
|
|
204
|
+
const session = await this.initSession();
|
|
205
|
+
|
|
206
|
+
// Check if session is about to expire (within 1 minute)
|
|
207
|
+
if (new Date() >= new Date(session.expiresAt.getTime() - 60000)) {
|
|
208
|
+
// Session expired or expiring soon
|
|
209
|
+
this.clientSession = null;
|
|
210
|
+
this.config.onSessionExpired?.();
|
|
211
|
+
const error = new Error('Session expired. Please refresh to continue.');
|
|
212
|
+
onEvent({ type: "error", error });
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build the chat request payload
|
|
217
|
+
const chatRequest: ClientChatRequest = {
|
|
218
|
+
session_id: session.sessionId,
|
|
219
|
+
messages: options.messages.map(m => ({
|
|
220
|
+
role: m.role,
|
|
221
|
+
content: m.rawContent || m.content,
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (this.debug) {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const response = await fetch(this.getClientApiUrl('chat'), {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify(chatRequest),
|
|
236
|
+
signal: controller.signal,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
const errorData = await response.json().catch(() => ({ error: 'Chat request failed' }));
|
|
241
|
+
|
|
242
|
+
if (response.status === 401) {
|
|
243
|
+
// Session expired
|
|
244
|
+
this.clientSession = null;
|
|
245
|
+
this.config.onSessionExpired?.();
|
|
246
|
+
const error = new Error('Session expired. Please refresh to continue.');
|
|
247
|
+
onEvent({ type: "error", error });
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (response.status === 429) {
|
|
252
|
+
const error = new Error(errorData.hint || 'Message limit reached for this session.');
|
|
253
|
+
onEvent({ type: "error", error });
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const error = new Error(errorData.error || 'Failed to send message');
|
|
258
|
+
onEvent({ type: "error", error });
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!response.body) {
|
|
263
|
+
const error = new Error('No response body received');
|
|
264
|
+
onEvent({ type: "error", error });
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
onEvent({ type: "status", status: "connected" });
|
|
269
|
+
|
|
270
|
+
// Stream the response (same SSE handling as proxy mode)
|
|
271
|
+
try {
|
|
272
|
+
await this.streamResponse(response.body, onEvent);
|
|
273
|
+
} finally {
|
|
274
|
+
onEvent({ type: "status", status: "idle" });
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
278
|
+
// Only emit error if it wasn't already emitted
|
|
279
|
+
if (!err.message.includes('Session expired') && !err.message.includes('Message limit')) {
|
|
280
|
+
onEvent({ type: "error", error: err });
|
|
281
|
+
}
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Proxy mode dispatch (original implementation)
|
|
288
|
+
*/
|
|
289
|
+
private async dispatchProxy(options: DispatchOptions, onEvent: SSEHandler) {
|
|
76
290
|
const controller = new AbortController();
|
|
77
291
|
if (options.signal) {
|
|
78
292
|
options.signal.addEventListener("abort", () => controller.abort());
|