glitchgrab 1.43.0 → 1.44.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 = {
@@ -762,13 +787,18 @@ async function assistTurn(params, token, baseUrl) {
762
787
  return offline;
763
788
  }
764
789
  }
765
- async function transcribeAudio(blob, token, baseUrl) {
790
+ async function transcribeAudio(blob, token, baseUrl, identity) {
766
791
  var _a;
767
792
  try {
768
793
  if (blob.size === 0) return "";
769
794
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/sdk/stt`;
770
795
  const form = new FormData();
771
796
  form.append("file", blob, "audio.webm");
797
+ const signed = signedFields(identity);
798
+ if (signed.userId && signed.signature) {
799
+ form.append("userId", signed.userId);
800
+ form.append("signature", signed.signature);
801
+ }
772
802
  const response = await fetch(url, {
773
803
  method: "POST",
774
804
  headers: { Authorization: `Bearer ${token}` },
@@ -1668,7 +1698,7 @@ function AssistSheet({
1668
1698
  minWidth: 0
1669
1699
  },
1670
1700
  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}` }),
1701
+ /* @__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
1702
  title && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1673
1703
  "#",
1674
1704
  duplicate.number,
@@ -3292,6 +3322,27 @@ function captureDefaultScreenshot(options) {
3292
3322
  }
3293
3323
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
3294
3324
  var SHOT_WAIT_MS = 3e3;
3325
+ var SIMILAR_WAIT_MS = 2500;
3326
+ var MAX_SIMILAR_SHOWN = 3;
3327
+ function ButtonSpinner() {
3328
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3329
+ "span",
3330
+ {
3331
+ "aria-hidden": "true",
3332
+ style: {
3333
+ display: "inline-block",
3334
+ width: "11px",
3335
+ height: "11px",
3336
+ marginRight: "6px",
3337
+ verticalAlign: "-1px",
3338
+ border: "2px solid currentColor",
3339
+ borderRightColor: "transparent",
3340
+ borderRadius: "50%",
3341
+ animation: "gg-spin .7s linear infinite"
3342
+ }
3343
+ }
3344
+ );
3345
+ }
3295
3346
  function detectDark() {
3296
3347
  if (typeof window === "undefined") return true;
3297
3348
  try {
@@ -3598,6 +3649,7 @@ function ReportDialog({
3598
3649
  sendFeedback: sendFeedback2,
3599
3650
  enhanceText: enhanceText2,
3600
3651
  assist,
3652
+ findSimilarIssues: findSimilarIssues2,
3601
3653
  assistContext = null,
3602
3654
  transcribeAudio: transcribeAudio2,
3603
3655
  types,
@@ -3617,6 +3669,11 @@ function ReportDialog({
3617
3669
  const [selfReportPrefill, setSelfReportPrefill] = (0, import_react.useState)("");
3618
3670
  const [typeChosen, setTypeChosen] = (0, import_react.useState)(false);
3619
3671
  const [duplicateIssueNumber, setDuplicateIssueNumber] = (0, import_react.useState)(null);
3672
+ const [similarIssues, setSimilarIssues] = (0, import_react.useState)(null);
3673
+ const [checkingSimilar, setCheckingSimilar] = (0, import_react.useState)(false);
3674
+ const [attachingTo, setAttachingTo] = (0, import_react.useState)(null);
3675
+ const [pickedSimilar, setPickedSimilar] = (0, import_react.useState)(null);
3676
+ const [addedToIssue, setAddedToIssue] = (0, import_react.useState)(null);
3620
3677
  const [aiConversationId, setAiConversationId] = (0, import_react.useState)(null);
3621
3678
  const [isEnhanced, setIsEnhanced] = (0, import_react.useState)(false);
3622
3679
  const [originalDescription, setOriginalDescription] = (0, import_react.useState)(
@@ -4047,6 +4104,10 @@ function ReportDialog({
4047
4104
  setReportType("BUG");
4048
4105
  setTypeChosen(false);
4049
4106
  setDuplicateIssueNumber(null);
4107
+ setSimilarIssues(null);
4108
+ setPickedSimilar(null);
4109
+ setCheckingSimilar(false);
4110
+ setAttachingTo(null);
4050
4111
  setSeverity(null);
4051
4112
  setValidationError(null);
4052
4113
  setSelfReportFrom(null);
@@ -4234,7 +4295,8 @@ function ReportDialog({
4234
4295
  stopListeningAndTranscribe();
4235
4296
  }
4236
4297
  };
4237
- const handleSubmit = async () => {
4298
+ const handleSubmit = async (opts) => {
4299
+ var _a2;
4238
4300
  try {
4239
4301
  if (isSubmitting) return;
4240
4302
  if (isRating) {
@@ -4271,6 +4333,22 @@ function ReportDialog({
4271
4333
  setStep(2);
4272
4334
  return;
4273
4335
  }
4336
+ const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
4337
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
4338
+ setIsSubmitting(true);
4339
+ setCheckingSimilar(true);
4340
+ const matches = await Promise.race([
4341
+ findSimilarIssues2(description.trim()).catch(() => null),
4342
+ new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
4343
+ ]);
4344
+ setCheckingSimilar(false);
4345
+ if (matches && matches.length > 0) {
4346
+ setSimilarIssues(matches.slice(0, MAX_SIMILAR_SHOWN));
4347
+ setPickedSimilar(null);
4348
+ setIsSubmitting(false);
4349
+ return;
4350
+ }
4351
+ }
4274
4352
  setIsSubmitting(true);
4275
4353
  let shots = screenshots;
4276
4354
  const pending = pendingShotRef.current;
@@ -4289,8 +4367,8 @@ function ReportDialog({
4289
4367
  if (showSeverity && reportType === "BUG" && severity) {
4290
4368
  metadata.severity = severity;
4291
4369
  }
4292
- if (duplicateIssueNumber) {
4293
- metadata.duplicateIssueNumber = String(duplicateIssueNumber);
4370
+ if (duplicateOf) {
4371
+ metadata.duplicateIssueNumber = String(duplicateOf);
4294
4372
  }
4295
4373
  if (aiConversationId) {
4296
4374
  metadata.aiConversationId = aiConversationId;
@@ -4303,20 +4381,26 @@ function ReportDialog({
4303
4381
  Object.keys(metadata).length > 0 ? metadata : void 0
4304
4382
  );
4305
4383
  if (result) {
4384
+ setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
4306
4385
  setSubmitted(true);
4307
4386
  setDescription("");
4308
4387
  setScreenshots([]);
4309
4388
  setAttachments([]);
4310
4389
  setAiConversationId(null);
4311
4390
  setDuplicateIssueNumber(null);
4391
+ setSimilarIssues(null);
4392
+ setPickedSimilar(null);
4312
4393
  setTimeout(() => {
4313
4394
  setSubmitted(false);
4314
4395
  handleClose();
4315
4396
  }, 2e3);
4316
4397
  }
4317
4398
  setIsSubmitting(false);
4399
+ setAttachingTo(null);
4318
4400
  } catch (e) {
4319
4401
  setIsSubmitting(false);
4402
+ setCheckingSimilar(false);
4403
+ setAttachingTo(null);
4320
4404
  }
4321
4405
  };
4322
4406
  if (!mounted) return null;
@@ -4324,6 +4408,7 @@ function ReportDialog({
4324
4408
  isOpen && (0, import_react_dom.createPortal)(
4325
4409
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
4326
4410
  @keyframes gg-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
4411
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
4327
4412
  @keyframes gg-b1{0%,100%{height:3px}40%{height:13px}}
4328
4413
  @keyframes gg-b2{0%,100%{height:8px}50%{height:3px}}
4329
4414
  @keyframes gg-b3{0%,100%{height:4px}30%{height:14px}70%{height:5px}}
@@ -4608,7 +4693,7 @@ function ReportDialog({
4608
4693
  flex: "1 1 auto",
4609
4694
  minHeight: 0
4610
4695
  },
4611
- children: submitted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
4696
+ children: submitted ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
4612
4697
  "div",
4613
4698
  {
4614
4699
  style: {
@@ -4618,10 +4703,7 @@ function ReportDialog({
4618
4703
  fontSize: "14px",
4619
4704
  fontWeight: 500
4620
4705
  },
4621
- children: [
4622
- getTypeLabel(reportType),
4623
- " sent. Thank you!"
4624
- ]
4706
+ children: addedToIssue ? `Added to #${addedToIssue}. Thank you!` : `${getTypeLabel(reportType)} sent. Thank you!`
4625
4707
  }
4626
4708
  ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
4627
4709
  step === 1 && headerSlot ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginBottom: "14px" }, children: headerSlot }) : null,
@@ -5846,11 +5928,194 @@ function ReportDialog({
5846
5928
  children: validationError
5847
5929
  }
5848
5930
  ),
5849
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5931
+ similarIssues ? (
5932
+ // Same visual language as the assistant's "Our team is
5933
+ // already on this" card, so both routes to a duplicate
5934
+ // read as one feature.
5935
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
5936
+ "div",
5937
+ {
5938
+ "data-gg-similar": "",
5939
+ style: {
5940
+ marginTop: "12px",
5941
+ display: "flex",
5942
+ flexDirection: "column",
5943
+ gap: "10px",
5944
+ padding: "12px",
5945
+ borderRadius: "10px",
5946
+ border: "1px solid rgba(245,158,11,0.4)",
5947
+ backgroundColor: "rgba(245,158,11,0.08)"
5948
+ },
5949
+ children: [
5950
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
5951
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: t2.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this" }),
5952
+ /* @__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." })
5953
+ ] }),
5954
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5955
+ "div",
5956
+ {
5957
+ role: "radiogroup",
5958
+ "aria-label": "Existing issues",
5959
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
5960
+ children: similarIssues.map((issue) => {
5961
+ const picked = pickedSimilar === issue.number;
5962
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
5963
+ "button",
5964
+ {
5965
+ type: "button",
5966
+ role: "radio",
5967
+ "aria-checked": picked,
5968
+ disabled: isSubmitting,
5969
+ onClick: () => setPickedSimilar(issue.number),
5970
+ style: {
5971
+ display: "flex",
5972
+ alignItems: "flex-start",
5973
+ gap: "10px",
5974
+ width: "100%",
5975
+ minHeight: "44px",
5976
+ padding: "9px 10px",
5977
+ borderRadius: "8px",
5978
+ border: `1px solid ${picked ? t2.accent : t2.border}`,
5979
+ boxShadow: picked ? `0 0 0 1px ${t2.accent}` : "none",
5980
+ backgroundColor: t2.bg,
5981
+ color: t2.text,
5982
+ textAlign: "left",
5983
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5984
+ fontFamily: "inherit"
5985
+ },
5986
+ children: [
5987
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5988
+ "span",
5989
+ {
5990
+ "aria-hidden": "true",
5991
+ style: {
5992
+ flexShrink: 0,
5993
+ marginTop: "2px",
5994
+ width: "14px",
5995
+ height: "14px",
5996
+ boxSizing: "border-box",
5997
+ borderRadius: "50%",
5998
+ border: `2px solid ${picked ? t2.accent : t2.textMuted}`,
5999
+ backgroundColor: picked ? t2.accent : "transparent",
6000
+ boxShadow: picked ? `inset 0 0 0 2px ${t2.bg}` : "none"
6001
+ }
6002
+ }
6003
+ ),
6004
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6005
+ "span",
6006
+ {
6007
+ style: {
6008
+ display: "flex",
6009
+ flexDirection: "column",
6010
+ gap: "3px",
6011
+ minWidth: 0
6012
+ },
6013
+ children: [
6014
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
6015
+ "span",
6016
+ {
6017
+ title: issue.title,
6018
+ style: {
6019
+ fontSize: "12.5px",
6020
+ lineHeight: 1.45,
6021
+ wordBreak: "break-word",
6022
+ overflow: "hidden",
6023
+ display: "-webkit-box",
6024
+ WebkitLineClamp: 3,
6025
+ WebkitBoxOrient: "vertical"
6026
+ },
6027
+ children: [
6028
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { color: t2.textMuted, fontWeight: 600 }, children: [
6029
+ "#",
6030
+ issue.number
6031
+ ] }),
6032
+ " ",
6033
+ issue.title
6034
+ ]
6035
+ }
6036
+ ),
6037
+ issue.status && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: "11.5px", color: t2.textMuted }, children: issue.status })
6038
+ ]
6039
+ }
6040
+ )
6041
+ ]
6042
+ },
6043
+ issue.number
6044
+ );
6045
+ })
6046
+ }
6047
+ ),
6048
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6049
+ "button",
6050
+ {
6051
+ type: "button",
6052
+ disabled: isSubmitting || !pickedSimilar,
6053
+ onClick: () => {
6054
+ if (!pickedSimilar) return;
6055
+ setAttachingTo(pickedSimilar);
6056
+ setDuplicateIssueNumber(pickedSimilar);
6057
+ void handleSubmit({ duplicateOf: pickedSimilar });
6058
+ },
6059
+ style: {
6060
+ width: "100%",
6061
+ minHeight: "40px",
6062
+ padding: "10px",
6063
+ borderRadius: "8px",
6064
+ border: "none",
6065
+ backgroundColor: isSubmitting || !pickedSimilar ? t2.bgSecondary : t2.accent,
6066
+ color: isSubmitting || !pickedSimilar ? t2.textMuted : t2.accentText,
6067
+ fontSize: "14px",
6068
+ fontWeight: 600,
6069
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
6070
+ fontFamily: "inherit"
6071
+ },
6072
+ children: typeof attachingTo === "number" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6073
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6074
+ "Adding to #",
6075
+ attachingTo,
6076
+ "\u2026"
6077
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
6078
+ }
6079
+ ),
6080
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
6081
+ "button",
6082
+ {
6083
+ type: "button",
6084
+ disabled: isSubmitting,
6085
+ onClick: () => {
6086
+ setAttachingTo("new");
6087
+ void handleSubmit({ skipSimilar: true });
6088
+ },
6089
+ style: {
6090
+ width: "100%",
6091
+ minHeight: "40px",
6092
+ marginTop: "-4px",
6093
+ padding: "8px",
6094
+ borderRadius: "8px",
6095
+ border: "none",
6096
+ backgroundColor: "transparent",
6097
+ color: isSubmitting ? t2.textMuted : t2.text,
6098
+ fontSize: "13px",
6099
+ fontWeight: 500,
6100
+ textDecoration: "underline",
6101
+ textUnderlineOffset: "3px",
6102
+ cursor: isSubmitting ? "not-allowed" : "pointer",
6103
+ fontFamily: "inherit"
6104
+ },
6105
+ children: attachingTo === "new" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6106
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6107
+ "Sending as new\u2026"
6108
+ ] }) : "No, mine is different \u2014 send as new"
6109
+ }
6110
+ )
6111
+ ]
6112
+ }
6113
+ )
6114
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
5850
6115
  "button",
5851
6116
  {
5852
6117
  type: "button",
5853
- onClick: handleSubmit,
6118
+ onClick: () => void handleSubmit(),
5854
6119
  disabled: submitDisabled,
5855
6120
  style: {
5856
6121
  marginTop: "12px",
@@ -5866,7 +6131,10 @@ function ReportDialog({
5866
6131
  fontFamily: "inherit",
5867
6132
  transition: "background-color 0.15s ease"
5868
6133
  },
5869
- children: isSubmitting ? shotPending && !isRating ? "Attaching screenshot\u2026" : "Sending..." : isRating ? "Send Rating" : "Send Report"
6134
+ children: checkingSimilar ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
6135
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ButtonSpinner, {}),
6136
+ "Checking for existing reports\u2026"
6137
+ ] }) : isSubmitting ? shotPending && !isRating ? "Attaching screenshot\u2026" : "Sending..." : isRating ? "Send Rating" : "Send Report"
5870
6138
  }
5871
6139
  )
5872
6140
  ] })
@@ -6178,7 +6446,7 @@ function ReportDialog({
6178
6446
  severityRefused: needsSeverity,
6179
6447
  isSubmitting,
6180
6448
  submitted,
6181
- onSend: () => void handleSubmit(),
6449
+ onSend: () => void handleSubmit({ skipSimilar: true }),
6182
6450
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
6183
6451
  setSelfReportPrefill(prefill != null ? prefill : "");
6184
6452
  setSelfReportFrom("assist");
@@ -7203,6 +7471,12 @@ function GlitchgrabProviderInner({
7203
7471
  },
7204
7472
  []
7205
7473
  );
7474
+ const signedUserId = session == null ? void 0 : session.userId;
7475
+ const signature = typeof (session == null ? void 0 : session.signature) === "string" ? session.signature : null;
7476
+ const identity = (0, import_react6.useMemo)(
7477
+ () => ({ userId: signedUserId != null ? signedUserId : null, signature }),
7478
+ [signedUserId, signature]
7479
+ );
7206
7480
  const enhance = (0, import_react6.useCallback)(
7207
7481
  async (text, screenshot) => {
7208
7482
  try {
@@ -7212,12 +7486,12 @@ function GlitchgrabProviderInner({
7212
7486
  visitedPages: ctx.visitedPages.slice(-5),
7213
7487
  breadcrumbs: ctx.breadcrumbs.slice(-10).map((b) => ({ type: b.type, message: b.message }))
7214
7488
  };
7215
- return await enhanceText(text, token, baseUrl, screenshot, context);
7489
+ return await enhanceText(text, token, baseUrl, screenshot, context, identity);
7216
7490
  } catch (e) {
7217
7491
  return text;
7218
7492
  }
7219
7493
  },
7220
- [token, baseUrl]
7494
+ [token, baseUrl, identity]
7221
7495
  );
7222
7496
  const assist = (0, import_react6.useCallback)(
7223
7497
  async (params) => {
@@ -7230,7 +7504,8 @@ function GlitchgrabProviderInner({
7230
7504
  reporter: session ? {
7231
7505
  key: session.userId,
7232
7506
  name: session.name ? String(session.name) : null,
7233
- email: session.email ? String(session.email) : null
7507
+ email: session.email ? String(session.email) : null,
7508
+ ...signature ? { signature } : {}
7234
7509
  } : void 0,
7235
7510
  context: {
7236
7511
  ...(_a = params.context) != null ? _a : {},
@@ -7245,15 +7520,19 @@ function GlitchgrabProviderInner({
7245
7520
  },
7246
7521
  [token, baseUrl, session]
7247
7522
  );
7523
+ const findSimilar = (0, import_react6.useCallback)(
7524
+ (description) => findSimilarIssues(description, token, baseUrl, identity),
7525
+ [token, baseUrl, identity]
7526
+ );
7248
7527
  const transcribe = (0, import_react6.useCallback)(
7249
7528
  async (blob) => {
7250
7529
  try {
7251
- return await transcribeAudio(blob, token, baseUrl);
7530
+ return await transcribeAudio(blob, token, baseUrl, identity);
7252
7531
  } catch (e) {
7253
7532
  return "";
7254
7533
  }
7255
7534
  },
7256
- [token, baseUrl]
7535
+ [token, baseUrl, identity]
7257
7536
  );
7258
7537
  const openReportDialog = (0, import_react6.useCallback)((options) => {
7259
7538
  if (typeof window !== "undefined") {
@@ -7271,6 +7550,8 @@ function GlitchgrabProviderInner({
7271
7550
  const [projectName, setProjectName] = (0, import_react6.useState)(null);
7272
7551
  const [aiAssist, setAiAssist] = (0, import_react6.useState)(false);
7273
7552
  const [glitchgrabReport, setGlitchgrabReport] = (0, import_react6.useState)(false);
7553
+ const [signedUsers, setSignedUsers] = (0, import_react6.useState)("OFF");
7554
+ const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
7274
7555
  const [bookingOpen, setBookingOpen] = (0, import_react6.useState)(false);
7275
7556
  const openBookingDialog = (0, import_react6.useCallback)(() => setBookingOpen(true), []);
7276
7557
  (0, import_react6.useEffect)(() => {
@@ -7283,12 +7564,25 @@ function GlitchgrabProviderInner({
7283
7564
  setProjectName(json.data.name);
7284
7565
  setAiAssist(json.data.aiAssist === true);
7285
7566
  setGlitchgrabReport(json.data.glitchgrabReport === true);
7567
+ const mode = json.data.signedUsers;
7568
+ setSignedUsers(mode === "WARN" || mode === "ENFORCE" ? mode : "OFF");
7286
7569
  }).catch(() => {
7287
7570
  });
7288
7571
  return () => {
7289
7572
  cancelled = true;
7290
7573
  };
7291
7574
  }, [baseUrl, token]);
7575
+ (0, import_react6.useEffect)(() => {
7576
+ var _a;
7577
+ try {
7578
+ if (signedUsers === "OFF" || signature || !session) return;
7579
+ if (typeof process === "undefined" || ((_a = process.env) == null ? void 0 : _a.NODE_ENV) !== "development") return;
7580
+ console.warn(
7581
+ 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.'
7582
+ );
7583
+ } catch (e) {
7584
+ }
7585
+ }, [signedUsers, signature, session]);
7292
7586
  (0, import_react6.useEffect)(() => {
7293
7587
  try {
7294
7588
  if (typeof window === "undefined") return;
@@ -7363,10 +7657,11 @@ function GlitchgrabProviderInner({
7363
7657
  {
7364
7658
  report,
7365
7659
  sendFeedback: feedback,
7366
- enhanceText: enhance,
7367
- assist: aiAssist ? assist : void 0,
7660
+ enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
7661
+ assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
7662
+ findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
7368
7663
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
7369
- transcribeAudio: transcribe,
7664
+ transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
7370
7665
  types,
7371
7666
  showSeverity,
7372
7667
  reporter,