@david-xpn/llm-ui-feedback 0.1.7 → 0.1.9

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.d.mts CHANGED
@@ -67,7 +67,9 @@ interface FeedbackWidgetProps {
67
67
  buttonColor?: string;
68
68
  /** Keyboard shortcut to toggle the panel, e.g. 'Alt+F'. No default (opt-in only). */
69
69
  hotkey?: string;
70
+ /** URL of the feedback viewer app. Shown as a link in the side panel. */
71
+ viewerUrl?: string;
70
72
  }
71
- declare function FeedbackWidget({ clientId, apiUrl, position, buttonColor, hotkey, }: FeedbackWidgetProps): React.ReactPortal | null;
73
+ declare function FeedbackWidget({ clientId, apiUrl, position, buttonColor, hotkey, viewerUrl, }: FeedbackWidgetProps): React.ReactPortal | null;
72
74
 
73
75
  export { type CapturedContext, type ComponentInfo, type Draft, type FeedbackEntry, type FeedbackGroup, FeedbackWidget, type FeedbackWidgetProps, type WidgetPosition };
package/dist/index.d.ts CHANGED
@@ -67,7 +67,9 @@ interface FeedbackWidgetProps {
67
67
  buttonColor?: string;
68
68
  /** Keyboard shortcut to toggle the panel, e.g. 'Alt+F'. No default (opt-in only). */
69
69
  hotkey?: string;
70
+ /** URL of the feedback viewer app. Shown as a link in the side panel. */
71
+ viewerUrl?: string;
70
72
  }
71
- declare function FeedbackWidget({ clientId, apiUrl, position, buttonColor, hotkey, }: FeedbackWidgetProps): React.ReactPortal | null;
73
+ declare function FeedbackWidget({ clientId, apiUrl, position, buttonColor, hotkey, viewerUrl, }: FeedbackWidgetProps): React.ReactPortal | null;
72
74
 
73
75
  export { type CapturedContext, type ComponentInfo, type Draft, type FeedbackEntry, type FeedbackGroup, FeedbackWidget, type FeedbackWidgetProps, type WidgetPosition };
package/dist/index.js CHANGED
@@ -66,31 +66,59 @@ function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, pos
66
66
  gap: 8
67
67
  };
68
68
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: anchor, children: [
69
- draftCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
69
+ draftCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
70
70
  "button",
71
71
  {
72
72
  onClick: onPanelToggle,
73
73
  "aria-label": panelOpen ? "Close drafts panel" : `Open drafts panel (${draftCount})`,
74
74
  style: {
75
- width: 32,
76
- height: 32,
75
+ width: 36,
76
+ height: 36,
77
77
  borderRadius: "50%",
78
78
  border: "none",
79
79
  background: panelOpen ? "#ef4444" : buttonColor,
80
80
  color: panelOpen ? "#fff" : getContrastColor(buttonColor),
81
- fontFamily: "system-ui, -apple-system, sans-serif",
82
- fontSize: 13,
83
- fontWeight: 700,
84
- lineHeight: 1,
85
81
  cursor: "pointer",
86
82
  display: "flex",
87
83
  alignItems: "center",
88
84
  justifyContent: "center",
89
85
  boxShadow: "0 2px 8px rgba(0,0,0,0.25)",
90
86
  transition: "background 0.15s",
91
- padding: 0
87
+ padding: 0,
88
+ position: "relative"
92
89
  },
93
- children: draftCount
90
+ children: [
91
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
92
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "3", width: "2", height: "2", rx: "0.5", fill: "currentColor" }),
93
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "3", width: "10", height: "2", rx: "0.5", fill: "currentColor" }),
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "8", width: "2", height: "2", rx: "0.5", fill: "currentColor" }),
95
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "8", width: "10", height: "2", rx: "0.5", fill: "currentColor" }),
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "13", width: "2", height: "2", rx: "0.5", fill: "currentColor" }),
97
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "13", width: "10", height: "2", rx: "0.5", fill: "currentColor" })
98
+ ] }),
99
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
100
+ "span",
101
+ {
102
+ style: {
103
+ position: "absolute",
104
+ top: -4,
105
+ right: -4,
106
+ minWidth: 16,
107
+ height: 16,
108
+ borderRadius: 8,
109
+ background: "#ef4444",
110
+ color: "#fff",
111
+ fontSize: 10,
112
+ fontWeight: 700,
113
+ fontFamily: "system-ui, -apple-system, sans-serif",
114
+ lineHeight: "16px",
115
+ textAlign: "center",
116
+ padding: "0 3px"
117
+ },
118
+ children: draftCount
119
+ }
120
+ )
121
+ ]
94
122
  }
95
123
  ),
96
124
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -470,7 +498,8 @@ function SidePanel({
470
498
  onSubmit,
471
499
  onClose,
472
500
  api,
473
- user
501
+ user,
502
+ viewerUrl
474
503
  }) {
475
504
  const isRight = position.includes("right");
476
505
  const allSelected = drafts.length > 0 && selectedDraftIds.size === drafts.length;
@@ -536,7 +565,25 @@ function SidePanel({
536
565
  user && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginTop: 6, fontSize: 12, color: "#6b7280", lineHeight: 1.4 }, children: [
537
566
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 500, color: "#374151" }, children: user.name }),
538
567
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: user.email })
539
- ] })
568
+ ] }),
569
+ viewerUrl && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
570
+ "a",
571
+ {
572
+ href: viewerUrl,
573
+ target: "_blank",
574
+ rel: "noopener noreferrer",
575
+ style: {
576
+ display: "inline-flex",
577
+ alignItems: "center",
578
+ gap: 4,
579
+ marginTop: 8,
580
+ fontSize: 12,
581
+ color: "#3b82f6",
582
+ textDecoration: "none"
583
+ },
584
+ children: "Open Feedback Viewer \u2197"
585
+ }
586
+ )
540
587
  ]
541
588
  }
542
589
  ),
@@ -668,6 +715,20 @@ function SidePanel({
668
715
 
669
716
  // src/components/DraftModal.tsx
670
717
  var import_react4 = require("react");
718
+
719
+ // src/utils/blob.ts
720
+ function dataUrlToBlob(dataUrl) {
721
+ const [header, base64] = dataUrl.split(",");
722
+ const mime = header.match(/:(.*?);/)?.[1] || "image/png";
723
+ const binary = atob(base64);
724
+ const bytes = new Uint8Array(binary.length);
725
+ for (let i = 0; i < binary.length; i++) {
726
+ bytes[i] = binary.charCodeAt(i);
727
+ }
728
+ return new Blob([bytes], { type: mime });
729
+ }
730
+
731
+ // src/components/DraftModal.tsx
671
732
  var import_jsx_runtime5 = require("react/jsx-runtime");
672
733
  var WIDGET_CONTAINER_ID2 = "llm-ui-feedback-root";
673
734
  function DraftModal({ pendingContext, addingDraft, onAdd, onCancel }) {
@@ -694,6 +755,46 @@ function DraftModal({ pendingContext, addingDraft, onAdd, onCancel }) {
694
755
  }
695
756
  };
696
757
  const { context, screenshot } = pendingContext;
758
+ const [copiedText, setCopiedText] = (0, import_react4.useState)(false);
759
+ const [copiedImage, setCopiedImage] = (0, import_react4.useState)(false);
760
+ const handleCopyText = (0, import_react4.useCallback)(async () => {
761
+ const lines = [
762
+ `Component: ${context.componentPath}`,
763
+ `Path: ${context.urlPath}`,
764
+ ...context.elementText ? [`Element: "${context.elementText}"`] : [],
765
+ ...comment.trim() ? [`
766
+ Comment: ${comment.trim()}`] : []
767
+ ];
768
+ try {
769
+ await navigator.clipboard.writeText(lines.join("\n"));
770
+ setCopiedText(true);
771
+ setTimeout(() => setCopiedText(false), 2e3);
772
+ } catch {
773
+ }
774
+ }, [context, comment]);
775
+ const handleCopyImage = (0, import_react4.useCallback)(async () => {
776
+ if (!screenshot) return;
777
+ try {
778
+ const blob = dataUrlToBlob(screenshot);
779
+ const pngBlob = blob.type === "image/png" ? blob : await new Promise((resolve) => {
780
+ const img = new Image();
781
+ img.onload = () => {
782
+ const canvas = document.createElement("canvas");
783
+ canvas.width = img.naturalWidth;
784
+ canvas.height = img.naturalHeight;
785
+ canvas.getContext("2d").drawImage(img, 0, 0);
786
+ canvas.toBlob((b) => resolve(b), "image/png");
787
+ };
788
+ img.src = screenshot;
789
+ });
790
+ await navigator.clipboard.write([
791
+ new ClipboardItem({ "image/png": pngBlob })
792
+ ]);
793
+ setCopiedImage(true);
794
+ setTimeout(() => setCopiedImage(false), 2e3);
795
+ } catch {
796
+ }
797
+ }, [screenshot]);
697
798
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
698
799
  "div",
699
800
  {
@@ -758,6 +859,44 @@ function DraftModal({ pendingContext, addingDraft, onAdd, onCancel }) {
758
859
  }
759
860
  }
760
861
  ),
862
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: 8, marginBottom: 12 }, children: [
863
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
864
+ "button",
865
+ {
866
+ onClick: handleCopyText,
867
+ style: {
868
+ padding: "5px 12px",
869
+ borderRadius: 6,
870
+ border: "1px solid #d1d5db",
871
+ background: copiedText ? "#22c55e" : "#f3f4f6",
872
+ color: copiedText ? "#fff" : "#374151",
873
+ fontSize: 12,
874
+ fontWeight: 500,
875
+ cursor: "pointer",
876
+ transition: "background 0.15s, color 0.15s"
877
+ },
878
+ children: copiedText ? "Copied!" : "Copy Text"
879
+ }
880
+ ),
881
+ screenshot && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
882
+ "button",
883
+ {
884
+ onClick: handleCopyImage,
885
+ style: {
886
+ padding: "5px 12px",
887
+ borderRadius: 6,
888
+ border: "1px solid #d1d5db",
889
+ background: copiedImage ? "#22c55e" : "#f3f4f6",
890
+ color: copiedImage ? "#fff" : "#374151",
891
+ fontSize: 12,
892
+ fontWeight: 500,
893
+ cursor: "pointer",
894
+ transition: "background 0.15s, color 0.15s"
895
+ },
896
+ children: copiedImage ? "Copied!" : "Copy Image"
897
+ }
898
+ )
899
+ ] }),
761
900
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
762
901
  "textarea",
763
902
  {
@@ -1093,18 +1232,6 @@ ${propsStr}`);
1093
1232
  return lines.join("\n");
1094
1233
  }
1095
1234
 
1096
- // src/utils/blob.ts
1097
- function dataUrlToBlob(dataUrl) {
1098
- const [header, base64] = dataUrl.split(",");
1099
- const mime = header.match(/:(.*?);/)?.[1] || "image/png";
1100
- const binary = atob(base64);
1101
- const bytes = new Uint8Array(binary.length);
1102
- for (let i = 0; i < binary.length; i++) {
1103
- bytes[i] = binary.charCodeAt(i);
1104
- }
1105
- return new Blob([bytes], { type: mime });
1106
- }
1107
-
1108
1235
  // src/hooks/useSession.ts
1109
1236
  var import_react6 = require("react");
1110
1237
  var SESSION_TOKEN_KEY = "llm_feedback_session_token";
@@ -1390,7 +1517,8 @@ function FeedbackWidget({
1390
1517
  apiUrl,
1391
1518
  position = "bottom-right",
1392
1519
  buttonColor = "#3b82f6",
1393
- hotkey
1520
+ hotkey,
1521
+ viewerUrl
1394
1522
  }) {
1395
1523
  const session = useSession(apiUrl, clientId);
1396
1524
  const [state, dispatch] = (0, import_react7.useReducer)(widgetReducer, initialState);
@@ -1402,7 +1530,6 @@ function FeedbackWidget({
1402
1530
  (0, import_react7.useEffect)(() => {
1403
1531
  if (session.status === "authenticated" && pendingOpenRef.current) {
1404
1532
  pendingOpenRef.current = false;
1405
- dispatch({ type: "OPEN_PANEL" });
1406
1533
  }
1407
1534
  }, [session.status]);
1408
1535
  (0, import_react7.useEffect)(() => {
@@ -1582,7 +1709,8 @@ function FeedbackWidget({
1582
1709
  onSubmit: () => dispatch({ type: "OPEN_SUBMIT_MODAL" }),
1583
1710
  onClose: () => dispatch({ type: "CLOSE_PANEL" }),
1584
1711
  api,
1585
- user: session.user
1712
+ user: session.user,
1713
+ viewerUrl
1586
1714
  }
1587
1715
  ),
1588
1716
  state.pendingContext && !state.picking && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(