vanilla-agent 1.22.0 → 1.24.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.
@@ -1400,3 +1400,227 @@ form:focus-within textarea {
1400
1400
  height: 14px;
1401
1401
  flex-shrink: 0;
1402
1402
  }
1403
+
1404
+ /* ============================================================================
1405
+ * Feedback UI Components (CSAT/NPS)
1406
+ * ============================================================================ */
1407
+
1408
+ .tvw-feedback-container {
1409
+ background: var(--cw-surface, #ffffff);
1410
+ border: 1px solid var(--cw-border, #e5e7eb);
1411
+ border-radius: var(--tvw-cw-radius-lg, 12px);
1412
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
1413
+ padding: 1.25rem;
1414
+ max-width: 100%;
1415
+ margin: 0.75rem;
1416
+ animation: tvw-feedback-fade-in 0.3s ease-out;
1417
+ }
1418
+
1419
+ @keyframes tvw-feedback-fade-in {
1420
+ from {
1421
+ opacity: 0;
1422
+ transform: translateY(10px);
1423
+ }
1424
+ to {
1425
+ opacity: 1;
1426
+ transform: translateY(0);
1427
+ }
1428
+ }
1429
+
1430
+ .tvw-feedback-content {
1431
+ display: flex;
1432
+ flex-direction: column;
1433
+ gap: 1rem;
1434
+ }
1435
+
1436
+ .tvw-feedback-header {
1437
+ text-align: center;
1438
+ }
1439
+
1440
+ .tvw-feedback-title {
1441
+ margin: 0 0 0.25rem 0;
1442
+ font-size: 1rem;
1443
+ font-weight: 600;
1444
+ color: var(--cw-primary, #111827);
1445
+ }
1446
+
1447
+ .tvw-feedback-subtitle {
1448
+ margin: 0;
1449
+ font-size: 0.875rem;
1450
+ color: var(--cw-muted, #6b7280);
1451
+ }
1452
+
1453
+ /* CSAT Star Rating */
1454
+ .tvw-feedback-rating-csat {
1455
+ display: flex;
1456
+ justify-content: center;
1457
+ gap: 0.5rem;
1458
+ }
1459
+
1460
+ .tvw-feedback-star-btn {
1461
+ background: transparent;
1462
+ border: none;
1463
+ cursor: pointer;
1464
+ padding: 0.25rem;
1465
+ color: var(--cw-border, #d1d5db);
1466
+ transition: color 0.2s ease, transform 0.15s ease;
1467
+ }
1468
+
1469
+ .tvw-feedback-star-btn:hover {
1470
+ transform: scale(1.15);
1471
+ }
1472
+
1473
+ .tvw-feedback-star-btn.selected {
1474
+ color: #fbbf24;
1475
+ }
1476
+
1477
+ .tvw-feedback-star-btn .tvw-feedback-star {
1478
+ width: 32px;
1479
+ height: 32px;
1480
+ }
1481
+
1482
+ .tvw-feedback-star-btn.selected .tvw-feedback-star {
1483
+ fill: #fbbf24;
1484
+ }
1485
+
1486
+ /* NPS Number Rating */
1487
+ .tvw-feedback-rating-nps {
1488
+ display: flex;
1489
+ flex-direction: column;
1490
+ gap: 0.5rem;
1491
+ }
1492
+
1493
+ .tvw-feedback-labels {
1494
+ display: flex;
1495
+ justify-content: space-between;
1496
+ font-size: 0.75rem;
1497
+ color: var(--cw-muted, #6b7280);
1498
+ }
1499
+
1500
+ .tvw-feedback-numbers {
1501
+ display: flex;
1502
+ gap: 0.25rem;
1503
+ justify-content: center;
1504
+ }
1505
+
1506
+ .tvw-feedback-number-btn {
1507
+ width: 28px;
1508
+ height: 28px;
1509
+ display: flex;
1510
+ align-items: center;
1511
+ justify-content: center;
1512
+ font-size: 0.75rem;
1513
+ font-weight: 500;
1514
+ border-radius: var(--tvw-cw-radius-sm, 6px);
1515
+ border: 1px solid var(--cw-border, #e5e7eb);
1516
+ background: var(--cw-surface, #ffffff);
1517
+ color: var(--cw-primary, #111827);
1518
+ cursor: pointer;
1519
+ transition: all 0.2s ease;
1520
+ }
1521
+
1522
+ .tvw-feedback-number-btn:hover {
1523
+ border-color: var(--cw-accent, #1d4ed8);
1524
+ background: var(--cw-container, #f3f4f6);
1525
+ }
1526
+
1527
+ .tvw-feedback-number-btn.selected {
1528
+ color: #ffffff;
1529
+ }
1530
+
1531
+ /* NPS Color coding */
1532
+ .tvw-feedback-number-btn.tvw-feedback-detractor.selected {
1533
+ background: #ef4444;
1534
+ border-color: #ef4444;
1535
+ }
1536
+
1537
+ .tvw-feedback-number-btn.tvw-feedback-passive.selected {
1538
+ background: #f59e0b;
1539
+ border-color: #f59e0b;
1540
+ }
1541
+
1542
+ .tvw-feedback-number-btn.tvw-feedback-promoter.selected {
1543
+ background: #22c55e;
1544
+ border-color: #22c55e;
1545
+ }
1546
+
1547
+ /* Comment textarea */
1548
+ .tvw-feedback-comment-container {
1549
+ width: 100%;
1550
+ }
1551
+
1552
+ .tvw-feedback-comment {
1553
+ width: 100%;
1554
+ padding: 0.625rem;
1555
+ font-size: 0.875rem;
1556
+ font-family: inherit;
1557
+ border: 1px solid var(--cw-border, #e5e7eb);
1558
+ border-radius: var(--tvw-cw-radius-sm, 6px);
1559
+ background: var(--cw-surface, #ffffff);
1560
+ color: var(--cw-primary, #111827);
1561
+ resize: vertical;
1562
+ box-sizing: border-box;
1563
+ }
1564
+
1565
+ .tvw-feedback-comment:focus {
1566
+ outline: none;
1567
+ border-color: var(--cw-accent, #1d4ed8);
1568
+ box-shadow: 0 0 0 2px rgba(29, 78, 216, 0.15);
1569
+ }
1570
+
1571
+ .tvw-feedback-comment::placeholder {
1572
+ color: var(--cw-muted, #9ca3af);
1573
+ }
1574
+
1575
+ /* Action buttons */
1576
+ .tvw-feedback-actions {
1577
+ display: flex;
1578
+ gap: 0.5rem;
1579
+ justify-content: flex-end;
1580
+ }
1581
+
1582
+ .tvw-feedback-btn {
1583
+ padding: 0.5rem 1rem;
1584
+ font-size: 0.875rem;
1585
+ font-weight: 500;
1586
+ border-radius: var(--tvw-cw-radius-sm, 6px);
1587
+ cursor: pointer;
1588
+ transition: all 0.2s ease;
1589
+ }
1590
+
1591
+ .tvw-feedback-btn-skip {
1592
+ background: transparent;
1593
+ border: 1px solid var(--cw-border, #e5e7eb);
1594
+ color: var(--cw-muted, #6b7280);
1595
+ }
1596
+
1597
+ .tvw-feedback-btn-skip:hover {
1598
+ background: var(--cw-container, #f3f4f6);
1599
+ color: var(--cw-primary, #111827);
1600
+ }
1601
+
1602
+ .tvw-feedback-btn-submit {
1603
+ background: var(--cw-accent, #1d4ed8);
1604
+ border: 1px solid var(--cw-accent, #1d4ed8);
1605
+ color: #ffffff;
1606
+ }
1607
+
1608
+ .tvw-feedback-btn-submit:hover:not(:disabled) {
1609
+ opacity: 0.9;
1610
+ }
1611
+
1612
+ .tvw-feedback-btn-submit:disabled {
1613
+ opacity: 0.6;
1614
+ cursor: not-allowed;
1615
+ }
1616
+
1617
+ /* Shake animation for validation */
1618
+ @keyframes tvw-feedback-shake {
1619
+ 0%, 100% { transform: translateX(0); }
1620
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
1621
+ 20%, 40%, 60%, 80% { transform: translateX(4px); }
1622
+ }
1623
+
1624
+ .tvw-feedback-shake {
1625
+ animation: tvw-feedback-shake 0.5s ease-in-out;
1626
+ }
package/src/types.ts CHANGED
@@ -22,6 +22,7 @@ export type AgentWidgetRequestPayload = {
22
22
  messages: AgentWidgetRequestPayloadMessage[];
23
23
  flowId?: string;
24
24
  context?: Record<string, unknown>;
25
+ metadata?: Record<string, unknown>;
25
26
  };
26
27
 
27
28
  export type AgentWidgetRequestMiddlewareContext = {
@@ -104,6 +105,23 @@ export type AgentWidgetMessageFeedback = {
104
105
 
105
106
  /**
106
107
  * Configuration for message action buttons (copy, upvote, downvote)
108
+ *
109
+ * **Client Token Mode**: When using `clientToken`, feedback is automatically
110
+ * sent to your Travrse backend. Just enable the buttons and you're done!
111
+ * The `onFeedback` and `onCopy` callbacks are optional for additional local handling.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // With clientToken - feedback is automatic!
116
+ * config: {
117
+ * clientToken: 'ct_live_...',
118
+ * messageActions: {
119
+ * showUpvote: true,
120
+ * showDownvote: true,
121
+ * // No onFeedback needed - sent to backend automatically
122
+ * }
123
+ * }
124
+ * ```
107
125
  */
108
126
  export type AgentWidgetMessageActionsConfig = {
109
127
  /**
@@ -117,13 +135,15 @@ export type AgentWidgetMessageActionsConfig = {
117
135
  */
118
136
  showCopy?: boolean;
119
137
  /**
120
- * Show upvote button
121
- * @default false (requires backend)
138
+ * Show upvote button.
139
+ * When using `clientToken`, feedback is sent to the backend automatically.
140
+ * @default false
122
141
  */
123
142
  showUpvote?: boolean;
124
143
  /**
125
- * Show downvote button
126
- * @default false (requires backend)
144
+ * Show downvote button.
145
+ * When using `clientToken`, feedback is sent to the backend automatically.
146
+ * @default false
127
147
  */
128
148
  showDownvote?: boolean;
129
149
  /**
@@ -144,11 +164,19 @@ export type AgentWidgetMessageActionsConfig = {
144
164
  */
145
165
  layout?: "pill-inside" | "row-inside";
146
166
  /**
147
- * Callback when user submits feedback (upvote/downvote)
167
+ * Callback when user submits feedback (upvote/downvote).
168
+ *
169
+ * **Note**: When using `clientToken`, feedback is AUTOMATICALLY sent to your
170
+ * backend via `/v1/client/feedback`. This callback is called IN ADDITION to
171
+ * the automatic submission, useful for updating local UI or analytics.
148
172
  */
149
173
  onFeedback?: (feedback: AgentWidgetMessageFeedback) => void;
150
174
  /**
151
- * Callback when user copies a message
175
+ * Callback when user copies a message.
176
+ *
177
+ * **Note**: When using `clientToken`, copy events are AUTOMATICALLY tracked
178
+ * via `/v1/client/feedback`. This callback is called IN ADDITION to the
179
+ * automatic tracking.
152
180
  */
153
181
  onCopy?: (message: AgentWidgetMessage) => void;
154
182
  };
@@ -567,9 +595,33 @@ export type ClientInitResponse = {
567
595
  export type ClientChatRequest = {
568
596
  session_id: string;
569
597
  messages: Array<{
598
+ id?: string;
570
599
  role: 'user' | 'assistant' | 'system';
571
600
  content: string;
572
601
  }>;
602
+ /** ID for the expected assistant response message */
603
+ assistant_message_id?: string;
604
+ metadata?: Record<string, unknown>;
605
+ context?: Record<string, unknown>;
606
+ };
607
+
608
+ /**
609
+ * Feedback types supported by the API
610
+ */
611
+ export type ClientFeedbackType = 'upvote' | 'downvote' | 'copy' | 'csat' | 'nps';
612
+
613
+ /**
614
+ * Request payload for /v1/client/feedback endpoint
615
+ */
616
+ export type ClientFeedbackRequest = {
617
+ session_id: string;
618
+ /** Required for upvote, downvote, copy feedback types */
619
+ message_id?: string;
620
+ type: ClientFeedbackType;
621
+ /** Required for csat (1-5) and nps (0-10) feedback types */
622
+ rating?: number;
623
+ /** Optional comment for any feedback type */
624
+ comment?: string;
573
625
  };
574
626
 
575
627
  // ============================================================================
package/src/ui.ts CHANGED
@@ -43,6 +43,12 @@ import {
43
43
  extractComponentDirectiveFromMessage,
44
44
  hasComponentDirective
45
45
  } from "./utils/component-middleware";
46
+ import {
47
+ createCSATFeedback,
48
+ createNPSFeedback,
49
+ type CSATFeedbackOptions,
50
+ type NPSFeedbackOptions
51
+ } from "./components/feedback";
46
52
 
47
53
  // Default localStorage key for chat history (automatically cleared on clear chat)
48
54
  const DEFAULT_CHAT_HISTORY_STORAGE_KEY = "vanilla-agent-chat-history";
@@ -91,6 +97,11 @@ type Controller = {
91
97
  isOpen: () => boolean;
92
98
  isVoiceActive: () => boolean;
93
99
  getState: () => AgentWidgetStateSnapshot;
100
+ // Feedback methods (CSAT/NPS)
101
+ showCSATFeedback: (options?: Partial<CSATFeedbackOptions>) => void;
102
+ showNPSFeedback: (options?: Partial<NPSFeedbackOptions>) => void;
103
+ submitCSATFeedback: (rating: number, comment?: string) => Promise<void>;
104
+ submitNPSFeedback: (rating: number, comment?: string) => Promise<void>;
94
105
  };
95
106
 
96
107
  const buildPostprocessor = (
@@ -229,13 +240,35 @@ export const createAgentExperience = (
229
240
  let showReasoning = config.features?.showReasoning ?? true;
230
241
  let showToolCalls = config.features?.showToolCalls ?? true;
231
242
 
232
- // Create message action callbacks that emit events
243
+ // Create message action callbacks that emit events and optionally send to API
233
244
  const messageActionCallbacks: MessageActionCallbacks = {
234
245
  onCopy: (message: AgentWidgetMessage) => {
235
246
  eventBus.emit("message:copy", message);
247
+ // Send copy feedback to API if in client token mode
248
+ if (session?.isClientTokenMode()) {
249
+ session.submitMessageFeedback(message.id, 'copy').catch((error) => {
250
+ if (config.debug) {
251
+ // eslint-disable-next-line no-console
252
+ console.error("[AgentWidget] Failed to submit copy feedback:", error);
253
+ }
254
+ });
255
+ }
256
+ // Call user-provided callback
257
+ config.messageActions?.onCopy?.(message);
236
258
  },
237
259
  onFeedback: (feedback: AgentWidgetMessageFeedback) => {
238
260
  eventBus.emit("message:feedback", feedback);
261
+ // Send feedback to API if in client token mode
262
+ if (session?.isClientTokenMode()) {
263
+ session.submitMessageFeedback(feedback.messageId, feedback.type).catch((error) => {
264
+ if (config.debug) {
265
+ // eslint-disable-next-line no-console
266
+ console.error("[AgentWidget] Failed to submit feedback:", error);
267
+ }
268
+ });
269
+ }
270
+ // Call user-provided callback
271
+ config.messageActions?.onFeedback?.(feedback);
239
272
  }
240
273
  };
241
274
 
@@ -2797,6 +2830,67 @@ export const createAgentExperience = (
2797
2830
  streaming: session.isStreaming()
2798
2831
  };
2799
2832
  },
2833
+ // Feedback methods (CSAT/NPS)
2834
+ showCSATFeedback(options?: Partial<CSATFeedbackOptions>) {
2835
+ // Auto-open widget if closed and launcher is enabled
2836
+ if (!open && launcherEnabled) {
2837
+ setOpenState(true, "system");
2838
+ }
2839
+
2840
+ // Remove any existing feedback forms
2841
+ const existingFeedback = messagesWrapper.querySelector('.tvw-feedback-container');
2842
+ if (existingFeedback) {
2843
+ existingFeedback.remove();
2844
+ }
2845
+
2846
+ const feedbackEl = createCSATFeedback({
2847
+ onSubmit: async (rating, comment) => {
2848
+ if (session.isClientTokenMode()) {
2849
+ await session.submitCSATFeedback(rating, comment);
2850
+ }
2851
+ options?.onSubmit?.(rating, comment);
2852
+ },
2853
+ onDismiss: options?.onDismiss,
2854
+ ...options,
2855
+ });
2856
+
2857
+ // Append to messages area at the bottom
2858
+ messagesWrapper.appendChild(feedbackEl);
2859
+ feedbackEl.scrollIntoView({ behavior: 'smooth', block: 'end' });
2860
+ },
2861
+ showNPSFeedback(options?: Partial<NPSFeedbackOptions>) {
2862
+ // Auto-open widget if closed and launcher is enabled
2863
+ if (!open && launcherEnabled) {
2864
+ setOpenState(true, "system");
2865
+ }
2866
+
2867
+ // Remove any existing feedback forms
2868
+ const existingFeedback = messagesWrapper.querySelector('.tvw-feedback-container');
2869
+ if (existingFeedback) {
2870
+ existingFeedback.remove();
2871
+ }
2872
+
2873
+ const feedbackEl = createNPSFeedback({
2874
+ onSubmit: async (rating, comment) => {
2875
+ if (session.isClientTokenMode()) {
2876
+ await session.submitNPSFeedback(rating, comment);
2877
+ }
2878
+ options?.onSubmit?.(rating, comment);
2879
+ },
2880
+ onDismiss: options?.onDismiss,
2881
+ ...options,
2882
+ });
2883
+
2884
+ // Append to messages area at the bottom
2885
+ messagesWrapper.appendChild(feedbackEl);
2886
+ feedbackEl.scrollIntoView({ behavior: 'smooth', block: 'end' });
2887
+ },
2888
+ async submitCSATFeedback(rating: number, comment?: string): Promise<void> {
2889
+ return session.submitCSATFeedback(rating, comment);
2890
+ },
2891
+ async submitNPSFeedback(rating: number, comment?: string): Promise<void> {
2892
+ return session.submitNPSFeedback(rating, comment);
2893
+ },
2800
2894
  destroy() {
2801
2895
  destroyCallbacks.forEach((cb) => cb());
2802
2896
  wrapper.remove();