@intentai/react 2.2.0 → 2.3.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 CHANGED
@@ -1,2129 +1,2 @@
1
- "use strict";
2
- "use client";
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/index.tsx
22
- var index_exports = {};
23
- __export(index_exports, {
24
- FeedbackButton: () => FeedbackButton,
25
- IntentAIProvider: () => IntentAIProvider,
26
- IntentAIWidget: () => IntentAIWidget,
27
- PremiumVoiceWidget: () => PremiumVoiceWidget,
28
- default: () => index_default,
29
- useIdentify: () => useIdentify,
30
- useIntentAI: () => useIntentAI,
31
- useSetMetadata: () => useSetMetadata,
32
- useWidgetControls: () => useWidgetControls
33
- });
34
- module.exports = __toCommonJS(index_exports);
35
- var import_react4 = require("react");
36
-
37
- // src/components/PremiumVoiceWidget.tsx
38
- var import_react = require("react");
39
- var import_framer_motion = require("framer-motion");
40
- var import_jsx_runtime = require("react/jsx-runtime");
41
- var WidgetContext = (0, import_react.createContext)(null);
42
- var useWidget = () => {
43
- const context = (0, import_react.useContext)(WidgetContext);
44
- if (!context) throw new Error("useWidget must be used within WidgetProvider");
45
- return context;
46
- };
47
- var useReducedMotion = () => {
48
- const [reducedMotion, setReducedMotion] = (0, import_react.useState)(false);
49
- (0, import_react.useEffect)(() => {
50
- const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
51
- setReducedMotion(mediaQuery.matches);
52
- const handler = (e) => setReducedMotion(e.matches);
53
- mediaQuery.addEventListener("change", handler);
54
- return () => mediaQuery.removeEventListener("change", handler);
55
- }, []);
56
- return reducedMotion;
57
- };
58
- var useScrollAwareness = () => {
59
- const [scrollState, setScrollState] = (0, import_react.useState)("idle");
60
- const scrollTimeout = (0, import_react.useRef)(null);
61
- (0, import_react.useEffect)(() => {
62
- const handleScroll = () => {
63
- setScrollState("scrolling");
64
- if (scrollTimeout.current) {
65
- clearTimeout(scrollTimeout.current);
66
- }
67
- scrollTimeout.current = setTimeout(() => {
68
- setScrollState("stopped");
69
- setTimeout(() => setScrollState("idle"), 300);
70
- }, 150);
71
- };
72
- window.addEventListener("scroll", handleScroll, { passive: true });
73
- return () => window.removeEventListener("scroll", handleScroll);
74
- }, []);
75
- return scrollState;
76
- };
77
- var useFrustrationDetection = () => {
78
- const [frustrationLevel, setFrustrationLevel] = (0, import_react.useState)(0);
79
- const clickTimestamps = (0, import_react.useRef)([]);
80
- const signals = (0, import_react.useRef)([]);
81
- (0, import_react.useEffect)(() => {
82
- const handleClick = () => {
83
- const now = Date.now();
84
- clickTimestamps.current.push(now);
85
- clickTimestamps.current = clickTimestamps.current.filter((t) => now - t < 500);
86
- if (clickTimestamps.current.length >= 4) {
87
- signals.current.push({ weight: 3, timestamp: now });
88
- clickTimestamps.current = [];
89
- }
90
- };
91
- const handleFormError = () => {
92
- signals.current.push({ weight: 2, timestamp: Date.now() });
93
- };
94
- document.addEventListener("click", handleClick);
95
- document.addEventListener("invalid", handleFormError, true);
96
- const decayInterval = setInterval(() => {
97
- const now = Date.now();
98
- signals.current = signals.current.filter((s) => now - s.timestamp < 3e4);
99
- const totalWeight = signals.current.reduce((sum, s) => sum + s.weight, 0);
100
- setFrustrationLevel(Math.min(10, totalWeight));
101
- }, 1e3);
102
- return () => {
103
- document.removeEventListener("click", handleClick);
104
- document.removeEventListener("invalid", handleFormError, true);
105
- clearInterval(decayInterval);
106
- };
107
- }, []);
108
- return frustrationLevel;
109
- };
110
- var useTimeOnPage = () => {
111
- const [timeOnPage, setTimeOnPage] = (0, import_react.useState)(0);
112
- (0, import_react.useEffect)(() => {
113
- const interval = setInterval(() => {
114
- setTimeOnPage((prev) => prev + 1);
115
- }, 1e3);
116
- return () => clearInterval(interval);
117
- }, []);
118
- return timeOnPage;
119
- };
120
- var useAudioLevel = (isRecording) => {
121
- const [audioLevel, setAudioLevel] = (0, import_react.useState)(0);
122
- const analyserRef = (0, import_react.useRef)(null);
123
- const animationRef = (0, import_react.useRef)();
124
- const streamRef = (0, import_react.useRef)(null);
125
- const audioContextRef = (0, import_react.useRef)(null);
126
- (0, import_react.useEffect)(() => {
127
- if (!isRecording) {
128
- setAudioLevel(0);
129
- if (animationRef.current) {
130
- cancelAnimationFrame(animationRef.current);
131
- animationRef.current = void 0;
132
- }
133
- if (streamRef.current) {
134
- streamRef.current.getTracks().forEach((track) => track.stop());
135
- streamRef.current = null;
136
- }
137
- if (audioContextRef.current && audioContextRef.current.state !== "closed") {
138
- audioContextRef.current.close().catch(() => {
139
- });
140
- audioContextRef.current = null;
141
- }
142
- analyserRef.current = null;
143
- return;
144
- }
145
- const initAudio = async () => {
146
- try {
147
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
148
- streamRef.current = stream;
149
- const audioContext = new AudioContext();
150
- audioContextRef.current = audioContext;
151
- const source = audioContext.createMediaStreamSource(stream);
152
- const analyser = audioContext.createAnalyser();
153
- analyser.fftSize = 256;
154
- source.connect(analyser);
155
- analyserRef.current = analyser;
156
- const dataArray = new Uint8Array(analyser.frequencyBinCount);
157
- const updateLevel = () => {
158
- if (!analyserRef.current) return;
159
- analyserRef.current.getByteFrequencyData(dataArray);
160
- const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
161
- setAudioLevel(average / 255);
162
- animationRef.current = requestAnimationFrame(updateLevel);
163
- };
164
- updateLevel();
165
- } catch (error) {
166
- console.error("Failed to access microphone:", error);
167
- }
168
- };
169
- initAudio();
170
- return () => {
171
- if (animationRef.current) {
172
- cancelAnimationFrame(animationRef.current);
173
- }
174
- if (streamRef.current) {
175
- streamRef.current.getTracks().forEach((track) => track.stop());
176
- }
177
- if (audioContextRef.current && audioContextRef.current.state !== "closed") {
178
- audioContextRef.current.close().catch(() => {
179
- });
180
- }
181
- };
182
- }, [isRecording]);
183
- return audioLevel;
184
- };
185
- var springPresets = {
186
- snappy: { type: "spring", stiffness: 500, damping: 30 },
187
- bouncy: { type: "spring", stiffness: 400, damping: 15 },
188
- smooth: { type: "spring", stiffness: 300, damping: 30 },
189
- gentle: { type: "spring", stiffness: 200, damping: 25 }
190
- };
191
- var triggerVariants = {
192
- idle: {
193
- scale: 1,
194
- opacity: 1
195
- },
196
- hover: {
197
- scale: 1.08,
198
- transition: springPresets.bouncy
199
- },
200
- tap: {
201
- scale: 0.95,
202
- transition: springPresets.snappy
203
- },
204
- scrolling: {
205
- scale: 0.85,
206
- opacity: 0.3,
207
- transition: { duration: 0.2 }
208
- },
209
- hidden: {
210
- scale: 0,
211
- opacity: 0,
212
- transition: springPresets.smooth
213
- }
214
- };
215
- var containerVariants = {
216
- trigger: {
217
- width: 56,
218
- height: 56,
219
- borderRadius: 28,
220
- transition: springPresets.smooth
221
- },
222
- expanded: {
223
- width: 360,
224
- height: "auto",
225
- borderRadius: 20,
226
- transition: {
227
- ...springPresets.smooth,
228
- staggerChildren: 0.05,
229
- delayChildren: 0.15
230
- }
231
- }
232
- };
233
- var contentVariants = {
234
- hidden: {
235
- opacity: 0,
236
- y: 20,
237
- scale: 0.95
238
- },
239
- visible: {
240
- opacity: 1,
241
- y: 0,
242
- scale: 1,
243
- transition: springPresets.smooth
244
- },
245
- exit: {
246
- opacity: 0,
247
- y: -10,
248
- scale: 0.95,
249
- transition: { duration: 0.2 }
250
- }
251
- };
252
- var BreathingRing = ({ isActive }) => {
253
- const { reducedMotion } = useWidget();
254
- if (reducedMotion) return null;
255
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
256
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
257
- import_framer_motion.motion.div,
258
- {
259
- animate: {
260
- scale: isActive ? [1, 1.3, 1] : 1,
261
- opacity: isActive ? [0.2, 0.4, 0.2] : 0.2
262
- },
263
- transition: {
264
- duration: 2.5,
265
- repeat: Infinity,
266
- ease: "easeInOut"
267
- },
268
- style: {
269
- position: "absolute",
270
- inset: -16,
271
- borderRadius: "50%",
272
- background: "radial-gradient(circle, var(--widget-primary) 0%, transparent 70%)",
273
- pointerEvents: "none",
274
- filter: "blur(8px)"
275
- }
276
- }
277
- ),
278
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
279
- import_framer_motion.motion.div,
280
- {
281
- className: "fiq-breathing-ring",
282
- animate: {
283
- scale: isActive ? [1, 1.08, 1] : 1,
284
- opacity: isActive ? [0.6, 1, 0.6] : 0.6
285
- },
286
- transition: {
287
- duration: 2,
288
- repeat: Infinity,
289
- ease: "easeInOut"
290
- },
291
- style: {
292
- position: "absolute",
293
- inset: -4,
294
- borderRadius: "50%",
295
- border: "2px solid var(--widget-primary)",
296
- pointerEvents: "none",
297
- opacity: 0.5
298
- }
299
- }
300
- )
301
- ] });
302
- };
303
- var AmbientOrb = ({ audioLevel, isRecording }) => {
304
- const { reducedMotion } = useWidget();
305
- const [particles, setParticles] = (0, import_react.useState)([]);
306
- (0, import_react.useEffect)(() => {
307
- if (!isRecording || audioLevel < 0.3 || reducedMotion) return;
308
- const particleCount = Math.floor(audioLevel * 4);
309
- const newParticles = Array.from({ length: particleCount }, () => ({
310
- id: Math.random().toString(36).slice(2, 11),
311
- x: 70,
312
- y: 70,
313
- angle: Math.random() * Math.PI * 2,
314
- speed: 1 + Math.random() * 3,
315
- size: 2 + Math.random() * 3,
316
- color: Math.random() > 0.5 ? "var(--widget-primary-light)" : "var(--widget-accent)"
317
- }));
318
- setParticles((prev) => [...prev, ...newParticles].slice(-40));
319
- }, [audioLevel, isRecording, reducedMotion]);
320
- (0, import_react.useEffect)(() => {
321
- if (particles.length === 0) return;
322
- const timeout = setTimeout(() => {
323
- setParticles((prev) => prev.slice(1));
324
- }, 100);
325
- return () => clearTimeout(timeout);
326
- }, [particles]);
327
- const orbScale = 1 + audioLevel * 0.2;
328
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "fiq-ambient-orb", style: { position: "relative", width: 140, height: 140, margin: "0 auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: 140, height: 140, viewBox: "0 0 140 140", children: [
329
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("defs", { children: [
330
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("radialGradient", { id: "orbGradient", cx: "30%", cy: "30%", children: [
331
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: "var(--widget-accent-light)" }),
332
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "40%", stopColor: "var(--widget-primary)" }),
333
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: "var(--widget-primary-hover)" })
334
- ] }),
335
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("filter", { id: "orbGlow", x: "-50%", y: "-50%", width: "200%", height: "200%", children: [
336
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feGaussianBlur", { stdDeviation: "8", result: "blur" }),
337
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("feComposite", { in: "SourceGraphic", in2: "blur", operator: "over" })
338
- ] })
339
- ] }),
340
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
341
- import_framer_motion.motion.circle,
342
- {
343
- cx: 70,
344
- cy: 70,
345
- r: 50,
346
- fill: "none",
347
- stroke: "var(--widget-primary-glow)",
348
- strokeWidth: 2,
349
- animate: {
350
- r: isRecording ? [50, 58, 50] : 50,
351
- opacity: isRecording ? [0.3, 0.6, 0.3] : 0.3
352
- },
353
- transition: {
354
- duration: 2,
355
- repeat: Infinity,
356
- ease: "easeInOut"
357
- }
358
- }
359
- ),
360
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
361
- import_framer_motion.motion.circle,
362
- {
363
- cx: 70,
364
- cy: 70,
365
- r: 40,
366
- fill: "url(#orbGradient)",
367
- filter: "url(#orbGlow)",
368
- animate: { scale: reducedMotion ? 1 : orbScale },
369
- transition: springPresets.snappy,
370
- style: { transformOrigin: "center" }
371
- }
372
- ),
373
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { transform: "translate(70, 70)", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
374
- import_framer_motion.motion.g,
375
- {
376
- animate: {
377
- opacity: isRecording ? [1, 0.7, 1] : 1
378
- },
379
- transition: {
380
- duration: 1,
381
- repeat: isRecording ? Infinity : 0
382
- },
383
- children: [
384
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "-8", y: "-20", width: "16", height: "26", rx: "8", fill: "white" }),
385
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
386
- "path",
387
- {
388
- d: "M-14 0 v6 a14 14 0 0 0 28 0 v-6",
389
- fill: "none",
390
- stroke: "white",
391
- strokeWidth: "3",
392
- strokeLinecap: "round"
393
- }
394
- ),
395
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "0", y1: "20", x2: "0", y2: "28", stroke: "white", strokeWidth: "3", strokeLinecap: "round" }),
396
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "-8", y1: "28", x2: "8", y2: "28", stroke: "white", strokeWidth: "3", strokeLinecap: "round" })
397
- ]
398
- }
399
- ) }),
400
- particles.filter((p) => p.size != null).map((particle) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
401
- import_framer_motion.motion.circle,
402
- {
403
- cx: 70,
404
- cy: 70,
405
- r: particle.size || 3,
406
- fill: particle.color,
407
- initial: { opacity: 0.8, scale: 1 },
408
- animate: {
409
- cx: 70 + Math.cos(particle.angle) * 50,
410
- cy: 70 + Math.sin(particle.angle) * 50,
411
- opacity: 0,
412
- scale: 0.5
413
- },
414
- transition: { duration: 1.5, ease: "easeOut" }
415
- },
416
- particle.id
417
- ))
418
- ] }) });
419
- };
420
- var RecordingTimer = ({
421
- seconds,
422
- maxDuration
423
- }) => {
424
- const minutes = Math.floor(seconds / 60);
425
- const secs = seconds % 60;
426
- const progress = seconds / maxDuration;
427
- const isNearLimit = seconds >= maxDuration - 30;
428
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "fiq-timer", style: { textAlign: "center" }, children: [
429
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
430
- import_framer_motion.motion.div,
431
- {
432
- style: {
433
- fontSize: 36,
434
- fontWeight: 700,
435
- fontFamily: "ui-monospace, monospace",
436
- color: isNearLimit ? "var(--widget-recording)" : "var(--widget-text-primary)"
437
- },
438
- animate: isNearLimit ? { scale: [1, 1.05, 1] } : {},
439
- transition: { duration: 0.5, repeat: isNearLimit ? Infinity : 0 },
440
- children: [
441
- String(minutes).padStart(2, "0"),
442
- ":",
443
- String(secs).padStart(2, "0")
444
- ]
445
- }
446
- ),
447
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
448
- "div",
449
- {
450
- style: {
451
- width: 160,
452
- height: 4,
453
- background: "var(--widget-glass-border)",
454
- borderRadius: 2,
455
- margin: "12px auto",
456
- overflow: "hidden"
457
- },
458
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
459
- import_framer_motion.motion.div,
460
- {
461
- style: {
462
- height: "100%",
463
- background: isNearLimit ? "var(--widget-recording)" : "var(--widget-primary)",
464
- borderRadius: 2
465
- },
466
- initial: { width: 0 },
467
- animate: { width: `${progress * 100}%` },
468
- transition: { duration: 0.5 }
469
- }
470
- )
471
- }
472
- )
473
- ] });
474
- };
475
- var TranscriptionDisplay = ({ text, isLive }) => {
476
- const { reducedMotion } = useWidget();
477
- const [displayedText, setDisplayedText] = (0, import_react.useState)("");
478
- const [cursorVisible, setCursorVisible] = (0, import_react.useState)(true);
479
- (0, import_react.useEffect)(() => {
480
- if (reducedMotion) {
481
- setDisplayedText(text);
482
- return;
483
- }
484
- if (text.length > displayedText.length) {
485
- const timeout = setTimeout(() => {
486
- setDisplayedText(text.slice(0, displayedText.length + 1));
487
- }, 30 + Math.random() * 40);
488
- return () => clearTimeout(timeout);
489
- }
490
- }, [text, displayedText, reducedMotion]);
491
- (0, import_react.useEffect)(() => {
492
- if (!isLive) return;
493
- const interval = setInterval(() => setCursorVisible((v) => !v), 530);
494
- return () => clearInterval(interval);
495
- }, [isLive]);
496
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
497
- "div",
498
- {
499
- className: "fiq-transcription",
500
- style: {
501
- padding: 16,
502
- background: "var(--widget-glass-bg)",
503
- borderRadius: 12,
504
- border: "1px solid var(--widget-glass-border)",
505
- minHeight: 80,
506
- maxHeight: 120,
507
- overflow: "auto"
508
- },
509
- children: [
510
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: { color: "var(--widget-text-primary)", fontSize: 14, lineHeight: 1.6, margin: 0 }, children: [
511
- displayedText || /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--widget-text-muted)", fontStyle: "italic" }, children: "Start speaking..." }),
512
- isLive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
513
- import_framer_motion.motion.span,
514
- {
515
- animate: { opacity: cursorVisible ? 1 : 0 },
516
- style: {
517
- display: "inline-block",
518
- width: 2,
519
- height: "1.2em",
520
- background: "var(--widget-primary)",
521
- marginLeft: 2,
522
- verticalAlign: "text-bottom"
523
- }
524
- }
525
- )
526
- ] }),
527
- isLive && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
528
- import_framer_motion.motion.div,
529
- {
530
- initial: { opacity: 0, y: 5 },
531
- animate: { opacity: 1, y: 0 },
532
- style: {
533
- display: "flex",
534
- alignItems: "center",
535
- gap: 8,
536
- marginTop: 12,
537
- color: "var(--widget-text-secondary)",
538
- fontSize: 12
539
- },
540
- children: [
541
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListeningDots, {}),
542
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Listening..." })
543
- ]
544
- }
545
- )
546
- ]
547
- }
548
- );
549
- };
550
- var ListeningDots = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: 4 }, children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
551
- import_framer_motion.motion.span,
552
- {
553
- animate: {
554
- scale: [1, 1.4, 1],
555
- opacity: [0.5, 1, 0.5]
556
- },
557
- transition: {
558
- duration: 1,
559
- repeat: Infinity,
560
- delay: i * 0.15
561
- },
562
- style: {
563
- width: 6,
564
- height: 6,
565
- borderRadius: "50%",
566
- background: "var(--widget-primary)"
567
- }
568
- },
569
- i
570
- )) });
571
- var CategorySelector = ({ categories, selected, onSelect }) => {
572
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "fiq-category-selector", children: [
573
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
574
- "label",
575
- {
576
- style: {
577
- display: "block",
578
- fontSize: 12,
579
- fontWeight: 600,
580
- color: "var(--widget-text-muted)",
581
- marginBottom: 12,
582
- textTransform: "uppercase",
583
- letterSpacing: "0.05em"
584
- },
585
- children: "Category"
586
- }
587
- ),
588
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
589
- "div",
590
- {
591
- style: {
592
- display: "flex",
593
- flexWrap: "wrap",
594
- gap: 8
595
- },
596
- children: categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
597
- import_framer_motion.motion.button,
598
- {
599
- whileHover: { scale: 1.04, y: -1 },
600
- whileTap: { scale: 0.96 },
601
- onClick: () => onSelect(category.id),
602
- style: {
603
- padding: "10px 18px",
604
- background: selected === category.id ? "linear-gradient(145deg, var(--widget-primary) 0%, var(--widget-accent) 100%)" : "var(--widget-glass-bg)",
605
- border: `1.5px solid ${selected === category.id ? "transparent" : "var(--widget-glass-border)"}`,
606
- borderRadius: 24,
607
- cursor: "pointer",
608
- transition: "all 0.2s ease",
609
- boxShadow: selected === category.id ? "0 4px 12px -2px var(--widget-primary-glow)" : "0 2px 4px rgba(0,0,0,0.1)",
610
- position: "relative",
611
- overflow: "hidden"
612
- },
613
- children: [
614
- selected === category.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
615
- "div",
616
- {
617
- style: {
618
- position: "absolute",
619
- inset: 0,
620
- background: "linear-gradient(180deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 50%)",
621
- borderRadius: "inherit",
622
- pointerEvents: "none"
623
- }
624
- }
625
- ),
626
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
627
- "span",
628
- {
629
- style: {
630
- fontSize: 13,
631
- fontWeight: 600,
632
- color: selected === category.id ? "white" : "var(--widget-text-primary)",
633
- position: "relative",
634
- zIndex: 1
635
- },
636
- children: category.label
637
- }
638
- )
639
- ]
640
- },
641
- category.id
642
- ))
643
- }
644
- )
645
- ] });
646
- };
647
- var SuccessCelebration = ({ onComplete }) => {
648
- const { reducedMotion } = useWidget();
649
- const confettiParticles = (0, import_react.useMemo)(
650
- () => reducedMotion ? [] : Array.from({ length: 30 }, (_, i) => ({
651
- id: i,
652
- angle: i / 30 * Math.PI * 2,
653
- distance: 100 + Math.random() * 80,
654
- size: 4 + Math.random() * 6,
655
- color: ["#10b981", "#22d3d3", "#fbbf24", "#f472b6"][Math.floor(Math.random() * 4)],
656
- rotation: Math.random() * 360
657
- })),
658
- [reducedMotion]
659
- );
660
- (0, import_react.useEffect)(() => {
661
- const timer = setTimeout(onComplete, 3e3);
662
- return () => clearTimeout(timer);
663
- }, [onComplete]);
664
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
665
- import_framer_motion.motion.div,
666
- {
667
- initial: { opacity: 0, scale: 0.8 },
668
- animate: { opacity: 1, scale: 1 },
669
- exit: { opacity: 0, scale: 0.8 },
670
- style: {
671
- display: "flex",
672
- flexDirection: "column",
673
- alignItems: "center",
674
- justifyContent: "center",
675
- padding: 40,
676
- textAlign: "center",
677
- position: "relative",
678
- overflow: "hidden"
679
- },
680
- children: [
681
- confettiParticles.map((particle) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
682
- import_framer_motion.motion.div,
683
- {
684
- initial: { x: 0, y: 0, scale: 0, rotate: 0 },
685
- animate: {
686
- x: Math.cos(particle.angle) * particle.distance,
687
- y: Math.sin(particle.angle) * particle.distance + 50,
688
- scale: [0, 1, 1, 0],
689
- rotate: particle.rotation
690
- },
691
- transition: { duration: 1.2, ease: [0.22, 1, 0.36, 1] },
692
- style: {
693
- position: "absolute",
694
- top: "50%",
695
- left: "50%",
696
- width: particle.size,
697
- height: particle.size,
698
- background: particle.color,
699
- borderRadius: Math.random() > 0.5 ? "50%" : 2
700
- }
701
- },
702
- particle.id
703
- )),
704
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.motion.div, { style: { marginBottom: 20 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: 80, height: 80, viewBox: "0 0 80 80", children: [
705
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
706
- import_framer_motion.motion.circle,
707
- {
708
- cx: 40,
709
- cy: 40,
710
- r: 36,
711
- fill: "var(--widget-success)",
712
- initial: { scale: 0 },
713
- animate: { scale: 1 },
714
- transition: springPresets.bouncy
715
- }
716
- ),
717
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
718
- import_framer_motion.motion.path,
719
- {
720
- d: "M24 40 L35 51 L56 28",
721
- fill: "none",
722
- stroke: "white",
723
- strokeWidth: 5,
724
- strokeLinecap: "round",
725
- strokeLinejoin: "round",
726
- initial: { pathLength: 0 },
727
- animate: { pathLength: 1 },
728
- transition: { delay: 0.3, duration: 0.5, ease: "easeOut" }
729
- }
730
- )
731
- ] }) }),
732
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
733
- import_framer_motion.motion.h3,
734
- {
735
- initial: { opacity: 0, y: 20 },
736
- animate: { opacity: 1, y: 0 },
737
- transition: { delay: 0.5 },
738
- style: {
739
- fontSize: 24,
740
- fontWeight: 700,
741
- color: "var(--widget-text-primary)",
742
- margin: 0,
743
- marginBottom: 8
744
- },
745
- children: "Thank you!"
746
- }
747
- ),
748
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
749
- import_framer_motion.motion.p,
750
- {
751
- initial: { opacity: 0, y: 20 },
752
- animate: { opacity: 1, y: 0 },
753
- transition: { delay: 0.6 },
754
- style: {
755
- fontSize: 14,
756
- color: "var(--widget-text-secondary)",
757
- margin: 0
758
- },
759
- children: "Your feedback helps us improve"
760
- }
761
- ),
762
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
763
- import_framer_motion.motion.div,
764
- {
765
- style: {
766
- position: "absolute",
767
- bottom: 20,
768
- left: "50%",
769
- transform: "translateX(-50%)",
770
- width: 100,
771
- height: 3,
772
- background: "var(--widget-glass-border)",
773
- borderRadius: 2,
774
- overflow: "hidden"
775
- },
776
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
777
- import_framer_motion.motion.div,
778
- {
779
- initial: { width: "100%" },
780
- animate: { width: 0 },
781
- transition: { duration: 3, ease: "linear" },
782
- style: {
783
- height: "100%",
784
- background: "var(--widget-primary)"
785
- }
786
- }
787
- )
788
- }
789
- )
790
- ]
791
- }
792
- );
793
- };
794
- var MagneticButton = ({ children, onClick, onPressStart, onPressEnd, variant = "primary", disabled, loading }) => {
795
- const buttonRef = (0, import_react.useRef)(null);
796
- const [magnetOffset, setMagnetOffset] = (0, import_react.useState)({ x: 0, y: 0 });
797
- const handleMouseMove = (e) => {
798
- if (!buttonRef.current || disabled) return;
799
- const rect = buttonRef.current.getBoundingClientRect();
800
- const centerX = rect.left + rect.width / 2;
801
- const centerY = rect.top + rect.height / 2;
802
- setMagnetOffset({
803
- x: (e.clientX - centerX) * 0.08,
804
- y: (e.clientY - centerY) * 0.08
805
- });
806
- };
807
- const handleMouseLeave = () => {
808
- setMagnetOffset({ x: 0, y: 0 });
809
- };
810
- const baseStyles = {
811
- width: "100%",
812
- padding: "16px 28px",
813
- fontSize: 15,
814
- fontWeight: 600,
815
- borderRadius: 14,
816
- border: "none",
817
- cursor: disabled ? "not-allowed" : "pointer",
818
- transition: "all 0.2s ease",
819
- display: "flex",
820
- alignItems: "center",
821
- justifyContent: "center",
822
- gap: 10,
823
- letterSpacing: "0.01em",
824
- position: "relative",
825
- overflow: "hidden"
826
- };
827
- const variantStyles = {
828
- primary: {
829
- background: "linear-gradient(145deg, var(--widget-primary) 0%, var(--widget-accent) 100%)",
830
- color: "white",
831
- boxShadow: `
832
- 0 4px 16px -2px var(--widget-primary-glow),
833
- 0 2px 8px -2px rgba(0, 0, 0, 0.3),
834
- inset 0 1px 1px rgba(255, 255, 255, 0.2)
835
- `
836
- },
837
- secondary: {
838
- background: "var(--widget-glass-bg)",
839
- color: "var(--widget-text-primary)",
840
- border: "1px solid var(--widget-glass-border)",
841
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
842
- },
843
- ghost: {
844
- background: "transparent",
845
- color: "var(--widget-text-secondary)"
846
- },
847
- recording: {
848
- background: "linear-gradient(145deg, #ef4444 0%, #dc2626 100%)",
849
- color: "white",
850
- boxShadow: `
851
- 0 4px 20px -2px var(--widget-recording-glow),
852
- 0 0 40px -5px var(--widget-recording-glow),
853
- inset 0 1px 1px rgba(255, 255, 255, 0.2)
854
- `
855
- }
856
- };
857
- const handlePressStart = () => {
858
- if (disabled || loading) return;
859
- onPressStart?.();
860
- };
861
- const handlePressEnd = () => {
862
- if (disabled || loading) return;
863
- onPressEnd?.();
864
- };
865
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
866
- import_framer_motion.motion.button,
867
- {
868
- ref: buttonRef,
869
- onClick,
870
- disabled: disabled || loading,
871
- onMouseMove: handleMouseMove,
872
- onMouseLeave: () => {
873
- handleMouseLeave();
874
- if (onPressEnd) handlePressEnd();
875
- },
876
- onMouseDown: onPressStart ? handlePressStart : void 0,
877
- onMouseUp: onPressEnd ? handlePressEnd : void 0,
878
- onTouchStart: onPressStart ? handlePressStart : void 0,
879
- onTouchEnd: onPressEnd ? handlePressEnd : void 0,
880
- animate: {
881
- x: magnetOffset.x,
882
- y: magnetOffset.y,
883
- opacity: disabled ? 0.5 : 1
884
- },
885
- whileHover: { scale: disabled ? 1 : 1.02, y: -1 },
886
- whileTap: { scale: disabled ? 1 : 0.97 },
887
- transition: springPresets.snappy,
888
- style: { ...baseStyles, ...variantStyles[variant] },
889
- children: [
890
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
891
- "div",
892
- {
893
- style: {
894
- position: "absolute",
895
- inset: 0,
896
- background: "linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 50%)",
897
- borderRadius: "inherit",
898
- pointerEvents: "none"
899
- }
900
- }
901
- ),
902
- loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
903
- import_framer_motion.motion.div,
904
- {
905
- animate: { rotate: 360 },
906
- transition: { duration: 1, repeat: Infinity, ease: "linear" },
907
- style: {
908
- width: 20,
909
- height: 20,
910
- border: "2.5px solid rgba(255,255,255,0.3)",
911
- borderTopColor: "white",
912
- borderRadius: "50%"
913
- }
914
- }
915
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { position: "relative", zIndex: 1, display: "flex", alignItems: "center", gap: 10 }, children })
916
- ]
917
- }
918
- );
919
- };
920
- var DEFAULT_CATEGORIES = [
921
- { id: "bug", label: "Bug", icon: "" },
922
- { id: "feature", label: "Feature", icon: "" },
923
- { id: "improvement", label: "Improve", icon: "" },
924
- { id: "praise", label: "Praise", icon: "" },
925
- { id: "other", label: "Other", icon: "" }
926
- ];
927
- var PremiumVoiceWidget = ({
928
- position = "bottom-right",
929
- primaryColor = "#10b981",
930
- accentColor = "#22d3d3",
931
- theme = "dark",
932
- categories = DEFAULT_CATEGORIES,
933
- allowVoice = true,
934
- allowText = true,
935
- maxRecordingDuration = 180,
936
- onSubmit,
937
- onOpen,
938
- onClose
939
- }) => {
940
- const [state, setState] = (0, import_react.useState)("idle");
941
- const [isExpanded, setIsExpanded] = (0, import_react.useState)(false);
942
- const [activeTab, setActiveTab] = (0, import_react.useState)(allowVoice ? "voice" : "text");
943
- const [selectedCategory, setSelectedCategory] = (0, import_react.useState)(null);
944
- const [isRecording, setIsRecording] = (0, import_react.useState)(false);
945
- const [recordingSeconds, setRecordingSeconds] = (0, import_react.useState)(0);
946
- const [transcription, setTranscription] = (0, import_react.useState)("");
947
- const [textContent, setTextContent] = (0, import_react.useState)("");
948
- const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
949
- const reducedMotion = useReducedMotion();
950
- const scrollState = useScrollAwareness();
951
- const frustrationLevel = useFrustrationDetection();
952
- const timeOnPage = useTimeOnPage();
953
- const audioLevel = useAudioLevel(isRecording);
954
- const triggerRef = (0, import_react.useRef)(null);
955
- const recordingInterval = (0, import_react.useRef)(null);
956
- const positionStyles = {
957
- "bottom-right": { bottom: 24, right: 24 },
958
- "bottom-left": { bottom: 24, left: 24 },
959
- "top-right": { top: 24, right: 24 },
960
- "top-left": { top: 24, left: 24 }
961
- };
962
- const cssVariables = {
963
- "--widget-primary": primaryColor,
964
- "--widget-primary-hover": adjustColor(primaryColor, -20),
965
- "--widget-primary-light": adjustColor(primaryColor, 30),
966
- "--widget-primary-glow": `${primaryColor}66`,
967
- "--widget-accent": accentColor,
968
- "--widget-accent-light": adjustColor(accentColor, 30),
969
- "--widget-accent-glow": `${accentColor}4d`,
970
- "--widget-glass-bg": theme === "dark" ? "rgba(24, 24, 27, 0.85)" : "rgba(255, 255, 255, 0.85)",
971
- "--widget-glass-border": theme === "dark" ? "rgba(63, 63, 70, 0.5)" : "rgba(0, 0, 0, 0.1)",
972
- "--widget-glass-highlight": theme === "dark" ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.02)",
973
- "--widget-text-primary": theme === "dark" ? "#fafafa" : "#18181b",
974
- "--widget-text-secondary": theme === "dark" ? "#a1a1aa" : "#71717a",
975
- "--widget-text-muted": theme === "dark" ? "#71717a" : "#a1a1aa",
976
- "--widget-recording": "#ef4444",
977
- "--widget-recording-glow": "rgba(239, 68, 68, 0.4)",
978
- "--widget-success": "#22c55e",
979
- "--widget-success-glow": "rgba(34, 197, 94, 0.4)",
980
- "--widget-frustrated": "#f97316"
981
- };
982
- const handleOpen = (0, import_react.useCallback)(() => {
983
- setIsExpanded(true);
984
- setState("open");
985
- onOpen?.();
986
- }, [onOpen]);
987
- const handleClose = (0, import_react.useCallback)(() => {
988
- setIsExpanded(false);
989
- setState("idle");
990
- setIsRecording(false);
991
- setRecordingSeconds(0);
992
- setTranscription("");
993
- onClose?.();
994
- }, [onClose]);
995
- const handleStartRecording = (0, import_react.useCallback)(() => {
996
- setIsRecording(true);
997
- setState("recording");
998
- setRecordingSeconds(0);
999
- const samplePhrases = [
1000
- "I really like the new dashboard design, ",
1001
- "but I think the navigation could be improved. ",
1002
- "Sometimes I have trouble finding the settings page."
1003
- ];
1004
- let phraseIndex = 0;
1005
- recordingInterval.current = setInterval(() => {
1006
- setRecordingSeconds((prev) => {
1007
- if (prev >= maxRecordingDuration) {
1008
- handleStopRecording();
1009
- return prev;
1010
- }
1011
- return prev + 1;
1012
- });
1013
- if (phraseIndex < samplePhrases.length && Math.random() > 0.7) {
1014
- setTranscription((prev) => prev + samplePhrases[phraseIndex]);
1015
- phraseIndex++;
1016
- }
1017
- }, 1e3);
1018
- }, [maxRecordingDuration]);
1019
- const handleStopRecording = (0, import_react.useCallback)(() => {
1020
- setIsRecording(false);
1021
- setState("open");
1022
- if (recordingInterval.current) {
1023
- clearInterval(recordingInterval.current);
1024
- }
1025
- }, []);
1026
- const handleSubmit = (0, import_react.useCallback)(async () => {
1027
- if (!selectedCategory) return;
1028
- setIsSubmitting(true);
1029
- setState("processing");
1030
- try {
1031
- await onSubmit?.({
1032
- type: activeTab,
1033
- content: activeTab === "voice" ? transcription : textContent,
1034
- category: selectedCategory,
1035
- transcription: activeTab === "voice" ? transcription : void 0
1036
- });
1037
- setState("success");
1038
- } catch (error) {
1039
- setState("error");
1040
- } finally {
1041
- setIsSubmitting(false);
1042
- }
1043
- }, [activeTab, selectedCategory, transcription, textContent, onSubmit]);
1044
- const handleSuccessComplete = (0, import_react.useCallback)(() => {
1045
- handleClose();
1046
- setSelectedCategory(null);
1047
- setTextContent("");
1048
- setTranscription("");
1049
- }, [handleClose]);
1050
- const contextValue = {
1051
- state,
1052
- setState,
1053
- audioLevel,
1054
- isRecording,
1055
- transcription,
1056
- selectedCategory,
1057
- setSelectedCategory,
1058
- frustrationLevel,
1059
- reducedMotion
1060
- };
1061
- const triggerState = (0, import_react.useMemo)(() => {
1062
- if (isExpanded) return "hidden";
1063
- if (scrollState === "scrolling") return "scrolling";
1064
- if (state === "hover") return "hover";
1065
- return "idle";
1066
- }, [isExpanded, scrollState, state]);
1067
- const shouldShowAttentionPulse = timeOnPage === 60 && !isExpanded && frustrationLevel < 3;
1068
- const [showPrompt, setShowPrompt] = (0, import_react.useState)(false);
1069
- const promptMessages = [
1070
- "Help us improve!",
1071
- "Got feedback?",
1072
- "Share your thoughts",
1073
- "We'd love to hear from you"
1074
- ];
1075
- const [promptMessage, setPromptMessage] = (0, import_react.useState)(promptMessages[0]);
1076
- (0, import_react.useEffect)(() => {
1077
- if (isExpanded) return;
1078
- const showTimes = [30, 90, 180];
1079
- if (showTimes.includes(timeOnPage)) {
1080
- setPromptMessage(promptMessages[Math.floor(Math.random() * promptMessages.length)]);
1081
- setShowPrompt(true);
1082
- const timer = setTimeout(() => setShowPrompt(false), 4e3);
1083
- return () => clearTimeout(timer);
1084
- }
1085
- }, [timeOnPage, isExpanded]);
1086
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WidgetContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1087
- "div",
1088
- {
1089
- className: "fiq-premium-widget",
1090
- style: {
1091
- ...cssVariables,
1092
- position: "fixed",
1093
- ...positionStyles[position],
1094
- zIndex: 2147483640,
1095
- fontFamily: "system-ui, -apple-system, sans-serif"
1096
- },
1097
- children: [
1098
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_framer_motion.AnimatePresence, { mode: "wait", children: [
1099
- !isExpanded && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1100
- import_framer_motion.motion.button,
1101
- {
1102
- ref: triggerRef,
1103
- variants: triggerVariants,
1104
- initial: "idle",
1105
- animate: triggerState,
1106
- exit: "hidden",
1107
- whileHover: "hover",
1108
- whileTap: "tap",
1109
- onClick: handleOpen,
1110
- onMouseEnter: () => setState("hover"),
1111
- onMouseLeave: () => setState("idle"),
1112
- "aria-label": "Open feedback widget",
1113
- "aria-expanded": isExpanded,
1114
- style: {
1115
- width: 60,
1116
- height: 60,
1117
- borderRadius: "50%",
1118
- border: "none",
1119
- cursor: "pointer",
1120
- position: "relative",
1121
- background: `linear-gradient(145deg, var(--widget-primary) 0%, var(--widget-accent) 100%)`,
1122
- boxShadow: `
1123
- 0 8px 32px -4px var(--widget-primary-glow),
1124
- 0 4px 16px -2px rgba(0, 0, 0, 0.4),
1125
- inset 0 1px 1px rgba(255, 255, 255, 0.3),
1126
- inset 0 -1px 1px rgba(0, 0, 0, 0.2)
1127
- `
1128
- },
1129
- children: [
1130
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BreathingRing, { isActive: !reducedMotion && triggerState === "idle" }),
1131
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1132
- "div",
1133
- {
1134
- style: {
1135
- position: "absolute",
1136
- inset: 0,
1137
- borderRadius: "50%",
1138
- background: "linear-gradient(180deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 100%)",
1139
- pointerEvents: "none"
1140
- }
1141
- }
1142
- ),
1143
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1144
- "div",
1145
- {
1146
- style: {
1147
- position: "absolute",
1148
- top: "50%",
1149
- left: "50%",
1150
- transform: "translate(-50%, -50%)",
1151
- zIndex: 1,
1152
- display: "flex",
1153
- alignItems: "center",
1154
- justifyContent: "center"
1155
- },
1156
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1157
- "svg",
1158
- {
1159
- width: 26,
1160
- height: 26,
1161
- viewBox: "0 0 24 24",
1162
- fill: "none",
1163
- style: { filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.2))" },
1164
- children: [
1165
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1166
- "path",
1167
- {
1168
- d: "M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z",
1169
- fill: "white",
1170
- opacity: "0.15"
1171
- }
1172
- ),
1173
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1174
- "path",
1175
- {
1176
- d: "M12 3a2.5 2.5 0 0 0-2.5 2.5v5a2.5 2.5 0 0 0 5 0v-5A2.5 2.5 0 0 0 12 3z",
1177
- fill: "white"
1178
- }
1179
- ),
1180
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1181
- "path",
1182
- {
1183
- d: "M16.5 10.5v1a4.5 4.5 0 0 1-9 0v-1",
1184
- stroke: "white",
1185
- strokeWidth: "2",
1186
- strokeLinecap: "round",
1187
- fill: "none"
1188
- }
1189
- ),
1190
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1191
- "path",
1192
- {
1193
- d: "M12 15.5v2.5M9.5 18h5",
1194
- stroke: "white",
1195
- strokeWidth: "2",
1196
- strokeLinecap: "round"
1197
- }
1198
- )
1199
- ]
1200
- }
1201
- )
1202
- }
1203
- ),
1204
- shouldShowAttentionPulse && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1205
- import_framer_motion.motion.div,
1206
- {
1207
- initial: { scale: 1, opacity: 1 },
1208
- animate: { scale: 1.6, opacity: 0 },
1209
- transition: { duration: 1.2, repeat: 3 },
1210
- style: {
1211
- position: "absolute",
1212
- inset: -2,
1213
- borderRadius: "50%",
1214
- border: "2px solid var(--widget-primary)",
1215
- pointerEvents: "none"
1216
- }
1217
- }
1218
- ),
1219
- frustrationLevel >= 5 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1220
- import_framer_motion.motion.div,
1221
- {
1222
- initial: { scale: 0 },
1223
- animate: { scale: 1 },
1224
- style: {
1225
- position: "absolute",
1226
- top: -2,
1227
- right: -2,
1228
- width: 18,
1229
- height: 18,
1230
- borderRadius: "50%",
1231
- background: "linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%)",
1232
- border: "2px solid white",
1233
- display: "flex",
1234
- alignItems: "center",
1235
- justifyContent: "center",
1236
- fontSize: 11,
1237
- fontWeight: 700,
1238
- color: "white",
1239
- boxShadow: "0 2px 8px rgba(239, 68, 68, 0.5)"
1240
- },
1241
- children: "!"
1242
- }
1243
- )
1244
- ]
1245
- },
1246
- "trigger"
1247
- ),
1248
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { children: showPrompt && !isExpanded && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1249
- import_framer_motion.motion.div,
1250
- {
1251
- initial: { opacity: 0, x: 10, scale: 0.9 },
1252
- animate: { opacity: 1, x: 0, scale: 1 },
1253
- exit: { opacity: 0, x: 10, scale: 0.9 },
1254
- transition: springPresets.smooth,
1255
- onClick: handleOpen,
1256
- style: {
1257
- position: "absolute",
1258
- right: 70,
1259
- top: "50%",
1260
- transform: "translateY(-50%)",
1261
- background: "var(--widget-glass-bg)",
1262
- backdropFilter: "blur(12px)",
1263
- WebkitBackdropFilter: "blur(12px)",
1264
- border: "1px solid var(--widget-glass-border)",
1265
- borderRadius: 12,
1266
- padding: "10px 16px",
1267
- boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
1268
- cursor: "pointer",
1269
- whiteSpace: "nowrap"
1270
- },
1271
- children: [
1272
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
1273
- fontSize: 13,
1274
- fontWeight: 500,
1275
- color: "var(--widget-text-primary)"
1276
- }, children: promptMessage }),
1277
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1278
- "div",
1279
- {
1280
- style: {
1281
- position: "absolute",
1282
- right: -6,
1283
- top: "50%",
1284
- transform: "translateY(-50%) rotate(45deg)",
1285
- width: 12,
1286
- height: 12,
1287
- background: "var(--widget-glass-bg)",
1288
- borderRight: "1px solid var(--widget-glass-border)",
1289
- borderTop: "1px solid var(--widget-glass-border)"
1290
- }
1291
- }
1292
- )
1293
- ]
1294
- }
1295
- ) })
1296
- ] }),
1297
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { mode: "wait", children: isExpanded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1298
- import_framer_motion.motion.div,
1299
- {
1300
- variants: containerVariants,
1301
- initial: "trigger",
1302
- animate: "expanded",
1303
- exit: "trigger",
1304
- style: {
1305
- background: theme === "dark" ? "linear-gradient(180deg, rgba(28, 28, 32, 0.95) 0%, rgba(20, 20, 24, 0.98) 100%)" : "linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 248, 250, 0.98) 100%)",
1306
- backdropFilter: "blur(24px) saturate(200%)",
1307
- WebkitBackdropFilter: "blur(24px) saturate(200%)",
1308
- border: "1px solid var(--widget-glass-border)",
1309
- boxShadow: `
1310
- 0 24px 80px -12px rgba(0, 0, 0, 0.6),
1311
- 0 0 1px 0 rgba(255, 255, 255, 0.1) inset,
1312
- 0 1px 0 0 rgba(255, 255, 255, 0.05) inset
1313
- `,
1314
- overflow: "hidden",
1315
- display: "flex",
1316
- flexDirection: "column"
1317
- },
1318
- role: "dialog",
1319
- "aria-modal": "true",
1320
- "aria-labelledby": "widget-title",
1321
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { mode: "wait", children: state === "success" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SuccessCelebration, { onComplete: handleSuccessComplete }, "success") : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1322
- import_framer_motion.motion.div,
1323
- {
1324
- variants: contentVariants,
1325
- initial: "hidden",
1326
- animate: "visible",
1327
- exit: "exit",
1328
- style: { display: "flex", flexDirection: "column", height: "100%" },
1329
- children: [
1330
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1331
- "div",
1332
- {
1333
- style: {
1334
- display: "flex",
1335
- justifyContent: "space-between",
1336
- alignItems: "center",
1337
- padding: "20px 24px",
1338
- borderBottom: "1px solid var(--widget-glass-border)",
1339
- background: theme === "dark" ? "linear-gradient(180deg, rgba(255,255,255,0.02) 0%, transparent 100%)" : "linear-gradient(180deg, rgba(0,0,0,0.01) 0%, transparent 100%)"
1340
- },
1341
- children: [
1342
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
1343
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1344
- "div",
1345
- {
1346
- style: {
1347
- width: 36,
1348
- height: 36,
1349
- borderRadius: 10,
1350
- background: "linear-gradient(145deg, var(--widget-primary) 0%, var(--widget-accent) 100%)",
1351
- display: "flex",
1352
- alignItems: "center",
1353
- justifyContent: "center",
1354
- boxShadow: "0 2px 8px -2px var(--widget-primary-glow)"
1355
- },
1356
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: 18, height: 18, viewBox: "0 0 24 24", fill: "white", children: [
1357
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3a2.5 2.5 0 0 0-2.5 2.5v5a2.5 2.5 0 0 0 5 0v-5A2.5 2.5 0 0 0 12 3z" }),
1358
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M16.5 10.5v1a4.5 4.5 0 0 1-9 0v-1", stroke: "white", strokeWidth: "2", strokeLinecap: "round", fill: "none" }),
1359
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 15.5v2.5M9.5 18h5", stroke: "white", strokeWidth: "2", strokeLinecap: "round" })
1360
- ] })
1361
- }
1362
- ),
1363
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1364
- "h2",
1365
- {
1366
- id: "widget-title",
1367
- style: {
1368
- fontSize: 17,
1369
- fontWeight: 700,
1370
- color: "var(--widget-text-primary)",
1371
- margin: 0,
1372
- letterSpacing: "-0.01em"
1373
- },
1374
- children: "Share Feedback"
1375
- }
1376
- )
1377
- ] }),
1378
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1379
- import_framer_motion.motion.button,
1380
- {
1381
- whileHover: { scale: 1.1, backgroundColor: "var(--widget-glass-border)" },
1382
- whileTap: { scale: 0.9 },
1383
- onClick: handleClose,
1384
- "aria-label": "Close widget",
1385
- style: {
1386
- width: 32,
1387
- height: 32,
1388
- borderRadius: 8,
1389
- border: "none",
1390
- background: "transparent",
1391
- color: "var(--widget-text-secondary)",
1392
- cursor: "pointer",
1393
- display: "flex",
1394
- alignItems: "center",
1395
- justifyContent: "center",
1396
- transition: "background 0.15s ease"
1397
- },
1398
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1399
- "svg",
1400
- {
1401
- width: 18,
1402
- height: 18,
1403
- viewBox: "0 0 24 24",
1404
- fill: "none",
1405
- stroke: "currentColor",
1406
- strokeWidth: 2.5,
1407
- strokeLinecap: "round",
1408
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
1409
- }
1410
- )
1411
- }
1412
- )
1413
- ]
1414
- }
1415
- ),
1416
- allowVoice && allowText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1417
- "div",
1418
- {
1419
- style: {
1420
- display: "flex",
1421
- padding: "8px 12px",
1422
- gap: 4,
1423
- background: "var(--widget-glass-bg)",
1424
- margin: "0 16px",
1425
- marginTop: 16,
1426
- borderRadius: 12,
1427
- border: "1px solid var(--widget-glass-border)"
1428
- },
1429
- children: ["voice", "text"].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1430
- import_framer_motion.motion.button,
1431
- {
1432
- onClick: () => setActiveTab(tab),
1433
- style: {
1434
- flex: 1,
1435
- padding: "10px 0",
1436
- border: "none",
1437
- background: activeTab === tab ? "linear-gradient(145deg, var(--widget-primary) 0%, var(--widget-accent) 100%)" : "transparent",
1438
- fontSize: 13,
1439
- fontWeight: 600,
1440
- color: activeTab === tab ? "white" : "var(--widget-text-secondary)",
1441
- cursor: "pointer",
1442
- position: "relative",
1443
- borderRadius: 8,
1444
- transition: "all 0.2s ease",
1445
- display: "flex",
1446
- alignItems: "center",
1447
- justifyContent: "center",
1448
- gap: 6,
1449
- boxShadow: activeTab === tab ? "0 2px 8px -2px var(--widget-primary-glow)" : "none"
1450
- },
1451
- whileHover: { scale: activeTab === tab ? 1 : 1.02 },
1452
- whileTap: { scale: 0.98 },
1453
- children: [
1454
- tab === "voice" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "currentColor", children: [
1455
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3a2.5 2.5 0 0 0-2.5 2.5v5a2.5 2.5 0 0 0 5 0v-5A2.5 2.5 0 0 0 12 3z" }),
1456
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M16.5 10.5v1a4.5 4.5 0 0 1-9 0v-1", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", fill: "none" })
1457
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 6h16M4 12h16M4 18h10" }) }),
1458
- tab === "voice" ? "Voice" : "Text"
1459
- ]
1460
- },
1461
- tab
1462
- ))
1463
- }
1464
- ),
1465
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "20px 24px" }, children: [
1466
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: 24 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1467
- CategorySelector,
1468
- {
1469
- categories,
1470
- selected: selectedCategory,
1471
- onSelect: setSelectedCategory
1472
- }
1473
- ) }),
1474
- activeTab === "voice" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center" }, children: [
1475
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1476
- "div",
1477
- {
1478
- style: {
1479
- background: "var(--widget-glass-bg)",
1480
- borderRadius: 16,
1481
- padding: "24px 16px",
1482
- border: "1px solid var(--widget-glass-border)",
1483
- marginBottom: 16
1484
- },
1485
- children: [
1486
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AmbientOrb, { audioLevel, isRecording }),
1487
- isRecording && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1488
- RecordingTimer,
1489
- {
1490
- seconds: recordingSeconds,
1491
- maxDuration: maxRecordingDuration
1492
- }
1493
- ),
1494
- !isRecording && recordingSeconds === 0 && !transcription && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1495
- "p",
1496
- {
1497
- style: {
1498
- color: "var(--widget-text-muted)",
1499
- fontSize: 13,
1500
- marginTop: 12,
1501
- marginBottom: 0
1502
- },
1503
- children: "Click Record to start"
1504
- }
1505
- )
1506
- ]
1507
- }
1508
- ),
1509
- (isRecording || transcription) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1510
- TranscriptionDisplay,
1511
- {
1512
- text: transcription,
1513
- isLive: isRecording
1514
- }
1515
- ) }),
1516
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1517
- MagneticButton,
1518
- {
1519
- onClick: isRecording ? handleStopRecording : handleStartRecording,
1520
- variant: isRecording ? "recording" : "primary",
1521
- children: isRecording ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1522
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1523
- import_framer_motion.motion.div,
1524
- {
1525
- animate: { scale: [1, 1.2, 1] },
1526
- transition: { duration: 1, repeat: Infinity },
1527
- style: {
1528
- width: 10,
1529
- height: 10,
1530
- borderRadius: 2,
1531
- background: "white"
1532
- }
1533
- }
1534
- ),
1535
- "Stop Recording"
1536
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1537
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3a2.5 2.5 0 0 0-2.5 2.5v5a2.5 2.5 0 0 0 5 0v-5A2.5 2.5 0 0 0 12 3z" }) }),
1538
- transcription ? "Record Again" : "Record"
1539
- ] })
1540
- }
1541
- )
1542
- ] }),
1543
- activeTab === "text" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1544
- "div",
1545
- {
1546
- style: {
1547
- position: "relative",
1548
- background: "var(--widget-glass-bg)",
1549
- border: "1px solid var(--widget-glass-border)",
1550
- borderRadius: 14,
1551
- overflow: "hidden"
1552
- },
1553
- children: [
1554
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1555
- "textarea",
1556
- {
1557
- value: textContent,
1558
- onChange: (e) => setTextContent(e.target.value),
1559
- placeholder: "Share your thoughts, ideas, or feedback...",
1560
- style: {
1561
- width: "100%",
1562
- minHeight: 140,
1563
- padding: 16,
1564
- fontSize: 14,
1565
- lineHeight: 1.7,
1566
- color: "var(--widget-text-primary)",
1567
- background: "transparent",
1568
- border: "none",
1569
- resize: "none",
1570
- fontFamily: "inherit",
1571
- outline: "none"
1572
- }
1573
- }
1574
- ),
1575
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1576
- "div",
1577
- {
1578
- style: {
1579
- display: "flex",
1580
- justifyContent: "flex-end",
1581
- padding: "8px 12px",
1582
- borderTop: "1px solid var(--widget-glass-border)",
1583
- background: theme === "dark" ? "rgba(0,0,0,0.2)" : "rgba(0,0,0,0.02)"
1584
- },
1585
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1586
- "span",
1587
- {
1588
- style: {
1589
- fontSize: 11,
1590
- fontWeight: 500,
1591
- color: textContent.length > 4500 ? "var(--widget-recording)" : "var(--widget-text-muted)"
1592
- },
1593
- children: [
1594
- textContent.length.toLocaleString(),
1595
- " / 5,000"
1596
- ]
1597
- }
1598
- )
1599
- }
1600
- )
1601
- ]
1602
- }
1603
- ) })
1604
- ] }),
1605
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1606
- "div",
1607
- {
1608
- style: {
1609
- padding: "16px 24px 20px",
1610
- borderTop: "1px solid var(--widget-glass-border)",
1611
- background: theme === "dark" ? "linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.2) 100%)" : "linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.02) 100%)"
1612
- },
1613
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1614
- MagneticButton,
1615
- {
1616
- onClick: handleSubmit,
1617
- disabled: !selectedCategory || activeTab === "voice" && !transcription || activeTab === "text" && !textContent.trim(),
1618
- loading: isSubmitting,
1619
- children: [
1620
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" }) }),
1621
- "Submit Feedback"
1622
- ]
1623
- }
1624
- )
1625
- }
1626
- )
1627
- ]
1628
- },
1629
- "content"
1630
- ) })
1631
- },
1632
- "modal"
1633
- ) }),
1634
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "aria-live": "polite", className: "sr-only", style: { position: "absolute", width: 1, height: 1, overflow: "hidden" }, children: [
1635
- isRecording && "Recording started. Speak now.",
1636
- !isRecording && transcription && `Transcription complete.`,
1637
- state === "success" && "Feedback submitted successfully."
1638
- ] })
1639
- ]
1640
- }
1641
- ) });
1642
- };
1643
- function adjustColor(color, amount) {
1644
- const hex = color.replace("#", "");
1645
- const num = parseInt(hex, 16);
1646
- const r = Math.min(255, Math.max(0, (num >> 16) + amount));
1647
- const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
1648
- const b = Math.min(255, Math.max(0, (num & 255) + amount));
1649
- return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
1650
- }
1651
-
1652
- // src/components/IntentAIProvider.tsx
1653
- var import_react2 = require("react");
1654
- var import_jsx_runtime2 = require("react/jsx-runtime");
1655
- var IntentAIContext = (0, import_react2.createContext)(null);
1656
- function useIntentAI() {
1657
- const context = (0, import_react2.useContext)(IntentAIContext);
1658
- if (!context) {
1659
- throw new Error("useIntentAI must be used within an IntentAIProvider");
1660
- }
1661
- return context;
1662
- }
1663
- function generateSessionId() {
1664
- return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1665
- }
1666
- function getDeviceInfo() {
1667
- const ua = navigator.userAgent;
1668
- let browser = "Unknown";
1669
- let browserVersion = "Unknown";
1670
- if (ua.includes("Firefox/")) {
1671
- browser = "Firefox";
1672
- const match = ua.match(/Firefox\/(\d+(\.\d+)?)/);
1673
- if (match) browserVersion = match[1];
1674
- } else if (ua.includes("Edg/")) {
1675
- browser = "Edge";
1676
- const match = ua.match(/Edg\/(\d+(\.\d+)?)/);
1677
- if (match) browserVersion = match[1];
1678
- } else if (ua.includes("Chrome/")) {
1679
- browser = "Chrome";
1680
- const match = ua.match(/Chrome\/(\d+(\.\d+)?)/);
1681
- if (match) browserVersion = match[1];
1682
- } else if (ua.includes("Safari/") && !ua.includes("Chrome")) {
1683
- browser = "Safari";
1684
- const match = ua.match(/Version\/(\d+(\.\d+)?)/);
1685
- if (match) browserVersion = match[1];
1686
- }
1687
- let os = "Unknown";
1688
- if (ua.includes("Windows NT 10")) os = "Windows 10";
1689
- else if (ua.includes("Windows")) os = "Windows";
1690
- else if (ua.includes("Mac OS X")) os = "macOS";
1691
- else if (ua.includes("Android")) os = "Android";
1692
- else if (ua.includes("iPhone") || ua.includes("iPad")) os = "iOS";
1693
- else if (ua.includes("Linux")) os = "Linux";
1694
- let device = "Desktop";
1695
- if (/iPhone|iPad|iPod/.test(ua)) device = "iOS Device";
1696
- else if (/Android.*Mobile/.test(ua)) device = "Android Phone";
1697
- else if (/Android/.test(ua)) device = "Android Tablet";
1698
- return { browser, browserVersion, os, device };
1699
- }
1700
- var DEFAULT_API_URL = "https://api-production-5b88.up.railway.app";
1701
- function IntentAIProvider({
1702
- apiKey,
1703
- apiUrl = DEFAULT_API_URL,
1704
- user: initialUser,
1705
- metadata: initialMetadata,
1706
- widgetConfig,
1707
- children,
1708
- disableWidget = false,
1709
- onFeedbackSubmitted,
1710
- onError
1711
- }) {
1712
- const userRef = (0, import_react2.useRef)(initialUser);
1713
- const metadataRef = (0, import_react2.useRef)(initialMetadata || {});
1714
- const sessionIdRef = (0, import_react2.useRef)(generateSessionId());
1715
- (0, import_react2.useEffect)(() => {
1716
- if (initialUser) {
1717
- userRef.current = initialUser;
1718
- }
1719
- }, [initialUser]);
1720
- (0, import_react2.useEffect)(() => {
1721
- if (initialMetadata) {
1722
- metadataRef.current = { ...metadataRef.current, ...initialMetadata };
1723
- }
1724
- }, [initialMetadata]);
1725
- const identify = (0, import_react2.useCallback)((user) => {
1726
- userRef.current = { ...userRef.current, ...user };
1727
- }, []);
1728
- const setMetadata = (0, import_react2.useCallback)((metadata) => {
1729
- metadataRef.current = { ...metadataRef.current, ...metadata };
1730
- }, []);
1731
- const clearUser = (0, import_react2.useCallback)(() => {
1732
- userRef.current = void 0;
1733
- }, []);
1734
- const handleSubmit = (0, import_react2.useCallback)(async (feedback) => {
1735
- const deviceInfo = getDeviceInfo();
1736
- const user = userRef.current;
1737
- const customMetadata = metadataRef.current;
1738
- const categoryMap = {
1739
- bug: "BUG",
1740
- feature: "FEATURE",
1741
- improvement: "IMPROVEMENT",
1742
- praise: "PRAISE",
1743
- other: "OTHER"
1744
- };
1745
- const payload = {
1746
- sessionId: sessionIdRef.current,
1747
- type: feedback.type === "voice" ? "VOICE" : "TEXT",
1748
- category: categoryMap[feedback.category.toLowerCase()] || feedback.category.toUpperCase(),
1749
- rawText: feedback.type === "text" ? feedback.content : void 0,
1750
- transcription: feedback.type === "voice" ? feedback.transcription || feedback.content : void 0,
1751
- pageUrl: typeof window !== "undefined" ? window.location.href : "",
1752
- pageTitle: typeof document !== "undefined" ? document.title : "",
1753
- userId: user?.id,
1754
- userEmail: user?.email,
1755
- userName: user?.name,
1756
- customData: {
1757
- ...customMetadata,
1758
- ...feedback.metadata
1759
- },
1760
- browser: deviceInfo.browser,
1761
- browserVersion: deviceInfo.browserVersion,
1762
- os: deviceInfo.os,
1763
- device: deviceInfo.device,
1764
- screenWidth: typeof window !== "undefined" ? window.screen.width : void 0,
1765
- screenHeight: typeof window !== "undefined" ? window.screen.height : void 0
1766
- };
1767
- try {
1768
- const response = await fetch(`${apiUrl}/feedback`, {
1769
- method: "POST",
1770
- headers: {
1771
- "Content-Type": "application/json",
1772
- "X-API-Key": apiKey
1773
- },
1774
- body: JSON.stringify(payload)
1775
- });
1776
- if (!response.ok) {
1777
- const errorData = await response.json().catch(() => ({}));
1778
- throw new Error(errorData.error || `Failed to submit feedback: ${response.status}`);
1779
- }
1780
- onFeedbackSubmitted?.();
1781
- } catch (error) {
1782
- const err = error instanceof Error ? error : new Error("Failed to submit feedback");
1783
- onError?.(err);
1784
- throw err;
1785
- }
1786
- }, [apiKey, apiUrl, onFeedbackSubmitted, onError]);
1787
- const contextValue = (0, import_react2.useMemo)(() => ({
1788
- identify,
1789
- setMetadata,
1790
- clearUser,
1791
- isConfigured: Boolean(apiKey)
1792
- }), [identify, setMetadata, clearUser, apiKey]);
1793
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(IntentAIContext.Provider, { value: contextValue, children: [
1794
- children,
1795
- !disableWidget && apiKey && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1796
- PremiumVoiceWidget,
1797
- {
1798
- ...widgetConfig,
1799
- onSubmit: handleSubmit
1800
- }
1801
- )
1802
- ] });
1803
- }
1804
- function useIdentify() {
1805
- const { identify } = useIntentAI();
1806
- return identify;
1807
- }
1808
- function useSetMetadata() {
1809
- const { setMetadata } = useIntentAI();
1810
- return setMetadata;
1811
- }
1812
-
1813
- // src/components/FeedbackButton.tsx
1814
- var import_react3 = require("react");
1815
- var import_framer_motion2 = require("framer-motion");
1816
- var import_jsx_runtime3 = require("react/jsx-runtime");
1817
- var FeedbackIcon = ({ size }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1818
- "svg",
1819
- {
1820
- width: size,
1821
- height: size,
1822
- viewBox: "0 0 24 24",
1823
- fill: "none",
1824
- stroke: "currentColor",
1825
- strokeWidth: "2",
1826
- strokeLinecap: "round",
1827
- strokeLinejoin: "round",
1828
- children: [
1829
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }),
1830
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "9", cy: "10", r: "1", fill: "currentColor" }),
1831
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "15", cy: "10", r: "1", fill: "currentColor" })
1832
- ]
1833
- }
1834
- );
1835
- function FeedbackButton({
1836
- variant = "primary",
1837
- size = "md",
1838
- showIcon = true,
1839
- icon,
1840
- label = "Feedback",
1841
- className,
1842
- style,
1843
- primaryColor = "#10b981",
1844
- onClick,
1845
- disabled = false
1846
- }) {
1847
- const [isHovered, setIsHovered] = (0, import_react3.useState)(false);
1848
- const [isPressed, setIsPressed] = (0, import_react3.useState)(false);
1849
- const buttonRef = (0, import_react3.useRef)(null);
1850
- const sizes = {
1851
- sm: {
1852
- padding: "8px 14px",
1853
- fontSize: 13,
1854
- iconSize: 14,
1855
- gap: 6,
1856
- borderRadius: 8
1857
- },
1858
- md: {
1859
- padding: "10px 18px",
1860
- fontSize: 14,
1861
- iconSize: 16,
1862
- gap: 8,
1863
- borderRadius: 10
1864
- },
1865
- lg: {
1866
- padding: "14px 24px",
1867
- fontSize: 15,
1868
- iconSize: 18,
1869
- gap: 10,
1870
- borderRadius: 12
1871
- }
1872
- };
1873
- const sizeConfig = sizes[size];
1874
- const variants = {
1875
- primary: {
1876
- background: `linear-gradient(145deg, ${primaryColor} 0%, ${adjustColor2(primaryColor, -20)} 100%)`,
1877
- color: "white",
1878
- border: "none",
1879
- boxShadow: `0 4px 16px -4px ${primaryColor}66, 0 2px 4px rgba(0, 0, 0, 0.1)`
1880
- },
1881
- secondary: {
1882
- background: "white",
1883
- color: "#374151",
1884
- border: "1px solid #e5e7eb",
1885
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)"
1886
- },
1887
- ghost: {
1888
- background: "transparent",
1889
- color: "#6b7280",
1890
- border: "1px solid transparent",
1891
- boxShadow: "none"
1892
- },
1893
- minimal: {
1894
- background: "transparent",
1895
- color: primaryColor,
1896
- border: "none",
1897
- boxShadow: "none"
1898
- }
1899
- };
1900
- const variantStyle = variants[variant];
1901
- const handleClick = (0, import_react3.useCallback)(() => {
1902
- if (disabled) return;
1903
- if (onClick) {
1904
- onClick();
1905
- return;
1906
- }
1907
- const widget = document.querySelector(".fiq-premium-widget");
1908
- if (widget) {
1909
- const trigger = widget.querySelector('button[aria-label="Open feedback widget"]');
1910
- if (trigger) {
1911
- trigger.click();
1912
- return;
1913
- }
1914
- }
1915
- const scriptWidget = document.getElementById("feedbackiq-widget");
1916
- if (scriptWidget?.shadowRoot) {
1917
- const trigger = scriptWidget.shadowRoot.querySelector(".fiq-trigger");
1918
- if (trigger) {
1919
- trigger.click();
1920
- }
1921
- }
1922
- }, [onClick, disabled]);
1923
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1924
- import_framer_motion2.motion.button,
1925
- {
1926
- ref: buttonRef,
1927
- onClick: handleClick,
1928
- onMouseEnter: () => setIsHovered(true),
1929
- onMouseLeave: () => {
1930
- setIsHovered(false);
1931
- setIsPressed(false);
1932
- },
1933
- onMouseDown: () => setIsPressed(true),
1934
- onMouseUp: () => setIsPressed(false),
1935
- disabled,
1936
- animate: {
1937
- scale: isPressed ? 0.97 : isHovered ? 1.02 : 1,
1938
- y: isHovered ? -1 : 0
1939
- },
1940
- transition: {
1941
- type: "spring",
1942
- stiffness: 400,
1943
- damping: 25
1944
- },
1945
- className,
1946
- style: {
1947
- display: "inline-flex",
1948
- alignItems: "center",
1949
- justifyContent: "center",
1950
- gap: sizeConfig.gap,
1951
- padding: sizeConfig.padding,
1952
- fontSize: sizeConfig.fontSize,
1953
- fontWeight: 600,
1954
- fontFamily: "system-ui, -apple-system, sans-serif",
1955
- borderRadius: sizeConfig.borderRadius,
1956
- cursor: disabled ? "not-allowed" : "pointer",
1957
- opacity: disabled ? 0.5 : 1,
1958
- transition: "all 0.2s ease",
1959
- position: "relative",
1960
- overflow: "hidden",
1961
- ...variantStyle,
1962
- ...style
1963
- },
1964
- children: [
1965
- variant === "primary" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1966
- import_framer_motion2.motion.div,
1967
- {
1968
- initial: { opacity: 0 },
1969
- animate: { opacity: isHovered ? 0.1 : 0 },
1970
- style: {
1971
- position: "absolute",
1972
- inset: 0,
1973
- background: "white",
1974
- pointerEvents: "none"
1975
- }
1976
- }
1977
- ),
1978
- variant === "primary" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1979
- "div",
1980
- {
1981
- style: {
1982
- position: "absolute",
1983
- inset: 0,
1984
- background: "linear-gradient(180deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 50%)",
1985
- borderRadius: "inherit",
1986
- pointerEvents: "none"
1987
- }
1988
- }
1989
- ),
1990
- showIcon && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { display: "flex", alignItems: "center", position: "relative", zIndex: 1 }, children: icon || /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FeedbackIcon, { size: sizeConfig.iconSize }) }),
1991
- label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { position: "relative", zIndex: 1 }, children: label })
1992
- ]
1993
- }
1994
- );
1995
- }
1996
- function adjustColor2(color, amount) {
1997
- const hex = color.replace("#", "");
1998
- const num = parseInt(hex, 16);
1999
- const r = Math.min(255, Math.max(0, (num >> 16) + amount));
2000
- const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
2001
- const b = Math.min(255, Math.max(0, (num & 255) + amount));
2002
- return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
2003
- }
2004
-
2005
- // src/index.tsx
2006
- function IntentAIWidget({
2007
- apiKey,
2008
- apiUrl,
2009
- widgetUrl,
2010
- position = "bottom-right",
2011
- theme = "light",
2012
- primaryColor,
2013
- allowVoice = true,
2014
- allowText = true,
2015
- allowScreenshot = true,
2016
- customMetadata,
2017
- user,
2018
- onOpen: _onOpen,
2019
- onClose: _onClose,
2020
- onFeedbackSubmitted: _onFeedbackSubmitted,
2021
- onError
2022
- }) {
2023
- const widgetRef = (0, import_react4.useRef)(null);
2024
- const scriptLoadedRef = (0, import_react4.useRef)(false);
2025
- const initWidget = (0, import_react4.useCallback)(() => {
2026
- if (!window.FeedbackIQ || widgetRef.current) return;
2027
- try {
2028
- widgetRef.current = new window.FeedbackIQ({
2029
- apiKey,
2030
- apiUrl,
2031
- widgetUrl,
2032
- position,
2033
- theme,
2034
- primaryColor,
2035
- allowVoice,
2036
- allowText,
2037
- allowScreenshot,
2038
- customMetadata,
2039
- user
2040
- });
2041
- } catch (error) {
2042
- onError?.(error instanceof Error ? error : new Error("Failed to initialize widget"));
2043
- }
2044
- void _onOpen;
2045
- void _onClose;
2046
- void _onFeedbackSubmitted;
2047
- }, [apiKey, apiUrl, widgetUrl, position, theme, primaryColor, allowVoice, allowText, allowScreenshot, customMetadata, user, onError]);
2048
- (0, import_react4.useEffect)(() => {
2049
- if (!widgetUrl) {
2050
- onError?.(new Error("widgetUrl is required for IntentAIWidget"));
2051
- return;
2052
- }
2053
- if (window.FeedbackIQ) {
2054
- initWidget();
2055
- return;
2056
- }
2057
- if (scriptLoadedRef.current) return;
2058
- const existingScript = document.querySelector(`script[src="${widgetUrl}"]`);
2059
- if (existingScript) {
2060
- existingScript.addEventListener("load", initWidget);
2061
- return;
2062
- }
2063
- scriptLoadedRef.current = true;
2064
- const script = document.createElement("script");
2065
- script.src = widgetUrl;
2066
- script.async = true;
2067
- script.onload = initWidget;
2068
- script.onerror = () => {
2069
- onError?.(new Error("Failed to load Intent AI widget script"));
2070
- };
2071
- document.head.appendChild(script);
2072
- return () => {
2073
- if (widgetRef.current) {
2074
- widgetRef.current.destroy();
2075
- widgetRef.current = null;
2076
- }
2077
- };
2078
- }, [initWidget, onError, widgetUrl]);
2079
- (0, import_react4.useEffect)(() => {
2080
- if (widgetRef.current && user) {
2081
- widgetRef.current.identify(user);
2082
- }
2083
- }, [user]);
2084
- (0, import_react4.useEffect)(() => {
2085
- if (widgetRef.current && customMetadata) {
2086
- widgetRef.current.setMetadata(customMetadata);
2087
- }
2088
- }, [customMetadata]);
2089
- return null;
2090
- }
2091
- function useWidgetControls() {
2092
- const open = (0, import_react4.useCallback)(() => {
2093
- const widget = document.getElementById("feedbackiq-widget");
2094
- if (widget) {
2095
- const trigger = widget.shadowRoot?.querySelector(".fiq-trigger");
2096
- trigger?.click();
2097
- }
2098
- }, []);
2099
- const close = (0, import_react4.useCallback)(() => {
2100
- const widget = document.getElementById("feedbackiq-widget");
2101
- if (widget) {
2102
- const closeBtn = widget.shadowRoot?.querySelector(".fiq-close");
2103
- closeBtn?.click();
2104
- }
2105
- }, []);
2106
- const identify = (0, import_react4.useCallback)((user) => {
2107
- if (window.IntentAI?.widget) {
2108
- window.IntentAI.widget.identify(user);
2109
- }
2110
- }, []);
2111
- const setMetadata = (0, import_react4.useCallback)((metadata) => {
2112
- if (window.IntentAI?.widget) {
2113
- window.IntentAI.widget.setMetadata(metadata);
2114
- }
2115
- }, []);
2116
- return { open, close, identify, setMetadata };
2117
- }
2118
- var index_default = IntentAIWidget;
2119
- // Annotate the CommonJS export names for ESM import in node:
2120
- 0 && (module.exports = {
2121
- FeedbackButton,
2122
- IntentAIProvider,
2123
- IntentAIWidget,
2124
- PremiumVoiceWidget,
2125
- useIdentify,
2126
- useIntentAI,
2127
- useSetMetadata,
2128
- useWidgetControls
2129
- });
1
+ 'use strict';var $=require('react'),jsxRuntime=require('react/jsx-runtime');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var $__default=/*#__PURE__*/_interopDefault($);var Y="https://intetnt-ai-v1-web-app-production.up.railway.app/widget/v1.js",k=null,E=false;function H(e){return E&&window.IntentAI?Promise.resolve():k||(k=new Promise((t,r)=>{if(document.querySelector(`script[src^="${Y}"]`)){window.IntentAI?(E=true,t()):window.addEventListener("intentai:ready",()=>{E=true,t();},{once:true});return}let n=document.createElement("script");n.src=Y,n.async=true,n.id="intentai-widget-script",n.setAttribute("data-api-key",e.apiKey),n.onload=()=>{window.addEventListener("intentai:ready",()=>{E=true,t();},{once:true}),setTimeout(()=>{!E&&window.IntentAI&&(E=true,t());},3e3);},n.onerror=()=>{k=null,r(new Error("Failed to load Intent AI widget script"));},document.head.appendChild(n);}),k)}var L={};var G=$.createContext(null),v=typeof window!="undefined";function j(e){if(e)return e;if(typeof process!="undefined"&&process.env){let t=process.env.NEXT_PUBLIC_INTENT_AI_KEY||process.env.REACT_APP_INTENT_AI_KEY||process.env.VITE_INTENT_AI_KEY;if(t)return t}try{if(typeof L!="undefined"&&L.env)return L.env.VITE_INTENT_AI_KEY}catch(t){}}function ee({children:e,apiKey:t,user:r,position:n="bottom-right",theme:p="auto",primaryColor:s,autoShow:a=true,metadata:I,onReady:f,onOpen:x,onClose:m,onSubmit:O,onError:C}){let[b,z]=$.useState(false),[N,F]=$.useState(null),R=$.useRef(r),l=$.useRef({onReady:f,onOpen:x,onClose:m,onSubmit:O,onError:C});l.current={onReady:f,onOpen:x,onClose:m,onSubmit:O,onError:C};let w=$.useMemo(()=>j(t),[t]);$.useEffect(()=>{w&&!w.startsWith("pk_")&&console.error('[IntentAI] Invalid API key format. Public keys should start with "pk_". Never use secret keys (sk_*) in client-side code.');},[w]),$.useEffect(()=>{if(!v)return;if(!w){console.warn("[IntentAI] No API key provided. Set the apiKey prop or NEXT_PUBLIC_INTENT_AI_KEY environment variable.");return}window.IntentAIConfig={apiKey:w,position:n,theme:p,primaryColor:s,autoShow:a,metadata:I};let d=()=>{var i,o,c;z(true),(o=(i=l.current).onReady)==null||o.call(i),R.current&&((c=window.IntentAI)==null||c.identify(R.current));},y=()=>{var i,o;(o=(i=l.current).onOpen)==null||o.call(i);},A=()=>{var i,o;(o=(i=l.current).onClose)==null||o.call(i);},W=i=>{var o,c;(c=(o=l.current).onSubmit)==null||c.call(o,i.detail);},V=i=>{var o,c;F(i.detail),(c=(o=l.current).onError)==null||c.call(o,i.detail);};return window.addEventListener("intentai:ready",d),window.addEventListener("intentai:open",y),window.addEventListener("intentai:close",A),window.addEventListener("intentai:submit",W),window.addEventListener("intentai:error",V),H({apiKey:w}).catch(i=>{var o,c;F(i),(c=(o=l.current).onError)==null||c.call(o,i);}),()=>{window.removeEventListener("intentai:ready",d),window.removeEventListener("intentai:open",y),window.removeEventListener("intentai:close",A),window.removeEventListener("intentai:submit",W),window.removeEventListener("intentai:error",V);}},[w,n,p,s,a,I]),$.useEffect(()=>{R.current=r,v&&b&&r&&window.IntentAI&&window.IntentAI.identify(r);},[r,b]);let M=$.useCallback(d=>{v&&(window.IntentAI?window.IntentAI.open(d):console.warn("[IntentAI] Widget not ready. Call will be ignored."));},[]),U=$.useCallback(()=>{var d;v&&((d=window.IntentAI)==null||d.close());},[]),B=$.useCallback(d=>{v&&(window.IntentAI?window.IntentAI.identify(d):R.current=d);},[]),K=$.useCallback((d,y)=>{var A;v&&((A=window.IntentAI)==null||A.track(d,y));},[]),S=$.useCallback(d=>{var y;v&&((y=window.IntentAI)==null||y.setMetadata(d));},[]),J=$.useMemo(()=>({isReady:b,error:N,open:M,close:U,identify:B,track:K,setMetadata:S}),[b,N,M,U,B,K,S]);return jsxRuntime.jsx(G.Provider,{value:J,children:e})}function u(){let e=$.useContext(G);if(!e)throw new Error('useIntentAI must be used within an IntentAIProvider. Wrap your app with <IntentAIProvider apiKey="pk_...">.');return e}function te(e){let{identify:t,isReady:r}=u();$.useEffect(()=>{r&&(e!=null&&e.id)&&t(e);},[r,e==null?void 0:e.id,e==null?void 0:e.email,e==null?void 0:e.name,t,e]);}var oe=$.forwardRef(function({openOptions:t,children:r="Feedback",onClick:n,disabled:p,...s},a){let{open:I,isReady:f}=u();return jsxRuntime.jsx("button",{ref:a,type:"button",onClick:m=>{n==null||n(m),m.defaultPrevented||I(t);},disabled:p||!f,"aria-label":"Open feedback",...s,children:r})});function ie({children:e,openOptions:t}){let{open:r,isReady:n,error:p}=u(),s=a=>{r(a||t);};return typeof e=="function"?e({open:s,isReady:n,error:p}):$__default.default.cloneElement(e,{onClick:a=>{var I,f;(f=(I=e.props).onClick)==null||f.call(I,a),a.defaultPrevented||s();},disabled:e.props.disabled||!n})}function de({user:e,children:t}){let{identify:r,isReady:n}=u();return $.useEffect(()=>{n&&(e!=null&&e.id)&&r(e);},[n,e,r]),jsxRuntime.jsx(jsxRuntime.Fragment,{children:t})}function ae({metadata:e,children:t}){let{setMetadata:r,isReady:n}=u();return $.useEffect(()=>{n&&e&&r(e);},[n,e,r]),jsxRuntime.jsx(jsxRuntime.Fragment,{children:t})}function se({event:e,properties:t,trigger:r="mount",children:n}){let{track:p,isReady:s}=u(),a=$__default.default.useRef(false);return $.useEffect(()=>{r==="mount"&&s&&!a.current&&(p(e,t),a.current=true);},[s,e,t,p,r]),r==="render"&&s&&p(e,t),jsxRuntime.jsx(jsxRuntime.Fragment,{children:n})}
2
+ exports.FeedbackButton=oe;exports.FeedbackTrigger=ie;exports.IdentifyUser=de;exports.IntentAIProvider=ee;exports.SetMetadata=ae;exports.TrackEvent=se;exports.useIdentify=te;exports.useIntentAI=u;