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

328 lines (307 loc) 11.8 kB
"use strict"; /** * @file EmbedButton.tsx * @description A customizable floating action button component for voice agent interactions. * Features: draggable, expandable, animated, with call controls and auto-trigger support. */ import LottieView from 'lottie-react-native'; import { useEffect, useMemo, useRef, useState } from 'react'; import { Image, Text, TouchableOpacity, View } from 'react-native'; import { GestureDetector } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import { ConnectionState } from 'livekit-client'; import { useVoiceAgent } from "../../hooks/voiceagent.js"; import { createEmbedButtonStyles } from "../styles/EmbedButton.style.js"; import { WaveformVisualizer } from "./EmbedAudioWave.js"; import Voice from "./EmbedVoice.js"; import Embed, { AgentEvent } from "../../events/embed.event.js"; // Helpers and constants import { BUTTON_DIMENSIONS, DEFAULT_GRADIENT_COLORS, formatDuration, ICON_URLS } from "../../hooks/EmbedButton.helpers.js"; // Custom hooks import { useCallDuration, useCallManagement, useConfigData, useInactivityBehavior } from "../../hooks/EmbedButton.hooks.js"; // Animation hooks import { Animated, createPanGesture, useAnimationValues, useBreathingAnimation, useButtonAnimatedStyles, useButtonAnimations, usePopupAnimatedStyles } from "../../hooks/EmbedButton.animations.js"; // ==================== STYLES CONFIG ==================== import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const defaultStyles = { buttonWidth: BUTTON_DIMENSIONS.WIDTH, buttonHeight: BUTTON_DIMENSIONS.HEIGHT, borderRadius: 100, marginBottom: 20, spacing: { SMALL: 10, MEDIUM: 15, LARGE: 25, EXTRA_SMALL: 5, EXTRA_LARGE: 35, EXTRA_EXTRA_LARGE: 45 } }; // ==================== MAIN COMPONENT ==================== /** * EmbedButton - Main voice agent floating action button * * @example * ```tsx * <EmbedButton /> * ``` */ export function EmbedButton() { // ==================== VOICE AGENT STATE ==================== const { initializeVoiceAgent, tokenDetails, endCall, isLoading, isMicMuted, muteMic, unmuteMic, connectionState, roomRef } = useVoiceAgent(); // ==================== LOCAL STATE ==================== const [isOpen, setIsOpen] = useState(false); const lottieRef = useRef(null); const styles = useMemo(() => createEmbedButtonStyles(defaultStyles), []); // ==================== CUSTOM HOOKS ==================== const configData = useConfigData(); const { callDuration, resetDuration } = useCallDuration(connectionState); const { handleStartCall, handleEndCall, handleMicToggle } = useCallManagement({ initializeVoiceAgent, endCall, muteMic, unmuteMic, isMicMuted, resetDuration, setIsOpen }); const { isAutoOpen, setIsAutoOpen } = useInactivityBehavior({ configData, isOpen, isLoading, hasActiveToken: !!tokenDetails?.token, onStartCall: handleStartCall }); // ==================== ANIMATIONS ==================== const animationValues = useAnimationValues(); const { isPressed, offset, start, menuAnimation, buttonWidth, buttonScale } = animationValues; useButtonAnimations(isOpen, menuAnimation, buttonWidth); useBreathingAnimation(isOpen, isAutoOpen, buttonScale); const buttonAnimatedStyles = useButtonAnimatedStyles(isOpen, offset, buttonWidth, isPressed, buttonScale); const popupAnimatedStyles = usePopupAnimatedStyles(offset, isPressed); const panGesture = useMemo(() => createPanGesture(isPressed, offset, start, isOpen, setIsAutoOpen), [isPressed, offset, start, isOpen, setIsAutoOpen]); // ==================== AGENT EVENT EMISSIONS ==================== // Emit agent connected/disconnected events based on connection state // Track previous connection state to properly detect disconnection const prevConnectionStateRef = useRef(connectionState); const lastEmittedEventRef = useRef(null); useEffect(() => { const prevState = prevConnectionStateRef.current; // Check if we should emit connected event if (connectionState === ConnectionState.Connected && prevState !== ConnectionState.Connected) { // Prevent duplicate connected events if (lastEmittedEventRef.current?.type === 'connected' && lastEmittedEventRef.current?.state === connectionState) { return; } // Update refs IMMEDIATELY (synchronously) before async emit prevConnectionStateRef.current = connectionState; lastEmittedEventRef.current = { type: 'connected', state: connectionState }; // Emit connected when entering Connected state Embed.event.emit(AgentEvent.AGENT_CONNECTED, { timestamp: new Date().toISOString(), metadata: { callDuration: 0 } }).catch(error => { console.error('Error emitting connected event:', error); }); } else if (prevState === ConnectionState.Connected && connectionState !== ConnectionState.Connected) { // Prevent duplicate disconnected events if (lastEmittedEventRef.current?.type === 'disconnected') { return; } // Update refs IMMEDIATELY (synchronously) before async emit prevConnectionStateRef.current = connectionState; lastEmittedEventRef.current = { type: 'disconnected', state: connectionState }; // Emit disconnected when LEAVING Connected state (to Connecting or Disconnected) // This catches the transition: Connected -> Connecting -> Disconnected Embed.event.emit(AgentEvent.AGENT_DISCONNECTED, { timestamp: new Date().toISOString(), metadata: { callDuration } }).catch(error => { console.error('Error emitting disconnected event:', error); }); } else { // Update previous state for other transitions prevConnectionStateRef.current = connectionState; } }, [connectionState, callDuration]); // Include callDuration to get fresh value // Emit popup visibility events when isAutoOpen changes useEffect(() => { const emitPopupEvent = async () => { await Embed.event.emit(AgentEvent.POPUP_MESSAGE_VISIBLE, { value: isAutoOpen, metadata: { trigger: isAutoOpen ? 'auto_inactivity' : 'manual_dismiss' } }); }; emitPopupEvent().catch(error => { console.error('Error emitting popup visibility event:', error); }); }, [isAutoOpen]); // ==================== HANDLERS ==================== const handleButtonPress = () => { setIsOpen(!isOpen); setIsAutoOpen(false); }; const handleConnected = () => { // Hook for handling successful connection }; // ==================== COMPUTED VALUES ==================== const lottieSource = useMemo(() => ({ uri: configData?.icon_animation || ICON_URLS.AMPLIFY_ANIMATION }), [configData?.icon_animation]); const statusText = useMemo(() => { if (isLoading) return 'Connecting...'; if (tokenDetails?.token) return formatDuration(callDuration); return configData?.agent_type || 'Onboarding Agent'; }, [isLoading, tokenDetails?.token, callDuration, configData?.agent_type]); const gradientColors = configData?.gradient || DEFAULT_GRADIENT_COLORS; const agentName = configData?.agent_name || 'Your AI Agent'; const popupText = configData?.popup_description || 'Any doubts? Ask agent now'; const connectButtonText = configData?.connect_button_title || 'Start Call'; // ==================== EARLY RETURNS ==================== if (!configData) return null; // ==================== RENDER ==================== return /*#__PURE__*/_jsxs(View, { style: styles.container, children: [isAutoOpen && !isOpen && /*#__PURE__*/_jsx(Animated.View, { style: [popupAnimatedStyles, styles.popupContainer], children: /*#__PURE__*/_jsx(Text, { style: styles.popupText, children: popupText }) }), /*#__PURE__*/_jsx(GestureDetector, { gesture: panGesture, children: /*#__PURE__*/_jsx(Animated.View, { pointerEvents: "auto", style: [styles.button, buttonAnimatedStyles, styles.buttonContent], children: /*#__PURE__*/_jsxs(LinearGradient, { colors: gradientColors, 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__*/_jsx(Voice, { url: tokenDetails.server_url, token: tokenDetails.token, onDisconnected: handleEndCall, roomRef: roomRef, onConnected: handleConnected }), /*#__PURE__*/_jsx(TouchableOpacity, { onPress: handleButtonPress, style: styles.pressable, children: /*#__PURE__*/_jsx(LottieView, { ref: lottieRef, source: lottieSource, autoPlay: true, loop: true, style: styles.iconImage, enableMergePathsAndroidForKitKatAndAbove: true, enableSafeModeAndroid: true }) }), isOpen && /*#__PURE__*/_jsxs(View, { style: styles.expandedContentContainer, children: [/*#__PURE__*/_jsxs(View, { style: styles.leftContentSection, children: [/*#__PURE__*/_jsx(Text, { style: [styles.agentNameText, styles.leftAlignedText], children: agentName }), /*#__PURE__*/_jsx(Text, { style: [styles.leftAlignedText, styles.statusText], children: statusText })] }), /*#__PURE__*/_jsx(View, { style: styles.middleContentSection, children: /*#__PURE__*/_jsx(WaveformVisualizer, { roomRef: roomRef }) }), /*#__PURE__*/_jsxs(View, { style: styles.rightContentSection, children: [!tokenDetails?.token && /*#__PURE__*/_jsx(View, { style: styles.buttonContainer, children: /*#__PURE__*/_jsx(TouchableOpacity, { onPress: handleStartCall, style: styles.startCallButton, children: /*#__PURE__*/_jsx(Text, { style: styles.startCallText, children: connectButtonText }) }) }), tokenDetails?.token && /*#__PURE__*/_jsxs(View, { style: styles.buttonContainer, children: [/*#__PURE__*/_jsx(TouchableOpacity, { style: styles.muteButton, onPress: handleMicToggle, children: /*#__PURE__*/_jsx(Image, { source: { uri: isMicMuted ? ICON_URLS.MIC_OFF : ICON_URLS.MIC_ON }, style: styles.buttonImage }) }), /*#__PURE__*/_jsx(TouchableOpacity, { onPress: handleEndCall, style: styles.endCallButton, children: /*#__PURE__*/_jsx(Image, { source: { uri: ICON_URLS.END_CALL }, style: styles.buttonImage }) })] })] })] })] }) }) })] }); } export default EmbedButton; //# sourceMappingURL=EmbedButton.js.map