
Security News
/Research
Wallet-Draining npm Package Impersonates Nodemailer to Hijack Crypto Transactions
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
@kelet-ai/feedback-ui
Advanced tools
A headless React component library for collecting explicit and implicit user feedback, with automatic state change tracking
A headless React component to collect feedback for product and AI features.
Perfect for capturing user reactions to AI-generated content, new features, documentation, and UI components with both explicit voting interfaces and implicit behavior tracking.
Get beautiful feedback components in 30 seconds with shadcn/ui integration:
npx shadcn add https://feedback-ui.kelet.ai/r/vote-feedback.json
import { ShadcnVoteFeedback } from '@/components/ui/vote-feedback';
function App() {
return (
<ShadcnVoteFeedback
tx_id="my-feature"
onFeedback={feedback => console.log(feedback)}
variant="outline"
/>
);
}
Result: Fully styled thumbs up/down buttons with popover feedback form.
First install shadcn/ui and then:
npx shadcn add https://feedback-ui.kelet.ai/r/vote-feedback.json
✅ Best for: Quick setup with beautiful pre-styled components
npm install @kelet-ai/feedback-ui
✅ Best for: Full control over styling and behavior
Users explicitly vote and provide comments:
import { VoteFeedback } from '@kelet-ai/feedback-ui';
<VoteFeedback.Root onFeedback={handleFeedback} tx_id="ai-response">
<VoteFeedback.UpvoteButton>👍 Helpful</VoteFeedback.UpvoteButton>
<VoteFeedback.DownvoteButton>👎 Not helpful</VoteFeedback.DownvoteButton>
<VoteFeedback.Popover>
<VoteFeedback.Textarea placeholder="How can we improve?" />
<VoteFeedback.SubmitButton>Send feedback</VoteFeedback.SubmitButton>
</VoteFeedback.Popover>
</VoteFeedback.Root>;
Capture user behavior automatically by using a drop-in replacement for useState:
import { useFeedbackState } from '@kelet-ai/feedback-ui';
function ContentEditor() {
// Drop-in useState replacement that tracks changes
const [content, setContent] = useFeedbackState(
'Start writing...',
'content-editor'
);
return (
<textarea
value={content}
onChange={e => setContent(e.target.value)}
placeholder="Edit this content..."
/>
);
// 🎯 Automatically sends feedback when user stops editing!
}
For advanced state management with automatic trigger tracking, by using a drop-in replacement for useReducer:
import { useFeedbackReducer } from '@kelet-ai/feedback-ui';
function TodoApp() {
const [todos, dispatch] = useFeedbackReducer(todoReducer, [], 'todo-app');
return (
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'New task' })}>
Add Todo
</button>
// 🎯 Automatically sends feedback with trigger_name: 'ADD_TODO'
);
}
Understanding these fundamental concepts will help you implement feedback collection effectively:
What: Unique tracking ID that connects feedback to its context
Purpose: Links feedback to specific sessions, users, or content pieces
Best Practice: Use traceable IDs from your logging system (session ID, trace ID, request ID)
// ✅ Good: Traceable tx_id
<VoteFeedback.Root tx_id="session-abc123-ai-response-456"/>
// ✅ Good: Content-based tx_id
<VoteFeedback.Root tx_id={`article-${articleId}-section-${sectionId}`}/>
// ❌ Poor: Generic tx_id
<VoteFeedback.Root tx_id="feedback"/>
Controls how feedback is collected and what data is captured:
// Explicit feedback - user clicks buttons
<VoteFeedback.Root onFeedback={handleExplicitFeedback}>
<VoteFeedback.UpvoteButton>👍 Helpful</VoteFeedback.UpvoteButton>
<VoteFeedback.DownvoteButton>👎 Not helpful</VoteFeedback.DownvoteButton>
</VoteFeedback.Root>
// Implicit feedback - tracks changes automatically
const [content, setContent] = useFeedbackState(
'Initial content',
'content-editor'
);
// Sends feedback when user stops editing
// Loading pattern - no noise generated
const [user, setUser] = useFeedbackState(null, 'user-data');
setUser(userData); // ❌ No feedback sent (ignoreInitialNullish: true)
setUser(updatedUser); // ✅ Feedback sent for real changes
Categorization system for grouping and analyzing feedback:
// Content creation triggers
'user_typing' | 'ai_generation' | 'spell_check' | 'auto_format';
// User interaction triggers
'manual_edit' | 'voice_input' | 'copy_paste' | 'drag_drop';
// Workflow triggers
'draft' | 'review' | 'approval' | 'publication';
// AI interaction triggers
'ai_completion' | 'ai_correction' | 'prompt_result';
const [document, setDocument] = useFeedbackState(
initialDoc,
'document-editor',
{ default_trigger_name: 'user_edit' }
);
// Different triggers for different actions
setDocument(aiGeneratedContent, 'ai_generation');
setDocument(userEditedContent, 'manual_refinement');
setDocument(spellCheckedContent, 'spell_check');
For implicit feedback, understanding how state changes are processed:
// User types: "Hello" → "Hello World" → "Hello World!"
// Only sends ONE feedback after user stops typing
const [text, setText] = useFeedbackState('', 'editor', {
debounceMs: 1500, // Wait 1.5s after last change
});
Three formats available for different use cases:
Format | Best For | Output |
---|---|---|
git | Code/text changes | Unified diff format |
object | Structured data | Deep object diff |
json | Simple before/after | JSON comparison |
Automatic classification of changes:
// Smart vote logic based on change magnitude
const [data, setData] = useFeedbackState(initial, 'tracker', {
vote: (before, after, diffPercentage) => {
// Small changes = refinement (positive)
if (diffPercentage <= 0.5) return 'upvote';
// Large changes = major revision (might indicate issues)
return 'downvote';
},
});
✅ Use traceable session/request IDs
✅ Include context in tx_id structure
✅ Keep tx_ids consistent across related actions
✅ Use explicit feedback for user opinions
✅ Use implicit feedback for behavior analysis
✅ Combine both for comprehensive insights
✅ Use consistent naming conventions
✅ Group related triggers with prefixes
✅ Document trigger meanings for your team
✅ Handle feedback data asynchronously
✅ Include relevant metadata for context
✅ Test both explicit and implicit flows
Automatically extract trace IDs to correlate feedback with distributed traces:
import { VoteFeedback, getOtelTraceId } from '@kelet-ai/feedback-ui';
<VoteFeedback.Root tx_id={getOtelTraceId} onFeedback={handleFeedback}>
<VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
<VoteFeedback.DownvoteButton>👎</VoteFeedback.DownvoteButton>
</VoteFeedback.Root>;
Requires @opentelemetry/api
and active Span to collect the trace_id from.
Main container component that manages feedback state.
<VoteFeedback.Root
tx_id="unique-id" // Required: Unique tracking ID
onFeedback={handleFeedback} // Required: Callback function
trigger_name="user_feedback" // Optional: Categorization
extra_metadata={{ page: 'home' }} // Optional: Additional data
>
{/* Child components */}
</VoteFeedback.Root>
Interactive voting buttons with built-in state management.
<VoteFeedback.UpvoteButton className="your-styles">
👍 Like
</VoteFeedback.UpvoteButton>
Context-aware feedback form that appears after voting.
<VoteFeedback.Popover className="your-popover-styles">
<VoteFeedback.Textarea placeholder="Tell us more..." />
<VoteFeedback.SubmitButton>Send</VoteFeedback.SubmitButton>
</VoteFeedback.Popover>
A drop-in replacement for React's useState that automatically tracks state changes.
const [count, setCount] = useFeedbackState(0, 'counter-widget');
const [profile, setProfile] = useFeedbackState(
{ name: '', email: '' },
state => `profile-${state.email}`, // Dynamic tx_id
{
debounceMs: 2000, // Wait time before sending feedback
diffType: 'object', // Format: 'git' | 'object' | 'json'
metadata: { component: 'UserProfile' },
vote: 'upvote', // Static vote or custom function
}
);
const [content, setContent] = useFeedbackState(
'Initial content',
'content-editor',
{ default_trigger_name: 'manual_edit' }
);
// Uses default trigger
setContent('User typed this');
// Override with specific trigger
setContent('AI generated this', 'ai_assistance');
setContent('Spell checker fixed this', 'spell_check');
A drop-in replacement for React's useReducer with automatic trigger name extraction from action types.
const [state, dispatch] = useFeedbackReducer(
counterReducer,
{ count: 0 },
'counter-widget'
);
dispatch({ type: 'increment' }); // trigger_name: 'increment'
dispatch({ type: 'reset' }); // trigger_name: 'reset'
dispatch({ type: 'custom' }, 'override'); // Custom trigger name
<VoteFeedback.Root onFeedback={feedback => console.log(feedback)}>
<VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
<VoteFeedback.DownvoteButton>👎</VoteFeedback.DownvoteButton>
</VoteFeedback.Root>
<VoteFeedback.Root onFeedback={handleFeedback}>
<VoteFeedback.UpvoteButton className="btn btn-success">
Like
</VoteFeedback.UpvoteButton>
<VoteFeedback.DownvoteButton className="btn btn-danger">
Dislike
</VoteFeedback.DownvoteButton>
<VoteFeedback.Popover className="popover">
<VoteFeedback.Textarea className="textarea" />
<VoteFeedback.SubmitButton className="btn btn-primary">
Submit
</VoteFeedback.SubmitButton>
</VoteFeedback.Popover>
</VoteFeedback.Root>
<VoteFeedback.UpvoteButton asChild>
<button className="custom-button">
<Icon name="thumbs-up" />
Like
</button>
</VoteFeedback.UpvoteButton>
<VoteFeedback.Root
tx_id="ai-response-123"
onFeedback={handleFeedback}
trigger_name="ai_evaluation"
extra_metadata={{
model: 'gpt-4',
prompt_length: 150,
response_time: 1200,
}}
>
<VoteFeedback.UpvoteButton>👍 Helpful</VoteFeedback.UpvoteButton>
<VoteFeedback.DownvoteButton>👎 Not helpful</VoteFeedback.DownvoteButton>
<VoteFeedback.Popover>
<VoteFeedback.Textarea placeholder="How can we improve this response?" />
<VoteFeedback.SubmitButton>Send feedback</VoteFeedback.SubmitButton>
</VoteFeedback.Popover>
</VoteFeedback.Root>
interface FeedbackData {
tx_id: string; // Unique tracking ID
vote: 'upvote' | 'downvote'; // User's vote
explanation?: string; // Optional user comment
extra_metadata?: Record<string, any>; // Additional context data
source?: 'IMPLICIT' | 'EXPLICIT'; // How feedback was collected
correction?: string; // For implicit feedback diffs
selection?: string; // Selected text context
trigger_name?: string; // Categorization tag
}
Prop | Type | Required | Description |
---|---|---|---|
tx_id | string | ✅ | Unique transaction ID for tracking |
onFeedback | (data: FeedbackData) => void | ✅ | Callback when feedback is submitted |
trigger_name | string | ❌ | Optional categorization tag |
extra_metadata | object | ❌ | Additional context data |
Option | Type | Default | Description |
---|---|---|---|
debounceMs | number | 1500 | Debounce time in milliseconds |
diffType | 'git' | 'object' | 'json' | 'git' | Diff output format |
compareWith | (a: T, b: T) => boolean | undefined | Custom equality function |
metadata | Record<string, any> | {} | Additional metadata |
vote | 'upvote' | 'downvote' | function | auto | Vote determination logic |
default_trigger_name | string | 'auto_state_change' | Default trigger name |
ignoreInitialNullish | boolean | true | Skip null/undefined → data transitions |
✅ Full keyboard navigation support
✅ ARIA labels and roles
✅ Focus management
✅ Screen reader compatible
✅ High contrast support
✅ Reduced motion respect
Tab
/ Shift+Tab
- Navigate between elementsEnter
/ Space
- Activate buttonsEscape
- Close popoverArrow Keys
- Navigate within popoverbun install # Install dependencies
bun dev # Start Storybook development server
bun run checks # Run all quality checks
bun run test:unit # Run unit tests
bun run test:storybook # Run Storybook interaction tests
bun run test:storybook:coverage # Run with coverage
bun build # Build library for production
bun run typecheck # TypeScript type checking
bun run lint # ESLint code quality checks
bun run prettier # Code formatting
bun run checks # Run all quality checks (lint, format, typecheck, tests)
// ❌ Missing required props
<VoteFeedback.Root>
<VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
</VoteFeedback.Root>
// ✅ Include required tx_id and onFeedback
<VoteFeedback.Root
tx_id="my-feature"
onFeedback={handleFeedback}
>
<VoteFeedback.UpvoteButton>👍</VoteFeedback.UpvoteButton>
</VoteFeedback.Root>
// ✅ Use asChild for custom components
<VoteFeedback.UpvoteButton asChild>
<MyCustomButton>Like</MyCustomButton>
</VoteFeedback.UpvoteButton>
// ✅ Ensure debounce time has passed(the time, not the configuration! :P) and state actually changed
const [value, setValue] = useFeedbackState('initial', 'test', {
debounceMs: 1000, // Wait 1 second after last change
});
// ✅ Components are unstyled by default - add your own CSS or use the Shadcn UI library
<VoteFeedback.UpvoteButton className="bg-green-500 text-white p-2">
👍 Like
</VoteFeedback.UpvoteButton>
✅ Use unique tx_ids for each feedback instance - the tx_id should be traceable back to the session's log
and allow us to understand the context of the feedback.
✅ Handle feedback data asynchronously in your callback
✅ Test keyboard navigation in your implementation
✅ Provide meaningful trigger names for categorization
✅ Include relevant metadata for context
❌ Don't use the same tx_id for multiple components. A tx_id should be traced back to the session's log - allows us to understand the context of the feedback.
📖 Full Documentation: https://feedback-ui.kelet.ai/
🎮 Interactive Examples: Storybook Documentation
🐛 Report Issues: GitHub Issues
💬 Discussions: GitHub Discussions
MIT License - See LICENSE file for details.
Built with ❤️ by the Kelet AI team
FAQs
A headless React component library for collecting explicit and implicit user feedback, with automatic state change tracking
We found that @kelet-ai/feedback-ui demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
/Research
Malicious npm package impersonates Nodemailer and drains wallets by hijacking crypto transactions across multiple blockchains.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.