vanilla-agent 0.2.0 → 1.0.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 +53 -21
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -61
- package/dist/index.d.ts +98 -61
- package/dist/index.global.js +36 -30
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +5 -5
- 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 +33 -0
- package/package.json +2 -2
- package/src/client.ts +14 -14
- package/src/components/forms.ts +7 -5
- package/src/components/launcher.ts +4 -4
- package/src/components/message-bubble.ts +3 -3
- package/src/components/messages.ts +4 -2
- package/src/components/panel.ts +254 -13
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +4 -4
- package/src/components/tool-bubble.ts +2 -2
- package/src/defaults.ts +180 -0
- package/src/index.ts +21 -18
- package/src/install.ts +8 -8
- package/src/plugins/registry.ts +7 -5
- package/src/plugins/types.ts +13 -11
- package/src/runtime/init.ts +11 -8
- package/src/session.ts +32 -23
- package/src/styles/widget.css +33 -0
- package/src/types.ts +56 -31
- package/src/ui.ts +330 -22
- package/src/utils/constants.ts +4 -2
- package/src/utils/dom.ts +2 -0
- package/src/utils/formatting.ts +8 -6
- package/src/utils/icons.ts +1 -1
- package/src/utils/positioning.ts +2 -0
- package/src/utils/theme.ts +4 -2
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||{},o=n.version||"latest",l=n.cdn||"jsdelivr",d=n.autoInit!==!1,c=()=>{if(n.cssUrl&&n.jsUrl)return{cssUrl:n.cssUrl,jsUrl:n.jsUrl};let
|
|
1
|
+
"use strict";var SiteAgentInstaller=(()=>{(function(){"use strict";if(window.__siteAgentInstallerLoaded)return;window.__siteAgentInstallerLoaded=!0;let n=window.siteAgentConfig||{},o=n.version||"latest",l=n.cdn||"jsdelivr",d=n.autoInit!==!1,c=()=>{if(n.cssUrl&&n.jsUrl)return{cssUrl:n.cssUrl,jsUrl:n.jsUrl};let e=`/npm/vanilla-agent@${o}/dist`;return l==="unpkg"?{cssUrl:`https://unpkg.com${e}/widget.css`,jsUrl:`https://unpkg.com${e}/index.global.js`}:{cssUrl:`https://cdn.jsdelivr.net${e}/widget.css`,jsUrl:`https://cdn.jsdelivr.net${e}/index.global.js`}},{cssUrl:r,jsUrl:s}=c(),g=()=>!!document.head.querySelector("link[data-vanilla-agent]")||!!document.head.querySelector('link[href*="widget.css"]'),u=()=>!!window.AgentWidget,f=()=>new Promise((i,e)=>{if(g()){i();return}let t=document.createElement("link");t.rel="stylesheet",t.href=r,t.setAttribute("data-vanilla-agent","true"),t.onload=()=>i(),t.onerror=()=>e(new Error(`Failed to load CSS from ${r}`)),document.head.appendChild(t)}),w=()=>new Promise((i,e)=>{if(u()){i();return}let t=document.createElement("script");t.src=s,t.async=!0,t.onload=()=>i(),t.onerror=()=>e(new Error(`Failed to load JS from ${s}`)),document.head.appendChild(t)}),p=()=>{if(!window.AgentWidget||!window.AgentWidget.initAgentWidget){console.warn("AgentWidget not available. Make sure the script loaded successfully.");return}let i=n.target||"body",e={...n.config};if(n.apiUrl&&!e.apiUrl&&(e.apiUrl=n.apiUrl),!(!e.apiUrl&&Object.keys(e).length===0))try{window.AgentWidget.initAgentWidget({target:i,config:e})}catch(t){console.error("Failed to initialize AgentWidget:",t)}},a=async()=>{try{await f(),await w(),d&&(n.config||n.apiUrl)&&setTimeout(p,0)}catch(i){console.error("Failed to install AgentWidget:",i)}};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
|
|
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 apiUrl from top-level config into widget config if present\n const widgetConfig = { ...config.config };\n if ((config as any).apiUrl && !widgetConfig.apiUrl) {\n widgetConfig.apiUrl = (config as any).apiUrl;\n }\n\n // Only initialize if config is provided\n if (!widgetConfig.apiUrl && 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 if (autoInit && (config.config || (config as any).apiUrl)) {\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":"2CAuBC,UAAW,CACV,aAGA,GAAK,OAAe,2BAClB,OAED,OAAe,2BAA6B,GAE7C,IAAMA,EAAiC,OAAO,iBAAmB,CAAC,EAC5DC,EAAUD,EAAO,SAAW,SAC5BE,EAAMF,EAAO,KAAO,WACpBG,EAAWH,EAAO,WAAa,GAG/BI,EAAa,IAAM,CACvB,GAAIJ,EAAO,QAAUA,EAAO,MAC1B,MAAO,CAAE,OAAQA,EAAO,OAAQ,MAAOA,EAAO,KAAM,EAItD,IAAMK,EAAW,sBAAuBJ,CAAO,QAE/C,OAAIC,IAAQ,QACH,CACL,OAAQ,oBAAoBG,CAAQ,cACpC,MAAO,oBAAoBA,CAAQ,kBACrC,EAEO,CACL,OAAQ,2BAA2BA,CAAQ,cAC3C,MAAO,2BAA2BA,CAAQ,kBAC5C,CAEJ,EAEM,CAAE,OAAAC,EAAQ,MAAAC,CAAM,EAAIH,EAAW,EAG/BI,EAAc,IACX,CAAC,CAAC,SAAS,KAAK,cAAc,0BAA0B,GACxD,CAAC,CAAC,SAAS,KAAK,cAAc,0BAA0B,EAI3DC,EAAa,IACV,CAAC,CAAE,OAAe,YAIrBC,EAAU,IACP,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtC,GAAIJ,EAAY,EAAG,CACjBG,EAAQ,EACR,MACF,CAEA,IAAME,EAAO,SAAS,cAAc,MAAM,EAC1CA,EAAK,IAAM,aACXA,EAAK,KAAOP,EACZO,EAAK,aAAa,qBAAsB,MAAM,EAC9CA,EAAK,OAAS,IAAMF,EAAQ,EAC5BE,EAAK,QAAU,IAAMD,EAAO,IAAI,MAAM,2BAA2BN,CAAM,EAAE,CAAC,EAC1E,SAAS,KAAK,YAAYO,CAAI,CAChC,CAAC,EAIGC,EAAS,IACN,IAAI,QAAQ,CAACH,EAASC,IAAW,CACtC,GAAIH,EAAW,EAAG,CAChBE,EAAQ,EACR,MACF,CAEA,IAAMI,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,IAAMR,EACbQ,EAAO,MAAQ,GACfA,EAAO,OAAS,IAAMJ,EAAQ,EAC9BI,EAAO,QAAU,IAAMH,EAAO,IAAI,MAAM,0BAA0BL,CAAK,EAAE,CAAC,EAC1E,SAAS,KAAK,YAAYQ,CAAM,CAClC,CAAC,EAIGC,EAAa,IAAM,CACvB,GAAI,CAAC,OAAO,aAAe,CAAC,OAAO,YAAY,gBAAiB,CAC9D,QAAQ,KAAK,sEAAsE,EACnF,MACF,CAEA,IAAMC,EAASjB,EAAO,QAAU,OAE1BkB,EAAe,CAAE,GAAGlB,EAAO,MAAO,EAMxC,GALKA,EAAe,QAAU,CAACkB,EAAa,SAC1CA,EAAa,OAAUlB,EAAe,QAIpC,GAACkB,EAAa,QAAU,OAAO,KAAKA,CAAY,EAAE,SAAW,GAIjE,GAAI,CACF,OAAO,YAAY,gBAAgB,CACjC,OAAAD,EACA,OAAQC,CACV,CAAC,CACH,OAASC,EAAO,CACd,QAAQ,MAAM,oCAAqCA,CAAK,CAC1D,CACF,EAGMC,EAAU,SAAY,CAC1B,GAAI,CACF,MAAMV,EAAQ,EACd,MAAMI,EAAO,EAETX,IAAaH,EAAO,QAAWA,EAAe,SAEhD,WAAWgB,EAAY,CAAC,CAE5B,OAASG,EAAO,CACd,QAAQ,MAAM,iCAAkCA,CAAK,CACvD,CACF,EAGI,SAAS,aAAe,UAC1B,SAAS,iBAAiB,mBAAoBC,CAAO,EAErDA,EAAQ,CAEZ,GAAG","names":["config","version","cdn","autoInit","getCdnBase","basePath","cssUrl","jsUrl","isCssLoaded","isJsLoaded","loadCSS","resolve","reject","link","loadJS","script","initWidget","target","widgetConfig","error","install"]}
|
package/dist/widget.css
CHANGED
|
@@ -572,6 +572,10 @@
|
|
|
572
572
|
width: 360px;
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
.tvw-w-\[400px\] {
|
|
576
|
+
width: 400px;
|
|
577
|
+
}
|
|
578
|
+
|
|
575
579
|
.tvw-h-\[640px\] {
|
|
576
580
|
height: 640px;
|
|
577
581
|
}
|
|
@@ -687,6 +691,35 @@ form:focus-within textarea {
|
|
|
687
691
|
opacity: 1;
|
|
688
692
|
}
|
|
689
693
|
|
|
694
|
+
/* Clear chat button tooltip */
|
|
695
|
+
.tvw-clear-chat-button-wrapper {
|
|
696
|
+
position: relative;
|
|
697
|
+
display: inline-flex;
|
|
698
|
+
align-items: center;
|
|
699
|
+
justify-content: center;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.tvw-clear-chat-tooltip {
|
|
703
|
+
background-color: #111827;
|
|
704
|
+
color: #ffffff;
|
|
705
|
+
padding: 6px 12px;
|
|
706
|
+
border-radius: 0.5rem;
|
|
707
|
+
font-size: 12px;
|
|
708
|
+
white-space: nowrap;
|
|
709
|
+
pointer-events: none;
|
|
710
|
+
z-index: 10000;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.tvw-clear-chat-tooltip-arrow {
|
|
714
|
+
content: "";
|
|
715
|
+
position: absolute;
|
|
716
|
+
top: 100%;
|
|
717
|
+
left: 50%;
|
|
718
|
+
transform: translateX(-50%);
|
|
719
|
+
border: 4px solid transparent;
|
|
720
|
+
border-top-color: #111827;
|
|
721
|
+
}
|
|
722
|
+
|
|
690
723
|
/* Typing indicator animation */
|
|
691
724
|
@keyframes typing {
|
|
692
725
|
0%, 100% {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "rimraf dist && npm run build:styles && npm run build:client && npm run build:installer",
|
|
55
55
|
"build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
|
|
56
|
-
"build:client": "tsup src/index.ts --format esm,cjs,iife --global-name
|
|
56
|
+
"build:client": "tsup src/index.ts --format esm,cjs,iife --global-name AgentWidget --minify --sourcemap --splitting false --dts --loader \".css=text\"",
|
|
57
57
|
"build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
|
|
58
58
|
"lint": "eslint . --ext .ts",
|
|
59
59
|
"typecheck": "tsc --noEmit"
|
package/src/client.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentWidgetConfig, AgentWidgetMessage, AgentWidgetEvent } from "./types";
|
|
2
2
|
|
|
3
3
|
type DispatchOptions = {
|
|
4
|
-
messages:
|
|
4
|
+
messages: AgentWidgetMessage[];
|
|
5
5
|
signal?: AbortSignal;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
type SSEHandler = (event:
|
|
8
|
+
type SSEHandler = (event: AgentWidgetEvent) => void;
|
|
9
9
|
|
|
10
10
|
const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
|
|
11
11
|
|
|
12
|
-
export class
|
|
12
|
+
export class AgentWidgetClient {
|
|
13
13
|
private readonly apiUrl: string;
|
|
14
14
|
private readonly headers: Record<string, string>;
|
|
15
15
|
private readonly debug: boolean;
|
|
16
16
|
|
|
17
|
-
constructor(private config:
|
|
17
|
+
constructor(private config: AgentWidgetConfig = {}) {
|
|
18
18
|
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
19
19
|
this.headers = {
|
|
20
20
|
"Content-Type": "application/json",
|
|
@@ -51,7 +51,7 @@ export class ChatWidgetClient {
|
|
|
51
51
|
|
|
52
52
|
if (this.debug) {
|
|
53
53
|
// eslint-disable-next-line no-console
|
|
54
|
-
console.debug("[
|
|
54
|
+
console.debug("[AgentWidgetClient] dispatch body", body);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const response = await fetch(this.apiUrl, {
|
|
@@ -89,7 +89,7 @@ export class ChatWidgetClient {
|
|
|
89
89
|
let sequenceCounter = 0;
|
|
90
90
|
const nextSequence = () => baseSequence + sequenceCounter++;
|
|
91
91
|
|
|
92
|
-
const cloneMessage = (msg:
|
|
92
|
+
const cloneMessage = (msg: AgentWidgetMessage): AgentWidgetMessage => {
|
|
93
93
|
const reasoning = msg.reasoning
|
|
94
94
|
? {
|
|
95
95
|
...msg.reasoning,
|
|
@@ -117,16 +117,16 @@ export class ChatWidgetClient {
|
|
|
117
117
|
};
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
const emitMessage = (msg:
|
|
120
|
+
const emitMessage = (msg: AgentWidgetMessage) => {
|
|
121
121
|
onEvent({
|
|
122
122
|
type: "message",
|
|
123
123
|
message: cloneMessage(msg)
|
|
124
124
|
});
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
let assistantMessage:
|
|
128
|
-
const reasoningMessages = new Map<string,
|
|
129
|
-
const toolMessages = new Map<string,
|
|
127
|
+
let assistantMessage: AgentWidgetMessage | null = null;
|
|
128
|
+
const reasoningMessages = new Map<string, AgentWidgetMessage>();
|
|
129
|
+
const toolMessages = new Map<string, AgentWidgetMessage>();
|
|
130
130
|
const reasoningContext = {
|
|
131
131
|
lastId: null as string | null,
|
|
132
132
|
byStep: new Map<string, string>()
|
|
@@ -224,7 +224,7 @@ export class ChatWidgetClient {
|
|
|
224
224
|
return existing;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
const message:
|
|
227
|
+
const message: AgentWidgetMessage = {
|
|
228
228
|
id: `reason-${reasoningId}`,
|
|
229
229
|
role: "assistant",
|
|
230
230
|
content: "",
|
|
@@ -286,7 +286,7 @@ export class ChatWidgetClient {
|
|
|
286
286
|
return existing;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
const message:
|
|
289
|
+
const message: AgentWidgetMessage = {
|
|
290
290
|
id: `tool-${toolId}`,
|
|
291
291
|
role: "assistant",
|
|
292
292
|
content: "",
|
|
@@ -556,7 +556,7 @@ export class ChatWidgetClient {
|
|
|
556
556
|
} else {
|
|
557
557
|
const existingAssistant = assistantMessage;
|
|
558
558
|
if (existingAssistant) {
|
|
559
|
-
const assistantFinal = existingAssistant as
|
|
559
|
+
const assistantFinal = existingAssistant as AgentWidgetMessage;
|
|
560
560
|
assistantFinal.streaming = false;
|
|
561
561
|
emitMessage(assistantFinal);
|
|
562
562
|
}
|
package/src/components/forms.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
|
+
import { AgentWidgetSession } from "../session";
|
|
4
4
|
|
|
5
5
|
export const formDefinitions: Record<
|
|
6
6
|
string,
|
|
@@ -40,9 +40,9 @@ export const formDefinitions: Record<
|
|
|
40
40
|
|
|
41
41
|
export const enhanceWithForms = (
|
|
42
42
|
bubble: HTMLElement,
|
|
43
|
-
message:
|
|
44
|
-
config:
|
|
45
|
-
session:
|
|
43
|
+
message: AgentWidgetMessage,
|
|
44
|
+
config: AgentWidgetConfig,
|
|
45
|
+
session: AgentWidgetSession
|
|
46
46
|
) => {
|
|
47
47
|
const placeholders = bubble.querySelectorAll<HTMLElement>("[data-tv-form]");
|
|
48
48
|
if (placeholders.length) {
|
|
@@ -163,3 +163,5 @@ export const enhanceWithForms = (
|
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
|
|
167
|
+
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import {
|
|
2
|
+
import { AgentWidgetConfig } from "../types";
|
|
3
3
|
import { positionMap } from "../utils/positioning";
|
|
4
4
|
import { renderLucideIcon } from "../utils/icons";
|
|
5
5
|
|
|
6
6
|
export interface LauncherButton {
|
|
7
7
|
element: HTMLButtonElement;
|
|
8
|
-
update: (config:
|
|
8
|
+
update: (config: AgentWidgetConfig) => void;
|
|
9
9
|
destroy: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const createLauncherButton = (
|
|
13
|
-
config:
|
|
13
|
+
config: AgentWidgetConfig | undefined,
|
|
14
14
|
onToggle: () => void
|
|
15
15
|
): LauncherButton => {
|
|
16
16
|
const button = createElement("button") as HTMLButtonElement;
|
|
@@ -26,7 +26,7 @@ export const createLauncherButton = (
|
|
|
26
26
|
`;
|
|
27
27
|
button.addEventListener("click", onToggle);
|
|
28
28
|
|
|
29
|
-
const update = (newConfig:
|
|
29
|
+
const update = (newConfig: AgentWidgetConfig) => {
|
|
30
30
|
const launcher = newConfig.launcher ?? {};
|
|
31
31
|
|
|
32
32
|
const titleEl = button.querySelector("[data-role='launcher-title']");
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import {
|
|
2
|
+
import { AgentWidgetMessage } from "../types";
|
|
3
3
|
|
|
4
4
|
export type MessageTransform = (context: {
|
|
5
5
|
text: string;
|
|
6
|
-
message:
|
|
6
|
+
message: AgentWidgetMessage;
|
|
7
7
|
streaming: boolean;
|
|
8
8
|
}) => string;
|
|
9
9
|
|
|
10
10
|
export const createStandardBubble = (
|
|
11
|
-
message:
|
|
11
|
+
message: AgentWidgetMessage,
|
|
12
12
|
transform: MessageTransform
|
|
13
13
|
): HTMLElement => {
|
|
14
14
|
const classes = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement, createFragment } from "../utils/dom";
|
|
2
|
-
import {
|
|
2
|
+
import { AgentWidgetMessage } from "../types";
|
|
3
3
|
import { MessageTransform } from "./message-bubble";
|
|
4
4
|
import { createStandardBubble } from "./message-bubble";
|
|
5
5
|
import { createReasoningBubble } from "./reasoning-bubble";
|
|
@@ -7,7 +7,7 @@ import { createToolBubble } from "./tool-bubble";
|
|
|
7
7
|
|
|
8
8
|
export const renderMessages = (
|
|
9
9
|
container: HTMLElement,
|
|
10
|
-
messages:
|
|
10
|
+
messages: AgentWidgetMessage[],
|
|
11
11
|
transform: MessageTransform,
|
|
12
12
|
showReasoning: boolean,
|
|
13
13
|
showToolCalls: boolean
|
|
@@ -41,3 +41,5 @@ export const renderMessages = (
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
|
|
45
|
+
|
package/src/components/panel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
2
|
import { renderLucideIcon } from "../utils/icons";
|
|
3
|
-
import {
|
|
3
|
+
import { AgentWidgetConfig } from "../types";
|
|
4
4
|
import { positionMap } from "../utils/positioning";
|
|
5
5
|
|
|
6
6
|
export interface PanelWrapper {
|
|
@@ -8,7 +8,7 @@ export interface PanelWrapper {
|
|
|
8
8
|
panel: HTMLElement;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const createWrapper = (config?:
|
|
11
|
+
export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
12
12
|
const launcherEnabled = config?.launcher?.enabled ?? true;
|
|
13
13
|
|
|
14
14
|
if (!launcherEnabled) {
|
|
@@ -40,7 +40,7 @@ export const createWrapper = (config?: ChatWidgetConfig): PanelWrapper => {
|
|
|
40
40
|
"tvw-relative tvw-min-h-[320px]"
|
|
41
41
|
);
|
|
42
42
|
const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
|
|
43
|
-
const width = launcherWidth ?? "min(
|
|
43
|
+
const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
|
|
44
44
|
panel.style.width = width;
|
|
45
45
|
panel.style.maxWidth = width;
|
|
46
46
|
|
|
@@ -63,10 +63,13 @@ export interface PanelElements {
|
|
|
63
63
|
introTitle: HTMLElement;
|
|
64
64
|
introSubtitle: HTMLElement;
|
|
65
65
|
closeButton: HTMLButtonElement;
|
|
66
|
+
closeButtonWrapper: HTMLElement;
|
|
67
|
+
clearChatButton: HTMLButtonElement | null;
|
|
68
|
+
clearChatButtonWrapper: HTMLElement | null;
|
|
66
69
|
iconHolder: HTMLElement;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
export const buildPanel = (config?:
|
|
72
|
+
export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelElements => {
|
|
70
73
|
const container = createElement(
|
|
71
74
|
"div",
|
|
72
75
|
"tvw-flex tvw-h-full tvw-w-full tvw-flex-col tvw-bg-cw-surface tvw-text-cw-primary tvw-rounded-2xl tvw-overflow-hidden tvw-shadow-2xl tvw-border tvw-border-cw-border"
|
|
@@ -141,19 +144,184 @@ export const buildPanel = (config?: ChatWidgetConfig, showClose = true): PanelEl
|
|
|
141
144
|
header.append(headerCopy);
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
// Create clear chat button if enabled
|
|
148
|
+
const clearChatConfig = launcher.clearChat ?? {};
|
|
149
|
+
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
150
|
+
let clearChatButton: HTMLButtonElement | null = null;
|
|
151
|
+
let clearChatButtonWrapper: HTMLElement | null = null;
|
|
152
|
+
|
|
153
|
+
if (clearChatEnabled) {
|
|
154
|
+
const clearChatSize = clearChatConfig.size ?? "32px";
|
|
155
|
+
const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
|
|
156
|
+
const clearChatIconColor = clearChatConfig.iconColor ?? "";
|
|
157
|
+
const clearChatBgColor = clearChatConfig.backgroundColor ?? "";
|
|
158
|
+
const clearChatBorderWidth = clearChatConfig.borderWidth ?? "";
|
|
159
|
+
const clearChatBorderColor = clearChatConfig.borderColor ?? "";
|
|
160
|
+
const clearChatBorderRadius = clearChatConfig.borderRadius ?? "";
|
|
161
|
+
const clearChatPaddingX = clearChatConfig.paddingX ?? "";
|
|
162
|
+
const clearChatPaddingY = clearChatConfig.paddingY ?? "";
|
|
163
|
+
const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
|
|
164
|
+
const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
|
|
165
|
+
|
|
166
|
+
// Create button wrapper for tooltip
|
|
167
|
+
clearChatButtonWrapper = createElement(
|
|
168
|
+
"div",
|
|
169
|
+
"tvw-relative tvw-ml-auto tvw-clear-chat-button-wrapper"
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
clearChatButton = createElement(
|
|
173
|
+
"button",
|
|
174
|
+
"tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none"
|
|
175
|
+
) as HTMLButtonElement;
|
|
176
|
+
|
|
177
|
+
clearChatButton.style.height = clearChatSize;
|
|
178
|
+
clearChatButton.style.width = clearChatSize;
|
|
179
|
+
clearChatButton.type = "button";
|
|
180
|
+
clearChatButton.setAttribute("aria-label", clearChatTooltipText);
|
|
181
|
+
|
|
182
|
+
// Add icon
|
|
183
|
+
const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
|
|
184
|
+
if (iconSvg) {
|
|
185
|
+
clearChatButton.appendChild(iconSvg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Apply styling from config
|
|
189
|
+
if (clearChatIconColor) {
|
|
190
|
+
clearChatButton.style.color = clearChatIconColor;
|
|
191
|
+
clearChatButton.classList.remove("tvw-text-cw-muted");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (clearChatBgColor) {
|
|
195
|
+
clearChatButton.style.backgroundColor = clearChatBgColor;
|
|
196
|
+
clearChatButton.classList.remove("hover:tvw-bg-gray-100");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (clearChatBorderWidth || clearChatBorderColor) {
|
|
200
|
+
const borderWidth = clearChatBorderWidth || "0px";
|
|
201
|
+
const borderColor = clearChatBorderColor || "transparent";
|
|
202
|
+
clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
|
|
203
|
+
clearChatButton.classList.remove("tvw-border-none");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (clearChatBorderRadius) {
|
|
207
|
+
clearChatButton.style.borderRadius = clearChatBorderRadius;
|
|
208
|
+
clearChatButton.classList.remove("tvw-rounded-full");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Apply padding styling
|
|
212
|
+
if (clearChatPaddingX) {
|
|
213
|
+
clearChatButton.style.paddingLeft = clearChatPaddingX;
|
|
214
|
+
clearChatButton.style.paddingRight = clearChatPaddingX;
|
|
215
|
+
} else {
|
|
216
|
+
clearChatButton.style.paddingLeft = "";
|
|
217
|
+
clearChatButton.style.paddingRight = "";
|
|
218
|
+
}
|
|
219
|
+
if (clearChatPaddingY) {
|
|
220
|
+
clearChatButton.style.paddingTop = clearChatPaddingY;
|
|
221
|
+
clearChatButton.style.paddingBottom = clearChatPaddingY;
|
|
222
|
+
} else {
|
|
223
|
+
clearChatButton.style.paddingTop = "";
|
|
224
|
+
clearChatButton.style.paddingBottom = "";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
clearChatButtonWrapper.appendChild(clearChatButton);
|
|
228
|
+
|
|
229
|
+
// Add tooltip with portaling to document.body to escape overflow clipping
|
|
230
|
+
if (clearChatShowTooltip && clearChatTooltipText && clearChatButton && clearChatButtonWrapper) {
|
|
231
|
+
let portaledTooltip: HTMLElement | null = null;
|
|
232
|
+
|
|
233
|
+
const showTooltip = () => {
|
|
234
|
+
if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
|
|
235
|
+
|
|
236
|
+
// Create tooltip element
|
|
237
|
+
portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
|
|
238
|
+
portaledTooltip.textContent = clearChatTooltipText;
|
|
239
|
+
|
|
240
|
+
// Add arrow
|
|
241
|
+
const arrow = createElement("div");
|
|
242
|
+
arrow.className = "tvw-clear-chat-tooltip-arrow";
|
|
243
|
+
portaledTooltip.appendChild(arrow);
|
|
244
|
+
|
|
245
|
+
// Get button position
|
|
246
|
+
const buttonRect = clearChatButton.getBoundingClientRect();
|
|
247
|
+
|
|
248
|
+
// Position tooltip above button
|
|
249
|
+
portaledTooltip.style.position = "fixed";
|
|
250
|
+
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
251
|
+
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
252
|
+
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
253
|
+
|
|
254
|
+
// Append to body
|
|
255
|
+
document.body.appendChild(portaledTooltip);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const hideTooltip = () => {
|
|
259
|
+
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
260
|
+
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
261
|
+
portaledTooltip = null;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Add event listeners
|
|
266
|
+
clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
|
|
267
|
+
clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
|
|
268
|
+
clearChatButton.addEventListener("focus", showTooltip);
|
|
269
|
+
clearChatButton.addEventListener("blur", hideTooltip);
|
|
270
|
+
|
|
271
|
+
// Store cleanup function on the button for later use
|
|
272
|
+
(clearChatButtonWrapper as any)._cleanupTooltip = () => {
|
|
273
|
+
hideTooltip();
|
|
274
|
+
if (clearChatButtonWrapper) {
|
|
275
|
+
clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
|
|
276
|
+
clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
|
|
277
|
+
}
|
|
278
|
+
if (clearChatButton) {
|
|
279
|
+
clearChatButton.removeEventListener("focus", showTooltip);
|
|
280
|
+
clearChatButton.removeEventListener("blur", hideTooltip);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
header.appendChild(clearChatButtonWrapper);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Create close button wrapper for tooltip positioning
|
|
289
|
+
const closeButtonWrapper = createElement(
|
|
290
|
+
"div",
|
|
291
|
+
closeButtonPlacement === "top-right"
|
|
292
|
+
? "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50"
|
|
293
|
+
: (clearChatEnabled
|
|
294
|
+
? ""
|
|
295
|
+
: "tvw-ml-auto")
|
|
296
|
+
);
|
|
297
|
+
|
|
144
298
|
// Create close button with base classes
|
|
145
299
|
const closeButton = createElement(
|
|
146
300
|
"button",
|
|
147
|
-
|
|
148
|
-
? "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50 tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none"
|
|
149
|
-
: "tvw-ml-auto tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none"
|
|
301
|
+
"tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-text-cw-muted hover:tvw-bg-gray-100 tvw-cursor-pointer tvw-border-none"
|
|
150
302
|
) as HTMLButtonElement;
|
|
151
303
|
closeButton.style.height = closeButtonSize;
|
|
152
304
|
closeButton.style.width = closeButtonSize;
|
|
153
305
|
closeButton.type = "button";
|
|
154
|
-
|
|
155
|
-
|
|
306
|
+
|
|
307
|
+
// Get tooltip config
|
|
308
|
+
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
309
|
+
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
310
|
+
|
|
311
|
+
closeButton.setAttribute("aria-label", closeButtonTooltipText);
|
|
156
312
|
closeButton.style.display = showClose ? "" : "none";
|
|
313
|
+
|
|
314
|
+
// Add icon or fallback text
|
|
315
|
+
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
316
|
+
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
317
|
+
|
|
318
|
+
// Try to render Lucide icon, fallback to text if not provided or fails
|
|
319
|
+
const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
|
|
320
|
+
if (iconSvg) {
|
|
321
|
+
closeButton.appendChild(iconSvg);
|
|
322
|
+
} else {
|
|
323
|
+
closeButton.textContent = closeButtonIconText;
|
|
324
|
+
}
|
|
157
325
|
|
|
158
326
|
// Apply close button styling from config
|
|
159
327
|
if (launcher.closeButtonColor) {
|
|
@@ -190,15 +358,85 @@ export const buildPanel = (config?: ChatWidgetConfig, showClose = true): PanelEl
|
|
|
190
358
|
closeButton.style.borderRadius = "";
|
|
191
359
|
closeButton.classList.add("tvw-rounded-full");
|
|
192
360
|
}
|
|
193
|
-
|
|
194
|
-
//
|
|
361
|
+
|
|
362
|
+
// Apply padding styling
|
|
363
|
+
if (launcher.closeButtonPaddingX) {
|
|
364
|
+
closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
|
|
365
|
+
closeButton.style.paddingRight = launcher.closeButtonPaddingX;
|
|
366
|
+
} else {
|
|
367
|
+
closeButton.style.paddingLeft = "";
|
|
368
|
+
closeButton.style.paddingRight = "";
|
|
369
|
+
}
|
|
370
|
+
if (launcher.closeButtonPaddingY) {
|
|
371
|
+
closeButton.style.paddingTop = launcher.closeButtonPaddingY;
|
|
372
|
+
closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
|
|
373
|
+
} else {
|
|
374
|
+
closeButton.style.paddingTop = "";
|
|
375
|
+
closeButton.style.paddingBottom = "";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
closeButtonWrapper.appendChild(closeButton);
|
|
379
|
+
|
|
380
|
+
// Add tooltip with portaling to document.body to escape overflow clipping
|
|
381
|
+
if (closeButtonShowTooltip && closeButtonTooltipText) {
|
|
382
|
+
let portaledTooltip: HTMLElement | null = null;
|
|
383
|
+
|
|
384
|
+
const showTooltip = () => {
|
|
385
|
+
if (portaledTooltip) return; // Already showing
|
|
386
|
+
|
|
387
|
+
// Create tooltip element
|
|
388
|
+
portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
|
|
389
|
+
portaledTooltip.textContent = closeButtonTooltipText;
|
|
390
|
+
|
|
391
|
+
// Add arrow
|
|
392
|
+
const arrow = createElement("div");
|
|
393
|
+
arrow.className = "tvw-clear-chat-tooltip-arrow";
|
|
394
|
+
portaledTooltip.appendChild(arrow);
|
|
395
|
+
|
|
396
|
+
// Get button position
|
|
397
|
+
const buttonRect = closeButton.getBoundingClientRect();
|
|
398
|
+
|
|
399
|
+
// Position tooltip above button
|
|
400
|
+
portaledTooltip.style.position = "fixed";
|
|
401
|
+
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
402
|
+
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
403
|
+
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
404
|
+
|
|
405
|
+
// Append to body
|
|
406
|
+
document.body.appendChild(portaledTooltip);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const hideTooltip = () => {
|
|
410
|
+
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
411
|
+
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
412
|
+
portaledTooltip = null;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Add event listeners
|
|
417
|
+
closeButtonWrapper.addEventListener("mouseenter", showTooltip);
|
|
418
|
+
closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
|
|
419
|
+
closeButton.addEventListener("focus", showTooltip);
|
|
420
|
+
closeButton.addEventListener("blur", hideTooltip);
|
|
421
|
+
|
|
422
|
+
// Store cleanup function on the wrapper for later use
|
|
423
|
+
(closeButtonWrapper as any)._cleanupTooltip = () => {
|
|
424
|
+
hideTooltip();
|
|
425
|
+
closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
|
|
426
|
+
closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
|
|
427
|
+
closeButton.removeEventListener("focus", showTooltip);
|
|
428
|
+
closeButton.removeEventListener("blur", hideTooltip);
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Position close button wrapper based on placement
|
|
195
433
|
if (closeButtonPlacement === "top-right") {
|
|
196
434
|
// Make container position relative for absolute positioning
|
|
197
435
|
container.style.position = "relative";
|
|
198
|
-
container.appendChild(
|
|
436
|
+
container.appendChild(closeButtonWrapper);
|
|
199
437
|
} else {
|
|
200
438
|
// Inline placement: append to header
|
|
201
|
-
header.appendChild(
|
|
439
|
+
header.appendChild(closeButtonWrapper);
|
|
202
440
|
}
|
|
203
441
|
|
|
204
442
|
const body = createElement(
|
|
@@ -547,6 +785,9 @@ export const buildPanel = (config?: ChatWidgetConfig, showClose = true): PanelEl
|
|
|
547
785
|
introTitle,
|
|
548
786
|
introSubtitle,
|
|
549
787
|
closeButton,
|
|
788
|
+
closeButtonWrapper,
|
|
789
|
+
clearChatButton,
|
|
790
|
+
clearChatButtonWrapper,
|
|
550
791
|
iconHolder
|
|
551
792
|
};
|
|
552
793
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import {
|
|
2
|
+
import { AgentWidgetMessage } from "../types";
|
|
3
3
|
import { describeReasonStatus } from "../utils/formatting";
|
|
4
4
|
|
|
5
5
|
// Expansion state per widget instance
|
|
6
6
|
const reasoningExpansionState = new Set<string>();
|
|
7
7
|
|
|
8
|
-
export const createReasoningBubble = (message:
|
|
8
|
+
export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement => {
|
|
9
9
|
const reasoning = message.reasoning;
|
|
10
10
|
const bubble = createElement(
|
|
11
11
|
"div",
|