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.
- package/README.md +57 -8
- package/dist/index.cjs +46 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +305 -1
- package/dist/index.d.ts +305 -1
- package/dist/index.global.js +69 -30
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer-builder.ts +366 -0
- package/src/components/header-builder.ts +454 -0
- package/src/components/header-layouts.ts +303 -0
- package/src/components/message-bubble.ts +251 -34
- package/src/components/panel.ts +46 -684
- package/src/defaults.ts +49 -1
- package/src/index.ts +43 -1
- package/src/runtime/init.ts +26 -0
- package/src/types.ts +231 -0
- package/src/ui.ts +452 -34
package/src/components/panel.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import { renderLucideIcon } from "../utils/icons";
|
|
3
2
|
import { AgentWidgetConfig } from "../types";
|
|
4
3
|
import { positionMap } from "../utils/positioning";
|
|
4
|
+
import { buildHeader, attachHeaderToContainer, HeaderElements } from "./header-builder";
|
|
5
|
+
import { buildHeaderWithLayout } from "./header-layouts";
|
|
6
|
+
import { buildComposer, ComposerElements } from "./composer-builder";
|
|
5
7
|
|
|
6
8
|
export interface PanelWrapper {
|
|
7
9
|
wrapper: HTMLElement;
|
|
@@ -12,13 +14,15 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
|
|
|
12
14
|
const launcherEnabled = config?.launcher?.enabled ?? true;
|
|
13
15
|
|
|
14
16
|
if (!launcherEnabled) {
|
|
17
|
+
// For inline embed mode, use flex layout to ensure the widget fills its container
|
|
18
|
+
// and only the chat messages area scrolls
|
|
15
19
|
const wrapper = createElement(
|
|
16
20
|
"div",
|
|
17
|
-
"tvw-relative tvw-w-full tvw-h-full"
|
|
21
|
+
"tvw-relative tvw-w-full tvw-h-full tvw-flex tvw-flex-col tvw-flex-1 tvw-min-h-0"
|
|
18
22
|
);
|
|
19
23
|
const panel = createElement(
|
|
20
24
|
"div",
|
|
21
|
-
"tvw-relative tvw-w-full tvw-
|
|
25
|
+
"tvw-relative tvw-w-full tvw-flex-1 tvw-flex tvw-flex-col tvw-min-h-0"
|
|
22
26
|
);
|
|
23
27
|
wrapper.appendChild(panel);
|
|
24
28
|
return { wrapper, panel };
|
|
@@ -69,383 +73,32 @@ export interface PanelElements {
|
|
|
69
73
|
iconHolder: HTMLElement;
|
|
70
74
|
headerTitle: HTMLElement;
|
|
71
75
|
headerSubtitle: HTMLElement;
|
|
76
|
+
// Exposed for potential header replacement
|
|
77
|
+
header: HTMLElement;
|
|
78
|
+
footer: HTMLElement;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelElements => {
|
|
82
|
+
// Use flex-1 and min-h-0 to ensure the container fills its parent and allows
|
|
83
|
+
// the body (chat messages area) to scroll while header/footer stay fixed
|
|
75
84
|
const container = createElement(
|
|
76
85
|
"div",
|
|
77
|
-
"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"
|
|
86
|
+
"tvw-flex tvw-h-full tvw-w-full tvw-flex-1 tvw-min-h-0 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"
|
|
78
87
|
);
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const launcher = config?.launcher ?? {};
|
|
86
|
-
const headerIconSize = launcher.headerIconSize ?? "48px";
|
|
87
|
-
const closeButtonSize = launcher.closeButtonSize ?? "32px";
|
|
88
|
-
const closeButtonPlacement = launcher.closeButtonPlacement ?? "inline";
|
|
89
|
-
const headerIconHidden = launcher.headerIconHidden ?? false;
|
|
90
|
-
const headerIconName = launcher.headerIconName;
|
|
91
|
-
|
|
92
|
-
const iconHolder = createElement(
|
|
93
|
-
"div",
|
|
94
|
-
"tvw-flex tvw-items-center tvw-justify-center tvw-rounded-xl tvw-bg-cw-primary tvw-text-white tvw-text-xl"
|
|
95
|
-
);
|
|
96
|
-
iconHolder.style.height = headerIconSize;
|
|
97
|
-
iconHolder.style.width = headerIconSize;
|
|
98
|
-
|
|
99
|
-
// Render icon based on priority: Lucide icon > iconUrl > agentIconText
|
|
100
|
-
if (!headerIconHidden) {
|
|
101
|
-
if (headerIconName) {
|
|
102
|
-
// Use Lucide icon
|
|
103
|
-
const iconSize = parseFloat(headerIconSize) || 24;
|
|
104
|
-
const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.6, "#ffffff", 2);
|
|
105
|
-
if (iconSvg) {
|
|
106
|
-
iconHolder.replaceChildren(iconSvg);
|
|
107
|
-
} else {
|
|
108
|
-
// Fallback to agentIconText if Lucide icon fails
|
|
109
|
-
iconHolder.textContent = config?.launcher?.agentIconText ?? "💬";
|
|
110
|
-
}
|
|
111
|
-
} else if (config?.launcher?.iconUrl) {
|
|
112
|
-
// Use image URL
|
|
113
|
-
const img = createElement("img") as HTMLImageElement;
|
|
114
|
-
img.src = config.launcher.iconUrl;
|
|
115
|
-
img.alt = "";
|
|
116
|
-
img.className = "tvw-rounded-xl tvw-object-cover";
|
|
117
|
-
img.style.height = headerIconSize;
|
|
118
|
-
img.style.width = headerIconSize;
|
|
119
|
-
iconHolder.replaceChildren(img);
|
|
120
|
-
} else {
|
|
121
|
-
// Use text/emoji
|
|
122
|
-
iconHolder.textContent = config?.launcher?.agentIconText ?? "💬";
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const headerCopy = createElement("div", "tvw-flex tvw-flex-col");
|
|
127
|
-
const title = createElement(
|
|
128
|
-
"span",
|
|
129
|
-
"tvw-text-base tvw-font-semibold"
|
|
130
|
-
);
|
|
131
|
-
title.textContent =
|
|
132
|
-
config?.launcher?.title ?? "Chat Assistant";
|
|
133
|
-
const subtitle = createElement(
|
|
134
|
-
"span",
|
|
135
|
-
"tvw-text-xs tvw-text-cw-muted"
|
|
136
|
-
);
|
|
137
|
-
subtitle.textContent =
|
|
138
|
-
config?.launcher?.subtitle ?? "Here to help you get answers fast";
|
|
139
|
-
|
|
140
|
-
headerCopy.append(title, subtitle);
|
|
141
|
-
|
|
142
|
-
// Only append iconHolder if not hidden
|
|
143
|
-
if (!headerIconHidden) {
|
|
144
|
-
header.append(iconHolder, headerCopy);
|
|
145
|
-
} else {
|
|
146
|
-
header.append(headerCopy);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Create clear chat button if enabled
|
|
150
|
-
const clearChatConfig = launcher.clearChat ?? {};
|
|
151
|
-
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
152
|
-
let clearChatButton: HTMLButtonElement | null = null;
|
|
153
|
-
let clearChatButtonWrapper: HTMLElement | null = null;
|
|
154
|
-
|
|
155
|
-
if (clearChatEnabled) {
|
|
156
|
-
const clearChatSize = clearChatConfig.size ?? "32px";
|
|
157
|
-
const clearChatIconName = clearChatConfig.iconName ?? "refresh-cw";
|
|
158
|
-
const clearChatIconColor = clearChatConfig.iconColor ?? "";
|
|
159
|
-
const clearChatBgColor = clearChatConfig.backgroundColor ?? "";
|
|
160
|
-
const clearChatBorderWidth = clearChatConfig.borderWidth ?? "";
|
|
161
|
-
const clearChatBorderColor = clearChatConfig.borderColor ?? "";
|
|
162
|
-
const clearChatBorderRadius = clearChatConfig.borderRadius ?? "";
|
|
163
|
-
const clearChatPaddingX = clearChatConfig.paddingX ?? "";
|
|
164
|
-
const clearChatPaddingY = clearChatConfig.paddingY ?? "";
|
|
165
|
-
const clearChatTooltipText = clearChatConfig.tooltipText ?? "Clear chat";
|
|
166
|
-
const clearChatShowTooltip = clearChatConfig.showTooltip ?? true;
|
|
167
|
-
|
|
168
|
-
// Create button wrapper for tooltip
|
|
169
|
-
clearChatButtonWrapper = createElement(
|
|
170
|
-
"div",
|
|
171
|
-
"tvw-relative tvw-ml-auto tvw-clear-chat-button-wrapper"
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
clearChatButton = createElement(
|
|
175
|
-
"button",
|
|
176
|
-
"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"
|
|
177
|
-
) as HTMLButtonElement;
|
|
178
|
-
|
|
179
|
-
clearChatButton.style.height = clearChatSize;
|
|
180
|
-
clearChatButton.style.width = clearChatSize;
|
|
181
|
-
clearChatButton.type = "button";
|
|
182
|
-
clearChatButton.setAttribute("aria-label", clearChatTooltipText);
|
|
183
|
-
|
|
184
|
-
// Add icon
|
|
185
|
-
const iconSvg = renderLucideIcon(clearChatIconName, "20px", clearChatIconColor || "", 2);
|
|
186
|
-
if (iconSvg) {
|
|
187
|
-
clearChatButton.appendChild(iconSvg);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Apply styling from config
|
|
191
|
-
if (clearChatIconColor) {
|
|
192
|
-
clearChatButton.style.color = clearChatIconColor;
|
|
193
|
-
clearChatButton.classList.remove("tvw-text-cw-muted");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (clearChatBgColor) {
|
|
197
|
-
clearChatButton.style.backgroundColor = clearChatBgColor;
|
|
198
|
-
clearChatButton.classList.remove("hover:tvw-bg-gray-100");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (clearChatBorderWidth || clearChatBorderColor) {
|
|
202
|
-
const borderWidth = clearChatBorderWidth || "0px";
|
|
203
|
-
const borderColor = clearChatBorderColor || "transparent";
|
|
204
|
-
clearChatButton.style.border = `${borderWidth} solid ${borderColor}`;
|
|
205
|
-
clearChatButton.classList.remove("tvw-border-none");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (clearChatBorderRadius) {
|
|
209
|
-
clearChatButton.style.borderRadius = clearChatBorderRadius;
|
|
210
|
-
clearChatButton.classList.remove("tvw-rounded-full");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Apply padding styling
|
|
214
|
-
if (clearChatPaddingX) {
|
|
215
|
-
clearChatButton.style.paddingLeft = clearChatPaddingX;
|
|
216
|
-
clearChatButton.style.paddingRight = clearChatPaddingX;
|
|
217
|
-
} else {
|
|
218
|
-
clearChatButton.style.paddingLeft = "";
|
|
219
|
-
clearChatButton.style.paddingRight = "";
|
|
220
|
-
}
|
|
221
|
-
if (clearChatPaddingY) {
|
|
222
|
-
clearChatButton.style.paddingTop = clearChatPaddingY;
|
|
223
|
-
clearChatButton.style.paddingBottom = clearChatPaddingY;
|
|
224
|
-
} else {
|
|
225
|
-
clearChatButton.style.paddingTop = "";
|
|
226
|
-
clearChatButton.style.paddingBottom = "";
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
clearChatButtonWrapper.appendChild(clearChatButton);
|
|
230
|
-
|
|
231
|
-
// Add tooltip with portaling to document.body to escape overflow clipping
|
|
232
|
-
if (clearChatShowTooltip && clearChatTooltipText && clearChatButton && clearChatButtonWrapper) {
|
|
233
|
-
let portaledTooltip: HTMLElement | null = null;
|
|
234
|
-
|
|
235
|
-
const showTooltip = () => {
|
|
236
|
-
if (portaledTooltip || !clearChatButton) return; // Already showing or button doesn't exist
|
|
237
|
-
|
|
238
|
-
// Create tooltip element
|
|
239
|
-
portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
|
|
240
|
-
portaledTooltip.textContent = clearChatTooltipText;
|
|
241
|
-
|
|
242
|
-
// Add arrow
|
|
243
|
-
const arrow = createElement("div");
|
|
244
|
-
arrow.className = "tvw-clear-chat-tooltip-arrow";
|
|
245
|
-
portaledTooltip.appendChild(arrow);
|
|
246
|
-
|
|
247
|
-
// Get button position
|
|
248
|
-
const buttonRect = clearChatButton.getBoundingClientRect();
|
|
249
|
-
|
|
250
|
-
// Position tooltip above button
|
|
251
|
-
portaledTooltip.style.position = "fixed";
|
|
252
|
-
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
253
|
-
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
254
|
-
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
255
|
-
|
|
256
|
-
// Append to body
|
|
257
|
-
document.body.appendChild(portaledTooltip);
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const hideTooltip = () => {
|
|
261
|
-
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
262
|
-
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
263
|
-
portaledTooltip = null;
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
// Add event listeners
|
|
268
|
-
clearChatButtonWrapper.addEventListener("mouseenter", showTooltip);
|
|
269
|
-
clearChatButtonWrapper.addEventListener("mouseleave", hideTooltip);
|
|
270
|
-
clearChatButton.addEventListener("focus", showTooltip);
|
|
271
|
-
clearChatButton.addEventListener("blur", hideTooltip);
|
|
272
|
-
|
|
273
|
-
// Store cleanup function on the button for later use
|
|
274
|
-
(clearChatButtonWrapper as any)._cleanupTooltip = () => {
|
|
275
|
-
hideTooltip();
|
|
276
|
-
if (clearChatButtonWrapper) {
|
|
277
|
-
clearChatButtonWrapper.removeEventListener("mouseenter", showTooltip);
|
|
278
|
-
clearChatButtonWrapper.removeEventListener("mouseleave", hideTooltip);
|
|
279
|
-
}
|
|
280
|
-
if (clearChatButton) {
|
|
281
|
-
clearChatButton.removeEventListener("focus", showTooltip);
|
|
282
|
-
clearChatButton.removeEventListener("blur", hideTooltip);
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
header.appendChild(clearChatButtonWrapper);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Create close button wrapper for tooltip positioning
|
|
291
|
-
const closeButtonWrapper = createElement(
|
|
292
|
-
"div",
|
|
293
|
-
closeButtonPlacement === "top-right"
|
|
294
|
-
? "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50"
|
|
295
|
-
: (clearChatEnabled
|
|
296
|
-
? ""
|
|
297
|
-
: "tvw-ml-auto")
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
// Create close button with base classes
|
|
301
|
-
const closeButton = createElement(
|
|
302
|
-
"button",
|
|
303
|
-
"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"
|
|
304
|
-
) as HTMLButtonElement;
|
|
305
|
-
closeButton.style.height = closeButtonSize;
|
|
306
|
-
closeButton.style.width = closeButtonSize;
|
|
307
|
-
closeButton.type = "button";
|
|
308
|
-
|
|
309
|
-
// Get tooltip config
|
|
310
|
-
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
311
|
-
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
312
|
-
|
|
313
|
-
closeButton.setAttribute("aria-label", closeButtonTooltipText);
|
|
314
|
-
closeButton.style.display = showClose ? "" : "none";
|
|
315
|
-
|
|
316
|
-
// Add icon or fallback text
|
|
317
|
-
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
318
|
-
const closeButtonIconText = launcher.closeButtonIconText ?? "×";
|
|
319
|
-
|
|
320
|
-
// Try to render Lucide icon, fallback to text if not provided or fails
|
|
321
|
-
const iconSvg = renderLucideIcon(closeButtonIconName, "20px", launcher.closeButtonColor || "", 2);
|
|
322
|
-
if (iconSvg) {
|
|
323
|
-
closeButton.appendChild(iconSvg);
|
|
324
|
-
} else {
|
|
325
|
-
closeButton.textContent = closeButtonIconText;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Apply close button styling from config
|
|
329
|
-
if (launcher.closeButtonColor) {
|
|
330
|
-
closeButton.style.color = launcher.closeButtonColor;
|
|
331
|
-
closeButton.classList.remove("tvw-text-cw-muted");
|
|
332
|
-
} else {
|
|
333
|
-
closeButton.style.color = "";
|
|
334
|
-
closeButton.classList.add("tvw-text-cw-muted");
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (launcher.closeButtonBackgroundColor) {
|
|
338
|
-
closeButton.style.backgroundColor = launcher.closeButtonBackgroundColor;
|
|
339
|
-
closeButton.classList.remove("hover:tvw-bg-gray-100");
|
|
340
|
-
} else {
|
|
341
|
-
closeButton.style.backgroundColor = "";
|
|
342
|
-
closeButton.classList.add("hover:tvw-bg-gray-100");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Apply border if width and/or color are provided
|
|
346
|
-
if (launcher.closeButtonBorderWidth || launcher.closeButtonBorderColor) {
|
|
347
|
-
const borderWidth = launcher.closeButtonBorderWidth || "0px";
|
|
348
|
-
const borderColor = launcher.closeButtonBorderColor || "transparent";
|
|
349
|
-
closeButton.style.border = `${borderWidth} solid ${borderColor}`;
|
|
350
|
-
closeButton.classList.remove("tvw-border-none");
|
|
351
|
-
} else {
|
|
352
|
-
closeButton.style.border = "";
|
|
353
|
-
closeButton.classList.add("tvw-border-none");
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (launcher.closeButtonBorderRadius) {
|
|
357
|
-
closeButton.style.borderRadius = launcher.closeButtonBorderRadius;
|
|
358
|
-
closeButton.classList.remove("tvw-rounded-full");
|
|
359
|
-
} else {
|
|
360
|
-
closeButton.style.borderRadius = "";
|
|
361
|
-
closeButton.classList.add("tvw-rounded-full");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Apply padding styling
|
|
365
|
-
if (launcher.closeButtonPaddingX) {
|
|
366
|
-
closeButton.style.paddingLeft = launcher.closeButtonPaddingX;
|
|
367
|
-
closeButton.style.paddingRight = launcher.closeButtonPaddingX;
|
|
368
|
-
} else {
|
|
369
|
-
closeButton.style.paddingLeft = "";
|
|
370
|
-
closeButton.style.paddingRight = "";
|
|
371
|
-
}
|
|
372
|
-
if (launcher.closeButtonPaddingY) {
|
|
373
|
-
closeButton.style.paddingTop = launcher.closeButtonPaddingY;
|
|
374
|
-
closeButton.style.paddingBottom = launcher.closeButtonPaddingY;
|
|
375
|
-
} else {
|
|
376
|
-
closeButton.style.paddingTop = "";
|
|
377
|
-
closeButton.style.paddingBottom = "";
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
closeButtonWrapper.appendChild(closeButton);
|
|
381
|
-
|
|
382
|
-
// Add tooltip with portaling to document.body to escape overflow clipping
|
|
383
|
-
if (closeButtonShowTooltip && closeButtonTooltipText) {
|
|
384
|
-
let portaledTooltip: HTMLElement | null = null;
|
|
385
|
-
|
|
386
|
-
const showTooltip = () => {
|
|
387
|
-
if (portaledTooltip) return; // Already showing
|
|
388
|
-
|
|
389
|
-
// Create tooltip element
|
|
390
|
-
portaledTooltip = createElement("div", "tvw-clear-chat-tooltip");
|
|
391
|
-
portaledTooltip.textContent = closeButtonTooltipText;
|
|
392
|
-
|
|
393
|
-
// Add arrow
|
|
394
|
-
const arrow = createElement("div");
|
|
395
|
-
arrow.className = "tvw-clear-chat-tooltip-arrow";
|
|
396
|
-
portaledTooltip.appendChild(arrow);
|
|
397
|
-
|
|
398
|
-
// Get button position
|
|
399
|
-
const buttonRect = closeButton.getBoundingClientRect();
|
|
400
|
-
|
|
401
|
-
// Position tooltip above button
|
|
402
|
-
portaledTooltip.style.position = "fixed";
|
|
403
|
-
portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
|
|
404
|
-
portaledTooltip.style.top = `${buttonRect.top - 8}px`;
|
|
405
|
-
portaledTooltip.style.transform = "translate(-50%, -100%)";
|
|
406
|
-
|
|
407
|
-
// Append to body
|
|
408
|
-
document.body.appendChild(portaledTooltip);
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const hideTooltip = () => {
|
|
412
|
-
if (portaledTooltip && portaledTooltip.parentNode) {
|
|
413
|
-
portaledTooltip.parentNode.removeChild(portaledTooltip);
|
|
414
|
-
portaledTooltip = null;
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// Add event listeners
|
|
419
|
-
closeButtonWrapper.addEventListener("mouseenter", showTooltip);
|
|
420
|
-
closeButtonWrapper.addEventListener("mouseleave", hideTooltip);
|
|
421
|
-
closeButton.addEventListener("focus", showTooltip);
|
|
422
|
-
closeButton.addEventListener("blur", hideTooltip);
|
|
423
|
-
|
|
424
|
-
// Store cleanup function on the wrapper for later use
|
|
425
|
-
(closeButtonWrapper as any)._cleanupTooltip = () => {
|
|
426
|
-
hideTooltip();
|
|
427
|
-
closeButtonWrapper.removeEventListener("mouseenter", showTooltip);
|
|
428
|
-
closeButtonWrapper.removeEventListener("mouseleave", hideTooltip);
|
|
429
|
-
closeButton.removeEventListener("focus", showTooltip);
|
|
430
|
-
closeButton.removeEventListener("blur", hideTooltip);
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Position close button wrapper based on placement
|
|
435
|
-
if (closeButtonPlacement === "top-right") {
|
|
436
|
-
// Make container position relative for absolute positioning
|
|
437
|
-
container.style.position = "relative";
|
|
438
|
-
container.appendChild(closeButtonWrapper);
|
|
439
|
-
} else {
|
|
440
|
-
// Inline placement: append to header
|
|
441
|
-
header.appendChild(closeButtonWrapper);
|
|
442
|
-
}
|
|
89
|
+
// Build header using layout config if available, otherwise use standard builder
|
|
90
|
+
const headerLayoutConfig = config?.layout?.header;
|
|
91
|
+
const headerElements: HeaderElements = headerLayoutConfig
|
|
92
|
+
? buildHeaderWithLayout(config!, headerLayoutConfig, { showClose })
|
|
93
|
+
: buildHeader({ config, showClose });
|
|
443
94
|
|
|
95
|
+
// Build body with intro card and messages wrapper
|
|
444
96
|
const body = createElement(
|
|
445
97
|
"div",
|
|
446
98
|
"tvw-flex tvw-flex-1 tvw-min-h-0 tvw-flex-col tvw-gap-6 tvw-overflow-y-auto tvw-bg-cw-container tvw-px-6 tvw-py-6"
|
|
447
99
|
);
|
|
448
100
|
body.id = "vanilla-agent-scroll-container";
|
|
101
|
+
|
|
449
102
|
const introCard = createElement(
|
|
450
103
|
"div",
|
|
451
104
|
"tvw-rounded-2xl tvw-bg-cw-surface tvw-p-6 tvw-shadow-sm"
|
|
@@ -471,331 +124,40 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
|
|
|
471
124
|
|
|
472
125
|
body.append(introCard, messagesWrapper);
|
|
473
126
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
"tvw-border-t-cw-divider tvw-bg-cw-surface tvw-px-6 tvw-py-4"
|
|
477
|
-
);
|
|
478
|
-
const suggestions = createElement(
|
|
479
|
-
"div",
|
|
480
|
-
"tvw-mb-3 tvw-flex tvw-flex-wrap tvw-gap-2"
|
|
481
|
-
);
|
|
482
|
-
// Determine gap based on voice recognition
|
|
483
|
-
const voiceRecognitionEnabledForGap = config?.voiceRecognition?.enabled === true;
|
|
484
|
-
const hasSpeechRecognitionForGap =
|
|
485
|
-
typeof window !== 'undefined' &&
|
|
486
|
-
(typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
|
|
487
|
-
typeof (window as any).SpeechRecognition !== 'undefined');
|
|
488
|
-
const shouldUseSmallGap = voiceRecognitionEnabledForGap && hasSpeechRecognitionForGap;
|
|
489
|
-
const gapClass = shouldUseSmallGap ? "tvw-gap-1" : "tvw-gap-3";
|
|
490
|
-
|
|
491
|
-
const composerForm = createElement(
|
|
492
|
-
"form",
|
|
493
|
-
`tvw-flex tvw-items-end ${gapClass} tvw-rounded-2xl tvw-border tvw-border-gray-200 tvw-bg-cw-input-background tvw-px-4 tvw-py-3`
|
|
494
|
-
) as HTMLFormElement;
|
|
495
|
-
// Prevent form from getting focus styles
|
|
496
|
-
composerForm.style.outline = "none";
|
|
497
|
-
|
|
498
|
-
const textarea = createElement("textarea") as HTMLTextAreaElement;
|
|
499
|
-
textarea.placeholder = config?.copy?.inputPlaceholder ?? "Type your message…";
|
|
500
|
-
textarea.className =
|
|
501
|
-
"tvw-min-h-[48px] tvw-flex-1 tvw-resize-none tvw-border-none tvw-bg-transparent tvw-text-sm tvw-text-cw-primary focus:tvw-outline-none focus:tvw-border-none";
|
|
502
|
-
textarea.rows = 1;
|
|
503
|
-
|
|
504
|
-
// Apply font family and weight from config
|
|
505
|
-
const fontFamily = config?.theme?.inputFontFamily ?? "sans-serif";
|
|
506
|
-
const fontWeight = config?.theme?.inputFontWeight ?? "400";
|
|
507
|
-
|
|
508
|
-
const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => {
|
|
509
|
-
switch (family) {
|
|
510
|
-
case "serif":
|
|
511
|
-
return 'Georgia, "Times New Roman", Times, serif';
|
|
512
|
-
case "mono":
|
|
513
|
-
return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
|
|
514
|
-
case "sans-serif":
|
|
515
|
-
default:
|
|
516
|
-
return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
textarea.style.fontFamily = getFontFamilyValue(fontFamily);
|
|
521
|
-
textarea.style.fontWeight = fontWeight;
|
|
522
|
-
|
|
523
|
-
// Explicitly remove border and outline on focus to prevent browser defaults
|
|
524
|
-
textarea.style.border = "none";
|
|
525
|
-
textarea.style.outline = "none";
|
|
526
|
-
textarea.style.borderWidth = "0";
|
|
527
|
-
textarea.style.borderStyle = "none";
|
|
528
|
-
textarea.style.borderColor = "transparent";
|
|
529
|
-
textarea.addEventListener("focus", () => {
|
|
530
|
-
textarea.style.border = "none";
|
|
531
|
-
textarea.style.outline = "none";
|
|
532
|
-
textarea.style.borderWidth = "0";
|
|
533
|
-
textarea.style.borderStyle = "none";
|
|
534
|
-
textarea.style.borderColor = "transparent";
|
|
535
|
-
textarea.style.boxShadow = "none";
|
|
536
|
-
});
|
|
537
|
-
textarea.addEventListener("blur", () => {
|
|
538
|
-
textarea.style.border = "none";
|
|
539
|
-
textarea.style.outline = "none";
|
|
540
|
-
});
|
|
541
|
-
// Send button configuration
|
|
542
|
-
const sendButtonConfig = config?.sendButton ?? {};
|
|
543
|
-
const useIcon = sendButtonConfig.useIcon ?? false;
|
|
544
|
-
const iconText = sendButtonConfig.iconText ?? "↑";
|
|
545
|
-
const iconName = sendButtonConfig.iconName;
|
|
546
|
-
const tooltipText = sendButtonConfig.tooltipText ?? "Send message";
|
|
547
|
-
const showTooltip = sendButtonConfig.showTooltip ?? false;
|
|
548
|
-
const buttonSize = sendButtonConfig.size ?? "40px";
|
|
549
|
-
const backgroundColor = sendButtonConfig.backgroundColor;
|
|
550
|
-
const textColor = sendButtonConfig.textColor;
|
|
127
|
+
// Build composer/footer using extracted builder
|
|
128
|
+
const composerElements: ComposerElements = buildComposer({ config });
|
|
551
129
|
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const sendButton = createElement(
|
|
556
|
-
"button",
|
|
557
|
-
useIcon
|
|
558
|
-
? "tvw-rounded-button tvw-flex tvw-items-center tvw-justify-center disabled:tvw-opacity-50 tvw-cursor-pointer"
|
|
559
|
-
: "tvw-rounded-button tvw-bg-cw-accent tvw-px-4 tvw-py-2 tvw-text-sm tvw-font-semibold disabled:tvw-opacity-50 tvw-cursor-pointer"
|
|
560
|
-
) as HTMLButtonElement;
|
|
561
|
-
|
|
562
|
-
sendButton.type = "submit";
|
|
563
|
-
|
|
564
|
-
if (useIcon) {
|
|
565
|
-
// Icon mode: circular button
|
|
566
|
-
sendButton.style.width = buttonSize;
|
|
567
|
-
sendButton.style.height = buttonSize;
|
|
568
|
-
sendButton.style.minWidth = buttonSize;
|
|
569
|
-
sendButton.style.minHeight = buttonSize;
|
|
570
|
-
sendButton.style.fontSize = "18px";
|
|
571
|
-
sendButton.style.lineHeight = "1";
|
|
572
|
-
|
|
573
|
-
// Clear any existing content
|
|
574
|
-
sendButton.innerHTML = "";
|
|
575
|
-
|
|
576
|
-
// Use Lucide icon if iconName is provided, otherwise fall back to iconText
|
|
577
|
-
if (iconName) {
|
|
578
|
-
const iconSize = parseFloat(buttonSize) || 24;
|
|
579
|
-
const iconColor = textColor && typeof textColor === 'string' && textColor.trim() ? textColor.trim() : "currentColor";
|
|
580
|
-
const iconSvg = renderLucideIcon(iconName, iconSize, iconColor, 2);
|
|
581
|
-
if (iconSvg) {
|
|
582
|
-
sendButton.appendChild(iconSvg);
|
|
583
|
-
sendButton.style.color = iconColor;
|
|
584
|
-
} else {
|
|
585
|
-
// Fallback to text if icon fails to render
|
|
586
|
-
sendButton.textContent = iconText;
|
|
587
|
-
if (textColor) {
|
|
588
|
-
sendButton.style.color = textColor;
|
|
589
|
-
} else {
|
|
590
|
-
sendButton.classList.add("tvw-text-white");
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
} else {
|
|
594
|
-
sendButton.textContent = iconText;
|
|
595
|
-
if (textColor) {
|
|
596
|
-
sendButton.style.color = textColor;
|
|
597
|
-
} else {
|
|
598
|
-
sendButton.classList.add("tvw-text-white");
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (backgroundColor) {
|
|
603
|
-
sendButton.style.backgroundColor = backgroundColor;
|
|
604
|
-
} else {
|
|
605
|
-
sendButton.classList.add("tvw-bg-cw-primary");
|
|
606
|
-
}
|
|
607
|
-
} else {
|
|
608
|
-
// Text mode: existing behavior
|
|
609
|
-
sendButton.textContent = config?.copy?.sendButtonLabel ?? "Send";
|
|
610
|
-
if (textColor) {
|
|
611
|
-
sendButton.style.color = textColor;
|
|
612
|
-
} else {
|
|
613
|
-
sendButton.classList.add("tvw-text-white");
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Apply existing styling from config
|
|
618
|
-
if (sendButtonConfig.borderWidth) {
|
|
619
|
-
sendButton.style.borderWidth = sendButtonConfig.borderWidth;
|
|
620
|
-
sendButton.style.borderStyle = "solid";
|
|
621
|
-
}
|
|
622
|
-
if (sendButtonConfig.borderColor) {
|
|
623
|
-
sendButton.style.borderColor = sendButtonConfig.borderColor;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Apply padding styling (works in both icon and text mode)
|
|
627
|
-
if (sendButtonConfig.paddingX) {
|
|
628
|
-
sendButton.style.paddingLeft = sendButtonConfig.paddingX;
|
|
629
|
-
sendButton.style.paddingRight = sendButtonConfig.paddingX;
|
|
630
|
-
} else {
|
|
631
|
-
sendButton.style.paddingLeft = "";
|
|
632
|
-
sendButton.style.paddingRight = "";
|
|
633
|
-
}
|
|
634
|
-
if (sendButtonConfig.paddingY) {
|
|
635
|
-
sendButton.style.paddingTop = sendButtonConfig.paddingY;
|
|
636
|
-
sendButton.style.paddingBottom = sendButtonConfig.paddingY;
|
|
637
|
-
} else {
|
|
638
|
-
sendButton.style.paddingTop = "";
|
|
639
|
-
sendButton.style.paddingBottom = "";
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Add tooltip if enabled
|
|
643
|
-
if (showTooltip && tooltipText) {
|
|
644
|
-
const tooltip = createElement("div", "tvw-send-button-tooltip");
|
|
645
|
-
tooltip.textContent = tooltipText;
|
|
646
|
-
sendButtonWrapper.appendChild(tooltip);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
sendButtonWrapper.appendChild(sendButton);
|
|
650
|
-
|
|
651
|
-
// Voice recognition mic button
|
|
652
|
-
const voiceRecognitionConfig = config?.voiceRecognition ?? {};
|
|
653
|
-
const voiceRecognitionEnabled = voiceRecognitionConfig.enabled === true;
|
|
654
|
-
let micButton: HTMLButtonElement | null = null;
|
|
655
|
-
let micButtonWrapper: HTMLElement | null = null;
|
|
656
|
-
|
|
657
|
-
// Check browser support for speech recognition
|
|
658
|
-
const hasSpeechRecognition =
|
|
659
|
-
typeof window !== 'undefined' &&
|
|
660
|
-
(typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
|
|
661
|
-
typeof (window as any).SpeechRecognition !== 'undefined');
|
|
662
|
-
|
|
663
|
-
if (voiceRecognitionEnabled && hasSpeechRecognition) {
|
|
664
|
-
micButtonWrapper = createElement("div", "tvw-send-button-wrapper");
|
|
665
|
-
micButton = createElement(
|
|
666
|
-
"button",
|
|
667
|
-
"tvw-rounded-button tvw-flex tvw-items-center tvw-justify-center disabled:tvw-opacity-50 tvw-cursor-pointer"
|
|
668
|
-
) as HTMLButtonElement;
|
|
669
|
-
|
|
670
|
-
micButton.type = "button";
|
|
671
|
-
micButton.setAttribute("aria-label", "Start voice recognition");
|
|
672
|
-
|
|
673
|
-
const micIconName = voiceRecognitionConfig.iconName ?? "mic";
|
|
674
|
-
const micIconSize = voiceRecognitionConfig.iconSize ?? buttonSize;
|
|
675
|
-
const micIconSizeNum = parseFloat(micIconSize) || 24;
|
|
676
|
-
|
|
677
|
-
// Use dedicated colors from voice recognition config, fallback to send button colors
|
|
678
|
-
const micBackgroundColor = voiceRecognitionConfig.backgroundColor ?? backgroundColor;
|
|
679
|
-
const micIconColor = voiceRecognitionConfig.iconColor ?? textColor;
|
|
680
|
-
|
|
681
|
-
micButton.style.width = micIconSize;
|
|
682
|
-
micButton.style.height = micIconSize;
|
|
683
|
-
micButton.style.minWidth = micIconSize;
|
|
684
|
-
micButton.style.minHeight = micIconSize;
|
|
685
|
-
micButton.style.fontSize = "18px";
|
|
686
|
-
micButton.style.lineHeight = "1";
|
|
687
|
-
|
|
688
|
-
// Use Lucide mic icon with configured color (stroke width 1.5 for minimalist outline style)
|
|
689
|
-
const iconColorValue = micIconColor || "currentColor";
|
|
690
|
-
const micIconSvg = renderLucideIcon(micIconName, micIconSizeNum, iconColorValue, 1.5);
|
|
691
|
-
if (micIconSvg) {
|
|
692
|
-
micButton.appendChild(micIconSvg);
|
|
693
|
-
micButton.style.color = iconColorValue;
|
|
694
|
-
} else {
|
|
695
|
-
// Fallback to text if icon fails
|
|
696
|
-
micButton.textContent = "🎤";
|
|
697
|
-
micButton.style.color = iconColorValue;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Apply background color
|
|
701
|
-
if (micBackgroundColor) {
|
|
702
|
-
micButton.style.backgroundColor = micBackgroundColor;
|
|
703
|
-
} else {
|
|
704
|
-
micButton.classList.add("tvw-bg-cw-primary");
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Apply icon/text color
|
|
708
|
-
if (micIconColor) {
|
|
709
|
-
micButton.style.color = micIconColor;
|
|
710
|
-
} else if (!micIconColor && !textColor) {
|
|
711
|
-
micButton.classList.add("tvw-text-white");
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Apply border styling
|
|
715
|
-
if (voiceRecognitionConfig.borderWidth) {
|
|
716
|
-
micButton.style.borderWidth = voiceRecognitionConfig.borderWidth;
|
|
717
|
-
micButton.style.borderStyle = "solid";
|
|
718
|
-
}
|
|
719
|
-
if (voiceRecognitionConfig.borderColor) {
|
|
720
|
-
micButton.style.borderColor = voiceRecognitionConfig.borderColor;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Apply padding styling
|
|
724
|
-
if (voiceRecognitionConfig.paddingX) {
|
|
725
|
-
micButton.style.paddingLeft = voiceRecognitionConfig.paddingX;
|
|
726
|
-
micButton.style.paddingRight = voiceRecognitionConfig.paddingX;
|
|
727
|
-
}
|
|
728
|
-
if (voiceRecognitionConfig.paddingY) {
|
|
729
|
-
micButton.style.paddingTop = voiceRecognitionConfig.paddingY;
|
|
730
|
-
micButton.style.paddingBottom = voiceRecognitionConfig.paddingY;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
micButtonWrapper.appendChild(micButton);
|
|
734
|
-
|
|
735
|
-
// Add tooltip if enabled
|
|
736
|
-
const tooltipText = voiceRecognitionConfig.tooltipText ?? "Start voice recognition";
|
|
737
|
-
const showTooltip = voiceRecognitionConfig.showTooltip ?? false;
|
|
738
|
-
if (showTooltip && tooltipText) {
|
|
739
|
-
const tooltip = createElement("div", "tvw-send-button-tooltip");
|
|
740
|
-
tooltip.textContent = tooltipText;
|
|
741
|
-
micButtonWrapper.appendChild(tooltip);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Focus textarea when composer form container is clicked
|
|
746
|
-
composerForm.addEventListener("click", (e) => {
|
|
747
|
-
// Don't focus if clicking on the send button, mic button, or their wrappers
|
|
748
|
-
if (e.target !== sendButton && e.target !== sendButtonWrapper &&
|
|
749
|
-
e.target !== micButton && e.target !== micButtonWrapper) {
|
|
750
|
-
textarea.focus();
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
// Append elements: textarea, mic button (if exists), send button
|
|
755
|
-
composerForm.append(textarea);
|
|
756
|
-
if (micButtonWrapper) {
|
|
757
|
-
composerForm.append(micButtonWrapper);
|
|
758
|
-
}
|
|
759
|
-
composerForm.append(sendButtonWrapper);
|
|
760
|
-
|
|
761
|
-
const statusText = createElement(
|
|
762
|
-
"div",
|
|
763
|
-
"tvw-mt-2 tvw-text-right tvw-text-xs tvw-text-cw-muted"
|
|
764
|
-
);
|
|
765
|
-
|
|
766
|
-
// Apply status indicator config
|
|
767
|
-
const statusConfig = config?.statusIndicator ?? {};
|
|
768
|
-
const isVisible = statusConfig.visible ?? true;
|
|
769
|
-
statusText.style.display = isVisible ? "" : "none";
|
|
770
|
-
statusText.textContent = statusConfig.idleText ?? "Online";
|
|
771
|
-
|
|
772
|
-
footer.append(suggestions, composerForm, statusText);
|
|
773
|
-
|
|
774
|
-
container.append(header, body, footer);
|
|
130
|
+
// Assemble container with header, body, and footer
|
|
131
|
+
attachHeaderToContainer(container, headerElements, config);
|
|
132
|
+
container.append(body, composerElements.footer);
|
|
775
133
|
|
|
776
134
|
return {
|
|
777
135
|
container,
|
|
778
136
|
body,
|
|
779
137
|
messagesWrapper,
|
|
780
|
-
suggestions,
|
|
781
|
-
textarea,
|
|
782
|
-
sendButton,
|
|
783
|
-
sendButtonWrapper,
|
|
784
|
-
micButton,
|
|
785
|
-
micButtonWrapper,
|
|
786
|
-
composerForm,
|
|
787
|
-
statusText,
|
|
138
|
+
suggestions: composerElements.suggestions,
|
|
139
|
+
textarea: composerElements.textarea,
|
|
140
|
+
sendButton: composerElements.sendButton,
|
|
141
|
+
sendButtonWrapper: composerElements.sendButtonWrapper,
|
|
142
|
+
micButton: composerElements.micButton,
|
|
143
|
+
micButtonWrapper: composerElements.micButtonWrapper,
|
|
144
|
+
composerForm: composerElements.composerForm,
|
|
145
|
+
statusText: composerElements.statusText,
|
|
788
146
|
introTitle,
|
|
789
147
|
introSubtitle,
|
|
790
|
-
closeButton,
|
|
791
|
-
closeButtonWrapper,
|
|
792
|
-
clearChatButton,
|
|
793
|
-
clearChatButtonWrapper,
|
|
794
|
-
iconHolder,
|
|
795
|
-
headerTitle:
|
|
796
|
-
headerSubtitle:
|
|
148
|
+
closeButton: headerElements.closeButton,
|
|
149
|
+
closeButtonWrapper: headerElements.closeButtonWrapper,
|
|
150
|
+
clearChatButton: headerElements.clearChatButton,
|
|
151
|
+
clearChatButtonWrapper: headerElements.clearChatButtonWrapper,
|
|
152
|
+
iconHolder: headerElements.iconHolder,
|
|
153
|
+
headerTitle: headerElements.headerTitle,
|
|
154
|
+
headerSubtitle: headerElements.headerSubtitle,
|
|
155
|
+
header: headerElements.header,
|
|
156
|
+
footer: composerElements.footer
|
|
797
157
|
};
|
|
798
158
|
};
|
|
799
159
|
|
|
800
|
-
|
|
801
|
-
|
|
160
|
+
// Re-export builder types and functions for plugin use
|
|
161
|
+
export { buildHeader, buildComposer, attachHeaderToContainer };
|
|
162
|
+
export type { HeaderElements, HeaderBuildContext } from "./header-builder";
|
|
163
|
+
export type { ComposerElements, ComposerBuildContext } from "./composer-builder";
|