glitchgrab 1.58.1 → 1.58.2

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
@@ -587,6 +587,9 @@ function contextMetadata(context) {
587
587
  var DEFAULT_BASE_URL = "https://glitchgrab.dev";
588
588
  var MAX_RETRIES = 3;
589
589
  var RETRY_BASE_MS = 500;
590
+ function reportAttemptTimeoutMs(bodyLength) {
591
+ return 1e4 + Math.ceil(bodyLength / 1e5) * 1e3;
592
+ }
590
593
  async function sendReport(payload, baseUrl) {
591
594
  var _a;
592
595
  try {
@@ -597,13 +600,18 @@ async function sendReport(payload, baseUrl) {
597
600
  Authorization: `Bearer ${payload.token}`
598
601
  };
599
602
  const useKeepalive = body.length < 6e4;
603
+ const timeoutMs = reportAttemptTimeoutMs(body.length);
600
604
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
605
+ const isLast = attempt === MAX_RETRIES - 1;
606
+ const controller = !isLast && typeof AbortController === "function" ? new AbortController() : null;
607
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
601
608
  try {
602
609
  const response = await fetch(url, {
603
610
  method: "POST",
604
611
  headers,
605
612
  body,
606
- ...useKeepalive ? { keepalive: true } : {}
613
+ ...useKeepalive ? { keepalive: true } : {},
614
+ ...controller ? { signal: controller.signal } : {}
607
615
  });
608
616
  if (response.ok) {
609
617
  const envelope = await response.json();
@@ -613,6 +621,8 @@ async function sendReport(payload, baseUrl) {
613
621
  return null;
614
622
  }
615
623
  } catch (e) {
624
+ } finally {
625
+ if (timer) clearTimeout(timer);
616
626
  }
617
627
  if (attempt < MAX_RETRIES - 1) {
618
628
  await new Promise(
@@ -729,7 +739,7 @@ async function enhanceText(text, token, baseUrl, screenshot, context, identity)
729
739
  return text;
730
740
  }
731
741
  }
732
- async function findSimilarIssues(description, token, baseUrl, identity) {
742
+ async function findSimilarIssues(description, token, baseUrl, identity, files) {
733
743
  var _a;
734
744
  try {
735
745
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/reports/similar`;
@@ -739,7 +749,11 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
739
749
  "Content-Type": "application/json",
740
750
  Authorization: `Bearer ${token}`
741
751
  },
742
- body: JSON.stringify({ text: description, ...signedFields(identity) })
752
+ body: JSON.stringify({
753
+ text: description,
754
+ ...(files == null ? void 0 : files.length) ? { files } : {},
755
+ ...signedFields(identity)
756
+ })
743
757
  });
744
758
  const envelope = await response.json().catch(() => null);
745
759
  if (!response.ok || !(envelope == null ? void 0 : envelope.success)) return null;
@@ -749,6 +763,25 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
749
763
  return null;
750
764
  }
751
765
  }
766
+ async function readAttachment(file, token, baseUrl, identity) {
767
+ var _a;
768
+ try {
769
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/read-file`;
770
+ const response = await fetch(url, {
771
+ method: "POST",
772
+ headers: {
773
+ "Content-Type": "application/json",
774
+ Authorization: `Bearer ${token}`
775
+ },
776
+ body: JSON.stringify({ name: file.name, data: file.dataUrl, ...signedFields(identity) })
777
+ });
778
+ const envelope = await response.json().catch(() => null);
779
+ if (!response.ok || !(envelope == null ? void 0 : envelope.success) || !((_a = envelope.data) == null ? void 0 : _a.enabled)) return null;
780
+ return typeof envelope.data.text === "string" ? envelope.data.text : null;
781
+ } catch (e) {
782
+ return null;
783
+ }
784
+ }
752
785
  async function assistTurn(params, token, baseUrl) {
753
786
  const offline = assistOffline();
754
787
  try {
@@ -1355,6 +1388,9 @@ var MAX_ASSIST_IMAGE_CHARS = 35e5;
1355
1388
  var MAX_ASSIST_FILES = 3;
1356
1389
  var MAX_ASSIST_FILE_CHARS = 3e5;
1357
1390
  var MAX_ASSIST_FILE_CHARS_EACH = 2e5;
1391
+ var MAX_SIMILAR_FILE_CHARS = 2e4;
1392
+ var MAX_SIMILAR_FILES_TOTAL_CHARS = 4e4;
1393
+ var MAX_READABLE_PDF_BYTES = 3 * 1024 * 1024;
1358
1394
  var TEXT_ATTACHMENT_EXTENSIONS = [
1359
1395
  ".html",
1360
1396
  ".htm",
@@ -1385,6 +1421,10 @@ var ATTACHMENT_ACCEPT = [
1385
1421
  ...BINARY_ATTACHMENT_EXTENSIONS
1386
1422
  ].join(",");
1387
1423
  var ASSIST_ATTACHMENT_ACCEPT = ["image/*", ...TEXT_ATTACHMENT_EXTENSIONS].join(",");
1424
+ var ASSIST_ATTACHMENT_ACCEPT_WITH_PDF = [ASSIST_ATTACHMENT_ACCEPT, ".pdf"].join(",");
1425
+ function isPdfFile(name) {
1426
+ return name.toLowerCase().endsWith(".pdf");
1427
+ }
1388
1428
  function isAssistReadableFile(name) {
1389
1429
  const lower = name.toLowerCase();
1390
1430
  return TEXT_ATTACHMENT_EXTENSIONS.some((ext) => lower.endsWith(ext));
@@ -1405,6 +1445,27 @@ function decodeTextDataUrl(dataUrl) {
1405
1445
  return null;
1406
1446
  }
1407
1447
  }
1448
+ function heldFileKey(file) {
1449
+ return `${file.name}:${file.size}`;
1450
+ }
1451
+ function heldFileText(file) {
1452
+ if (isPdfFile(file.name)) return typeof file.text === "string" && file.text.trim() ? file.text : null;
1453
+ if (!isAssistReadableFile(file.name)) return null;
1454
+ const text = decodeTextDataUrl(file.dataUrl);
1455
+ return text && text.trim() ? text : null;
1456
+ }
1457
+ function filesForSimilar(files, each, total) {
1458
+ const picked = [];
1459
+ let used = 0;
1460
+ for (let i = files.length - 1; i >= 0 && used < total; i--) {
1461
+ const text = heldFileText(files[i]);
1462
+ if (!text) continue;
1463
+ const content = text.slice(0, Math.min(each, total - used));
1464
+ picked.unshift({ name: files[i].name, content });
1465
+ used += content.length;
1466
+ }
1467
+ return picked;
1468
+ }
1408
1469
  function getTypeLabel(type) {
1409
1470
  switch (type) {
1410
1471
  case "RATING":
@@ -1848,9 +1909,8 @@ function pickAssistFiles(files) {
1848
1909
  let total = 0;
1849
1910
  for (let i = files.length - 1; i >= 0 && picked.length < MAX_ASSIST_FILES; i--) {
1850
1911
  const attached = files[i];
1851
- if (!isAssistReadableFile(attached.name)) continue;
1852
- const text = decodeTextDataUrl(attached.dataUrl);
1853
- if (!text || !text.trim()) continue;
1912
+ const text = heldFileText(attached);
1913
+ if (!text) continue;
1854
1914
  const content = text.slice(0, MAX_ASSIST_FILE_CHARS_EACH);
1855
1915
  if (total + content.length > MAX_ASSIST_FILE_CHARS) continue;
1856
1916
  picked.unshift({ index: i, file: { name: attached.name, content } });
@@ -1947,6 +2007,7 @@ function AssistSheet({
1947
2007
  pageShot = null,
1948
2008
  onAddFiles,
1949
2009
  files = NO_FILES,
2010
+ canReadPdf = false,
1950
2011
  onRemoveFile,
1951
2012
  onRemoveImage,
1952
2013
  attachmentCount,
@@ -1974,6 +2035,8 @@ function AssistSheet({
1974
2035
  isSubmitting,
1975
2036
  submitted,
1976
2037
  onSend,
2038
+ similarSlot,
2039
+ submittedText,
1977
2040
  onDegrade,
1978
2041
  onClose,
1979
2042
  onFinish,
@@ -1997,6 +2060,7 @@ function AssistSheet({
1997
2060
  const [autoFileLeft, setAutoFileLeft] = useState2(null);
1998
2061
  const scrollRef = useRef2(null);
1999
2062
  const draftRef = useRef2(null);
2063
+ const composerRef = useRef2(null);
2000
2064
  const seededRef = useRef2(false);
2001
2065
  const seedPendingRef = useRef2(false);
2002
2066
  const eventsRef = useRef2([]);
@@ -2038,6 +2102,12 @@ function AssistSheet({
2038
2102
  var _a;
2039
2103
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2040
2104
  }, [phase]);
2105
+ useEffect2(() => {
2106
+ const el = composerRef.current;
2107
+ if (!el) return;
2108
+ el.style.height = "auto";
2109
+ el.style.height = `${el.scrollHeight + 2}px`;
2110
+ }, [input]);
2041
2111
  useEffect2(() => {
2042
2112
  if (autoFileLeft === null) return;
2043
2113
  if (isSubmitting || submitted || phase !== "draft") {
@@ -2264,6 +2334,7 @@ function AssistSheet({
2264
2334
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
2265
2335
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
2266
2336
  @keyframes gg-autofile{from{transform:scaleX(0)}to{transform:scaleX(1)}}
2337
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
2267
2338
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
2268
2339
  [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible,[data-gg-assist-remove]:focus-visible,[data-gg-stop-auto-file]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
2269
2340
  ` }),
@@ -3138,7 +3209,7 @@ function AssistSheet({
3138
3209
  }
3139
3210
  )
3140
3211
  ] }),
3141
- submitted && /* @__PURE__ */ jsxs3(
3212
+ submitted && /* @__PURE__ */ jsx3(
3142
3213
  "div",
3143
3214
  {
3144
3215
  style: {
@@ -3148,10 +3219,7 @@ function AssistSheet({
3148
3219
  fontSize: "14px",
3149
3220
  fontWeight: 600
3150
3221
  },
3151
- children: [
3152
- reportTypeLabel,
3153
- " sent. Thank you!"
3154
- ]
3222
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3155
3223
  }
3156
3224
  )
3157
3225
  ]
@@ -3217,342 +3285,363 @@ function AssistSheet({
3217
3285
  // instead of remounting, and a draft is something you can
3218
3286
  // still reply to (#360), not a form that replaced the chat.
3219
3287
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
3220
- (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ jsxs3(
3221
- "div",
3222
- {
3223
- style: {
3224
- display: "flex",
3225
- gap: "8px",
3226
- alignItems: "center",
3227
- flexWrap: "wrap",
3228
- marginBottom: "8px"
3229
- },
3230
- children: [
3231
- screenshots.map((shot, i) => {
3232
- const read = readShots.has(shot);
3233
- return /* @__PURE__ */ jsxs3(
3234
- "div",
3235
- {
3236
- style: {
3237
- position: "relative",
3238
- width: "40px",
3239
- height: "40px",
3240
- flexShrink: 0
3241
- },
3242
- children: [
3243
- /* @__PURE__ */ jsx3(
3244
- "img",
3245
- {
3246
- src: shot,
3247
- alt: read ? "Attached image the assistant reads" : "Attached image",
3248
- "data-gg-assist-thumb": "",
3249
- style: {
3250
- display: "block",
3251
- width: "100%",
3252
- height: "100%",
3253
- objectFit: "cover",
3254
- borderRadius: "8px",
3255
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3256
- opacity: read ? 1 : 0.45,
3257
- boxSizing: "border-box"
3288
+ /* @__PURE__ */ jsxs3("div", { style: { display: similarSlot ? "none" : "contents" }, children: [
3289
+ (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ jsxs3(
3290
+ "div",
3291
+ {
3292
+ style: {
3293
+ display: "flex",
3294
+ gap: "8px",
3295
+ alignItems: "center",
3296
+ flexWrap: "wrap",
3297
+ marginBottom: "8px"
3298
+ },
3299
+ children: [
3300
+ screenshots.map((shot, i) => {
3301
+ const read = readShots.has(shot);
3302
+ return /* @__PURE__ */ jsxs3(
3303
+ "div",
3304
+ {
3305
+ style: {
3306
+ position: "relative",
3307
+ width: "40px",
3308
+ height: "40px",
3309
+ flexShrink: 0
3310
+ },
3311
+ children: [
3312
+ /* @__PURE__ */ jsx3(
3313
+ "img",
3314
+ {
3315
+ src: shot,
3316
+ alt: read ? "Attached image the assistant reads" : "Attached image",
3317
+ "data-gg-assist-thumb": "",
3318
+ style: {
3319
+ display: "block",
3320
+ width: "100%",
3321
+ height: "100%",
3322
+ objectFit: "cover",
3323
+ borderRadius: "8px",
3324
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
3325
+ opacity: read ? 1 : 0.45,
3326
+ boxSizing: "border-box"
3327
+ }
3258
3328
  }
3259
- }
3260
- ),
3261
- onRemoveImage && /* @__PURE__ */ jsx3(
3262
- "button",
3263
- {
3264
- type: "button",
3265
- "data-gg-assist-remove": "",
3266
- "aria-label": `Remove image ${i + 1}`,
3267
- title: "Remove this image",
3268
- onClick: () => {
3269
- stopAutoFile("changed attachments");
3270
- logEvent(
3271
- "image_removed",
3272
- pageShot && shot === pageShot ? "page capture" : "attached"
3273
- );
3274
- onRemoveImage(i);
3275
- },
3276
- style: {
3277
- position: "absolute",
3278
- top: "-13px",
3279
- right: "-13px",
3280
- width: "32px",
3281
- height: "32px",
3282
- border: "none",
3283
- background: "transparent",
3284
- padding: 0,
3285
- cursor: "pointer",
3286
- display: "flex",
3287
- alignItems: "center",
3288
- justifyContent: "center",
3289
- borderRadius: "50%"
3290
- },
3291
- children: /* @__PURE__ */ jsx3(
3292
- "span",
3293
- {
3294
- "aria-hidden": "true",
3295
- style: {
3296
- width: "20px",
3297
- height: "20px",
3298
- boxSizing: "border-box",
3299
- borderRadius: "50%",
3300
- border: `2px solid ${t.bg}`,
3301
- background: "#ef4444",
3302
- color: "#fff",
3303
- display: "flex",
3304
- alignItems: "center",
3305
- justifyContent: "center"
3306
- },
3307
- children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3308
- "path",
3309
- {
3310
- d: "M1 1L9 9M9 1L1 9",
3311
- stroke: "currentColor",
3312
- strokeWidth: "1.6",
3313
- strokeLinecap: "round"
3314
- }
3315
- ) })
3329
+ ),
3330
+ onRemoveImage && /* @__PURE__ */ jsx3(
3331
+ "button",
3332
+ {
3333
+ type: "button",
3334
+ "data-gg-assist-remove": "",
3335
+ "aria-label": `Remove image ${i + 1}`,
3336
+ title: "Remove this image",
3337
+ onClick: () => {
3338
+ stopAutoFile("changed attachments");
3339
+ logEvent(
3340
+ "image_removed",
3341
+ pageShot && shot === pageShot ? "page capture" : "attached"
3342
+ );
3343
+ onRemoveImage(i);
3344
+ },
3345
+ style: {
3346
+ position: "absolute",
3347
+ top: "-13px",
3348
+ right: "-13px",
3349
+ width: "32px",
3350
+ height: "32px",
3351
+ border: "none",
3352
+ background: "transparent",
3353
+ padding: 0,
3354
+ cursor: "pointer",
3355
+ display: "flex",
3356
+ alignItems: "center",
3357
+ justifyContent: "center",
3358
+ borderRadius: "50%"
3359
+ },
3360
+ children: /* @__PURE__ */ jsx3(
3361
+ "span",
3362
+ {
3363
+ "aria-hidden": "true",
3364
+ style: {
3365
+ width: "20px",
3366
+ height: "20px",
3367
+ boxSizing: "border-box",
3368
+ borderRadius: "50%",
3369
+ border: `2px solid ${t.bg}`,
3370
+ background: "#ef4444",
3371
+ color: "#fff",
3372
+ display: "flex",
3373
+ alignItems: "center",
3374
+ justifyContent: "center"
3375
+ },
3376
+ children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3377
+ "path",
3378
+ {
3379
+ d: "M1 1L9 9M9 1L1 9",
3380
+ stroke: "currentColor",
3381
+ strokeWidth: "1.6",
3382
+ strokeLinecap: "round"
3383
+ }
3384
+ ) })
3385
+ }
3386
+ )
3387
+ }
3388
+ )
3389
+ ]
3390
+ },
3391
+ `${i}-${shot.slice(-16)}`
3392
+ );
3393
+ }),
3394
+ files.map((file, i) => {
3395
+ const read = readFiles.has(i);
3396
+ const pdf = isPdfFile(file.name);
3397
+ const why = read ? "the assistant reads this file" : pdf && file.reading ? "reading the text in this PDF\u2026" : pdf && file.text !== void 0 ? file.text === null ? file.tooLarge ? "attached to the report \u2014 too large for the assistant to read (over 3 MB)" : "attached to the report \u2014 couldn't read this PDF" : file.text.trim() ? "attached, but more than one message can carry, so the assistant is not reading it" : "attached to the report \u2014 this PDF has no text to read (a scan?)" : 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";
3398
+ return /* @__PURE__ */ jsxs3(
3399
+ "div",
3400
+ {
3401
+ "data-gg-assist-file": "",
3402
+ "data-gg-assist-file-read": read ? "" : void 0,
3403
+ "data-gg-assist-file-reading": pdf && file.reading ? "" : void 0,
3404
+ title: `${file.name} \u2014 ${why}`,
3405
+ style: {
3406
+ position: "relative",
3407
+ height: "40px",
3408
+ maxWidth: "170px",
3409
+ display: "flex",
3410
+ alignItems: "center",
3411
+ gap: "6px",
3412
+ padding: "0 10px",
3413
+ borderRadius: "8px",
3414
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
3415
+ opacity: read || pdf && file.reading ? 1 : 0.55,
3416
+ boxSizing: "border-box",
3417
+ flexShrink: 0
3418
+ },
3419
+ children: [
3420
+ pdf && file.reading ? /* @__PURE__ */ jsx3(
3421
+ "span",
3422
+ {
3423
+ role: "status",
3424
+ "aria-label": "Reading this PDF",
3425
+ style: {
3426
+ width: "12px",
3427
+ height: "12px",
3428
+ flexShrink: 0,
3429
+ borderRadius: "50%",
3430
+ border: `1.5px solid ${t.inputBorder}`,
3431
+ borderTopColor: t.accent,
3432
+ boxSizing: "border-box",
3433
+ animation: "gg-spin .8s linear infinite"
3316
3434
  }
3317
- )
3318
- }
3319
- )
3320
- ]
3321
- },
3322
- `${i}-${shot.slice(-16)}`
3323
- );
3324
- }),
3325
- files.map((file, i) => {
3326
- const read = readFiles.has(i);
3327
- 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";
3328
- return /* @__PURE__ */ jsxs3(
3329
- "div",
3435
+ }
3436
+ ) : /* @__PURE__ */ jsx3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx3(
3437
+ "path",
3438
+ {
3439
+ d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
3440
+ stroke: t.textMuted,
3441
+ strokeWidth: "1.5",
3442
+ strokeLinecap: "round",
3443
+ strokeLinejoin: "round"
3444
+ }
3445
+ ) }),
3446
+ /* @__PURE__ */ jsx3(
3447
+ "span",
3448
+ {
3449
+ style: {
3450
+ fontSize: "11.5px",
3451
+ color: t.text,
3452
+ overflow: "hidden",
3453
+ textOverflow: "ellipsis",
3454
+ whiteSpace: "nowrap"
3455
+ },
3456
+ children: file.name
3457
+ }
3458
+ ),
3459
+ onRemoveFile && /* @__PURE__ */ jsx3(
3460
+ "button",
3461
+ {
3462
+ type: "button",
3463
+ "data-gg-assist-remove-file": "",
3464
+ "aria-label": `Remove file ${file.name}`,
3465
+ title: "Remove this file",
3466
+ onClick: () => {
3467
+ stopAutoFile("changed attachments");
3468
+ logEvent("file_removed", read ? "read" : "not read");
3469
+ onRemoveFile(i);
3470
+ },
3471
+ style: {
3472
+ position: "absolute",
3473
+ top: "-13px",
3474
+ right: "-13px",
3475
+ width: "32px",
3476
+ height: "32px",
3477
+ border: "none",
3478
+ background: "transparent",
3479
+ padding: 0,
3480
+ cursor: "pointer",
3481
+ display: "flex",
3482
+ alignItems: "center",
3483
+ justifyContent: "center",
3484
+ borderRadius: "50%"
3485
+ },
3486
+ children: /* @__PURE__ */ jsx3(
3487
+ "span",
3488
+ {
3489
+ "aria-hidden": "true",
3490
+ style: {
3491
+ width: "20px",
3492
+ height: "20px",
3493
+ boxSizing: "border-box",
3494
+ borderRadius: "50%",
3495
+ border: `2px solid ${t.bg}`,
3496
+ background: "#ef4444",
3497
+ color: "#fff",
3498
+ display: "flex",
3499
+ alignItems: "center",
3500
+ justifyContent: "center"
3501
+ },
3502
+ children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3503
+ "path",
3504
+ {
3505
+ d: "M1 1L9 9M9 1L1 9",
3506
+ stroke: "currentColor",
3507
+ strokeWidth: "1.6",
3508
+ strokeLinecap: "round"
3509
+ }
3510
+ ) })
3511
+ }
3512
+ )
3513
+ }
3514
+ )
3515
+ ]
3516
+ },
3517
+ `file-${i}-${file.name}`
3518
+ );
3519
+ }),
3520
+ onAddFiles && /* @__PURE__ */ jsxs3(
3521
+ "label",
3330
3522
  {
3331
- "data-gg-assist-file": "",
3332
- "data-gg-assist-file-read": read ? "" : void 0,
3333
- title: `${file.name} \u2014 ${why}`,
3523
+ "data-gg-assist-attach": "",
3524
+ title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3334
3525
  style: {
3335
- position: "relative",
3526
+ width: "40px",
3336
3527
  height: "40px",
3337
- maxWidth: "170px",
3338
- display: "flex",
3339
- alignItems: "center",
3340
- gap: "6px",
3341
- padding: "0 10px",
3342
3528
  borderRadius: "8px",
3343
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3344
- opacity: read ? 1 : 0.55,
3345
- boxSizing: "border-box",
3529
+ border: `1px dashed ${t.inputBorder}`,
3530
+ display: "inline-flex",
3531
+ alignItems: "center",
3532
+ justifyContent: "center",
3533
+ color: t.textMuted,
3534
+ fontSize: "18px",
3535
+ cursor: "pointer",
3346
3536
  flexShrink: 0
3347
3537
  },
3348
3538
  children: [
3349
- /* @__PURE__ */ jsx3("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx3(
3350
- "path",
3351
- {
3352
- d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
3353
- stroke: t.textMuted,
3354
- strokeWidth: "1.5",
3355
- strokeLinecap: "round",
3356
- strokeLinejoin: "round"
3357
- }
3358
- ) }),
3539
+ "+",
3359
3540
  /* @__PURE__ */ jsx3(
3360
- "span",
3361
- {
3362
- style: {
3363
- fontSize: "11.5px",
3364
- color: t.text,
3365
- overflow: "hidden",
3366
- textOverflow: "ellipsis",
3367
- whiteSpace: "nowrap"
3368
- },
3369
- children: file.name
3370
- }
3371
- ),
3372
- onRemoveFile && /* @__PURE__ */ jsx3(
3373
- "button",
3541
+ "input",
3374
3542
  {
3375
- type: "button",
3376
- "data-gg-assist-remove-file": "",
3377
- "aria-label": `Remove file ${file.name}`,
3378
- title: "Remove this file",
3379
- onClick: () => {
3380
- stopAutoFile("changed attachments");
3381
- logEvent("file_removed", read ? "read" : "not read");
3382
- onRemoveFile(i);
3383
- },
3384
- style: {
3385
- position: "absolute",
3386
- top: "-13px",
3387
- right: "-13px",
3388
- width: "32px",
3389
- height: "32px",
3390
- border: "none",
3391
- background: "transparent",
3392
- padding: 0,
3393
- cursor: "pointer",
3394
- display: "flex",
3395
- alignItems: "center",
3396
- justifyContent: "center",
3397
- borderRadius: "50%"
3398
- },
3399
- children: /* @__PURE__ */ jsx3(
3400
- "span",
3401
- {
3402
- "aria-hidden": "true",
3403
- style: {
3404
- width: "20px",
3405
- height: "20px",
3406
- boxSizing: "border-box",
3407
- borderRadius: "50%",
3408
- border: `2px solid ${t.bg}`,
3409
- background: "#ef4444",
3410
- color: "#fff",
3411
- display: "flex",
3412
- alignItems: "center",
3413
- justifyContent: "center"
3414
- },
3415
- children: /* @__PURE__ */ jsx3("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx3(
3416
- "path",
3417
- {
3418
- d: "M1 1L9 9M9 1L1 9",
3419
- stroke: "currentColor",
3420
- strokeWidth: "1.6",
3421
- strokeLinecap: "round"
3422
- }
3423
- ) })
3543
+ type: "file",
3544
+ accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
3545
+ multiple: true,
3546
+ onChange: (e) => {
3547
+ var _a;
3548
+ const picked = Array.from((_a = e.target.files) != null ? _a : []);
3549
+ if (picked.length) {
3550
+ stopAutoFile("changed attachments");
3551
+ const images = picked.filter((f) => f.type.startsWith("image/")).length;
3552
+ if (images) logEvent("image_added", String(images));
3553
+ if (picked.length > images) {
3554
+ logEvent("file_added", String(picked.length - images));
3555
+ }
3556
+ onAddFiles(picked);
3424
3557
  }
3425
- )
3558
+ e.target.value = "";
3559
+ },
3560
+ style: { display: "none" }
3426
3561
  }
3427
3562
  )
3428
3563
  ]
3429
- },
3430
- `file-${i}-${file.name}`
3431
- );
3432
- }),
3433
- onAddFiles && /* @__PURE__ */ jsxs3(
3434
- "label",
3435
- {
3436
- "data-gg-assist-attach": "",
3437
- title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3438
- style: {
3439
- width: "40px",
3440
- height: "40px",
3441
- borderRadius: "8px",
3442
- border: `1px dashed ${t.inputBorder}`,
3443
- display: "inline-flex",
3444
- alignItems: "center",
3445
- justifyContent: "center",
3446
- color: t.textMuted,
3447
- fontSize: "18px",
3448
- cursor: "pointer",
3449
- flexShrink: 0
3450
- },
3451
- children: [
3452
- "+",
3453
- /* @__PURE__ */ jsx3(
3454
- "input",
3455
- {
3456
- type: "file",
3457
- accept: ASSIST_ATTACHMENT_ACCEPT,
3458
- multiple: true,
3459
- onChange: (e) => {
3460
- var _a;
3461
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
3462
- if (picked.length) {
3463
- stopAutoFile("changed attachments");
3464
- const images = picked.filter((f) => f.type.startsWith("image/")).length;
3465
- if (images) logEvent("image_added", String(images));
3466
- if (picked.length > images) {
3467
- logEvent("file_added", String(picked.length - images));
3468
- }
3469
- onAddFiles(picked);
3470
- }
3471
- e.target.value = "";
3472
- },
3473
- style: { display: "none" }
3474
- }
3475
- )
3476
- ]
3477
- }
3478
- ),
3479
- readShots.size < screenshots.length && /* @__PURE__ */ jsxs3("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
3480
- readShots.size,
3481
- " of ",
3482
- screenshots.length,
3483
- " read"
3484
- ] })
3485
- ]
3486
- }
3487
- ),
3488
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
3489
- /* @__PURE__ */ jsx3(
3490
- "textarea",
3491
- {
3492
- value: input,
3493
- onChange: (e) => {
3494
- stopAutoFile("typing a change");
3495
- setInput(e.target.value);
3496
- },
3497
- onKeyDown: (e) => {
3498
- if (e.key === "Enter" && !e.shiftKey) {
3499
- e.preventDefault();
3500
- sendTyped(input);
3501
- }
3502
- },
3503
- placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
3504
- rows: 1,
3505
- disabled: busy,
3506
- autoFocus: true,
3507
- style: {
3508
- flex: 1,
3509
- minHeight: "42px",
3510
- maxHeight: "120px",
3511
- padding: "11px 12px",
3512
- borderRadius: "10px",
3513
- border: `1px solid ${t.inputBorder}`,
3514
- backgroundColor: t.inputBg,
3515
- color: t.text,
3516
- fontSize: "13px",
3517
- lineHeight: 1.4,
3518
- fontFamily: "inherit",
3519
- resize: "none",
3520
- outline: "none",
3521
- boxSizing: "border-box"
3522
- }
3564
+ }
3565
+ ),
3566
+ readShots.size < screenshots.length && /* @__PURE__ */ jsxs3("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
3567
+ readShots.size,
3568
+ " of ",
3569
+ screenshots.length,
3570
+ " read"
3571
+ ] })
3572
+ ]
3523
3573
  }
3524
3574
  ),
3525
- /* @__PURE__ */ jsx3(
3526
- "button",
3527
- {
3528
- type: "button",
3529
- "data-gg-sheet-send": "",
3530
- onClick: () => sendTyped(input),
3531
- disabled: busy || !input.trim(),
3532
- "aria-label": "Send message",
3533
- "data-gg-send-message": "",
3534
- style: {
3535
- flexShrink: 0,
3536
- width: "42px",
3537
- height: "42px",
3538
- borderRadius: "10px",
3539
- border: "none",
3540
- display: "inline-flex",
3541
- alignItems: "center",
3542
- justifyContent: "center",
3543
- backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
3544
- color: busy || !input.trim() ? t.textMuted : t.accentText,
3545
- cursor: busy || !input.trim() ? "default" : "pointer"
3546
- },
3547
- children: /* @__PURE__ */ jsx3("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
3548
- "path",
3549
- {
3550
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
3551
- fill: "currentColor"
3575
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
3576
+ /* @__PURE__ */ jsx3(
3577
+ "textarea",
3578
+ {
3579
+ ref: composerRef,
3580
+ value: input,
3581
+ onChange: (e) => {
3582
+ stopAutoFile("typing a change");
3583
+ setInput(e.target.value);
3584
+ },
3585
+ onKeyDown: (e) => {
3586
+ if (e.key === "Enter" && !e.shiftKey) {
3587
+ e.preventDefault();
3588
+ sendTyped(input);
3589
+ }
3590
+ },
3591
+ placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
3592
+ rows: 3,
3593
+ disabled: busy,
3594
+ autoFocus: true,
3595
+ style: {
3596
+ flex: 1,
3597
+ minHeight: "72px",
3598
+ maxHeight: "220px",
3599
+ padding: "11px 12px",
3600
+ borderRadius: "10px",
3601
+ border: `1px solid ${t.inputBorder}`,
3602
+ backgroundColor: t.inputBg,
3603
+ color: t.text,
3604
+ fontSize: "13px",
3605
+ lineHeight: 1.4,
3606
+ fontFamily: "inherit",
3607
+ resize: "none",
3608
+ outline: "none",
3609
+ boxSizing: "border-box"
3552
3610
  }
3553
- ) })
3554
- }
3555
- )
3611
+ }
3612
+ ),
3613
+ /* @__PURE__ */ jsx3(
3614
+ "button",
3615
+ {
3616
+ type: "button",
3617
+ "data-gg-sheet-send": "",
3618
+ onClick: () => sendTyped(input),
3619
+ disabled: busy || !input.trim(),
3620
+ "aria-label": "Send message",
3621
+ "data-gg-send-message": "",
3622
+ style: {
3623
+ flexShrink: 0,
3624
+ width: "42px",
3625
+ height: "42px",
3626
+ borderRadius: "10px",
3627
+ border: "none",
3628
+ display: "inline-flex",
3629
+ alignItems: "center",
3630
+ justifyContent: "center",
3631
+ backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
3632
+ color: busy || !input.trim() ? t.textMuted : t.accentText,
3633
+ cursor: busy || !input.trim() ? "default" : "pointer"
3634
+ },
3635
+ children: /* @__PURE__ */ jsx3("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
3636
+ "path",
3637
+ {
3638
+ d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
3639
+ fill: "currentColor"
3640
+ }
3641
+ ) })
3642
+ }
3643
+ )
3644
+ ] })
3556
3645
  ] }),
3557
3646
  phase === "draft" && /* @__PURE__ */ jsxs3(Fragment2, { children: [
3558
3647
  validationError && /* @__PURE__ */ jsx3(
@@ -3566,7 +3655,7 @@ function AssistSheet({
3566
3655
  children: validationError
3567
3656
  }
3568
3657
  ),
3569
- autoFileLeft !== null ? (
3658
+ similarSlot ? similarSlot : autoFileLeft !== null ? (
3570
3659
  // #402: "Yes, that's it" was the confirmation. The draft
3571
3660
  // files itself unless they stop it — the bar shows how
3572
3661
  // long is left, the button sends now, the × on its right
@@ -4622,6 +4711,7 @@ function ReportDialog({
4622
4711
  enhanceText: enhanceText2,
4623
4712
  assist,
4624
4713
  findSimilarIssues: findSimilarIssues2,
4714
+ readFile,
4625
4715
  assistContext = null,
4626
4716
  transcribeAudio: transcribeAudio2,
4627
4717
  types,
@@ -4675,6 +4765,15 @@ function ReportDialog({
4675
4765
  const pendingShotRef = useRef4(null);
4676
4766
  const openGenRef = useRef4(0);
4677
4767
  const [attachments, setAttachments] = useState4([]);
4768
+ const [pdfTexts, setPdfTexts] = useState4({});
4769
+ const heldFiles = useMemo2(
4770
+ () => attachments.map((a) => {
4771
+ const read = pdfTexts[heldFileKey(a)];
4772
+ if (!read) return a;
4773
+ return "reading" in read ? { ...a, reading: true } : { ...a, text: read.text, ...read.tooLarge ? { tooLarge: true } : {} };
4774
+ }),
4775
+ [attachments, pdfTexts]
4776
+ );
4678
4777
  const [previewIndex, setPreviewIndex] = useState4(null);
4679
4778
  const [annotatingIndex, setAnnotatingIndex] = useState4(null);
4680
4779
  const [previewZoomed, setPreviewZoomed] = useState4(false);
@@ -4835,10 +4934,16 @@ function ReportDialog({
4835
4934
  files.filter((file) => !file.type.startsWith("image/")).forEach((file) => {
4836
4935
  const reader = new FileReader();
4837
4936
  reader.onload = () => {
4838
- setAttachments((prev) => [
4839
- ...prev,
4840
- { name: file.name, size: file.size, dataUrl: reader.result }
4841
- ]);
4937
+ const dataUrl = reader.result;
4938
+ setAttachments((prev) => [...prev, { name: file.name, size: file.size, dataUrl }]);
4939
+ const key = heldFileKey(file);
4940
+ if (!readFile || !isPdfFile(file.name) || pdfTexts[key]) return;
4941
+ if (file.size > MAX_READABLE_PDF_BYTES) {
4942
+ setPdfTexts((prev) => ({ ...prev, [key]: { text: null, tooLarge: true } }));
4943
+ return;
4944
+ }
4945
+ setPdfTexts((prev) => ({ ...prev, [key]: { reading: true } }));
4946
+ void Promise.resolve().then(() => readFile({ name: file.name, dataUrl })).catch(() => null).then((text) => setPdfTexts((prev) => ({ ...prev, [key]: { text } })));
4842
4947
  };
4843
4948
  reader.readAsDataURL(file);
4844
4949
  });
@@ -5274,6 +5379,10 @@ function ReportDialog({
5274
5379
  stopListeningAndTranscribe();
5275
5380
  }
5276
5381
  };
5382
+ useEffect4(() => {
5383
+ setSimilarIssues(null);
5384
+ setPickedSimilar(null);
5385
+ }, [description]);
5277
5386
  const handleSubmit = async (opts) => {
5278
5387
  var _a2, _b, _c;
5279
5388
  try {
@@ -5314,11 +5423,17 @@ function ReportDialog({
5314
5423
  }
5315
5424
  const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
5316
5425
  const attachKind = (opts == null ? void 0 : opts.duplicateOf) ? (_b = opts.kind) != null ? _b : "duplicate" : duplicateKind;
5317
- if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
5426
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf) {
5427
+ const similarFiles = filesForSimilar(
5428
+ heldFiles,
5429
+ MAX_SIMILAR_FILE_CHARS,
5430
+ MAX_SIMILAR_FILES_TOTAL_CHARS
5431
+ );
5318
5432
  setIsSubmitting(true);
5319
5433
  setCheckingSimilar(true);
5320
5434
  const matches = await Promise.race([
5321
- findSimilarIssues2(description.trim()).catch(() => null),
5435
+ // A PDF still being read is left out rather than waited for.
5436
+ (similarFiles.length ? findSimilarIssues2(description.trim(), similarFiles) : findSimilarIssues2(description.trim())).catch(() => null),
5322
5437
  new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
5323
5438
  ]);
5324
5439
  setCheckingSimilar(false);
@@ -5350,6 +5465,8 @@ function ReportDialog({
5350
5465
  if (duplicateOf) {
5351
5466
  metadata.duplicateIssueNumber = String(duplicateOf);
5352
5467
  if (attachKind === "related") metadata.duplicateKind = "related";
5468
+ } else if (opts == null ? void 0 : opts.possiblyRelated) {
5469
+ metadata.possiblyRelatedIssue = String(opts.possiblyRelated);
5353
5470
  }
5354
5471
  if (aiConversationId) {
5355
5472
  metadata.aiConversationId = aiConversationId;
@@ -5363,7 +5480,9 @@ function ReportDialog({
5363
5480
  Object.keys(metadata).length > 0 ? metadata : void 0
5364
5481
  );
5365
5482
  if (result) {
5366
- setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
5483
+ setAddedToIssue(
5484
+ duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : result.status === "DUPLICATE" && result.issueNumber ? result.issueNumber : null
5485
+ );
5367
5486
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5368
5487
  const queued = result.status === "QUEUED" && !!result.message;
5369
5488
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
@@ -5371,6 +5490,7 @@ function ReportDialog({
5371
5490
  setDescription("");
5372
5491
  setScreenshots([]);
5373
5492
  setAttachments([]);
5493
+ setPdfTexts({});
5374
5494
  setAiConversationId(null);
5375
5495
  setIssueTitle("");
5376
5496
  setDuplicateIssueNumber(null);
@@ -5391,6 +5511,220 @@ function ReportDialog({
5391
5511
  setAttachingTo(null);
5392
5512
  }
5393
5513
  };
5514
+ const similarCard = similarIssues ? /* @__PURE__ */ jsxs5(
5515
+ "div",
5516
+ {
5517
+ "data-gg-similar": "",
5518
+ style: {
5519
+ marginTop: "12px",
5520
+ display: "flex",
5521
+ flexDirection: "column",
5522
+ gap: "10px",
5523
+ padding: "12px",
5524
+ borderRadius: "10px",
5525
+ border: "1px solid rgba(245,158,11,0.4)",
5526
+ backgroundColor: "rgba(245,158,11,0.08)"
5527
+ },
5528
+ children: [
5529
+ /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
5530
+ /* @__PURE__ */ jsx5("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
5531
+ /* @__PURE__ */ jsx5("span", { style: { color: t.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches. Same problem, or a different request for the same feature \u2014 it's added to that issue, so the team has it in one place." : "Same problem, or a different request for the same feature? Pick it \u2014 it's added to that issue, so the team has it in one place." })
5532
+ ] }),
5533
+ /* @__PURE__ */ jsx5(
5534
+ "div",
5535
+ {
5536
+ role: "radiogroup",
5537
+ "aria-label": "Existing issues",
5538
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
5539
+ children: similarIssues.map((issue) => {
5540
+ const picked = pickedSimilar === issue.number;
5541
+ return /* @__PURE__ */ jsxs5(
5542
+ "button",
5543
+ {
5544
+ type: "button",
5545
+ role: "radio",
5546
+ "aria-checked": picked,
5547
+ disabled: isSubmitting,
5548
+ onClick: () => setPickedSimilar(issue.number),
5549
+ style: {
5550
+ display: "flex",
5551
+ alignItems: "flex-start",
5552
+ gap: "10px",
5553
+ width: "100%",
5554
+ minHeight: "44px",
5555
+ padding: "9px 10px",
5556
+ borderRadius: "8px",
5557
+ border: `1px solid ${picked ? t.accent : t.border}`,
5558
+ boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
5559
+ backgroundColor: t.bg,
5560
+ color: t.text,
5561
+ textAlign: "left",
5562
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5563
+ fontFamily: "inherit"
5564
+ },
5565
+ children: [
5566
+ /* @__PURE__ */ jsx5(
5567
+ "span",
5568
+ {
5569
+ "aria-hidden": "true",
5570
+ style: {
5571
+ flexShrink: 0,
5572
+ marginTop: "2px",
5573
+ width: "14px",
5574
+ height: "14px",
5575
+ boxSizing: "border-box",
5576
+ borderRadius: "50%",
5577
+ border: `2px solid ${picked ? t.accent : t.textMuted}`,
5578
+ backgroundColor: picked ? t.accent : "transparent",
5579
+ boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
5580
+ }
5581
+ }
5582
+ ),
5583
+ /* @__PURE__ */ jsxs5(
5584
+ "span",
5585
+ {
5586
+ style: {
5587
+ display: "flex",
5588
+ flexDirection: "column",
5589
+ gap: "3px",
5590
+ minWidth: 0
5591
+ },
5592
+ children: [
5593
+ /* @__PURE__ */ jsxs5(
5594
+ "span",
5595
+ {
5596
+ title: issue.title,
5597
+ style: {
5598
+ fontSize: "12.5px",
5599
+ lineHeight: 1.45,
5600
+ wordBreak: "break-word",
5601
+ overflow: "hidden",
5602
+ display: "-webkit-box",
5603
+ WebkitLineClamp: 3,
5604
+ WebkitBoxOrient: "vertical"
5605
+ },
5606
+ children: [
5607
+ /* @__PURE__ */ jsxs5("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
5608
+ "#",
5609
+ issue.number
5610
+ ] }),
5611
+ " ",
5612
+ issue.title
5613
+ ]
5614
+ }
5615
+ ),
5616
+ issue.status && /* @__PURE__ */ jsx5("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
5617
+ ]
5618
+ }
5619
+ )
5620
+ ]
5621
+ },
5622
+ issue.number
5623
+ );
5624
+ })
5625
+ }
5626
+ ),
5627
+ /* @__PURE__ */ jsx5(
5628
+ "button",
5629
+ {
5630
+ type: "button",
5631
+ disabled: isSubmitting || !pickedSimilar,
5632
+ onClick: () => {
5633
+ if (!pickedSimilar) return;
5634
+ setAttachingTo(pickedSimilar);
5635
+ setDuplicateIssueNumber(pickedSimilar);
5636
+ setDuplicateKind("duplicate");
5637
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
5638
+ },
5639
+ style: {
5640
+ width: "100%",
5641
+ minHeight: "40px",
5642
+ padding: "10px",
5643
+ borderRadius: "8px",
5644
+ border: "none",
5645
+ backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
5646
+ color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
5647
+ fontSize: "14px",
5648
+ fontWeight: 600,
5649
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
5650
+ fontFamily: "inherit"
5651
+ },
5652
+ children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
5653
+ /* @__PURE__ */ jsx5(ButtonSpinner, {}),
5654
+ "Adding to #",
5655
+ attachingTo,
5656
+ "\u2026"
5657
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
5658
+ }
5659
+ ),
5660
+ pickedSimilar && /* @__PURE__ */ jsx5(
5661
+ "button",
5662
+ {
5663
+ type: "button",
5664
+ disabled: isSubmitting,
5665
+ onClick: () => {
5666
+ setAttachingTo(pickedSimilar);
5667
+ setDuplicateIssueNumber(pickedSimilar);
5668
+ setDuplicateKind("related");
5669
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
5670
+ },
5671
+ style: {
5672
+ width: "100%",
5673
+ minHeight: "40px",
5674
+ marginTop: "-4px",
5675
+ padding: "9px 10px",
5676
+ borderRadius: "8px",
5677
+ border: "1px solid rgba(245,158,11,0.4)",
5678
+ backgroundColor: "transparent",
5679
+ color: isSubmitting ? t.textMuted : t.text,
5680
+ fontSize: "13px",
5681
+ fontWeight: 600,
5682
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5683
+ fontFamily: "inherit"
5684
+ },
5685
+ children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
5686
+ /* @__PURE__ */ jsx5(ButtonSpinner, {}),
5687
+ "Adding to #",
5688
+ attachingTo,
5689
+ "\u2026"
5690
+ ] }) : `Related request \u2014 add to #${pickedSimilar}`
5691
+ }
5692
+ ),
5693
+ /* @__PURE__ */ jsx5(
5694
+ "button",
5695
+ {
5696
+ type: "button",
5697
+ disabled: isSubmitting,
5698
+ onClick: () => {
5699
+ var _a2;
5700
+ setAttachingTo("new");
5701
+ void handleSubmit({ skipSimilar: true, possiblyRelated: (_a2 = similarIssues[0]) == null ? void 0 : _a2.number });
5702
+ },
5703
+ style: {
5704
+ width: "100%",
5705
+ minHeight: "40px",
5706
+ marginTop: "-4px",
5707
+ padding: "8px",
5708
+ borderRadius: "8px",
5709
+ border: "none",
5710
+ backgroundColor: "transparent",
5711
+ color: isSubmitting ? t.textMuted : t.text,
5712
+ fontSize: "13px",
5713
+ fontWeight: 500,
5714
+ textDecoration: "underline",
5715
+ textUnderlineOffset: "3px",
5716
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5717
+ fontFamily: "inherit"
5718
+ },
5719
+ children: attachingTo === "new" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
5720
+ /* @__PURE__ */ jsx5(ButtonSpinner, {}),
5721
+ "Sending as new\u2026"
5722
+ ] }) : "No, mine is different \u2014 send as new"
5723
+ }
5724
+ )
5725
+ ]
5726
+ }
5727
+ ) : null;
5394
5728
  if (!mounted) return null;
5395
5729
  return /* @__PURE__ */ jsxs5(Fragment4, { children: [
5396
5730
  isOpen && createPortal3(
@@ -6929,224 +7263,7 @@ function ReportDialog({
6929
7263
  children: validationError
6930
7264
  }
6931
7265
  ),
6932
- similarIssues ? (
6933
- // Same visual language as the assistant's "Our team is
6934
- // already on this" card, so both routes to a duplicate
6935
- // read as one feature.
6936
- /* @__PURE__ */ jsxs5(
6937
- "div",
6938
- {
6939
- "data-gg-similar": "",
6940
- style: {
6941
- marginTop: "12px",
6942
- display: "flex",
6943
- flexDirection: "column",
6944
- gap: "10px",
6945
- padding: "12px",
6946
- borderRadius: "10px",
6947
- border: "1px solid rgba(245,158,11,0.4)",
6948
- backgroundColor: "rgba(245,158,11,0.08)"
6949
- },
6950
- children: [
6951
- /* @__PURE__ */ jsxs5("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
6952
- /* @__PURE__ */ jsx5("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
6953
- /* @__PURE__ */ jsx5("span", { style: { color: t.textMuted, fontSize: "12px", lineHeight: 1.5 }, children: similarIssues.length > 1 ? "Pick the one that matches. Same problem, or a different request for the same feature \u2014 it's added to that issue, so the team has it in one place." : "Same problem, or a different request for the same feature? Pick it \u2014 it's added to that issue, so the team has it in one place." })
6954
- ] }),
6955
- /* @__PURE__ */ jsx5(
6956
- "div",
6957
- {
6958
- role: "radiogroup",
6959
- "aria-label": "Existing issues",
6960
- style: { display: "flex", flexDirection: "column", gap: "6px" },
6961
- children: similarIssues.map((issue) => {
6962
- const picked = pickedSimilar === issue.number;
6963
- return /* @__PURE__ */ jsxs5(
6964
- "button",
6965
- {
6966
- type: "button",
6967
- role: "radio",
6968
- "aria-checked": picked,
6969
- disabled: isSubmitting,
6970
- onClick: () => setPickedSimilar(issue.number),
6971
- style: {
6972
- display: "flex",
6973
- alignItems: "flex-start",
6974
- gap: "10px",
6975
- width: "100%",
6976
- minHeight: "44px",
6977
- padding: "9px 10px",
6978
- borderRadius: "8px",
6979
- border: `1px solid ${picked ? t.accent : t.border}`,
6980
- boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
6981
- backgroundColor: t.bg,
6982
- color: t.text,
6983
- textAlign: "left",
6984
- cursor: isSubmitting ? "not-allowed" : "pointer",
6985
- fontFamily: "inherit"
6986
- },
6987
- children: [
6988
- /* @__PURE__ */ jsx5(
6989
- "span",
6990
- {
6991
- "aria-hidden": "true",
6992
- style: {
6993
- flexShrink: 0,
6994
- marginTop: "2px",
6995
- width: "14px",
6996
- height: "14px",
6997
- boxSizing: "border-box",
6998
- borderRadius: "50%",
6999
- border: `2px solid ${picked ? t.accent : t.textMuted}`,
7000
- backgroundColor: picked ? t.accent : "transparent",
7001
- boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
7002
- }
7003
- }
7004
- ),
7005
- /* @__PURE__ */ jsxs5(
7006
- "span",
7007
- {
7008
- style: {
7009
- display: "flex",
7010
- flexDirection: "column",
7011
- gap: "3px",
7012
- minWidth: 0
7013
- },
7014
- children: [
7015
- /* @__PURE__ */ jsxs5(
7016
- "span",
7017
- {
7018
- title: issue.title,
7019
- style: {
7020
- fontSize: "12.5px",
7021
- lineHeight: 1.45,
7022
- wordBreak: "break-word",
7023
- overflow: "hidden",
7024
- display: "-webkit-box",
7025
- WebkitLineClamp: 3,
7026
- WebkitBoxOrient: "vertical"
7027
- },
7028
- children: [
7029
- /* @__PURE__ */ jsxs5("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
7030
- "#",
7031
- issue.number
7032
- ] }),
7033
- " ",
7034
- issue.title
7035
- ]
7036
- }
7037
- ),
7038
- issue.status && /* @__PURE__ */ jsx5("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
7039
- ]
7040
- }
7041
- )
7042
- ]
7043
- },
7044
- issue.number
7045
- );
7046
- })
7047
- }
7048
- ),
7049
- /* @__PURE__ */ jsx5(
7050
- "button",
7051
- {
7052
- type: "button",
7053
- disabled: isSubmitting || !pickedSimilar,
7054
- onClick: () => {
7055
- if (!pickedSimilar) return;
7056
- setAttachingTo(pickedSimilar);
7057
- setDuplicateIssueNumber(pickedSimilar);
7058
- setDuplicateKind("duplicate");
7059
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
7060
- },
7061
- style: {
7062
- width: "100%",
7063
- minHeight: "40px",
7064
- padding: "10px",
7065
- borderRadius: "8px",
7066
- border: "none",
7067
- backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
7068
- color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
7069
- fontSize: "14px",
7070
- fontWeight: 600,
7071
- cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
7072
- fontFamily: "inherit"
7073
- },
7074
- children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
7075
- /* @__PURE__ */ jsx5(ButtonSpinner, {}),
7076
- "Adding to #",
7077
- attachingTo,
7078
- "\u2026"
7079
- ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
7080
- }
7081
- ),
7082
- pickedSimilar && /* @__PURE__ */ jsx5(
7083
- "button",
7084
- {
7085
- type: "button",
7086
- disabled: isSubmitting,
7087
- onClick: () => {
7088
- setAttachingTo(pickedSimilar);
7089
- setDuplicateIssueNumber(pickedSimilar);
7090
- setDuplicateKind("related");
7091
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
7092
- },
7093
- style: {
7094
- width: "100%",
7095
- minHeight: "40px",
7096
- marginTop: "-4px",
7097
- padding: "9px 10px",
7098
- borderRadius: "8px",
7099
- border: "1px solid rgba(245,158,11,0.4)",
7100
- backgroundColor: "transparent",
7101
- color: isSubmitting ? t.textMuted : t.text,
7102
- fontSize: "13px",
7103
- fontWeight: 600,
7104
- cursor: isSubmitting ? "not-allowed" : "pointer",
7105
- fontFamily: "inherit"
7106
- },
7107
- children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
7108
- /* @__PURE__ */ jsx5(ButtonSpinner, {}),
7109
- "Adding to #",
7110
- attachingTo,
7111
- "\u2026"
7112
- ] }) : `Related request \u2014 add to #${pickedSimilar}`
7113
- }
7114
- ),
7115
- /* @__PURE__ */ jsx5(
7116
- "button",
7117
- {
7118
- type: "button",
7119
- disabled: isSubmitting,
7120
- onClick: () => {
7121
- setAttachingTo("new");
7122
- void handleSubmit({ skipSimilar: true });
7123
- },
7124
- style: {
7125
- width: "100%",
7126
- minHeight: "40px",
7127
- marginTop: "-4px",
7128
- padding: "8px",
7129
- borderRadius: "8px",
7130
- border: "none",
7131
- backgroundColor: "transparent",
7132
- color: isSubmitting ? t.textMuted : t.text,
7133
- fontSize: "13px",
7134
- fontWeight: 500,
7135
- textDecoration: "underline",
7136
- textUnderlineOffset: "3px",
7137
- cursor: isSubmitting ? "not-allowed" : "pointer",
7138
- fontFamily: "inherit"
7139
- },
7140
- children: attachingTo === "new" ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
7141
- /* @__PURE__ */ jsx5(ButtonSpinner, {}),
7142
- "Sending as new\u2026"
7143
- ] }) : "No, mine is different \u2014 send as new"
7144
- }
7145
- )
7146
- ]
7147
- }
7148
- )
7149
- ) : /* @__PURE__ */ jsx5(
7266
+ similarCard != null ? similarCard : /* @__PURE__ */ jsx5(
7150
7267
  "button",
7151
7268
  {
7152
7269
  type: "button",
@@ -7445,7 +7562,8 @@ function ReportDialog({
7445
7562
  pageShot,
7446
7563
  onAddFiles: addFiles,
7447
7564
  onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
7448
- files: attachments,
7565
+ files: heldFiles,
7566
+ canReadPdf: !!readFile,
7449
7567
  onRemoveFile: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
7450
7568
  attachmentCount: screenshots.length + attachments.length,
7451
7569
  context: { ...assistContext != null ? assistContext : {}, reportType },
@@ -7459,6 +7577,10 @@ function ReportDialog({
7459
7577
  setStep(2);
7460
7578
  },
7461
7579
  onDuplicateChange: (issueNumber, kind) => {
7580
+ if (issueNumber) {
7581
+ setSimilarIssues(null);
7582
+ setPickedSimilar(null);
7583
+ }
7462
7584
  setDuplicateIssueNumber(issueNumber);
7463
7585
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7464
7586
  },
@@ -7488,7 +7610,9 @@ function ReportDialog({
7488
7610
  severityRefused: needsSeverity,
7489
7611
  isSubmitting,
7490
7612
  submitted,
7491
- onSend: () => void handleSubmit({ skipSimilar: true }),
7613
+ onSend: () => void handleSubmit(),
7614
+ similarSlot: similarCard,
7615
+ submittedText: addedToIssue ? `Added to #${addedToIssue}${addedAsRelated ? " as a related request" : ""}. Thank you!` : null,
7492
7616
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
7493
7617
  setSelfReportPrefill(prefill != null ? prefill : "");
7494
7618
  setSelfReportFrom("assist");
@@ -7782,7 +7906,9 @@ function BookingDialog({
7782
7906
  token,
7783
7907
  baseUrl,
7784
7908
  open,
7785
- onClose
7909
+ onClose,
7910
+ preview = false,
7911
+ availabilityUrl
7786
7912
  }) {
7787
7913
  var _a, _b, _c, _d, _e;
7788
7914
  const wide = useMediaQuery(WIDE_QUERY, true);
@@ -7816,10 +7942,12 @@ function BookingDialog({
7816
7942
  const [meetUrl, setMeetUrl] = useState5(null);
7817
7943
  const [busy, setBusy] = useState5(false);
7818
7944
  const [error, setError] = useState5(null);
7819
- const headers = useMemo3(
7820
- () => ({ Authorization: `Bearer ${token}`, "Content-Type": "application/json" }),
7821
- [token]
7822
- );
7945
+ const [previewEnded, setPreviewEnded] = useState5(false);
7946
+ const headers = useMemo3(() => {
7947
+ const out = { "Content-Type": "application/json" };
7948
+ if (token) out.Authorization = `Bearer ${token}`;
7949
+ return out;
7950
+ }, [token]);
7823
7951
  const loadMonth = useCallback2(
7824
7952
  async (target) => {
7825
7953
  var _a2, _b2, _c2;
@@ -7831,8 +7959,9 @@ function BookingDialog({
7831
7959
  return;
7832
7960
  }
7833
7961
  try {
7962
+ const source = availabilityUrl != null ? availabilityUrl : `${baseUrl}/api/v1/sdk/booking/availability`;
7834
7963
  const res = await fetch(
7835
- `${baseUrl}/api/v1/sdk/booking/availability?from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}`,
7964
+ `${source}${source.includes("?") ? "&" : "?"}from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}${preview ? "&preview=1" : ""}`,
7836
7965
  { headers }
7837
7966
  );
7838
7967
  const json = await res.json();
@@ -7861,7 +7990,7 @@ function BookingDialog({
7861
7990
  setPageLoaded(true);
7862
7991
  }
7863
7992
  },
7864
- [baseUrl, headers]
7993
+ [baseUrl, headers, availabilityUrl, preview]
7865
7994
  );
7866
7995
  useEffect5(() => {
7867
7996
  if (!open) return;
@@ -7875,6 +8004,7 @@ function BookingDialog({
7875
8004
  setCode("");
7876
8005
  setBookingId(null);
7877
8006
  setError(null);
8007
+ setPreviewEnded(false);
7878
8008
  setAvatarFailed(false);
7879
8009
  setPageLoaded(false);
7880
8010
  setAsked(false);
@@ -7913,6 +8043,10 @@ function BookingDialog({
7913
8043
  async function startBooking() {
7914
8044
  var _a2, _b2;
7915
8045
  if (!selected) return;
8046
+ if (preview) {
8047
+ setPreviewEnded(true);
8048
+ return;
8049
+ }
7916
8050
  setBusy(true);
7917
8051
  setError(null);
7918
8052
  try {
@@ -8106,6 +8240,22 @@ function BookingDialog({
8106
8240
  ) : null,
8107
8241
  page.hostName && /* @__PURE__ */ jsx6("div", { style: { fontSize: 14, fontWeight: 500, ...muted }, children: page.hostName })
8108
8242
  ] }),
8243
+ preview && /* @__PURE__ */ jsx6(
8244
+ "div",
8245
+ {
8246
+ style: {
8247
+ alignSelf: "flex-start",
8248
+ padding: "3px 9px",
8249
+ borderRadius: 999,
8250
+ border: `1px dashed ${t.border}`,
8251
+ fontSize: 11.5,
8252
+ fontWeight: 600,
8253
+ letterSpacing: "0.02em",
8254
+ color: t.muted
8255
+ },
8256
+ children: "Preview \u2014 nothing is booked"
8257
+ }
8258
+ ),
8109
8259
  /* @__PURE__ */ jsx6("div", { style: { fontSize: 20, fontWeight: 600, lineHeight: 1.25, paddingRight: 32, overflowWrap: "anywhere" }, children: page.title }),
8110
8260
  page.description && /* @__PURE__ */ jsx6("div", { style: { fontSize: 13, lineHeight: 1.5, overflowWrap: "anywhere", ...muted }, children: page.description }),
8111
8261
  /* @__PURE__ */ jsxs6("div", { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4, fontSize: 14 }, children: [
@@ -8543,6 +8693,7 @@ function BookingDialog({
8543
8693
  },
8544
8694
  children: /* @__PURE__ */ jsxs6("div", { style: { maxWidth: 440, display: "flex", flexDirection: "column", gap: 14 }, children: [
8545
8695
  error && /* @__PURE__ */ jsx6("div", { style: { color: t.danger, fontSize: 13, lineHeight: 1.5 }, children: error }),
8696
+ previewEnded && /* @__PURE__ */ jsx6("div", { style: { fontSize: 13, lineHeight: 1.5, ...muted }, children: "End of the preview \u2014 a prospect would be booked here. Nothing was booked and no code was sent." }),
8546
8697
  step === "details" && /* @__PURE__ */ jsxs6(Fragment5, { children: [
8547
8698
  /* @__PURE__ */ jsx6(Field, { t, label: "Your name", value: name, onChange: setName, autoComplete: "name" }),
8548
8699
  /* @__PURE__ */ jsx6(Field, { t, label: "Email", value: email, onChange: setEmail, type: "email", autoComplete: "email" }),
@@ -9383,7 +9534,11 @@ function GlitchgrabProviderInner({
9383
9534
  [token, baseUrl, session]
9384
9535
  );
9385
9536
  const findSimilar = useCallback3(
9386
- (description) => findSimilarIssues(description, token, baseUrl, identity),
9537
+ (description, files) => findSimilarIssues(description, token, baseUrl, identity, files),
9538
+ [token, baseUrl, identity]
9539
+ );
9540
+ const readFile = useCallback3(
9541
+ (file) => readAttachment(file, token, baseUrl, identity),
9387
9542
  [token, baseUrl, identity]
9388
9543
  );
9389
9544
  const transcribe = useCallback3(
@@ -9414,8 +9569,9 @@ function GlitchgrabProviderInner({
9414
9569
  const [glitchgrabReport, setGlitchgrabReport] = useState6(false);
9415
9570
  const [signedUsers, setSignedUsers] = useState6("OFF");
9416
9571
  const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
9417
- const [bookingOpen, setBookingOpen] = useState6(false);
9418
- const openBookingDialog = useCallback3(() => setBookingOpen(true), []);
9572
+ const [booking, setBooking] = useState6("closed");
9573
+ const openBookingDialog = useCallback3(() => setBooking("live"), []);
9574
+ const openBookingPreview = useCallback3(() => setBooking("preview"), []);
9419
9575
  useEffect6(() => {
9420
9576
  let cancelled = false;
9421
9577
  fetch(`${baseUrl}/api/v1/sdk/project`, {
@@ -9490,6 +9646,7 @@ function GlitchgrabProviderInner({
9490
9646
  addBreadcrumb: addBreadcrumb2,
9491
9647
  openReportDialog,
9492
9648
  openBookingDialog,
9649
+ openBookingPreview,
9493
9650
  enhanceText: enhance,
9494
9651
  shortcutLabel
9495
9652
  },
@@ -9511,8 +9668,9 @@ function GlitchgrabProviderInner({
9511
9668
  {
9512
9669
  token,
9513
9670
  baseUrl: baseUrl != null ? baseUrl : DEFAULT_BASE_URL2,
9514
- open: bookingOpen,
9515
- onClose: () => setBookingOpen(false)
9671
+ open: booking !== "closed",
9672
+ preview: booking === "preview",
9673
+ onClose: () => setBooking("closed")
9516
9674
  }
9517
9675
  ),
9518
9676
  /* @__PURE__ */ jsx7(
@@ -9523,6 +9681,7 @@ function GlitchgrabProviderInner({
9523
9681
  enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
9524
9682
  assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
9525
9683
  findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
9684
+ readFile: aiAssist && !aiBlockedUnsigned ? readFile : void 0,
9526
9685
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
9527
9686
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9528
9687
  types,
@@ -9926,6 +10085,7 @@ function useGlitchgrabFeedback(query) {
9926
10085
  }
9927
10086
  export {
9928
10087
  BookDemoButton,
10088
+ BookingDialog,
9929
10089
  FeedbackButton,
9930
10090
  GLITCHGRAB_SHORTCUT2 as GLITCHGRAB_SHORTCUT,
9931
10091
  GLITCHGRAB_SHORTCUT_MAC2 as GLITCHGRAB_SHORTCUT_MAC,