glitchgrab 1.43.0 → 1.45.0

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
@@ -693,7 +693,11 @@ async function sendFeedback(payload, baseUrl) {
693
693
  return null;
694
694
  }
695
695
  }
696
- async function enhanceText(text, token, baseUrl, screenshot, context) {
696
+ function signedFields(identity) {
697
+ if (!(identity == null ? void 0 : identity.signature) || !identity.userId) return {};
698
+ return { userId: identity.userId, signature: identity.signature };
699
+ }
700
+ async function enhanceText(text, token, baseUrl, screenshot, context, identity) {
697
701
  var _a;
698
702
  try {
699
703
  const trimmed = text.trim();
@@ -708,7 +712,8 @@ async function enhanceText(text, token, baseUrl, screenshot, context) {
708
712
  body: JSON.stringify({
709
713
  text: trimmed,
710
714
  ...screenshot ? { screenshot } : {},
711
- ...context ? { context } : {}
715
+ ...context ? { context } : {},
716
+ ...signedFields(identity)
712
717
  })
713
718
  });
714
719
  if (!response.ok) return text;
@@ -721,6 +726,26 @@ async function enhanceText(text, token, baseUrl, screenshot, context) {
721
726
  return text;
722
727
  }
723
728
  }
729
+ async function findSimilarIssues(description, token, baseUrl, identity) {
730
+ var _a;
731
+ try {
732
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/reports/similar`;
733
+ const response = await fetch(url, {
734
+ method: "POST",
735
+ headers: {
736
+ "Content-Type": "application/json",
737
+ Authorization: `Bearer ${token}`
738
+ },
739
+ body: JSON.stringify({ text: description, ...signedFields(identity) })
740
+ });
741
+ const envelope = await response.json().catch(() => null);
742
+ if (!response.ok || !(envelope == null ? void 0 : envelope.success)) return null;
743
+ if (!((_a = envelope.data) == null ? void 0 : _a.enabled)) return [];
744
+ return Array.isArray(envelope.data.matches) ? envelope.data.matches : [];
745
+ } catch (e) {
746
+ return null;
747
+ }
748
+ }
724
749
  async function assistTurn(params, token, baseUrl) {
725
750
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
726
751
  const offline = {
@@ -745,7 +770,9 @@ async function assistTurn(params, token, baseUrl) {
745
770
  conversationId: null,
746
771
  question: null,
747
772
  report: null,
748
- degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : offline.degraded
773
+ degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : offline.degraded,
774
+ // An hourly limit, not a cap: the dialog keeps the assistant button.
775
+ retryable: (envelope == null ? void 0 : envelope.retryable) === true
749
776
  };
750
777
  }
751
778
  return {
@@ -762,13 +789,18 @@ async function assistTurn(params, token, baseUrl) {
762
789
  return offline;
763
790
  }
764
791
  }
765
- async function transcribeAudio(blob, token, baseUrl) {
792
+ async function transcribeAudio(blob, token, baseUrl, identity) {
766
793
  var _a;
767
794
  try {
768
795
  if (blob.size === 0) return "";
769
796
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/stt`;
770
797
  const form = new FormData();
771
798
  form.append("file", blob, "audio.webm");
799
+ const signed = signedFields(identity);
800
+ if (signed.userId && signed.signature) {
801
+ form.append("userId", signed.userId);
802
+ form.append("signature", signed.signature);
803
+ }
772
804
  const response = await fetch(url, {
773
805
  method: "POST",
774
806
  headers: { Authorization: `Bearer ${token}` },
@@ -1337,6 +1369,59 @@ var SEVERITY_LABELS = {
1337
1369
  };
1338
1370
  var MAX_ASSIST_IMAGES = 3;
1339
1371
  var MAX_ASSIST_IMAGE_CHARS = 35e5;
1372
+ var MAX_ASSIST_FILES = 3;
1373
+ var MAX_ASSIST_FILE_CHARS = 3e5;
1374
+ var MAX_ASSIST_FILE_CHARS_EACH = 2e5;
1375
+ var TEXT_ATTACHMENT_EXTENSIONS = [
1376
+ ".html",
1377
+ ".htm",
1378
+ ".txt",
1379
+ ".md",
1380
+ ".json",
1381
+ ".xml",
1382
+ ".log",
1383
+ ".yml",
1384
+ ".yaml",
1385
+ ".csv",
1386
+ ".har",
1387
+ ".js",
1388
+ ".mjs",
1389
+ ".cjs",
1390
+ ".ts",
1391
+ ".tsx",
1392
+ ".jsx",
1393
+ ".css",
1394
+ ".scss",
1395
+ ".py",
1396
+ ".sh"
1397
+ ];
1398
+ var BINARY_ATTACHMENT_EXTENSIONS = [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".zip"];
1399
+ var ATTACHMENT_ACCEPT = [
1400
+ "image/*",
1401
+ ...TEXT_ATTACHMENT_EXTENSIONS,
1402
+ ...BINARY_ATTACHMENT_EXTENSIONS
1403
+ ].join(",");
1404
+ var ASSIST_ATTACHMENT_ACCEPT = ["image/*", ...TEXT_ATTACHMENT_EXTENSIONS].join(",");
1405
+ function isAssistReadableFile(name) {
1406
+ const lower = name.toLowerCase();
1407
+ return TEXT_ATTACHMENT_EXTENSIONS.some((ext) => lower.endsWith(ext));
1408
+ }
1409
+ function decodeTextDataUrl(dataUrl) {
1410
+ try {
1411
+ if (!dataUrl.startsWith("data:")) return null;
1412
+ const comma = dataUrl.indexOf(",");
1413
+ if (comma < 0) return null;
1414
+ const meta = dataUrl.slice(5, comma);
1415
+ const payload = dataUrl.slice(comma + 1);
1416
+ if (!meta.endsWith(";base64")) return decodeURIComponent(payload);
1417
+ const binary = atob(payload);
1418
+ const bytes = new Uint8Array(binary.length);
1419
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1420
+ return new TextDecoder("utf-8").decode(bytes);
1421
+ } catch (e) {
1422
+ return null;
1423
+ }
1424
+ }
1340
1425
  function getTypeLabel(type) {
1341
1426
  switch (type) {
1342
1427
  case "RATING":
@@ -1368,6 +1453,22 @@ function pickAssistImages(all) {
1368
1453
  }
1369
1454
  return picked;
1370
1455
  }
1456
+ var NO_FILES = [];
1457
+ function pickAssistFiles(files) {
1458
+ const picked = [];
1459
+ let total = 0;
1460
+ for (let i = files.length - 1; i >= 0 && picked.length < MAX_ASSIST_FILES; i--) {
1461
+ const attached = files[i];
1462
+ if (!isAssistReadableFile(attached.name)) continue;
1463
+ const text = decodeTextDataUrl(attached.dataUrl);
1464
+ if (!text || !text.trim()) continue;
1465
+ const content = text.slice(0, MAX_ASSIST_FILE_CHARS_EACH);
1466
+ if (total + content.length > MAX_ASSIST_FILE_CHARS) continue;
1467
+ picked.unshift({ index: i, file: { name: attached.name, content } });
1468
+ total += content.length;
1469
+ }
1470
+ return picked;
1471
+ }
1371
1472
  var STARTERS = [
1372
1473
  "Something on this page is broken",
1373
1474
  "It did the wrong thing",
@@ -1444,7 +1545,9 @@ function AssistSheet({
1444
1545
  theme: t2,
1445
1546
  screenshots,
1446
1547
  pageShot = null,
1447
- onAddImages,
1548
+ onAddFiles,
1549
+ files = NO_FILES,
1550
+ onRemoveFile,
1448
1551
  onRemoveImage,
1449
1552
  attachmentCount,
1450
1553
  context,
@@ -1515,6 +1618,8 @@ function AssistSheet({
1515
1618
  }
1516
1619
  }
1517
1620
  const readShots = new Set(pickAssistImages(screenshots));
1621
+ const sentFiles = (0, import_react3.useMemo)(() => pickAssistFiles(files), [files]);
1622
+ const readFiles = new Set(sentFiles.map((f) => f.index));
1518
1623
  function leave(type, detail) {
1519
1624
  logEvent(type, detail);
1520
1625
  flushEvents();
@@ -1528,10 +1633,10 @@ function AssistSheet({
1528
1633
  var _a;
1529
1634
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1530
1635
  }, [phase]);
1531
- function degradeKeepingEvents(events, message) {
1636
+ function degradeKeepingEvents(events, message, retryable) {
1532
1637
  if (events) eventsRef.current = [...events, ...eventsRef.current];
1533
1638
  flushEvents();
1534
- onDegrade(message);
1639
+ onDegrade(message, retryable);
1535
1640
  }
1536
1641
  async function runTurn(history2) {
1537
1642
  var _a, _b, _c, _d;
@@ -1549,6 +1654,9 @@ function AssistSheet({
1549
1654
  ),
1550
1655
  conversationId,
1551
1656
  screenshots: sent,
1657
+ // Only when there is something: an older server then sees the body
1658
+ // it always did.
1659
+ ...sentFiles.length > 0 ? { files: sentFiles.map((f) => f.file) } : {},
1552
1660
  context: {
1553
1661
  ...context != null ? context : {},
1554
1662
  imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached"),
@@ -1568,7 +1676,7 @@ function AssistSheet({
1568
1676
  setBusy(false);
1569
1677
  }
1570
1678
  if (result.degraded) {
1571
- degradeKeepingEvents(events, result.degraded);
1679
+ degradeKeepingEvents(events, result.degraded, result.retryable === true);
1572
1680
  return;
1573
1681
  }
1574
1682
  if (result.conversationId) {
@@ -1668,7 +1776,7 @@ function AssistSheet({
1668
1776
  minWidth: 0
1669
1777
  },
1670
1778
  children: [
1671
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1779
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.text, fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1672
1780
  title && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1673
1781
  "#",
1674
1782
  duplicate.number,
@@ -2556,7 +2664,7 @@ function AssistSheet({
2556
2664
  // instead of remounting, and a draft is something you can
2557
2665
  // still reply to (#360), not a form that replaced the chat.
2558
2666
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2559
- (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2667
+ (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2560
2668
  "div",
2561
2669
  {
2562
2670
  style: {
@@ -2660,11 +2768,118 @@ function AssistSheet({
2660
2768
  `${i}-${shot.slice(-16)}`
2661
2769
  );
2662
2770
  }),
2663
- onAddImages && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2771
+ files.map((file, i) => {
2772
+ const read = readFiles.has(i);
2773
+ const why = read ? "the assistant reads this file" : isAssistReadableFile(file.name) ? "attached, but more than one message can carry, so the assistant is not reading it" : "attached to the report \u2014 the assistant can't read this kind of file";
2774
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2775
+ "div",
2776
+ {
2777
+ "data-gg-assist-file": "",
2778
+ "data-gg-assist-file-read": read ? "" : void 0,
2779
+ title: `${file.name} \u2014 ${why}`,
2780
+ style: {
2781
+ position: "relative",
2782
+ height: "40px",
2783
+ maxWidth: "170px",
2784
+ display: "flex",
2785
+ alignItems: "center",
2786
+ gap: "6px",
2787
+ padding: "0 10px",
2788
+ borderRadius: "8px",
2789
+ border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2790
+ opacity: read ? 1 : 0.55,
2791
+ boxSizing: "border-box",
2792
+ flexShrink: 0
2793
+ },
2794
+ children: [
2795
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2796
+ "path",
2797
+ {
2798
+ d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
2799
+ stroke: t2.textMuted,
2800
+ strokeWidth: "1.5",
2801
+ strokeLinecap: "round",
2802
+ strokeLinejoin: "round"
2803
+ }
2804
+ ) }),
2805
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2806
+ "span",
2807
+ {
2808
+ style: {
2809
+ fontSize: "11.5px",
2810
+ color: t2.text,
2811
+ overflow: "hidden",
2812
+ textOverflow: "ellipsis",
2813
+ whiteSpace: "nowrap"
2814
+ },
2815
+ children: file.name
2816
+ }
2817
+ ),
2818
+ onRemoveFile && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2819
+ "button",
2820
+ {
2821
+ type: "button",
2822
+ "data-gg-assist-remove-file": "",
2823
+ "aria-label": `Remove file ${file.name}`,
2824
+ title: "Remove this file",
2825
+ onClick: () => {
2826
+ logEvent("file_removed", read ? "read" : "not read");
2827
+ onRemoveFile(i);
2828
+ },
2829
+ style: {
2830
+ position: "absolute",
2831
+ top: "-13px",
2832
+ right: "-13px",
2833
+ width: "32px",
2834
+ height: "32px",
2835
+ border: "none",
2836
+ background: "transparent",
2837
+ padding: 0,
2838
+ cursor: "pointer",
2839
+ display: "flex",
2840
+ alignItems: "center",
2841
+ justifyContent: "center",
2842
+ borderRadius: "50%"
2843
+ },
2844
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2845
+ "span",
2846
+ {
2847
+ "aria-hidden": "true",
2848
+ style: {
2849
+ width: "20px",
2850
+ height: "20px",
2851
+ boxSizing: "border-box",
2852
+ borderRadius: "50%",
2853
+ border: `2px solid ${t2.bg}`,
2854
+ background: "#ef4444",
2855
+ color: "#fff",
2856
+ display: "flex",
2857
+ alignItems: "center",
2858
+ justifyContent: "center"
2859
+ },
2860
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2861
+ "path",
2862
+ {
2863
+ d: "M1 1L9 9M9 1L1 9",
2864
+ stroke: "currentColor",
2865
+ strokeWidth: "1.6",
2866
+ strokeLinecap: "round"
2867
+ }
2868
+ ) })
2869
+ }
2870
+ )
2871
+ }
2872
+ )
2873
+ ]
2874
+ },
2875
+ `file-${i}-${file.name}`
2876
+ );
2877
+ }),
2878
+ onAddFiles && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2664
2879
  "label",
2665
2880
  {
2666
2881
  "data-gg-assist-attach": "",
2667
- title: "Attach an image \u2014 or just paste one",
2882
+ title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
2668
2883
  style: {
2669
2884
  width: "40px",
2670
2885
  height: "40px",
@@ -2684,14 +2899,18 @@ function AssistSheet({
2684
2899
  "input",
2685
2900
  {
2686
2901
  type: "file",
2687
- accept: "image/*",
2902
+ accept: ASSIST_ATTACHMENT_ACCEPT,
2688
2903
  multiple: true,
2689
2904
  onChange: (e) => {
2690
2905
  var _a;
2691
- const files = Array.from((_a = e.target.files) != null ? _a : []);
2692
- if (files.length) {
2693
- logEvent("image_added", String(files.length));
2694
- onAddImages(files);
2906
+ const picked = Array.from((_a = e.target.files) != null ? _a : []);
2907
+ if (picked.length) {
2908
+ const images = picked.filter((f) => f.type.startsWith("image/")).length;
2909
+ if (images) logEvent("image_added", String(images));
2910
+ if (picked.length > images) {
2911
+ logEvent("file_added", String(picked.length - images));
2912
+ }
2913
+ onAddFiles(picked);
2695
2914
  }
2696
2915
  e.target.value = "";
2697
2916
  },
@@ -2872,6 +3091,7 @@ function AssistSheet({
2872
3091
  }
2873
3092
  var FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
2874
3093
  var FAILED = "Couldn't send it. Try again in a moment.";
3094
+ var MAX_SELF_REPORT_IMAGES = 5;
2875
3095
  function describeSentAlong(hasChat, screenshotCount) {
2876
3096
  const parts = [
2877
3097
  ...hasChat ? ["your AI chat"] : [],
@@ -2895,11 +3115,49 @@ function GlitchgrabProblemPanel({
2895
3115
  const [busy, setBusy] = (0, import_react4.useState)(false);
2896
3116
  const [sent, setSent] = (0, import_react4.useState)(false);
2897
3117
  const [error, setError] = (0, import_react4.useState)(null);
3118
+ const [images, setImages] = (0, import_react4.useState)(
3119
+ () => screenshots.slice(0, MAX_SELF_REPORT_IMAGES)
3120
+ );
2898
3121
  const inputRef = (0, import_react4.useRef)(null);
3122
+ const fileRef = (0, import_react4.useRef)(null);
2899
3123
  (0, import_react4.useEffect)(() => {
2900
3124
  var _a;
2901
3125
  (_a = inputRef.current) == null ? void 0 : _a.focus();
2902
3126
  }, []);
3127
+ const addImages = (0, import_react4.useCallback)((files) => {
3128
+ const picked = files.filter((file) => file.type.startsWith("image/"));
3129
+ if (!picked.length) return;
3130
+ void Promise.all(picked.map((file) => encodeImageFile(file).catch(() => null))).then(
3131
+ (encoded) => {
3132
+ const added = encoded.filter((url) => !!url);
3133
+ if (added.length) {
3134
+ setImages((prev) => [...prev, ...added].slice(0, MAX_SELF_REPORT_IMAGES));
3135
+ }
3136
+ }
3137
+ );
3138
+ }, []);
3139
+ (0, import_react4.useEffect)(() => {
3140
+ const onPaste = (e) => {
3141
+ var _a;
3142
+ try {
3143
+ const items = (_a = e.clipboardData) == null ? void 0 : _a.items;
3144
+ if (!items) return;
3145
+ const files = [];
3146
+ for (let i = 0; i < items.length; i++) {
3147
+ if (items[i].kind !== "file") continue;
3148
+ const file = items[i].getAsFile();
3149
+ if (file) files.push(file);
3150
+ }
3151
+ if (!files.length) return;
3152
+ e.preventDefault();
3153
+ e.stopImmediatePropagation();
3154
+ addImages(files);
3155
+ } catch (e2) {
3156
+ }
3157
+ };
3158
+ window.addEventListener("paste", onPaste, true);
3159
+ return () => window.removeEventListener("paste", onPaste, true);
3160
+ }, [addImages]);
2903
3161
  (0, import_react4.useEffect)(() => {
2904
3162
  const onKey = (e) => {
2905
3163
  if (e.key !== "Escape") return;
@@ -2915,7 +3173,13 @@ function GlitchgrabProblemPanel({
2915
3173
  setBusy(true);
2916
3174
  setError(null);
2917
3175
  try {
2918
- const result = await send({ description, surface, conversationId, reportType, screenshots });
3176
+ const result = await send({
3177
+ description,
3178
+ surface,
3179
+ conversationId,
3180
+ reportType,
3181
+ screenshots: images
3182
+ });
2919
3183
  if (result == null ? void 0 : result.success) setSent(true);
2920
3184
  else setError((result == null ? void 0 : result.message) || FAILED);
2921
3185
  } catch (e) {
@@ -3068,6 +3332,134 @@ function GlitchgrabProblemPanel({
3068
3332
  }
3069
3333
  }
3070
3334
  ),
3335
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3336
+ "div",
3337
+ {
3338
+ "data-gg-self-report-images": "",
3339
+ style: {
3340
+ display: "flex",
3341
+ flexWrap: "wrap",
3342
+ alignItems: "center",
3343
+ gap: "8px",
3344
+ marginTop: "10px"
3345
+ },
3346
+ children: [
3347
+ images.map((src, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { position: "relative", width: "48px", height: "48px" }, children: [
3348
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3349
+ "img",
3350
+ {
3351
+ src,
3352
+ alt: `Screenshot ${i + 1}`,
3353
+ style: {
3354
+ width: "100%",
3355
+ height: "100%",
3356
+ objectFit: "cover",
3357
+ display: "block",
3358
+ boxSizing: "border-box",
3359
+ borderRadius: "8px",
3360
+ border: `1px solid ${t2.inputBorder}`
3361
+ }
3362
+ }
3363
+ ),
3364
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3365
+ "button",
3366
+ {
3367
+ type: "button",
3368
+ "data-gg-self-report-remove-image": "",
3369
+ "aria-label": `Remove screenshot ${i + 1}`,
3370
+ title: "Don't send this one",
3371
+ onClick: () => setImages((prev) => prev.filter((_, j) => j !== i)),
3372
+ style: {
3373
+ position: "absolute",
3374
+ top: "-13px",
3375
+ right: "-13px",
3376
+ width: "32px",
3377
+ height: "32px",
3378
+ border: "none",
3379
+ background: "transparent",
3380
+ padding: 0,
3381
+ cursor: "pointer",
3382
+ display: "flex",
3383
+ alignItems: "center",
3384
+ justifyContent: "center",
3385
+ borderRadius: "50%"
3386
+ },
3387
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3388
+ "span",
3389
+ {
3390
+ "aria-hidden": "true",
3391
+ style: {
3392
+ width: "20px",
3393
+ height: "20px",
3394
+ boxSizing: "border-box",
3395
+ borderRadius: "50%",
3396
+ border: `2px solid ${t2.bg}`,
3397
+ background: "#ef4444",
3398
+ color: "#fff",
3399
+ display: "flex",
3400
+ alignItems: "center",
3401
+ justifyContent: "center"
3402
+ },
3403
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3404
+ "path",
3405
+ {
3406
+ d: "M1 1L9 9M9 1L1 9",
3407
+ stroke: "currentColor",
3408
+ strokeWidth: "1.6",
3409
+ strokeLinecap: "round"
3410
+ }
3411
+ ) })
3412
+ }
3413
+ )
3414
+ }
3415
+ )
3416
+ ] }, i)),
3417
+ images.length < MAX_SELF_REPORT_IMAGES && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3418
+ "button",
3419
+ {
3420
+ type: "button",
3421
+ "data-gg-self-report-attach": "",
3422
+ "aria-label": "Add screenshot",
3423
+ title: "Add a screenshot \u2014 or paste one",
3424
+ onClick: () => {
3425
+ var _a;
3426
+ return (_a = fileRef.current) == null ? void 0 : _a.click();
3427
+ },
3428
+ style: {
3429
+ height: "48px",
3430
+ padding: "0 12px",
3431
+ boxSizing: "border-box",
3432
+ borderRadius: "8px",
3433
+ border: `1px dashed ${t2.inputBorder}`,
3434
+ background: "transparent",
3435
+ color: t2.textMuted,
3436
+ fontSize: "12.5px",
3437
+ fontWeight: 600,
3438
+ fontFamily: "inherit",
3439
+ cursor: "pointer"
3440
+ },
3441
+ children: images.length >= 3 ? "+ Add" : "+ Add screenshot"
3442
+ }
3443
+ ),
3444
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3445
+ "input",
3446
+ {
3447
+ ref: fileRef,
3448
+ type: "file",
3449
+ accept: "image/*",
3450
+ multiple: true,
3451
+ "data-gg-self-report-file": "",
3452
+ onChange: (e) => {
3453
+ var _a;
3454
+ addImages(Array.from((_a = e.target.files) != null ? _a : []));
3455
+ e.target.value = "";
3456
+ },
3457
+ style: { display: "none" }
3458
+ }
3459
+ )
3460
+ ]
3461
+ }
3462
+ ),
3071
3463
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3072
3464
  "p",
3073
3465
  {
@@ -3076,7 +3468,7 @@ function GlitchgrabProblemPanel({
3076
3468
  children: [
3077
3469
  "Also sent, so we can see what happened:",
3078
3470
  " ",
3079
- describeSentAlong(conversationId !== null, screenshots.length),
3471
+ describeSentAlong(conversationId !== null, images.length),
3080
3472
  "."
3081
3473
  ]
3082
3474
  }
@@ -3117,38 +3509,6 @@ function isMac() {
3117
3509
  function getShortcutLabel() {
3118
3510
  return isMac() ? GLITCHGRAB_SHORTCUT_MAC : GLITCHGRAB_SHORTCUT;
3119
3511
  }
3120
- var ATTACHMENT_ACCEPT = [
3121
- "image/*",
3122
- // text — inlined into the issue body
3123
- ".html",
3124
- ".htm",
3125
- ".txt",
3126
- ".md",
3127
- ".json",
3128
- ".xml",
3129
- ".log",
3130
- ".yml",
3131
- ".yaml",
3132
- ".csv",
3133
- ".har",
3134
- ".js",
3135
- ".mjs",
3136
- ".cjs",
3137
- ".ts",
3138
- ".tsx",
3139
- ".jsx",
3140
- ".css",
3141
- ".scss",
3142
- ".py",
3143
- ".sh",
3144
- // binary documents
3145
- ".pdf",
3146
- ".doc",
3147
- ".docx",
3148
- ".xls",
3149
- ".xlsx",
3150
- ".zip"
3151
- ].join(",");
3152
3512
  var MAX_SCALE = 2;
3153
3513
  var FRAME_TIMEOUT_MS = 3e3;
3154
3514
  var GRAB_TIMEOUT_MS = FRAME_TIMEOUT_MS + 2e3;
@@ -3292,6 +3652,27 @@ function captureDefaultScreenshot(options) {
3292
3652
  }
3293
3653
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
3294
3654
  var SHOT_WAIT_MS = 3e3;
3655
+ var SIMILAR_WAIT_MS = 2500;
3656
+ var MAX_SIMILAR_SHOWN = 3;
3657
+ function ButtonSpinner() {
3658
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3659
+ "span",
3660
+ {
3661
+ "aria-hidden": "true",
3662
+ style: {
3663
+ display: "inline-block",
3664
+ width: "11px",
3665
+ height: "11px",
3666
+ marginRight: "6px",
3667
+ verticalAlign: "-1px",
3668
+ border: "2px solid currentColor",
3669
+ borderRightColor: "transparent",
3670
+ borderRadius: "50%",
3671
+ animation: "gg-spin .7s linear infinite"
3672
+ }
3673
+ }
3674
+ );
3675
+ }
3295
3676
  function detectDark() {
3296
3677
  if (typeof window === "undefined") return true;
3297
3678
  try {
@@ -3598,6 +3979,7 @@ function ReportDialog({
3598
3979
  sendFeedback: sendFeedback2,
3599
3980
  enhanceText: enhanceText2,
3600
3981
  assist,
3982
+ findSimilarIssues: findSimilarIssues2,
3601
3983
  assistContext = null,
3602
3984
  transcribeAudio: transcribeAudio2,
3603
3985
  types,
@@ -3617,6 +3999,11 @@ function ReportDialog({
3617
3999
  const [selfReportPrefill, setSelfReportPrefill] = (0, import_react.useState)("");
3618
4000
  const [typeChosen, setTypeChosen] = (0, import_react.useState)(false);
3619
4001
  const [duplicateIssueNumber, setDuplicateIssueNumber] = (0, import_react.useState)(null);
4002
+ const [similarIssues, setSimilarIssues] = (0, import_react.useState)(null);
4003
+ const [checkingSimilar, setCheckingSimilar] = (0, import_react.useState)(false);
4004
+ const [attachingTo, setAttachingTo] = (0, import_react.useState)(null);
4005
+ const [pickedSimilar, setPickedSimilar] = (0, import_react.useState)(null);
4006
+ const [addedToIssue, setAddedToIssue] = (0, import_react.useState)(null);
3620
4007
  const [aiConversationId, setAiConversationId] = (0, import_react.useState)(null);
3621
4008
  const [isEnhanced, setIsEnhanced] = (0, import_react.useState)(false);
3622
4009
  const [originalDescription, setOriginalDescription] = (0, import_react.useState)(
@@ -4047,6 +4434,10 @@ function ReportDialog({
4047
4434
  setReportType("BUG");
4048
4435
  setTypeChosen(false);
4049
4436
  setDuplicateIssueNumber(null);
4437
+ setSimilarIssues(null);
4438
+ setPickedSimilar(null);
4439
+ setCheckingSimilar(false);
4440
+ setAttachingTo(null);
4050
4441
  setSeverity(null);
4051
4442
  setValidationError(null);
4052
4443
  setSelfReportFrom(null);
@@ -4234,7 +4625,8 @@ function ReportDialog({
4234
4625
  stopListeningAndTranscribe();
4235
4626
  }
4236
4627
  };
4237
- const handleSubmit = async () => {
4628
+ const handleSubmit = async (opts) => {
4629
+ var _a2;
4238
4630
  try {
4239
4631
  if (isSubmitting) return;
4240
4632
  if (isRating) {
@@ -4271,6 +4663,22 @@ function ReportDialog({
4271
4663
  setStep(2);
4272
4664
  return;
4273
4665
  }
4666
+ const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
4667
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
4668
+ setIsSubmitting(true);
4669
+ setCheckingSimilar(true);
4670
+ const matches = await Promise.race([
4671
+ findSimilarIssues2(description.trim()).catch(() => null),
4672
+ new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
4673
+ ]);
4674
+ setCheckingSimilar(false);
4675
+ if (matches && matches.length > 0) {
4676
+ setSimilarIssues(matches.slice(0, MAX_SIMILAR_SHOWN));
4677
+ setPickedSimilar(null);
4678
+ setIsSubmitting(false);
4679
+ return;
4680
+ }
4681
+ }
4274
4682
  setIsSubmitting(true);
4275
4683
  let shots = screenshots;
4276
4684
  const pending = pendingShotRef.current;
@@ -4289,8 +4697,8 @@ function ReportDialog({
4289
4697
  if (showSeverity && reportType === "BUG" && severity) {
4290
4698
  metadata.severity = severity;
4291
4699
  }
4292
- if (duplicateIssueNumber) {
4293
- metadata.duplicateIssueNumber = String(duplicateIssueNumber);
4700
+ if (duplicateOf) {
4701
+ metadata.duplicateIssueNumber = String(duplicateOf);
4294
4702
  }
4295
4703
  if (aiConversationId) {
4296
4704
  metadata.aiConversationId = aiConversationId;
@@ -4303,20 +4711,26 @@ function ReportDialog({
4303
4711
  Object.keys(metadata).length > 0 ? metadata : void 0
4304
4712
  );
4305
4713
  if (result) {
4714
+ setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
4306
4715
  setSubmitted(true);
4307
4716
  setDescription("");
4308
4717
  setScreenshots([]);
4309
4718
  setAttachments([]);
4310
4719
  setAiConversationId(null);
4311
4720
  setDuplicateIssueNumber(null);
4721
+ setSimilarIssues(null);
4722
+ setPickedSimilar(null);
4312
4723
  setTimeout(() => {
4313
4724
  setSubmitted(false);
4314
4725
  handleClose();
4315
4726
  }, 2e3);
4316
4727
  }
4317
4728
  setIsSubmitting(false);
4729
+ setAttachingTo(null);
4318
4730
  } catch (e) {
4319
4731
  setIsSubmitting(false);
4732
+ setCheckingSimilar(false);
4733
+ setAttachingTo(null);
4320
4734
  }
4321
4735
  };
4322
4736
  if (!mounted) return null;
@@ -4324,6 +4738,7 @@ function ReportDialog({
4324
4738
  isOpen && (0, import_react_dom.createPortal)(
4325
4739
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
4326
4740
  @keyframes gg-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
4741
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
4327
4742
  @keyframes gg-b1{0%,100%{height:3px}40%{height:13px}}
4328
4743
  @keyframes gg-b2{0%,100%{height:8px}50%{height:3px}}
4329
4744
  @keyframes gg-b3{0%,100%{height:4px}30%{height:14px}70%{height:5px}}
@@ -4608,7 +5023,7 @@ function ReportDialog({
4608
5023
  flex: "1 1 auto",
4609
5024
  minHeight: 0
4610
5025
  },
4611
- children: submitted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
5026
+ children: submitted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
4612
5027
  "div",
4613
5028
  {
4614
5029
  style: {
@@ -4618,10 +5033,7 @@ function ReportDialog({
4618
5033
  fontSize: "14px",
4619
5034
  fontWeight: 500
4620
5035
  },
4621
- children: [
4622
- getTypeLabel(reportType),
4623
- " sent. Thank you!"
4624
- ]
5036
+ children: addedToIssue ? `Added to #${addedToIssue}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
4625
5037
  }
4626
5038
  ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
4627
5039
  step === 1 && headerSlot ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginBottom: "14px" }, children: headerSlot }) : null,
@@ -5846,11 +6258,194 @@ function ReportDialog({
5846
6258
  children: validationError
5847
6259
  }
5848
6260
  ),
5849
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6261
+ similarIssues ? (
6262
+ // Same visual language as the assistant's "Our team is
6263
+ // already on this" card, so both routes to a duplicate
6264
+ // read as one feature.
6265
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6266
+ "div",
6267
+ {
6268
+ "data-gg-similar": "",
6269
+ style: {
6270
+ marginTop: "12px",
6271
+ display: "flex",
6272
+ flexDirection: "column",
6273
+ gap: "10px",
6274
+ padding: "12px",
6275
+ borderRadius: "10px",
6276
+ border: "1px solid rgba(245,158,11,0.4)",
6277
+ backgroundColor: "rgba(245,158,11,0.08)"
6278
+ },
6279
+ children: [
6280
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6281
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this" }),
6282
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches \u2014 your report is added to it, so the team sees everyone hitting it in one place." : "If it's the same problem, pick it \u2014 your report is added to it, so the team sees everyone hitting it in one place." })
6283
+ ] }),
6284
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6285
+ "div",
6286
+ {
6287
+ role: "radiogroup",
6288
+ "aria-label": "Existing issues",
6289
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
6290
+ children: similarIssues.map((issue) => {
6291
+ const picked = pickedSimilar === issue.number;
6292
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6293
+ "button",
6294
+ {
6295
+ type: "button",
6296
+ role: "radio",
6297
+ "aria-checked": picked,
6298
+ disabled: isSubmitting,
6299
+ onClick: () => setPickedSimilar(issue.number),
6300
+ style: {
6301
+ display: "flex",
6302
+ alignItems: "flex-start",
6303
+ gap: "10px",
6304
+ width: "100%",
6305
+ minHeight: "44px",
6306
+ padding: "9px 10px",
6307
+ borderRadius: "8px",
6308
+ border: `1px solid ${picked ? t2.accent : t2.border}`,
6309
+ boxShadow: picked ? `0 0 0 1px ${t2.accent}` : "none",
6310
+ backgroundColor: t2.bg,
6311
+ color: t2.text,
6312
+ textAlign: "left",
6313
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6314
+ fontFamily: "inherit"
6315
+ },
6316
+ children: [
6317
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6318
+ "span",
6319
+ {
6320
+ "aria-hidden": "true",
6321
+ style: {
6322
+ flexShrink: 0,
6323
+ marginTop: "2px",
6324
+ width: "14px",
6325
+ height: "14px",
6326
+ boxSizing: "border-box",
6327
+ borderRadius: "50%",
6328
+ border: `2px solid ${picked ? t2.accent : t2.textMuted}`,
6329
+ backgroundColor: picked ? t2.accent : "transparent",
6330
+ boxShadow: picked ? `inset 0 0 0 2px ${t2.bg}` : "none"
6331
+ }
6332
+ }
6333
+ ),
6334
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6335
+ "span",
6336
+ {
6337
+ style: {
6338
+ display: "flex",
6339
+ flexDirection: "column",
6340
+ gap: "3px",
6341
+ minWidth: 0
6342
+ },
6343
+ children: [
6344
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6345
+ "span",
6346
+ {
6347
+ title: issue.title,
6348
+ style: {
6349
+ fontSize: "12.5px",
6350
+ lineHeight: 1.45,
6351
+ wordBreak: "break-word",
6352
+ overflow: "hidden",
6353
+ display: "-webkit-box",
6354
+ WebkitLineClamp: 3,
6355
+ WebkitBoxOrient: "vertical"
6356
+ },
6357
+ children: [
6358
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { color: t2.textMuted, fontWeight: 600 }, children: [
6359
+ "#",
6360
+ issue.number
6361
+ ] }),
6362
+ " ",
6363
+ issue.title
6364
+ ]
6365
+ }
6366
+ ),
6367
+ issue.status && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "11.5px", color: t2.textMuted }, children: issue.status })
6368
+ ]
6369
+ }
6370
+ )
6371
+ ]
6372
+ },
6373
+ issue.number
6374
+ );
6375
+ })
6376
+ }
6377
+ ),
6378
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6379
+ "button",
6380
+ {
6381
+ type: "button",
6382
+ disabled: isSubmitting || !pickedSimilar,
6383
+ onClick: () => {
6384
+ if (!pickedSimilar) return;
6385
+ setAttachingTo(pickedSimilar);
6386
+ setDuplicateIssueNumber(pickedSimilar);
6387
+ void handleSubmit({ duplicateOf: pickedSimilar });
6388
+ },
6389
+ style: {
6390
+ width: "100%",
6391
+ minHeight: "40px",
6392
+ padding: "10px",
6393
+ borderRadius: "8px",
6394
+ border: "none",
6395
+ backgroundColor: isSubmitting || !pickedSimilar ? t2.bgSecondary : t2.accent,
6396
+ color: isSubmitting || !pickedSimilar ? t2.textMuted : t2.accentText,
6397
+ fontSize: "14px",
6398
+ fontWeight: 600,
6399
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6400
+ fontFamily: "inherit"
6401
+ },
6402
+ children: typeof attachingTo === "number" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6403
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6404
+ "Adding to #",
6405
+ attachingTo,
6406
+ "\u2026"
6407
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6408
+ }
6409
+ ),
6410
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6411
+ "button",
6412
+ {
6413
+ type: "button",
6414
+ disabled: isSubmitting,
6415
+ onClick: () => {
6416
+ setAttachingTo("new");
6417
+ void handleSubmit({ skipSimilar: true });
6418
+ },
6419
+ style: {
6420
+ width: "100%",
6421
+ minHeight: "40px",
6422
+ marginTop: "-4px",
6423
+ padding: "8px",
6424
+ borderRadius: "8px",
6425
+ border: "none",
6426
+ backgroundColor: "transparent",
6427
+ color: isSubmitting ? t2.textMuted : t2.text,
6428
+ fontSize: "13px",
6429
+ fontWeight: 500,
6430
+ textDecoration: "underline",
6431
+ textUnderlineOffset: "3px",
6432
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6433
+ fontFamily: "inherit"
6434
+ },
6435
+ children: attachingTo === "new" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6436
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6437
+ "Sending as new\u2026"
6438
+ ] }) : "No, mine is different \u2014 send as new"
6439
+ }
6440
+ )
6441
+ ]
6442
+ }
6443
+ )
6444
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5850
6445
  "button",
5851
6446
  {
5852
6447
  type: "button",
5853
- onClick: handleSubmit,
6448
+ onClick: () => void handleSubmit(),
5854
6449
  disabled: submitDisabled,
5855
6450
  style: {
5856
6451
  marginTop: "12px",
@@ -5866,7 +6461,10 @@ function ReportDialog({
5866
6461
  fontFamily: "inherit",
5867
6462
  transition: "background-color 0.15s ease"
5868
6463
  },
5869
- children: isSubmitting ? shotPending && !isRating ? "Attaching screenshot\u2026" : "Sending..." : isRating ? "Send Rating" : "Send Report"
6464
+ children: checkingSimilar ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6465
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6466
+ "Checking for existing reports\u2026"
6467
+ ] }) : isSubmitting ? shotPending && !isRating ? "Attaching screenshot\u2026" : "Sending..." : isRating ? "Send Rating" : "Send Report"
5870
6468
  }
5871
6469
  )
5872
6470
  ] })
@@ -6140,8 +6738,10 @@ function ReportDialog({
6140
6738
  theme: layerTheme,
6141
6739
  screenshots,
6142
6740
  pageShot,
6143
- onAddImages: addFiles,
6741
+ onAddFiles: addFiles,
6144
6742
  onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
6743
+ files: attachments,
6744
+ onRemoveFile: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
6145
6745
  attachmentCount: screenshots.length + attachments.length,
6146
6746
  context: { ...assistContext != null ? assistContext : {}, reportType },
6147
6747
  reportTypeLabel: getTypeLabel(reportType),
@@ -6178,14 +6778,14 @@ function ReportDialog({
6178
6778
  severityRefused: needsSeverity,
6179
6779
  isSubmitting,
6180
6780
  submitted,
6181
- onSend: () => void handleSubmit(),
6781
+ onSend: () => void handleSubmit({ skipSimilar: true }),
6182
6782
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
6183
6783
  setSelfReportPrefill(prefill != null ? prefill : "");
6184
6784
  setSelfReportFrom("assist");
6185
6785
  } : void 0,
6186
- onDegrade: (message) => {
6786
+ onDegrade: (message, retryable) => {
6187
6787
  setAssistOpen(false);
6188
- setAssistUsed(true);
6788
+ if (!retryable) setAssistUsed(true);
6189
6789
  setAssistNotice(message);
6190
6790
  },
6191
6791
  onClose: () => {
@@ -7203,6 +7803,12 @@ function GlitchgrabProviderInner({
7203
7803
  },
7204
7804
  []
7205
7805
  );
7806
+ const signedUserId = session == null ? void 0 : session.userId;
7807
+ const signature = typeof (session == null ? void 0 : session.signature) === "string" ? session.signature : null;
7808
+ const identity = (0, import_react6.useMemo)(
7809
+ () => ({ userId: signedUserId != null ? signedUserId : null, signature }),
7810
+ [signedUserId, signature]
7811
+ );
7206
7812
  const enhance = (0, import_react6.useCallback)(
7207
7813
  async (text, screenshot) => {
7208
7814
  try {
@@ -7212,12 +7818,12 @@ function GlitchgrabProviderInner({
7212
7818
  visitedPages: ctx.visitedPages.slice(-5),
7213
7819
  breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
7214
7820
  };
7215
- return await enhanceText(text, token, baseUrl, screenshot, context);
7821
+ return await enhanceText(text, token, baseUrl, screenshot, context, identity);
7216
7822
  } catch (e) {
7217
7823
  return text;
7218
7824
  }
7219
7825
  },
7220
- [token, baseUrl]
7826
+ [token, baseUrl, identity]
7221
7827
  );
7222
7828
  const assist = (0, import_react6.useCallback)(
7223
7829
  async (params) => {
@@ -7230,7 +7836,8 @@ function GlitchgrabProviderInner({
7230
7836
  reporter: session ? {
7231
7837
  key: session.userId,
7232
7838
  name: session.name ? String(session.name) : null,
7233
- email: session.email ? String(session.email) : null
7839
+ email: session.email ? String(session.email) : null,
7840
+ ...signature ? { signature } : {}
7234
7841
  } : void 0,
7235
7842
  context: {
7236
7843
  ...(_a = params.context) != null ? _a : {},
@@ -7245,15 +7852,19 @@ function GlitchgrabProviderInner({
7245
7852
  },
7246
7853
  [token, baseUrl, session]
7247
7854
  );
7855
+ const findSimilar = (0, import_react6.useCallback)(
7856
+ (description) => findSimilarIssues(description, token, baseUrl, identity),
7857
+ [token, baseUrl, identity]
7858
+ );
7248
7859
  const transcribe = (0, import_react6.useCallback)(
7249
7860
  async (blob) => {
7250
7861
  try {
7251
- return await transcribeAudio(blob, token, baseUrl);
7862
+ return await transcribeAudio(blob, token, baseUrl, identity);
7252
7863
  } catch (e) {
7253
7864
  return "";
7254
7865
  }
7255
7866
  },
7256
- [token, baseUrl]
7867
+ [token, baseUrl, identity]
7257
7868
  );
7258
7869
  const openReportDialog = (0, import_react6.useCallback)((options) => {
7259
7870
  if (typeof window !== "undefined") {
@@ -7271,6 +7882,8 @@ function GlitchgrabProviderInner({
7271
7882
  const [projectName, setProjectName] = (0, import_react6.useState)(null);
7272
7883
  const [aiAssist, setAiAssist] = (0, import_react6.useState)(false);
7273
7884
  const [glitchgrabReport, setGlitchgrabReport] = (0, import_react6.useState)(false);
7885
+ const [signedUsers, setSignedUsers] = (0, import_react6.useState)("OFF");
7886
+ const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
7274
7887
  const [bookingOpen, setBookingOpen] = (0, import_react6.useState)(false);
7275
7888
  const openBookingDialog = (0, import_react6.useCallback)(() => setBookingOpen(true), []);
7276
7889
  (0, import_react6.useEffect)(() => {
@@ -7283,12 +7896,25 @@ function GlitchgrabProviderInner({
7283
7896
  setProjectName(json.data.name);
7284
7897
  setAiAssist(json.data.aiAssist === true);
7285
7898
  setGlitchgrabReport(json.data.glitchgrabReport === true);
7899
+ const mode = json.data.signedUsers;
7900
+ setSignedUsers(mode === "WARN" || mode === "ENFORCE" ? mode : "OFF");
7286
7901
  }).catch(() => {
7287
7902
  });
7288
7903
  return () => {
7289
7904
  cancelled = true;
7290
7905
  };
7291
7906
  }, [baseUrl, token]);
7907
+ (0, import_react6.useEffect)(() => {
7908
+ var _a;
7909
+ try {
7910
+ if (signedUsers === "OFF" || signature || !session) return;
7911
+ if (typeof process === "undefined" || ((_a = process.env) == null ? void 0 : _a.NODE_ENV) !== "development") return;
7912
+ console.warn(
7913
+ signedUsers === "ENFORCE" ? '[glitchgrab] This project enforces signed users and `session.signature` is missing \u2014 AI features are hidden. Sign it on your server with signSession() from "glitchgrab/server".' : '[glitchgrab] This project warns on unsigned users and `session.signature` is missing. Sign it on your server with signSession() from "glitchgrab/server" before the owner switches to Enforce.'
7914
+ );
7915
+ } catch (e) {
7916
+ }
7917
+ }, [signedUsers, signature, session]);
7292
7918
  (0, import_react6.useEffect)(() => {
7293
7919
  try {
7294
7920
  if (typeof window === "undefined") return;
@@ -7363,10 +7989,11 @@ function GlitchgrabProviderInner({
7363
7989
  {
7364
7990
  report,
7365
7991
  sendFeedback: feedback,
7366
- enhanceText: enhance,
7367
- assist: aiAssist ? assist : void 0,
7992
+ enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
7993
+ assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
7994
+ findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
7368
7995
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
7369
- transcribeAudio: transcribe,
7996
+ transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
7370
7997
  types,
7371
7998
  showSeverity,
7372
7999
  reporter,