254 lines
5.8 KiB
TypeScript
254 lines
5.8 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Animated,
|
|
Easing,
|
|
View,
|
|
Text,
|
|
Image,
|
|
} from 'react-native';
|
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
|
import { colors, spacing, borderRadius, typography, shadows, iconSizes } from '../theme';
|
|
import { ChatbotModal } from './ChatbotModal';
|
|
|
|
const logo = require('../assets/ic_launcher_foreground.png');
|
|
|
|
export const FloatingAssistantButton: React.FC = () => {
|
|
const [scaleValue] = useState(new Animated.Value(1));
|
|
const [pulseValue] = useState(new Animated.Value(1));
|
|
const [glowValue] = useState(new Animated.Value(0));
|
|
const [isChatbotVisible, setIsChatbotVisible] = useState(false);
|
|
const [showLabel, setShowLabel] = useState(true);
|
|
|
|
useEffect(() => {
|
|
// Continuous pulse animation
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseValue, {
|
|
toValue: 1.15,
|
|
duration: 1500,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseValue, {
|
|
toValue: 1,
|
|
duration: 1500,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: true,
|
|
}),
|
|
])
|
|
).start();
|
|
|
|
// Glow animation
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(glowValue, {
|
|
toValue: 1,
|
|
duration: 2000,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(glowValue, {
|
|
toValue: 0,
|
|
duration: 2000,
|
|
easing: Easing.inOut(Easing.ease),
|
|
useNativeDriver: true,
|
|
}),
|
|
])
|
|
).start();
|
|
|
|
// Hide label after 5 seconds
|
|
const timer = setTimeout(() => {
|
|
setShowLabel(false);
|
|
}, 5000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [pulseValue, glowValue]);
|
|
|
|
const handlePressIn = () => {
|
|
Animated.spring(scaleValue, {
|
|
toValue: 0.9,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
};
|
|
|
|
const handlePressOut = () => {
|
|
Animated.spring(scaleValue, {
|
|
toValue: 1,
|
|
friction: 3,
|
|
tension: 40,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
};
|
|
|
|
const handlePress = () => {
|
|
setIsChatbotVisible(true);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Pulse ring */}
|
|
<Animated.View
|
|
style={[
|
|
styles.pulseRing,
|
|
{
|
|
transform: [{ scale: pulseValue }],
|
|
opacity: pulseValue.interpolate({
|
|
inputRange: [1, 1.15],
|
|
outputRange: [0.4, 0],
|
|
}),
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* Glow effect */}
|
|
<Animated.View
|
|
style={[
|
|
styles.glowRing,
|
|
{
|
|
opacity: glowValue.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0.2, 0.6],
|
|
}),
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* Label */}
|
|
{showLabel && (
|
|
<Animated.View style={styles.labelContainer}>
|
|
<View style={styles.label}>
|
|
<Text style={styles.labelText}>Ask AI Assistant</Text>
|
|
<View style={styles.labelArrow} />
|
|
</View>
|
|
</Animated.View>
|
|
)}
|
|
|
|
{/* Main button */}
|
|
<Animated.View
|
|
style={[
|
|
styles.buttonContainer,
|
|
{
|
|
transform: [{ scale: scaleValue }],
|
|
},
|
|
]}
|
|
>
|
|
<TouchableOpacity
|
|
style={styles.button}
|
|
onPress={handlePress}
|
|
onPressIn={handlePressIn}
|
|
onPressOut={handlePressOut}
|
|
activeOpacity={0.9}
|
|
>
|
|
<Image source={logo} style={styles.buttonLogo} resizeMode="contain" />
|
|
{/* Microphone indicator */}
|
|
<View style={styles.micIndicator}>
|
|
<MaterialCommunityIcons
|
|
name="microphone"
|
|
size={12}
|
|
color={colors.background}
|
|
/>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</Animated.View>
|
|
|
|
{/* Chatbot Modal */}
|
|
<ChatbotModal
|
|
visible={isChatbotVisible}
|
|
onClose={() => setIsChatbotVisible(false)}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
pulseRing: {
|
|
position: 'absolute',
|
|
bottom: 90,
|
|
right: 20,
|
|
width: 76,
|
|
height: 76,
|
|
borderRadius: 38,
|
|
backgroundColor: colors.highlight,
|
|
},
|
|
glowRing: {
|
|
position: 'absolute',
|
|
bottom: 85,
|
|
right: 15,
|
|
width: 86,
|
|
height: 86,
|
|
borderRadius: 43,
|
|
backgroundColor: colors.highlight,
|
|
},
|
|
labelContainer: {
|
|
position: 'absolute',
|
|
bottom: 100,
|
|
right: 100,
|
|
},
|
|
label: {
|
|
backgroundColor: colors.surfaceAlt,
|
|
paddingHorizontal: spacing.lg,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.lg,
|
|
borderWidth: 2,
|
|
borderColor: colors.border,
|
|
...shadows.md,
|
|
},
|
|
labelText: {
|
|
...typography.caption,
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
color: colors.highlightAlt,
|
|
},
|
|
labelArrow: {
|
|
position: 'absolute',
|
|
top: '50%',
|
|
right: -6,
|
|
width: 12,
|
|
height: 12,
|
|
marginTop: -6,
|
|
backgroundColor: colors.surfaceAlt,
|
|
borderRightWidth: 2,
|
|
borderBottomWidth: 2,
|
|
borderColor: colors.border,
|
|
transform: [{ rotate: '-45deg' }],
|
|
},
|
|
buttonContainer: {
|
|
position: 'absolute',
|
|
bottom: 90,
|
|
right: 20,
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 36,
|
|
},
|
|
button: {
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 36,
|
|
backgroundColor: colors.highlight,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 3,
|
|
borderColor: colors.highlightAlt,
|
|
...shadows.glowStrong,
|
|
},
|
|
buttonLogo: {
|
|
width: 48,
|
|
height: 48,
|
|
},
|
|
micIndicator: {
|
|
position: 'absolute',
|
|
bottom: 4,
|
|
right: 4,
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
backgroundColor: '#4ADE80',
|
|
borderWidth: 2,
|
|
borderColor: colors.background,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
});
|