@jarve/bug-reporter 0.4.2 → 1.0.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 +3 -0
- package/dist/index.d.mts +134 -14
- package/dist/index.d.ts +134 -14
- package/dist/index.js +653 -373
- package/dist/index.mjs +646 -368
- package/dist/styles.css +2 -0
- package/package.json +23 -9
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -20,26 +20,26 @@ var __spreadValues = (a, b) => {
|
|
|
20
20
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
21
|
|
|
22
22
|
// src/bug-reporter.tsx
|
|
23
|
-
import { useState as useState4, useEffect as useEffect4, useCallback as useCallback3 } from "react";
|
|
23
|
+
import { useState as useState4, useEffect as useEffect4, useCallback as useCallback3, useMemo as useMemo2 } from "react";
|
|
24
24
|
|
|
25
25
|
// src/floating-button.tsx
|
|
26
26
|
import { useState, useEffect, useRef } from "react";
|
|
27
27
|
import { Bug } from "lucide-react";
|
|
28
|
-
|
|
29
|
-
// src/cn.ts
|
|
30
|
-
import { clsx } from "clsx";
|
|
31
|
-
import { twMerge } from "tailwind-merge";
|
|
32
|
-
function cn(...inputs) {
|
|
33
|
-
return twMerge(clsx(inputs));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/floating-button.tsx
|
|
28
|
+
import { cn, on } from "@jarve/widget-shared";
|
|
37
29
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
38
30
|
var STACK_OFFSET = 56;
|
|
39
|
-
function
|
|
31
|
+
function hasLauncherElement() {
|
|
32
|
+
return typeof document !== "undefined" && !!document.querySelector("[data-jarve-launcher]");
|
|
33
|
+
}
|
|
34
|
+
function FloatingButton({
|
|
35
|
+
isActive,
|
|
36
|
+
onClick,
|
|
37
|
+
position = "right",
|
|
38
|
+
zIndexBase = 1e4
|
|
39
|
+
}) {
|
|
40
40
|
const [hovered, setHovered] = useState(false);
|
|
41
41
|
const [stackOffset, setStackOffset] = useState(0);
|
|
42
|
-
const [launcherPresent, setLauncherPresent] = useState(
|
|
42
|
+
const [launcherPresent, setLauncherPresent] = useState(hasLauncherElement);
|
|
43
43
|
const ref = useRef(null);
|
|
44
44
|
const isLeft = position === "left";
|
|
45
45
|
const sideClasses = isLeft ? "left-4 md:left-6" : "right-4 md:right-6";
|
|
@@ -48,6 +48,7 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
48
48
|
if (!el) return;
|
|
49
49
|
let rafId;
|
|
50
50
|
const recalculate = () => {
|
|
51
|
+
if (!ref.current) return;
|
|
51
52
|
const widgets = Array.from(
|
|
52
53
|
document.querySelectorAll(`[data-jarve-widget][data-jarve-position="${position}"]`)
|
|
53
54
|
);
|
|
@@ -59,25 +60,26 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
59
60
|
const index = widgets.indexOf(el);
|
|
60
61
|
setStackOffset(index > 0 ? index * STACK_OFFSET : 0);
|
|
61
62
|
};
|
|
62
|
-
|
|
63
|
-
const observer = new MutationObserver(() => {
|
|
63
|
+
const scheduleRecalculate = () => {
|
|
64
64
|
cancelAnimationFrame(rafId);
|
|
65
65
|
rafId = requestAnimationFrame(recalculate);
|
|
66
|
-
}
|
|
67
|
-
|
|
66
|
+
};
|
|
67
|
+
recalculate();
|
|
68
|
+
const offReg = on("jarve:widget-registered", () => scheduleRecalculate());
|
|
69
|
+
const offDereg = on("jarve:widget-deregistered", () => scheduleRecalculate());
|
|
68
70
|
return () => {
|
|
69
|
-
|
|
71
|
+
offReg();
|
|
72
|
+
offDereg();
|
|
70
73
|
cancelAnimationFrame(rafId);
|
|
71
74
|
};
|
|
72
75
|
}, [position]);
|
|
73
76
|
useEffect(() => {
|
|
74
|
-
const
|
|
75
|
-
|
|
77
|
+
const offMounted = on("jarve:launcher-mounted", () => setLauncherPresent(true));
|
|
78
|
+
const offUnmounted = on("jarve:launcher-unmounted", () => setLauncherPresent(false));
|
|
79
|
+
return () => {
|
|
80
|
+
offMounted();
|
|
81
|
+
offUnmounted();
|
|
76
82
|
};
|
|
77
|
-
checkLauncher();
|
|
78
|
-
const observer = new MutationObserver(checkLauncher);
|
|
79
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
80
|
-
return () => observer.disconnect();
|
|
81
83
|
}, []);
|
|
82
84
|
if (launcherPresent) return null;
|
|
83
85
|
return /* @__PURE__ */ jsxs(
|
|
@@ -86,8 +88,10 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
86
88
|
ref,
|
|
87
89
|
"data-jarve-widget": "bug-reporter",
|
|
88
90
|
"data-jarve-position": position,
|
|
89
|
-
className: cn("fixed z-[
|
|
90
|
-
style:
|
|
91
|
+
className: cn("fixed z-[calc(var(--jarve-z-base)-1)]", "bottom-4 md:bottom-6", sideClasses),
|
|
92
|
+
style: __spreadValues({
|
|
93
|
+
["--jarve-z-base"]: String(zIndexBase)
|
|
94
|
+
}, stackOffset > 0 ? { transform: `translateY(-${stackOffset}px)` } : {}),
|
|
91
95
|
onMouseEnter: () => setHovered(true),
|
|
92
96
|
onMouseLeave: () => setHovered(false),
|
|
93
97
|
children: [
|
|
@@ -121,7 +125,7 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
121
125
|
onClick,
|
|
122
126
|
className: cn(
|
|
123
127
|
"flex items-center justify-center rounded-full shadow-lg transition-all duration-200",
|
|
124
|
-
"
|
|
128
|
+
"focus:ring-2 focus:ring-offset-2 focus:outline-none motion-safe:hover:scale-110",
|
|
125
129
|
"h-11 w-11 md:h-12 md:w-12",
|
|
126
130
|
isActive ? "animate-pulse bg-red-500 text-white focus:ring-red-400" : "bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-400"
|
|
127
131
|
),
|
|
@@ -137,10 +141,9 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
137
141
|
|
|
138
142
|
// src/capture-overlay.tsx
|
|
139
143
|
import { useEffect as useEffect2, useState as useState2, useCallback, useRef as useRef2 } from "react";
|
|
140
|
-
import { toPng } from "html-to-image";
|
|
141
144
|
|
|
142
145
|
// src/utils.ts
|
|
143
|
-
import {
|
|
146
|
+
import { parseUserAgent as parseSharedUserAgent } from "@jarve/widget-shared";
|
|
144
147
|
function getNearestSection(element) {
|
|
145
148
|
let current = element;
|
|
146
149
|
while (current && current !== document.body) {
|
|
@@ -170,13 +173,7 @@ function getDeviceType() {
|
|
|
170
173
|
return "desktop";
|
|
171
174
|
}
|
|
172
175
|
function parseUserAgent() {
|
|
173
|
-
|
|
174
|
-
const browser = parser.getBrowser();
|
|
175
|
-
const osInfo = parser.getOS();
|
|
176
|
-
return {
|
|
177
|
-
browser: `${browser.name || "Unknown"} ${browser.version || ""}`.trim(),
|
|
178
|
-
os: `${osInfo.name || "Unknown"} ${osInfo.version || ""}`.trim()
|
|
179
|
-
};
|
|
176
|
+
return parseSharedUserAgent();
|
|
180
177
|
}
|
|
181
178
|
function buildSelectorPath(element, stopAt) {
|
|
182
179
|
const parts = [];
|
|
@@ -247,84 +244,114 @@ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, cl
|
|
|
247
244
|
};
|
|
248
245
|
}
|
|
249
246
|
|
|
247
|
+
// src/console-capture.ts
|
|
248
|
+
import { createPatchManager } from "@jarve/widget-shared";
|
|
249
|
+
|
|
250
|
+
// src/redact.ts
|
|
251
|
+
var SENSITIVE_JSON_KEY = /("(?:token|password|secret|api_?key|authorization)"\s*:\s*")[^"]*"/gi;
|
|
252
|
+
var BEARER_TOKEN = /(bearer)\s+[^\s"',;}]+/gi;
|
|
253
|
+
var COOKIE_HEADER = /(set-cookie|cookie)\s*:\s*[^\r\n]+/gi;
|
|
254
|
+
function redactSensitive(input) {
|
|
255
|
+
if (input === null || input === void 0) return input;
|
|
256
|
+
return input.replace(COOKIE_HEADER, (_, header) => `${header}: [REDACTED]`).replace(SENSITIVE_JSON_KEY, (_, keyPrefix) => `${keyPrefix}[REDACTED]"`).replace(BEARER_TOKEN, (_, word) => `${word} [REDACTED]`);
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
// src/console-capture.ts
|
|
251
260
|
var MAX_ERRORS = 50;
|
|
261
|
+
var MAX_MSG_LEN = 500;
|
|
262
|
+
function serializeArg(arg) {
|
|
263
|
+
var _a;
|
|
264
|
+
if (typeof arg === "string") return arg;
|
|
265
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}
|
|
266
|
+
${(_a = arg.stack) != null ? _a : ""}`;
|
|
267
|
+
try {
|
|
268
|
+
return JSON.stringify(arg);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return String(arg);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
252
273
|
var capturedErrors = [];
|
|
253
|
-
var isCapturing = false;
|
|
254
274
|
var originalConsoleError = null;
|
|
275
|
+
var patchedConsoleErrorRef = null;
|
|
255
276
|
var errorListener = null;
|
|
256
277
|
var rejectionListener = null;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return String(a).slice(0, MAX_MSG_LEN);
|
|
278
|
+
var releasePatch = null;
|
|
279
|
+
var patchManager = createPatchManager({
|
|
280
|
+
markerKey: "__jarveBugReporterConsolePatch",
|
|
281
|
+
install: () => {
|
|
282
|
+
capturedErrors = [];
|
|
283
|
+
originalConsoleError = console.error;
|
|
284
|
+
const patchedConsoleError = (...args) => {
|
|
285
|
+
const message = args.map((arg) => redactSensitive(serializeArg(arg)).slice(0, MAX_MSG_LEN)).join(" ").slice(0, MAX_MSG_LEN);
|
|
286
|
+
capturedErrors.push({
|
|
287
|
+
message,
|
|
288
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
289
|
+
});
|
|
290
|
+
if (capturedErrors.length > MAX_ERRORS) {
|
|
291
|
+
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
272
292
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
capturedErrors
|
|
293
|
+
originalConsoleError.apply(console, args);
|
|
294
|
+
};
|
|
295
|
+
patchedConsoleError.__bugReporterPatched = true;
|
|
296
|
+
patchedConsoleErrorRef = patchedConsoleError;
|
|
297
|
+
console.error = patchedConsoleError;
|
|
298
|
+
errorListener = (event) => {
|
|
299
|
+
capturedErrors.push({
|
|
300
|
+
message: event.message,
|
|
301
|
+
source: event.filename,
|
|
302
|
+
lineno: event.lineno,
|
|
303
|
+
colno: event.colno,
|
|
304
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
305
|
+
});
|
|
306
|
+
if (capturedErrors.length > MAX_ERRORS) {
|
|
307
|
+
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
window.addEventListener("error", errorListener);
|
|
311
|
+
rejectionListener = (event) => {
|
|
312
|
+
capturedErrors.push({
|
|
313
|
+
message: `Unhandled Promise Rejection: ${event.reason instanceof Error ? event.reason.message : String(event.reason)}`,
|
|
314
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
315
|
+
});
|
|
316
|
+
if (capturedErrors.length > MAX_ERRORS) {
|
|
317
|
+
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
window.addEventListener("unhandledrejection", rejectionListener);
|
|
321
|
+
},
|
|
322
|
+
uninstall: () => {
|
|
323
|
+
if (console.error === patchedConsoleErrorRef && originalConsoleError) {
|
|
324
|
+
console.error = originalConsoleError;
|
|
325
|
+
} else {
|
|
326
|
+
console.warn(
|
|
327
|
+
"Bug reporter: console.error was replaced by another library after we patched it. Leaving the foreign patch in place \u2014 original console.error is no longer reachable via our handle."
|
|
328
|
+
);
|
|
280
329
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
patchedConsoleError.__bugReporterPatched = true;
|
|
284
|
-
console.error = patchedConsoleError;
|
|
285
|
-
errorListener = (event) => {
|
|
286
|
-
capturedErrors.push({
|
|
287
|
-
message: event.message,
|
|
288
|
-
source: event.filename,
|
|
289
|
-
lineno: event.lineno,
|
|
290
|
-
colno: event.colno,
|
|
291
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
292
|
-
});
|
|
293
|
-
if (capturedErrors.length > MAX_ERRORS) {
|
|
294
|
-
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
330
|
+
if (patchedConsoleErrorRef) {
|
|
331
|
+
delete patchedConsoleErrorRef.__bugReporterPatched;
|
|
295
332
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
302
|
-
});
|
|
303
|
-
if (capturedErrors.length > MAX_ERRORS) {
|
|
304
|
-
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
333
|
+
originalConsoleError = null;
|
|
334
|
+
patchedConsoleErrorRef = null;
|
|
335
|
+
if (errorListener) {
|
|
336
|
+
window.removeEventListener("error", errorListener);
|
|
337
|
+
errorListener = null;
|
|
305
338
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
function stopCapturing() {
|
|
310
|
-
if (!isCapturing) return;
|
|
311
|
-
isCapturing = false;
|
|
312
|
-
if (originalConsoleError) {
|
|
313
|
-
const restoreTarget = originalConsoleError;
|
|
314
|
-
console.error = restoreTarget;
|
|
315
|
-
if (console.error !== restoreTarget) {
|
|
316
|
-
console.warn("Bug reporter: failed to restore original console.error");
|
|
339
|
+
if (rejectionListener) {
|
|
340
|
+
window.removeEventListener("unhandledrejection", rejectionListener);
|
|
341
|
+
rejectionListener = null;
|
|
317
342
|
}
|
|
318
|
-
originalConsoleError = null;
|
|
319
|
-
}
|
|
320
|
-
if (errorListener) {
|
|
321
|
-
window.removeEventListener("error", errorListener);
|
|
322
|
-
errorListener = null;
|
|
323
|
-
}
|
|
324
|
-
if (rejectionListener) {
|
|
325
|
-
window.removeEventListener("unhandledrejection", rejectionListener);
|
|
326
|
-
rejectionListener = null;
|
|
327
343
|
}
|
|
344
|
+
});
|
|
345
|
+
function startCapturing() {
|
|
346
|
+
if (typeof window === "undefined") return;
|
|
347
|
+
if (releasePatch) return;
|
|
348
|
+
releasePatch = patchManager.acquire();
|
|
349
|
+
}
|
|
350
|
+
function stopCapturing() {
|
|
351
|
+
if (!releasePatch) return;
|
|
352
|
+
const release = releasePatch;
|
|
353
|
+
releasePatch = null;
|
|
354
|
+
release();
|
|
328
355
|
}
|
|
329
356
|
function getCapturedErrors() {
|
|
330
357
|
return [...capturedErrors];
|
|
@@ -334,17 +361,38 @@ function clearCapturedErrors() {
|
|
|
334
361
|
}
|
|
335
362
|
|
|
336
363
|
// src/network-capture.ts
|
|
364
|
+
import { createPatchManager as createPatchManager2 } from "@jarve/widget-shared";
|
|
337
365
|
var MAX_REQUESTS = 30;
|
|
338
366
|
var MAX_BODY_READ_BYTES = 64 * 1024;
|
|
339
367
|
var TRUNCATE_LEN = 500;
|
|
340
368
|
var capturedRequests = [];
|
|
341
|
-
var isCapturing2 = false;
|
|
342
369
|
var originalFetch = null;
|
|
370
|
+
var patchedFetchRef = null;
|
|
371
|
+
var releasePatch2 = null;
|
|
372
|
+
var bodyCaptureMode = "off";
|
|
343
373
|
function truncateBody(body, maxLen = TRUNCATE_LEN) {
|
|
344
374
|
if (!body) return null;
|
|
345
375
|
if (body.length <= maxLen) return body;
|
|
346
376
|
return body.slice(0, maxLen) + "...(truncated)";
|
|
347
377
|
}
|
|
378
|
+
function sanitizeRequestUrl(raw) {
|
|
379
|
+
try {
|
|
380
|
+
const parsed = new URL(raw);
|
|
381
|
+
return {
|
|
382
|
+
url: parsed.origin + parsed.pathname,
|
|
383
|
+
hasQueryString: parsed.search.length > 0 || parsed.hash.length > 0
|
|
384
|
+
};
|
|
385
|
+
} catch (e) {
|
|
386
|
+
const queryIdx = raw.indexOf("?");
|
|
387
|
+
const hashIdx = raw.indexOf("#");
|
|
388
|
+
const cutoffs = [queryIdx, hashIdx].filter((i) => i >= 0);
|
|
389
|
+
if (cutoffs.length === 0) {
|
|
390
|
+
return { url: raw, hasQueryString: false };
|
|
391
|
+
}
|
|
392
|
+
const cut = Math.min(...cutoffs);
|
|
393
|
+
return { url: raw.slice(0, cut), hasQueryString: true };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
348
396
|
async function readBoundedBody(response) {
|
|
349
397
|
try {
|
|
350
398
|
const contentLength = response.headers.get("Content-Length");
|
|
@@ -372,72 +420,98 @@ async function readBoundedBody(response) {
|
|
|
372
420
|
}
|
|
373
421
|
const decoder = new TextDecoder();
|
|
374
422
|
const text2 = chunks.map((c) => decoder.decode(c, { stream: true })).join("");
|
|
375
|
-
return truncateBody(text2);
|
|
423
|
+
return truncateBody(redactSensitive(text2));
|
|
376
424
|
}
|
|
377
425
|
const text = await cloned.text();
|
|
378
|
-
return truncateBody(text);
|
|
426
|
+
return truncateBody(redactSensitive(text));
|
|
379
427
|
} catch (e) {
|
|
380
428
|
return null;
|
|
381
429
|
}
|
|
382
430
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
431
|
+
var patchManager2 = createPatchManager2({
|
|
432
|
+
markerKey: "__jarveBugReporterFetchPatch",
|
|
433
|
+
install: () => {
|
|
434
|
+
if (typeof window === "undefined") return;
|
|
435
|
+
capturedRequests = [];
|
|
436
|
+
originalFetch = window.fetch;
|
|
437
|
+
const patchedFetch = async function patchedFetch2(input, init) {
|
|
438
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
439
|
+
const method = (init == null ? void 0 : init.method) || (typeof input !== "string" && !(input instanceof URL) ? input.method : "GET") || "GET";
|
|
440
|
+
if (rawUrl.includes("/api/bug-reporter/") || rawUrl.includes("/bug-reporter/external/") || rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) {
|
|
441
|
+
return originalFetch.call(window, input, init);
|
|
442
|
+
}
|
|
443
|
+
const { url, hasQueryString } = sanitizeRequestUrl(rawUrl);
|
|
444
|
+
try {
|
|
445
|
+
const response = await originalFetch.call(window, input, init);
|
|
446
|
+
if (response.status >= 400) {
|
|
447
|
+
const responseBody = bodyCaptureMode === "on" ? await readBoundedBody(response) : null;
|
|
448
|
+
capturedRequests.push({
|
|
449
|
+
url,
|
|
450
|
+
hasQueryString,
|
|
451
|
+
method: method.toUpperCase(),
|
|
452
|
+
status: response.status,
|
|
453
|
+
statusText: response.statusText,
|
|
454
|
+
responseBody,
|
|
455
|
+
contentType: response.headers.get("content-type"),
|
|
456
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
457
|
+
});
|
|
458
|
+
if (capturedRequests.length > MAX_REQUESTS) {
|
|
459
|
+
capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return response;
|
|
463
|
+
} catch (error) {
|
|
399
464
|
capturedRequests.push({
|
|
400
465
|
url,
|
|
466
|
+
hasQueryString,
|
|
401
467
|
method: method.toUpperCase(),
|
|
402
|
-
status:
|
|
403
|
-
statusText:
|
|
404
|
-
responseBody,
|
|
468
|
+
status: 0,
|
|
469
|
+
statusText: error instanceof Error ? error.message : "Network error",
|
|
470
|
+
responseBody: null,
|
|
471
|
+
contentType: null,
|
|
405
472
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
406
473
|
});
|
|
407
474
|
if (capturedRequests.length > MAX_REQUESTS) {
|
|
408
475
|
capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
|
|
409
476
|
}
|
|
477
|
+
throw error;
|
|
410
478
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (capturedRequests.length > MAX_REQUESTS) {
|
|
422
|
-
capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
|
|
423
|
-
}
|
|
424
|
-
throw error;
|
|
479
|
+
};
|
|
480
|
+
patchedFetch.__bugReporterPatched = true;
|
|
481
|
+
patchedFetchRef = patchedFetch;
|
|
482
|
+
window.fetch = patchedFetch;
|
|
483
|
+
},
|
|
484
|
+
uninstall: () => {
|
|
485
|
+
if (typeof window === "undefined") {
|
|
486
|
+
originalFetch = null;
|
|
487
|
+
patchedFetchRef = null;
|
|
488
|
+
return;
|
|
425
489
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
window.fetch = restoreTarget;
|
|
436
|
-
if (window.fetch !== restoreTarget) {
|
|
437
|
-
console.warn("Bug reporter: failed to restore original fetch");
|
|
490
|
+
if (window.fetch === patchedFetchRef && originalFetch) {
|
|
491
|
+
window.fetch = originalFetch;
|
|
492
|
+
} else {
|
|
493
|
+
console.warn(
|
|
494
|
+
"Bug reporter: window.fetch was replaced by another library after we patched it. Leaving the foreign patch in place \u2014 original fetch is no longer reachable via our handle."
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (patchedFetchRef) {
|
|
498
|
+
delete patchedFetchRef.__bugReporterPatched;
|
|
438
499
|
}
|
|
439
500
|
originalFetch = null;
|
|
501
|
+
patchedFetchRef = null;
|
|
440
502
|
}
|
|
503
|
+
});
|
|
504
|
+
function startNetworkCapture(options = {}) {
|
|
505
|
+
if (typeof window === "undefined") return;
|
|
506
|
+
bodyCaptureMode = options.captureResponseBodies ? "on" : "off";
|
|
507
|
+
if (releasePatch2) return;
|
|
508
|
+
releasePatch2 = patchManager2.acquire();
|
|
509
|
+
}
|
|
510
|
+
function stopNetworkCapture() {
|
|
511
|
+
if (!releasePatch2) return;
|
|
512
|
+
const release = releasePatch2;
|
|
513
|
+
releasePatch2 = null;
|
|
514
|
+
release();
|
|
441
515
|
}
|
|
442
516
|
function getCapturedNetworkErrors() {
|
|
443
517
|
return [...capturedRequests];
|
|
@@ -451,11 +525,16 @@ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
|
451
525
|
function dataUrlToBlob(dataUrl) {
|
|
452
526
|
var _a;
|
|
453
527
|
const [header, base64] = dataUrl.split(",");
|
|
528
|
+
if (!header || !base64) return new Blob();
|
|
454
529
|
const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
530
|
+
try {
|
|
531
|
+
const bytes = atob(base64);
|
|
532
|
+
const arr = new Uint8Array(bytes.length);
|
|
533
|
+
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
|
|
534
|
+
return new Blob([arr], { type: mime });
|
|
535
|
+
} catch (e) {
|
|
536
|
+
return new Blob();
|
|
537
|
+
}
|
|
459
538
|
}
|
|
460
539
|
function CaptureOverlay({
|
|
461
540
|
isActive,
|
|
@@ -463,11 +542,12 @@ function CaptureOverlay({
|
|
|
463
542
|
reporterName,
|
|
464
543
|
reporterEmail,
|
|
465
544
|
onCapture,
|
|
466
|
-
onCancel
|
|
545
|
+
onCancel,
|
|
546
|
+
zIndexBase = 1e4
|
|
467
547
|
}) {
|
|
468
548
|
const [hoveredElement, setHoveredElement] = useState2(null);
|
|
469
549
|
const [hoveredRect, setHoveredRect] = useState2(null);
|
|
470
|
-
const [
|
|
550
|
+
const [isCapturing, setIsCapturing] = useState2(false);
|
|
471
551
|
const [isTouchMode, setIsTouchMode] = useState2(false);
|
|
472
552
|
const [selectedSection, setSelectedSection] = useState2(null);
|
|
473
553
|
const [selectedRect, setSelectedRect] = useState2(null);
|
|
@@ -481,10 +561,20 @@ function CaptureOverlay({
|
|
|
481
561
|
setIsTouchMode(isTouchCapable());
|
|
482
562
|
}
|
|
483
563
|
}, [isActive]);
|
|
564
|
+
useEffect2(() => {
|
|
565
|
+
if (typeof document === "undefined") return;
|
|
566
|
+
if (!isActive || isTouchMode) return;
|
|
567
|
+
const root = document.documentElement;
|
|
568
|
+
root.classList.add("jarve-capturing");
|
|
569
|
+
return () => {
|
|
570
|
+
root.classList.remove("jarve-capturing");
|
|
571
|
+
};
|
|
572
|
+
}, [isActive, isTouchMode]);
|
|
484
573
|
const captureScreenshot = useCallback(
|
|
485
574
|
async (section, target, coords) => {
|
|
486
575
|
const elementInfo = collectElementInfo(target, section, coords);
|
|
487
576
|
setIsCapturing(true);
|
|
577
|
+
const { toPng } = await import("html-to-image");
|
|
488
578
|
try {
|
|
489
579
|
setHoveredElement(null);
|
|
490
580
|
setHoveredRect(null);
|
|
@@ -561,7 +651,7 @@ function CaptureOverlay({
|
|
|
561
651
|
);
|
|
562
652
|
const handleMouseMove = useCallback(
|
|
563
653
|
(e) => {
|
|
564
|
-
if (!isActive ||
|
|
654
|
+
if (!isActive || isCapturing || isTouchMode) return;
|
|
565
655
|
if (rafRef.current) return;
|
|
566
656
|
rafRef.current = requestAnimationFrame(() => {
|
|
567
657
|
rafRef.current = null;
|
|
@@ -579,11 +669,11 @@ function CaptureOverlay({
|
|
|
579
669
|
setHoveredRect(section ? section.getBoundingClientRect() : null);
|
|
580
670
|
});
|
|
581
671
|
},
|
|
582
|
-
[isActive,
|
|
672
|
+
[isActive, isCapturing, isTouchMode]
|
|
583
673
|
);
|
|
584
674
|
const handleClick = useCallback(
|
|
585
675
|
async (e) => {
|
|
586
|
-
if (!isActive ||
|
|
676
|
+
if (!isActive || isCapturing || isTouchMode) return;
|
|
587
677
|
const target = e.target;
|
|
588
678
|
if (!(target instanceof HTMLElement)) return;
|
|
589
679
|
if (target.closest("[data-bug-reporter]")) return;
|
|
@@ -593,16 +683,18 @@ function CaptureOverlay({
|
|
|
593
683
|
if (!section) return;
|
|
594
684
|
await captureScreenshot(section, target, extractCoordinates(e));
|
|
595
685
|
},
|
|
596
|
-
[isActive,
|
|
686
|
+
[isActive, isCapturing, isTouchMode, captureScreenshot]
|
|
597
687
|
);
|
|
598
688
|
const handleTouchEnd = useCallback(
|
|
599
689
|
(e) => {
|
|
600
|
-
if (!isActive ||
|
|
690
|
+
if (!isActive || isCapturing) return;
|
|
601
691
|
const touch = e.changedTouches[0];
|
|
602
692
|
if (!touch) return;
|
|
603
693
|
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
604
694
|
if (!(target instanceof HTMLElement)) return;
|
|
605
695
|
if (target.closest("[data-bug-reporter]")) return;
|
|
696
|
+
if (e.cancelable) e.preventDefault();
|
|
697
|
+
e.stopPropagation();
|
|
606
698
|
const section = getNearestSection(target);
|
|
607
699
|
if (!section) return;
|
|
608
700
|
setSelectedSection(section);
|
|
@@ -610,7 +702,7 @@ function CaptureOverlay({
|
|
|
610
702
|
setSelectedTarget(target);
|
|
611
703
|
touchCoordsRef.current = extractCoordinates(touch);
|
|
612
704
|
},
|
|
613
|
-
[isActive,
|
|
705
|
+
[isActive, isCapturing]
|
|
614
706
|
);
|
|
615
707
|
const handleConfirmCapture = useCallback(async () => {
|
|
616
708
|
if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
|
|
@@ -660,7 +752,7 @@ function CaptureOverlay({
|
|
|
660
752
|
document.addEventListener("keydown", handleKeyDown);
|
|
661
753
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
662
754
|
if (isTouchMode) {
|
|
663
|
-
document.addEventListener("touchend", handleTouchEnd, { passive:
|
|
755
|
+
document.addEventListener("touchend", handleTouchEnd, { passive: false });
|
|
664
756
|
} else {
|
|
665
757
|
document.addEventListener("mousemove", handleMouseMove, true);
|
|
666
758
|
document.addEventListener("click", handleClick, true);
|
|
@@ -692,9 +784,10 @@ function CaptureOverlay({
|
|
|
692
784
|
"div",
|
|
693
785
|
{
|
|
694
786
|
"data-bug-reporter": true,
|
|
695
|
-
role: "
|
|
696
|
-
"aria-live": "
|
|
697
|
-
className: "fixed top-0 right-0 left-0 z-[
|
|
787
|
+
role: "status",
|
|
788
|
+
"aria-live": "polite",
|
|
789
|
+
className: "fixed top-0 right-0 left-0 z-[calc(var(--jarve-z-base))] flex items-center justify-center gap-3 bg-indigo-600 px-4 py-2 text-center text-sm font-medium text-white",
|
|
790
|
+
style: { ["--jarve-z-base"]: String(zIndexBase) },
|
|
698
791
|
children: isTouchMode ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
699
792
|
/* @__PURE__ */ jsx2("span", { children: "Tap the section with the bug" }),
|
|
700
793
|
/* @__PURE__ */ jsx2(
|
|
@@ -718,8 +811,9 @@ function CaptureOverlay({
|
|
|
718
811
|
{
|
|
719
812
|
ref: overlayRef,
|
|
720
813
|
"data-bug-reporter": true,
|
|
721
|
-
className: "pointer-events-none fixed z-[
|
|
814
|
+
className: "pointer-events-none fixed z-[calc(var(--jarve-z-base)-2)] rounded-sm border-2 border-indigo-500 transition-all duration-150 ease-out",
|
|
722
815
|
style: {
|
|
816
|
+
["--jarve-z-base"]: String(zIndexBase),
|
|
723
817
|
top: highlightRect.top - 2,
|
|
724
818
|
left: highlightRect.left - 2,
|
|
725
819
|
width: highlightRect.width + 4,
|
|
@@ -728,12 +822,15 @@ function CaptureOverlay({
|
|
|
728
822
|
}
|
|
729
823
|
}
|
|
730
824
|
),
|
|
731
|
-
isTouchMode && selectedSection && !
|
|
825
|
+
isTouchMode && selectedSection && !isCapturing && /* @__PURE__ */ jsx2(
|
|
732
826
|
"div",
|
|
733
827
|
{
|
|
734
828
|
"data-bug-reporter": true,
|
|
735
|
-
className: "fixed right-0 bottom-0 left-0 z-[
|
|
736
|
-
style: {
|
|
829
|
+
className: "fixed right-0 bottom-0 left-0 z-[calc(var(--jarve-z-base))] border-t border-gray-200 bg-white shadow-lg",
|
|
830
|
+
style: {
|
|
831
|
+
["--jarve-z-base"]: String(zIndexBase),
|
|
832
|
+
paddingBottom: "env(safe-area-inset-bottom, 0px)"
|
|
833
|
+
},
|
|
737
834
|
children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
|
|
738
835
|
/* @__PURE__ */ jsx2("span", { className: "truncate text-sm font-medium text-gray-900", children: "Capture this section?" }),
|
|
739
836
|
/* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 gap-2", children: [
|
|
@@ -761,132 +858,186 @@ function CaptureOverlay({
|
|
|
761
858
|
] })
|
|
762
859
|
] })
|
|
763
860
|
}
|
|
764
|
-
)
|
|
765
|
-
!isTouchMode && /* @__PURE__ */ jsx2("style", { children: `* { cursor: crosshair !important; }` })
|
|
861
|
+
)
|
|
766
862
|
] });
|
|
767
863
|
}
|
|
768
864
|
|
|
769
865
|
// src/report-modal.tsx
|
|
770
866
|
import { useState as useState3, useRef as useRef3, useEffect as useEffect3, useCallback as useCallback2, useMemo } from "react";
|
|
771
867
|
import { X, Send, Loader2, CheckCircle2 } from "lucide-react";
|
|
868
|
+
import {
|
|
869
|
+
cn as cn2,
|
|
870
|
+
createTimeoutAbortSignal,
|
|
871
|
+
useEscapeKey,
|
|
872
|
+
useFocusTrap,
|
|
873
|
+
useResolvedTheme,
|
|
874
|
+
useReturnFocus
|
|
875
|
+
} from "@jarve/widget-shared";
|
|
772
876
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
877
|
+
var STARTER_ASSISTANT_MESSAGE = {
|
|
878
|
+
role: "assistant",
|
|
879
|
+
content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
|
|
880
|
+
};
|
|
881
|
+
var DRAFT_STORAGE_PREFIX = "jarve:draft:bug-reporter:";
|
|
882
|
+
function draftStorageKey(siteId) {
|
|
883
|
+
return `${DRAFT_STORAGE_PREFIX}${siteId}`;
|
|
884
|
+
}
|
|
885
|
+
function readDraft(siteId) {
|
|
886
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
887
|
+
try {
|
|
888
|
+
const raw = sessionStorage.getItem(draftStorageKey(siteId));
|
|
889
|
+
if (!raw) return null;
|
|
890
|
+
const parsed = JSON.parse(raw);
|
|
891
|
+
if (typeof (parsed == null ? void 0 : parsed.input) !== "string" || !Array.isArray(parsed == null ? void 0 : parsed.messages)) return null;
|
|
892
|
+
return parsed;
|
|
893
|
+
} catch (e) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function writeDraft(siteId, draft) {
|
|
898
|
+
if (typeof sessionStorage === "undefined") return;
|
|
899
|
+
try {
|
|
900
|
+
sessionStorage.setItem(draftStorageKey(siteId), JSON.stringify(draft));
|
|
901
|
+
} catch (e) {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function clearDraft(siteId) {
|
|
905
|
+
if (typeof sessionStorage === "undefined") return;
|
|
906
|
+
try {
|
|
907
|
+
sessionStorage.removeItem(draftStorageKey(siteId));
|
|
908
|
+
} catch (e) {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
773
911
|
function ReportModal({
|
|
774
912
|
isOpen,
|
|
775
913
|
captureResult,
|
|
776
914
|
apiConfig,
|
|
777
915
|
siteId,
|
|
778
|
-
|
|
779
|
-
|
|
916
|
+
onClose,
|
|
917
|
+
zIndexBase = 1e4,
|
|
918
|
+
theme = "auto",
|
|
919
|
+
onBeforeSubmit
|
|
780
920
|
}) {
|
|
781
|
-
const
|
|
921
|
+
const resolvedTheme = useResolvedTheme(theme);
|
|
922
|
+
const [messages, setMessages] = useState3([STARTER_ASSISTANT_MESSAGE]);
|
|
782
923
|
const [input, setInput] = useState3("");
|
|
783
924
|
const [isLoading, setIsLoading] = useState3(false);
|
|
784
925
|
const [modalState, setModalState] = useState3("chatting");
|
|
785
926
|
const [reportId, setReportId] = useState3(null);
|
|
786
927
|
const [screenshotUrl, setScreenshotUrl] = useState3(null);
|
|
787
928
|
const [errorMessage, setErrorMessage] = useState3(null);
|
|
788
|
-
const
|
|
929
|
+
const [submitAlert, setSubmitAlert] = useState3(null);
|
|
930
|
+
const chatScrollRef = useRef3(null);
|
|
789
931
|
const inputRef = useRef3(null);
|
|
790
|
-
const
|
|
932
|
+
const dialogRef = useRef3(null);
|
|
933
|
+
const [sessionToken, setSessionToken] = useState3(() => "");
|
|
934
|
+
const activeSessionRef = useRef3("");
|
|
935
|
+
const abortControllerRef = useRef3(null);
|
|
936
|
+
if (abortControllerRef.current === null) {
|
|
937
|
+
abortControllerRef.current = new AbortController();
|
|
938
|
+
}
|
|
939
|
+
useEffect3(() => {
|
|
940
|
+
var _a, _b, _c;
|
|
941
|
+
if (isOpen) {
|
|
942
|
+
const next = crypto.randomUUID();
|
|
943
|
+
activeSessionRef.current = next;
|
|
944
|
+
setSessionToken(next);
|
|
945
|
+
const draft = readDraft(siteId);
|
|
946
|
+
setMessages(draft && draft.messages.length > 0 ? draft.messages : [STARTER_ASSISTANT_MESSAGE]);
|
|
947
|
+
setInput((_a = draft == null ? void 0 : draft.input) != null ? _a : "");
|
|
948
|
+
setIsLoading(false);
|
|
949
|
+
setModalState("chatting");
|
|
950
|
+
setReportId(null);
|
|
951
|
+
setErrorMessage(null);
|
|
952
|
+
setSubmitAlert(null);
|
|
953
|
+
if ((_b = abortControllerRef.current) == null ? void 0 : _b.signal.aborted) {
|
|
954
|
+
abortControllerRef.current = new AbortController();
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
(_c = abortControllerRef.current) == null ? void 0 : _c.abort();
|
|
958
|
+
}
|
|
959
|
+
}, [isOpen, siteId]);
|
|
960
|
+
useEffect3(() => {
|
|
961
|
+
return () => {
|
|
962
|
+
var _a;
|
|
963
|
+
(_a = abortControllerRef.current) == null ? void 0 : _a.abort();
|
|
964
|
+
};
|
|
965
|
+
}, []);
|
|
966
|
+
useEffect3(() => {
|
|
967
|
+
if (!isOpen) return;
|
|
968
|
+
if (modalState === "submitted" || modalState === "submitting") return;
|
|
969
|
+
const hasNonStarterMessages = messages.length > 1 || messages.length === 1 && messages[0] !== STARTER_ASSISTANT_MESSAGE;
|
|
970
|
+
if (!input && !hasNonStarterMessages) return;
|
|
971
|
+
writeDraft(siteId, { input, messages });
|
|
972
|
+
}, [isOpen, modalState, input, messages, siteId]);
|
|
791
973
|
const apiHeaders = useMemo(
|
|
792
974
|
() => ({
|
|
793
975
|
"Content-Type": "application/json",
|
|
794
|
-
"X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim()
|
|
976
|
+
"X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim(),
|
|
977
|
+
"X-Jarve-Session": sessionToken
|
|
978
|
+
}),
|
|
979
|
+
[apiConfig.apiKey, sessionToken]
|
|
980
|
+
);
|
|
981
|
+
const submitHeaders = useMemo(
|
|
982
|
+
() => ({
|
|
983
|
+
"X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim(),
|
|
984
|
+
"X-Jarve-Session": sessionToken
|
|
795
985
|
}),
|
|
796
|
-
[apiConfig.apiKey]
|
|
986
|
+
[apiConfig.apiKey, sessionToken]
|
|
797
987
|
);
|
|
798
988
|
useEffect3(() => {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
setScreenshotUrl(
|
|
802
|
-
|
|
803
|
-
return url;
|
|
804
|
-
});
|
|
805
|
-
return () => {
|
|
806
|
-
URL.revokeObjectURL(url);
|
|
807
|
-
setScreenshotUrl(null);
|
|
808
|
-
};
|
|
989
|
+
const blob = captureResult == null ? void 0 : captureResult.screenshot;
|
|
990
|
+
if (!blob || blob.size === 0) {
|
|
991
|
+
setScreenshotUrl(null);
|
|
992
|
+
return;
|
|
809
993
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
994
|
+
const url = URL.createObjectURL(blob);
|
|
995
|
+
setScreenshotUrl(url);
|
|
996
|
+
return () => {
|
|
997
|
+
URL.revokeObjectURL(url);
|
|
998
|
+
};
|
|
814
999
|
}, [captureResult]);
|
|
815
|
-
const sendInitialMessage = useCallback2(async () => {
|
|
816
|
-
if (!captureResult) return;
|
|
817
|
-
setIsLoading(true);
|
|
818
|
-
try {
|
|
819
|
-
const response = await fetch(`${apiConfig.apiUrl}/chat`, {
|
|
820
|
-
method: "POST",
|
|
821
|
-
headers: apiHeaders,
|
|
822
|
-
body: JSON.stringify({
|
|
823
|
-
messages: [],
|
|
824
|
-
metadata: captureResult.metadata,
|
|
825
|
-
consoleErrors: captureResult.consoleErrors,
|
|
826
|
-
networkErrors: captureResult.networkErrors,
|
|
827
|
-
clickedElement: captureResult.metadata.clickedElement || null
|
|
828
|
-
})
|
|
829
|
-
});
|
|
830
|
-
if (response.status === 401) {
|
|
831
|
-
console.error(
|
|
832
|
-
"Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
|
|
833
|
-
);
|
|
834
|
-
setMessages([
|
|
835
|
-
{
|
|
836
|
-
role: "assistant",
|
|
837
|
-
content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
|
|
838
|
-
}
|
|
839
|
-
]);
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
if (!response.ok) throw new Error("Failed to get AI response");
|
|
843
|
-
const data = await response.json();
|
|
844
|
-
setMessages([{ role: "assistant", content: data.message }]);
|
|
845
|
-
} catch (e) {
|
|
846
|
-
setMessages([
|
|
847
|
-
{
|
|
848
|
-
role: "assistant",
|
|
849
|
-
content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
|
|
850
|
-
}
|
|
851
|
-
]);
|
|
852
|
-
} finally {
|
|
853
|
-
setIsLoading(false);
|
|
854
|
-
}
|
|
855
|
-
}, [captureResult, apiConfig.apiUrl, apiHeaders]);
|
|
856
1000
|
useEffect3(() => {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1001
|
+
const container = chatScrollRef.current;
|
|
1002
|
+
if (!container || typeof container.scrollTo !== "function") return;
|
|
1003
|
+
const gap = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
1004
|
+
if (gap < 100) {
|
|
1005
|
+
container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
|
|
860
1006
|
}
|
|
861
|
-
}, [isOpen, captureResult, sendInitialMessage]);
|
|
862
|
-
useEffect3(() => {
|
|
863
|
-
var _a;
|
|
864
|
-
(_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
865
1007
|
}, [messages]);
|
|
1008
|
+
useReturnFocus(isOpen);
|
|
1009
|
+
useFocusTrap(isOpen, dialogRef);
|
|
866
1010
|
useEffect3(() => {
|
|
867
1011
|
var _a;
|
|
868
1012
|
if (isOpen && !isLoading) {
|
|
869
1013
|
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
870
1014
|
}
|
|
871
1015
|
}, [isOpen, isLoading, messages]);
|
|
1016
|
+
const buildFetchSignal = useCallback2(() => {
|
|
1017
|
+
const controller = abortControllerRef.current;
|
|
1018
|
+
return createTimeoutAbortSignal(controller.signal, 3e4);
|
|
1019
|
+
}, []);
|
|
1020
|
+
const runWithFetchSignal = useCallback2(
|
|
1021
|
+
async (input2, init) => {
|
|
1022
|
+
const { signal, cleanup } = buildFetchSignal();
|
|
1023
|
+
try {
|
|
1024
|
+
return await fetch(input2, __spreadProps(__spreadValues({}, init), { signal }));
|
|
1025
|
+
} finally {
|
|
1026
|
+
cleanup();
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
[buildFetchSignal]
|
|
1030
|
+
);
|
|
872
1031
|
const submitReport = useCallback2(
|
|
873
1032
|
async (conversation, structuredReport) => {
|
|
874
1033
|
if (!captureResult || modalState !== "chatting") return;
|
|
875
1034
|
setModalState("submitting");
|
|
1035
|
+
const sessionAtDispatch = activeSessionRef.current;
|
|
876
1036
|
try {
|
|
877
|
-
let screenshotBase64;
|
|
878
|
-
if (captureResult.screenshot.size > 0) {
|
|
879
|
-
const reader = new FileReader();
|
|
880
|
-
screenshotBase64 = await new Promise((resolve, reject) => {
|
|
881
|
-
reader.onload = () => resolve(reader.result);
|
|
882
|
-
reader.onerror = reject;
|
|
883
|
-
reader.readAsDataURL(captureResult.screenshot);
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
1037
|
let report = structuredReport;
|
|
887
1038
|
if (!report) {
|
|
888
1039
|
try {
|
|
889
|
-
const summaryResponse = await
|
|
1040
|
+
const summaryResponse = await runWithFetchSignal(`${apiConfig.apiUrl}/chat`, {
|
|
890
1041
|
method: "POST",
|
|
891
1042
|
headers: apiHeaders,
|
|
892
1043
|
body: JSON.stringify({
|
|
@@ -898,6 +1049,7 @@ function ReportModal({
|
|
|
898
1049
|
requestSummary: true
|
|
899
1050
|
})
|
|
900
1051
|
});
|
|
1052
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
901
1053
|
if (summaryResponse.ok) {
|
|
902
1054
|
const data2 = await summaryResponse.json();
|
|
903
1055
|
report = data2.structuredReport;
|
|
@@ -905,46 +1057,87 @@ function ReportModal({
|
|
|
905
1057
|
} catch (e) {
|
|
906
1058
|
}
|
|
907
1059
|
}
|
|
908
|
-
|
|
1060
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1061
|
+
const basePayload = {
|
|
1062
|
+
metadata: captureResult.metadata,
|
|
1063
|
+
conversation,
|
|
1064
|
+
structuredReport: report,
|
|
1065
|
+
consoleErrors: captureResult.consoleErrors,
|
|
1066
|
+
networkErrors: captureResult.networkErrors,
|
|
1067
|
+
clickedElement: captureResult.metadata.clickedElement || null
|
|
1068
|
+
};
|
|
1069
|
+
let finalPayload = basePayload;
|
|
1070
|
+
if (onBeforeSubmit) {
|
|
1071
|
+
try {
|
|
1072
|
+
const maybe = await onBeforeSubmit(basePayload);
|
|
1073
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1074
|
+
if (maybe === null || maybe === false) {
|
|
1075
|
+
setSubmitAlert("Submission cancelled. Update your report and try again.");
|
|
1076
|
+
setModalState("chatting");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
finalPayload = maybe;
|
|
1080
|
+
} catch (hookErr) {
|
|
1081
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1082
|
+
console.error("Bug reporter: onBeforeSubmit hook rejected the payload", hookErr);
|
|
1083
|
+
setSubmitAlert("Submission failed \u2014 please try again.");
|
|
1084
|
+
setModalState("chatting");
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const formData = new FormData();
|
|
1089
|
+
if (captureResult.screenshot.size > 0) {
|
|
1090
|
+
formData.append("screenshot", captureResult.screenshot);
|
|
1091
|
+
}
|
|
1092
|
+
formData.append("payload", JSON.stringify(finalPayload));
|
|
1093
|
+
const response = await runWithFetchSignal(`${apiConfig.apiUrl}/submit`, {
|
|
909
1094
|
method: "POST",
|
|
910
|
-
headers:
|
|
911
|
-
body:
|
|
912
|
-
screenshot: screenshotBase64,
|
|
913
|
-
metadata: captureResult.metadata,
|
|
914
|
-
conversation,
|
|
915
|
-
structuredReport: report,
|
|
916
|
-
consoleErrors: captureResult.consoleErrors,
|
|
917
|
-
networkErrors: captureResult.networkErrors,
|
|
918
|
-
clickedElement: captureResult.metadata.clickedElement || null
|
|
919
|
-
})
|
|
1095
|
+
headers: submitHeaders,
|
|
1096
|
+
body: formData
|
|
920
1097
|
});
|
|
1098
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
921
1099
|
if (!response.ok) {
|
|
922
1100
|
const errorData = await response.json().catch(() => ({}));
|
|
923
1101
|
throw new Error(errorData.error || "Failed to submit report");
|
|
924
1102
|
}
|
|
925
1103
|
const data = await response.json();
|
|
1104
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
926
1105
|
setReportId(data.id);
|
|
927
1106
|
setModalState("submitted");
|
|
1107
|
+
clearDraft(siteId);
|
|
928
1108
|
} catch (err) {
|
|
1109
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
929
1110
|
console.error("Bug reporter: failed to submit report", err);
|
|
930
1111
|
setErrorMessage(err instanceof Error ? err.message : "Failed to submit report");
|
|
931
1112
|
setModalState("error");
|
|
932
1113
|
}
|
|
933
1114
|
},
|
|
934
|
-
[
|
|
1115
|
+
[
|
|
1116
|
+
captureResult,
|
|
1117
|
+
apiConfig.apiUrl,
|
|
1118
|
+
apiHeaders,
|
|
1119
|
+
submitHeaders,
|
|
1120
|
+
modalState,
|
|
1121
|
+
siteId,
|
|
1122
|
+
onBeforeSubmit,
|
|
1123
|
+
runWithFetchSignal
|
|
1124
|
+
]
|
|
935
1125
|
);
|
|
936
1126
|
const handleManualSubmit = useCallback2(() => {
|
|
1127
|
+
setSubmitAlert(null);
|
|
937
1128
|
submitReport(messages);
|
|
938
1129
|
}, [submitReport, messages]);
|
|
939
1130
|
async function sendMessage() {
|
|
940
1131
|
if (!input.trim() || isLoading || !captureResult) return;
|
|
941
1132
|
const userMessage = input.trim();
|
|
942
1133
|
setInput("");
|
|
1134
|
+
setSubmitAlert(null);
|
|
943
1135
|
const newMessages = [...messages, { role: "user", content: userMessage }];
|
|
944
1136
|
setMessages(newMessages);
|
|
945
1137
|
setIsLoading(true);
|
|
1138
|
+
const sessionAtDispatch = activeSessionRef.current;
|
|
946
1139
|
try {
|
|
947
|
-
const response = await
|
|
1140
|
+
const response = await runWithFetchSignal(`${apiConfig.apiUrl}/chat`, {
|
|
948
1141
|
method: "POST",
|
|
949
1142
|
headers: apiHeaders,
|
|
950
1143
|
body: JSON.stringify({
|
|
@@ -955,6 +1148,7 @@ function ReportModal({
|
|
|
955
1148
|
clickedElement: captureResult.metadata.clickedElement || null
|
|
956
1149
|
})
|
|
957
1150
|
});
|
|
1151
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
958
1152
|
if (response.status === 401) {
|
|
959
1153
|
console.error(
|
|
960
1154
|
"Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
|
|
@@ -970,6 +1164,7 @@ function ReportModal({
|
|
|
970
1164
|
}
|
|
971
1165
|
if (!response.ok) throw new Error("Failed to get AI response");
|
|
972
1166
|
const data = await response.json();
|
|
1167
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
973
1168
|
setMessages([...newMessages, { role: "assistant", content: data.message }]);
|
|
974
1169
|
if (data.readyToSubmit && data.structuredReport) {
|
|
975
1170
|
await submitReport(
|
|
@@ -978,6 +1173,7 @@ function ReportModal({
|
|
|
978
1173
|
);
|
|
979
1174
|
}
|
|
980
1175
|
} catch (e) {
|
|
1176
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
981
1177
|
setMessages([
|
|
982
1178
|
...newMessages,
|
|
983
1179
|
{
|
|
@@ -986,22 +1182,23 @@ function ReportModal({
|
|
|
986
1182
|
}
|
|
987
1183
|
]);
|
|
988
1184
|
} finally {
|
|
989
|
-
|
|
1185
|
+
if (sessionAtDispatch === activeSessionRef.current) {
|
|
1186
|
+
setIsLoading(false);
|
|
1187
|
+
}
|
|
990
1188
|
}
|
|
991
1189
|
}
|
|
992
1190
|
function handleClose() {
|
|
993
|
-
|
|
1191
|
+
var _a;
|
|
1192
|
+
(_a = abortControllerRef.current) == null ? void 0 : _a.abort();
|
|
1193
|
+
setMessages([STARTER_ASSISTANT_MESSAGE]);
|
|
994
1194
|
setInput("");
|
|
995
1195
|
setModalState("chatting");
|
|
996
1196
|
setReportId(null);
|
|
997
1197
|
setErrorMessage(null);
|
|
998
|
-
|
|
999
|
-
URL.revokeObjectURL(screenshotUrl);
|
|
1000
|
-
}
|
|
1001
|
-
setScreenshotUrl(null);
|
|
1002
|
-
hasInitRef.current = false;
|
|
1198
|
+
setSubmitAlert(null);
|
|
1003
1199
|
onClose();
|
|
1004
1200
|
}
|
|
1201
|
+
useEscapeKey(handleClose, isOpen);
|
|
1005
1202
|
function handleKeyDown(e) {
|
|
1006
1203
|
if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
|
|
1007
1204
|
e.preventDefault();
|
|
@@ -1013,14 +1210,24 @@ function ReportModal({
|
|
|
1013
1210
|
"div",
|
|
1014
1211
|
{
|
|
1015
1212
|
"data-bug-reporter": true,
|
|
1016
|
-
className: "fixed inset-0 z-[
|
|
1213
|
+
className: "fixed inset-0 z-[calc(var(--jarve-z-base)+1)] flex items-center justify-center bg-black/50 backdrop-blur-sm",
|
|
1214
|
+
style: { ["--jarve-z-base"]: String(zIndexBase) },
|
|
1017
1215
|
onClick: (e) => {
|
|
1018
1216
|
if (e.target === e.currentTarget) handleClose();
|
|
1019
1217
|
},
|
|
1020
1218
|
children: /* @__PURE__ */ jsxs3(
|
|
1021
1219
|
"div",
|
|
1022
1220
|
{
|
|
1023
|
-
|
|
1221
|
+
ref: dialogRef,
|
|
1222
|
+
role: "dialog",
|
|
1223
|
+
"aria-modal": "true",
|
|
1224
|
+
"aria-labelledby": "jarve-bug-reporter-title",
|
|
1225
|
+
className: cn2(
|
|
1226
|
+
// Self-owned theme wrapper — `dark` toggles the existing Tailwind
|
|
1227
|
+
// `dark:` utilities via the shared `@custom-variant dark (&:is(.dark *))`
|
|
1228
|
+
// rule in styles.css. This makes the widget independent of the
|
|
1229
|
+
// host app's Tailwind darkMode config. See Issue E11.
|
|
1230
|
+
resolvedTheme === "dark" ? "jarve-theme-dark dark" : "jarve-theme-light",
|
|
1024
1231
|
"flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-950",
|
|
1025
1232
|
"mx-4 w-full max-w-lg",
|
|
1026
1233
|
"max-[768px]:mx-0 max-[768px]:h-full max-[768px]:max-w-none max-[768px]:rounded-none",
|
|
@@ -1029,7 +1236,14 @@ function ReportModal({
|
|
|
1029
1236
|
children: [
|
|
1030
1237
|
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between border-b border-gray-200 bg-gray-50/30 px-4 py-3 dark:border-gray-800 dark:bg-gray-900/30", children: [
|
|
1031
1238
|
/* @__PURE__ */ jsxs3("div", { children: [
|
|
1032
|
-
/* @__PURE__ */ jsx3(
|
|
1239
|
+
/* @__PURE__ */ jsx3(
|
|
1240
|
+
"h2",
|
|
1241
|
+
{
|
|
1242
|
+
id: "jarve-bug-reporter-title",
|
|
1243
|
+
className: "text-sm font-semibold text-gray-900 dark:text-gray-100",
|
|
1244
|
+
children: "Bug Report"
|
|
1245
|
+
}
|
|
1246
|
+
),
|
|
1033
1247
|
/* @__PURE__ */ jsx3("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: siteId })
|
|
1034
1248
|
] }),
|
|
1035
1249
|
/* @__PURE__ */ jsx3(
|
|
@@ -1050,58 +1264,76 @@ function ReportModal({
|
|
|
1050
1264
|
className: "max-h-40 w-full rounded-md border border-gray-200 object-contain dark:border-gray-700"
|
|
1051
1265
|
}
|
|
1052
1266
|
) }),
|
|
1053
|
-
/* @__PURE__ */ jsx3(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
"
|
|
1058
|
-
" ",
|
|
1059
|
-
/* @__PURE__ */
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
children: "
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1267
|
+
/* @__PURE__ */ jsx3(
|
|
1268
|
+
"div",
|
|
1269
|
+
{
|
|
1270
|
+
ref: chatScrollRef,
|
|
1271
|
+
"data-testid": "chat-scroll",
|
|
1272
|
+
className: "min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-3",
|
|
1273
|
+
children: modalState === "submitted" ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
|
|
1274
|
+
/* @__PURE__ */ jsx3(CheckCircle2, { className: "mb-3 h-12 w-12 text-green-500" }),
|
|
1275
|
+
/* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
|
|
1276
|
+
/* @__PURE__ */ jsxs3("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [
|
|
1277
|
+
"Reference:",
|
|
1278
|
+
" ",
|
|
1279
|
+
/* @__PURE__ */ jsx3("code", { className: "rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800", children: reportId == null ? void 0 : reportId.slice(0, 8) })
|
|
1280
|
+
] }),
|
|
1281
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: "Thanks for the report \u2014 we'll look into it." }),
|
|
1282
|
+
/* @__PURE__ */ jsx3(
|
|
1283
|
+
"button",
|
|
1284
|
+
{
|
|
1285
|
+
onClick: handleClose,
|
|
1286
|
+
className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
|
|
1287
|
+
children: "Done"
|
|
1288
|
+
}
|
|
1289
|
+
)
|
|
1290
|
+
] }) : modalState === "error" ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
|
|
1291
|
+
/* @__PURE__ */ jsx3(X, { className: "mb-3 h-12 w-12 text-red-500" }),
|
|
1292
|
+
/* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
|
|
1293
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: errorMessage || "Something went wrong. Please try again." }),
|
|
1294
|
+
/* @__PURE__ */ jsx3(
|
|
1295
|
+
"button",
|
|
1296
|
+
{
|
|
1297
|
+
onClick: () => setModalState("chatting"),
|
|
1298
|
+
className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
|
|
1299
|
+
children: "Try Again"
|
|
1300
|
+
}
|
|
1301
|
+
)
|
|
1302
|
+
] }) : modalState === "submitting" ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center py-8", children: [
|
|
1303
|
+
/* @__PURE__ */ jsx3(Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
|
|
1304
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
|
|
1305
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1306
|
+
(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." }),
|
|
1307
|
+
messages.map((msg, i) => /* @__PURE__ */ jsx3(
|
|
1308
|
+
"div",
|
|
1309
|
+
{
|
|
1310
|
+
className: cn2(
|
|
1311
|
+
"text-sm leading-relaxed",
|
|
1312
|
+
msg.role === "assistant" ? "rounded-lg bg-gray-100/50 p-3 text-gray-900 dark:bg-gray-800/50 dark:text-gray-100" : "ml-8 rounded-lg bg-indigo-600 p-3 text-white"
|
|
1313
|
+
),
|
|
1314
|
+
children: msg.content
|
|
1315
|
+
},
|
|
1316
|
+
i
|
|
1317
|
+
)),
|
|
1318
|
+
isLoading && /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
|
|
1319
|
+
/* @__PURE__ */ jsx3(Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
|
|
1320
|
+
/* @__PURE__ */ jsx3("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
|
|
1321
|
+
] })
|
|
1322
|
+
] })
|
|
1323
|
+
}
|
|
1324
|
+
),
|
|
1325
|
+
modalState === "chatting" && /* @__PURE__ */ jsx3("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", children: [
|
|
1326
|
+
submitAlert && // onBeforeSubmit rejected the payload — keep the draft and
|
|
1327
|
+
// let the user retry. role="alert" announces to AT users.
|
|
1328
|
+
// See Issue B8.
|
|
1074
1329
|
/* @__PURE__ */ jsx3(
|
|
1075
|
-
"
|
|
1330
|
+
"p",
|
|
1076
1331
|
{
|
|
1077
|
-
|
|
1078
|
-
className: "
|
|
1079
|
-
children:
|
|
1332
|
+
role: "alert",
|
|
1333
|
+
className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300",
|
|
1334
|
+
children: submitAlert
|
|
1080
1335
|
}
|
|
1081
|
-
)
|
|
1082
|
-
] }) : modalState === "submitting" ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center justify-center py-8", children: [
|
|
1083
|
-
/* @__PURE__ */ jsx3(Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
|
|
1084
|
-
/* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
|
|
1085
|
-
] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1086
|
-
(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." }),
|
|
1087
|
-
messages.map((msg, i) => /* @__PURE__ */ jsx3(
|
|
1088
|
-
"div",
|
|
1089
|
-
{
|
|
1090
|
-
className: cn(
|
|
1091
|
-
"text-sm leading-relaxed",
|
|
1092
|
-
msg.role === "assistant" ? "rounded-lg bg-gray-100/50 p-3 text-gray-900 dark:bg-gray-800/50 dark:text-gray-100" : "ml-8 rounded-lg bg-indigo-600 p-3 text-white"
|
|
1093
|
-
),
|
|
1094
|
-
children: msg.content
|
|
1095
|
-
},
|
|
1096
|
-
i
|
|
1097
|
-
)),
|
|
1098
|
-
isLoading && /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
|
|
1099
|
-
/* @__PURE__ */ jsx3(Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
|
|
1100
|
-
/* @__PURE__ */ jsx3("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
|
|
1101
|
-
] }),
|
|
1102
|
-
/* @__PURE__ */ jsx3("div", { ref: chatEndRef })
|
|
1103
|
-
] }) }),
|
|
1104
|
-
modalState === "chatting" && /* @__PURE__ */ jsx3("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-2", children: [
|
|
1336
|
+
),
|
|
1105
1337
|
/* @__PURE__ */ jsx3(
|
|
1106
1338
|
"textarea",
|
|
1107
1339
|
{
|
|
@@ -1159,34 +1391,66 @@ function ReportModal({
|
|
|
1159
1391
|
}
|
|
1160
1392
|
|
|
1161
1393
|
// src/bug-reporter.tsx
|
|
1394
|
+
import { emit, useHasMounted } from "@jarve/widget-shared";
|
|
1162
1395
|
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1396
|
+
function isValidApiUrl(raw) {
|
|
1397
|
+
if (typeof raw !== "string" || raw.length === 0) return false;
|
|
1398
|
+
try {
|
|
1399
|
+
const parsed = new URL(raw);
|
|
1400
|
+
if (parsed.protocol === "https:") return true;
|
|
1401
|
+
if (parsed.protocol === "http:" && parsed.hostname === "localhost") return true;
|
|
1402
|
+
return false;
|
|
1403
|
+
} catch (e) {
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1163
1407
|
function JarveBugReporter({
|
|
1164
1408
|
apiUrl,
|
|
1165
1409
|
apiKey,
|
|
1410
|
+
siteId,
|
|
1166
1411
|
user,
|
|
1167
1412
|
buttonPosition,
|
|
1413
|
+
zIndexBase,
|
|
1414
|
+
captureResponseBodies = false,
|
|
1415
|
+
theme,
|
|
1416
|
+
onBeforeSubmit,
|
|
1168
1417
|
children
|
|
1169
1418
|
}) {
|
|
1170
|
-
const
|
|
1419
|
+
const mounted = useHasMounted();
|
|
1420
|
+
const apiUrlValid = useMemo2(() => isValidApiUrl(apiUrl), [apiUrl]);
|
|
1421
|
+
useEffect4(() => {
|
|
1422
|
+
if (!apiUrlValid) {
|
|
1423
|
+
console.error(
|
|
1424
|
+
`Jarve bug reporter: invalid apiUrl \u2014 widget disabled (received: ${JSON.stringify(apiUrl)})`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
}, [apiUrl, apiUrlValid]);
|
|
1171
1428
|
const [captureMode, setCaptureMode] = useState4(false);
|
|
1172
1429
|
const [captureResult, setCaptureResult] = useState4(null);
|
|
1173
1430
|
const [showModal, setShowModal] = useState4(false);
|
|
1431
|
+
const [isPrimary, setIsPrimary] = useState4(false);
|
|
1174
1432
|
useEffect4(() => {
|
|
1433
|
+
if (!apiUrlValid || !isPrimary) return;
|
|
1175
1434
|
startCapturing();
|
|
1176
|
-
startNetworkCapture();
|
|
1435
|
+
startNetworkCapture({ captureResponseBodies });
|
|
1177
1436
|
return () => {
|
|
1178
1437
|
stopCapturing();
|
|
1179
1438
|
stopNetworkCapture();
|
|
1180
1439
|
};
|
|
1181
|
-
}, []);
|
|
1440
|
+
}, [apiUrlValid, isPrimary, captureResponseBodies]);
|
|
1182
1441
|
const toggleCaptureMode = useCallback3(() => {
|
|
1183
1442
|
setCaptureMode((prev) => !prev);
|
|
1184
1443
|
}, []);
|
|
1185
1444
|
useEffect4(() => {
|
|
1186
1445
|
if (typeof window === "undefined") return;
|
|
1446
|
+
if (!apiUrlValid) return;
|
|
1187
1447
|
if (!window.__jarve_widgets) {
|
|
1188
1448
|
window.__jarve_widgets = /* @__PURE__ */ new Map();
|
|
1189
1449
|
}
|
|
1450
|
+
if (window.__jarve_widgets.has("bug-reporter")) {
|
|
1451
|
+
console.warn("JarveBugReporter: instance already mounted; additional instance ignored");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1190
1454
|
window.__jarve_widgets.set("bug-reporter", {
|
|
1191
1455
|
type: "bug-reporter",
|
|
1192
1456
|
label: "Report a Bug",
|
|
@@ -1196,28 +1460,25 @@ function JarveBugReporter({
|
|
|
1196
1460
|
isActive: false,
|
|
1197
1461
|
trigger: toggleCaptureMode
|
|
1198
1462
|
});
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
);
|
|
1463
|
+
setIsPrimary(true);
|
|
1464
|
+
emit("jarve:widget-registered", { type: "bug-reporter" });
|
|
1202
1465
|
return () => {
|
|
1203
1466
|
var _a;
|
|
1204
1467
|
(_a = window.__jarve_widgets) == null ? void 0 : _a.delete("bug-reporter");
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
);
|
|
1468
|
+
setIsPrimary(false);
|
|
1469
|
+
emit("jarve:widget-deregistered", { type: "bug-reporter" });
|
|
1208
1470
|
};
|
|
1209
|
-
}, [toggleCaptureMode]);
|
|
1471
|
+
}, [toggleCaptureMode, apiUrlValid]);
|
|
1210
1472
|
useEffect4(() => {
|
|
1211
1473
|
var _a;
|
|
1212
1474
|
if (typeof window === "undefined") return;
|
|
1475
|
+
if (!isPrimary) return;
|
|
1213
1476
|
const entry = (_a = window.__jarve_widgets) == null ? void 0 : _a.get("bug-reporter");
|
|
1214
1477
|
if (entry) {
|
|
1215
1478
|
entry.isActive = captureMode || showModal;
|
|
1216
|
-
|
|
1217
|
-
new CustomEvent("jarve:state-change", { detail: { type: "bug-reporter" } })
|
|
1218
|
-
);
|
|
1479
|
+
emit("jarve:state-change", { type: "bug-reporter", isActive: entry.isActive });
|
|
1219
1480
|
}
|
|
1220
|
-
}, [captureMode, showModal]);
|
|
1481
|
+
}, [captureMode, showModal, isPrimary]);
|
|
1221
1482
|
const handleCapture = useCallback3((result) => {
|
|
1222
1483
|
setCaptureResult(result);
|
|
1223
1484
|
setCaptureMode(false);
|
|
@@ -1227,49 +1488,66 @@ function JarveBugReporter({
|
|
|
1227
1488
|
setCaptureMode(false);
|
|
1228
1489
|
}, []);
|
|
1229
1490
|
const handleCloseModal = useCallback3(() => {
|
|
1491
|
+
setCaptureMode(false);
|
|
1230
1492
|
setShowModal(false);
|
|
1231
1493
|
setCaptureResult(null);
|
|
1232
1494
|
clearCapturedErrors();
|
|
1233
1495
|
clearCapturedNetworkErrors();
|
|
1234
1496
|
}, []);
|
|
1235
|
-
|
|
1497
|
+
useEffect4(() => {
|
|
1498
|
+
if (typeof window === "undefined") return;
|
|
1499
|
+
if (!isPrimary) return;
|
|
1500
|
+
window.addEventListener("jarve:close-bug-modal", handleCloseModal);
|
|
1501
|
+
return () => {
|
|
1502
|
+
window.removeEventListener("jarve:close-bug-modal", handleCloseModal);
|
|
1503
|
+
};
|
|
1504
|
+
}, [isPrimary, handleCloseModal]);
|
|
1236
1505
|
const reporterName = (user == null ? void 0 : user.name) || "Anonymous";
|
|
1237
1506
|
const reporterEmail = (user == null ? void 0 : user.email) || "unknown@external";
|
|
1507
|
+
if (!apiUrlValid) {
|
|
1508
|
+
return /* @__PURE__ */ jsx4(Fragment3, { children });
|
|
1509
|
+
}
|
|
1238
1510
|
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1239
1511
|
children,
|
|
1240
|
-
/* @__PURE__ */
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1512
|
+
mounted && isPrimary && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1513
|
+
/* @__PURE__ */ jsx4(
|
|
1514
|
+
FloatingButton,
|
|
1515
|
+
{
|
|
1516
|
+
isActive: captureMode,
|
|
1517
|
+
onClick: toggleCaptureMode,
|
|
1518
|
+
position: buttonPosition,
|
|
1519
|
+
zIndexBase
|
|
1520
|
+
}
|
|
1521
|
+
),
|
|
1522
|
+
/* @__PURE__ */ jsx4(
|
|
1523
|
+
CaptureOverlay,
|
|
1524
|
+
{
|
|
1525
|
+
isActive: captureMode,
|
|
1526
|
+
siteId,
|
|
1527
|
+
reporterName,
|
|
1528
|
+
reporterEmail,
|
|
1529
|
+
onCapture: handleCapture,
|
|
1530
|
+
onCancel: handleCancelCapture,
|
|
1531
|
+
zIndexBase
|
|
1532
|
+
}
|
|
1533
|
+
),
|
|
1534
|
+
/* @__PURE__ */ jsx4(
|
|
1535
|
+
ReportModal,
|
|
1536
|
+
{
|
|
1537
|
+
isOpen: showModal,
|
|
1538
|
+
captureResult,
|
|
1539
|
+
apiConfig: { apiUrl, apiKey },
|
|
1540
|
+
siteId,
|
|
1541
|
+
user: { name: reporterName, email: reporterEmail },
|
|
1542
|
+
onClose: handleCloseModal,
|
|
1543
|
+
zIndexBase,
|
|
1544
|
+
theme,
|
|
1545
|
+
onBeforeSubmit
|
|
1546
|
+
}
|
|
1547
|
+
)
|
|
1548
|
+
] })
|
|
1270
1549
|
] });
|
|
1271
1550
|
}
|
|
1272
1551
|
export {
|
|
1273
1552
|
JarveBugReporter
|
|
1274
1553
|
};
|
|
1275
|
-
//# sourceMappingURL=index.mjs.map
|