@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
JavaScript
"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