@runtypelabs/persona 1.36.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 +1080 -0
- package/dist/index.cjs +140 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2626 -0
- package/dist/index.d.ts +2626 -0
- package/dist/index.global.js +1843 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/install.global.js +2 -0
- package/dist/install.global.js.map +1 -0
- package/dist/widget.css +1627 -0
- package/package.json +79 -0
- package/src/@types/idiomorph.d.ts +37 -0
- package/src/client.test.ts +387 -0
- package/src/client.ts +1589 -0
- package/src/components/composer-builder.ts +530 -0
- package/src/components/feedback.ts +379 -0
- package/src/components/forms.ts +170 -0
- package/src/components/header-builder.ts +455 -0
- package/src/components/header-layouts.ts +303 -0
- package/src/components/launcher.ts +193 -0
- package/src/components/message-bubble.ts +528 -0
- package/src/components/messages.ts +54 -0
- package/src/components/panel.ts +204 -0
- package/src/components/reasoning-bubble.ts +144 -0
- package/src/components/registry.ts +87 -0
- package/src/components/suggestions.ts +97 -0
- package/src/components/tool-bubble.ts +288 -0
- package/src/defaults.ts +321 -0
- package/src/index.ts +175 -0
- package/src/install.ts +284 -0
- package/src/plugins/registry.ts +77 -0
- package/src/plugins/types.ts +95 -0
- package/src/postprocessors.ts +194 -0
- package/src/runtime/init.ts +162 -0
- package/src/session.ts +376 -0
- package/src/styles/tailwind.css +20 -0
- package/src/styles/widget.css +1627 -0
- package/src/types.ts +1635 -0
- package/src/ui.ts +3341 -0
- package/src/utils/actions.ts +227 -0
- package/src/utils/attachment-manager.ts +384 -0
- package/src/utils/code-generators.test.ts +500 -0
- package/src/utils/code-generators.ts +1806 -0
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +16 -0
- package/src/utils/content.ts +306 -0
- package/src/utils/dom.ts +25 -0
- package/src/utils/events.ts +41 -0
- package/src/utils/formatting.test.ts +166 -0
- package/src/utils/formatting.ts +470 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/message-id.ts +37 -0
- package/src/utils/morph.ts +36 -0
- package/src/utils/positioning.ts +17 -0
- package/src/utils/storage.ts +72 -0
- package/src/utils/theme.ts +105 -0
- package/src/widget.css +1 -0
- package/widget.css +1 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import {
|
|
3
|
+
AgentWidgetMessage,
|
|
4
|
+
AgentWidgetMessageLayoutConfig,
|
|
5
|
+
AgentWidgetAvatarConfig,
|
|
6
|
+
AgentWidgetTimestampConfig,
|
|
7
|
+
AgentWidgetMessageActionsConfig,
|
|
8
|
+
AgentWidgetMessageFeedback
|
|
9
|
+
} from "../types";
|
|
10
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
11
|
+
|
|
12
|
+
export type MessageTransform = (context: {
|
|
13
|
+
text: string;
|
|
14
|
+
message: AgentWidgetMessage;
|
|
15
|
+
streaming: boolean;
|
|
16
|
+
raw?: string;
|
|
17
|
+
}) => string;
|
|
18
|
+
|
|
19
|
+
export type MessageActionCallbacks = {
|
|
20
|
+
onCopy?: (message: AgentWidgetMessage) => void;
|
|
21
|
+
onFeedback?: (feedback: AgentWidgetMessageFeedback) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Create typing indicator element
|
|
25
|
+
export const createTypingIndicator = (): HTMLElement => {
|
|
26
|
+
const container = document.createElement("div");
|
|
27
|
+
container.className = "tvw-flex tvw-items-center tvw-space-x-1 tvw-h-5 tvw-mt-2";
|
|
28
|
+
|
|
29
|
+
const dot1 = document.createElement("div");
|
|
30
|
+
dot1.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
|
|
31
|
+
dot1.style.animationDelay = "0ms";
|
|
32
|
+
|
|
33
|
+
const dot2 = document.createElement("div");
|
|
34
|
+
dot2.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
|
|
35
|
+
dot2.style.animationDelay = "250ms";
|
|
36
|
+
|
|
37
|
+
const dot3 = document.createElement("div");
|
|
38
|
+
dot3.className = "tvw-bg-cw-primary tvw-animate-typing tvw-rounded-full tvw-h-1.5 tvw-w-1.5";
|
|
39
|
+
dot3.style.animationDelay = "500ms";
|
|
40
|
+
|
|
41
|
+
const srOnly = document.createElement("span");
|
|
42
|
+
srOnly.className = "tvw-sr-only";
|
|
43
|
+
srOnly.textContent = "Loading";
|
|
44
|
+
|
|
45
|
+
container.appendChild(dot1);
|
|
46
|
+
container.appendChild(dot2);
|
|
47
|
+
container.appendChild(dot3);
|
|
48
|
+
container.appendChild(srOnly);
|
|
49
|
+
|
|
50
|
+
return container;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create an avatar element
|
|
55
|
+
*/
|
|
56
|
+
const createAvatar = (
|
|
57
|
+
avatarConfig: AgentWidgetAvatarConfig,
|
|
58
|
+
role: "user" | "assistant"
|
|
59
|
+
): HTMLElement => {
|
|
60
|
+
const avatar = createElement(
|
|
61
|
+
"div",
|
|
62
|
+
"tvw-flex-shrink-0 tvw-w-8 tvw-h-8 tvw-rounded-full tvw-flex tvw-items-center tvw-justify-center tvw-text-sm"
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const avatarContent = role === "user"
|
|
66
|
+
? avatarConfig.userAvatar
|
|
67
|
+
: avatarConfig.assistantAvatar;
|
|
68
|
+
|
|
69
|
+
if (avatarContent) {
|
|
70
|
+
// Check if it's a URL or emoji/text
|
|
71
|
+
if (avatarContent.startsWith("http") || avatarContent.startsWith("/") || avatarContent.startsWith("data:")) {
|
|
72
|
+
const img = createElement("img") as HTMLImageElement;
|
|
73
|
+
img.src = avatarContent;
|
|
74
|
+
img.alt = role === "user" ? "User" : "Assistant";
|
|
75
|
+
img.className = "tvw-w-full tvw-h-full tvw-rounded-full tvw-object-cover";
|
|
76
|
+
avatar.appendChild(img);
|
|
77
|
+
} else {
|
|
78
|
+
// Emoji or text
|
|
79
|
+
avatar.textContent = avatarContent;
|
|
80
|
+
avatar.classList.add(
|
|
81
|
+
role === "user" ? "tvw-bg-cw-accent" : "tvw-bg-cw-primary",
|
|
82
|
+
"tvw-text-white"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// Default avatar
|
|
87
|
+
avatar.textContent = role === "user" ? "U" : "A";
|
|
88
|
+
avatar.classList.add(
|
|
89
|
+
role === "user" ? "tvw-bg-cw-accent" : "tvw-bg-cw-primary",
|
|
90
|
+
"tvw-text-white"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return avatar;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a timestamp element
|
|
99
|
+
*/
|
|
100
|
+
const createTimestamp = (
|
|
101
|
+
message: AgentWidgetMessage,
|
|
102
|
+
timestampConfig: AgentWidgetTimestampConfig
|
|
103
|
+
): HTMLElement => {
|
|
104
|
+
const timestamp = createElement(
|
|
105
|
+
"div",
|
|
106
|
+
"tvw-text-xs tvw-text-cw-muted"
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const date = new Date(message.createdAt);
|
|
110
|
+
|
|
111
|
+
if (timestampConfig.format) {
|
|
112
|
+
timestamp.textContent = timestampConfig.format(date);
|
|
113
|
+
} else {
|
|
114
|
+
// Default format: HH:MM
|
|
115
|
+
timestamp.textContent = date.toLocaleTimeString([], {
|
|
116
|
+
hour: "2-digit",
|
|
117
|
+
minute: "2-digit"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return timestamp;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get bubble classes based on layout preset
|
|
126
|
+
*/
|
|
127
|
+
const getBubbleClasses = (
|
|
128
|
+
role: "user" | "assistant" | "system",
|
|
129
|
+
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
130
|
+
): string[] => {
|
|
131
|
+
const baseClasses = ["vanilla-message-bubble", "tvw-max-w-[85%]"];
|
|
132
|
+
|
|
133
|
+
switch (layout) {
|
|
134
|
+
case "flat":
|
|
135
|
+
// Flat layout: no bubble styling, just text
|
|
136
|
+
if (role === "user") {
|
|
137
|
+
baseClasses.push(
|
|
138
|
+
"vanilla-message-user-bubble",
|
|
139
|
+
"tvw-ml-auto",
|
|
140
|
+
"tvw-text-cw-primary",
|
|
141
|
+
"tvw-py-2"
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
baseClasses.push(
|
|
145
|
+
"vanilla-message-assistant-bubble",
|
|
146
|
+
"tvw-text-cw-primary",
|
|
147
|
+
"tvw-py-2"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case "minimal":
|
|
153
|
+
// Minimal layout: reduced padding and styling
|
|
154
|
+
baseClasses.push(
|
|
155
|
+
"tvw-text-sm",
|
|
156
|
+
"tvw-leading-relaxed"
|
|
157
|
+
);
|
|
158
|
+
if (role === "user") {
|
|
159
|
+
baseClasses.push(
|
|
160
|
+
"vanilla-message-user-bubble",
|
|
161
|
+
"tvw-ml-auto",
|
|
162
|
+
"tvw-bg-cw-accent",
|
|
163
|
+
"tvw-text-white",
|
|
164
|
+
"tvw-px-3",
|
|
165
|
+
"tvw-py-2",
|
|
166
|
+
"tvw-rounded-lg"
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
baseClasses.push(
|
|
170
|
+
"vanilla-message-assistant-bubble",
|
|
171
|
+
"tvw-bg-cw-surface",
|
|
172
|
+
"tvw-text-cw-primary",
|
|
173
|
+
"tvw-px-3",
|
|
174
|
+
"tvw-py-2",
|
|
175
|
+
"tvw-rounded-lg"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case "bubble":
|
|
181
|
+
default:
|
|
182
|
+
// Default bubble layout
|
|
183
|
+
baseClasses.push(
|
|
184
|
+
"tvw-rounded-2xl",
|
|
185
|
+
"tvw-text-sm",
|
|
186
|
+
"tvw-leading-relaxed",
|
|
187
|
+
"tvw-shadow-sm"
|
|
188
|
+
);
|
|
189
|
+
if (role === "user") {
|
|
190
|
+
baseClasses.push(
|
|
191
|
+
"vanilla-message-user-bubble",
|
|
192
|
+
"tvw-ml-auto",
|
|
193
|
+
"tvw-bg-cw-accent",
|
|
194
|
+
"tvw-text-white",
|
|
195
|
+
"tvw-px-5",
|
|
196
|
+
"tvw-py-3"
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
baseClasses.push(
|
|
200
|
+
"vanilla-message-assistant-bubble",
|
|
201
|
+
"tvw-bg-cw-surface",
|
|
202
|
+
"tvw-border",
|
|
203
|
+
"tvw-border-cw-message-border",
|
|
204
|
+
"tvw-text-cw-primary",
|
|
205
|
+
"tvw-px-5",
|
|
206
|
+
"tvw-py-3"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return baseClasses;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create message action buttons (copy, upvote, downvote)
|
|
217
|
+
*/
|
|
218
|
+
export const createMessageActions = (
|
|
219
|
+
message: AgentWidgetMessage,
|
|
220
|
+
actionsConfig: AgentWidgetMessageActionsConfig,
|
|
221
|
+
callbacks?: MessageActionCallbacks
|
|
222
|
+
): HTMLElement => {
|
|
223
|
+
const showCopy = actionsConfig.showCopy ?? true;
|
|
224
|
+
const showUpvote = actionsConfig.showUpvote ?? true;
|
|
225
|
+
const showDownvote = actionsConfig.showDownvote ?? true;
|
|
226
|
+
const visibility = actionsConfig.visibility ?? "hover";
|
|
227
|
+
const align = actionsConfig.align ?? "right";
|
|
228
|
+
const layout = actionsConfig.layout ?? "pill-inside";
|
|
229
|
+
|
|
230
|
+
// Map alignment to CSS class
|
|
231
|
+
const alignClass = {
|
|
232
|
+
left: "tvw-message-actions-left",
|
|
233
|
+
center: "tvw-message-actions-center",
|
|
234
|
+
right: "tvw-message-actions-right",
|
|
235
|
+
}[align];
|
|
236
|
+
|
|
237
|
+
// Map layout to CSS class
|
|
238
|
+
const layoutClass = {
|
|
239
|
+
"pill-inside": "tvw-message-actions-pill",
|
|
240
|
+
"row-inside": "tvw-message-actions-row",
|
|
241
|
+
}[layout];
|
|
242
|
+
|
|
243
|
+
const container = createElement(
|
|
244
|
+
"div",
|
|
245
|
+
`tvw-message-actions tvw-flex tvw-items-center tvw-gap-1 tvw-mt-2 ${alignClass} ${layoutClass} ${
|
|
246
|
+
visibility === "hover" ? "tvw-message-actions-hover" : ""
|
|
247
|
+
}`
|
|
248
|
+
);
|
|
249
|
+
// Set id for idiomorph matching (prevents recreation on morph)
|
|
250
|
+
container.id = `actions-${message.id}`;
|
|
251
|
+
container.setAttribute("data-actions-for", message.id);
|
|
252
|
+
|
|
253
|
+
// Track vote state for this message
|
|
254
|
+
let currentVote: "upvote" | "downvote" | null = null;
|
|
255
|
+
|
|
256
|
+
const createActionButton = (
|
|
257
|
+
iconName: string,
|
|
258
|
+
label: string,
|
|
259
|
+
onClick: () => void,
|
|
260
|
+
dataAction?: string
|
|
261
|
+
): HTMLButtonElement => {
|
|
262
|
+
const button = document.createElement("button");
|
|
263
|
+
button.className = "tvw-message-action-btn";
|
|
264
|
+
button.setAttribute("aria-label", label);
|
|
265
|
+
button.setAttribute("title", label);
|
|
266
|
+
if (dataAction) {
|
|
267
|
+
button.setAttribute("data-action", dataAction);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const icon = renderLucideIcon(iconName, 14, "currentColor", 2);
|
|
271
|
+
if (icon) {
|
|
272
|
+
button.appendChild(icon);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
button.addEventListener("click", (e) => {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
onClick();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return button;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Copy button
|
|
285
|
+
if (showCopy) {
|
|
286
|
+
const copyButton = createActionButton("copy", "Copy message", () => {
|
|
287
|
+
// Copy to clipboard
|
|
288
|
+
const textToCopy = message.content || "";
|
|
289
|
+
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
290
|
+
// Show success feedback - swap icon temporarily
|
|
291
|
+
copyButton.classList.add("tvw-message-action-success");
|
|
292
|
+
const checkIcon = renderLucideIcon("check", 14, "currentColor", 2);
|
|
293
|
+
if (checkIcon) {
|
|
294
|
+
copyButton.innerHTML = "";
|
|
295
|
+
copyButton.appendChild(checkIcon);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Restore original icon after 2 seconds
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
copyButton.classList.remove("tvw-message-action-success");
|
|
301
|
+
const originalIcon = renderLucideIcon("copy", 14, "currentColor", 2);
|
|
302
|
+
if (originalIcon) {
|
|
303
|
+
copyButton.innerHTML = "";
|
|
304
|
+
copyButton.appendChild(originalIcon);
|
|
305
|
+
}
|
|
306
|
+
}, 2000);
|
|
307
|
+
}).catch((err) => {
|
|
308
|
+
if (typeof console !== "undefined") {
|
|
309
|
+
console.error("[AgentWidget] Failed to copy message:", err);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Trigger callback
|
|
314
|
+
if (callbacks?.onCopy) {
|
|
315
|
+
callbacks.onCopy(message);
|
|
316
|
+
}
|
|
317
|
+
if (actionsConfig.onCopy) {
|
|
318
|
+
actionsConfig.onCopy(message);
|
|
319
|
+
}
|
|
320
|
+
}, "copy");
|
|
321
|
+
container.appendChild(copyButton);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Upvote button
|
|
325
|
+
if (showUpvote) {
|
|
326
|
+
const upvoteButton = createActionButton("thumbs-up", "Upvote", () => {
|
|
327
|
+
const wasActive = currentVote === "upvote";
|
|
328
|
+
|
|
329
|
+
// Toggle state
|
|
330
|
+
if (wasActive) {
|
|
331
|
+
currentVote = null;
|
|
332
|
+
upvoteButton.classList.remove("tvw-message-action-active");
|
|
333
|
+
} else {
|
|
334
|
+
// Remove downvote if active
|
|
335
|
+
const downvoteBtn = container.querySelector('[data-action="downvote"]');
|
|
336
|
+
if (downvoteBtn) {
|
|
337
|
+
downvoteBtn.classList.remove("tvw-message-action-active");
|
|
338
|
+
}
|
|
339
|
+
currentVote = "upvote";
|
|
340
|
+
upvoteButton.classList.add("tvw-message-action-active");
|
|
341
|
+
|
|
342
|
+
// Trigger feedback
|
|
343
|
+
const feedback: AgentWidgetMessageFeedback = {
|
|
344
|
+
type: "upvote",
|
|
345
|
+
messageId: message.id,
|
|
346
|
+
message
|
|
347
|
+
};
|
|
348
|
+
if (callbacks?.onFeedback) {
|
|
349
|
+
callbacks.onFeedback(feedback);
|
|
350
|
+
}
|
|
351
|
+
if (actionsConfig.onFeedback) {
|
|
352
|
+
actionsConfig.onFeedback(feedback);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}, "upvote");
|
|
356
|
+
container.appendChild(upvoteButton);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Downvote button
|
|
360
|
+
if (showDownvote) {
|
|
361
|
+
const downvoteButton = createActionButton("thumbs-down", "Downvote", () => {
|
|
362
|
+
const wasActive = currentVote === "downvote";
|
|
363
|
+
|
|
364
|
+
// Toggle state
|
|
365
|
+
if (wasActive) {
|
|
366
|
+
currentVote = null;
|
|
367
|
+
downvoteButton.classList.remove("tvw-message-action-active");
|
|
368
|
+
} else {
|
|
369
|
+
// Remove upvote if active
|
|
370
|
+
const upvoteBtn = container.querySelector('[data-action="upvote"]');
|
|
371
|
+
if (upvoteBtn) {
|
|
372
|
+
upvoteBtn.classList.remove("tvw-message-action-active");
|
|
373
|
+
}
|
|
374
|
+
currentVote = "downvote";
|
|
375
|
+
downvoteButton.classList.add("tvw-message-action-active");
|
|
376
|
+
|
|
377
|
+
// Trigger feedback
|
|
378
|
+
const feedback: AgentWidgetMessageFeedback = {
|
|
379
|
+
type: "downvote",
|
|
380
|
+
messageId: message.id,
|
|
381
|
+
message
|
|
382
|
+
};
|
|
383
|
+
if (callbacks?.onFeedback) {
|
|
384
|
+
callbacks.onFeedback(feedback);
|
|
385
|
+
}
|
|
386
|
+
if (actionsConfig.onFeedback) {
|
|
387
|
+
actionsConfig.onFeedback(feedback);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}, "downvote");
|
|
391
|
+
container.appendChild(downvoteButton);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return container;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create standard message bubble
|
|
399
|
+
* Supports layout configuration for avatars, timestamps, and visual presets
|
|
400
|
+
*/
|
|
401
|
+
export const createStandardBubble = (
|
|
402
|
+
message: AgentWidgetMessage,
|
|
403
|
+
transform: MessageTransform,
|
|
404
|
+
layoutConfig?: AgentWidgetMessageLayoutConfig,
|
|
405
|
+
actionsConfig?: AgentWidgetMessageActionsConfig,
|
|
406
|
+
actionCallbacks?: MessageActionCallbacks
|
|
407
|
+
): HTMLElement => {
|
|
408
|
+
const config = layoutConfig ?? {};
|
|
409
|
+
const layout = config.layout ?? "bubble";
|
|
410
|
+
const avatarConfig = config.avatar;
|
|
411
|
+
const timestampConfig = config.timestamp;
|
|
412
|
+
const showAvatar = avatarConfig?.show ?? false;
|
|
413
|
+
const showTimestamp = timestampConfig?.show ?? false;
|
|
414
|
+
const avatarPosition = avatarConfig?.position ?? "left";
|
|
415
|
+
const timestampPosition = timestampConfig?.position ?? "below";
|
|
416
|
+
|
|
417
|
+
// Create the bubble element
|
|
418
|
+
const classes = getBubbleClasses(message.role, layout);
|
|
419
|
+
const bubble = createElement("div", classes.join(" "));
|
|
420
|
+
// Set id for idiomorph matching
|
|
421
|
+
bubble.id = `bubble-${message.id}`;
|
|
422
|
+
bubble.setAttribute("data-message-id", message.id);
|
|
423
|
+
|
|
424
|
+
// Add message content
|
|
425
|
+
const contentDiv = document.createElement("div");
|
|
426
|
+
contentDiv.innerHTML = transform({
|
|
427
|
+
text: message.content,
|
|
428
|
+
message,
|
|
429
|
+
streaming: Boolean(message.streaming),
|
|
430
|
+
raw: message.rawContent
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Add inline timestamp if configured
|
|
434
|
+
if (showTimestamp && timestampPosition === "inline" && message.createdAt) {
|
|
435
|
+
const timestamp = createTimestamp(message, timestampConfig!);
|
|
436
|
+
timestamp.classList.add("tvw-ml-2", "tvw-inline");
|
|
437
|
+
contentDiv.appendChild(timestamp);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
bubble.appendChild(contentDiv);
|
|
441
|
+
|
|
442
|
+
// Add timestamp below if configured
|
|
443
|
+
if (showTimestamp && timestampPosition === "below" && message.createdAt) {
|
|
444
|
+
const timestamp = createTimestamp(message, timestampConfig!);
|
|
445
|
+
timestamp.classList.add("tvw-mt-1");
|
|
446
|
+
bubble.appendChild(timestamp);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Add typing indicator if this is a streaming assistant message
|
|
450
|
+
if (message.streaming && message.role === "assistant") {
|
|
451
|
+
if (!message.content || !message.content.trim()) {
|
|
452
|
+
const typingIndicator = createTypingIndicator();
|
|
453
|
+
bubble.appendChild(typingIndicator);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Add message actions for assistant messages (only when not streaming and has content)
|
|
458
|
+
const shouldShowActions =
|
|
459
|
+
message.role === "assistant" &&
|
|
460
|
+
!message.streaming &&
|
|
461
|
+
message.content &&
|
|
462
|
+
message.content.trim() &&
|
|
463
|
+
actionsConfig?.enabled !== false;
|
|
464
|
+
|
|
465
|
+
if (shouldShowActions && actionsConfig) {
|
|
466
|
+
const actions = createMessageActions(message, actionsConfig, actionCallbacks);
|
|
467
|
+
bubble.appendChild(actions);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// If no avatar needed, return bubble directly
|
|
471
|
+
if (!showAvatar || message.role === "system") {
|
|
472
|
+
return bubble;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Create wrapper with avatar
|
|
476
|
+
const wrapper = createElement(
|
|
477
|
+
"div",
|
|
478
|
+
`tvw-flex tvw-gap-2 ${message.role === "user" ? "tvw-flex-row-reverse" : ""}`
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const avatar = createAvatar(avatarConfig!, message.role);
|
|
482
|
+
|
|
483
|
+
if (avatarPosition === "right" || (avatarPosition === "left" && message.role === "user")) {
|
|
484
|
+
wrapper.append(bubble, avatar);
|
|
485
|
+
} else {
|
|
486
|
+
wrapper.append(avatar, bubble);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Adjust bubble max-width when avatar is present
|
|
490
|
+
bubble.classList.remove("tvw-max-w-[85%]");
|
|
491
|
+
bubble.classList.add("tvw-max-w-[calc(85%-2.5rem)]");
|
|
492
|
+
|
|
493
|
+
return wrapper;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Create bubble with custom renderer support
|
|
498
|
+
* Uses custom renderer if provided in layout config, otherwise falls back to standard bubble
|
|
499
|
+
*/
|
|
500
|
+
export const createBubbleWithLayout = (
|
|
501
|
+
message: AgentWidgetMessage,
|
|
502
|
+
transform: MessageTransform,
|
|
503
|
+
layoutConfig?: AgentWidgetMessageLayoutConfig,
|
|
504
|
+
actionsConfig?: AgentWidgetMessageActionsConfig,
|
|
505
|
+
actionCallbacks?: MessageActionCallbacks
|
|
506
|
+
): HTMLElement => {
|
|
507
|
+
const config = layoutConfig ?? {};
|
|
508
|
+
|
|
509
|
+
// Check for custom renderers
|
|
510
|
+
if (message.role === "user" && config.renderUserMessage) {
|
|
511
|
+
return config.renderUserMessage({
|
|
512
|
+
message,
|
|
513
|
+
config: {} as any, // Will be populated by caller
|
|
514
|
+
streaming: Boolean(message.streaming)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (message.role === "assistant" && config.renderAssistantMessage) {
|
|
519
|
+
return config.renderAssistantMessage({
|
|
520
|
+
message,
|
|
521
|
+
config: {} as any, // Will be populated by caller
|
|
522
|
+
streaming: Boolean(message.streaming)
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Fall back to standard bubble
|
|
527
|
+
return createStandardBubble(message, transform, layoutConfig, actionsConfig, actionCallbacks);
|
|
528
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createElement, createFragment } from "../utils/dom";
|
|
2
|
+
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
|
+
import { MessageTransform, MessageActionCallbacks } from "./message-bubble";
|
|
4
|
+
import { createStandardBubble } from "./message-bubble";
|
|
5
|
+
import { createReasoningBubble } from "./reasoning-bubble";
|
|
6
|
+
import { createToolBubble } from "./tool-bubble";
|
|
7
|
+
|
|
8
|
+
export const renderMessages = (
|
|
9
|
+
container: HTMLElement,
|
|
10
|
+
messages: AgentWidgetMessage[],
|
|
11
|
+
transform: MessageTransform,
|
|
12
|
+
showReasoning: boolean,
|
|
13
|
+
showToolCalls: boolean,
|
|
14
|
+
config?: AgentWidgetConfig,
|
|
15
|
+
actionCallbacks?: MessageActionCallbacks
|
|
16
|
+
) => {
|
|
17
|
+
container.innerHTML = "";
|
|
18
|
+
const fragment = createFragment();
|
|
19
|
+
|
|
20
|
+
messages.forEach((message) => {
|
|
21
|
+
let bubble: HTMLElement;
|
|
22
|
+
if (message.variant === "reasoning" && message.reasoning) {
|
|
23
|
+
if (!showReasoning) return;
|
|
24
|
+
bubble = createReasoningBubble(message);
|
|
25
|
+
} else if (message.variant === "tool" && message.toolCall) {
|
|
26
|
+
if (!showToolCalls) return;
|
|
27
|
+
bubble = createToolBubble(message, config);
|
|
28
|
+
} else {
|
|
29
|
+
bubble = createStandardBubble(
|
|
30
|
+
message,
|
|
31
|
+
transform,
|
|
32
|
+
config?.layout?.messages,
|
|
33
|
+
config?.messageActions,
|
|
34
|
+
actionCallbacks
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const wrapper = createElement("div", "tvw-flex");
|
|
39
|
+
if (message.role === "user") {
|
|
40
|
+
wrapper.classList.add("tvw-justify-end");
|
|
41
|
+
}
|
|
42
|
+
wrapper.appendChild(bubble);
|
|
43
|
+
fragment.appendChild(wrapper);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
container.appendChild(fragment);
|
|
47
|
+
container.scrollTop = container.scrollHeight;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|