@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/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 import_react4 = require("react");
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({ onClick, active, position, buttonColor }) {
55
+ function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, position, buttonColor }) {
59
56
  const isBottom = position.includes("bottom");
60
57
  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
- );
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 getElementBeneath(x, y) {
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 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;
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
- 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) => {
202
+ function handleMouseDown(e) {
203
+ if (e.button !== 0) return;
121
204
  e.preventDefault();
122
205
  e.stopPropagation();
123
- clearOutline();
124
- const el = getElementBeneath(e.clientX, e.clientY);
125
- if (el) {
126
- onPick(el, e.clientX, e.clientY);
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
- [onPick, clearOutline]
130
- );
131
- const handleKeyDown = (0, import_react.useCallback)(
132
- (e) => {
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
- clearOutline();
135
- onCancel();
237
+ activeRef.current = false;
238
+ startRef.current = null;
239
+ setSelRect(null);
240
+ setHoverRect(null);
241
+ onCancelRef.current();
136
242
  }
137
- },
138
- [onCancel, clearOutline]
139
- );
140
- (0, import_react.useEffect)(() => {
243
+ }
244
+ document.addEventListener("mousedown", handleMouseDown, true);
141
245
  document.addEventListener("mousemove", handleMouseMove, true);
142
- document.addEventListener("click", handleClick, true);
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("click", handleClick, true);
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
- }, [handleMouseMove, handleClick, handleKeyDown]);
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
- right: 0,
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
- 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"
505
+ display: "flex",
506
+ flexDirection: "column",
507
+ fontFamily: "system-ui, sans-serif"
170
508
  },
171
509
  children: [
172
- "Click on any element to leave feedback. Press ",
173
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: "Esc" }),
174
- " to cancel."
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/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
- }
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
- 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
685
+ return () => {
686
+ for (const el of inerted) {
687
+ el.removeAttribute("inert");
688
+ }
284
689
  };
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);
690
+ }, []);
691
+ const handleAdd = () => {
692
+ if (comment.trim() && !addingDraft) {
693
+ onAdd(comment.trim());
302
694
  }
303
695
  };
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)(
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) onClose();
711
+ if (e.target === e.currentTarget && !addingDraft) onCancel();
329
712
  },
330
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
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: "80vh",
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, 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:" }),
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.url
732
+ context.componentPath
366
733
  ] }),
367
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
368
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Component:" }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
735
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { children: "Path:" }),
369
736
  " ",
370
- context.componentPath
737
+ context.urlPath
371
738
  ] }),
372
- context.elementText && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
373
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Element:" }),
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, 100),
742
+ context.elementText.slice(0, 80),
376
743
  '"'
377
744
  ] })
378
745
  ] }),
379
- screenshot && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
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
- !saved ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
393
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
394
- "textarea",
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
- value: comment,
397
- onChange: (e) => setComment(e.target.value),
398
- placeholder: "Describe the issue or suggestion...",
399
- autoFocus: true,
787
+ onClick: onCancel,
788
+ disabled: addingDraft,
400
789
  style: {
401
- width: "100%",
402
- minHeight: 80,
403
- padding: 10,
404
- borderRadius: 8,
790
+ padding: "8px 16px",
791
+ borderRadius: 6,
405
792
  border: "1px solid #d1d5db",
406
- fontSize: 14,
407
- resize: "vertical",
408
- boxSizing: "border-box"
409
- }
793
+ background: "#f3f4f6",
794
+ color: "#374151",
795
+ fontSize: 13,
796
+ cursor: "pointer"
797
+ },
798
+ children: "Cancel"
410
799
  }
411
800
  ),
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",
801
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
802
+ "button",
430
803
  {
431
- readOnly: true,
432
- value: buildPrompt(context, comment || "(no comment)"),
804
+ onClick: handleAdd,
805
+ disabled: !comment.trim() || addingDraft,
433
806
  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"
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
- onClick: (e) => e.target.select()
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/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;
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
- clearEntries();
609
- setConfirmClear(false);
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
- top: 0,
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
- flexDirection: "column",
844
+ alignItems: "center",
845
+ justifyContent: "center",
846
+ background: "rgba(0,0,0,0.4)",
628
847
  fontFamily: "system-ui, sans-serif"
629
848
  },
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)(
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: onClose,
896
+ onClick: onCancel,
897
+ disabled: submitting,
652
898
  style: {
653
- background: "none",
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
- fontSize: 20,
656
- cursor: "pointer",
657
- color: "#666",
658
- padding: "0 4px"
919
+ background: !title.trim() || submitting ? "#93c5fd" : "#3b82f6",
920
+ color: "#fff",
921
+ fontSize: 13,
922
+ cursor: !title.trim() || submitting ? "default" : "pointer"
659
923
  },
660
- children: "\xD7"
924
+ children: submitting ? "Submitting..." : "Submit"
661
925
  }
662
926
  )
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
- ]
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(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, {
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 ctx = canvas.getContext("2d");
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.strokeStyle = "red";
998
+ ctx.drawImage(fullCanvas, sx, sy, sw, sh, 0, 0, sw, sh);
999
+ ctx.strokeStyle = "#3b82f6";
833
1000
  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();
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 canvas.toDataURL("image/png");
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 state.mode === "idle" ? { mode: "picking" } : state;
864
- case "CANCEL":
865
- case "CLOSE":
866
- return { mode: "idle" };
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 { mode: "dialog", context: action.context, screenshot: action.screenshot };
869
- case "OPEN_LIST":
870
- return state.mode === "idle" ? { mode: "list" } : 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 [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);
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
- }, [state.mode]);
905
- (0, import_react4.useEffect)(() => {
906
- setEntryCount(loadEntries().length);
907
- }, []);
908
- (0, import_react4.useEffect)(() => {
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, state.mode]);
923
- const handleToggle = (0, import_react4.useCallback)(() => {
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 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;
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: x,
937
- clickY: y
1403
+ clickX: centerX,
1404
+ clickY: centerY
938
1405
  };
1406
+ const isDrag = rect.width > 20 && rect.height > 20;
939
1407
  let screenshot = null;
940
- try {
941
- screenshot = await captureScreenshot(x, y);
942
- } catch {
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 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" });
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
- state.mode === "idle" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
961
- ListButton,
1482
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1483
+ FloatingButton,
962
1484
  {
963
- onClick: handleOpenList,
964
- count: entryCount,
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
- FloatingButton,
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
- buttonColor
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.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 })
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