@david-xpn/llm-ui-feedback 0.1.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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +992 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +952 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
FeedbackWidget: () => FeedbackWidget,
|
|
34
|
+
clearEntries: () => clearEntries,
|
|
35
|
+
deleteEntry: () => deleteEntry,
|
|
36
|
+
loadEntries: () => loadEntries
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/components/FeedbackWidget.tsx
|
|
41
|
+
var import_react4 = require("react");
|
|
42
|
+
var import_react_dom = require("react-dom");
|
|
43
|
+
|
|
44
|
+
// src/utils/color.ts
|
|
45
|
+
function getContrastColor(hexColor) {
|
|
46
|
+
const hex = hexColor.replace("#", "");
|
|
47
|
+
const fullHex = hex.length === 3 ? hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] : hex;
|
|
48
|
+
const r = parseInt(fullHex.substring(0, 2), 16) / 255;
|
|
49
|
+
const g = parseInt(fullHex.substring(2, 4), 16) / 255;
|
|
50
|
+
const b = parseInt(fullHex.substring(4, 6), 16) / 255;
|
|
51
|
+
const toLinear = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
52
|
+
const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
53
|
+
return luminance > 0.179 ? "#000000" : "#ffffff";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/components/FloatingButton.tsx
|
|
57
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
58
|
+
function FloatingButton({ onClick, active, position, buttonColor }) {
|
|
59
|
+
const isBottom = position.includes("bottom");
|
|
60
|
+
const isRight = position.includes("right");
|
|
61
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
62
|
+
"button",
|
|
63
|
+
{
|
|
64
|
+
onClick,
|
|
65
|
+
"aria-label": active ? "Cancel feedback" : "Add feedback",
|
|
66
|
+
style: {
|
|
67
|
+
position: "fixed",
|
|
68
|
+
[isBottom ? "bottom" : "top"]: 24,
|
|
69
|
+
[isRight ? "right" : "left"]: 24,
|
|
70
|
+
zIndex: 99999,
|
|
71
|
+
width: 48,
|
|
72
|
+
height: 48,
|
|
73
|
+
borderRadius: "50%",
|
|
74
|
+
border: "none",
|
|
75
|
+
background: active ? "#ef4444" : buttonColor,
|
|
76
|
+
color: active ? "#fff" : getContrastColor(buttonColor),
|
|
77
|
+
fontSize: 24,
|
|
78
|
+
cursor: "pointer",
|
|
79
|
+
display: "flex",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
justifyContent: "center",
|
|
82
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
|
|
83
|
+
transition: "background 0.15s, transform 0.15s",
|
|
84
|
+
transform: active ? "rotate(45deg)" : "none"
|
|
85
|
+
},
|
|
86
|
+
children: "+"
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/components/PickOverlay.tsx
|
|
92
|
+
var import_react = require("react");
|
|
93
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
94
|
+
var WIDGET_CONTAINER_ID = "llm-ui-feedback-root";
|
|
95
|
+
function getElementBeneath(x, y) {
|
|
96
|
+
const container = document.getElementById(WIDGET_CONTAINER_ID);
|
|
97
|
+
if (container) container.style.display = "none";
|
|
98
|
+
const el = document.elementFromPoint(x, y);
|
|
99
|
+
if (container) container.style.display = "";
|
|
100
|
+
if (el && container?.contains(el)) return null;
|
|
101
|
+
return el;
|
|
102
|
+
}
|
|
103
|
+
function PickOverlay({ onPick, onCancel }) {
|
|
104
|
+
const lastOutlinedRef = (0, import_react.useRef)(null);
|
|
105
|
+
const clearOutline = (0, import_react.useCallback)(() => {
|
|
106
|
+
if (lastOutlinedRef.current) {
|
|
107
|
+
lastOutlinedRef.current.style.outline = "";
|
|
108
|
+
lastOutlinedRef.current = null;
|
|
109
|
+
}
|
|
110
|
+
}, []);
|
|
111
|
+
const handleMouseMove = (0, import_react.useCallback)((e) => {
|
|
112
|
+
const el = getElementBeneath(e.clientX, e.clientY);
|
|
113
|
+
clearOutline();
|
|
114
|
+
if (el && el instanceof HTMLElement) {
|
|
115
|
+
el.style.outline = "2px solid #3b82f6";
|
|
116
|
+
lastOutlinedRef.current = el;
|
|
117
|
+
}
|
|
118
|
+
}, [clearOutline]);
|
|
119
|
+
const handleClick = (0, import_react.useCallback)(
|
|
120
|
+
(e) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
clearOutline();
|
|
124
|
+
const el = getElementBeneath(e.clientX, e.clientY);
|
|
125
|
+
if (el) {
|
|
126
|
+
onPick(el, e.clientX, e.clientY);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[onPick, clearOutline]
|
|
130
|
+
);
|
|
131
|
+
const handleKeyDown = (0, import_react.useCallback)(
|
|
132
|
+
(e) => {
|
|
133
|
+
if (e.key === "Escape") {
|
|
134
|
+
clearOutline();
|
|
135
|
+
onCancel();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
[onCancel, clearOutline]
|
|
139
|
+
);
|
|
140
|
+
(0, import_react.useEffect)(() => {
|
|
141
|
+
document.addEventListener("mousemove", handleMouseMove, true);
|
|
142
|
+
document.addEventListener("click", handleClick, true);
|
|
143
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
144
|
+
return () => {
|
|
145
|
+
document.removeEventListener("mousemove", handleMouseMove, true);
|
|
146
|
+
document.removeEventListener("click", handleClick, true);
|
|
147
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
148
|
+
if (lastOutlinedRef.current) {
|
|
149
|
+
lastOutlinedRef.current.style.outline = "";
|
|
150
|
+
lastOutlinedRef.current = null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}, [handleMouseMove, handleClick, handleKeyDown]);
|
|
154
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
155
|
+
"div",
|
|
156
|
+
{
|
|
157
|
+
style: {
|
|
158
|
+
position: "fixed",
|
|
159
|
+
top: 0,
|
|
160
|
+
left: 0,
|
|
161
|
+
right: 0,
|
|
162
|
+
zIndex: 99998,
|
|
163
|
+
background: "rgba(59, 130, 246, 0.08)",
|
|
164
|
+
padding: "8px 16px",
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
fontSize: 14,
|
|
167
|
+
color: "#1e40af",
|
|
168
|
+
fontFamily: "system-ui, sans-serif",
|
|
169
|
+
pointerEvents: "none"
|
|
170
|
+
},
|
|
171
|
+
children: [
|
|
172
|
+
"Click on any element to leave feedback. Press ",
|
|
173
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: "Esc" }),
|
|
174
|
+
" to cancel."
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/components/FeedbackDialog.tsx
|
|
181
|
+
var import_react2 = require("react");
|
|
182
|
+
|
|
183
|
+
// src/utils/prompt.ts
|
|
184
|
+
function buildPrompt(context, comment) {
|
|
185
|
+
const propsStr = context.components.filter((c) => Object.keys(c.props).length > 0).map((c) => ` ${c.name}: ${JSON.stringify(c.props)}`).join("\n");
|
|
186
|
+
const lines = [
|
|
187
|
+
`Page: ${context.url}`,
|
|
188
|
+
`Component: ${context.componentPath}`
|
|
189
|
+
];
|
|
190
|
+
if (propsStr) {
|
|
191
|
+
lines.push(`Props:
|
|
192
|
+
${propsStr}`);
|
|
193
|
+
}
|
|
194
|
+
if (context.elementText) {
|
|
195
|
+
lines.push(`Element text: "${context.elementText}"`);
|
|
196
|
+
}
|
|
197
|
+
lines.push("", `User feedback: "${comment}"`, "", "Screenshot attached with red X marking the clicked element.");
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/utils/storage.ts
|
|
202
|
+
var STORAGE_KEY = "llm_ui_feedback_items";
|
|
203
|
+
function loadEntries() {
|
|
204
|
+
try {
|
|
205
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
206
|
+
return raw ? JSON.parse(raw) : [];
|
|
207
|
+
} catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function saveEntry(entry) {
|
|
212
|
+
const entries = loadEntries();
|
|
213
|
+
entries.push(entry);
|
|
214
|
+
try {
|
|
215
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
216
|
+
return { ok: true };
|
|
217
|
+
} catch (e) {
|
|
218
|
+
if (e instanceof DOMException && (e.name === "QuotaExceededError" || e.code === 22)) {
|
|
219
|
+
const oldestWithScreenshot = entries.findIndex((ent) => ent.screenshot !== null);
|
|
220
|
+
if (oldestWithScreenshot >= 0) {
|
|
221
|
+
entries[oldestWithScreenshot] = { ...entries[oldestWithScreenshot], screenshot: null };
|
|
222
|
+
try {
|
|
223
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
224
|
+
return { ok: true };
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: false, error: "Storage is full. Delete old feedback entries to free space." };
|
|
229
|
+
}
|
|
230
|
+
return { ok: false, error: "Failed to save feedback entry." };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function deleteEntry(id) {
|
|
234
|
+
const entries = loadEntries().filter((e) => e.id !== id);
|
|
235
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
|
236
|
+
}
|
|
237
|
+
function clearEntries() {
|
|
238
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/components/FeedbackDialog.tsx
|
|
242
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
243
|
+
async function copyToClipboard(text) {
|
|
244
|
+
if (navigator.clipboard?.writeText) {
|
|
245
|
+
try {
|
|
246
|
+
await navigator.clipboard.writeText(text);
|
|
247
|
+
return "success";
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const textarea = document.createElement("textarea");
|
|
253
|
+
textarea.value = text;
|
|
254
|
+
textarea.style.cssText = "position:fixed;left:-9999px;top:-9999px";
|
|
255
|
+
document.body.appendChild(textarea);
|
|
256
|
+
textarea.focus();
|
|
257
|
+
textarea.select();
|
|
258
|
+
const ok = document.execCommand("copy");
|
|
259
|
+
document.body.removeChild(textarea);
|
|
260
|
+
if (ok) return "fallback-success";
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
return "failed";
|
|
264
|
+
}
|
|
265
|
+
function FeedbackDialog({ context, screenshot, onClose }) {
|
|
266
|
+
const [comment, setComment] = (0, import_react2.useState)("");
|
|
267
|
+
const [saved, setSaved] = (0, import_react2.useState)(false);
|
|
268
|
+
const [copyStatus, setCopyStatus] = (0, import_react2.useState)("idle");
|
|
269
|
+
const [saveError, setSaveError] = (0, import_react2.useState)(null);
|
|
270
|
+
const [showPromptText, setShowPromptText] = (0, import_react2.useState)(false);
|
|
271
|
+
const handleSubmit = () => {
|
|
272
|
+
if (!comment.trim()) return;
|
|
273
|
+
const prompt = buildPrompt(context, comment);
|
|
274
|
+
const entry = {
|
|
275
|
+
id: `f_${Date.now()}`,
|
|
276
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
277
|
+
url: context.url,
|
|
278
|
+
componentPath: context.componentPath,
|
|
279
|
+
components: context.components,
|
|
280
|
+
elementText: context.elementText,
|
|
281
|
+
comment,
|
|
282
|
+
screenshot,
|
|
283
|
+
prompt
|
|
284
|
+
};
|
|
285
|
+
const result = saveEntry(entry);
|
|
286
|
+
if (result.ok) {
|
|
287
|
+
setSaved(true);
|
|
288
|
+
setSaveError(null);
|
|
289
|
+
} else {
|
|
290
|
+
setSaveError(result.error || "Failed to save.");
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const handleCopyPrompt = async () => {
|
|
294
|
+
const prompt = buildPrompt(context, comment || "(no comment)");
|
|
295
|
+
const result = await copyToClipboard(prompt);
|
|
296
|
+
if (result === "failed") {
|
|
297
|
+
setCopyStatus("failed");
|
|
298
|
+
setShowPromptText(true);
|
|
299
|
+
} else {
|
|
300
|
+
setCopyStatus("copied");
|
|
301
|
+
setTimeout(() => setCopyStatus("idle"), 2e3);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const handleDownloadScreenshot = () => {
|
|
305
|
+
if (!screenshot) return;
|
|
306
|
+
const link = document.createElement("a");
|
|
307
|
+
link.download = `feedback_${Date.now()}.png`;
|
|
308
|
+
link.href = screenshot;
|
|
309
|
+
link.click();
|
|
310
|
+
};
|
|
311
|
+
const copyButtonLabel = copyStatus === "copied" ? "Copied!" : copyStatus === "failed" ? "Copy failed" : "Copy Prompt";
|
|
312
|
+
const copyButtonBg = copyStatus === "copied" ? "#dcfce7" : copyStatus === "failed" ? "#fee2e2" : "#f3f4f6";
|
|
313
|
+
const copyButtonColor = copyStatus === "copied" ? "#16a34a" : copyStatus === "failed" ? "#dc2626" : "#374151";
|
|
314
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
315
|
+
"div",
|
|
316
|
+
{
|
|
317
|
+
style: {
|
|
318
|
+
position: "fixed",
|
|
319
|
+
inset: 0,
|
|
320
|
+
zIndex: 99999,
|
|
321
|
+
display: "flex",
|
|
322
|
+
alignItems: "center",
|
|
323
|
+
justifyContent: "center",
|
|
324
|
+
background: "rgba(0,0,0,0.4)",
|
|
325
|
+
fontFamily: "system-ui, sans-serif"
|
|
326
|
+
},
|
|
327
|
+
onClick: (e) => {
|
|
328
|
+
if (e.target === e.currentTarget) onClose();
|
|
329
|
+
},
|
|
330
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
331
|
+
"div",
|
|
332
|
+
{
|
|
333
|
+
style: {
|
|
334
|
+
background: "#fff",
|
|
335
|
+
borderRadius: 12,
|
|
336
|
+
padding: 24,
|
|
337
|
+
width: 480,
|
|
338
|
+
maxWidth: "90vw",
|
|
339
|
+
maxHeight: "80vh",
|
|
340
|
+
overflow: "auto",
|
|
341
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.2)"
|
|
342
|
+
},
|
|
343
|
+
children: [
|
|
344
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }, children: [
|
|
345
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: { margin: 0, fontSize: 16 }, children: "Leave Feedback" }),
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
347
|
+
"button",
|
|
348
|
+
{
|
|
349
|
+
onClick: onClose,
|
|
350
|
+
style: {
|
|
351
|
+
background: "none",
|
|
352
|
+
border: "none",
|
|
353
|
+
fontSize: 20,
|
|
354
|
+
cursor: "pointer",
|
|
355
|
+
color: "#666"
|
|
356
|
+
},
|
|
357
|
+
children: "x"
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 12, color: "#666", marginBottom: 12 }, children: [
|
|
362
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
363
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Page:" }),
|
|
364
|
+
" ",
|
|
365
|
+
context.url
|
|
366
|
+
] }),
|
|
367
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Component:" }),
|
|
369
|
+
" ",
|
|
370
|
+
context.componentPath
|
|
371
|
+
] }),
|
|
372
|
+
context.elementText && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
373
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Element:" }),
|
|
374
|
+
' "',
|
|
375
|
+
context.elementText.slice(0, 100),
|
|
376
|
+
'"'
|
|
377
|
+
] })
|
|
378
|
+
] }),
|
|
379
|
+
screenshot && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
380
|
+
"img",
|
|
381
|
+
{
|
|
382
|
+
src: screenshot,
|
|
383
|
+
alt: "Screenshot",
|
|
384
|
+
style: {
|
|
385
|
+
width: "100%",
|
|
386
|
+
borderRadius: 8,
|
|
387
|
+
border: "1px solid #e5e7eb",
|
|
388
|
+
marginBottom: 12
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
),
|
|
392
|
+
!saved ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
393
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
394
|
+
"textarea",
|
|
395
|
+
{
|
|
396
|
+
value: comment,
|
|
397
|
+
onChange: (e) => setComment(e.target.value),
|
|
398
|
+
placeholder: "Describe the issue or suggestion...",
|
|
399
|
+
autoFocus: true,
|
|
400
|
+
style: {
|
|
401
|
+
width: "100%",
|
|
402
|
+
minHeight: 80,
|
|
403
|
+
padding: 10,
|
|
404
|
+
borderRadius: 8,
|
|
405
|
+
border: "1px solid #d1d5db",
|
|
406
|
+
fontSize: 14,
|
|
407
|
+
resize: "vertical",
|
|
408
|
+
boxSizing: "border-box"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
),
|
|
412
|
+
saveError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#dc2626", fontSize: 13, marginTop: 8 }, children: saveError }),
|
|
413
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 12 }, children: [
|
|
414
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleSubmit, style: btnStyle("#3b82f6", "#fff"), children: "Save" }),
|
|
415
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleCopyPrompt, style: btnStyle(copyButtonBg, copyButtonColor), children: copyButtonLabel }),
|
|
416
|
+
screenshot && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleDownloadScreenshot, style: btnStyle("#f3f4f6", "#374151"), children: "Download Screenshot" })
|
|
417
|
+
] })
|
|
418
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { textAlign: "center", padding: 16, color: "#16a34a" }, children: [
|
|
419
|
+
"Saved to localStorage.",
|
|
420
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }, children: [
|
|
421
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleCopyPrompt, style: btnStyle(copyButtonBg, copyButtonColor), children: copyButtonLabel }),
|
|
422
|
+
screenshot && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleDownloadScreenshot, style: btnStyle("#f3f4f6", "#374151"), children: "Download Screenshot" }),
|
|
423
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: onClose, style: btnStyle("#3b82f6", "#fff"), children: "Done" })
|
|
424
|
+
] })
|
|
425
|
+
] }),
|
|
426
|
+
showPromptText && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 12 }, children: [
|
|
427
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, color: "#666", marginBottom: 4 }, children: "Could not copy automatically. Select and copy manually:" }),
|
|
428
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
429
|
+
"textarea",
|
|
430
|
+
{
|
|
431
|
+
readOnly: true,
|
|
432
|
+
value: buildPrompt(context, comment || "(no comment)"),
|
|
433
|
+
style: {
|
|
434
|
+
width: "100%",
|
|
435
|
+
minHeight: 120,
|
|
436
|
+
padding: 10,
|
|
437
|
+
borderRadius: 8,
|
|
438
|
+
border: "1px solid #d1d5db",
|
|
439
|
+
fontSize: 12,
|
|
440
|
+
fontFamily: "monospace",
|
|
441
|
+
resize: "vertical",
|
|
442
|
+
boxSizing: "border-box",
|
|
443
|
+
background: "#f9fafb"
|
|
444
|
+
},
|
|
445
|
+
onClick: (e) => e.target.select()
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
] })
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
function btnStyle(bg, color) {
|
|
456
|
+
return {
|
|
457
|
+
padding: "8px 16px",
|
|
458
|
+
borderRadius: 6,
|
|
459
|
+
border: "1px solid #d1d5db",
|
|
460
|
+
background: bg,
|
|
461
|
+
color,
|
|
462
|
+
fontSize: 13,
|
|
463
|
+
cursor: "pointer"
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/components/FeedbackListPanel.tsx
|
|
468
|
+
var import_react3 = require("react");
|
|
469
|
+
|
|
470
|
+
// src/utils/time.ts
|
|
471
|
+
function relativeTime(iso) {
|
|
472
|
+
const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1e3);
|
|
473
|
+
if (seconds < 0) return "just now";
|
|
474
|
+
if (seconds < 60) return "just now";
|
|
475
|
+
const minutes = Math.floor(seconds / 60);
|
|
476
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
477
|
+
const hours = Math.floor(minutes / 60);
|
|
478
|
+
if (hours < 24) return `${hours}h ago`;
|
|
479
|
+
const days = Math.floor(hours / 24);
|
|
480
|
+
if (days < 30) return `${days}d ago`;
|
|
481
|
+
return new Date(iso).toLocaleDateString();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/components/EntryCard.tsx
|
|
485
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
486
|
+
function EntryCard({ entry, onDelete, onCopyPrompt }) {
|
|
487
|
+
const truncatedComment = entry.comment.length > 80 ? entry.comment.slice(0, 80) + "..." : entry.comment;
|
|
488
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
489
|
+
"div",
|
|
490
|
+
{
|
|
491
|
+
style: {
|
|
492
|
+
padding: 12,
|
|
493
|
+
borderBottom: "1px solid #e5e7eb",
|
|
494
|
+
fontFamily: "system-ui, sans-serif"
|
|
495
|
+
},
|
|
496
|
+
children: [
|
|
497
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 6 }, children: [
|
|
498
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 13, color: "#111827", flex: 1, marginRight: 8 }, children: truncatedComment || "(no comment)" }),
|
|
499
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 11, color: "#9ca3af", whiteSpace: "nowrap", flexShrink: 0 }, children: relativeTime(entry.timestamp) })
|
|
500
|
+
] }),
|
|
501
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 11, color: "#6b7280", marginBottom: 4 }, children: entry.url }),
|
|
502
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 11, color: "#9ca3af", marginBottom: 8 }, children: entry.componentPath }),
|
|
503
|
+
entry.screenshot && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
504
|
+
"img",
|
|
505
|
+
{
|
|
506
|
+
src: entry.screenshot,
|
|
507
|
+
alt: "Screenshot thumbnail",
|
|
508
|
+
style: {
|
|
509
|
+
width: 60,
|
|
510
|
+
height: 40,
|
|
511
|
+
objectFit: "cover",
|
|
512
|
+
borderRadius: 4,
|
|
513
|
+
border: "1px solid #e5e7eb",
|
|
514
|
+
marginBottom: 8,
|
|
515
|
+
display: "block"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
),
|
|
519
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
|
|
520
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
521
|
+
"button",
|
|
522
|
+
{
|
|
523
|
+
onClick: () => onCopyPrompt(entry.prompt),
|
|
524
|
+
style: {
|
|
525
|
+
padding: "4px 10px",
|
|
526
|
+
fontSize: 11,
|
|
527
|
+
borderRadius: 4,
|
|
528
|
+
border: "1px solid #d1d5db",
|
|
529
|
+
background: "#f9fafb",
|
|
530
|
+
color: "#374151",
|
|
531
|
+
cursor: "pointer"
|
|
532
|
+
},
|
|
533
|
+
children: "Copy Prompt"
|
|
534
|
+
}
|
|
535
|
+
),
|
|
536
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
537
|
+
"button",
|
|
538
|
+
{
|
|
539
|
+
onClick: () => onDelete(entry.id),
|
|
540
|
+
style: {
|
|
541
|
+
padding: "4px 10px",
|
|
542
|
+
fontSize: 11,
|
|
543
|
+
borderRadius: 4,
|
|
544
|
+
border: "1px solid #fecaca",
|
|
545
|
+
background: "#fef2f2",
|
|
546
|
+
color: "#dc2626",
|
|
547
|
+
cursor: "pointer"
|
|
548
|
+
},
|
|
549
|
+
children: "Delete"
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
] })
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/components/FeedbackListPanel.tsx
|
|
559
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
560
|
+
async function copyToClipboard2(text) {
|
|
561
|
+
if (navigator.clipboard?.writeText) {
|
|
562
|
+
try {
|
|
563
|
+
await navigator.clipboard.writeText(text);
|
|
564
|
+
return true;
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const textarea = document.createElement("textarea");
|
|
570
|
+
textarea.value = text;
|
|
571
|
+
textarea.style.cssText = "position:fixed;left:-9999px;top:-9999px";
|
|
572
|
+
document.body.appendChild(textarea);
|
|
573
|
+
textarea.focus();
|
|
574
|
+
textarea.select();
|
|
575
|
+
const ok = document.execCommand("copy");
|
|
576
|
+
document.body.removeChild(textarea);
|
|
577
|
+
return ok;
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
function FeedbackListPanel({ onClose, position }) {
|
|
583
|
+
const [entries, setEntries] = (0, import_react3.useState)(() => loadEntries());
|
|
584
|
+
const [confirmClear, setConfirmClear] = (0, import_react3.useState)(false);
|
|
585
|
+
const [copiedId, setCopiedId] = (0, import_react3.useState)(null);
|
|
586
|
+
const isRight = position.includes("right");
|
|
587
|
+
const refreshEntries = (0, import_react3.useCallback)(() => {
|
|
588
|
+
setEntries(loadEntries());
|
|
589
|
+
}, []);
|
|
590
|
+
const handleDelete = (0, import_react3.useCallback)(
|
|
591
|
+
(id) => {
|
|
592
|
+
deleteEntry(id);
|
|
593
|
+
refreshEntries();
|
|
594
|
+
},
|
|
595
|
+
[refreshEntries]
|
|
596
|
+
);
|
|
597
|
+
const handleCopyPrompt = (0, import_react3.useCallback)(async (prompt) => {
|
|
598
|
+
const ok = await copyToClipboard2(prompt);
|
|
599
|
+
if (ok) {
|
|
600
|
+
}
|
|
601
|
+
}, []);
|
|
602
|
+
const handleClear = (0, import_react3.useCallback)(() => {
|
|
603
|
+
if (!confirmClear) {
|
|
604
|
+
setConfirmClear(true);
|
|
605
|
+
setTimeout(() => setConfirmClear(false), 3e3);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
clearEntries();
|
|
609
|
+
setConfirmClear(false);
|
|
610
|
+
refreshEntries();
|
|
611
|
+
}, [confirmClear, refreshEntries]);
|
|
612
|
+
const sortedEntries = [...entries].reverse();
|
|
613
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
614
|
+
"div",
|
|
615
|
+
{
|
|
616
|
+
style: {
|
|
617
|
+
position: "fixed",
|
|
618
|
+
top: 0,
|
|
619
|
+
[isRight ? "right" : "left"]: 0,
|
|
620
|
+
bottom: 0,
|
|
621
|
+
width: 380,
|
|
622
|
+
maxWidth: "90vw",
|
|
623
|
+
zIndex: 99999,
|
|
624
|
+
background: "#fff",
|
|
625
|
+
boxShadow: isRight ? "-4px 0 24px rgba(0,0,0,0.15)" : "4px 0 24px rgba(0,0,0,0.15)",
|
|
626
|
+
display: "flex",
|
|
627
|
+
flexDirection: "column",
|
|
628
|
+
fontFamily: "system-ui, sans-serif"
|
|
629
|
+
},
|
|
630
|
+
children: [
|
|
631
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
632
|
+
"div",
|
|
633
|
+
{
|
|
634
|
+
style: {
|
|
635
|
+
padding: 16,
|
|
636
|
+
borderBottom: "1px solid #e5e7eb",
|
|
637
|
+
display: "flex",
|
|
638
|
+
justifyContent: "space-between",
|
|
639
|
+
alignItems: "center",
|
|
640
|
+
flexShrink: 0
|
|
641
|
+
},
|
|
642
|
+
children: [
|
|
643
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("h3", { style: { margin: 0, fontSize: 15, color: "#111827" }, children: [
|
|
644
|
+
"Feedback (",
|
|
645
|
+
entries.length,
|
|
646
|
+
")"
|
|
647
|
+
] }),
|
|
648
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
649
|
+
"button",
|
|
650
|
+
{
|
|
651
|
+
onClick: onClose,
|
|
652
|
+
style: {
|
|
653
|
+
background: "none",
|
|
654
|
+
border: "none",
|
|
655
|
+
fontSize: 20,
|
|
656
|
+
cursor: "pointer",
|
|
657
|
+
color: "#666",
|
|
658
|
+
padding: "0 4px"
|
|
659
|
+
},
|
|
660
|
+
children: "\xD7"
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
]
|
|
664
|
+
}
|
|
665
|
+
),
|
|
666
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { flex: 1, overflowY: "auto", padding: 0 }, children: sortedEntries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
667
|
+
"div",
|
|
668
|
+
{
|
|
669
|
+
style: {
|
|
670
|
+
textAlign: "center",
|
|
671
|
+
padding: 32,
|
|
672
|
+
color: "#9ca3af",
|
|
673
|
+
fontSize: 13
|
|
674
|
+
},
|
|
675
|
+
children: "No feedback entries yet"
|
|
676
|
+
}
|
|
677
|
+
) : sortedEntries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
678
|
+
EntryCard,
|
|
679
|
+
{
|
|
680
|
+
entry,
|
|
681
|
+
onDelete: handleDelete,
|
|
682
|
+
onCopyPrompt: handleCopyPrompt
|
|
683
|
+
},
|
|
684
|
+
entry.id
|
|
685
|
+
)) }),
|
|
686
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
687
|
+
"div",
|
|
688
|
+
{
|
|
689
|
+
style: {
|
|
690
|
+
padding: "12px 16px",
|
|
691
|
+
borderTop: "1px solid #e5e7eb",
|
|
692
|
+
flexShrink: 0
|
|
693
|
+
},
|
|
694
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
695
|
+
"button",
|
|
696
|
+
{
|
|
697
|
+
onClick: handleClear,
|
|
698
|
+
disabled: entries.length === 0,
|
|
699
|
+
style: {
|
|
700
|
+
width: "100%",
|
|
701
|
+
padding: "8px 16px",
|
|
702
|
+
borderRadius: 6,
|
|
703
|
+
border: "1px solid #d1d5db",
|
|
704
|
+
background: confirmClear ? "#fef2f2" : "#f9fafb",
|
|
705
|
+
color: confirmClear ? "#dc2626" : entries.length === 0 ? "#9ca3af" : "#374151",
|
|
706
|
+
fontSize: 13,
|
|
707
|
+
cursor: entries.length === 0 ? "default" : "pointer"
|
|
708
|
+
},
|
|
709
|
+
children: confirmClear ? "Are you sure? Clear" : "Clear All"
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
]
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/components/ListButton.tsx
|
|
720
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
721
|
+
function ListButton({ onClick, count, position, buttonColor }) {
|
|
722
|
+
if (count === 0) return null;
|
|
723
|
+
const isBottom = position.includes("bottom");
|
|
724
|
+
const isRight = position.includes("right");
|
|
725
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
726
|
+
"button",
|
|
727
|
+
{
|
|
728
|
+
onClick,
|
|
729
|
+
"aria-label": `View ${count} feedback entries`,
|
|
730
|
+
style: {
|
|
731
|
+
position: "fixed",
|
|
732
|
+
[isBottom ? "bottom" : "top"]: 80,
|
|
733
|
+
[isRight ? "right" : "left"]: 24,
|
|
734
|
+
zIndex: 99999,
|
|
735
|
+
width: 36,
|
|
736
|
+
height: 36,
|
|
737
|
+
borderRadius: "50%",
|
|
738
|
+
border: "1px solid #d1d5db",
|
|
739
|
+
background: "#f3f4f6",
|
|
740
|
+
color: "#374151",
|
|
741
|
+
fontSize: 16,
|
|
742
|
+
cursor: "pointer",
|
|
743
|
+
display: "flex",
|
|
744
|
+
alignItems: "center",
|
|
745
|
+
justifyContent: "center",
|
|
746
|
+
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
|
|
747
|
+
padding: 0
|
|
748
|
+
},
|
|
749
|
+
children: [
|
|
750
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { lineHeight: 1 }, children: "\u2630" }),
|
|
751
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
752
|
+
"span",
|
|
753
|
+
{
|
|
754
|
+
style: {
|
|
755
|
+
position: "absolute",
|
|
756
|
+
top: -4,
|
|
757
|
+
[isRight ? "right" : "left"]: -4,
|
|
758
|
+
background: buttonColor,
|
|
759
|
+
color: getContrastColor(buttonColor),
|
|
760
|
+
fontSize: 10,
|
|
761
|
+
fontWeight: 600,
|
|
762
|
+
borderRadius: "50%",
|
|
763
|
+
width: 18,
|
|
764
|
+
height: 18,
|
|
765
|
+
display: "flex",
|
|
766
|
+
alignItems: "center",
|
|
767
|
+
justifyContent: "center",
|
|
768
|
+
lineHeight: 1
|
|
769
|
+
},
|
|
770
|
+
children: count > 99 ? "99+" : count
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
]
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/utils/fiber.ts
|
|
779
|
+
var minificationWarned = false;
|
|
780
|
+
function getFiber(el) {
|
|
781
|
+
const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
782
|
+
return key ? el[key] : null;
|
|
783
|
+
}
|
|
784
|
+
function pickShortProps(props) {
|
|
785
|
+
const short = {};
|
|
786
|
+
for (const [key, value] of Object.entries(props)) {
|
|
787
|
+
if (key === "children") continue;
|
|
788
|
+
if (typeof value === "function") continue;
|
|
789
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
790
|
+
short[key] = value;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return short;
|
|
794
|
+
}
|
|
795
|
+
function getComponentPath(el, maxDepth = 8) {
|
|
796
|
+
const fiber = getFiber(el);
|
|
797
|
+
if (!fiber) return [];
|
|
798
|
+
const path = [];
|
|
799
|
+
let current = fiber;
|
|
800
|
+
while (current && path.length < maxDepth) {
|
|
801
|
+
if (typeof current.type === "function" || typeof current.type === "object") {
|
|
802
|
+
const name = current.type?.displayName || current.type?.name || null;
|
|
803
|
+
if (name) {
|
|
804
|
+
if (!minificationWarned && name.length <= 2 && /^[a-z]/.test(name)) {
|
|
805
|
+
minificationWarned = true;
|
|
806
|
+
console.warn(
|
|
807
|
+
'[llm-ui-feedback] Component names appear minified (e.g., "' + name + '"). The generated LLM prompts will lack meaningful component paths. To fix, configure your bundler to preserve function names. See: https://github.com/user/llm-ui-feedback/blob/main/docs/production-setup.md'
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
path.push({
|
|
811
|
+
name,
|
|
812
|
+
props: pickShortProps(current.memoizedProps || {})
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
current = current.return;
|
|
817
|
+
}
|
|
818
|
+
return path.reverse();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/utils/screenshot.ts
|
|
822
|
+
var import_html2canvas_pro = __toESM(require("html2canvas-pro"));
|
|
823
|
+
async function captureScreenshot(clickX, clickY) {
|
|
824
|
+
const docX = clickX + window.scrollX;
|
|
825
|
+
const docY = clickY + window.scrollY;
|
|
826
|
+
const canvas = await (0, import_html2canvas_pro.default)(document.body, {
|
|
827
|
+
logging: false,
|
|
828
|
+
useCORS: true
|
|
829
|
+
});
|
|
830
|
+
const ctx = canvas.getContext("2d");
|
|
831
|
+
if (ctx) {
|
|
832
|
+
ctx.strokeStyle = "red";
|
|
833
|
+
ctx.lineWidth = 3;
|
|
834
|
+
const size = 14;
|
|
835
|
+
ctx.beginPath();
|
|
836
|
+
ctx.moveTo(docX - size, docY - size);
|
|
837
|
+
ctx.lineTo(docX + size, docY + size);
|
|
838
|
+
ctx.moveTo(docX + size, docY - size);
|
|
839
|
+
ctx.lineTo(docX - size, docY + size);
|
|
840
|
+
ctx.stroke();
|
|
841
|
+
ctx.beginPath();
|
|
842
|
+
ctx.arc(docX, docY, size + 4, 0, Math.PI * 2);
|
|
843
|
+
ctx.stroke();
|
|
844
|
+
}
|
|
845
|
+
return canvas.toDataURL("image/png");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/components/FeedbackWidget.tsx
|
|
849
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
850
|
+
var CONTAINER_ID = "llm-ui-feedback-root";
|
|
851
|
+
function getOrCreateContainer() {
|
|
852
|
+
let el = document.getElementById(CONTAINER_ID);
|
|
853
|
+
if (!el) {
|
|
854
|
+
el = document.createElement("div");
|
|
855
|
+
el.id = CONTAINER_ID;
|
|
856
|
+
document.body.appendChild(el);
|
|
857
|
+
}
|
|
858
|
+
return el;
|
|
859
|
+
}
|
|
860
|
+
function widgetReducer(state, action) {
|
|
861
|
+
switch (action.type) {
|
|
862
|
+
case "START_PICKING":
|
|
863
|
+
return state.mode === "idle" ? { mode: "picking" } : state;
|
|
864
|
+
case "CANCEL":
|
|
865
|
+
case "CLOSE":
|
|
866
|
+
return { mode: "idle" };
|
|
867
|
+
case "ELEMENT_PICKED":
|
|
868
|
+
return { mode: "dialog", context: action.context, screenshot: action.screenshot };
|
|
869
|
+
case "OPEN_LIST":
|
|
870
|
+
return state.mode === "idle" ? { mode: "list" } : state;
|
|
871
|
+
default:
|
|
872
|
+
return state;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function parseHotkey(hotkey) {
|
|
876
|
+
const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
|
|
877
|
+
const key = parts.pop();
|
|
878
|
+
return {
|
|
879
|
+
key,
|
|
880
|
+
ctrl: parts.includes("ctrl"),
|
|
881
|
+
alt: parts.includes("alt"),
|
|
882
|
+
shift: parts.includes("shift"),
|
|
883
|
+
meta: parts.includes("meta")
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function isEditableTarget(el) {
|
|
887
|
+
if (!el) return false;
|
|
888
|
+
const tag = el.tagName;
|
|
889
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
890
|
+
if (el.isContentEditable) return true;
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
function FeedbackWidget({
|
|
894
|
+
position = "bottom-right",
|
|
895
|
+
buttonColor = "#3b82f6",
|
|
896
|
+
hotkey
|
|
897
|
+
} = {}) {
|
|
898
|
+
const [state, dispatch] = (0, import_react4.useReducer)(widgetReducer, { mode: "idle" });
|
|
899
|
+
const [entryCount, setEntryCount] = (0, import_react4.useState)(0);
|
|
900
|
+
(0, import_react4.useEffect)(() => {
|
|
901
|
+
if (state.mode === "idle") {
|
|
902
|
+
setEntryCount(loadEntries().length);
|
|
903
|
+
}
|
|
904
|
+
}, [state.mode]);
|
|
905
|
+
(0, import_react4.useEffect)(() => {
|
|
906
|
+
setEntryCount(loadEntries().length);
|
|
907
|
+
}, []);
|
|
908
|
+
(0, import_react4.useEffect)(() => {
|
|
909
|
+
if (!hotkey) return;
|
|
910
|
+
const parsed = parseHotkey(hotkey);
|
|
911
|
+
function handler(e) {
|
|
912
|
+
if (isEditableTarget(document.activeElement)) return;
|
|
913
|
+
if (e.key.toLowerCase() === parsed.key && e.ctrlKey === parsed.ctrl && e.altKey === parsed.alt && e.shiftKey === parsed.shift && e.metaKey === parsed.meta) {
|
|
914
|
+
e.preventDefault();
|
|
915
|
+
dispatch(
|
|
916
|
+
state.mode === "picking" ? { type: "CANCEL" } : state.mode === "idle" ? { type: "START_PICKING" } : { type: "CLOSE" }
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
window.addEventListener("keydown", handler);
|
|
921
|
+
return () => window.removeEventListener("keydown", handler);
|
|
922
|
+
}, [hotkey, state.mode]);
|
|
923
|
+
const handleToggle = (0, import_react4.useCallback)(() => {
|
|
924
|
+
dispatch({ type: "START_PICKING" });
|
|
925
|
+
}, []);
|
|
926
|
+
const handlePick = (0, import_react4.useCallback)(async (element, x, y) => {
|
|
927
|
+
const components = getComponentPath(element);
|
|
928
|
+
const componentPath = components.map((c) => c.name).join(" > ") || "(no React component found)";
|
|
929
|
+
const rawText = (element.innerText || element.textContent || "").trim();
|
|
930
|
+
const elementText = rawText.length > 100 ? rawText.slice(0, 100) + "..." : rawText;
|
|
931
|
+
const context = {
|
|
932
|
+
url: window.location.href,
|
|
933
|
+
componentPath,
|
|
934
|
+
components,
|
|
935
|
+
elementText,
|
|
936
|
+
clickX: x,
|
|
937
|
+
clickY: y
|
|
938
|
+
};
|
|
939
|
+
let screenshot = null;
|
|
940
|
+
try {
|
|
941
|
+
screenshot = await captureScreenshot(x, y);
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
dispatch({ type: "ELEMENT_PICKED", context, screenshot });
|
|
945
|
+
}, []);
|
|
946
|
+
const handleCancel = (0, import_react4.useCallback)(() => {
|
|
947
|
+
dispatch({ type: "CANCEL" });
|
|
948
|
+
}, []);
|
|
949
|
+
const handleClose = (0, import_react4.useCallback)(() => {
|
|
950
|
+
dispatch({ type: "CLOSE" });
|
|
951
|
+
}, []);
|
|
952
|
+
const handleOpenList = (0, import_react4.useCallback)(() => {
|
|
953
|
+
dispatch({ type: "OPEN_LIST" });
|
|
954
|
+
}, []);
|
|
955
|
+
const handleCloseList = (0, import_react4.useCallback)(() => {
|
|
956
|
+
dispatch({ type: "CLOSE" });
|
|
957
|
+
}, []);
|
|
958
|
+
return (0, import_react_dom.createPortal)(
|
|
959
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
960
|
+
state.mode === "idle" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
961
|
+
ListButton,
|
|
962
|
+
{
|
|
963
|
+
onClick: handleOpenList,
|
|
964
|
+
count: entryCount,
|
|
965
|
+
position,
|
|
966
|
+
buttonColor
|
|
967
|
+
}
|
|
968
|
+
),
|
|
969
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
970
|
+
FloatingButton,
|
|
971
|
+
{
|
|
972
|
+
onClick: state.mode === "picking" ? handleCancel : handleToggle,
|
|
973
|
+
active: state.mode === "picking",
|
|
974
|
+
position,
|
|
975
|
+
buttonColor
|
|
976
|
+
}
|
|
977
|
+
),
|
|
978
|
+
state.mode === "picking" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PickOverlay, { onPick: handlePick, onCancel: handleCancel }),
|
|
979
|
+
state.mode === "dialog" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FeedbackDialog, { context: state.context, screenshot: state.screenshot, onClose: handleClose }),
|
|
980
|
+
state.mode === "list" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FeedbackListPanel, { onClose: handleCloseList, position })
|
|
981
|
+
] }),
|
|
982
|
+
getOrCreateContainer()
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
986
|
+
0 && (module.exports = {
|
|
987
|
+
FeedbackWidget,
|
|
988
|
+
clearEntries,
|
|
989
|
+
deleteEntry,
|
|
990
|
+
loadEntries
|
|
991
|
+
});
|
|
992
|
+
//# sourceMappingURL=index.js.map
|