glitchgrab 1.58.0 → 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.js CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
34
  BookDemoButton: () => BookDemoButton,
35
+ BookingDialog: () => BookingDialog,
35
36
  FeedbackButton: () => FeedbackButton,
36
37
  GLITCHGRAB_SHORTCUT: () => GLITCHGRAB_SHORTCUT2,
37
38
  GLITCHGRAB_SHORTCUT_MAC: () => GLITCHGRAB_SHORTCUT_MAC2,
@@ -647,6 +648,9 @@ function contextMetadata(context) {
647
648
  var DEFAULT_BASE_URL = "https://glitchgrab.dev";
648
649
  var MAX_RETRIES = 3;
649
650
  var RETRY_BASE_MS = 500;
651
+ function reportAttemptTimeoutMs(bodyLength) {
652
+ return 1e4 + Math.ceil(bodyLength / 1e5) * 1e3;
653
+ }
650
654
  async function sendReport(payload, baseUrl) {
651
655
  var _a;
652
656
  try {
@@ -657,13 +661,18 @@ async function sendReport(payload, baseUrl) {
657
661
  Authorization: `Bearer ${payload.token}`
658
662
  };
659
663
  const useKeepalive = body.length < 6e4;
664
+ const timeoutMs = reportAttemptTimeoutMs(body.length);
660
665
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
666
+ const isLast = attempt === MAX_RETRIES - 1;
667
+ const controller = !isLast && typeof AbortController === "function" ? new AbortController() : null;
668
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
661
669
  try {
662
670
  const response = await fetch(url, {
663
671
  method: "POST",
664
672
  headers,
665
673
  body,
666
- ...useKeepalive ? { keepalive: true } : {}
674
+ ...useKeepalive ? { keepalive: true } : {},
675
+ ...controller ? { signal: controller.signal } : {}
667
676
  });
668
677
  if (response.ok) {
669
678
  const envelope = await response.json();
@@ -673,6 +682,8 @@ async function sendReport(payload, baseUrl) {
673
682
  return null;
674
683
  }
675
684
  } catch (e) {
685
+ } finally {
686
+ if (timer) clearTimeout(timer);
676
687
  }
677
688
  if (attempt < MAX_RETRIES - 1) {
678
689
  await new Promise(
@@ -789,7 +800,7 @@ async function enhanceText(text, token, baseUrl, screenshot, context, identity)
789
800
  return text;
790
801
  }
791
802
  }
792
- async function findSimilarIssues(description, token, baseUrl, identity) {
803
+ async function findSimilarIssues(description, token, baseUrl, identity, files) {
793
804
  var _a;
794
805
  try {
795
806
  const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/reports/similar`;
@@ -799,7 +810,11 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
799
810
  "Content-Type": "application/json",
800
811
  Authorization: `Bearer ${token}`
801
812
  },
802
- body: JSON.stringify({ text: description, ...signedFields(identity) })
813
+ body: JSON.stringify({
814
+ text: description,
815
+ ...(files == null ? void 0 : files.length) ? { files } : {},
816
+ ...signedFields(identity)
817
+ })
803
818
  });
804
819
  const envelope = await response.json().catch(() => null);
805
820
  if (!response.ok || !(envelope == null ? void 0 : envelope.success)) return null;
@@ -809,6 +824,25 @@ async function findSimilarIssues(description, token, baseUrl, identity) {
809
824
  return null;
810
825
  }
811
826
  }
827
+ async function readAttachment(file, token, baseUrl, identity) {
828
+ var _a;
829
+ try {
830
+ const url = `${baseUrl != null ? baseUrl : DEFAULT_BASE_URL}/api/v1/ai/read-file`;
831
+ const response = await fetch(url, {
832
+ method: "POST",
833
+ headers: {
834
+ "Content-Type": "application/json",
835
+ Authorization: `Bearer ${token}`
836
+ },
837
+ body: JSON.stringify({ name: file.name, data: file.dataUrl, ...signedFields(identity) })
838
+ });
839
+ const envelope = await response.json().catch(() => null);
840
+ if (!response.ok || !(envelope == null ? void 0 : envelope.success) || !((_a = envelope.data) == null ? void 0 : _a.enabled)) return null;
841
+ return typeof envelope.data.text === "string" ? envelope.data.text : null;
842
+ } catch (e) {
843
+ return null;
844
+ }
845
+ }
812
846
  async function assistTurn(params, token, baseUrl) {
813
847
  const offline = assistOffline();
814
848
  try {
@@ -1410,6 +1444,9 @@ var MAX_ASSIST_IMAGE_CHARS = 35e5;
1410
1444
  var MAX_ASSIST_FILES = 3;
1411
1445
  var MAX_ASSIST_FILE_CHARS = 3e5;
1412
1446
  var MAX_ASSIST_FILE_CHARS_EACH = 2e5;
1447
+ var MAX_SIMILAR_FILE_CHARS = 2e4;
1448
+ var MAX_SIMILAR_FILES_TOTAL_CHARS = 4e4;
1449
+ var MAX_READABLE_PDF_BYTES = 3 * 1024 * 1024;
1413
1450
  var TEXT_ATTACHMENT_EXTENSIONS = [
1414
1451
  ".html",
1415
1452
  ".htm",
@@ -1440,6 +1477,10 @@ var ATTACHMENT_ACCEPT = [
1440
1477
  ...BINARY_ATTACHMENT_EXTENSIONS
1441
1478
  ].join(",");
1442
1479
  var ASSIST_ATTACHMENT_ACCEPT = ["image/*", ...TEXT_ATTACHMENT_EXTENSIONS].join(",");
1480
+ var ASSIST_ATTACHMENT_ACCEPT_WITH_PDF = [ASSIST_ATTACHMENT_ACCEPT, ".pdf"].join(",");
1481
+ function isPdfFile(name) {
1482
+ return name.toLowerCase().endsWith(".pdf");
1483
+ }
1443
1484
  function isAssistReadableFile(name) {
1444
1485
  const lower = name.toLowerCase();
1445
1486
  return TEXT_ATTACHMENT_EXTENSIONS.some((ext) => lower.endsWith(ext));
@@ -1460,6 +1501,27 @@ function decodeTextDataUrl(dataUrl) {
1460
1501
  return null;
1461
1502
  }
1462
1503
  }
1504
+ function heldFileKey(file) {
1505
+ return `${file.name}:${file.size}`;
1506
+ }
1507
+ function heldFileText(file) {
1508
+ if (isPdfFile(file.name)) return typeof file.text === "string" && file.text.trim() ? file.text : null;
1509
+ if (!isAssistReadableFile(file.name)) return null;
1510
+ const text = decodeTextDataUrl(file.dataUrl);
1511
+ return text && text.trim() ? text : null;
1512
+ }
1513
+ function filesForSimilar(files, each, total) {
1514
+ const picked = [];
1515
+ let used = 0;
1516
+ for (let i = files.length - 1; i >= 0 && used < total; i--) {
1517
+ const text = heldFileText(files[i]);
1518
+ if (!text) continue;
1519
+ const content = text.slice(0, Math.min(each, total - used));
1520
+ picked.unshift({ name: files[i].name, content });
1521
+ used += content.length;
1522
+ }
1523
+ return picked;
1524
+ }
1463
1525
  function getTypeLabel(type) {
1464
1526
  switch (type) {
1465
1527
  case "RATING":
@@ -1903,9 +1965,8 @@ function pickAssistFiles(files) {
1903
1965
  let total = 0;
1904
1966
  for (let i = files.length - 1; i >= 0 && picked.length < MAX_ASSIST_FILES; i--) {
1905
1967
  const attached = files[i];
1906
- if (!isAssistReadableFile(attached.name)) continue;
1907
- const text = decodeTextDataUrl(attached.dataUrl);
1908
- if (!text || !text.trim()) continue;
1968
+ const text = heldFileText(attached);
1969
+ if (!text) continue;
1909
1970
  const content = text.slice(0, MAX_ASSIST_FILE_CHARS_EACH);
1910
1971
  if (total + content.length > MAX_ASSIST_FILE_CHARS) continue;
1911
1972
  picked.unshift({ index: i, file: { name: attached.name, content } });
@@ -2002,6 +2063,7 @@ function AssistSheet({
2002
2063
  pageShot = null,
2003
2064
  onAddFiles,
2004
2065
  files = NO_FILES,
2066
+ canReadPdf = false,
2005
2067
  onRemoveFile,
2006
2068
  onRemoveImage,
2007
2069
  attachmentCount,
@@ -2029,6 +2091,8 @@ function AssistSheet({
2029
2091
  isSubmitting,
2030
2092
  submitted,
2031
2093
  onSend,
2094
+ similarSlot,
2095
+ submittedText,
2032
2096
  onDegrade,
2033
2097
  onClose,
2034
2098
  onFinish,
@@ -2052,6 +2116,7 @@ function AssistSheet({
2052
2116
  const [autoFileLeft, setAutoFileLeft] = (0, import_react3.useState)(null);
2053
2117
  const scrollRef = (0, import_react3.useRef)(null);
2054
2118
  const draftRef = (0, import_react3.useRef)(null);
2119
+ const composerRef = (0, import_react3.useRef)(null);
2055
2120
  const seededRef = (0, import_react3.useRef)(false);
2056
2121
  const seedPendingRef = (0, import_react3.useRef)(false);
2057
2122
  const eventsRef = (0, import_react3.useRef)([]);
@@ -2093,6 +2158,12 @@ function AssistSheet({
2093
2158
  var _a;
2094
2159
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
2095
2160
  }, [phase]);
2161
+ (0, import_react3.useEffect)(() => {
2162
+ const el = composerRef.current;
2163
+ if (!el) return;
2164
+ el.style.height = "auto";
2165
+ el.style.height = `${el.scrollHeight + 2}px`;
2166
+ }, [input]);
2096
2167
  (0, import_react3.useEffect)(() => {
2097
2168
  if (autoFileLeft === null) return;
2098
2169
  if (isSubmitting || submitted || phase !== "draft") {
@@ -2319,6 +2390,7 @@ function AssistSheet({
2319
2390
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
2320
2391
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
2321
2392
  @keyframes gg-autofile{from{transform:scaleX(0)}to{transform:scaleX(1)}}
2393
+ @keyframes gg-spin{to{transform:rotate(360deg)}}
2322
2394
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
2323
2395
  [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}
2324
2396
  ` }),
@@ -3193,7 +3265,7 @@ function AssistSheet({
3193
3265
  }
3194
3266
  )
3195
3267
  ] }),
3196
- submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3268
+ submitted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3197
3269
  "div",
3198
3270
  {
3199
3271
  style: {
@@ -3203,10 +3275,7 @@ function AssistSheet({
3203
3275
  fontSize: "14px",
3204
3276
  fontWeight: 600
3205
3277
  },
3206
- children: [
3207
- reportTypeLabel,
3208
- " sent. Thank you!"
3209
- ]
3278
+ children: submittedText != null ? submittedText : `${reportTypeLabel} sent. Thank you!`
3210
3279
  }
3211
3280
  )
3212
3281
  ]
@@ -3272,342 +3341,363 @@ function AssistSheet({
3272
3341
  // instead of remounting, and a draft is something you can
3273
3342
  // still reply to (#360), not a form that replaced the chat.
3274
3343
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3275
- (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3276
- "div",
3277
- {
3278
- style: {
3279
- display: "flex",
3280
- gap: "8px",
3281
- alignItems: "center",
3282
- flexWrap: "wrap",
3283
- marginBottom: "8px"
3284
- },
3285
- children: [
3286
- screenshots.map((shot, i) => {
3287
- const read = readShots.has(shot);
3288
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3289
- "div",
3290
- {
3291
- style: {
3292
- position: "relative",
3293
- width: "40px",
3294
- height: "40px",
3295
- flexShrink: 0
3296
- },
3297
- children: [
3298
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3299
- "img",
3300
- {
3301
- src: shot,
3302
- alt: read ? "Attached image the assistant reads" : "Attached image",
3303
- "data-gg-assist-thumb": "",
3304
- style: {
3305
- display: "block",
3306
- width: "100%",
3307
- height: "100%",
3308
- objectFit: "cover",
3309
- borderRadius: "8px",
3310
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3311
- opacity: read ? 1 : 0.45,
3312
- boxSizing: "border-box"
3344
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: similarSlot ? "none" : "contents" }, children: [
3345
+ (screenshots.length > 0 || files.length > 0 || onAddFiles) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3346
+ "div",
3347
+ {
3348
+ style: {
3349
+ display: "flex",
3350
+ gap: "8px",
3351
+ alignItems: "center",
3352
+ flexWrap: "wrap",
3353
+ marginBottom: "8px"
3354
+ },
3355
+ children: [
3356
+ screenshots.map((shot, i) => {
3357
+ const read = readShots.has(shot);
3358
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3359
+ "div",
3360
+ {
3361
+ style: {
3362
+ position: "relative",
3363
+ width: "40px",
3364
+ height: "40px",
3365
+ flexShrink: 0
3366
+ },
3367
+ children: [
3368
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3369
+ "img",
3370
+ {
3371
+ src: shot,
3372
+ alt: read ? "Attached image the assistant reads" : "Attached image",
3373
+ "data-gg-assist-thumb": "",
3374
+ style: {
3375
+ display: "block",
3376
+ width: "100%",
3377
+ height: "100%",
3378
+ objectFit: "cover",
3379
+ borderRadius: "8px",
3380
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
3381
+ opacity: read ? 1 : 0.45,
3382
+ boxSizing: "border-box"
3383
+ }
3313
3384
  }
3314
- }
3315
- ),
3316
- onRemoveImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3317
- "button",
3318
- {
3319
- type: "button",
3320
- "data-gg-assist-remove": "",
3321
- "aria-label": `Remove image ${i + 1}`,
3322
- title: "Remove this image",
3323
- onClick: () => {
3324
- stopAutoFile("changed attachments");
3325
- logEvent(
3326
- "image_removed",
3327
- pageShot && shot === pageShot ? "page capture" : "attached"
3328
- );
3329
- onRemoveImage(i);
3330
- },
3331
- style: {
3332
- position: "absolute",
3333
- top: "-13px",
3334
- right: "-13px",
3335
- width: "32px",
3336
- height: "32px",
3337
- border: "none",
3338
- background: "transparent",
3339
- padding: 0,
3340
- cursor: "pointer",
3341
- display: "flex",
3342
- alignItems: "center",
3343
- justifyContent: "center",
3344
- borderRadius: "50%"
3345
- },
3346
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3347
- "span",
3348
- {
3349
- "aria-hidden": "true",
3350
- style: {
3351
- width: "20px",
3352
- height: "20px",
3353
- boxSizing: "border-box",
3354
- borderRadius: "50%",
3355
- border: `2px solid ${t.bg}`,
3356
- background: "#ef4444",
3357
- color: "#fff",
3358
- display: "flex",
3359
- alignItems: "center",
3360
- justifyContent: "center"
3361
- },
3362
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3363
- "path",
3364
- {
3365
- d: "M1 1L9 9M9 1L1 9",
3366
- stroke: "currentColor",
3367
- strokeWidth: "1.6",
3368
- strokeLinecap: "round"
3369
- }
3370
- ) })
3385
+ ),
3386
+ onRemoveImage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3387
+ "button",
3388
+ {
3389
+ type: "button",
3390
+ "data-gg-assist-remove": "",
3391
+ "aria-label": `Remove image ${i + 1}`,
3392
+ title: "Remove this image",
3393
+ onClick: () => {
3394
+ stopAutoFile("changed attachments");
3395
+ logEvent(
3396
+ "image_removed",
3397
+ pageShot && shot === pageShot ? "page capture" : "attached"
3398
+ );
3399
+ onRemoveImage(i);
3400
+ },
3401
+ style: {
3402
+ position: "absolute",
3403
+ top: "-13px",
3404
+ right: "-13px",
3405
+ width: "32px",
3406
+ height: "32px",
3407
+ border: "none",
3408
+ background: "transparent",
3409
+ padding: 0,
3410
+ cursor: "pointer",
3411
+ display: "flex",
3412
+ alignItems: "center",
3413
+ justifyContent: "center",
3414
+ borderRadius: "50%"
3415
+ },
3416
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3417
+ "span",
3418
+ {
3419
+ "aria-hidden": "true",
3420
+ style: {
3421
+ width: "20px",
3422
+ height: "20px",
3423
+ boxSizing: "border-box",
3424
+ borderRadius: "50%",
3425
+ border: `2px solid ${t.bg}`,
3426
+ background: "#ef4444",
3427
+ color: "#fff",
3428
+ display: "flex",
3429
+ alignItems: "center",
3430
+ justifyContent: "center"
3431
+ },
3432
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3433
+ "path",
3434
+ {
3435
+ d: "M1 1L9 9M9 1L1 9",
3436
+ stroke: "currentColor",
3437
+ strokeWidth: "1.6",
3438
+ strokeLinecap: "round"
3439
+ }
3440
+ ) })
3441
+ }
3442
+ )
3443
+ }
3444
+ )
3445
+ ]
3446
+ },
3447
+ `${i}-${shot.slice(-16)}`
3448
+ );
3449
+ }),
3450
+ files.map((file, i) => {
3451
+ const read = readFiles.has(i);
3452
+ const pdf = isPdfFile(file.name);
3453
+ 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";
3454
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3455
+ "div",
3456
+ {
3457
+ "data-gg-assist-file": "",
3458
+ "data-gg-assist-file-read": read ? "" : void 0,
3459
+ "data-gg-assist-file-reading": pdf && file.reading ? "" : void 0,
3460
+ title: `${file.name} \u2014 ${why}`,
3461
+ style: {
3462
+ position: "relative",
3463
+ height: "40px",
3464
+ maxWidth: "170px",
3465
+ display: "flex",
3466
+ alignItems: "center",
3467
+ gap: "6px",
3468
+ padding: "0 10px",
3469
+ borderRadius: "8px",
3470
+ border: `1px solid ${read ? t.accent : t.inputBorder}`,
3471
+ opacity: read || pdf && file.reading ? 1 : 0.55,
3472
+ boxSizing: "border-box",
3473
+ flexShrink: 0
3474
+ },
3475
+ children: [
3476
+ pdf && file.reading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3477
+ "span",
3478
+ {
3479
+ role: "status",
3480
+ "aria-label": "Reading this PDF",
3481
+ style: {
3482
+ width: "12px",
3483
+ height: "12px",
3484
+ flexShrink: 0,
3485
+ borderRadius: "50%",
3486
+ border: `1.5px solid ${t.inputBorder}`,
3487
+ borderTopColor: t.accent,
3488
+ boxSizing: "border-box",
3489
+ animation: "gg-spin .8s linear infinite"
3371
3490
  }
3372
- )
3373
- }
3374
- )
3375
- ]
3376
- },
3377
- `${i}-${shot.slice(-16)}`
3378
- );
3379
- }),
3380
- files.map((file, i) => {
3381
- const read = readFiles.has(i);
3382
- 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";
3383
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3384
- "div",
3491
+ }
3492
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3493
+ "path",
3494
+ {
3495
+ d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
3496
+ stroke: t.textMuted,
3497
+ strokeWidth: "1.5",
3498
+ strokeLinecap: "round",
3499
+ strokeLinejoin: "round"
3500
+ }
3501
+ ) }),
3502
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3503
+ "span",
3504
+ {
3505
+ style: {
3506
+ fontSize: "11.5px",
3507
+ color: t.text,
3508
+ overflow: "hidden",
3509
+ textOverflow: "ellipsis",
3510
+ whiteSpace: "nowrap"
3511
+ },
3512
+ children: file.name
3513
+ }
3514
+ ),
3515
+ onRemoveFile && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3516
+ "button",
3517
+ {
3518
+ type: "button",
3519
+ "data-gg-assist-remove-file": "",
3520
+ "aria-label": `Remove file ${file.name}`,
3521
+ title: "Remove this file",
3522
+ onClick: () => {
3523
+ stopAutoFile("changed attachments");
3524
+ logEvent("file_removed", read ? "read" : "not read");
3525
+ onRemoveFile(i);
3526
+ },
3527
+ style: {
3528
+ position: "absolute",
3529
+ top: "-13px",
3530
+ right: "-13px",
3531
+ width: "32px",
3532
+ height: "32px",
3533
+ border: "none",
3534
+ background: "transparent",
3535
+ padding: 0,
3536
+ cursor: "pointer",
3537
+ display: "flex",
3538
+ alignItems: "center",
3539
+ justifyContent: "center",
3540
+ borderRadius: "50%"
3541
+ },
3542
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3543
+ "span",
3544
+ {
3545
+ "aria-hidden": "true",
3546
+ style: {
3547
+ width: "20px",
3548
+ height: "20px",
3549
+ boxSizing: "border-box",
3550
+ borderRadius: "50%",
3551
+ border: `2px solid ${t.bg}`,
3552
+ background: "#ef4444",
3553
+ color: "#fff",
3554
+ display: "flex",
3555
+ alignItems: "center",
3556
+ justifyContent: "center"
3557
+ },
3558
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3559
+ "path",
3560
+ {
3561
+ d: "M1 1L9 9M9 1L1 9",
3562
+ stroke: "currentColor",
3563
+ strokeWidth: "1.6",
3564
+ strokeLinecap: "round"
3565
+ }
3566
+ ) })
3567
+ }
3568
+ )
3569
+ }
3570
+ )
3571
+ ]
3572
+ },
3573
+ `file-${i}-${file.name}`
3574
+ );
3575
+ }),
3576
+ onAddFiles && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3577
+ "label",
3385
3578
  {
3386
- "data-gg-assist-file": "",
3387
- "data-gg-assist-file-read": read ? "" : void 0,
3388
- title: `${file.name} \u2014 ${why}`,
3579
+ "data-gg-assist-attach": "",
3580
+ title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3389
3581
  style: {
3390
- position: "relative",
3582
+ width: "40px",
3391
3583
  height: "40px",
3392
- maxWidth: "170px",
3393
- display: "flex",
3394
- alignItems: "center",
3395
- gap: "6px",
3396
- padding: "0 10px",
3397
3584
  borderRadius: "8px",
3398
- border: `1px solid ${read ? t.accent : t.inputBorder}`,
3399
- opacity: read ? 1 : 0.55,
3400
- boxSizing: "border-box",
3585
+ border: `1px dashed ${t.inputBorder}`,
3586
+ display: "inline-flex",
3587
+ alignItems: "center",
3588
+ justifyContent: "center",
3589
+ color: t.textMuted,
3590
+ fontSize: "18px",
3591
+ cursor: "pointer",
3401
3592
  flexShrink: 0
3402
3593
  },
3403
3594
  children: [
3404
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3405
- "path",
3406
- {
3407
- d: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6",
3408
- stroke: t.textMuted,
3409
- strokeWidth: "1.5",
3410
- strokeLinecap: "round",
3411
- strokeLinejoin: "round"
3412
- }
3413
- ) }),
3595
+ "+",
3414
3596
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3415
- "span",
3416
- {
3417
- style: {
3418
- fontSize: "11.5px",
3419
- color: t.text,
3420
- overflow: "hidden",
3421
- textOverflow: "ellipsis",
3422
- whiteSpace: "nowrap"
3423
- },
3424
- children: file.name
3425
- }
3426
- ),
3427
- onRemoveFile && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3428
- "button",
3597
+ "input",
3429
3598
  {
3430
- type: "button",
3431
- "data-gg-assist-remove-file": "",
3432
- "aria-label": `Remove file ${file.name}`,
3433
- title: "Remove this file",
3434
- onClick: () => {
3435
- stopAutoFile("changed attachments");
3436
- logEvent("file_removed", read ? "read" : "not read");
3437
- onRemoveFile(i);
3438
- },
3439
- style: {
3440
- position: "absolute",
3441
- top: "-13px",
3442
- right: "-13px",
3443
- width: "32px",
3444
- height: "32px",
3445
- border: "none",
3446
- background: "transparent",
3447
- padding: 0,
3448
- cursor: "pointer",
3449
- display: "flex",
3450
- alignItems: "center",
3451
- justifyContent: "center",
3452
- borderRadius: "50%"
3453
- },
3454
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3455
- "span",
3456
- {
3457
- "aria-hidden": "true",
3458
- style: {
3459
- width: "20px",
3460
- height: "20px",
3461
- boxSizing: "border-box",
3462
- borderRadius: "50%",
3463
- border: `2px solid ${t.bg}`,
3464
- background: "#ef4444",
3465
- color: "#fff",
3466
- display: "flex",
3467
- alignItems: "center",
3468
- justifyContent: "center"
3469
- },
3470
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3471
- "path",
3472
- {
3473
- d: "M1 1L9 9M9 1L1 9",
3474
- stroke: "currentColor",
3475
- strokeWidth: "1.6",
3476
- strokeLinecap: "round"
3477
- }
3478
- ) })
3599
+ type: "file",
3600
+ accept: canReadPdf ? ASSIST_ATTACHMENT_ACCEPT_WITH_PDF : ASSIST_ATTACHMENT_ACCEPT,
3601
+ multiple: true,
3602
+ onChange: (e) => {
3603
+ var _a;
3604
+ const picked = Array.from((_a = e.target.files) != null ? _a : []);
3605
+ if (picked.length) {
3606
+ stopAutoFile("changed attachments");
3607
+ const images = picked.filter((f) => f.type.startsWith("image/")).length;
3608
+ if (images) logEvent("image_added", String(images));
3609
+ if (picked.length > images) {
3610
+ logEvent("file_added", String(picked.length - images));
3611
+ }
3612
+ onAddFiles(picked);
3479
3613
  }
3480
- )
3614
+ e.target.value = "";
3615
+ },
3616
+ style: { display: "none" }
3481
3617
  }
3482
3618
  )
3483
3619
  ]
3484
- },
3485
- `file-${i}-${file.name}`
3486
- );
3487
- }),
3488
- onAddFiles && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3489
- "label",
3490
- {
3491
- "data-gg-assist-attach": "",
3492
- title: "Attach an image or a file (html, json, log\u2026) \u2014 or paste one",
3493
- style: {
3494
- width: "40px",
3495
- height: "40px",
3496
- borderRadius: "8px",
3497
- border: `1px dashed ${t.inputBorder}`,
3498
- display: "inline-flex",
3499
- alignItems: "center",
3500
- justifyContent: "center",
3501
- color: t.textMuted,
3502
- fontSize: "18px",
3503
- cursor: "pointer",
3504
- flexShrink: 0
3505
- },
3506
- children: [
3507
- "+",
3508
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3509
- "input",
3510
- {
3511
- type: "file",
3512
- accept: ASSIST_ATTACHMENT_ACCEPT,
3513
- multiple: true,
3514
- onChange: (e) => {
3515
- var _a;
3516
- const picked = Array.from((_a = e.target.files) != null ? _a : []);
3517
- if (picked.length) {
3518
- stopAutoFile("changed attachments");
3519
- const images = picked.filter((f) => f.type.startsWith("image/")).length;
3520
- if (images) logEvent("image_added", String(images));
3521
- if (picked.length > images) {
3522
- logEvent("file_added", String(picked.length - images));
3523
- }
3524
- onAddFiles(picked);
3525
- }
3526
- e.target.value = "";
3527
- },
3528
- style: { display: "none" }
3529
- }
3530
- )
3531
- ]
3532
- }
3533
- ),
3534
- readShots.size < screenshots.length && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
3535
- readShots.size,
3536
- " of ",
3537
- screenshots.length,
3538
- " read"
3539
- ] })
3540
- ]
3541
- }
3542
- ),
3543
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
3544
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3545
- "textarea",
3546
- {
3547
- value: input,
3548
- onChange: (e) => {
3549
- stopAutoFile("typing a change");
3550
- setInput(e.target.value);
3551
- },
3552
- onKeyDown: (e) => {
3553
- if (e.key === "Enter" && !e.shiftKey) {
3554
- e.preventDefault();
3555
- sendTyped(input);
3556
- }
3557
- },
3558
- placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
3559
- rows: 1,
3560
- disabled: busy,
3561
- autoFocus: true,
3562
- style: {
3563
- flex: 1,
3564
- minHeight: "42px",
3565
- maxHeight: "120px",
3566
- padding: "11px 12px",
3567
- borderRadius: "10px",
3568
- border: `1px solid ${t.inputBorder}`,
3569
- backgroundColor: t.inputBg,
3570
- color: t.text,
3571
- fontSize: "13px",
3572
- lineHeight: 1.4,
3573
- fontFamily: "inherit",
3574
- resize: "none",
3575
- outline: "none",
3576
- boxSizing: "border-box"
3577
- }
3620
+ }
3621
+ ),
3622
+ readShots.size < screenshots.length && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { color: t.textMuted, fontSize: "11px" }, children: [
3623
+ readShots.size,
3624
+ " of ",
3625
+ screenshots.length,
3626
+ " read"
3627
+ ] })
3628
+ ]
3578
3629
  }
3579
3630
  ),
3580
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3581
- "button",
3582
- {
3583
- type: "button",
3584
- "data-gg-sheet-send": "",
3585
- onClick: () => sendTyped(input),
3586
- disabled: busy || !input.trim(),
3587
- "aria-label": "Send message",
3588
- "data-gg-send-message": "",
3589
- style: {
3590
- flexShrink: 0,
3591
- width: "42px",
3592
- height: "42px",
3593
- borderRadius: "10px",
3594
- border: "none",
3595
- display: "inline-flex",
3596
- alignItems: "center",
3597
- justifyContent: "center",
3598
- backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
3599
- color: busy || !input.trim() ? t.textMuted : t.accentText,
3600
- cursor: busy || !input.trim() ? "default" : "pointer"
3601
- },
3602
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3603
- "path",
3604
- {
3605
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
3606
- fill: "currentColor"
3631
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
3632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3633
+ "textarea",
3634
+ {
3635
+ ref: composerRef,
3636
+ value: input,
3637
+ onChange: (e) => {
3638
+ stopAutoFile("typing a change");
3639
+ setInput(e.target.value);
3640
+ },
3641
+ onKeyDown: (e) => {
3642
+ if (e.key === "Enter" && !e.shiftKey) {
3643
+ e.preventDefault();
3644
+ sendTyped(input);
3645
+ }
3646
+ },
3647
+ placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
3648
+ rows: 3,
3649
+ disabled: busy,
3650
+ autoFocus: true,
3651
+ style: {
3652
+ flex: 1,
3653
+ minHeight: "72px",
3654
+ maxHeight: "220px",
3655
+ padding: "11px 12px",
3656
+ borderRadius: "10px",
3657
+ border: `1px solid ${t.inputBorder}`,
3658
+ backgroundColor: t.inputBg,
3659
+ color: t.text,
3660
+ fontSize: "13px",
3661
+ lineHeight: 1.4,
3662
+ fontFamily: "inherit",
3663
+ resize: "none",
3664
+ outline: "none",
3665
+ boxSizing: "border-box"
3607
3666
  }
3608
- ) })
3609
- }
3610
- )
3667
+ }
3668
+ ),
3669
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3670
+ "button",
3671
+ {
3672
+ type: "button",
3673
+ "data-gg-sheet-send": "",
3674
+ onClick: () => sendTyped(input),
3675
+ disabled: busy || !input.trim(),
3676
+ "aria-label": "Send message",
3677
+ "data-gg-send-message": "",
3678
+ style: {
3679
+ flexShrink: 0,
3680
+ width: "42px",
3681
+ height: "42px",
3682
+ borderRadius: "10px",
3683
+ border: "none",
3684
+ display: "inline-flex",
3685
+ alignItems: "center",
3686
+ justifyContent: "center",
3687
+ backgroundColor: busy || !input.trim() ? t.bgSecondary : t.accent,
3688
+ color: busy || !input.trim() ? t.textMuted : t.accentText,
3689
+ cursor: busy || !input.trim() ? "default" : "pointer"
3690
+ },
3691
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3692
+ "path",
3693
+ {
3694
+ d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
3695
+ fill: "currentColor"
3696
+ }
3697
+ ) })
3698
+ }
3699
+ )
3700
+ ] })
3611
3701
  ] }),
3612
3702
  phase === "draft" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3613
3703
  validationError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -3621,7 +3711,7 @@ function AssistSheet({
3621
3711
  children: validationError
3622
3712
  }
3623
3713
  ),
3624
- autoFileLeft !== null ? (
3714
+ similarSlot ? similarSlot : autoFileLeft !== null ? (
3625
3715
  // #402: "Yes, that's it" was the confirmation. The draft
3626
3716
  // files itself unless they stop it — the bar shows how
3627
3717
  // long is left, the button sends now, the × on its right
@@ -4677,6 +4767,7 @@ function ReportDialog({
4677
4767
  enhanceText: enhanceText2,
4678
4768
  assist,
4679
4769
  findSimilarIssues: findSimilarIssues2,
4770
+ readFile,
4680
4771
  assistContext = null,
4681
4772
  transcribeAudio: transcribeAudio2,
4682
4773
  types,
@@ -4730,6 +4821,15 @@ function ReportDialog({
4730
4821
  const pendingShotRef = (0, import_react.useRef)(null);
4731
4822
  const openGenRef = (0, import_react.useRef)(0);
4732
4823
  const [attachments, setAttachments] = (0, import_react.useState)([]);
4824
+ const [pdfTexts, setPdfTexts] = (0, import_react.useState)({});
4825
+ const heldFiles = (0, import_react.useMemo)(
4826
+ () => attachments.map((a) => {
4827
+ const read = pdfTexts[heldFileKey(a)];
4828
+ if (!read) return a;
4829
+ return "reading" in read ? { ...a, reading: true } : { ...a, text: read.text, ...read.tooLarge ? { tooLarge: true } : {} };
4830
+ }),
4831
+ [attachments, pdfTexts]
4832
+ );
4733
4833
  const [previewIndex, setPreviewIndex] = (0, import_react.useState)(null);
4734
4834
  const [annotatingIndex, setAnnotatingIndex] = (0, import_react.useState)(null);
4735
4835
  const [previewZoomed, setPreviewZoomed] = (0, import_react.useState)(false);
@@ -4890,10 +4990,16 @@ function ReportDialog({
4890
4990
  files.filter((file) => !file.type.startsWith("image/")).forEach((file) => {
4891
4991
  const reader = new FileReader();
4892
4992
  reader.onload = () => {
4893
- setAttachments((prev) => [
4894
- ...prev,
4895
- { name: file.name, size: file.size, dataUrl: reader.result }
4896
- ]);
4993
+ const dataUrl = reader.result;
4994
+ setAttachments((prev) => [...prev, { name: file.name, size: file.size, dataUrl }]);
4995
+ const key = heldFileKey(file);
4996
+ if (!readFile || !isPdfFile(file.name) || pdfTexts[key]) return;
4997
+ if (file.size > MAX_READABLE_PDF_BYTES) {
4998
+ setPdfTexts((prev) => ({ ...prev, [key]: { text: null, tooLarge: true } }));
4999
+ return;
5000
+ }
5001
+ setPdfTexts((prev) => ({ ...prev, [key]: { reading: true } }));
5002
+ void Promise.resolve().then(() => readFile({ name: file.name, dataUrl })).catch(() => null).then((text) => setPdfTexts((prev) => ({ ...prev, [key]: { text } })));
4897
5003
  };
4898
5004
  reader.readAsDataURL(file);
4899
5005
  });
@@ -5329,6 +5435,10 @@ function ReportDialog({
5329
5435
  stopListeningAndTranscribe();
5330
5436
  }
5331
5437
  };
5438
+ (0, import_react.useEffect)(() => {
5439
+ setSimilarIssues(null);
5440
+ setPickedSimilar(null);
5441
+ }, [description]);
5332
5442
  const handleSubmit = async (opts) => {
5333
5443
  var _a2, _b, _c;
5334
5444
  try {
@@ -5369,11 +5479,17 @@ function ReportDialog({
5369
5479
  }
5370
5480
  const duplicateOf = (_a2 = opts == null ? void 0 : opts.duplicateOf) != null ? _a2 : duplicateIssueNumber;
5371
5481
  const attachKind = (opts == null ? void 0 : opts.duplicateOf) ? (_b = opts.kind) != null ? _b : "duplicate" : duplicateKind;
5372
- if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf && !aiConversationId) {
5482
+ if (findSimilarIssues2 && !(opts == null ? void 0 : opts.skipSimilar) && !duplicateOf) {
5483
+ const similarFiles = filesForSimilar(
5484
+ heldFiles,
5485
+ MAX_SIMILAR_FILE_CHARS,
5486
+ MAX_SIMILAR_FILES_TOTAL_CHARS
5487
+ );
5373
5488
  setIsSubmitting(true);
5374
5489
  setCheckingSimilar(true);
5375
5490
  const matches = await Promise.race([
5376
- findSimilarIssues2(description.trim()).catch(() => null),
5491
+ // A PDF still being read is left out rather than waited for.
5492
+ (similarFiles.length ? findSimilarIssues2(description.trim(), similarFiles) : findSimilarIssues2(description.trim())).catch(() => null),
5377
5493
  new Promise((resolve) => setTimeout(() => resolve(null), SIMILAR_WAIT_MS))
5378
5494
  ]);
5379
5495
  setCheckingSimilar(false);
@@ -5405,6 +5521,8 @@ function ReportDialog({
5405
5521
  if (duplicateOf) {
5406
5522
  metadata.duplicateIssueNumber = String(duplicateOf);
5407
5523
  if (attachKind === "related") metadata.duplicateKind = "related";
5524
+ } else if (opts == null ? void 0 : opts.possiblyRelated) {
5525
+ metadata.possiblyRelatedIssue = String(opts.possiblyRelated);
5408
5526
  }
5409
5527
  if (aiConversationId) {
5410
5528
  metadata.aiConversationId = aiConversationId;
@@ -5418,7 +5536,9 @@ function ReportDialog({
5418
5536
  Object.keys(metadata).length > 0 ? metadata : void 0
5419
5537
  );
5420
5538
  if (result) {
5421
- setAddedToIssue(duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : null);
5539
+ setAddedToIssue(
5540
+ duplicateOf && result.issueNumber === duplicateOf ? duplicateOf : result.status === "DUPLICATE" && result.issueNumber ? result.issueNumber : null
5541
+ );
5422
5542
  setAddedAsRelated(!!duplicateOf && result.issueNumber === duplicateOf && attachKind === "related");
5423
5543
  const queued = result.status === "QUEUED" && !!result.message;
5424
5544
  setQueuedMessage(queued ? (_c = result.message) != null ? _c : null : null);
@@ -5426,6 +5546,7 @@ function ReportDialog({
5426
5546
  setDescription("");
5427
5547
  setScreenshots([]);
5428
5548
  setAttachments([]);
5549
+ setPdfTexts({});
5429
5550
  setAiConversationId(null);
5430
5551
  setIssueTitle("");
5431
5552
  setDuplicateIssueNumber(null);
@@ -5446,6 +5567,220 @@ function ReportDialog({
5446
5567
  setAttachingTo(null);
5447
5568
  }
5448
5569
  };
5570
+ const similarCard = similarIssues ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
5571
+ "div",
5572
+ {
5573
+ "data-gg-similar": "",
5574
+ style: {
5575
+ marginTop: "12px",
5576
+ display: "flex",
5577
+ flexDirection: "column",
5578
+ gap: "10px",
5579
+ padding: "12px",
5580
+ borderRadius: "10px",
5581
+ border: "1px solid rgba(245,158,11,0.4)",
5582
+ backgroundColor: "rgba(245,158,11,0.08)"
5583
+ },
5584
+ children: [
5585
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
5586
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
5587
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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." })
5588
+ ] }),
5589
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5590
+ "div",
5591
+ {
5592
+ role: "radiogroup",
5593
+ "aria-label": "Existing issues",
5594
+ style: { display: "flex", flexDirection: "column", gap: "6px" },
5595
+ children: similarIssues.map((issue) => {
5596
+ const picked = pickedSimilar === issue.number;
5597
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
5598
+ "button",
5599
+ {
5600
+ type: "button",
5601
+ role: "radio",
5602
+ "aria-checked": picked,
5603
+ disabled: isSubmitting,
5604
+ onClick: () => setPickedSimilar(issue.number),
5605
+ style: {
5606
+ display: "flex",
5607
+ alignItems: "flex-start",
5608
+ gap: "10px",
5609
+ width: "100%",
5610
+ minHeight: "44px",
5611
+ padding: "9px 10px",
5612
+ borderRadius: "8px",
5613
+ border: `1px solid ${picked ? t.accent : t.border}`,
5614
+ boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
5615
+ backgroundColor: t.bg,
5616
+ color: t.text,
5617
+ textAlign: "left",
5618
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5619
+ fontFamily: "inherit"
5620
+ },
5621
+ children: [
5622
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5623
+ "span",
5624
+ {
5625
+ "aria-hidden": "true",
5626
+ style: {
5627
+ flexShrink: 0,
5628
+ marginTop: "2px",
5629
+ width: "14px",
5630
+ height: "14px",
5631
+ boxSizing: "border-box",
5632
+ borderRadius: "50%",
5633
+ border: `2px solid ${picked ? t.accent : t.textMuted}`,
5634
+ backgroundColor: picked ? t.accent : "transparent",
5635
+ boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
5636
+ }
5637
+ }
5638
+ ),
5639
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
5640
+ "span",
5641
+ {
5642
+ style: {
5643
+ display: "flex",
5644
+ flexDirection: "column",
5645
+ gap: "3px",
5646
+ minWidth: 0
5647
+ },
5648
+ children: [
5649
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
5650
+ "span",
5651
+ {
5652
+ title: issue.title,
5653
+ style: {
5654
+ fontSize: "12.5px",
5655
+ lineHeight: 1.45,
5656
+ wordBreak: "break-word",
5657
+ overflow: "hidden",
5658
+ display: "-webkit-box",
5659
+ WebkitLineClamp: 3,
5660
+ WebkitBoxOrient: "vertical"
5661
+ },
5662
+ children: [
5663
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
5664
+ "#",
5665
+ issue.number
5666
+ ] }),
5667
+ " ",
5668
+ issue.title
5669
+ ]
5670
+ }
5671
+ ),
5672
+ issue.status && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
5673
+ ]
5674
+ }
5675
+ )
5676
+ ]
5677
+ },
5678
+ issue.number
5679
+ );
5680
+ })
5681
+ }
5682
+ ),
5683
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5684
+ "button",
5685
+ {
5686
+ type: "button",
5687
+ disabled: isSubmitting || !pickedSimilar,
5688
+ onClick: () => {
5689
+ if (!pickedSimilar) return;
5690
+ setAttachingTo(pickedSimilar);
5691
+ setDuplicateIssueNumber(pickedSimilar);
5692
+ setDuplicateKind("duplicate");
5693
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
5694
+ },
5695
+ style: {
5696
+ width: "100%",
5697
+ minHeight: "40px",
5698
+ padding: "10px",
5699
+ borderRadius: "8px",
5700
+ border: "none",
5701
+ backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
5702
+ color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
5703
+ fontSize: "14px",
5704
+ fontWeight: 600,
5705
+ cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
5706
+ fontFamily: "inherit"
5707
+ },
5708
+ children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5709
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
5710
+ "Adding to #",
5711
+ attachingTo,
5712
+ "\u2026"
5713
+ ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
5714
+ }
5715
+ ),
5716
+ pickedSimilar && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5717
+ "button",
5718
+ {
5719
+ type: "button",
5720
+ disabled: isSubmitting,
5721
+ onClick: () => {
5722
+ setAttachingTo(pickedSimilar);
5723
+ setDuplicateIssueNumber(pickedSimilar);
5724
+ setDuplicateKind("related");
5725
+ void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
5726
+ },
5727
+ style: {
5728
+ width: "100%",
5729
+ minHeight: "40px",
5730
+ marginTop: "-4px",
5731
+ padding: "9px 10px",
5732
+ borderRadius: "8px",
5733
+ border: "1px solid rgba(245,158,11,0.4)",
5734
+ backgroundColor: "transparent",
5735
+ color: isSubmitting ? t.textMuted : t.text,
5736
+ fontSize: "13px",
5737
+ fontWeight: 600,
5738
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5739
+ fontFamily: "inherit"
5740
+ },
5741
+ children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5742
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
5743
+ "Adding to #",
5744
+ attachingTo,
5745
+ "\u2026"
5746
+ ] }) : `Related request \u2014 add to #${pickedSimilar}`
5747
+ }
5748
+ ),
5749
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5750
+ "button",
5751
+ {
5752
+ type: "button",
5753
+ disabled: isSubmitting,
5754
+ onClick: () => {
5755
+ var _a2;
5756
+ setAttachingTo("new");
5757
+ void handleSubmit({ skipSimilar: true, possiblyRelated: (_a2 = similarIssues[0]) == null ? void 0 : _a2.number });
5758
+ },
5759
+ style: {
5760
+ width: "100%",
5761
+ minHeight: "40px",
5762
+ marginTop: "-4px",
5763
+ padding: "8px",
5764
+ borderRadius: "8px",
5765
+ border: "none",
5766
+ backgroundColor: "transparent",
5767
+ color: isSubmitting ? t.textMuted : t.text,
5768
+ fontSize: "13px",
5769
+ fontWeight: 500,
5770
+ textDecoration: "underline",
5771
+ textUnderlineOffset: "3px",
5772
+ cursor: isSubmitting ? "not-allowed" : "pointer",
5773
+ fontFamily: "inherit"
5774
+ },
5775
+ children: attachingTo === "new" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5776
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
5777
+ "Sending as new\u2026"
5778
+ ] }) : "No, mine is different \u2014 send as new"
5779
+ }
5780
+ )
5781
+ ]
5782
+ }
5783
+ ) : null;
5449
5784
  if (!mounted) return null;
5450
5785
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5451
5786
  isOpen && (0, import_react_dom.createPortal)(
@@ -6984,224 +7319,7 @@ function ReportDialog({
6984
7319
  children: validationError
6985
7320
  }
6986
7321
  ),
6987
- similarIssues ? (
6988
- // Same visual language as the assistant's "Our team is
6989
- // already on this" card, so both routes to a duplicate
6990
- // read as one feature.
6991
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
6992
- "div",
6993
- {
6994
- "data-gg-similar": "",
6995
- style: {
6996
- marginTop: "12px",
6997
- display: "flex",
6998
- flexDirection: "column",
6999
- gap: "10px",
7000
- padding: "12px",
7001
- borderRadius: "10px",
7002
- border: "1px solid rgba(245,158,11,0.4)",
7003
- backgroundColor: "rgba(245,158,11,0.08)"
7004
- },
7005
- children: [
7006
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
7007
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: t.text, fontWeight: 600, fontSize: "13px" }, children: "Our team may already have this, or something close" }),
7008
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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." })
7009
- ] }),
7010
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7011
- "div",
7012
- {
7013
- role: "radiogroup",
7014
- "aria-label": "Existing issues",
7015
- style: { display: "flex", flexDirection: "column", gap: "6px" },
7016
- children: similarIssues.map((issue) => {
7017
- const picked = pickedSimilar === issue.number;
7018
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
7019
- "button",
7020
- {
7021
- type: "button",
7022
- role: "radio",
7023
- "aria-checked": picked,
7024
- disabled: isSubmitting,
7025
- onClick: () => setPickedSimilar(issue.number),
7026
- style: {
7027
- display: "flex",
7028
- alignItems: "flex-start",
7029
- gap: "10px",
7030
- width: "100%",
7031
- minHeight: "44px",
7032
- padding: "9px 10px",
7033
- borderRadius: "8px",
7034
- border: `1px solid ${picked ? t.accent : t.border}`,
7035
- boxShadow: picked ? `0 0 0 1px ${t.accent}` : "none",
7036
- backgroundColor: t.bg,
7037
- color: t.text,
7038
- textAlign: "left",
7039
- cursor: isSubmitting ? "not-allowed" : "pointer",
7040
- fontFamily: "inherit"
7041
- },
7042
- children: [
7043
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7044
- "span",
7045
- {
7046
- "aria-hidden": "true",
7047
- style: {
7048
- flexShrink: 0,
7049
- marginTop: "2px",
7050
- width: "14px",
7051
- height: "14px",
7052
- boxSizing: "border-box",
7053
- borderRadius: "50%",
7054
- border: `2px solid ${picked ? t.accent : t.textMuted}`,
7055
- backgroundColor: picked ? t.accent : "transparent",
7056
- boxShadow: picked ? `inset 0 0 0 2px ${t.bg}` : "none"
7057
- }
7058
- }
7059
- ),
7060
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
7061
- "span",
7062
- {
7063
- style: {
7064
- display: "flex",
7065
- flexDirection: "column",
7066
- gap: "3px",
7067
- minWidth: 0
7068
- },
7069
- children: [
7070
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
7071
- "span",
7072
- {
7073
- title: issue.title,
7074
- style: {
7075
- fontSize: "12.5px",
7076
- lineHeight: 1.45,
7077
- wordBreak: "break-word",
7078
- overflow: "hidden",
7079
- display: "-webkit-box",
7080
- WebkitLineClamp: 3,
7081
- WebkitBoxOrient: "vertical"
7082
- },
7083
- children: [
7084
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { color: t.textMuted, fontWeight: 600 }, children: [
7085
- "#",
7086
- issue.number
7087
- ] }),
7088
- " ",
7089
- issue.title
7090
- ]
7091
- }
7092
- ),
7093
- issue.status && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "11.5px", color: t.textMuted }, children: issue.status })
7094
- ]
7095
- }
7096
- )
7097
- ]
7098
- },
7099
- issue.number
7100
- );
7101
- })
7102
- }
7103
- ),
7104
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7105
- "button",
7106
- {
7107
- type: "button",
7108
- disabled: isSubmitting || !pickedSimilar,
7109
- onClick: () => {
7110
- if (!pickedSimilar) return;
7111
- setAttachingTo(pickedSimilar);
7112
- setDuplicateIssueNumber(pickedSimilar);
7113
- setDuplicateKind("duplicate");
7114
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "duplicate" });
7115
- },
7116
- style: {
7117
- width: "100%",
7118
- minHeight: "40px",
7119
- padding: "10px",
7120
- borderRadius: "8px",
7121
- border: "none",
7122
- backgroundColor: isSubmitting || !pickedSimilar ? t.bgSecondary : t.accent,
7123
- color: isSubmitting || !pickedSimilar ? t.textMuted : t.accentText,
7124
- fontSize: "14px",
7125
- fontWeight: 600,
7126
- cursor: isSubmitting || !pickedSimilar ? "not-allowed" : "pointer",
7127
- fontFamily: "inherit"
7128
- },
7129
- children: typeof attachingTo === "number" && duplicateKind !== "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
7130
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
7131
- "Adding to #",
7132
- attachingTo,
7133
- "\u2026"
7134
- ] }) : pickedSimilar ? `Add to #${pickedSimilar}` : "Pick the matching issue"
7135
- }
7136
- ),
7137
- pickedSimilar && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7138
- "button",
7139
- {
7140
- type: "button",
7141
- disabled: isSubmitting,
7142
- onClick: () => {
7143
- setAttachingTo(pickedSimilar);
7144
- setDuplicateIssueNumber(pickedSimilar);
7145
- setDuplicateKind("related");
7146
- void handleSubmit({ duplicateOf: pickedSimilar, kind: "related" });
7147
- },
7148
- style: {
7149
- width: "100%",
7150
- minHeight: "40px",
7151
- marginTop: "-4px",
7152
- padding: "9px 10px",
7153
- borderRadius: "8px",
7154
- border: "1px solid rgba(245,158,11,0.4)",
7155
- backgroundColor: "transparent",
7156
- color: isSubmitting ? t.textMuted : t.text,
7157
- fontSize: "13px",
7158
- fontWeight: 600,
7159
- cursor: isSubmitting ? "not-allowed" : "pointer",
7160
- fontFamily: "inherit"
7161
- },
7162
- children: typeof attachingTo === "number" && duplicateKind === "related" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
7163
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
7164
- "Adding to #",
7165
- attachingTo,
7166
- "\u2026"
7167
- ] }) : `Related request \u2014 add to #${pickedSimilar}`
7168
- }
7169
- ),
7170
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7171
- "button",
7172
- {
7173
- type: "button",
7174
- disabled: isSubmitting,
7175
- onClick: () => {
7176
- setAttachingTo("new");
7177
- void handleSubmit({ skipSimilar: true });
7178
- },
7179
- style: {
7180
- width: "100%",
7181
- minHeight: "40px",
7182
- marginTop: "-4px",
7183
- padding: "8px",
7184
- borderRadius: "8px",
7185
- border: "none",
7186
- backgroundColor: "transparent",
7187
- color: isSubmitting ? t.textMuted : t.text,
7188
- fontSize: "13px",
7189
- fontWeight: 500,
7190
- textDecoration: "underline",
7191
- textUnderlineOffset: "3px",
7192
- cursor: isSubmitting ? "not-allowed" : "pointer",
7193
- fontFamily: "inherit"
7194
- },
7195
- children: attachingTo === "new" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
7196
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ButtonSpinner, {}),
7197
- "Sending as new\u2026"
7198
- ] }) : "No, mine is different \u2014 send as new"
7199
- }
7200
- )
7201
- ]
7202
- }
7203
- )
7204
- ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7322
+ similarCard != null ? similarCard : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
7205
7323
  "button",
7206
7324
  {
7207
7325
  type: "button",
@@ -7500,7 +7618,8 @@ function ReportDialog({
7500
7618
  pageShot,
7501
7619
  onAddFiles: addFiles,
7502
7620
  onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
7503
- files: attachments,
7621
+ files: heldFiles,
7622
+ canReadPdf: !!readFile,
7504
7623
  onRemoveFile: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
7505
7624
  attachmentCount: screenshots.length + attachments.length,
7506
7625
  context: { ...assistContext != null ? assistContext : {}, reportType },
@@ -7514,6 +7633,10 @@ function ReportDialog({
7514
7633
  setStep(2);
7515
7634
  },
7516
7635
  onDuplicateChange: (issueNumber, kind) => {
7636
+ if (issueNumber) {
7637
+ setSimilarIssues(null);
7638
+ setPickedSimilar(null);
7639
+ }
7517
7640
  setDuplicateIssueNumber(issueNumber);
7518
7641
  setDuplicateKind(issueNumber && kind === "related" ? "related" : "duplicate");
7519
7642
  },
@@ -7543,7 +7666,9 @@ function ReportDialog({
7543
7666
  severityRefused: needsSeverity,
7544
7667
  isSubmitting,
7545
7668
  submitted,
7546
- onSend: () => void handleSubmit({ skipSimilar: true }),
7669
+ onSend: () => void handleSubmit(),
7670
+ similarSlot: similarCard,
7671
+ submittedText: addedToIssue ? `Added to #${addedToIssue}${addedAsRelated ? " as a related request" : ""}. Thank you!` : null,
7547
7672
  onReportGlitchgrabProblem: reportGlitchgrabProblem ? (prefill) => {
7548
7673
  setSelfReportPrefill(prefill != null ? prefill : "");
7549
7674
  setSelfReportFrom("assist");
@@ -7837,7 +7962,9 @@ function BookingDialog({
7837
7962
  token,
7838
7963
  baseUrl,
7839
7964
  open,
7840
- onClose
7965
+ onClose,
7966
+ preview = false,
7967
+ availabilityUrl
7841
7968
  }) {
7842
7969
  var _a, _b, _c, _d, _e;
7843
7970
  const wide = useMediaQuery(WIDE_QUERY, true);
@@ -7865,15 +7992,18 @@ function BookingDialog({
7865
7992
  const [answers, setAnswers] = (0, import_react5.useState)({});
7866
7993
  const [pageLoaded, setPageLoaded] = (0, import_react5.useState)(false);
7867
7994
  const [asked, setAsked] = (0, import_react5.useState)(false);
7995
+ const [requireOtp, setRequireOtp] = (0, import_react5.useState)(true);
7868
7996
  const [code, setCode] = (0, import_react5.useState)("");
7869
7997
  const [bookingId, setBookingId] = (0, import_react5.useState)(null);
7870
7998
  const [meetUrl, setMeetUrl] = (0, import_react5.useState)(null);
7871
7999
  const [busy, setBusy] = (0, import_react5.useState)(false);
7872
8000
  const [error, setError] = (0, import_react5.useState)(null);
7873
- const headers = (0, import_react5.useMemo)(
7874
- () => ({ Authorization: `Bearer ${token}`, "Content-Type": "application/json" }),
7875
- [token]
7876
- );
8001
+ const [previewEnded, setPreviewEnded] = (0, import_react5.useState)(false);
8002
+ const headers = (0, import_react5.useMemo)(() => {
8003
+ const out = { "Content-Type": "application/json" };
8004
+ if (token) out.Authorization = `Bearer ${token}`;
8005
+ return out;
8006
+ }, [token]);
7877
8007
  const loadMonth = (0, import_react5.useCallback)(
7878
8008
  async (target) => {
7879
8009
  var _a2, _b2, _c2;
@@ -7885,8 +8015,9 @@ function BookingDialog({
7885
8015
  return;
7886
8016
  }
7887
8017
  try {
8018
+ const source = availabilityUrl != null ? availabilityUrl : `${baseUrl}/api/v1/sdk/booking/availability`;
7888
8019
  const res = await fetch(
7889
- `${baseUrl}/api/v1/sdk/booking/availability?from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}`,
8020
+ `${source}${source.includes("?") ? "&" : "?"}from=${encodeURIComponent(new Date(from).toISOString())}&to=${encodeURIComponent(new Date(to).toISOString())}${preview ? "&preview=1" : ""}`,
7890
8021
  { headers }
7891
8022
  );
7892
8023
  const json = await res.json();
@@ -7907,6 +8038,7 @@ function BookingDialog({
7907
8038
  });
7908
8039
  setWhatsappUrl((_c2 = data.whatsappUrl) != null ? _c2 : null);
7909
8040
  setQuestions(readQuestions(data.questions));
8041
+ setRequireOtp(data.requireOtp !== false);
7910
8042
  setPageLoaded(true);
7911
8043
  } catch (e) {
7912
8044
  setError("Couldn't reach the booking service");
@@ -7914,7 +8046,7 @@ function BookingDialog({
7914
8046
  setPageLoaded(true);
7915
8047
  }
7916
8048
  },
7917
- [baseUrl, headers]
8049
+ [baseUrl, headers, availabilityUrl, preview]
7918
8050
  );
7919
8051
  (0, import_react5.useEffect)(() => {
7920
8052
  if (!open) return;
@@ -7928,6 +8060,7 @@ function BookingDialog({
7928
8060
  setCode("");
7929
8061
  setBookingId(null);
7930
8062
  setError(null);
8063
+ setPreviewEnded(false);
7931
8064
  setAvatarFailed(false);
7932
8065
  setPageLoaded(false);
7933
8066
  setAsked(false);
@@ -7964,8 +8097,12 @@ function BookingDialog({
7964
8097
  const view = step !== "slots" ? step : !pageLoaded ? "loading" : questions.length > 0 && !asked ? "questions" : "slots";
7965
8098
  const roomy = step === "slots";
7966
8099
  async function startBooking() {
7967
- var _a2;
8100
+ var _a2, _b2;
7968
8101
  if (!selected) return;
8102
+ if (preview) {
8103
+ setPreviewEnded(true);
8104
+ return;
8105
+ }
7969
8106
  setBusy(true);
7970
8107
  setError(null);
7971
8108
  try {
@@ -7980,7 +8117,10 @@ function BookingDialog({
7980
8117
  answers: Object.fromEntries(given.map((q) => [q.id, q.answer])),
7981
8118
  startsAt: selected.startsAt,
7982
8119
  // The zone the times were shown in, which is the one the visitor chose.
7983
- timezone: zone
8120
+ timezone: zone,
8121
+ // This dialog can show a booking confirmed in one step; the server
8122
+ // only skips the code for a dialog that says so.
8123
+ instant: true
7984
8124
  })
7985
8125
  });
7986
8126
  const json = await res.json();
@@ -7988,6 +8128,11 @@ function BookingDialog({
7988
8128
  setError((_a2 = json.error) != null ? _a2 : "Couldn't hold that slot");
7989
8129
  return;
7990
8130
  }
8131
+ if (json.data.confirmed) {
8132
+ setMeetUrl((_b2 = json.data.meetUrl) != null ? _b2 : null);
8133
+ setStep("done");
8134
+ return;
8135
+ }
7991
8136
  setBookingId(json.data.bookingId);
7992
8137
  setStep("code");
7993
8138
  } catch (e) {
@@ -8151,6 +8296,22 @@ function BookingDialog({
8151
8296
  ) : null,
8152
8297
  page.hostName && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 14, fontWeight: 500, ...muted }, children: page.hostName })
8153
8298
  ] }),
8299
+ preview && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
8300
+ "div",
8301
+ {
8302
+ style: {
8303
+ alignSelf: "flex-start",
8304
+ padding: "3px 9px",
8305
+ borderRadius: 999,
8306
+ border: `1px dashed ${t.border}`,
8307
+ fontSize: 11.5,
8308
+ fontWeight: 600,
8309
+ letterSpacing: "0.02em",
8310
+ color: t.muted
8311
+ },
8312
+ children: "Preview \u2014 nothing is booked"
8313
+ }
8314
+ ),
8154
8315
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 20, fontWeight: 600, lineHeight: 1.25, paddingRight: 32, overflowWrap: "anywhere" }, children: page.title }),
8155
8316
  page.description && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 13, lineHeight: 1.5, overflowWrap: "anywhere", ...muted }, children: page.description }),
8156
8317
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 4, fontSize: 14 }, children: [
@@ -8588,6 +8749,7 @@ function BookingDialog({
8588
8749
  },
8589
8750
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { maxWidth: 440, display: "flex", flexDirection: "column", gap: 14 }, children: [
8590
8751
  error && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { color: t.danger, fontSize: 13, lineHeight: 1.5 }, children: error }),
8752
+ previewEnded && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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." }),
8591
8753
  step === "details" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
8592
8754
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Field, { t, label: "Your name", value: name, onChange: setName, autoComplete: "name" }),
8593
8755
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Field, { t, label: "Email", value: email, onChange: setEmail, type: "email", autoComplete: "email" }),
@@ -8600,7 +8762,7 @@ function BookingDialog({
8600
8762
  onChange: setPhone,
8601
8763
  type: "tel",
8602
8764
  autoComplete: "tel",
8603
- hint: "We send a code here to confirm, then a reminder before the call."
8765
+ hint: requireOtp ? "We send a code here to confirm, then a reminder before the call." : "We send a reminder here before the call."
8604
8766
  }
8605
8767
  ),
8606
8768
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Field, { t, label: "Anything we should know? (optional)", value: note, onChange: setNote, multiline: true }),
@@ -8625,7 +8787,7 @@ function BookingDialog({
8625
8787
  onClick: startBooking,
8626
8788
  disabled: !canSubmitDetails || busy,
8627
8789
  style: primaryStyle(t, !canSubmitDetails || busy),
8628
- children: busy ? "Sending code\u2026" : "Send verification code"
8790
+ children: requireOtp ? busy ? "Sending code\u2026" : "Send verification code" : busy ? "Booking\u2026" : "Book demo"
8629
8791
  }
8630
8792
  )
8631
8793
  ] })
@@ -9428,7 +9590,11 @@ function GlitchgrabProviderInner({
9428
9590
  [token, baseUrl, session]
9429
9591
  );
9430
9592
  const findSimilar = (0, import_react6.useCallback)(
9431
- (description) => findSimilarIssues(description, token, baseUrl, identity),
9593
+ (description, files) => findSimilarIssues(description, token, baseUrl, identity, files),
9594
+ [token, baseUrl, identity]
9595
+ );
9596
+ const readFile = (0, import_react6.useCallback)(
9597
+ (file) => readAttachment(file, token, baseUrl, identity),
9432
9598
  [token, baseUrl, identity]
9433
9599
  );
9434
9600
  const transcribe = (0, import_react6.useCallback)(
@@ -9459,8 +9625,9 @@ function GlitchgrabProviderInner({
9459
9625
  const [glitchgrabReport, setGlitchgrabReport] = (0, import_react6.useState)(false);
9460
9626
  const [signedUsers, setSignedUsers] = (0, import_react6.useState)("OFF");
9461
9627
  const aiBlockedUnsigned = signedUsers === "ENFORCE" && !signature;
9462
- const [bookingOpen, setBookingOpen] = (0, import_react6.useState)(false);
9463
- const openBookingDialog = (0, import_react6.useCallback)(() => setBookingOpen(true), []);
9628
+ const [booking, setBooking] = (0, import_react6.useState)("closed");
9629
+ const openBookingDialog = (0, import_react6.useCallback)(() => setBooking("live"), []);
9630
+ const openBookingPreview = (0, import_react6.useCallback)(() => setBooking("preview"), []);
9464
9631
  (0, import_react6.useEffect)(() => {
9465
9632
  let cancelled = false;
9466
9633
  fetch(`${baseUrl}/api/v1/sdk/project`, {
@@ -9535,6 +9702,7 @@ function GlitchgrabProviderInner({
9535
9702
  addBreadcrumb: addBreadcrumb2,
9536
9703
  openReportDialog,
9537
9704
  openBookingDialog,
9705
+ openBookingPreview,
9538
9706
  enhanceText: enhance,
9539
9707
  shortcutLabel
9540
9708
  },
@@ -9556,8 +9724,9 @@ function GlitchgrabProviderInner({
9556
9724
  {
9557
9725
  token,
9558
9726
  baseUrl: baseUrl != null ? baseUrl : DEFAULT_BASE_URL2,
9559
- open: bookingOpen,
9560
- onClose: () => setBookingOpen(false)
9727
+ open: booking !== "closed",
9728
+ preview: booking === "preview",
9729
+ onClose: () => setBooking("closed")
9561
9730
  }
9562
9731
  ),
9563
9732
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -9568,6 +9737,7 @@ function GlitchgrabProviderInner({
9568
9737
  enhanceText: aiAssist && !aiBlockedUnsigned ? enhance : void 0,
9569
9738
  assist: aiAssist && !aiBlockedUnsigned ? assist : void 0,
9570
9739
  findSimilarIssues: aiAssist && !aiBlockedUnsigned ? findSimilar : void 0,
9740
+ readFile: aiAssist && !aiBlockedUnsigned ? readFile : void 0,
9571
9741
  reportGlitchgrabProblem: glitchgrabReport ? reportGlitchgrabProblem : void 0,
9572
9742
  transcribeAudio: aiBlockedUnsigned ? void 0 : transcribe,
9573
9743
  types,
@@ -9972,6 +10142,7 @@ function useGlitchgrabFeedback(query) {
9972
10142
  // Annotate the CommonJS export names for ESM import in node:
9973
10143
  0 && (module.exports = {
9974
10144
  BookDemoButton,
10145
+ BookingDialog,
9975
10146
  FeedbackButton,
9976
10147
  GLITCHGRAB_SHORTCUT,
9977
10148
  GLITCHGRAB_SHORTCUT_MAC,