@intentai/react 2.1.1 → 2.2.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.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/index.tsx
4
- import { useEffect as useEffect2, useRef as useRef2, useCallback as useCallback2 } from "react";
4
+ import { useEffect as useEffect3, useRef as useRef4, useCallback as useCallback4 } from "react";
5
5
 
6
6
  // src/components/PremiumVoiceWidget.tsx
7
7
  import {
@@ -98,15 +98,33 @@ var useAudioLevel = (isRecording) => {
98
98
  const [audioLevel, setAudioLevel] = useState(0);
99
99
  const analyserRef = useRef(null);
100
100
  const animationRef = useRef();
101
+ const streamRef = useRef(null);
102
+ const audioContextRef = useRef(null);
101
103
  useEffect(() => {
102
104
  if (!isRecording) {
103
105
  setAudioLevel(0);
106
+ if (animationRef.current) {
107
+ cancelAnimationFrame(animationRef.current);
108
+ animationRef.current = void 0;
109
+ }
110
+ if (streamRef.current) {
111
+ streamRef.current.getTracks().forEach((track) => track.stop());
112
+ streamRef.current = null;
113
+ }
114
+ if (audioContextRef.current && audioContextRef.current.state !== "closed") {
115
+ audioContextRef.current.close().catch(() => {
116
+ });
117
+ audioContextRef.current = null;
118
+ }
119
+ analyserRef.current = null;
104
120
  return;
105
121
  }
106
122
  const initAudio = async () => {
107
123
  try {
108
124
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
125
+ streamRef.current = stream;
109
126
  const audioContext = new AudioContext();
127
+ audioContextRef.current = audioContext;
110
128
  const source = audioContext.createMediaStreamSource(stream);
111
129
  const analyser = audioContext.createAnalyser();
112
130
  analyser.fftSize = 256;
@@ -130,6 +148,13 @@ var useAudioLevel = (isRecording) => {
130
148
  if (animationRef.current) {
131
149
  cancelAnimationFrame(animationRef.current);
132
150
  }
151
+ if (streamRef.current) {
152
+ streamRef.current.getTracks().forEach((track) => track.stop());
153
+ }
154
+ if (audioContextRef.current && audioContextRef.current.state !== "closed") {
155
+ audioContextRef.current.close().catch(() => {
156
+ });
157
+ }
133
158
  };
134
159
  }, [isRecording]);
135
160
  return audioLevel;
@@ -349,12 +374,12 @@ var AmbientOrb = ({ audioLevel, isRecording }) => {
349
374
  ]
350
375
  }
351
376
  ) }),
352
- particles.map((particle) => /* @__PURE__ */ jsx(
377
+ particles.filter((p) => p.size != null).map((particle) => /* @__PURE__ */ jsx(
353
378
  motion.circle,
354
379
  {
355
380
  cx: 70,
356
381
  cy: 70,
357
- r: particle.size,
382
+ r: particle.size || 3,
358
383
  fill: particle.color,
359
384
  initial: { opacity: 0.8, scale: 1 },
360
385
  animate: {
@@ -1601,6 +1626,359 @@ function adjustColor(color, amount) {
1601
1626
  return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
1602
1627
  }
1603
1628
 
1629
+ // src/components/IntentAIProvider.tsx
1630
+ import { createContext as createContext2, useContext as useContext2, useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useEffect as useEffect2 } from "react";
1631
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1632
+ var IntentAIContext = createContext2(null);
1633
+ function useIntentAI() {
1634
+ const context = useContext2(IntentAIContext);
1635
+ if (!context) {
1636
+ throw new Error("useIntentAI must be used within an IntentAIProvider");
1637
+ }
1638
+ return context;
1639
+ }
1640
+ function generateSessionId() {
1641
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1642
+ }
1643
+ function getDeviceInfo() {
1644
+ const ua = navigator.userAgent;
1645
+ let browser = "Unknown";
1646
+ let browserVersion = "Unknown";
1647
+ if (ua.includes("Firefox/")) {
1648
+ browser = "Firefox";
1649
+ const match = ua.match(/Firefox\/(\d+(\.\d+)?)/);
1650
+ if (match) browserVersion = match[1];
1651
+ } else if (ua.includes("Edg/")) {
1652
+ browser = "Edge";
1653
+ const match = ua.match(/Edg\/(\d+(\.\d+)?)/);
1654
+ if (match) browserVersion = match[1];
1655
+ } else if (ua.includes("Chrome/")) {
1656
+ browser = "Chrome";
1657
+ const match = ua.match(/Chrome\/(\d+(\.\d+)?)/);
1658
+ if (match) browserVersion = match[1];
1659
+ } else if (ua.includes("Safari/") && !ua.includes("Chrome")) {
1660
+ browser = "Safari";
1661
+ const match = ua.match(/Version\/(\d+(\.\d+)?)/);
1662
+ if (match) browserVersion = match[1];
1663
+ }
1664
+ let os = "Unknown";
1665
+ if (ua.includes("Windows NT 10")) os = "Windows 10";
1666
+ else if (ua.includes("Windows")) os = "Windows";
1667
+ else if (ua.includes("Mac OS X")) os = "macOS";
1668
+ else if (ua.includes("Android")) os = "Android";
1669
+ else if (ua.includes("iPhone") || ua.includes("iPad")) os = "iOS";
1670
+ else if (ua.includes("Linux")) os = "Linux";
1671
+ let device = "Desktop";
1672
+ if (/iPhone|iPad|iPod/.test(ua)) device = "iOS Device";
1673
+ else if (/Android.*Mobile/.test(ua)) device = "Android Phone";
1674
+ else if (/Android/.test(ua)) device = "Android Tablet";
1675
+ return { browser, browserVersion, os, device };
1676
+ }
1677
+ var DEFAULT_API_URL = "https://api-production-5b88.up.railway.app";
1678
+ function IntentAIProvider({
1679
+ apiKey,
1680
+ apiUrl = DEFAULT_API_URL,
1681
+ user: initialUser,
1682
+ metadata: initialMetadata,
1683
+ widgetConfig,
1684
+ children,
1685
+ disableWidget = false,
1686
+ onFeedbackSubmitted,
1687
+ onError
1688
+ }) {
1689
+ const userRef = useRef2(initialUser);
1690
+ const metadataRef = useRef2(initialMetadata || {});
1691
+ const sessionIdRef = useRef2(generateSessionId());
1692
+ useEffect2(() => {
1693
+ if (initialUser) {
1694
+ userRef.current = initialUser;
1695
+ }
1696
+ }, [initialUser]);
1697
+ useEffect2(() => {
1698
+ if (initialMetadata) {
1699
+ metadataRef.current = { ...metadataRef.current, ...initialMetadata };
1700
+ }
1701
+ }, [initialMetadata]);
1702
+ const identify = useCallback2((user) => {
1703
+ userRef.current = { ...userRef.current, ...user };
1704
+ }, []);
1705
+ const setMetadata = useCallback2((metadata) => {
1706
+ metadataRef.current = { ...metadataRef.current, ...metadata };
1707
+ }, []);
1708
+ const clearUser = useCallback2(() => {
1709
+ userRef.current = void 0;
1710
+ }, []);
1711
+ const handleSubmit = useCallback2(async (feedback) => {
1712
+ const deviceInfo = getDeviceInfo();
1713
+ const user = userRef.current;
1714
+ const customMetadata = metadataRef.current;
1715
+ const categoryMap = {
1716
+ bug: "BUG",
1717
+ feature: "FEATURE",
1718
+ improvement: "IMPROVEMENT",
1719
+ praise: "PRAISE",
1720
+ other: "OTHER"
1721
+ };
1722
+ const payload = {
1723
+ sessionId: sessionIdRef.current,
1724
+ type: feedback.type === "voice" ? "VOICE" : "TEXT",
1725
+ category: categoryMap[feedback.category.toLowerCase()] || feedback.category.toUpperCase(),
1726
+ rawText: feedback.type === "text" ? feedback.content : void 0,
1727
+ transcription: feedback.type === "voice" ? feedback.transcription || feedback.content : void 0,
1728
+ pageUrl: typeof window !== "undefined" ? window.location.href : "",
1729
+ pageTitle: typeof document !== "undefined" ? document.title : "",
1730
+ userId: user?.id,
1731
+ userEmail: user?.email,
1732
+ userName: user?.name,
1733
+ customData: {
1734
+ ...customMetadata,
1735
+ ...feedback.metadata
1736
+ },
1737
+ browser: deviceInfo.browser,
1738
+ browserVersion: deviceInfo.browserVersion,
1739
+ os: deviceInfo.os,
1740
+ device: deviceInfo.device,
1741
+ screenWidth: typeof window !== "undefined" ? window.screen.width : void 0,
1742
+ screenHeight: typeof window !== "undefined" ? window.screen.height : void 0
1743
+ };
1744
+ try {
1745
+ const response = await fetch(`${apiUrl}/feedback`, {
1746
+ method: "POST",
1747
+ headers: {
1748
+ "Content-Type": "application/json",
1749
+ "X-API-Key": apiKey
1750
+ },
1751
+ body: JSON.stringify(payload)
1752
+ });
1753
+ if (!response.ok) {
1754
+ const errorData = await response.json().catch(() => ({}));
1755
+ throw new Error(errorData.error || `Failed to submit feedback: ${response.status}`);
1756
+ }
1757
+ onFeedbackSubmitted?.();
1758
+ } catch (error) {
1759
+ const err = error instanceof Error ? error : new Error("Failed to submit feedback");
1760
+ onError?.(err);
1761
+ throw err;
1762
+ }
1763
+ }, [apiKey, apiUrl, onFeedbackSubmitted, onError]);
1764
+ const contextValue = useMemo2(() => ({
1765
+ identify,
1766
+ setMetadata,
1767
+ clearUser,
1768
+ isConfigured: Boolean(apiKey)
1769
+ }), [identify, setMetadata, clearUser, apiKey]);
1770
+ return /* @__PURE__ */ jsxs2(IntentAIContext.Provider, { value: contextValue, children: [
1771
+ children,
1772
+ !disableWidget && apiKey && /* @__PURE__ */ jsx2(
1773
+ PremiumVoiceWidget,
1774
+ {
1775
+ ...widgetConfig,
1776
+ onSubmit: handleSubmit
1777
+ }
1778
+ )
1779
+ ] });
1780
+ }
1781
+ function useIdentify() {
1782
+ const { identify } = useIntentAI();
1783
+ return identify;
1784
+ }
1785
+ function useSetMetadata() {
1786
+ const { setMetadata } = useIntentAI();
1787
+ return setMetadata;
1788
+ }
1789
+
1790
+ // src/components/FeedbackButton.tsx
1791
+ import { useCallback as useCallback3, useState as useState2, useRef as useRef3 } from "react";
1792
+ import { motion as motion2 } from "framer-motion";
1793
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1794
+ var FeedbackIcon = ({ size }) => /* @__PURE__ */ jsxs3(
1795
+ "svg",
1796
+ {
1797
+ width: size,
1798
+ height: size,
1799
+ viewBox: "0 0 24 24",
1800
+ fill: "none",
1801
+ stroke: "currentColor",
1802
+ strokeWidth: "2",
1803
+ strokeLinecap: "round",
1804
+ strokeLinejoin: "round",
1805
+ children: [
1806
+ /* @__PURE__ */ jsx3("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }),
1807
+ /* @__PURE__ */ jsx3("circle", { cx: "9", cy: "10", r: "1", fill: "currentColor" }),
1808
+ /* @__PURE__ */ jsx3("circle", { cx: "15", cy: "10", r: "1", fill: "currentColor" })
1809
+ ]
1810
+ }
1811
+ );
1812
+ function FeedbackButton({
1813
+ variant = "primary",
1814
+ size = "md",
1815
+ showIcon = true,
1816
+ icon,
1817
+ label = "Feedback",
1818
+ className,
1819
+ style,
1820
+ primaryColor = "#10b981",
1821
+ onClick,
1822
+ disabled = false
1823
+ }) {
1824
+ const [isHovered, setIsHovered] = useState2(false);
1825
+ const [isPressed, setIsPressed] = useState2(false);
1826
+ const buttonRef = useRef3(null);
1827
+ const sizes = {
1828
+ sm: {
1829
+ padding: "8px 14px",
1830
+ fontSize: 13,
1831
+ iconSize: 14,
1832
+ gap: 6,
1833
+ borderRadius: 8
1834
+ },
1835
+ md: {
1836
+ padding: "10px 18px",
1837
+ fontSize: 14,
1838
+ iconSize: 16,
1839
+ gap: 8,
1840
+ borderRadius: 10
1841
+ },
1842
+ lg: {
1843
+ padding: "14px 24px",
1844
+ fontSize: 15,
1845
+ iconSize: 18,
1846
+ gap: 10,
1847
+ borderRadius: 12
1848
+ }
1849
+ };
1850
+ const sizeConfig = sizes[size];
1851
+ const variants = {
1852
+ primary: {
1853
+ background: `linear-gradient(145deg, ${primaryColor} 0%, ${adjustColor2(primaryColor, -20)} 100%)`,
1854
+ color: "white",
1855
+ border: "none",
1856
+ boxShadow: `0 4px 16px -4px ${primaryColor}66, 0 2px 4px rgba(0, 0, 0, 0.1)`
1857
+ },
1858
+ secondary: {
1859
+ background: "white",
1860
+ color: "#374151",
1861
+ border: "1px solid #e5e7eb",
1862
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)"
1863
+ },
1864
+ ghost: {
1865
+ background: "transparent",
1866
+ color: "#6b7280",
1867
+ border: "1px solid transparent",
1868
+ boxShadow: "none"
1869
+ },
1870
+ minimal: {
1871
+ background: "transparent",
1872
+ color: primaryColor,
1873
+ border: "none",
1874
+ boxShadow: "none"
1875
+ }
1876
+ };
1877
+ const variantStyle = variants[variant];
1878
+ const handleClick = useCallback3(() => {
1879
+ if (disabled) return;
1880
+ if (onClick) {
1881
+ onClick();
1882
+ return;
1883
+ }
1884
+ const widget = document.querySelector(".fiq-premium-widget");
1885
+ if (widget) {
1886
+ const trigger = widget.querySelector('button[aria-label="Open feedback widget"]');
1887
+ if (trigger) {
1888
+ trigger.click();
1889
+ return;
1890
+ }
1891
+ }
1892
+ const scriptWidget = document.getElementById("feedbackiq-widget");
1893
+ if (scriptWidget?.shadowRoot) {
1894
+ const trigger = scriptWidget.shadowRoot.querySelector(".fiq-trigger");
1895
+ if (trigger) {
1896
+ trigger.click();
1897
+ }
1898
+ }
1899
+ }, [onClick, disabled]);
1900
+ return /* @__PURE__ */ jsxs3(
1901
+ motion2.button,
1902
+ {
1903
+ ref: buttonRef,
1904
+ onClick: handleClick,
1905
+ onMouseEnter: () => setIsHovered(true),
1906
+ onMouseLeave: () => {
1907
+ setIsHovered(false);
1908
+ setIsPressed(false);
1909
+ },
1910
+ onMouseDown: () => setIsPressed(true),
1911
+ onMouseUp: () => setIsPressed(false),
1912
+ disabled,
1913
+ animate: {
1914
+ scale: isPressed ? 0.97 : isHovered ? 1.02 : 1,
1915
+ y: isHovered ? -1 : 0
1916
+ },
1917
+ transition: {
1918
+ type: "spring",
1919
+ stiffness: 400,
1920
+ damping: 25
1921
+ },
1922
+ className,
1923
+ style: {
1924
+ display: "inline-flex",
1925
+ alignItems: "center",
1926
+ justifyContent: "center",
1927
+ gap: sizeConfig.gap,
1928
+ padding: sizeConfig.padding,
1929
+ fontSize: sizeConfig.fontSize,
1930
+ fontWeight: 600,
1931
+ fontFamily: "system-ui, -apple-system, sans-serif",
1932
+ borderRadius: sizeConfig.borderRadius,
1933
+ cursor: disabled ? "not-allowed" : "pointer",
1934
+ opacity: disabled ? 0.5 : 1,
1935
+ transition: "all 0.2s ease",
1936
+ position: "relative",
1937
+ overflow: "hidden",
1938
+ ...variantStyle,
1939
+ ...style
1940
+ },
1941
+ children: [
1942
+ variant === "primary" && /* @__PURE__ */ jsx3(
1943
+ motion2.div,
1944
+ {
1945
+ initial: { opacity: 0 },
1946
+ animate: { opacity: isHovered ? 0.1 : 0 },
1947
+ style: {
1948
+ position: "absolute",
1949
+ inset: 0,
1950
+ background: "white",
1951
+ pointerEvents: "none"
1952
+ }
1953
+ }
1954
+ ),
1955
+ variant === "primary" && /* @__PURE__ */ jsx3(
1956
+ "div",
1957
+ {
1958
+ style: {
1959
+ position: "absolute",
1960
+ inset: 0,
1961
+ background: "linear-gradient(180deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 50%)",
1962
+ borderRadius: "inherit",
1963
+ pointerEvents: "none"
1964
+ }
1965
+ }
1966
+ ),
1967
+ showIcon && /* @__PURE__ */ jsx3("span", { style: { display: "flex", alignItems: "center", position: "relative", zIndex: 1 }, children: icon || /* @__PURE__ */ jsx3(FeedbackIcon, { size: sizeConfig.iconSize }) }),
1968
+ label && /* @__PURE__ */ jsx3("span", { style: { position: "relative", zIndex: 1 }, children: label })
1969
+ ]
1970
+ }
1971
+ );
1972
+ }
1973
+ function adjustColor2(color, amount) {
1974
+ const hex = color.replace("#", "");
1975
+ const num = parseInt(hex, 16);
1976
+ const r = Math.min(255, Math.max(0, (num >> 16) + amount));
1977
+ const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
1978
+ const b = Math.min(255, Math.max(0, (num & 255) + amount));
1979
+ return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
1980
+ }
1981
+
1604
1982
  // src/index.tsx
1605
1983
  function IntentAIWidget({
1606
1984
  apiKey,
@@ -1619,9 +1997,9 @@ function IntentAIWidget({
1619
1997
  onFeedbackSubmitted: _onFeedbackSubmitted,
1620
1998
  onError
1621
1999
  }) {
1622
- const widgetRef = useRef2(null);
1623
- const scriptLoadedRef = useRef2(false);
1624
- const initWidget = useCallback2(() => {
2000
+ const widgetRef = useRef4(null);
2001
+ const scriptLoadedRef = useRef4(false);
2002
+ const initWidget = useCallback4(() => {
1625
2003
  if (!window.FeedbackIQ || widgetRef.current) return;
1626
2004
  try {
1627
2005
  widgetRef.current = new window.FeedbackIQ({
@@ -1644,7 +2022,7 @@ function IntentAIWidget({
1644
2022
  void _onClose;
1645
2023
  void _onFeedbackSubmitted;
1646
2024
  }, [apiKey, apiUrl, widgetUrl, position, theme, primaryColor, allowVoice, allowText, allowScreenshot, customMetadata, user, onError]);
1647
- useEffect2(() => {
2025
+ useEffect3(() => {
1648
2026
  if (!widgetUrl) {
1649
2027
  onError?.(new Error("widgetUrl is required for IntentAIWidget"));
1650
2028
  return;
@@ -1675,39 +2053,39 @@ function IntentAIWidget({
1675
2053
  }
1676
2054
  };
1677
2055
  }, [initWidget, onError, widgetUrl]);
1678
- useEffect2(() => {
2056
+ useEffect3(() => {
1679
2057
  if (widgetRef.current && user) {
1680
2058
  widgetRef.current.identify(user);
1681
2059
  }
1682
2060
  }, [user]);
1683
- useEffect2(() => {
2061
+ useEffect3(() => {
1684
2062
  if (widgetRef.current && customMetadata) {
1685
2063
  widgetRef.current.setMetadata(customMetadata);
1686
2064
  }
1687
2065
  }, [customMetadata]);
1688
2066
  return null;
1689
2067
  }
1690
- function useIntentAI() {
1691
- const open = useCallback2(() => {
2068
+ function useWidgetControls() {
2069
+ const open = useCallback4(() => {
1692
2070
  const widget = document.getElementById("feedbackiq-widget");
1693
2071
  if (widget) {
1694
2072
  const trigger = widget.shadowRoot?.querySelector(".fiq-trigger");
1695
2073
  trigger?.click();
1696
2074
  }
1697
2075
  }, []);
1698
- const close = useCallback2(() => {
2076
+ const close = useCallback4(() => {
1699
2077
  const widget = document.getElementById("feedbackiq-widget");
1700
2078
  if (widget) {
1701
2079
  const closeBtn = widget.shadowRoot?.querySelector(".fiq-close");
1702
2080
  closeBtn?.click();
1703
2081
  }
1704
2082
  }, []);
1705
- const identify = useCallback2((user) => {
2083
+ const identify = useCallback4((user) => {
1706
2084
  if (window.IntentAI?.widget) {
1707
2085
  window.IntentAI.widget.identify(user);
1708
2086
  }
1709
2087
  }, []);
1710
- const setMetadata = useCallback2((metadata) => {
2088
+ const setMetadata = useCallback4((metadata) => {
1711
2089
  if (window.IntentAI?.widget) {
1712
2090
  window.IntentAI.widget.setMetadata(metadata);
1713
2091
  }
@@ -1716,8 +2094,13 @@ function useIntentAI() {
1716
2094
  }
1717
2095
  var index_default = IntentAIWidget;
1718
2096
  export {
2097
+ FeedbackButton,
2098
+ IntentAIProvider,
1719
2099
  IntentAIWidget,
1720
2100
  PremiumVoiceWidget,
1721
2101
  index_default as default,
1722
- useIntentAI
2102
+ useIdentify,
2103
+ useIntentAI,
2104
+ useSetMetadata,
2105
+ useWidgetControls
1723
2106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentai/react",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "React component for Intent AI feedback widget - easily collect user feedback with AI-powered analysis",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",