@jarve/bug-reporter 0.1.0 → 0.2.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 +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +358 -160
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +359 -161
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -43,7 +43,7 @@ function FloatingButton({ isActive, onClick }) {
|
|
|
43
43
|
"fixed bottom-6 right-6 z-[9999] flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-all duration-200",
|
|
44
44
|
"hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2",
|
|
45
45
|
isActive ? "bg-red-500 text-white animate-pulse focus:ring-red-400" : "bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-400",
|
|
46
|
-
"bottom-4 right-4 h-
|
|
46
|
+
"bottom-4 right-4 h-11 w-11 md:bottom-6 md:right-6 md:h-12 md:w-12"
|
|
47
47
|
),
|
|
48
48
|
title: isActive ? "Cancel bug capture" : "Report a bug",
|
|
49
49
|
"aria-label": isActive ? "Cancel bug capture" : "Report a bug",
|
|
@@ -111,7 +111,18 @@ function buildSelectorPath(element, stopAt) {
|
|
|
111
111
|
}
|
|
112
112
|
return parts.join(" > ");
|
|
113
113
|
}
|
|
114
|
-
function
|
|
114
|
+
function isTouchCapable() {
|
|
115
|
+
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
116
|
+
}
|
|
117
|
+
function extractCoordinates(e) {
|
|
118
|
+
return {
|
|
119
|
+
pageX: e.pageX,
|
|
120
|
+
pageY: e.pageY,
|
|
121
|
+
clientX: e.clientX,
|
|
122
|
+
clientY: e.clientY
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function collectElementInfo(target, section, coords) {
|
|
115
126
|
const sectionRect = section.getBoundingClientRect();
|
|
116
127
|
const dataAttributes = {};
|
|
117
128
|
for (const attr of Array.from(target.attributes)) {
|
|
@@ -127,10 +138,10 @@ function collectElementInfo(target, section, event) {
|
|
|
127
138
|
ariaLabel: target.getAttribute("aria-label") || null,
|
|
128
139
|
dataAttributes,
|
|
129
140
|
selectorPath: buildSelectorPath(target, section),
|
|
130
|
-
clickX:
|
|
131
|
-
clickY:
|
|
132
|
-
relativeClickX:
|
|
133
|
-
relativeClickY:
|
|
141
|
+
clickX: coords.pageX,
|
|
142
|
+
clickY: coords.pageY,
|
|
143
|
+
relativeClickX: coords.clientX - sectionRect.left,
|
|
144
|
+
relativeClickY: coords.clientY - sectionRect.top
|
|
134
145
|
};
|
|
135
146
|
}
|
|
136
147
|
function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
|
|
@@ -242,14 +253,51 @@ function clearCapturedErrors() {
|
|
|
242
253
|
|
|
243
254
|
// src/network-capture.ts
|
|
244
255
|
var MAX_REQUESTS = 30;
|
|
256
|
+
var MAX_BODY_READ_BYTES = 64 * 1024;
|
|
257
|
+
var TRUNCATE_LEN = 500;
|
|
245
258
|
var capturedRequests = [];
|
|
246
259
|
var isCapturing2 = false;
|
|
247
260
|
var originalFetch = null;
|
|
248
|
-
function truncateBody(body, maxLen =
|
|
261
|
+
function truncateBody(body, maxLen = TRUNCATE_LEN) {
|
|
249
262
|
if (!body) return null;
|
|
250
263
|
if (body.length <= maxLen) return body;
|
|
251
264
|
return body.slice(0, maxLen) + "...(truncated)";
|
|
252
265
|
}
|
|
266
|
+
async function readBoundedBody(response) {
|
|
267
|
+
try {
|
|
268
|
+
const contentLength = response.headers.get("Content-Length");
|
|
269
|
+
if (contentLength) {
|
|
270
|
+
const size = parseInt(contentLength, 10);
|
|
271
|
+
if (!isNaN(size) && size > MAX_BODY_READ_BYTES) {
|
|
272
|
+
return `[body too large: ${size} bytes]`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const cloned = response.clone();
|
|
276
|
+
if (cloned.body && typeof cloned.body.getReader === "function") {
|
|
277
|
+
const reader = cloned.body.getReader();
|
|
278
|
+
const chunks = [];
|
|
279
|
+
let totalBytes = 0;
|
|
280
|
+
while (true) {
|
|
281
|
+
const { done, value } = await reader.read();
|
|
282
|
+
if (done) break;
|
|
283
|
+
totalBytes += value.byteLength;
|
|
284
|
+
if (totalBytes > MAX_BODY_READ_BYTES) {
|
|
285
|
+
reader.cancel().catch(() => {
|
|
286
|
+
});
|
|
287
|
+
return `[body too large: >${MAX_BODY_READ_BYTES} bytes]`;
|
|
288
|
+
}
|
|
289
|
+
chunks.push(value);
|
|
290
|
+
}
|
|
291
|
+
const decoder = new TextDecoder();
|
|
292
|
+
const text2 = chunks.map((c) => decoder.decode(c, { stream: true })).join("");
|
|
293
|
+
return truncateBody(text2);
|
|
294
|
+
}
|
|
295
|
+
const text = await cloned.text();
|
|
296
|
+
return truncateBody(text);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
253
301
|
function startNetworkCapture() {
|
|
254
302
|
if (isCapturing2 || typeof window === "undefined") return;
|
|
255
303
|
if (window.fetch.__bugReporterPatched) return;
|
|
@@ -265,13 +313,7 @@ function startNetworkCapture() {
|
|
|
265
313
|
try {
|
|
266
314
|
const response = await originalFetch.call(window, input, init);
|
|
267
315
|
if (response.status >= 400) {
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const cloned = response.clone();
|
|
271
|
-
const text = await cloned.text();
|
|
272
|
-
responseBody = truncateBody(text);
|
|
273
|
-
} catch (e) {
|
|
274
|
-
}
|
|
316
|
+
const responseBody = await readBoundedBody(response);
|
|
275
317
|
capturedRequests.push({
|
|
276
318
|
url,
|
|
277
319
|
method: method.toUpperCase(),
|
|
@@ -324,6 +366,15 @@ function clearCapturedNetworkErrors() {
|
|
|
324
366
|
|
|
325
367
|
// src/capture-overlay.tsx
|
|
326
368
|
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
369
|
+
function dataUrlToBlob(dataUrl) {
|
|
370
|
+
var _a;
|
|
371
|
+
const [header, base64] = dataUrl.split(",");
|
|
372
|
+
const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
|
|
373
|
+
const bytes = atob(base64);
|
|
374
|
+
const arr = new Uint8Array(bytes.length);
|
|
375
|
+
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
|
|
376
|
+
return new Blob([arr], { type: mime });
|
|
377
|
+
}
|
|
327
378
|
function CaptureOverlay({
|
|
328
379
|
isActive,
|
|
329
380
|
siteId,
|
|
@@ -335,12 +386,79 @@ function CaptureOverlay({
|
|
|
335
386
|
const [hoveredElement, setHoveredElement] = useState(null);
|
|
336
387
|
const [hoveredRect, setHoveredRect] = useState(null);
|
|
337
388
|
const [isCapturing3, setIsCapturing] = useState(false);
|
|
389
|
+
const [isTouchMode, setIsTouchMode] = useState(false);
|
|
390
|
+
const [selectedSection, setSelectedSection] = useState(null);
|
|
391
|
+
const [selectedRect, setSelectedRect] = useState(null);
|
|
392
|
+
const [selectedTarget, setSelectedTarget] = useState(null);
|
|
338
393
|
const overlayRef = useRef(null);
|
|
339
394
|
const hoveredElementRef = useRef(null);
|
|
340
395
|
const rafRef = useRef(null);
|
|
396
|
+
const touchCoordsRef = useRef(null);
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
if (isActive) {
|
|
399
|
+
setIsTouchMode(isTouchCapable());
|
|
400
|
+
}
|
|
401
|
+
}, [isActive]);
|
|
402
|
+
const captureScreenshot = useCallback(
|
|
403
|
+
async (section, target, coords) => {
|
|
404
|
+
const elementInfo = collectElementInfo(target, section, coords);
|
|
405
|
+
setIsCapturing(true);
|
|
406
|
+
try {
|
|
407
|
+
setHoveredElement(null);
|
|
408
|
+
setHoveredRect(null);
|
|
409
|
+
hoveredElementRef.current = null;
|
|
410
|
+
setSelectedSection(null);
|
|
411
|
+
setSelectedRect(null);
|
|
412
|
+
setSelectedTarget(null);
|
|
413
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
414
|
+
const MAX_DIMENSION = 2e3;
|
|
415
|
+
const sectionRect = section.getBoundingClientRect();
|
|
416
|
+
const pixelRatio = sectionRect.width > MAX_DIMENSION || sectionRect.height > MAX_DIMENSION ? 1 : 2;
|
|
417
|
+
const dataUrl = await toPng(section, {
|
|
418
|
+
quality: 0.9,
|
|
419
|
+
pixelRatio,
|
|
420
|
+
skipFonts: true
|
|
421
|
+
});
|
|
422
|
+
const blob = dataUrlToBlob(dataUrl);
|
|
423
|
+
const metadata = collectMetadata(section, siteId, reporterName, reporterEmail, elementInfo);
|
|
424
|
+
const consoleErrors = getCapturedErrors();
|
|
425
|
+
const networkErrors = getCapturedNetworkErrors();
|
|
426
|
+
onCapture({ screenshot: blob, metadata, consoleErrors, networkErrors });
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.warn("Bug reporter: first capture attempt failed, retrying with simpler settings", err);
|
|
429
|
+
try {
|
|
430
|
+
const dataUrl = await toPng(section, {
|
|
431
|
+
quality: 0.6,
|
|
432
|
+
pixelRatio: 1,
|
|
433
|
+
skipFonts: true,
|
|
434
|
+
cacheBust: true
|
|
435
|
+
});
|
|
436
|
+
const retryBlob = dataUrlToBlob(dataUrl);
|
|
437
|
+
const metadata = collectMetadata(section, siteId, reporterName, reporterEmail, elementInfo);
|
|
438
|
+
const consoleErrors = getCapturedErrors();
|
|
439
|
+
const networkErrors = getCapturedNetworkErrors();
|
|
440
|
+
onCapture({ screenshot: retryBlob, metadata, consoleErrors, networkErrors });
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error("Bug reporter: screenshot capture failed after retry");
|
|
443
|
+
const metadata = collectMetadata(section, siteId, reporterName, reporterEmail, elementInfo);
|
|
444
|
+
const consoleErrors = getCapturedErrors();
|
|
445
|
+
const networkErrors = getCapturedNetworkErrors();
|
|
446
|
+
onCapture({
|
|
447
|
+
screenshot: new Blob(),
|
|
448
|
+
metadata: __spreadProps(__spreadValues({}, metadata), { screenshotCaptureFailed: true }),
|
|
449
|
+
consoleErrors,
|
|
450
|
+
networkErrors
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} finally {
|
|
454
|
+
setIsCapturing(false);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
[siteId, reporterName, reporterEmail, onCapture]
|
|
458
|
+
);
|
|
341
459
|
const handleMouseMove = useCallback(
|
|
342
460
|
(e) => {
|
|
343
|
-
if (!isActive || isCapturing3) return;
|
|
461
|
+
if (!isActive || isCapturing3 || isTouchMode) return;
|
|
344
462
|
if (rafRef.current) return;
|
|
345
463
|
rafRef.current = requestAnimationFrame(() => {
|
|
346
464
|
rafRef.current = null;
|
|
@@ -358,12 +476,11 @@ function CaptureOverlay({
|
|
|
358
476
|
setHoveredRect(section ? section.getBoundingClientRect() : null);
|
|
359
477
|
});
|
|
360
478
|
},
|
|
361
|
-
[isActive, isCapturing3]
|
|
479
|
+
[isActive, isCapturing3, isTouchMode]
|
|
362
480
|
);
|
|
363
481
|
const handleClick = useCallback(
|
|
364
482
|
async (e) => {
|
|
365
|
-
|
|
366
|
-
if (!isActive || isCapturing3) return;
|
|
483
|
+
if (!isActive || isCapturing3 || isTouchMode) return;
|
|
367
484
|
const target = e.target;
|
|
368
485
|
if (!(target instanceof HTMLElement)) return;
|
|
369
486
|
if (target.closest("[data-bug-reporter]")) return;
|
|
@@ -371,59 +488,41 @@ function CaptureOverlay({
|
|
|
371
488
|
e.stopPropagation();
|
|
372
489
|
const section = getNearestSection(target);
|
|
373
490
|
if (!section) return;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
console.error("Bug reporter: failed to capture screenshot", err);
|
|
407
|
-
const metadata = collectMetadata(
|
|
408
|
-
section,
|
|
409
|
-
siteId,
|
|
410
|
-
reporterName,
|
|
411
|
-
reporterEmail,
|
|
412
|
-
elementInfo
|
|
413
|
-
);
|
|
414
|
-
const consoleErrors = getCapturedErrors();
|
|
415
|
-
const networkErrors = getCapturedNetworkErrors();
|
|
416
|
-
onCapture({
|
|
417
|
-
screenshot: new Blob(),
|
|
418
|
-
metadata: __spreadProps(__spreadValues({}, metadata), { screenshotCaptureFailed: true }),
|
|
419
|
-
consoleErrors,
|
|
420
|
-
networkErrors
|
|
421
|
-
});
|
|
422
|
-
} finally {
|
|
423
|
-
setIsCapturing(false);
|
|
491
|
+
await captureScreenshot(section, target, extractCoordinates(e));
|
|
492
|
+
},
|
|
493
|
+
[isActive, isCapturing3, isTouchMode, captureScreenshot]
|
|
494
|
+
);
|
|
495
|
+
const handleTouchEnd = useCallback(
|
|
496
|
+
(e) => {
|
|
497
|
+
if (!isActive || isCapturing3) return;
|
|
498
|
+
const touch = e.changedTouches[0];
|
|
499
|
+
if (!touch) return;
|
|
500
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
501
|
+
if (!(target instanceof HTMLElement)) return;
|
|
502
|
+
if (target.closest("[data-bug-reporter]")) return;
|
|
503
|
+
const section = getNearestSection(target);
|
|
504
|
+
if (!section) return;
|
|
505
|
+
setSelectedSection(section);
|
|
506
|
+
setSelectedRect(section.getBoundingClientRect());
|
|
507
|
+
setSelectedTarget(target);
|
|
508
|
+
touchCoordsRef.current = extractCoordinates(touch);
|
|
509
|
+
},
|
|
510
|
+
[isActive, isCapturing3]
|
|
511
|
+
);
|
|
512
|
+
const handleConfirmCapture = useCallback(async () => {
|
|
513
|
+
if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
|
|
514
|
+
await captureScreenshot(selectedSection, selectedTarget, touchCoordsRef.current);
|
|
515
|
+
}, [selectedSection, selectedTarget, captureScreenshot]);
|
|
516
|
+
const handlePointerDown = useCallback(
|
|
517
|
+
(e) => {
|
|
518
|
+
if (!isActive) return;
|
|
519
|
+
if (e.pointerType === "touch") {
|
|
520
|
+
setIsTouchMode(true);
|
|
521
|
+
} else if (e.pointerType === "mouse") {
|
|
522
|
+
setIsTouchMode(false);
|
|
424
523
|
}
|
|
425
524
|
},
|
|
426
|
-
[isActive
|
|
525
|
+
[isActive]
|
|
427
526
|
);
|
|
428
527
|
const handleKeyDown = useCallback(
|
|
429
528
|
(e) => {
|
|
@@ -436,65 +535,126 @@ function CaptureOverlay({
|
|
|
436
535
|
[isActive, onCancel]
|
|
437
536
|
);
|
|
438
537
|
const handleScroll = useCallback(() => {
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
538
|
+
if (hoveredElementRef.current) {
|
|
539
|
+
setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
|
|
540
|
+
}
|
|
541
|
+
if (selectedSection) {
|
|
542
|
+
setSelectedRect(selectedSection.getBoundingClientRect());
|
|
543
|
+
}
|
|
544
|
+
}, [selectedSection]);
|
|
442
545
|
useEffect(() => {
|
|
443
546
|
if (!isActive) {
|
|
444
547
|
setHoveredElement(null);
|
|
445
548
|
setHoveredRect(null);
|
|
446
549
|
hoveredElementRef.current = null;
|
|
550
|
+
setSelectedSection(null);
|
|
551
|
+
setSelectedRect(null);
|
|
552
|
+
setSelectedTarget(null);
|
|
553
|
+
touchCoordsRef.current = null;
|
|
447
554
|
return;
|
|
448
555
|
}
|
|
449
|
-
document.addEventListener("
|
|
450
|
-
document.addEventListener("click", handleClick, true);
|
|
556
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
451
557
|
document.addEventListener("keydown", handleKeyDown);
|
|
452
558
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
559
|
+
if (isTouchMode) {
|
|
560
|
+
document.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
561
|
+
} else {
|
|
562
|
+
document.addEventListener("mousemove", handleMouseMove, true);
|
|
563
|
+
document.addEventListener("click", handleClick, true);
|
|
564
|
+
}
|
|
453
565
|
return () => {
|
|
454
|
-
document.removeEventListener("
|
|
455
|
-
document.removeEventListener("click", handleClick, true);
|
|
566
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
456
567
|
document.removeEventListener("keydown", handleKeyDown);
|
|
457
568
|
window.removeEventListener("scroll", handleScroll);
|
|
569
|
+
document.removeEventListener("touchend", handleTouchEnd);
|
|
570
|
+
document.removeEventListener("mousemove", handleMouseMove, true);
|
|
571
|
+
document.removeEventListener("click", handleClick, true);
|
|
458
572
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
459
573
|
};
|
|
460
|
-
}, [isActive, handleMouseMove, handleClick, handleKeyDown, handleScroll]);
|
|
461
|
-
|
|
574
|
+
}, [isActive, isTouchMode, handleMouseMove, handleClick, handleTouchEnd, handlePointerDown, handleKeyDown, handleScroll]);
|
|
575
|
+
const highlightRect = isTouchMode ? selectedRect : hoveredRect;
|
|
576
|
+
const showHighlight = isTouchMode ? !!selectedSection : !!hoveredElement && !!hoveredRect;
|
|
577
|
+
if (!isActive) return null;
|
|
462
578
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
463
|
-
/* @__PURE__ */
|
|
579
|
+
/* @__PURE__ */ jsx2(
|
|
464
580
|
"div",
|
|
465
581
|
{
|
|
466
582
|
"data-bug-reporter": true,
|
|
467
583
|
role: "alert",
|
|
468
584
|
"aria-live": "assertive",
|
|
469
|
-
className: "fixed top-0 left-0 right-0 z-[10000] bg-indigo-600 text-white text-center py-2 px-4 text-sm font-medium",
|
|
470
|
-
children: [
|
|
585
|
+
className: "fixed top-0 left-0 right-0 z-[10000] bg-indigo-600 text-white text-center py-2 px-4 text-sm font-medium flex items-center justify-center gap-3",
|
|
586
|
+
children: isTouchMode ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
587
|
+
/* @__PURE__ */ jsx2("span", { children: "Tap the section with the bug" }),
|
|
588
|
+
/* @__PURE__ */ jsx2(
|
|
589
|
+
"button",
|
|
590
|
+
{
|
|
591
|
+
onClick: onCancel,
|
|
592
|
+
className: "px-3 py-1 min-h-[44px] bg-white/20 rounded-md text-sm font-medium",
|
|
593
|
+
children: "Cancel"
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
471
597
|
"Click on the section with the bug. Press ",
|
|
472
598
|
/* @__PURE__ */ jsx2("kbd", { className: "px-1.5 py-0.5 bg-indigo-800 rounded text-xs mx-1", children: "Esc" }),
|
|
473
599
|
" to cancel."
|
|
474
|
-
]
|
|
600
|
+
] })
|
|
475
601
|
}
|
|
476
602
|
),
|
|
477
|
-
/* @__PURE__ */ jsx2(
|
|
603
|
+
showHighlight && highlightRect && /* @__PURE__ */ jsx2(
|
|
478
604
|
"div",
|
|
479
605
|
{
|
|
480
606
|
ref: overlayRef,
|
|
481
607
|
"data-bug-reporter": true,
|
|
482
608
|
className: "fixed pointer-events-none z-[9998] border-2 border-indigo-500 rounded-sm transition-all duration-150 ease-out",
|
|
483
609
|
style: {
|
|
484
|
-
top:
|
|
485
|
-
left:
|
|
486
|
-
width:
|
|
487
|
-
height:
|
|
610
|
+
top: highlightRect.top - 2,
|
|
611
|
+
left: highlightRect.left - 2,
|
|
612
|
+
width: highlightRect.width + 4,
|
|
613
|
+
height: highlightRect.height + 4,
|
|
488
614
|
backgroundColor: "rgba(99, 102, 241, 0.08)"
|
|
489
615
|
}
|
|
490
616
|
}
|
|
491
617
|
),
|
|
492
|
-
|
|
618
|
+
isTouchMode && selectedSection && !isCapturing3 && /* @__PURE__ */ jsx2(
|
|
619
|
+
"div",
|
|
620
|
+
{
|
|
621
|
+
"data-bug-reporter": true,
|
|
622
|
+
className: "fixed bottom-0 left-0 right-0 z-[10000] bg-white border-t border-gray-200 shadow-lg",
|
|
623
|
+
style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" },
|
|
624
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 gap-3", children: [
|
|
625
|
+
/* @__PURE__ */ jsx2("span", { className: "text-sm font-medium text-gray-900 truncate", children: "Capture this section?" }),
|
|
626
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 shrink-0", children: [
|
|
627
|
+
/* @__PURE__ */ jsx2(
|
|
628
|
+
"button",
|
|
629
|
+
{
|
|
630
|
+
onClick: () => {
|
|
631
|
+
setSelectedSection(null);
|
|
632
|
+
setSelectedRect(null);
|
|
633
|
+
setSelectedTarget(null);
|
|
634
|
+
touchCoordsRef.current = null;
|
|
635
|
+
},
|
|
636
|
+
className: "px-4 min-h-[44px] rounded-md border border-gray-300 text-sm font-medium text-gray-700",
|
|
637
|
+
children: "Cancel"
|
|
638
|
+
}
|
|
639
|
+
),
|
|
640
|
+
/* @__PURE__ */ jsx2(
|
|
641
|
+
"button",
|
|
642
|
+
{
|
|
643
|
+
onClick: handleConfirmCapture,
|
|
644
|
+
className: "px-4 min-h-[44px] rounded-md bg-indigo-600 text-white text-sm font-medium",
|
|
645
|
+
children: "Capture"
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
] })
|
|
649
|
+
] })
|
|
650
|
+
}
|
|
651
|
+
),
|
|
652
|
+
!isTouchMode && /* @__PURE__ */ jsx2("style", { children: `* { cursor: crosshair !important; }` })
|
|
493
653
|
] });
|
|
494
654
|
}
|
|
495
655
|
|
|
496
656
|
// src/report-modal.tsx
|
|
497
|
-
import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
657
|
+
import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2, useMemo } from "react";
|
|
498
658
|
import { X, Send, Loader2, CheckCircle2 } from "lucide-react";
|
|
499
659
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
500
660
|
function ReportModal({
|
|
@@ -515,34 +675,31 @@ function ReportModal({
|
|
|
515
675
|
const chatEndRef = useRef2(null);
|
|
516
676
|
const inputRef = useRef2(null);
|
|
517
677
|
const hasInitRef = useRef2(false);
|
|
518
|
-
const apiHeaders =
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
678
|
+
const apiHeaders = useMemo(
|
|
679
|
+
() => ({
|
|
680
|
+
"Content-Type": "application/json",
|
|
681
|
+
"X-Bug-Reporter-Key": apiConfig.apiKey
|
|
682
|
+
}),
|
|
683
|
+
[apiConfig.apiKey]
|
|
684
|
+
);
|
|
522
685
|
useEffect2(() => {
|
|
523
686
|
if ((captureResult == null ? void 0 : captureResult.screenshot) && captureResult.screenshot.size > 0) {
|
|
524
687
|
const url = URL.createObjectURL(captureResult.screenshot);
|
|
525
|
-
setScreenshotUrl(
|
|
526
|
-
|
|
688
|
+
setScreenshotUrl((prev) => {
|
|
689
|
+
if (prev) URL.revokeObjectURL(prev);
|
|
690
|
+
return url;
|
|
691
|
+
});
|
|
692
|
+
return () => {
|
|
693
|
+
URL.revokeObjectURL(url);
|
|
694
|
+
setScreenshotUrl(null);
|
|
695
|
+
};
|
|
527
696
|
}
|
|
697
|
+
setScreenshotUrl((prev) => {
|
|
698
|
+
if (prev) URL.revokeObjectURL(prev);
|
|
699
|
+
return null;
|
|
700
|
+
});
|
|
528
701
|
}, [captureResult]);
|
|
529
|
-
|
|
530
|
-
if (isOpen && captureResult && !hasInitRef.current) {
|
|
531
|
-
hasInitRef.current = true;
|
|
532
|
-
sendInitialMessage();
|
|
533
|
-
}
|
|
534
|
-
}, [isOpen, captureResult]);
|
|
535
|
-
useEffect2(() => {
|
|
536
|
-
var _a;
|
|
537
|
-
(_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
538
|
-
}, [messages]);
|
|
539
|
-
useEffect2(() => {
|
|
540
|
-
var _a;
|
|
541
|
-
if (isOpen && !isLoading) {
|
|
542
|
-
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
543
|
-
}
|
|
544
|
-
}, [isOpen, isLoading, messages]);
|
|
545
|
-
async function sendInitialMessage() {
|
|
702
|
+
const sendInitialMessage = useCallback2(async () => {
|
|
546
703
|
if (!captureResult) return;
|
|
547
704
|
setIsLoading(true);
|
|
548
705
|
try {
|
|
@@ -557,6 +714,16 @@ function ReportModal({
|
|
|
557
714
|
clickedElement: captureResult.metadata.clickedElement || null
|
|
558
715
|
})
|
|
559
716
|
});
|
|
717
|
+
if (response.status === 401) {
|
|
718
|
+
console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
|
|
719
|
+
setMessages([
|
|
720
|
+
{
|
|
721
|
+
role: "assistant",
|
|
722
|
+
content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
|
|
723
|
+
}
|
|
724
|
+
]);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
560
727
|
if (!response.ok) throw new Error("Failed to get AI response");
|
|
561
728
|
const data = await response.json();
|
|
562
729
|
setMessages([{ role: "assistant", content: data.message }]);
|
|
@@ -570,53 +737,23 @@ function ReportModal({
|
|
|
570
737
|
} finally {
|
|
571
738
|
setIsLoading(false);
|
|
572
739
|
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const newMessages = [
|
|
579
|
-
...messages,
|
|
580
|
-
{ role: "user", content: userMessage }
|
|
581
|
-
];
|
|
582
|
-
setMessages(newMessages);
|
|
583
|
-
setIsLoading(true);
|
|
584
|
-
try {
|
|
585
|
-
const response = await fetch(`${apiConfig.apiUrl}/chat`, {
|
|
586
|
-
method: "POST",
|
|
587
|
-
headers: apiHeaders,
|
|
588
|
-
body: JSON.stringify({
|
|
589
|
-
messages: newMessages,
|
|
590
|
-
metadata: captureResult.metadata,
|
|
591
|
-
consoleErrors: captureResult.consoleErrors,
|
|
592
|
-
networkErrors: captureResult.networkErrors,
|
|
593
|
-
clickedElement: captureResult.metadata.clickedElement || null
|
|
594
|
-
})
|
|
595
|
-
});
|
|
596
|
-
if (!response.ok) throw new Error("Failed to get AI response");
|
|
597
|
-
const data = await response.json();
|
|
598
|
-
setMessages([
|
|
599
|
-
...newMessages,
|
|
600
|
-
{ role: "assistant", content: data.message }
|
|
601
|
-
]);
|
|
602
|
-
if (data.readyToSubmit && data.structuredReport) {
|
|
603
|
-
await submitReport(
|
|
604
|
-
[...newMessages, { role: "assistant", content: data.message }],
|
|
605
|
-
data.structuredReport
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
} catch (e) {
|
|
609
|
-
setMessages([
|
|
610
|
-
...newMessages,
|
|
611
|
-
{
|
|
612
|
-
role: "assistant",
|
|
613
|
-
content: "I had trouble processing that. Could you try describing the issue again?"
|
|
614
|
-
}
|
|
615
|
-
]);
|
|
616
|
-
} finally {
|
|
617
|
-
setIsLoading(false);
|
|
740
|
+
}, [captureResult, apiConfig.apiUrl, apiHeaders]);
|
|
741
|
+
useEffect2(() => {
|
|
742
|
+
if (isOpen && captureResult && !hasInitRef.current) {
|
|
743
|
+
hasInitRef.current = true;
|
|
744
|
+
sendInitialMessage();
|
|
618
745
|
}
|
|
619
|
-
}
|
|
746
|
+
}, [isOpen, captureResult, sendInitialMessage]);
|
|
747
|
+
useEffect2(() => {
|
|
748
|
+
var _a;
|
|
749
|
+
(_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
750
|
+
}, [messages]);
|
|
751
|
+
useEffect2(() => {
|
|
752
|
+
var _a;
|
|
753
|
+
if (isOpen && !isLoading) {
|
|
754
|
+
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
755
|
+
}
|
|
756
|
+
}, [isOpen, isLoading, messages]);
|
|
620
757
|
const submitReport = useCallback2(
|
|
621
758
|
async (conversation, structuredReport) => {
|
|
622
759
|
if (!captureResult || modalState !== "chatting") return;
|
|
@@ -681,10 +818,67 @@ function ReportModal({
|
|
|
681
818
|
setModalState("error");
|
|
682
819
|
}
|
|
683
820
|
},
|
|
684
|
-
[captureResult, apiConfig, apiHeaders, modalState]
|
|
821
|
+
[captureResult, apiConfig.apiUrl, apiHeaders, modalState]
|
|
685
822
|
);
|
|
686
|
-
|
|
823
|
+
const handleManualSubmit = useCallback2(() => {
|
|
687
824
|
submitReport(messages);
|
|
825
|
+
}, [submitReport, messages]);
|
|
826
|
+
async function sendMessage() {
|
|
827
|
+
if (!input.trim() || isLoading || !captureResult) return;
|
|
828
|
+
const userMessage = input.trim();
|
|
829
|
+
setInput("");
|
|
830
|
+
const newMessages = [
|
|
831
|
+
...messages,
|
|
832
|
+
{ role: "user", content: userMessage }
|
|
833
|
+
];
|
|
834
|
+
setMessages(newMessages);
|
|
835
|
+
setIsLoading(true);
|
|
836
|
+
try {
|
|
837
|
+
const response = await fetch(`${apiConfig.apiUrl}/chat`, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers: apiHeaders,
|
|
840
|
+
body: JSON.stringify({
|
|
841
|
+
messages: newMessages,
|
|
842
|
+
metadata: captureResult.metadata,
|
|
843
|
+
consoleErrors: captureResult.consoleErrors,
|
|
844
|
+
networkErrors: captureResult.networkErrors,
|
|
845
|
+
clickedElement: captureResult.metadata.clickedElement || null
|
|
846
|
+
})
|
|
847
|
+
});
|
|
848
|
+
if (response.status === 401) {
|
|
849
|
+
console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
|
|
850
|
+
setMessages([
|
|
851
|
+
...newMessages,
|
|
852
|
+
{
|
|
853
|
+
role: "assistant",
|
|
854
|
+
content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
|
|
855
|
+
}
|
|
856
|
+
]);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (!response.ok) throw new Error("Failed to get AI response");
|
|
860
|
+
const data = await response.json();
|
|
861
|
+
setMessages([
|
|
862
|
+
...newMessages,
|
|
863
|
+
{ role: "assistant", content: data.message }
|
|
864
|
+
]);
|
|
865
|
+
if (data.readyToSubmit && data.structuredReport) {
|
|
866
|
+
await submitReport(
|
|
867
|
+
[...newMessages, { role: "assistant", content: data.message }],
|
|
868
|
+
data.structuredReport
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
} catch (e) {
|
|
872
|
+
setMessages([
|
|
873
|
+
...newMessages,
|
|
874
|
+
{
|
|
875
|
+
role: "assistant",
|
|
876
|
+
content: "I had trouble processing that. Could you try describing the issue again?"
|
|
877
|
+
}
|
|
878
|
+
]);
|
|
879
|
+
} finally {
|
|
880
|
+
setIsLoading(false);
|
|
881
|
+
}
|
|
688
882
|
}
|
|
689
883
|
function handleClose() {
|
|
690
884
|
setMessages([]);
|
|
@@ -692,11 +886,15 @@ function ReportModal({
|
|
|
692
886
|
setModalState("chatting");
|
|
693
887
|
setReportId(null);
|
|
694
888
|
setErrorMessage(null);
|
|
889
|
+
if (screenshotUrl) {
|
|
890
|
+
URL.revokeObjectURL(screenshotUrl);
|
|
891
|
+
}
|
|
892
|
+
setScreenshotUrl(null);
|
|
695
893
|
hasInitRef.current = false;
|
|
696
894
|
onClose();
|
|
697
895
|
}
|
|
698
896
|
function handleKeyDown(e) {
|
|
699
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
897
|
+
if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
|
|
700
898
|
e.preventDefault();
|
|
701
899
|
sendMessage();
|
|
702
900
|
}
|