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/README.md CHANGED
@@ -832,7 +832,7 @@ response.headers.set(
832
832
 
833
833
  - `connect-src https://glitchgrab.dev` — required for `sendReport`, `enhanceText`, `transcribeAudio`, and the report-fetching hooks/REST calls. All SDK requests go to this single host by default.
834
834
  - If you pass a custom `baseUrl` prop, allow that host instead (self-hosted or proxied deployments).
835
- - Screenshot capture (`html2canvas-pro`) runs entirely client-side against `document.body` — no network request, no extra `img-src`/`connect-src` needed for it.
835
+ - Screenshot capture runs entirely client-side — no network request, no extra `img-src`/`connect-src` needed for it. In Chrome and Edge the browser asks "Allow this site to see this tab?" when the dialog opens, and the screenshot is the tab's real pixels (the share stops after one frame). If the reporter clicks Cancel, or in Safari/Firefox, it falls back to re-drawing `document.body` with `html2canvas-pro`, which needs no permission but can drift from the real UI. If the SDK runs inside an iframe, the frame needs `allow="display-capture"` for the real-pixel path.
836
836
  - `img-src` only matters if your `session` carries an avatar URL — the report dialog renders it. If your CSP blocks that host the dialog falls back to the reporter's initials, so it degrades rather than breaking.
837
837
  - No `script-src`, `style-src`, or `frame-src` allowances are required — the SDK doesn't load remote scripts, styles, or iframes.
838
838
 
package/dist/index.js CHANGED
@@ -1314,6 +1314,7 @@ function AssistSheet({
1314
1314
  assist,
1315
1315
  theme: t2,
1316
1316
  screenshots,
1317
+ pageShot = null,
1317
1318
  onAddImages,
1318
1319
  attachmentCount,
1319
1320
  context,
@@ -1359,6 +1360,34 @@ function AssistSheet({
1359
1360
  const inputRef = (0, import_react3.useRef)(null);
1360
1361
  const draftRef = (0, import_react3.useRef)(null);
1361
1362
  const seededRef = (0, import_react3.useRef)(false);
1363
+ const eventsRef = (0, import_react3.useRef)([]);
1364
+ const aiDraftRef = (0, import_react3.useRef)(null);
1365
+ function logEvent(type, detail) {
1366
+ eventsRef.current.push({ type, at: Date.now(), ...detail ? { detail } : {} });
1367
+ }
1368
+ function takeEvents() {
1369
+ if (eventsRef.current.length === 0) return void 0;
1370
+ const events = eventsRef.current;
1371
+ eventsRef.current = [];
1372
+ return events;
1373
+ }
1374
+ function flushEvents() {
1375
+ if (!conversationId) return;
1376
+ const events = takeEvents();
1377
+ if (!events) return;
1378
+ try {
1379
+ void Promise.resolve(
1380
+ assist({ messages: [], conversationId, events, screenshots: [], context: null })
1381
+ ).catch(() => {
1382
+ });
1383
+ } catch (e) {
1384
+ }
1385
+ }
1386
+ function leave(type, detail) {
1387
+ logEvent(type, detail);
1388
+ flushEvents();
1389
+ onClose();
1390
+ }
1362
1391
  (0, import_react3.useEffect)(() => {
1363
1392
  const el = scrollRef.current;
1364
1393
  if (el) el.scrollTop = el.scrollHeight;
@@ -1367,27 +1396,45 @@ function AssistSheet({
1367
1396
  var _a;
1368
1397
  if (phase === "draft") (_a = draftRef.current) == null ? void 0 : _a.focus();
1369
1398
  }, [phase]);
1399
+ function degradeKeepingEvents(events, message) {
1400
+ if (events) eventsRef.current = [...events, ...eventsRef.current];
1401
+ flushEvents();
1402
+ onDegrade(message);
1403
+ }
1370
1404
  async function runTurn(history2) {
1371
1405
  var _a, _b, _c, _d, _e;
1372
1406
  setBusy(true);
1373
1407
  let result;
1408
+ const events = takeEvents();
1374
1409
  try {
1375
1410
  const sent = screenshots.slice(-MAX_ASSIST_IMAGES);
1376
1411
  result = await assist({
1377
- messages: history2,
1412
+ // `kind` only on drafts, so an older server sees the exact shape it
1413
+ // always did — it just learns the draft text now instead of a
1414
+ // placeholder line.
1415
+ messages: history2.map(
1416
+ (m) => m.kind ? { role: m.role, content: m.content, kind: m.kind } : m
1417
+ ),
1378
1418
  conversationId,
1379
1419
  screenshots: sent,
1380
1420
  screenshot: (_a = sent[sent.length - 1]) != null ? _a : null,
1381
- context
1421
+ context: {
1422
+ ...context != null ? context : {},
1423
+ imageSources: sent.map((shot) => pageShot && shot === pageShot ? "page" : "attached")
1424
+ },
1425
+ events
1382
1426
  });
1383
1427
  } catch (e) {
1384
- onDegrade("The assistant is unavailable \u2014 write your report below and send it as normal.");
1428
+ degradeKeepingEvents(
1429
+ events,
1430
+ "The assistant is unavailable \u2014 write your report below and send it as normal."
1431
+ );
1385
1432
  return;
1386
1433
  } finally {
1387
1434
  setBusy(false);
1388
1435
  }
1389
1436
  if (result.degraded) {
1390
- onDegrade(result.degraded);
1437
+ degradeKeepingEvents(events, result.degraded);
1391
1438
  return;
1392
1439
  }
1393
1440
  if (result.conversationId) {
@@ -1404,7 +1451,9 @@ function AssistSheet({
1404
1451
  }
1405
1452
  if (result.report) {
1406
1453
  onDescriptionChange(result.report);
1407
- setMessages([...history2, { role: "assistant", content: "Here's your report \u2014 have a read." }]);
1454
+ aiDraftRef.current = result.report;
1455
+ logEvent("draft_shown");
1456
+ setMessages([...history2, { role: "assistant", content: result.report, kind: "report" }]);
1408
1457
  setOptions([]);
1409
1458
  setPhase("draft");
1410
1459
  return;
@@ -1439,6 +1488,36 @@ function AssistSheet({
1439
1488
  onPickType(tile);
1440
1489
  setPhase(tile === "RATING" ? "rating" : "chat");
1441
1490
  }
1491
+ function keepChatting() {
1492
+ 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
+ }
1504
+ }
1505
+ return next;
1506
+ });
1507
+ setPhase("chat");
1508
+ requestAnimationFrame(() => {
1509
+ var _a;
1510
+ return (_a = inputRef.current) == null ? void 0 : _a.focus();
1511
+ });
1512
+ }
1513
+ function sendDraft() {
1514
+ const edited = aiDraftRef.current !== null && description.trim() !== aiDraftRef.current.trim();
1515
+ logEvent("draft_sent", edited ? "edited" : "as drafted");
1516
+ flushEvents();
1517
+ onSend();
1518
+ }
1519
+ const lastMessage = messages[messages.length - 1];
1520
+ const correcting = phase === "chat" && (lastMessage == null ? void 0 : lastMessage.kind) === "report";
1442
1521
  const empty = phase === "chat" && messages.length === 0 && !busy;
1443
1522
  const initials = (reporterName != null ? reporterName : "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => {
1444
1523
  var _a, _b;
@@ -1484,7 +1563,7 @@ function AssistSheet({
1484
1563
  animation: "gg-sheet-fade .18s ease",
1485
1564
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1486
1565
  },
1487
- onClick: onClose,
1566
+ onClick: () => leave("closed"),
1488
1567
  onPointerDown: (e) => e.stopPropagation(),
1489
1568
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1490
1569
  "div",
@@ -1564,7 +1643,7 @@ function AssistSheet({
1564
1643
  {
1565
1644
  type: "button",
1566
1645
  "data-gg-write-myself": "",
1567
- onClick: onClose,
1646
+ onClick: () => leave("write_myself", `header, ${phase}`),
1568
1647
  "aria-label": "Write it myself instead",
1569
1648
  style: {
1570
1649
  flexShrink: 0,
@@ -1820,7 +1899,10 @@ function AssistSheet({
1820
1899
  {
1821
1900
  type: "button",
1822
1901
  "data-gg-chip": "",
1823
- onClick: () => send(s),
1902
+ onClick: () => {
1903
+ logEvent("starter_tap", s);
1904
+ send(s);
1905
+ },
1824
1906
  style: {
1825
1907
  border: `1px solid ${t2.inputBorder}`,
1826
1908
  background: "transparent",
@@ -1840,6 +1922,60 @@ function AssistSheet({
1840
1922
  ] }),
1841
1923
  messages.map((m, i) => {
1842
1924
  const mine = m.role === "user";
1925
+ if (m.kind === "report") {
1926
+ const live = phase === "draft" && i === messages.length - 1;
1927
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: "8px", minWidth: 0 }, children: [
1928
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Avatar, { who: "assistant", theme: t2 }),
1929
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1930
+ "div",
1931
+ {
1932
+ "data-gg-draft-bubble": "",
1933
+ style: {
1934
+ maxWidth: "82%",
1935
+ minWidth: 0,
1936
+ padding: "9px 12px",
1937
+ borderRadius: "12px 12px 12px 4px",
1938
+ fontSize: "13px",
1939
+ lineHeight: 1.55,
1940
+ backgroundColor: t2.bgSecondary,
1941
+ color: t2.text,
1942
+ border: live ? "none" : `1px solid ${t2.accent}66`
1943
+ },
1944
+ children: live ? "Here's your report \u2014 have a read." : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
1945
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1946
+ "span",
1947
+ {
1948
+ style: {
1949
+ display: "block",
1950
+ fontSize: "10px",
1951
+ fontWeight: 700,
1952
+ letterSpacing: ".06em",
1953
+ textTransform: "uppercase",
1954
+ color: t2.accent,
1955
+ marginBottom: "4px"
1956
+ },
1957
+ children: "Draft so far"
1958
+ }
1959
+ ),
1960
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1961
+ "span",
1962
+ {
1963
+ style: {
1964
+ display: "block",
1965
+ whiteSpace: "pre-wrap",
1966
+ wordBreak: "break-word",
1967
+ maxHeight: "160px",
1968
+ overflowY: "auto",
1969
+ color: t2.textMuted
1970
+ },
1971
+ children: m.content
1972
+ }
1973
+ )
1974
+ ] })
1975
+ }
1976
+ )
1977
+ ] }, i);
1978
+ }
1843
1979
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1844
1980
  "div",
1845
1981
  {
@@ -1894,7 +2030,10 @@ function AssistSheet({
1894
2030
  type: "button",
1895
2031
  "data-gg-chip": "",
1896
2032
  "data-gg-option": "",
1897
- onClick: () => send(option),
2033
+ onClick: () => {
2034
+ logEvent("option_tap", option);
2035
+ send(option);
2036
+ },
1898
2037
  style: {
1899
2038
  border: `1px solid ${t2.inputBorder}`,
1900
2039
  background: "transparent",
@@ -2096,7 +2235,7 @@ function AssistSheet({
2096
2235
  "button",
2097
2236
  {
2098
2237
  type: "button",
2099
- onClick: () => setPhase("chat"),
2238
+ onClick: keepChatting,
2100
2239
  style: {
2101
2240
  alignSelf: "flex-start",
2102
2241
  border: "none",
@@ -2231,7 +2370,7 @@ function AssistSheet({
2231
2370
  {
2232
2371
  type: "button",
2233
2372
  "data-gg-sheet-send": "",
2234
- onClick: onSend,
2373
+ onClick: sendDraft,
2235
2374
  disabled: isSubmitting || !description.trim(),
2236
2375
  style: {
2237
2376
  width: "100%",
@@ -2309,7 +2448,10 @@ function AssistSheet({
2309
2448
  onChange: (e) => {
2310
2449
  var _a;
2311
2450
  const files = Array.from((_a = e.target.files) != null ? _a : []);
2312
- if (files.length) onAddImages(files);
2451
+ if (files.length) {
2452
+ logEvent("image_added", String(files.length));
2453
+ onAddImages(files);
2454
+ }
2313
2455
  e.target.value = "";
2314
2456
  },
2315
2457
  style: { display: "none" }
@@ -2339,7 +2481,7 @@ function AssistSheet({
2339
2481
  send(input);
2340
2482
  }
2341
2483
  },
2342
- placeholder: "Type your answer\u2026",
2484
+ placeholder: correcting ? "What should change in the draft?" : "Type your answer\u2026",
2343
2485
  rows: 1,
2344
2486
  disabled: busy,
2345
2487
  autoFocus: true,
@@ -2399,7 +2541,7 @@ function AssistSheet({
2399
2541
  {
2400
2542
  type: "button",
2401
2543
  "data-gg-write-myself": "",
2402
- onClick: onClose,
2544
+ onClick: () => leave("write_myself", "footer"),
2403
2545
  style: {
2404
2546
  display: "block",
2405
2547
  margin: "8px auto 0",
@@ -2471,6 +2613,107 @@ var ATTACHMENT_ACCEPT = [
2471
2613
  ".xlsx",
2472
2614
  ".zip"
2473
2615
  ].join(",");
2616
+ var MAX_SCALE = 2;
2617
+ var FRAME_TIMEOUT_MS = 3e3;
2618
+ function canCaptureTabPixels() {
2619
+ var _a;
2620
+ try {
2621
+ return typeof navigator !== "undefined" && typeof ((_a = navigator.mediaDevices) == null ? void 0 : _a.getDisplayMedia) === "function" && typeof globalThis.CaptureController !== "undefined";
2622
+ } catch (e) {
2623
+ return false;
2624
+ }
2625
+ }
2626
+ var HAVE_CURRENT_DATA = 2;
2627
+ function waitForFrame(video) {
2628
+ if (video.readyState >= HAVE_CURRENT_DATA && video.videoWidth > 0) {
2629
+ return Promise.resolve(true);
2630
+ }
2631
+ return new Promise((resolve) => {
2632
+ const timer = setTimeout(() => resolve(false), FRAME_TIMEOUT_MS);
2633
+ const done = () => {
2634
+ clearTimeout(timer);
2635
+ resolve(true);
2636
+ };
2637
+ if (typeof video.requestVideoFrameCallback === "function") {
2638
+ video.requestVideoFrameCallback(done);
2639
+ } else if (video.readyState >= 2) {
2640
+ done();
2641
+ } else {
2642
+ video.addEventListener("loadeddata", done, { once: true });
2643
+ }
2644
+ });
2645
+ }
2646
+ async function grabFrame(stream) {
2647
+ const video = document.createElement("video");
2648
+ video.muted = true;
2649
+ video.playsInline = true;
2650
+ video.srcObject = stream;
2651
+ try {
2652
+ await video.play();
2653
+ if (!await waitForFrame(video)) return null;
2654
+ if (!video.videoWidth || !video.videoHeight) return null;
2655
+ const maxWidth = Math.max(1, window.innerWidth) * MAX_SCALE;
2656
+ const factor = Math.min(1, maxWidth / video.videoWidth);
2657
+ const canvas = document.createElement("canvas");
2658
+ canvas.width = Math.round(video.videoWidth * factor);
2659
+ canvas.height = Math.round(video.videoHeight * factor);
2660
+ const ctx = canvas.getContext("2d");
2661
+ if (!ctx) return null;
2662
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2663
+ return canvas;
2664
+ } finally {
2665
+ video.srcObject = null;
2666
+ }
2667
+ }
2668
+ async function captureViaTabPixels() {
2669
+ var _a, _b;
2670
+ if (!canCaptureTabPixels()) return null;
2671
+ let stream = null;
2672
+ try {
2673
+ const options = {
2674
+ video: { displaySurface: "browser" },
2675
+ audio: false,
2676
+ preferCurrentTab: true,
2677
+ selfBrowserSurface: "include",
2678
+ surfaceSwitching: "exclude",
2679
+ monitorTypeSurfaces: "exclude"
2680
+ };
2681
+ stream = await navigator.mediaDevices.getDisplayMedia(options);
2682
+ const surface = (_b = (_a = stream.getVideoTracks()[0]) == null ? void 0 : _a.getSettings()) == null ? void 0 : _b.displaySurface;
2683
+ if (surface && surface !== "browser") return null;
2684
+ const canvas = await grabFrame(stream);
2685
+ return canvas ? encodeScreenshot(canvas) : null;
2686
+ } catch (e) {
2687
+ return null;
2688
+ } finally {
2689
+ stream == null ? void 0 : stream.getTracks().forEach((track) => track.stop());
2690
+ }
2691
+ }
2692
+ async function captureViaHtml2Canvas() {
2693
+ try {
2694
+ const { default: html2canvas } = await import("html2canvas-pro");
2695
+ const scale = Math.min(Math.max(window.devicePixelRatio || 1, 1), MAX_SCALE);
2696
+ const canvas = await html2canvas(document.body, {
2697
+ scale,
2698
+ logging: false,
2699
+ useCORS: true,
2700
+ allowTaint: true,
2701
+ x: window.scrollX,
2702
+ y: window.scrollY,
2703
+ width: window.innerWidth,
2704
+ height: window.innerHeight,
2705
+ windowWidth: window.innerWidth,
2706
+ windowHeight: window.innerHeight
2707
+ });
2708
+ return encodeScreenshot(canvas);
2709
+ } catch (e) {
2710
+ return null;
2711
+ }
2712
+ }
2713
+ async function captureDefaultScreenshot() {
2714
+ var _a;
2715
+ return (_a = await captureViaTabPixels()) != null ? _a : await captureViaHtml2Canvas();
2716
+ }
2474
2717
  var SEVERITY_REQUIRED = "Pick how bad it is before sending.";
2475
2718
  function useIsDark() {
2476
2719
  if (typeof window === "undefined") return true;
@@ -2745,27 +2988,6 @@ function isLowQualityText(text) {
2745
2988
  }
2746
2989
  return null;
2747
2990
  }
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
2991
  var MIC_MESSAGES = {
2770
2992
  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
2993
  notFound: "No microphone found. Connect one and try again, or type your report instead.",
@@ -2803,7 +3025,7 @@ function ReportDialog({
2803
3025
  transcribeAudio: transcribeAudio2,
2804
3026
  types,
2805
3027
  showSeverity = true,
2806
- captureScreenshot = captureViaHtml2Canvas,
3028
+ captureScreenshot = captureDefaultScreenshot,
2807
3029
  reporter,
2808
3030
  onClose,
2809
3031
  headerSlot
@@ -2833,6 +3055,7 @@ function ReportDialog({
2833
3055
  const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
2834
3056
  const [submitted, setSubmitted] = (0, import_react.useState)(false);
2835
3057
  const [screenshots, setScreenshots] = (0, import_react.useState)([]);
3058
+ const [pageShot, setPageShot] = (0, import_react.useState)(null);
2836
3059
  const [attachments, setAttachments] = (0, import_react.useState)([]);
2837
3060
  const [previewIndex, setPreviewIndex] = (0, import_react.useState)(null);
2838
3061
  const [annotatingIndex, setAnnotatingIndex] = (0, import_react.useState)(null);
@@ -2998,6 +3221,7 @@ function ReportDialog({
2998
3221
  setStep(2);
2999
3222
  }
3000
3223
  const shot = await captureScreenshot();
3224
+ setPageShot(shot != null ? shot : null);
3001
3225
  if (shot) setScreenshots([shot]);
3002
3226
  if (assist && !assistUsed) setAssistOpen(true);
3003
3227
  setIsOpen(true);
@@ -5195,6 +5419,7 @@ function ReportDialog({
5195
5419
  imageSrc: screenshots[annotatingIndex],
5196
5420
  onCancel: () => setAnnotatingIndex(null),
5197
5421
  onSave: (dataUrl) => {
5422
+ if (screenshots[annotatingIndex] === pageShot) setPageShot(dataUrl);
5198
5423
  setScreenshots(
5199
5424
  (prev) => prev.map((s, i) => i === annotatingIndex ? dataUrl : s)
5200
5425
  );
@@ -5220,6 +5445,7 @@ function ReportDialog({
5220
5445
  accentText: t2.accentText
5221
5446
  },
5222
5447
  screenshots,
5448
+ pageShot,
5223
5449
  onAddImages: addFiles,
5224
5450
  attachmentCount: screenshots.length + attachments.length,
5225
5451
  context: { ...assistContext != null ? assistContext : {}, reportType },