vanilla-agent 1.10.0 → 1.11.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.
@@ -0,0 +1,454 @@
1
+ import { createElement } from "../utils/dom";
2
+ import { renderLucideIcon } from "../utils/icons";
3
+ import { AgentWidgetConfig } from "../types";
4
+
5
+ export interface HeaderElements {
6
+ header: HTMLElement;
7
+ iconHolder: HTMLElement;
8
+ headerTitle: HTMLElement;
9
+ headerSubtitle: HTMLElement;
10
+ closeButton: HTMLButtonElement;
11
+ closeButtonWrapper: HTMLElement;
12
+ clearChatButton: HTMLButtonElement | null;
13
+ clearChatButtonWrapper: HTMLElement | null;
14
+ }
15
+
16
+ export interface HeaderBuildContext {
17
+ config?: AgentWidgetConfig;
18
+ showClose?: boolean;
19
+ onClose?: () => void;
20
+ onClearChat?: () => void;
21
+ }
22
+
23
+ /**
24
+ * Build the header section of the panel.
25
+ * Extracted for reuse and plugin override support.
26
+ */
27
+ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
28
+ const { config, showClose = true } = context;
29
+
30
+ const header = createElement(
31
+ "div",
32
+ "tvw-flex tvw-items-center tvw-gap-3 tvw-bg-cw-surface tvw-px-6 tvw-py-5 tvw-border-b-cw-divider"
33
+ );
34
+
35
+ const launcher = config?.launcher ?? {};
36
+ const headerIconSize = launcher.headerIconSize ?? "48px";
37
+ const closeButtonSize = launcher.closeButtonSize ?? "32px";
38
+ const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
39
+ const headerIconHidden = launcher.headerIconHidden ?? false;
40
+ const headerIconName = launcher.headerIconName;
41
+
42
+ const iconHolder = createElement(
43
+ "div",
44
+ "tvw-flex tvw-items-center tvw-justify-center tvw-rounded-xl tvw-bg-cw-primary tvw-text-white tvw-text-xl"
45
+ );
46
+ iconHolder.style.height = headerIconSize;
47
+ iconHolder.style.width = headerIconSize;
48
+
49
+ // Render icon based on priority: Lucide icon > iconUrl > agentIconText
50
+ if (!headerIconHidden) {
51
+ if (headerIconName) {
52
+ // Use Lucide icon
53
+ const iconSize = parseFloat(headerIconSize) || 24;
54
+ const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "#ffffff", 2);
55
+ if (iconSvg) {
56
+ iconHolder.replaceChildren(iconSvg);
57
+ } else {
58
+ // Fallback to agentIconText if Lucide icon fails
59
+ iconHolder.textContent = config?.launcher?.agentIconText ?? "💬";
60
+ }
61
+ } else if (config?.launcher?.iconUrl) {
62
+ // Use image URL
63
+ const img = createElement("img") as HTMLImageElement;
64
+ img.src = config.launcher.iconUrl;
65
+ img.alt = "";
66
+ img.className = "tvw-rounded-xl tvw-object-cover";
67
+ img.style.height = headerIconSize;
68
+ img.style.width = headerIconSize;
69
+ iconHolder.replaceChildren(img);
70
+ } else {
71
+ // Use text/emoji
72
+ iconHolder.textContent = config?.launcher?.agentIconText ?? "💬";
73
+ }
74
+ }
75
+
76
+ const headerCopy = createElement("div", "tvw-flex tvw-flex-col");
77
+ const title = createElement("span", "tvw-text-base tvw-font-semibold");
78
+ title.textContent = config?.launcher?.title ?? "Chat Assistant";
79
+ const subtitle = createElement("span", "tvw-text-xs tvw-text-cw-muted");
80
+ subtitle.textContent =
81
+ config?.launcher?.subtitle ?? "Here to help you get answers fast";
82
+
83
+ headerCopy.append(title, subtitle);
84
+
85
+ // Only append iconHolder if not hidden
86
+ if (!headerIconHidden) {
87
+ header.append(iconHolder, headerCopy);
88
+ } else {
89
+ header.append(headerCopy);
90
+ }
91
+
92
+ // Create clear chat button if enabled
93
+ const clearChatConfig = launcher.clearChat ?? {};
94
+ const clearChatEnabled = clearChatConfig.enabled ?? true;
95
+ const clearChatPlacement = clearChatConfig.placement ?? "inline";
96
+ let clearChatButton: HTMLButtonElement | null = null;
97
+ let clearChatButtonWrapper: HTMLElement | null = null;
98
+
99
+ if (clearChatEnabled) {
100
+ const clearChatSize = clearChatConfig.size ?? "32px";
101
+ const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
102
+ const clearChatIconColor = clearChatConfig.iconColor ?? "";
103
+ const clearChatBgColor = clearChatConfig.backgroundColor ?? "";
104
+ const clearChatBorderWidth = clearChatConfig.borderWidth ?? "";
105
+ const clearChatBorderColor = clearChatConfig.borderColor ?? "";
106
+ const clearChatBorderRadius = clearChatConfig.borderRadius ?? "";
107
+ const clearChatPaddingX = clearChatConfig.paddingX ?? "";
108
+ const clearChatPaddingY = clearChatConfig.paddingY ?? "";
109
+ const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
110
+ const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
111
+
112
+ // Create button wrapper for tooltip - positioned based on placement
113
+ // Note: Don't use tvw-clear-chat-button-wrapper class for top-right mode as its
114
+ // display: inline-flex causes alignment issues with the close button
115
+ clearChatButtonWrapper = createElement(
116
+ "div",
117
+ clearChatPlacement === "top-right"
118
+ ? "tvw-absolute tvw-top-4 tvw-z-50"
119
+ : "tvw-relative tvw-ml-auto tvw-clear-chat-button-wrapper"
120
+ );
121
+
122
+ // Position to the left of the close button (which is at right: 1rem/16px)
123
+ // Close button is ~32px wide, plus small gap = 48px from right
124
+ if (clearChatPlacement === "top-right") {
125
+ clearChatButtonWrapper.style.right = "48px";
126
+ }
127
+
128
+ clearChatButton = createElement(
129
+ "button",
130
+ "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"
131
+ ) as HTMLButtonElement;
132
+
133
+ clearChatButton.style.height = clearChatSize;
134
+ clearChatButton.style.width = clearChatSize;
135
+ clearChatButton.type = "button";
136
+ clearChatButton.setAttribute("aria-label", clearChatTooltipText);
137
+
138
+ // Add icon
139
+ const iconSvg = renderLucideIcon(
140
+ clearChatIconName,
141
+ "20px",
142
+ clearChatIconColor || "",
143
+ 2
144
+ );
145
+ if (iconSvg) {
146
+ clearChatButton.appendChild(iconSvg);
147
+ }
148
+
149
+ // Apply styling from config
150
+ if (clearChatIconColor) {
151
+ clearChatButton.style.color = clearChatIconColor;
152
+ clearChatButton.classList.remove("tvw-text-cw-muted");
153
+ }
154
+
155
+ if (clearChatBgColor) {
156
+ clearChatButton.style.backgroundColor = clearChatBgColor;
157
+ clearChatButton.classList.remove("hover:tvw-bg-gray-100");
158
+ }
159
+
160
+ if (clearChatBorderWidth || clearChatBorderColor) {
161
+ const borderWidth = clearChatBorderWidth || "0px";
162
+ const borderColor = clearChatBorderColor || "transparent";
163
+ clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
164
+ clearChatButton.classList.remove("tvw-border-none");
165
+ }
166
+
167
+ if (clearChatBorderRadius) {
168
+ clearChatButton.style.borderRadius = clearChatBorderRadius;
169
+ clearChatButton.classList.remove("tvw-rounded-full");
170
+ }
171
+
172
+ // Apply padding styling
173
+ if (clearChatPaddingX) {
174
+ clearChatButton.style.paddingLeft = clearChatPaddingX;
175
+ clearChatButton.style.paddingRight = clearChatPaddingX;
176
+ } else {
177
+ clearChatButton.style.paddingLeft = "";
178
+ clearChatButton.style.paddingRight = "";
179
+ }
180
+ if (clearChatPaddingY) {
181
+ clearChatButton.style.paddingTop = clearChatPaddingY;
182
+ clearChatButton.style.paddingBottom = clearChatPaddingY;
183
+ } else {
184
+ clearChatButton.style.paddingTop = "";
185
+ clearChatButton.style.paddingBottom = "";
186
+ }
187
+
188
+ clearChatButtonWrapper.appendChild(clearChatButton);
189
+
190
+ // Add tooltip with portaling to document.body to escape overflow clipping
191
+ if (
192
+ clearChatShowTooltip &&
193
+ clearChatTooltipText &&
194
+ clearChatButton &&
195
+ clearChatButtonWrapper
196
+ ) {
197
+ let portaledTooltip: HTMLElement | null = null;
198
+
199
+ const showTooltip = () => {
200
+ if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
201
+
202
+ // Create tooltip element
203
+ portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
204
+ portaledTooltip.textContent = clearChatTooltipText;
205
+
206
+ // Add arrow
207
+ const arrow = createElement("div");
208
+ arrow.className = "tvw-clear-chat-tooltip-arrow";
209
+ portaledTooltip.appendChild(arrow);
210
+
211
+ // Get button position
212
+ const buttonRect = clearChatButton.getBoundingClientRect();
213
+
214
+ // Position tooltip above button
215
+ portaledTooltip.style.position = "fixed";
216
+ portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
217
+ portaledTooltip.style.top = `${buttonRect.top - 8}px`;
218
+ portaledTooltip.style.transform = "translate(-50%, -100%)";
219
+
220
+ // Append to body
221
+ document.body.appendChild(portaledTooltip);
222
+ };
223
+
224
+ const hideTooltip = () => {
225
+ if (portaledTooltip && portaledTooltip.parentNode) {
226
+ portaledTooltip.parentNode.removeChild(portaledTooltip);
227
+ portaledTooltip = null;
228
+ }
229
+ };
230
+
231
+ // Add event listeners
232
+ clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
233
+ clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
234
+ clearChatButton.addEventListener("focus", showTooltip);
235
+ clearChatButton.addEventListener("blur", hideTooltip);
236
+
237
+ // Store cleanup function on the button for later use
238
+ (clearChatButtonWrapper as any)._cleanupTooltip = () => {
239
+ hideTooltip();
240
+ if (clearChatButtonWrapper) {
241
+ clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
242
+ clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
243
+ }
244
+ if (clearChatButton) {
245
+ clearChatButton.removeEventListener("focus", showTooltip);
246
+ clearChatButton.removeEventListener("blur", hideTooltip);
247
+ }
248
+ };
249
+ }
250
+
251
+ // Only append to header if inline placement
252
+ if (clearChatPlacement === "inline") {
253
+ header.appendChild(clearChatButtonWrapper);
254
+ }
255
+ }
256
+
257
+ // Create close button wrapper for tooltip positioning
258
+ // Only needs ml-auto if clear chat is disabled or top-right positioned
259
+ const closeButtonWrapper = createElement(
260
+ "div",
261
+ closeButtonPlacement === "top-right"
262
+ ? "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50"
263
+ : clearChatEnabled && clearChatPlacement === "inline"
264
+ ? ""
265
+ : "tvw-ml-auto"
266
+ );
267
+
268
+ // Create close button with base classes
269
+ const closeButton = createElement(
270
+ "button",
271
+ "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"
272
+ ) as HTMLButtonElement;
273
+ closeButton.style.height = closeButtonSize;
274
+ closeButton.style.width = closeButtonSize;
275
+ closeButton.type = "button";
276
+
277
+ // Get tooltip config
278
+ const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
279
+ const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
280
+
281
+ closeButton.setAttribute("aria-label", closeButtonTooltipText);
282
+ closeButton.style.display = showClose ? "" : "none";
283
+
284
+ // Add icon or fallback text
285
+ const closeButtonIconName = launcher.closeButtonIconName ?? "x";
286
+ const closeButtonIconText = launcher.closeButtonIconText ?? "×";
287
+
288
+ // Try to render Lucide icon, fallback to text if not provided or fails
289
+ const closeIconSvg = renderLucideIcon(
290
+ closeButtonIconName,
291
+ "20px",
292
+ launcher.closeButtonColor || "",
293
+ 2
294
+ );
295
+ if (closeIconSvg) {
296
+ closeButton.appendChild(closeIconSvg);
297
+ } else {
298
+ closeButton.textContent = closeButtonIconText;
299
+ }
300
+
301
+ // Apply close button styling from config
302
+ if (launcher.closeButtonColor) {
303
+ closeButton.style.color = launcher.closeButtonColor;
304
+ closeButton.classList.remove("tvw-text-cw-muted");
305
+ } else {
306
+ closeButton.style.color = "";
307
+ closeButton.classList.add("tvw-text-cw-muted");
308
+ }
309
+
310
+ if (launcher.closeButtonBackgroundColor) {
311
+ closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
312
+ closeButton.classList.remove("hover:tvw-bg-gray-100");
313
+ } else {
314
+ closeButton.style.backgroundColor = "";
315
+ closeButton.classList.add("hover:tvw-bg-gray-100");
316
+ }
317
+
318
+ // Apply border if width and/or color are provided
319
+ if (launcher.closeButtonBorderWidth || launcher.closeButtonBorderColor) {
320
+ const borderWidth = launcher.closeButtonBorderWidth || "0px";
321
+ const borderColor = launcher.closeButtonBorderColor || "transparent";
322
+ closeButton.style.border = `${borderWidth} solid ${borderColor}`;
323
+ closeButton.classList.remove("tvw-border-none");
324
+ } else {
325
+ closeButton.style.border = "";
326
+ closeButton.classList.add("tvw-border-none");
327
+ }
328
+
329
+ if (launcher.closeButtonBorderRadius) {
330
+ closeButton.style.borderRadius = launcher.closeButtonBorderRadius;
331
+ closeButton.classList.remove("tvw-rounded-full");
332
+ } else {
333
+ closeButton.style.borderRadius = "";
334
+ closeButton.classList.add("tvw-rounded-full");
335
+ }
336
+
337
+ // Apply padding styling
338
+ if (launcher.closeButtonPaddingX) {
339
+ closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
340
+ closeButton.style.paddingRight = launcher.closeButtonPaddingX;
341
+ } else {
342
+ closeButton.style.paddingLeft = "";
343
+ closeButton.style.paddingRight = "";
344
+ }
345
+ if (launcher.closeButtonPaddingY) {
346
+ closeButton.style.paddingTop = launcher.closeButtonPaddingY;
347
+ closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
348
+ } else {
349
+ closeButton.style.paddingTop = "";
350
+ closeButton.style.paddingBottom = "";
351
+ }
352
+
353
+ closeButtonWrapper.appendChild(closeButton);
354
+
355
+ // Add tooltip with portaling to document.body to escape overflow clipping
356
+ if (closeButtonShowTooltip && closeButtonTooltipText) {
357
+ let portaledTooltip: HTMLElement | null = null;
358
+
359
+ const showTooltip = () => {
360
+ if (portaledTooltip) return; // Already showing
361
+
362
+ // Create tooltip element
363
+ portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
364
+ portaledTooltip.textContent = closeButtonTooltipText;
365
+
366
+ // Add arrow
367
+ const arrow = createElement("div");
368
+ arrow.className = "tvw-clear-chat-tooltip-arrow";
369
+ portaledTooltip.appendChild(arrow);
370
+
371
+ // Get button position
372
+ const buttonRect = closeButton.getBoundingClientRect();
373
+
374
+ // Position tooltip above button
375
+ portaledTooltip.style.position = "fixed";
376
+ portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
377
+ portaledTooltip.style.top = `${buttonRect.top - 8}px`;
378
+ portaledTooltip.style.transform = "translate(-50%, -100%)";
379
+
380
+ // Append to body
381
+ document.body.appendChild(portaledTooltip);
382
+ };
383
+
384
+ const hideTooltip = () => {
385
+ if (portaledTooltip && portaledTooltip.parentNode) {
386
+ portaledTooltip.parentNode.removeChild(portaledTooltip);
387
+ portaledTooltip = null;
388
+ }
389
+ };
390
+
391
+ // Add event listeners
392
+ closeButtonWrapper.addEventListener("mouseenter", showTooltip);
393
+ closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
394
+ closeButton.addEventListener("focus", showTooltip);
395
+ closeButton.addEventListener("blur", hideTooltip);
396
+
397
+ // Store cleanup function on the wrapper for later use
398
+ (closeButtonWrapper as any)._cleanupTooltip = () => {
399
+ hideTooltip();
400
+ closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
401
+ closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
402
+ closeButton.removeEventListener("focus", showTooltip);
403
+ closeButton.removeEventListener("blur", hideTooltip);
404
+ };
405
+ }
406
+
407
+ // Inline placement: append close button to header
408
+ if (closeButtonPlacement !== "top-right") {
409
+ header.appendChild(closeButtonWrapper);
410
+ }
411
+
412
+ return {
413
+ header,
414
+ iconHolder,
415
+ headerTitle: title,
416
+ headerSubtitle: subtitle,
417
+ closeButton,
418
+ closeButtonWrapper,
419
+ clearChatButton,
420
+ clearChatButtonWrapper
421
+ };
422
+ };
423
+
424
+ /**
425
+ * Attach header elements to the container, handling placement modes.
426
+ */
427
+ export const attachHeaderToContainer = (
428
+ container: HTMLElement,
429
+ headerElements: HeaderElements,
430
+ config?: AgentWidgetConfig
431
+ ): void => {
432
+ const launcher = config?.launcher ?? {};
433
+ const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
434
+ const clearChatPlacement = launcher.clearChat?.placement ?? "inline";
435
+
436
+ // Add header to container
437
+ container.appendChild(headerElements.header);
438
+
439
+ // Position close button wrapper if top-right placement
440
+ if (closeButtonPlacement === "top-right") {
441
+ container.style.position = "relative";
442
+ container.appendChild(headerElements.closeButtonWrapper);
443
+ }
444
+
445
+ // Position clear chat button wrapper if top-right placement
446
+ if (
447
+ headerElements.clearChatButtonWrapper &&
448
+ clearChatPlacement === "top-right"
449
+ ) {
450
+ container.style.position = "relative";
451
+ container.appendChild(headerElements.clearChatButtonWrapper);
452
+ }
453
+ };
454
+