@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/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-10 w-10 md:bottom-6 md:right-6 md:h-12 md:w-12"
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 collectElementInfo(target, section, event) {
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: event.pageX,
131
- clickY: event.pageY,
132
- relativeClickX: event.clientX - sectionRect.left,
133
- relativeClickY: event.clientY - sectionRect.top
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 = 500) {
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
- let responseBody = null;
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
- var _a;
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
- const elementInfo = collectElementInfo(target, section, e);
375
- setIsCapturing(true);
376
- try {
377
- setHoveredElement(null);
378
- setHoveredRect(null);
379
- hoveredElementRef.current = null;
380
- await new Promise((r) => setTimeout(r, 50));
381
- const MAX_DIMENSION = 2e3;
382
- const sectionRect = section.getBoundingClientRect();
383
- const pixelRatio = sectionRect.width > MAX_DIMENSION || sectionRect.height > MAX_DIMENSION ? 1 : 2;
384
- const dataUrl = await toPng(section, {
385
- quality: 0.9,
386
- pixelRatio,
387
- skipFonts: true
388
- });
389
- const [header, base64] = dataUrl.split(",");
390
- const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
391
- const bytes = atob(base64);
392
- const arr = new Uint8Array(bytes.length);
393
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
394
- const blob = new Blob([arr], { type: mime });
395
- const metadata = collectMetadata(
396
- section,
397
- siteId,
398
- reporterName,
399
- reporterEmail,
400
- elementInfo
401
- );
402
- const consoleErrors = getCapturedErrors();
403
- const networkErrors = getCapturedNetworkErrors();
404
- onCapture({ screenshot: blob, metadata, consoleErrors, networkErrors });
405
- } catch (err) {
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, isCapturing3, siteId, reporterName, reporterEmail, onCapture]
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 (!hoveredElementRef.current) return;
440
- setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
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("mousemove", handleMouseMove, true);
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("mousemove", handleMouseMove, true);
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
- if (!isActive || !hoveredElement || !hoveredRect) return null;
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__ */ jsxs(
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: hoveredRect.top - 2,
485
- left: hoveredRect.left - 2,
486
- width: hoveredRect.width + 4,
487
- height: hoveredRect.height + 4,
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
- isActive && /* @__PURE__ */ jsx2("style", { children: `* { cursor: crosshair !important; }` })
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
- "Content-Type": "application/json",
520
- "X-Bug-Reporter-Key": apiConfig.apiKey
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(url);
526
- return () => URL.revokeObjectURL(url);
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
- useEffect2(() => {
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
- async function sendMessage() {
575
- if (!input.trim() || isLoading || !captureResult) return;
576
- const userMessage = input.trim();
577
- setInput("");
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
- function handleManualSubmit() {
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
  }