vanilla-agent 0.2.0 → 1.1.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 +73 -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 +43 -4
- 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 +73 -0
- package/src/types.ts +56 -31
- package/src/ui.ts +338 -53
- 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/src/ui.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { escapeHtml } from "./postprocessors";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { AgentWidgetSession, AgentWidgetSessionStatus } from "./session";
|
|
3
|
+
import { AgentWidgetConfig, AgentWidgetMessage } from "./types";
|
|
4
4
|
import { applyThemeVariables } from "./utils/theme";
|
|
5
5
|
import { renderLucideIcon } from "./utils/icons";
|
|
6
6
|
import { createElement } from "./utils/dom";
|
|
@@ -8,22 +8,24 @@ import { statusCopy } from "./utils/constants";
|
|
|
8
8
|
import { createLauncherButton } from "./components/launcher";
|
|
9
9
|
import { createWrapper, buildPanel } from "./components/panel";
|
|
10
10
|
import { MessageTransform } from "./components/message-bubble";
|
|
11
|
-
import { createStandardBubble } from "./components/message-bubble";
|
|
11
|
+
import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
|
|
12
12
|
import { createReasoningBubble } from "./components/reasoning-bubble";
|
|
13
13
|
import { createToolBubble } from "./components/tool-bubble";
|
|
14
14
|
import { createSuggestions } from "./components/suggestions";
|
|
15
15
|
import { enhanceWithForms } from "./components/forms";
|
|
16
16
|
import { pluginRegistry } from "./plugins/registry";
|
|
17
|
+
import { mergeWithDefaults } from "./defaults";
|
|
17
18
|
|
|
18
19
|
type Controller = {
|
|
19
|
-
update: (config:
|
|
20
|
+
update: (config: AgentWidgetConfig) => void;
|
|
20
21
|
destroy: () => void;
|
|
21
22
|
open: () => void;
|
|
22
23
|
close: () => void;
|
|
23
24
|
toggle: () => void;
|
|
25
|
+
clearChat: () => void;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
const buildPostprocessor = (cfg?:
|
|
28
|
+
const buildPostprocessor = (cfg?: AgentWidgetConfig): MessageTransform => {
|
|
27
29
|
if (cfg?.postprocessMessage) {
|
|
28
30
|
return (context) =>
|
|
29
31
|
cfg.postprocessMessage!({
|
|
@@ -35,16 +37,16 @@ const buildPostprocessor = (cfg?: ChatWidgetConfig): MessageTransform => {
|
|
|
35
37
|
return ({ text }) => escapeHtml(text);
|
|
36
38
|
};
|
|
37
39
|
|
|
38
|
-
export const
|
|
40
|
+
export const createAgentExperience = (
|
|
39
41
|
mount: HTMLElement,
|
|
40
|
-
initialConfig?:
|
|
42
|
+
initialConfig?: AgentWidgetConfig
|
|
41
43
|
): Controller => {
|
|
42
44
|
// Tailwind config uses important: "#vanilla-agent-root", so ensure mount has this ID
|
|
43
45
|
if (!mount.id || mount.id !== "vanilla-agent-root") {
|
|
44
46
|
mount.id = "vanilla-agent-root";
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
let config =
|
|
49
|
+
let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
|
|
48
50
|
applyThemeVariables(mount, config);
|
|
49
51
|
|
|
50
52
|
// Get plugins for this instance
|
|
@@ -61,7 +63,7 @@ export const createChatExperience = (
|
|
|
61
63
|
|
|
62
64
|
// Get status indicator config
|
|
63
65
|
const statusConfig = config.statusIndicator ?? {};
|
|
64
|
-
const getStatusText = (status:
|
|
66
|
+
const getStatusText = (status: AgentWidgetSessionStatus): string => {
|
|
65
67
|
if (status === "idle") return statusConfig.idleText ?? statusCopy.idle;
|
|
66
68
|
if (status === "connecting") return statusConfig.connectingText ?? statusCopy.connecting;
|
|
67
69
|
if (status === "connected") return statusConfig.connectedText ?? statusCopy.connected;
|
|
@@ -97,7 +99,7 @@ export const createChatExperience = (
|
|
|
97
99
|
const destroyCallbacks: Array<() => void> = [];
|
|
98
100
|
const suggestionsManager = createSuggestions(suggestions);
|
|
99
101
|
let closeHandler: (() => void) | null = null;
|
|
100
|
-
let session:
|
|
102
|
+
let session: AgentWidgetSession;
|
|
101
103
|
let isStreaming = false;
|
|
102
104
|
let shouldAutoScroll = true;
|
|
103
105
|
let lastScrollTop = 0;
|
|
@@ -146,39 +148,11 @@ export const createChatExperience = (
|
|
|
146
148
|
});
|
|
147
149
|
};
|
|
148
150
|
|
|
149
|
-
// Create typing indicator element
|
|
150
|
-
const createTypingIndicator = (): HTMLElement => {
|
|
151
|
-
const container = document.createElement("div");
|
|
152
|
-
container.className = "tvw-flex tvw-items-center tvw-space-x-1 tvw-h-5";
|
|
153
|
-
|
|
154
|
-
const dot1 = document.createElement("div");
|
|
155
|
-
dot1.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
|
|
156
|
-
dot1.style.animationDelay = "0ms";
|
|
157
|
-
|
|
158
|
-
const dot2 = document.createElement("div");
|
|
159
|
-
dot2.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
|
|
160
|
-
dot2.style.animationDelay = "250ms";
|
|
161
|
-
|
|
162
|
-
const dot3 = document.createElement("div");
|
|
163
|
-
dot3.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
|
|
164
|
-
dot3.style.animationDelay = "500ms";
|
|
165
|
-
|
|
166
|
-
const srOnly = document.createElement("span");
|
|
167
|
-
srOnly.className = "tvw-sr-only";
|
|
168
|
-
srOnly.textContent = "Loading";
|
|
169
|
-
|
|
170
|
-
container.appendChild(dot1);
|
|
171
|
-
container.appendChild(dot2);
|
|
172
|
-
container.appendChild(dot3);
|
|
173
|
-
container.appendChild(srOnly);
|
|
174
|
-
|
|
175
|
-
return container;
|
|
176
|
-
};
|
|
177
151
|
|
|
178
152
|
// Message rendering with plugin support
|
|
179
153
|
const renderMessagesWithPlugins = (
|
|
180
154
|
container: HTMLElement,
|
|
181
|
-
messages:
|
|
155
|
+
messages: AgentWidgetMessage[],
|
|
182
156
|
transform: MessageTransform
|
|
183
157
|
) => {
|
|
184
158
|
container.innerHTML = "";
|
|
@@ -256,12 +230,36 @@ export const createChatExperience = (
|
|
|
256
230
|
fragment.appendChild(wrapper);
|
|
257
231
|
});
|
|
258
232
|
|
|
259
|
-
// Add typing indicator if streaming
|
|
260
|
-
|
|
233
|
+
// Add standalone typing indicator only if streaming but no assistant message is streaming yet
|
|
234
|
+
// (This shows while waiting for the stream to start)
|
|
235
|
+
const hasStreamingAssistantMessage = messages.some(
|
|
236
|
+
(msg) => msg.role === "assistant" && msg.streaming && msg.content && msg.content.trim()
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage) {
|
|
261
240
|
const typingIndicator = createTypingIndicator();
|
|
241
|
+
|
|
242
|
+
// Create a bubble wrapper for the typing indicator (similar to assistant messages)
|
|
243
|
+
const typingBubble = document.createElement("div");
|
|
244
|
+
typingBubble.className = [
|
|
245
|
+
"tvw-max-w-[85%]",
|
|
246
|
+
"tvw-rounded-2xl",
|
|
247
|
+
"tvw-text-sm",
|
|
248
|
+
"tvw-leading-relaxed",
|
|
249
|
+
"tvw-shadow-sm",
|
|
250
|
+
"tvw-bg-cw-surface",
|
|
251
|
+
"tvw-border",
|
|
252
|
+
"tvw-border-cw-message-border",
|
|
253
|
+
"tvw-text-cw-primary",
|
|
254
|
+
"tvw-px-5",
|
|
255
|
+
"tvw-py-3"
|
|
256
|
+
].join(" ");
|
|
257
|
+
|
|
258
|
+
typingBubble.appendChild(typingIndicator);
|
|
259
|
+
|
|
262
260
|
const typingWrapper = document.createElement("div");
|
|
263
|
-
typingWrapper.className = "tvw-flex
|
|
264
|
-
typingWrapper.appendChild(
|
|
261
|
+
typingWrapper.className = "tvw-flex";
|
|
262
|
+
typingWrapper.appendChild(typingBubble);
|
|
265
263
|
fragment.appendChild(typingWrapper);
|
|
266
264
|
}
|
|
267
265
|
|
|
@@ -317,9 +315,14 @@ export const createChatExperience = (
|
|
|
317
315
|
introSubtitle.textContent =
|
|
318
316
|
config.copy?.welcomeSubtitle ??
|
|
319
317
|
"Ask anything about your account or products.";
|
|
320
|
-
textarea.placeholder = config.copy?.inputPlaceholder ?? "
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
textarea.placeholder = config.copy?.inputPlaceholder ?? "How can I help...";
|
|
319
|
+
|
|
320
|
+
// Only update send button text if NOT using icon mode
|
|
321
|
+
const useIcon = config.sendButton?.useIcon ?? false;
|
|
322
|
+
if (!useIcon) {
|
|
323
|
+
sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
|
|
324
|
+
}
|
|
325
|
+
|
|
323
326
|
// Update textarea font family and weight
|
|
324
327
|
const fontFamily = config.theme?.inputFontFamily ?? "sans-serif";
|
|
325
328
|
const fontWeight = config.theme?.inputFontWeight ?? "400";
|
|
@@ -340,7 +343,7 @@ export const createChatExperience = (
|
|
|
340
343
|
textarea.style.fontWeight = fontWeight;
|
|
341
344
|
};
|
|
342
345
|
|
|
343
|
-
session = new
|
|
346
|
+
session = new AgentWidgetSession(config, {
|
|
344
347
|
onMessagesChanged(messages) {
|
|
345
348
|
renderMessagesWithPlugins(messagesWrapper, messages, postprocess);
|
|
346
349
|
// Re-render suggestions to hide them after first user message
|
|
@@ -359,7 +362,7 @@ export const createChatExperience = (
|
|
|
359
362
|
},
|
|
360
363
|
onStatusChanged(status) {
|
|
361
364
|
const currentStatusConfig = config.statusIndicator ?? {};
|
|
362
|
-
const getCurrentStatusText = (status:
|
|
365
|
+
const getCurrentStatusText = (status: AgentWidgetSessionStatus): string => {
|
|
363
366
|
if (status === "idle") return currentStatusConfig.idleText ?? statusCopy.idle;
|
|
364
367
|
if (status === "connecting") return currentStatusConfig.connectingText ?? statusCopy.connecting;
|
|
365
368
|
if (status === "connected") return currentStatusConfig.connectedText ?? statusCopy.connected;
|
|
@@ -568,7 +571,7 @@ export const createChatExperience = (
|
|
|
568
571
|
};
|
|
569
572
|
|
|
570
573
|
// Function to create mic button dynamically
|
|
571
|
-
const createMicButton = (voiceConfig:
|
|
574
|
+
const createMicButton = (voiceConfig: AgentWidgetConfig['voiceRecognition'], sendButtonConfig: AgentWidgetConfig['sendButton']): { micButton: HTMLButtonElement; micButtonWrapper: HTMLElement } | null => {
|
|
572
575
|
const hasSpeechRecognition =
|
|
573
576
|
typeof window !== 'undefined' &&
|
|
574
577
|
(typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
|
|
@@ -711,7 +714,7 @@ export const createChatExperience = (
|
|
|
711
714
|
return;
|
|
712
715
|
}
|
|
713
716
|
const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
|
|
714
|
-
const width = launcherWidth ?? "min(
|
|
717
|
+
const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
|
|
715
718
|
panel.style.width = width;
|
|
716
719
|
panel.style.maxWidth = width;
|
|
717
720
|
const viewportHeight = window.innerHeight;
|
|
@@ -777,6 +780,25 @@ export const createChatExperience = (
|
|
|
777
780
|
|
|
778
781
|
refreshCloseButton();
|
|
779
782
|
|
|
783
|
+
// Setup clear chat button click handler
|
|
784
|
+
const setupClearChatButton = () => {
|
|
785
|
+
const { clearChatButton } = panelElements;
|
|
786
|
+
if (!clearChatButton) return;
|
|
787
|
+
|
|
788
|
+
clearChatButton.addEventListener("click", () => {
|
|
789
|
+
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
790
|
+
session.clearMessages();
|
|
791
|
+
|
|
792
|
+
// Dispatch custom event for external handlers (e.g., localStorage clearing in examples)
|
|
793
|
+
const clearEvent = new CustomEvent("vanilla-agent:clear-chat", {
|
|
794
|
+
detail: { timestamp: new Date().toISOString() }
|
|
795
|
+
});
|
|
796
|
+
window.dispatchEvent(clearEvent);
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
setupClearChatButton();
|
|
801
|
+
|
|
780
802
|
composerForm.addEventListener("submit", handleSubmit);
|
|
781
803
|
textarea.addEventListener("keydown", handleInputEnter);
|
|
782
804
|
|
|
@@ -796,7 +818,7 @@ export const createChatExperience = (
|
|
|
796
818
|
}
|
|
797
819
|
|
|
798
820
|
return {
|
|
799
|
-
update(nextConfig:
|
|
821
|
+
update(nextConfig: AgentWidgetConfig) {
|
|
800
822
|
config = { ...config, ...nextConfig };
|
|
801
823
|
applyThemeVariables(mount, config);
|
|
802
824
|
|
|
@@ -995,6 +1017,259 @@ export const createChatExperience = (
|
|
|
995
1017
|
closeButton.style.borderRadius = "";
|
|
996
1018
|
closeButton.classList.add("tvw-rounded-full");
|
|
997
1019
|
}
|
|
1020
|
+
|
|
1021
|
+
// Update padding
|
|
1022
|
+
if (launcher.closeButtonPaddingX) {
|
|
1023
|
+
closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
|
|
1024
|
+
closeButton.style.paddingRight = launcher.closeButtonPaddingX;
|
|
1025
|
+
} else {
|
|
1026
|
+
closeButton.style.paddingLeft = "";
|
|
1027
|
+
closeButton.style.paddingRight = "";
|
|
1028
|
+
}
|
|
1029
|
+
if (launcher.closeButtonPaddingY) {
|
|
1030
|
+
closeButton.style.paddingTop = launcher.closeButtonPaddingY;
|
|
1031
|
+
closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
|
|
1032
|
+
} else {
|
|
1033
|
+
closeButton.style.paddingTop = "";
|
|
1034
|
+
closeButton.style.paddingBottom = "";
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Update icon
|
|
1038
|
+
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
1039
|
+
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
1040
|
+
|
|
1041
|
+
// Clear existing content and render new icon
|
|
1042
|
+
closeButton.innerHTML = "";
|
|
1043
|
+
const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
|
|
1044
|
+
if (iconSvg) {
|
|
1045
|
+
closeButton.appendChild(iconSvg);
|
|
1046
|
+
} else {
|
|
1047
|
+
closeButton.textContent = closeButtonIconText;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Update tooltip
|
|
1051
|
+
const { closeButtonWrapper } = panelElements;
|
|
1052
|
+
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
1053
|
+
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
1054
|
+
|
|
1055
|
+
closeButton.setAttribute("aria-label", closeButtonTooltipText);
|
|
1056
|
+
|
|
1057
|
+
if (closeButtonWrapper) {
|
|
1058
|
+
// Clean up old tooltip event listeners if they exist
|
|
1059
|
+
if ((closeButtonWrapper as any)._cleanupTooltip) {
|
|
1060
|
+
(closeButtonWrapper as any)._cleanupTooltip();
|
|
1061
|
+
delete (closeButtonWrapper as any)._cleanupTooltip;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Set up new portaled tooltip with event listeners
|
|
1065
|
+
if (closeButtonShowTooltip && closeButtonTooltipText) {
|
|
1066
|
+
let portaledTooltip: HTMLElement | null = null;
|
|
1067
|
+
|
|
1068
|
+
const showTooltip = () => {
|
|
1069
|
+
if (portaledTooltip || !closeButton) return; // Already showing or button doesn't exist
|
|
1070
|
+
|
|
1071
|
+
// Create tooltip element
|
|
1072
|
+
portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
|
|
1073
|
+
portaledTooltip.textContent = closeButtonTooltipText;
|
|
1074
|
+
|
|
1075
|
+
// Add arrow
|
|
1076
|
+
const arrow = createElement("div");
|
|
1077
|
+
arrow.className = "tvw-clear-chat-tooltip-arrow";
|
|
1078
|
+
portaledTooltip.appendChild(arrow);
|
|
1079
|
+
|
|
1080
|
+
// Get button position
|
|
1081
|
+
const buttonRect = closeButton.getBoundingClientRect();
|
|
1082
|
+
|
|
1083
|
+
// Position tooltip above button
|
|
1084
|
+
portaledTooltip.style.position = "fixed";
|
|
1085
|
+
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
1086
|
+
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
1087
|
+
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
1088
|
+
|
|
1089
|
+
// Append to body
|
|
1090
|
+
document.body.appendChild(portaledTooltip);
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const hideTooltip = () => {
|
|
1094
|
+
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
1095
|
+
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
1096
|
+
portaledTooltip = null;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// Add event listeners
|
|
1101
|
+
closeButtonWrapper.addEventListener("mouseenter", showTooltip);
|
|
1102
|
+
closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
|
|
1103
|
+
closeButton.addEventListener("focus", showTooltip);
|
|
1104
|
+
closeButton.addEventListener("blur", hideTooltip);
|
|
1105
|
+
|
|
1106
|
+
// Store cleanup function on the wrapper for later use
|
|
1107
|
+
(closeButtonWrapper as any)._cleanupTooltip = () => {
|
|
1108
|
+
hideTooltip();
|
|
1109
|
+
if (closeButtonWrapper) {
|
|
1110
|
+
closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
|
|
1111
|
+
closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
|
|
1112
|
+
}
|
|
1113
|
+
if (closeButton) {
|
|
1114
|
+
closeButton.removeEventListener("focus", showTooltip);
|
|
1115
|
+
closeButton.removeEventListener("blur", hideTooltip);
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Update clear chat button styling from config
|
|
1123
|
+
const { clearChatButton, clearChatButtonWrapper } = panelElements;
|
|
1124
|
+
if (clearChatButton) {
|
|
1125
|
+
const clearChatConfig = launcher.clearChat ?? {};
|
|
1126
|
+
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
1127
|
+
|
|
1128
|
+
// Show/hide button based on enabled state
|
|
1129
|
+
if (clearChatButtonWrapper) {
|
|
1130
|
+
clearChatButtonWrapper.style.display = clearChatEnabled ? "" : "none";
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (clearChatEnabled) {
|
|
1134
|
+
// Update size
|
|
1135
|
+
const clearChatSize = clearChatConfig.size ?? "32px";
|
|
1136
|
+
clearChatButton.style.height = clearChatSize;
|
|
1137
|
+
clearChatButton.style.width = clearChatSize;
|
|
1138
|
+
|
|
1139
|
+
// Update icon
|
|
1140
|
+
const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
|
|
1141
|
+
const clearChatIconColor = clearChatConfig.iconColor ?? "";
|
|
1142
|
+
|
|
1143
|
+
// Clear existing icon and render new one
|
|
1144
|
+
clearChatButton.innerHTML = "";
|
|
1145
|
+
const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
|
|
1146
|
+
if (iconSvg) {
|
|
1147
|
+
clearChatButton.appendChild(iconSvg);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Update icon color
|
|
1151
|
+
if (clearChatIconColor) {
|
|
1152
|
+
clearChatButton.style.color = clearChatIconColor;
|
|
1153
|
+
clearChatButton.classList.remove("tvw-text-cw-muted");
|
|
1154
|
+
} else {
|
|
1155
|
+
clearChatButton.style.color = "";
|
|
1156
|
+
clearChatButton.classList.add("tvw-text-cw-muted");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Update background color
|
|
1160
|
+
if (clearChatConfig.backgroundColor) {
|
|
1161
|
+
clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;
|
|
1162
|
+
clearChatButton.classList.remove("hover:tvw-bg-gray-100");
|
|
1163
|
+
} else {
|
|
1164
|
+
clearChatButton.style.backgroundColor = "";
|
|
1165
|
+
clearChatButton.classList.add("hover:tvw-bg-gray-100");
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Update border
|
|
1169
|
+
if (clearChatConfig.borderWidth || clearChatConfig.borderColor) {
|
|
1170
|
+
const borderWidth = clearChatConfig.borderWidth || "0px";
|
|
1171
|
+
const borderColor = clearChatConfig.borderColor || "transparent";
|
|
1172
|
+
clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
|
|
1173
|
+
clearChatButton.classList.remove("tvw-border-none");
|
|
1174
|
+
} else {
|
|
1175
|
+
clearChatButton.style.border = "";
|
|
1176
|
+
clearChatButton.classList.add("tvw-border-none");
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Update border radius
|
|
1180
|
+
if (clearChatConfig.borderRadius) {
|
|
1181
|
+
clearChatButton.style.borderRadius = clearChatConfig.borderRadius;
|
|
1182
|
+
clearChatButton.classList.remove("tvw-rounded-full");
|
|
1183
|
+
} else {
|
|
1184
|
+
clearChatButton.style.borderRadius = "";
|
|
1185
|
+
clearChatButton.classList.add("tvw-rounded-full");
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Update padding
|
|
1189
|
+
if (clearChatConfig.paddingX) {
|
|
1190
|
+
clearChatButton.style.paddingLeft = clearChatConfig.paddingX;
|
|
1191
|
+
clearChatButton.style.paddingRight = clearChatConfig.paddingX;
|
|
1192
|
+
} else {
|
|
1193
|
+
clearChatButton.style.paddingLeft = "";
|
|
1194
|
+
clearChatButton.style.paddingRight = "";
|
|
1195
|
+
}
|
|
1196
|
+
if (clearChatConfig.paddingY) {
|
|
1197
|
+
clearChatButton.style.paddingTop = clearChatConfig.paddingY;
|
|
1198
|
+
clearChatButton.style.paddingBottom = clearChatConfig.paddingY;
|
|
1199
|
+
} else {
|
|
1200
|
+
clearChatButton.style.paddingTop = "";
|
|
1201
|
+
clearChatButton.style.paddingBottom = "";
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
|
|
1205
|
+
const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
|
|
1206
|
+
|
|
1207
|
+
clearChatButton.setAttribute("aria-label", clearChatTooltipText);
|
|
1208
|
+
|
|
1209
|
+
if (clearChatButtonWrapper) {
|
|
1210
|
+
// Clean up old tooltip event listeners if they exist
|
|
1211
|
+
if ((clearChatButtonWrapper as any)._cleanupTooltip) {
|
|
1212
|
+
(clearChatButtonWrapper as any)._cleanupTooltip();
|
|
1213
|
+
delete (clearChatButtonWrapper as any)._cleanupTooltip;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Set up new portaled tooltip with event listeners
|
|
1217
|
+
if (clearChatShowTooltip && clearChatTooltipText) {
|
|
1218
|
+
let portaledTooltip: HTMLElement | null = null;
|
|
1219
|
+
|
|
1220
|
+
const showTooltip = () => {
|
|
1221
|
+
if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
|
|
1222
|
+
|
|
1223
|
+
// Create tooltip element
|
|
1224
|
+
portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
|
|
1225
|
+
portaledTooltip.textContent = clearChatTooltipText;
|
|
1226
|
+
|
|
1227
|
+
// Add arrow
|
|
1228
|
+
const arrow = createElement("div");
|
|
1229
|
+
arrow.className = "tvw-clear-chat-tooltip-arrow";
|
|
1230
|
+
portaledTooltip.appendChild(arrow);
|
|
1231
|
+
|
|
1232
|
+
// Get button position
|
|
1233
|
+
const buttonRect = clearChatButton.getBoundingClientRect();
|
|
1234
|
+
|
|
1235
|
+
// Position tooltip above button
|
|
1236
|
+
portaledTooltip.style.position = "fixed";
|
|
1237
|
+
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
1238
|
+
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
1239
|
+
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
1240
|
+
|
|
1241
|
+
// Append to body
|
|
1242
|
+
document.body.appendChild(portaledTooltip);
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
const hideTooltip = () => {
|
|
1246
|
+
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
1247
|
+
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
1248
|
+
portaledTooltip = null;
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// Add event listeners
|
|
1253
|
+
clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
|
|
1254
|
+
clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
|
|
1255
|
+
clearChatButton.addEventListener("focus", showTooltip);
|
|
1256
|
+
clearChatButton.addEventListener("blur", hideTooltip);
|
|
1257
|
+
|
|
1258
|
+
// Store cleanup function on the button for later use
|
|
1259
|
+
(clearChatButtonWrapper as any)._cleanupTooltip = () => {
|
|
1260
|
+
hideTooltip();
|
|
1261
|
+
if (clearChatButtonWrapper) {
|
|
1262
|
+
clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
|
|
1263
|
+
clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
|
|
1264
|
+
}
|
|
1265
|
+
if (clearChatButton) {
|
|
1266
|
+
clearChatButton.removeEventListener("focus", showTooltip);
|
|
1267
|
+
clearChatButton.removeEventListener("blur", hideTooltip);
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
998
1273
|
}
|
|
999
1274
|
|
|
1000
1275
|
postprocess = buildPostprocessor(config);
|
|
@@ -1289,7 +1564,7 @@ export const createChatExperience = (
|
|
|
1289
1564
|
// Update status text if status is currently set
|
|
1290
1565
|
if (session) {
|
|
1291
1566
|
const currentStatus = session.getStatus();
|
|
1292
|
-
const getCurrentStatusText = (status:
|
|
1567
|
+
const getCurrentStatusText = (status: AgentWidgetSessionStatus): string => {
|
|
1293
1568
|
if (status === "idle") return statusIndicatorConfig.idleText ?? statusCopy.idle;
|
|
1294
1569
|
if (status === "connecting") return statusIndicatorConfig.connectingText ?? statusCopy.connecting;
|
|
1295
1570
|
if (status === "connected") return statusIndicatorConfig.connectedText ?? statusCopy.connected;
|
|
@@ -1311,6 +1586,16 @@ export const createChatExperience = (
|
|
|
1311
1586
|
if (!launcherEnabled) return;
|
|
1312
1587
|
setOpenState(!open);
|
|
1313
1588
|
},
|
|
1589
|
+
clearChat() {
|
|
1590
|
+
// Clear messages in session (this will trigger onMessagesChanged which re-renders)
|
|
1591
|
+
session.clearMessages();
|
|
1592
|
+
|
|
1593
|
+
// Dispatch custom event for external handlers (e.g., localStorage clearing in examples)
|
|
1594
|
+
const clearEvent = new CustomEvent("vanilla-agent:clear-chat", {
|
|
1595
|
+
detail: { timestamp: new Date().toISOString() }
|
|
1596
|
+
});
|
|
1597
|
+
window.dispatchEvent(clearEvent);
|
|
1598
|
+
},
|
|
1314
1599
|
destroy() {
|
|
1315
1600
|
destroyCallbacks.forEach((cb) => cb());
|
|
1316
1601
|
wrapper.remove();
|
|
@@ -1322,4 +1607,4 @@ export const createChatExperience = (
|
|
|
1322
1607
|
};
|
|
1323
1608
|
};
|
|
1324
1609
|
|
|
1325
|
-
export type
|
|
1610
|
+
export type AgentWidgetController = Controller;
|
package/src/utils/constants.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentWidgetSessionStatus } from "../session";
|
|
2
2
|
|
|
3
|
-
export const statusCopy: Record<
|
|
3
|
+
export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
|
|
4
4
|
idle: "Online",
|
|
5
5
|
connecting: "Connecting…",
|
|
6
6
|
connected: "Streaming…",
|
|
@@ -9,3 +9,5 @@ export const statusCopy: Record<ChatWidgetSessionStatus, string> = {
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
|
|
13
|
+
|
package/src/utils/dom.ts
CHANGED
package/src/utils/formatting.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentWidgetReasoning, AgentWidgetToolCall } from "../types";
|
|
2
2
|
|
|
3
3
|
export const formatUnknownValue = (value: unknown): string => {
|
|
4
4
|
if (value === null) return "null";
|
|
@@ -14,7 +14,7 @@ export const formatUnknownValue = (value: unknown): string => {
|
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export const formatReasoningDuration = (reasoning:
|
|
17
|
+
export const formatReasoningDuration = (reasoning: AgentWidgetReasoning) => {
|
|
18
18
|
const end = reasoning.completedAt ?? Date.now();
|
|
19
19
|
const start = reasoning.startedAt ?? end;
|
|
20
20
|
const durationMs =
|
|
@@ -32,13 +32,13 @@ export const formatReasoningDuration = (reasoning: ChatWidgetReasoning) => {
|
|
|
32
32
|
return `Thought for ${formatted} seconds`;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export const describeReasonStatus = (reasoning:
|
|
35
|
+
export const describeReasonStatus = (reasoning: AgentWidgetReasoning) => {
|
|
36
36
|
if (reasoning.status === "complete") return formatReasoningDuration(reasoning);
|
|
37
37
|
if (reasoning.status === "pending") return "Waiting";
|
|
38
38
|
return "";
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
export const formatToolDuration = (tool:
|
|
41
|
+
export const formatToolDuration = (tool: AgentWidgetToolCall) => {
|
|
42
42
|
const durationMs =
|
|
43
43
|
typeof tool.duration === "number"
|
|
44
44
|
? tool.duration
|
|
@@ -60,13 +60,13 @@ export const formatToolDuration = (tool: ChatWidgetToolCall) => {
|
|
|
60
60
|
return `Used tool for ${formatted} seconds`;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
export const describeToolStatus = (status:
|
|
63
|
+
export const describeToolStatus = (status: AgentWidgetToolCall["status"]) => {
|
|
64
64
|
if (status === "complete") return "";
|
|
65
65
|
if (status === "pending") return "Starting";
|
|
66
66
|
return "Running";
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
export const describeToolTitle = (tool:
|
|
69
|
+
export const describeToolTitle = (tool: AgentWidgetToolCall) => {
|
|
70
70
|
if (tool.status === "complete") {
|
|
71
71
|
return formatToolDuration(tool);
|
|
72
72
|
}
|
|
@@ -75,3 +75,5 @@ export const describeToolTitle = (tool: ChatWidgetToolCall) => {
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
|
|
79
|
+
|
package/src/utils/icons.ts
CHANGED
package/src/utils/positioning.ts
CHANGED
package/src/utils/theme.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentWidgetConfig } from "../types";
|
|
2
2
|
|
|
3
3
|
export const applyThemeVariables = (
|
|
4
4
|
element: HTMLElement,
|
|
5
|
-
config?:
|
|
5
|
+
config?: AgentWidgetConfig
|
|
6
6
|
) => {
|
|
7
7
|
const theme = config?.theme ?? {};
|
|
8
8
|
Object.entries(theme).forEach(([key, value]) => {
|
|
@@ -18,3 +18,5 @@ export const applyThemeVariables = (
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
|