vanilla-agent 1.21.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.
@@ -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(message, transform);
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",
@@ -168,6 +170,15 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
168
170
  },
169
171
  disableDefaultStyles: false,
170
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
+ },
171
182
  debug: false,
172
183
  };
173
184
 
@@ -252,5 +263,9 @@ export function mergeWithDefaults(
252
263
  ...config.markdown?.options,
253
264
  },
254
265
  },
266
+ messageActions: {
267
+ ...DEFAULT_WIDGET_CONFIG.messageActions,
268
+ ...config.messageActions,
269
+ },
255
270
  };
256
271
  }
package/src/index.ts CHANGED
@@ -32,7 +32,14 @@ export type {
32
32
  // Markdown types
33
33
  AgentWidgetMarkdownConfig,
34
34
  AgentWidgetMarkdownOptions,
35
- AgentWidgetMarkdownRendererOverrides
35
+ AgentWidgetMarkdownRendererOverrides,
36
+ // Message actions types
37
+ AgentWidgetMessageActionsConfig,
38
+ AgentWidgetMessageFeedback,
39
+ // Client token types
40
+ ClientSession,
41
+ ClientInitResponse,
42
+ ClientChatRequest
36
43
  } from "./types";
37
44
 
38
45
  export { initAgentWidgetFn as initAgentWidget };
@@ -120,8 +127,9 @@ export type {
120
127
  export {
121
128
  createStandardBubble,
122
129
  createBubbleWithLayout,
123
- createTypingIndicator
130
+ createTypingIndicator,
131
+ createMessageActions
124
132
  } from "./components/message-bubble";
125
- export type { MessageTransform } from "./components/message-bubble";
133
+ export type { MessageTransform, MessageActionCallbacks } from "./components/message-bubble";
126
134
 
127
135
  export default initAgentWidgetFn;
package/src/install.ts CHANGED
@@ -12,6 +12,10 @@ interface SiteAgentInstallConfig {
12
12
  target?: string | HTMLElement;
13
13
  config?: any;
14
14
  autoInit?: boolean;
15
+ // Client token mode options (can also be set via data attributes)
16
+ clientToken?: string;
17
+ flowId?: string;
18
+ apiUrl?: string;
15
19
  }
16
20
 
17
21
  declare global {
@@ -30,7 +34,45 @@ declare global {
30
34
  }
31
35
  (window as any).__siteAgentInstallerLoaded = true;
32
36
 
33
- const config: SiteAgentInstallConfig = window.siteAgentConfig || {};
37
+ /**
38
+ * Read configuration from data attributes on the current script tag.
39
+ * Supports: data-travrse-token, data-flow-id, data-api-url
40
+ */
41
+ const getConfigFromScript = (): Partial<SiteAgentInstallConfig> => {
42
+ // Try to get the current script element
43
+ const script = document.currentScript as HTMLScriptElement | null;
44
+ if (!script) return {};
45
+
46
+ const scriptConfig: Partial<SiteAgentInstallConfig> = {};
47
+
48
+ // Client token from data attribute (primary method for client token mode)
49
+ const token = script.getAttribute('data-travrse-token');
50
+ if (token) {
51
+ scriptConfig.clientToken = token;
52
+ }
53
+
54
+ // Optional flow ID
55
+ const flowId = script.getAttribute('data-flow-id');
56
+ if (flowId) {
57
+ scriptConfig.flowId = flowId;
58
+ }
59
+
60
+ // Optional API URL override
61
+ const apiUrl = script.getAttribute('data-api-url');
62
+ if (apiUrl) {
63
+ scriptConfig.apiUrl = apiUrl;
64
+ }
65
+
66
+ return scriptConfig;
67
+ };
68
+
69
+ // Get config from script attributes (must be called synchronously during script execution)
70
+ const scriptConfig = getConfigFromScript();
71
+
72
+ // Merge script attributes with window config (script attributes take precedence)
73
+ const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};
74
+ const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };
75
+
34
76
  const version = config.version || "latest";
35
77
  const cdn = config.cdn || "jsdelivr";
36
78
  const autoInit = config.autoInit !== false; // Default to true
@@ -113,14 +155,27 @@ declare global {
113
155
  }
114
156
 
115
157
  const target = config.target || "body";
116
- // Merge apiUrl from top-level config into widget config if present
158
+ // Merge top-level config options into widget config
117
159
  const widgetConfig = { ...config.config };
118
- if ((config as any).apiUrl && !widgetConfig.apiUrl) {
119
- widgetConfig.apiUrl = (config as any).apiUrl;
160
+
161
+ // Merge apiUrl from top-level config into widget config if present
162
+ if (config.apiUrl && !widgetConfig.apiUrl) {
163
+ widgetConfig.apiUrl = config.apiUrl;
164
+ }
165
+
166
+ // Merge clientToken from top-level config into widget config if present
167
+ if (config.clientToken && !widgetConfig.clientToken) {
168
+ widgetConfig.clientToken = config.clientToken;
169
+ }
170
+
171
+ // Merge flowId from top-level config into widget config if present
172
+ if (config.flowId && !widgetConfig.flowId) {
173
+ widgetConfig.flowId = config.flowId;
120
174
  }
121
175
 
122
- // Only initialize if config is provided
123
- if (!widgetConfig.apiUrl && Object.keys(widgetConfig).length === 0) {
176
+ // Only initialize if we have either apiUrl OR clientToken (or other config)
177
+ const hasApiConfig = widgetConfig.apiUrl || widgetConfig.clientToken;
178
+ if (!hasApiConfig && Object.keys(widgetConfig).length === 0) {
124
179
  return;
125
180
  }
126
181
 
@@ -140,7 +195,14 @@ declare global {
140
195
  await loadCSS();
141
196
  await loadJS();
142
197
 
143
- if (autoInit && (config.config || (config as any).apiUrl)) {
198
+ // Auto-init if we have config OR apiUrl OR clientToken
199
+ const shouldAutoInit = autoInit && (
200
+ config.config ||
201
+ config.apiUrl ||
202
+ config.clientToken
203
+ );
204
+
205
+ if (shouldAutoInit) {
144
206
  // Wait a tick to ensure AgentWidget is fully initialized
145
207
  setTimeout(initWidget, 0);
146
208
  }
package/src/session.ts CHANGED
@@ -2,7 +2,8 @@ import { AgentWidgetClient } from "./client";
2
2
  import {
3
3
  AgentWidgetConfig,
4
4
  AgentWidgetEvent,
5
- AgentWidgetMessage
5
+ AgentWidgetMessage,
6
+ ClientSession
6
7
  } from "./types";
7
8
 
8
9
  export type AgentWidgetSessionStatus =
@@ -25,6 +26,9 @@ export class AgentWidgetSession {
25
26
  private streaming = false;
26
27
  private abortController: AbortController | null = null;
27
28
  private sequenceCounter = Date.now();
29
+
30
+ // Client token session management
31
+ private clientSession: ClientSession | null = null;
28
32
 
29
33
  constructor(
30
34
  private config: AgentWidgetConfig = {},
@@ -43,6 +47,78 @@ export class AgentWidgetSession {
43
47
  this.callbacks.onStatusChanged(this.status);
44
48
  }
45
49
 
50
+ /**
51
+ * Check if running in client token mode
52
+ */
53
+ public isClientTokenMode(): boolean {
54
+ return this.client.isClientTokenMode();
55
+ }
56
+
57
+ /**
58
+ * Initialize the client session (for client token mode).
59
+ * This is called automatically on first message, but can be called
60
+ * explicitly to pre-initialize the session and get config from server.
61
+ */
62
+ public async initClientSession(): Promise<ClientSession | null> {
63
+ if (!this.isClientTokenMode()) {
64
+ return null;
65
+ }
66
+
67
+ try {
68
+ const session = await this.client.initSession();
69
+ this.setClientSession(session);
70
+ return session;
71
+ } catch (error) {
72
+ this.callbacks.onError?.(
73
+ error instanceof Error ? error : new Error(String(error))
74
+ );
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Set the client session after initialization
81
+ */
82
+ public setClientSession(session: ClientSession): void {
83
+ this.clientSession = session;
84
+
85
+ // Optionally add welcome message from session config
86
+ if (session.config.welcomeMessage && this.messages.length === 0) {
87
+ const welcomeMessage: AgentWidgetMessage = {
88
+ id: `welcome-${Date.now()}`,
89
+ role: "assistant",
90
+ content: session.config.welcomeMessage,
91
+ createdAt: new Date().toISOString(),
92
+ sequence: this.nextSequence()
93
+ };
94
+ this.appendMessage(welcomeMessage);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get current client session
100
+ */
101
+ public getClientSession(): ClientSession | null {
102
+ return this.clientSession ?? this.client.getClientSession();
103
+ }
104
+
105
+ /**
106
+ * Check if session is valid and not expired
107
+ */
108
+ public isSessionValid(): boolean {
109
+ const session = this.getClientSession();
110
+ if (!session) return false;
111
+ return new Date() < session.expiresAt;
112
+ }
113
+
114
+ /**
115
+ * Clear session (on expiry or error)
116
+ */
117
+ public clearClientSession(): void {
118
+ this.clientSession = null;
119
+ this.client.clearClientSession();
120
+ }
121
+
46
122
  public updateConfig(next: AgentWidgetConfig) {
47
123
  this.config = { ...this.config, ...next };
48
124
  this.client = new AgentWidgetClient(this.config);