@jarve/bug-reporter 0.1.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 ADDED
@@ -0,0 +1,919 @@
1
+ 'use client';
2
+ var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
4
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
5
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __spreadValues = (a, b) => {
10
+ for (var prop in b || (b = {}))
11
+ if (__hasOwnProp.call(b, prop))
12
+ __defNormalProp(a, prop, b[prop]);
13
+ if (__getOwnPropSymbols)
14
+ for (var prop of __getOwnPropSymbols(b)) {
15
+ if (__propIsEnum.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ }
18
+ return a;
19
+ };
20
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
+
22
+ // src/bug-reporter.tsx
23
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3 } from "react";
24
+
25
+ // src/floating-button.tsx
26
+ import { Bug } from "lucide-react";
27
+
28
+ // src/cn.ts
29
+ import { clsx } from "clsx";
30
+ import { twMerge } from "tailwind-merge";
31
+ function cn(...inputs) {
32
+ return twMerge(clsx(inputs));
33
+ }
34
+
35
+ // src/floating-button.tsx
36
+ import { jsx } from "react/jsx-runtime";
37
+ function FloatingButton({ isActive, onClick }) {
38
+ return /* @__PURE__ */ jsx(
39
+ "button",
40
+ {
41
+ onClick,
42
+ className: cn(
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
+ "hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2",
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"
47
+ ),
48
+ title: isActive ? "Cancel bug capture" : "Report a bug",
49
+ "aria-label": isActive ? "Cancel bug capture" : "Report a bug",
50
+ children: /* @__PURE__ */ jsx(Bug, { className: "h-4 w-4 md:h-5 md:w-5" })
51
+ }
52
+ );
53
+ }
54
+
55
+ // src/capture-overlay.tsx
56
+ import { useEffect, useState, useCallback, useRef } from "react";
57
+ import { toPng } from "html-to-image";
58
+
59
+ // src/utils.ts
60
+ import { UAParser } from "ua-parser-js";
61
+ function getNearestSection(element) {
62
+ let current = element;
63
+ while (current && current !== document.body) {
64
+ if (current.id || current.dataset.section || current.tagName === "SECTION" || current.tagName === "MAIN" || current.tagName === "ARTICLE" || current.tagName === "NAV" || current.tagName === "HEADER" || current.tagName === "FOOTER" || current.role === "main" || current.role === "navigation") {
65
+ return current;
66
+ }
67
+ current = current.parentElement;
68
+ }
69
+ return document.querySelector("main") || document.body;
70
+ }
71
+ function getSectionId(element) {
72
+ var _a;
73
+ if (element.id) return `#${element.id}`;
74
+ if (element.dataset.section) return element.dataset.section;
75
+ if (element.tagName === "MAIN") return "main";
76
+ if (element.tagName === "SECTION") {
77
+ const heading = element.querySelector("h1, h2, h3");
78
+ if (heading) return ((_a = heading.textContent) == null ? void 0 : _a.trim().slice(0, 60)) || null;
79
+ }
80
+ return element.tagName.toLowerCase();
81
+ }
82
+ function getDeviceType() {
83
+ const width = window.innerWidth;
84
+ const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
85
+ if (width <= 768 && hasTouch) return "mobile";
86
+ if (width <= 1024 && hasTouch) return "tablet";
87
+ return "desktop";
88
+ }
89
+ function parseUserAgent() {
90
+ const parser = new UAParser(navigator.userAgent);
91
+ const browser = parser.getBrowser();
92
+ const osInfo = parser.getOS();
93
+ return {
94
+ browser: `${browser.name || "Unknown"} ${browser.version || ""}`.trim(),
95
+ os: `${osInfo.name || "Unknown"} ${osInfo.version || ""}`.trim()
96
+ };
97
+ }
98
+ function buildSelectorPath(element, stopAt) {
99
+ const parts = [];
100
+ let current = element;
101
+ while (current && current !== document.body && current !== stopAt) {
102
+ let selector = current.tagName.toLowerCase();
103
+ if (current.id) {
104
+ selector += `#${current.id}`;
105
+ } else if (current.className && typeof current.className === "string") {
106
+ const classes = current.className.trim().split(/\s+/).slice(0, 2).join(".");
107
+ if (classes) selector += `.${classes}`;
108
+ }
109
+ parts.unshift(selector);
110
+ current = current.parentElement;
111
+ }
112
+ return parts.join(" > ");
113
+ }
114
+ function collectElementInfo(target, section, event) {
115
+ const sectionRect = section.getBoundingClientRect();
116
+ const dataAttributes = {};
117
+ for (const attr of Array.from(target.attributes)) {
118
+ if (attr.name.startsWith("data-") && attr.name !== "data-bug-reporter") {
119
+ dataAttributes[attr.name] = attr.value;
120
+ }
121
+ }
122
+ return {
123
+ tagName: target.tagName,
124
+ textContent: (target.textContent || "").trim().slice(0, 200) || null,
125
+ className: typeof target.className === "string" ? target.className : "",
126
+ id: target.id || null,
127
+ ariaLabel: target.getAttribute("aria-label") || null,
128
+ dataAttributes,
129
+ selectorPath: buildSelectorPath(target, section),
130
+ clickX: event.pageX,
131
+ clickY: event.pageY,
132
+ relativeClickX: event.clientX - sectionRect.left,
133
+ relativeClickY: event.clientY - sectionRect.top
134
+ };
135
+ }
136
+ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
137
+ const { browser, os } = parseUserAgent();
138
+ const now = /* @__PURE__ */ new Date();
139
+ return {
140
+ pageUrl: window.location.href,
141
+ sectionId: getSectionId(sectionElement),
142
+ viewportWidth: window.innerWidth,
143
+ viewportHeight: window.innerHeight,
144
+ deviceType: getDeviceType(),
145
+ browser,
146
+ os,
147
+ timestamp: now.toISOString(),
148
+ timestampUtc: now.toUTCString(),
149
+ siteId,
150
+ reporterName,
151
+ reporterEmail,
152
+ clickedElement
153
+ };
154
+ }
155
+
156
+ // src/console-capture.ts
157
+ var MAX_ERRORS = 50;
158
+ var capturedErrors = [];
159
+ var isCapturing = false;
160
+ var originalConsoleError = null;
161
+ var errorListener = null;
162
+ var rejectionListener = null;
163
+ function startCapturing() {
164
+ if (isCapturing) return;
165
+ if (console.error.__bugReporterPatched)
166
+ return;
167
+ isCapturing = true;
168
+ capturedErrors = [];
169
+ originalConsoleError = console.error;
170
+ const patchedConsoleError = (...args) => {
171
+ const MAX_MSG_LEN = 500;
172
+ const message = args.map((a) => {
173
+ if (typeof a === "string") return a.slice(0, MAX_MSG_LEN);
174
+ try {
175
+ const s = JSON.stringify(a);
176
+ return s.slice(0, MAX_MSG_LEN);
177
+ } catch (e) {
178
+ return String(a).slice(0, MAX_MSG_LEN);
179
+ }
180
+ }).join(" ").slice(0, MAX_MSG_LEN);
181
+ capturedErrors.push({
182
+ message,
183
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
184
+ });
185
+ if (capturedErrors.length > MAX_ERRORS) {
186
+ capturedErrors = capturedErrors.slice(-MAX_ERRORS);
187
+ }
188
+ originalConsoleError.apply(console, args);
189
+ };
190
+ patchedConsoleError.__bugReporterPatched = true;
191
+ console.error = patchedConsoleError;
192
+ errorListener = (event) => {
193
+ capturedErrors.push({
194
+ message: event.message,
195
+ source: event.filename,
196
+ lineno: event.lineno,
197
+ colno: event.colno,
198
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
199
+ });
200
+ if (capturedErrors.length > MAX_ERRORS) {
201
+ capturedErrors = capturedErrors.slice(-MAX_ERRORS);
202
+ }
203
+ };
204
+ window.addEventListener("error", errorListener);
205
+ rejectionListener = (event) => {
206
+ capturedErrors.push({
207
+ message: `Unhandled Promise Rejection: ${event.reason instanceof Error ? event.reason.message : String(event.reason)}`,
208
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
209
+ });
210
+ if (capturedErrors.length > MAX_ERRORS) {
211
+ capturedErrors = capturedErrors.slice(-MAX_ERRORS);
212
+ }
213
+ };
214
+ window.addEventListener("unhandledrejection", rejectionListener);
215
+ }
216
+ function stopCapturing() {
217
+ if (!isCapturing) return;
218
+ isCapturing = false;
219
+ if (originalConsoleError) {
220
+ const restoreTarget = originalConsoleError;
221
+ console.error = restoreTarget;
222
+ if (console.error !== restoreTarget) {
223
+ console.warn("Bug reporter: failed to restore original console.error");
224
+ }
225
+ originalConsoleError = null;
226
+ }
227
+ if (errorListener) {
228
+ window.removeEventListener("error", errorListener);
229
+ errorListener = null;
230
+ }
231
+ if (rejectionListener) {
232
+ window.removeEventListener("unhandledrejection", rejectionListener);
233
+ rejectionListener = null;
234
+ }
235
+ }
236
+ function getCapturedErrors() {
237
+ return [...capturedErrors];
238
+ }
239
+ function clearCapturedErrors() {
240
+ capturedErrors = [];
241
+ }
242
+
243
+ // src/network-capture.ts
244
+ var MAX_REQUESTS = 30;
245
+ var capturedRequests = [];
246
+ var isCapturing2 = false;
247
+ var originalFetch = null;
248
+ function truncateBody(body, maxLen = 500) {
249
+ if (!body) return null;
250
+ if (body.length <= maxLen) return body;
251
+ return body.slice(0, maxLen) + "...(truncated)";
252
+ }
253
+ function startNetworkCapture() {
254
+ if (isCapturing2 || typeof window === "undefined") return;
255
+ if (window.fetch.__bugReporterPatched) return;
256
+ isCapturing2 = true;
257
+ capturedRequests = [];
258
+ originalFetch = window.fetch;
259
+ const patchedFetch = async function patchedFetch2(input, init) {
260
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
261
+ const method = (init == null ? void 0 : init.method) || (typeof input !== "string" && !(input instanceof URL) ? input.method : "GET") || "GET";
262
+ if (url.includes("/api/bug-reporter/") || url.includes("/bug-reporter/external/") || url.startsWith("data:") || url.startsWith("blob:")) {
263
+ return originalFetch.call(window, input, init);
264
+ }
265
+ try {
266
+ const response = await originalFetch.call(window, input, init);
267
+ 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
+ }
275
+ capturedRequests.push({
276
+ url,
277
+ method: method.toUpperCase(),
278
+ status: response.status,
279
+ statusText: response.statusText,
280
+ responseBody,
281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
282
+ });
283
+ if (capturedRequests.length > MAX_REQUESTS) {
284
+ capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
285
+ }
286
+ }
287
+ return response;
288
+ } catch (error) {
289
+ capturedRequests.push({
290
+ url,
291
+ method: method.toUpperCase(),
292
+ status: 0,
293
+ statusText: error instanceof Error ? error.message : "Network error",
294
+ responseBody: null,
295
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
296
+ });
297
+ if (capturedRequests.length > MAX_REQUESTS) {
298
+ capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
299
+ }
300
+ throw error;
301
+ }
302
+ };
303
+ patchedFetch.__bugReporterPatched = true;
304
+ window.fetch = patchedFetch;
305
+ }
306
+ function stopNetworkCapture() {
307
+ if (!isCapturing2) return;
308
+ isCapturing2 = false;
309
+ if (originalFetch) {
310
+ const restoreTarget = originalFetch;
311
+ window.fetch = restoreTarget;
312
+ if (window.fetch !== restoreTarget) {
313
+ console.warn("Bug reporter: failed to restore original fetch");
314
+ }
315
+ originalFetch = null;
316
+ }
317
+ }
318
+ function getCapturedNetworkErrors() {
319
+ return [...capturedRequests];
320
+ }
321
+ function clearCapturedNetworkErrors() {
322
+ capturedRequests = [];
323
+ }
324
+
325
+ // src/capture-overlay.tsx
326
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
327
+ function CaptureOverlay({
328
+ isActive,
329
+ siteId,
330
+ reporterName,
331
+ reporterEmail,
332
+ onCapture,
333
+ onCancel
334
+ }) {
335
+ const [hoveredElement, setHoveredElement] = useState(null);
336
+ const [hoveredRect, setHoveredRect] = useState(null);
337
+ const [isCapturing3, setIsCapturing] = useState(false);
338
+ const overlayRef = useRef(null);
339
+ const hoveredElementRef = useRef(null);
340
+ const rafRef = useRef(null);
341
+ const handleMouseMove = useCallback(
342
+ (e) => {
343
+ if (!isActive || isCapturing3) return;
344
+ if (rafRef.current) return;
345
+ rafRef.current = requestAnimationFrame(() => {
346
+ rafRef.current = null;
347
+ const target = e.target;
348
+ if (!(target instanceof HTMLElement)) return;
349
+ if (target.closest("[data-bug-reporter]")) {
350
+ setHoveredElement(null);
351
+ setHoveredRect(null);
352
+ hoveredElementRef.current = null;
353
+ return;
354
+ }
355
+ const section = getNearestSection(target);
356
+ setHoveredElement(section);
357
+ hoveredElementRef.current = section;
358
+ setHoveredRect(section ? section.getBoundingClientRect() : null);
359
+ });
360
+ },
361
+ [isActive, isCapturing3]
362
+ );
363
+ const handleClick = useCallback(
364
+ async (e) => {
365
+ var _a;
366
+ if (!isActive || isCapturing3) return;
367
+ const target = e.target;
368
+ if (!(target instanceof HTMLElement)) return;
369
+ if (target.closest("[data-bug-reporter]")) return;
370
+ e.preventDefault();
371
+ e.stopPropagation();
372
+ const section = getNearestSection(target);
373
+ 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);
424
+ }
425
+ },
426
+ [isActive, isCapturing3, siteId, reporterName, reporterEmail, onCapture]
427
+ );
428
+ const handleKeyDown = useCallback(
429
+ (e) => {
430
+ if (e.key === "Escape" && isActive) {
431
+ e.preventDefault();
432
+ e.stopPropagation();
433
+ onCancel();
434
+ }
435
+ },
436
+ [isActive, onCancel]
437
+ );
438
+ const handleScroll = useCallback(() => {
439
+ if (!hoveredElementRef.current) return;
440
+ setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
441
+ }, []);
442
+ useEffect(() => {
443
+ if (!isActive) {
444
+ setHoveredElement(null);
445
+ setHoveredRect(null);
446
+ hoveredElementRef.current = null;
447
+ return;
448
+ }
449
+ document.addEventListener("mousemove", handleMouseMove, true);
450
+ document.addEventListener("click", handleClick, true);
451
+ document.addEventListener("keydown", handleKeyDown);
452
+ window.addEventListener("scroll", handleScroll, { passive: true });
453
+ return () => {
454
+ document.removeEventListener("mousemove", handleMouseMove, true);
455
+ document.removeEventListener("click", handleClick, true);
456
+ document.removeEventListener("keydown", handleKeyDown);
457
+ window.removeEventListener("scroll", handleScroll);
458
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
459
+ };
460
+ }, [isActive, handleMouseMove, handleClick, handleKeyDown, handleScroll]);
461
+ if (!isActive || !hoveredElement || !hoveredRect) return null;
462
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
463
+ /* @__PURE__ */ jsxs(
464
+ "div",
465
+ {
466
+ "data-bug-reporter": true,
467
+ role: "alert",
468
+ "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: [
471
+ "Click on the section with the bug. Press ",
472
+ /* @__PURE__ */ jsx2("kbd", { className: "px-1.5 py-0.5 bg-indigo-800 rounded text-xs mx-1", children: "Esc" }),
473
+ " to cancel."
474
+ ]
475
+ }
476
+ ),
477
+ /* @__PURE__ */ jsx2(
478
+ "div",
479
+ {
480
+ ref: overlayRef,
481
+ "data-bug-reporter": true,
482
+ className: "fixed pointer-events-none z-[9998] border-2 border-indigo-500 rounded-sm transition-all duration-150 ease-out",
483
+ style: {
484
+ top: hoveredRect.top - 2,
485
+ left: hoveredRect.left - 2,
486
+ width: hoveredRect.width + 4,
487
+ height: hoveredRect.height + 4,
488
+ backgroundColor: "rgba(99, 102, 241, 0.08)"
489
+ }
490
+ }
491
+ ),
492
+ isActive && /* @__PURE__ */ jsx2("style", { children: `* { cursor: crosshair !important; }` })
493
+ ] });
494
+ }
495
+
496
+ // src/report-modal.tsx
497
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
498
+ import { X, Send, Loader2, CheckCircle2 } from "lucide-react";
499
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
500
+ function ReportModal({
501
+ isOpen,
502
+ captureResult,
503
+ apiConfig,
504
+ siteId,
505
+ user,
506
+ onClose
507
+ }) {
508
+ const [messages, setMessages] = useState2([]);
509
+ const [input, setInput] = useState2("");
510
+ const [isLoading, setIsLoading] = useState2(false);
511
+ const [modalState, setModalState] = useState2("chatting");
512
+ const [reportId, setReportId] = useState2(null);
513
+ const [screenshotUrl, setScreenshotUrl] = useState2(null);
514
+ const [errorMessage, setErrorMessage] = useState2(null);
515
+ const chatEndRef = useRef2(null);
516
+ const inputRef = useRef2(null);
517
+ const hasInitRef = useRef2(false);
518
+ const apiHeaders = {
519
+ "Content-Type": "application/json",
520
+ "X-Bug-Reporter-Key": apiConfig.apiKey
521
+ };
522
+ useEffect2(() => {
523
+ if ((captureResult == null ? void 0 : captureResult.screenshot) && captureResult.screenshot.size > 0) {
524
+ const url = URL.createObjectURL(captureResult.screenshot);
525
+ setScreenshotUrl(url);
526
+ return () => URL.revokeObjectURL(url);
527
+ }
528
+ }, [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() {
546
+ if (!captureResult) return;
547
+ setIsLoading(true);
548
+ try {
549
+ const response = await fetch(`${apiConfig.apiUrl}/chat`, {
550
+ method: "POST",
551
+ headers: apiHeaders,
552
+ body: JSON.stringify({
553
+ messages: [],
554
+ metadata: captureResult.metadata,
555
+ consoleErrors: captureResult.consoleErrors,
556
+ networkErrors: captureResult.networkErrors,
557
+ clickedElement: captureResult.metadata.clickedElement || null
558
+ })
559
+ });
560
+ if (!response.ok) throw new Error("Failed to get AI response");
561
+ const data = await response.json();
562
+ setMessages([{ role: "assistant", content: data.message }]);
563
+ } catch (e) {
564
+ setMessages([
565
+ {
566
+ role: "assistant",
567
+ content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
568
+ }
569
+ ]);
570
+ } finally {
571
+ setIsLoading(false);
572
+ }
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);
618
+ }
619
+ }
620
+ const submitReport = useCallback2(
621
+ async (conversation, structuredReport) => {
622
+ if (!captureResult || modalState !== "chatting") return;
623
+ setModalState("submitting");
624
+ try {
625
+ let screenshotBase64;
626
+ if (captureResult.screenshot.size > 0) {
627
+ const reader = new FileReader();
628
+ screenshotBase64 = await new Promise((resolve, reject) => {
629
+ reader.onload = () => resolve(reader.result);
630
+ reader.onerror = reject;
631
+ reader.readAsDataURL(captureResult.screenshot);
632
+ });
633
+ }
634
+ let report = structuredReport;
635
+ if (!report) {
636
+ try {
637
+ const summaryResponse = await fetch(`${apiConfig.apiUrl}/chat`, {
638
+ method: "POST",
639
+ headers: apiHeaders,
640
+ body: JSON.stringify({
641
+ messages: conversation,
642
+ metadata: captureResult.metadata,
643
+ consoleErrors: captureResult.consoleErrors,
644
+ networkErrors: captureResult.networkErrors,
645
+ clickedElement: captureResult.metadata.clickedElement || null,
646
+ requestSummary: true
647
+ })
648
+ });
649
+ if (summaryResponse.ok) {
650
+ const data2 = await summaryResponse.json();
651
+ report = data2.structuredReport;
652
+ }
653
+ } catch (e) {
654
+ }
655
+ }
656
+ const response = await fetch(`${apiConfig.apiUrl}/submit`, {
657
+ method: "POST",
658
+ headers: apiHeaders,
659
+ body: JSON.stringify({
660
+ screenshot: screenshotBase64,
661
+ metadata: captureResult.metadata,
662
+ conversation,
663
+ structuredReport: report,
664
+ consoleErrors: captureResult.consoleErrors,
665
+ networkErrors: captureResult.networkErrors,
666
+ clickedElement: captureResult.metadata.clickedElement || null
667
+ })
668
+ });
669
+ if (!response.ok) {
670
+ const errorData = await response.json().catch(() => ({}));
671
+ throw new Error(errorData.error || "Failed to submit report");
672
+ }
673
+ const data = await response.json();
674
+ setReportId(data.id);
675
+ setModalState("submitted");
676
+ } catch (err) {
677
+ console.error("Bug reporter: failed to submit report", err);
678
+ setErrorMessage(
679
+ err instanceof Error ? err.message : "Failed to submit report"
680
+ );
681
+ setModalState("error");
682
+ }
683
+ },
684
+ [captureResult, apiConfig, apiHeaders, modalState]
685
+ );
686
+ function handleManualSubmit() {
687
+ submitReport(messages);
688
+ }
689
+ function handleClose() {
690
+ setMessages([]);
691
+ setInput("");
692
+ setModalState("chatting");
693
+ setReportId(null);
694
+ setErrorMessage(null);
695
+ hasInitRef.current = false;
696
+ onClose();
697
+ }
698
+ function handleKeyDown(e) {
699
+ if (e.key === "Enter" && !e.shiftKey) {
700
+ e.preventDefault();
701
+ sendMessage();
702
+ }
703
+ }
704
+ if (!isOpen) return null;
705
+ return /* @__PURE__ */ jsx3(
706
+ "div",
707
+ {
708
+ "data-bug-reporter": true,
709
+ className: "fixed inset-0 z-[10001] flex items-center justify-center bg-black/50 backdrop-blur-sm",
710
+ onClick: (e) => {
711
+ if (e.target === e.currentTarget) handleClose();
712
+ },
713
+ children: /* @__PURE__ */ jsxs2(
714
+ "div",
715
+ {
716
+ className: cn(
717
+ "bg-white dark:bg-gray-950 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-800 flex flex-col overflow-hidden",
718
+ "w-full max-w-lg mx-4",
719
+ "max-[768px]:mx-0 max-[768px]:rounded-none max-[768px]:max-w-none max-[768px]:h-full",
720
+ "min-[769px]:max-h-[85vh]"
721
+ ),
722
+ children: [
723
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-800 bg-gray-50/30 dark:bg-gray-900/30", children: [
724
+ /* @__PURE__ */ jsxs2("div", { children: [
725
+ /* @__PURE__ */ jsx3("h2", { className: "font-semibold text-sm text-gray-900 dark:text-gray-100", children: "Bug Report" }),
726
+ /* @__PURE__ */ jsx3("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: siteId })
727
+ ] }),
728
+ /* @__PURE__ */ jsx3(
729
+ "button",
730
+ {
731
+ onClick: handleClose,
732
+ className: "p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
733
+ "aria-label": "Close",
734
+ children: /* @__PURE__ */ jsx3(X, { className: "h-4 w-4 text-gray-600 dark:text-gray-400" })
735
+ }
736
+ )
737
+ ] }),
738
+ screenshotUrl && /* @__PURE__ */ jsx3("div", { className: "px-4 py-3 border-b border-gray-200 dark:border-gray-800 bg-gray-50/10 dark:bg-gray-900/10", children: /* @__PURE__ */ jsx3(
739
+ "img",
740
+ {
741
+ src: screenshotUrl,
742
+ alt: "Captured section",
743
+ className: "w-full max-h-40 object-contain rounded-md border border-gray-200 dark:border-gray-700"
744
+ }
745
+ ) }),
746
+ /* @__PURE__ */ jsx3("div", { className: "flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-0", children: modalState === "submitted" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
747
+ /* @__PURE__ */ jsx3(CheckCircle2, { className: "h-12 w-12 text-green-500 mb-3" }),
748
+ /* @__PURE__ */ jsx3("h3", { className: "font-semibold text-lg text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
749
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: [
750
+ "Reference: ",
751
+ /* @__PURE__ */ jsx3("code", { className: "text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded", children: reportId == null ? void 0 : reportId.slice(0, 8) })
752
+ ] }),
753
+ /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-2", children: "Thanks for the report \u2014 we'll look into it." }),
754
+ /* @__PURE__ */ jsx3(
755
+ "button",
756
+ {
757
+ onClick: handleClose,
758
+ className: "mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700 transition-colors",
759
+ children: "Done"
760
+ }
761
+ )
762
+ ] }) : modalState === "error" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
763
+ /* @__PURE__ */ jsx3(X, { className: "h-12 w-12 text-red-500 mb-3" }),
764
+ /* @__PURE__ */ jsx3("h3", { className: "font-semibold text-lg text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
765
+ /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: errorMessage || "Something went wrong. Please try again." }),
766
+ /* @__PURE__ */ jsx3(
767
+ "button",
768
+ {
769
+ onClick: () => setModalState("chatting"),
770
+ className: "mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700 transition-colors",
771
+ children: "Try Again"
772
+ }
773
+ )
774
+ ] }) : modalState === "submitting" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8", children: [
775
+ /* @__PURE__ */ jsx3(Loader2, { className: "h-8 w-8 animate-spin text-indigo-500 mb-3" }),
776
+ /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
777
+ ] }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
778
+ (captureResult == null ? void 0 : captureResult.screenshot.size) === 0 && /* @__PURE__ */ jsx3("p", { className: "text-xs text-amber-600", children: "Screenshot could not be captured. Please describe the visual issue in detail." }),
779
+ messages.map((msg, i) => /* @__PURE__ */ jsx3(
780
+ "div",
781
+ {
782
+ className: cn(
783
+ "text-sm leading-relaxed",
784
+ msg.role === "assistant" ? "bg-gray-100/50 dark:bg-gray-800/50 rounded-lg p-3 text-gray-900 dark:text-gray-100" : "bg-indigo-600 text-white rounded-lg p-3 ml-8"
785
+ ),
786
+ children: msg.content
787
+ },
788
+ i
789
+ )),
790
+ isLoading && /* @__PURE__ */ jsxs2("div", { className: "bg-gray-100/50 dark:bg-gray-800/50 rounded-lg p-3 flex items-center gap-2", children: [
791
+ /* @__PURE__ */ jsx3(Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
792
+ /* @__PURE__ */ jsx3("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
793
+ ] }),
794
+ /* @__PURE__ */ jsx3("div", { ref: chatEndRef })
795
+ ] }) }),
796
+ modalState === "chatting" && /* @__PURE__ */ jsxs2("div", { className: "border-t border-gray-200 dark:border-gray-800 px-4 py-3", children: [
797
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
798
+ /* @__PURE__ */ jsx3(
799
+ "textarea",
800
+ {
801
+ ref: inputRef,
802
+ value: input,
803
+ onChange: (e) => setInput(e.target.value),
804
+ onKeyDown: handleKeyDown,
805
+ placeholder: "Describe what's wrong...",
806
+ rows: 2,
807
+ className: "flex-1 resize-none rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-950 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent",
808
+ disabled: isLoading
809
+ }
810
+ ),
811
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1", children: [
812
+ /* @__PURE__ */ jsx3(
813
+ "button",
814
+ {
815
+ onClick: sendMessage,
816
+ disabled: !input.trim() || isLoading,
817
+ className: "p-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
818
+ title: "Send message",
819
+ children: /* @__PURE__ */ jsx3(Send, { className: "h-4 w-4" })
820
+ }
821
+ ),
822
+ messages.length >= 2 && /* @__PURE__ */ jsx3(
823
+ "button",
824
+ {
825
+ onClick: handleManualSubmit,
826
+ disabled: isLoading,
827
+ className: "px-2 py-1 rounded-md bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 text-xs font-medium transition-colors",
828
+ title: "Submit report now",
829
+ children: "Submit"
830
+ }
831
+ )
832
+ ] })
833
+ ] }),
834
+ captureResult && (captureResult.consoleErrors.length > 0 || captureResult.networkErrors.length > 0) && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-amber-600 mt-1.5", children: [
835
+ [
836
+ captureResult.consoleErrors.length > 0 ? `${captureResult.consoleErrors.length} console error${captureResult.consoleErrors.length !== 1 ? "s" : ""}` : null,
837
+ captureResult.networkErrors.length > 0 ? `${captureResult.networkErrors.length} failed request${captureResult.networkErrors.length !== 1 ? "s" : ""}` : null
838
+ ].filter(Boolean).join(" + "),
839
+ " ",
840
+ "captured \u2014 these will be included in the report."
841
+ ] })
842
+ ] })
843
+ ]
844
+ }
845
+ )
846
+ }
847
+ );
848
+ }
849
+
850
+ // src/bug-reporter.tsx
851
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
852
+ function JarveBugReporter({
853
+ apiUrl,
854
+ apiKey,
855
+ user,
856
+ children
857
+ }) {
858
+ const [captureMode, setCaptureMode] = useState3(false);
859
+ const [captureResult, setCaptureResult] = useState3(null);
860
+ const [showModal, setShowModal] = useState3(false);
861
+ useEffect3(() => {
862
+ startCapturing();
863
+ startNetworkCapture();
864
+ return () => {
865
+ stopCapturing();
866
+ stopNetworkCapture();
867
+ };
868
+ }, []);
869
+ const toggleCaptureMode = useCallback3(() => {
870
+ setCaptureMode((prev) => !prev);
871
+ }, []);
872
+ const handleCapture = useCallback3((result) => {
873
+ setCaptureResult(result);
874
+ setCaptureMode(false);
875
+ setShowModal(true);
876
+ }, []);
877
+ const handleCancelCapture = useCallback3(() => {
878
+ setCaptureMode(false);
879
+ }, []);
880
+ const handleCloseModal = useCallback3(() => {
881
+ setShowModal(false);
882
+ setCaptureResult(null);
883
+ clearCapturedErrors();
884
+ clearCapturedNetworkErrors();
885
+ }, []);
886
+ const siteId = apiKey.startsWith("brk_") ? apiKey.slice(4, 12) : "external";
887
+ const reporterName = (user == null ? void 0 : user.name) || "Anonymous";
888
+ const reporterEmail = (user == null ? void 0 : user.email) || "unknown@external";
889
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
890
+ children,
891
+ /* @__PURE__ */ jsx4(FloatingButton, { isActive: captureMode, onClick: toggleCaptureMode }),
892
+ /* @__PURE__ */ jsx4(
893
+ CaptureOverlay,
894
+ {
895
+ isActive: captureMode,
896
+ siteId,
897
+ reporterName,
898
+ reporterEmail,
899
+ onCapture: handleCapture,
900
+ onCancel: handleCancelCapture
901
+ }
902
+ ),
903
+ /* @__PURE__ */ jsx4(
904
+ ReportModal,
905
+ {
906
+ isOpen: showModal,
907
+ captureResult,
908
+ apiConfig: { apiUrl, apiKey },
909
+ siteId,
910
+ user: { name: reporterName, email: reporterEmail },
911
+ onClose: handleCloseModal
912
+ }
913
+ )
914
+ ] });
915
+ }
916
+ export {
917
+ JarveBugReporter
918
+ };
919
+ //# sourceMappingURL=index.mjs.map