@david-xpn/llm-ui-feedback 0.1.13 → 0.1.15

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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/FeedbackWidget.tsx
2
- import { useReducer, useCallback as useCallback4, useEffect as useEffect4, useMemo, useRef as useRef3 } from "react";
2
+ import { useReducer, useCallback as useCallback4, useEffect as useEffect5, useMemo, useRef as useRef3 } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
 
5
5
  // src/utils/color.ts
@@ -16,7 +16,7 @@ function getContrastColor(hexColor) {
16
16
 
17
17
  // src/components/FloatingButton.tsx
18
18
  import { jsx, jsxs } from "react/jsx-runtime";
19
- function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, position, buttonColor }) {
19
+ function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, position, buttonColor, authenticated }) {
20
20
  const isBottom = position.includes("bottom");
21
21
  const isRight = position.includes("right");
22
22
  const anchor = {
@@ -30,7 +30,7 @@ function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, pos
30
30
  gap: 8
31
31
  };
32
32
  return /* @__PURE__ */ jsxs("div", { style: anchor, children: [
33
- draftCount > 0 && /* @__PURE__ */ jsxs(
33
+ authenticated && /* @__PURE__ */ jsxs(
34
34
  "button",
35
35
  {
36
36
  onClick: onPanelToggle,
@@ -60,7 +60,7 @@ function FloatingButton({ onPickClick, onPanelToggle, draftCount, panelOpen, pos
60
60
  /* @__PURE__ */ jsx("rect", { x: "2", y: "13", width: "2", height: "2", rx: "0.5", fill: "currentColor" }),
61
61
  /* @__PURE__ */ jsx("rect", { x: "6", y: "13", width: "10", height: "2", rx: "0.5", fill: "currentColor" })
62
62
  ] }),
63
- /* @__PURE__ */ jsx(
63
+ draftCount > 0 && /* @__PURE__ */ jsx(
64
64
  "span",
65
65
  {
66
66
  style: {
@@ -513,7 +513,7 @@ function SidePanel({
513
513
  "Feedback Session",
514
514
  /* @__PURE__ */ jsxs4("span", { style: { fontSize: 11, fontWeight: 400, color: "#9ca3af", marginLeft: 6 }, children: [
515
515
  "v",
516
- "0.1.13"
516
+ "0.1.15"
517
517
  ] })
518
518
  ] }),
519
519
  /* @__PURE__ */ jsx4(
@@ -933,13 +933,61 @@ Comment: ${comment.trim()}`] : []
933
933
  }
934
934
 
935
935
  // src/components/SubmitModal.tsx
936
- import { useState as useState5 } from "react";
936
+ import { useState as useState5, useEffect as useEffect3 } from "react";
937
937
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
938
- function SubmitModal({ count, onSubmit, onCancel, onDone, submitting, submittedUrl }) {
938
+ function SubmitModal({ count, onSubmit, onCancel, onDone, submitting, submittedUrl, api }) {
939
939
  const [title, setTitle] = useState5("");
940
940
  const [copied, setCopied] = useState5(false);
941
+ const [linearConfigured, setLinearConfigured] = useState5(null);
942
+ const [linearLoading, setLinearLoading] = useState5(true);
943
+ const [createTicket, setCreateTicket] = useState5(true);
944
+ const [ticketTitle, setTicketTitle] = useState5("");
945
+ const [ticketTitleEdited, setTicketTitleEdited] = useState5(false);
946
+ const [projects, setProjects] = useState5([]);
947
+ const [labels, setLabels] = useState5([]);
948
+ const [selectedProjectId, setSelectedProjectId] = useState5("");
949
+ const [selectedLabelIds, setSelectedLabelIds] = useState5([]);
950
+ const [linearError, setLinearError] = useState5(null);
951
+ useEffect3(() => {
952
+ let cancelled = false;
953
+ setLinearLoading(true);
954
+ api.getLinearStatus().then(({ configured }) => {
955
+ if (cancelled) return;
956
+ setLinearConfigured(configured);
957
+ if (!configured) {
958
+ setLinearLoading(false);
959
+ return;
960
+ }
961
+ return Promise.all([api.fetchLinearProjects(), api.fetchLinearLabels()]).then(([p, l]) => {
962
+ if (cancelled) return;
963
+ setProjects(p);
964
+ setLabels(l);
965
+ if (p.length > 0) setSelectedProjectId(p[0].id);
966
+ setLinearLoading(false);
967
+ });
968
+ }).catch((err) => {
969
+ if (!cancelled) {
970
+ setLinearConfigured(false);
971
+ setLinearError(err.message);
972
+ setLinearLoading(false);
973
+ }
974
+ });
975
+ return () => {
976
+ cancelled = true;
977
+ };
978
+ }, [api]);
979
+ useEffect3(() => {
980
+ if (!ticketTitleEdited) setTicketTitle(title);
981
+ }, [title, ticketTitleEdited]);
941
982
  const handleSubmit = () => {
942
- if (title.trim() && !submitting) {
983
+ if (!title.trim() || submitting) return;
984
+ if (createTicket && linearConfigured && ticketTitle.trim()) {
985
+ onSubmit(title.trim(), {
986
+ title: ticketTitle.trim(),
987
+ ...selectedProjectId ? { projectId: selectedProjectId } : {},
988
+ ...selectedLabelIds.length ? { labelIds: selectedLabelIds } : {}
989
+ });
990
+ } else {
943
991
  onSubmit(title.trim());
944
992
  }
945
993
  };
@@ -952,6 +1000,18 @@ function SubmitModal({ count, onSubmit, onCancel, onDone, submitting, submittedU
952
1000
  } catch {
953
1001
  }
954
1002
  };
1003
+ const toggleLabel = (id) => {
1004
+ setSelectedLabelIds((prev) => prev.includes(id) ? prev.filter((l) => l !== id) : [...prev, id]);
1005
+ };
1006
+ const inputStyle = {
1007
+ width: "100%",
1008
+ padding: 10,
1009
+ borderRadius: 8,
1010
+ border: "1px solid #d1d5db",
1011
+ fontSize: 14,
1012
+ boxSizing: "border-box",
1013
+ background: "#fff"
1014
+ };
955
1015
  return /* @__PURE__ */ jsx6(
956
1016
  "div",
957
1017
  {
@@ -978,8 +1038,10 @@ function SubmitModal({ count, onSubmit, onCancel, onDone, submitting, submittedU
978
1038
  background: "#fff",
979
1039
  borderRadius: 12,
980
1040
  padding: 24,
981
- width: 400,
982
- maxWidth: "90vw",
1041
+ width: 460,
1042
+ maxWidth: "92vw",
1043
+ maxHeight: "90vh",
1044
+ overflowY: "auto",
983
1045
  boxShadow: "0 8px 32px rgba(0,0,0,0.2)"
984
1046
  },
985
1047
  children: submittedUrl ? /* @__PURE__ */ jsxs6(Fragment2, { children: [
@@ -1041,7 +1103,7 @@ function SubmitModal({ count, onSubmit, onCancel, onDone, submitting, submittedU
1041
1103
  ) })
1042
1104
  ] }) : /* @__PURE__ */ jsxs6(Fragment2, { children: [
1043
1105
  /* @__PURE__ */ jsx6("h3", { style: { margin: "0 0 16px", fontSize: 16 }, children: "Submit Feedback" }),
1044
- /* @__PURE__ */ jsxs6("p", { style: { margin: "0 0 12px", fontSize: 14, color: "#6b7280" }, children: [
1106
+ /* @__PURE__ */ jsxs6("p", { style: { margin: "0 0 8px", fontSize: 13, color: "#6b7280" }, children: [
1045
1107
  count,
1046
1108
  " item",
1047
1109
  count !== 1 ? "s" : "",
@@ -1055,20 +1117,138 @@ function SubmitModal({ count, onSubmit, onCancel, onDone, submitting, submittedU
1055
1117
  onChange: (e) => setTitle(e.target.value),
1056
1118
  placeholder: "e.g. Homepage redesign feedback",
1057
1119
  autoFocus: true,
1058
- onKeyDown: (e) => {
1059
- if (e.key === "Enter") handleSubmit();
1120
+ style: inputStyle
1121
+ }
1122
+ ),
1123
+ linearLoading && /* @__PURE__ */ jsxs6(
1124
+ "div",
1125
+ {
1126
+ style: {
1127
+ marginTop: 16,
1128
+ padding: 12,
1129
+ borderRadius: 8,
1130
+ border: "1px solid #e5e7eb",
1131
+ background: "#f9fafb",
1132
+ display: "flex",
1133
+ alignItems: "center",
1134
+ gap: 10,
1135
+ fontSize: 13,
1136
+ color: "#6b7280"
1060
1137
  },
1138
+ children: [
1139
+ /* @__PURE__ */ jsx6("style", { children: `@keyframes llmuif-spin { to { transform: rotate(360deg); } }` }),
1140
+ /* @__PURE__ */ jsx6(
1141
+ "span",
1142
+ {
1143
+ style: {
1144
+ display: "inline-block",
1145
+ width: 14,
1146
+ height: 14,
1147
+ border: "2px solid #d1d5db",
1148
+ borderTopColor: "#3b82f6",
1149
+ borderRadius: "50%",
1150
+ animation: "llmuif-spin 0.8s linear infinite"
1151
+ }
1152
+ }
1153
+ ),
1154
+ "Loading Linear projects and labels\u2026"
1155
+ ]
1156
+ }
1157
+ ),
1158
+ !linearLoading && linearConfigured && /* @__PURE__ */ jsxs6(
1159
+ "div",
1160
+ {
1061
1161
  style: {
1062
- width: "100%",
1063
- padding: 10,
1162
+ marginTop: 16,
1163
+ padding: 12,
1064
1164
  borderRadius: 8,
1065
- border: "1px solid #d1d5db",
1066
- fontSize: 14,
1067
- boxSizing: "border-box"
1068
- }
1165
+ border: "1px solid #e5e7eb",
1166
+ background: "#f9fafb"
1167
+ },
1168
+ children: [
1169
+ /* @__PURE__ */ jsxs6("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, fontWeight: 500, color: "#111827" }, children: [
1170
+ /* @__PURE__ */ jsx6(
1171
+ "input",
1172
+ {
1173
+ type: "checkbox",
1174
+ checked: createTicket,
1175
+ onChange: (e) => setCreateTicket(e.target.checked)
1176
+ }
1177
+ ),
1178
+ "Also create a Linear ticket"
1179
+ ] }),
1180
+ createTicket && /* @__PURE__ */ jsxs6("div", { style: { marginTop: 12, display: "flex", flexDirection: "column", gap: 10 }, children: [
1181
+ /* @__PURE__ */ jsxs6("div", { children: [
1182
+ /* @__PURE__ */ jsx6("label", { style: { display: "block", fontSize: 12, color: "#6b7280", marginBottom: 4 }, children: "Ticket title" }),
1183
+ /* @__PURE__ */ jsx6(
1184
+ "input",
1185
+ {
1186
+ type: "text",
1187
+ value: ticketTitle,
1188
+ onChange: (e) => {
1189
+ setTicketTitleEdited(true);
1190
+ setTicketTitle(e.target.value);
1191
+ },
1192
+ placeholder: "Linear ticket title",
1193
+ style: inputStyle
1194
+ }
1195
+ )
1196
+ ] }),
1197
+ /* @__PURE__ */ jsxs6("div", { children: [
1198
+ /* @__PURE__ */ jsx6("label", { style: { display: "block", fontSize: 12, color: "#6b7280", marginBottom: 4 }, children: "Project" }),
1199
+ /* @__PURE__ */ jsxs6(
1200
+ "select",
1201
+ {
1202
+ value: selectedProjectId,
1203
+ onChange: (e) => setSelectedProjectId(e.target.value),
1204
+ style: { ...inputStyle, appearance: "auto" },
1205
+ children: [
1206
+ projects.length === 0 && /* @__PURE__ */ jsx6("option", { value: "", children: "(no projects)" }),
1207
+ projects.map((p) => /* @__PURE__ */ jsxs6("option", { value: p.id, children: [
1208
+ p.name,
1209
+ p.teams[0] ? ` \u2014 ${p.teams[0].key}` : ""
1210
+ ] }, p.id))
1211
+ ]
1212
+ }
1213
+ )
1214
+ ] }),
1215
+ /* @__PURE__ */ jsxs6("div", { children: [
1216
+ /* @__PURE__ */ jsx6("label", { style: { display: "block", fontSize: 12, color: "#6b7280", marginBottom: 4 }, children: "Labels" }),
1217
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: [
1218
+ labels.length === 0 && /* @__PURE__ */ jsx6("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "(no labels)" }),
1219
+ labels.map((l) => {
1220
+ const active = selectedLabelIds.includes(l.id);
1221
+ return /* @__PURE__ */ jsx6(
1222
+ "button",
1223
+ {
1224
+ type: "button",
1225
+ onClick: () => toggleLabel(l.id),
1226
+ style: {
1227
+ padding: "4px 10px",
1228
+ borderRadius: 999,
1229
+ border: `1px solid ${active ? l.color : "#d1d5db"}`,
1230
+ background: active ? l.color : "#fff",
1231
+ color: active ? "#fff" : "#374151",
1232
+ fontSize: 12,
1233
+ cursor: "pointer"
1234
+ },
1235
+ children: l.name
1236
+ },
1237
+ l.id
1238
+ );
1239
+ })
1240
+ ] })
1241
+ ] })
1242
+ ] })
1243
+ ]
1069
1244
  }
1070
1245
  ),
1071
- /* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: 8, marginTop: 16, justifyContent: "flex-end" }, children: [
1246
+ !linearLoading && linearConfigured === false && !linearError && /* @__PURE__ */ jsx6("p", { style: { marginTop: 12, fontSize: 12, color: "#9ca3af" }, children: "Linear integration is not configured. Add a Linear token in the viewer Settings page to also create tickets from here." }),
1247
+ linearError && /* @__PURE__ */ jsxs6("p", { style: { marginTop: 12, fontSize: 12, color: "#b91c1c" }, children: [
1248
+ "Linear: ",
1249
+ linearError
1250
+ ] }),
1251
+ /* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: 8, marginTop: 20, justifyContent: "flex-end" }, children: [
1072
1252
  /* @__PURE__ */ jsx6(
1073
1253
  "button",
1074
1254
  {
@@ -1203,7 +1383,7 @@ ${propsStr}`);
1203
1383
  }
1204
1384
 
1205
1385
  // src/hooks/useSession.ts
1206
- import { useState as useState6, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef2 } from "react";
1386
+ import { useState as useState6, useEffect as useEffect4, useCallback as useCallback3, useRef as useRef2 } from "react";
1207
1387
  var SESSION_TOKEN_KEY = "llm_feedback_session_token";
1208
1388
  var USER_KEY = "llm_feedback_user";
1209
1389
  var AUTH_BYPASS = !!(typeof globalThis !== "undefined" && globalThis.__FEEDBACK_AUTH_BYPASS__);
@@ -1221,7 +1401,7 @@ function useSession(apiUrl, clientId) {
1221
1401
  const [status, setStatus] = useState6(AUTH_BYPASS ? "authenticated" : "loading");
1222
1402
  const [user, setUser] = useState6(AUTH_BYPASS ? BYPASS_USER : null);
1223
1403
  const pendingAuthRef = useRef2(false);
1224
- useEffect3(() => {
1404
+ useEffect4(() => {
1225
1405
  if (AUTH_BYPASS) return;
1226
1406
  let cancelled = false;
1227
1407
  async function checkSession() {
@@ -1262,7 +1442,7 @@ function useSession(apiUrl, clientId) {
1262
1442
  cancelled = true;
1263
1443
  };
1264
1444
  }, [apiUrl]);
1265
- useEffect3(() => {
1445
+ useEffect4(() => {
1266
1446
  function handleMessage(event) {
1267
1447
  if (event.data?.type !== "feedback-auth") return;
1268
1448
  const { sessionToken, user: userData } = event.data;
@@ -1360,6 +1540,17 @@ function createApiClient(apiUrl, clientId) {
1360
1540
  body: JSON.stringify(data)
1361
1541
  });
1362
1542
  },
1543
+ async getLinearStatus() {
1544
+ return apiFetch("/linear/status");
1545
+ },
1546
+ async fetchLinearProjects() {
1547
+ const data = await apiFetch("/linear/projects");
1548
+ return data.items;
1549
+ },
1550
+ async fetchLinearLabels() {
1551
+ const data = await apiFetch("/linear/labels");
1552
+ return data.items;
1553
+ },
1363
1554
  async getUploadUrl(entryId, timestamp) {
1364
1555
  return apiFetch("/upload-url", {
1365
1556
  method: "POST",
@@ -1497,12 +1688,12 @@ function FeedbackWidget({
1497
1688
  () => createApiClient(apiUrl, clientId),
1498
1689
  [apiUrl, clientId]
1499
1690
  );
1500
- useEffect4(() => {
1691
+ useEffect5(() => {
1501
1692
  if (session.status === "authenticated" && pendingOpenRef.current) {
1502
1693
  pendingOpenRef.current = false;
1503
1694
  }
1504
1695
  }, [session.status]);
1505
- useEffect4(() => {
1696
+ useEffect5(() => {
1506
1697
  if (session.status === "authenticated") {
1507
1698
  api.fetchDrafts().then((drafts) => {
1508
1699
  dispatch({ type: "SET_DRAFTS", drafts });
@@ -1510,7 +1701,7 @@ function FeedbackWidget({
1510
1701
  });
1511
1702
  }
1512
1703
  }, [state.panelOpen, session.status, api]);
1513
- useEffect4(() => {
1704
+ useEffect5(() => {
1514
1705
  if (!hotkey) return;
1515
1706
  const parsed = parseHotkey(hotkey);
1516
1707
  function handler(e) {
@@ -1638,12 +1829,16 @@ function FeedbackWidget({
1638
1829
  }
1639
1830
  }, [api]);
1640
1831
  const submittedDraftIdsRef = useRef3([]);
1641
- const handleSubmit = useCallback4(async (title) => {
1832
+ const handleSubmit = useCallback4(async (title, linearTicket) => {
1642
1833
  dispatch({ type: "SET_SUBMITTING", submitting: true });
1643
1834
  const draftIds = Array.from(state.selectedDraftIds);
1644
1835
  submittedDraftIdsRef.current = draftIds;
1645
1836
  try {
1646
- const result = await api.submitSession({ title, draftIds });
1837
+ const result = await api.submitSession({
1838
+ title,
1839
+ draftIds,
1840
+ ...linearTicket ? { linearTicket } : {}
1841
+ });
1647
1842
  const group = result.feedbackGroup;
1648
1843
  const shareUrl = group.shareToken ? `${apiUrl}/feedback-groups/${group.id}/markdown?token=${group.shareToken}` : "";
1649
1844
  dispatch({ type: "SUBMIT_SUCCESS", shareUrl });
@@ -1666,7 +1861,8 @@ function FeedbackWidget({
1666
1861
  draftCount: state.drafts.length,
1667
1862
  panelOpen: state.panelOpen,
1668
1863
  position,
1669
- buttonColor
1864
+ buttonColor,
1865
+ authenticated: session.status === "authenticated"
1670
1866
  }
1671
1867
  ),
1672
1868
  state.picking && /* @__PURE__ */ jsx7(PickOverlay, { onPick: handlePick, onCancel: handleCancelPicking }),
@@ -1704,7 +1900,8 @@ function FeedbackWidget({
1704
1900
  onCancel: () => dispatch({ type: "CLOSE_SUBMIT_MODAL" }),
1705
1901
  onDone: handleSubmitDone,
1706
1902
  submitting: state.submitting,
1707
- submittedUrl: state.submittedShareUrl
1903
+ submittedUrl: state.submittedShareUrl,
1904
+ api
1708
1905
  }
1709
1906
  )
1710
1907
  ] }),