|
|
import React, { useState, useRef, useEffect } from 'react'; |
|
|
import "./global.css" |
|
|
import { |
|
|
View, |
|
|
Text, |
|
|
TextInput, |
|
|
ScrollView, |
|
|
Pressable, |
|
|
SafeAreaView, |
|
|
Animated, |
|
|
Keyboard, |
|
|
TouchableWithoutFeedback, |
|
|
TouchableOpacity, |
|
|
Platform, |
|
|
KeyboardAvoidingView, |
|
|
Alert, |
|
|
Image, |
|
|
Dimensions, |
|
|
ActivityIndicator |
|
|
} from 'react-native'; |
|
|
import { StatusBar } from 'expo-status-bar'; |
|
|
import { FontAwesome5, Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; |
|
|
import Markdown from 'react-native-markdown-display'; |
|
|
import * as Clipboard from 'expo-clipboard'; |
|
|
import { marked } from 'marked'; |
|
|
import highlightjs from 'highlight.js/lib/core'; |
|
|
import javascript from 'highlight.js/lib/languages/javascript'; |
|
|
import python from 'highlight.js/lib/languages/python'; |
|
|
import { Link, router, useRouter, Stack } from 'expo-router'; |
|
|
import Constants from 'expo-constants'; |
|
|
import * as Haptics from 'expo-haptics'; |
|
|
|
|
|
|
|
|
highlightjs.registerLanguage('javascript', javascript); |
|
|
highlightjs.registerLanguage('python', python); |
|
|
|
|
|
|
|
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); |
|
|
|
|
|
|
|
|
const SUGGESTED_QUESTIONS = [ |
|
|
"What are common symptoms of PCOS?", |
|
|
"How can I manage menstrual pain naturally?", |
|
|
"What should I know before pregnancy planning?", |
|
|
"Is my period flow normal?", |
|
|
"Why am I experiencing fatigue during periods?" |
|
|
]; |
|
|
|
|
|
const FilleAI = () => { |
|
|
const router = useRouter(); |
|
|
|
|
|
const [isSearchSubmitted, setIsSearchSubmitted] = useState(false); |
|
|
const [isLoading, setIsLoading] = useState(false); |
|
|
const [messages, setMessages] = useState<{ type: string; text: string }[]>([]); |
|
|
const [inputText, setInputText] = useState(''); |
|
|
const [inputHeight, setInputHeight] = useState(40); |
|
|
const [suggestedQuestions, setSuggestedQuestions] = useState(SUGGESTED_QUESTIONS); |
|
|
const scrollViewRef = useRef<ScrollView>(null); |
|
|
const inputRef = useRef<TextInput>(null); |
|
|
|
|
|
|
|
|
const welcomeOpacity = useRef(new Animated.Value(1)).current; |
|
|
const centerContentMargin = useRef(new Animated.Value(150)).current; |
|
|
const floatingButtonScale = useRef(new Animated.Value(1)).current; |
|
|
const inputContainerAnimation = useRef(new Animated.Value(0)).current; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const pulsate = Animated.loop( |
|
|
Animated.sequence([ |
|
|
Animated.timing(floatingButtonScale, { |
|
|
toValue: 1.05, |
|
|
duration: 1000, |
|
|
useNativeDriver: true, |
|
|
}), |
|
|
Animated.timing(floatingButtonScale, { |
|
|
toValue: 1, |
|
|
duration: 1000, |
|
|
useNativeDriver: true, |
|
|
}), |
|
|
]) |
|
|
); |
|
|
|
|
|
if (!isSearchSubmitted) { |
|
|
pulsate.start(); |
|
|
} else { |
|
|
pulsate.stop(); |
|
|
} |
|
|
|
|
|
return () => pulsate.stop(); |
|
|
}, [isSearchSubmitted]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
Animated.timing(inputContainerAnimation, { |
|
|
toValue: 1, |
|
|
duration: 500, |
|
|
delay: 500, |
|
|
useNativeDriver: true, |
|
|
}).start(); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const updateInputHeight = (height: number) => { |
|
|
const newHeight = Math.min(Math.max(40, height), 100); |
|
|
setInputHeight(newHeight); |
|
|
}; |
|
|
|
|
|
|
|
|
const formatMessage = (text: string) => { |
|
|
marked.setOptions({ |
|
|
highlight: function(code, language) { |
|
|
if (language && highlightjs.getLanguage(language)) { |
|
|
return highlightjs.highlight(code, { language: language }).value; |
|
|
} |
|
|
return highlightjs.highlightAuto(code).value; |
|
|
} |
|
|
}); |
|
|
|
|
|
return marked.parse(text); |
|
|
}; |
|
|
|
|
|
|
|
|
const UserMessage = ({ text }: { text: string }) => ( |
|
|
<View style={{ |
|
|
alignSelf: 'flex-end', |
|
|
maxWidth: '80%', |
|
|
marginVertical: 8, |
|
|
transform: [{ translateY: 0 }], // For animation preparation |
|
|
}}> |
|
|
<View style={{ |
|
|
backgroundColor: '#FF7B00', |
|
|
borderRadius: 18, |
|
|
borderTopRightRadius: 4, |
|
|
padding: 12, |
|
|
shadowColor: "#000", |
|
|
shadowOffset: { width: 0, height: 2 }, |
|
|
shadowOpacity: 0.15, |
|
|
shadowRadius: 3, |
|
|
elevation: 3, |
|
|
}}> |
|
|
<Text style={{ color: 'white', fontSize: 16, fontWeight: '500' }}>{text}</Text> |
|
|
</View> |
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-end', marginTop: 4, marginRight: 4 }}> |
|
|
<Text style={{ color: '#AAAAAA', fontSize: 12, marginRight: 5 }}>You</Text> |
|
|
<View style={{ |
|
|
width: 24, |
|
|
height: 24, |
|
|
borderRadius: 12, |
|
|
backgroundColor: '#555', |
|
|
alignItems: 'center', |
|
|
justifyContent: 'center' |
|
|
}}> |
|
|
<FontAwesome5 name="user" size={12} color="#FFF" /> |
|
|
</View> |
|
|
</View> |
|
|
</View> |
|
|
); |
|
|
|
|
|
|
|
|
const ComputerMessage = ({ text }: { text: string }) => { |
|
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null); |
|
|
|
|
|
const handleCopyCode = async (code: string, index: number) => { |
|
|
await Clipboard.setStringAsync(code); |
|
|
setCopiedIndex(index); |
|
|
|
|
|
|
|
|
if (Platform.OS !== 'web') { |
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|
|
} |
|
|
|
|
|
setTimeout(() => setCopiedIndex(null), 2000); |
|
|
}; |
|
|
|
|
|
const markdownStyles = { |
|
|
body: { |
|
|
color: 'white', |
|
|
fontSize: 16, |
|
|
lineHeight: 24, |
|
|
}, |
|
|
heading1: { |
|
|
fontWeight: 'bold', |
|
|
fontSize: 22, |
|
|
marginTop: 8, |
|
|
marginBottom: 4, |
|
|
color: 'white', |
|
|
}, |
|
|
heading2: { |
|
|
fontWeight: 'bold', |
|
|
fontSize: 20, |
|
|
marginTop: 8, |
|
|
marginBottom: 4, |
|
|
color: 'white', |
|
|
}, |
|
|
heading3: { |
|
|
fontWeight: 'bold', |
|
|
fontSize: 18, |
|
|
marginTop: 8, |
|
|
marginBottom: 4, |
|
|
color: 'white', |
|
|
}, |
|
|
link: { |
|
|
color: '#FF9D4F', |
|
|
textDecorationLine: "underline", |
|
|
}, |
|
|
blockquote: { |
|
|
borderLeftWidth: 3, |
|
|
borderLeftColor: '#FF7B00', |
|
|
paddingLeft: 10, |
|
|
fontStyle: 'italic', |
|
|
}, |
|
|
code_block: { |
|
|
backgroundColor: 'rgba(0, 0, 0, 0.3)', |
|
|
padding: 10, |
|
|
borderRadius: 5, |
|
|
fontFamily: 'monospace', |
|
|
fontSize: 14, |
|
|
}, |
|
|
code_inline: { |
|
|
backgroundColor: 'rgba(0, 0, 0, 0.3)', |
|
|
padding: 4, |
|
|
borderRadius: 3, |
|
|
fontFamily: 'monospace', |
|
|
fontSize: 14, |
|
|
}, |
|
|
list_item: { |
|
|
marginBottom: 6, |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
const renderCodeBlock = (props: { content: string; language?: string; index: number }) => { |
|
|
const { content, language } = props; |
|
|
return ( |
|
|
<View style={{ |
|
|
backgroundColor: 'rgba(0, 0, 0, 0.4)', |
|
|
borderRadius: 6, |
|
|
overflow: 'hidden', |
|
|
marginVertical: 10, |
|
|
}}> |
|
|
<View style={{ |
|
|
flexDirection: 'row', |
|
|
justifyContent: 'space-between', |
|
|
alignItems: 'center', |
|
|
padding: 8, |
|
|
backgroundColor: 'rgba(0, 0, 0, 0.2)', |
|
|
borderBottomWidth: 1, |
|
|
borderBottomColor: 'rgba(255, 255, 255, 0.1)', |
|
|
}}> |
|
|
<Text style={{ |
|
|
fontSize: 12, |
|
|
fontWeight: 'bold', |
|
|
textTransform: 'uppercase', |
|
|
color: 'rgba(255, 255, 255, 0.7)', |
|
|
}}> |
|
|
{language || 'code'} |
|
|
</Text> |
|
|
<TouchableOpacity |
|
|
onPress={() => handleCopyCode(content, props.index)} |
|
|
style={{ |
|
|
backgroundColor: 'transparent', |
|
|
padding: 4, |
|
|
borderRadius: 4, |
|
|
}}> |
|
|
<FontAwesome5 |
|
|
name={copiedIndex === props.index ? "check" : "copy"} |
|
|
size={14} |
|
|
color="white" |
|
|
/> |
|
|
</TouchableOpacity> |
|
|
</View> |
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}> |
|
|
<Text style={{ |
|
|
fontFamily: 'monospace', |
|
|
padding: 12, |
|
|
color: 'white', |
|
|
fontSize: 14, |
|
|
}}> |
|
|
{content} |
|
|
</Text> |
|
|
</ScrollView> |
|
|
</View> |
|
|
); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<View style={{ |
|
|
alignSelf: 'flex-start', |
|
|
maxWidth: '80%', |
|
|
marginVertical: 8, |
|
|
flexDirection: 'row', |
|
|
alignItems: 'flex-end', |
|
|
}}> |
|
|
<View style={{ |
|
|
width: 32, |
|
|
height: 32, |
|
|
borderRadius: 16, |
|
|
backgroundColor: '#FF7B00', |
|
|
alignItems: 'center', |
|
|
justifyContent: 'center', |
|
|
marginRight: 8, |
|
|
marginBottom: 4, |
|
|
}}> |
|
|
<FontAwesome5 name="robot" size={16} color="#FFF" /> |
|
|
</View> |
|
|
<View style={{ flex: 1 }}> |
|
|
<View style={{ |
|
|
backgroundColor: '#332940', |
|
|
borderRadius: 18, |
|
|
borderBottomLeftRadius: 4, |
|
|
padding: 12, |
|
|
shadowColor: "#000", |
|
|
shadowOffset: { width: 0, height: 2 }, |
|
|
shadowOpacity: 0.15, |
|
|
shadowRadius: 3, |
|
|
elevation: 2, |
|
|
}}> |
|
|
<Markdown |
|
|
style={markdownStyles as any} |
|
|
rules={{ |
|
|
code_block: (node, children, parent, styles, renderContent) => { |
|
|
return renderCodeBlock({ |
|
|
content: node.content, |
|
|
language: (node as any).language, |
|
|
index: parseInt(node.key, 10), |
|
|
}); |
|
|
} |
|
|
}} |
|
|
> |
|
|
{text} |
|
|
</Markdown> |
|
|
</View> |
|
|
<Text style={{ color: '#AAAAAA', fontSize: 12, marginLeft: 4, marginTop: 4 }}>Fille AI</Text> |
|
|
</View> |
|
|
</View> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const LoadingIndicator = () => { |
|
|
const [dotIndex, setDotIndex] = useState(0); |
|
|
|
|
|
useEffect(() => { |
|
|
const interval = setInterval(() => { |
|
|
setDotIndex(prev => (prev + 1) % 4); |
|
|
}, 300); |
|
|
return () => clearInterval(interval); |
|
|
}, []); |
|
|
|
|
|
return ( |
|
|
<View style={{ |
|
|
alignSelf: 'flex-start', |
|
|
marginVertical: 8, |
|
|
flexDirection: 'row', |
|
|
alignItems: 'flex-end', |
|
|
}}> |
|
|
<View style={{ |
|
|
width: 32, |
|
|
height: 32, |
|
|
borderRadius: 16, |
|
|
backgroundColor: '#FF7B00', |
|
|
alignItems: 'center', |
|
|
justifyContent: 'center', |
|
|
marginRight: 8, |
|
|
marginBottom: 4, |
|
|
}}> |
|
|
<FontAwesome5 name="robot" size={16} color="#FFF" /> |
|
|
</View> |
|
|
<View style={{ |
|
|
backgroundColor: '#332940', |
|
|
borderRadius: 18, |
|
|
paddingHorizontal: 16, |
|
|
paddingVertical: 12, |
|
|
flexDirection: 'row', |
|
|
alignItems: 'center', |
|
|
shadowColor: "#000", |
|
|
shadowOffset: { width: 0, height: 2 }, |
|
|
shadowOpacity: 0.15, |
|
|
shadowRadius: 3, |
|
|
elevation: 2, |
|
|
}}> |
|
|
{[0, 1, 2].map((i) => ( |
|
|
<View |
|
|
key={i} |
|
|
style={{ |
|
|
width: 10, |
|
|
height: 10, |
|
|
borderRadius: 5, |
|
|
backgroundColor: 'white', |
|
|
opacity: dotIndex === i ? 0.9 : 0.4, |
|
|
marginHorizontal: 4, |
|
|
transform: [{ |
|
|
translateY: dotIndex === i ? -4 : 0 |
|
|
}] |
|
|
}} |
|
|
/> |
|
|
))} |
|
|
</View> |
|
|
</View> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const SuggestionBubble = ({ text }: { text: string }) => ( |
|
|
<Animated.View style={{ |
|
|
transform: [{ scale: floatingButtonScale }] |
|
|
}}> |
|
|
<TouchableOpacity |
|
|
style={{ |
|
|
backgroundColor: 'rgba(255, 123, 0, 0.15)', |
|
|
borderWidth: 1, |
|
|
borderColor: 'rgba(255, 123, 0, 0.3)', |
|
|
borderRadius: 20, |
|
|
paddingHorizontal: 16, |
|
|
paddingVertical: 10, |
|
|
marginRight: 10, |
|
|
marginBottom: 10, |
|
|
shadowColor: "#FF7B00", |
|
|
shadowOffset: { width: 0, height: 2 }, |
|
|
shadowOpacity: 0.2, |
|
|
shadowRadius: 3, |
|
|
elevation: 2, |
|
|
}} |
|
|
onPress={() => { |
|
|
// Add haptic feedback |
|
|
if (Platform.OS !== 'web') { |
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|
|
} |
|
|
setInputText(text); |
|
|
handleSubmit(text); |
|
|
}} |
|
|
> |
|
|
<Text style={{ color: '#FF9D4F', fontSize: 14 }}>{text}</Text> |
|
|
</TouchableOpacity> |
|
|
</Animated.View> |
|
|
); |
|
|
|
|
|
|
|
|
const getServerUrl = () => { |
|
|
if (Platform.OS === 'android') { |
|
|
|
|
|
return "http://10.0.2.2:8000/chat"; |
|
|
} else if (Platform.OS === 'ios') { |
|
|
return "http://localhost:8000/chat"; |
|
|
} else { |
|
|
|
|
|
return "http://localhost:8000/chat"; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const handleSubmit = async (text = inputText) => { |
|
|
const messageToSend = text.trim(); |
|
|
if (messageToSend === '' || isLoading) return; |
|
|
|
|
|
|
|
|
if (Platform.OS !== 'web') { |
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); |
|
|
} |
|
|
|
|
|
|
|
|
const userMessage = messageToSend; |
|
|
setMessages(prevMessages => [...prevMessages, { type: 'user', text: userMessage }]); |
|
|
setInputText(''); |
|
|
setInputHeight(40); |
|
|
|
|
|
|
|
|
if (!isSearchSubmitted) { |
|
|
Animated.parallel([ |
|
|
Animated.timing(welcomeOpacity, { |
|
|
toValue: 0, |
|
|
duration: 300, |
|
|
useNativeDriver: true, |
|
|
}), |
|
|
Animated.timing(centerContentMargin, { |
|
|
toValue: 20, |
|
|
duration: 500, |
|
|
useNativeDriver: false, |
|
|
}), |
|
|
]).start(() => { |
|
|
setIsSearchSubmitted(true); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
setIsLoading(true); |
|
|
|
|
|
try { |
|
|
const serverUrl = getServerUrl(); |
|
|
console.log(`Sending request to: ${serverUrl}`); |
|
|
|
|
|
|
|
|
const response = await fetch(serverUrl, { |
|
|
method: "POST", |
|
|
headers: { |
|
|
"Content-Type": "application/json", |
|
|
"Accept": "application/json", |
|
|
}, |
|
|
body: JSON.stringify({ message: userMessage }), |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`Failed to get response: ${response.status}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
|
|
|
const aiResponse = typeof data.response === 'string' |
|
|
? data.response |
|
|
: JSON.stringify(data.response); |
|
|
|
|
|
|
|
|
if (Platform.OS !== 'web') { |
|
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); |
|
|
} |
|
|
|
|
|
setMessages(prevMessages => [...prevMessages, { type: 'computer', text: aiResponse }]); |
|
|
|
|
|
|
|
|
updateSuggestedQuestions(userMessage, aiResponse); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error:', error); |
|
|
|
|
|
|
|
|
if (Platform.OS !== 'web') { |
|
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); |
|
|
} |
|
|
|
|
|
Alert.alert( |
|
|
"Connection Error", |
|
|
`Failed to connect to server: ${(error as Error).message}\n\nMake sure your server is running and the device can reach it.`, |
|
|
[{ text: "OK" }] |
|
|
); |
|
|
|
|
|
setMessages(prevMessages => [ |
|
|
...prevMessages, |
|
|
{ type: 'computer', text: 'Sorry, there was an error processing your request. Please check your network connection and make sure the server is running.' }, |
|
|
]); |
|
|
} finally { |
|
|
setIsLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const updateSuggestedQuestions = (userMessage: string, aiResponse: string) => { |
|
|
|
|
|
const periodRelated = userMessage.toLowerCase().includes('period') || |
|
|
aiResponse.toLowerCase().includes('period'); |
|
|
|
|
|
const painRelated = userMessage.toLowerCase().includes('pain') || |
|
|
aiResponse.toLowerCase().includes('pain'); |
|
|
|
|
|
const pregnancyRelated = userMessage.toLowerCase().includes('pregnan') || |
|
|
aiResponse.toLowerCase().includes('pregnan'); |
|
|
|
|
|
const hormoneRelated = userMessage.toLowerCase().includes('hormone') || |
|
|
aiResponse.toLowerCase().includes('hormone'); |
|
|
|
|
|
let newSuggestions = [...SUGGESTED_QUESTIONS]; |
|
|
|
|
|
if (periodRelated) { |
|
|
newSuggestions = [ |
|
|
"What causes irregular periods?", |
|
|
"How can I track my menstrual cycle?", |
|
|
"When should I be concerned about heavy flow?", |
|
|
]; |
|
|
} else if (painRelated) { |
|
|
newSuggestions = [ |
|
|
"What are natural remedies for cramps?", |
|
|
"Should I see a doctor about period pain?", |
|
|
"How can exercise help with menstrual pain?", |
|
|
]; |
|
|
} else if (pregnancyRelated) { |
|
|
newSuggestions = [ |
|
|
"What prenatal vitamins should I take?", |
|
|
"How does ovulation tracking work?", |
|
|
"What are early signs of pregnancy?", |
|
|
]; |
|
|
} else if (hormoneRelated) { |
|
|
newSuggestions = [ |
|
|
"How do hormones affect mood?", |
|
|
"What foods help balance hormones?", |
|
|
"How does stress impact hormonal health?", |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
newSuggestions.sort(() => Math.random() - 0.5); |
|
|
|
|
|
|
|
|
setSuggestedQuestions(newSuggestions.slice(0, 3)); |
|
|
}; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (scrollViewRef.current && messages.length > 0) { |
|
|
setTimeout(() => { |
|
|
if (scrollViewRef.current) { |
|
|
scrollViewRef.current.scrollToEnd({ animated: true }); |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
}, [messages]); |
|
|
|
|
|
return ( |
|
|
<> |
|
|
{/* Add Stack.Screen options to hide the header */} |
|
|
<Stack.Screen options={{ headerShown: false }} /> |
|
|
|
|
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> |
|
|
<SafeAreaView style={{ flex: 1, backgroundColor: '#1A1C25' }}> |
|
|
<StatusBar style="light" /> |
|
|
|
|
|
{/* Navbar */} |
|
|
<View style={{ |
|
|
padding: 20, |
|
|
flexDirection: 'row', |
|
|
justifyContent: 'space-between', |
|
|
alignItems: 'center', |
|
|
borderBottomWidth: 1, |
|
|
borderBottomColor: 'rgba(255,255,255,0.1)' |
|
|
}}> |
|
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}> |
|
|
<FontAwesome5 name="heartbeat" size={18} color="#FF7B00" style={{ marginRight: 10 }} /> |
|
|
<Text style={{ color: 'white', fontWeight: 'bold', fontSize: 18 }}> |
|
|
FILLE <Text style={{ color: '#FF7B00' }}>AI</Text> |
|
|
</Text> |
|
|
</View> |
|
|
<TouchableOpacity |
|
|
style={{ |
|
|
backgroundColor: '#332940', |
|
|
paddingHorizontal: 12, |
|
|
paddingVertical: 6, |
|
|
borderRadius: 16, |
|
|
flexDirection: 'row', |
|
|
alignItems: 'center' |
|
|
}} |
|
|
onPress={() => { |
|
|
if (Platform.OS !== 'web') { |
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); |
|
|
} |
|
|
router.push("/realchat"); |
|
|
}} |
|
|
> |
|
|
<MaterialCommunityIcons name="doctor" size={16} color="#FF9D4F" style={{ marginRight: 6 }} /> |
|
|
<Text style={{ color: '#FF9D4F', fontSize: 13 }}>Talk to Doctor</Text> |
|
|
</TouchableOpacity> |
|
|
</View> |
|
|
|
|
|
{/* Main Content */} |
|
|
<KeyboardAvoidingView |
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined} |
|
|
style={{ flex: 1 }} |
|
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 80 : 0} |
|
|
> |
|
|
<View style={{ flex: 1, justifyContent: 'space-between' }}> |
|
|
{/* Center Content */} |
|
|
<Animated.View style={{ |
|
|
flex: 1, |
|
|
marginTop: centerContentMargin, |
|
|
}}> |
|
|
{/* Welcome Message */} |
|
|
{!isSearchSubmitted && ( |
|
|
<Animated.View style={{ |
|
|
opacity: welcomeOpacity, |
|
|
alignItems: 'center', |
|
|
padding: 20, |
|
|
}}> |
|
|
<View style={{ |
|
|
width: 100, |
|
|
height: 100, |
|
|
borderRadius: 50, |
|
|
backgroundColor: '#FF7B00', |
|
|
justifyContent: 'center', |
|
|
alignItems: 'center', |
|
|
marginBottom: 20, |
|
|
shadowColor: "#FF7B00", |
|
|
shadowOffset: { width: 0, height: 0 }, |
|
|
shadowOpacity: 0.6, |
|
|
shadowRadius: 20, |
|
|
}}> |
|
|
<FontAwesome5 name="female" size={50} color="white" /> |
|
|
</View> |
|
|
<Text style={{ |
|
|
color: 'white', |
|
|
fontWeight: 'bold', |
|
|
fontSize: 28, |
|
|
textAlign: 'center' |
|
|
}}> |
|
|
Hello, GIRL! |
|
|
</Text> |
|
|
<Text style={{ |
|
|
color: '#AAAAAA', |
|
|
fontSize: 16, |
|
|
marginTop: 12, |
|
|
textAlign: 'center', |
|
|
paddingHorizontal: 30, |
|
|
lineHeight: 22 |
|
|
}}> |
|
|
I'm your health companion. Ready to share your problems and feelings today? |
|
|
</Text> |
|
|
|
|
|
{/* Suggested Topics */} |
|
|
<Text style={{ color: 'white', marginTop: 30, marginBottom: 15, fontWeight: '600' }}> |
|
|
Try asking about: |
|
|
</Text> |
|
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' }}> |
|
|
{suggestedQuestions.map((question, index) => ( |
|
|
<SuggestionBubble key={index} text={question} /> |
|
|
))} |
|
|
</View> |
|
|
</Animated.View> |
|
|
)} |
|
|
|
|
|
{/* Conversation Container */} |
|
|
<ScrollView |
|
|
ref={scrollViewRef} |
|
|
style={{ |
|
|
display: isSearchSubmitted ? 'flex' : 'none', |
|
|
paddingHorizontal: 16, |
|
|
}} |
|
|
contentContainerStyle={{ paddingBottom: 16 }} |
|
|
showsVerticalScrollIndicator={false} |
|
|
> |
|
|
{messages.map((message, index) => ( |
|
|
(message as { type: string; text: string }).type === 'user' ? ( |
|
|
<UserMessage key={index} text={(message as { type: string; text: string }).text} /> |
|
|
) : ( |
|
|
<ComputerMessage key={index} text={(message as { type: string; text: string }).text} /> |
|
|
) |
|
|
))} |
|
|
{isLoading && <LoadingIndicator />} |
|
|
</ScrollView> |
|
|
</Animated.View> |
|
|
|
|
|
{/* Suggestions row (visible only after first message) */} |
|
|
{isSearchSubmitted && !isLoading && messages.length > 0 && ( |
|
|
<View style={{ |
|
|
paddingHorizontal: 20, |
|
|
paddingVertical: 10 |
|
|
}}> |
|
|
<ScrollView |
|
|
horizontal |
|
|
showsHorizontalScrollIndicator={false} |
|
|
style={{ flexDirection: 'row' }} |
|
|
contentContainerStyle={{ paddingRight: 20 }} |
|
|
> |
|
|
{suggestedQuestions.map((question, index) => ( |
|
|
<SuggestionBubble key={index} text={question} /> |
|
|
))} |
|
|
</ScrollView> |
|
|
</View> |
|
|
)} |
|
|
|
|
|
{/* Input Container */} |
|
|
<Animated.View style={{ |
|
|
flexDirection: 'row', |
|
|
alignItems: 'flex-end', |
|
|
marginHorizontal: 20, |
|
|
marginBottom: 20, |
|
|
marginTop: 10, |
|
|
backgroundColor: '#24273A', |
|
|
borderRadius: 25, |
|
|
paddingHorizontal: 10, |
|
|
paddingVertical: 5, |
|
|
shadowColor: "#000", |
|
|
shadowOffset: { width: 0, height: 4 }, |
|
|
shadowOpacity: 0.2, |
|
|
shadowRadius: 5, |
|
|
elevation: 5, |
|
|
opacity: inputContainerAnimation, |
|
|
transform: [{ |
|
|
translateY: inputContainerAnimation.interpolate({ |
|
|
inputRange: [0, 1], |
|
|
outputRange: [20, 0] |
|
|
}) |
|
|
}] |
|
|
}}> |
|
|
<TextInput |
|
|
ref={inputRef} |
|
|
style={{ |
|
|
flex: 1, |
|
|
color: 'white', |
|
|
fontSize: 16, |
|
|
paddingVertical: 12, |
|
|
paddingHorizontal: 12, |
|
|
minHeight: inputHeight, |
|
|
maxHeight: 100, |
|
|
}} |
|
|
placeholder="Ask me anything about women's health" |
|
|
placeholderTextColor="rgba(255,255,255,0.4)" |
|
|
value={inputText} |
|
|
onChangeText={setInputText} |
|
|
multiline |
|
|
onContentSizeChange={(e) => |
|
|
updateInputHeight(e.nativeEvent.contentSize.height) |
|
|
} |
|
|
/> |
|
|
<TouchableOpacity |
|
|
onPress={() => handleSubmit()} |
|
|
disabled={isLoading || inputText.trim() === ''} |
|
|
style={{ |
|
|
padding: 10, |
|
|
backgroundColor: (isLoading || inputText.trim() === '') ? '#444' : '#FF7B00', |
|
|
borderRadius: 20, |
|
|
width: 40, |
|
|
height: 40, |
|
|
justifyContent: 'center', |
|
|
alignItems: 'center', |
|
|
marginBottom: 6, |
|
|
}} |
|
|
> |
|
|
<Ionicons name="send" size={18} color="white" /> |
|
|
</TouchableOpacity> |
|
|
</Animated.View> |
|
|
</View> |
|
|
</KeyboardAvoidingView> |
|
|
|
|
|
{/* Footer */} |
|
|
<View style={{ |
|
|
paddingBottom: 10, |
|
|
alignItems: 'center', |
|
|
borderTopWidth: 1, |
|
|
borderTopColor: 'rgba(255,255,255,0.1)', |
|
|
paddingTop: 10 |
|
|
}}> |
|
|
<Text style={{ color: 'grey', fontSize: 12 }}> |
|
|
For informational purposes. Consult a healthcare professional. |
|
|
</Text> |
|
|
</View> |
|
|
</SafeAreaView> |
|
|
</TouchableWithoutFeedback> |
|
|
</> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default FilleAI; |