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.mjs CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  useContext,
7
7
  useRef as useRef5,
8
8
  useEffect as useEffect6,
9
- useCallback as useCallback2,
10
- useMemo as useMemo3,
9
+ useCallback as useCallback3,
10
+ useMemo as useMemo4,
11
11
  useState as useState6
12
12
  } from "react";
13
13
 
@@ -634,7 +634,11 @@ async function sendFeedback(payload, baseUrl) {
634
634
  return null;
635
635
  }
636
636
  }
637
- async function enhanceText(text, token, baseUrl, screenshot, context) {
637
+ function signedFields(identity) {
638
+ if (!(identity == null ? void 0 : identity.signature) || !identity.userId) return {};
639
+ return { userId: identity.userId, signature: identity.signature };
640
+ }
641
+ async function enhanceText(text, token, baseUrl, screenshot, context, identity) {
638
642
  var _a;
639
643
  try {
640
644
  const trimmed = text.trim();
@@ -649,7 +653,8 @@ async function enhanceText(text, token, baseUrl, screenshot, context) {
649
653
  body: JSON.stringify({
650
654
  text: trimmed,
651
655
  ...screenshot ? { screenshot } : {},
652
- ...context ? { context } : {}
656
+ ...context ? { context } : {},
657
+ ...signedFields(identity)
653
658
  })
654
659
  });
655
660
  if (!response.ok) return text;
@@ -662,6 +667,26 @@ async function enhanceText(text, token, baseUrl, screenshot, context) {
662
667
  return text;
663
668
  }
664
669
  }
670
+ async function findSimilarIssues(description, token, baseUrl, identity) {
671
+ var _a;
672
+ try {
673
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/reports/similar`;
674
+ const response = await fetch(url, {
675
+ method: "POST",
676
+ headers: {
677
+ "Content-Type": "application/json",
678
+ Authorization: `Bearer ${token}`
679
+ },
680
+ body: JSON.stringify({ text: description, ...signedFields(identity) })
681
+ });
682
+ const envelope = await response.json().catch(() => null);
683
+ if (!response.ok || !(envelope == null ? void 0 : envelope.success)) return null;
684
+ if (!((_a = envelope.data) == null ? void 0 : _a.enabled)) return [];
685
+ return Array.isArray(envelope.data.matches) ? envelope.data.matches : [];
686
+ } catch (e) {
687
+ return null;
688
+ }
689
+ }
665
690
  async function assistTurn(params, token, baseUrl) {
666
691
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
667
692
  const offline = {
@@ -686,7 +711,9 @@ async function assistTurn(params, token, baseUrl) {
686
711
  conversationId: null,
687
712
  question: null,
688
713
  report: null,
689
- degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : offline.degraded
714
+ degraded: (_a = envelope == null ? void 0 : envelope.error) != null ? _a : offline.degraded,
715
+ // An hourly limit, not a cap: the dialog keeps the assistant button.
716
+ retryable: (envelope == null ? void 0 : envelope.retryable) === true
690
717
  };
691
718
  }
692
719
  return {
@@ -703,13 +730,18 @@ async function assistTurn(params, token, baseUrl) {
703
730
  return offline;
704
731
  }
705
732
  }
706
- async function transcribeAudio(blob, token, baseUrl) {
733
+ async function transcribeAudio(blob, token, baseUrl, identity) {
707
734
  var _a;
708
735
  try {
709
736
  if (blob.size === 0) return "";
710
737
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/stt`;
711
738
  const form = new FormData();
712
739
  form.append("file", blob, "audio.webm");
740
+ const signed = signedFields(identity);
741
+ if (signed.userId && signed.signature) {
742
+ form.append("userId", signed.userId);
743
+ form.append("signature", signed.signature);
744
+ }
713
745
  const response = await fetch(url, {
714
746
  method: "POST",
715
747
  headers: { Authorization: `Bearer ${token}` },
@@ -914,15 +946,15 @@ import {
914
946
  useState as useState4,
915
947
  useRef as useRef4,
916
948
  useEffect as useEffect4,
917
- useMemo
949
+ useMemo as useMemo2
918
950
  } from "react";
919
951
  import { createPortal as createPortal3 } from "react-dom";
920
952
  import { useEffect, useRef, useState } from "react";
921
953
  import { jsx, jsxs } from "react/jsx-runtime";
922
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
954
+ import { useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
923
955
  import { createPortal } from "react-dom";
924
956
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
925
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
957
+ import { useCallback, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
926
958
  import { createPortal as createPortal2 } from "react-dom";
927
959
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
928
960
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -1283,6 +1315,59 @@ var SEVERITY_LABELS = {
1283
1315
  };
1284
1316
  var MAX_ASSIST_IMAGES = 3;
1285
1317
  var MAX_ASSIST_IMAGE_CHARS = 35e5;
1318
+ var MAX_ASSIST_FILES = 3;
1319
+ var MAX_ASSIST_FILE_CHARS = 3e5;
1320
+ var MAX_ASSIST_FILE_CHARS_EACH = 2e5;
1321
+ var TEXT_ATTACHMENT_EXTENSIONS = [
1322
+ ".html",
1323
+ ".htm",
1324
+ ".txt",
1325
+ ".md",
1326
+ ".json",
1327
+ ".xml",
1328
+ ".log",
1329
+ ".yml",
1330
+ ".yaml",
1331
+ ".csv",
1332
+ ".har",
1333
+ ".js",
1334
+ ".mjs",
1335
+ ".cjs",
1336
+ ".ts",
1337
+ ".tsx",
1338
+ ".jsx",
1339
+ ".css",
1340
+ ".scss",
1341
+ ".py",
1342
+ ".sh"
1343
+ ];
1344
+ var BINARY_ATTACHMENT_EXTENSIONS = [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".zip"];
1345
+ var ATTACHMENT_ACCEPT = [
1346
+ "image/*",
1347
+ ...TEXT_ATTACHMENT_EXTENSIONS,
1348
+ ...BINARY_ATTACHMENT_EXTENSIONS
1349
+ ].join(",");
1350
+ var ASSIST_ATTACHMENT_ACCEPT = ["image/*", ...TEXT_ATTACHMENT_EXTENSIONS].join(",");
1351
+ function isAssistReadableFile(name) {
1352
+ const lower = name.toLowerCase();
1353
+ return TEXT_ATTACHMENT_EXTENSIONS.some((ext) => lower.endsWith(ext));
1354
+ }
1355
+ function decodeTextDataUrl(dataUrl) {
1356
+ try {
1357
+ if (!dataUrl.startsWith("data:")) return null;
1358
+ const comma = dataUrl.indexOf(",");
1359
+ if (comma < 0) return null;
1360
+ const meta = dataUrl.slice(5, comma);
1361
+ const payload = dataUrl.slice(comma + 1);
1362
+ if (!meta.endsWith(";base64")) return decodeURIComponent(payload);
1363
+ const binary = atob(payload);
1364
+ const bytes = new Uint8Array(binary.length);
1365
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1366
+ return new TextDecoder("utf-8").decode(bytes);
1367
+ } catch (e) {
1368
+ return null;
1369
+ }
1370
+ }
1286
1371
  function getTypeLabel(type) {
1287
1372
  switch (type) {
1288
1373
  case "RATING":
@@ -1314,6 +1399,22 @@ function pickAssistImages(all) {
1314
1399
  }
1315
1400
  return picked;
1316
1401
  }
1402
+ var NO_FILES = [];
1403
+ function pickAssistFiles(files) {
1404
+ const picked = [];
1405
+ let total = 0;
1406
+ for (let i = files.length - 1; i >= 0 && picked.length < MAX_ASSIST_FILES; i--) {
1407
+ const attached = files[i];
1408
+ if (!isAssistReadableFile(attached.name)) continue;
1409
+ const text = decodeTextDataUrl(attached.dataUrl);
1410
+ if (!text || !text.trim()) continue;
1411
+ const content = text.slice(0, MAX_ASSIST_FILE_CHARS_EACH);
1412
+ if (total + content.length > MAX_ASSIST_FILE_CHARS) continue;
1413
+ picked.unshift({ index: i, file: { name: attached.name, content } });
1414
+ total += content.length;
1415
+ }
1416
+ return picked;
1417
+ }
1317
1418
  var STARTERS = [
1318
1419
  "Something on this page is broken",
1319
1420
  "It did the wrong thing",
@@ -1390,7 +1491,9 @@ function AssistSheet({
1390
1491
  theme: t2,
1391
1492
  screenshots,
1392
1493
  pageShot = null,
1393
- onAddImages,
1494
+ onAddFiles,
1495
+ files = NO_FILES,
1496
+ onRemoveFile,
1394
1497
  onRemoveImage,
1395
1498
  attachmentCount,
1396
1499
  context,
@@ -1461,6 +1564,8 @@ function AssistSheet({
1461
1564
  }
1462
1565
  }
1463
1566
  const readShots = new Set(pickAssistImages(screenshots));
1567
+ const sentFiles = useMemo(() => pickAssistFiles(files), [files]);
1568
+ const readFiles = new Set(sentFiles.map((f) => f.index));
1464
1569
  function leave(type, detail) {
1465
1570
  logEvent(type, detail);
1466
1571
  flushEvents();
@@ -1474,10 +1579,10 @@ function AssistSheet({
1474
1579
  var _a;
1475
1580
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1476
1581
  }, [phase]);
1477
- function degradeKeepingEvents(events, message) {
1582
+ function degradeKeepingEvents(events, message, retryable) {
1478
1583
  if (events) eventsRef.current = [...events, ...eventsRef.current];
1479
1584
  flushEvents();
1480
- onDegrade(message);
1585
+ onDegrade(message, retryable);
1481
1586
  }
1482
1587
  async function runTurn(history2) {
1483
1588
  var _a, _b, _c, _d;
@@ -1495,6 +1600,9 @@ function AssistSheet({
1495
1600
  ),
1496
1601
  conversationId,
1497
1602
  screenshots: sent,
1603
+ // Only when there is something: an older server then sees the body
1604
+ // it always did.
1605
+ ...sentFiles.length > 0 ? { files: sentFiles.map((f) => f.file) } : {},
1498
1606
  context: {
1499
1607
  ...context != null ? context : {},
1500
1608
  imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached"),
@@ -1514,7 +1622,7 @@ function AssistSheet({
1514
1622
  setBusy(false);
1515
1623
  }
1516
1624
  if (result.degraded) {
1517
- degradeKeepingEvents(events, result.degraded);
1625
+ degradeKeepingEvents(events, result.degraded, result.retryable === true);
1518
1626
  return;
1519
1627
  }
1520
1628
  if (result.conversationId) {
@@ -1614,7 +1722,7 @@ function AssistSheet({
1614
1722
  minWidth: 0
1615
1723
  },
1616
1724
  children: [
1617
- /* @__PURE__ */ jsx2("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1725
+ /* @__PURE__ */ jsx2("span", { style: { color: t2.text, fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1618
1726
  title && /* @__PURE__ */ jsxs2("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1619
1727
  "#",
1620
1728
  duplicate.number,
@@ -2502,7 +2610,7 @@ function AssistSheet({
2502
2610
  // instead of remounting, and a draft is something you can
2503
2611
  // still reply to (#360), not a form that replaced the chat.
2504
2612
  /* @__PURE__ */ jsxs2(Fragment, { children: [
2505
- (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ jsxs2(
2613
+ (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ jsxs2(
2506
2614
  "div",
2507
2615
  {
2508
2616
  style: {
@@ -2606,11 +2714,118 @@ function AssistSheet({
2606
2714
  `${i}-${shot.slice(-16)}`
2607
2715
  );
2608
2716
  }),
2609
- onAddImages && /* @__PURE__ */ jsxs2(
2717
+ files.map((file, i) => {
2718
+ const read = readFiles.has(i);
2719
+ 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";
2720
+ return /* @__PURE__ */ jsxs2(
2721
+ "div",
2722
+ {
2723
+ "data-gg-assist-file": "",
2724
+ "data-gg-assist-file-read": read ? "" : void 0,
2725
+ title: `${file.name} \u2014 ${why}`,
2726
+ style: {
2727
+ position: "relative",
2728
+ height: "40px",
2729
+ maxWidth: "170px",
2730
+ display: "flex",
2731
+ alignItems: "center",
2732
+ gap: "6px",
2733
+ padding: "0 10px",
2734
+ borderRadius: "8px",
2735
+ border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2736
+ opacity: read ? 1 : 0.55,
2737
+ boxSizing: "border-box",
2738
+ flexShrink: 0
2739
+ },
2740
+ children: [
2741
+ /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx2(
2742
+ "path",
2743
+ {
2744
+ d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
2745
+ stroke: t2.textMuted,
2746
+ strokeWidth: "1.5",
2747
+ strokeLinecap: "round",
2748
+ strokeLinejoin: "round"
2749
+ }
2750
+ ) }),
2751
+ /* @__PURE__ */ jsx2(
2752
+ "span",
2753
+ {
2754
+ style: {
2755
+ fontSize: "11.5px",
2756
+ color: t2.text,
2757
+ overflow: "hidden",
2758
+ textOverflow: "ellipsis",
2759
+ whiteSpace: "nowrap"
2760
+ },
2761
+ children: file.name
2762
+ }
2763
+ ),
2764
+ onRemoveFile && /* @__PURE__ */ jsx2(
2765
+ "button",
2766
+ {
2767
+ type: "button",
2768
+ "data-gg-assist-remove-file": "",
2769
+ "aria-label": `Remove file ${file.name}`,
2770
+ title: "Remove this file",
2771
+ onClick: () => {
2772
+ logEvent("file_removed", read ? "read" : "not read");
2773
+ onRemoveFile(i);
2774
+ },
2775
+ style: {
2776
+ position: "absolute",
2777
+ top: "-13px",
2778
+ right: "-13px",
2779
+ width: "32px",
2780
+ height: "32px",
2781
+ border: "none",
2782
+ background: "transparent",
2783
+ padding: 0,
2784
+ cursor: "pointer",
2785
+ display: "flex",
2786
+ alignItems: "center",
2787
+ justifyContent: "center",
2788
+ borderRadius: "50%"
2789
+ },
2790
+ children: /* @__PURE__ */ jsx2(
2791
+ "span",
2792
+ {
2793
+ "aria-hidden": "true",
2794
+ style: {
2795
+ width: "20px",
2796
+ height: "20px",
2797
+ boxSizing: "border-box",
2798
+ borderRadius: "50%",
2799
+ border: `2px solid ${t2.bg}`,
2800
+ background: "#ef4444",
2801
+ color: "#fff",
2802
+ display: "flex",
2803
+ alignItems: "center",
2804
+ justifyContent: "center"
2805
+ },
2806
+ children: /* @__PURE__ */ jsx2("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx2(
2807
+ "path",
2808
+ {
2809
+ d: "M1 1L9 9M9 1L1 9",
2810
+ stroke: "currentColor",
2811
+ strokeWidth: "1.6",
2812
+ strokeLinecap: "round"
2813
+ }
2814
+ ) })
2815
+ }
2816
+ )
2817
+ }
2818
+ )
2819
+ ]
2820
+ },
2821
+ `file-${i}-${file.name}`
2822
+ );
2823
+ }),
2824
+ onAddFiles && /* @__PURE__ */ jsxs2(
2610
2825
  "label",
2611
2826
  {
2612
2827
  "data-gg-assist-attach": "",
2613
- title: "Attach an image \u2014 or just paste one",
2828
+ title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
2614
2829
  style: {
2615
2830
  width: "40px",
2616
2831
  height: "40px",
@@ -2630,14 +2845,18 @@ function AssistSheet({
2630
2845
  "input",
2631
2846
  {
2632
2847
  type: "file",
2633
- accept: "image/*",
2848
+ accept: ASSIST_ATTACHMENT_ACCEPT,
2634
2849
  multiple: true,
2635
2850
  onChange: (e) => {
2636
2851
  var _a;
2637
- const files = Array.from((_a = e.target.files) != null ? _a : []);
2638
- if (files.length) {
2639
- logEvent("image_added", String(files.length));
2640
- onAddImages(files);
2852
+ const picked = Array.from((_a = e.target.files) != null ? _a : []);
2853
+ if (picked.length) {
2854
+ const images = picked.filter((f) => f.type.startsWith("image/")).length;
2855
+ if (images) logEvent("image_added", String(images));
2856
+ if (picked.length > images) {
2857
+ logEvent("file_added", String(picked.length - images));
2858
+ }
2859
+ onAddFiles(picked);
2641
2860
  }
2642
2861
  e.target.value = "";
2643
2862
  },
@@ -2818,6 +3037,7 @@ function AssistSheet({
2818
3037
  }
2819
3038
  var FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
2820
3039
  var FAILED = "Couldn't send it. Try again in a moment.";
3040
+ var MAX_SELF_REPORT_IMAGES = 5;
2821
3041
  function describeSentAlong(hasChat, screenshotCount) {
2822
3042
  const parts = [
2823
3043
  ...hasChat ? ["your AI chat"] : [],
@@ -2841,11 +3061,49 @@ function GlitchgrabProblemPanel({
2841
3061
  const [busy, setBusy] = useState3(false);
2842
3062
  const [sent, setSent] = useState3(false);
2843
3063
  const [error, setError] = useState3(null);
3064
+ const [images, setImages] = useState3(
3065
+ () => screenshots.slice(0, MAX_SELF_REPORT_IMAGES)
3066
+ );
2844
3067
  const inputRef = useRef3(null);
3068
+ const fileRef = useRef3(null);
2845
3069
  useEffect3(() => {
2846
3070
  var _a;
2847
3071
  (_a = inputRef.current) == null ? void 0 : _a.focus();
2848
3072
  }, []);
3073
+ const addImages = useCallback((files) => {
3074
+ const picked = files.filter((file) => file.type.startsWith("image/"));
3075
+ if (!picked.length) return;
3076
+ void Promise.all(picked.map((file) => encodeImageFile(file).catch(() => null))).then(
3077
+ (encoded) => {
3078
+ const added = encoded.filter((url) => !!url);
3079
+ if (added.length) {
3080
+ setImages((prev) => [...prev, ...added].slice(0, MAX_SELF_REPORT_IMAGES));
3081
+ }
3082
+ }
3083
+ );
3084
+ }, []);
3085
+ useEffect3(() => {
3086
+ const onPaste = (e) => {
3087
+ var _a;
3088
+ try {
3089
+ const items = (_a = e.clipboardData) == null ? void 0 : _a.items;
3090
+ if (!items) return;
3091
+ const files = [];
3092
+ for (let i = 0; i < items.length; i++) {
3093
+ if (items[i].kind !== "file") continue;
3094
+ const file = items[i].getAsFile();
3095
+ if (file) files.push(file);
3096
+ }
3097
+ if (!files.length) return;
3098
+ e.preventDefault();
3099
+ e.stopImmediatePropagation();
3100
+ addImages(files);
3101
+ } catch (e2) {
3102
+ }
3103
+ };
3104
+ window.addEventListener("paste", onPaste, true);
3105
+ return () => window.removeEventListener("paste", onPaste, true);
3106
+ }, [addImages]);
2849
3107
  useEffect3(() => {
2850
3108
  const onKey = (e) => {
2851
3109
  if (e.key !== "Escape") return;
@@ -2861,7 +3119,13 @@ function GlitchgrabProblemPanel({
2861
3119
  setBusy(true);
2862
3120
  setError(null);
2863
3121
  try {
2864
- const result = await send({ description, surface, conversationId, reportType, screenshots });
3122
+ const result = await send({
3123
+ description,
3124
+ surface,
3125
+ conversationId,
3126
+ reportType,
3127
+ screenshots: images
3128
+ });
2865
3129
  if (result == null ? void 0 : result.success) setSent(true);
2866
3130
  else setError((result == null ? void 0 : result.message) || FAILED);
2867
3131
  } catch (e) {
@@ -3014,6 +3278,134 @@ function GlitchgrabProblemPanel({
3014
3278
  }
3015
3279
  }
3016
3280
  ),
3281
+ /* @__PURE__ */ jsxs3(
3282
+ "div",
3283
+ {
3284
+ "data-gg-self-report-images": "",
3285
+ style: {
3286
+ display: "flex",
3287
+ flexWrap: "wrap",
3288
+ alignItems: "center",
3289
+ gap: "8px",
3290
+ marginTop: "10px"
3291
+ },
3292
+ children: [
3293
+ images.map((src, i) => /* @__PURE__ */ jsxs3("div", { style: { position: "relative", width: "48px", height: "48px" }, children: [
3294
+ /* @__PURE__ */ jsx3(
3295
+ "img",
3296
+ {
3297
+ src,
3298
+ alt: `Screenshot ${i + 1}`,
3299
+ style: {
3300
+ width: "100%",
3301
+ height: "100%",
3302
+ objectFit: "cover",
3303
+ display: "block",
3304
+ boxSizing: "border-box",
3305
+ borderRadius: "8px",
3306
+ border: `1px solid ${t2.inputBorder}`
3307
+ }
3308
+ }
3309
+ ),
3310
+ /* @__PURE__ */ jsx3(
3311
+ "button",
3312
+ {
3313
+ type: "button",
3314
+ "data-gg-self-report-remove-image": "",
3315
+ "aria-label": `Remove screenshot ${i + 1}`,
3316
+ title: "Don't send this one",
3317
+ onClick: () => setImages((prev) => prev.filter((_, j) => j !== i)),
3318
+ style: {
3319
+ position: "absolute",
3320
+ top: "-13px",
3321
+ right: "-13px",
3322
+ width: "32px",
3323
+ height: "32px",
3324
+ border: "none",
3325
+ background: "transparent",
3326
+ padding: 0,
3327
+ cursor: "pointer",
3328
+ display: "flex",
3329
+ alignItems: "center",
3330
+ justifyContent: "center",
3331
+ borderRadius: "50%"
3332
+ },
3333
+ children: /* @__PURE__ */ jsx3(
3334
+ "span",
3335
+ {
3336
+ "aria-hidden": "true",
3337
+ style: {
3338
+ width: "20px",
3339
+ height: "20px",
3340
+ boxSizing: "border-box",
3341
+ borderRadius: "50%",
3342
+ border: `2px solid ${t2.bg}`,
3343
+ background: "#ef4444",
3344
+ color: "#fff",
3345
+ display: "flex",
3346
+ alignItems: "center",
3347
+ justifyContent: "center"
3348
+ },
3349
+ children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3350
+ "path",
3351
+ {
3352
+ d: "M1 1L9 9M9 1L1 9",
3353
+ stroke: "currentColor",
3354
+ strokeWidth: "1.6",
3355
+ strokeLinecap: "round"
3356
+ }
3357
+ ) })
3358
+ }
3359
+ )
3360
+ }
3361
+ )
3362
+ ] }, i)),
3363
+ images.length < MAX_SELF_REPORT_IMAGES && /* @__PURE__ */ jsx3(
3364
+ "button",
3365
+ {
3366
+ type: "button",
3367
+ "data-gg-self-report-attach": "",
3368
+ "aria-label": "Add screenshot",
3369
+ title: "Add a screenshot \u2014 or paste one",
3370
+ onClick: () => {
3371
+ var _a;
3372
+ return (_a = fileRef.current) == null ? void 0 : _a.click();
3373
+ },
3374
+ style: {
3375
+ height: "48px",
3376
+ padding: "0 12px",
3377
+ boxSizing: "border-box",
3378
+ borderRadius: "8px",
3379
+ border: `1px dashed ${t2.inputBorder}`,
3380
+ background: "transparent",
3381
+ color: t2.textMuted,
3382
+ fontSize: "12.5px",
3383
+ fontWeight: 600,
3384
+ fontFamily: "inherit",
3385
+ cursor: "pointer"
3386
+ },
3387
+ children: images.length >= 3 ? "+ Add" : "+ Add screenshot"
3388
+ }
3389
+ ),
3390
+ /* @__PURE__ */ jsx3(
3391
+ "input",
3392
+ {
3393
+ ref: fileRef,
3394
+ type: "file",
3395
+ accept: "image/*",
3396
+ multiple: true,
3397
+ "data-gg-self-report-file": "",
3398
+ onChange: (e) => {
3399
+ var _a;
3400
+ addImages(Array.from((_a = e.target.files) != null ? _a : []));
3401
+ e.target.value = "";
3402
+ },
3403
+ style: { display: "none" }
3404
+ }
3405
+ )
3406
+ ]
3407
+ }
3408
+ ),
3017
3409
  /* @__PURE__ */ jsxs3(
3018
3410
  "p",
3019
3411
  {
@@ -3022,7 +3414,7 @@ function GlitchgrabProblemPanel({
3022
3414
  children: [
3023
3415
  "Also sent, so we can see what happened:",
3024
3416
  " ",
3025
- describeSentAlong(conversationId !== null, screenshots.length),
3417
+ describeSentAlong(conversationId !== null, images.length),
3026
3418
  "."
3027
3419
  ]
3028
3420
  }
@@ -3063,38 +3455,6 @@ function isMac() {
3063
3455
  function getShortcutLabel() {
3064
3456
  return isMac() ? GLITCHGRAB_SHORTCUT_MAC : GLITCHGRAB_SHORTCUT;
3065
3457
  }
3066
- var ATTACHMENT_ACCEPT = [
3067
- "image/*",
3068
- // text — inlined into the issue body
3069
- ".html",
3070
- ".htm",
3071
- ".txt",
3072
- ".md",
3073
- ".json",
3074
- ".xml",
3075
- ".log",
3076
- ".yml",
3077
- ".yaml",
3078
- ".csv",
3079
- ".har",
3080
- ".js",
3081
- ".mjs",
3082
- ".cjs",
3083
- ".ts",
3084
- ".tsx",
3085
- ".jsx",
3086
- ".css",
3087
- ".scss",
3088
- ".py",
3089
- ".sh",
3090
- // binary documents
3091
- ".pdf",
3092
- ".doc",
3093
- ".docx",
3094
- ".xls",
3095
- ".xlsx",
3096
- ".zip"
3097
- ].join(",");
3098
3458
  var MAX_SCALE = 2;
3099
3459
  var FRAME_TIMEOUT_MS = 3e3;
3100
3460
  var GRAB_TIMEOUT_MS = FRAME_TIMEOUT_MS + 2e3;
@@ -3238,6 +3598,27 @@ function captureDefaultScreenshot(options) {
3238
3598
  }
3239
3599
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
3240
3600
  var SHOT_WAIT_MS = 3e3;
3601
+ var SIMILAR_WAIT_MS = 2500;
3602
+ var MAX_SIMILAR_SHOWN = 3;
3603
+ function ButtonSpinner() {
3604
+ return /* @__PURE__ */ jsx4(
3605
+ "span",
3606
+ {
3607
+ "aria-hidden": "true",
3608
+ style: {
3609
+ display: "inline-block",
3610
+ width: "11px",
3611
+ height: "11px",
3612
+ marginRight: "6px",
3613
+ verticalAlign: "-1px",
3614
+ border: "2px solid currentColor",
3615
+ borderRightColor: "transparent",
3616
+ borderRadius: "50%",
3617
+ animation: "gg-spin .7s linear infinite"
3618
+ }
3619
+ }
3620
+ );
3621
+ }
3241
3622
  function detectDark() {
3242
3623
  if (typeof window === "undefined") return true;
3243
3624
  try {
@@ -3544,6 +3925,7 @@ function ReportDialog({
3544
3925
  sendFeedback: sendFeedback2,
3545
3926
  enhanceText: enhanceText2,
3546
3927
  assist,
3928
+ findSimilarIssues: findSimilarIssues2,
3547
3929
  assistContext = null,
3548
3930
  transcribeAudio: transcribeAudio2,
3549
3931
  types,
@@ -3563,6 +3945,11 @@ function ReportDialog({
3563
3945
  const [selfReportPrefill, setSelfReportPrefill] = useState4("");
3564
3946
  const [typeChosen, setTypeChosen] = useState4(false);
3565
3947
  const [duplicateIssueNumber, setDuplicateIssueNumber] = useState4(null);
3948
+ const [similarIssues, setSimilarIssues] = useState4(null);
3949
+ const [checkingSimilar, setCheckingSimilar] = useState4(false);
3950
+ const [attachingTo, setAttachingTo] = useState4(null);
3951
+ const [pickedSimilar, setPickedSimilar] = useState4(null);
3952
+ const [addedToIssue, setAddedToIssue] = useState4(null);
3566
3953
  const [aiConversationId, setAiConversationId] = useState4(null);
3567
3954
  const [isEnhanced, setIsEnhanced] = useState4(false);
3568
3955
  const [originalDescription, setOriginalDescription] = useState4(
@@ -3713,7 +4100,7 @@ function ReportDialog({
3713
4100
  const next = current === -1 ? 0 : Math.min(Math.max(current + delta, 0), tiles.length - 1);
3714
4101
  (_c = tiles[next]) == null ? void 0 : _c.focus();
3715
4102
  };
3716
- const isDark = useMemo(() => isOpen ? detectDark() : true, [isOpen]);
4103
+ const isDark = useMemo2(() => isOpen ? detectDark() : true, [isOpen]);
3717
4104
  const t2 = getTheme(isDark);
3718
4105
  const layerTheme = {
3719
4106
  bg: t2.bg,
@@ -3993,6 +4380,10 @@ function ReportDialog({
3993
4380
  setReportType("BUG");
3994
4381
  setTypeChosen(false);
3995
4382
  setDuplicateIssueNumber(null);
4383
+ setSimilarIssues(null);
4384
+ setPickedSimilar(null);
4385
+ setCheckingSimilar(false);
4386
+ setAttachingTo(null);
3996
4387
  setSeverity(null);
3997
4388
  setValidationError(null);
3998
4389
  setSelfReportFrom(null);
@@ -4180,7 +4571,8 @@ function ReportDialog({
4180
4571
  stopListeningAndTranscribe();
4181
4572
  }
4182
4573
  };
4183
- const handleSubmit = async () => {
4574
+ const handleSubmit = async (opts) => {
4575
+ var _a2;
4184
4576
  try {
4185
4577
  if (isSubmitting) return;
4186
4578
  if (isRating) {
@@ -4217,6 +4609,22 @@ function ReportDialog({
4217
4609
  setStep(2);
4218
4610
  return;
4219
4611
  }
4612
+ const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
4613
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
4614
+ setIsSubmitting(true);
4615
+ setCheckingSimilar(true);
4616
+ const matches = await Promise.race([
4617
+ findSimilarIssues2(description.trim()).catch(() => null),
4618
+ new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
4619
+ ]);
4620
+ setCheckingSimilar(false);
4621
+ if (matches && matches.length > 0) {
4622
+ setSimilarIssues(matches.slice(0, MAX_SIMILAR_SHOWN));
4623
+ setPickedSimilar(null);
4624
+ setIsSubmitting(false);
4625
+ return;
4626
+ }
4627
+ }
4220
4628
  setIsSubmitting(true);
4221
4629
  let shots = screenshots;
4222
4630
  const pending = pendingShotRef.current;
@@ -4235,8 +4643,8 @@ function ReportDialog({
4235
4643
  if (showSeverity && reportType === "BUG" && severity) {
4236
4644
  metadata.severity = severity;
4237
4645
  }
4238
- if (duplicateIssueNumber) {
4239
- metadata.duplicateIssueNumber = String(duplicateIssueNumber);
4646
+ if (duplicateOf) {
4647
+ metadata.duplicateIssueNumber = String(duplicateOf);
4240
4648
  }
4241
4649
  if (aiConversationId) {
4242
4650
  metadata.aiConversationId = aiConversationId;
@@ -4249,20 +4657,26 @@ function ReportDialog({
4249
4657
  Object.keys(metadata).length > 0 ? metadata : void 0
4250
4658
  );
4251
4659
  if (result) {
4660
+ setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
4252
4661
  setSubmitted(true);
4253
4662
  setDescription("");
4254
4663
  setScreenshots([]);
4255
4664
  setAttachments([]);
4256
4665
  setAiConversationId(null);
4257
4666
  setDuplicateIssueNumber(null);
4667
+ setSimilarIssues(null);
4668
+ setPickedSimilar(null);
4258
4669
  setTimeout(() => {
4259
4670
  setSubmitted(false);
4260
4671
  handleClose();
4261
4672
  }, 2e3);
4262
4673
  }
4263
4674
  setIsSubmitting(false);
4675
+ setAttachingTo(null);
4264
4676
  } catch (e) {
4265
4677
  setIsSubmitting(false);
4678
+ setCheckingSimilar(false);
4679
+ setAttachingTo(null);
4266
4680
  }
4267
4681
  };
4268
4682
  if (!mounted) return null;
@@ -4270,6 +4684,7 @@ function ReportDialog({
4270
4684
  isOpen && createPortal3(
4271
4685
  /* @__PURE__ */ jsx4("style", { children: `
4272
4686
  @keyframes gg-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
4687
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
4273
4688
  @keyframes gg-b1{0%,100%{height:3px}40%{height:13px}}
4274
4689
  @keyframes gg-b2{0%,100%{height:8px}50%{height:3px}}
4275
4690
  @keyframes gg-b3{0%,100%{height:4px}30%{height:14px}70%{height:5px}}
@@ -4554,7 +4969,7 @@ function ReportDialog({
4554
4969
  flex: "1 1 auto",
4555
4970
  minHeight: 0
4556
4971
  },
4557
- children: submitted ? /* @__PURE__ */ jsxs4(
4972
+ children: submitted ? /* @__PURE__ */ jsx4(
4558
4973
  "div",
4559
4974
  {
4560
4975
  style: {
@@ -4564,10 +4979,7 @@ function ReportDialog({
4564
4979
  fontSize: "14px",
4565
4980
  fontWeight: 500
4566
4981
  },
4567
- children: [
4568
- getTypeLabel(reportType),
4569
- " sent. Thank you!"
4570
- ]
4982
+ children: addedToIssue ? `Added to #${addedToIssue}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
4571
4983
  }
4572
4984
  ) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
4573
4985
  step === 1 && headerSlot ? /* @__PURE__ */ jsx4("div", { style: { marginBottom: "14px" }, children: headerSlot }) : null,
@@ -5792,11 +6204,194 @@ function ReportDialog({
5792
6204
  children: validationError
5793
6205
  }
5794
6206
  ),
5795
- /* @__PURE__ */ jsx4(
6207
+ similarIssues ? (
6208
+ // Same visual language as the assistant's "Our team is
6209
+ // already on this" card, so both routes to a duplicate
6210
+ // read as one feature.
6211
+ /* @__PURE__ */ jsxs4(
6212
+ "div",
6213
+ {
6214
+ "data-gg-similar": "",
6215
+ style: {
6216
+ marginTop: "12px",
6217
+ display: "flex",
6218
+ flexDirection: "column",
6219
+ gap: "10px",
6220
+ padding: "12px",
6221
+ borderRadius: "10px",
6222
+ border: "1px solid rgba(245,158,11,0.4)",
6223
+ backgroundColor: "rgba(245,158,11,0.08)"
6224
+ },
6225
+ children: [
6226
+ /* @__PURE__ */ jsxs4("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6227
+ /* @__PURE__ */ jsx4("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this" }),
6228
+ /* @__PURE__ */ jsx4("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." })
6229
+ ] }),
6230
+ /* @__PURE__ */ jsx4(
6231
+ "div",
6232
+ {
6233
+ role: "radiogroup",
6234
+ "aria-label": "Existing issues",
6235
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
6236
+ children: similarIssues.map((issue) => {
6237
+ const picked = pickedSimilar === issue.number;
6238
+ return /* @__PURE__ */ jsxs4(
6239
+ "button",
6240
+ {
6241
+ type: "button",
6242
+ role: "radio",
6243
+ "aria-checked": picked,
6244
+ disabled: isSubmitting,
6245
+ onClick: () => setPickedSimilar(issue.number),
6246
+ style: {
6247
+ display: "flex",
6248
+ alignItems: "flex-start",
6249
+ gap: "10px",
6250
+ width: "100%",
6251
+ minHeight: "44px",
6252
+ padding: "9px 10px",
6253
+ borderRadius: "8px",
6254
+ border: `1px solid ${picked ? t2.accent : t2.border}`,
6255
+ boxShadow: picked ? `0 0 0 1px ${t2.accent}` : "none",
6256
+ backgroundColor: t2.bg,
6257
+ color: t2.text,
6258
+ textAlign: "left",
6259
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6260
+ fontFamily: "inherit"
6261
+ },
6262
+ children: [
6263
+ /* @__PURE__ */ jsx4(
6264
+ "span",
6265
+ {
6266
+ "aria-hidden": "true",
6267
+ style: {
6268
+ flexShrink: 0,
6269
+ marginTop: "2px",
6270
+ width: "14px",
6271
+ height: "14px",
6272
+ boxSizing: "border-box",
6273
+ borderRadius: "50%",
6274
+ border: `2px solid ${picked ? t2.accent : t2.textMuted}`,
6275
+ backgroundColor: picked ? t2.accent : "transparent",
6276
+ boxShadow: picked ? `inset 0 0 0 2px ${t2.bg}` : "none"
6277
+ }
6278
+ }
6279
+ ),
6280
+ /* @__PURE__ */ jsxs4(
6281
+ "span",
6282
+ {
6283
+ style: {
6284
+ display: "flex",
6285
+ flexDirection: "column",
6286
+ gap: "3px",
6287
+ minWidth: 0
6288
+ },
6289
+ children: [
6290
+ /* @__PURE__ */ jsxs4(
6291
+ "span",
6292
+ {
6293
+ title: issue.title,
6294
+ style: {
6295
+ fontSize: "12.5px",
6296
+ lineHeight: 1.45,
6297
+ wordBreak: "break-word",
6298
+ overflow: "hidden",
6299
+ display: "-webkit-box",
6300
+ WebkitLineClamp: 3,
6301
+ WebkitBoxOrient: "vertical"
6302
+ },
6303
+ children: [
6304
+ /* @__PURE__ */ jsxs4("span", { style: { color: t2.textMuted, fontWeight: 600 }, children: [
6305
+ "#",
6306
+ issue.number
6307
+ ] }),
6308
+ " ",
6309
+ issue.title
6310
+ ]
6311
+ }
6312
+ ),
6313
+ issue.status && /* @__PURE__ */ jsx4("span", { style: { fontSize: "11.5px", color: t2.textMuted }, children: issue.status })
6314
+ ]
6315
+ }
6316
+ )
6317
+ ]
6318
+ },
6319
+ issue.number
6320
+ );
6321
+ })
6322
+ }
6323
+ ),
6324
+ /* @__PURE__ */ jsx4(
6325
+ "button",
6326
+ {
6327
+ type: "button",
6328
+ disabled: isSubmitting || !pickedSimilar,
6329
+ onClick: () => {
6330
+ if (!pickedSimilar) return;
6331
+ setAttachingTo(pickedSimilar);
6332
+ setDuplicateIssueNumber(pickedSimilar);
6333
+ void handleSubmit({ duplicateOf: pickedSimilar });
6334
+ },
6335
+ style: {
6336
+ width: "100%",
6337
+ minHeight: "40px",
6338
+ padding: "10px",
6339
+ borderRadius: "8px",
6340
+ border: "none",
6341
+ backgroundColor: isSubmitting || !pickedSimilar ? t2.bgSecondary : t2.accent,
6342
+ color: isSubmitting || !pickedSimilar ? t2.textMuted : t2.accentText,
6343
+ fontSize: "14px",
6344
+ fontWeight: 600,
6345
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6346
+ fontFamily: "inherit"
6347
+ },
6348
+ children: typeof attachingTo === "number" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
6349
+ /* @__PURE__ */ jsx4(ButtonSpinner, {}),
6350
+ "Adding to #",
6351
+ attachingTo,
6352
+ "\u2026"
6353
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6354
+ }
6355
+ ),
6356
+ /* @__PURE__ */ jsx4(
6357
+ "button",
6358
+ {
6359
+ type: "button",
6360
+ disabled: isSubmitting,
6361
+ onClick: () => {
6362
+ setAttachingTo("new");
6363
+ void handleSubmit({ skipSimilar: true });
6364
+ },
6365
+ style: {
6366
+ width: "100%",
6367
+ minHeight: "40px",
6368
+ marginTop: "-4px",
6369
+ padding: "8px",
6370
+ borderRadius: "8px",
6371
+ border: "none",
6372
+ backgroundColor: "transparent",
6373
+ color: isSubmitting ? t2.textMuted : t2.text,
6374
+ fontSize: "13px",
6375
+ fontWeight: 500,
6376
+ textDecoration: "underline",
6377
+ textUnderlineOffset: "3px",
6378
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6379
+ fontFamily: "inherit"
6380
+ },
6381
+ children: attachingTo === "new" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
6382
+ /* @__PURE__ */ jsx4(ButtonSpinner, {}),
6383
+ "Sending as new\u2026"
6384
+ ] }) : "No, mine is different \u2014 send as new"
6385
+ }
6386
+ )
6387
+ ]
6388
+ }
6389
+ )
6390
+ ) : /* @__PURE__ */ jsx4(
5796
6391
  "button",
5797
6392
  {
5798
6393
  type: "button",
5799
- onClick: handleSubmit,
6394
+ onClick: () => void handleSubmit(),
5800
6395
  disabled: submitDisabled,
5801
6396
  style: {
5802
6397
  marginTop: "12px",
@@ -5812,7 +6407,10 @@ function ReportDialog({
5812
6407
  fontFamily: "inherit",
5813
6408
  transition: "background-color 0.15s ease"
5814
6409
  },
5815
- children: isSubmitting ? shotPending && !isRating ? "Attaching screenshot\u2026" : "Sending..." : isRating ? "Send Rating" : "Send Report"
6410
+ children: checkingSimilar ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
6411
+ /* @__PURE__ */ jsx4(ButtonSpinner, {}),
6412
+ "Checking for existing reports\u2026"
6413
+ ] }) : isSubmitting ? shotPending && !isRating ? "Attaching screenshot\u2026" : "Sending..." : isRating ? "Send Rating" : "Send Report"
5816
6414
  }
5817
6415
  )
5818
6416
  ] })
@@ -6086,8 +6684,10 @@ function ReportDialog({
6086
6684
  theme: layerTheme,
6087
6685
  screenshots,
6088
6686
  pageShot,
6089
- onAddImages: addFiles,
6687
+ onAddFiles: addFiles,
6090
6688
  onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
6689
+ files: attachments,
6690
+ onRemoveFile: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
6091
6691
  attachmentCount: screenshots.length + attachments.length,
6092
6692
  context: { ...assistContext != null ? assistContext : {}, reportType },
6093
6693
  reportTypeLabel: getTypeLabel(reportType),
@@ -6124,14 +6724,14 @@ function ReportDialog({
6124
6724
  severityRefused: needsSeverity,
6125
6725
  isSubmitting,
6126
6726
  submitted,
6127
- onSend: () => void handleSubmit(),
6727
+ onSend: () => void handleSubmit({ skipSimilar: true }),
6128
6728
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
6129
6729
  setSelfReportPrefill(prefill != null ? prefill : "");
6130
6730
  setSelfReportFrom("assist");
6131
6731
  } : void 0,
6132
- onDegrade: (message) => {
6732
+ onDegrade: (message, retryable) => {
6133
6733
  setAssistOpen(false);
6134
- setAssistUsed(true);
6734
+ if (!retryable) setAssistUsed(true);
6135
6735
  setAssistNotice(message);
6136
6736
  },
6137
6737
  onClose: () => {
@@ -6277,7 +6877,7 @@ function getTypeIcon(type, color, size = 24) {
6277
6877
  }
6278
6878
 
6279
6879
  // src/booking-dialog.tsx
6280
- import { useCallback, useEffect as useEffect5, useMemo as useMemo2, useState as useState5 } from "react";
6880
+ import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo3, useState as useState5 } from "react";
6281
6881
  import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
6282
6882
  var t = {
6283
6883
  bg: "#141414",
@@ -6332,11 +6932,11 @@ function BookingDialog({
6332
6932
  const [meetUrl, setMeetUrl] = useState5(null);
6333
6933
  const [busy, setBusy] = useState5(false);
6334
6934
  const [error, setError] = useState5(null);
6335
- const headers = useMemo2(
6935
+ const headers = useMemo3(
6336
6936
  () => ({ Authorization: `Bearer ${token}`, "Content-Type": "application/json" }),
6337
6937
  [token]
6338
6938
  );
6339
- const loadSlots = useCallback(async () => {
6939
+ const loadSlots = useCallback2(async () => {
6340
6940
  var _a2, _b, _c;
6341
6941
  setError(null);
6342
6942
  setSlots(null);
@@ -6843,7 +7443,7 @@ function GlitchgrabProviderInner({
6843
7443
  sessionProp.avatarUrl,
6844
7444
  sessionProp.image
6845
7445
  ].map((v) => v == null ? "" : String(v)).join("\0") : "";
6846
- const session = useMemo3(() => sessionProp, [sessionKey]);
7446
+ const session = useMemo4(() => sessionProp, [sessionKey]);
6847
7447
  setRelease(release2);
6848
7448
  if (appContext2) setContexts(appContext2);
6849
7449
  registerCaptureConfig({
@@ -7024,7 +7624,7 @@ function GlitchgrabProviderInner({
7024
7624
  } catch (e) {
7025
7625
  }
7026
7626
  }, [token, baseUrl, onError, onReportSent, session, ignoreErrors]);
7027
- const report = useCallback2(
7627
+ const report = useCallback3(
7028
7628
  async (type, description, metadata) => {
7029
7629
  var _a;
7030
7630
  try {
@@ -7058,11 +7658,11 @@ function GlitchgrabProviderInner({
7058
7658
  },
7059
7659
  [token, baseUrl, onReportSent, session]
7060
7660
  );
7061
- const reportBug = useCallback2(
7661
+ const reportBug = useCallback3(
7062
7662
  (description, metadata) => report("BUG", description, metadata),
7063
7663
  [report]
7064
7664
  );
7065
- const reportGlitchgrabProblem = useCallback2(
7665
+ const reportGlitchgrabProblem = useCallback3(
7066
7666
  async (params) => {
7067
7667
  var _a, _b;
7068
7668
  try {
@@ -7096,7 +7696,7 @@ function GlitchgrabProviderInner({
7096
7696
  },
7097
7697
  [token, baseUrl, session]
7098
7698
  );
7099
- const feedback = useCallback2(
7699
+ const feedback = useCallback3(
7100
7700
  async (rating, message, metadata) => {
7101
7701
  try {
7102
7702
  const context = captureContext(visitedPagesRef.current);
@@ -7124,32 +7724,38 @@ function GlitchgrabProviderInner({
7124
7724
  },
7125
7725
  [token, baseUrl, session]
7126
7726
  );
7127
- const openFeedbackDialog = useCallback2(() => {
7727
+ const openFeedbackDialog = useCallback3(() => {
7128
7728
  if (typeof window !== "undefined") {
7129
7729
  window.dispatchEvent(
7130
7730
  new CustomEvent("glitchgrab:open-report", { detail: { type: "RATING" } })
7131
7731
  );
7132
7732
  }
7133
7733
  }, []);
7134
- const captureError2 = useCallback2(
7734
+ const captureError2 = useCallback3(
7135
7735
  (error, options) => {
7136
7736
  captureError(error, options);
7137
7737
  },
7138
7738
  []
7139
7739
  );
7140
- const setContext2 = useCallback2((key, value) => {
7740
+ const setContext2 = useCallback3((key, value) => {
7141
7741
  setContext(key, value);
7142
7742
  }, []);
7143
- const setContexts2 = useCallback2((values) => {
7743
+ const setContexts2 = useCallback3((values) => {
7144
7744
  setContexts(values);
7145
7745
  }, []);
7146
- const addBreadcrumb2 = useCallback2(
7746
+ const addBreadcrumb2 = useCallback3(
7147
7747
  (message, data) => {
7148
7748
  addBreadcrumb("custom", message, data);
7149
7749
  },
7150
7750
  []
7151
7751
  );
7152
- const enhance = useCallback2(
7752
+ const signedUserId = session == null ? void 0 : session.userId;
7753
+ const signature = typeof (session == null ? void 0 : session.signature) === "string" ? session.signature : null;
7754
+ const identity = useMemo4(
7755
+ () => ({ userId: signedUserId != null ? signedUserId : null, signature }),
7756
+ [signedUserId, signature]
7757
+ );
7758
+ const enhance = useCallback3(
7153
7759
  async (text, screenshot) => {
7154
7760
  try {
7155
7761
  const ctx = captureContext(visitedPagesRef.current);
@@ -7158,14 +7764,14 @@ function GlitchgrabProviderInner({
7158
7764
  visitedPages: ctx.visitedPages.slice(-5),
7159
7765
  breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
7160
7766
  };
7161
- return await enhanceText(text, token, baseUrl, screenshot, context);
7767
+ return await enhanceText(text, token, baseUrl, screenshot, context, identity);
7162
7768
  } catch (e) {
7163
7769
  return text;
7164
7770
  }
7165
7771
  },
7166
- [token, baseUrl]
7772
+ [token, baseUrl, identity]
7167
7773
  );
7168
- const assist = useCallback2(
7774
+ const assist = useCallback3(
7169
7775
  async (params) => {
7170
7776
  var _a;
7171
7777
  const ctx = captureContext(visitedPagesRef.current);
@@ -7176,7 +7782,8 @@ function GlitchgrabProviderInner({
7176
7782
  reporter: session ? {
7177
7783
  key: session.userId,
7178
7784
  name: session.name ? String(session.name) : null,
7179
- email: session.email ? String(session.email) : null
7785
+ email: session.email ? String(session.email) : null,
7786
+ ...signature ? { signature } : {}
7180
7787
  } : void 0,
7181
7788
  context: {
7182
7789
  ...(_a = params.context) != null ? _a : {},
@@ -7191,17 +7798,21 @@ function GlitchgrabProviderInner({
7191
7798
  },
7192
7799
  [token, baseUrl, session]
7193
7800
  );
7194
- const transcribe = useCallback2(
7801
+ const findSimilar = useCallback3(
7802
+ (description) => findSimilarIssues(description, token, baseUrl, identity),
7803
+ [token, baseUrl, identity]
7804
+ );
7805
+ const transcribe = useCallback3(
7195
7806
  async (blob) => {
7196
7807
  try {
7197
- return await transcribeAudio(blob, token, baseUrl);
7808
+ return await transcribeAudio(blob, token, baseUrl, identity);
7198
7809
  } catch (e) {
7199
7810
  return "";
7200
7811
  }
7201
7812
  },
7202
- [token, baseUrl]
7813
+ [token, baseUrl, identity]
7203
7814
  );
7204
- const openReportDialog = useCallback2((options) => {
7815
+ const openReportDialog = useCallback3((options) => {
7205
7816
  if (typeof window !== "undefined") {
7206
7817
  window.dispatchEvent(new CustomEvent("glitchgrab:open-report", { detail: options }));
7207
7818
  }
@@ -7217,8 +7828,10 @@ function GlitchgrabProviderInner({
7217
7828
  const [projectName, setProjectName] = useState6(null);
7218
7829
  const [aiAssist, setAiAssist] = useState6(false);
7219
7830
  const [glitchgrabReport, setGlitchgrabReport] = useState6(false);
7831
+ const [signedUsers, setSignedUsers] = useState6("OFF");
7832
+ const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
7220
7833
  const [bookingOpen, setBookingOpen] = useState6(false);
7221
- const openBookingDialog = useCallback2(() => setBookingOpen(true), []);
7834
+ const openBookingDialog = useCallback3(() => setBookingOpen(true), []);
7222
7835
  useEffect6(() => {
7223
7836
  let cancelled = false;
7224
7837
  fetch(`${baseUrl}/api/v1/sdk/project`, {
@@ -7229,12 +7842,25 @@ function GlitchgrabProviderInner({
7229
7842
  setProjectName(json.data.name);
7230
7843
  setAiAssist(json.data.aiAssist === true);
7231
7844
  setGlitchgrabReport(json.data.glitchgrabReport === true);
7845
+ const mode = json.data.signedUsers;
7846
+ setSignedUsers(mode === "WARN" || mode === "ENFORCE" ? mode : "OFF");
7232
7847
  }).catch(() => {
7233
7848
  });
7234
7849
  return () => {
7235
7850
  cancelled = true;
7236
7851
  };
7237
7852
  }, [baseUrl, token]);
7853
+ useEffect6(() => {
7854
+ var _a;
7855
+ try {
7856
+ if (signedUsers === "OFF" || signature || !session) return;
7857
+ if (typeof process === "undefined" || ((_a = process.env) == null ? void 0 : _a.NODE_ENV) !== "development") return;
7858
+ console.warn(
7859
+ 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.'
7860
+ );
7861
+ } catch (e) {
7862
+ }
7863
+ }, [signedUsers, signature, session]);
7238
7864
  useEffect6(() => {
7239
7865
  try {
7240
7866
  if (typeof window === "undefined") return;
@@ -7249,7 +7875,7 @@ function GlitchgrabProviderInner({
7249
7875
  } catch (e) {
7250
7876
  }
7251
7877
  }, [openReportDialog]);
7252
- const reporter = useMemo3(() => {
7878
+ const reporter = useMemo4(() => {
7253
7879
  var _a;
7254
7880
  if (!session) return null;
7255
7881
  const avatar = (_a = session.avatarUrl) != null ? _a : session.image;
@@ -7309,10 +7935,11 @@ function GlitchgrabProviderInner({
7309
7935
  {
7310
7936
  report,
7311
7937
  sendFeedback: feedback,
7312
- enhanceText: enhance,
7313
- assist: aiAssist ? assist : void 0,
7938
+ enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
7939
+ assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
7940
+ findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
7314
7941
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
7315
- transcribeAudio: transcribe,
7942
+ transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
7316
7943
  types,
7317
7944
  showSeverity,
7318
7945
  reporter,
@@ -7547,7 +8174,7 @@ function FeedbackButton({
7547
8174
  }
7548
8175
 
7549
8176
  // src/use-reports.ts
7550
- import { useState as useState8, useEffect as useEffect7, useCallback as useCallback3 } from "react";
8177
+ import { useState as useState8, useEffect as useEffect7, useCallback as useCallback4 } from "react";
7551
8178
  var BASE_URL = "https://glitchgrab.dev";
7552
8179
  function useGlitchgrabReports({
7553
8180
  token,
@@ -7558,7 +8185,7 @@ function useGlitchgrabReports({
7558
8185
  const [isLoading, setIsLoading] = useState8(true);
7559
8186
  const [isFetching, setIsFetching] = useState8(false);
7560
8187
  const [error, setError] = useState8(null);
7561
- const fetchReports = useCallback3(async () => {
8188
+ const fetchReports = useCallback4(async () => {
7562
8189
  var _a, _b;
7563
8190
  if (!token || !userId) {
7564
8191
  setIsLoading(false);
@@ -7623,7 +8250,7 @@ function useGlitchgrabActions({
7623
8250
  }) {
7624
8251
  const [isPending, setIsPending] = useState8(false);
7625
8252
  const [error, setError] = useState8(null);
7626
- const run = useCallback3(
8253
+ const run = useCallback4(
7627
8254
  async (reportId, body) => {
7628
8255
  setIsPending(true);
7629
8256
  setError(null);
@@ -7653,7 +8280,7 @@ function useGlitchgrabActions({
7653
8280
  }
7654
8281
 
7655
8282
  // src/use-feedback.ts
7656
- import { useState as useState9, useEffect as useEffect8, useCallback as useCallback4 } from "react";
8283
+ import { useState as useState9, useEffect as useEffect8, useCallback as useCallback5 } from "react";
7657
8284
  var DEFAULT_BASE_URL3 = "https://glitchgrab.dev";
7658
8285
  function buildUrl({
7659
8286
  approvedOnly,
@@ -7683,7 +8310,7 @@ function useGlitchgrabFeedback(query) {
7683
8310
  const [isLoading, setIsLoading] = useState9(true);
7684
8311
  const [isFetching, setIsFetching] = useState9(false);
7685
8312
  const [error, setError] = useState9(null);
7686
- const load = useCallback4(async () => {
8313
+ const load = useCallback5(async () => {
7687
8314
  if (!token) {
7688
8315
  setIsLoading(false);
7689
8316
  return;