@kelet-ai/feedback-ui 1.0.2 → 1.1.1

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,6 +352,88 @@ 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
438
  const DefaultKeletBaseUrl = "https://api.kelet.ai/api";
357
439
  const useKelet = () => {
@@ -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) {
@@ -383,15 +468,21 @@ const KeletProvider = ({ apiKey, project, baseUrl, children }) => {
383
468
  }
384
469
  const feedback = async (data) => {
385
470
  const resolvedBaseUrl = baseUrl || DefaultKeletBaseUrl;
386
- const url = `${resolvedBaseUrl}/projects/${project}/feedback`;
471
+ const url = `${resolvedBaseUrl}/projects/${project}/signal`;
472
+ const capturedEvent = getLatestEvent();
473
+ const metadata = {
474
+ ...data.extra_metadata ?? {},
475
+ ...capturedEvent && { $dom_event: capturedEvent }
476
+ };
387
477
  const req = {
388
- tx_id: data.tx_id,
478
+ session_id: data.session_id,
389
479
  source: data.source || "EXPLICIT",
390
480
  vote: data.vote,
391
481
  explanation: data.explanation,
392
482
  correction: data.correction,
393
- selection: data.selection
394
- // Include trigger_name if needed in the future
483
+ selection: data.selection,
484
+ trigger_name: data.trigger_name,
485
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0
395
486
  };
396
487
  const response = await fetch(url, {
397
488
  method: "POST",
@@ -453,11 +544,11 @@ const VoteFeedbackRoot = ({
453
544
  children,
454
545
  onFeedback,
455
546
  defaultText = "",
456
- tx_id: txIdProp,
547
+ session_id: sessionIdProp,
457
548
  extra_metadata,
458
549
  trigger_name
459
550
  }) => {
460
- const tx_id = typeof txIdProp === "function" ? txIdProp() : txIdProp;
551
+ const session_id = typeof sessionIdProp === "function" ? sessionIdProp() : sessionIdProp;
461
552
  const [showPopover, setShowPopover] = useState(false);
462
553
  const [feedbackText, setFeedbackText] = useState(defaultText);
463
554
  const [isSubmitting, setIsSubmitting] = useState(false);
@@ -474,11 +565,11 @@ const VoteFeedbackRoot = ({
474
565
  setIsSubmitting(false);
475
566
  setVote(null);
476
567
  setTimeout(() => triggerRef.current?.focus(), 0);
477
- }, [tx_id, defaultText]);
568
+ }, [session_id, defaultText]);
478
569
  const handleUpvote = useCallback(async () => {
479
570
  setVote("upvote");
480
571
  const data = {
481
- tx_id,
572
+ session_id,
482
573
  vote: "upvote",
483
574
  ...extra_metadata && { extra_metadata },
484
575
  ...trigger_name && { trigger_name }
@@ -489,12 +580,12 @@ const VoteFeedbackRoot = ({
489
580
  } finally {
490
581
  setIsSubmitting(false);
491
582
  }
492
- }, [handler, tx_id, extra_metadata, trigger_name]);
583
+ }, [handler, session_id, extra_metadata, trigger_name]);
493
584
  const handleDownvote = useCallback(async () => {
494
585
  setVote("downvote");
495
586
  if (handler) {
496
587
  const data = {
497
- tx_id,
588
+ session_id,
498
589
  vote: "downvote",
499
590
  ...extra_metadata && { extra_metadata },
500
591
  ...trigger_name && { trigger_name }
@@ -517,7 +608,7 @@ const VoteFeedbackRoot = ({
517
608
  document.body.appendChild(announcement);
518
609
  setTimeout(() => document.body.removeChild(announcement), 1e3);
519
610
  }, 0);
520
- }, [handler, tx_id, extra_metadata, trigger_name]);
611
+ }, [handler, session_id, extra_metadata, trigger_name]);
521
612
  const handleTextareaChange = useCallback(
522
613
  (e) => {
523
614
  setFeedbackText(e.target.value);
@@ -528,7 +619,7 @@ const VoteFeedbackRoot = ({
528
619
  const hasText = feedbackText.trim().length > 0;
529
620
  if (hasText) {
530
621
  const data = {
531
- tx_id,
622
+ session_id,
532
623
  vote: "downvote",
533
624
  explanation: feedbackText,
534
625
  ...extra_metadata && { extra_metadata },
@@ -552,7 +643,14 @@ const VoteFeedbackRoot = ({
552
643
  document.body.appendChild(announcement);
553
644
  setTimeout(() => document.body.removeChild(announcement), 1e3);
554
645
  }
555
- }, [handler, feedbackText, defaultText, tx_id, extra_metadata, trigger_name]);
646
+ }, [
647
+ handler,
648
+ feedbackText,
649
+ defaultText,
650
+ session_id,
651
+ extra_metadata,
652
+ trigger_name
653
+ ]);
556
654
  const handleKeyDown = useCallback(
557
655
  (e) => {
558
656
  if (e.key === "Escape") {
@@ -601,7 +699,7 @@ const VoteFeedbackRoot = ({
601
699
  triggerRef,
602
700
  popoverId,
603
701
  triggerId,
604
- tx_id,
702
+ session_id,
605
703
  extra_metadata,
606
704
  trigger_name
607
705
  };
@@ -2016,7 +2114,7 @@ function stringify(value) {
2016
2114
  return String(value);
2017
2115
  }
2018
2116
  }
2019
- function useStateChangeTracking(currentState, tx_id, options) {
2117
+ function useStateChangeTracking(currentState, session_id, options) {
2020
2118
  const defaultFeedbackHandler = useDefaultFeedbackHandler();
2021
2119
  const feedbackHandler = options?.onFeedback || defaultFeedbackHandler;
2022
2120
  const debounceMs = options?.debounceMs ?? 3e3;
@@ -2051,9 +2149,9 @@ function useStateChangeTracking(currentState, tx_id, options) {
2051
2149
  } else {
2052
2150
  vote = diffPercentage > 0.5 ? "downvote" : "upvote";
2053
2151
  }
2054
- const idString = typeof tx_id === "function" ? tx_id(endState) : tx_id;
2152
+ const idString = typeof session_id === "function" ? session_id(endState) : session_id;
2055
2153
  feedbackHandler({
2056
- tx_id: idString,
2154
+ session_id: idString,
2057
2155
  vote,
2058
2156
  explanation: `State change with diff percentage: ${(diffPercentage * 100).toFixed(1)}%`,
2059
2157
  correction: diffString,
@@ -2062,7 +2160,7 @@ function useStateChangeTracking(currentState, tx_id, options) {
2062
2160
  trigger_name: triggerName
2063
2161
  });
2064
2162
  },
2065
- [options, tx_id, diffType, feedbackHandler]
2163
+ [options, session_id, diffType, feedbackHandler]
2066
2164
  );
2067
2165
  const notifyChange = useCallback(
2068
2166
  (trigger_name) => {
@@ -2131,7 +2229,7 @@ function useStateChangeTracking(currentState, tx_id, options) {
2131
2229
  };
2132
2230
  }, [
2133
2231
  currentState,
2134
- tx_id,
2232
+ session_id,
2135
2233
  options,
2136
2234
  feedbackHandler,
2137
2235
  diffType,
@@ -2145,9 +2243,9 @@ function useStateChangeTracking(currentState, tx_id, options) {
2145
2243
  notifyChange
2146
2244
  };
2147
2245
  }
2148
- function useFeedbackState(initialState, tx_id, options) {
2246
+ function useFeedbackState(initialState, session_id, options) {
2149
2247
  const [state, setStateInternal] = useState(initialState);
2150
- const { notifyChange } = useStateChangeTracking(state, tx_id, options);
2248
+ const { notifyChange } = useStateChangeTracking(state, session_id, options);
2151
2249
  const setState = useCallback(
2152
2250
  (value, trigger_name) => {
2153
2251
  notifyChange(trigger_name);