@kelet-ai/feedback-ui 1.1.4 → 1.3.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/README.md CHANGED
@@ -126,6 +126,43 @@ function ContentEditor() {
126
126
  }
127
127
  ```
128
128
 
129
+ ### **Pattern 4: Manual Signal Sending**
130
+
131
+ Send signals directly without tying them to component state — useful for custom events, page views, or any interaction not covered by the other patterns:
132
+
133
+ ```tsx
134
+ import { KeletProvider, useKeletSignal } from "@kelet-ai/feedback-ui"
135
+
136
+ function CopyButton({ sessionId }: { sessionId: string }) {
137
+ const sendSignal = useKeletSignal()
138
+
139
+ return (
140
+ <button
141
+ onClick={() => {
142
+ navigator.clipboard.writeText("...")
143
+ sendSignal({
144
+ session_id: sessionId,
145
+ kind: "feedback",
146
+ source: "human",
147
+ score: 1,
148
+ trigger_name: "copy_button_clicked",
149
+ })
150
+ }}
151
+ >
152
+ Copy
153
+ </button>
154
+ )
155
+ }
156
+
157
+ function App() {
158
+ return (
159
+ <KeletProvider apiKey="..." project="my-project">
160
+ <CopyButton sessionId="response-abc123" />
161
+ </KeletProvider>
162
+ )
163
+ }
164
+ ```
165
+
129
166
  ### **Pattern 3: Complex State with Reducer**
130
167
 
131
168
  For advanced state management with automatic trigger tracking, by using a drop-in replacement for useReducer:
@@ -359,6 +396,39 @@ Context-aware feedback form that appears after voting.
359
396
 
360
397
  ---
361
398
 
399
+ ## ✉️ Manual Signal Sending
400
+
401
+ ### **useKeletSignal Hook**
402
+
403
+ Returns the signal-sending function from the nearest `KeletProvider`. Use this to send signals manually for any event — custom interactions, page views, feature usage — without tying them to component state.
404
+
405
+ Safe to call outside a provider: returns a no-op and logs a warning.
406
+
407
+ ```tsx
408
+ import { useKeletSignal } from "@kelet-ai/feedback-ui"
409
+
410
+ function MyComponent() {
411
+ const sendSignal = useKeletSignal()
412
+
413
+ const handleAction = () => {
414
+ sendSignal({
415
+ session_id: "my-session",
416
+ kind: "feedback",
417
+ source: "human",
418
+ score: 1,
419
+ trigger_name: "user_action",
420
+ metadata: { context: "sidebar" },
421
+ })
422
+ }
423
+
424
+ return <button onClick={handleAction}>Do something</button>
425
+ }
426
+ ```
427
+
428
+ Requires a `KeletProvider` ancestor to send signals to the API.
429
+
430
+ ---
431
+
362
432
  ## 📊 Automatic Feedback Hooks
363
433
 
364
434
  ### **useFeedbackState Hook**
@@ -1,8 +1,9 @@
1
+ import { default as React } from 'react';
1
2
  import { DownvoteButtonProps, PopoverProps, SubmitButtonProps, TextareaProps, UpvoteButtonProps, VoteFeedbackRootProps } from '../types';
2
3
  export declare const VoteFeedback: {
3
4
  Root: ({ children, onFeedback, defaultText, session_id: sessionIdProp, metadata, trigger_name: triggerProp, trace_id, }: VoteFeedbackRootProps) => import("react/jsx-runtime").JSX.Element;
4
5
  UpvoteButton: ({ asChild, children, onClick, ...props }: UpvoteButtonProps) => import("react/jsx-runtime").JSX.Element;
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
+ DownvoteButton: ({ asChild, children, onClick, ...props }: DownvoteButtonProps) => string | number | bigint | true | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element;
6
7
  Popover: ({ asChild, children, ...props }: PopoverProps) => import("react/jsx-runtime").JSX.Element | null;
7
8
  Textarea: ({ asChild, value, onChange, ...props }: TextareaProps) => import("react/jsx-runtime").JSX.Element;
8
9
  SubmitButton: ({ asChild, children, onClick, ...props }: SubmitButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -12,6 +12,26 @@ interface KeletProviderProps {
12
12
  }
13
13
  export declare const KeletContext: React.Context<KeletContextValue | null>;
14
14
  export declare const useKelet: () => KeletContextValue;
15
+ /**
16
+ * Returns the signal-sending function from the nearest KeletProvider.
17
+ *
18
+ * Use this hook to send signals manually — e.g. tracking custom events,
19
+ * page views, or any interaction not covered by useFeedbackState.
20
+ *
21
+ * Safe to call outside a provider: returns a no-op and logs a warning.
22
+ *
23
+ * @example
24
+ * const sendSignal = useKeletSignal()
25
+ *
26
+ * sendSignal({
27
+ * session_id: 'my-session',
28
+ * kind: 'feedback',
29
+ * source: 'human',
30
+ * score: 1,
31
+ * trigger_name: 'copy_button_clicked',
32
+ * })
33
+ */
34
+ export declare const useKeletSignal: () => ((data: FeedbackData) => Promise<void>);
15
35
  export declare const useDefaultFeedbackHandler: () => ((data: FeedbackData) => Promise<void>);
16
36
  export declare const KeletProvider: React.FC<React.PropsWithChildren<KeletProviderProps>>;
17
37
  export {};
@@ -1,4 +1,4 @@
1
- import require$$0$1, { createContext, useEffect, useContext, useCallback, isValidElement, cloneElement, useState, useRef, useId } from "react";
1
+ import require$$0$1, { createContext, useEffect, useContext, useCallback, isValidElement, cloneElement, useRef, useState, 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
  }
@@ -443,7 +443,7 @@ const useKelet = () => {
443
443
  }
444
444
  return context;
445
445
  };
446
- const useDefaultFeedbackHandler = () => {
446
+ const useKeletSignal = () => {
447
447
  const context = useContext(KeletContext);
448
448
  if (!context) {
449
449
  console.warn(
@@ -455,6 +455,7 @@ const useDefaultFeedbackHandler = () => {
455
455
  return context.feedback;
456
456
  }
457
457
  };
458
+ const useDefaultFeedbackHandler = useKeletSignal;
458
459
  const KeletProvider = ({ apiKey, project, baseUrl, children }) => {
459
460
  useEffect(() => {
460
461
  initEventCapture();
@@ -574,6 +575,11 @@ const VoteFeedbackRoot = ({
574
575
  setVote(null);
575
576
  setTimeout(() => triggerRef.current?.focus(), 0);
576
577
  }, [session_id, defaultText]);
578
+ const closePopover = useCallback(() => {
579
+ setShowPopover(false);
580
+ setFeedbackText(defaultText);
581
+ setTimeout(() => triggerRef.current?.focus(), 0);
582
+ }, [defaultText]);
577
583
  const handleUpvote = useCallback(async () => {
578
584
  setVote("upvote");
579
585
  const data = {
@@ -672,9 +678,7 @@ const VoteFeedbackRoot = ({
672
678
  const handleKeyDown = useCallback(
673
679
  (e) => {
674
680
  if (e.key === "Escape") {
675
- setShowPopover(false);
676
- setFeedbackText(defaultText);
677
- triggerRef.current?.focus();
681
+ closePopover();
678
682
  } else if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
679
683
  e.preventDefault();
680
684
  handleSubmit().then((_) => {
@@ -697,7 +701,7 @@ const VoteFeedbackRoot = ({
697
701
  }
698
702
  }
699
703
  },
700
- [handleSubmit, showPopover, popoverId, defaultText]
704
+ [handleSubmit, showPopover, popoverId, closePopover]
701
705
  );
702
706
  const contextValue = {
703
707
  onFeedback: handler,
@@ -713,6 +717,7 @@ const VoteFeedbackRoot = ({
713
717
  handleTextareaChange,
714
718
  handleSubmit,
715
719
  handleKeyDown,
720
+ closePopover,
716
721
  textareaRef,
717
722
  triggerRef,
718
723
  popoverId,
@@ -802,7 +807,25 @@ const DownvoteButton = ({
802
807
  return /* @__PURE__ */ jsxRuntimeExports.jsx("button", { ...slotProps, children: typeof children === "function" ? children({ isSelected }) : children });
803
808
  };
804
809
  const Popover = ({ asChild, children, ...props }) => {
805
- const { showPopover, handleKeyDown, popoverId, triggerId } = useVoteFeedbackContext();
810
+ const {
811
+ showPopover,
812
+ closePopover,
813
+ handleKeyDown,
814
+ popoverId,
815
+ triggerId,
816
+ triggerRef
817
+ } = useVoteFeedbackContext();
818
+ const containerRef = useRef(null);
819
+ useEffect(() => {
820
+ if (!showPopover) return;
821
+ const handleMouseDown = (event) => {
822
+ if (containerRef.current && !containerRef.current.contains(event.target) && !triggerRef.current?.contains(event.target)) {
823
+ closePopover();
824
+ }
825
+ };
826
+ document.addEventListener("mousedown", handleMouseDown);
827
+ return () => document.removeEventListener("mousedown", handleMouseDown);
828
+ }, [showPopover, closePopover, triggerRef]);
806
829
  if (!showPopover) {
807
830
  return null;
808
831
  }
@@ -818,14 +841,14 @@ const Popover = ({ asChild, children, ...props }) => {
818
841
  };
819
842
  if (asChild && isValidElement(children)) {
820
843
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
821
- cloneElement(
822
- children,
823
- mergeProps(slotProps, children.props)
824
- ),
844
+ cloneElement(children, {
845
+ ...mergeProps(slotProps, children.props),
846
+ ref: containerRef
847
+ }),
825
848
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { id: `${popoverId}-description`, className: "sr-only", children: "Provide additional feedback for your downvote" })
826
849
  ] });
827
850
  }
828
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ...slotProps, children: [
851
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, ...slotProps, children: [
829
852
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { id: `${popoverId}-description`, className: "sr-only", children: "Provide additional feedback for your downvote" }),
830
853
  children
831
854
  ] });
@@ -2275,6 +2298,7 @@ export {
2275
2298
  VoteFeedback,
2276
2299
  useDefaultFeedbackHandler,
2277
2300
  useFeedbackState,
2278
- useKelet
2301
+ useKelet,
2302
+ useKeletSignal
2279
2303
  };
2280
2304
  //# sourceMappingURL=feedback-ui.es.js.map