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