@stephenov/feedbackwidget 2.0.0 → 2.0.2

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