@stephenov/feedbackwidget 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auto.js ADDED
@@ -0,0 +1,1529 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/auto.tsx
32
+ var auto_exports = {};
33
+ __export(auto_exports, {
34
+ AutoFeedbackWidget: () => AutoFeedbackWidget,
35
+ default: () => auto_default
36
+ });
37
+ module.exports = __toCommonJS(auto_exports);
38
+ var import_react5 = require("react");
39
+
40
+ // src/components/FeedbackWidget.tsx
41
+ var import_react4 = require("react");
42
+
43
+ // src/components/FeedbackTrigger.tsx
44
+ var import_react = require("react");
45
+
46
+ // src/utils.ts
47
+ function cn(...classes) {
48
+ return classes.filter(Boolean).join(" ");
49
+ }
50
+ function formatDuration(seconds) {
51
+ const mins = Math.floor(seconds / 60);
52
+ const secs = seconds % 60;
53
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
54
+ }
55
+ function getBrowserInfo() {
56
+ if (typeof window === "undefined") return {};
57
+ return {
58
+ userAgent: navigator.userAgent,
59
+ language: navigator.language,
60
+ viewport: {
61
+ width: window.innerWidth,
62
+ height: window.innerHeight
63
+ },
64
+ screen: {
65
+ width: screen.width,
66
+ height: screen.height
67
+ },
68
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
69
+ };
70
+ }
71
+ function isVoiceSupported() {
72
+ if (typeof window === "undefined") return false;
73
+ return !!(navigator.mediaDevices && typeof MediaRecorder !== "undefined");
74
+ }
75
+ var DEFAULT_LABELS = {
76
+ triggerTooltip: "Send feedback",
77
+ panelTitle: "Send Feedback",
78
+ voiceButton: "Voice",
79
+ textButton: "Type",
80
+ submitButton: "Send Feedback",
81
+ successMessage: "Thanks for your feedback!",
82
+ placeholder: "What's on your mind?"
83
+ };
84
+
85
+ // src/components/icons.tsx
86
+ var import_jsx_runtime = require("react/jsx-runtime");
87
+ function ZapIcon({ className, size = 24 }) {
88
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
89
+ "svg",
90
+ {
91
+ xmlns: "http://www.w3.org/2000/svg",
92
+ width: size,
93
+ height: size,
94
+ viewBox: "0 0 24 24",
95
+ fill: "none",
96
+ stroke: "currentColor",
97
+ strokeWidth: "2.5",
98
+ strokeLinecap: "round",
99
+ strokeLinejoin: "round",
100
+ className,
101
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "13 2 3 14 12 14 11 22 21 10 12 10 13 2" })
102
+ }
103
+ );
104
+ }
105
+ function MicIcon({ className, size = 24 }) {
106
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
107
+ "svg",
108
+ {
109
+ xmlns: "http://www.w3.org/2000/svg",
110
+ width: size,
111
+ height: size,
112
+ viewBox: "0 0 24 24",
113
+ fill: "none",
114
+ stroke: "currentColor",
115
+ strokeWidth: "2",
116
+ strokeLinecap: "round",
117
+ strokeLinejoin: "round",
118
+ className,
119
+ children: [
120
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }),
121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
122
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", x2: "12", y1: "19", y2: "22" })
123
+ ]
124
+ }
125
+ );
126
+ }
127
+ function TypeIcon({ className, size = 24 }) {
128
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
129
+ "svg",
130
+ {
131
+ xmlns: "http://www.w3.org/2000/svg",
132
+ width: size,
133
+ height: size,
134
+ viewBox: "0 0 24 24",
135
+ fill: "none",
136
+ stroke: "currentColor",
137
+ strokeWidth: "2",
138
+ strokeLinecap: "round",
139
+ strokeLinejoin: "round",
140
+ className,
141
+ children: [
142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "4 7 4 4 20 4 20 7" }),
143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "9", x2: "15", y1: "20", y2: "20" }),
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", x2: "12", y1: "4", y2: "20" })
145
+ ]
146
+ }
147
+ );
148
+ }
149
+ function XIcon({ className, size = 24 }) {
150
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
151
+ "svg",
152
+ {
153
+ xmlns: "http://www.w3.org/2000/svg",
154
+ width: size,
155
+ height: size,
156
+ viewBox: "0 0 24 24",
157
+ fill: "none",
158
+ stroke: "currentColor",
159
+ strokeWidth: "2",
160
+ strokeLinecap: "round",
161
+ strokeLinejoin: "round",
162
+ className,
163
+ children: [
164
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }),
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })
166
+ ]
167
+ }
168
+ );
169
+ }
170
+ function ImagePlusIcon({ className, size = 24 }) {
171
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
172
+ "svg",
173
+ {
174
+ xmlns: "http://www.w3.org/2000/svg",
175
+ width: size,
176
+ height: size,
177
+ viewBox: "0 0 24 24",
178
+ fill: "none",
179
+ stroke: "currentColor",
180
+ strokeWidth: "2",
181
+ strokeLinecap: "round",
182
+ strokeLinejoin: "round",
183
+ className,
184
+ children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }),
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "16", x2: "22", y1: "5", y2: "5" }),
187
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "19", x2: "19", y1: "2", y2: "8" }),
188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "9", cy: "9", r: "2" }),
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
190
+ ]
191
+ }
192
+ );
193
+ }
194
+ function CheckIcon({ className, size = 24 }) {
195
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
196
+ "svg",
197
+ {
198
+ xmlns: "http://www.w3.org/2000/svg",
199
+ width: size,
200
+ height: size,
201
+ viewBox: "0 0 24 24",
202
+ fill: "none",
203
+ stroke: "currentColor",
204
+ strokeWidth: "2",
205
+ strokeLinecap: "round",
206
+ strokeLinejoin: "round",
207
+ className,
208
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "20 6 9 17 4 12" })
209
+ }
210
+ );
211
+ }
212
+ function LoaderIcon({ className, size = 24 }) {
213
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
214
+ "svg",
215
+ {
216
+ xmlns: "http://www.w3.org/2000/svg",
217
+ width: size,
218
+ height: size,
219
+ viewBox: "0 0 24 24",
220
+ fill: "none",
221
+ stroke: "currentColor",
222
+ strokeWidth: "2",
223
+ strokeLinecap: "round",
224
+ strokeLinejoin: "round",
225
+ className,
226
+ style: { animation: "spin 1s linear infinite" },
227
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
228
+ }
229
+ );
230
+ }
231
+ function SquareIcon({ className, size = 24 }) {
232
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
233
+ "svg",
234
+ {
235
+ xmlns: "http://www.w3.org/2000/svg",
236
+ width: size,
237
+ height: size,
238
+ viewBox: "0 0 24 24",
239
+ fill: "currentColor",
240
+ stroke: "currentColor",
241
+ strokeWidth: "2",
242
+ strokeLinecap: "round",
243
+ strokeLinejoin: "round",
244
+ className,
245
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: "14", height: "14", x: "5", y: "5", rx: "2" })
246
+ }
247
+ );
248
+ }
249
+ function AlertCircleIcon({ className, size = 24 }) {
250
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
251
+ "svg",
252
+ {
253
+ xmlns: "http://www.w3.org/2000/svg",
254
+ width: size,
255
+ height: size,
256
+ viewBox: "0 0 24 24",
257
+ fill: "none",
258
+ stroke: "currentColor",
259
+ strokeWidth: "2",
260
+ strokeLinecap: "round",
261
+ strokeLinejoin: "round",
262
+ className,
263
+ children: [
264
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
265
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", x2: "12", y1: "8", y2: "12" }),
266
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })
267
+ ]
268
+ }
269
+ );
270
+ }
271
+
272
+ // src/components/FeedbackTrigger.tsx
273
+ var import_jsx_runtime2 = require("react/jsx-runtime");
274
+ var POSITION_KEY = "feedbackwidget-trigger-position";
275
+ var DEFAULT_OFFSET = 24;
276
+ var BUTTON_SIZE = 52;
277
+ function FeedbackTrigger({
278
+ onClick,
279
+ isOpen,
280
+ position = "bottom-right",
281
+ accentColor = "#BDE0C2",
282
+ tooltip = "Send feedback"
283
+ }) {
284
+ const [coords, setCoords] = (0, import_react.useState)(null);
285
+ const [isDragging, setIsDragging] = (0, import_react.useState)(false);
286
+ const [dragStart, setDragStart] = (0, import_react.useState)(null);
287
+ const [hasMoved, setHasMoved] = (0, import_react.useState)(false);
288
+ const buttonRef = (0, import_react.useRef)(null);
289
+ const getDefaultPosition = (0, import_react.useCallback)(() => {
290
+ const w = typeof window !== "undefined" ? window.innerWidth : 1024;
291
+ const h = typeof window !== "undefined" ? window.innerHeight : 768;
292
+ switch (position) {
293
+ case "top-left":
294
+ return { x: DEFAULT_OFFSET, y: DEFAULT_OFFSET };
295
+ case "top-right":
296
+ return { x: w - BUTTON_SIZE - DEFAULT_OFFSET, y: DEFAULT_OFFSET };
297
+ case "bottom-left":
298
+ return { x: DEFAULT_OFFSET, y: h - BUTTON_SIZE - DEFAULT_OFFSET - 80 };
299
+ case "bottom-right":
300
+ default:
301
+ return { x: w - BUTTON_SIZE - DEFAULT_OFFSET, y: h - BUTTON_SIZE - DEFAULT_OFFSET - 80 };
302
+ }
303
+ }, [position]);
304
+ (0, import_react.useEffect)(() => {
305
+ const saved = localStorage.getItem(POSITION_KEY);
306
+ if (saved) {
307
+ try {
308
+ const parsed = JSON.parse(saved);
309
+ const maxX = window.innerWidth - BUTTON_SIZE - DEFAULT_OFFSET;
310
+ const maxY = window.innerHeight - BUTTON_SIZE - DEFAULT_OFFSET;
311
+ setCoords({
312
+ x: Math.min(Math.max(DEFAULT_OFFSET, parsed.x), maxX),
313
+ y: Math.min(Math.max(DEFAULT_OFFSET, parsed.y), maxY)
314
+ });
315
+ } catch {
316
+ setCoords(getDefaultPosition());
317
+ }
318
+ } else {
319
+ setCoords(getDefaultPosition());
320
+ }
321
+ }, [getDefaultPosition]);
322
+ (0, import_react.useEffect)(() => {
323
+ const handleResize = () => {
324
+ if (coords) {
325
+ const maxX = window.innerWidth - BUTTON_SIZE - DEFAULT_OFFSET;
326
+ const maxY = window.innerHeight - BUTTON_SIZE - DEFAULT_OFFSET;
327
+ const newX = Math.min(coords.x, maxX);
328
+ const newY = Math.min(coords.y, maxY);
329
+ if (newX !== coords.x || newY !== coords.y) {
330
+ setCoords({ x: newX, y: newY });
331
+ }
332
+ }
333
+ };
334
+ window.addEventListener("resize", handleResize);
335
+ return () => window.removeEventListener("resize", handleResize);
336
+ }, [coords]);
337
+ const savePosition = (0, import_react.useCallback)((pos) => {
338
+ localStorage.setItem(POSITION_KEY, JSON.stringify(pos));
339
+ }, []);
340
+ const handleMouseDown = (e) => {
341
+ if (e.button !== 0) return;
342
+ e.preventDefault();
343
+ setIsDragging(true);
344
+ setHasMoved(false);
345
+ setDragStart({ x: e.clientX, y: e.clientY });
346
+ };
347
+ const handleTouchStart = (e) => {
348
+ const touch = e.touches[0];
349
+ setIsDragging(true);
350
+ setHasMoved(false);
351
+ setDragStart({ x: touch.clientX, y: touch.clientY });
352
+ };
353
+ const handleMove = (0, import_react.useCallback)(
354
+ (clientX, clientY) => {
355
+ if (!isDragging || !dragStart || !coords) return;
356
+ const deltaX = clientX - dragStart.x;
357
+ const deltaY = clientY - dragStart.y;
358
+ if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
359
+ setHasMoved(true);
360
+ }
361
+ const maxX = window.innerWidth - BUTTON_SIZE - DEFAULT_OFFSET;
362
+ const maxY = window.innerHeight - BUTTON_SIZE - DEFAULT_OFFSET;
363
+ const newX = Math.min(Math.max(DEFAULT_OFFSET, coords.x + deltaX), maxX);
364
+ const newY = Math.min(Math.max(DEFAULT_OFFSET, coords.y + deltaY), maxY);
365
+ setCoords({ x: newX, y: newY });
366
+ setDragStart({ x: clientX, y: clientY });
367
+ },
368
+ [isDragging, dragStart, coords]
369
+ );
370
+ const handleMouseMove = (0, import_react.useCallback)(
371
+ (e) => handleMove(e.clientX, e.clientY),
372
+ [handleMove]
373
+ );
374
+ const handleTouchMove = (0, import_react.useCallback)(
375
+ (e) => handleMove(e.touches[0].clientX, e.touches[0].clientY),
376
+ [handleMove]
377
+ );
378
+ const handleEnd = (0, import_react.useCallback)(() => {
379
+ if (isDragging && coords) {
380
+ savePosition(coords);
381
+ }
382
+ setIsDragging(false);
383
+ setDragStart(null);
384
+ }, [isDragging, coords, savePosition]);
385
+ (0, import_react.useEffect)(() => {
386
+ if (isDragging) {
387
+ window.addEventListener("mousemove", handleMouseMove);
388
+ window.addEventListener("mouseup", handleEnd);
389
+ window.addEventListener("touchmove", handleTouchMove, { passive: false });
390
+ window.addEventListener("touchend", handleEnd);
391
+ }
392
+ return () => {
393
+ window.removeEventListener("mousemove", handleMouseMove);
394
+ window.removeEventListener("mouseup", handleEnd);
395
+ window.removeEventListener("touchmove", handleTouchMove);
396
+ window.removeEventListener("touchend", handleEnd);
397
+ };
398
+ }, [isDragging, handleMouseMove, handleTouchMove, handleEnd]);
399
+ const handleClick = () => {
400
+ if (!hasMoved) {
401
+ onClick();
402
+ }
403
+ };
404
+ if (!coords || isOpen) return null;
405
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
406
+ "button",
407
+ {
408
+ ref: buttonRef,
409
+ onMouseDown: handleMouseDown,
410
+ onTouchStart: handleTouchStart,
411
+ onClick: handleClick,
412
+ className: cn(
413
+ "fw-trigger",
414
+ isDragging && "fw-trigger--dragging"
415
+ ),
416
+ style: {
417
+ left: coords.x,
418
+ top: coords.y,
419
+ width: BUTTON_SIZE,
420
+ height: BUTTON_SIZE,
421
+ backgroundColor: accentColor
422
+ },
423
+ title: tooltip,
424
+ "aria-label": tooltip,
425
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ZapIcon, { size: 24 })
426
+ }
427
+ );
428
+ }
429
+
430
+ // src/components/FeedbackPanel.tsx
431
+ var React = __toESM(require("react"));
432
+ var import_react3 = require("react");
433
+
434
+ // src/components/VoiceRecorder.tsx
435
+ var import_react2 = require("react");
436
+ var import_jsx_runtime3 = require("react/jsx-runtime");
437
+ var MAX_DURATION_SECONDS = 120;
438
+ function VoiceRecorder({
439
+ onSubmit,
440
+ isSubmitting,
441
+ maxDuration = MAX_DURATION_SECONDS,
442
+ accentColor = "#BDE0C2"
443
+ }) {
444
+ const [isRecording, setIsRecording] = (0, import_react2.useState)(false);
445
+ const [duration, setDuration] = (0, import_react2.useState)(0);
446
+ const [error, setError] = (0, import_react2.useState)(null);
447
+ const [isSupported, setIsSupported] = (0, import_react2.useState)(true);
448
+ const [audioLevel, setAudioLevel] = (0, import_react2.useState)(0);
449
+ const mediaRecorderRef = (0, import_react2.useRef)(null);
450
+ const chunksRef = (0, import_react2.useRef)([]);
451
+ const timerRef = (0, import_react2.useRef)(null);
452
+ const streamRef = (0, import_react2.useRef)(null);
453
+ const analyzerRef = (0, import_react2.useRef)(null);
454
+ const animationRef = (0, import_react2.useRef)(null);
455
+ const durationRef = (0, import_react2.useRef)(0);
456
+ (0, import_react2.useEffect)(() => {
457
+ setIsSupported(isVoiceSupported());
458
+ }, []);
459
+ (0, import_react2.useEffect)(() => {
460
+ return () => {
461
+ stopRecording();
462
+ if (animationRef.current) {
463
+ cancelAnimationFrame(animationRef.current);
464
+ }
465
+ };
466
+ }, []);
467
+ const updateAudioLevel = (0, import_react2.useCallback)(() => {
468
+ if (!analyzerRef.current || !isRecording) return;
469
+ const dataArray = new Uint8Array(analyzerRef.current.frequencyBinCount);
470
+ analyzerRef.current.getByteFrequencyData(dataArray);
471
+ const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
472
+ setAudioLevel(average / 255);
473
+ animationRef.current = requestAnimationFrame(updateAudioLevel);
474
+ }, [isRecording]);
475
+ const startRecording = async () => {
476
+ setError(null);
477
+ chunksRef.current = [];
478
+ try {
479
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
480
+ streamRef.current = stream;
481
+ const audioContext = new AudioContext();
482
+ const source = audioContext.createMediaStreamSource(stream);
483
+ const analyzer = audioContext.createAnalyser();
484
+ analyzer.fftSize = 256;
485
+ source.connect(analyzer);
486
+ analyzerRef.current = analyzer;
487
+ const mimeType = MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : MediaRecorder.isTypeSupported("audio/mp4") ? "audio/mp4" : "audio/ogg";
488
+ const mediaRecorder = new MediaRecorder(stream, { mimeType });
489
+ mediaRecorderRef.current = mediaRecorder;
490
+ mediaRecorder.ondataavailable = (e) => {
491
+ if (e.data.size > 0) {
492
+ chunksRef.current.push(e.data);
493
+ }
494
+ };
495
+ mediaRecorder.onstop = async () => {
496
+ const audioBlob = new Blob(chunksRef.current, { type: mimeType });
497
+ const finalDuration = durationRef.current;
498
+ if (streamRef.current) {
499
+ streamRef.current.getTracks().forEach((track) => track.stop());
500
+ streamRef.current = null;
501
+ }
502
+ if (audioBlob.size > 0 && finalDuration > 0) {
503
+ await onSubmit(audioBlob, finalDuration);
504
+ }
505
+ };
506
+ mediaRecorder.start(1e3);
507
+ setIsRecording(true);
508
+ setDuration(0);
509
+ durationRef.current = 0;
510
+ timerRef.current = setInterval(() => {
511
+ setDuration((prev) => {
512
+ const newDuration = prev + 1;
513
+ durationRef.current = newDuration;
514
+ if (newDuration >= maxDuration) {
515
+ stopRecording();
516
+ }
517
+ return newDuration;
518
+ });
519
+ }, 1e3);
520
+ animationRef.current = requestAnimationFrame(updateAudioLevel);
521
+ } catch (err) {
522
+ console.error("Error starting recording:", err);
523
+ if (err instanceof Error) {
524
+ if (err.name === "NotAllowedError") {
525
+ setError("Microphone access denied. Please allow microphone access.");
526
+ } else if (err.name === "NotFoundError") {
527
+ setError("No microphone found. Please connect a microphone.");
528
+ } else {
529
+ setError("Failed to start recording. Please try typing instead.");
530
+ }
531
+ }
532
+ }
533
+ };
534
+ const stopRecording = () => {
535
+ if (timerRef.current) {
536
+ clearInterval(timerRef.current);
537
+ timerRef.current = null;
538
+ }
539
+ if (animationRef.current) {
540
+ cancelAnimationFrame(animationRef.current);
541
+ animationRef.current = null;
542
+ }
543
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
544
+ mediaRecorderRef.current.stop();
545
+ }
546
+ setIsRecording(false);
547
+ setAudioLevel(0);
548
+ };
549
+ if (!isSupported) {
550
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fw-voice-unsupported", children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fw-voice-error-icon", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertCircleIcon, { size: 32 }) }),
552
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { children: [
553
+ "Voice recording not supported in this browser.",
554
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("br", {}),
555
+ "Please use the Type option instead."
556
+ ] })
557
+ ] });
558
+ }
559
+ if (error) {
560
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fw-voice-error", children: [
561
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fw-voice-error-icon fw-voice-error-icon--error", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertCircleIcon, { size: 32 }) }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: error }),
563
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
564
+ "button",
565
+ {
566
+ onClick: () => setError(null),
567
+ className: "fw-voice-retry-btn",
568
+ children: "Try Again"
569
+ }
570
+ )
571
+ ] });
572
+ }
573
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fw-voice-recorder", children: [
574
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
575
+ "button",
576
+ {
577
+ onClick: isRecording ? stopRecording : startRecording,
578
+ disabled: isSubmitting,
579
+ className: cn(
580
+ "fw-voice-btn",
581
+ isRecording && "fw-voice-btn--recording",
582
+ isSubmitting && "fw-voice-btn--disabled"
583
+ ),
584
+ style: {
585
+ backgroundColor: isRecording ? "#ef4444" : accentColor
586
+ },
587
+ children: [
588
+ isRecording && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
589
+ "div",
590
+ {
591
+ className: "fw-voice-pulse",
592
+ style: {
593
+ transform: `scale(${1 + audioLevel * 0.3})`,
594
+ opacity: 0.5 + audioLevel * 0.5
595
+ }
596
+ }
597
+ ),
598
+ isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LoaderIcon, { size: 40, className: "fw-voice-icon fw-voice-icon--spin" }) : isRecording ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SquareIcon, { size: 40, className: "fw-voice-icon" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MicIcon, { size: 40, className: "fw-voice-icon" })
599
+ ]
600
+ }
601
+ ),
602
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fw-voice-status", children: isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fw-voice-text", children: "Sending feedback..." }) : isRecording ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
603
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fw-voice-timer", children: formatDuration(duration) }),
604
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fw-voice-hint", children: "Tap to stop and send" }),
605
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "fw-voice-limit", children: [
606
+ "Max ",
607
+ Math.floor(maxDuration / 60),
608
+ " minutes"
609
+ ] })
610
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fw-voice-cta", children: "Tap to record" }),
612
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fw-voice-hint", children: "Share your feedback by voice" })
613
+ ] }) }),
614
+ isRecording && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fw-voice-visualizer", children: [...Array(7)].map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
615
+ "div",
616
+ {
617
+ className: "fw-voice-bar",
618
+ style: {
619
+ height: `${Math.max(8, audioLevel * 100 * Math.sin(i + Date.now() / 200) % 32)}px`,
620
+ backgroundColor: "#ef4444"
621
+ }
622
+ },
623
+ i
624
+ )) })
625
+ ] });
626
+ }
627
+
628
+ // src/api/client.ts
629
+ var API_BASE = "https://feedbackwidget-api.vercel.app/api/v1";
630
+ var FeedbackApiClient = class {
631
+ constructor(apiKey, baseUrl) {
632
+ this.apiKey = apiKey;
633
+ this.baseUrl = baseUrl || API_BASE;
634
+ }
635
+ /** Validate API key and get project config */
636
+ async validate() {
637
+ try {
638
+ const response = await fetch(`${this.baseUrl}/validate`, {
639
+ method: "GET",
640
+ headers: {
641
+ Authorization: `Bearer ${this.apiKey}`
642
+ }
643
+ });
644
+ if (!response.ok) {
645
+ const error = await response.json().catch(() => ({}));
646
+ return {
647
+ valid: false,
648
+ error: error.message || `HTTP ${response.status}`
649
+ };
650
+ }
651
+ return await response.json();
652
+ } catch (error) {
653
+ return {
654
+ valid: false,
655
+ error: error instanceof Error ? error.message : "Network error"
656
+ };
657
+ }
658
+ }
659
+ /** Submit feedback */
660
+ async submit(params) {
661
+ const formData = new FormData();
662
+ formData.append("type", params.type);
663
+ formData.append("page_path", params.pagePath);
664
+ formData.append("page_title", params.pageTitle);
665
+ formData.append("browser_info", JSON.stringify(getBrowserInfo()));
666
+ if (params.type === "text" && params.message) {
667
+ formData.append("message", params.message);
668
+ }
669
+ if (params.type === "voice" && params.audioBlob) {
670
+ formData.append("audio", params.audioBlob, "feedback.webm");
671
+ if (params.audioDuration) {
672
+ formData.append("audio_duration", String(Math.round(params.audioDuration)));
673
+ }
674
+ }
675
+ if (params.screenshots && params.screenshots.length > 0) {
676
+ params.screenshots.forEach((file, index) => {
677
+ formData.append(`screenshot_${index}`, file);
678
+ });
679
+ formData.append("screenshot_count", String(params.screenshots.length));
680
+ }
681
+ if (params.user) {
682
+ if (params.user.id) formData.append("user_id", params.user.id);
683
+ if (params.user.email) formData.append("user_email", params.user.email);
684
+ if (params.user.name) formData.append("user_name", params.user.name);
685
+ const { id, email, name, ...rest } = params.user;
686
+ if (Object.keys(rest).length > 0) {
687
+ formData.append("user_metadata", JSON.stringify(rest));
688
+ }
689
+ }
690
+ if (params.metadata) {
691
+ formData.append("custom_metadata", JSON.stringify(params.metadata));
692
+ }
693
+ const response = await fetch(`${this.baseUrl}/feedback`, {
694
+ method: "POST",
695
+ headers: {
696
+ Authorization: `Bearer ${this.apiKey}`
697
+ },
698
+ body: formData
699
+ });
700
+ if (!response.ok) {
701
+ const error = await response.json().catch(() => ({}));
702
+ throw new Error(error.message || error.error || `HTTP ${response.status}`);
703
+ }
704
+ return await response.json();
705
+ }
706
+ };
707
+ function createApiClient(apiKey, baseUrl) {
708
+ return new FeedbackApiClient(apiKey, baseUrl);
709
+ }
710
+
711
+ // src/components/FeedbackPanel.tsx
712
+ var import_jsx_runtime4 = require("react/jsx-runtime");
713
+ function FeedbackPanel({
714
+ isOpen,
715
+ onClose,
716
+ apiKey,
717
+ apiBaseUrl,
718
+ accentColor = "#BDE0C2",
719
+ allowVoice = true,
720
+ allowText = true,
721
+ allowScreenshots = true,
722
+ maxVoiceDuration = 120,
723
+ user,
724
+ metadata,
725
+ labels = {},
726
+ onSubmit,
727
+ onError
728
+ }) {
729
+ const mergedLabels = { ...DEFAULT_LABELS, ...labels };
730
+ const defaultMode = allowVoice ? "voice" : "text";
731
+ const [mode, setMode] = (0, import_react3.useState)(defaultMode);
732
+ const [message, setMessage] = (0, import_react3.useState)("");
733
+ const [screenshots, setScreenshots] = (0, import_react3.useState)([]);
734
+ const [isSubmitting, setIsSubmitting] = (0, import_react3.useState)(false);
735
+ const [isSuccess, setIsSuccess] = (0, import_react3.useState)(false);
736
+ const fileInputRef = (0, import_react3.useRef)(null);
737
+ const apiClient = React.useMemo(
738
+ () => createApiClient(apiKey, apiBaseUrl),
739
+ [apiKey, apiBaseUrl]
740
+ );
741
+ (0, import_react3.useEffect)(() => {
742
+ if (!isOpen) {
743
+ const timer = setTimeout(() => {
744
+ setMode(defaultMode);
745
+ setMessage("");
746
+ setScreenshots([]);
747
+ setIsSuccess(false);
748
+ }, 300);
749
+ return () => clearTimeout(timer);
750
+ }
751
+ }, [isOpen, defaultMode]);
752
+ const handleScreenshotUpload = (e) => {
753
+ const files = Array.from(e.target.files || []);
754
+ if (files.length === 0) return;
755
+ const remaining = 5 - screenshots.length;
756
+ if (remaining <= 0) return;
757
+ const newFiles = files.slice(0, remaining);
758
+ setScreenshots((prev) => [...prev, ...newFiles]);
759
+ if (fileInputRef.current) {
760
+ fileInputRef.current.value = "";
761
+ }
762
+ };
763
+ const removeScreenshot = (index) => {
764
+ setScreenshots((prev) => prev.filter((_, i) => i !== index));
765
+ };
766
+ const getPageInfo = () => ({
767
+ pagePath: typeof window !== "undefined" ? window.location.pathname : "/",
768
+ pageTitle: typeof document !== "undefined" ? document.title : "Unknown"
769
+ });
770
+ const handleVoiceSubmit = async (audioBlob, duration) => {
771
+ await submitFeedback("voice", void 0, audioBlob, duration);
772
+ };
773
+ const handleTextSubmit = async () => {
774
+ if (!message.trim()) return;
775
+ await submitFeedback("text", message.trim());
776
+ };
777
+ const submitFeedback = async (type, text, audioBlob, audioDuration) => {
778
+ setIsSubmitting(true);
779
+ try {
780
+ const { pagePath: pagePath2, pageTitle } = getPageInfo();
781
+ const result = await apiClient.submit({
782
+ type,
783
+ message: text,
784
+ audioBlob,
785
+ audioDuration,
786
+ screenshots,
787
+ pagePath: pagePath2,
788
+ pageTitle,
789
+ user,
790
+ metadata
791
+ });
792
+ setIsSuccess(true);
793
+ if (onSubmit) {
794
+ onSubmit({
795
+ id: result.id,
796
+ type,
797
+ message: text,
798
+ pagePath: pagePath2,
799
+ pageTitle,
800
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
801
+ });
802
+ }
803
+ setTimeout(() => {
804
+ onClose();
805
+ }, 1500);
806
+ } catch (error) {
807
+ console.error("Feedback submission error:", error);
808
+ if (onError && error instanceof Error) {
809
+ onError(error);
810
+ }
811
+ } finally {
812
+ setIsSubmitting(false);
813
+ }
814
+ };
815
+ const { pagePath } = getPageInfo();
816
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
817
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "fw-backdrop", onClick: onClose }),
818
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
819
+ "div",
820
+ {
821
+ className: cn(
822
+ "fw-panel",
823
+ isOpen ? "fw-panel--open" : "fw-panel--closed"
824
+ ),
825
+ children: [
826
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-panel-header", children: [
827
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { className: "fw-panel-title", children: mergedLabels.panelTitle }),
828
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: onClose, className: "fw-panel-close", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(XIcon, { size: 20 }) })
829
+ ] }),
830
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "fw-panel-content", children: isSuccess ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-success", children: [
831
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
832
+ "div",
833
+ {
834
+ className: "fw-success-icon",
835
+ style: { backgroundColor: accentColor },
836
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CheckIcon, { size: 40 })
837
+ }
838
+ ),
839
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "fw-success-title", children: "Thanks!" }),
840
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "fw-success-text", children: mergedLabels.successMessage })
841
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-context", children: [
843
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "fw-context-label", children: "Page:" }),
844
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "fw-context-value", children: pagePath })
845
+ ] }),
846
+ allowVoice && allowText && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-mode-selector", children: [
847
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
848
+ "button",
849
+ {
850
+ onClick: () => setMode("voice"),
851
+ disabled: isSubmitting,
852
+ className: cn(
853
+ "fw-mode-btn",
854
+ mode === "voice" && "fw-mode-btn--active"
855
+ ),
856
+ style: mode === "voice" ? { backgroundColor: accentColor } : {},
857
+ children: [
858
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MicIcon, { size: 16 }),
859
+ mergedLabels.voiceButton
860
+ ]
861
+ }
862
+ ),
863
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
864
+ "button",
865
+ {
866
+ onClick: () => setMode("text"),
867
+ disabled: isSubmitting,
868
+ className: cn(
869
+ "fw-mode-btn",
870
+ mode === "text" && "fw-mode-btn--active"
871
+ ),
872
+ style: mode === "text" ? { backgroundColor: accentColor } : {},
873
+ children: [
874
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypeIcon, { size: 16 }),
875
+ mergedLabels.textButton
876
+ ]
877
+ }
878
+ )
879
+ ] }),
880
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "fw-input-area", children: mode === "voice" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
881
+ VoiceRecorder,
882
+ {
883
+ onSubmit: handleVoiceSubmit,
884
+ isSubmitting,
885
+ maxDuration: maxVoiceDuration,
886
+ accentColor
887
+ }
888
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-text-input", children: [
889
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
890
+ "textarea",
891
+ {
892
+ value: message,
893
+ onChange: (e) => setMessage(e.target.value),
894
+ placeholder: mergedLabels.placeholder,
895
+ className: "fw-textarea",
896
+ disabled: isSubmitting
897
+ }
898
+ ),
899
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
900
+ "button",
901
+ {
902
+ onClick: handleTextSubmit,
903
+ disabled: !message.trim() || isSubmitting,
904
+ className: "fw-submit-btn",
905
+ children: isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
906
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LoaderIcon, { size: 16, className: "fw-icon-spin" }),
907
+ "Sending..."
908
+ ] }) : mergedLabels.submitButton
909
+ }
910
+ )
911
+ ] }) }),
912
+ allowScreenshots && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-screenshots", children: [
913
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-screenshots-header", children: [
914
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "fw-screenshots-label", children: "Screenshots" }),
915
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
916
+ "button",
917
+ {
918
+ onClick: () => fileInputRef.current?.click(),
919
+ disabled: screenshots.length >= 5 || isSubmitting,
920
+ className: "fw-screenshots-add",
921
+ children: [
922
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ImagePlusIcon, { size: 16 }),
923
+ "Add"
924
+ ]
925
+ }
926
+ ),
927
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
928
+ "input",
929
+ {
930
+ ref: fileInputRef,
931
+ type: "file",
932
+ accept: "image/*",
933
+ multiple: true,
934
+ onChange: handleScreenshotUpload,
935
+ style: { display: "none" }
936
+ }
937
+ )
938
+ ] }),
939
+ screenshots.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "fw-screenshots-grid", children: screenshots.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "fw-screenshot", children: [
940
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
941
+ "img",
942
+ {
943
+ src: URL.createObjectURL(file),
944
+ alt: `Screenshot ${index + 1}`
945
+ }
946
+ ),
947
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
948
+ "button",
949
+ {
950
+ onClick: () => removeScreenshot(index),
951
+ className: "fw-screenshot-remove",
952
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(XIcon, { size: 12 })
953
+ }
954
+ )
955
+ ] }, index)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "fw-screenshots-hint", children: "Optional: Add screenshots to illustrate your feedback" })
956
+ ] })
957
+ ] }) })
958
+ ]
959
+ }
960
+ )
961
+ ] });
962
+ }
963
+
964
+ // src/components/FeedbackWidget.tsx
965
+ var import_jsx_runtime5 = require("react/jsx-runtime");
966
+ function FeedbackWidget({
967
+ apiKey,
968
+ position = "bottom-right",
969
+ theme = "light",
970
+ accentColor = "#BDE0C2",
971
+ allowVoice = true,
972
+ allowText = true,
973
+ allowScreenshots = true,
974
+ maxVoiceDuration = 120,
975
+ user,
976
+ metadata,
977
+ onOpen,
978
+ onClose,
979
+ onSubmit,
980
+ onError,
981
+ triggerComponent,
982
+ labels
983
+ }) {
984
+ const [isOpen, setIsOpen] = (0, import_react4.useState)(false);
985
+ const [mounted, setMounted] = (0, import_react4.useState)(false);
986
+ (0, import_react4.useEffect)(() => {
987
+ setMounted(true);
988
+ }, []);
989
+ const handleOpen = () => {
990
+ setIsOpen(true);
991
+ onOpen?.();
992
+ };
993
+ const handleClose = () => {
994
+ setIsOpen(false);
995
+ onClose?.();
996
+ };
997
+ if (!mounted) return null;
998
+ const apiBaseUrl = typeof window !== "undefined" ? window.__FEEDBACK_API_URL__ : void 0;
999
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(FeedbackStyles, { theme }),
1001
+ triggerComponent ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { onClick: handleOpen, children: triggerComponent }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1002
+ FeedbackTrigger,
1003
+ {
1004
+ onClick: handleOpen,
1005
+ isOpen,
1006
+ position,
1007
+ accentColor,
1008
+ tooltip: labels?.triggerTooltip
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1012
+ FeedbackPanel,
1013
+ {
1014
+ isOpen,
1015
+ onClose: handleClose,
1016
+ apiKey,
1017
+ apiBaseUrl,
1018
+ accentColor,
1019
+ allowVoice,
1020
+ allowText,
1021
+ allowScreenshots,
1022
+ maxVoiceDuration,
1023
+ user,
1024
+ metadata,
1025
+ labels,
1026
+ onSubmit,
1027
+ onError
1028
+ }
1029
+ )
1030
+ ] });
1031
+ }
1032
+ function FeedbackStyles({ theme }) {
1033
+ const styles = `
1034
+ @keyframes fw-spin {
1035
+ from { transform: rotate(0deg); }
1036
+ to { transform: rotate(360deg); }
1037
+ }
1038
+
1039
+ .fw-trigger {
1040
+ position: fixed;
1041
+ z-index: 9998;
1042
+ display: flex;
1043
+ align-items: center;
1044
+ justify-content: center;
1045
+ border-radius: 50%;
1046
+ border: none;
1047
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1048
+ cursor: pointer;
1049
+ transition: transform 0.15s, box-shadow 0.15s;
1050
+ user-select: none;
1051
+ touch-action: none;
1052
+ color: #0a0a0a;
1053
+ }
1054
+ .fw-trigger:hover {
1055
+ transform: scale(1.05);
1056
+ box-shadow: 0 6px 20px rgba(0,0,0,0.2);
1057
+ }
1058
+ .fw-trigger:active {
1059
+ transform: scale(0.95);
1060
+ }
1061
+ .fw-trigger--dragging {
1062
+ transform: scale(1.1);
1063
+ box-shadow: 0 8px 24px rgba(0,0,0,0.25);
1064
+ cursor: grabbing;
1065
+ }
1066
+
1067
+ .fw-backdrop {
1068
+ position: fixed;
1069
+ inset: 0;
1070
+ background: rgba(0,0,0,0.3);
1071
+ z-index: 9998;
1072
+ }
1073
+
1074
+ .fw-panel {
1075
+ position: fixed;
1076
+ top: 0;
1077
+ right: 0;
1078
+ height: 100%;
1079
+ width: 100%;
1080
+ max-width: 400px;
1081
+ background: ${theme === "dark" ? "#1a1a1a" : "#ffffff"};
1082
+ z-index: 9999;
1083
+ box-shadow: -4px 0 24px rgba(0,0,0,0.15);
1084
+ border-left: 1px solid ${theme === "dark" ? "#333" : "#e5e5e5"};
1085
+ transform: translateX(100%);
1086
+ transition: transform 0.3s ease-out;
1087
+ }
1088
+ .fw-panel--open {
1089
+ transform: translateX(0);
1090
+ }
1091
+ .fw-panel--closed {
1092
+ transform: translateX(100%);
1093
+ }
1094
+
1095
+ .fw-panel-header {
1096
+ display: flex;
1097
+ align-items: center;
1098
+ justify-content: space-between;
1099
+ padding: 16px 20px;
1100
+ border-bottom: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1101
+ }
1102
+ .fw-panel-title {
1103
+ margin: 0;
1104
+ font-size: 18px;
1105
+ font-weight: 600;
1106
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1107
+ }
1108
+ .fw-panel-close {
1109
+ padding: 8px;
1110
+ background: none;
1111
+ border: none;
1112
+ cursor: pointer;
1113
+ border-radius: 8px;
1114
+ color: ${theme === "dark" ? "#999" : "#666"};
1115
+ transition: background 0.15s;
1116
+ }
1117
+ .fw-panel-close:hover {
1118
+ background: ${theme === "dark" ? "#333" : "#f5f5f5"};
1119
+ }
1120
+
1121
+ .fw-panel-content {
1122
+ display: flex;
1123
+ flex-direction: column;
1124
+ height: calc(100% - 65px);
1125
+ overflow-y: auto;
1126
+ }
1127
+
1128
+ .fw-context {
1129
+ display: flex;
1130
+ align-items: center;
1131
+ gap: 8px;
1132
+ padding: 12px 20px;
1133
+ background: ${theme === "dark" ? "#222" : "#f9f9f9"};
1134
+ border-bottom: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1135
+ font-size: 14px;
1136
+ }
1137
+ .fw-context-label {
1138
+ font-weight: 500;
1139
+ color: ${theme === "dark" ? "#999" : "#666"};
1140
+ }
1141
+ .fw-context-value {
1142
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1143
+ overflow: hidden;
1144
+ text-overflow: ellipsis;
1145
+ white-space: nowrap;
1146
+ }
1147
+
1148
+ .fw-mode-selector {
1149
+ display: flex;
1150
+ gap: 8px;
1151
+ padding: 16px 20px;
1152
+ border-bottom: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1153
+ }
1154
+ .fw-mode-btn {
1155
+ flex: 1;
1156
+ display: flex;
1157
+ align-items: center;
1158
+ justify-content: center;
1159
+ gap: 8px;
1160
+ padding: 10px 16px;
1161
+ border-radius: 8px;
1162
+ border: none;
1163
+ font-size: 14px;
1164
+ font-weight: 500;
1165
+ cursor: pointer;
1166
+ transition: background 0.15s;
1167
+ background: ${theme === "dark" ? "#333" : "#f0f0f0"};
1168
+ color: ${theme === "dark" ? "#ccc" : "#666"};
1169
+ }
1170
+ .fw-mode-btn:hover:not(:disabled) {
1171
+ background: ${theme === "dark" ? "#444" : "#e5e5e5"};
1172
+ }
1173
+ .fw-mode-btn--active {
1174
+ color: #0a0a0a;
1175
+ }
1176
+ .fw-mode-btn:disabled {
1177
+ opacity: 0.5;
1178
+ cursor: not-allowed;
1179
+ }
1180
+
1181
+ .fw-input-area {
1182
+ flex: 1;
1183
+ padding: 20px;
1184
+ }
1185
+
1186
+ .fw-text-input {
1187
+ display: flex;
1188
+ flex-direction: column;
1189
+ gap: 16px;
1190
+ }
1191
+ .fw-textarea {
1192
+ width: 100%;
1193
+ min-height: 150px;
1194
+ padding: 12px;
1195
+ border: 1px solid ${theme === "dark" ? "#444" : "#e5e5e5"};
1196
+ border-radius: 8px;
1197
+ font-size: 14px;
1198
+ font-family: inherit;
1199
+ resize: none;
1200
+ background: ${theme === "dark" ? "#222" : "#fff"};
1201
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1202
+ }
1203
+ .fw-textarea:focus {
1204
+ outline: none;
1205
+ border-color: #BDE0C2;
1206
+ }
1207
+ .fw-textarea::placeholder {
1208
+ color: ${theme === "dark" ? "#666" : "#999"};
1209
+ }
1210
+ .fw-submit-btn {
1211
+ display: flex;
1212
+ align-items: center;
1213
+ justify-content: center;
1214
+ gap: 8px;
1215
+ width: 100%;
1216
+ padding: 12px 16px;
1217
+ background: #0a0a0a;
1218
+ color: #fff;
1219
+ border: none;
1220
+ border-radius: 8px;
1221
+ font-size: 14px;
1222
+ font-weight: 500;
1223
+ cursor: pointer;
1224
+ transition: background 0.15s;
1225
+ }
1226
+ .fw-submit-btn:hover:not(:disabled) {
1227
+ background: #1a1a1a;
1228
+ }
1229
+ .fw-submit-btn:disabled {
1230
+ opacity: 0.5;
1231
+ cursor: not-allowed;
1232
+ }
1233
+
1234
+ .fw-screenshots {
1235
+ padding: 16px 20px;
1236
+ border-top: 1px solid ${theme === "dark" ? "#333" : "#f0f0f0"};
1237
+ }
1238
+ .fw-screenshots-header {
1239
+ display: flex;
1240
+ align-items: center;
1241
+ justify-content: space-between;
1242
+ margin-bottom: 12px;
1243
+ }
1244
+ .fw-screenshots-label {
1245
+ font-size: 14px;
1246
+ font-weight: 500;
1247
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1248
+ }
1249
+ .fw-screenshots-add {
1250
+ display: flex;
1251
+ align-items: center;
1252
+ gap: 6px;
1253
+ padding: 6px 12px;
1254
+ background: ${theme === "dark" ? "#333" : "#f0f0f0"};
1255
+ border: none;
1256
+ border-radius: 6px;
1257
+ font-size: 13px;
1258
+ cursor: pointer;
1259
+ color: ${theme === "dark" ? "#ccc" : "#666"};
1260
+ transition: background 0.15s;
1261
+ }
1262
+ .fw-screenshots-add:hover:not(:disabled) {
1263
+ background: ${theme === "dark" ? "#444" : "#e5e5e5"};
1264
+ }
1265
+ .fw-screenshots-add:disabled {
1266
+ opacity: 0.5;
1267
+ cursor: not-allowed;
1268
+ }
1269
+ .fw-screenshots-grid {
1270
+ display: flex;
1271
+ flex-wrap: wrap;
1272
+ gap: 8px;
1273
+ }
1274
+ .fw-screenshot {
1275
+ position: relative;
1276
+ }
1277
+ .fw-screenshot img {
1278
+ width: 64px;
1279
+ height: 64px;
1280
+ object-fit: cover;
1281
+ border-radius: 8px;
1282
+ border: 1px solid ${theme === "dark" ? "#444" : "#e5e5e5"};
1283
+ }
1284
+ .fw-screenshot-remove {
1285
+ position: absolute;
1286
+ top: -6px;
1287
+ right: -6px;
1288
+ width: 20px;
1289
+ height: 20px;
1290
+ display: flex;
1291
+ align-items: center;
1292
+ justify-content: center;
1293
+ background: #ef4444;
1294
+ color: #fff;
1295
+ border: none;
1296
+ border-radius: 50%;
1297
+ cursor: pointer;
1298
+ opacity: 0;
1299
+ transition: opacity 0.15s;
1300
+ }
1301
+ .fw-screenshot:hover .fw-screenshot-remove {
1302
+ opacity: 1;
1303
+ }
1304
+ .fw-screenshots-hint {
1305
+ margin: 0;
1306
+ font-size: 13px;
1307
+ color: ${theme === "dark" ? "#666" : "#999"};
1308
+ }
1309
+
1310
+ .fw-success {
1311
+ flex: 1;
1312
+ display: flex;
1313
+ flex-direction: column;
1314
+ align-items: center;
1315
+ justify-content: center;
1316
+ padding: 32px;
1317
+ }
1318
+ .fw-success-icon {
1319
+ width: 80px;
1320
+ height: 80px;
1321
+ border-radius: 50%;
1322
+ display: flex;
1323
+ align-items: center;
1324
+ justify-content: center;
1325
+ margin-bottom: 24px;
1326
+ color: #0a0a0a;
1327
+ }
1328
+ .fw-success-title {
1329
+ margin: 0 0 8px 0;
1330
+ font-size: 20px;
1331
+ font-weight: 600;
1332
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1333
+ }
1334
+ .fw-success-text {
1335
+ margin: 0;
1336
+ color: ${theme === "dark" ? "#999" : "#666"};
1337
+ }
1338
+
1339
+ /* Voice Recorder */
1340
+ .fw-voice-recorder {
1341
+ display: flex;
1342
+ flex-direction: column;
1343
+ align-items: center;
1344
+ justify-content: center;
1345
+ padding: 32px 0;
1346
+ }
1347
+ .fw-voice-btn {
1348
+ position: relative;
1349
+ width: 96px;
1350
+ height: 96px;
1351
+ border-radius: 50%;
1352
+ border: none;
1353
+ display: flex;
1354
+ align-items: center;
1355
+ justify-content: center;
1356
+ cursor: pointer;
1357
+ transition: transform 0.15s, box-shadow 0.15s;
1358
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1359
+ color: #0a0a0a;
1360
+ }
1361
+ .fw-voice-btn:hover:not(:disabled) {
1362
+ transform: scale(1.05);
1363
+ box-shadow: 0 6px 20px rgba(0,0,0,0.2);
1364
+ }
1365
+ .fw-voice-btn:active:not(:disabled) {
1366
+ transform: scale(0.95);
1367
+ }
1368
+ .fw-voice-btn--recording {
1369
+ box-shadow: 0 4px 20px rgba(239,68,68,0.4);
1370
+ }
1371
+ .fw-voice-btn--recording:hover {
1372
+ box-shadow: 0 6px 24px rgba(239,68,68,0.5);
1373
+ }
1374
+ .fw-voice-btn--disabled {
1375
+ opacity: 0.5;
1376
+ cursor: not-allowed;
1377
+ }
1378
+ .fw-voice-pulse {
1379
+ position: absolute;
1380
+ inset: 0;
1381
+ border-radius: 50%;
1382
+ border: 4px solid rgba(239,68,68,0.3);
1383
+ animation: fw-pulse 1s infinite;
1384
+ }
1385
+ @keyframes fw-pulse {
1386
+ 0%, 100% { opacity: 0.5; }
1387
+ 50% { opacity: 1; }
1388
+ }
1389
+ .fw-voice-icon {
1390
+ color: #fff;
1391
+ }
1392
+ .fw-voice-icon--spin {
1393
+ animation: fw-spin 1s linear infinite;
1394
+ }
1395
+ .fw-voice-status {
1396
+ margin-top: 24px;
1397
+ text-align: center;
1398
+ }
1399
+ .fw-voice-timer {
1400
+ font-size: 32px;
1401
+ font-family: monospace;
1402
+ font-weight: 600;
1403
+ color: ${theme === "dark" ? "#fff" : "#0a0a0a"};
1404
+ margin: 0;
1405
+ }
1406
+ .fw-voice-cta {
1407
+ font-size: 16px;
1408
+ font-weight: 500;
1409
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1410
+ margin: 0;
1411
+ }
1412
+ .fw-voice-hint {
1413
+ font-size: 14px;
1414
+ color: ${theme === "dark" ? "#666" : "#999"};
1415
+ margin: 4px 0 0 0;
1416
+ }
1417
+ .fw-voice-limit {
1418
+ font-size: 12px;
1419
+ color: ${theme === "dark" ? "#555" : "#bbb"};
1420
+ margin: 8px 0 0 0;
1421
+ }
1422
+ .fw-voice-text {
1423
+ color: ${theme === "dark" ? "#999" : "#666"};
1424
+ margin: 0;
1425
+ }
1426
+ .fw-voice-visualizer {
1427
+ display: flex;
1428
+ align-items: flex-end;
1429
+ justify-content: center;
1430
+ gap: 4px;
1431
+ height: 32px;
1432
+ margin-top: 24px;
1433
+ }
1434
+ .fw-voice-bar {
1435
+ width: 6px;
1436
+ border-radius: 3px;
1437
+ transition: height 0.075s;
1438
+ }
1439
+ .fw-voice-unsupported,
1440
+ .fw-voice-error {
1441
+ display: flex;
1442
+ flex-direction: column;
1443
+ align-items: center;
1444
+ justify-content: center;
1445
+ padding: 32px;
1446
+ text-align: center;
1447
+ }
1448
+ .fw-voice-error-icon {
1449
+ width: 64px;
1450
+ height: 64px;
1451
+ border-radius: 50%;
1452
+ background: ${theme === "dark" ? "#333" : "#f5f5f5"};
1453
+ display: flex;
1454
+ align-items: center;
1455
+ justify-content: center;
1456
+ margin-bottom: 16px;
1457
+ color: ${theme === "dark" ? "#666" : "#999"};
1458
+ }
1459
+ .fw-voice-error-icon--error {
1460
+ background: #fef2f2;
1461
+ color: #ef4444;
1462
+ }
1463
+ .fw-voice-unsupported p,
1464
+ .fw-voice-error p {
1465
+ margin: 0;
1466
+ font-size: 14px;
1467
+ color: ${theme === "dark" ? "#999" : "#666"};
1468
+ }
1469
+ .fw-voice-retry-btn {
1470
+ margin-top: 16px;
1471
+ padding: 8px 16px;
1472
+ background: ${theme === "dark" ? "#333" : "#f0f0f0"};
1473
+ border: none;
1474
+ border-radius: 6px;
1475
+ font-size: 14px;
1476
+ cursor: pointer;
1477
+ color: ${theme === "dark" ? "#ccc" : "#333"};
1478
+ }
1479
+ .fw-voice-retry-btn:hover {
1480
+ background: ${theme === "dark" ? "#444" : "#e5e5e5"};
1481
+ }
1482
+
1483
+ .fw-icon-spin {
1484
+ animation: fw-spin 1s linear infinite;
1485
+ }
1486
+ `;
1487
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { dangerouslySetInnerHTML: { __html: styles } });
1488
+ }
1489
+
1490
+ // src/auto.tsx
1491
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1492
+ function AutoFeedbackWidget() {
1493
+ const [mounted, setMounted] = (0, import_react5.useState)(false);
1494
+ (0, import_react5.useEffect)(() => {
1495
+ setMounted(true);
1496
+ }, []);
1497
+ const apiKey = typeof process !== "undefined" && (process.env?.NEXT_PUBLIC_FEEDBACKWIDGET_API_KEY || process.env?.VITE_FEEDBACKWIDGET_API_KEY || process.env?.REACT_APP_FEEDBACKWIDGET_API_KEY);
1498
+ if (!mounted || !apiKey) {
1499
+ return null;
1500
+ }
1501
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FeedbackWidget, { apiKey });
1502
+ }
1503
+ if (typeof window !== "undefined") {
1504
+ const init = () => {
1505
+ const scriptTag = document.querySelector(
1506
+ "script[data-feedbackwidget-key]"
1507
+ );
1508
+ const apiKey = window.FEEDBACKWIDGET_API_KEY || scriptTag?.dataset.feedbackwidgetKey;
1509
+ if (apiKey) {
1510
+ const container = document.createElement("div");
1511
+ container.id = "feedbackwidget-auto";
1512
+ document.body.appendChild(container);
1513
+ import("react-dom/client").then(({ createRoot }) => {
1514
+ const root = createRoot(container);
1515
+ root.render(/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FeedbackWidget, { apiKey }));
1516
+ });
1517
+ }
1518
+ };
1519
+ if (document.readyState === "loading") {
1520
+ document.addEventListener("DOMContentLoaded", init);
1521
+ } else {
1522
+ init();
1523
+ }
1524
+ }
1525
+ var auto_default = AutoFeedbackWidget;
1526
+ // Annotate the CommonJS export names for ESM import in node:
1527
+ 0 && (module.exports = {
1528
+ AutoFeedbackWidget
1529
+ });