glitchgrab 1.40.0 → 1.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -904,6 +904,7 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
904
904
  var MAX_SCREENSHOT_DATA_URL_LENGTH = 18e5;
905
905
  var QUALITY_LADDER = [0.92, 0.85, 0.75];
906
906
  var DOWNSCALE_FACTOR = 0.7;
907
+ var MAX_ROUNDS = 4;
907
908
  function downscale(canvas, factor) {
908
909
  const next = document.createElement("canvas");
909
910
  next.width = Math.max(1, Math.round(canvas.width * factor));
@@ -917,18 +918,55 @@ function downscale(canvas, factor) {
917
918
  }
918
919
  function encodeScreenshot(canvas, maxLength = MAX_SCREENSHOT_DATA_URL_LENGTH) {
919
920
  let current = canvas;
920
- for (let round = 0; round < 2; round++) {
921
- let encoded = "";
921
+ let encoded = "";
922
+ for (let round = 0; round < MAX_ROUNDS; round++) {
922
923
  for (const quality of QUALITY_LADDER) {
923
924
  encoded = current.toDataURL("image/jpeg", quality);
924
925
  if (encoded.length <= maxLength) return encoded;
925
926
  }
926
- if (round === 1) return encoded;
927
+ if (round === MAX_ROUNDS - 1) break;
927
928
  const smaller = downscale(current, DOWNSCALE_FACTOR);
928
- if (!smaller) return encoded;
929
+ if (!smaller) break;
929
930
  current = smaller;
930
931
  }
931
- return current.toDataURL("image/jpeg", QUALITY_LADDER[0]);
932
+ return encoded;
933
+ }
934
+ function readAsDataUrl(file) {
935
+ return new Promise((resolve, reject) => {
936
+ const reader = new FileReader();
937
+ reader.onload = () => resolve(reader.result);
938
+ reader.onerror = () => reject(reader.error);
939
+ reader.readAsDataURL(file);
940
+ });
941
+ }
942
+ async function reencodeImageFile(file, maxLength) {
943
+ var _a;
944
+ if (typeof createImageBitmap !== "function") return null;
945
+ const bitmap = await createImageBitmap(file);
946
+ try {
947
+ const canvas = document.createElement("canvas");
948
+ canvas.width = bitmap.width;
949
+ canvas.height = bitmap.height;
950
+ const ctx = canvas.getContext("2d");
951
+ if (!ctx) return null;
952
+ ctx.fillStyle = "#ffffff";
953
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
954
+ ctx.drawImage(bitmap, 0, 0);
955
+ return encodeScreenshot(canvas, maxLength);
956
+ } finally {
957
+ (_a = bitmap.close) == null ? void 0 : _a.call(bitmap);
958
+ }
959
+ }
960
+ async function encodeImageFile(file, maxLength = MAX_SCREENSHOT_DATA_URL_LENGTH) {
961
+ const rawLength = Math.ceil(file.size / 3) * 4 + 64;
962
+ if (rawLength > maxLength) {
963
+ try {
964
+ const shrunk = await reencodeImageFile(file, maxLength);
965
+ if (shrunk && shrunk.length < rawLength) return shrunk;
966
+ } catch (e) {
967
+ }
968
+ }
969
+ return readAsDataUrl(file);
932
970
  }
933
971
  var PALETTE = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#a855f7", "#ffffff"];
934
972
  function drawStroke(ctx, s) {
@@ -1219,6 +1257,7 @@ var SEVERITY_LABELS = {
1219
1257
  critical: "Critical"
1220
1258
  };
1221
1259
  var MAX_ASSIST_IMAGES = 3;
1260
+ var MAX_ASSIST_IMAGE_CHARS = 35e5;
1222
1261
  function getTypeLabel(type) {
1223
1262
  switch (type) {
1224
1263
  case "RATING":
@@ -1239,6 +1278,17 @@ function getTypeLabel(type) {
1239
1278
  return "Other";
1240
1279
  }
1241
1280
  }
1281
+ function pickAssistImages(all) {
1282
+ const picked = [];
1283
+ let total = 0;
1284
+ for (let i = all.length - 1; i >= 0 && picked.length < MAX_ASSIST_IMAGES; i--) {
1285
+ const shot = all[i];
1286
+ if (total + shot.length > MAX_ASSIST_IMAGE_CHARS) continue;
1287
+ picked.unshift(shot);
1288
+ total += shot.length;
1289
+ }
1290
+ return picked;
1291
+ }
1242
1292
  var STARTERS = [
1243
1293
  "Something on this page is broken",
1244
1294
  "It did the wrong thing",
@@ -1314,7 +1364,9 @@ function AssistSheet({
1314
1364
  assist,
1315
1365
  theme: t2,
1316
1366
  screenshots,
1367
+ pageShot = null,
1317
1368
  onAddImages,
1369
+ onRemoveImage,
1318
1370
  attachmentCount,
1319
1371
  context,
1320
1372
  reportTypeLabel,
@@ -1356,9 +1408,37 @@ function AssistSheet({
1356
1408
  !typePicked ? "type" : currentTile === "RATING" ? "rating" : "chat"
1357
1409
  );
1358
1410
  const scrollRef = (0, import_react3.useRef)(null);
1359
- const inputRef = (0, import_react3.useRef)(null);
1360
1411
  const draftRef = (0, import_react3.useRef)(null);
1361
1412
  const seededRef = (0, import_react3.useRef)(false);
1413
+ const eventsRef = (0, import_react3.useRef)([]);
1414
+ const aiDraftRef = (0, import_react3.useRef)(null);
1415
+ function logEvent(type, detail) {
1416
+ eventsRef.current.push({ type, at: Date.now(), ...detail ? { detail } : {} });
1417
+ }
1418
+ function takeEvents() {
1419
+ if (eventsRef.current.length === 0) return void 0;
1420
+ const events = eventsRef.current;
1421
+ eventsRef.current = [];
1422
+ return events;
1423
+ }
1424
+ function flushEvents() {
1425
+ if (!conversationId) return;
1426
+ const events = takeEvents();
1427
+ if (!events) return;
1428
+ try {
1429
+ void Promise.resolve(
1430
+ assist({ messages: [], conversationId, events, screenshots: [], context: null })
1431
+ ).catch(() => {
1432
+ });
1433
+ } catch (e) {
1434
+ }
1435
+ }
1436
+ const readShots = new Set(pickAssistImages(screenshots));
1437
+ function leave(type, detail) {
1438
+ logEvent(type, detail);
1439
+ flushEvents();
1440
+ onClose();
1441
+ }
1362
1442
  (0, import_react3.useEffect)(() => {
1363
1443
  const el = scrollRef.current;
1364
1444
  if (el) el.scrollTop = el.scrollHeight;
@@ -1367,35 +1447,52 @@ function AssistSheet({
1367
1447
  var _a;
1368
1448
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1369
1449
  }, [phase]);
1450
+ function degradeKeepingEvents(events, message) {
1451
+ if (events) eventsRef.current = [...events, ...eventsRef.current];
1452
+ flushEvents();
1453
+ onDegrade(message);
1454
+ }
1370
1455
  async function runTurn(history2) {
1371
- var _a, _b, _c, _d, _e;
1456
+ var _a, _b, _c, _d;
1372
1457
  setBusy(true);
1373
1458
  let result;
1459
+ const events = takeEvents();
1374
1460
  try {
1375
- const sent = screenshots.slice(-MAX_ASSIST_IMAGES);
1461
+ const sent = pickAssistImages(screenshots);
1376
1462
  result = await assist({
1377
- messages: history2,
1463
+ // `kind` only on drafts, so an older server sees the exact shape it
1464
+ // always did — it just learns the draft text now instead of a
1465
+ // placeholder line.
1466
+ messages: history2.map(
1467
+ (m) => m.kind ? { role: m.role, content: m.content, kind: m.kind } : m
1468
+ ),
1378
1469
  conversationId,
1379
1470
  screenshots: sent,
1380
- screenshot: (_a = sent[sent.length - 1]) != null ? _a : null,
1381
- context
1471
+ context: {
1472
+ ...context != null ? context : {},
1473
+ imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached")
1474
+ },
1475
+ events
1382
1476
  });
1383
1477
  } catch (e) {
1384
- onDegrade("The assistant is unavailable \u2014 write your report below and send it as normal.");
1478
+ degradeKeepingEvents(
1479
+ events,
1480
+ "The assistant is unavailable \u2014 write your report below and send it as normal."
1481
+ );
1385
1482
  return;
1386
1483
  } finally {
1387
1484
  setBusy(false);
1388
1485
  }
1389
1486
  if (result.degraded) {
1390
- onDegrade(result.degraded);
1487
+ degradeKeepingEvents(events, result.degraded);
1391
1488
  return;
1392
1489
  }
1393
1490
  if (result.conversationId) {
1394
1491
  setConversationId(result.conversationId);
1395
1492
  onConversationChange == null ? void 0 : onConversationChange(result.conversationId);
1396
1493
  }
1397
- setDuplicate((_b = result.duplicate) != null ? _b : null);
1398
- onDuplicateChange == null ? void 0 : onDuplicateChange((_d = (_c = result.duplicate) == null ? void 0 : _c.number) != null ? _d : null);
1494
+ setDuplicate((_a = result.duplicate) != null ? _a : null);
1495
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_c = (_b = result.duplicate) == null ? void 0 : _b.number) != null ? _c : null);
1399
1496
  if (result.solved) {
1400
1497
  setSolved(result.solved);
1401
1498
  setOptions([]);
@@ -1404,14 +1501,16 @@ function AssistSheet({
1404
1501
  }
1405
1502
  if (result.report) {
1406
1503
  onDescriptionChange(result.report);
1407
- setMessages([...history2, { role: "assistant", content: "Here's your report \u2014 have a read." }]);
1504
+ aiDraftRef.current = result.report;
1505
+ logEvent("draft_shown");
1506
+ setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
1408
1507
  setOptions([]);
1409
1508
  setPhase("draft");
1410
1509
  return;
1411
1510
  }
1412
1511
  if (result.question) {
1413
1512
  setMessages([...history2, { role: "assistant", content: result.question }]);
1414
- setOptions((_e = result.options) != null ? _e : []);
1513
+ setOptions((_d = result.options) != null ? _d : []);
1415
1514
  return;
1416
1515
  }
1417
1516
  onDegrade("The assistant had nothing to add \u2014 write your report below and send it as normal.");
@@ -1430,7 +1529,14 @@ function AssistSheet({
1430
1529
  const value = text.trim();
1431
1530
  if (!value || busy) return;
1432
1531
  setOptions([]);
1433
- const history2 = [...messages, { role: "user", content: value }];
1532
+ const underDraft = phase === "draft";
1533
+ if (underDraft) {
1534
+ const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1535
+ logEvent("keep_chatting", edited ? "after editing" : void 0);
1536
+ setPhase("chat");
1537
+ }
1538
+ const base = underDraft ? withCurrentDraft(messages) : messages;
1539
+ const history2 = [...base, { role: "user", content: value }];
1434
1540
  setMessages(history2);
1435
1541
  setInput("");
1436
1542
  void runTurn(history2);
@@ -1439,7 +1545,56 @@ function AssistSheet({
1439
1545
  onPickType(tile);
1440
1546
  setPhase(tile === "RATING" ? "rating" : "chat");
1441
1547
  }
1548
+ function withCurrentDraft(history2) {
1549
+ const current = description.trim();
1550
+ const next = [...history2];
1551
+ for (let i = next.length - 1; i >= 0; i--) {
1552
+ if (next[i].kind === "report") {
1553
+ if (current) next[i] = { ...next[i], content: current };
1554
+ break;
1555
+ }
1556
+ }
1557
+ return next;
1558
+ }
1559
+ function sendDraft() {
1560
+ const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1561
+ logEvent("draft_sent", edited ? "edited" : "as drafted");
1562
+ flushEvents();
1563
+ onSend();
1564
+ }
1442
1565
  const empty = phase === "chat" && messages.length === 0 && !busy;
1566
+ function renderKnownIssue({ title, footer }) {
1567
+ if (!duplicate) return null;
1568
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1569
+ "div",
1570
+ {
1571
+ "data-gg-known-issue": "",
1572
+ style: {
1573
+ display: "flex",
1574
+ flexDirection: "column",
1575
+ gap: "3px",
1576
+ padding: "9px 10px",
1577
+ borderRadius: "8px",
1578
+ border: "1px solid rgba(245,158,11,0.4)",
1579
+ backgroundColor: "rgba(245,158,11,0.08)",
1580
+ fontSize: "12px",
1581
+ lineHeight: 1.5,
1582
+ minWidth: 0
1583
+ },
1584
+ children: [
1585
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1586
+ title && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1587
+ "#",
1588
+ duplicate.number,
1589
+ " ",
1590
+ duplicate.title
1591
+ ] }),
1592
+ duplicate.status && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.status }),
1593
+ footer && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.textMuted }, children: footer })
1594
+ ]
1595
+ }
1596
+ );
1597
+ }
1443
1598
  const initials = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
1444
1599
  var _a, _b;
1445
1600
  return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
@@ -1464,7 +1619,7 @@ function AssistSheet({
1464
1619
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
1465
1620
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
1466
1621
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
1467
- [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
1622
+ [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible,[data-gg-assist-remove]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
1468
1623
  ` }),
1469
1624
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1470
1625
  "div",
@@ -1484,7 +1639,7 @@ function AssistSheet({
1484
1639
  animation: "gg-sheet-fade .18s ease",
1485
1640
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1486
1641
  },
1487
- onClick: onClose,
1642
+ onClick: () => leave("closed"),
1488
1643
  onPointerDown: (e) => e.stopPropagation(),
1489
1644
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1490
1645
  "div",
@@ -1564,7 +1719,7 @@ function AssistSheet({
1564
1719
  {
1565
1720
  type: "button",
1566
1721
  "data-gg-write-myself": "",
1567
- onClick: onClose,
1722
+ onClick: () => leave("write_myself", `header, ${phase}`),
1568
1723
  "aria-label": "Write it myself instead",
1569
1724
  style: {
1570
1725
  flexShrink: 0,
@@ -1820,7 +1975,10 @@ function AssistSheet({
1820
1975
  {
1821
1976
  type: "button",
1822
1977
  "data-gg-chip": "",
1823
- onClick: () => send(s),
1978
+ onClick: () => {
1979
+ logEvent("starter_tap", s);
1980
+ send(s);
1981
+ },
1824
1982
  style: {
1825
1983
  border: `1px solid ${t2.inputBorder}`,
1826
1984
  background: "transparent",
@@ -1840,6 +1998,60 @@ function AssistSheet({
1840
1998
  ] }),
1841
1999
  messages.map((m, i) => {
1842
2000
  const mine = m.role === "user";
2001
+ if (m.kind === "report") {
2002
+ const live = phase === "draft" && i === messages.length - 1;
2003
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
2004
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Avatar, { who: "assistant", theme: t2 }),
2005
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2006
+ "div",
2007
+ {
2008
+ "data-gg-draft-bubble": "",
2009
+ style: {
2010
+ maxWidth: "82%",
2011
+ minWidth: 0,
2012
+ padding: "9px 12px",
2013
+ borderRadius: "12px 12px 12px 4px",
2014
+ fontSize: "13px",
2015
+ lineHeight: 1.55,
2016
+ backgroundColor: t2.bgSecondary,
2017
+ color: t2.text,
2018
+ border: live ? "none" : `1px solid ${t2.accent}66`
2019
+ },
2020
+ children: live ? "Here's your report \u2014 have a read." : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2021
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2022
+ "span",
2023
+ {
2024
+ style: {
2025
+ display: "block",
2026
+ fontSize: "10px",
2027
+ fontWeight: 700,
2028
+ letterSpacing: ".06em",
2029
+ textTransform: "uppercase",
2030
+ color: t2.accent,
2031
+ marginBottom: "4px"
2032
+ },
2033
+ children: "Draft so far"
2034
+ }
2035
+ ),
2036
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2037
+ "span",
2038
+ {
2039
+ style: {
2040
+ display: "block",
2041
+ whiteSpace: "pre-wrap",
2042
+ wordBreak: "break-word",
2043
+ maxHeight: "160px",
2044
+ overflowY: "auto",
2045
+ color: t2.textMuted
2046
+ },
2047
+ children: m.content
2048
+ }
2049
+ )
2050
+ ] })
2051
+ }
2052
+ )
2053
+ ] }, i);
2054
+ }
1843
2055
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1844
2056
  "div",
1845
2057
  {
@@ -1873,6 +2085,7 @@ function AssistSheet({
1873
2085
  i
1874
2086
  );
1875
2087
  }),
2088
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
1876
2089
  options.length > 0 && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1877
2090
  "div",
1878
2091
  {
@@ -1894,7 +2107,10 @@ function AssistSheet({
1894
2107
  type: "button",
1895
2108
  "data-gg-chip": "",
1896
2109
  "data-gg-option": "",
1897
- onClick: () => send(option),
2110
+ onClick: () => {
2111
+ logEvent("option_tap", option);
2112
+ send(option);
2113
+ },
1898
2114
  style: {
1899
2115
  border: `1px solid ${t2.inputBorder}`,
1900
2116
  background: "transparent",
@@ -1968,35 +2184,13 @@ function AssistSheet({
1968
2184
  textTransform: "uppercase",
1969
2185
  color: t2.accent
1970
2186
  },
1971
- children: "Your report \xB7 edit anything"
1972
- }
1973
- ),
1974
- duplicate && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1975
- "div",
1976
- {
1977
- style: {
1978
- display: "flex",
1979
- flexDirection: "column",
1980
- gap: "3px",
1981
- padding: "9px 10px",
1982
- borderRadius: "8px",
1983
- border: "1px solid rgba(245,158,11,0.4)",
1984
- backgroundColor: "rgba(245,158,11,0.08)",
1985
- fontSize: "12px",
1986
- lineHeight: 1.5
1987
- },
1988
- children: [
1989
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: "Our team is already on this" }),
1990
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1991
- "#",
1992
- duplicate.number,
1993
- " ",
1994
- duplicate.title
1995
- ] }),
1996
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: t2.textMuted }, children: "Your details will be added to it instead of opening a new issue." })
1997
- ]
2187
+ children: duplicate ? `What we'll add to #${duplicate.number} \xB7 edit anything` : "Your report \xB7 edit anything"
1998
2188
  }
1999
2189
  ),
2190
+ renderKnownIssue({
2191
+ title: true,
2192
+ footer: "Your details will be added to it instead of opening a new issue."
2193
+ }),
2000
2194
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2001
2195
  "textarea",
2002
2196
  {
@@ -2091,26 +2285,7 @@ function AssistSheet({
2091
2285
  " ",
2092
2286
  attachmentCount === 1 ? "attachment" : "attachments",
2093
2287
  " will be sent with this."
2094
- ] }),
2095
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2096
- "button",
2097
- {
2098
- type: "button",
2099
- onClick: () => setPhase("chat"),
2100
- style: {
2101
- alignSelf: "flex-start",
2102
- border: "none",
2103
- background: "transparent",
2104
- color: t2.textMuted,
2105
- fontSize: "12px",
2106
- fontFamily: "inherit",
2107
- padding: "9px 0",
2108
- cursor: "pointer",
2109
- textDecoration: "underline"
2110
- },
2111
- children: "Not quite \u2014 keep chatting"
2112
- }
2113
- )
2288
+ ] })
2114
2289
  ]
2115
2290
  }
2116
2291
  ),
@@ -2214,192 +2389,276 @@ function AssistSheet({
2214
2389
  },
2215
2390
  children: isSubmitting ? "Sending\u2026" : rating < 1 ? "Pick a star first" : "Send Rating"
2216
2391
  }
2217
- ) : phase === "draft" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2218
- validationError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2219
- "p",
2220
- {
2221
- style: {
2222
- margin: "0 0 8px",
2223
- color: "#ef4444",
2224
- fontSize: "12px"
2225
- },
2226
- children: validationError
2227
- }
2228
- ),
2229
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2230
- "button",
2231
- {
2232
- type: "button",
2233
- "data-gg-sheet-send": "",
2234
- onClick: onSend,
2235
- disabled: isSubmitting || !description.trim(),
2236
- style: {
2237
- width: "100%",
2238
- padding: "12px",
2239
- borderRadius: "10px",
2240
- border: "none",
2241
- backgroundColor: isSubmitting || !description.trim() ? t2.bgSecondary : t2.accent,
2242
- color: isSubmitting || !description.trim() ? t2.textMuted : t2.accentText,
2243
- fontSize: "14px",
2244
- fontWeight: 700,
2245
- fontFamily: "inherit",
2246
- cursor: isSubmitting || !description.trim() ? "default" : "pointer"
2247
- },
2248
- children: isSubmitting ? "Sending\u2026" : duplicate ? `Add to #${duplicate.number}` : "Send Report"
2249
- }
2250
- )
2251
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2252
- (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2253
- "div",
2254
- {
2255
- style: {
2256
- display: "flex",
2257
- gap: "8px",
2258
- alignItems: "center",
2259
- flexWrap: "wrap",
2260
- marginBottom: "8px"
2261
- },
2262
- children: [
2263
- screenshots.map((shot, i) => {
2264
- const read = i >= screenshots.length - MAX_ASSIST_IMAGES;
2265
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2266
- "img",
2392
+ ) : (
2393
+ // Chat AND draft share this branch, so the chat box is the same
2394
+ // element on both sides of a draft landing — it stays put
2395
+ // instead of remounting, and a draft is something you can
2396
+ // still reply to (#360), not a form that replaced the chat.
2397
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2398
+ (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2399
+ "div",
2400
+ {
2401
+ style: {
2402
+ display: "flex",
2403
+ gap: "8px",
2404
+ alignItems: "center",
2405
+ flexWrap: "wrap",
2406
+ marginBottom: "8px"
2407
+ },
2408
+ children: [
2409
+ screenshots.map((shot, i) => {
2410
+ const read = readShots.has(shot);
2411
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2412
+ "div",
2413
+ {
2414
+ style: {
2415
+ position: "relative",
2416
+ width: "40px",
2417
+ height: "40px",
2418
+ flexShrink: 0
2419
+ },
2420
+ children: [
2421
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2422
+ "img",
2423
+ {
2424
+ src: shot,
2425
+ alt: read ? "Attached image the assistant reads" : "Attached image",
2426
+ "data-gg-assist-thumb": "",
2427
+ style: {
2428
+ display: "block",
2429
+ width: "100%",
2430
+ height: "100%",
2431
+ objectFit: "cover",
2432
+ borderRadius: "8px",
2433
+ border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2434
+ opacity: read ? 1 : 0.45,
2435
+ boxSizing: "border-box"
2436
+ }
2437
+ }
2438
+ ),
2439
+ onRemoveImage && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2440
+ "button",
2441
+ {
2442
+ type: "button",
2443
+ "data-gg-assist-remove": "",
2444
+ "aria-label": `Remove image ${i + 1}`,
2445
+ title: "Remove this image",
2446
+ onClick: () => {
2447
+ logEvent(
2448
+ "image_removed",
2449
+ pageShot && shot === pageShot ? "page capture" : "attached"
2450
+ );
2451
+ onRemoveImage(i);
2452
+ },
2453
+ style: {
2454
+ position: "absolute",
2455
+ top: "-13px",
2456
+ right: "-13px",
2457
+ width: "32px",
2458
+ height: "32px",
2459
+ border: "none",
2460
+ background: "transparent",
2461
+ padding: 0,
2462
+ cursor: "pointer",
2463
+ display: "flex",
2464
+ alignItems: "center",
2465
+ justifyContent: "center",
2466
+ borderRadius: "50%"
2467
+ },
2468
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2469
+ "span",
2470
+ {
2471
+ "aria-hidden": "true",
2472
+ style: {
2473
+ width: "20px",
2474
+ height: "20px",
2475
+ boxSizing: "border-box",
2476
+ borderRadius: "50%",
2477
+ border: `2px solid ${t2.bg}`,
2478
+ background: "#ef4444",
2479
+ color: "#fff",
2480
+ display: "flex",
2481
+ alignItems: "center",
2482
+ justifyContent: "center"
2483
+ },
2484
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2485
+ "path",
2486
+ {
2487
+ d: "M1 1L9 9M9 1L1 9",
2488
+ stroke: "currentColor",
2489
+ strokeWidth: "1.6",
2490
+ strokeLinecap: "round"
2491
+ }
2492
+ ) })
2493
+ }
2494
+ )
2495
+ }
2496
+ )
2497
+ ]
2498
+ },
2499
+ `${i}-${shot.slice(-16)}`
2500
+ );
2501
+ }),
2502
+ onAddImages && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2503
+ "label",
2267
2504
  {
2268
- src: shot,
2269
- alt: read ? "Attached image the assistant reads" : "Attached image",
2270
- "data-gg-assist-thumb": "",
2505
+ "data-gg-assist-attach": "",
2506
+ title: "Attach an image \u2014 or just paste one",
2271
2507
  style: {
2272
2508
  width: "40px",
2273
2509
  height: "40px",
2274
- objectFit: "cover",
2275
2510
  borderRadius: "8px",
2276
- border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2277
- opacity: read ? 1 : 0.45
2278
- }
2279
- },
2280
- `${i}-${shot.slice(-16)}`
2281
- );
2282
- }),
2283
- onAddImages && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2284
- "label",
2285
- {
2286
- "data-gg-assist-attach": "",
2287
- title: "Attach an image \u2014 or just paste one",
2288
- style: {
2289
- width: "40px",
2290
- height: "40px",
2291
- borderRadius: "8px",
2292
- border: `1px dashed ${t2.inputBorder}`,
2293
- display: "inline-flex",
2294
- alignItems: "center",
2295
- justifyContent: "center",
2296
- color: t2.textMuted,
2297
- fontSize: "18px",
2298
- cursor: "pointer",
2299
- flexShrink: 0
2300
- },
2301
- children: [
2302
- "+",
2303
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2304
- "input",
2305
- {
2306
- type: "file",
2307
- accept: "image/*",
2308
- multiple: true,
2309
- onChange: (e) => {
2310
- var _a;
2311
- const files = Array.from((_a = e.target.files) != null ? _a : []);
2312
- if (files.length) onAddImages(files);
2313
- e.target.value = "";
2314
- },
2315
- style: { display: "none" }
2316
- }
2317
- )
2318
- ]
2319
- }
2320
- ),
2321
- screenshots.length > MAX_ASSIST_IMAGES && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, fontSize: "11px" }, children: [
2322
- "newest ",
2323
- MAX_ASSIST_IMAGES,
2324
- " read"
2325
- ] })
2326
- ]
2327
- }
2328
- ),
2329
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
2330
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2331
- "textarea",
2332
- {
2333
- ref: inputRef,
2334
- value: input,
2335
- onChange: (e) => setInput(e.target.value),
2336
- onKeyDown: (e) => {
2337
- if (e.key === "Enter" && !e.shiftKey) {
2338
- e.preventDefault();
2339
- send(input);
2340
- }
2341
- },
2342
- placeholder: "Type your answer\u2026",
2343
- rows: 1,
2344
- disabled: busy,
2345
- autoFocus: true,
2346
- style: {
2347
- flex: 1,
2348
- minHeight: "42px",
2349
- maxHeight: "120px",
2350
- padding: "11px 12px",
2351
- borderRadius: "10px",
2352
- border: `1px solid ${t2.inputBorder}`,
2353
- backgroundColor: t2.inputBg,
2354
- color: t2.text,
2355
- fontSize: "13px",
2356
- lineHeight: 1.4,
2357
- fontFamily: "inherit",
2358
- resize: "none",
2359
- outline: "none",
2360
- boxSizing: "border-box"
2361
- }
2511
+ border: `1px dashed ${t2.inputBorder}`,
2512
+ display: "inline-flex",
2513
+ alignItems: "center",
2514
+ justifyContent: "center",
2515
+ color: t2.textMuted,
2516
+ fontSize: "18px",
2517
+ cursor: "pointer",
2518
+ flexShrink: 0
2519
+ },
2520
+ children: [
2521
+ "+",
2522
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2523
+ "input",
2524
+ {
2525
+ type: "file",
2526
+ accept: "image/*",
2527
+ multiple: true,
2528
+ onChange: (e) => {
2529
+ var _a;
2530
+ const files = Array.from((_a = e.target.files) != null ? _a : []);
2531
+ if (files.length) {
2532
+ logEvent("image_added", String(files.length));
2533
+ onAddImages(files);
2534
+ }
2535
+ e.target.value = "";
2536
+ },
2537
+ style: { display: "none" }
2538
+ }
2539
+ )
2540
+ ]
2541
+ }
2542
+ ),
2543
+ readShots.size < screenshots.length && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, fontSize: "11px" }, children: [
2544
+ readShots.size,
2545
+ " of ",
2546
+ screenshots.length,
2547
+ " read"
2548
+ ] })
2549
+ ]
2362
2550
  }
2363
2551
  ),
2364
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2365
- "button",
2366
- {
2367
- type: "button",
2368
- "data-gg-sheet-send": "",
2369
- onClick: () => send(input),
2370
- disabled: busy || !input.trim(),
2371
- "aria-label": "Send message",
2372
- "data-gg-send-message": "",
2373
- style: {
2374
- flexShrink: 0,
2375
- width: "42px",
2376
- height: "42px",
2377
- borderRadius: "10px",
2378
- border: "none",
2379
- display: "inline-flex",
2380
- alignItems: "center",
2381
- justifyContent: "center",
2382
- backgroundColor: busy || !input.trim() ? t2.bgSecondary : t2.accent,
2383
- color: busy || !input.trim() ? t2.textMuted : t2.accentText,
2384
- cursor: busy || !input.trim() ? "default" : "pointer"
2385
- },
2386
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2387
- "path",
2388
- {
2389
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
2390
- fill: "currentColor"
2552
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
2553
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2554
+ "textarea",
2555
+ {
2556
+ value: input,
2557
+ onChange: (e) => setInput(e.target.value),
2558
+ onKeyDown: (e) => {
2559
+ if (e.key === "Enter" && !e.shiftKey) {
2560
+ e.preventDefault();
2561
+ send(input);
2562
+ }
2563
+ },
2564
+ placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
2565
+ rows: 1,
2566
+ disabled: busy,
2567
+ autoFocus: true,
2568
+ style: {
2569
+ flex: 1,
2570
+ minHeight: "42px",
2571
+ maxHeight: "120px",
2572
+ padding: "11px 12px",
2573
+ borderRadius: "10px",
2574
+ border: `1px solid ${t2.inputBorder}`,
2575
+ backgroundColor: t2.inputBg,
2576
+ color: t2.text,
2577
+ fontSize: "13px",
2578
+ lineHeight: 1.4,
2579
+ fontFamily: "inherit",
2580
+ resize: "none",
2581
+ outline: "none",
2582
+ boxSizing: "border-box"
2391
2583
  }
2392
- ) })
2393
- }
2394
- )
2584
+ }
2585
+ ),
2586
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2587
+ "button",
2588
+ {
2589
+ type: "button",
2590
+ "data-gg-sheet-send": "",
2591
+ onClick: () => send(input),
2592
+ disabled: busy || !input.trim(),
2593
+ "aria-label": "Send message",
2594
+ "data-gg-send-message": "",
2595
+ style: {
2596
+ flexShrink: 0,
2597
+ width: "42px",
2598
+ height: "42px",
2599
+ borderRadius: "10px",
2600
+ border: "none",
2601
+ display: "inline-flex",
2602
+ alignItems: "center",
2603
+ justifyContent: "center",
2604
+ backgroundColor: busy || !input.trim() ? t2.bgSecondary : t2.accent,
2605
+ color: busy || !input.trim() ? t2.textMuted : t2.accentText,
2606
+ cursor: busy || !input.trim() ? "default" : "pointer"
2607
+ },
2608
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2609
+ "path",
2610
+ {
2611
+ d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
2612
+ fill: "currentColor"
2613
+ }
2614
+ ) })
2615
+ }
2616
+ )
2617
+ ] }),
2618
+ phase === "draft" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2619
+ validationError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2620
+ "p",
2621
+ {
2622
+ style: {
2623
+ margin: "8px 0 0",
2624
+ color: "#ef4444",
2625
+ fontSize: "12px"
2626
+ },
2627
+ children: validationError
2628
+ }
2629
+ ),
2630
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2631
+ "button",
2632
+ {
2633
+ type: "button",
2634
+ "data-gg-sheet-send": "",
2635
+ onClick: sendDraft,
2636
+ disabled: isSubmitting || !description.trim(),
2637
+ style: {
2638
+ width: "100%",
2639
+ marginTop: "8px",
2640
+ padding: "12px",
2641
+ borderRadius: "10px",
2642
+ border: "none",
2643
+ backgroundColor: isSubmitting || !description.trim() ? t2.bgSecondary : t2.accent,
2644
+ color: isSubmitting || !description.trim() ? t2.textMuted : t2.accentText,
2645
+ fontSize: "14px",
2646
+ fontWeight: 700,
2647
+ fontFamily: "inherit",
2648
+ cursor: isSubmitting || !description.trim() ? "default" : "pointer"
2649
+ },
2650
+ children: isSubmitting ? "Sending\u2026" : duplicate ? `Add to #${duplicate.number}` : "Send Report"
2651
+ }
2652
+ )
2653
+ ] })
2395
2654
  ] })
2396
- ] }),
2655
+ ),
2397
2656
  phase === "chat" && messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2398
2657
  "button",
2399
2658
  {
2400
2659
  type: "button",
2401
2660
  "data-gg-write-myself": "",
2402
- onClick: onClose,
2661
+ onClick: () => leave("write_myself", "footer"),
2403
2662
  style: {
2404
2663
  display: "block",
2405
2664
  margin: "8px auto 0",
@@ -2471,6 +2730,107 @@ var ATTACHMENT_ACCEPT = [
2471
2730
  ".xlsx",
2472
2731
  ".zip"
2473
2732
  ].join(",");
2733
+ var MAX_SCALE = 2;
2734
+ var FRAME_TIMEOUT_MS = 3e3;
2735
+ function canCaptureTabPixels() {
2736
+ var _a;
2737
+ try {
2738
+ return typeof navigator !== "undefined" && typeof ((_a = navigator.mediaDevices) == null ? void 0 : _a.getDisplayMedia) === "function" && typeof globalThis.CaptureController !== "undefined";
2739
+ } catch (e) {
2740
+ return false;
2741
+ }
2742
+ }
2743
+ var HAVE_CURRENT_DATA = 2;
2744
+ function waitForFrame(video) {
2745
+ if (video.readyState >= HAVE_CURRENT_DATA && video.videoWidth > 0) {
2746
+ return Promise.resolve(true);
2747
+ }
2748
+ return new Promise((resolve) => {
2749
+ const timer = setTimeout(() => resolve(false), FRAME_TIMEOUT_MS);
2750
+ const done = () => {
2751
+ clearTimeout(timer);
2752
+ resolve(true);
2753
+ };
2754
+ if (typeof video.requestVideoFrameCallback === "function") {
2755
+ video.requestVideoFrameCallback(done);
2756
+ } else if (video.readyState >= 2) {
2757
+ done();
2758
+ } else {
2759
+ video.addEventListener("loadeddata", done, { once: true });
2760
+ }
2761
+ });
2762
+ }
2763
+ async function grabFrame(stream) {
2764
+ const video = document.createElement("video");
2765
+ video.muted = true;
2766
+ video.playsInline = true;
2767
+ video.srcObject = stream;
2768
+ try {
2769
+ await video.play();
2770
+ if (!await waitForFrame(video)) return null;
2771
+ if (!video.videoWidth || !video.videoHeight) return null;
2772
+ const maxWidth = Math.max(1, window.innerWidth) * MAX_SCALE;
2773
+ const factor = Math.min(1, maxWidth / video.videoWidth);
2774
+ const canvas = document.createElement("canvas");
2775
+ canvas.width = Math.round(video.videoWidth * factor);
2776
+ canvas.height = Math.round(video.videoHeight * factor);
2777
+ const ctx = canvas.getContext("2d");
2778
+ if (!ctx) return null;
2779
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2780
+ return canvas;
2781
+ } finally {
2782
+ video.srcObject = null;
2783
+ }
2784
+ }
2785
+ async function captureViaTabPixels() {
2786
+ var _a, _b;
2787
+ if (!canCaptureTabPixels()) return null;
2788
+ let stream = null;
2789
+ try {
2790
+ const options = {
2791
+ video: { displaySurface: "browser" },
2792
+ audio: false,
2793
+ preferCurrentTab: true,
2794
+ selfBrowserSurface: "include",
2795
+ surfaceSwitching: "exclude",
2796
+ monitorTypeSurfaces: "exclude"
2797
+ };
2798
+ stream = await navigator.mediaDevices.getDisplayMedia(options);
2799
+ const surface = (_b = (_a = stream.getVideoTracks()[0]) == null ? void 0 : _a.getSettings()) == null ? void 0 : _b.displaySurface;
2800
+ if (surface && surface !== "browser") return null;
2801
+ const canvas = await grabFrame(stream);
2802
+ return canvas ? encodeScreenshot(canvas) : null;
2803
+ } catch (e) {
2804
+ return null;
2805
+ } finally {
2806
+ stream == null ? void 0 : stream.getTracks().forEach((track) => track.stop());
2807
+ }
2808
+ }
2809
+ async function captureViaHtml2Canvas() {
2810
+ try {
2811
+ const { default: html2canvas } = await import("html2canvas-pro");
2812
+ const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), MAX_SCALE);
2813
+ const canvas = await html2canvas(document.body, {
2814
+ scale,
2815
+ logging: false,
2816
+ useCORS: true,
2817
+ allowTaint: true,
2818
+ x: window.scrollX,
2819
+ y: window.scrollY,
2820
+ width: window.innerWidth,
2821
+ height: window.innerHeight,
2822
+ windowWidth: window.innerWidth,
2823
+ windowHeight: window.innerHeight
2824
+ });
2825
+ return encodeScreenshot(canvas);
2826
+ } catch (e) {
2827
+ return null;
2828
+ }
2829
+ }
2830
+ async function captureDefaultScreenshot() {
2831
+ var _a;
2832
+ return (_a = await captureViaTabPixels()) != null ? _a : await captureViaHtml2Canvas();
2833
+ }
2474
2834
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
2475
2835
  function useIsDark() {
2476
2836
  if (typeof window === "undefined") return true;
@@ -2745,27 +3105,6 @@ function isLowQualityText(text) {
2745
3105
  }
2746
3106
  return null;
2747
3107
  }
2748
- async function captureViaHtml2Canvas() {
2749
- try {
2750
- const { default: html2canvas } = await import("html2canvas-pro");
2751
- const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
2752
- const canvas = await html2canvas(document.body, {
2753
- scale,
2754
- logging: false,
2755
- useCORS: true,
2756
- allowTaint: true,
2757
- x: window.scrollX,
2758
- y: window.scrollY,
2759
- width: window.innerWidth,
2760
- height: window.innerHeight,
2761
- windowWidth: window.innerWidth,
2762
- windowHeight: window.innerHeight
2763
- });
2764
- return encodeScreenshot(canvas);
2765
- } catch (e) {
2766
- return null;
2767
- }
2768
- }
2769
3108
  var MIC_MESSAGES = {
2770
3109
  blocked: "Microphone is blocked for this site. Allow it in your browser's site settings, then reload this page. You can type your report instead.",
2771
3110
  notFound: "No microphone found. Connect one and try again, or type your report instead.",
@@ -2803,7 +3142,7 @@ function ReportDialog({
2803
3142
  transcribeAudio: transcribeAudio2,
2804
3143
  types,
2805
3144
  showSeverity = true,
2806
- captureScreenshot = captureViaHtml2Canvas,
3145
+ captureScreenshot = captureDefaultScreenshot,
2807
3146
  reporter,
2808
3147
  onClose,
2809
3148
  headerSlot
@@ -2833,6 +3172,7 @@ function ReportDialog({
2833
3172
  const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
2834
3173
  const [submitted, setSubmitted] = (0, import_react.useState)(false);
2835
3174
  const [screenshots, setScreenshots] = (0, import_react.useState)([]);
3175
+ const [pageShot, setPageShot] = (0, import_react.useState)(null);
2836
3176
  const [attachments, setAttachments] = (0, import_react.useState)([]);
2837
3177
  const [previewIndex, setPreviewIndex] = (0, import_react.useState)(null);
2838
3178
  const [annotatingIndex, setAnnotatingIndex] = (0, import_react.useState)(null);
@@ -2971,20 +3311,23 @@ function ReportDialog({
2971
3311
  e.target.value = "";
2972
3312
  };
2973
3313
  const addFiles = (files) => {
2974
- files.forEach((file) => {
3314
+ const images = files.filter((file) => file.type.startsWith("image/"));
3315
+ if (images.length > 0) {
3316
+ void Promise.all(images.map((file) => encodeImageFile(file).catch(() => null))).then(
3317
+ (encoded) => {
3318
+ const added = encoded.filter((url) => !!url);
3319
+ if (added.length > 0) setScreenshots((prev) => [...prev, ...added]);
3320
+ }
3321
+ );
3322
+ }
3323
+ files.filter((file) => !file.type.startsWith("image/")).forEach((file) => {
2975
3324
  const reader = new FileReader();
2976
- if (file.type.startsWith("image/")) {
2977
- reader.onload = () => {
2978
- setScreenshots((prev) => [...prev, reader.result]);
2979
- };
2980
- } else {
2981
- reader.onload = () => {
2982
- setAttachments((prev) => [
2983
- ...prev,
2984
- { name: file.name, size: file.size, dataUrl: reader.result }
2985
- ]);
2986
- };
2987
- }
3325
+ reader.onload = () => {
3326
+ setAttachments((prev) => [
3327
+ ...prev,
3328
+ { name: file.name, size: file.size, dataUrl: reader.result }
3329
+ ]);
3330
+ };
2988
3331
  reader.readAsDataURL(file);
2989
3332
  });
2990
3333
  };
@@ -2998,6 +3341,7 @@ function ReportDialog({
2998
3341
  setStep(2);
2999
3342
  }
3000
3343
  const shot = await captureScreenshot();
3344
+ setPageShot(shot != null ? shot : null);
3001
3345
  if (shot) setScreenshots([shot]);
3002
3346
  if (assist && !assistUsed) setAssistOpen(true);
3003
3347
  setIsOpen(true);
@@ -5195,6 +5539,7 @@ function ReportDialog({
5195
5539
  imageSrc: screenshots[annotatingIndex],
5196
5540
  onCancel: () => setAnnotatingIndex(null),
5197
5541
  onSave: (dataUrl) => {
5542
+ if (screenshots[annotatingIndex] === pageShot) setPageShot(dataUrl);
5198
5543
  setScreenshots(
5199
5544
  (prev) => prev.map((s, i) => i === annotatingIndex ? dataUrl : s)
5200
5545
  );
@@ -5220,7 +5565,9 @@ function ReportDialog({
5220
5565
  accentText: t2.accentText
5221
5566
  },
5222
5567
  screenshots,
5568
+ pageShot,
5223
5569
  onAddImages: addFiles,
5570
+ onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
5224
5571
  attachmentCount: screenshots.length + attachments.length,
5225
5572
  context: { ...assistContext != null ? assistContext : {}, reportType },
5226
5573
  reportTypeLabel: getTypeLabel(reportType),