@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.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __defProps = Object.defineProperties;
|
|
5
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
6
|
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
7
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
8
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
10
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
11
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
12
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
@@ -33,6 +34,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
33
34
|
}
|
|
34
35
|
return to;
|
|
35
36
|
};
|
|
37
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
38
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
39
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
40
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
41
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
42
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
43
|
+
mod
|
|
44
|
+
));
|
|
36
45
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
46
|
|
|
38
47
|
// src/index.ts
|
|
@@ -48,21 +57,21 @@ var import_react4 = require("react");
|
|
|
48
57
|
// src/floating-button.tsx
|
|
49
58
|
var import_react = require("react");
|
|
50
59
|
var import_lucide_react = require("lucide-react");
|
|
51
|
-
|
|
52
|
-
// src/cn.ts
|
|
53
|
-
var import_clsx = require("clsx");
|
|
54
|
-
var import_tailwind_merge = require("tailwind-merge");
|
|
55
|
-
function cn(...inputs) {
|
|
56
|
-
return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/floating-button.tsx
|
|
60
|
+
var import_widget_shared = require("@jarve/widget-shared");
|
|
60
61
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
61
62
|
var STACK_OFFSET = 56;
|
|
62
|
-
function
|
|
63
|
+
function hasLauncherElement() {
|
|
64
|
+
return typeof document !== "undefined" && !!document.querySelector("[data-jarve-launcher]");
|
|
65
|
+
}
|
|
66
|
+
function FloatingButton({
|
|
67
|
+
isActive,
|
|
68
|
+
onClick,
|
|
69
|
+
position = "right",
|
|
70
|
+
zIndexBase = 1e4
|
|
71
|
+
}) {
|
|
63
72
|
const [hovered, setHovered] = (0, import_react.useState)(false);
|
|
64
73
|
const [stackOffset, setStackOffset] = (0, import_react.useState)(0);
|
|
65
|
-
const [launcherPresent, setLauncherPresent] = (0, import_react.useState)(
|
|
74
|
+
const [launcherPresent, setLauncherPresent] = (0, import_react.useState)(hasLauncherElement);
|
|
66
75
|
const ref = (0, import_react.useRef)(null);
|
|
67
76
|
const isLeft = position === "left";
|
|
68
77
|
const sideClasses = isLeft ? "left-4 md:left-6" : "right-4 md:right-6";
|
|
@@ -71,6 +80,7 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
71
80
|
if (!el) return;
|
|
72
81
|
let rafId;
|
|
73
82
|
const recalculate = () => {
|
|
83
|
+
if (!ref.current) return;
|
|
74
84
|
const widgets = Array.from(
|
|
75
85
|
document.querySelectorAll(`[data-jarve-widget][data-jarve-position="${position}"]`)
|
|
76
86
|
);
|
|
@@ -82,25 +92,26 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
82
92
|
const index = widgets.indexOf(el);
|
|
83
93
|
setStackOffset(index > 0 ? index * STACK_OFFSET : 0);
|
|
84
94
|
};
|
|
85
|
-
|
|
86
|
-
const observer = new MutationObserver(() => {
|
|
95
|
+
const scheduleRecalculate = () => {
|
|
87
96
|
cancelAnimationFrame(rafId);
|
|
88
97
|
rafId = requestAnimationFrame(recalculate);
|
|
89
|
-
}
|
|
90
|
-
|
|
98
|
+
};
|
|
99
|
+
recalculate();
|
|
100
|
+
const offReg = (0, import_widget_shared.on)("jarve:widget-registered", () => scheduleRecalculate());
|
|
101
|
+
const offDereg = (0, import_widget_shared.on)("jarve:widget-deregistered", () => scheduleRecalculate());
|
|
91
102
|
return () => {
|
|
92
|
-
|
|
103
|
+
offReg();
|
|
104
|
+
offDereg();
|
|
93
105
|
cancelAnimationFrame(rafId);
|
|
94
106
|
};
|
|
95
107
|
}, [position]);
|
|
96
108
|
(0, import_react.useEffect)(() => {
|
|
97
|
-
const
|
|
98
|
-
|
|
109
|
+
const offMounted = (0, import_widget_shared.on)("jarve:launcher-mounted", () => setLauncherPresent(true));
|
|
110
|
+
const offUnmounted = (0, import_widget_shared.on)("jarve:launcher-unmounted", () => setLauncherPresent(false));
|
|
111
|
+
return () => {
|
|
112
|
+
offMounted();
|
|
113
|
+
offUnmounted();
|
|
99
114
|
};
|
|
100
|
-
checkLauncher();
|
|
101
|
-
const observer = new MutationObserver(checkLauncher);
|
|
102
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
103
|
-
return () => observer.disconnect();
|
|
104
115
|
}, []);
|
|
105
116
|
if (launcherPresent) return null;
|
|
106
117
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -109,15 +120,17 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
109
120
|
ref,
|
|
110
121
|
"data-jarve-widget": "bug-reporter",
|
|
111
122
|
"data-jarve-position": position,
|
|
112
|
-
className: cn("fixed z-[
|
|
113
|
-
style:
|
|
123
|
+
className: (0, import_widget_shared.cn)("fixed z-[calc(var(--jarve-z-base)-1)]", "bottom-4 md:bottom-6", sideClasses),
|
|
124
|
+
style: __spreadValues({
|
|
125
|
+
["--jarve-z-base"]: String(zIndexBase)
|
|
126
|
+
}, stackOffset > 0 ? { transform: `translateY(-${stackOffset}px)` } : {}),
|
|
114
127
|
onMouseEnter: () => setHovered(true),
|
|
115
128
|
onMouseLeave: () => setHovered(false),
|
|
116
129
|
children: [
|
|
117
130
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
118
131
|
"div",
|
|
119
132
|
{
|
|
120
|
-
className: cn(
|
|
133
|
+
className: (0, import_widget_shared.cn)(
|
|
121
134
|
"pointer-events-none absolute bottom-full mb-3 w-max max-w-[240px] rounded-xl bg-gray-900/95 px-3.5 py-2.5 text-xs leading-relaxed text-white shadow-xl backdrop-blur-sm transition-all duration-200",
|
|
122
135
|
isLeft ? "left-0" : "right-0",
|
|
123
136
|
hovered && !isActive ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0"
|
|
@@ -129,7 +142,7 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
129
142
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
130
143
|
"div",
|
|
131
144
|
{
|
|
132
|
-
className: cn(
|
|
145
|
+
className: (0, import_widget_shared.cn)(
|
|
133
146
|
"absolute top-full h-0 w-0 border-x-[6px] border-t-[6px] border-x-transparent border-t-gray-900",
|
|
134
147
|
isLeft ? "left-4" : "right-4"
|
|
135
148
|
)
|
|
@@ -142,9 +155,9 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
142
155
|
"button",
|
|
143
156
|
{
|
|
144
157
|
onClick,
|
|
145
|
-
className: cn(
|
|
158
|
+
className: (0, import_widget_shared.cn)(
|
|
146
159
|
"flex items-center justify-center rounded-full shadow-lg transition-all duration-200",
|
|
147
|
-
"
|
|
160
|
+
"focus:ring-2 focus:ring-offset-2 focus:outline-none motion-safe:hover:scale-110",
|
|
148
161
|
"h-11 w-11 md:h-12 md:w-12",
|
|
149
162
|
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"
|
|
150
163
|
),
|
|
@@ -160,10 +173,9 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
|
|
|
160
173
|
|
|
161
174
|
// src/capture-overlay.tsx
|
|
162
175
|
var import_react2 = require("react");
|
|
163
|
-
var import_html_to_image = require("html-to-image");
|
|
164
176
|
|
|
165
177
|
// src/utils.ts
|
|
166
|
-
var
|
|
178
|
+
var import_widget_shared2 = require("@jarve/widget-shared");
|
|
167
179
|
function getNearestSection(element) {
|
|
168
180
|
let current = element;
|
|
169
181
|
while (current && current !== document.body) {
|
|
@@ -193,13 +205,7 @@ function getDeviceType() {
|
|
|
193
205
|
return "desktop";
|
|
194
206
|
}
|
|
195
207
|
function parseUserAgent() {
|
|
196
|
-
|
|
197
|
-
const browser = parser.getBrowser();
|
|
198
|
-
const osInfo = parser.getOS();
|
|
199
|
-
return {
|
|
200
|
-
browser: `${browser.name || "Unknown"} ${browser.version || ""}`.trim(),
|
|
201
|
-
os: `${osInfo.name || "Unknown"} ${osInfo.version || ""}`.trim()
|
|
202
|
-
};
|
|
208
|
+
return (0, import_widget_shared2.parseUserAgent)();
|
|
203
209
|
}
|
|
204
210
|
function buildSelectorPath(element, stopAt) {
|
|
205
211
|
const parts = [];
|
|
@@ -270,84 +276,114 @@ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, cl
|
|
|
270
276
|
};
|
|
271
277
|
}
|
|
272
278
|
|
|
279
|
+
// src/console-capture.ts
|
|
280
|
+
var import_widget_shared3 = require("@jarve/widget-shared");
|
|
281
|
+
|
|
282
|
+
// src/redact.ts
|
|
283
|
+
var SENSITIVE_JSON_KEY = /("(?:token|password|secret|api_?key|authorization)"\s*:\s*")[^"]*"/gi;
|
|
284
|
+
var BEARER_TOKEN = /(bearer)\s+[^\s"',;}]+/gi;
|
|
285
|
+
var COOKIE_HEADER = /(set-cookie|cookie)\s*:\s*[^\r\n]+/gi;
|
|
286
|
+
function redactSensitive(input) {
|
|
287
|
+
if (input === null || input === void 0) return input;
|
|
288
|
+
return input.replace(COOKIE_HEADER, (_, header) => `${header}: [REDACTED]`).replace(SENSITIVE_JSON_KEY, (_, keyPrefix) => `${keyPrefix}[REDACTED]"`).replace(BEARER_TOKEN, (_, word) => `${word} [REDACTED]`);
|
|
289
|
+
}
|
|
290
|
+
|
|
273
291
|
// src/console-capture.ts
|
|
274
292
|
var MAX_ERRORS = 50;
|
|
293
|
+
var MAX_MSG_LEN = 500;
|
|
294
|
+
function serializeArg(arg) {
|
|
295
|
+
var _a;
|
|
296
|
+
if (typeof arg === "string") return arg;
|
|
297
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}
|
|
298
|
+
${(_a = arg.stack) != null ? _a : ""}`;
|
|
299
|
+
try {
|
|
300
|
+
return JSON.stringify(arg);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
return String(arg);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
275
305
|
var capturedErrors = [];
|
|
276
|
-
var isCapturing = false;
|
|
277
306
|
var originalConsoleError = null;
|
|
307
|
+
var patchedConsoleErrorRef = null;
|
|
278
308
|
var errorListener = null;
|
|
279
309
|
var rejectionListener = null;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return String(a).slice(0, MAX_MSG_LEN);
|
|
310
|
+
var releasePatch = null;
|
|
311
|
+
var patchManager = (0, import_widget_shared3.createPatchManager)({
|
|
312
|
+
markerKey: "__jarveBugReporterConsolePatch",
|
|
313
|
+
install: () => {
|
|
314
|
+
capturedErrors = [];
|
|
315
|
+
originalConsoleError = console.error;
|
|
316
|
+
const patchedConsoleError = (...args) => {
|
|
317
|
+
const message = args.map((arg) => redactSensitive(serializeArg(arg)).slice(0, MAX_MSG_LEN)).join(" ").slice(0, MAX_MSG_LEN);
|
|
318
|
+
capturedErrors.push({
|
|
319
|
+
message,
|
|
320
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
321
|
+
});
|
|
322
|
+
if (capturedErrors.length > MAX_ERRORS) {
|
|
323
|
+
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
295
324
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
capturedErrors
|
|
325
|
+
originalConsoleError.apply(console, args);
|
|
326
|
+
};
|
|
327
|
+
patchedConsoleError.__bugReporterPatched = true;
|
|
328
|
+
patchedConsoleErrorRef = patchedConsoleError;
|
|
329
|
+
console.error = patchedConsoleError;
|
|
330
|
+
errorListener = (event) => {
|
|
331
|
+
capturedErrors.push({
|
|
332
|
+
message: event.message,
|
|
333
|
+
source: event.filename,
|
|
334
|
+
lineno: event.lineno,
|
|
335
|
+
colno: event.colno,
|
|
336
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
337
|
+
});
|
|
338
|
+
if (capturedErrors.length > MAX_ERRORS) {
|
|
339
|
+
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
window.addEventListener("error", errorListener);
|
|
343
|
+
rejectionListener = (event) => {
|
|
344
|
+
capturedErrors.push({
|
|
345
|
+
message: `Unhandled Promise Rejection: ${event.reason instanceof Error ? event.reason.message : String(event.reason)}`,
|
|
346
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
347
|
+
});
|
|
348
|
+
if (capturedErrors.length > MAX_ERRORS) {
|
|
349
|
+
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
window.addEventListener("unhandledrejection", rejectionListener);
|
|
353
|
+
},
|
|
354
|
+
uninstall: () => {
|
|
355
|
+
if (console.error === patchedConsoleErrorRef && originalConsoleError) {
|
|
356
|
+
console.error = originalConsoleError;
|
|
357
|
+
} else {
|
|
358
|
+
console.warn(
|
|
359
|
+
"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."
|
|
360
|
+
);
|
|
303
361
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
patchedConsoleError.__bugReporterPatched = true;
|
|
307
|
-
console.error = patchedConsoleError;
|
|
308
|
-
errorListener = (event) => {
|
|
309
|
-
capturedErrors.push({
|
|
310
|
-
message: event.message,
|
|
311
|
-
source: event.filename,
|
|
312
|
-
lineno: event.lineno,
|
|
313
|
-
colno: event.colno,
|
|
314
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
315
|
-
});
|
|
316
|
-
if (capturedErrors.length > MAX_ERRORS) {
|
|
317
|
-
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
362
|
+
if (patchedConsoleErrorRef) {
|
|
363
|
+
delete patchedConsoleErrorRef.__bugReporterPatched;
|
|
318
364
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
325
|
-
});
|
|
326
|
-
if (capturedErrors.length > MAX_ERRORS) {
|
|
327
|
-
capturedErrors = capturedErrors.slice(-MAX_ERRORS);
|
|
365
|
+
originalConsoleError = null;
|
|
366
|
+
patchedConsoleErrorRef = null;
|
|
367
|
+
if (errorListener) {
|
|
368
|
+
window.removeEventListener("error", errorListener);
|
|
369
|
+
errorListener = null;
|
|
328
370
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
function stopCapturing() {
|
|
333
|
-
if (!isCapturing) return;
|
|
334
|
-
isCapturing = false;
|
|
335
|
-
if (originalConsoleError) {
|
|
336
|
-
const restoreTarget = originalConsoleError;
|
|
337
|
-
console.error = restoreTarget;
|
|
338
|
-
if (console.error !== restoreTarget) {
|
|
339
|
-
console.warn("Bug reporter: failed to restore original console.error");
|
|
371
|
+
if (rejectionListener) {
|
|
372
|
+
window.removeEventListener("unhandledrejection", rejectionListener);
|
|
373
|
+
rejectionListener = null;
|
|
340
374
|
}
|
|
341
|
-
originalConsoleError = null;
|
|
342
|
-
}
|
|
343
|
-
if (errorListener) {
|
|
344
|
-
window.removeEventListener("error", errorListener);
|
|
345
|
-
errorListener = null;
|
|
346
|
-
}
|
|
347
|
-
if (rejectionListener) {
|
|
348
|
-
window.removeEventListener("unhandledrejection", rejectionListener);
|
|
349
|
-
rejectionListener = null;
|
|
350
375
|
}
|
|
376
|
+
});
|
|
377
|
+
function startCapturing() {
|
|
378
|
+
if (typeof window === "undefined") return;
|
|
379
|
+
if (releasePatch) return;
|
|
380
|
+
releasePatch = patchManager.acquire();
|
|
381
|
+
}
|
|
382
|
+
function stopCapturing() {
|
|
383
|
+
if (!releasePatch) return;
|
|
384
|
+
const release = releasePatch;
|
|
385
|
+
releasePatch = null;
|
|
386
|
+
release();
|
|
351
387
|
}
|
|
352
388
|
function getCapturedErrors() {
|
|
353
389
|
return [...capturedErrors];
|
|
@@ -357,17 +393,38 @@ function clearCapturedErrors() {
|
|
|
357
393
|
}
|
|
358
394
|
|
|
359
395
|
// src/network-capture.ts
|
|
396
|
+
var import_widget_shared4 = require("@jarve/widget-shared");
|
|
360
397
|
var MAX_REQUESTS = 30;
|
|
361
398
|
var MAX_BODY_READ_BYTES = 64 * 1024;
|
|
362
399
|
var TRUNCATE_LEN = 500;
|
|
363
400
|
var capturedRequests = [];
|
|
364
|
-
var isCapturing2 = false;
|
|
365
401
|
var originalFetch = null;
|
|
402
|
+
var patchedFetchRef = null;
|
|
403
|
+
var releasePatch2 = null;
|
|
404
|
+
var bodyCaptureMode = "off";
|
|
366
405
|
function truncateBody(body, maxLen = TRUNCATE_LEN) {
|
|
367
406
|
if (!body) return null;
|
|
368
407
|
if (body.length <= maxLen) return body;
|
|
369
408
|
return body.slice(0, maxLen) + "...(truncated)";
|
|
370
409
|
}
|
|
410
|
+
function sanitizeRequestUrl(raw) {
|
|
411
|
+
try {
|
|
412
|
+
const parsed = new URL(raw);
|
|
413
|
+
return {
|
|
414
|
+
url: parsed.origin + parsed.pathname,
|
|
415
|
+
hasQueryString: parsed.search.length > 0 || parsed.hash.length > 0
|
|
416
|
+
};
|
|
417
|
+
} catch (e) {
|
|
418
|
+
const queryIdx = raw.indexOf("?");
|
|
419
|
+
const hashIdx = raw.indexOf("#");
|
|
420
|
+
const cutoffs = [queryIdx, hashIdx].filter((i) => i >= 0);
|
|
421
|
+
if (cutoffs.length === 0) {
|
|
422
|
+
return { url: raw, hasQueryString: false };
|
|
423
|
+
}
|
|
424
|
+
const cut = Math.min(...cutoffs);
|
|
425
|
+
return { url: raw.slice(0, cut), hasQueryString: true };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
371
428
|
async function readBoundedBody(response) {
|
|
372
429
|
try {
|
|
373
430
|
const contentLength = response.headers.get("Content-Length");
|
|
@@ -395,72 +452,98 @@ async function readBoundedBody(response) {
|
|
|
395
452
|
}
|
|
396
453
|
const decoder = new TextDecoder();
|
|
397
454
|
const text2 = chunks.map((c) => decoder.decode(c, { stream: true })).join("");
|
|
398
|
-
return truncateBody(text2);
|
|
455
|
+
return truncateBody(redactSensitive(text2));
|
|
399
456
|
}
|
|
400
457
|
const text = await cloned.text();
|
|
401
|
-
return truncateBody(text);
|
|
458
|
+
return truncateBody(redactSensitive(text));
|
|
402
459
|
} catch (e) {
|
|
403
460
|
return null;
|
|
404
461
|
}
|
|
405
462
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
463
|
+
var patchManager2 = (0, import_widget_shared4.createPatchManager)({
|
|
464
|
+
markerKey: "__jarveBugReporterFetchPatch",
|
|
465
|
+
install: () => {
|
|
466
|
+
if (typeof window === "undefined") return;
|
|
467
|
+
capturedRequests = [];
|
|
468
|
+
originalFetch = window.fetch;
|
|
469
|
+
const patchedFetch = async function patchedFetch2(input, init) {
|
|
470
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
471
|
+
const method = (init == null ? void 0 : init.method) || (typeof input !== "string" && !(input instanceof URL) ? input.method : "GET") || "GET";
|
|
472
|
+
if (rawUrl.includes("/api/bug-reporter/") || rawUrl.includes("/bug-reporter/external/") || rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) {
|
|
473
|
+
return originalFetch.call(window, input, init);
|
|
474
|
+
}
|
|
475
|
+
const { url, hasQueryString } = sanitizeRequestUrl(rawUrl);
|
|
476
|
+
try {
|
|
477
|
+
const response = await originalFetch.call(window, input, init);
|
|
478
|
+
if (response.status >= 400) {
|
|
479
|
+
const responseBody = bodyCaptureMode === "on" ? await readBoundedBody(response) : null;
|
|
480
|
+
capturedRequests.push({
|
|
481
|
+
url,
|
|
482
|
+
hasQueryString,
|
|
483
|
+
method: method.toUpperCase(),
|
|
484
|
+
status: response.status,
|
|
485
|
+
statusText: response.statusText,
|
|
486
|
+
responseBody,
|
|
487
|
+
contentType: response.headers.get("content-type"),
|
|
488
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
489
|
+
});
|
|
490
|
+
if (capturedRequests.length > MAX_REQUESTS) {
|
|
491
|
+
capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return response;
|
|
495
|
+
} catch (error) {
|
|
422
496
|
capturedRequests.push({
|
|
423
497
|
url,
|
|
498
|
+
hasQueryString,
|
|
424
499
|
method: method.toUpperCase(),
|
|
425
|
-
status:
|
|
426
|
-
statusText:
|
|
427
|
-
responseBody,
|
|
500
|
+
status: 0,
|
|
501
|
+
statusText: error instanceof Error ? error.message : "Network error",
|
|
502
|
+
responseBody: null,
|
|
503
|
+
contentType: null,
|
|
428
504
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
429
505
|
});
|
|
430
506
|
if (capturedRequests.length > MAX_REQUESTS) {
|
|
431
507
|
capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
|
|
432
508
|
}
|
|
509
|
+
throw error;
|
|
433
510
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (capturedRequests.length > MAX_REQUESTS) {
|
|
445
|
-
capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
|
|
446
|
-
}
|
|
447
|
-
throw error;
|
|
511
|
+
};
|
|
512
|
+
patchedFetch.__bugReporterPatched = true;
|
|
513
|
+
patchedFetchRef = patchedFetch;
|
|
514
|
+
window.fetch = patchedFetch;
|
|
515
|
+
},
|
|
516
|
+
uninstall: () => {
|
|
517
|
+
if (typeof window === "undefined") {
|
|
518
|
+
originalFetch = null;
|
|
519
|
+
patchedFetchRef = null;
|
|
520
|
+
return;
|
|
448
521
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
window.fetch = restoreTarget;
|
|
459
|
-
if (window.fetch !== restoreTarget) {
|
|
460
|
-
console.warn("Bug reporter: failed to restore original fetch");
|
|
522
|
+
if (window.fetch === patchedFetchRef && originalFetch) {
|
|
523
|
+
window.fetch = originalFetch;
|
|
524
|
+
} else {
|
|
525
|
+
console.warn(
|
|
526
|
+
"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."
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (patchedFetchRef) {
|
|
530
|
+
delete patchedFetchRef.__bugReporterPatched;
|
|
461
531
|
}
|
|
462
532
|
originalFetch = null;
|
|
533
|
+
patchedFetchRef = null;
|
|
463
534
|
}
|
|
535
|
+
});
|
|
536
|
+
function startNetworkCapture(options = {}) {
|
|
537
|
+
if (typeof window === "undefined") return;
|
|
538
|
+
bodyCaptureMode = options.captureResponseBodies ? "on" : "off";
|
|
539
|
+
if (releasePatch2) return;
|
|
540
|
+
releasePatch2 = patchManager2.acquire();
|
|
541
|
+
}
|
|
542
|
+
function stopNetworkCapture() {
|
|
543
|
+
if (!releasePatch2) return;
|
|
544
|
+
const release = releasePatch2;
|
|
545
|
+
releasePatch2 = null;
|
|
546
|
+
release();
|
|
464
547
|
}
|
|
465
548
|
function getCapturedNetworkErrors() {
|
|
466
549
|
return [...capturedRequests];
|
|
@@ -474,11 +557,16 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
|
474
557
|
function dataUrlToBlob(dataUrl) {
|
|
475
558
|
var _a;
|
|
476
559
|
const [header, base64] = dataUrl.split(",");
|
|
560
|
+
if (!header || !base64) return new Blob();
|
|
477
561
|
const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
562
|
+
try {
|
|
563
|
+
const bytes = atob(base64);
|
|
564
|
+
const arr = new Uint8Array(bytes.length);
|
|
565
|
+
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
|
|
566
|
+
return new Blob([arr], { type: mime });
|
|
567
|
+
} catch (e) {
|
|
568
|
+
return new Blob();
|
|
569
|
+
}
|
|
482
570
|
}
|
|
483
571
|
function CaptureOverlay({
|
|
484
572
|
isActive,
|
|
@@ -486,11 +574,12 @@ function CaptureOverlay({
|
|
|
486
574
|
reporterName,
|
|
487
575
|
reporterEmail,
|
|
488
576
|
onCapture,
|
|
489
|
-
onCancel
|
|
577
|
+
onCancel,
|
|
578
|
+
zIndexBase = 1e4
|
|
490
579
|
}) {
|
|
491
580
|
const [hoveredElement, setHoveredElement] = (0, import_react2.useState)(null);
|
|
492
581
|
const [hoveredRect, setHoveredRect] = (0, import_react2.useState)(null);
|
|
493
|
-
const [
|
|
582
|
+
const [isCapturing, setIsCapturing] = (0, import_react2.useState)(false);
|
|
494
583
|
const [isTouchMode, setIsTouchMode] = (0, import_react2.useState)(false);
|
|
495
584
|
const [selectedSection, setSelectedSection] = (0, import_react2.useState)(null);
|
|
496
585
|
const [selectedRect, setSelectedRect] = (0, import_react2.useState)(null);
|
|
@@ -504,10 +593,20 @@ function CaptureOverlay({
|
|
|
504
593
|
setIsTouchMode(isTouchCapable());
|
|
505
594
|
}
|
|
506
595
|
}, [isActive]);
|
|
596
|
+
(0, import_react2.useEffect)(() => {
|
|
597
|
+
if (typeof document === "undefined") return;
|
|
598
|
+
if (!isActive || isTouchMode) return;
|
|
599
|
+
const root = document.documentElement;
|
|
600
|
+
root.classList.add("jarve-capturing");
|
|
601
|
+
return () => {
|
|
602
|
+
root.classList.remove("jarve-capturing");
|
|
603
|
+
};
|
|
604
|
+
}, [isActive, isTouchMode]);
|
|
507
605
|
const captureScreenshot = (0, import_react2.useCallback)(
|
|
508
606
|
async (section, target, coords) => {
|
|
509
607
|
const elementInfo = collectElementInfo(target, section, coords);
|
|
510
608
|
setIsCapturing(true);
|
|
609
|
+
const { toPng } = await import("html-to-image");
|
|
511
610
|
try {
|
|
512
611
|
setHoveredElement(null);
|
|
513
612
|
setHoveredRect(null);
|
|
@@ -519,7 +618,7 @@ function CaptureOverlay({
|
|
|
519
618
|
const MAX_DIMENSION = 2e3;
|
|
520
619
|
const sectionRect = section.getBoundingClientRect();
|
|
521
620
|
const pixelRatio = sectionRect.width > MAX_DIMENSION || sectionRect.height > MAX_DIMENSION ? 1 : 2;
|
|
522
|
-
const dataUrl = await
|
|
621
|
+
const dataUrl = await toPng(section, {
|
|
523
622
|
quality: 0.9,
|
|
524
623
|
pixelRatio,
|
|
525
624
|
skipFonts: true
|
|
@@ -541,7 +640,7 @@ function CaptureOverlay({
|
|
|
541
640
|
err
|
|
542
641
|
);
|
|
543
642
|
try {
|
|
544
|
-
const dataUrl = await
|
|
643
|
+
const dataUrl = await toPng(section, {
|
|
545
644
|
quality: 0.6,
|
|
546
645
|
pixelRatio: 1,
|
|
547
646
|
skipFonts: true,
|
|
@@ -584,7 +683,7 @@ function CaptureOverlay({
|
|
|
584
683
|
);
|
|
585
684
|
const handleMouseMove = (0, import_react2.useCallback)(
|
|
586
685
|
(e) => {
|
|
587
|
-
if (!isActive ||
|
|
686
|
+
if (!isActive || isCapturing || isTouchMode) return;
|
|
588
687
|
if (rafRef.current) return;
|
|
589
688
|
rafRef.current = requestAnimationFrame(() => {
|
|
590
689
|
rafRef.current = null;
|
|
@@ -602,11 +701,11 @@ function CaptureOverlay({
|
|
|
602
701
|
setHoveredRect(section ? section.getBoundingClientRect() : null);
|
|
603
702
|
});
|
|
604
703
|
},
|
|
605
|
-
[isActive,
|
|
704
|
+
[isActive, isCapturing, isTouchMode]
|
|
606
705
|
);
|
|
607
706
|
const handleClick = (0, import_react2.useCallback)(
|
|
608
707
|
async (e) => {
|
|
609
|
-
if (!isActive ||
|
|
708
|
+
if (!isActive || isCapturing || isTouchMode) return;
|
|
610
709
|
const target = e.target;
|
|
611
710
|
if (!(target instanceof HTMLElement)) return;
|
|
612
711
|
if (target.closest("[data-bug-reporter]")) return;
|
|
@@ -616,16 +715,18 @@ function CaptureOverlay({
|
|
|
616
715
|
if (!section) return;
|
|
617
716
|
await captureScreenshot(section, target, extractCoordinates(e));
|
|
618
717
|
},
|
|
619
|
-
[isActive,
|
|
718
|
+
[isActive, isCapturing, isTouchMode, captureScreenshot]
|
|
620
719
|
);
|
|
621
720
|
const handleTouchEnd = (0, import_react2.useCallback)(
|
|
622
721
|
(e) => {
|
|
623
|
-
if (!isActive ||
|
|
722
|
+
if (!isActive || isCapturing) return;
|
|
624
723
|
const touch = e.changedTouches[0];
|
|
625
724
|
if (!touch) return;
|
|
626
725
|
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
627
726
|
if (!(target instanceof HTMLElement)) return;
|
|
628
727
|
if (target.closest("[data-bug-reporter]")) return;
|
|
728
|
+
if (e.cancelable) e.preventDefault();
|
|
729
|
+
e.stopPropagation();
|
|
629
730
|
const section = getNearestSection(target);
|
|
630
731
|
if (!section) return;
|
|
631
732
|
setSelectedSection(section);
|
|
@@ -633,7 +734,7 @@ function CaptureOverlay({
|
|
|
633
734
|
setSelectedTarget(target);
|
|
634
735
|
touchCoordsRef.current = extractCoordinates(touch);
|
|
635
736
|
},
|
|
636
|
-
[isActive,
|
|
737
|
+
[isActive, isCapturing]
|
|
637
738
|
);
|
|
638
739
|
const handleConfirmCapture = (0, import_react2.useCallback)(async () => {
|
|
639
740
|
if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
|
|
@@ -683,7 +784,7 @@ function CaptureOverlay({
|
|
|
683
784
|
document.addEventListener("keydown", handleKeyDown);
|
|
684
785
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
685
786
|
if (isTouchMode) {
|
|
686
|
-
document.addEventListener("touchend", handleTouchEnd, { passive:
|
|
787
|
+
document.addEventListener("touchend", handleTouchEnd, { passive: false });
|
|
687
788
|
} else {
|
|
688
789
|
document.addEventListener("mousemove", handleMouseMove, true);
|
|
689
790
|
document.addEventListener("click", handleClick, true);
|
|
@@ -715,9 +816,10 @@ function CaptureOverlay({
|
|
|
715
816
|
"div",
|
|
716
817
|
{
|
|
717
818
|
"data-bug-reporter": true,
|
|
718
|
-
role: "
|
|
719
|
-
"aria-live": "
|
|
720
|
-
className: "fixed top-0 right-0 left-0 z-[
|
|
819
|
+
role: "status",
|
|
820
|
+
"aria-live": "polite",
|
|
821
|
+
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",
|
|
822
|
+
style: { ["--jarve-z-base"]: String(zIndexBase) },
|
|
721
823
|
children: isTouchMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
722
824
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Tap the section with the bug" }),
|
|
723
825
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
@@ -741,8 +843,9 @@ function CaptureOverlay({
|
|
|
741
843
|
{
|
|
742
844
|
ref: overlayRef,
|
|
743
845
|
"data-bug-reporter": true,
|
|
744
|
-
className: "pointer-events-none fixed z-[
|
|
846
|
+
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",
|
|
745
847
|
style: {
|
|
848
|
+
["--jarve-z-base"]: String(zIndexBase),
|
|
746
849
|
top: highlightRect.top - 2,
|
|
747
850
|
left: highlightRect.left - 2,
|
|
748
851
|
width: highlightRect.width + 4,
|
|
@@ -751,12 +854,15 @@ function CaptureOverlay({
|
|
|
751
854
|
}
|
|
752
855
|
}
|
|
753
856
|
),
|
|
754
|
-
isTouchMode && selectedSection && !
|
|
857
|
+
isTouchMode && selectedSection && !isCapturing && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
755
858
|
"div",
|
|
756
859
|
{
|
|
757
860
|
"data-bug-reporter": true,
|
|
758
|
-
className: "fixed right-0 bottom-0 left-0 z-[
|
|
759
|
-
style: {
|
|
861
|
+
className: "fixed right-0 bottom-0 left-0 z-[calc(var(--jarve-z-base))] border-t border-gray-200 bg-white shadow-lg",
|
|
862
|
+
style: {
|
|
863
|
+
["--jarve-z-base"]: String(zIndexBase),
|
|
864
|
+
paddingBottom: "env(safe-area-inset-bottom, 0px)"
|
|
865
|
+
},
|
|
760
866
|
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
|
|
761
867
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate text-sm font-medium text-gray-900", children: "Capture this section?" }),
|
|
762
868
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex shrink-0 gap-2", children: [
|
|
@@ -784,132 +890,179 @@ function CaptureOverlay({
|
|
|
784
890
|
] })
|
|
785
891
|
] })
|
|
786
892
|
}
|
|
787
|
-
)
|
|
788
|
-
!isTouchMode && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `* { cursor: crosshair !important; }` })
|
|
893
|
+
)
|
|
789
894
|
] });
|
|
790
895
|
}
|
|
791
896
|
|
|
792
897
|
// src/report-modal.tsx
|
|
793
898
|
var import_react3 = require("react");
|
|
794
899
|
var import_lucide_react2 = require("lucide-react");
|
|
900
|
+
var import_widget_shared5 = require("@jarve/widget-shared");
|
|
795
901
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
902
|
+
var STARTER_ASSISTANT_MESSAGE = {
|
|
903
|
+
role: "assistant",
|
|
904
|
+
content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
|
|
905
|
+
};
|
|
906
|
+
var DRAFT_STORAGE_PREFIX = "jarve:draft:bug-reporter:";
|
|
907
|
+
function draftStorageKey(siteId) {
|
|
908
|
+
return `${DRAFT_STORAGE_PREFIX}${siteId}`;
|
|
909
|
+
}
|
|
910
|
+
function readDraft(siteId) {
|
|
911
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
912
|
+
try {
|
|
913
|
+
const raw = sessionStorage.getItem(draftStorageKey(siteId));
|
|
914
|
+
if (!raw) return null;
|
|
915
|
+
const parsed = JSON.parse(raw);
|
|
916
|
+
if (typeof (parsed == null ? void 0 : parsed.input) !== "string" || !Array.isArray(parsed == null ? void 0 : parsed.messages)) return null;
|
|
917
|
+
return parsed;
|
|
918
|
+
} catch (e) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function writeDraft(siteId, draft) {
|
|
923
|
+
if (typeof sessionStorage === "undefined") return;
|
|
924
|
+
try {
|
|
925
|
+
sessionStorage.setItem(draftStorageKey(siteId), JSON.stringify(draft));
|
|
926
|
+
} catch (e) {
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function clearDraft(siteId) {
|
|
930
|
+
if (typeof sessionStorage === "undefined") return;
|
|
931
|
+
try {
|
|
932
|
+
sessionStorage.removeItem(draftStorageKey(siteId));
|
|
933
|
+
} catch (e) {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
796
936
|
function ReportModal({
|
|
797
937
|
isOpen,
|
|
798
938
|
captureResult,
|
|
799
939
|
apiConfig,
|
|
800
940
|
siteId,
|
|
801
|
-
|
|
802
|
-
|
|
941
|
+
onClose,
|
|
942
|
+
zIndexBase = 1e4,
|
|
943
|
+
theme = "auto",
|
|
944
|
+
onBeforeSubmit
|
|
803
945
|
}) {
|
|
804
|
-
const
|
|
946
|
+
const resolvedTheme = (0, import_widget_shared5.useResolvedTheme)(theme);
|
|
947
|
+
const [messages, setMessages] = (0, import_react3.useState)([STARTER_ASSISTANT_MESSAGE]);
|
|
805
948
|
const [input, setInput] = (0, import_react3.useState)("");
|
|
806
949
|
const [isLoading, setIsLoading] = (0, import_react3.useState)(false);
|
|
807
950
|
const [modalState, setModalState] = (0, import_react3.useState)("chatting");
|
|
808
951
|
const [reportId, setReportId] = (0, import_react3.useState)(null);
|
|
809
952
|
const [screenshotUrl, setScreenshotUrl] = (0, import_react3.useState)(null);
|
|
810
953
|
const [errorMessage, setErrorMessage] = (0, import_react3.useState)(null);
|
|
811
|
-
const
|
|
954
|
+
const [submitAlert, setSubmitAlert] = (0, import_react3.useState)(null);
|
|
955
|
+
const chatScrollRef = (0, import_react3.useRef)(null);
|
|
812
956
|
const inputRef = (0, import_react3.useRef)(null);
|
|
813
|
-
const
|
|
957
|
+
const dialogRef = (0, import_react3.useRef)(null);
|
|
958
|
+
const [sessionToken, setSessionToken] = (0, import_react3.useState)(() => "");
|
|
959
|
+
const activeSessionRef = (0, import_react3.useRef)("");
|
|
960
|
+
const abortControllerRef = (0, import_react3.useRef)(null);
|
|
961
|
+
if (abortControllerRef.current === null) {
|
|
962
|
+
abortControllerRef.current = new AbortController();
|
|
963
|
+
}
|
|
964
|
+
(0, import_react3.useEffect)(() => {
|
|
965
|
+
var _a, _b, _c;
|
|
966
|
+
if (isOpen) {
|
|
967
|
+
const next = crypto.randomUUID();
|
|
968
|
+
activeSessionRef.current = next;
|
|
969
|
+
setSessionToken(next);
|
|
970
|
+
const draft = readDraft(siteId);
|
|
971
|
+
setMessages(draft && draft.messages.length > 0 ? draft.messages : [STARTER_ASSISTANT_MESSAGE]);
|
|
972
|
+
setInput((_a = draft == null ? void 0 : draft.input) != null ? _a : "");
|
|
973
|
+
setIsLoading(false);
|
|
974
|
+
setModalState("chatting");
|
|
975
|
+
setReportId(null);
|
|
976
|
+
setErrorMessage(null);
|
|
977
|
+
setSubmitAlert(null);
|
|
978
|
+
if ((_b = abortControllerRef.current) == null ? void 0 : _b.signal.aborted) {
|
|
979
|
+
abortControllerRef.current = new AbortController();
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
(_c = abortControllerRef.current) == null ? void 0 : _c.abort();
|
|
983
|
+
}
|
|
984
|
+
}, [isOpen, siteId]);
|
|
985
|
+
(0, import_react3.useEffect)(() => {
|
|
986
|
+
return () => {
|
|
987
|
+
var _a;
|
|
988
|
+
(_a = abortControllerRef.current) == null ? void 0 : _a.abort();
|
|
989
|
+
};
|
|
990
|
+
}, []);
|
|
991
|
+
(0, import_react3.useEffect)(() => {
|
|
992
|
+
if (!isOpen) return;
|
|
993
|
+
if (modalState === "submitted" || modalState === "submitting") return;
|
|
994
|
+
const hasNonStarterMessages = messages.length > 1 || messages.length === 1 && messages[0] !== STARTER_ASSISTANT_MESSAGE;
|
|
995
|
+
if (!input && !hasNonStarterMessages) return;
|
|
996
|
+
writeDraft(siteId, { input, messages });
|
|
997
|
+
}, [isOpen, modalState, input, messages, siteId]);
|
|
814
998
|
const apiHeaders = (0, import_react3.useMemo)(
|
|
815
999
|
() => ({
|
|
816
1000
|
"Content-Type": "application/json",
|
|
817
|
-
"X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim()
|
|
1001
|
+
"X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim(),
|
|
1002
|
+
"X-Jarve-Session": sessionToken
|
|
1003
|
+
}),
|
|
1004
|
+
[apiConfig.apiKey, sessionToken]
|
|
1005
|
+
);
|
|
1006
|
+
const submitHeaders = (0, import_react3.useMemo)(
|
|
1007
|
+
() => ({
|
|
1008
|
+
"X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim(),
|
|
1009
|
+
"X-Jarve-Session": sessionToken
|
|
818
1010
|
}),
|
|
819
|
-
[apiConfig.apiKey]
|
|
1011
|
+
[apiConfig.apiKey, sessionToken]
|
|
820
1012
|
);
|
|
821
1013
|
(0, import_react3.useEffect)(() => {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
setScreenshotUrl(
|
|
825
|
-
|
|
826
|
-
return url;
|
|
827
|
-
});
|
|
828
|
-
return () => {
|
|
829
|
-
URL.revokeObjectURL(url);
|
|
830
|
-
setScreenshotUrl(null);
|
|
831
|
-
};
|
|
1014
|
+
const blob = captureResult == null ? void 0 : captureResult.screenshot;
|
|
1015
|
+
if (!blob || blob.size === 0) {
|
|
1016
|
+
setScreenshotUrl(null);
|
|
1017
|
+
return;
|
|
832
1018
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1019
|
+
const url = URL.createObjectURL(blob);
|
|
1020
|
+
setScreenshotUrl(url);
|
|
1021
|
+
return () => {
|
|
1022
|
+
URL.revokeObjectURL(url);
|
|
1023
|
+
};
|
|
837
1024
|
}, [captureResult]);
|
|
838
|
-
const sendInitialMessage = (0, import_react3.useCallback)(async () => {
|
|
839
|
-
if (!captureResult) return;
|
|
840
|
-
setIsLoading(true);
|
|
841
|
-
try {
|
|
842
|
-
const response = await fetch(`${apiConfig.apiUrl}/chat`, {
|
|
843
|
-
method: "POST",
|
|
844
|
-
headers: apiHeaders,
|
|
845
|
-
body: JSON.stringify({
|
|
846
|
-
messages: [],
|
|
847
|
-
metadata: captureResult.metadata,
|
|
848
|
-
consoleErrors: captureResult.consoleErrors,
|
|
849
|
-
networkErrors: captureResult.networkErrors,
|
|
850
|
-
clickedElement: captureResult.metadata.clickedElement || null
|
|
851
|
-
})
|
|
852
|
-
});
|
|
853
|
-
if (response.status === 401) {
|
|
854
|
-
console.error(
|
|
855
|
-
"Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
|
|
856
|
-
);
|
|
857
|
-
setMessages([
|
|
858
|
-
{
|
|
859
|
-
role: "assistant",
|
|
860
|
-
content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
|
|
861
|
-
}
|
|
862
|
-
]);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (!response.ok) throw new Error("Failed to get AI response");
|
|
866
|
-
const data = await response.json();
|
|
867
|
-
setMessages([{ role: "assistant", content: data.message }]);
|
|
868
|
-
} catch (e) {
|
|
869
|
-
setMessages([
|
|
870
|
-
{
|
|
871
|
-
role: "assistant",
|
|
872
|
-
content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
|
|
873
|
-
}
|
|
874
|
-
]);
|
|
875
|
-
} finally {
|
|
876
|
-
setIsLoading(false);
|
|
877
|
-
}
|
|
878
|
-
}, [captureResult, apiConfig.apiUrl, apiHeaders]);
|
|
879
1025
|
(0, import_react3.useEffect)(() => {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1026
|
+
const container = chatScrollRef.current;
|
|
1027
|
+
if (!container || typeof container.scrollTo !== "function") return;
|
|
1028
|
+
const gap = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
1029
|
+
if (gap < 100) {
|
|
1030
|
+
container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
|
|
883
1031
|
}
|
|
884
|
-
}, [isOpen, captureResult, sendInitialMessage]);
|
|
885
|
-
(0, import_react3.useEffect)(() => {
|
|
886
|
-
var _a;
|
|
887
|
-
(_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
888
1032
|
}, [messages]);
|
|
1033
|
+
(0, import_widget_shared5.useReturnFocus)(isOpen);
|
|
1034
|
+
(0, import_widget_shared5.useFocusTrap)(isOpen, dialogRef);
|
|
889
1035
|
(0, import_react3.useEffect)(() => {
|
|
890
1036
|
var _a;
|
|
891
1037
|
if (isOpen && !isLoading) {
|
|
892
1038
|
(_a = inputRef.current) == null ? void 0 : _a.focus();
|
|
893
1039
|
}
|
|
894
1040
|
}, [isOpen, isLoading, messages]);
|
|
1041
|
+
const buildFetchSignal = (0, import_react3.useCallback)(() => {
|
|
1042
|
+
const controller = abortControllerRef.current;
|
|
1043
|
+
return (0, import_widget_shared5.createTimeoutAbortSignal)(controller.signal, 3e4);
|
|
1044
|
+
}, []);
|
|
1045
|
+
const runWithFetchSignal = (0, import_react3.useCallback)(
|
|
1046
|
+
async (input2, init) => {
|
|
1047
|
+
const { signal, cleanup } = buildFetchSignal();
|
|
1048
|
+
try {
|
|
1049
|
+
return await fetch(input2, __spreadProps(__spreadValues({}, init), { signal }));
|
|
1050
|
+
} finally {
|
|
1051
|
+
cleanup();
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
[buildFetchSignal]
|
|
1055
|
+
);
|
|
895
1056
|
const submitReport = (0, import_react3.useCallback)(
|
|
896
1057
|
async (conversation, structuredReport) => {
|
|
897
1058
|
if (!captureResult || modalState !== "chatting") return;
|
|
898
1059
|
setModalState("submitting");
|
|
1060
|
+
const sessionAtDispatch = activeSessionRef.current;
|
|
899
1061
|
try {
|
|
900
|
-
let screenshotBase64;
|
|
901
|
-
if (captureResult.screenshot.size > 0) {
|
|
902
|
-
const reader = new FileReader();
|
|
903
|
-
screenshotBase64 = await new Promise((resolve, reject) => {
|
|
904
|
-
reader.onload = () => resolve(reader.result);
|
|
905
|
-
reader.onerror = reject;
|
|
906
|
-
reader.readAsDataURL(captureResult.screenshot);
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
1062
|
let report = structuredReport;
|
|
910
1063
|
if (!report) {
|
|
911
1064
|
try {
|
|
912
|
-
const summaryResponse = await
|
|
1065
|
+
const summaryResponse = await runWithFetchSignal(`${apiConfig.apiUrl}/chat`, {
|
|
913
1066
|
method: "POST",
|
|
914
1067
|
headers: apiHeaders,
|
|
915
1068
|
body: JSON.stringify({
|
|
@@ -921,6 +1074,7 @@ function ReportModal({
|
|
|
921
1074
|
requestSummary: true
|
|
922
1075
|
})
|
|
923
1076
|
});
|
|
1077
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
924
1078
|
if (summaryResponse.ok) {
|
|
925
1079
|
const data2 = await summaryResponse.json();
|
|
926
1080
|
report = data2.structuredReport;
|
|
@@ -928,46 +1082,87 @@ function ReportModal({
|
|
|
928
1082
|
} catch (e) {
|
|
929
1083
|
}
|
|
930
1084
|
}
|
|
931
|
-
|
|
1085
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1086
|
+
const basePayload = {
|
|
1087
|
+
metadata: captureResult.metadata,
|
|
1088
|
+
conversation,
|
|
1089
|
+
structuredReport: report,
|
|
1090
|
+
consoleErrors: captureResult.consoleErrors,
|
|
1091
|
+
networkErrors: captureResult.networkErrors,
|
|
1092
|
+
clickedElement: captureResult.metadata.clickedElement || null
|
|
1093
|
+
};
|
|
1094
|
+
let finalPayload = basePayload;
|
|
1095
|
+
if (onBeforeSubmit) {
|
|
1096
|
+
try {
|
|
1097
|
+
const maybe = await onBeforeSubmit(basePayload);
|
|
1098
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1099
|
+
if (maybe === null || maybe === false) {
|
|
1100
|
+
setSubmitAlert("Submission cancelled. Update your report and try again.");
|
|
1101
|
+
setModalState("chatting");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
finalPayload = maybe;
|
|
1105
|
+
} catch (hookErr) {
|
|
1106
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1107
|
+
console.error("Bug reporter: onBeforeSubmit hook rejected the payload", hookErr);
|
|
1108
|
+
setSubmitAlert("Submission failed \u2014 please try again.");
|
|
1109
|
+
setModalState("chatting");
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const formData = new FormData();
|
|
1114
|
+
if (captureResult.screenshot.size > 0) {
|
|
1115
|
+
formData.append("screenshot", captureResult.screenshot);
|
|
1116
|
+
}
|
|
1117
|
+
formData.append("payload", JSON.stringify(finalPayload));
|
|
1118
|
+
const response = await runWithFetchSignal(`${apiConfig.apiUrl}/submit`, {
|
|
932
1119
|
method: "POST",
|
|
933
|
-
headers:
|
|
934
|
-
body:
|
|
935
|
-
screenshot: screenshotBase64,
|
|
936
|
-
metadata: captureResult.metadata,
|
|
937
|
-
conversation,
|
|
938
|
-
structuredReport: report,
|
|
939
|
-
consoleErrors: captureResult.consoleErrors,
|
|
940
|
-
networkErrors: captureResult.networkErrors,
|
|
941
|
-
clickedElement: captureResult.metadata.clickedElement || null
|
|
942
|
-
})
|
|
1120
|
+
headers: submitHeaders,
|
|
1121
|
+
body: formData
|
|
943
1122
|
});
|
|
1123
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
944
1124
|
if (!response.ok) {
|
|
945
1125
|
const errorData = await response.json().catch(() => ({}));
|
|
946
1126
|
throw new Error(errorData.error || "Failed to submit report");
|
|
947
1127
|
}
|
|
948
1128
|
const data = await response.json();
|
|
1129
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
949
1130
|
setReportId(data.id);
|
|
950
1131
|
setModalState("submitted");
|
|
1132
|
+
clearDraft(siteId);
|
|
951
1133
|
} catch (err) {
|
|
1134
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
952
1135
|
console.error("Bug reporter: failed to submit report", err);
|
|
953
1136
|
setErrorMessage(err instanceof Error ? err.message : "Failed to submit report");
|
|
954
1137
|
setModalState("error");
|
|
955
1138
|
}
|
|
956
1139
|
},
|
|
957
|
-
[
|
|
1140
|
+
[
|
|
1141
|
+
captureResult,
|
|
1142
|
+
apiConfig.apiUrl,
|
|
1143
|
+
apiHeaders,
|
|
1144
|
+
submitHeaders,
|
|
1145
|
+
modalState,
|
|
1146
|
+
siteId,
|
|
1147
|
+
onBeforeSubmit,
|
|
1148
|
+
runWithFetchSignal
|
|
1149
|
+
]
|
|
958
1150
|
);
|
|
959
1151
|
const handleManualSubmit = (0, import_react3.useCallback)(() => {
|
|
1152
|
+
setSubmitAlert(null);
|
|
960
1153
|
submitReport(messages);
|
|
961
1154
|
}, [submitReport, messages]);
|
|
962
1155
|
async function sendMessage() {
|
|
963
1156
|
if (!input.trim() || isLoading || !captureResult) return;
|
|
964
1157
|
const userMessage = input.trim();
|
|
965
1158
|
setInput("");
|
|
1159
|
+
setSubmitAlert(null);
|
|
966
1160
|
const newMessages = [...messages, { role: "user", content: userMessage }];
|
|
967
1161
|
setMessages(newMessages);
|
|
968
1162
|
setIsLoading(true);
|
|
1163
|
+
const sessionAtDispatch = activeSessionRef.current;
|
|
969
1164
|
try {
|
|
970
|
-
const response = await
|
|
1165
|
+
const response = await runWithFetchSignal(`${apiConfig.apiUrl}/chat`, {
|
|
971
1166
|
method: "POST",
|
|
972
1167
|
headers: apiHeaders,
|
|
973
1168
|
body: JSON.stringify({
|
|
@@ -978,6 +1173,7 @@ function ReportModal({
|
|
|
978
1173
|
clickedElement: captureResult.metadata.clickedElement || null
|
|
979
1174
|
})
|
|
980
1175
|
});
|
|
1176
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
981
1177
|
if (response.status === 401) {
|
|
982
1178
|
console.error(
|
|
983
1179
|
"Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
|
|
@@ -993,6 +1189,7 @@ function ReportModal({
|
|
|
993
1189
|
}
|
|
994
1190
|
if (!response.ok) throw new Error("Failed to get AI response");
|
|
995
1191
|
const data = await response.json();
|
|
1192
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
996
1193
|
setMessages([...newMessages, { role: "assistant", content: data.message }]);
|
|
997
1194
|
if (data.readyToSubmit && data.structuredReport) {
|
|
998
1195
|
await submitReport(
|
|
@@ -1001,6 +1198,7 @@ function ReportModal({
|
|
|
1001
1198
|
);
|
|
1002
1199
|
}
|
|
1003
1200
|
} catch (e) {
|
|
1201
|
+
if (sessionAtDispatch !== activeSessionRef.current) return;
|
|
1004
1202
|
setMessages([
|
|
1005
1203
|
...newMessages,
|
|
1006
1204
|
{
|
|
@@ -1009,22 +1207,23 @@ function ReportModal({
|
|
|
1009
1207
|
}
|
|
1010
1208
|
]);
|
|
1011
1209
|
} finally {
|
|
1012
|
-
|
|
1210
|
+
if (sessionAtDispatch === activeSessionRef.current) {
|
|
1211
|
+
setIsLoading(false);
|
|
1212
|
+
}
|
|
1013
1213
|
}
|
|
1014
1214
|
}
|
|
1015
1215
|
function handleClose() {
|
|
1016
|
-
|
|
1216
|
+
var _a;
|
|
1217
|
+
(_a = abortControllerRef.current) == null ? void 0 : _a.abort();
|
|
1218
|
+
setMessages([STARTER_ASSISTANT_MESSAGE]);
|
|
1017
1219
|
setInput("");
|
|
1018
1220
|
setModalState("chatting");
|
|
1019
1221
|
setReportId(null);
|
|
1020
1222
|
setErrorMessage(null);
|
|
1021
|
-
|
|
1022
|
-
URL.revokeObjectURL(screenshotUrl);
|
|
1023
|
-
}
|
|
1024
|
-
setScreenshotUrl(null);
|
|
1025
|
-
hasInitRef.current = false;
|
|
1223
|
+
setSubmitAlert(null);
|
|
1026
1224
|
onClose();
|
|
1027
1225
|
}
|
|
1226
|
+
(0, import_widget_shared5.useEscapeKey)(handleClose, isOpen);
|
|
1028
1227
|
function handleKeyDown(e) {
|
|
1029
1228
|
if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
|
|
1030
1229
|
e.preventDefault();
|
|
@@ -1036,14 +1235,24 @@ function ReportModal({
|
|
|
1036
1235
|
"div",
|
|
1037
1236
|
{
|
|
1038
1237
|
"data-bug-reporter": true,
|
|
1039
|
-
className: "fixed inset-0 z-[
|
|
1238
|
+
className: "fixed inset-0 z-[calc(var(--jarve-z-base)+1)] flex items-center justify-center bg-black/50 backdrop-blur-sm",
|
|
1239
|
+
style: { ["--jarve-z-base"]: String(zIndexBase) },
|
|
1040
1240
|
onClick: (e) => {
|
|
1041
1241
|
if (e.target === e.currentTarget) handleClose();
|
|
1042
1242
|
},
|
|
1043
1243
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1044
1244
|
"div",
|
|
1045
1245
|
{
|
|
1046
|
-
|
|
1246
|
+
ref: dialogRef,
|
|
1247
|
+
role: "dialog",
|
|
1248
|
+
"aria-modal": "true",
|
|
1249
|
+
"aria-labelledby": "jarve-bug-reporter-title",
|
|
1250
|
+
className: (0, import_widget_shared5.cn)(
|
|
1251
|
+
// Self-owned theme wrapper — `dark` toggles the existing Tailwind
|
|
1252
|
+
// `dark:` utilities via the shared `@custom-variant dark (&:is(.dark *))`
|
|
1253
|
+
// rule in styles.css. This makes the widget independent of the
|
|
1254
|
+
// host app's Tailwind darkMode config. See Issue E11.
|
|
1255
|
+
resolvedTheme === "dark" ? "jarve-theme-dark dark" : "jarve-theme-light",
|
|
1047
1256
|
"flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-950",
|
|
1048
1257
|
"mx-4 w-full max-w-lg",
|
|
1049
1258
|
"max-[768px]:mx-0 max-[768px]:h-full max-[768px]:max-w-none max-[768px]:rounded-none",
|
|
@@ -1052,7 +1261,14 @@ function ReportModal({
|
|
|
1052
1261
|
children: [
|
|
1053
1262
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("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: [
|
|
1054
1263
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
1055
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1264
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1265
|
+
"h2",
|
|
1266
|
+
{
|
|
1267
|
+
id: "jarve-bug-reporter-title",
|
|
1268
|
+
className: "text-sm font-semibold text-gray-900 dark:text-gray-100",
|
|
1269
|
+
children: "Bug Report"
|
|
1270
|
+
}
|
|
1271
|
+
),
|
|
1056
1272
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: siteId })
|
|
1057
1273
|
] }),
|
|
1058
1274
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -1073,58 +1289,76 @@ function ReportModal({
|
|
|
1073
1289
|
className: "max-h-40 w-full rounded-md border border-gray-200 object-contain dark:border-gray-700"
|
|
1074
1290
|
}
|
|
1075
1291
|
) }),
|
|
1076
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
"
|
|
1081
|
-
" ",
|
|
1082
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
children: "
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1292
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1293
|
+
"div",
|
|
1294
|
+
{
|
|
1295
|
+
ref: chatScrollRef,
|
|
1296
|
+
"data-testid": "chat-scroll",
|
|
1297
|
+
className: "min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-3",
|
|
1298
|
+
children: modalState === "submitted" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
|
|
1299
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.CheckCircle2, { className: "mb-3 h-12 w-12 text-green-500" }),
|
|
1300
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
|
|
1301
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [
|
|
1302
|
+
"Reference:",
|
|
1303
|
+
" ",
|
|
1304
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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) })
|
|
1305
|
+
] }),
|
|
1306
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: "Thanks for the report \u2014 we'll look into it." }),
|
|
1307
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1308
|
+
"button",
|
|
1309
|
+
{
|
|
1310
|
+
onClick: handleClose,
|
|
1311
|
+
className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
|
|
1312
|
+
children: "Done"
|
|
1313
|
+
}
|
|
1314
|
+
)
|
|
1315
|
+
] }) : modalState === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
|
|
1316
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.X, { className: "mb-3 h-12 w-12 text-red-500" }),
|
|
1317
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
|
|
1318
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: errorMessage || "Something went wrong. Please try again." }),
|
|
1319
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1320
|
+
"button",
|
|
1321
|
+
{
|
|
1322
|
+
onClick: () => setModalState("chatting"),
|
|
1323
|
+
className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
|
|
1324
|
+
children: "Try Again"
|
|
1325
|
+
}
|
|
1326
|
+
)
|
|
1327
|
+
] }) : modalState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8", children: [
|
|
1328
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
|
|
1329
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
|
|
1330
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1331
|
+
(captureResult == null ? void 0 : captureResult.screenshot.size) === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-amber-600", children: "Screenshot could not be captured. Please describe the visual issue in detail." }),
|
|
1332
|
+
messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1333
|
+
"div",
|
|
1334
|
+
{
|
|
1335
|
+
className: (0, import_widget_shared5.cn)(
|
|
1336
|
+
"text-sm leading-relaxed",
|
|
1337
|
+
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"
|
|
1338
|
+
),
|
|
1339
|
+
children: msg.content
|
|
1340
|
+
},
|
|
1341
|
+
i
|
|
1342
|
+
)),
|
|
1343
|
+
isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
|
|
1344
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
|
|
1345
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
|
|
1346
|
+
] })
|
|
1347
|
+
] })
|
|
1348
|
+
}
|
|
1349
|
+
),
|
|
1350
|
+
modalState === "chatting" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
1351
|
+
submitAlert && // onBeforeSubmit rejected the payload — keep the draft and
|
|
1352
|
+
// let the user retry. role="alert" announces to AT users.
|
|
1353
|
+
// See Issue B8.
|
|
1097
1354
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1098
|
-
"
|
|
1355
|
+
"p",
|
|
1099
1356
|
{
|
|
1100
|
-
|
|
1101
|
-
className: "
|
|
1102
|
-
children:
|
|
1357
|
+
role: "alert",
|
|
1358
|
+
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",
|
|
1359
|
+
children: submitAlert
|
|
1103
1360
|
}
|
|
1104
|
-
)
|
|
1105
|
-
] }) : modalState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8", children: [
|
|
1106
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
|
|
1107
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
|
|
1108
|
-
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1109
|
-
(captureResult == null ? void 0 : captureResult.screenshot.size) === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-amber-600", children: "Screenshot could not be captured. Please describe the visual issue in detail." }),
|
|
1110
|
-
messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1111
|
-
"div",
|
|
1112
|
-
{
|
|
1113
|
-
className: cn(
|
|
1114
|
-
"text-sm leading-relaxed",
|
|
1115
|
-
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"
|
|
1116
|
-
),
|
|
1117
|
-
children: msg.content
|
|
1118
|
-
},
|
|
1119
|
-
i
|
|
1120
|
-
)),
|
|
1121
|
-
isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
|
|
1122
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
|
|
1123
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
|
|
1124
|
-
] }),
|
|
1125
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: chatEndRef })
|
|
1126
|
-
] }) }),
|
|
1127
|
-
modalState === "chatting" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
1361
|
+
),
|
|
1128
1362
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1129
1363
|
"textarea",
|
|
1130
1364
|
{
|
|
@@ -1182,34 +1416,66 @@ function ReportModal({
|
|
|
1182
1416
|
}
|
|
1183
1417
|
|
|
1184
1418
|
// src/bug-reporter.tsx
|
|
1419
|
+
var import_widget_shared6 = require("@jarve/widget-shared");
|
|
1185
1420
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1421
|
+
function isValidApiUrl(raw) {
|
|
1422
|
+
if (typeof raw !== "string" || raw.length === 0) return false;
|
|
1423
|
+
try {
|
|
1424
|
+
const parsed = new URL(raw);
|
|
1425
|
+
if (parsed.protocol === "https:") return true;
|
|
1426
|
+
if (parsed.protocol === "http:" && parsed.hostname === "localhost") return true;
|
|
1427
|
+
return false;
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1186
1432
|
function JarveBugReporter({
|
|
1187
1433
|
apiUrl,
|
|
1188
1434
|
apiKey,
|
|
1435
|
+
siteId,
|
|
1189
1436
|
user,
|
|
1190
1437
|
buttonPosition,
|
|
1438
|
+
zIndexBase,
|
|
1439
|
+
captureResponseBodies = false,
|
|
1440
|
+
theme,
|
|
1441
|
+
onBeforeSubmit,
|
|
1191
1442
|
children
|
|
1192
1443
|
}) {
|
|
1193
|
-
const
|
|
1444
|
+
const mounted = (0, import_widget_shared6.useHasMounted)();
|
|
1445
|
+
const apiUrlValid = (0, import_react4.useMemo)(() => isValidApiUrl(apiUrl), [apiUrl]);
|
|
1446
|
+
(0, import_react4.useEffect)(() => {
|
|
1447
|
+
if (!apiUrlValid) {
|
|
1448
|
+
console.error(
|
|
1449
|
+
`Jarve bug reporter: invalid apiUrl \u2014 widget disabled (received: ${JSON.stringify(apiUrl)})`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
}, [apiUrl, apiUrlValid]);
|
|
1194
1453
|
const [captureMode, setCaptureMode] = (0, import_react4.useState)(false);
|
|
1195
1454
|
const [captureResult, setCaptureResult] = (0, import_react4.useState)(null);
|
|
1196
1455
|
const [showModal, setShowModal] = (0, import_react4.useState)(false);
|
|
1456
|
+
const [isPrimary, setIsPrimary] = (0, import_react4.useState)(false);
|
|
1197
1457
|
(0, import_react4.useEffect)(() => {
|
|
1458
|
+
if (!apiUrlValid || !isPrimary) return;
|
|
1198
1459
|
startCapturing();
|
|
1199
|
-
startNetworkCapture();
|
|
1460
|
+
startNetworkCapture({ captureResponseBodies });
|
|
1200
1461
|
return () => {
|
|
1201
1462
|
stopCapturing();
|
|
1202
1463
|
stopNetworkCapture();
|
|
1203
1464
|
};
|
|
1204
|
-
}, []);
|
|
1465
|
+
}, [apiUrlValid, isPrimary, captureResponseBodies]);
|
|
1205
1466
|
const toggleCaptureMode = (0, import_react4.useCallback)(() => {
|
|
1206
1467
|
setCaptureMode((prev) => !prev);
|
|
1207
1468
|
}, []);
|
|
1208
1469
|
(0, import_react4.useEffect)(() => {
|
|
1209
1470
|
if (typeof window === "undefined") return;
|
|
1471
|
+
if (!apiUrlValid) return;
|
|
1210
1472
|
if (!window.__jarve_widgets) {
|
|
1211
1473
|
window.__jarve_widgets = /* @__PURE__ */ new Map();
|
|
1212
1474
|
}
|
|
1475
|
+
if (window.__jarve_widgets.has("bug-reporter")) {
|
|
1476
|
+
console.warn("JarveBugReporter: instance already mounted; additional instance ignored");
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1213
1479
|
window.__jarve_widgets.set("bug-reporter", {
|
|
1214
1480
|
type: "bug-reporter",
|
|
1215
1481
|
label: "Report a Bug",
|
|
@@ -1219,28 +1485,25 @@ function JarveBugReporter({
|
|
|
1219
1485
|
isActive: false,
|
|
1220
1486
|
trigger: toggleCaptureMode
|
|
1221
1487
|
});
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
);
|
|
1488
|
+
setIsPrimary(true);
|
|
1489
|
+
(0, import_widget_shared6.emit)("jarve:widget-registered", { type: "bug-reporter" });
|
|
1225
1490
|
return () => {
|
|
1226
1491
|
var _a;
|
|
1227
1492
|
(_a = window.__jarve_widgets) == null ? void 0 : _a.delete("bug-reporter");
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
);
|
|
1493
|
+
setIsPrimary(false);
|
|
1494
|
+
(0, import_widget_shared6.emit)("jarve:widget-deregistered", { type: "bug-reporter" });
|
|
1231
1495
|
};
|
|
1232
|
-
}, [toggleCaptureMode]);
|
|
1496
|
+
}, [toggleCaptureMode, apiUrlValid]);
|
|
1233
1497
|
(0, import_react4.useEffect)(() => {
|
|
1234
1498
|
var _a;
|
|
1235
1499
|
if (typeof window === "undefined") return;
|
|
1500
|
+
if (!isPrimary) return;
|
|
1236
1501
|
const entry = (_a = window.__jarve_widgets) == null ? void 0 : _a.get("bug-reporter");
|
|
1237
1502
|
if (entry) {
|
|
1238
1503
|
entry.isActive = captureMode || showModal;
|
|
1239
|
-
|
|
1240
|
-
new CustomEvent("jarve:state-change", { detail: { type: "bug-reporter" } })
|
|
1241
|
-
);
|
|
1504
|
+
(0, import_widget_shared6.emit)("jarve:state-change", { type: "bug-reporter", isActive: entry.isActive });
|
|
1242
1505
|
}
|
|
1243
|
-
}, [captureMode, showModal]);
|
|
1506
|
+
}, [captureMode, showModal, isPrimary]);
|
|
1244
1507
|
const handleCapture = (0, import_react4.useCallback)((result) => {
|
|
1245
1508
|
setCaptureResult(result);
|
|
1246
1509
|
setCaptureMode(false);
|
|
@@ -1250,50 +1513,67 @@ function JarveBugReporter({
|
|
|
1250
1513
|
setCaptureMode(false);
|
|
1251
1514
|
}, []);
|
|
1252
1515
|
const handleCloseModal = (0, import_react4.useCallback)(() => {
|
|
1516
|
+
setCaptureMode(false);
|
|
1253
1517
|
setShowModal(false);
|
|
1254
1518
|
setCaptureResult(null);
|
|
1255
1519
|
clearCapturedErrors();
|
|
1256
1520
|
clearCapturedNetworkErrors();
|
|
1257
1521
|
}, []);
|
|
1258
|
-
|
|
1522
|
+
(0, import_react4.useEffect)(() => {
|
|
1523
|
+
if (typeof window === "undefined") return;
|
|
1524
|
+
if (!isPrimary) return;
|
|
1525
|
+
window.addEventListener("jarve:close-bug-modal", handleCloseModal);
|
|
1526
|
+
return () => {
|
|
1527
|
+
window.removeEventListener("jarve:close-bug-modal", handleCloseModal);
|
|
1528
|
+
};
|
|
1529
|
+
}, [isPrimary, handleCloseModal]);
|
|
1259
1530
|
const reporterName = (user == null ? void 0 : user.name) || "Anonymous";
|
|
1260
1531
|
const reporterEmail = (user == null ? void 0 : user.email) || "unknown@external";
|
|
1532
|
+
if (!apiUrlValid) {
|
|
1533
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children });
|
|
1534
|
+
}
|
|
1261
1535
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1262
1536
|
children,
|
|
1263
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1537
|
+
mounted && isPrimary && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1538
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1539
|
+
FloatingButton,
|
|
1540
|
+
{
|
|
1541
|
+
isActive: captureMode,
|
|
1542
|
+
onClick: toggleCaptureMode,
|
|
1543
|
+
position: buttonPosition,
|
|
1544
|
+
zIndexBase
|
|
1545
|
+
}
|
|
1546
|
+
),
|
|
1547
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1548
|
+
CaptureOverlay,
|
|
1549
|
+
{
|
|
1550
|
+
isActive: captureMode,
|
|
1551
|
+
siteId,
|
|
1552
|
+
reporterName,
|
|
1553
|
+
reporterEmail,
|
|
1554
|
+
onCapture: handleCapture,
|
|
1555
|
+
onCancel: handleCancelCapture,
|
|
1556
|
+
zIndexBase
|
|
1557
|
+
}
|
|
1558
|
+
),
|
|
1559
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1560
|
+
ReportModal,
|
|
1561
|
+
{
|
|
1562
|
+
isOpen: showModal,
|
|
1563
|
+
captureResult,
|
|
1564
|
+
apiConfig: { apiUrl, apiKey },
|
|
1565
|
+
siteId,
|
|
1566
|
+
user: { name: reporterName, email: reporterEmail },
|
|
1567
|
+
onClose: handleCloseModal,
|
|
1568
|
+
zIndexBase,
|
|
1569
|
+
theme,
|
|
1570
|
+
onBeforeSubmit
|
|
1571
|
+
}
|
|
1572
|
+
)
|
|
1573
|
+
] })
|
|
1293
1574
|
] });
|
|
1294
1575
|
}
|
|
1295
1576
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1296
1577
|
0 && (module.exports = {
|
|
1297
1578
|
JarveBugReporter
|
|
1298
1579
|
});
|
|
1299
|
-
//# sourceMappingURL=index.js.map
|