vanilla-agent 1.20.0 → 1.22.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 +87 -0
- package/dist/index.cjs +24 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +608 -6
- package/dist/index.d.ts +608 -6
- package/dist/index.global.js +44 -44
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +23 -23
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +467 -5
- package/package.json +1 -1
- package/src/client.ts +215 -1
- package/src/components/launcher.ts +10 -1
- package/src/components/message-bubble.ts +208 -4
- package/src/components/messages.ts +10 -3
- package/src/defaults.ts +32 -0
- package/src/index.ts +20 -4
- package/src/install.ts +69 -7
- package/src/postprocessors.ts +124 -6
- package/src/session.ts +77 -1
- package/src/styles/widget.css +467 -5
- package/src/types.ts +487 -0
- package/src/ui.ts +40 -5
package/src/client.ts
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
AgentWidgetCustomFetch,
|
|
10
10
|
AgentWidgetSSEEventParser,
|
|
11
11
|
AgentWidgetHeadersFunction,
|
|
12
|
-
AgentWidgetSSEEventResult
|
|
12
|
+
AgentWidgetSSEEventResult,
|
|
13
|
+
ClientSession,
|
|
14
|
+
ClientInitResponse,
|
|
15
|
+
ClientChatRequest
|
|
13
16
|
} from "./types";
|
|
14
17
|
import {
|
|
15
18
|
extractTextFromJson,
|
|
@@ -27,6 +30,7 @@ type DispatchOptions = {
|
|
|
27
30
|
type SSEHandler = (event: AgentWidgetEvent) => void;
|
|
28
31
|
|
|
29
32
|
const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
|
|
33
|
+
const DEFAULT_CLIENT_API_BASE = "https://api.travrse.ai";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Maps parserType string to the corresponding parser factory function
|
|
@@ -55,6 +59,10 @@ export class AgentWidgetClient {
|
|
|
55
59
|
private readonly customFetch?: AgentWidgetCustomFetch;
|
|
56
60
|
private readonly parseSSEEvent?: AgentWidgetSSEEventParser;
|
|
57
61
|
private readonly getHeaders?: AgentWidgetHeadersFunction;
|
|
62
|
+
|
|
63
|
+
// Client token mode properties
|
|
64
|
+
private clientSession: ClientSession | null = null;
|
|
65
|
+
private sessionInitPromise: Promise<ClientSession> | null = null;
|
|
58
66
|
|
|
59
67
|
constructor(private config: AgentWidgetConfig = {}) {
|
|
60
68
|
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
@@ -72,7 +80,213 @@ export class AgentWidgetClient {
|
|
|
72
80
|
this.getHeaders = config.getHeaders;
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Check if running in client token mode
|
|
85
|
+
*/
|
|
86
|
+
public isClientTokenMode(): boolean {
|
|
87
|
+
return !!this.config.clientToken;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the appropriate API URL based on mode
|
|
92
|
+
*/
|
|
93
|
+
private getClientApiUrl(endpoint: 'init' | 'chat'): string {
|
|
94
|
+
const baseUrl = this.config.apiUrl?.replace(/\/+$/, '').replace(/\/v1\/dispatch$/, '') || DEFAULT_CLIENT_API_BASE;
|
|
95
|
+
return endpoint === 'init'
|
|
96
|
+
? `${baseUrl}/v1/client/init`
|
|
97
|
+
: `${baseUrl}/v1/client/chat`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the current client session (if any)
|
|
102
|
+
*/
|
|
103
|
+
public getClientSession(): ClientSession | null {
|
|
104
|
+
return this.clientSession;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Initialize session for client token mode.
|
|
109
|
+
* Called automatically on first message if not already initialized.
|
|
110
|
+
*/
|
|
111
|
+
public async initSession(): Promise<ClientSession> {
|
|
112
|
+
if (!this.isClientTokenMode()) {
|
|
113
|
+
throw new Error('initSession() only available in client token mode');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return existing session if valid
|
|
117
|
+
if (this.clientSession && new Date() < this.clientSession.expiresAt) {
|
|
118
|
+
return this.clientSession;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Deduplicate concurrent init calls
|
|
122
|
+
if (this.sessionInitPromise) {
|
|
123
|
+
return this.sessionInitPromise;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.sessionInitPromise = this._doInitSession();
|
|
127
|
+
try {
|
|
128
|
+
const session = await this.sessionInitPromise;
|
|
129
|
+
this.clientSession = session;
|
|
130
|
+
this.config.onSessionInit?.(session);
|
|
131
|
+
return session;
|
|
132
|
+
} finally {
|
|
133
|
+
this.sessionInitPromise = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async _doInitSession(): Promise<ClientSession> {
|
|
138
|
+
const response = await fetch(this.getClientApiUrl('init'), {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
token: this.config.clientToken,
|
|
145
|
+
flow_id: this.config.flowId,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const error = await response.json().catch(() => ({ error: 'Session initialization failed' }));
|
|
151
|
+
if (response.status === 401) {
|
|
152
|
+
throw new Error(`Invalid client token: ${error.hint || error.error}`);
|
|
153
|
+
}
|
|
154
|
+
if (response.status === 403) {
|
|
155
|
+
throw new Error(`Origin not allowed: ${error.hint || error.error}`);
|
|
156
|
+
}
|
|
157
|
+
throw new Error(error.error || 'Failed to initialize session');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const data: ClientInitResponse = await response.json();
|
|
161
|
+
return {
|
|
162
|
+
sessionId: data.session_id,
|
|
163
|
+
expiresAt: new Date(data.expires_at),
|
|
164
|
+
flow: data.flow,
|
|
165
|
+
config: {
|
|
166
|
+
welcomeMessage: data.config.welcome_message,
|
|
167
|
+
placeholder: data.config.placeholder,
|
|
168
|
+
theme: data.config.theme,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear the current client session
|
|
175
|
+
*/
|
|
176
|
+
public clearClientSession(): void {
|
|
177
|
+
this.clientSession = null;
|
|
178
|
+
this.sessionInitPromise = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Send a message - handles both proxy and client token modes
|
|
183
|
+
*/
|
|
75
184
|
public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
|
|
185
|
+
if (this.isClientTokenMode()) {
|
|
186
|
+
return this.dispatchClientToken(options, onEvent);
|
|
187
|
+
}
|
|
188
|
+
return this.dispatchProxy(options, onEvent);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Client token mode dispatch
|
|
193
|
+
*/
|
|
194
|
+
private async dispatchClientToken(options: DispatchOptions, onEvent: SSEHandler) {
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
if (options.signal) {
|
|
197
|
+
options.signal.addEventListener("abort", () => controller.abort());
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
onEvent({ type: "status", status: "connecting" });
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Ensure session is initialized
|
|
204
|
+
const session = await this.initSession();
|
|
205
|
+
|
|
206
|
+
// Check if session is about to expire (within 1 minute)
|
|
207
|
+
if (new Date() >= new Date(session.expiresAt.getTime() - 60000)) {
|
|
208
|
+
// Session expired or expiring soon
|
|
209
|
+
this.clientSession = null;
|
|
210
|
+
this.config.onSessionExpired?.();
|
|
211
|
+
const error = new Error('Session expired. Please refresh to continue.');
|
|
212
|
+
onEvent({ type: "error", error });
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build the chat request payload
|
|
217
|
+
const chatRequest: ClientChatRequest = {
|
|
218
|
+
session_id: session.sessionId,
|
|
219
|
+
messages: options.messages.map(m => ({
|
|
220
|
+
role: m.role,
|
|
221
|
+
content: m.rawContent || m.content,
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (this.debug) {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.debug("[AgentWidgetClient] client token dispatch", chatRequest);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const response = await fetch(this.getClientApiUrl('chat'), {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify(chatRequest),
|
|
236
|
+
signal: controller.signal,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
const errorData = await response.json().catch(() => ({ error: 'Chat request failed' }));
|
|
241
|
+
|
|
242
|
+
if (response.status === 401) {
|
|
243
|
+
// Session expired
|
|
244
|
+
this.clientSession = null;
|
|
245
|
+
this.config.onSessionExpired?.();
|
|
246
|
+
const error = new Error('Session expired. Please refresh to continue.');
|
|
247
|
+
onEvent({ type: "error", error });
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (response.status === 429) {
|
|
252
|
+
const error = new Error(errorData.hint || 'Message limit reached for this session.');
|
|
253
|
+
onEvent({ type: "error", error });
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const error = new Error(errorData.error || 'Failed to send message');
|
|
258
|
+
onEvent({ type: "error", error });
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!response.body) {
|
|
263
|
+
const error = new Error('No response body received');
|
|
264
|
+
onEvent({ type: "error", error });
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
onEvent({ type: "status", status: "connected" });
|
|
269
|
+
|
|
270
|
+
// Stream the response (same SSE handling as proxy mode)
|
|
271
|
+
try {
|
|
272
|
+
await this.streamResponse(response.body, onEvent);
|
|
273
|
+
} finally {
|
|
274
|
+
onEvent({ type: "status", status: "idle" });
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
278
|
+
// Only emit error if it wasn't already emitted
|
|
279
|
+
if (!err.message.includes('Session expired') && !err.message.includes('Message limit')) {
|
|
280
|
+
onEvent({ type: "error", error: err });
|
|
281
|
+
}
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Proxy mode dispatch (original implementation)
|
|
288
|
+
*/
|
|
289
|
+
private async dispatchProxy(options: DispatchOptions, onEvent: SSEHandler) {
|
|
76
290
|
const controller = new AbortController();
|
|
77
291
|
if (options.signal) {
|
|
78
292
|
options.signal.addEventListener("abort", () => controller.abort());
|
|
@@ -158,10 +158,19 @@ export const createLauncherButton = (
|
|
|
158
158
|
? positionMap[launcher.position]
|
|
159
159
|
: positionMap["bottom-right"];
|
|
160
160
|
|
|
161
|
+
// Removed hardcoded border/shadow classes (tvw-shadow-lg, tvw-border, tvw-border-gray-200)
|
|
162
|
+
// These are now applied via inline styles from config
|
|
161
163
|
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-
|
|
164
|
+
"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-transition hover:tvw-translate-y-[-2px] tvw-cursor-pointer tvw-z-50";
|
|
163
165
|
|
|
164
166
|
button.className = `${base} ${positionClass}`;
|
|
167
|
+
|
|
168
|
+
// Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
|
|
169
|
+
const defaultBorder = "1px solid #e5e7eb";
|
|
170
|
+
const defaultShadow = "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)";
|
|
171
|
+
|
|
172
|
+
button.style.border = launcher.border ?? defaultBorder;
|
|
173
|
+
button.style.boxShadow = launcher.shadow ?? defaultShadow;
|
|
165
174
|
};
|
|
166
175
|
|
|
167
176
|
const destroy = () => {
|
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
AgentWidgetMessage,
|
|
4
4
|
AgentWidgetMessageLayoutConfig,
|
|
5
5
|
AgentWidgetAvatarConfig,
|
|
6
|
-
AgentWidgetTimestampConfig
|
|
6
|
+
AgentWidgetTimestampConfig,
|
|
7
|
+
AgentWidgetMessageActionsConfig,
|
|
8
|
+
AgentWidgetMessageFeedback
|
|
7
9
|
} from "../types";
|
|
10
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
8
11
|
|
|
9
12
|
export type MessageTransform = (context: {
|
|
10
13
|
text: string;
|
|
@@ -13,6 +16,11 @@ export type MessageTransform = (context: {
|
|
|
13
16
|
raw?: string;
|
|
14
17
|
}) => string;
|
|
15
18
|
|
|
19
|
+
export type MessageActionCallbacks = {
|
|
20
|
+
onCopy?: (message: AgentWidgetMessage) => void;
|
|
21
|
+
onFeedback?: (feedback: AgentWidgetMessageFeedback) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
// Create typing indicator element
|
|
17
25
|
export const createTypingIndicator = (): HTMLElement => {
|
|
18
26
|
const container = document.createElement("div");
|
|
@@ -204,6 +212,185 @@ const getBubbleClasses = (
|
|
|
204
212
|
return baseClasses;
|
|
205
213
|
};
|
|
206
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
|
+
|
|
250
|
+
// Track vote state for this message
|
|
251
|
+
let currentVote: "upvote" | "downvote" | null = null;
|
|
252
|
+
|
|
253
|
+
const createActionButton = (
|
|
254
|
+
iconName: string,
|
|
255
|
+
label: string,
|
|
256
|
+
onClick: () => void,
|
|
257
|
+
dataAction?: string
|
|
258
|
+
): HTMLButtonElement => {
|
|
259
|
+
const button = document.createElement("button");
|
|
260
|
+
button.className = "tvw-message-action-btn";
|
|
261
|
+
button.setAttribute("aria-label", label);
|
|
262
|
+
button.setAttribute("title", label);
|
|
263
|
+
if (dataAction) {
|
|
264
|
+
button.setAttribute("data-action", dataAction);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const icon = renderLucideIcon(iconName, 14, "currentColor", 2);
|
|
268
|
+
if (icon) {
|
|
269
|
+
button.appendChild(icon);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
button.addEventListener("click", (e) => {
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
e.stopPropagation();
|
|
275
|
+
onClick();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return button;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Copy button
|
|
282
|
+
if (showCopy) {
|
|
283
|
+
const copyButton = createActionButton("copy", "Copy message", () => {
|
|
284
|
+
// Copy to clipboard
|
|
285
|
+
const textToCopy = message.content || "";
|
|
286
|
+
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
287
|
+
// Show success feedback - swap icon temporarily
|
|
288
|
+
copyButton.classList.add("tvw-message-action-success");
|
|
289
|
+
const checkIcon = renderLucideIcon("check", 14, "currentColor", 2);
|
|
290
|
+
if (checkIcon) {
|
|
291
|
+
copyButton.innerHTML = "";
|
|
292
|
+
copyButton.appendChild(checkIcon);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Restore original icon after 2 seconds
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
copyButton.classList.remove("tvw-message-action-success");
|
|
298
|
+
const originalIcon = renderLucideIcon("copy", 14, "currentColor", 2);
|
|
299
|
+
if (originalIcon) {
|
|
300
|
+
copyButton.innerHTML = "";
|
|
301
|
+
copyButton.appendChild(originalIcon);
|
|
302
|
+
}
|
|
303
|
+
}, 2000);
|
|
304
|
+
}).catch((err) => {
|
|
305
|
+
if (typeof console !== "undefined") {
|
|
306
|
+
console.error("[AgentWidget] Failed to copy message:", err);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Trigger callback
|
|
311
|
+
if (callbacks?.onCopy) {
|
|
312
|
+
callbacks.onCopy(message);
|
|
313
|
+
}
|
|
314
|
+
if (actionsConfig.onCopy) {
|
|
315
|
+
actionsConfig.onCopy(message);
|
|
316
|
+
}
|
|
317
|
+
}, "copy");
|
|
318
|
+
container.appendChild(copyButton);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Upvote button
|
|
322
|
+
if (showUpvote) {
|
|
323
|
+
const upvoteButton = createActionButton("thumbs-up", "Upvote", () => {
|
|
324
|
+
const wasActive = currentVote === "upvote";
|
|
325
|
+
|
|
326
|
+
// Toggle state
|
|
327
|
+
if (wasActive) {
|
|
328
|
+
currentVote = null;
|
|
329
|
+
upvoteButton.classList.remove("tvw-message-action-active");
|
|
330
|
+
} else {
|
|
331
|
+
// Remove downvote if active
|
|
332
|
+
const downvoteBtn = container.querySelector('[data-action="downvote"]');
|
|
333
|
+
if (downvoteBtn) {
|
|
334
|
+
downvoteBtn.classList.remove("tvw-message-action-active");
|
|
335
|
+
}
|
|
336
|
+
currentVote = "upvote";
|
|
337
|
+
upvoteButton.classList.add("tvw-message-action-active");
|
|
338
|
+
|
|
339
|
+
// Trigger feedback
|
|
340
|
+
const feedback: AgentWidgetMessageFeedback = {
|
|
341
|
+
type: "upvote",
|
|
342
|
+
messageId: message.id,
|
|
343
|
+
message
|
|
344
|
+
};
|
|
345
|
+
if (callbacks?.onFeedback) {
|
|
346
|
+
callbacks.onFeedback(feedback);
|
|
347
|
+
}
|
|
348
|
+
if (actionsConfig.onFeedback) {
|
|
349
|
+
actionsConfig.onFeedback(feedback);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}, "upvote");
|
|
353
|
+
container.appendChild(upvoteButton);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Downvote button
|
|
357
|
+
if (showDownvote) {
|
|
358
|
+
const downvoteButton = createActionButton("thumbs-down", "Downvote", () => {
|
|
359
|
+
const wasActive = currentVote === "downvote";
|
|
360
|
+
|
|
361
|
+
// Toggle state
|
|
362
|
+
if (wasActive) {
|
|
363
|
+
currentVote = null;
|
|
364
|
+
downvoteButton.classList.remove("tvw-message-action-active");
|
|
365
|
+
} else {
|
|
366
|
+
// Remove upvote if active
|
|
367
|
+
const upvoteBtn = container.querySelector('[data-action="upvote"]');
|
|
368
|
+
if (upvoteBtn) {
|
|
369
|
+
upvoteBtn.classList.remove("tvw-message-action-active");
|
|
370
|
+
}
|
|
371
|
+
currentVote = "downvote";
|
|
372
|
+
downvoteButton.classList.add("tvw-message-action-active");
|
|
373
|
+
|
|
374
|
+
// Trigger feedback
|
|
375
|
+
const feedback: AgentWidgetMessageFeedback = {
|
|
376
|
+
type: "downvote",
|
|
377
|
+
messageId: message.id,
|
|
378
|
+
message
|
|
379
|
+
};
|
|
380
|
+
if (callbacks?.onFeedback) {
|
|
381
|
+
callbacks.onFeedback(feedback);
|
|
382
|
+
}
|
|
383
|
+
if (actionsConfig.onFeedback) {
|
|
384
|
+
actionsConfig.onFeedback(feedback);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}, "downvote");
|
|
388
|
+
container.appendChild(downvoteButton);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return container;
|
|
392
|
+
};
|
|
393
|
+
|
|
207
394
|
/**
|
|
208
395
|
* Create standard message bubble
|
|
209
396
|
* Supports layout configuration for avatars, timestamps, and visual presets
|
|
@@ -211,7 +398,9 @@ const getBubbleClasses = (
|
|
|
211
398
|
export const createStandardBubble = (
|
|
212
399
|
message: AgentWidgetMessage,
|
|
213
400
|
transform: MessageTransform,
|
|
214
|
-
layoutConfig?: AgentWidgetMessageLayoutConfig
|
|
401
|
+
layoutConfig?: AgentWidgetMessageLayoutConfig,
|
|
402
|
+
actionsConfig?: AgentWidgetMessageActionsConfig,
|
|
403
|
+
actionCallbacks?: MessageActionCallbacks
|
|
215
404
|
): HTMLElement => {
|
|
216
405
|
const config = layoutConfig ?? {};
|
|
217
406
|
const layout = config.layout ?? "bubble";
|
|
@@ -259,6 +448,19 @@ export const createStandardBubble = (
|
|
|
259
448
|
}
|
|
260
449
|
}
|
|
261
450
|
|
|
451
|
+
// Add message actions for assistant messages (only when not streaming and has content)
|
|
452
|
+
const shouldShowActions =
|
|
453
|
+
message.role === "assistant" &&
|
|
454
|
+
!message.streaming &&
|
|
455
|
+
message.content &&
|
|
456
|
+
message.content.trim() &&
|
|
457
|
+
actionsConfig?.enabled !== false;
|
|
458
|
+
|
|
459
|
+
if (shouldShowActions && actionsConfig) {
|
|
460
|
+
const actions = createMessageActions(message, actionsConfig, actionCallbacks);
|
|
461
|
+
bubble.appendChild(actions);
|
|
462
|
+
}
|
|
463
|
+
|
|
262
464
|
// If no avatar needed, return bubble directly
|
|
263
465
|
if (!showAvatar || message.role === "system") {
|
|
264
466
|
return bubble;
|
|
@@ -292,7 +494,9 @@ export const createStandardBubble = (
|
|
|
292
494
|
export const createBubbleWithLayout = (
|
|
293
495
|
message: AgentWidgetMessage,
|
|
294
496
|
transform: MessageTransform,
|
|
295
|
-
layoutConfig?: AgentWidgetMessageLayoutConfig
|
|
497
|
+
layoutConfig?: AgentWidgetMessageLayoutConfig,
|
|
498
|
+
actionsConfig?: AgentWidgetMessageActionsConfig,
|
|
499
|
+
actionCallbacks?: MessageActionCallbacks
|
|
296
500
|
): HTMLElement => {
|
|
297
501
|
const config = layoutConfig ?? {};
|
|
298
502
|
|
|
@@ -314,5 +518,5 @@ export const createBubbleWithLayout = (
|
|
|
314
518
|
}
|
|
315
519
|
|
|
316
520
|
// Fall back to standard bubble
|
|
317
|
-
return createStandardBubble(message, transform, layoutConfig);
|
|
521
|
+
return createStandardBubble(message, transform, layoutConfig, actionsConfig, actionCallbacks);
|
|
318
522
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement, createFragment } from "../utils/dom";
|
|
2
2
|
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
|
-
import { MessageTransform } from "./message-bubble";
|
|
3
|
+
import { MessageTransform, MessageActionCallbacks } from "./message-bubble";
|
|
4
4
|
import { createStandardBubble } from "./message-bubble";
|
|
5
5
|
import { createReasoningBubble } from "./reasoning-bubble";
|
|
6
6
|
import { createToolBubble } from "./tool-bubble";
|
|
@@ -11,7 +11,8 @@ export const renderMessages = (
|
|
|
11
11
|
transform: MessageTransform,
|
|
12
12
|
showReasoning: boolean,
|
|
13
13
|
showToolCalls: boolean,
|
|
14
|
-
config?: AgentWidgetConfig
|
|
14
|
+
config?: AgentWidgetConfig,
|
|
15
|
+
actionCallbacks?: MessageActionCallbacks
|
|
15
16
|
) => {
|
|
16
17
|
container.innerHTML = "";
|
|
17
18
|
const fragment = createFragment();
|
|
@@ -25,7 +26,13 @@ export const renderMessages = (
|
|
|
25
26
|
if (!showToolCalls) return;
|
|
26
27
|
bubble = createToolBubble(message, config);
|
|
27
28
|
} else {
|
|
28
|
-
bubble = createStandardBubble(
|
|
29
|
+
bubble = createStandardBubble(
|
|
30
|
+
message,
|
|
31
|
+
transform,
|
|
32
|
+
config?.layout?.messages,
|
|
33
|
+
config?.messageActions,
|
|
34
|
+
actionCallbacks
|
|
35
|
+
);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const wrapper = createElement("div", "tvw-flex");
|
package/src/defaults.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { AgentWidgetConfig } from "./types";
|
|
|
6
6
|
*/
|
|
7
7
|
export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
8
8
|
apiUrl: "http://localhost:43111/api/chat/dispatch",
|
|
9
|
+
// Client token mode defaults (optional, only used when clientToken is set)
|
|
10
|
+
clientToken: undefined,
|
|
9
11
|
theme: {
|
|
10
12
|
primary: "#111827",
|
|
11
13
|
accent: "#1d4ed8",
|
|
@@ -76,6 +78,8 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
76
78
|
paddingY: "0px",
|
|
77
79
|
},
|
|
78
80
|
headerIconHidden: false,
|
|
81
|
+
border: "1px solid #e5e7eb",
|
|
82
|
+
shadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
|
|
79
83
|
},
|
|
80
84
|
copy: {
|
|
81
85
|
welcomeTitle: "Hello 👋",
|
|
@@ -159,6 +163,22 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
159
163
|
},
|
|
160
164
|
slots: {},
|
|
161
165
|
},
|
|
166
|
+
markdown: {
|
|
167
|
+
options: {
|
|
168
|
+
gfm: true,
|
|
169
|
+
breaks: true,
|
|
170
|
+
},
|
|
171
|
+
disableDefaultStyles: false,
|
|
172
|
+
},
|
|
173
|
+
messageActions: {
|
|
174
|
+
enabled: true,
|
|
175
|
+
showCopy: true,
|
|
176
|
+
showUpvote: false, // Requires backend - disabled by default
|
|
177
|
+
showDownvote: false, // Requires backend - disabled by default
|
|
178
|
+
visibility: "hover",
|
|
179
|
+
align: "right",
|
|
180
|
+
layout: "pill-inside",
|
|
181
|
+
},
|
|
162
182
|
debug: false,
|
|
163
183
|
};
|
|
164
184
|
|
|
@@ -235,5 +255,17 @@ export function mergeWithDefaults(
|
|
|
235
255
|
...config.layout?.slots,
|
|
236
256
|
},
|
|
237
257
|
},
|
|
258
|
+
markdown: {
|
|
259
|
+
...DEFAULT_WIDGET_CONFIG.markdown,
|
|
260
|
+
...config.markdown,
|
|
261
|
+
options: {
|
|
262
|
+
...DEFAULT_WIDGET_CONFIG.markdown?.options,
|
|
263
|
+
...config.markdown?.options,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
messageActions: {
|
|
267
|
+
...DEFAULT_WIDGET_CONFIG.messageActions,
|
|
268
|
+
...config.messageActions,
|
|
269
|
+
},
|
|
238
270
|
};
|
|
239
271
|
}
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,18 @@ export type {
|
|
|
28
28
|
SlotRenderer,
|
|
29
29
|
SlotRenderContext,
|
|
30
30
|
HeaderRenderContext,
|
|
31
|
-
MessageRenderContext
|
|
31
|
+
MessageRenderContext,
|
|
32
|
+
// Markdown types
|
|
33
|
+
AgentWidgetMarkdownConfig,
|
|
34
|
+
AgentWidgetMarkdownOptions,
|
|
35
|
+
AgentWidgetMarkdownRendererOverrides,
|
|
36
|
+
// Message actions types
|
|
37
|
+
AgentWidgetMessageActionsConfig,
|
|
38
|
+
AgentWidgetMessageFeedback,
|
|
39
|
+
// Client token types
|
|
40
|
+
ClientSession,
|
|
41
|
+
ClientInitResponse,
|
|
42
|
+
ClientChatRequest
|
|
32
43
|
} from "./types";
|
|
33
44
|
|
|
34
45
|
export { initAgentWidgetFn as initAgentWidget };
|
|
@@ -50,8 +61,12 @@ export {
|
|
|
50
61
|
export {
|
|
51
62
|
markdownPostprocessor,
|
|
52
63
|
escapeHtml,
|
|
53
|
-
directivePostprocessor
|
|
64
|
+
directivePostprocessor,
|
|
65
|
+
createMarkdownProcessor,
|
|
66
|
+
createMarkdownProcessorFromConfig,
|
|
67
|
+
createDirectivePostprocessor
|
|
54
68
|
} from "./postprocessors";
|
|
69
|
+
export type { MarkdownProcessorOptions } from "./postprocessors";
|
|
55
70
|
export {
|
|
56
71
|
createPlainTextParser,
|
|
57
72
|
createJsonStreamParser,
|
|
@@ -112,8 +127,9 @@ export type {
|
|
|
112
127
|
export {
|
|
113
128
|
createStandardBubble,
|
|
114
129
|
createBubbleWithLayout,
|
|
115
|
-
createTypingIndicator
|
|
130
|
+
createTypingIndicator,
|
|
131
|
+
createMessageActions
|
|
116
132
|
} from "./components/message-bubble";
|
|
117
|
-
export type { MessageTransform } from "./components/message-bubble";
|
|
133
|
+
export type { MessageTransform, MessageActionCallbacks } from "./components/message-bubble";
|
|
118
134
|
|
|
119
135
|
export default initAgentWidgetFn;
|