@kelet-ai/feedback-ui 1.0.2 → 1.1.3

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/README.md CHANGED
@@ -28,7 +28,7 @@ import { ShadcnVoteFeedback } from '@/components/ui/vote-feedback';
28
28
  function App() {
29
29
  return (
30
30
  <ShadcnVoteFeedback
31
- tx_id="my-feature"
31
+ session_id="my-feature"
32
32
  onFeedback={feedback => console.log(feedback)}
33
33
  variant="outline"
34
34
  />
@@ -91,7 +91,7 @@ Users explicitly vote and provide comments:
91
91
  ```tsx
92
92
  import { VoteFeedback } from '@kelet-ai/feedback-ui';
93
93
 
94
- <VoteFeedback.Root onFeedback={handleFeedback} tx_id="ai-response">
94
+ <VoteFeedback.Root onFeedback={handleFeedback} session_id="ai-response">
95
95
  <VoteFeedback.UpvoteButton>👍 Helpful</VoteFeedback.UpvoteButton>
96
96
  <VoteFeedback.DownvoteButton>👎 Not helpful</VoteFeedback.DownvoteButton>
97
97
  <VoteFeedback.Popover>
@@ -158,14 +158,14 @@ Understanding these fundamental concepts will help you implement feedback collec
158
158
  **Best Practice**: Use traceable IDs from your logging system (session ID, trace ID, request ID)
159
159
 
160
160
  ```tsx
161
- // ✅ Good: Traceable tx_id
162
- <VoteFeedback.Root tx_id="session-abc123-ai-response-456"/>
161
+ // ✅ Good: Traceable session_id
162
+ <VoteFeedback.Root session_id="session-abc123-ai-response-456"/>
163
163
 
164
- // ✅ Good: Content-based tx_id
165
- <VoteFeedback.Root tx_id={`article-${articleId}-section-${sectionId}`}/>
164
+ // ✅ Good: Content-based session_id
165
+ <VoteFeedback.Root session_id={`article-${articleId}-section-${sectionId}`}/>
166
166
 
167
- // ❌ Poor: Generic tx_id
168
- <VoteFeedback.Root tx_id="feedback"/>
167
+ // ❌ Poor: Generic session_id
168
+ <VoteFeedback.Root session_id="feedback"/>
169
169
  ```
170
170
 
171
171
  ### **📊 Feedback Sources**
@@ -296,8 +296,8 @@ const [data, setData] = useFeedbackState(initial, 'tracker', {
296
296
  #### **Identifiers**
297
297
 
298
298
  ✅ Use traceable session/request IDs
299
- ✅ Include context in tx_id structure
300
- ✅ Keep tx_ids consistent across related actions
299
+ ✅ Include context in session_id structure
300
+ ✅ Keep session_ids consistent across related actions
301
301
 
302
302
  #### **Feedback Sources**
303
303
 
@@ -327,7 +327,7 @@ Main container component that manages feedback state.
327
327
 
328
328
  ```tsx
329
329
  <VoteFeedback.Root
330
- tx_id="unique-id" // Required: Unique tracking ID
330
+ session_id="unique-id" // Required: Unique tracking ID
331
331
  onFeedback={handleFeedback} // Required: Callback function
332
332
  trigger_name="user_feedback" // Optional: Categorization
333
333
  extra_metadata={{ page: 'home' }} // Optional: Additional data
@@ -376,7 +376,7 @@ const [count, setCount] = useFeedbackState(0, 'counter-widget');
376
376
  ```tsx
377
377
  const [profile, setProfile] = useFeedbackState(
378
378
  { name: '', email: '' },
379
- state => `profile-${state.email}`, // Dynamic tx_id
379
+ state => `profile-${state.email}`, // Dynamic session_id
380
380
  {
381
381
  debounceMs: 2000, // Wait time before sending feedback
382
382
  diffType: 'object', // Format: 'git' | 'object' | 'json'
@@ -466,7 +466,7 @@ dispatch({ type: 'custom' }, 'override'); // Custom trigger name
466
466
 
467
467
  ```tsx
468
468
  <VoteFeedback.Root
469
- tx_id="ai-response-123"
469
+ session_id="ai-response-123"
470
470
  onFeedback={handleFeedback}
471
471
  trigger_name="ai_evaluation"
472
472
  extra_metadata={{
@@ -492,7 +492,7 @@ dispatch({ type: 'custom' }, 'override'); // Custom trigger name
492
492
 
493
493
  ```typescript
494
494
  interface FeedbackData {
495
- tx_id: string; // Unique tracking ID
495
+ session_id: string; // Unique tracking ID
496
496
  vote: 'upvote' | 'downvote'; // User's vote
497
497
  explanation?: string; // Optional user comment
498
498
  extra_metadata?: Record<string, any>; // Additional context data
@@ -507,7 +507,7 @@ interface FeedbackData {
507
507
 
508
508
  | Prop | Type | Required | Description |
509
509
  | ---------------- | ------------------------------ | -------- | ----------------------------------- |
510
- | `tx_id` | `string` | ✅ | Unique transaction ID for tracking |
510
+ | `session_id` | `string` | ✅ | Unique session ID for tracking |
511
511
  | `onFeedback` | `(data: FeedbackData) => void` | ✅ | Callback when feedback is submitted |
512
512
  | `trigger_name` | `string` | ❌ | Optional categorization tag |
513
513
  | `extra_metadata` | `object` | ❌ | Additional context data |
@@ -593,9 +593,9 @@ bun run checks # Run all quality checks (lint, format, typecheck, tests)
593
593
  <VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
594
594
  </VoteFeedback.Root>
595
595
 
596
- // ✅ Include required tx_id and onFeedback
596
+ // ✅ Include required session_id and onFeedback
597
597
  <VoteFeedback.Root
598
- tx_id="my-feature"
598
+ session_id="my-feature"
599
599
  onFeedback={handleFeedback}
600
600
  >
601
601
  <VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
@@ -631,14 +631,14 @@ const [value, setValue] = useFeedbackState('initial', 'test', {
631
631
 
632
632
  ### **Best Practices**
633
633
 
634
- ✅ **Use unique tx_ids** for each feedback instance - the tx_id should be traceable back to the session's log
634
+ ✅ **Use unique session_ids** for each feedback instance - the session_id should be traceable back to the session's log
635
635
  and allow us to understand the context of the feedback.
636
636
  ✅ **Handle feedback data asynchronously** in your callback
637
637
  ✅ **Test keyboard navigation** in your implementation
638
638
  ✅ **Provide meaningful trigger names** for categorization
639
639
  ✅ **Include relevant metadata** for context
640
640
 
641
- ❌ **Don't use the same tx_id** for multiple components. A tx_id should be traced back to the session's log -
641
+ ❌ **Don't use the same session_id** for multiple components. A session_id should be traced back to the session's log -
642
642
  allows us to understand the context of the feedback.
643
643
 
644
644
  ---
@@ -1,6 +1,6 @@
1
1
  import { DownvoteButtonProps, PopoverProps, SubmitButtonProps, TextareaProps, UpvoteButtonProps, VoteFeedbackRootProps } from '../types';
2
2
  export declare const VoteFeedback: {
3
- Root: ({ children, onFeedback, defaultText, tx_id: txIdProp, extra_metadata, trigger_name, }: VoteFeedbackRootProps) => import("react/jsx-runtime").JSX.Element;
3
+ Root: ({ children, onFeedback, defaultText, session_id: sessionIdProp, extra_metadata, trigger_name, }: VoteFeedbackRootProps) => import("react/jsx-runtime").JSX.Element;
4
4
  UpvoteButton: ({ asChild, children, onClick, ...props }: UpvoteButtonProps) => import("react/jsx-runtime").JSX.Element;
5
5
  DownvoteButton: ({ asChild, children, onClick, ...props }: DownvoteButtonProps) => string | number | bigint | true | Iterable<import('react').ReactNode> | Promise<string | number | bigint | boolean | import('react').ReactPortal | import('react').ReactElement<unknown, string | import('react').JSXElementConstructor<any>> | Iterable<import('react').ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element;
6
6
  Popover: ({ asChild, children, ...props }: PopoverProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -1,4 +1,4 @@
1
- import require$$0$1, { createContext, useContext, useCallback, isValidElement, cloneElement, useState, useRef, useId, useEffect } from "react";
1
+ import require$$0$1, { createContext, useEffect, useContext, useCallback, isValidElement, cloneElement, useState, useRef, useId } from "react";
2
2
  function getDefaultExportFromCjs(x) {
3
3
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
4
4
  }
@@ -352,8 +352,90 @@ function requireJsxRuntime() {
352
352
  return jsxRuntime.exports;
353
353
  }
354
354
  var jsxRuntimeExports = requireJsxRuntime();
355
+ let latestEvent = null;
356
+ let isInitialized = false;
357
+ function serializeTarget(target) {
358
+ if (!target || !(target instanceof Element)) return "unknown";
359
+ const el = target;
360
+ if (el.id) return `#${el.id}`;
361
+ const dataId = el.getAttribute("data-feedback-id");
362
+ if (dataId) return `[data-feedback-id="${dataId}"]`;
363
+ const path = [];
364
+ let current = el;
365
+ let depth = 0;
366
+ const MAX_DEPTH = 5;
367
+ while (current && current !== document.body && depth < MAX_DEPTH) {
368
+ let selector = current.tagName.toLowerCase();
369
+ if (current.classList.length > 0) {
370
+ const className = Array.from(current.classList)[0];
371
+ selector += `.${className}`;
372
+ }
373
+ const parent = current.parentElement;
374
+ if (parent) {
375
+ const siblings = Array.from(parent.children);
376
+ const index = siblings.indexOf(current) + 1;
377
+ if (siblings.length > 1) {
378
+ selector += `:nth-child(${index})`;
379
+ }
380
+ }
381
+ path.unshift(selector);
382
+ current = parent;
383
+ depth++;
384
+ }
385
+ return path.join(" > ");
386
+ }
387
+ function getTargetText(target) {
388
+ if (!target || !(target instanceof Element)) return "";
389
+ const el = target;
390
+ const ariaLabel = el.getAttribute("aria-label");
391
+ if (ariaLabel) return ariaLabel.substring(0, 50);
392
+ const text = el.textContent?.trim() || "";
393
+ return text.substring(0, 50);
394
+ }
395
+ function captureEvent(e) {
396
+ const captured = {
397
+ type: e.type,
398
+ targetSelector: serializeTarget(e.target),
399
+ targetText: getTargetText(e.target),
400
+ timestamp: Date.now()
401
+ };
402
+ if (e instanceof MouseEvent) {
403
+ captured.coordinates = { x: e.clientX, y: e.clientY };
404
+ } else if (e instanceof KeyboardEvent) {
405
+ captured.key = e.key;
406
+ }
407
+ return captured;
408
+ }
409
+ function initEventCapture() {
410
+ if (isInitialized || typeof window === "undefined") return;
411
+ const eventTypes = ["click", "keydown", "submit", "change"];
412
+ eventTypes.forEach((type) => {
413
+ window.addEventListener(
414
+ type,
415
+ (e) => {
416
+ latestEvent = captureEvent(e);
417
+ },
418
+ {
419
+ capture: true,
420
+ // Intercept before React handlers
421
+ passive: true
422
+ // Better scroll performance
423
+ }
424
+ );
425
+ });
426
+ isInitialized = true;
427
+ }
428
+ function getLatestEvent() {
429
+ if (!latestEvent) return null;
430
+ const age = Date.now() - latestEvent.timestamp;
431
+ if (age > 1e4) {
432
+ latestEvent = null;
433
+ return null;
434
+ }
435
+ return { ...latestEvent };
436
+ }
355
437
  const KeletContext = createContext(null);
356
- const DefaultKeletBaseUrl = "https://api.kelet.ai/api";
438
+ const DefaultKeletBaseUrl = "https://api.kelet.ai";
357
439
  const useKelet = () => {
358
440
  const context = useContext(KeletContext);
359
441
  if (!context) {
@@ -374,6 +456,9 @@ const useDefaultFeedbackHandler = () => {
374
456
  }
375
457
  };
376
458
  const KeletProvider = ({ apiKey, project, baseUrl, children }) => {
459
+ useEffect(() => {
460
+ initEventCapture();
461
+ }, []);
377
462
  const parentContext = useContext(KeletContext);
378
463
  const resolvedApiKey = apiKey || parentContext?.api_key;
379
464
  if (!resolvedApiKey) {
@@ -381,17 +466,29 @@ const KeletProvider = ({ apiKey, project, baseUrl, children }) => {
381
466
  "apiKey is required either directly or from a parent KeletProvider"
382
467
  );
383
468
  }
469
+ let resolvedBaseUrl = baseUrl || DefaultKeletBaseUrl;
470
+ if (resolvedBaseUrl.endsWith("/api")) {
471
+ resolvedBaseUrl = resolvedBaseUrl.slice(0, -4);
472
+ }
473
+ if (resolvedBaseUrl.endsWith("/")) {
474
+ resolvedBaseUrl = resolvedBaseUrl.slice(0, -1);
475
+ }
384
476
  const feedback = async (data) => {
385
- const resolvedBaseUrl = baseUrl || DefaultKeletBaseUrl;
386
- const url = `${resolvedBaseUrl}/projects/${project}/feedback`;
477
+ const url = `${resolvedBaseUrl}/api/projects/${project}/signal`;
478
+ const capturedEvent = getLatestEvent();
479
+ const metadata = {
480
+ ...data.extra_metadata ?? {},
481
+ ...capturedEvent && { $dom_event: capturedEvent }
482
+ };
387
483
  const req = {
388
- tx_id: data.tx_id,
484
+ session_id: data.session_id,
389
485
  source: data.source || "EXPLICIT",
390
486
  vote: data.vote,
391
487
  explanation: data.explanation,
392
488
  correction: data.correction,
393
- selection: data.selection
394
- // Include trigger_name if needed in the future
489
+ selection: data.selection,
490
+ trigger_name: data.trigger_name,
491
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0
395
492
  };
396
493
  const response = await fetch(url, {
397
494
  method: "POST",
@@ -453,11 +550,11 @@ const VoteFeedbackRoot = ({
453
550
  children,
454
551
  onFeedback,
455
552
  defaultText = "",
456
- tx_id: txIdProp,
553
+ session_id: sessionIdProp,
457
554
  extra_metadata,
458
555
  trigger_name
459
556
  }) => {
460
- const tx_id = typeof txIdProp === "function" ? txIdProp() : txIdProp;
557
+ const session_id = typeof sessionIdProp === "function" ? sessionIdProp() : sessionIdProp;
461
558
  const [showPopover, setShowPopover] = useState(false);
462
559
  const [feedbackText, setFeedbackText] = useState(defaultText);
463
560
  const [isSubmitting, setIsSubmitting] = useState(false);
@@ -474,11 +571,11 @@ const VoteFeedbackRoot = ({
474
571
  setIsSubmitting(false);
475
572
  setVote(null);
476
573
  setTimeout(() => triggerRef.current?.focus(), 0);
477
- }, [tx_id, defaultText]);
574
+ }, [session_id, defaultText]);
478
575
  const handleUpvote = useCallback(async () => {
479
576
  setVote("upvote");
480
577
  const data = {
481
- tx_id,
578
+ session_id,
482
579
  vote: "upvote",
483
580
  ...extra_metadata && { extra_metadata },
484
581
  ...trigger_name && { trigger_name }
@@ -489,12 +586,12 @@ const VoteFeedbackRoot = ({
489
586
  } finally {
490
587
  setIsSubmitting(false);
491
588
  }
492
- }, [handler, tx_id, extra_metadata, trigger_name]);
589
+ }, [handler, session_id, extra_metadata, trigger_name]);
493
590
  const handleDownvote = useCallback(async () => {
494
591
  setVote("downvote");
495
592
  if (handler) {
496
593
  const data = {
497
- tx_id,
594
+ session_id,
498
595
  vote: "downvote",
499
596
  ...extra_metadata && { extra_metadata },
500
597
  ...trigger_name && { trigger_name }
@@ -517,7 +614,7 @@ const VoteFeedbackRoot = ({
517
614
  document.body.appendChild(announcement);
518
615
  setTimeout(() => document.body.removeChild(announcement), 1e3);
519
616
  }, 0);
520
- }, [handler, tx_id, extra_metadata, trigger_name]);
617
+ }, [handler, session_id, extra_metadata, trigger_name]);
521
618
  const handleTextareaChange = useCallback(
522
619
  (e) => {
523
620
  setFeedbackText(e.target.value);
@@ -528,7 +625,7 @@ const VoteFeedbackRoot = ({
528
625
  const hasText = feedbackText.trim().length > 0;
529
626
  if (hasText) {
530
627
  const data = {
531
- tx_id,
628
+ session_id,
532
629
  vote: "downvote",
533
630
  explanation: feedbackText,
534
631
  ...extra_metadata && { extra_metadata },
@@ -552,7 +649,14 @@ const VoteFeedbackRoot = ({
552
649
  document.body.appendChild(announcement);
553
650
  setTimeout(() => document.body.removeChild(announcement), 1e3);
554
651
  }
555
- }, [handler, feedbackText, defaultText, tx_id, extra_metadata, trigger_name]);
652
+ }, [
653
+ handler,
654
+ feedbackText,
655
+ defaultText,
656
+ session_id,
657
+ extra_metadata,
658
+ trigger_name
659
+ ]);
556
660
  const handleKeyDown = useCallback(
557
661
  (e) => {
558
662
  if (e.key === "Escape") {
@@ -601,7 +705,7 @@ const VoteFeedbackRoot = ({
601
705
  triggerRef,
602
706
  popoverId,
603
707
  triggerId,
604
- tx_id,
708
+ session_id,
605
709
  extra_metadata,
606
710
  trigger_name
607
711
  };
@@ -2016,7 +2120,7 @@ function stringify(value) {
2016
2120
  return String(value);
2017
2121
  }
2018
2122
  }
2019
- function useStateChangeTracking(currentState, tx_id, options) {
2123
+ function useStateChangeTracking(currentState, session_id, options) {
2020
2124
  const defaultFeedbackHandler = useDefaultFeedbackHandler();
2021
2125
  const feedbackHandler = options?.onFeedback || defaultFeedbackHandler;
2022
2126
  const debounceMs = options?.debounceMs ?? 3e3;
@@ -2051,9 +2155,9 @@ function useStateChangeTracking(currentState, tx_id, options) {
2051
2155
  } else {
2052
2156
  vote = diffPercentage > 0.5 ? "downvote" : "upvote";
2053
2157
  }
2054
- const idString = typeof tx_id === "function" ? tx_id(endState) : tx_id;
2158
+ const idString = typeof session_id === "function" ? session_id(endState) : session_id;
2055
2159
  feedbackHandler({
2056
- tx_id: idString,
2160
+ session_id: idString,
2057
2161
  vote,
2058
2162
  explanation: `State change with diff percentage: ${(diffPercentage * 100).toFixed(1)}%`,
2059
2163
  correction: diffString,
@@ -2062,7 +2166,7 @@ function useStateChangeTracking(currentState, tx_id, options) {
2062
2166
  trigger_name: triggerName
2063
2167
  });
2064
2168
  },
2065
- [options, tx_id, diffType, feedbackHandler]
2169
+ [options, session_id, diffType, feedbackHandler]
2066
2170
  );
2067
2171
  const notifyChange = useCallback(
2068
2172
  (trigger_name) => {
@@ -2131,7 +2235,7 @@ function useStateChangeTracking(currentState, tx_id, options) {
2131
2235
  };
2132
2236
  }, [
2133
2237
  currentState,
2134
- tx_id,
2238
+ session_id,
2135
2239
  options,
2136
2240
  feedbackHandler,
2137
2241
  diffType,
@@ -2145,9 +2249,9 @@ function useStateChangeTracking(currentState, tx_id, options) {
2145
2249
  notifyChange
2146
2250
  };
2147
2251
  }
2148
- function useFeedbackState(initialState, tx_id, options) {
2252
+ function useFeedbackState(initialState, session_id, options) {
2149
2253
  const [state, setStateInternal] = useState(initialState);
2150
- const { notifyChange } = useStateChangeTracking(state, tx_id, options);
2254
+ const { notifyChange } = useStateChangeTracking(state, session_id, options);
2151
2255
  const setState = useCallback(
2152
2256
  (value, trigger_name) => {
2153
2257
  notifyChange(trigger_name);