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.
@@ -1215,3 +1215,188 @@ form:focus-within textarea {
1215
1215
  .vanilla-message-user-bubble code:not(pre code) {
1216
1216
  background-color: rgba(255, 255, 255, 0.2);
1217
1217
  }
1218
+
1219
+ /* ============================================
1220
+ Message Action Buttons (Copy, Upvote, Downvote)
1221
+ ============================================ */
1222
+
1223
+ /* Make message bubble position relative for overlay positioning */
1224
+ .vanilla-message-bubble {
1225
+ position: relative;
1226
+ }
1227
+
1228
+ /* Fade-in animation for action buttons */
1229
+ @keyframes tvw-message-actions-fade-in {
1230
+ from {
1231
+ opacity: 0;
1232
+ }
1233
+ to {
1234
+ opacity: 1;
1235
+ }
1236
+ }
1237
+
1238
+ /* Base action bar styles */
1239
+ .tvw-message-actions {
1240
+ display: flex;
1241
+ align-items: center;
1242
+ gap: 0.25rem;
1243
+ margin-top: 0.5rem;
1244
+ padding-top: 0.5rem;
1245
+ border-top: 1px solid var(--cw-divider, #f1f5f9);
1246
+ /* Fade in when first shown (for "always" visibility) */
1247
+ animation: tvw-message-actions-fade-in 0.3s ease-out;
1248
+ }
1249
+
1250
+ /* Action bar alignment */
1251
+ .tvw-message-actions-left {
1252
+ justify-content: flex-start;
1253
+ }
1254
+
1255
+ .tvw-message-actions-center {
1256
+ justify-content: center;
1257
+ }
1258
+
1259
+ .tvw-message-actions-right {
1260
+ justify-content: flex-end;
1261
+ }
1262
+
1263
+ /* Hover visibility mode - overlay on desktop */
1264
+ @media (hover: hover) {
1265
+ .tvw-message-actions-hover {
1266
+ /* Hidden by default */
1267
+ opacity: 0;
1268
+ pointer-events: none;
1269
+ transition: opacity 0.15s ease-in-out;
1270
+ }
1271
+
1272
+ /* Pill layout - compact floating pill */
1273
+ .tvw-message-actions-hover.tvw-message-actions-pill {
1274
+ position: absolute;
1275
+ bottom: 0.5rem;
1276
+ margin-top: 0;
1277
+ padding: 0.25rem;
1278
+ border-top: none;
1279
+ width: fit-content;
1280
+ background-color: var(--cw-surface, #ffffff);
1281
+ border: 1px solid var(--cw-divider, #f1f5f9);
1282
+ border-radius: var(--cw-radius-md, 0.75rem);
1283
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
1284
+ }
1285
+
1286
+ /* Pill layout - position based on alignment */
1287
+ .tvw-message-actions-hover.tvw-message-actions-pill.tvw-message-actions-left {
1288
+ left: 0.75rem;
1289
+ right: auto;
1290
+ }
1291
+
1292
+ .tvw-message-actions-hover.tvw-message-actions-pill.tvw-message-actions-center {
1293
+ left: 50%;
1294
+ right: auto;
1295
+ transform: translateX(-50%);
1296
+ }
1297
+
1298
+ .tvw-message-actions-hover.tvw-message-actions-pill.tvw-message-actions-right {
1299
+ right: 0.75rem;
1300
+ left: auto;
1301
+ }
1302
+
1303
+ /* Row layout - full-width bar at bottom */
1304
+ .tvw-message-actions-hover.tvw-message-actions-row {
1305
+ position: absolute;
1306
+ bottom: 0;
1307
+ left: 0;
1308
+ right: 0;
1309
+ margin-top: 0;
1310
+ padding: 0.5rem 0.75rem;
1311
+ border-top: none;
1312
+ background: linear-gradient(
1313
+ to top,
1314
+ var(--cw-surface, #ffffff) 70%,
1315
+ transparent
1316
+ );
1317
+ border-radius: 0 0 var(--cw-radius-lg, 1.5rem) var(--cw-radius-lg, 1.5rem);
1318
+ }
1319
+
1320
+ .vanilla-message-bubble:hover .tvw-message-actions-hover,
1321
+ .vanilla-message-bubble:focus-within .tvw-message-actions-hover {
1322
+ opacity: 1;
1323
+ pointer-events: auto;
1324
+ }
1325
+ }
1326
+
1327
+ /* On touch devices (no hover support), show inline and always visible */
1328
+ @media (hover: none) {
1329
+ .tvw-message-actions-hover {
1330
+ /* Keep normal flow positioning on mobile */
1331
+ position: static;
1332
+ opacity: 1;
1333
+ }
1334
+ }
1335
+
1336
+ /* Action button base styles */
1337
+ .tvw-message-action-btn {
1338
+ display: inline-flex;
1339
+ align-items: center;
1340
+ justify-content: center;
1341
+ width: 28px;
1342
+ height: 28px;
1343
+ padding: 0;
1344
+ border: none;
1345
+ border-radius: 6px;
1346
+ background-color: transparent;
1347
+ color: var(--cw-muted, #6b7280);
1348
+ cursor: pointer;
1349
+ transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
1350
+ }
1351
+
1352
+ .tvw-message-action-btn:hover {
1353
+ background-color: var(--cw-container, #f8fafc);
1354
+ color: var(--cw-primary, #111827);
1355
+ }
1356
+
1357
+ .tvw-message-action-btn:active {
1358
+ transform: scale(0.95);
1359
+ }
1360
+
1361
+ .tvw-message-action-btn:focus {
1362
+ outline: none;
1363
+ box-shadow: 0 0 0 2px var(--cw-accent, #1d4ed8);
1364
+ }
1365
+
1366
+ .tvw-message-action-btn:focus:not(:focus-visible) {
1367
+ box-shadow: none;
1368
+ }
1369
+
1370
+ .tvw-message-action-btn:focus-visible {
1371
+ box-shadow: 0 0 0 2px var(--cw-accent, #1d4ed8);
1372
+ }
1373
+
1374
+ /* Active state (voted) */
1375
+ .tvw-message-action-btn.tvw-message-action-active {
1376
+ background-color: var(--cw-accent, #1d4ed8);
1377
+ color: #ffffff;
1378
+ }
1379
+
1380
+ .tvw-message-action-btn.tvw-message-action-active:hover {
1381
+ background-color: var(--cw-accent, #1d4ed8);
1382
+ color: #ffffff;
1383
+ opacity: 0.9;
1384
+ }
1385
+
1386
+ /* Success state (after copy) */
1387
+ .tvw-message-action-btn.tvw-message-action-success {
1388
+ background-color: #10b981;
1389
+ color: #ffffff;
1390
+ }
1391
+
1392
+ .tvw-message-action-btn.tvw-message-action-success:hover {
1393
+ background-color: #10b981;
1394
+ color: #ffffff;
1395
+ }
1396
+
1397
+ /* Icon styling within buttons */
1398
+ .tvw-message-action-btn svg {
1399
+ width: 14px;
1400
+ height: 14px;
1401
+ flex-shrink: 0;
1402
+ }
package/src/types.ts CHANGED
@@ -93,6 +93,66 @@ export type AgentWidgetActionEventPayload = {
93
93
  message: AgentWidgetMessage;
94
94
  };
95
95
 
96
+ /**
97
+ * Feedback event payload for upvote/downvote actions on messages
98
+ */
99
+ export type AgentWidgetMessageFeedback = {
100
+ type: "upvote" | "downvote";
101
+ messageId: string;
102
+ message: AgentWidgetMessage;
103
+ };
104
+
105
+ /**
106
+ * Configuration for message action buttons (copy, upvote, downvote)
107
+ */
108
+ export type AgentWidgetMessageActionsConfig = {
109
+ /**
110
+ * Enable/disable message actions entirely
111
+ * @default true
112
+ */
113
+ enabled?: boolean;
114
+ /**
115
+ * Show copy button
116
+ * @default true
117
+ */
118
+ showCopy?: boolean;
119
+ /**
120
+ * Show upvote button
121
+ * @default false (requires backend)
122
+ */
123
+ showUpvote?: boolean;
124
+ /**
125
+ * Show downvote button
126
+ * @default false (requires backend)
127
+ */
128
+ showDownvote?: boolean;
129
+ /**
130
+ * Visibility mode: 'always' shows buttons always, 'hover' shows on hover only
131
+ * @default 'hover'
132
+ */
133
+ visibility?: "always" | "hover";
134
+ /**
135
+ * Horizontal alignment of action buttons
136
+ * @default 'right'
137
+ */
138
+ align?: "left" | "center" | "right";
139
+ /**
140
+ * Layout style for action buttons
141
+ * - 'pill-inside': Compact floating pill around just the buttons (default for hover)
142
+ * - 'row-inside': Full-width row at the bottom of the message
143
+ * @default 'pill-inside'
144
+ */
145
+ layout?: "pill-inside" | "row-inside";
146
+ /**
147
+ * Callback when user submits feedback (upvote/downvote)
148
+ */
149
+ onFeedback?: (feedback: AgentWidgetMessageFeedback) => void;
150
+ /**
151
+ * Callback when user copies a message
152
+ */
153
+ onCopy?: (message: AgentWidgetMessage) => void;
154
+ };
155
+
96
156
  export type AgentWidgetStateEvent = {
97
157
  open: boolean;
98
158
  source: "user" | "auto" | "api" | "system";
@@ -114,6 +174,8 @@ export type AgentWidgetControllerEventMap = {
114
174
  "widget:opened": AgentWidgetStateEvent;
115
175
  "widget:closed": AgentWidgetStateEvent;
116
176
  "widget:state": AgentWidgetStateSnapshot;
177
+ "message:feedback": AgentWidgetMessageFeedback;
178
+ "message:copy": AgentWidgetMessage;
117
179
  };
118
180
 
119
181
  export type AgentWidgetFeatureFlags = {
@@ -454,6 +516,62 @@ export type AgentWidgetCustomFetch = (
454
516
  */
455
517
  export type AgentWidgetHeadersFunction = () => Record<string, string> | Promise<Record<string, string>>;
456
518
 
519
+ // ============================================================================
520
+ // Client Token Types
521
+ // ============================================================================
522
+
523
+ /**
524
+ * Session information returned after client token initialization.
525
+ * Contains session ID, expiry time, flow info, and config from the server.
526
+ */
527
+ export type ClientSession = {
528
+ /** Unique session identifier */
529
+ sessionId: string;
530
+ /** When the session expires */
531
+ expiresAt: Date;
532
+ /** Flow information */
533
+ flow: {
534
+ id: string;
535
+ name: string;
536
+ description: string | null;
537
+ };
538
+ /** Configuration from the server */
539
+ config: {
540
+ welcomeMessage: string | null;
541
+ placeholder: string;
542
+ theme: Record<string, unknown> | null;
543
+ };
544
+ };
545
+
546
+ /**
547
+ * Raw API response from /v1/client/init endpoint
548
+ */
549
+ export type ClientInitResponse = {
550
+ session_id: string;
551
+ expires_at: string;
552
+ flow: {
553
+ id: string;
554
+ name: string;
555
+ description: string | null;
556
+ };
557
+ config: {
558
+ welcome_message: string | null;
559
+ placeholder: string;
560
+ theme: Record<string, unknown> | null;
561
+ };
562
+ };
563
+
564
+ /**
565
+ * Request payload for /v1/client/chat endpoint
566
+ */
567
+ export type ClientChatRequest = {
568
+ session_id: string;
569
+ messages: Array<{
570
+ role: 'user' | 'assistant' | 'system';
571
+ content: string;
572
+ }>;
573
+ };
574
+
457
575
  // ============================================================================
458
576
  // Layout Configuration Types
459
577
  // ============================================================================
@@ -890,6 +1008,47 @@ export type AgentWidgetMarkdownConfig = {
890
1008
  export type AgentWidgetConfig = {
891
1009
  apiUrl?: string;
892
1010
  flowId?: string;
1011
+ /**
1012
+ * Client token for direct browser-to-API communication.
1013
+ * When set, the widget uses /v1/client/* endpoints instead of /v1/dispatch.
1014
+ * Mutually exclusive with apiKey/headers authentication.
1015
+ *
1016
+ * @example
1017
+ * ```typescript
1018
+ * config: {
1019
+ * clientToken: 'ct_live_flow01k7_a8b9c0d1e2f3g4h5i6j7k8l9'
1020
+ * }
1021
+ * ```
1022
+ */
1023
+ clientToken?: string;
1024
+ /**
1025
+ * Callback when session is initialized (client token mode only).
1026
+ * Receives session info including expiry time.
1027
+ *
1028
+ * @example
1029
+ * ```typescript
1030
+ * config: {
1031
+ * onSessionInit: (session) => {
1032
+ * console.log('Session started:', session.sessionId);
1033
+ * }
1034
+ * }
1035
+ * ```
1036
+ */
1037
+ onSessionInit?: (session: ClientSession) => void;
1038
+ /**
1039
+ * Callback when session expires or errors (client token mode only).
1040
+ * Widget should prompt user to refresh.
1041
+ *
1042
+ * @example
1043
+ * ```typescript
1044
+ * config: {
1045
+ * onSessionExpired: () => {
1046
+ * alert('Your session has expired. Please refresh the page.');
1047
+ * }
1048
+ * }
1049
+ * ```
1050
+ */
1051
+ onSessionExpired?: () => void;
893
1052
  /**
894
1053
  * Static headers to include with each request.
895
1054
  * For dynamic headers (e.g., auth tokens), use `getHeaders` instead.
@@ -1122,6 +1281,31 @@ export type AgentWidgetConfig = {
1122
1281
  * ```
1123
1282
  */
1124
1283
  markdown?: AgentWidgetMarkdownConfig;
1284
+
1285
+ /**
1286
+ * Configuration for message action buttons (copy, upvote, downvote).
1287
+ * Shows action buttons on assistant messages for user feedback.
1288
+ *
1289
+ * @example
1290
+ * ```typescript
1291
+ * config: {
1292
+ * messageActions: {
1293
+ * enabled: true,
1294
+ * showCopy: true,
1295
+ * showUpvote: true,
1296
+ * showDownvote: true,
1297
+ * visibility: 'hover',
1298
+ * onFeedback: (feedback) => {
1299
+ * console.log('Feedback:', feedback.type, feedback.messageId);
1300
+ * },
1301
+ * onCopy: (message) => {
1302
+ * console.log('Copied message:', message.id);
1303
+ * }
1304
+ * }
1305
+ * }
1306
+ * ```
1307
+ */
1308
+ messageActions?: AgentWidgetMessageActionsConfig;
1125
1309
  };
1126
1310
 
1127
1311
  export type AgentWidgetMessageRole = "user" | "assistant" | "system";
package/src/ui.ts CHANGED
@@ -11,7 +11,8 @@ import {
11
11
  AgentWidgetStateEvent,
12
12
  AgentWidgetStateSnapshot,
13
13
  WidgetLayoutSlot,
14
- SlotRenderer
14
+ SlotRenderer,
15
+ AgentWidgetMessageFeedback
15
16
  } from "./types";
16
17
  import { applyThemeVariables } from "./utils/theme";
17
18
  import { renderLucideIcon } from "./utils/icons";
@@ -21,7 +22,7 @@ import { createLauncherButton } from "./components/launcher";
21
22
  import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
22
23
  import { positionMap } from "./utils/positioning";
23
24
  import type { HeaderElements, ComposerElements } from "./components/panel";
24
- import { MessageTransform } from "./components/message-bubble";
25
+ import { MessageTransform, MessageActionCallbacks } from "./components/message-bubble";
25
26
  import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
26
27
  import { createReasoningBubble } from "./components/reasoning-bubble";
27
28
  import { createToolBubble } from "./components/tool-bubble";
@@ -228,6 +229,16 @@ export const createAgentExperience = (
228
229
  let showReasoning = config.features?.showReasoning ?? true;
229
230
  let showToolCalls = config.features?.showToolCalls ?? true;
230
231
 
232
+ // Create message action callbacks that emit events
233
+ const messageActionCallbacks: MessageActionCallbacks = {
234
+ onCopy: (message: AgentWidgetMessage) => {
235
+ eventBus.emit("message:copy", message);
236
+ },
237
+ onFeedback: (feedback: AgentWidgetMessageFeedback) => {
238
+ eventBus.emit("message:feedback", feedback);
239
+ }
240
+ };
241
+
231
242
  // Get status indicator config
232
243
  const statusConfig = config.statusIndicator ?? {};
233
244
  const getStatusText = (status: AgentWidgetSessionStatus): string => {
@@ -877,7 +888,13 @@ export const createAgentExperience = (
877
888
  bubble = matchingPlugin.renderMessage({
878
889
  message,
879
890
  defaultRenderer: () => {
880
- const b = createStandardBubble(message, transform, messageLayoutConfig);
891
+ const b = createStandardBubble(
892
+ message,
893
+ transform,
894
+ messageLayoutConfig,
895
+ config.messageActions,
896
+ messageActionCallbacks
897
+ );
881
898
  if (message.role !== "user") {
882
899
  enhanceWithForms(b, message, config, session);
883
900
  }
@@ -957,7 +974,13 @@ export const createAgentExperience = (
957
974
  streaming: Boolean(message.streaming)
958
975
  });
959
976
  } else {
960
- bubble = createStandardBubble(message, transform, messageLayoutConfig);
977
+ bubble = createStandardBubble(
978
+ message,
979
+ transform,
980
+ messageLayoutConfig,
981
+ config.messageActions,
982
+ messageActionCallbacks
983
+ );
961
984
  }
962
985
  if (message.role !== "user" && bubble) {
963
986
  enhanceWithForms(bubble, message, config, session);