@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.
Files changed (61) hide show
  1. package/README.md +1080 -0
  2. package/dist/index.cjs +140 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2626 -0
  5. package/dist/index.d.ts +2626 -0
  6. package/dist/index.global.js +1843 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +140 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/install.global.js +2 -0
  11. package/dist/install.global.js.map +1 -0
  12. package/dist/widget.css +1627 -0
  13. package/package.json +79 -0
  14. package/src/@types/idiomorph.d.ts +37 -0
  15. package/src/client.test.ts +387 -0
  16. package/src/client.ts +1589 -0
  17. package/src/components/composer-builder.ts +530 -0
  18. package/src/components/feedback.ts +379 -0
  19. package/src/components/forms.ts +170 -0
  20. package/src/components/header-builder.ts +455 -0
  21. package/src/components/header-layouts.ts +303 -0
  22. package/src/components/launcher.ts +193 -0
  23. package/src/components/message-bubble.ts +528 -0
  24. package/src/components/messages.ts +54 -0
  25. package/src/components/panel.ts +204 -0
  26. package/src/components/reasoning-bubble.ts +144 -0
  27. package/src/components/registry.ts +87 -0
  28. package/src/components/suggestions.ts +97 -0
  29. package/src/components/tool-bubble.ts +288 -0
  30. package/src/defaults.ts +321 -0
  31. package/src/index.ts +175 -0
  32. package/src/install.ts +284 -0
  33. package/src/plugins/registry.ts +77 -0
  34. package/src/plugins/types.ts +95 -0
  35. package/src/postprocessors.ts +194 -0
  36. package/src/runtime/init.ts +162 -0
  37. package/src/session.ts +376 -0
  38. package/src/styles/tailwind.css +20 -0
  39. package/src/styles/widget.css +1627 -0
  40. package/src/types.ts +1635 -0
  41. package/src/ui.ts +3341 -0
  42. package/src/utils/actions.ts +227 -0
  43. package/src/utils/attachment-manager.ts +384 -0
  44. package/src/utils/code-generators.test.ts +500 -0
  45. package/src/utils/code-generators.ts +1806 -0
  46. package/src/utils/component-middleware.ts +137 -0
  47. package/src/utils/component-parser.ts +119 -0
  48. package/src/utils/constants.ts +16 -0
  49. package/src/utils/content.ts +306 -0
  50. package/src/utils/dom.ts +25 -0
  51. package/src/utils/events.ts +41 -0
  52. package/src/utils/formatting.test.ts +166 -0
  53. package/src/utils/formatting.ts +470 -0
  54. package/src/utils/icons.ts +92 -0
  55. package/src/utils/message-id.ts +37 -0
  56. package/src/utils/morph.ts +36 -0
  57. package/src/utils/positioning.ts +17 -0
  58. package/src/utils/storage.ts +72 -0
  59. package/src/utils/theme.ts +105 -0
  60. package/src/widget.css +1 -0
  61. 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
+