vanilla-agent 0.1.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 +310 -0
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +370 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.global.js +1710 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +14 -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 +752 -0
- package/package.json +61 -0
- package/src/client.ts +577 -0
- package/src/components/forms.ts +165 -0
- package/src/components/launcher.ts +184 -0
- package/src/components/message-bubble.ts +52 -0
- package/src/components/messages.ts +43 -0
- package/src/components/panel.ts +555 -0
- package/src/components/reasoning-bubble.ts +114 -0
- package/src/components/suggestions.ts +52 -0
- package/src/components/tool-bubble.ts +158 -0
- package/src/index.ts +37 -0
- package/src/install.ts +159 -0
- package/src/plugins/registry.ts +72 -0
- package/src/plugins/types.ts +90 -0
- package/src/postprocessors.ts +76 -0
- package/src/runtime/init.ts +116 -0
- package/src/session.ts +206 -0
- package/src/styles/tailwind.css +19 -0
- package/src/styles/widget.css +752 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +1325 -0
- package/src/utils/constants.ts +11 -0
- package/src/utils/dom.ts +20 -0
- package/src/utils/formatting.ts +77 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/positioning.ts +12 -0
- package/src/utils/theme.ts +20 -0
- package/src/widget.css +1 -0
- package/widget.css +1 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import { ChatWidgetMessage, ChatWidgetConfig } from "../types";
|
|
3
|
+
import { ChatWidgetSession } from "../session";
|
|
4
|
+
|
|
5
|
+
export const formDefinitions: Record<
|
|
6
|
+
string,
|
|
7
|
+
{
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
fields: Array<{
|
|
11
|
+
name: string;
|
|
12
|
+
label: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
type?: "text" | "email" | "textarea";
|
|
15
|
+
required?: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
submitLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
> = {
|
|
20
|
+
init: {
|
|
21
|
+
title: "Schedule a Demo",
|
|
22
|
+
description: "Share the basics and we'll follow up with a confirmation.",
|
|
23
|
+
fields: [
|
|
24
|
+
{ name: "name", label: "Full name", placeholder: "Jane Doe", required: true },
|
|
25
|
+
{ name: "email", label: "Work email", placeholder: "jane@example.com", type: "email", required: true },
|
|
26
|
+
{ name: "notes", label: "What would you like to cover?", type: "textarea" }
|
|
27
|
+
],
|
|
28
|
+
submitLabel: "Submit details"
|
|
29
|
+
},
|
|
30
|
+
followup: {
|
|
31
|
+
title: "Additional Information",
|
|
32
|
+
description: "Provide any extra details to tailor the next steps.",
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: "company", label: "Company", placeholder: "Acme Inc." },
|
|
35
|
+
{ name: "context", label: "Context", type: "textarea", placeholder: "Share more about your use case" }
|
|
36
|
+
],
|
|
37
|
+
submitLabel: "Send"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const enhanceWithForms = (
|
|
42
|
+
bubble: HTMLElement,
|
|
43
|
+
message: ChatWidgetMessage,
|
|
44
|
+
config: ChatWidgetConfig,
|
|
45
|
+
session: ChatWidgetSession
|
|
46
|
+
) => {
|
|
47
|
+
const placeholders = bubble.querySelectorAll<HTMLElement>("[data-tv-form]");
|
|
48
|
+
if (placeholders.length) {
|
|
49
|
+
placeholders.forEach((placeholder) => {
|
|
50
|
+
if (placeholder.dataset.enhanced === "true") return;
|
|
51
|
+
const type = placeholder.dataset.tvForm ?? "init";
|
|
52
|
+
placeholder.dataset.enhanced = "true";
|
|
53
|
+
|
|
54
|
+
const definition = formDefinitions[type] ?? formDefinitions.init;
|
|
55
|
+
placeholder.classList.add("tvw-form-card", "tvw-space-y-4");
|
|
56
|
+
|
|
57
|
+
const heading = createElement("div", "tvw-space-y-1");
|
|
58
|
+
const title = createElement(
|
|
59
|
+
"h3",
|
|
60
|
+
"tvw-text-base tvw-font-semibold tvw-text-cw-primary"
|
|
61
|
+
);
|
|
62
|
+
title.textContent = definition.title;
|
|
63
|
+
heading.appendChild(title);
|
|
64
|
+
if (definition.description) {
|
|
65
|
+
const desc = createElement(
|
|
66
|
+
"p",
|
|
67
|
+
"tvw-text-sm tvw-text-cw-muted"
|
|
68
|
+
);
|
|
69
|
+
desc.textContent = definition.description;
|
|
70
|
+
heading.appendChild(desc);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const form = document.createElement("form");
|
|
74
|
+
form.className = "tvw-form-grid tvw-space-y-3";
|
|
75
|
+
|
|
76
|
+
definition.fields.forEach((field) => {
|
|
77
|
+
const group = createElement("label", "tvw-form-field tvw-flex tvw-flex-col tvw-gap-1");
|
|
78
|
+
group.htmlFor = `${message.id}-${type}-${field.name}`;
|
|
79
|
+
const label = createElement("span", "tvw-text-xs tvw-font-medium tvw-text-cw-muted");
|
|
80
|
+
label.textContent = field.label;
|
|
81
|
+
group.appendChild(label);
|
|
82
|
+
|
|
83
|
+
const inputType = field.type ?? "text";
|
|
84
|
+
let control: HTMLInputElement | HTMLTextAreaElement;
|
|
85
|
+
if (inputType === "textarea") {
|
|
86
|
+
control = document.createElement("textarea");
|
|
87
|
+
control.rows = 3;
|
|
88
|
+
} else {
|
|
89
|
+
control = document.createElement("input");
|
|
90
|
+
control.type = inputType;
|
|
91
|
+
}
|
|
92
|
+
control.className =
|
|
93
|
+
"tvw-rounded-xl tvw-border tvw-border-gray-200 tvw-bg-white tvw-px-3 tvw-py-2 tvw-text-sm tvw-text-cw-primary focus:tvw-outline-none focus:tvw-border-cw-primary";
|
|
94
|
+
control.id = `${message.id}-${type}-${field.name}`;
|
|
95
|
+
control.name = field.name;
|
|
96
|
+
control.placeholder = field.placeholder ?? "";
|
|
97
|
+
if (field.required) {
|
|
98
|
+
control.required = true;
|
|
99
|
+
}
|
|
100
|
+
group.appendChild(control);
|
|
101
|
+
form.appendChild(group);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const actions = createElement(
|
|
105
|
+
"div",
|
|
106
|
+
"tvw-flex tvw-items-center tvw-justify-between tvw-gap-2"
|
|
107
|
+
);
|
|
108
|
+
const status = createElement(
|
|
109
|
+
"div",
|
|
110
|
+
"tvw-text-xs tvw-text-cw-muted tvw-min-h-[1.5rem]"
|
|
111
|
+
);
|
|
112
|
+
const submit = createElement(
|
|
113
|
+
"button",
|
|
114
|
+
"tvw-inline-flex tvw-items-center tvw-rounded-full tvw-bg-cw-primary tvw-px-4 tvw-py-2 tvw-text-sm tvw-font-semibold tvw-text-white disabled:tvw-opacity-60 tvw-cursor-pointer"
|
|
115
|
+
) as HTMLButtonElement;
|
|
116
|
+
submit.type = "submit";
|
|
117
|
+
submit.textContent = definition.submitLabel ?? "Submit";
|
|
118
|
+
actions.appendChild(status);
|
|
119
|
+
actions.appendChild(submit);
|
|
120
|
+
form.appendChild(actions);
|
|
121
|
+
|
|
122
|
+
placeholder.replaceChildren(heading, form);
|
|
123
|
+
|
|
124
|
+
form.addEventListener("submit", async (event) => {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
const formEndpoint = config.formEndpoint ?? "/form";
|
|
127
|
+
const formData = new FormData(form as HTMLFormElement);
|
|
128
|
+
const payload: Record<string, unknown> = {};
|
|
129
|
+
formData.forEach((value, key) => {
|
|
130
|
+
payload[key] = value;
|
|
131
|
+
});
|
|
132
|
+
payload["type"] = type;
|
|
133
|
+
|
|
134
|
+
submit.disabled = true;
|
|
135
|
+
status.textContent = "Submitting…";
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(formEndpoint, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json"
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify(payload)
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Form submission failed (${response.status})`);
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
status.textContent = data.message ?? "Thanks! We'll be in touch soon.";
|
|
150
|
+
if (data.success && data.nextPrompt) {
|
|
151
|
+
await session.sendMessage(String(data.nextPrompt));
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
status.textContent =
|
|
155
|
+
error instanceof Error ? error.message : "Something went wrong. Please try again.";
|
|
156
|
+
} finally {
|
|
157
|
+
submit.disabled = false;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import { ChatWidgetConfig } from "../types";
|
|
3
|
+
import { positionMap } from "../utils/positioning";
|
|
4
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
5
|
+
|
|
6
|
+
export interface LauncherButton {
|
|
7
|
+
element: HTMLButtonElement;
|
|
8
|
+
update: (config: ChatWidgetConfig) => void;
|
|
9
|
+
destroy: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const createLauncherButton = (
|
|
13
|
+
config: ChatWidgetConfig | undefined,
|
|
14
|
+
onToggle: () => void
|
|
15
|
+
): LauncherButton => {
|
|
16
|
+
const button = createElement("button") as HTMLButtonElement;
|
|
17
|
+
button.type = "button";
|
|
18
|
+
button.innerHTML = `
|
|
19
|
+
<span class="tvw-inline-flex tvw-items-center tvw-justify-center tvw-rounded-full tvw-bg-cw-primary tvw-text-white" data-role="launcher-icon">💬</span>
|
|
20
|
+
<img data-role="launcher-image" class="tvw-rounded-full tvw-object-cover" alt="" style="display:none" />
|
|
21
|
+
<span class="tvw-flex tvw-flex-col tvw-items-start tvw-text-left">
|
|
22
|
+
<span class="tvw-text-sm tvw-font-semibold tvw-text-cw-primary" data-role="launcher-title"></span>
|
|
23
|
+
<span class="tvw-text-xs tvw-text-cw-muted" data-role="launcher-subtitle"></span>
|
|
24
|
+
</span>
|
|
25
|
+
<span class="tvw-ml-2 tvw-grid tvw-place-items-center tvw-rounded-full tvw-bg-cw-primary tvw-text-cw-call-to-action" data-role="launcher-call-to-action-icon">↗</span>
|
|
26
|
+
`;
|
|
27
|
+
button.addEventListener("click", onToggle);
|
|
28
|
+
|
|
29
|
+
const update = (newConfig: ChatWidgetConfig) => {
|
|
30
|
+
const launcher = newConfig.launcher ?? {};
|
|
31
|
+
|
|
32
|
+
const titleEl = button.querySelector("[data-role='launcher-title']");
|
|
33
|
+
if (titleEl) {
|
|
34
|
+
titleEl.textContent = launcher.title ?? "Chat Assistant";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const subtitleEl = button.querySelector("[data-role='launcher-subtitle']");
|
|
38
|
+
if (subtitleEl) {
|
|
39
|
+
subtitleEl.textContent = launcher.subtitle ?? "Get answers fast";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Hide/show text container
|
|
43
|
+
const textContainer = button.querySelector(".tvw-flex-col");
|
|
44
|
+
if (textContainer) {
|
|
45
|
+
if (launcher.textHidden) {
|
|
46
|
+
(textContainer as HTMLElement).style.display = "none";
|
|
47
|
+
} else {
|
|
48
|
+
(textContainer as HTMLElement).style.display = "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const icon = button.querySelector<HTMLSpanElement>("[data-role='launcher-icon']");
|
|
53
|
+
if (icon) {
|
|
54
|
+
if (launcher.agentIconHidden) {
|
|
55
|
+
icon.style.display = "none";
|
|
56
|
+
} else {
|
|
57
|
+
const iconSize = launcher.agentIconSize ?? "40px";
|
|
58
|
+
icon.style.height = iconSize;
|
|
59
|
+
icon.style.width = iconSize;
|
|
60
|
+
|
|
61
|
+
// Clear existing content
|
|
62
|
+
icon.innerHTML = "";
|
|
63
|
+
|
|
64
|
+
// Render icon based on priority: Lucide icon > iconUrl > agentIconText
|
|
65
|
+
if (launcher.agentIconName) {
|
|
66
|
+
// Use Lucide icon
|
|
67
|
+
const iconSizeNum = parseFloat(iconSize) || 24;
|
|
68
|
+
const iconSvg = renderLucideIcon(launcher.agentIconName, iconSizeNum * 0.6, "#ffffff", 2);
|
|
69
|
+
if (iconSvg) {
|
|
70
|
+
icon.appendChild(iconSvg);
|
|
71
|
+
icon.style.display = "";
|
|
72
|
+
} else {
|
|
73
|
+
// Fallback to agentIconText if Lucide icon fails
|
|
74
|
+
icon.textContent = launcher.agentIconText ?? "💬";
|
|
75
|
+
icon.style.display = "";
|
|
76
|
+
}
|
|
77
|
+
} else if (launcher.iconUrl) {
|
|
78
|
+
// Use image URL - hide icon span and show img
|
|
79
|
+
icon.style.display = "none";
|
|
80
|
+
} else {
|
|
81
|
+
// Use text/emoji
|
|
82
|
+
icon.textContent = launcher.agentIconText ?? "💬";
|
|
83
|
+
icon.style.display = "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const img = button.querySelector<HTMLImageElement>("[data-role='launcher-image']");
|
|
89
|
+
if (img) {
|
|
90
|
+
const iconSize = launcher.agentIconSize ?? "40px";
|
|
91
|
+
img.style.height = iconSize;
|
|
92
|
+
img.style.width = iconSize;
|
|
93
|
+
if (launcher.iconUrl && !launcher.agentIconName && !launcher.agentIconHidden) {
|
|
94
|
+
// Only show image if not using Lucide icon and not hidden
|
|
95
|
+
img.src = launcher.iconUrl;
|
|
96
|
+
img.style.display = "block";
|
|
97
|
+
} else {
|
|
98
|
+
img.style.display = "none";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const callToActionIconEl = button.querySelector<HTMLSpanElement>("[data-role='launcher-call-to-action-icon']");
|
|
103
|
+
if (callToActionIconEl) {
|
|
104
|
+
const callToActionIconSize = launcher.callToActionIconSize ?? "32px";
|
|
105
|
+
callToActionIconEl.style.height = callToActionIconSize;
|
|
106
|
+
callToActionIconEl.style.width = callToActionIconSize;
|
|
107
|
+
|
|
108
|
+
// Apply background color if configured
|
|
109
|
+
if (launcher.callToActionIconBackgroundColor) {
|
|
110
|
+
callToActionIconEl.style.backgroundColor = launcher.callToActionIconBackgroundColor;
|
|
111
|
+
callToActionIconEl.classList.remove("tvw-bg-cw-primary");
|
|
112
|
+
} else {
|
|
113
|
+
callToActionIconEl.style.backgroundColor = "";
|
|
114
|
+
callToActionIconEl.classList.add("tvw-bg-cw-primary");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Calculate padding to adjust icon size
|
|
118
|
+
let paddingTotal = 0;
|
|
119
|
+
if (launcher.callToActionIconPadding) {
|
|
120
|
+
callToActionIconEl.style.boxSizing = "border-box";
|
|
121
|
+
callToActionIconEl.style.padding = launcher.callToActionIconPadding;
|
|
122
|
+
// Parse padding value to calculate total padding (padding applies to both sides)
|
|
123
|
+
const paddingValue = parseFloat(launcher.callToActionIconPadding) || 0;
|
|
124
|
+
paddingTotal = paddingValue * 2; // padding on both sides
|
|
125
|
+
} else {
|
|
126
|
+
callToActionIconEl.style.boxSizing = "";
|
|
127
|
+
callToActionIconEl.style.padding = "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (launcher.callToActionIconHidden) {
|
|
131
|
+
callToActionIconEl.style.display = "none";
|
|
132
|
+
} else {
|
|
133
|
+
callToActionIconEl.style.display = "";
|
|
134
|
+
|
|
135
|
+
// Clear existing content
|
|
136
|
+
callToActionIconEl.innerHTML = "";
|
|
137
|
+
|
|
138
|
+
// Use Lucide icon if provided, otherwise fall back to text
|
|
139
|
+
if (launcher.callToActionIconName) {
|
|
140
|
+
// Calculate actual icon size by subtracting padding
|
|
141
|
+
const containerSize = parseFloat(callToActionIconSize) || 24;
|
|
142
|
+
const iconSize = Math.max(containerSize - paddingTotal, 8); // Ensure minimum size of 8px
|
|
143
|
+
const iconSvg = renderLucideIcon(launcher.callToActionIconName, iconSize, "currentColor", 2);
|
|
144
|
+
if (iconSvg) {
|
|
145
|
+
callToActionIconEl.appendChild(iconSvg);
|
|
146
|
+
} else {
|
|
147
|
+
// Fallback to text if icon fails to render
|
|
148
|
+
callToActionIconEl.textContent = launcher.callToActionIconText ?? "↗";
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
callToActionIconEl.textContent = launcher.callToActionIconText ?? "↗";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const positionClass =
|
|
157
|
+
launcher.position && positionMap[launcher.position]
|
|
158
|
+
? positionMap[launcher.position]
|
|
159
|
+
: positionMap["bottom-right"];
|
|
160
|
+
|
|
161
|
+
const base =
|
|
162
|
+
"tvw-fixed tvw-flex tvw-items-center tvw-gap-3 tvw-rounded-launcher tvw-bg-cw-surface tvw-py-2.5 tvw-pl-3 tvw-pr-3 tvw-shadow-lg tvw-border tvw-border-gray-200 tvw-transition hover:tvw-translate-y-[-2px] tvw-cursor-pointer";
|
|
163
|
+
|
|
164
|
+
button.className = `${base} ${positionClass}`;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const destroy = () => {
|
|
168
|
+
button.removeEventListener("click", onToggle);
|
|
169
|
+
button.remove();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Initial update
|
|
173
|
+
if (config) {
|
|
174
|
+
update(config);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
element: button,
|
|
179
|
+
update,
|
|
180
|
+
destroy
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import { ChatWidgetMessage } from "../types";
|
|
3
|
+
|
|
4
|
+
export type MessageTransform = (context: {
|
|
5
|
+
text: string;
|
|
6
|
+
message: ChatWidgetMessage;
|
|
7
|
+
streaming: boolean;
|
|
8
|
+
}) => string;
|
|
9
|
+
|
|
10
|
+
export const createStandardBubble = (
|
|
11
|
+
message: ChatWidgetMessage,
|
|
12
|
+
transform: MessageTransform
|
|
13
|
+
): HTMLElement => {
|
|
14
|
+
const classes = [
|
|
15
|
+
"tvw-max-w-[85%]",
|
|
16
|
+
"tvw-rounded-2xl",
|
|
17
|
+
"tvw-text-sm",
|
|
18
|
+
"tvw-leading-relaxed",
|
|
19
|
+
"tvw-shadow-sm"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (message.role === "user") {
|
|
23
|
+
classes.push(
|
|
24
|
+
"tvw-ml-auto",
|
|
25
|
+
"tvw-bg-cw-accent",
|
|
26
|
+
"tvw-text-white",
|
|
27
|
+
"tvw-px-5",
|
|
28
|
+
"tvw-py-3"
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
classes.push(
|
|
32
|
+
"tvw-bg-cw-surface",
|
|
33
|
+
"tvw-border",
|
|
34
|
+
"tvw-border-cw-message-border",
|
|
35
|
+
"tvw-text-cw-primary",
|
|
36
|
+
"tvw-px-5",
|
|
37
|
+
"tvw-py-3"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bubble = createElement("div", classes.join(" "));
|
|
42
|
+
bubble.innerHTML = transform({
|
|
43
|
+
text: message.content,
|
|
44
|
+
message,
|
|
45
|
+
streaming: Boolean(message.streaming)
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return bubble;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createElement, createFragment } from "../utils/dom";
|
|
2
|
+
import { ChatWidgetMessage } from "../types";
|
|
3
|
+
import { MessageTransform } 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: ChatWidgetMessage[],
|
|
11
|
+
transform: MessageTransform,
|
|
12
|
+
showReasoning: boolean,
|
|
13
|
+
showToolCalls: boolean
|
|
14
|
+
) => {
|
|
15
|
+
container.innerHTML = "";
|
|
16
|
+
const fragment = createFragment();
|
|
17
|
+
|
|
18
|
+
messages.forEach((message) => {
|
|
19
|
+
let bubble: HTMLElement;
|
|
20
|
+
if (message.variant === "reasoning" && message.reasoning) {
|
|
21
|
+
if (!showReasoning) return;
|
|
22
|
+
bubble = createReasoningBubble(message);
|
|
23
|
+
} else if (message.variant === "tool" && message.toolCall) {
|
|
24
|
+
if (!showToolCalls) return;
|
|
25
|
+
bubble = createToolBubble(message);
|
|
26
|
+
} else {
|
|
27
|
+
bubble = createStandardBubble(message, transform);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wrapper = createElement("div", "tvw-flex");
|
|
31
|
+
if (message.role === "user") {
|
|
32
|
+
wrapper.classList.add("tvw-justify-end");
|
|
33
|
+
}
|
|
34
|
+
wrapper.appendChild(bubble);
|
|
35
|
+
fragment.appendChild(wrapper);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
container.appendChild(fragment);
|
|
39
|
+
container.scrollTop = container.scrollHeight;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|