@stephenov/feedbackwidget 0.1.0

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