import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
FlatList,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
SafeAreaView,
} from 'react-native';
import { useLLM, LLAMA3_2_1B, Message } from 'react-native-executorch';
function ChatApp() {
const llm = useLLM({ model: LLAMA3_2_1B });
const [input, setInput] = useState('');
const [displayMessages, setDisplayMessages] = useState<Message[]>([]);
useEffect(() => {
if (llm.messageHistory.length > 0) {
setDisplayMessages(llm.messageHistory);
}
}, [llm.messageHistory]);
const handleSend = async () => {
if (!input.trim() || !llm.isReady || llm.isGenerating) return;
const userMessage = input;
setInput('');
// Show user message immediately
setDisplayMessages(prev => [
...prev,
{ role: 'user', content: userMessage },
]);
try {
await llm.sendMessage(userMessage);
} catch (error) {
console.error('Error sending message:', error);
}
};
const renderMessage = ({ item }: { item: Message }) => (
<View
style={[
styles.messageBubble,
item.role === 'user' ? styles.userBubble : styles.aiBubble,
]}
>
<Text style={styles.messageRole}>
{item.role === 'user' ? 'You' : 'AI'}
</Text>
<Text style={styles.messageText}>{item.content}</Text>
</View>
);
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>AI Chat</Text>
{!llm.isReady && (
<Text style={styles.headerStatus}>
Loading model... {Math.round(llm.downloadProgress * 100)}%
</Text>
)}
{llm.error && (
<Text style={styles.headerError}>Error: {llm.error.message}</Text>
)}
</View>
<FlatList
data={displayMessages}
renderItem={renderMessage}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={styles.messageList}
inverted={false}
/>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
<View style={styles.inputContainer}>
<TextInput
style={styles.textInput}
value={input}
onChangeText={setInput}
placeholder="Type your message..."
editable={llm.isReady && !llm.isGenerating}
multiline
maxLength={500}
/>
<TouchableOpacity
style={[
styles.sendButton,
(!llm.isReady || llm.isGenerating || !input.trim()) &&
styles.sendButtonDisabled,
]}
onPress={handleSend}
disabled={!llm.isReady || llm.isGenerating || !input.trim()}
>
<Text style={styles.sendButtonText}>
{llm.isGenerating ? '•••' : 'Send'}
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
},
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
backgroundColor: '#f8f9fa',
},
headerTitle: {
fontSize: 20,
fontWeight: '600',
},
headerStatus: {
fontSize: 12,
color: '#666',
marginTop: 4,
},
headerError: {
fontSize: 12,
color: '#d32f2f',
marginTop: 4,
},
messageList: {
padding: 16,
},
messageBubble: {
maxWidth: '75%',
padding: 12,
borderRadius: 16,
marginBottom: 12,
},
userBubble: {
alignSelf: 'flex-end',
backgroundColor: '#007AFF',
},
aiBubble: {
alignSelf: 'flex-start',
backgroundColor: '#e8e8e8',
},
messageRole: {
fontSize: 10,
fontWeight: '600',
marginBottom: 4,
opacity: 0.7,
},
messageText: {
fontSize: 14,
lineHeight: 20,
},
inputContainer: {
flexDirection: 'row',
padding: 12,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
backgroundColor: '#ffffff',
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: '#d0d0d0',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 8,
marginRight: 8,
maxHeight: 100,
fontSize: 14,
},
sendButton: {
backgroundColor: '#007AFF',
borderRadius: 20,
paddingHorizontal: 20,
justifyContent: 'center',
alignItems: 'center',
},
sendButtonDisabled: {
backgroundColor: '#cccccc',
},
sendButtonText: {
color: '#ffffff',
fontWeight: '600',
fontSize: 14,
},
});
export default ChatApp;