@david-xpn/llm-ui-feedback 0.1.0 → 0.1.2
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 +16 -0
- package/dist/index.d.mts +40 -9
- package/dist/index.d.ts +40 -9
- package/dist/index.js +1219 -675
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1222 -676
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -30,15 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
FeedbackWidget: () => FeedbackWidget
|
|
34
|
-
clearEntries: () => clearEntries,
|
|
35
|
-
deleteEntry: () => deleteEntry,
|
|
36
|
-
loadEntries: () => loadEntries
|
|
33
|
+
FeedbackWidget: () => FeedbackWidget
|
|
37
34
|
});
|
|
38
35
|
module.exports = __toCommonJS(index_exports);
|
|
39
36
|
|
|
40
37
|
// src/components/FeedbackWidget.tsx
|
|
41
|
-
var
|
|
38
|
+
var import_react7 = require("react");
|
|
42
39
|
var import_react_dom = require("react-dom");
|
|
43
40
|
|
|
44
41
|
// src/utils/color.ts
|
|
@@ -55,44 +52,107 @@ function getContrastColor(hexColor) {
|
|
|
55
52
|
|
|
56
53
|
// src/components/FloatingButton.tsx
|
|
57
54
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
58
|
-
function FloatingButton({
|
|
55
|
+
function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, position, buttonColor }) {
|
|
59
56
|
const isBottom = position.includes("bottom");
|
|
60
57
|
const isRight = position.includes("right");
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
58
|
+
const anchor = {
|
|
59
|
+
position: "fixed",
|
|
60
|
+
[isBottom ? "bottom" : "top"]: 24,
|
|
61
|
+
[isRight ? "right" : "left"]: 24,
|
|
62
|
+
zIndex: 99999,
|
|
63
|
+
display: "flex",
|
|
64
|
+
flexDirection: "column",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
gap: 8
|
|
67
|
+
};
|
|
68
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: anchor, children: [
|
|
69
|
+
draftCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
70
|
+
"button",
|
|
71
|
+
{
|
|
72
|
+
onClick: onPanelToggle,
|
|
73
|
+
"aria-label": panelOpen ? "Close drafts panel" : `Open drafts panel (${draftCount})`,
|
|
74
|
+
style: {
|
|
75
|
+
width: 32,
|
|
76
|
+
height: 32,
|
|
77
|
+
borderRadius: "50%",
|
|
78
|
+
border: "none",
|
|
79
|
+
background: panelOpen ? "#ef4444" : buttonColor,
|
|
80
|
+
color: panelOpen ? "#fff" : getContrastColor(buttonColor),
|
|
81
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
82
|
+
fontSize: 13,
|
|
83
|
+
fontWeight: 700,
|
|
84
|
+
lineHeight: 1,
|
|
85
|
+
cursor: "pointer",
|
|
86
|
+
display: "flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
|
|
90
|
+
transition: "background 0.15s",
|
|
91
|
+
padding: 0
|
|
92
|
+
},
|
|
93
|
+
children: draftCount
|
|
94
|
+
}
|
|
95
|
+
),
|
|
96
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
97
|
+
"button",
|
|
98
|
+
{
|
|
99
|
+
onClick: panelOpen ? onPanelToggle : onPickClick,
|
|
100
|
+
"aria-label": panelOpen ? "Close panel" : "Pick element for feedback",
|
|
101
|
+
style: {
|
|
102
|
+
width: 48,
|
|
103
|
+
height: 48,
|
|
104
|
+
borderRadius: "50%",
|
|
105
|
+
border: "none",
|
|
106
|
+
background: panelOpen ? "#ef4444" : buttonColor,
|
|
107
|
+
color: panelOpen ? "#fff" : getContrastColor(buttonColor),
|
|
108
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
109
|
+
fontSize: 28,
|
|
110
|
+
fontWeight: 300,
|
|
111
|
+
lineHeight: 1,
|
|
112
|
+
cursor: "pointer",
|
|
113
|
+
display: "flex",
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
justifyContent: "center",
|
|
116
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
|
|
117
|
+
transition: "background 0.15s, transform 0.15s",
|
|
118
|
+
transform: panelOpen ? "rotate(45deg)" : "none",
|
|
119
|
+
padding: 0
|
|
120
|
+
},
|
|
121
|
+
children: "+"
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
] });
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
// src/components/PickOverlay.tsx
|
|
92
128
|
var import_react = require("react");
|
|
93
129
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
94
130
|
var WIDGET_CONTAINER_ID = "llm-ui-feedback-root";
|
|
95
|
-
function
|
|
131
|
+
function getElementsBeneathRect(rect) {
|
|
132
|
+
const container = document.getElementById(WIDGET_CONTAINER_ID);
|
|
133
|
+
if (container) container.style.display = "none";
|
|
134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
135
|
+
const step = 8;
|
|
136
|
+
for (let x = rect.x; x <= rect.x + rect.width; x += step) {
|
|
137
|
+
for (let y = rect.y; y <= rect.y + rect.height; y += step) {
|
|
138
|
+
const el = document.elementFromPoint(x, y);
|
|
139
|
+
if (el && !seen.has(el)) seen.add(el);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [x, y] of [
|
|
143
|
+
[rect.x, rect.y],
|
|
144
|
+
[rect.x + rect.width, rect.y],
|
|
145
|
+
[rect.x, rect.y + rect.height],
|
|
146
|
+
[rect.x + rect.width, rect.y + rect.height],
|
|
147
|
+
[rect.x + rect.width / 2, rect.y + rect.height / 2]
|
|
148
|
+
]) {
|
|
149
|
+
const el = document.elementFromPoint(x, y);
|
|
150
|
+
if (el && !seen.has(el)) seen.add(el);
|
|
151
|
+
}
|
|
152
|
+
if (container) container.style.display = "";
|
|
153
|
+
return Array.from(seen).filter((el) => !container?.contains(el));
|
|
154
|
+
}
|
|
155
|
+
function getElementAtPoint(x, y) {
|
|
96
156
|
const container = document.getElementById(WIDGET_CONTAINER_ID);
|
|
97
157
|
if (container) container.style.display = "none";
|
|
98
158
|
const el = document.elementFromPoint(x, y);
|
|
@@ -100,218 +160,541 @@ function getElementBeneath(x, y) {
|
|
|
100
160
|
if (el && container?.contains(el)) return null;
|
|
101
161
|
return el;
|
|
102
162
|
}
|
|
163
|
+
var DRAG_THRESHOLD = 10;
|
|
103
164
|
function PickOverlay({ onPick, onCancel }) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
165
|
+
const activeRef = (0, import_react.useRef)(false);
|
|
166
|
+
const startRef = (0, import_react.useRef)(null);
|
|
167
|
+
const isDragRef = (0, import_react.useRef)(false);
|
|
168
|
+
const [hoverRect, setHoverRect] = (0, import_react.useState)(null);
|
|
169
|
+
const [selRect, setSelRect] = (0, import_react.useState)(null);
|
|
170
|
+
const onPickRef = (0, import_react.useRef)(onPick);
|
|
171
|
+
onPickRef.current = onPick;
|
|
172
|
+
const onCancelRef = (0, import_react.useRef)(onCancel);
|
|
173
|
+
onCancelRef.current = onCancel;
|
|
174
|
+
(0, import_react.useEffect)(() => {
|
|
175
|
+
function handleMouseMove(e) {
|
|
176
|
+
if (activeRef.current && startRef.current) {
|
|
177
|
+
const dx = Math.abs(e.clientX - startRef.current.x);
|
|
178
|
+
const dy = Math.abs(e.clientY - startRef.current.y);
|
|
179
|
+
if (dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD) {
|
|
180
|
+
isDragRef.current = true;
|
|
181
|
+
}
|
|
182
|
+
if (isDragRef.current) {
|
|
183
|
+
setHoverRect(null);
|
|
184
|
+
setSelRect({
|
|
185
|
+
left: Math.min(startRef.current.x, e.clientX),
|
|
186
|
+
top: Math.min(startRef.current.y, e.clientY),
|
|
187
|
+
width: Math.abs(e.clientX - startRef.current.x),
|
|
188
|
+
height: Math.abs(e.clientY - startRef.current.y)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
} else {
|
|
193
|
+
const el = getElementAtPoint(e.clientX, e.clientY);
|
|
194
|
+
if (el) {
|
|
195
|
+
const r = el.getBoundingClientRect();
|
|
196
|
+
setHoverRect({ x: r.left, y: r.top, width: r.width, height: r.height });
|
|
197
|
+
} else {
|
|
198
|
+
setHoverRect(null);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
109
201
|
}
|
|
110
|
-
|
|
111
|
-
|
|
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) => {
|
|
202
|
+
function handleMouseDown(e) {
|
|
203
|
+
if (e.button !== 0) return;
|
|
121
204
|
e.preventDefault();
|
|
122
205
|
e.stopPropagation();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
206
|
+
startRef.current = { x: e.clientX, y: e.clientY };
|
|
207
|
+
isDragRef.current = false;
|
|
208
|
+
activeRef.current = true;
|
|
209
|
+
}
|
|
210
|
+
function handleMouseUp(e) {
|
|
211
|
+
if (!activeRef.current || !startRef.current) return;
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
e.stopPropagation();
|
|
214
|
+
const start = startRef.current;
|
|
215
|
+
activeRef.current = false;
|
|
216
|
+
startRef.current = null;
|
|
217
|
+
setSelRect(null);
|
|
218
|
+
setHoverRect(null);
|
|
219
|
+
if (!isDragRef.current) {
|
|
220
|
+
const el = getElementAtPoint(e.clientX, e.clientY);
|
|
221
|
+
if (el) {
|
|
222
|
+
const r = el.getBoundingClientRect();
|
|
223
|
+
onPickRef.current({ x: r.left, y: r.top, width: r.width, height: r.height }, [el]);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
127
226
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
227
|
+
const x = Math.min(start.x, e.clientX);
|
|
228
|
+
const y = Math.min(start.y, e.clientY);
|
|
229
|
+
const width = Math.abs(e.clientX - start.x);
|
|
230
|
+
const height = Math.abs(e.clientY - start.y);
|
|
231
|
+
const rect = { x, y, width, height };
|
|
232
|
+
const elements = getElementsBeneathRect(rect);
|
|
233
|
+
onPickRef.current(rect, elements);
|
|
234
|
+
}
|
|
235
|
+
function handleKeyDown(e) {
|
|
133
236
|
if (e.key === "Escape") {
|
|
134
|
-
|
|
135
|
-
|
|
237
|
+
activeRef.current = false;
|
|
238
|
+
startRef.current = null;
|
|
239
|
+
setSelRect(null);
|
|
240
|
+
setHoverRect(null);
|
|
241
|
+
onCancelRef.current();
|
|
136
242
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
);
|
|
140
|
-
(0, import_react.useEffect)(() => {
|
|
243
|
+
}
|
|
244
|
+
document.addEventListener("mousedown", handleMouseDown, true);
|
|
141
245
|
document.addEventListener("mousemove", handleMouseMove, true);
|
|
142
|
-
document.addEventListener("
|
|
246
|
+
document.addEventListener("mouseup", handleMouseUp, true);
|
|
143
247
|
document.addEventListener("keydown", handleKeyDown, true);
|
|
144
248
|
return () => {
|
|
249
|
+
document.removeEventListener("mousedown", handleMouseDown, true);
|
|
145
250
|
document.removeEventListener("mousemove", handleMouseMove, true);
|
|
146
|
-
document.removeEventListener("
|
|
251
|
+
document.removeEventListener("mouseup", handleMouseUp, true);
|
|
147
252
|
document.removeEventListener("keydown", handleKeyDown, true);
|
|
148
|
-
if (lastOutlinedRef.current) {
|
|
149
|
-
lastOutlinedRef.current.style.outline = "";
|
|
150
|
-
lastOutlinedRef.current = null;
|
|
151
|
-
}
|
|
152
253
|
};
|
|
153
|
-
}, [
|
|
154
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
254
|
+
}, []);
|
|
255
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
256
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
257
|
+
"div",
|
|
258
|
+
{
|
|
259
|
+
style: {
|
|
260
|
+
position: "fixed",
|
|
261
|
+
top: 0,
|
|
262
|
+
left: 0,
|
|
263
|
+
right: 0,
|
|
264
|
+
bottom: 0,
|
|
265
|
+
zIndex: 99997,
|
|
266
|
+
cursor: "crosshair"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
271
|
+
"div",
|
|
272
|
+
{
|
|
273
|
+
style: {
|
|
274
|
+
position: "fixed",
|
|
275
|
+
top: 0,
|
|
276
|
+
left: 0,
|
|
277
|
+
right: 0,
|
|
278
|
+
zIndex: 99998,
|
|
279
|
+
background: "rgba(59, 130, 246, 0.08)",
|
|
280
|
+
padding: "8px 16px",
|
|
281
|
+
textAlign: "center",
|
|
282
|
+
fontSize: 14,
|
|
283
|
+
color: "#1e40af",
|
|
284
|
+
fontFamily: "system-ui, sans-serif",
|
|
285
|
+
pointerEvents: "none"
|
|
286
|
+
},
|
|
287
|
+
children: [
|
|
288
|
+
"Click an element or drag to select an area. Press ",
|
|
289
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: "Esc" }),
|
|
290
|
+
" to cancel."
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
),
|
|
294
|
+
hoverRect && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
295
|
+
"div",
|
|
296
|
+
{
|
|
297
|
+
style: {
|
|
298
|
+
position: "fixed",
|
|
299
|
+
left: hoverRect.x,
|
|
300
|
+
top: hoverRect.y,
|
|
301
|
+
width: hoverRect.width,
|
|
302
|
+
height: hoverRect.height,
|
|
303
|
+
border: "2px solid #3b82f6",
|
|
304
|
+
background: "rgba(59, 130, 246, 0.08)",
|
|
305
|
+
zIndex: 99998,
|
|
306
|
+
pointerEvents: "none",
|
|
307
|
+
borderRadius: 2,
|
|
308
|
+
transition: "all 0.1s ease-out"
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
),
|
|
312
|
+
selRect && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
313
|
+
"div",
|
|
314
|
+
{
|
|
315
|
+
style: {
|
|
316
|
+
position: "fixed",
|
|
317
|
+
left: selRect.left,
|
|
318
|
+
top: selRect.top,
|
|
319
|
+
width: selRect.width,
|
|
320
|
+
height: selRect.height,
|
|
321
|
+
border: "2px dashed #3b82f6",
|
|
322
|
+
background: "rgba(59, 130, 246, 0.1)",
|
|
323
|
+
zIndex: 99998,
|
|
324
|
+
pointerEvents: "none",
|
|
325
|
+
borderRadius: 2
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/components/SidePanel.tsx
|
|
333
|
+
var import_react3 = __toESM(require("react"));
|
|
334
|
+
|
|
335
|
+
// src/components/DraftItem.tsx
|
|
336
|
+
var import_react2 = require("react");
|
|
337
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
338
|
+
function DraftItem({ draft, selected, screenshotUrl, onToggleSelect, onUpdate, onDelete, onPreviewScreenshot }) {
|
|
339
|
+
const [editing, setEditing] = (0, import_react2.useState)(false);
|
|
340
|
+
const [editComment, setEditComment] = (0, import_react2.useState)(draft.comment);
|
|
341
|
+
const [confirmDelete, setConfirmDelete] = (0, import_react2.useState)(false);
|
|
342
|
+
const handleSaveEdit = () => {
|
|
343
|
+
if (editComment.trim()) {
|
|
344
|
+
onUpdate(draft.id, editComment.trim());
|
|
345
|
+
}
|
|
346
|
+
setEditing(false);
|
|
347
|
+
};
|
|
348
|
+
const truncatedPath = draft.componentPath.length > 50 ? "..." + draft.componentPath.slice(-47) : draft.componentPath;
|
|
349
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
350
|
+
"div",
|
|
351
|
+
{
|
|
352
|
+
style: {
|
|
353
|
+
padding: "10px 12px",
|
|
354
|
+
borderBottom: "1px solid #e5e7eb",
|
|
355
|
+
fontSize: 13,
|
|
356
|
+
fontFamily: "system-ui, sans-serif"
|
|
357
|
+
},
|
|
358
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 8 }, children: [
|
|
359
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
360
|
+
"input",
|
|
361
|
+
{
|
|
362
|
+
type: "checkbox",
|
|
363
|
+
checked: selected,
|
|
364
|
+
onChange: () => onToggleSelect(draft.id),
|
|
365
|
+
style: { marginTop: 3, cursor: "pointer" }
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#6b7280", fontSize: 11, marginBottom: 2 }, title: draft.componentPath, children: truncatedPath }),
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#9ca3af", fontSize: 11, marginBottom: 4 }, title: draft.url, children: draft.url.length > 60 ? draft.url.slice(0, 60) + "..." : draft.url }),
|
|
371
|
+
screenshotUrl && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
372
|
+
"img",
|
|
373
|
+
{
|
|
374
|
+
src: screenshotUrl,
|
|
375
|
+
alt: "Screenshot",
|
|
376
|
+
onClick: () => onPreviewScreenshot(draft.id),
|
|
377
|
+
style: {
|
|
378
|
+
width: "100%",
|
|
379
|
+
maxHeight: 160,
|
|
380
|
+
objectFit: "cover",
|
|
381
|
+
borderRadius: 4,
|
|
382
|
+
border: "1px solid #e5e7eb",
|
|
383
|
+
cursor: "pointer",
|
|
384
|
+
marginBottom: 6
|
|
385
|
+
},
|
|
386
|
+
title: "Click to enlarge"
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
editing ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
390
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
391
|
+
"textarea",
|
|
392
|
+
{
|
|
393
|
+
value: editComment,
|
|
394
|
+
onChange: (e) => setEditComment(e.target.value),
|
|
395
|
+
autoFocus: true,
|
|
396
|
+
style: {
|
|
397
|
+
width: "100%",
|
|
398
|
+
minHeight: 48,
|
|
399
|
+
padding: 6,
|
|
400
|
+
borderRadius: 4,
|
|
401
|
+
border: "1px solid #d1d5db",
|
|
402
|
+
fontSize: 13,
|
|
403
|
+
resize: "vertical",
|
|
404
|
+
boxSizing: "border-box"
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
),
|
|
408
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
|
|
409
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: handleSaveEdit, style: smallBtn("#3b82f6", "#fff"), children: "Save" }),
|
|
410
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => {
|
|
411
|
+
setEditing(false);
|
|
412
|
+
setEditComment(draft.comment);
|
|
413
|
+
}, style: smallBtn("#f3f4f6", "#374151"), children: "Cancel" })
|
|
414
|
+
] })
|
|
415
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
416
|
+
"div",
|
|
417
|
+
{
|
|
418
|
+
onClick: () => setEditing(true),
|
|
419
|
+
style: { cursor: "pointer", color: "#1f2937", lineHeight: 1.4 },
|
|
420
|
+
title: "Click to edit",
|
|
421
|
+
children: draft.comment
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
] }),
|
|
425
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { flexShrink: 0 }, children: confirmDelete ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 4, fontSize: 12 }, children: [
|
|
426
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => onDelete(draft.id), style: smallBtn("#ef4444", "#fff"), children: "Yes" }),
|
|
427
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setConfirmDelete(false), style: smallBtn("#f3f4f6", "#374151"), children: "No" })
|
|
428
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
429
|
+
"button",
|
|
430
|
+
{
|
|
431
|
+
onClick: () => setConfirmDelete(true),
|
|
432
|
+
style: {
|
|
433
|
+
background: "none",
|
|
434
|
+
border: "none",
|
|
435
|
+
cursor: "pointer",
|
|
436
|
+
color: "#9ca3af",
|
|
437
|
+
fontSize: 16,
|
|
438
|
+
padding: "0 4px"
|
|
439
|
+
},
|
|
440
|
+
title: "Delete draft",
|
|
441
|
+
children: "x"
|
|
442
|
+
}
|
|
443
|
+
) })
|
|
444
|
+
] })
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
function smallBtn(bg, color) {
|
|
449
|
+
return {
|
|
450
|
+
padding: "3px 8px",
|
|
451
|
+
borderRadius: 4,
|
|
452
|
+
border: "1px solid #d1d5db",
|
|
453
|
+
background: bg,
|
|
454
|
+
color,
|
|
455
|
+
fontSize: 12,
|
|
456
|
+
cursor: "pointer"
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/components/SidePanel.tsx
|
|
461
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
462
|
+
function SidePanel({
|
|
463
|
+
position,
|
|
464
|
+
drafts,
|
|
465
|
+
selectedDraftIds,
|
|
466
|
+
onToggleSelect,
|
|
467
|
+
onSelectAll,
|
|
468
|
+
onUpdateDraft,
|
|
469
|
+
onDeleteDraft,
|
|
470
|
+
onSubmit,
|
|
471
|
+
onClose,
|
|
472
|
+
api,
|
|
473
|
+
user
|
|
474
|
+
}) {
|
|
475
|
+
const isRight = position.includes("right");
|
|
476
|
+
const allSelected = drafts.length > 0 && selectedDraftIds.size === drafts.length;
|
|
477
|
+
const [screenshotUrls, setScreenshotUrls] = (0, import_react3.useState)({});
|
|
478
|
+
const [previewUrl, setPreviewUrl] = (0, import_react3.useState)(null);
|
|
479
|
+
import_react3.default.useEffect(() => {
|
|
480
|
+
const draftsWithScreenshots = drafts.filter((d) => d.screenshotKey && !screenshotUrls[d.id]);
|
|
481
|
+
draftsWithScreenshots.forEach((draft) => {
|
|
482
|
+
api.getScreenshotUrl(draft.id).then((url) => {
|
|
483
|
+
setScreenshotUrls((prev) => ({ ...prev, [draft.id]: url }));
|
|
484
|
+
}).catch(() => {
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
}, [drafts, api]);
|
|
488
|
+
const handlePreviewScreenshot = (draftId) => {
|
|
489
|
+
const url = screenshotUrls[draftId];
|
|
490
|
+
if (url) setPreviewUrl(url);
|
|
491
|
+
};
|
|
492
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
155
493
|
"div",
|
|
156
494
|
{
|
|
157
495
|
style: {
|
|
158
496
|
position: "fixed",
|
|
159
497
|
top: 0,
|
|
160
|
-
left: 0,
|
|
161
|
-
|
|
498
|
+
[isRight ? "right" : "left"]: 0,
|
|
499
|
+
width: 360,
|
|
500
|
+
maxWidth: "100vw",
|
|
501
|
+
height: "100vh",
|
|
502
|
+
background: "#fff",
|
|
503
|
+
boxShadow: isRight ? "-4px 0 16px rgba(0,0,0,0.1)" : "4px 0 16px rgba(0,0,0,0.1)",
|
|
162
504
|
zIndex: 99998,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
fontSize: 14,
|
|
167
|
-
color: "#1e40af",
|
|
168
|
-
fontFamily: "system-ui, sans-serif",
|
|
169
|
-
pointerEvents: "none"
|
|
505
|
+
display: "flex",
|
|
506
|
+
flexDirection: "column",
|
|
507
|
+
fontFamily: "system-ui, sans-serif"
|
|
170
508
|
},
|
|
171
509
|
children: [
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
510
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
511
|
+
"div",
|
|
512
|
+
{
|
|
513
|
+
style: {
|
|
514
|
+
padding: "16px 16px 12px",
|
|
515
|
+
borderBottom: "1px solid #e5e7eb"
|
|
516
|
+
},
|
|
517
|
+
children: [
|
|
518
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
|
|
519
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { style: { margin: 0, fontSize: 15, fontWeight: 600 }, children: "Feedback Session" }),
|
|
520
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
521
|
+
"button",
|
|
522
|
+
{
|
|
523
|
+
onClick: onClose,
|
|
524
|
+
style: {
|
|
525
|
+
background: "none",
|
|
526
|
+
border: "none",
|
|
527
|
+
fontSize: 18,
|
|
528
|
+
cursor: "pointer",
|
|
529
|
+
color: "#6b7280",
|
|
530
|
+
padding: "0 4px"
|
|
531
|
+
},
|
|
532
|
+
children: "x"
|
|
533
|
+
}
|
|
534
|
+
)
|
|
535
|
+
] }),
|
|
536
|
+
user && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginTop: 6, fontSize: 12, color: "#6b7280", lineHeight: 1.4 }, children: [
|
|
537
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 500, color: "#374151" }, children: user.name }),
|
|
538
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: user.email })
|
|
539
|
+
] })
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: drafts.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { padding: 24, textAlign: "center", color: "#9ca3af", fontSize: 13 }, children: "No drafts yet. Pick an element to start." }) : drafts.map((draft) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
544
|
+
DraftItem,
|
|
545
|
+
{
|
|
546
|
+
draft,
|
|
547
|
+
selected: selectedDraftIds.has(draft.id),
|
|
548
|
+
screenshotUrl: screenshotUrls[draft.id] || null,
|
|
549
|
+
onToggleSelect,
|
|
550
|
+
onUpdate: onUpdateDraft,
|
|
551
|
+
onDelete: onDeleteDraft,
|
|
552
|
+
onPreviewScreenshot: handlePreviewScreenshot
|
|
553
|
+
},
|
|
554
|
+
draft.id
|
|
555
|
+
)) }),
|
|
556
|
+
drafts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
557
|
+
"div",
|
|
558
|
+
{
|
|
559
|
+
style: {
|
|
560
|
+
padding: "12px 16px",
|
|
561
|
+
borderTop: "1px solid #e5e7eb",
|
|
562
|
+
display: "flex",
|
|
563
|
+
alignItems: "center",
|
|
564
|
+
gap: 12
|
|
565
|
+
},
|
|
566
|
+
children: [
|
|
567
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 13, cursor: "pointer", whiteSpace: "nowrap" }, children: [
|
|
568
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
569
|
+
"input",
|
|
570
|
+
{
|
|
571
|
+
type: "checkbox",
|
|
572
|
+
checked: allSelected,
|
|
573
|
+
onChange: (e) => onSelectAll(e.target.checked)
|
|
574
|
+
}
|
|
575
|
+
),
|
|
576
|
+
"Select All"
|
|
577
|
+
] }),
|
|
578
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
579
|
+
"button",
|
|
580
|
+
{
|
|
581
|
+
onClick: onSubmit,
|
|
582
|
+
disabled: selectedDraftIds.size === 0,
|
|
583
|
+
style: {
|
|
584
|
+
flex: 1,
|
|
585
|
+
padding: "8px 12px",
|
|
586
|
+
borderRadius: 6,
|
|
587
|
+
border: "none",
|
|
588
|
+
background: selectedDraftIds.size === 0 ? "#93c5fd" : "#3b82f6",
|
|
589
|
+
color: "#fff",
|
|
590
|
+
fontSize: 13,
|
|
591
|
+
fontWeight: 500,
|
|
592
|
+
cursor: selectedDraftIds.size === 0 ? "default" : "pointer"
|
|
593
|
+
},
|
|
594
|
+
children: [
|
|
595
|
+
"Submit Feedback (",
|
|
596
|
+
selectedDraftIds.size,
|
|
597
|
+
")"
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
]
|
|
602
|
+
}
|
|
603
|
+
),
|
|
604
|
+
previewUrl && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
605
|
+
"div",
|
|
606
|
+
{
|
|
607
|
+
onClick: () => setPreviewUrl(null),
|
|
608
|
+
style: {
|
|
609
|
+
position: "fixed",
|
|
610
|
+
top: 0,
|
|
611
|
+
left: 0,
|
|
612
|
+
right: 0,
|
|
613
|
+
bottom: 0,
|
|
614
|
+
background: "rgba(0,0,0,0.6)",
|
|
615
|
+
zIndex: 99999,
|
|
616
|
+
display: "flex",
|
|
617
|
+
alignItems: "center",
|
|
618
|
+
justifyContent: "center"
|
|
619
|
+
},
|
|
620
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
621
|
+
"div",
|
|
622
|
+
{
|
|
623
|
+
onClick: (e) => e.stopPropagation(),
|
|
624
|
+
style: {
|
|
625
|
+
background: "#fff",
|
|
626
|
+
borderRadius: 8,
|
|
627
|
+
padding: 8,
|
|
628
|
+
maxWidth: "90vw",
|
|
629
|
+
maxHeight: "90vh",
|
|
630
|
+
position: "relative"
|
|
631
|
+
},
|
|
632
|
+
children: [
|
|
633
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
634
|
+
"button",
|
|
635
|
+
{
|
|
636
|
+
onClick: () => setPreviewUrl(null),
|
|
637
|
+
style: {
|
|
638
|
+
position: "absolute",
|
|
639
|
+
top: 4,
|
|
640
|
+
right: 8,
|
|
641
|
+
background: "none",
|
|
642
|
+
border: "none",
|
|
643
|
+
fontSize: 18,
|
|
644
|
+
cursor: "pointer",
|
|
645
|
+
color: "#6b7280",
|
|
646
|
+
zIndex: 1
|
|
647
|
+
},
|
|
648
|
+
children: "x"
|
|
649
|
+
}
|
|
650
|
+
),
|
|
651
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
652
|
+
"img",
|
|
653
|
+
{
|
|
654
|
+
src: previewUrl,
|
|
655
|
+
alt: "Screenshot preview",
|
|
656
|
+
style: { maxWidth: "85vw", maxHeight: "85vh", display: "block", borderRadius: 4 }
|
|
657
|
+
}
|
|
658
|
+
)
|
|
659
|
+
]
|
|
660
|
+
}
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
)
|
|
175
664
|
]
|
|
176
665
|
}
|
|
177
666
|
);
|
|
178
667
|
}
|
|
179
668
|
|
|
180
|
-
// src/components/
|
|
181
|
-
var
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
}
|
|
669
|
+
// src/components/DraftModal.tsx
|
|
670
|
+
var import_react4 = require("react");
|
|
671
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
672
|
+
var WIDGET_CONTAINER_ID2 = "llm-ui-feedback-root";
|
|
673
|
+
function DraftModal({ pendingContext, addingDraft, onAdd, onCancel }) {
|
|
674
|
+
const [comment, setComment] = (0, import_react4.useState)("");
|
|
675
|
+
(0, import_react4.useEffect)(() => {
|
|
676
|
+
const container = document.getElementById(WIDGET_CONTAINER_ID2);
|
|
677
|
+
const inerted = [];
|
|
678
|
+
for (const child of Array.from(document.body.children)) {
|
|
679
|
+
if (child === container || child.id === WIDGET_CONTAINER_ID2) continue;
|
|
680
|
+
if (!child.hasAttribute("inert")) {
|
|
681
|
+
child.setAttribute("inert", "");
|
|
682
|
+
inerted.push(child);
|
|
227
683
|
}
|
|
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
684
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
685
|
+
return () => {
|
|
686
|
+
for (const el of inerted) {
|
|
687
|
+
el.removeAttribute("inert");
|
|
688
|
+
}
|
|
284
689
|
};
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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);
|
|
690
|
+
}, []);
|
|
691
|
+
const handleAdd = () => {
|
|
692
|
+
if (comment.trim() && !addingDraft) {
|
|
693
|
+
onAdd(comment.trim());
|
|
302
694
|
}
|
|
303
695
|
};
|
|
304
|
-
const
|
|
305
|
-
|
|
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)(
|
|
696
|
+
const { context, screenshot } = pendingContext;
|
|
697
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
315
698
|
"div",
|
|
316
699
|
{
|
|
317
700
|
style: {
|
|
@@ -325,9 +708,9 @@ function FeedbackDialog({ context, screenshot, onClose }) {
|
|
|
325
708
|
fontFamily: "system-ui, sans-serif"
|
|
326
709
|
},
|
|
327
710
|
onClick: (e) => {
|
|
328
|
-
if (e.target === e.currentTarget)
|
|
711
|
+
if (e.target === e.currentTarget && !addingDraft) onCancel();
|
|
329
712
|
},
|
|
330
|
-
children: /* @__PURE__ */ (0,
|
|
713
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
331
714
|
"div",
|
|
332
715
|
{
|
|
333
716
|
style: {
|
|
@@ -336,47 +719,31 @@ function FeedbackDialog({ context, screenshot, onClose }) {
|
|
|
336
719
|
padding: 24,
|
|
337
720
|
width: 480,
|
|
338
721
|
maxWidth: "90vw",
|
|
339
|
-
maxHeight: "
|
|
722
|
+
maxHeight: "85vh",
|
|
340
723
|
overflow: "auto",
|
|
341
724
|
boxShadow: "0 8px 32px rgba(0,0,0,0.2)"
|
|
342
725
|
},
|
|
343
726
|
children: [
|
|
344
|
-
/* @__PURE__ */ (0,
|
|
345
|
-
|
|
346
|
-
/* @__PURE__ */ (0,
|
|
347
|
-
"
|
|
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:" }),
|
|
727
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { style: { margin: "0 0 16px", fontSize: 16 }, children: "New Feedback" }),
|
|
728
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { fontSize: 12, color: "#6b7280", marginBottom: 12 }, children: [
|
|
729
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
|
|
730
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Component:" }),
|
|
364
731
|
" ",
|
|
365
|
-
context.
|
|
732
|
+
context.componentPath
|
|
366
733
|
] }),
|
|
367
|
-
/* @__PURE__ */ (0,
|
|
368
|
-
/* @__PURE__ */ (0,
|
|
734
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Path:" }),
|
|
369
736
|
" ",
|
|
370
|
-
context.
|
|
737
|
+
context.urlPath
|
|
371
738
|
] }),
|
|
372
|
-
context.elementText && /* @__PURE__ */ (0,
|
|
373
|
-
/* @__PURE__ */ (0,
|
|
739
|
+
context.elementText && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
|
|
740
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Element:" }),
|
|
374
741
|
' "',
|
|
375
|
-
context.elementText.slice(0,
|
|
742
|
+
context.elementText.slice(0, 80),
|
|
376
743
|
'"'
|
|
377
744
|
] })
|
|
378
745
|
] }),
|
|
379
|
-
screenshot && /* @__PURE__ */ (0,
|
|
746
|
+
screenshot && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
380
747
|
"img",
|
|
381
748
|
{
|
|
382
749
|
src: screenshot,
|
|
@@ -385,64 +752,67 @@ function FeedbackDialog({ context, screenshot, onClose }) {
|
|
|
385
752
|
width: "100%",
|
|
386
753
|
borderRadius: 8,
|
|
387
754
|
border: "1px solid #e5e7eb",
|
|
388
|
-
marginBottom: 12
|
|
755
|
+
marginBottom: 12,
|
|
756
|
+
maxHeight: 180,
|
|
757
|
+
objectFit: "cover"
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
),
|
|
761
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
762
|
+
"textarea",
|
|
763
|
+
{
|
|
764
|
+
value: comment,
|
|
765
|
+
onChange: (e) => setComment(e.target.value),
|
|
766
|
+
placeholder: "Describe the issue or suggestion...",
|
|
767
|
+
autoFocus: true,
|
|
768
|
+
onKeyDown: (e) => {
|
|
769
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleAdd();
|
|
770
|
+
},
|
|
771
|
+
style: {
|
|
772
|
+
width: "100%",
|
|
773
|
+
minHeight: 80,
|
|
774
|
+
padding: 10,
|
|
775
|
+
borderRadius: 8,
|
|
776
|
+
border: "1px solid #d1d5db",
|
|
777
|
+
fontSize: 14,
|
|
778
|
+
resize: "vertical",
|
|
779
|
+
boxSizing: "border-box"
|
|
389
780
|
}
|
|
390
781
|
}
|
|
391
782
|
),
|
|
392
|
-
|
|
393
|
-
/* @__PURE__ */ (0,
|
|
394
|
-
"
|
|
783
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 16, justifyContent: "flex-end" }, children: [
|
|
784
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
785
|
+
"button",
|
|
395
786
|
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
placeholder: "Describe the issue or suggestion...",
|
|
399
|
-
autoFocus: true,
|
|
787
|
+
onClick: onCancel,
|
|
788
|
+
disabled: addingDraft,
|
|
400
789
|
style: {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
padding: 10,
|
|
404
|
-
borderRadius: 8,
|
|
790
|
+
padding: "8px 16px",
|
|
791
|
+
borderRadius: 6,
|
|
405
792
|
border: "1px solid #d1d5db",
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
793
|
+
background: "#f3f4f6",
|
|
794
|
+
color: "#374151",
|
|
795
|
+
fontSize: 13,
|
|
796
|
+
cursor: "pointer"
|
|
797
|
+
},
|
|
798
|
+
children: "Cancel"
|
|
410
799
|
}
|
|
411
800
|
),
|
|
412
|
-
|
|
413
|
-
|
|
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",
|
|
801
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
802
|
+
"button",
|
|
430
803
|
{
|
|
431
|
-
|
|
432
|
-
|
|
804
|
+
onClick: handleAdd,
|
|
805
|
+
disabled: !comment.trim() || addingDraft,
|
|
433
806
|
style: {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
fontSize:
|
|
440
|
-
|
|
441
|
-
resize: "vertical",
|
|
442
|
-
boxSizing: "border-box",
|
|
443
|
-
background: "#f9fafb"
|
|
807
|
+
padding: "8px 16px",
|
|
808
|
+
borderRadius: 6,
|
|
809
|
+
border: "none",
|
|
810
|
+
background: !comment.trim() || addingDraft ? "#93c5fd" : "#3b82f6",
|
|
811
|
+
color: "#fff",
|
|
812
|
+
fontSize: 13,
|
|
813
|
+
cursor: !comment.trim() || addingDraft ? "default" : "pointer"
|
|
444
814
|
},
|
|
445
|
-
|
|
815
|
+
children: addingDraft ? "Adding..." : "Add to Drafts"
|
|
446
816
|
}
|
|
447
817
|
)
|
|
448
818
|
] })
|
|
@@ -452,325 +822,112 @@ function FeedbackDialog({ context, screenshot, onClose }) {
|
|
|
452
822
|
}
|
|
453
823
|
);
|
|
454
824
|
}
|
|
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
825
|
|
|
558
|
-
// src/components/
|
|
559
|
-
var
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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;
|
|
826
|
+
// src/components/SubmitModal.tsx
|
|
827
|
+
var import_react5 = require("react");
|
|
828
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
829
|
+
function SubmitModal({ count, onSubmit, onCancel, submitting }) {
|
|
830
|
+
const [title, setTitle] = (0, import_react5.useState)("");
|
|
831
|
+
const handleSubmit = () => {
|
|
832
|
+
if (title.trim() && !submitting) {
|
|
833
|
+
onSubmit(title.trim());
|
|
607
834
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
refreshEntries();
|
|
611
|
-
}, [confirmClear, refreshEntries]);
|
|
612
|
-
const sortedEntries = [...entries].reverse();
|
|
613
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
835
|
+
};
|
|
836
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
614
837
|
"div",
|
|
615
838
|
{
|
|
616
839
|
style: {
|
|
617
840
|
position: "fixed",
|
|
618
|
-
|
|
619
|
-
[isRight ? "right" : "left"]: 0,
|
|
620
|
-
bottom: 0,
|
|
621
|
-
width: 380,
|
|
622
|
-
maxWidth: "90vw",
|
|
841
|
+
inset: 0,
|
|
623
842
|
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
843
|
display: "flex",
|
|
627
|
-
|
|
844
|
+
alignItems: "center",
|
|
845
|
+
justifyContent: "center",
|
|
846
|
+
background: "rgba(0,0,0,0.4)",
|
|
628
847
|
fontFamily: "system-ui, sans-serif"
|
|
629
848
|
},
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
849
|
+
onClick: (e) => {
|
|
850
|
+
if (e.target === e.currentTarget && !submitting) onCancel();
|
|
851
|
+
},
|
|
852
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
853
|
+
"div",
|
|
854
|
+
{
|
|
855
|
+
style: {
|
|
856
|
+
background: "#fff",
|
|
857
|
+
borderRadius: 12,
|
|
858
|
+
padding: 24,
|
|
859
|
+
width: 400,
|
|
860
|
+
maxWidth: "90vw",
|
|
861
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.2)"
|
|
862
|
+
},
|
|
863
|
+
children: [
|
|
864
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { style: { margin: "0 0 16px", fontSize: 16 }, children: "Submit Feedback" }),
|
|
865
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { style: { margin: "0 0 12px", fontSize: 14, color: "#6b7280" }, children: [
|
|
866
|
+
count,
|
|
867
|
+
" item",
|
|
868
|
+
count !== 1 ? "s" : "",
|
|
869
|
+
" selected. Name this feedback session:"
|
|
870
|
+
] }),
|
|
871
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
872
|
+
"input",
|
|
873
|
+
{
|
|
874
|
+
type: "text",
|
|
875
|
+
value: title,
|
|
876
|
+
onChange: (e) => setTitle(e.target.value),
|
|
877
|
+
placeholder: "e.g. Homepage redesign feedback",
|
|
878
|
+
autoFocus: true,
|
|
879
|
+
onKeyDown: (e) => {
|
|
880
|
+
if (e.key === "Enter") handleSubmit();
|
|
881
|
+
},
|
|
882
|
+
style: {
|
|
883
|
+
width: "100%",
|
|
884
|
+
padding: 10,
|
|
885
|
+
borderRadius: 8,
|
|
886
|
+
border: "1px solid #d1d5db",
|
|
887
|
+
fontSize: 14,
|
|
888
|
+
boxSizing: "border-box"
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
),
|
|
892
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 16, justifyContent: "flex-end" }, children: [
|
|
893
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
649
894
|
"button",
|
|
650
895
|
{
|
|
651
|
-
onClick:
|
|
896
|
+
onClick: onCancel,
|
|
897
|
+
disabled: submitting,
|
|
652
898
|
style: {
|
|
653
|
-
|
|
899
|
+
padding: "8px 16px",
|
|
900
|
+
borderRadius: 6,
|
|
901
|
+
border: "1px solid #d1d5db",
|
|
902
|
+
background: "#f3f4f6",
|
|
903
|
+
color: "#374151",
|
|
904
|
+
fontSize: 13,
|
|
905
|
+
cursor: "pointer"
|
|
906
|
+
},
|
|
907
|
+
children: "Cancel"
|
|
908
|
+
}
|
|
909
|
+
),
|
|
910
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
911
|
+
"button",
|
|
912
|
+
{
|
|
913
|
+
onClick: handleSubmit,
|
|
914
|
+
disabled: !title.trim() || submitting,
|
|
915
|
+
style: {
|
|
916
|
+
padding: "8px 16px",
|
|
917
|
+
borderRadius: 6,
|
|
654
918
|
border: "none",
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
919
|
+
background: !title.trim() || submitting ? "#93c5fd" : "#3b82f6",
|
|
920
|
+
color: "#fff",
|
|
921
|
+
fontSize: 13,
|
|
922
|
+
cursor: !title.trim() || submitting ? "default" : "pointer"
|
|
659
923
|
},
|
|
660
|
-
children: "
|
|
924
|
+
children: submitting ? "Submitting..." : "Submit"
|
|
661
925
|
}
|
|
662
926
|
)
|
|
663
|
-
]
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
]
|
|
927
|
+
] })
|
|
928
|
+
]
|
|
929
|
+
}
|
|
930
|
+
)
|
|
774
931
|
}
|
|
775
932
|
);
|
|
776
933
|
}
|
|
@@ -820,31 +977,247 @@ function getComponentPath(el, maxDepth = 8) {
|
|
|
820
977
|
|
|
821
978
|
// src/utils/screenshot.ts
|
|
822
979
|
var import_html2canvas_pro = __toESM(require("html2canvas-pro"));
|
|
823
|
-
async function captureScreenshot(
|
|
824
|
-
const
|
|
825
|
-
const docY = clickY + window.scrollY;
|
|
826
|
-
const canvas = await (0, import_html2canvas_pro.default)(document.body, {
|
|
980
|
+
async function captureScreenshot(rect) {
|
|
981
|
+
const fullCanvas = await (0, import_html2canvas_pro.default)(document.body, {
|
|
827
982
|
logging: false,
|
|
828
983
|
useCORS: true
|
|
829
984
|
});
|
|
830
|
-
const
|
|
985
|
+
const docX = rect.x + window.scrollX;
|
|
986
|
+
const docY = rect.y + window.scrollY;
|
|
987
|
+
const scaleX = fullCanvas.width / document.documentElement.scrollWidth;
|
|
988
|
+
const scaleY = fullCanvas.height / document.documentElement.scrollHeight;
|
|
989
|
+
const sx = Math.round(docX * scaleX);
|
|
990
|
+
const sy = Math.round(docY * scaleY);
|
|
991
|
+
const sw = Math.round(rect.width * scaleX);
|
|
992
|
+
const sh = Math.round(rect.height * scaleY);
|
|
993
|
+
const cropped = document.createElement("canvas");
|
|
994
|
+
cropped.width = sw;
|
|
995
|
+
cropped.height = sh;
|
|
996
|
+
const ctx = cropped.getContext("2d");
|
|
831
997
|
if (ctx) {
|
|
832
|
-
ctx.
|
|
998
|
+
ctx.drawImage(fullCanvas, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
999
|
+
ctx.strokeStyle = "#3b82f6";
|
|
833
1000
|
ctx.lineWidth = 3;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1001
|
+
ctx.strokeRect(1, 1, sw - 2, sh - 2);
|
|
1002
|
+
}
|
|
1003
|
+
return cropped.toDataURL("image/png");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/utils/prompt.ts
|
|
1007
|
+
function buildPrompt(context, comment) {
|
|
1008
|
+
const propsStr = context.components.filter((c) => Object.keys(c.props).length > 0).map((c) => ` ${c.name}: ${JSON.stringify(c.props)}`).join("\n");
|
|
1009
|
+
const lines = [
|
|
1010
|
+
`Page: ${context.url}`,
|
|
1011
|
+
`Path: ${context.urlPath}`,
|
|
1012
|
+
`Reported at: ${context.reportedAt}`,
|
|
1013
|
+
`Component: ${context.componentPath}`
|
|
1014
|
+
];
|
|
1015
|
+
if (propsStr) {
|
|
1016
|
+
lines.push(`Props:
|
|
1017
|
+
${propsStr}`);
|
|
1018
|
+
}
|
|
1019
|
+
if (context.elementText) {
|
|
1020
|
+
lines.push(`Element text: "${context.elementText}"`);
|
|
1021
|
+
}
|
|
1022
|
+
lines.push("", `User feedback: "${comment}"`, "", "Screenshot attached showing the selected region.");
|
|
1023
|
+
return lines.join("\n");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/utils/blob.ts
|
|
1027
|
+
function dataUrlToBlob(dataUrl) {
|
|
1028
|
+
const [header, base64] = dataUrl.split(",");
|
|
1029
|
+
const mime = header.match(/:(.*?);/)?.[1] || "image/png";
|
|
1030
|
+
const binary = atob(base64);
|
|
1031
|
+
const bytes = new Uint8Array(binary.length);
|
|
1032
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1033
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1034
|
+
}
|
|
1035
|
+
return new Blob([bytes], { type: mime });
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/hooks/useSession.ts
|
|
1039
|
+
var import_react6 = require("react");
|
|
1040
|
+
var import_meta = {};
|
|
1041
|
+
var SESSION_TOKEN_KEY = "llm_feedback_session_token";
|
|
1042
|
+
var USER_KEY = "llm_feedback_user";
|
|
1043
|
+
var AUTH_BYPASS = typeof globalThis !== "undefined" && globalThis.__FEEDBACK_AUTH_BYPASS__ || typeof import_meta !== "undefined" && import_meta.env?.VITE_AUTH_BYPASS === "true";
|
|
1044
|
+
function getSessionToken() {
|
|
1045
|
+
if (AUTH_BYPASS) return "bypass-token";
|
|
1046
|
+
return localStorage.getItem(SESSION_TOKEN_KEY);
|
|
1047
|
+
}
|
|
1048
|
+
var BYPASS_USER = {
|
|
1049
|
+
email: "bypass@test.local",
|
|
1050
|
+
name: "Bypass User",
|
|
1051
|
+
picture: "",
|
|
1052
|
+
sub: "bypass-user"
|
|
1053
|
+
};
|
|
1054
|
+
function useSession(apiUrl, clientId) {
|
|
1055
|
+
const [status, setStatus] = (0, import_react6.useState)(AUTH_BYPASS ? "authenticated" : "loading");
|
|
1056
|
+
const [user, setUser] = (0, import_react6.useState)(AUTH_BYPASS ? BYPASS_USER : null);
|
|
1057
|
+
const pendingAuthRef = (0, import_react6.useRef)(false);
|
|
1058
|
+
(0, import_react6.useEffect)(() => {
|
|
1059
|
+
if (AUTH_BYPASS) return;
|
|
1060
|
+
let cancelled = false;
|
|
1061
|
+
async function checkSession() {
|
|
1062
|
+
const token = localStorage.getItem(SESSION_TOKEN_KEY);
|
|
1063
|
+
const savedUser = localStorage.getItem(USER_KEY);
|
|
1064
|
+
if (!token || !savedUser) {
|
|
1065
|
+
if (!cancelled) setStatus("unauthenticated");
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const res = await fetch(`${apiUrl}/auth/status`, {
|
|
1070
|
+
headers: { "X-Session-Token": token }
|
|
1071
|
+
});
|
|
1072
|
+
const data = await res.json();
|
|
1073
|
+
if (!cancelled) {
|
|
1074
|
+
if (data.authenticated) {
|
|
1075
|
+
setUser(data.user);
|
|
1076
|
+
setStatus("authenticated");
|
|
1077
|
+
} else {
|
|
1078
|
+
localStorage.removeItem(SESSION_TOKEN_KEY);
|
|
1079
|
+
localStorage.removeItem(USER_KEY);
|
|
1080
|
+
setStatus("unauthenticated");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
} catch {
|
|
1084
|
+
if (!cancelled) {
|
|
1085
|
+
try {
|
|
1086
|
+
setUser(JSON.parse(savedUser));
|
|
1087
|
+
setStatus("authenticated");
|
|
1088
|
+
} catch {
|
|
1089
|
+
setStatus("unauthenticated");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
checkSession();
|
|
1095
|
+
return () => {
|
|
1096
|
+
cancelled = true;
|
|
1097
|
+
};
|
|
1098
|
+
}, [apiUrl]);
|
|
1099
|
+
(0, import_react6.useEffect)(() => {
|
|
1100
|
+
function handleMessage(event) {
|
|
1101
|
+
if (event.data?.type !== "feedback-auth") return;
|
|
1102
|
+
const { sessionToken, user: userData } = event.data;
|
|
1103
|
+
if (sessionToken && userData) {
|
|
1104
|
+
localStorage.setItem(SESSION_TOKEN_KEY, sessionToken);
|
|
1105
|
+
localStorage.setItem(USER_KEY, JSON.stringify(userData));
|
|
1106
|
+
setUser(userData);
|
|
1107
|
+
setStatus("authenticated");
|
|
1108
|
+
pendingAuthRef.current = false;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
window.addEventListener("message", handleMessage);
|
|
1112
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
1113
|
+
}, []);
|
|
1114
|
+
const signIn = (0, import_react6.useCallback)(() => {
|
|
1115
|
+
if (pendingAuthRef.current) return;
|
|
1116
|
+
pendingAuthRef.current = true;
|
|
1117
|
+
const loginUrl = `${apiUrl}/auth/login?clientId=${encodeURIComponent(clientId)}&origin=${encodeURIComponent(window.location.origin)}`;
|
|
1118
|
+
const popup = window.open(loginUrl, "feedback-auth", "width=500,height=600,menubar=no,toolbar=no");
|
|
1119
|
+
if (!popup) {
|
|
1120
|
+
pendingAuthRef.current = false;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const interval = setInterval(() => {
|
|
1124
|
+
if (popup.closed) {
|
|
1125
|
+
clearInterval(interval);
|
|
1126
|
+
pendingAuthRef.current = false;
|
|
1127
|
+
}
|
|
1128
|
+
}, 500);
|
|
1129
|
+
}, [apiUrl, clientId]);
|
|
1130
|
+
const signOut = (0, import_react6.useCallback)(async () => {
|
|
1131
|
+
const token = localStorage.getItem(SESSION_TOKEN_KEY);
|
|
1132
|
+
localStorage.removeItem(SESSION_TOKEN_KEY);
|
|
1133
|
+
localStorage.removeItem(USER_KEY);
|
|
1134
|
+
setUser(null);
|
|
1135
|
+
setStatus("unauthenticated");
|
|
1136
|
+
if (token) {
|
|
1137
|
+
try {
|
|
1138
|
+
await fetch(`${apiUrl}/auth/logout`, {
|
|
1139
|
+
method: "POST",
|
|
1140
|
+
headers: { "X-Session-Token": token }
|
|
1141
|
+
});
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}, [apiUrl]);
|
|
1146
|
+
return { status, user, signIn, signOut };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/api/client.ts
|
|
1150
|
+
function createApiClient(apiUrl, clientId) {
|
|
1151
|
+
async function apiFetch(path, options = {}) {
|
|
1152
|
+
const token = getSessionToken();
|
|
1153
|
+
const res = await fetch(`${apiUrl}${path}`, {
|
|
1154
|
+
...options,
|
|
1155
|
+
headers: {
|
|
1156
|
+
"Content-Type": "application/json",
|
|
1157
|
+
...token ? { "X-Session-Token": token } : {},
|
|
1158
|
+
"X-Client-Id": clientId,
|
|
1159
|
+
...options.headers
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
if (!res.ok) {
|
|
1163
|
+
const body = await res.json().catch(() => ({}));
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
body.error || `API error ${res.status}`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (res.status === 204) return void 0;
|
|
1169
|
+
return res.json();
|
|
844
1170
|
}
|
|
845
|
-
return
|
|
1171
|
+
return {
|
|
1172
|
+
async fetchDrafts() {
|
|
1173
|
+
const data = await apiFetch("/drafts");
|
|
1174
|
+
return data.items;
|
|
1175
|
+
},
|
|
1176
|
+
async createDraft(draft) {
|
|
1177
|
+
return apiFetch("/drafts", {
|
|
1178
|
+
method: "POST",
|
|
1179
|
+
body: JSON.stringify(draft)
|
|
1180
|
+
});
|
|
1181
|
+
},
|
|
1182
|
+
async updateDraft(id, data) {
|
|
1183
|
+
return apiFetch(`/drafts/${id}`, {
|
|
1184
|
+
method: "PATCH",
|
|
1185
|
+
body: JSON.stringify(data)
|
|
1186
|
+
});
|
|
1187
|
+
},
|
|
1188
|
+
async deleteDraft(id) {
|
|
1189
|
+
await apiFetch(`/drafts/${id}`, { method: "DELETE" });
|
|
1190
|
+
},
|
|
1191
|
+
async submitSession(data) {
|
|
1192
|
+
return apiFetch("/feedback-groups/submit", {
|
|
1193
|
+
method: "POST",
|
|
1194
|
+
body: JSON.stringify(data)
|
|
1195
|
+
});
|
|
1196
|
+
},
|
|
1197
|
+
async getUploadUrl(entryId, timestamp) {
|
|
1198
|
+
return apiFetch("/upload-url", {
|
|
1199
|
+
method: "POST",
|
|
1200
|
+
body: JSON.stringify({ entryId, timestamp })
|
|
1201
|
+
});
|
|
1202
|
+
},
|
|
1203
|
+
async getScreenshotUrl(draftId) {
|
|
1204
|
+
const data = await apiFetch(`/drafts/${draftId}/screenshot-url`);
|
|
1205
|
+
return data.url;
|
|
1206
|
+
},
|
|
1207
|
+
async uploadScreenshot(uploadUrl, blob) {
|
|
1208
|
+
const res = await fetch(uploadUrl, {
|
|
1209
|
+
method: "PUT",
|
|
1210
|
+
body: blob,
|
|
1211
|
+
headers: { "Content-Type": "image/png" }
|
|
1212
|
+
});
|
|
1213
|
+
if (!res.ok) throw new Error(`S3 upload failed: ${res.status}`);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
846
1216
|
}
|
|
847
1217
|
|
|
1218
|
+
// src/config.ts
|
|
1219
|
+
var DEFAULT_API_URL = "https://your-lambda-function-url.lambda-url.us-east-1.on.aws";
|
|
1220
|
+
|
|
848
1221
|
// src/components/FeedbackWidget.tsx
|
|
849
1222
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
850
1223
|
var CONTAINER_ID = "llm-ui-feedback-root";
|
|
@@ -857,17 +1230,69 @@ function getOrCreateContainer() {
|
|
|
857
1230
|
}
|
|
858
1231
|
return el;
|
|
859
1232
|
}
|
|
1233
|
+
var initialState = {
|
|
1234
|
+
panelOpen: false,
|
|
1235
|
+
picking: false,
|
|
1236
|
+
pendingContext: null,
|
|
1237
|
+
drafts: [],
|
|
1238
|
+
selectedDraftIds: /* @__PURE__ */ new Set(),
|
|
1239
|
+
submitModalOpen: false,
|
|
1240
|
+
addingDraft: false,
|
|
1241
|
+
submitting: false
|
|
1242
|
+
};
|
|
860
1243
|
function widgetReducer(state, action) {
|
|
861
1244
|
switch (action.type) {
|
|
1245
|
+
case "TOGGLE_PANEL":
|
|
1246
|
+
return state.panelOpen ? { ...state, panelOpen: false, picking: false, pendingContext: null } : { ...state, panelOpen: true };
|
|
1247
|
+
case "OPEN_PANEL":
|
|
1248
|
+
return { ...state, panelOpen: true };
|
|
1249
|
+
case "CLOSE_PANEL":
|
|
1250
|
+
return { ...state, panelOpen: false, picking: false, pendingContext: null };
|
|
862
1251
|
case "START_PICKING":
|
|
863
|
-
return
|
|
864
|
-
case "
|
|
865
|
-
|
|
866
|
-
|
|
1252
|
+
return { ...state, picking: true };
|
|
1253
|
+
case "CANCEL_PICKING":
|
|
1254
|
+
return { ...state, picking: false };
|
|
1255
|
+
case "CANCEL_DRAFT_MODAL":
|
|
1256
|
+
return { ...state, pendingContext: null, addingDraft: false };
|
|
867
1257
|
case "ELEMENT_PICKED":
|
|
868
|
-
return {
|
|
869
|
-
case "
|
|
870
|
-
return state
|
|
1258
|
+
return { ...state, picking: false, pendingContext: { context: action.context, screenshot: action.screenshot } };
|
|
1259
|
+
case "SET_ADDING_DRAFT":
|
|
1260
|
+
return { ...state, addingDraft: action.adding };
|
|
1261
|
+
case "DRAFT_ADDED":
|
|
1262
|
+
return { ...state, pendingContext: null, addingDraft: false, drafts: [action.draft, ...state.drafts] };
|
|
1263
|
+
case "SET_DRAFTS":
|
|
1264
|
+
return { ...state, drafts: action.drafts };
|
|
1265
|
+
case "DRAFT_UPDATED":
|
|
1266
|
+
return { ...state, drafts: state.drafts.map((d) => d.id === action.id ? { ...d, comment: action.comment } : d) };
|
|
1267
|
+
case "DRAFT_DELETED": {
|
|
1268
|
+
const next = new Set(state.selectedDraftIds);
|
|
1269
|
+
next.delete(action.id);
|
|
1270
|
+
return { ...state, drafts: state.drafts.filter((d) => d.id !== action.id), selectedDraftIds: next };
|
|
1271
|
+
}
|
|
1272
|
+
case "TOGGLE_SELECT": {
|
|
1273
|
+
const next = new Set(state.selectedDraftIds);
|
|
1274
|
+
if (next.has(action.id)) next.delete(action.id);
|
|
1275
|
+
else next.add(action.id);
|
|
1276
|
+
return { ...state, selectedDraftIds: next };
|
|
1277
|
+
}
|
|
1278
|
+
case "SELECT_ALL":
|
|
1279
|
+
return { ...state, selectedDraftIds: action.selected ? new Set(state.drafts.map((d) => d.id)) : /* @__PURE__ */ new Set() };
|
|
1280
|
+
case "OPEN_SUBMIT_MODAL":
|
|
1281
|
+
return { ...state, submitModalOpen: true };
|
|
1282
|
+
case "CLOSE_SUBMIT_MODAL":
|
|
1283
|
+
return { ...state, submitModalOpen: false };
|
|
1284
|
+
case "SET_SUBMITTING":
|
|
1285
|
+
return { ...state, submitting: action.submitting };
|
|
1286
|
+
case "SUBMIT_COMPLETE": {
|
|
1287
|
+
const submitted = new Set(action.submittedIds);
|
|
1288
|
+
return {
|
|
1289
|
+
...state,
|
|
1290
|
+
submitting: false,
|
|
1291
|
+
submitModalOpen: false,
|
|
1292
|
+
drafts: state.drafts.filter((d) => !submitted.has(d.id)),
|
|
1293
|
+
selectedDraftIds: /* @__PURE__ */ new Set()
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
871
1296
|
default:
|
|
872
1297
|
return state;
|
|
873
1298
|
}
|
|
@@ -891,102 +1316,221 @@ function isEditableTarget(el) {
|
|
|
891
1316
|
return false;
|
|
892
1317
|
}
|
|
893
1318
|
function FeedbackWidget({
|
|
1319
|
+
clientId,
|
|
1320
|
+
apiUrl = DEFAULT_API_URL,
|
|
894
1321
|
position = "bottom-right",
|
|
895
1322
|
buttonColor = "#3b82f6",
|
|
896
1323
|
hotkey
|
|
897
|
-
}
|
|
898
|
-
const
|
|
899
|
-
const [
|
|
900
|
-
(0,
|
|
901
|
-
|
|
902
|
-
|
|
1324
|
+
}) {
|
|
1325
|
+
const session = useSession(apiUrl, clientId);
|
|
1326
|
+
const [state, dispatch] = (0, import_react7.useReducer)(widgetReducer, initialState);
|
|
1327
|
+
const pendingOpenRef = (0, import_react7.useRef)(false);
|
|
1328
|
+
const api = (0, import_react7.useMemo)(
|
|
1329
|
+
() => createApiClient(apiUrl, clientId),
|
|
1330
|
+
[apiUrl, clientId]
|
|
1331
|
+
);
|
|
1332
|
+
(0, import_react7.useEffect)(() => {
|
|
1333
|
+
if (session.status === "authenticated" && pendingOpenRef.current) {
|
|
1334
|
+
pendingOpenRef.current = false;
|
|
1335
|
+
dispatch({ type: "OPEN_PANEL" });
|
|
903
1336
|
}
|
|
904
|
-
}, [
|
|
905
|
-
(0,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1337
|
+
}, [session.status]);
|
|
1338
|
+
(0, import_react7.useEffect)(() => {
|
|
1339
|
+
if (session.status === "authenticated") {
|
|
1340
|
+
api.fetchDrafts().then((drafts) => {
|
|
1341
|
+
dispatch({ type: "SET_DRAFTS", drafts });
|
|
1342
|
+
}).catch(() => {
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
}, [state.panelOpen, session.status, api]);
|
|
1346
|
+
(0, import_react7.useEffect)(() => {
|
|
909
1347
|
if (!hotkey) return;
|
|
910
1348
|
const parsed = parseHotkey(hotkey);
|
|
911
1349
|
function handler(e) {
|
|
912
1350
|
if (isEditableTarget(document.activeElement)) return;
|
|
913
1351
|
if (e.key.toLowerCase() === parsed.key && e.ctrlKey === parsed.ctrl && e.altKey === parsed.alt && e.shiftKey === parsed.shift && e.metaKey === parsed.meta) {
|
|
914
1352
|
e.preventDefault();
|
|
915
|
-
dispatch(
|
|
916
|
-
state.mode === "picking" ? { type: "CANCEL" } : state.mode === "idle" ? { type: "START_PICKING" } : { type: "CLOSE" }
|
|
917
|
-
);
|
|
1353
|
+
dispatch({ type: "TOGGLE_PANEL" });
|
|
918
1354
|
}
|
|
919
1355
|
}
|
|
920
1356
|
window.addEventListener("keydown", handler);
|
|
921
1357
|
return () => window.removeEventListener("keydown", handler);
|
|
922
|
-
}, [hotkey
|
|
923
|
-
const
|
|
1358
|
+
}, [hotkey]);
|
|
1359
|
+
const handlePickClick = (0, import_react7.useCallback)(() => {
|
|
1360
|
+
if (session.status === "unauthenticated") {
|
|
1361
|
+
pendingOpenRef.current = true;
|
|
1362
|
+
session.signIn();
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
924
1365
|
dispatch({ type: "START_PICKING" });
|
|
925
|
-
}, []);
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1366
|
+
}, [session]);
|
|
1367
|
+
const handlePanelToggle = (0, import_react7.useCallback)(() => {
|
|
1368
|
+
if (session.status === "unauthenticated") {
|
|
1369
|
+
pendingOpenRef.current = true;
|
|
1370
|
+
session.signIn();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
dispatch({ type: "TOGGLE_PANEL" });
|
|
1374
|
+
}, [session]);
|
|
1375
|
+
const handlePick = (0, import_react7.useCallback)(async (rect, elements) => {
|
|
1376
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1377
|
+
const allComponents = [];
|
|
1378
|
+
for (const el of elements) {
|
|
1379
|
+
for (const comp of getComponentPath(el)) {
|
|
1380
|
+
if (!seenNames.has(comp.name)) {
|
|
1381
|
+
seenNames.add(comp.name);
|
|
1382
|
+
allComponents.push(comp);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const componentPath = allComponents.map((c) => c.name).join(", ") || "(no React component found)";
|
|
1387
|
+
const textParts = /* @__PURE__ */ new Set();
|
|
1388
|
+
for (const el of elements) {
|
|
1389
|
+
const raw = (el.innerText || el.textContent || "").trim();
|
|
1390
|
+
if (raw) textParts.add(raw);
|
|
1391
|
+
}
|
|
1392
|
+
const combined = Array.from(textParts).join(" ").trim();
|
|
1393
|
+
const elementText = combined.length > 200 ? combined.slice(0, 200) + "..." : combined;
|
|
1394
|
+
const centerX = rect.x + rect.width / 2;
|
|
1395
|
+
const centerY = rect.y + rect.height / 2;
|
|
931
1396
|
const context = {
|
|
932
1397
|
url: window.location.href,
|
|
1398
|
+
urlPath: window.location.pathname,
|
|
1399
|
+
reportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
933
1400
|
componentPath,
|
|
934
|
-
components,
|
|
1401
|
+
components: allComponents,
|
|
935
1402
|
elementText,
|
|
936
|
-
clickX:
|
|
937
|
-
clickY:
|
|
1403
|
+
clickX: centerX,
|
|
1404
|
+
clickY: centerY
|
|
938
1405
|
};
|
|
1406
|
+
const isDrag = rect.width > 20 && rect.height > 20;
|
|
939
1407
|
let screenshot = null;
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1408
|
+
if (isDrag) {
|
|
1409
|
+
try {
|
|
1410
|
+
screenshot = await captureScreenshot(rect);
|
|
1411
|
+
} catch {
|
|
1412
|
+
}
|
|
943
1413
|
}
|
|
944
1414
|
dispatch({ type: "ELEMENT_PICKED", context, screenshot });
|
|
945
1415
|
}, []);
|
|
946
|
-
const
|
|
947
|
-
dispatch({ type: "
|
|
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" });
|
|
1416
|
+
const handleCancelPicking = (0, import_react7.useCallback)(() => {
|
|
1417
|
+
dispatch({ type: "CANCEL_PICKING" });
|
|
957
1418
|
}, []);
|
|
1419
|
+
const handleAddDraft = (0, import_react7.useCallback)(async (comment) => {
|
|
1420
|
+
if (!state.pendingContext) return;
|
|
1421
|
+
dispatch({ type: "SET_ADDING_DRAFT", adding: true });
|
|
1422
|
+
const { context, screenshot } = state.pendingContext;
|
|
1423
|
+
const entryId = `f_${Date.now()}`;
|
|
1424
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1425
|
+
let screenshotKey;
|
|
1426
|
+
if (screenshot) {
|
|
1427
|
+
const blob = dataUrlToBlob(screenshot);
|
|
1428
|
+
try {
|
|
1429
|
+
const { uploadUrl, key } = await api.getUploadUrl(entryId, timestamp);
|
|
1430
|
+
await api.uploadScreenshot(uploadUrl, blob);
|
|
1431
|
+
screenshotKey = key;
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
const draft = await api.createDraft({
|
|
1437
|
+
id: entryId,
|
|
1438
|
+
url: context.url,
|
|
1439
|
+
componentPath: context.componentPath,
|
|
1440
|
+
components: context.components,
|
|
1441
|
+
elementText: context.elementText,
|
|
1442
|
+
comment,
|
|
1443
|
+
prompt: buildPrompt(context, comment),
|
|
1444
|
+
...screenshotKey ? { screenshotKey } : {}
|
|
1445
|
+
});
|
|
1446
|
+
dispatch({ type: "DRAFT_ADDED", draft });
|
|
1447
|
+
} catch {
|
|
1448
|
+
dispatch({ type: "SET_ADDING_DRAFT", adding: false });
|
|
1449
|
+
}
|
|
1450
|
+
}, [state.pendingContext, api]);
|
|
1451
|
+
const handleUpdateDraft = (0, import_react7.useCallback)(async (id, comment) => {
|
|
1452
|
+
dispatch({ type: "DRAFT_UPDATED", id, comment });
|
|
1453
|
+
try {
|
|
1454
|
+
await api.updateDraft(id, { comment });
|
|
1455
|
+
} catch {
|
|
1456
|
+
api.fetchDrafts().then((drafts) => dispatch({ type: "SET_DRAFTS", drafts })).catch(() => {
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}, [api]);
|
|
1460
|
+
const handleDeleteDraft = (0, import_react7.useCallback)(async (id) => {
|
|
1461
|
+
dispatch({ type: "DRAFT_DELETED", id });
|
|
1462
|
+
try {
|
|
1463
|
+
await api.deleteDraft(id);
|
|
1464
|
+
} catch {
|
|
1465
|
+
api.fetchDrafts().then((drafts) => dispatch({ type: "SET_DRAFTS", drafts })).catch(() => {
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}, [api]);
|
|
1469
|
+
const handleSubmit = (0, import_react7.useCallback)(async (title) => {
|
|
1470
|
+
dispatch({ type: "SET_SUBMITTING", submitting: true });
|
|
1471
|
+
const draftIds = Array.from(state.selectedDraftIds);
|
|
1472
|
+
try {
|
|
1473
|
+
await api.submitSession({ title, draftIds });
|
|
1474
|
+
dispatch({ type: "SUBMIT_COMPLETE", submittedIds: draftIds });
|
|
1475
|
+
} catch {
|
|
1476
|
+
dispatch({ type: "SET_SUBMITTING", submitting: false });
|
|
1477
|
+
}
|
|
1478
|
+
}, [state.selectedDraftIds, api]);
|
|
1479
|
+
if (session.status === "loading") return null;
|
|
958
1480
|
return (0, import_react_dom.createPortal)(
|
|
959
1481
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
960
|
-
|
|
961
|
-
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1483
|
+
FloatingButton,
|
|
962
1484
|
{
|
|
963
|
-
|
|
964
|
-
|
|
1485
|
+
onPickClick: handlePickClick,
|
|
1486
|
+
onPanelToggle: handlePanelToggle,
|
|
1487
|
+
draftCount: state.drafts.length,
|
|
1488
|
+
panelOpen: state.panelOpen,
|
|
965
1489
|
position,
|
|
966
1490
|
buttonColor
|
|
967
1491
|
}
|
|
968
1492
|
),
|
|
969
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
970
|
-
|
|
1493
|
+
state.picking && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PickOverlay, { onPick: handlePick, onCancel: handleCancelPicking }),
|
|
1494
|
+
state.panelOpen && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1495
|
+
SidePanel,
|
|
971
1496
|
{
|
|
972
|
-
onClick: state.mode === "picking" ? handleCancel : handleToggle,
|
|
973
|
-
active: state.mode === "picking",
|
|
974
1497
|
position,
|
|
975
|
-
|
|
1498
|
+
drafts: state.drafts,
|
|
1499
|
+
selectedDraftIds: state.selectedDraftIds,
|
|
1500
|
+
onToggleSelect: (id) => dispatch({ type: "TOGGLE_SELECT", id }),
|
|
1501
|
+
onSelectAll: (selected) => dispatch({ type: "SELECT_ALL", selected }),
|
|
1502
|
+
onUpdateDraft: handleUpdateDraft,
|
|
1503
|
+
onDeleteDraft: handleDeleteDraft,
|
|
1504
|
+
onSubmit: () => dispatch({ type: "OPEN_SUBMIT_MODAL" }),
|
|
1505
|
+
onClose: () => dispatch({ type: "CLOSE_PANEL" }),
|
|
1506
|
+
api,
|
|
1507
|
+
user: session.user
|
|
1508
|
+
}
|
|
1509
|
+
),
|
|
1510
|
+
state.pendingContext && !state.picking && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1511
|
+
DraftModal,
|
|
1512
|
+
{
|
|
1513
|
+
pendingContext: state.pendingContext,
|
|
1514
|
+
addingDraft: state.addingDraft,
|
|
1515
|
+
onAdd: handleAddDraft,
|
|
1516
|
+
onCancel: () => dispatch({ type: "CANCEL_DRAFT_MODAL" })
|
|
976
1517
|
}
|
|
977
1518
|
),
|
|
978
|
-
state.
|
|
979
|
-
|
|
980
|
-
|
|
1519
|
+
state.submitModalOpen && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1520
|
+
SubmitModal,
|
|
1521
|
+
{
|
|
1522
|
+
count: state.selectedDraftIds.size,
|
|
1523
|
+
onSubmit: handleSubmit,
|
|
1524
|
+
onCancel: () => dispatch({ type: "CLOSE_SUBMIT_MODAL" }),
|
|
1525
|
+
submitting: state.submitting
|
|
1526
|
+
}
|
|
1527
|
+
)
|
|
981
1528
|
] }),
|
|
982
1529
|
getOrCreateContainer()
|
|
983
1530
|
);
|
|
984
1531
|
}
|
|
985
1532
|
// Annotate the CommonJS export names for ESM import in node:
|
|
986
1533
|
0 && (module.exports = {
|
|
987
|
-
FeedbackWidget
|
|
988
|
-
clearEntries,
|
|
989
|
-
deleteEntry,
|
|
990
|
-
loadEntries
|
|
1534
|
+
FeedbackWidget
|
|
991
1535
|
});
|
|
992
1536
|
//# sourceMappingURL=index.js.map
|