glitchgrab 1.40.0 → 1.41.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
@@ -1259,6 +1259,7 @@ function AssistSheet({
1259
1259
  assist,
1260
1260
  theme: t2,
1261
1261
  screenshots,
1262
+ pageShot = null,
1262
1263
  onAddImages,
1263
1264
  attachmentCount,
1264
1265
  context,
@@ -1304,6 +1305,34 @@ function AssistSheet({
1304
1305
  const inputRef = useRef2(null);
1305
1306
  const draftRef = useRef2(null);
1306
1307
  const seededRef = useRef2(false);
1308
+ const eventsRef = useRef2([]);
1309
+ const aiDraftRef = useRef2(null);
1310
+ function logEvent(type, detail) {
1311
+ eventsRef.current.push({ type, at: Date.now(), ...detail ? { detail } : {} });
1312
+ }
1313
+ function takeEvents() {
1314
+ if (eventsRef.current.length === 0) return void 0;
1315
+ const events = eventsRef.current;
1316
+ eventsRef.current = [];
1317
+ return events;
1318
+ }
1319
+ function flushEvents() {
1320
+ if (!conversationId) return;
1321
+ const events = takeEvents();
1322
+ if (!events) return;
1323
+ try {
1324
+ void Promise.resolve(
1325
+ assist({ messages: [], conversationId, events, screenshots: [], context: null })
1326
+ ).catch(() => {
1327
+ });
1328
+ } catch (e) {
1329
+ }
1330
+ }
1331
+ function leave(type, detail) {
1332
+ logEvent(type, detail);
1333
+ flushEvents();
1334
+ onClose();
1335
+ }
1307
1336
  useEffect2(() => {
1308
1337
  const el = scrollRef.current;
1309
1338
  if (el) el.scrollTop = el.scrollHeight;
@@ -1312,27 +1341,45 @@ function AssistSheet({
1312
1341
  var _a;
1313
1342
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1314
1343
  }, [phase]);
1344
+ function degradeKeepingEvents(events, message) {
1345
+ if (events) eventsRef.current = [...events, ...eventsRef.current];
1346
+ flushEvents();
1347
+ onDegrade(message);
1348
+ }
1315
1349
  async function runTurn(history2) {
1316
1350
  var _a, _b, _c, _d, _e;
1317
1351
  setBusy(true);
1318
1352
  let result;
1353
+ const events = takeEvents();
1319
1354
  try {
1320
1355
  const sent = screenshots.slice(-MAX_ASSIST_IMAGES);
1321
1356
  result = await assist({
1322
- messages: history2,
1357
+ // `kind` only on drafts, so an older server sees the exact shape it
1358
+ // always did — it just learns the draft text now instead of a
1359
+ // placeholder line.
1360
+ messages: history2.map(
1361
+ (m) => m.kind ? { role: m.role, content: m.content, kind: m.kind } : m
1362
+ ),
1323
1363
  conversationId,
1324
1364
  screenshots: sent,
1325
1365
  screenshot: (_a = sent[sent.length - 1]) != null ? _a : null,
1326
- context
1366
+ context: {
1367
+ ...context != null ? context : {},
1368
+ imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached")
1369
+ },
1370
+ events
1327
1371
  });
1328
1372
  } catch (e) {
1329
- onDegrade("The assistant is unavailable \u2014 write your report below and send it as normal.");
1373
+ degradeKeepingEvents(
1374
+ events,
1375
+ "The assistant is unavailable \u2014 write your report below and send it as normal."
1376
+ );
1330
1377
  return;
1331
1378
  } finally {
1332
1379
  setBusy(false);
1333
1380
  }
1334
1381
  if (result.degraded) {
1335
- onDegrade(result.degraded);
1382
+ degradeKeepingEvents(events, result.degraded);
1336
1383
  return;
1337
1384
  }
1338
1385
  if (result.conversationId) {
@@ -1349,7 +1396,9 @@ function AssistSheet({
1349
1396
  }
1350
1397
  if (result.report) {
1351
1398
  onDescriptionChange(result.report);
1352
- setMessages([...history2, { role: "assistant", content: "Here's your report \u2014 have a read." }]);
1399
+ aiDraftRef.current = result.report;
1400
+ logEvent("draft_shown");
1401
+ setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
1353
1402
  setOptions([]);
1354
1403
  setPhase("draft");
1355
1404
  return;
@@ -1384,6 +1433,36 @@ function AssistSheet({
1384
1433
  onPickType(tile);
1385
1434
  setPhase(tile === "RATING" ? "rating" : "chat");
1386
1435
  }
1436
+ function keepChatting() {
1437
+ const current = description.trim();
1438
+ logEvent(
1439
+ "keep_chatting",
1440
+ aiDraftRef.current && current !== aiDraftRef.current.trim() ? "after editing" : void 0
1441
+ );
1442
+ setMessages((prev) => {
1443
+ const next = [...prev];
1444
+ for (let i = next.length - 1; i >= 0; i--) {
1445
+ if (next[i].kind === "report") {
1446
+ if (current) next[i] = { ...next[i], content: current };
1447
+ break;
1448
+ }
1449
+ }
1450
+ return next;
1451
+ });
1452
+ setPhase("chat");
1453
+ requestAnimationFrame(() => {
1454
+ var _a;
1455
+ return (_a = inputRef.current) == null ? void 0 : _a.focus();
1456
+ });
1457
+ }
1458
+ function sendDraft() {
1459
+ const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1460
+ logEvent("draft_sent", edited ? "edited" : "as drafted");
1461
+ flushEvents();
1462
+ onSend();
1463
+ }
1464
+ const lastMessage = messages[messages.length - 1];
1465
+ const correcting = phase === "chat" && (lastMessage == null ? void 0 : lastMessage.kind) === "report";
1387
1466
  const empty = phase === "chat" && messages.length === 0 && !busy;
1388
1467
  const initials = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
1389
1468
  var _a, _b;
@@ -1429,7 +1508,7 @@ function AssistSheet({
1429
1508
  animation: "gg-sheet-fade .18s ease",
1430
1509
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1431
1510
  },
1432
- onClick: onClose,
1511
+ onClick: () => leave("closed"),
1433
1512
  onPointerDown: (e) => e.stopPropagation(),
1434
1513
  children: /* @__PURE__ */ jsxs2(
1435
1514
  "div",
@@ -1509,7 +1588,7 @@ function AssistSheet({
1509
1588
  {
1510
1589
  type: "button",
1511
1590
  "data-gg-write-myself": "",
1512
- onClick: onClose,
1591
+ onClick: () => leave("write_myself", `header, ${phase}`),
1513
1592
  "aria-label": "Write it myself instead",
1514
1593
  style: {
1515
1594
  flexShrink: 0,
@@ -1765,7 +1844,10 @@ function AssistSheet({
1765
1844
  {
1766
1845
  type: "button",
1767
1846
  "data-gg-chip": "",
1768
- onClick: () => send(s),
1847
+ onClick: () => {
1848
+ logEvent("starter_tap", s);
1849
+ send(s);
1850
+ },
1769
1851
  style: {
1770
1852
  border: `1px solid ${t2.inputBorder}`,
1771
1853
  background: "transparent",
@@ -1785,6 +1867,60 @@ function AssistSheet({
1785
1867
  ] }),
1786
1868
  messages.map((m, i) => {
1787
1869
  const mine = m.role === "user";
1870
+ if (m.kind === "report") {
1871
+ const live = phase === "draft" && i === messages.length - 1;
1872
+ return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
1873
+ /* @__PURE__ */ jsx2(Avatar, { who: "assistant", theme: t2 }),
1874
+ /* @__PURE__ */ jsx2(
1875
+ "div",
1876
+ {
1877
+ "data-gg-draft-bubble": "",
1878
+ style: {
1879
+ maxWidth: "82%",
1880
+ minWidth: 0,
1881
+ padding: "9px 12px",
1882
+ borderRadius: "12px 12px 12px 4px",
1883
+ fontSize: "13px",
1884
+ lineHeight: 1.55,
1885
+ backgroundColor: t2.bgSecondary,
1886
+ color: t2.text,
1887
+ border: live ? "none" : `1px solid ${t2.accent}66`
1888
+ },
1889
+ children: live ? "Here's your report \u2014 have a read." : /* @__PURE__ */ jsxs2(Fragment, { children: [
1890
+ /* @__PURE__ */ jsx2(
1891
+ "span",
1892
+ {
1893
+ style: {
1894
+ display: "block",
1895
+ fontSize: "10px",
1896
+ fontWeight: 700,
1897
+ letterSpacing: ".06em",
1898
+ textTransform: "uppercase",
1899
+ color: t2.accent,
1900
+ marginBottom: "4px"
1901
+ },
1902
+ children: "Draft so far"
1903
+ }
1904
+ ),
1905
+ /* @__PURE__ */ jsx2(
1906
+ "span",
1907
+ {
1908
+ style: {
1909
+ display: "block",
1910
+ whiteSpace: "pre-wrap",
1911
+ wordBreak: "break-word",
1912
+ maxHeight: "160px",
1913
+ overflowY: "auto",
1914
+ color: t2.textMuted
1915
+ },
1916
+ children: m.content
1917
+ }
1918
+ )
1919
+ ] })
1920
+ }
1921
+ )
1922
+ ] }, i);
1923
+ }
1788
1924
  return /* @__PURE__ */ jsxs2(
1789
1925
  "div",
1790
1926
  {
@@ -1839,7 +1975,10 @@ function AssistSheet({
1839
1975
  type: "button",
1840
1976
  "data-gg-chip": "",
1841
1977
  "data-gg-option": "",
1842
- onClick: () => send(option),
1978
+ onClick: () => {
1979
+ logEvent("option_tap", option);
1980
+ send(option);
1981
+ },
1843
1982
  style: {
1844
1983
  border: `1px solid ${t2.inputBorder}`,
1845
1984
  background: "transparent",
@@ -2041,7 +2180,7 @@ function AssistSheet({
2041
2180
  "button",
2042
2181
  {
2043
2182
  type: "button",
2044
- onClick: () => setPhase("chat"),
2183
+ onClick: keepChatting,
2045
2184
  style: {
2046
2185
  alignSelf: "flex-start",
2047
2186
  border: "none",
@@ -2176,7 +2315,7 @@ function AssistSheet({
2176
2315
  {
2177
2316
  type: "button",
2178
2317
  "data-gg-sheet-send": "",
2179
- onClick: onSend,
2318
+ onClick: sendDraft,
2180
2319
  disabled: isSubmitting || !description.trim(),
2181
2320
  style: {
2182
2321
  width: "100%",
@@ -2254,7 +2393,10 @@ function AssistSheet({
2254
2393
  onChange: (e) => {
2255
2394
  var _a;
2256
2395
  const files = Array.from((_a = e.target.files) != null ? _a : []);
2257
- if (files.length) onAddImages(files);
2396
+ if (files.length) {
2397
+ logEvent("image_added", String(files.length));
2398
+ onAddImages(files);
2399
+ }
2258
2400
  e.target.value = "";
2259
2401
  },
2260
2402
  style: { display: "none" }
@@ -2284,7 +2426,7 @@ function AssistSheet({
2284
2426
  send(input);
2285
2427
  }
2286
2428
  },
2287
- placeholder: "Type your answer\u2026",
2429
+ placeholder: correcting ? "What should change in the draft?" : "Type your answer\u2026",
2288
2430
  rows: 1,
2289
2431
  disabled: busy,
2290
2432
  autoFocus: true,
@@ -2344,7 +2486,7 @@ function AssistSheet({
2344
2486
  {
2345
2487
  type: "button",
2346
2488
  "data-gg-write-myself": "",
2347
- onClick: onClose,
2489
+ onClick: () => leave("write_myself", "footer"),
2348
2490
  style: {
2349
2491
  display: "block",
2350
2492
  margin: "8px auto 0",
@@ -2416,6 +2558,107 @@ var ATTACHMENT_ACCEPT = [
2416
2558
  ".xlsx",
2417
2559
  ".zip"
2418
2560
  ].join(",");
2561
+ var MAX_SCALE = 2;
2562
+ var FRAME_TIMEOUT_MS = 3e3;
2563
+ function canCaptureTabPixels() {
2564
+ var _a;
2565
+ try {
2566
+ return typeof navigator !== "undefined" && typeof ((_a = navigator.mediaDevices) == null ? void 0 : _a.getDisplayMedia) === "function" && typeof globalThis.CaptureController !== "undefined";
2567
+ } catch (e) {
2568
+ return false;
2569
+ }
2570
+ }
2571
+ var HAVE_CURRENT_DATA = 2;
2572
+ function waitForFrame(video) {
2573
+ if (video.readyState >= HAVE_CURRENT_DATA && video.videoWidth > 0) {
2574
+ return Promise.resolve(true);
2575
+ }
2576
+ return new Promise((resolve) => {
2577
+ const timer = setTimeout(() => resolve(false), FRAME_TIMEOUT_MS);
2578
+ const done = () => {
2579
+ clearTimeout(timer);
2580
+ resolve(true);
2581
+ };
2582
+ if (typeof video.requestVideoFrameCallback === "function") {
2583
+ video.requestVideoFrameCallback(done);
2584
+ } else if (video.readyState >= 2) {
2585
+ done();
2586
+ } else {
2587
+ video.addEventListener("loadeddata", done, { once: true });
2588
+ }
2589
+ });
2590
+ }
2591
+ async function grabFrame(stream) {
2592
+ const video = document.createElement("video");
2593
+ video.muted = true;
2594
+ video.playsInline = true;
2595
+ video.srcObject = stream;
2596
+ try {
2597
+ await video.play();
2598
+ if (!await waitForFrame(video)) return null;
2599
+ if (!video.videoWidth || !video.videoHeight) return null;
2600
+ const maxWidth = Math.max(1, window.innerWidth) * MAX_SCALE;
2601
+ const factor = Math.min(1, maxWidth / video.videoWidth);
2602
+ const canvas = document.createElement("canvas");
2603
+ canvas.width = Math.round(video.videoWidth * factor);
2604
+ canvas.height = Math.round(video.videoHeight * factor);
2605
+ const ctx = canvas.getContext("2d");
2606
+ if (!ctx) return null;
2607
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2608
+ return canvas;
2609
+ } finally {
2610
+ video.srcObject = null;
2611
+ }
2612
+ }
2613
+ async function captureViaTabPixels() {
2614
+ var _a, _b;
2615
+ if (!canCaptureTabPixels()) return null;
2616
+ let stream = null;
2617
+ try {
2618
+ const options = {
2619
+ video: { displaySurface: "browser" },
2620
+ audio: false,
2621
+ preferCurrentTab: true,
2622
+ selfBrowserSurface: "include",
2623
+ surfaceSwitching: "exclude",
2624
+ monitorTypeSurfaces: "exclude"
2625
+ };
2626
+ stream = await navigator.mediaDevices.getDisplayMedia(options);
2627
+ const surface = (_b = (_a = stream.getVideoTracks()[0]) == null ? void 0 : _a.getSettings()) == null ? void 0 : _b.displaySurface;
2628
+ if (surface && surface !== "browser") return null;
2629
+ const canvas = await grabFrame(stream);
2630
+ return canvas ? encodeScreenshot(canvas) : null;
2631
+ } catch (e) {
2632
+ return null;
2633
+ } finally {
2634
+ stream == null ? void 0 : stream.getTracks().forEach((track) => track.stop());
2635
+ }
2636
+ }
2637
+ async function captureViaHtml2Canvas() {
2638
+ try {
2639
+ const { default: html2canvas } = await import("html2canvas-pro");
2640
+ const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), MAX_SCALE);
2641
+ const canvas = await html2canvas(document.body, {
2642
+ scale,
2643
+ logging: false,
2644
+ useCORS: true,
2645
+ allowTaint: true,
2646
+ x: window.scrollX,
2647
+ y: window.scrollY,
2648
+ width: window.innerWidth,
2649
+ height: window.innerHeight,
2650
+ windowWidth: window.innerWidth,
2651
+ windowHeight: window.innerHeight
2652
+ });
2653
+ return encodeScreenshot(canvas);
2654
+ } catch (e) {
2655
+ return null;
2656
+ }
2657
+ }
2658
+ async function captureDefaultScreenshot() {
2659
+ var _a;
2660
+ return (_a = await captureViaTabPixels()) != null ? _a : await captureViaHtml2Canvas();
2661
+ }
2419
2662
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
2420
2663
  function useIsDark() {
2421
2664
  if (typeof window === "undefined") return true;
@@ -2690,27 +2933,6 @@ function isLowQualityText(text) {
2690
2933
  }
2691
2934
  return null;
2692
2935
  }
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
2936
  var MIC_MESSAGES = {
2715
2937
  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
2938
  notFound: "No microphone found. Connect one and try again, or type your report instead.",
@@ -2748,7 +2970,7 @@ function ReportDialog({
2748
2970
  transcribeAudio: transcribeAudio2,
2749
2971
  types,
2750
2972
  showSeverity = true,
2751
- captureScreenshot = captureViaHtml2Canvas,
2973
+ captureScreenshot = captureDefaultScreenshot,
2752
2974
  reporter,
2753
2975
  onClose,
2754
2976
  headerSlot
@@ -2778,6 +3000,7 @@ function ReportDialog({
2778
3000
  const [isSubmitting, setIsSubmitting] = useState3(false);
2779
3001
  const [submitted, setSubmitted] = useState3(false);
2780
3002
  const [screenshots, setScreenshots] = useState3([]);
3003
+ const [pageShot, setPageShot] = useState3(null);
2781
3004
  const [attachments, setAttachments] = useState3([]);
2782
3005
  const [previewIndex, setPreviewIndex] = useState3(null);
2783
3006
  const [annotatingIndex, setAnnotatingIndex] = useState3(null);
@@ -2943,6 +3166,7 @@ function ReportDialog({
2943
3166
  setStep(2);
2944
3167
  }
2945
3168
  const shot = await captureScreenshot();
3169
+ setPageShot(shot != null ? shot : null);
2946
3170
  if (shot) setScreenshots([shot]);
2947
3171
  if (assist && !assistUsed) setAssistOpen(true);
2948
3172
  setIsOpen(true);
@@ -5140,6 +5364,7 @@ function ReportDialog({
5140
5364
  imageSrc: screenshots[annotatingIndex],
5141
5365
  onCancel: () => setAnnotatingIndex(null),
5142
5366
  onSave: (dataUrl) => {
5367
+ if (screenshots[annotatingIndex] === pageShot) setPageShot(dataUrl);
5143
5368
  setScreenshots(
5144
5369
  (prev) => prev.map((s, i) => i === annotatingIndex ? dataUrl : s)
5145
5370
  );
@@ -5165,6 +5390,7 @@ function ReportDialog({
5165
5390
  accentText: t2.accentText
5166
5391
  },
5167
5392
  screenshots,
5393
+ pageShot,
5168
5394
  onAddImages: addFiles,
5169
5395
  attachmentCount: screenshots.length + attachments.length,
5170
5396
  context: { ...assistContext != null ? assistContext : {}, reportType },