Skip to content

Commit 8816fbc

Browse files
rizalibnumsluszniak
authored andcommitted
feat: add bare React Native LLM chat example app
1 parent ac84e19 commit 8816fbc

File tree

14 files changed

+583
-65
lines changed

14 files changed

+583
-65
lines changed

apps/bare_rn/.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module.exports = {
22
root: true,
33
extends: '@react-native',
4+
parserOptions: {
5+
requireConfigFile: false,
6+
},
47
};

apps/bare_rn/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,5 @@ yarn-error.log
7676

7777
# Custom
7878
!/ios/
79-
!/android/
79+
!/android/
80+
/assets/ai-models/

apps/bare_rn/App.tsx

Lines changed: 298 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,316 @@
1-
/**
2-
* Sample React Native App
3-
* https://github.com/facebook/react-native
4-
*
5-
* @format
6-
*/
7-
8-
import { NewAppScreen } from '@react-native/new-app-screen';
9-
import { StatusBar, StyleSheet, useColorScheme, View } from 'react-native';
1+
import React, { useEffect, useRef, useState } from 'react';
102
import {
11-
SafeAreaProvider,
12-
useSafeAreaInsets,
13-
} from 'react-native-safe-area-context';
3+
ActivityIndicator,
4+
Keyboard,
5+
KeyboardAvoidingView,
6+
Modal,
7+
Platform,
8+
ScrollView,
9+
StyleSheet,
10+
Text,
11+
TextInput,
12+
TouchableWithoutFeedback,
13+
View,
14+
} from 'react-native';
15+
import {
16+
initExecutorch,
17+
useLLM,
18+
LLAMA3_2_1B_SPINQUANT,
19+
} from 'react-native-executorch';
20+
import { BareResourceFetcher } from '@rn-executorch/bare-adapter';
21+
import { setConfig } from '@kesha-antonov/react-native-background-downloader';
22+
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
1423

15-
function App() {
16-
const isDarkMode = useColorScheme() === 'dark';
24+
// Configure Background Downloader logging
25+
setConfig({
26+
isLogsEnabled: true,
27+
logCallback: log => {
28+
console.log('[BackgroundDownloader]', log);
29+
},
30+
});
31+
32+
// Initialize Executorch with bare adapter
33+
initExecutorch({
34+
resourceFetcher: BareResourceFetcher,
35+
});
1736

37+
const ColorPalette = {
38+
primary: '#001A72',
39+
blueLight: '#C1C6E5',
40+
blueDark: '#6676AA',
41+
white: '#FFFFFF',
42+
gray100: '#F5F5F5',
43+
gray200: '#E0E0E0',
44+
};
45+
46+
function Spinner({
47+
visible,
48+
textContent,
49+
}: {
50+
visible: boolean;
51+
textContent: string;
52+
}) {
1853
return (
19-
<SafeAreaProvider>
20-
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
21-
<AppContent />
22-
</SafeAreaProvider>
54+
<Modal transparent={true} animationType="fade" visible={visible}>
55+
<View style={spinnerStyles.overlay}>
56+
<View style={spinnerStyles.container}>
57+
<ActivityIndicator size="large" color="#FFFFFF" />
58+
<Text style={spinnerStyles.text}>{textContent}</Text>
59+
</View>
60+
</View>
61+
</Modal>
2362
);
2463
}
2564

26-
function AppContent() {
27-
const safeAreaInsets = useSafeAreaInsets();
65+
const spinnerStyles = StyleSheet.create({
66+
overlay: {
67+
flex: 1,
68+
justifyContent: 'center',
69+
alignItems: 'center',
70+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
71+
},
72+
container: {
73+
padding: 25,
74+
alignItems: 'center',
75+
justifyContent: 'center',
76+
},
77+
text: {
78+
marginTop: 15,
79+
color: ColorPalette.white,
80+
fontSize: 18,
81+
fontWeight: 'bold',
82+
},
83+
});
84+
85+
function App() {
86+
const [userInput, setUserInput] = useState('');
87+
const [isTextInputFocused, setIsTextInputFocused] = useState(false);
88+
const textInputRef = useRef<TextInput>(null);
89+
const scrollViewRef = useRef<ScrollView>(null);
90+
91+
const llm = useLLM({ model: LLAMA3_2_1B_SPINQUANT });
92+
// Alternatively, to use a custom local model, uncomment below:
93+
// const llm = useLLM({ model: {
94+
// modelSource: require('./assets/ai-models/smolLm2/smolLm2_135M/smolLm2_135M_bf16.pte'),
95+
// tokenizerSource: require('./assets/ai-models/smolLm2/tokenizer.json'),
96+
// tokenizerConfigSource: require('./assets/ai-models/smolLm2/tokenizer_config.json'),
97+
// } });
98+
99+
useEffect(() => {
100+
if (llm.error) {
101+
console.log('LLM error:', llm.error);
102+
}
103+
}, [llm.error]);
104+
105+
const sendMessage = async () => {
106+
if (!userInput.trim()) return;
107+
108+
setUserInput('');
109+
textInputRef.current?.clear();
110+
try {
111+
await llm.sendMessage(userInput);
112+
} catch (e) {
113+
console.error(e);
114+
}
115+
};
28116

29117
return (
30-
<View style={styles.container}>
31-
<NewAppScreen
32-
templateFileName="App.tsx"
33-
safeAreaInsets={safeAreaInsets}
34-
/>
35-
</View>
118+
<SafeAreaProvider>
119+
<KeyboardAvoidingView
120+
style={styles.container}
121+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
122+
keyboardVerticalOffset={Platform.OS === 'ios' ? 100 : 0}
123+
>
124+
<Spinner
125+
visible={!llm.isReady}
126+
textContent={`Loading model ${(llm.downloadProgress * 100).toFixed(0)}%`}
127+
/>
128+
129+
<SafeAreaView style={styles.content}>
130+
{llm.messageHistory.length > 0 || llm.isGenerating ? (
131+
<ScrollView
132+
ref={scrollViewRef}
133+
style={styles.chatContainer}
134+
contentContainerStyle={styles.chatContent}
135+
onContentSizeChange={() =>
136+
scrollViewRef.current?.scrollToEnd({ animated: true })
137+
}
138+
keyboardShouldPersistTaps="handled"
139+
>
140+
{llm.messageHistory.map((message, index) => (
141+
<View
142+
key={index}
143+
style={[
144+
styles.messageBubble,
145+
message.role === 'user'
146+
? styles.userMessage
147+
: styles.aiMessage,
148+
]}
149+
>
150+
<Text
151+
style={[
152+
styles.messageText,
153+
message.role === 'user'
154+
? styles.userMessageText
155+
: styles.aiMessageText,
156+
]}
157+
>
158+
{message.content}
159+
</Text>
160+
</View>
161+
))}
162+
{llm.isGenerating && llm.response && (
163+
<View style={[styles.messageBubble, styles.aiMessage]}>
164+
<Text style={styles.aiMessageText}>{llm.response}</Text>
165+
<ActivityIndicator
166+
size="small"
167+
color={ColorPalette.primary}
168+
/>
169+
</View>
170+
)}
171+
</ScrollView>
172+
) : (
173+
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
174+
<View style={styles.welcomeContainer}>
175+
<Text style={styles.welcomeTitle}>Hello! 👋</Text>
176+
<Text style={styles.welcomeSubtitle}>
177+
What can I help you with?
178+
</Text>
179+
</View>
180+
</TouchableWithoutFeedback>
181+
)}
182+
183+
<View style={styles.inputContainer}>
184+
<TextInput
185+
ref={textInputRef}
186+
style={[
187+
styles.textInput,
188+
{
189+
borderColor: isTextInputFocused
190+
? ColorPalette.blueDark
191+
: ColorPalette.blueLight,
192+
},
193+
]}
194+
placeholder="Your message"
195+
placeholderTextColor={ColorPalette.blueLight}
196+
multiline
197+
onFocus={() => setIsTextInputFocused(true)}
198+
onBlur={() => setIsTextInputFocused(false)}
199+
onChangeText={setUserInput}
200+
value={userInput}
201+
/>
202+
{userInput.trim() && !llm.isGenerating && (
203+
<View style={styles.sendButton}>
204+
<Text style={styles.sendButtonText} onPress={sendMessage}>
205+
Send
206+
</Text>
207+
</View>
208+
)}
209+
{llm.isGenerating && (
210+
<View style={styles.sendButton}>
211+
<Text style={styles.sendButtonText} onPress={llm.interrupt}>
212+
Stop
213+
</Text>
214+
</View>
215+
)}
216+
</View>
217+
</SafeAreaView>
218+
</KeyboardAvoidingView>
219+
</SafeAreaProvider>
36220
);
37221
}
38222

39223
const styles = StyleSheet.create({
40224
container: {
41225
flex: 1,
226+
backgroundColor: ColorPalette.white,
227+
},
228+
content: {
229+
flex: 1,
230+
},
231+
chatContainer: {
232+
flex: 1,
233+
width: '100%',
234+
},
235+
chatContent: {
236+
padding: 16,
237+
flexGrow: 1,
238+
},
239+
welcomeContainer: {
240+
flex: 1,
241+
alignItems: 'center',
242+
justifyContent: 'center',
243+
padding: 20,
244+
},
245+
welcomeTitle: {
246+
fontSize: 32,
247+
fontWeight: 'bold',
248+
color: ColorPalette.primary,
249+
marginBottom: 12,
250+
},
251+
welcomeSubtitle: {
252+
fontSize: 18,
253+
color: ColorPalette.blueDark,
254+
textAlign: 'center',
255+
},
256+
messageBubble: {
257+
maxWidth: '80%',
258+
padding: 12,
259+
borderRadius: 16,
260+
marginBottom: 8,
261+
},
262+
userMessage: {
263+
alignSelf: 'flex-end',
264+
backgroundColor: ColorPalette.primary,
265+
},
266+
aiMessage: {
267+
alignSelf: 'flex-start',
268+
backgroundColor: ColorPalette.gray100,
269+
borderWidth: 1,
270+
borderColor: ColorPalette.gray200,
271+
},
272+
messageText: {
273+
fontSize: 15,
274+
lineHeight: 20,
275+
},
276+
userMessageText: {
277+
color: ColorPalette.white,
278+
},
279+
aiMessageText: {
280+
color: ColorPalette.primary,
281+
},
282+
inputContainer: {
283+
flexDirection: 'row',
284+
padding: 16,
285+
borderTopWidth: 1,
286+
borderTopColor: ColorPalette.gray200,
287+
alignItems: 'flex-end',
288+
backgroundColor: ColorPalette.white,
289+
},
290+
textInput: {
291+
flex: 1,
292+
borderWidth: 1,
293+
borderRadius: 20,
294+
paddingHorizontal: 16,
295+
paddingVertical: 10,
296+
fontSize: 15,
297+
color: ColorPalette.primary,
298+
maxHeight: 100,
299+
marginRight: 8,
300+
},
301+
sendButton: {
302+
backgroundColor: ColorPalette.primary,
303+
paddingHorizontal: 20,
304+
paddingVertical: 10,
305+
borderRadius: 20,
306+
minWidth: 70,
307+
alignItems: 'center',
308+
justifyContent: 'center',
309+
},
310+
sendButtonText: {
311+
color: ColorPalette.white,
312+
fontSize: 16,
313+
fontWeight: '600',
42314
},
43315
});
44316

apps/bare_rn/android/app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ android {
108108
}
109109

110110
dependencies {
111+
// MMKV for persistent state storage in background downloads
112+
implementation 'com.tencent:mmkv-shared:2.2.4'
111113
// The version of react-native is set by the React Native Gradle Plugin
112114
implementation("com.facebook.react:react-android")
113115

apps/bare_rn/android/gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ android.useAndroidX=true
2525
# Use this property to specify which architecture you want to build.
2626
# You can also override it from the CLI using
2727
# ./gradlew <task> -PreactNativeArchitectures=x86_64
28-
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
28+
# MMKV no longer supports the armeabi arch officially, remove the 32-bit architectures and just keep the 64-bit ones
29+
reactNativeArchitectures=arm64-v8a,x86_64
2930

3031
# Use this property to enable support to the new architecture.
3132
# This will allow you to use TurboModules and the Fabric render in

apps/bare_rn/babel.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
module.exports = {
22
presets: ['module:@react-native/babel-preset'],
3+
plugins: ['@babel/plugin-transform-export-namespace-from'],
34
};

0 commit comments

Comments
 (0)