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.
package/dist/widget.css CHANGED
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "repository": {
54
54
  "type": "git",
55
- "url": "git+https://github.com/becomevocal/chaty.git"
55
+ "url": "git+https://github.com/becomevocal/vanilla-agent.git"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rimraf dist && npm run build:styles && npm run build:client && npm run build:installer",
package/src/client.ts CHANGED
@@ -12,7 +12,9 @@ import {
12
12
  AgentWidgetSSEEventResult,
13
13
  ClientSession,
14
14
  ClientInitResponse,
15
- ClientChatRequest
15
+ ClientChatRequest,
16
+ ClientFeedbackRequest,
17
+ ClientFeedbackType
16
18
  } from "./types";
17
19
  import {
18
20
  extractTextFromJson,
@@ -25,6 +27,8 @@ import {
25
27
  type DispatchOptions = {
26
28
  messages: AgentWidgetMessage[];
27
29
  signal?: AbortSignal;
30
+ /** Pre-generated ID for the expected assistant response (for feedback tracking) */
31
+ assistantMessageId?: string;
28
32
  };
29
33
 
30
34
  type SSEHandler = (event: AgentWidgetEvent) => void;
@@ -178,6 +182,166 @@ export class AgentWidgetClient {
178
182
  this.sessionInitPromise = null;
179
183
  }
180
184
 
185
+ /**
186
+ * Get the feedback API URL
187
+ */
188
+ private getFeedbackApiUrl(): string {
189
+ const baseUrl = this.config.apiUrl?.replace(/\/+$/, '').replace(/\/v1\/dispatch$/, '') || DEFAULT_CLIENT_API_BASE;
190
+ return `${baseUrl}/v1/client/feedback`;
191
+ }
192
+
193
+ /**
194
+ * Send feedback for a message (client token mode only).
195
+ * Supports upvote, downvote, copy, csat, and nps feedback types.
196
+ *
197
+ * @param feedback - The feedback request payload
198
+ * @returns Promise that resolves when feedback is sent successfully
199
+ * @throws Error if not in client token mode or if session is invalid
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * // Message feedback (upvote/downvote/copy)
204
+ * await client.sendFeedback({
205
+ * session_id: sessionId,
206
+ * message_id: messageId,
207
+ * type: 'upvote'
208
+ * });
209
+ *
210
+ * // CSAT feedback (1-5 rating)
211
+ * await client.sendFeedback({
212
+ * session_id: sessionId,
213
+ * type: 'csat',
214
+ * rating: 5,
215
+ * comment: 'Great experience!'
216
+ * });
217
+ *
218
+ * // NPS feedback (0-10 rating)
219
+ * await client.sendFeedback({
220
+ * session_id: sessionId,
221
+ * type: 'nps',
222
+ * rating: 9
223
+ * });
224
+ * ```
225
+ */
226
+ public async sendFeedback(feedback: ClientFeedbackRequest): Promise<void> {
227
+ if (!this.isClientTokenMode()) {
228
+ throw new Error('sendFeedback() only available in client token mode');
229
+ }
230
+
231
+ const session = this.getClientSession();
232
+ if (!session) {
233
+ throw new Error('No active session. Please initialize session first.');
234
+ }
235
+
236
+ // Validate message_id is provided for message-level feedback types
237
+ const messageFeedbackTypes: ClientFeedbackType[] = ['upvote', 'downvote', 'copy'];
238
+ if (messageFeedbackTypes.includes(feedback.type) && !feedback.message_id) {
239
+ throw new Error(`message_id is required for ${feedback.type} feedback type`);
240
+ }
241
+
242
+ // Validate rating is provided for csat/nps feedback types
243
+ if (feedback.type === 'csat') {
244
+ if (feedback.rating === undefined || feedback.rating < 1 || feedback.rating > 5) {
245
+ throw new Error('CSAT rating must be between 1 and 5');
246
+ }
247
+ }
248
+ if (feedback.type === 'nps') {
249
+ if (feedback.rating === undefined || feedback.rating < 0 || feedback.rating > 10) {
250
+ throw new Error('NPS rating must be between 0 and 10');
251
+ }
252
+ }
253
+
254
+ if (this.debug) {
255
+ // eslint-disable-next-line no-console
256
+ console.debug("[AgentWidgetClient] sending feedback", feedback);
257
+ }
258
+
259
+ const response = await fetch(this.getFeedbackApiUrl(), {
260
+ method: 'POST',
261
+ headers: {
262
+ 'Content-Type': 'application/json',
263
+ },
264
+ body: JSON.stringify(feedback),
265
+ });
266
+
267
+ if (!response.ok) {
268
+ const errorData = await response.json().catch(() => ({ error: 'Feedback submission failed' }));
269
+
270
+ if (response.status === 401) {
271
+ this.clientSession = null;
272
+ this.config.onSessionExpired?.();
273
+ throw new Error('Session expired. Please refresh to continue.');
274
+ }
275
+
276
+ throw new Error(errorData.error || 'Failed to submit feedback');
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Submit message feedback (upvote, downvote, or copy).
282
+ * Convenience method for sendFeedback with message-level feedback.
283
+ *
284
+ * @param messageId - The ID of the message to provide feedback for
285
+ * @param type - The feedback type: 'upvote', 'downvote', or 'copy'
286
+ */
287
+ public async submitMessageFeedback(
288
+ messageId: string,
289
+ type: 'upvote' | 'downvote' | 'copy'
290
+ ): Promise<void> {
291
+ const session = this.getClientSession();
292
+ if (!session) {
293
+ throw new Error('No active session. Please initialize session first.');
294
+ }
295
+
296
+ return this.sendFeedback({
297
+ session_id: session.sessionId,
298
+ message_id: messageId,
299
+ type,
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Submit CSAT (Customer Satisfaction) feedback.
305
+ * Convenience method for sendFeedback with CSAT feedback.
306
+ *
307
+ * @param rating - Rating from 1 to 5
308
+ * @param comment - Optional comment
309
+ */
310
+ public async submitCSATFeedback(rating: number, comment?: string): Promise<void> {
311
+ const session = this.getClientSession();
312
+ if (!session) {
313
+ throw new Error('No active session. Please initialize session first.');
314
+ }
315
+
316
+ return this.sendFeedback({
317
+ session_id: session.sessionId,
318
+ type: 'csat',
319
+ rating,
320
+ comment,
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Submit NPS (Net Promoter Score) feedback.
326
+ * Convenience method for sendFeedback with NPS feedback.
327
+ *
328
+ * @param rating - Rating from 0 to 10
329
+ * @param comment - Optional comment
330
+ */
331
+ public async submitNPSFeedback(rating: number, comment?: string): Promise<void> {
332
+ const session = this.getClientSession();
333
+ if (!session) {
334
+ throw new Error('No active session. Please initialize session first.');
335
+ }
336
+
337
+ return this.sendFeedback({
338
+ session_id: session.sessionId,
339
+ type: 'nps',
340
+ rating,
341
+ comment,
342
+ });
343
+ }
344
+
181
345
  /**
182
346
  * Send a message - handles both proxy and client token modes
183
347
  */
@@ -213,13 +377,22 @@ export class AgentWidgetClient {
213
377
  throw error;
214
378
  }
215
379
 
216
- // Build the chat request payload
380
+ // Build the standard payload to get context/metadata from middleware
381
+ const basePayload = await this.buildPayload(options.messages);
382
+
383
+ // Build the chat request payload with message IDs for feedback tracking
217
384
  const chatRequest: ClientChatRequest = {
218
385
  session_id: session.sessionId,
219
386
  messages: options.messages.map(m => ({
387
+ id: m.id, // Include message ID for tracking
220
388
  role: m.role,
221
389
  content: m.rawContent || m.content,
222
390
  })),
391
+ // Include pre-generated assistant message ID if provided
392
+ ...(options.assistantMessageId && { assistant_message_id: options.assistantMessageId }),
393
+ // Include metadata/context from middleware if present
394
+ ...(basePayload.metadata && { metadata: basePayload.metadata }),
395
+ ...(basePayload.context && { context: basePayload.context }),
223
396
  };
224
397
 
225
398
  if (this.debug) {
@@ -269,7 +442,7 @@ export class AgentWidgetClient {
269
442
 
270
443
  // Stream the response (same SSE handling as proxy mode)
271
444
  try {
272
- await this.streamResponse(response.body, onEvent);
445
+ await this.streamResponse(response.body, onEvent, options.assistantMessageId);
273
446
  } finally {
274
447
  onEvent({ type: "status", status: "idle" });
275
448
  }
@@ -492,7 +665,8 @@ export class AgentWidgetClient {
492
665
 
493
666
  private async streamResponse(
494
667
  body: ReadableStream<Uint8Array>,
495
- onEvent: SSEHandler
668
+ onEvent: SSEHandler,
669
+ assistantMessageId?: string
496
670
  ) {
497
671
  const reader = body.getReader();
498
672
  const decoder = new TextDecoder();
@@ -585,7 +759,8 @@ export class AgentWidgetClient {
585
759
  const ensureAssistantMessage = () => {
586
760
  if (assistantMessage) return assistantMessage;
587
761
  assistantMessage = {
588
- id: `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
762
+ // Use pre-generated ID if provided, otherwise generate one
763
+ id: assistantMessageId ?? `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
589
764
  role: "assistant",
590
765
  content: "",
591
766
  createdAt: new Date().toISOString(),