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