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/src/types.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { ChatWidgetPlugin } from "./plugins/types";
1
+ import type { AgentWidgetPlugin } from "./plugins/types";
2
2
 
3
- export type ChatWidgetFeatureFlags = {
3
+ export type AgentWidgetFeatureFlags = {
4
4
  showReasoning?: boolean;
5
5
  showToolCalls?: boolean;
6
6
  };
7
7
 
8
- export type ChatWidgetTheme = {
8
+ export type AgentWidgetTheme = {
9
9
  primary?: string;
10
10
  secondary?: string;
11
11
  surface?: string;
@@ -24,6 +24,9 @@ export type ChatWidgetTheme = {
24
24
  closeButtonColor?: string;
25
25
  closeButtonBackgroundColor?: string;
26
26
  closeButtonBorderColor?: string;
27
+ clearChatIconColor?: string;
28
+ clearChatBackgroundColor?: string;
29
+ clearChatBorderColor?: string;
27
30
  micIconColor?: string;
28
31
  micBackgroundColor?: string;
29
32
  micBorderColor?: string;
@@ -39,7 +42,7 @@ export type ChatWidgetTheme = {
39
42
  buttonRadius?: string;
40
43
  };
41
44
 
42
- export type ChatWidgetLauncherConfig = {
45
+ export type AgentWidgetLauncherConfig = {
43
46
  enabled?: boolean;
44
47
  title?: string;
45
48
  subtitle?: string;
@@ -68,10 +71,17 @@ export type ChatWidgetLauncherConfig = {
68
71
  closeButtonBorderWidth?: string;
69
72
  closeButtonBorderColor?: string;
70
73
  closeButtonBorderRadius?: string;
74
+ closeButtonPaddingX?: string;
75
+ closeButtonPaddingY?: string;
71
76
  closeButtonPlacement?: "inline" | "top-right";
77
+ closeButtonIconName?: string;
78
+ closeButtonIconText?: string;
79
+ closeButtonTooltipText?: string;
80
+ closeButtonShowTooltip?: boolean;
81
+ clearChat?: AgentWidgetClearChatConfig;
72
82
  };
73
83
 
74
- export type ChatWidgetSendButtonConfig = {
84
+ export type AgentWidgetSendButtonConfig = {
75
85
  borderWidth?: string;
76
86
  borderColor?: string;
77
87
  paddingX?: string;
@@ -86,7 +96,22 @@ export type ChatWidgetSendButtonConfig = {
86
96
  size?: string;
87
97
  };
88
98
 
89
- export type ChatWidgetStatusIndicatorConfig = {
99
+ export type AgentWidgetClearChatConfig = {
100
+ enabled?: boolean;
101
+ iconName?: string;
102
+ iconColor?: string;
103
+ backgroundColor?: string;
104
+ borderWidth?: string;
105
+ borderColor?: string;
106
+ borderRadius?: string;
107
+ size?: string;
108
+ paddingX?: string;
109
+ paddingY?: string;
110
+ tooltipText?: string;
111
+ showTooltip?: boolean;
112
+ };
113
+
114
+ export type AgentWidgetStatusIndicatorConfig = {
90
115
  visible?: boolean;
91
116
  idleText?: string;
92
117
  connectingText?: string;
@@ -94,7 +119,7 @@ export type ChatWidgetStatusIndicatorConfig = {
94
119
  errorText?: string;
95
120
  };
96
121
 
97
- export type ChatWidgetVoiceRecognitionConfig = {
122
+ export type AgentWidgetVoiceRecognitionConfig = {
98
123
  enabled?: boolean;
99
124
  pauseDuration?: number;
100
125
  iconName?: string;
@@ -113,7 +138,7 @@ export type ChatWidgetVoiceRecognitionConfig = {
113
138
  showRecordingIndicator?: boolean;
114
139
  };
115
140
 
116
- export type ChatWidgetConfig = {
141
+ export type AgentWidgetConfig = {
117
142
  apiUrl?: string;
118
143
  flowId?: string;
119
144
  headers?: Record<string, string>;
@@ -123,28 +148,28 @@ export type ChatWidgetConfig = {
123
148
  inputPlaceholder?: string;
124
149
  sendButtonLabel?: string;
125
150
  };
126
- theme?: ChatWidgetTheme;
127
- features?: ChatWidgetFeatureFlags;
128
- launcher?: ChatWidgetLauncherConfig;
129
- initialMessages?: ChatWidgetMessage[];
151
+ theme?: AgentWidgetTheme;
152
+ features?: AgentWidgetFeatureFlags;
153
+ launcher?: AgentWidgetLauncherConfig;
154
+ initialMessages?: AgentWidgetMessage[];
130
155
  suggestionChips?: string[];
131
156
  debug?: boolean;
132
157
  formEndpoint?: string;
133
158
  launcherWidth?: string;
134
- sendButton?: ChatWidgetSendButtonConfig;
135
- statusIndicator?: ChatWidgetStatusIndicatorConfig;
136
- voiceRecognition?: ChatWidgetVoiceRecognitionConfig;
159
+ sendButton?: AgentWidgetSendButtonConfig;
160
+ statusIndicator?: AgentWidgetStatusIndicatorConfig;
161
+ voiceRecognition?: AgentWidgetVoiceRecognitionConfig;
137
162
  postprocessMessage?: (context: {
138
163
  text: string;
139
- message: ChatWidgetMessage;
164
+ message: AgentWidgetMessage;
140
165
  streaming: boolean;
141
166
  }) => string;
142
- plugins?: ChatWidgetPlugin[];
167
+ plugins?: AgentWidgetPlugin[];
143
168
  };
144
169
 
145
- export type ChatWidgetMessageRole = "user" | "assistant" | "system";
170
+ export type AgentWidgetMessageRole = "user" | "assistant" | "system";
146
171
 
147
- export type ChatWidgetReasoning = {
172
+ export type AgentWidgetReasoning = {
148
173
  id: string;
149
174
  status: "pending" | "streaming" | "complete";
150
175
  chunks: string[];
@@ -153,7 +178,7 @@ export type ChatWidgetReasoning = {
153
178
  durationMs?: number;
154
179
  };
155
180
 
156
- export type ChatWidgetToolCall = {
181
+ export type AgentWidgetToolCall = {
157
182
  id: string;
158
183
  name?: string;
159
184
  status: "pending" | "running" | "complete";
@@ -166,29 +191,29 @@ export type ChatWidgetToolCall = {
166
191
  durationMs?: number;
167
192
  };
168
193
 
169
- export type ChatWidgetMessageVariant = "assistant" | "reasoning" | "tool";
194
+ export type AgentWidgetMessageVariant = "assistant" | "reasoning" | "tool";
170
195
 
171
- export type ChatWidgetMessage = {
196
+ export type AgentWidgetMessage = {
172
197
  id: string;
173
- role: ChatWidgetMessageRole;
198
+ role: AgentWidgetMessageRole;
174
199
  content: string;
175
200
  createdAt: string;
176
201
  streaming?: boolean;
177
- variant?: ChatWidgetMessageVariant;
202
+ variant?: AgentWidgetMessageVariant;
178
203
  sequence?: number;
179
- reasoning?: ChatWidgetReasoning;
180
- toolCall?: ChatWidgetToolCall;
181
- tools?: ChatWidgetToolCall[];
204
+ reasoning?: AgentWidgetReasoning;
205
+ toolCall?: AgentWidgetToolCall;
206
+ tools?: AgentWidgetToolCall[];
182
207
  };
183
208
 
184
- export type ChatWidgetEvent =
185
- | { type: "message"; message: ChatWidgetMessage }
209
+ export type AgentWidgetEvent =
210
+ | { type: "message"; message: AgentWidgetMessage }
186
211
  | { type: "status"; status: "connecting" | "connected" | "error" | "idle" }
187
212
  | { type: "error"; error: Error };
188
213
 
189
- export type ChatWidgetInitOptions = {
214
+ export type AgentWidgetInitOptions = {
190
215
  target: string | HTMLElement;
191
- config?: ChatWidgetConfig;
216
+ config?: AgentWidgetConfig;
192
217
  useShadowDom?: boolean;
193
218
  onReady?: () => void;
194
219
  };
package/src/ui.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { escapeHtml } from "./postprocessors";
2
- import { ChatWidgetSession, ChatWidgetSessionStatus } from "./session";
3
- import { ChatWidgetConfig, ChatWidgetMessage } from "./types";
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";
@@ -14,16 +14,18 @@ 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: ChatWidgetConfig) => void;
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?: ChatWidgetConfig): MessageTransform => {
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 createChatExperience = (
40
+ export const createAgentExperience = (
39
41
  mount: HTMLElement,
40
- initialConfig?: ChatWidgetConfig
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 = { ...initialConfig };
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: ChatWidgetSessionStatus): string => {
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: ChatWidgetSession;
102
+ let session: AgentWidgetSession;
101
103
  let isStreaming = false;
102
104
  let shouldAutoScroll = true;
103
105
  let lastScrollTop = 0;
@@ -178,7 +180,7 @@ export const createChatExperience = (
178
180
  // Message rendering with plugin support
179
181
  const renderMessagesWithPlugins = (
180
182
  container: HTMLElement,
181
- messages: ChatWidgetMessage[],
183
+ messages: AgentWidgetMessage[],
182
184
  transform: MessageTransform
183
185
  ) => {
184
186
  container.innerHTML = "";
@@ -259,9 +261,28 @@ export const createChatExperience = (
259
261
  // Add typing indicator if streaming and there's at least one user message
260
262
  if (isStreaming && messages.some((msg) => msg.role === "user")) {
261
263
  const typingIndicator = createTypingIndicator();
264
+
265
+ // Create a bubble wrapper for the typing indicator (similar to assistant messages)
266
+ const typingBubble = document.createElement("div");
267
+ typingBubble.className = [
268
+ "tvw-max-w-[85%]",
269
+ "tvw-rounded-2xl",
270
+ "tvw-text-sm",
271
+ "tvw-leading-relaxed",
272
+ "tvw-shadow-sm",
273
+ "tvw-bg-cw-surface",
274
+ "tvw-border",
275
+ "tvw-border-cw-message-border",
276
+ "tvw-text-cw-primary",
277
+ "tvw-px-5",
278
+ "tvw-py-3"
279
+ ].join(" ");
280
+
281
+ typingBubble.appendChild(typingIndicator);
282
+
262
283
  const typingWrapper = document.createElement("div");
263
- typingWrapper.className = "tvw-flex tvw-justify-end";
264
- typingWrapper.appendChild(typingIndicator);
284
+ typingWrapper.className = "tvw-flex";
285
+ typingWrapper.appendChild(typingBubble);
265
286
  fragment.appendChild(typingWrapper);
266
287
  }
267
288
 
@@ -317,9 +338,14 @@ export const createChatExperience = (
317
338
  introSubtitle.textContent =
318
339
  config.copy?.welcomeSubtitle ??
319
340
  "Ask anything about your account or products.";
320
- textarea.placeholder = config.copy?.inputPlaceholder ?? "Type your message…";
321
- sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
322
-
341
+ textarea.placeholder = config.copy?.inputPlaceholder ?? "How can I help...";
342
+
343
+ // Only update send button text if NOT using icon mode
344
+ const useIcon = config.sendButton?.useIcon ?? false;
345
+ if (!useIcon) {
346
+ sendButton.textContent = config.copy?.sendButtonLabel ?? "Send";
347
+ }
348
+
323
349
  // Update textarea font family and weight
324
350
  const fontFamily = config.theme?.inputFontFamily ?? "sans-serif";
325
351
  const fontWeight = config.theme?.inputFontWeight ?? "400";
@@ -340,7 +366,7 @@ export const createChatExperience = (
340
366
  textarea.style.fontWeight = fontWeight;
341
367
  };
342
368
 
343
- session = new ChatWidgetSession(config, {
369
+ session = new AgentWidgetSession(config, {
344
370
  onMessagesChanged(messages) {
345
371
  renderMessagesWithPlugins(messagesWrapper, messages, postprocess);
346
372
  // Re-render suggestions to hide them after first user message
@@ -359,7 +385,7 @@ export const createChatExperience = (
359
385
  },
360
386
  onStatusChanged(status) {
361
387
  const currentStatusConfig = config.statusIndicator ?? {};
362
- const getCurrentStatusText = (status: ChatWidgetSessionStatus): string => {
388
+ const getCurrentStatusText = (status: AgentWidgetSessionStatus): string => {
363
389
  if (status === "idle") return currentStatusConfig.idleText ?? statusCopy.idle;
364
390
  if (status === "connecting") return currentStatusConfig.connectingText ?? statusCopy.connecting;
365
391
  if (status === "connected") return currentStatusConfig.connectedText ?? statusCopy.connected;
@@ -568,7 +594,7 @@ export const createChatExperience = (
568
594
  };
569
595
 
570
596
  // Function to create mic button dynamically
571
- const createMicButton = (voiceConfig: ChatWidgetConfig['voiceRecognition'], sendButtonConfig: ChatWidgetConfig['sendButton']): { micButton: HTMLButtonElement; micButtonWrapper: HTMLElement } | null => {
597
+ const createMicButton = (voiceConfig: AgentWidgetConfig['voiceRecognition'], sendButtonConfig: AgentWidgetConfig['sendButton']): { micButton: HTMLButtonElement; micButtonWrapper: HTMLElement } | null => {
572
598
  const hasSpeechRecognition =
573
599
  typeof window !== 'undefined' &&
574
600
  (typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
@@ -711,7 +737,7 @@ export const createChatExperience = (
711
737
  return;
712
738
  }
713
739
  const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
714
- const width = launcherWidth ?? "min(360px, calc(100vw - 24px))";
740
+ const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
715
741
  panel.style.width = width;
716
742
  panel.style.maxWidth = width;
717
743
  const viewportHeight = window.innerHeight;
@@ -777,6 +803,25 @@ export const createChatExperience = (
777
803
 
778
804
  refreshCloseButton();
779
805
 
806
+ // Setup clear chat button click handler
807
+ const setupClearChatButton = () => {
808
+ const { clearChatButton } = panelElements;
809
+ if (!clearChatButton) return;
810
+
811
+ clearChatButton.addEventListener("click", () => {
812
+ // Clear messages in session (this will trigger onMessagesChanged which re-renders)
813
+ session.clearMessages();
814
+
815
+ // Dispatch custom event for external handlers (e.g., localStorage clearing in examples)
816
+ const clearEvent = new CustomEvent("vanilla-agent:clear-chat", {
817
+ detail: { timestamp: new Date().toISOString() }
818
+ });
819
+ window.dispatchEvent(clearEvent);
820
+ });
821
+ };
822
+
823
+ setupClearChatButton();
824
+
780
825
  composerForm.addEventListener("submit", handleSubmit);
781
826
  textarea.addEventListener("keydown", handleInputEnter);
782
827
 
@@ -796,7 +841,7 @@ export const createChatExperience = (
796
841
  }
797
842
 
798
843
  return {
799
- update(nextConfig: ChatWidgetConfig) {
844
+ update(nextConfig: AgentWidgetConfig) {
800
845
  config = { ...config, ...nextConfig };
801
846
  applyThemeVariables(mount, config);
802
847
 
@@ -995,6 +1040,259 @@ export const createChatExperience = (
995
1040
  closeButton.style.borderRadius = "";
996
1041
  closeButton.classList.add("tvw-rounded-full");
997
1042
  }
1043
+
1044
+ // Update padding
1045
+ if (launcher.closeButtonPaddingX) {
1046
+ closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
1047
+ closeButton.style.paddingRight = launcher.closeButtonPaddingX;
1048
+ } else {
1049
+ closeButton.style.paddingLeft = "";
1050
+ closeButton.style.paddingRight = "";
1051
+ }
1052
+ if (launcher.closeButtonPaddingY) {
1053
+ closeButton.style.paddingTop = launcher.closeButtonPaddingY;
1054
+ closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
1055
+ } else {
1056
+ closeButton.style.paddingTop = "";
1057
+ closeButton.style.paddingBottom = "";
1058
+ }
1059
+
1060
+ // Update icon
1061
+ const closeButtonIconName = launcher.closeButtonIconName ?? "x";
1062
+ const closeButtonIconText = launcher.closeButtonIconText ?? "×";
1063
+
1064
+ // Clear existing content and render new icon
1065
+ closeButton.innerHTML = "";
1066
+ const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
1067
+ if (iconSvg) {
1068
+ closeButton.appendChild(iconSvg);
1069
+ } else {
1070
+ closeButton.textContent = closeButtonIconText;
1071
+ }
1072
+
1073
+ // Update tooltip
1074
+ const { closeButtonWrapper } = panelElements;
1075
+ const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
1076
+ const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
1077
+
1078
+ closeButton.setAttribute("aria-label", closeButtonTooltipText);
1079
+
1080
+ if (closeButtonWrapper) {
1081
+ // Clean up old tooltip event listeners if they exist
1082
+ if ((closeButtonWrapper as any)._cleanupTooltip) {
1083
+ (closeButtonWrapper as any)._cleanupTooltip();
1084
+ delete (closeButtonWrapper as any)._cleanupTooltip;
1085
+ }
1086
+
1087
+ // Set up new portaled tooltip with event listeners
1088
+ if (closeButtonShowTooltip && closeButtonTooltipText) {
1089
+ let portaledTooltip: HTMLElement | null = null;
1090
+
1091
+ const showTooltip = () => {
1092
+ if (portaledTooltip || !closeButton) return; // Already showing or button doesn't exist
1093
+
1094
+ // Create tooltip element
1095
+ portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
1096
+ portaledTooltip.textContent = closeButtonTooltipText;
1097
+
1098
+ // Add arrow
1099
+ const arrow = createElement("div");
1100
+ arrow.className = "tvw-clear-chat-tooltip-arrow";
1101
+ portaledTooltip.appendChild(arrow);
1102
+
1103
+ // Get button position
1104
+ const buttonRect = closeButton.getBoundingClientRect();
1105
+
1106
+ // Position tooltip above button
1107
+ portaledTooltip.style.position = "fixed";
1108
+ portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
1109
+ portaledTooltip.style.top = `${buttonRect.top - 8}px`;
1110
+ portaledTooltip.style.transform = "translate(-50%, -100%)";
1111
+
1112
+ // Append to body
1113
+ document.body.appendChild(portaledTooltip);
1114
+ };
1115
+
1116
+ const hideTooltip = () => {
1117
+ if (portaledTooltip && portaledTooltip.parentNode) {
1118
+ portaledTooltip.parentNode.removeChild(portaledTooltip);
1119
+ portaledTooltip = null;
1120
+ }
1121
+ };
1122
+
1123
+ // Add event listeners
1124
+ closeButtonWrapper.addEventListener("mouseenter", showTooltip);
1125
+ closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
1126
+ closeButton.addEventListener("focus", showTooltip);
1127
+ closeButton.addEventListener("blur", hideTooltip);
1128
+
1129
+ // Store cleanup function on the wrapper for later use
1130
+ (closeButtonWrapper as any)._cleanupTooltip = () => {
1131
+ hideTooltip();
1132
+ if (closeButtonWrapper) {
1133
+ closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
1134
+ closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
1135
+ }
1136
+ if (closeButton) {
1137
+ closeButton.removeEventListener("focus", showTooltip);
1138
+ closeButton.removeEventListener("blur", hideTooltip);
1139
+ }
1140
+ };
1141
+ }
1142
+ }
1143
+ }
1144
+
1145
+ // Update clear chat button styling from config
1146
+ const { clearChatButton, clearChatButtonWrapper } = panelElements;
1147
+ if (clearChatButton) {
1148
+ const clearChatConfig = launcher.clearChat ?? {};
1149
+ const clearChatEnabled = clearChatConfig.enabled ?? true;
1150
+
1151
+ // Show/hide button based on enabled state
1152
+ if (clearChatButtonWrapper) {
1153
+ clearChatButtonWrapper.style.display = clearChatEnabled ? "" : "none";
1154
+ }
1155
+
1156
+ if (clearChatEnabled) {
1157
+ // Update size
1158
+ const clearChatSize = clearChatConfig.size ?? "32px";
1159
+ clearChatButton.style.height = clearChatSize;
1160
+ clearChatButton.style.width = clearChatSize;
1161
+
1162
+ // Update icon
1163
+ const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
1164
+ const clearChatIconColor = clearChatConfig.iconColor ?? "";
1165
+
1166
+ // Clear existing icon and render new one
1167
+ clearChatButton.innerHTML = "";
1168
+ const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
1169
+ if (iconSvg) {
1170
+ clearChatButton.appendChild(iconSvg);
1171
+ }
1172
+
1173
+ // Update icon color
1174
+ if (clearChatIconColor) {
1175
+ clearChatButton.style.color = clearChatIconColor;
1176
+ clearChatButton.classList.remove("tvw-text-cw-muted");
1177
+ } else {
1178
+ clearChatButton.style.color = "";
1179
+ clearChatButton.classList.add("tvw-text-cw-muted");
1180
+ }
1181
+
1182
+ // Update background color
1183
+ if (clearChatConfig.backgroundColor) {
1184
+ clearChatButton.style.backgroundColor = clearChatConfig.backgroundColor;
1185
+ clearChatButton.classList.remove("hover:tvw-bg-gray-100");
1186
+ } else {
1187
+ clearChatButton.style.backgroundColor = "";
1188
+ clearChatButton.classList.add("hover:tvw-bg-gray-100");
1189
+ }
1190
+
1191
+ // Update border
1192
+ if (clearChatConfig.borderWidth || clearChatConfig.borderColor) {
1193
+ const borderWidth = clearChatConfig.borderWidth || "0px";
1194
+ const borderColor = clearChatConfig.borderColor || "transparent";
1195
+ clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
1196
+ clearChatButton.classList.remove("tvw-border-none");
1197
+ } else {
1198
+ clearChatButton.style.border = "";
1199
+ clearChatButton.classList.add("tvw-border-none");
1200
+ }
1201
+
1202
+ // Update border radius
1203
+ if (clearChatConfig.borderRadius) {
1204
+ clearChatButton.style.borderRadius = clearChatConfig.borderRadius;
1205
+ clearChatButton.classList.remove("tvw-rounded-full");
1206
+ } else {
1207
+ clearChatButton.style.borderRadius = "";
1208
+ clearChatButton.classList.add("tvw-rounded-full");
1209
+ }
1210
+
1211
+ // Update padding
1212
+ if (clearChatConfig.paddingX) {
1213
+ clearChatButton.style.paddingLeft = clearChatConfig.paddingX;
1214
+ clearChatButton.style.paddingRight = clearChatConfig.paddingX;
1215
+ } else {
1216
+ clearChatButton.style.paddingLeft = "";
1217
+ clearChatButton.style.paddingRight = "";
1218
+ }
1219
+ if (clearChatConfig.paddingY) {
1220
+ clearChatButton.style.paddingTop = clearChatConfig.paddingY;
1221
+ clearChatButton.style.paddingBottom = clearChatConfig.paddingY;
1222
+ } else {
1223
+ clearChatButton.style.paddingTop = "";
1224
+ clearChatButton.style.paddingBottom = "";
1225
+ }
1226
+
1227
+ const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
1228
+ const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
1229
+
1230
+ clearChatButton.setAttribute("aria-label", clearChatTooltipText);
1231
+
1232
+ if (clearChatButtonWrapper) {
1233
+ // Clean up old tooltip event listeners if they exist
1234
+ if ((clearChatButtonWrapper as any)._cleanupTooltip) {
1235
+ (clearChatButtonWrapper as any)._cleanupTooltip();
1236
+ delete (clearChatButtonWrapper as any)._cleanupTooltip;
1237
+ }
1238
+
1239
+ // Set up new portaled tooltip with event listeners
1240
+ if (clearChatShowTooltip && clearChatTooltipText) {
1241
+ let portaledTooltip: HTMLElement | null = null;
1242
+
1243
+ const showTooltip = () => {
1244
+ if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
1245
+
1246
+ // Create tooltip element
1247
+ portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
1248
+ portaledTooltip.textContent = clearChatTooltipText;
1249
+
1250
+ // Add arrow
1251
+ const arrow = createElement("div");
1252
+ arrow.className = "tvw-clear-chat-tooltip-arrow";
1253
+ portaledTooltip.appendChild(arrow);
1254
+
1255
+ // Get button position
1256
+ const buttonRect = clearChatButton.getBoundingClientRect();
1257
+
1258
+ // Position tooltip above button
1259
+ portaledTooltip.style.position = "fixed";
1260
+ portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
1261
+ portaledTooltip.style.top = `${buttonRect.top - 8}px`;
1262
+ portaledTooltip.style.transform = "translate(-50%, -100%)";
1263
+
1264
+ // Append to body
1265
+ document.body.appendChild(portaledTooltip);
1266
+ };
1267
+
1268
+ const hideTooltip = () => {
1269
+ if (portaledTooltip && portaledTooltip.parentNode) {
1270
+ portaledTooltip.parentNode.removeChild(portaledTooltip);
1271
+ portaledTooltip = null;
1272
+ }
1273
+ };
1274
+
1275
+ // Add event listeners
1276
+ clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
1277
+ clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
1278
+ clearChatButton.addEventListener("focus", showTooltip);
1279
+ clearChatButton.addEventListener("blur", hideTooltip);
1280
+
1281
+ // Store cleanup function on the button for later use
1282
+ (clearChatButtonWrapper as any)._cleanupTooltip = () => {
1283
+ hideTooltip();
1284
+ if (clearChatButtonWrapper) {
1285
+ clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
1286
+ clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
1287
+ }
1288
+ if (clearChatButton) {
1289
+ clearChatButton.removeEventListener("focus", showTooltip);
1290
+ clearChatButton.removeEventListener("blur", hideTooltip);
1291
+ }
1292
+ };
1293
+ }
1294
+ }
1295
+ }
998
1296
  }
999
1297
 
1000
1298
  postprocess = buildPostprocessor(config);
@@ -1289,7 +1587,7 @@ export const createChatExperience = (
1289
1587
  // Update status text if status is currently set
1290
1588
  if (session) {
1291
1589
  const currentStatus = session.getStatus();
1292
- const getCurrentStatusText = (status: ChatWidgetSessionStatus): string => {
1590
+ const getCurrentStatusText = (status: AgentWidgetSessionStatus): string => {
1293
1591
  if (status === "idle") return statusIndicatorConfig.idleText ?? statusCopy.idle;
1294
1592
  if (status === "connecting") return statusIndicatorConfig.connectingText ?? statusCopy.connecting;
1295
1593
  if (status === "connected") return statusIndicatorConfig.connectedText ?? statusCopy.connected;
@@ -1311,6 +1609,16 @@ export const createChatExperience = (
1311
1609
  if (!launcherEnabled) return;
1312
1610
  setOpenState(!open);
1313
1611
  },
1612
+ clearChat() {
1613
+ // Clear messages in session (this will trigger onMessagesChanged which re-renders)
1614
+ session.clearMessages();
1615
+
1616
+ // Dispatch custom event for external handlers (e.g., localStorage clearing in examples)
1617
+ const clearEvent = new CustomEvent("vanilla-agent:clear-chat", {
1618
+ detail: { timestamp: new Date().toISOString() }
1619
+ });
1620
+ window.dispatchEvent(clearEvent);
1621
+ },
1314
1622
  destroy() {
1315
1623
  destroyCallbacks.forEach((cb) => cb());
1316
1624
  wrapper.remove();
@@ -1322,4 +1630,4 @@ export const createChatExperience = (
1322
1630
  };
1323
1631
  };
1324
1632
 
1325
- export type ChatWidgetController = Controller;
1633
+ export type AgentWidgetController = Controller;
@@ -1,6 +1,6 @@
1
- import { ChatWidgetSessionStatus } from "../session";
1
+ import { AgentWidgetSessionStatus } from "../session";
2
2
 
3
- export const statusCopy: Record<ChatWidgetSessionStatus, string> = {
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
@@ -18,3 +18,5 @@ export const createFragment = (): DocumentFragment => {
18
18
 
19
19
 
20
20
 
21
+
22
+