vanilla-agent 1.9.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 +303 -8
- package/dist/index.cjs +46 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +581 -1
- package/dist/index.d.ts +581 -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/client.ts +141 -12
- package/src/components/composer-builder.ts +366 -0
- package/src/components/forms.ts +1 -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/components/registry.ts +87 -0
- package/src/defaults.ts +49 -1
- package/src/index.ts +64 -2
- package/src/plugins/registry.ts +1 -0
- package/src/plugins/types.ts +1 -0
- package/src/runtime/init.ts +26 -0
- package/src/types.ts +381 -0
- package/src/ui.ts +521 -40
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/dom.ts +1 -0
- package/src/utils/formatting.ts +33 -8
- package/src/utils/positioning.ts +1 -0
- package/src/utils/theme.ts +1 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
3
|
+
import { AgentWidgetConfig, AgentWidgetHeaderLayoutConfig } from "../types";
|
|
4
|
+
import { buildHeader, HeaderElements, attachHeaderToContainer } from "./header-builder";
|
|
5
|
+
|
|
6
|
+
export interface HeaderLayoutContext {
|
|
7
|
+
config: AgentWidgetConfig;
|
|
8
|
+
showClose?: boolean;
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
onClearChat?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type HeaderLayoutRenderer = (context: HeaderLayoutContext) => HeaderElements;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build default header layout
|
|
17
|
+
* Full header with icon, title, subtitle, clear chat, and close button
|
|
18
|
+
*/
|
|
19
|
+
export const buildDefaultHeader: HeaderLayoutRenderer = (context) => {
|
|
20
|
+
return buildHeader({
|
|
21
|
+
config: context.config,
|
|
22
|
+
showClose: context.showClose,
|
|
23
|
+
onClose: context.onClose,
|
|
24
|
+
onClearChat: context.onClearChat
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build minimal header layout
|
|
30
|
+
* Simplified layout with just title and close button
|
|
31
|
+
*/
|
|
32
|
+
export const buildMinimalHeader: HeaderLayoutRenderer = (context) => {
|
|
33
|
+
const { config, showClose = true, onClose } = context;
|
|
34
|
+
const launcher = config?.launcher ?? {};
|
|
35
|
+
|
|
36
|
+
const header = createElement(
|
|
37
|
+
"div",
|
|
38
|
+
"tvw-flex tvw-items-center tvw-justify-between tvw-bg-cw-surface tvw-px-6 tvw-py-4 tvw-border-b-cw-divider"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Title only (no icon, no subtitle)
|
|
42
|
+
const title = createElement("span", "tvw-text-base tvw-font-semibold");
|
|
43
|
+
title.textContent = launcher.title ?? "Chat Assistant";
|
|
44
|
+
|
|
45
|
+
header.appendChild(title);
|
|
46
|
+
|
|
47
|
+
// Close button
|
|
48
|
+
const closeButtonSize = launcher.closeButtonSize ?? "32px";
|
|
49
|
+
const closeButtonWrapper = createElement("div", "");
|
|
50
|
+
|
|
51
|
+
const closeButton = createElement(
|
|
52
|
+
"button",
|
|
53
|
+
"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"
|
|
54
|
+
) as HTMLButtonElement;
|
|
55
|
+
closeButton.style.height = closeButtonSize;
|
|
56
|
+
closeButton.style.width = closeButtonSize;
|
|
57
|
+
closeButton.type = "button";
|
|
58
|
+
closeButton.setAttribute("aria-label", "Close chat");
|
|
59
|
+
closeButton.style.display = showClose ? "" : "none";
|
|
60
|
+
|
|
61
|
+
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
62
|
+
const closeIconSvg = renderLucideIcon(
|
|
63
|
+
closeButtonIconName,
|
|
64
|
+
"20px",
|
|
65
|
+
launcher.closeButtonColor || "",
|
|
66
|
+
2
|
|
67
|
+
);
|
|
68
|
+
if (closeIconSvg) {
|
|
69
|
+
closeButton.appendChild(closeIconSvg);
|
|
70
|
+
} else {
|
|
71
|
+
closeButton.textContent = "×";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (onClose) {
|
|
75
|
+
closeButton.addEventListener("click", onClose);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
closeButtonWrapper.appendChild(closeButton);
|
|
79
|
+
header.appendChild(closeButtonWrapper);
|
|
80
|
+
|
|
81
|
+
// Create placeholder elements for compatibility
|
|
82
|
+
const iconHolder = createElement("div");
|
|
83
|
+
iconHolder.style.display = "none";
|
|
84
|
+
const headerSubtitle = createElement("span");
|
|
85
|
+
headerSubtitle.style.display = "none";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
header,
|
|
89
|
+
iconHolder,
|
|
90
|
+
headerTitle: title,
|
|
91
|
+
headerSubtitle,
|
|
92
|
+
closeButton,
|
|
93
|
+
closeButtonWrapper,
|
|
94
|
+
clearChatButton: null,
|
|
95
|
+
clearChatButtonWrapper: null
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build expanded header layout
|
|
101
|
+
* Full branding area with additional space for custom content
|
|
102
|
+
*/
|
|
103
|
+
export const buildExpandedHeader: HeaderLayoutRenderer = (context) => {
|
|
104
|
+
const { config, showClose = true, onClose, onClearChat } = context;
|
|
105
|
+
const launcher = config?.launcher ?? {};
|
|
106
|
+
|
|
107
|
+
const header = createElement(
|
|
108
|
+
"div",
|
|
109
|
+
"tvw-flex tvw-flex-col tvw-bg-cw-surface tvw-px-6 tvw-py-5 tvw-border-b-cw-divider"
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Top row: icon + text + buttons
|
|
113
|
+
const topRow = createElement(
|
|
114
|
+
"div",
|
|
115
|
+
"tvw-flex tvw-items-center tvw-gap-3"
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Icon
|
|
119
|
+
const headerIconSize = launcher.headerIconSize ?? "56px";
|
|
120
|
+
const iconHolder = createElement(
|
|
121
|
+
"div",
|
|
122
|
+
"tvw-flex tvw-items-center tvw-justify-center tvw-rounded-xl tvw-bg-cw-primary tvw-text-white tvw-text-2xl"
|
|
123
|
+
);
|
|
124
|
+
iconHolder.style.height = headerIconSize;
|
|
125
|
+
iconHolder.style.width = headerIconSize;
|
|
126
|
+
|
|
127
|
+
const headerIconName = launcher.headerIconName;
|
|
128
|
+
if (headerIconName) {
|
|
129
|
+
const iconSize = parseFloat(headerIconSize) || 24;
|
|
130
|
+
const iconSvg = renderLucideIcon(headerIconName, iconSize * 0.5, "#ffffff", 2);
|
|
131
|
+
if (iconSvg) {
|
|
132
|
+
iconHolder.replaceChildren(iconSvg);
|
|
133
|
+
} else {
|
|
134
|
+
iconHolder.textContent = launcher.agentIconText ?? "💬";
|
|
135
|
+
}
|
|
136
|
+
} else if (launcher.iconUrl) {
|
|
137
|
+
const img = createElement("img") as HTMLImageElement;
|
|
138
|
+
img.src = launcher.iconUrl;
|
|
139
|
+
img.alt = "";
|
|
140
|
+
img.className = "tvw-rounded-xl tvw-object-cover";
|
|
141
|
+
img.style.height = headerIconSize;
|
|
142
|
+
img.style.width = headerIconSize;
|
|
143
|
+
iconHolder.replaceChildren(img);
|
|
144
|
+
} else {
|
|
145
|
+
iconHolder.textContent = launcher.agentIconText ?? "💬";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Title and subtitle
|
|
149
|
+
const headerCopy = createElement("div", "tvw-flex tvw-flex-col tvw-flex-1");
|
|
150
|
+
const title = createElement("span", "tvw-text-lg tvw-font-semibold");
|
|
151
|
+
title.textContent = launcher.title ?? "Chat Assistant";
|
|
152
|
+
const subtitle = createElement("span", "tvw-text-sm tvw-text-cw-muted");
|
|
153
|
+
subtitle.textContent = launcher.subtitle ?? "Here to help you get answers fast";
|
|
154
|
+
headerCopy.append(title, subtitle);
|
|
155
|
+
|
|
156
|
+
topRow.append(iconHolder, headerCopy);
|
|
157
|
+
|
|
158
|
+
// Close button
|
|
159
|
+
const closeButtonSize = launcher.closeButtonSize ?? "32px";
|
|
160
|
+
const closeButtonWrapper = createElement("div", "");
|
|
161
|
+
|
|
162
|
+
const closeButton = createElement(
|
|
163
|
+
"button",
|
|
164
|
+
"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"
|
|
165
|
+
) as HTMLButtonElement;
|
|
166
|
+
closeButton.style.height = closeButtonSize;
|
|
167
|
+
closeButton.style.width = closeButtonSize;
|
|
168
|
+
closeButton.type = "button";
|
|
169
|
+
closeButton.setAttribute("aria-label", "Close chat");
|
|
170
|
+
closeButton.style.display = showClose ? "" : "none";
|
|
171
|
+
|
|
172
|
+
const closeButtonIconName = launcher.closeButtonIconName ?? "x";
|
|
173
|
+
const closeIconSvg = renderLucideIcon(
|
|
174
|
+
closeButtonIconName,
|
|
175
|
+
"20px",
|
|
176
|
+
launcher.closeButtonColor || "",
|
|
177
|
+
2
|
|
178
|
+
);
|
|
179
|
+
if (closeIconSvg) {
|
|
180
|
+
closeButton.appendChild(closeIconSvg);
|
|
181
|
+
} else {
|
|
182
|
+
closeButton.textContent = "×";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (onClose) {
|
|
186
|
+
closeButton.addEventListener("click", onClose);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
closeButtonWrapper.appendChild(closeButton);
|
|
190
|
+
topRow.appendChild(closeButtonWrapper);
|
|
191
|
+
|
|
192
|
+
header.appendChild(topRow);
|
|
193
|
+
|
|
194
|
+
// Bottom row: additional space for status or branding
|
|
195
|
+
const bottomRow = createElement(
|
|
196
|
+
"div",
|
|
197
|
+
"tvw-mt-3 tvw-pt-3 tvw-border-t tvw-border-gray-100 tvw-text-xs tvw-text-cw-muted"
|
|
198
|
+
);
|
|
199
|
+
bottomRow.textContent = "Online and ready to help";
|
|
200
|
+
header.appendChild(bottomRow);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
header,
|
|
204
|
+
iconHolder,
|
|
205
|
+
headerTitle: title,
|
|
206
|
+
headerSubtitle: subtitle,
|
|
207
|
+
closeButton,
|
|
208
|
+
closeButtonWrapper,
|
|
209
|
+
clearChatButton: null,
|
|
210
|
+
clearChatButtonWrapper: null
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Header layout registry
|
|
216
|
+
* Maps layout names to their renderer functions
|
|
217
|
+
*/
|
|
218
|
+
export const headerLayouts: Record<string, HeaderLayoutRenderer> = {
|
|
219
|
+
default: buildDefaultHeader,
|
|
220
|
+
minimal: buildMinimalHeader,
|
|
221
|
+
expanded: buildExpandedHeader
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get header layout renderer by name
|
|
226
|
+
*/
|
|
227
|
+
export const getHeaderLayout = (layoutName: string): HeaderLayoutRenderer => {
|
|
228
|
+
return headerLayouts[layoutName] ?? headerLayouts.default;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Build header based on layout configuration
|
|
233
|
+
* Applies layout config settings to determine which layout to use
|
|
234
|
+
*/
|
|
235
|
+
export const buildHeaderWithLayout = (
|
|
236
|
+
config: AgentWidgetConfig,
|
|
237
|
+
layoutConfig?: AgentWidgetHeaderLayoutConfig,
|
|
238
|
+
context?: Partial<HeaderLayoutContext>
|
|
239
|
+
): HeaderElements => {
|
|
240
|
+
// If custom render is provided, use it
|
|
241
|
+
if (layoutConfig?.render) {
|
|
242
|
+
const customHeader = layoutConfig.render({
|
|
243
|
+
config,
|
|
244
|
+
onClose: context?.onClose,
|
|
245
|
+
onClearChat: context?.onClearChat
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Wrap in HeaderElements structure
|
|
249
|
+
const iconHolder = createElement("div");
|
|
250
|
+
iconHolder.style.display = "none";
|
|
251
|
+
const headerTitle = createElement("span");
|
|
252
|
+
const headerSubtitle = createElement("span");
|
|
253
|
+
const closeButton = createElement("button") as HTMLButtonElement;
|
|
254
|
+
closeButton.style.display = "none";
|
|
255
|
+
const closeButtonWrapper = createElement("div");
|
|
256
|
+
closeButtonWrapper.style.display = "none";
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
header: customHeader,
|
|
260
|
+
iconHolder,
|
|
261
|
+
headerTitle,
|
|
262
|
+
headerSubtitle,
|
|
263
|
+
closeButton,
|
|
264
|
+
closeButtonWrapper,
|
|
265
|
+
clearChatButton: null,
|
|
266
|
+
clearChatButtonWrapper: null
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Get layout renderer
|
|
271
|
+
const layoutName = layoutConfig?.layout ?? "default";
|
|
272
|
+
const layoutRenderer = getHeaderLayout(layoutName);
|
|
273
|
+
|
|
274
|
+
// Build header with layout
|
|
275
|
+
const headerElements = layoutRenderer({
|
|
276
|
+
config,
|
|
277
|
+
showClose: layoutConfig?.showCloseButton ?? context?.showClose ?? true,
|
|
278
|
+
onClose: context?.onClose,
|
|
279
|
+
onClearChat: context?.onClearChat
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Apply visibility settings from layout config
|
|
283
|
+
if (layoutConfig) {
|
|
284
|
+
if (layoutConfig.showIcon === false) {
|
|
285
|
+
headerElements.iconHolder.style.display = "none";
|
|
286
|
+
}
|
|
287
|
+
if (layoutConfig.showTitle === false) {
|
|
288
|
+
headerElements.headerTitle.style.display = "none";
|
|
289
|
+
}
|
|
290
|
+
if (layoutConfig.showSubtitle === false) {
|
|
291
|
+
headerElements.headerSubtitle.style.display = "none";
|
|
292
|
+
}
|
|
293
|
+
if (layoutConfig.showCloseButton === false) {
|
|
294
|
+
headerElements.closeButton.style.display = "none";
|
|
295
|
+
}
|
|
296
|
+
if (layoutConfig.showClearChat === false && headerElements.clearChatButtonWrapper) {
|
|
297
|
+
headerElements.clearChatButtonWrapper.style.display = "none";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return headerElements;
|
|
302
|
+
};
|
|
303
|
+
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createElement } from "../utils/dom";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AgentWidgetMessage,
|
|
4
|
+
AgentWidgetMessageLayoutConfig,
|
|
5
|
+
AgentWidgetAvatarConfig,
|
|
6
|
+
AgentWidgetTimestampConfig
|
|
7
|
+
} from "../types";
|
|
3
8
|
|
|
4
9
|
export type MessageTransform = (context: {
|
|
5
10
|
text: string;
|
|
@@ -37,40 +42,188 @@ export const createTypingIndicator = (): HTMLElement => {
|
|
|
37
42
|
return container;
|
|
38
43
|
};
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Create an avatar element
|
|
47
|
+
*/
|
|
48
|
+
const createAvatar = (
|
|
49
|
+
avatarConfig: AgentWidgetAvatarConfig,
|
|
50
|
+
role: "user" | "assistant"
|
|
43
51
|
): HTMLElement => {
|
|
44
|
-
const
|
|
45
|
-
"
|
|
46
|
-
"tvw-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"tvw-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
const avatar = createElement(
|
|
53
|
+
"div",
|
|
54
|
+
"tvw-flex-shrink-0 tvw-w-8 tvw-h-8 tvw-rounded-full tvw-flex tvw-items-center tvw-justify-center tvw-text-sm"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const avatarContent = role === "user"
|
|
58
|
+
? avatarConfig.userAvatar
|
|
59
|
+
: avatarConfig.assistantAvatar;
|
|
60
|
+
|
|
61
|
+
if (avatarContent) {
|
|
62
|
+
// Check if it's a URL or emoji/text
|
|
63
|
+
if (avatarContent.startsWith("http") || avatarContent.startsWith("/") || avatarContent.startsWith("data:")) {
|
|
64
|
+
const img = createElement("img") as HTMLImageElement;
|
|
65
|
+
img.src = avatarContent;
|
|
66
|
+
img.alt = role === "user" ? "User" : "Assistant";
|
|
67
|
+
img.className = "tvw-w-full tvw-h-full tvw-rounded-full tvw-object-cover";
|
|
68
|
+
avatar.appendChild(img);
|
|
69
|
+
} else {
|
|
70
|
+
// Emoji or text
|
|
71
|
+
avatar.textContent = avatarContent;
|
|
72
|
+
avatar.classList.add(
|
|
73
|
+
role === "user" ? "tvw-bg-cw-accent" : "tvw-bg-cw-primary",
|
|
74
|
+
"tvw-text-white"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
62
77
|
} else {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"tvw-
|
|
67
|
-
"tvw-
|
|
68
|
-
"tvw-text-cw-primary",
|
|
69
|
-
"tvw-px-5",
|
|
70
|
-
"tvw-py-3"
|
|
78
|
+
// Default avatar
|
|
79
|
+
avatar.textContent = role === "user" ? "U" : "A";
|
|
80
|
+
avatar.classList.add(
|
|
81
|
+
role === "user" ? "tvw-bg-cw-accent" : "tvw-bg-cw-primary",
|
|
82
|
+
"tvw-text-white"
|
|
71
83
|
);
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
return avatar;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a timestamp element
|
|
91
|
+
*/
|
|
92
|
+
const createTimestamp = (
|
|
93
|
+
message: AgentWidgetMessage,
|
|
94
|
+
timestampConfig: AgentWidgetTimestampConfig
|
|
95
|
+
): HTMLElement => {
|
|
96
|
+
const timestamp = createElement(
|
|
97
|
+
"div",
|
|
98
|
+
"tvw-text-xs tvw-text-cw-muted"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const date = new Date(message.createdAt);
|
|
102
|
+
|
|
103
|
+
if (timestampConfig.format) {
|
|
104
|
+
timestamp.textContent = timestampConfig.format(date);
|
|
105
|
+
} else {
|
|
106
|
+
// Default format: HH:MM
|
|
107
|
+
timestamp.textContent = date.toLocaleTimeString([], {
|
|
108
|
+
hour: "2-digit",
|
|
109
|
+
minute: "2-digit"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return timestamp;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get bubble classes based on layout preset
|
|
118
|
+
*/
|
|
119
|
+
const getBubbleClasses = (
|
|
120
|
+
role: "user" | "assistant" | "system",
|
|
121
|
+
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
122
|
+
): string[] => {
|
|
123
|
+
const baseClasses = ["vanilla-message-bubble", "tvw-max-w-[85%]"];
|
|
124
|
+
|
|
125
|
+
switch (layout) {
|
|
126
|
+
case "flat":
|
|
127
|
+
// Flat layout: no bubble styling, just text
|
|
128
|
+
if (role === "user") {
|
|
129
|
+
baseClasses.push(
|
|
130
|
+
"vanilla-message-user-bubble",
|
|
131
|
+
"tvw-ml-auto",
|
|
132
|
+
"tvw-text-cw-primary",
|
|
133
|
+
"tvw-py-2"
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
baseClasses.push(
|
|
137
|
+
"vanilla-message-assistant-bubble",
|
|
138
|
+
"tvw-text-cw-primary",
|
|
139
|
+
"tvw-py-2"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case "minimal":
|
|
145
|
+
// Minimal layout: reduced padding and styling
|
|
146
|
+
baseClasses.push(
|
|
147
|
+
"tvw-text-sm",
|
|
148
|
+
"tvw-leading-relaxed"
|
|
149
|
+
);
|
|
150
|
+
if (role === "user") {
|
|
151
|
+
baseClasses.push(
|
|
152
|
+
"vanilla-message-user-bubble",
|
|
153
|
+
"tvw-ml-auto",
|
|
154
|
+
"tvw-bg-cw-accent",
|
|
155
|
+
"tvw-text-white",
|
|
156
|
+
"tvw-px-3",
|
|
157
|
+
"tvw-py-2",
|
|
158
|
+
"tvw-rounded-lg"
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
baseClasses.push(
|
|
162
|
+
"vanilla-message-assistant-bubble",
|
|
163
|
+
"tvw-bg-cw-surface",
|
|
164
|
+
"tvw-text-cw-primary",
|
|
165
|
+
"tvw-px-3",
|
|
166
|
+
"tvw-py-2",
|
|
167
|
+
"tvw-rounded-lg"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case "bubble":
|
|
173
|
+
default:
|
|
174
|
+
// Default bubble layout
|
|
175
|
+
baseClasses.push(
|
|
176
|
+
"tvw-rounded-2xl",
|
|
177
|
+
"tvw-text-sm",
|
|
178
|
+
"tvw-leading-relaxed",
|
|
179
|
+
"tvw-shadow-sm"
|
|
180
|
+
);
|
|
181
|
+
if (role === "user") {
|
|
182
|
+
baseClasses.push(
|
|
183
|
+
"vanilla-message-user-bubble",
|
|
184
|
+
"tvw-ml-auto",
|
|
185
|
+
"tvw-bg-cw-accent",
|
|
186
|
+
"tvw-text-white",
|
|
187
|
+
"tvw-px-5",
|
|
188
|
+
"tvw-py-3"
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
baseClasses.push(
|
|
192
|
+
"vanilla-message-assistant-bubble",
|
|
193
|
+
"tvw-bg-cw-surface",
|
|
194
|
+
"tvw-border",
|
|
195
|
+
"tvw-border-cw-message-border",
|
|
196
|
+
"tvw-text-cw-primary",
|
|
197
|
+
"tvw-px-5",
|
|
198
|
+
"tvw-py-3"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return baseClasses;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create standard message bubble
|
|
209
|
+
* Supports layout configuration for avatars, timestamps, and visual presets
|
|
210
|
+
*/
|
|
211
|
+
export const createStandardBubble = (
|
|
212
|
+
message: AgentWidgetMessage,
|
|
213
|
+
transform: MessageTransform,
|
|
214
|
+
layoutConfig?: AgentWidgetMessageLayoutConfig
|
|
215
|
+
): HTMLElement => {
|
|
216
|
+
const config = layoutConfig ?? {};
|
|
217
|
+
const layout = config.layout ?? "bubble";
|
|
218
|
+
const avatarConfig = config.avatar;
|
|
219
|
+
const timestampConfig = config.timestamp;
|
|
220
|
+
const showAvatar = avatarConfig?.show ?? false;
|
|
221
|
+
const showTimestamp = timestampConfig?.show ?? false;
|
|
222
|
+
const avatarPosition = avatarConfig?.position ?? "left";
|
|
223
|
+
const timestampPosition = timestampConfig?.position ?? "below";
|
|
224
|
+
|
|
225
|
+
// Create the bubble element
|
|
226
|
+
const classes = getBubbleClasses(message.role, layout);
|
|
74
227
|
const bubble = createElement("div", classes.join(" "));
|
|
75
228
|
|
|
76
229
|
// Add message content
|
|
@@ -81,21 +234,85 @@ export const createStandardBubble = (
|
|
|
81
234
|
streaming: Boolean(message.streaming),
|
|
82
235
|
raw: message.rawContent
|
|
83
236
|
});
|
|
237
|
+
|
|
238
|
+
// Add inline timestamp if configured
|
|
239
|
+
if (showTimestamp && timestampPosition === "inline" && message.createdAt) {
|
|
240
|
+
const timestamp = createTimestamp(message, timestampConfig!);
|
|
241
|
+
timestamp.classList.add("tvw-ml-2", "tvw-inline");
|
|
242
|
+
contentDiv.appendChild(timestamp);
|
|
243
|
+
}
|
|
244
|
+
|
|
84
245
|
bubble.appendChild(contentDiv);
|
|
85
246
|
|
|
247
|
+
// Add timestamp below if configured
|
|
248
|
+
if (showTimestamp && timestampPosition === "below" && message.createdAt) {
|
|
249
|
+
const timestamp = createTimestamp(message, timestampConfig!);
|
|
250
|
+
timestamp.classList.add("tvw-mt-1");
|
|
251
|
+
bubble.appendChild(timestamp);
|
|
252
|
+
}
|
|
253
|
+
|
|
86
254
|
// Add typing indicator if this is a streaming assistant message
|
|
87
|
-
// Show it when streaming starts (even if content is empty), hide it once content appears
|
|
88
255
|
if (message.streaming && message.role === "assistant") {
|
|
89
|
-
// Only show typing indicator if content is empty or just starting
|
|
90
|
-
// Hide it once we have substantial content
|
|
91
256
|
if (!message.content || !message.content.trim()) {
|
|
92
257
|
const typingIndicator = createTypingIndicator();
|
|
93
258
|
bubble.appendChild(typingIndicator);
|
|
94
259
|
}
|
|
95
260
|
}
|
|
96
261
|
|
|
97
|
-
return bubble
|
|
262
|
+
// If no avatar needed, return bubble directly
|
|
263
|
+
if (!showAvatar || message.role === "system") {
|
|
264
|
+
return bubble;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Create wrapper with avatar
|
|
268
|
+
const wrapper = createElement(
|
|
269
|
+
"div",
|
|
270
|
+
`tvw-flex tvw-gap-2 ${message.role === "user" ? "tvw-flex-row-reverse" : ""}`
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const avatar = createAvatar(avatarConfig!, message.role);
|
|
274
|
+
|
|
275
|
+
if (avatarPosition === "right" || (avatarPosition === "left" && message.role === "user")) {
|
|
276
|
+
wrapper.append(bubble, avatar);
|
|
277
|
+
} else {
|
|
278
|
+
wrapper.append(avatar, bubble);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Adjust bubble max-width when avatar is present
|
|
282
|
+
bubble.classList.remove("tvw-max-w-[85%]");
|
|
283
|
+
bubble.classList.add("tvw-max-w-[calc(85%-2.5rem)]");
|
|
284
|
+
|
|
285
|
+
return wrapper;
|
|
98
286
|
};
|
|
99
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Create bubble with custom renderer support
|
|
290
|
+
* Uses custom renderer if provided in layout config, otherwise falls back to standard bubble
|
|
291
|
+
*/
|
|
292
|
+
export const createBubbleWithLayout = (
|
|
293
|
+
message: AgentWidgetMessage,
|
|
294
|
+
transform: MessageTransform,
|
|
295
|
+
layoutConfig?: AgentWidgetMessageLayoutConfig
|
|
296
|
+
): HTMLElement => {
|
|
297
|
+
const config = layoutConfig ?? {};
|
|
100
298
|
|
|
299
|
+
// Check for custom renderers
|
|
300
|
+
if (message.role === "user" && config.renderUserMessage) {
|
|
301
|
+
return config.renderUserMessage({
|
|
302
|
+
message,
|
|
303
|
+
config: {} as any, // Will be populated by caller
|
|
304
|
+
streaming: Boolean(message.streaming)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
101
307
|
|
|
308
|
+
if (message.role === "assistant" && config.renderAssistantMessage) {
|
|
309
|
+
return config.renderAssistantMessage({
|
|
310
|
+
message,
|
|
311
|
+
config: {} as any, // Will be populated by caller
|
|
312
|
+
streaming: Boolean(message.streaming)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Fall back to standard bubble
|
|
317
|
+
return createStandardBubble(message, transform, layoutConfig);
|
|
318
|
+
};
|