UNPKG

@revrag-ai/embed-react-native

Version:

A powerful React Native library for integrating AI-powered voice agents into mobile applications. Features real-time voice communication, intelligent speech processing, customizable UI components, and comprehensive event handling for building conversation

477 lines (452 loc) 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmbedButton = EmbedButton; exports.default = void 0; var _lottieReactNative = _interopRequireDefault(require("lottie-react-native")); var _react = require("react"); var _reactNative = require("react-native"); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _reactNativeLinearGradient = _interopRequireDefault(require("react-native-linear-gradient")); var _voiceagent = require("../../hooks/voiceagent.js"); var _storeKey = require("../../store/store.key.js"); var _EmbedButtonStyle = require("../styles/EmbedButton.style.js"); var _reanimatedHelper = require("../../utils/reanimated.helper.js"); var _EmbedVoice = _interopRequireDefault(require("./EmbedVoice.js")); var _EmbedAudioWave = require("./EmbedAudioWave.js"); var _permision = require("../../utils/permision.js"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * @file OnwidButton.tsx * @description A customizable floating action button component for React Native applications. * This component provides a draggable, expandable button with animation support and gradient styling. */ // Get reanimated API with fallbacks const { useSharedValue, useAnimatedStyle, withTiming, withSpring, withRepeat, withSequence, runOnJS, Easing, Animated, isAvailable: isReanimatedAvailable } = (0, _reanimatedHelper.getReanimatedAPI)(); // Show warning if reanimated is not available if (!isReanimatedAvailable) { (0, _reanimatedHelper.showReanimatedSetupError)(); } const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = _reactNative.Dimensions.get('window'); const BUTTON_WIDTH = 60; const EXPANDED_WIDTH = SCREEN_WIDTH * 0.9; const BUTTON_HEIGHT = 60; const GRADIENT_COLORS = ['#1E0844', '#B391F3']; // Define mic icons as base64 images for portability const MIC_ON_ICON = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/Mute+button.png'; const MIC_OFF_ICON = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/unmute.png'; // Add end call icon const END_CALL_ICON = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/end+button.png'; const AMPLIFY_ANIMATION = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/amplify.json'; /** * Default styles configuration for the button */ const defaultStyles = { buttonWidth: 60, buttonHeight: 60, borderRadius: 100, marginBottom: 20, spacing: { SMALL: 10, MEDIUM: 15, LARGE: 25, EXTRA_SMALL: 5, EXTRA_LARGE: 35, EXTRA_EXTRA_LARGE: 45 } }; /** * OnwidButton Component * * A floating action button that can be dragged around the screen and expanded to show additional content. * Features include: * - Draggable functionality * - Expandable menu * - Animated transitions * - Gradient background * - Customizable styling * * @component * @example * ```tsx * <OnwidButton * isOpen={false} * onPress={(isOpen) => console.log('Button pressed:', isOpen)} * menuComponent={<YourMenuComponent />} * /> * ``` */ /** * Generates random sentences for testing or placeholder content * @param count - Number of sentences to generate (default: 1) * @param minWords - Minimum words per sentence (default: 5) * @param maxWords - Maximum words per sentence (default: 15) * @returns Array of random sentences */ function EmbedButton() { const { initializeVoiceAgent, tokenDetails, endCall, isLoading, isMicMuted, muteMic, unmuteMic, connectionState, roomRef, getPopupDescription } = (0, _voiceagent.useVoiceAgent)(); // State management const [configData, setConfigData] = (0, _react.useState)(null); const [popupDescription, setPopupDescription] = (0, _react.useState)(''); const [isOpen, setIsOpen] = (0, _react.useState)(false); const [callDuration, setCallDuration] = (0, _react.useState)(0); const timerRef = (0, _react.useRef)(null); const lottieRef = (0, _react.useRef)(null); // Animation values const isPressed = useSharedValue(false); const offset = useSharedValue({ x: 0, y: 0 }); const start = useSharedValue({ x: 0, y: 0 }); const menuAnimation = useSharedValue(0); const buttonWidth = useSharedValue(BUTTON_WIDTH); const buttonScale = useSharedValue(1); // Styles const styles = (0, _EmbedButtonStyle.createEmbedButtonStyles)(defaultStyles); const [isAutoOpen, setIsAutoOpen] = (0, _react.useState)(false); (0, _react.useEffect)(() => { const autoOpenTimer = setTimeout(async () => { if (!isOpen) { const response = await getPopupDescription(); if (response) { setPopupDescription(response); setIsAutoOpen(true); } else { // todo: handle no popup description found console.warn('No popup description found'); } } }, 15000); // 15 seconds return () => { clearTimeout(autoOpenTimer); }; }, [isOpen]); /** * Fetch agent configuration data */ (0, _react.useEffect)(() => { const fetchAgentData = async () => { try { const data = await (0, _storeKey.getAgentData)('@config_data'); setConfigData(data?.ui_config); } catch (error) { // todo: handle error fetching agent data } }; fetchAgentData(); }, []); /** * Set up a timer to track call duration when connected */ (0, _react.useEffect)(() => { if (connectionState === 'connected' && !timerRef.current) { timerRef.current = setInterval(() => { setCallDuration(prev => prev + 1); }, 1000); } else if (connectionState !== 'connected' && timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; // If we were previously connected and now disconnected, show an error if (callDuration > 0) { // todo: handle call disconnected } } return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }; }, [connectionState, callDuration]); /** * Handle menu animation and button width transitions */ (0, _react.useEffect)(() => { menuAnimation.value = withTiming(isOpen ? 0.8 : 0, { duration: 300 }); buttonWidth.value = withTiming(isOpen ? EXPANDED_WIDTH : BUTTON_WIDTH); }, [isOpen, menuAnimation, buttonWidth]); // Add breathing animation when button is closed but isAutoOpen is true (0, _react.useEffect)(() => { if (!isOpen && isAutoOpen) { // Start breathing animation with faster speed buttonScale.value = withRepeat(withSequence(withTiming(1.1, { duration: 1500, // Reduced from 1000ms to 600ms easing: Easing.inOut(Easing.ease) }), withTiming(1, { duration: 1500, // Reduced from 1000ms to 600ms easing: Easing.inOut(Easing.ease) })), -1, // Infinite repeat false // Don't reverse ); } else { // Reset animation buttonScale.value = withTiming(1, { duration: 300 }); } }, [isOpen, isAutoOpen]); /** * Animated styles for the button */ const animatedStyles = useAnimatedStyle(() => { const maxX = SCREEN_WIDTH - (isOpen ? EXPANDED_WIDTH : BUTTON_WIDTH) - 35; const clampedX = Math.min(Math.max(offset.value.x, -maxX), 0); return { width: buttonWidth.value, height: BUTTON_HEIGHT, transform: [{ translateX: clampedX }, { translateY: offset.value.y }, { scale: withSpring(isPressed.value ? 0.95 : buttonScale.value) }], justifyContent: isOpen ? 'space-between' : 'flex-start', overflow: 'hidden' }; }); /** * Animated styles for the text */ const animatedTextStyles = useAnimatedStyle(() => { const maxX = SCREEN_WIDTH; const clampedX = Math.min(Math.max(offset.value.x, -maxX), 0); return { transform: [{ translateX: clampedX }, { translateY: offset.value.y }, { scale: withSpring(isPressed.value ? 1 : 1) }] }; }); /** * Pan gesture handler for drag functionality */ const gesture = _reactNativeGestureHandler.Gesture.Pan().onBegin(() => { isPressed.value = true; if (isAutoOpen) { runOnJS(setIsAutoOpen)(false); } }).onUpdate(e => { const maxX = SCREEN_WIDTH - (isOpen ? EXPANDED_WIDTH : BUTTON_WIDTH) - 0; const newX = Math.min(Math.max(e.translationX + start.value.x, -maxX), 0); const maxY = SCREEN_HEIGHT - 150; const newY = Math.min(Math.max(e.translationY + start.value.y, -maxY), 0); offset.value = { x: newX, y: newY }; }).onEnd(() => { start.value = { x: offset.value.x, y: offset.value.y }; }).onFinalize(() => { isPressed.value = false; }); /** * Handle button press events */ const handlePress = () => { setIsOpen(!isOpen); setIsAutoOpen(false); }; const handleStartCall = async () => { await (0, _permision.checkPermissions)(); setCallDuration(0); await initializeVoiceAgent(); }; /** * Render the button icon/animation */ const remoteSource = (0, _react.useMemo)(() => ({ uri: configData?.icon_animation || AMPLIFY_ANIMATION }), [configData?.icon_animation]); const renderIcon = () => { // When isAutoOpen is true, we don't play the Lottie animation return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_lottieReactNative.default, { ref: lottieRef, source: remoteSource, autoPlay: true, loop: true, style: styles.iconImage, enableMergePathsAndroidForKitKatAndAbove: true, enableSafeModeAndroid: true }) }); }; const handleConnected = () => { // todo: handle call connected }; const handleEndCall = async () => { setIsOpen(false); if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } setCallDuration(0); await endCall(); }; const handleMicToggle = () => { if (isMicMuted) { unmuteMic(); } else { muteMic(); } }; // Format duration to MM:SS const formatDuration = seconds => { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; }; // Get the status text based on current state const getStatusText = () => { if (isLoading) { return 'Connecting...'; } else if (tokenDetails?.token) { return `${formatDuration(callDuration)}`; } else { return configData?.agent_type || 'Onboarding Agent'; } }; if (!configData) return null; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.container, children: [isAutoOpen && !isOpen && /*#__PURE__*/(0, _jsxRuntime.jsx)(Animated.View, { style: [animatedTextStyles, styles.popupContainer], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.popupText, children: popupDescription || 'Revrag' }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, { gesture: gesture, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Animated.View, { style: [styles.button, animatedStyles, styles.buttonContent, { pointerEvents: 'auto' }], children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeLinearGradient.default, { colors: configData?.gradient || GRADIENT_COLORS, start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [styles.linearGradient, isOpen ? styles.expandedLinearGradient : styles.collapsedLinearGradient], angle: 0, angleCenter: { x: 0.5, y: 0.5 }, children: [tokenDetails?.token && /*#__PURE__*/(0, _jsxRuntime.jsx)(_EmbedVoice.default, { url: tokenDetails?.server_url, token: tokenDetails?.token, onDisconnected: handleEndCall, roomRef: roomRef, onConnected: handleConnected }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { onPress: handlePress, style: styles.pressable, children: renderIcon() }), isOpen && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.expandedContentContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.leftContentSection, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.agentNameText, styles.leftAlignedText], children: configData?.agent_name || 'Revrag' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.leftAlignedText, styles.statusText], children: getStatusText() })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.middleContentSection, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_EmbedAudioWave.WaveformVisualizer, { roomRef: roomRef }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.rightContentSection, children: [!tokenDetails?.token && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.buttonContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { onPress: handleStartCall, style: styles.startCallButton, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.startCallText, children: configData?.start_call_text || 'Start Call' }) }) }), tokenDetails?.token && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.buttonContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.muteButton, onPress: handleMicToggle, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: isMicMuted ? MIC_OFF_ICON : MIC_ON_ICON }, style: styles.buttonImage }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { onPress: handleEndCall, style: styles.endCallButton, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, { source: { uri: END_CALL_ICON }, style: styles.buttonImage }) })] })] })] })] }) }) })] }); } // Export default for easier imports var _default = exports.default = EmbedButton; //# sourceMappingURL=EmbedButton.js.map