vanilla-agent 1.22.0 → 1.23.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 = {
@@ -567,9 +568,33 @@ export type ClientInitResponse = {
567
568
  export type ClientChatRequest = {
568
569
  session_id: string;
569
570
  messages: Array<{
571
+ id?: string;
570
572
  role: 'user' | 'assistant' | 'system';
571
573
  content: string;
572
574
  }>;
575
+ /** ID for the expected assistant response message */
576
+ assistant_message_id?: string;
577
+ metadata?: Record<string, unknown>;
578
+ context?: Record<string, unknown>;
579
+ };
580
+
581
+ /**
582
+ * Feedback types supported by the API
583
+ */
584
+ export type ClientFeedbackType = 'upvote' | 'downvote' | 'copy' | 'csat' | 'nps';
585
+
586
+ /**
587
+ * Request payload for /v1/client/feedback endpoint
588
+ */
589
+ export type ClientFeedbackRequest = {
590
+ session_id: string;
591
+ /** Required for upvote, downvote, copy feedback types */
592
+ message_id?: string;
593
+ type: ClientFeedbackType;
594
+ /** Required for csat (1-5) and nps (0-10) feedback types */
595
+ rating?: number;
596
+ /** Optional comment for any feedback type */
597
+ comment?: string;
573
598
  };
574
599
 
575
600
  // ============================================================================
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();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Message ID utilities for client-side message tracking
3
+ * Used for feedback integration with the Travrse API
4
+ */
5
+
6
+ /**
7
+ * Generate a unique message ID for tracking
8
+ * Format: msg_{timestamp_base36}_{random_8chars}
9
+ */
10
+ export function generateMessageId(): string {
11
+ const timestamp = Date.now().toString(36);
12
+ const random = Math.random().toString(36).substring(2, 10);
13
+ return `msg_${timestamp}_${random}`;
14
+ }
15
+
16
+ /**
17
+ * Generate a unique user message ID
18
+ * Format: usr_{timestamp_base36}_{random_8chars}
19
+ */
20
+ export function generateUserMessageId(): string {
21
+ const timestamp = Date.now().toString(36);
22
+ const random = Math.random().toString(36).substring(2, 10);
23
+ return `usr_${timestamp}_${random}`;
24
+ }
25
+
26
+ /**
27
+ * Generate a unique assistant message ID
28
+ * Format: ast_{timestamp_base36}_{random_8chars}
29
+ */
30
+ export function generateAssistantMessageId(): string {
31
+ const timestamp = Date.now().toString(36);
32
+ const random = Math.random().toString(36).substring(2, 10);
33
+ return `ast_${timestamp}_${random}`;
34
+ }
35
+