React SDK
Package: @kelet-ai/feedback-ui | Version: 1.1.3 | Requires: React ≥19
Install
Section titled “Install”npm install @kelet-ai/feedback-uiKey types
Section titled “Key types”Two API keys — never mix them:
- Secret key (
KELET_API_KEY): server-only. Used in SDK calls. - Publishable key: frontend-safe. Used in
KeletProvider.
VITE_KELET_PUBLISHABLE_KEY=pk_... # ViteNEXT_PUBLIC_KELET_PUBLISHABLE_KEY=pk_... # Next.jsGet both from Settings → API Keys in the console.
KeletProvider
Section titled “KeletProvider”Wrap your app root.
import { KeletProvider } from '@kelet-ai/feedback-ui';
function App() { return ( <KeletProvider apiKey={import.meta.env.VITE_KELET_PUBLISHABLE_KEY} project="my-agent"> <YourApp /> </KeletProvider> );}| Prop | Type | Description |
|---|---|---|
apiKey | string | Required. Publishable key (not secret key) |
project | string | Required. Kelet project name |
baseUrl | string | Optional. Override API URL |
Multi-project apps: nest a second KeletProvider with only project — it inherits apiKey from the outer
provider.
<KeletProvider apiKey="pk_..." project="agent-a"> <AgentAFeature /> <KeletProvider project="agent-b"> <AgentBFeature /> </KeletProvider></KeletProvider>VoteFeedback
Section titled “VoteFeedback”Headless compound component for thumbs up/down with optional text feedback.
import { VoteFeedback } from '@kelet-ai/feedback-ui';
function AgentResponse({ sessionId, content }) { return ( <div> <p>{content}</p> <VoteFeedback.Root session_id={sessionId} trigger_name="user-vote"> <VoteFeedback.UpvoteButton> {({ isSelected }) => <button>{isSelected ? 'Liked' : 'Good'}</button>} </VoteFeedback.UpvoteButton> <VoteFeedback.DownvoteButton> {({ isSelected }) => <button>{isSelected ? 'Disliked' : 'Bad'}</button>} </VoteFeedback.DownvoteButton> <VoteFeedback.Popover> <VoteFeedback.Textarea placeholder="What went wrong?" /> <VoteFeedback.SubmitButton>Submit</VoteFeedback.SubmitButton> </VoteFeedback.Popover> </VoteFeedback.Root> </div> );}VoteFeedback.Root props
Section titled “VoteFeedback.Root props”| Prop | Type | Description |
|---|---|---|
session_id | string | () => string | Required. Must match the server agentic_session() session ID |
trigger_name | string | Optional. Signal category label, e.g., user-vote |
trace_id | string | Optional. Attach feedback to a specific trace |
metadata | object | Optional. Extra context |
onFeedback | (data) => void | Optional. Override the default feedback handler |
session_id must exactly match what the server used in agentic_session(). If they differ, feedback is captured but
silently unlinked from the trace.
Propagating session ID end-to-end
Section titled “Propagating session ID end-to-end”client generates UUID → sends in request body → server: agentic_session(session_id=uuid) → server returns X-Session-ID response header → client reads header → passes to VoteFeedback.Root session_iduseFeedbackState
Section titled “useFeedbackState”Drop-in for useState. Tracks every state change as an edit signal — automatically distinguishes AI-generated content
from user edits.
import { useFeedbackState } from '@kelet-ai/feedback-ui';
function EditableResponse({ sessionId, aiResponse }) { const [text, setText] = useFeedbackState(aiResponse, sessionId);
return ( <textarea value={text} onChange={(e) => setText(e.target.value, 'user-edit')} /> );}Pass a trigger_name as the second argument to setState:
- When setting AI-generated content:
setText(aiOutput, 'ai-generation') - When the user edits:
setText(userValue, 'user-edit')
Without trigger names, Kelet can’t distinguish “user accepted AI output” from “user corrected it.”
useFeedbackReducer
Section titled “useFeedbackReducer”Drop-in for useReducer. Action type fields become trigger names automatically.
import { useFeedbackReducer } from '@kelet-ai/feedback-ui';
function reducer(state, action) { switch (action.type) { case 'accept': return { ...state, accepted: true }; case 'edit': return { ...state, content: action.payload }; default: return state; }}
function Component({ sessionId }) { const [state, dispatch] = useFeedbackReducer(reducer, initialState, sessionId);
return ( <button onClick={() => dispatch({ type: 'accept' })}>Accept</button> );}Each action.type becomes the trigger_name in the signal automatically.
Which hook to use
Section titled “Which hook to use”| Use case | Hook |
|---|---|
| Thumbs up/down rating | VoteFeedback |
| Editable AI-generated text | useFeedbackState with trigger names |
| Complex reducer-based state | useFeedbackReducer |