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.mjs CHANGED
@@ -849,6 +849,7 @@ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-run
849
849
  var MAX_SCREENSHOT_DATA_URL_LENGTH = 18e5;
850
850
  var QUALITY_LADDER = [0.92, 0.85, 0.75];
851
851
  var DOWNSCALE_FACTOR = 0.7;
852
+ var MAX_ROUNDS = 4;
852
853
  function downscale(canvas, factor) {
853
854
  const next = document.createElement("canvas");
854
855
  next.width = Math.max(1, Math.round(canvas.width * factor));
@@ -862,18 +863,55 @@ function downscale(canvas, factor) {
862
863
  }
863
864
  function encodeScreenshot(canvas, maxLength = MAX_SCREENSHOT_DATA_URL_LENGTH) {
864
865
  let current = canvas;
865
- for (let round = 0; round < 2; round++) {
866
- let encoded = "";
866
+ let encoded = "";
867
+ for (let round = 0; round < MAX_ROUNDS; round++) {
867
868
  for (const quality of QUALITY_LADDER) {
868
869
  encoded = current.toDataURL("image/jpeg", quality);
869
870
  if (encoded.length <= maxLength) return encoded;
870
871
  }
871
- if (round === 1) return encoded;
872
+ if (round === MAX_ROUNDS - 1) break;
872
873
  const smaller = downscale(current, DOWNSCALE_FACTOR);
873
- if (!smaller) return encoded;
874
+ if (!smaller) break;
874
875
  current = smaller;
875
876
  }
876
- return current.toDataURL("image/jpeg", QUALITY_LADDER[0]);
877
+ return encoded;
878
+ }
879
+ function readAsDataUrl(file) {
880
+ return new Promise((resolve, reject) => {
881
+ const reader = new FileReader();
882
+ reader.onload = () => resolve(reader.result);
883
+ reader.onerror = () => reject(reader.error);
884
+ reader.readAsDataURL(file);
885
+ });
886
+ }
887
+ async function reencodeImageFile(file, maxLength) {
888
+ var _a;
889
+ if (typeof createImageBitmap !== "function") return null;
890
+ const bitmap = await createImageBitmap(file);
891
+ try {
892
+ const canvas = document.createElement("canvas");
893
+ canvas.width = bitmap.width;
894
+ canvas.height = bitmap.height;
895
+ const ctx = canvas.getContext("2d");
896
+ if (!ctx) return null;
897
+ ctx.fillStyle = "#ffffff";
898
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
899
+ ctx.drawImage(bitmap, 0, 0);
900
+ return encodeScreenshot(canvas, maxLength);
901
+ } finally {
902
+ (_a = bitmap.close) == null ? void 0 : _a.call(bitmap);
903
+ }
904
+ }
905
+ async function encodeImageFile(file, maxLength = MAX_SCREENSHOT_DATA_URL_LENGTH) {
906
+ const rawLength = Math.ceil(file.size / 3) * 4 + 64;
907
+ if (rawLength > maxLength) {
908
+ try {
909
+ const shrunk = await reencodeImageFile(file, maxLength);
910
+ if (shrunk && shrunk.length < rawLength) return shrunk;
911
+ } catch (e) {
912
+ }
913
+ }
914
+ return readAsDataUrl(file);
877
915
  }
878
916
  var PALETTE = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#a855f7", "#ffffff"];
879
917
  function drawStroke(ctx, s) {
@@ -1164,6 +1202,7 @@ var SEVERITY_LABELS = {
1164
1202
  critical: "Critical"
1165
1203
  };
1166
1204
  var MAX_ASSIST_IMAGES = 3;
1205
+ var MAX_ASSIST_IMAGE_CHARS = 35e5;
1167
1206
  function getTypeLabel(type) {
1168
1207
  switch (type) {
1169
1208
  case "RATING":
@@ -1184,6 +1223,17 @@ function getTypeLabel(type) {
1184
1223
  return "Other";
1185
1224
  }
1186
1225
  }
1226
+ function pickAssistImages(all) {
1227
+ const picked = [];
1228
+ let total = 0;
1229
+ for (let i = all.length - 1; i >= 0 && picked.length < MAX_ASSIST_IMAGES; i--) {
1230
+ const shot = all[i];
1231
+ if (total + shot.length > MAX_ASSIST_IMAGE_CHARS) continue;
1232
+ picked.unshift(shot);
1233
+ total += shot.length;
1234
+ }
1235
+ return picked;
1236
+ }
1187
1237
  var STARTERS = [
1188
1238
  "Something on this page is broken",
1189
1239
  "It did the wrong thing",
@@ -1259,7 +1309,9 @@ function AssistSheet({
1259
1309
  assist,
1260
1310
  theme: t2,
1261
1311
  screenshots,
1312
+ pageShot = null,
1262
1313
  onAddImages,
1314
+ onRemoveImage,
1263
1315
  attachmentCount,
1264
1316
  context,
1265
1317
  reportTypeLabel,
@@ -1301,9 +1353,37 @@ function AssistSheet({
1301
1353
  !typePicked ? "type" : currentTile === "RATING" ? "rating" : "chat"
1302
1354
  );
1303
1355
  const scrollRef = useRef2(null);
1304
- const inputRef = useRef2(null);
1305
1356
  const draftRef = useRef2(null);
1306
1357
  const seededRef = useRef2(false);
1358
+ const eventsRef = useRef2([]);
1359
+ const aiDraftRef = useRef2(null);
1360
+ function logEvent(type, detail) {
1361
+ eventsRef.current.push({ type, at: Date.now(), ...detail ? { detail } : {} });
1362
+ }
1363
+ function takeEvents() {
1364
+ if (eventsRef.current.length === 0) return void 0;
1365
+ const events = eventsRef.current;
1366
+ eventsRef.current = [];
1367
+ return events;
1368
+ }
1369
+ function flushEvents() {
1370
+ if (!conversationId) return;
1371
+ const events = takeEvents();
1372
+ if (!events) return;
1373
+ try {
1374
+ void Promise.resolve(
1375
+ assist({ messages: [], conversationId, events, screenshots: [], context: null })
1376
+ ).catch(() => {
1377
+ });
1378
+ } catch (e) {
1379
+ }
1380
+ }
1381
+ const readShots = new Set(pickAssistImages(screenshots));
1382
+ function leave(type, detail) {
1383
+ logEvent(type, detail);
1384
+ flushEvents();
1385
+ onClose();
1386
+ }
1307
1387
  useEffect2(() => {
1308
1388
  const el = scrollRef.current;
1309
1389
  if (el) el.scrollTop = el.scrollHeight;
@@ -1312,35 +1392,52 @@ function AssistSheet({
1312
1392
  var _a;
1313
1393
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1314
1394
  }, [phase]);
1395
+ function degradeKeepingEvents(events, message) {
1396
+ if (events) eventsRef.current = [...events, ...eventsRef.current];
1397
+ flushEvents();
1398
+ onDegrade(message);
1399
+ }
1315
1400
  async function runTurn(history2) {
1316
- var _a, _b, _c, _d, _e;
1401
+ var _a, _b, _c, _d;
1317
1402
  setBusy(true);
1318
1403
  let result;
1404
+ const events = takeEvents();
1319
1405
  try {
1320
- const sent = screenshots.slice(-MAX_ASSIST_IMAGES);
1406
+ const sent = pickAssistImages(screenshots);
1321
1407
  result = await assist({
1322
- messages: history2,
1408
+ // `kind` only on drafts, so an older server sees the exact shape it
1409
+ // always did — it just learns the draft text now instead of a
1410
+ // placeholder line.
1411
+ messages: history2.map(
1412
+ (m) => m.kind ? { role: m.role, content: m.content, kind: m.kind } : m
1413
+ ),
1323
1414
  conversationId,
1324
1415
  screenshots: sent,
1325
- screenshot: (_a = sent[sent.length - 1]) != null ? _a : null,
1326
- context
1416
+ context: {
1417
+ ...context != null ? context : {},
1418
+ imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached")
1419
+ },
1420
+ events
1327
1421
  });
1328
1422
  } catch (e) {
1329
- onDegrade("The assistant is unavailable \u2014 write your report below and send it as normal.");
1423
+ degradeKeepingEvents(
1424
+ events,
1425
+ "The assistant is unavailable \u2014 write your report below and send it as normal."
1426
+ );
1330
1427
  return;
1331
1428
  } finally {
1332
1429
  setBusy(false);
1333
1430
  }
1334
1431
  if (result.degraded) {
1335
- onDegrade(result.degraded);
1432
+ degradeKeepingEvents(events, result.degraded);
1336
1433
  return;
1337
1434
  }
1338
1435
  if (result.conversationId) {
1339
1436
  setConversationId(result.conversationId);
1340
1437
  onConversationChange == null ? void 0 : onConversationChange(result.conversationId);
1341
1438
  }
1342
- setDuplicate((_b = result.duplicate) != null ? _b : null);
1343
- onDuplicateChange == null ? void 0 : onDuplicateChange((_d = (_c = result.duplicate) == null ? void 0 : _c.number) != null ? _d : null);
1439
+ setDuplicate((_a = result.duplicate) != null ? _a : null);
1440
+ onDuplicateChange == null ? void 0 : onDuplicateChange((_c = (_b = result.duplicate) == null ? void 0 : _b.number) != null ? _c : null);
1344
1441
  if (result.solved) {
1345
1442
  setSolved(result.solved);
1346
1443
  setOptions([]);
@@ -1349,14 +1446,16 @@ function AssistSheet({
1349
1446
  }
1350
1447
  if (result.report) {
1351
1448
  onDescriptionChange(result.report);
1352
- setMessages([...history2, { role: "assistant", content: "Here's your report \u2014 have a read." }]);
1449
+ aiDraftRef.current = result.report;
1450
+ logEvent("draft_shown");
1451
+ setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
1353
1452
  setOptions([]);
1354
1453
  setPhase("draft");
1355
1454
  return;
1356
1455
  }
1357
1456
  if (result.question) {
1358
1457
  setMessages([...history2, { role: "assistant", content: result.question }]);
1359
- setOptions((_e = result.options) != null ? _e : []);
1458
+ setOptions((_d = result.options) != null ? _d : []);
1360
1459
  return;
1361
1460
  }
1362
1461
  onDegrade("The assistant had nothing to add \u2014 write your report below and send it as normal.");
@@ -1375,7 +1474,14 @@ function AssistSheet({
1375
1474
  const value = text.trim();
1376
1475
  if (!value || busy) return;
1377
1476
  setOptions([]);
1378
- const history2 = [...messages, { role: "user", content: value }];
1477
+ const underDraft = phase === "draft";
1478
+ if (underDraft) {
1479
+ const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1480
+ logEvent("keep_chatting", edited ? "after editing" : void 0);
1481
+ setPhase("chat");
1482
+ }
1483
+ const base = underDraft ? withCurrentDraft(messages) : messages;
1484
+ const history2 = [...base, { role: "user", content: value }];
1379
1485
  setMessages(history2);
1380
1486
  setInput("");
1381
1487
  void runTurn(history2);
@@ -1384,7 +1490,56 @@ function AssistSheet({
1384
1490
  onPickType(tile);
1385
1491
  setPhase(tile === "RATING" ? "rating" : "chat");
1386
1492
  }
1493
+ function withCurrentDraft(history2) {
1494
+ const current = description.trim();
1495
+ const next = [...history2];
1496
+ for (let i = next.length - 1; i >= 0; i--) {
1497
+ if (next[i].kind === "report") {
1498
+ if (current) next[i] = { ...next[i], content: current };
1499
+ break;
1500
+ }
1501
+ }
1502
+ return next;
1503
+ }
1504
+ function sendDraft() {
1505
+ const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1506
+ logEvent("draft_sent", edited ? "edited" : "as drafted");
1507
+ flushEvents();
1508
+ onSend();
1509
+ }
1387
1510
  const empty = phase === "chat" && messages.length === 0 && !busy;
1511
+ function renderKnownIssue({ title, footer }) {
1512
+ if (!duplicate) return null;
1513
+ return /* @__PURE__ */ jsxs2(
1514
+ "div",
1515
+ {
1516
+ "data-gg-known-issue": "",
1517
+ style: {
1518
+ display: "flex",
1519
+ flexDirection: "column",
1520
+ gap: "3px",
1521
+ padding: "9px 10px",
1522
+ borderRadius: "8px",
1523
+ border: "1px solid rgba(245,158,11,0.4)",
1524
+ backgroundColor: "rgba(245,158,11,0.08)",
1525
+ fontSize: "12px",
1526
+ lineHeight: 1.5,
1527
+ minWidth: 0
1528
+ },
1529
+ children: [
1530
+ /* @__PURE__ */ jsx2("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: title ? "Our team is already on this" : `Our team is already on #${duplicate.number}` }),
1531
+ title && /* @__PURE__ */ jsxs2("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1532
+ "#",
1533
+ duplicate.number,
1534
+ " ",
1535
+ duplicate.title
1536
+ ] }),
1537
+ duplicate.status && /* @__PURE__ */ jsx2("span", { style: { color: t2.text, fontWeight: 600 }, children: duplicate.status }),
1538
+ footer && /* @__PURE__ */ jsx2("span", { style: { color: t2.textMuted }, children: footer })
1539
+ ]
1540
+ }
1541
+ );
1542
+ }
1388
1543
  const initials = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
1389
1544
  var _a, _b;
1390
1545
  return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
@@ -1409,7 +1564,7 @@ function AssistSheet({
1409
1564
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
1410
1565
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
1411
1566
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
1412
- [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
1567
+ [data-gg-sheet-send]:focus-visible,[data-gg-chip]:focus-visible,[data-gg-assist-remove]:focus-visible{outline:2px solid currentColor;outline-offset:2px}
1413
1568
  ` }),
1414
1569
  /* @__PURE__ */ jsx2(
1415
1570
  "div",
@@ -1429,7 +1584,7 @@ function AssistSheet({
1429
1584
  animation: "gg-sheet-fade .18s ease",
1430
1585
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1431
1586
  },
1432
- onClick: onClose,
1587
+ onClick: () => leave("closed"),
1433
1588
  onPointerDown: (e) => e.stopPropagation(),
1434
1589
  children: /* @__PURE__ */ jsxs2(
1435
1590
  "div",
@@ -1509,7 +1664,7 @@ function AssistSheet({
1509
1664
  {
1510
1665
  type: "button",
1511
1666
  "data-gg-write-myself": "",
1512
- onClick: onClose,
1667
+ onClick: () => leave("write_myself", `header, ${phase}`),
1513
1668
  "aria-label": "Write it myself instead",
1514
1669
  style: {
1515
1670
  flexShrink: 0,
@@ -1765,7 +1920,10 @@ function AssistSheet({
1765
1920
  {
1766
1921
  type: "button",
1767
1922
  "data-gg-chip": "",
1768
- onClick: () => send(s),
1923
+ onClick: () => {
1924
+ logEvent("starter_tap", s);
1925
+ send(s);
1926
+ },
1769
1927
  style: {
1770
1928
  border: `1px solid ${t2.inputBorder}`,
1771
1929
  background: "transparent",
@@ -1785,6 +1943,60 @@ function AssistSheet({
1785
1943
  ] }),
1786
1944
  messages.map((m, i) => {
1787
1945
  const mine = m.role === "user";
1946
+ if (m.kind === "report") {
1947
+ const live = phase === "draft" && i === messages.length - 1;
1948
+ return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
1949
+ /* @__PURE__ */ jsx2(Avatar, { who: "assistant", theme: t2 }),
1950
+ /* @__PURE__ */ jsx2(
1951
+ "div",
1952
+ {
1953
+ "data-gg-draft-bubble": "",
1954
+ style: {
1955
+ maxWidth: "82%",
1956
+ minWidth: 0,
1957
+ padding: "9px 12px",
1958
+ borderRadius: "12px 12px 12px 4px",
1959
+ fontSize: "13px",
1960
+ lineHeight: 1.55,
1961
+ backgroundColor: t2.bgSecondary,
1962
+ color: t2.text,
1963
+ border: live ? "none" : `1px solid ${t2.accent}66`
1964
+ },
1965
+ children: live ? "Here's your report \u2014 have a read." : /* @__PURE__ */ jsxs2(Fragment, { children: [
1966
+ /* @__PURE__ */ jsx2(
1967
+ "span",
1968
+ {
1969
+ style: {
1970
+ display: "block",
1971
+ fontSize: "10px",
1972
+ fontWeight: 700,
1973
+ letterSpacing: ".06em",
1974
+ textTransform: "uppercase",
1975
+ color: t2.accent,
1976
+ marginBottom: "4px"
1977
+ },
1978
+ children: "Draft so far"
1979
+ }
1980
+ ),
1981
+ /* @__PURE__ */ jsx2(
1982
+ "span",
1983
+ {
1984
+ style: {
1985
+ display: "block",
1986
+ whiteSpace: "pre-wrap",
1987
+ wordBreak: "break-word",
1988
+ maxHeight: "160px",
1989
+ overflowY: "auto",
1990
+ color: t2.textMuted
1991
+ },
1992
+ children: m.content
1993
+ }
1994
+ )
1995
+ ] })
1996
+ }
1997
+ )
1998
+ ] }, i);
1999
+ }
1788
2000
  return /* @__PURE__ */ jsxs2(
1789
2001
  "div",
1790
2002
  {
@@ -1818,6 +2030,7 @@ function AssistSheet({
1818
2030
  i
1819
2031
  );
1820
2032
  }),
2033
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ jsx2("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
1821
2034
  options.length > 0 && !busy && phase === "chat" && /* @__PURE__ */ jsx2(
1822
2035
  "div",
1823
2036
  {
@@ -1839,7 +2052,10 @@ function AssistSheet({
1839
2052
  type: "button",
1840
2053
  "data-gg-chip": "",
1841
2054
  "data-gg-option": "",
1842
- onClick: () => send(option),
2055
+ onClick: () => {
2056
+ logEvent("option_tap", option);
2057
+ send(option);
2058
+ },
1843
2059
  style: {
1844
2060
  border: `1px solid ${t2.inputBorder}`,
1845
2061
  background: "transparent",
@@ -1913,35 +2129,13 @@ function AssistSheet({
1913
2129
  textTransform: "uppercase",
1914
2130
  color: t2.accent
1915
2131
  },
1916
- children: "Your report \xB7 edit anything"
1917
- }
1918
- ),
1919
- duplicate && /* @__PURE__ */ jsxs2(
1920
- "div",
1921
- {
1922
- style: {
1923
- display: "flex",
1924
- flexDirection: "column",
1925
- gap: "3px",
1926
- padding: "9px 10px",
1927
- borderRadius: "8px",
1928
- border: "1px solid rgba(245,158,11,0.4)",
1929
- backgroundColor: "rgba(245,158,11,0.08)",
1930
- fontSize: "12px",
1931
- lineHeight: 1.5
1932
- },
1933
- children: [
1934
- /* @__PURE__ */ jsx2("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: "Our team is already on this" }),
1935
- /* @__PURE__ */ jsxs2("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
1936
- "#",
1937
- duplicate.number,
1938
- " ",
1939
- duplicate.title
1940
- ] }),
1941
- /* @__PURE__ */ jsx2("span", { style: { color: t2.textMuted }, children: "Your details will be added to it instead of opening a new issue." })
1942
- ]
2132
+ children: duplicate ? `What we'll add to #${duplicate.number} \xB7 edit anything` : "Your report \xB7 edit anything"
1943
2133
  }
1944
2134
  ),
2135
+ renderKnownIssue({
2136
+ title: true,
2137
+ footer: "Your details will be added to it instead of opening a new issue."
2138
+ }),
1945
2139
  /* @__PURE__ */ jsx2(
1946
2140
  "textarea",
1947
2141
  {
@@ -2036,26 +2230,7 @@ function AssistSheet({
2036
2230
  " ",
2037
2231
  attachmentCount === 1 ? "attachment" : "attachments",
2038
2232
  " will be sent with this."
2039
- ] }),
2040
- /* @__PURE__ */ jsx2(
2041
- "button",
2042
- {
2043
- type: "button",
2044
- onClick: () => setPhase("chat"),
2045
- style: {
2046
- alignSelf: "flex-start",
2047
- border: "none",
2048
- background: "transparent",
2049
- color: t2.textMuted,
2050
- fontSize: "12px",
2051
- fontFamily: "inherit",
2052
- padding: "9px 0",
2053
- cursor: "pointer",
2054
- textDecoration: "underline"
2055
- },
2056
- children: "Not quite \u2014 keep chatting"
2057
- }
2058
- )
2233
+ ] })
2059
2234
  ]
2060
2235
  }
2061
2236
  ),
@@ -2159,192 +2334,276 @@ function AssistSheet({
2159
2334
  },
2160
2335
  children: isSubmitting ? "Sending\u2026" : rating < 1 ? "Pick a star first" : "Send Rating"
2161
2336
  }
2162
- ) : phase === "draft" ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2163
- validationError && /* @__PURE__ */ jsx2(
2164
- "p",
2165
- {
2166
- style: {
2167
- margin: "0 0 8px",
2168
- color: "#ef4444",
2169
- fontSize: "12px"
2170
- },
2171
- children: validationError
2172
- }
2173
- ),
2174
- /* @__PURE__ */ jsx2(
2175
- "button",
2176
- {
2177
- type: "button",
2178
- "data-gg-sheet-send": "",
2179
- onClick: onSend,
2180
- disabled: isSubmitting || !description.trim(),
2181
- style: {
2182
- width: "100%",
2183
- padding: "12px",
2184
- borderRadius: "10px",
2185
- border: "none",
2186
- backgroundColor: isSubmitting || !description.trim() ? t2.bgSecondary : t2.accent,
2187
- color: isSubmitting || !description.trim() ? t2.textMuted : t2.accentText,
2188
- fontSize: "14px",
2189
- fontWeight: 700,
2190
- fontFamily: "inherit",
2191
- cursor: isSubmitting || !description.trim() ? "default" : "pointer"
2192
- },
2193
- children: isSubmitting ? "Sending\u2026" : duplicate ? `Add to #${duplicate.number}` : "Send Report"
2194
- }
2195
- )
2196
- ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
2197
- (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ jsxs2(
2198
- "div",
2199
- {
2200
- style: {
2201
- display: "flex",
2202
- gap: "8px",
2203
- alignItems: "center",
2204
- flexWrap: "wrap",
2205
- marginBottom: "8px"
2206
- },
2207
- children: [
2208
- screenshots.map((shot, i) => {
2209
- const read = i >= screenshots.length - MAX_ASSIST_IMAGES;
2210
- return /* @__PURE__ */ jsx2(
2211
- "img",
2337
+ ) : (
2338
+ // Chat AND draft share this branch, so the chat box is the same
2339
+ // element on both sides of a draft landing — it stays put
2340
+ // instead of remounting, and a draft is something you can
2341
+ // still reply to (#360), not a form that replaced the chat.
2342
+ /* @__PURE__ */ jsxs2(Fragment, { children: [
2343
+ (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ jsxs2(
2344
+ "div",
2345
+ {
2346
+ style: {
2347
+ display: "flex",
2348
+ gap: "8px",
2349
+ alignItems: "center",
2350
+ flexWrap: "wrap",
2351
+ marginBottom: "8px"
2352
+ },
2353
+ children: [
2354
+ screenshots.map((shot, i) => {
2355
+ const read = readShots.has(shot);
2356
+ return /* @__PURE__ */ jsxs2(
2357
+ "div",
2358
+ {
2359
+ style: {
2360
+ position: "relative",
2361
+ width: "40px",
2362
+ height: "40px",
2363
+ flexShrink: 0
2364
+ },
2365
+ children: [
2366
+ /* @__PURE__ */ jsx2(
2367
+ "img",
2368
+ {
2369
+ src: shot,
2370
+ alt: read ? "Attached image the assistant reads" : "Attached image",
2371
+ "data-gg-assist-thumb": "",
2372
+ style: {
2373
+ display: "block",
2374
+ width: "100%",
2375
+ height: "100%",
2376
+ objectFit: "cover",
2377
+ borderRadius: "8px",
2378
+ border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2379
+ opacity: read ? 1 : 0.45,
2380
+ boxSizing: "border-box"
2381
+ }
2382
+ }
2383
+ ),
2384
+ onRemoveImage && /* @__PURE__ */ jsx2(
2385
+ "button",
2386
+ {
2387
+ type: "button",
2388
+ "data-gg-assist-remove": "",
2389
+ "aria-label": `Remove image ${i + 1}`,
2390
+ title: "Remove this image",
2391
+ onClick: () => {
2392
+ logEvent(
2393
+ "image_removed",
2394
+ pageShot && shot === pageShot ? "page capture" : "attached"
2395
+ );
2396
+ onRemoveImage(i);
2397
+ },
2398
+ style: {
2399
+ position: "absolute",
2400
+ top: "-13px",
2401
+ right: "-13px",
2402
+ width: "32px",
2403
+ height: "32px",
2404
+ border: "none",
2405
+ background: "transparent",
2406
+ padding: 0,
2407
+ cursor: "pointer",
2408
+ display: "flex",
2409
+ alignItems: "center",
2410
+ justifyContent: "center",
2411
+ borderRadius: "50%"
2412
+ },
2413
+ children: /* @__PURE__ */ jsx2(
2414
+ "span",
2415
+ {
2416
+ "aria-hidden": "true",
2417
+ style: {
2418
+ width: "20px",
2419
+ height: "20px",
2420
+ boxSizing: "border-box",
2421
+ borderRadius: "50%",
2422
+ border: `2px solid ${t2.bg}`,
2423
+ background: "#ef4444",
2424
+ color: "#fff",
2425
+ display: "flex",
2426
+ alignItems: "center",
2427
+ justifyContent: "center"
2428
+ },
2429
+ children: /* @__PURE__ */ jsx2("svg", { width: "8", height: "8", viewBox: "0 0 10 10", fill: "none", children: /* @__PURE__ */ jsx2(
2430
+ "path",
2431
+ {
2432
+ d: "M1 1L9 9M9 1L1 9",
2433
+ stroke: "currentColor",
2434
+ strokeWidth: "1.6",
2435
+ strokeLinecap: "round"
2436
+ }
2437
+ ) })
2438
+ }
2439
+ )
2440
+ }
2441
+ )
2442
+ ]
2443
+ },
2444
+ `${i}-${shot.slice(-16)}`
2445
+ );
2446
+ }),
2447
+ onAddImages && /* @__PURE__ */ jsxs2(
2448
+ "label",
2212
2449
  {
2213
- src: shot,
2214
- alt: read ? "Attached image the assistant reads" : "Attached image",
2215
- "data-gg-assist-thumb": "",
2450
+ "data-gg-assist-attach": "",
2451
+ title: "Attach an image \u2014 or just paste one",
2216
2452
  style: {
2217
2453
  width: "40px",
2218
2454
  height: "40px",
2219
- objectFit: "cover",
2220
2455
  borderRadius: "8px",
2221
- border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2222
- opacity: read ? 1 : 0.45
2223
- }
2224
- },
2225
- `${i}-${shot.slice(-16)}`
2226
- );
2227
- }),
2228
- onAddImages && /* @__PURE__ */ jsxs2(
2229
- "label",
2230
- {
2231
- "data-gg-assist-attach": "",
2232
- title: "Attach an image \u2014 or just paste one",
2233
- style: {
2234
- width: "40px",
2235
- height: "40px",
2236
- borderRadius: "8px",
2237
- border: `1px dashed ${t2.inputBorder}`,
2238
- display: "inline-flex",
2239
- alignItems: "center",
2240
- justifyContent: "center",
2241
- color: t2.textMuted,
2242
- fontSize: "18px",
2243
- cursor: "pointer",
2244
- flexShrink: 0
2245
- },
2246
- children: [
2247
- "+",
2248
- /* @__PURE__ */ jsx2(
2249
- "input",
2250
- {
2251
- type: "file",
2252
- accept: "image/*",
2253
- multiple: true,
2254
- onChange: (e) => {
2255
- var _a;
2256
- const files = Array.from((_a = e.target.files) != null ? _a : []);
2257
- if (files.length) onAddImages(files);
2258
- e.target.value = "";
2259
- },
2260
- style: { display: "none" }
2261
- }
2262
- )
2263
- ]
2264
- }
2265
- ),
2266
- screenshots.length > MAX_ASSIST_IMAGES && /* @__PURE__ */ jsxs2("span", { style: { color: t2.textMuted, fontSize: "11px" }, children: [
2267
- "newest ",
2268
- MAX_ASSIST_IMAGES,
2269
- " read"
2270
- ] })
2271
- ]
2272
- }
2273
- ),
2274
- /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
2275
- /* @__PURE__ */ jsx2(
2276
- "textarea",
2277
- {
2278
- ref: inputRef,
2279
- value: input,
2280
- onChange: (e) => setInput(e.target.value),
2281
- onKeyDown: (e) => {
2282
- if (e.key === "Enter" && !e.shiftKey) {
2283
- e.preventDefault();
2284
- send(input);
2285
- }
2286
- },
2287
- placeholder: "Type your answer\u2026",
2288
- rows: 1,
2289
- disabled: busy,
2290
- autoFocus: true,
2291
- style: {
2292
- flex: 1,
2293
- minHeight: "42px",
2294
- maxHeight: "120px",
2295
- padding: "11px 12px",
2296
- borderRadius: "10px",
2297
- border: `1px solid ${t2.inputBorder}`,
2298
- backgroundColor: t2.inputBg,
2299
- color: t2.text,
2300
- fontSize: "13px",
2301
- lineHeight: 1.4,
2302
- fontFamily: "inherit",
2303
- resize: "none",
2304
- outline: "none",
2305
- boxSizing: "border-box"
2306
- }
2456
+ border: `1px dashed ${t2.inputBorder}`,
2457
+ display: "inline-flex",
2458
+ alignItems: "center",
2459
+ justifyContent: "center",
2460
+ color: t2.textMuted,
2461
+ fontSize: "18px",
2462
+ cursor: "pointer",
2463
+ flexShrink: 0
2464
+ },
2465
+ children: [
2466
+ "+",
2467
+ /* @__PURE__ */ jsx2(
2468
+ "input",
2469
+ {
2470
+ type: "file",
2471
+ accept: "image/*",
2472
+ multiple: true,
2473
+ onChange: (e) => {
2474
+ var _a;
2475
+ const files = Array.from((_a = e.target.files) != null ? _a : []);
2476
+ if (files.length) {
2477
+ logEvent("image_added", String(files.length));
2478
+ onAddImages(files);
2479
+ }
2480
+ e.target.value = "";
2481
+ },
2482
+ style: { display: "none" }
2483
+ }
2484
+ )
2485
+ ]
2486
+ }
2487
+ ),
2488
+ readShots.size < screenshots.length && /* @__PURE__ */ jsxs2("span", { style: { color: t2.textMuted, fontSize: "11px" }, children: [
2489
+ readShots.size,
2490
+ " of ",
2491
+ screenshots.length,
2492
+ " read"
2493
+ ] })
2494
+ ]
2307
2495
  }
2308
2496
  ),
2309
- /* @__PURE__ */ jsx2(
2310
- "button",
2311
- {
2312
- type: "button",
2313
- "data-gg-sheet-send": "",
2314
- onClick: () => send(input),
2315
- disabled: busy || !input.trim(),
2316
- "aria-label": "Send message",
2317
- "data-gg-send-message": "",
2318
- style: {
2319
- flexShrink: 0,
2320
- width: "42px",
2321
- height: "42px",
2322
- borderRadius: "10px",
2323
- border: "none",
2324
- display: "inline-flex",
2325
- alignItems: "center",
2326
- justifyContent: "center",
2327
- backgroundColor: busy || !input.trim() ? t2.bgSecondary : t2.accent,
2328
- color: busy || !input.trim() ? t2.textMuted : t2.accentText,
2329
- cursor: busy || !input.trim() ? "default" : "pointer"
2330
- },
2331
- children: /* @__PURE__ */ jsx2("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(
2332
- "path",
2333
- {
2334
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
2335
- fill: "currentColor"
2497
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
2498
+ /* @__PURE__ */ jsx2(
2499
+ "textarea",
2500
+ {
2501
+ value: input,
2502
+ onChange: (e) => setInput(e.target.value),
2503
+ onKeyDown: (e) => {
2504
+ if (e.key === "Enter" && !e.shiftKey) {
2505
+ e.preventDefault();
2506
+ send(input);
2507
+ }
2508
+ },
2509
+ placeholder: phase === "draft" ? "Anything to change? Tell me here\u2026" : "Type your answer\u2026",
2510
+ rows: 1,
2511
+ disabled: busy,
2512
+ autoFocus: true,
2513
+ style: {
2514
+ flex: 1,
2515
+ minHeight: "42px",
2516
+ maxHeight: "120px",
2517
+ padding: "11px 12px",
2518
+ borderRadius: "10px",
2519
+ border: `1px solid ${t2.inputBorder}`,
2520
+ backgroundColor: t2.inputBg,
2521
+ color: t2.text,
2522
+ fontSize: "13px",
2523
+ lineHeight: 1.4,
2524
+ fontFamily: "inherit",
2525
+ resize: "none",
2526
+ outline: "none",
2527
+ boxSizing: "border-box"
2336
2528
  }
2337
- ) })
2338
- }
2339
- )
2529
+ }
2530
+ ),
2531
+ /* @__PURE__ */ jsx2(
2532
+ "button",
2533
+ {
2534
+ type: "button",
2535
+ "data-gg-sheet-send": "",
2536
+ onClick: () => send(input),
2537
+ disabled: busy || !input.trim(),
2538
+ "aria-label": "Send message",
2539
+ "data-gg-send-message": "",
2540
+ style: {
2541
+ flexShrink: 0,
2542
+ width: "42px",
2543
+ height: "42px",
2544
+ borderRadius: "10px",
2545
+ border: "none",
2546
+ display: "inline-flex",
2547
+ alignItems: "center",
2548
+ justifyContent: "center",
2549
+ backgroundColor: busy || !input.trim() ? t2.bgSecondary : t2.accent,
2550
+ color: busy || !input.trim() ? t2.textMuted : t2.accentText,
2551
+ cursor: busy || !input.trim() ? "default" : "pointer"
2552
+ },
2553
+ children: /* @__PURE__ */ jsx2("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(
2554
+ "path",
2555
+ {
2556
+ d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
2557
+ fill: "currentColor"
2558
+ }
2559
+ ) })
2560
+ }
2561
+ )
2562
+ ] }),
2563
+ phase === "draft" && /* @__PURE__ */ jsxs2(Fragment, { children: [
2564
+ validationError && /* @__PURE__ */ jsx2(
2565
+ "p",
2566
+ {
2567
+ style: {
2568
+ margin: "8px 0 0",
2569
+ color: "#ef4444",
2570
+ fontSize: "12px"
2571
+ },
2572
+ children: validationError
2573
+ }
2574
+ ),
2575
+ /* @__PURE__ */ jsx2(
2576
+ "button",
2577
+ {
2578
+ type: "button",
2579
+ "data-gg-sheet-send": "",
2580
+ onClick: sendDraft,
2581
+ disabled: isSubmitting || !description.trim(),
2582
+ style: {
2583
+ width: "100%",
2584
+ marginTop: "8px",
2585
+ padding: "12px",
2586
+ borderRadius: "10px",
2587
+ border: "none",
2588
+ backgroundColor: isSubmitting || !description.trim() ? t2.bgSecondary : t2.accent,
2589
+ color: isSubmitting || !description.trim() ? t2.textMuted : t2.accentText,
2590
+ fontSize: "14px",
2591
+ fontWeight: 700,
2592
+ fontFamily: "inherit",
2593
+ cursor: isSubmitting || !description.trim() ? "default" : "pointer"
2594
+ },
2595
+ children: isSubmitting ? "Sending\u2026" : duplicate ? `Add to #${duplicate.number}` : "Send Report"
2596
+ }
2597
+ )
2598
+ ] })
2340
2599
  ] })
2341
- ] }),
2600
+ ),
2342
2601
  phase === "chat" && messages.length > 0 && /* @__PURE__ */ jsx2(
2343
2602
  "button",
2344
2603
  {
2345
2604
  type: "button",
2346
2605
  "data-gg-write-myself": "",
2347
- onClick: onClose,
2606
+ onClick: () => leave("write_myself", "footer"),
2348
2607
  style: {
2349
2608
  display: "block",
2350
2609
  margin: "8px auto 0",
@@ -2416,6 +2675,107 @@ var ATTACHMENT_ACCEPT = [
2416
2675
  ".xlsx",
2417
2676
  ".zip"
2418
2677
  ].join(",");
2678
+ var MAX_SCALE = 2;
2679
+ var FRAME_TIMEOUT_MS = 3e3;
2680
+ function canCaptureTabPixels() {
2681
+ var _a;
2682
+ try {
2683
+ return typeof navigator !== "undefined" && typeof ((_a = navigator.mediaDevices) == null ? void 0 : _a.getDisplayMedia) === "function" && typeof globalThis.CaptureController !== "undefined";
2684
+ } catch (e) {
2685
+ return false;
2686
+ }
2687
+ }
2688
+ var HAVE_CURRENT_DATA = 2;
2689
+ function waitForFrame(video) {
2690
+ if (video.readyState >= HAVE_CURRENT_DATA && video.videoWidth > 0) {
2691
+ return Promise.resolve(true);
2692
+ }
2693
+ return new Promise((resolve) => {
2694
+ const timer = setTimeout(() => resolve(false), FRAME_TIMEOUT_MS);
2695
+ const done = () => {
2696
+ clearTimeout(timer);
2697
+ resolve(true);
2698
+ };
2699
+ if (typeof video.requestVideoFrameCallback === "function") {
2700
+ video.requestVideoFrameCallback(done);
2701
+ } else if (video.readyState >= 2) {
2702
+ done();
2703
+ } else {
2704
+ video.addEventListener("loadeddata", done, { once: true });
2705
+ }
2706
+ });
2707
+ }
2708
+ async function grabFrame(stream) {
2709
+ const video = document.createElement("video");
2710
+ video.muted = true;
2711
+ video.playsInline = true;
2712
+ video.srcObject = stream;
2713
+ try {
2714
+ await video.play();
2715
+ if (!await waitForFrame(video)) return null;
2716
+ if (!video.videoWidth || !video.videoHeight) return null;
2717
+ const maxWidth = Math.max(1, window.innerWidth) * MAX_SCALE;
2718
+ const factor = Math.min(1, maxWidth / video.videoWidth);
2719
+ const canvas = document.createElement("canvas");
2720
+ canvas.width = Math.round(video.videoWidth * factor);
2721
+ canvas.height = Math.round(video.videoHeight * factor);
2722
+ const ctx = canvas.getContext("2d");
2723
+ if (!ctx) return null;
2724
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2725
+ return canvas;
2726
+ } finally {
2727
+ video.srcObject = null;
2728
+ }
2729
+ }
2730
+ async function captureViaTabPixels() {
2731
+ var _a, _b;
2732
+ if (!canCaptureTabPixels()) return null;
2733
+ let stream = null;
2734
+ try {
2735
+ const options = {
2736
+ video: { displaySurface: "browser" },
2737
+ audio: false,
2738
+ preferCurrentTab: true,
2739
+ selfBrowserSurface: "include",
2740
+ surfaceSwitching: "exclude",
2741
+ monitorTypeSurfaces: "exclude"
2742
+ };
2743
+ stream = await navigator.mediaDevices.getDisplayMedia(options);
2744
+ const surface = (_b = (_a = stream.getVideoTracks()[0]) == null ? void 0 : _a.getSettings()) == null ? void 0 : _b.displaySurface;
2745
+ if (surface && surface !== "browser") return null;
2746
+ const canvas = await grabFrame(stream);
2747
+ return canvas ? encodeScreenshot(canvas) : null;
2748
+ } catch (e) {
2749
+ return null;
2750
+ } finally {
2751
+ stream == null ? void 0 : stream.getTracks().forEach((track) => track.stop());
2752
+ }
2753
+ }
2754
+ async function captureViaHtml2Canvas() {
2755
+ try {
2756
+ const { default: html2canvas } = await import("html2canvas-pro");
2757
+ const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), MAX_SCALE);
2758
+ const canvas = await html2canvas(document.body, {
2759
+ scale,
2760
+ logging: false,
2761
+ useCORS: true,
2762
+ allowTaint: true,
2763
+ x: window.scrollX,
2764
+ y: window.scrollY,
2765
+ width: window.innerWidth,
2766
+ height: window.innerHeight,
2767
+ windowWidth: window.innerWidth,
2768
+ windowHeight: window.innerHeight
2769
+ });
2770
+ return encodeScreenshot(canvas);
2771
+ } catch (e) {
2772
+ return null;
2773
+ }
2774
+ }
2775
+ async function captureDefaultScreenshot() {
2776
+ var _a;
2777
+ return (_a = await captureViaTabPixels()) != null ? _a : await captureViaHtml2Canvas();
2778
+ }
2419
2779
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
2420
2780
  function useIsDark() {
2421
2781
  if (typeof window === "undefined") return true;
@@ -2690,27 +3050,6 @@ function isLowQualityText(text) {
2690
3050
  }
2691
3051
  return null;
2692
3052
  }
2693
- async function captureViaHtml2Canvas() {
2694
- try {
2695
- const { default: html2canvas } = await import("html2canvas-pro");
2696
- const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
2697
- const canvas = await html2canvas(document.body, {
2698
- scale,
2699
- logging: false,
2700
- useCORS: true,
2701
- allowTaint: true,
2702
- x: window.scrollX,
2703
- y: window.scrollY,
2704
- width: window.innerWidth,
2705
- height: window.innerHeight,
2706
- windowWidth: window.innerWidth,
2707
- windowHeight: window.innerHeight
2708
- });
2709
- return encodeScreenshot(canvas);
2710
- } catch (e) {
2711
- return null;
2712
- }
2713
- }
2714
3053
  var MIC_MESSAGES = {
2715
3054
  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.",
2716
3055
  notFound: "No microphone found. Connect one and try again, or type your report instead.",
@@ -2748,7 +3087,7 @@ function ReportDialog({
2748
3087
  transcribeAudio: transcribeAudio2,
2749
3088
  types,
2750
3089
  showSeverity = true,
2751
- captureScreenshot = captureViaHtml2Canvas,
3090
+ captureScreenshot = captureDefaultScreenshot,
2752
3091
  reporter,
2753
3092
  onClose,
2754
3093
  headerSlot
@@ -2778,6 +3117,7 @@ function ReportDialog({
2778
3117
  const [isSubmitting, setIsSubmitting] = useState3(false);
2779
3118
  const [submitted, setSubmitted] = useState3(false);
2780
3119
  const [screenshots, setScreenshots] = useState3([]);
3120
+ const [pageShot, setPageShot] = useState3(null);
2781
3121
  const [attachments, setAttachments] = useState3([]);
2782
3122
  const [previewIndex, setPreviewIndex] = useState3(null);
2783
3123
  const [annotatingIndex, setAnnotatingIndex] = useState3(null);
@@ -2916,20 +3256,23 @@ function ReportDialog({
2916
3256
  e.target.value = "";
2917
3257
  };
2918
3258
  const addFiles = (files) => {
2919
- files.forEach((file) => {
3259
+ const images = files.filter((file) => file.type.startsWith("image/"));
3260
+ if (images.length > 0) {
3261
+ void Promise.all(images.map((file) => encodeImageFile(file).catch(() => null))).then(
3262
+ (encoded) => {
3263
+ const added = encoded.filter((url) => !!url);
3264
+ if (added.length > 0) setScreenshots((prev) => [...prev, ...added]);
3265
+ }
3266
+ );
3267
+ }
3268
+ files.filter((file) => !file.type.startsWith("image/")).forEach((file) => {
2920
3269
  const reader = new FileReader();
2921
- if (file.type.startsWith("image/")) {
2922
- reader.onload = () => {
2923
- setScreenshots((prev) => [...prev, reader.result]);
2924
- };
2925
- } else {
2926
- reader.onload = () => {
2927
- setAttachments((prev) => [
2928
- ...prev,
2929
- { name: file.name, size: file.size, dataUrl: reader.result }
2930
- ]);
2931
- };
2932
- }
3270
+ reader.onload = () => {
3271
+ setAttachments((prev) => [
3272
+ ...prev,
3273
+ { name: file.name, size: file.size, dataUrl: reader.result }
3274
+ ]);
3275
+ };
2933
3276
  reader.readAsDataURL(file);
2934
3277
  });
2935
3278
  };
@@ -2943,6 +3286,7 @@ function ReportDialog({
2943
3286
  setStep(2);
2944
3287
  }
2945
3288
  const shot = await captureScreenshot();
3289
+ setPageShot(shot != null ? shot : null);
2946
3290
  if (shot) setScreenshots([shot]);
2947
3291
  if (assist && !assistUsed) setAssistOpen(true);
2948
3292
  setIsOpen(true);
@@ -5140,6 +5484,7 @@ function ReportDialog({
5140
5484
  imageSrc: screenshots[annotatingIndex],
5141
5485
  onCancel: () => setAnnotatingIndex(null),
5142
5486
  onSave: (dataUrl) => {
5487
+ if (screenshots[annotatingIndex] === pageShot) setPageShot(dataUrl);
5143
5488
  setScreenshots(
5144
5489
  (prev) => prev.map((s, i) => i === annotatingIndex ? dataUrl : s)
5145
5490
  );
@@ -5165,7 +5510,9 @@ function ReportDialog({
5165
5510
  accentText: t2.accentText
5166
5511
  },
5167
5512
  screenshots,
5513
+ pageShot,
5168
5514
  onAddImages: addFiles,
5515
+ onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
5169
5516
  attachmentCount: screenshots.length + attachments.length,
5170
5517
  context: { ...assistContext != null ? assistContext : {}, reportType },
5171
5518
  reportTypeLabel: getTypeLabel(reportType),