@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 +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +154 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +168 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
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:
|
|
76
|
-
height:
|
|
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:
|
|
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)(
|