@david-xpn/llm-ui-feedback
v0.1.0
Published
Drop-in React feedback widget that captures component context for LLM consumption
Maintainers
Readme
@david-xpn/llm-ui-feedback
Drop-in React feedback widget that captures component context for LLM consumption.
Users click on any element in your app, leave a comment, and the widget captures a rich context bundle — React component tree, props, screenshot with marker, and a pre-formatted LLM prompt — ready to paste into Claude, ChatGPT, or any LLM chat.
Features
- One-line integration — add
<FeedbackWidget />to your root, done - React component tree capture — walks the fiber tree to extract component names and props
- Screenshot with X marker — captures the viewport and marks the clicked element
- LLM-ready prompt — generates a structured prompt with page URL, component path, props, element text, and user comment
- Copy & download — copy the prompt to clipboard and download the screenshot
- localStorage persistence — no backend needed, all data stays client-side
- Feedback list panel — view, copy, and manage all saved feedback entries
- Configurable — position, color, and keyboard shortcut
Install
npm install @david-xpn/llm-ui-feedbackQuick Start
import { FeedbackWidget } from '@david-xpn/llm-ui-feedback';
function App() {
return (
<>
<YourApp />
<FeedbackWidget />
</>
);
}That's it. A floating + button appears in the bottom-right corner. Click it to enter pick mode, click any element, leave a comment, and get an LLM-ready prompt.
Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| position | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' | 'bottom-right' | Corner position for the floating button |
| buttonColor | string | '#3b82f6' | Hex color for the FAB button |
| hotkey | string | undefined | Keyboard shortcut to toggle pick mode (e.g. 'Alt+F') |
Example with all props
<FeedbackWidget
position="bottom-left"
buttonColor="#10b981"
hotkey="Alt+F"
/>How It Works
- User clicks the
+floating button to enter pick mode - User clicks any element on the page
- The widget captures:
- Component path — React fiber tree walk (e.g.
App > Layout > Dashboard > MetricCard) - Props — scalar props (strings, numbers, booleans) from each component
- Element text — innerText of the clicked element (truncated to 100 chars)
- Page URL — current
window.location.href - Screenshot — viewport capture via
html2canvas-prowith a red X at the click position
- Component path — React fiber tree walk (e.g.
- User types a comment and saves
- The entry is stored in
localStoragewith a pre-formatted LLM prompt - User can copy the prompt and download the screenshot to paste into any LLM
Example prompt output
Page: https://myapp.com/dashboard/metrics
Component: App > Layout > Dashboard > MetricCard
Props:
MetricCard: {"metricId":"revenue","period":"Q4"}
Element text: "Revenue: $1.2M"
User feedback: "This number looks wrong, should be $1.4M"
Screenshot attached with red X marking the clicked element.Exported API
// Components
import { FeedbackWidget } from '@david-xpn/llm-ui-feedback';
// Types
import type {
FeedbackWidgetProps,
FeedbackEntry,
CapturedContext,
ComponentInfo,
WidgetPosition,
} from '@david-xpn/llm-ui-feedback';
// Storage utilities (for programmatic access)
import {
loadEntries,
deleteEntry,
clearEntries,
} from '@david-xpn/llm-ui-feedback';Storage utilities
| Function | Description |
| --- | --- |
| loadEntries() | Returns all saved FeedbackEntry[] from localStorage |
| deleteEntry(id) | Deletes a single entry by ID |
| clearEntries() | Removes all feedback entries |
Production Setup
In production builds, minifiers rename component functions (MetricCard becomes t), which makes the component path useless. Configure your bundler to preserve function names:
Vite
// vite.config.ts
export default defineConfig({
esbuild: {
keepNames: true,
},
});webpack (Terser)
// webpack.config.js
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: { keep_fnames: true },
}),
],
},
};Next.js
// next.config.js
module.exports = {
webpack: (config) => {
const terser = config.optimization?.minimizer?.find(
(p) => p.constructor.name === 'TerserPlugin'
);
if (terser) {
terser.options.terserOptions = {
...terser.options.terserOptions,
keep_fnames: true,
};
}
return config;
},
};See docs/production-setup.md for more bundler configurations.
Requirements
- React 18+
- Browser environment (uses
localStorage,document,html2canvas-pro)
License
MIT
