glitchgrab 1.41.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",
@@ -1316,6 +1366,7 @@ function AssistSheet({
1316
1366
  screenshots,
1317
1367
  pageShot = null,
1318
1368
  onAddImages,
1369
+ onRemoveImage,
1319
1370
  attachmentCount,
1320
1371
  context,
1321
1372
  reportTypeLabel,
@@ -1357,7 +1408,6 @@ function AssistSheet({
1357
1408
  !typePicked ? "type" : currentTile === "RATING" ? "rating" : "chat"
1358
1409
  );
1359
1410
  const scrollRef = (0, import_react3.useRef)(null);
1360
- const inputRef = (0, import_react3.useRef)(null);
1361
1411
  const draftRef = (0, import_react3.useRef)(null);
1362
1412
  const seededRef = (0, import_react3.useRef)(false);
1363
1413
  const eventsRef = (0, import_react3.useRef)([]);
@@ -1383,6 +1433,7 @@ function AssistSheet({
1383
1433
  } catch (e) {
1384
1434
  }
1385
1435
  }
1436
+ const readShots = new Set(pickAssistImages(screenshots));
1386
1437
  function leave(type, detail) {
1387
1438
  logEvent(type, detail);
1388
1439
  flushEvents();
@@ -1402,12 +1453,12 @@ function AssistSheet({
1402
1453
  onDegrade(message);
1403
1454
  }
1404
1455
  async function runTurn(history2) {
1405
- var _a, _b, _c, _d, _e;
1456
+ var _a, _b, _c, _d;
1406
1457
  setBusy(true);
1407
1458
  let result;
1408
1459
  const events = takeEvents();
1409
1460
  try {
1410
- const sent = screenshots.slice(-MAX_ASSIST_IMAGES);
1461
+ const sent = pickAssistImages(screenshots);
1411
1462
  result = await assist({
1412
1463
  // `kind` only on drafts, so an older server sees the exact shape it
1413
1464
  // always did — it just learns the draft text now instead of a
@@ -1417,7 +1468,6 @@ function AssistSheet({
1417
1468
  ),
1418
1469
  conversationId,
1419
1470
  screenshots: sent,
1420
- screenshot: (_a = sent[sent.length - 1]) != null ? _a : null,
1421
1471
  context: {
1422
1472
  ...context != null ? context : {},
1423
1473
  imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached")
@@ -1441,8 +1491,8 @@ function AssistSheet({
1441
1491
  setConversationId(result.conversationId);
1442
1492
  onConversationChange == null ? void 0 : onConversationChange(result.conversationId);
1443
1493
  }
1444
- setDuplicate((_b = result.duplicate) != null ? _b : null);
1445
- 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);
1446
1496
  if (result.solved) {
1447
1497
  setSolved(result.solved);
1448
1498
  setOptions([]);
@@ -1460,7 +1510,7 @@ function AssistSheet({
1460
1510
  }
1461
1511
  if (result.question) {
1462
1512
  setMessages([...history2, { role: "assistant", content: result.question }]);
1463
- setOptions((_e = result.options) != null ? _e : []);
1513
+ setOptions((_d = result.options) != null ? _d : []);
1464
1514
  return;
1465
1515
  }
1466
1516
  onDegrade("The assistant had nothing to add \u2014 write your report below and send it as normal.");
@@ -1479,7 +1529,14 @@ function AssistSheet({
1479
1529
  const value = text.trim();
1480
1530
  if (!value || busy) return;
1481
1531
  setOptions([]);
1482
- 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 }];
1483
1540
  setMessages(history2);
1484
1541
  setInput("");
1485
1542
  void runTurn(history2);
@@ -1488,27 +1545,16 @@ function AssistSheet({
1488
1545
  onPickType(tile);
1489
1546
  setPhase(tile === "RATING" ? "rating" : "chat");
1490
1547
  }
1491
- function keepChatting() {
1548
+ function withCurrentDraft(history2) {
1492
1549
  const current = description.trim();
1493
- logEvent(
1494
- "keep_chatting",
1495
- aiDraftRef.current && current !== aiDraftRef.current.trim() ? "after editing" : void 0
1496
- );
1497
- setMessages((prev) => {
1498
- const next = [...prev];
1499
- for (let i = next.length - 1; i >= 0; i--) {
1500
- if (next[i].kind === "report") {
1501
- if (current) next[i] = { ...next[i], content: current };
1502
- break;
1503
- }
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;
1504
1555
  }
1505
- return next;
1506
- });
1507
- setPhase("chat");
1508
- requestAnimationFrame(() => {
1509
- var _a;
1510
- return (_a = inputRef.current) == null ? void 0 : _a.focus();
1511
- });
1556
+ }
1557
+ return next;
1512
1558
  }
1513
1559
  function sendDraft() {
1514
1560
  const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
@@ -1516,9 +1562,39 @@ function AssistSheet({
1516
1562
  flushEvents();
1517
1563
  onSend();
1518
1564
  }
1519
- const lastMessage = messages[messages.length - 1];
1520
- const correcting = phase === "chat" && (lastMessage == null ? void 0 : lastMessage.kind) === "report";
1521
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
+ }
1522
1598
  const initials = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
1523
1599
  var _a, _b;
1524
1600
  return (_b = (_a = w[0]) == null ? void 0 : _a.toUpperCase()) != null ? _b : "";
@@ -1543,7 +1619,7 @@ function AssistSheet({
1543
1619
  @keyframes gg-sheet-up{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
1544
1620
  @keyframes gg-sheet-fade{from{opacity:0}to{opacity:1}}
1545
1621
  @keyframes gg-dot{0%,80%,100%{transform:translateY(0);opacity:.4}40%{transform:translateY(-3px);opacity:1}}
1546
- [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}
1547
1623
  ` }),
1548
1624
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1549
1625
  "div",
@@ -2009,6 +2085,7 @@ function AssistSheet({
2009
2085
  i
2010
2086
  );
2011
2087
  }),
2088
+ duplicate && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { paddingLeft: "32px", minWidth: 0 }, children: renderKnownIssue({ title: false }) }),
2012
2089
  options.length > 0 && !busy && phase === "chat" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2013
2090
  "div",
2014
2091
  {
@@ -2107,35 +2184,13 @@ function AssistSheet({
2107
2184
  textTransform: "uppercase",
2108
2185
  color: t2.accent
2109
2186
  },
2110
- children: "Your report \xB7 edit anything"
2111
- }
2112
- ),
2113
- duplicate && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2114
- "div",
2115
- {
2116
- style: {
2117
- display: "flex",
2118
- flexDirection: "column",
2119
- gap: "3px",
2120
- padding: "9px 10px",
2121
- borderRadius: "8px",
2122
- border: "1px solid rgba(245,158,11,0.4)",
2123
- backgroundColor: "rgba(245,158,11,0.08)",
2124
- fontSize: "12px",
2125
- lineHeight: 1.5
2126
- },
2127
- children: [
2128
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: "#f59e0b", fontWeight: 600 }, children: "Our team is already on this" }),
2129
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, wordBreak: "break-word" }, children: [
2130
- "#",
2131
- duplicate.number,
2132
- " ",
2133
- duplicate.title
2134
- ] }),
2135
- /* @__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." })
2136
- ]
2187
+ children: duplicate ? `What we'll add to #${duplicate.number} \xB7 edit anything` : "Your report \xB7 edit anything"
2137
2188
  }
2138
2189
  ),
2190
+ renderKnownIssue({
2191
+ title: true,
2192
+ footer: "Your details will be added to it instead of opening a new issue."
2193
+ }),
2139
2194
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2140
2195
  "textarea",
2141
2196
  {
@@ -2230,26 +2285,7 @@ function AssistSheet({
2230
2285
  " ",
2231
2286
  attachmentCount === 1 ? "attachment" : "attachments",
2232
2287
  " will be sent with this."
2233
- ] }),
2234
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2235
- "button",
2236
- {
2237
- type: "button",
2238
- onClick: keepChatting,
2239
- style: {
2240
- alignSelf: "flex-start",
2241
- border: "none",
2242
- background: "transparent",
2243
- color: t2.textMuted,
2244
- fontSize: "12px",
2245
- fontFamily: "inherit",
2246
- padding: "9px 0",
2247
- cursor: "pointer",
2248
- textDecoration: "underline"
2249
- },
2250
- children: "Not quite \u2014 keep chatting"
2251
- }
2252
- )
2288
+ ] })
2253
2289
  ]
2254
2290
  }
2255
2291
  ),
@@ -2353,189 +2389,270 @@ function AssistSheet({
2353
2389
  },
2354
2390
  children: isSubmitting ? "Sending\u2026" : rating < 1 ? "Pick a star first" : "Send Rating"
2355
2391
  }
2356
- ) : phase === "draft" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2357
- validationError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2358
- "p",
2359
- {
2360
- style: {
2361
- margin: "0 0 8px",
2362
- color: "#ef4444",
2363
- fontSize: "12px"
2364
- },
2365
- children: validationError
2366
- }
2367
- ),
2368
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2369
- "button",
2370
- {
2371
- type: "button",
2372
- "data-gg-sheet-send": "",
2373
- onClick: sendDraft,
2374
- disabled: isSubmitting || !description.trim(),
2375
- style: {
2376
- width: "100%",
2377
- padding: "12px",
2378
- borderRadius: "10px",
2379
- border: "none",
2380
- backgroundColor: isSubmitting || !description.trim() ? t2.bgSecondary : t2.accent,
2381
- color: isSubmitting || !description.trim() ? t2.textMuted : t2.accentText,
2382
- fontSize: "14px",
2383
- fontWeight: 700,
2384
- fontFamily: "inherit",
2385
- cursor: isSubmitting || !description.trim() ? "default" : "pointer"
2386
- },
2387
- children: isSubmitting ? "Sending\u2026" : duplicate ? `Add to #${duplicate.number}` : "Send Report"
2388
- }
2389
- )
2390
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2391
- (screenshots.length > 0 || onAddImages) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2392
- "div",
2393
- {
2394
- style: {
2395
- display: "flex",
2396
- gap: "8px",
2397
- alignItems: "center",
2398
- flexWrap: "wrap",
2399
- marginBottom: "8px"
2400
- },
2401
- children: [
2402
- screenshots.map((shot, i) => {
2403
- const read = i >= screenshots.length - MAX_ASSIST_IMAGES;
2404
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2405
- "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",
2406
2504
  {
2407
- src: shot,
2408
- alt: read ? "Attached image the assistant reads" : "Attached image",
2409
- "data-gg-assist-thumb": "",
2505
+ "data-gg-assist-attach": "",
2506
+ title: "Attach an image \u2014 or just paste one",
2410
2507
  style: {
2411
2508
  width: "40px",
2412
2509
  height: "40px",
2413
- objectFit: "cover",
2414
2510
  borderRadius: "8px",
2415
- border: `1px solid ${read ? t2.accent : t2.inputBorder}`,
2416
- opacity: read ? 1 : 0.45
2417
- }
2418
- },
2419
- `${i}-${shot.slice(-16)}`
2420
- );
2421
- }),
2422
- onAddImages && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2423
- "label",
2424
- {
2425
- "data-gg-assist-attach": "",
2426
- title: "Attach an image \u2014 or just paste one",
2427
- style: {
2428
- width: "40px",
2429
- height: "40px",
2430
- borderRadius: "8px",
2431
- border: `1px dashed ${t2.inputBorder}`,
2432
- display: "inline-flex",
2433
- alignItems: "center",
2434
- justifyContent: "center",
2435
- color: t2.textMuted,
2436
- fontSize: "18px",
2437
- cursor: "pointer",
2438
- flexShrink: 0
2439
- },
2440
- children: [
2441
- "+",
2442
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2443
- "input",
2444
- {
2445
- type: "file",
2446
- accept: "image/*",
2447
- multiple: true,
2448
- onChange: (e) => {
2449
- var _a;
2450
- const files = Array.from((_a = e.target.files) != null ? _a : []);
2451
- if (files.length) {
2452
- logEvent("image_added", String(files.length));
2453
- onAddImages(files);
2454
- }
2455
- e.target.value = "";
2456
- },
2457
- style: { display: "none" }
2458
- }
2459
- )
2460
- ]
2461
- }
2462
- ),
2463
- screenshots.length > MAX_ASSIST_IMAGES && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: t2.textMuted, fontSize: "11px" }, children: [
2464
- "newest ",
2465
- MAX_ASSIST_IMAGES,
2466
- " read"
2467
- ] })
2468
- ]
2469
- }
2470
- ),
2471
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px", alignItems: "flex-end" }, children: [
2472
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2473
- "textarea",
2474
- {
2475
- ref: inputRef,
2476
- value: input,
2477
- onChange: (e) => setInput(e.target.value),
2478
- onKeyDown: (e) => {
2479
- if (e.key === "Enter" && !e.shiftKey) {
2480
- e.preventDefault();
2481
- send(input);
2482
- }
2483
- },
2484
- placeholder: correcting ? "What should change in the draft?" : "Type your answer\u2026",
2485
- rows: 1,
2486
- disabled: busy,
2487
- autoFocus: true,
2488
- style: {
2489
- flex: 1,
2490
- minHeight: "42px",
2491
- maxHeight: "120px",
2492
- padding: "11px 12px",
2493
- borderRadius: "10px",
2494
- border: `1px solid ${t2.inputBorder}`,
2495
- backgroundColor: t2.inputBg,
2496
- color: t2.text,
2497
- fontSize: "13px",
2498
- lineHeight: 1.4,
2499
- fontFamily: "inherit",
2500
- resize: "none",
2501
- outline: "none",
2502
- boxSizing: "border-box"
2503
- }
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
+ ]
2504
2550
  }
2505
2551
  ),
2506
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2507
- "button",
2508
- {
2509
- type: "button",
2510
- "data-gg-sheet-send": "",
2511
- onClick: () => send(input),
2512
- disabled: busy || !input.trim(),
2513
- "aria-label": "Send message",
2514
- "data-gg-send-message": "",
2515
- style: {
2516
- flexShrink: 0,
2517
- width: "42px",
2518
- height: "42px",
2519
- borderRadius: "10px",
2520
- border: "none",
2521
- display: "inline-flex",
2522
- alignItems: "center",
2523
- justifyContent: "center",
2524
- backgroundColor: busy || !input.trim() ? t2.bgSecondary : t2.accent,
2525
- color: busy || !input.trim() ? t2.textMuted : t2.accentText,
2526
- cursor: busy || !input.trim() ? "default" : "pointer"
2527
- },
2528
- 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)(
2529
- "path",
2530
- {
2531
- d: "M3 20.5L21.5 12 3 3.5V10l12 2-12 2v6.5z",
2532
- 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"
2533
2583
  }
2534
- ) })
2535
- }
2536
- )
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
+ ] })
2537
2654
  ] })
2538
- ] }),
2655
+ ),
2539
2656
  phase === "chat" && messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2540
2657
  "button",
2541
2658
  {
@@ -3194,20 +3311,23 @@ function ReportDialog({
3194
3311
  e.target.value = "";
3195
3312
  };
3196
3313
  const addFiles = (files) => {
3197
- 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) => {
3198
3324
  const reader = new FileReader();
3199
- if (file.type.startsWith("image/")) {
3200
- reader.onload = () => {
3201
- setScreenshots((prev) => [...prev, reader.result]);
3202
- };
3203
- } else {
3204
- reader.onload = () => {
3205
- setAttachments((prev) => [
3206
- ...prev,
3207
- { name: file.name, size: file.size, dataUrl: reader.result }
3208
- ]);
3209
- };
3210
- }
3325
+ reader.onload = () => {
3326
+ setAttachments((prev) => [
3327
+ ...prev,
3328
+ { name: file.name, size: file.size, dataUrl: reader.result }
3329
+ ]);
3330
+ };
3211
3331
  reader.readAsDataURL(file);
3212
3332
  });
3213
3333
  };
@@ -5447,6 +5567,7 @@ function ReportDialog({
5447
5567
  screenshots,
5448
5568
  pageShot,
5449
5569
  onAddImages: addFiles,
5570
+ onRemoveImage: (index) => setScreenshots((prev) => prev.filter((_, i) => i !== index)),
5450
5571
  attachmentCount: screenshots.length + attachments.length,
5451
5572
  context: { ...assistContext != null ? assistContext : {}, reportType },
5452
5573
  reportTypeLabel: getTypeLabel(reportType),