|
1 | | -import { Text, View, StyleSheet } from 'react-native'; |
2 | | -import { multiply } from 'react-native-ai-kit'; |
| 1 | +import { useRef, useState } from 'react'; |
| 2 | +import { |
| 3 | + KeyboardAvoidingView, |
| 4 | + Platform, |
| 5 | + View, |
| 6 | + TextInput, |
| 7 | + TouchableOpacity, |
| 8 | + Text, |
| 9 | + ActivityIndicator, |
| 10 | + StyleSheet, |
| 11 | +} from 'react-native'; |
| 12 | +import { useChat, ChatList, ChatBubble } from 'react-native-ai-kit'; |
3 | 13 |
|
4 | | -const result = multiply(3, 7); |
| 14 | +const API_URL = 'https://api.openai.com/v1/chat/completions'; |
| 15 | +const API_KEY = ''; |
5 | 16 |
|
6 | 17 | export default function App() { |
| 18 | + const [input, setInput] = useState(''); |
| 19 | + const [apiKey, setApiKey] = useState(API_KEY); |
| 20 | + const cumulativeTokens = useRef(0); |
| 21 | + const lastTotalRef = useRef(0); |
| 22 | + |
| 23 | + const { messages, sendMessage, isStreaming, tokenUsage, error, stop } = |
| 24 | + useChat({ |
| 25 | + apiUrl: API_URL, |
| 26 | + systemPrompt: 'You are a helpful assistant. Keep responses concise.', |
| 27 | + headers: { |
| 28 | + 'Content-Type': 'application/json', |
| 29 | + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), |
| 30 | + }, |
| 31 | + }); |
| 32 | + |
| 33 | + // Accumulate totalTokens from each completed response |
| 34 | + if (tokenUsage && tokenUsage.totalTokens !== lastTotalRef.current) { |
| 35 | + cumulativeTokens.current += tokenUsage.totalTokens; |
| 36 | + lastTotalRef.current = tokenUsage.totalTokens; |
| 37 | + } |
| 38 | + |
| 39 | + const handleSend = () => { |
| 40 | + const text = input.trim(); |
| 41 | + if (!text || isStreaming) return; |
| 42 | + setInput(''); |
| 43 | + sendMessage(text); |
| 44 | + }; |
| 45 | + |
7 | 46 | return ( |
8 | | - <View style={styles.container}> |
9 | | - <Text>Result: {result}</Text> |
10 | | - </View> |
| 47 | + <KeyboardAvoidingView |
| 48 | + style={styles.container} |
| 49 | + behavior={Platform.OS === 'ios' ? 'padding' : undefined} |
| 50 | + > |
| 51 | + {/* Header */} |
| 52 | + <View style={styles.header}> |
| 53 | + <Text style={styles.headerTitle}>react-native-ai-kit</Text> |
| 54 | + {!apiKey && ( |
| 55 | + <TextInput |
| 56 | + style={styles.apiKeyInput} |
| 57 | + placeholder="Enter OpenAI API key..." |
| 58 | + value={apiKey} |
| 59 | + onChangeText={setApiKey} |
| 60 | + autoCapitalize="none" |
| 61 | + autoCorrect={false} |
| 62 | + secureTextEntry |
| 63 | + /> |
| 64 | + )} |
| 65 | + </View> |
| 66 | + |
| 67 | + {/* Token stats bar */} |
| 68 | + <View style={styles.statsBar}> |
| 69 | + <View style={styles.statItem}> |
| 70 | + <Text style={styles.statLabel}>Req</Text> |
| 71 | + <Text style={styles.statValue}> |
| 72 | + {messages.filter((m) => m.role === 'user').length} |
| 73 | + </Text> |
| 74 | + </View> |
| 75 | + <View style={styles.statDivider} /> |
| 76 | + <View style={styles.statItem}> |
| 77 | + <Text style={styles.statLabel}>Resp</Text> |
| 78 | + <Text style={styles.statValue}> |
| 79 | + {messages.filter((m) => m.role === 'assistant').length} |
| 80 | + </Text> |
| 81 | + </View> |
| 82 | + <View style={styles.statDivider} /> |
| 83 | + <View style={styles.statItem}> |
| 84 | + <Text style={styles.statLabel}>Prompt</Text> |
| 85 | + <Text style={styles.statValue}> |
| 86 | + {tokenUsage?.promptTokens ?? '—'} |
| 87 | + </Text> |
| 88 | + </View> |
| 89 | + <View style={styles.statDivider} /> |
| 90 | + <View style={styles.statItem}> |
| 91 | + <Text style={styles.statLabel}>Complet.</Text> |
| 92 | + <Text style={styles.statValue}> |
| 93 | + {tokenUsage?.completionTokens ?? '—'} |
| 94 | + </Text> |
| 95 | + </View> |
| 96 | + <View style={styles.statDivider} /> |
| 97 | + <View style={styles.statItem}> |
| 98 | + <Text style={styles.statLabel}>Total</Text> |
| 99 | + <Text style={[styles.statValue, styles.statTotal]}> |
| 100 | + {tokenUsage?.totalTokens ?? '—'} |
| 101 | + </Text> |
| 102 | + </View> |
| 103 | + <View style={styles.statDivider} /> |
| 104 | + <View style={styles.statItem}> |
| 105 | + <Text style={styles.statLabel}>All</Text> |
| 106 | + <Text style={[styles.statValue, styles.statAll]}> |
| 107 | + {cumulativeTokens.current || '—'} |
| 108 | + </Text> |
| 109 | + </View> |
| 110 | + </View> |
| 111 | + |
| 112 | + {/* Messages */} |
| 113 | + <ChatList |
| 114 | + messages={messages} |
| 115 | + style={styles.chatList} |
| 116 | + renderStreamingIndicator={() => |
| 117 | + isStreaming && !messages[messages.length - 1]?.content ? ( |
| 118 | + <View style={styles.streamingRow}> |
| 119 | + <Text style={styles.streamingText}>AI is typing</Text> |
| 120 | + <ActivityIndicator size="small" color="#007AFF" /> |
| 121 | + </View> |
| 122 | + ) : null |
| 123 | + } |
| 124 | + renderMessage={(message) => |
| 125 | + message.content ? ( |
| 126 | + <ChatBubble |
| 127 | + message={message} |
| 128 | + variant={message.role === 'user' ? 'user' : 'assistant'} |
| 129 | + /> |
| 130 | + ) : null |
| 131 | + } |
| 132 | + /> |
| 133 | + |
| 134 | + {/* Error */} |
| 135 | + {error && ( |
| 136 | + <View style={styles.errorRow}> |
| 137 | + <Text style={styles.errorText}>{error.message}</Text> |
| 138 | + </View> |
| 139 | + )} |
| 140 | + |
| 141 | + {/* Input */} |
| 142 | + <View style={styles.inputRow}> |
| 143 | + <TextInput |
| 144 | + style={styles.input} |
| 145 | + placeholder="Type a message..." |
| 146 | + value={input} |
| 147 | + onChangeText={setInput} |
| 148 | + editable={!isStreaming} |
| 149 | + multiline |
| 150 | + /> |
| 151 | + {isStreaming ? ( |
| 152 | + <TouchableOpacity style={styles.stopButton} onPress={stop}> |
| 153 | + <Text style={styles.stopButtonText}>Stop</Text> |
| 154 | + </TouchableOpacity> |
| 155 | + ) : ( |
| 156 | + <TouchableOpacity |
| 157 | + style={styles.sendButton} |
| 158 | + onPress={handleSend} |
| 159 | + disabled={!input.trim()} |
| 160 | + > |
| 161 | + <Text |
| 162 | + style={[ |
| 163 | + styles.sendButtonText, |
| 164 | + !input.trim() && styles.sendButtonDisabled, |
| 165 | + ]} |
| 166 | + > |
| 167 | + Send |
| 168 | + </Text> |
| 169 | + </TouchableOpacity> |
| 170 | + )} |
| 171 | + </View> |
| 172 | + </KeyboardAvoidingView> |
11 | 173 | ); |
12 | 174 | } |
13 | 175 |
|
14 | 176 | const styles = StyleSheet.create({ |
15 | 177 | container: { |
16 | 178 | flex: 1, |
| 179 | + paddingTop: Platform.OS === 'ios' ? 50 : 20, |
| 180 | + backgroundColor: '#FFFFFF', |
| 181 | + }, |
| 182 | + header: { |
| 183 | + paddingHorizontal: 16, |
| 184 | + paddingTop: 4, |
| 185 | + paddingBottom: 8, |
| 186 | + borderBottomWidth: StyleSheet.hairlineWidth, |
| 187 | + borderBottomColor: '#C7C7CC', |
| 188 | + }, |
| 189 | + headerTitle: { |
| 190 | + fontSize: 20, |
| 191 | + fontWeight: '700', |
| 192 | + color: '#000000', |
| 193 | + }, |
| 194 | + apiKeyInput: { |
| 195 | + marginTop: 8, |
| 196 | + height: 36, |
| 197 | + borderRadius: 8, |
| 198 | + borderWidth: 1, |
| 199 | + borderColor: '#C7C7CC', |
| 200 | + paddingHorizontal: 10, |
| 201 | + fontSize: 14, |
| 202 | + }, |
| 203 | + statsBar: { |
| 204 | + flexDirection: 'row', |
| 205 | + alignItems: 'center', |
| 206 | + justifyContent: 'center', |
| 207 | + paddingHorizontal: 16, |
| 208 | + paddingVertical: 6, |
| 209 | + backgroundColor: '#F9F9F9', |
| 210 | + borderBottomWidth: StyleSheet.hairlineWidth, |
| 211 | + borderBottomColor: '#E5E5EA', |
| 212 | + }, |
| 213 | + statItem: { |
| 214 | + alignItems: 'center', |
| 215 | + paddingHorizontal: 10, |
| 216 | + }, |
| 217 | + statLabel: { |
| 218 | + fontSize: 10, |
| 219 | + color: '#8E8E93', |
| 220 | + textTransform: 'uppercase', |
| 221 | + fontWeight: '600', |
| 222 | + }, |
| 223 | + statValue: { |
| 224 | + fontSize: 16, |
| 225 | + fontWeight: '700', |
| 226 | + color: '#000000', |
| 227 | + }, |
| 228 | + statTotal: { |
| 229 | + color: '#007AFF', |
| 230 | + }, |
| 231 | + statAll: { |
| 232 | + color: '#34C759', |
| 233 | + }, |
| 234 | + statDivider: { |
| 235 | + width: 1, |
| 236 | + height: 24, |
| 237 | + backgroundColor: '#E5E5EA', |
| 238 | + }, |
| 239 | + chatList: { |
| 240 | + flex: 1, |
| 241 | + backgroundColor: '#F2F2F7', |
| 242 | + }, |
| 243 | + streamingRow: { |
| 244 | + flexDirection: 'row', |
17 | 245 | alignItems: 'center', |
| 246 | + paddingHorizontal: 16, |
| 247 | + paddingVertical: 8, |
| 248 | + }, |
| 249 | + streamingText: { |
| 250 | + fontSize: 13, |
| 251 | + color: '#8E8E93', |
| 252 | + fontStyle: 'italic', |
| 253 | + marginRight: 8, |
| 254 | + }, |
| 255 | + errorRow: { |
| 256 | + paddingHorizontal: 16, |
| 257 | + paddingVertical: 6, |
| 258 | + backgroundColor: '#FF3B3020', |
| 259 | + }, |
| 260 | + errorText: { |
| 261 | + color: '#FF3B30', |
| 262 | + fontSize: 13, |
| 263 | + }, |
| 264 | + inputRow: { |
| 265 | + flexDirection: 'row', |
| 266 | + alignItems: 'flex-end', |
| 267 | + paddingHorizontal: 12, |
| 268 | + paddingVertical: 8, |
| 269 | + paddingBottom: Platform.OS === 'ios' ? 28 : 8, |
| 270 | + borderTopWidth: StyleSheet.hairlineWidth, |
| 271 | + borderTopColor: '#C7C7CC', |
| 272 | + backgroundColor: '#FFFFFF', |
| 273 | + }, |
| 274 | + input: { |
| 275 | + flex: 1, |
| 276 | + minHeight: 36, |
| 277 | + maxHeight: 100, |
| 278 | + borderRadius: 18, |
| 279 | + borderWidth: 1, |
| 280 | + borderColor: '#C7C7CC', |
| 281 | + paddingHorizontal: 14, |
| 282 | + paddingVertical: 8, |
| 283 | + fontSize: 15, |
| 284 | + }, |
| 285 | + sendButton: { |
| 286 | + marginLeft: 8, |
| 287 | + height: 36, |
| 288 | + paddingHorizontal: 16, |
| 289 | + borderRadius: 18, |
| 290 | + backgroundColor: '#007AFF', |
18 | 291 | justifyContent: 'center', |
| 292 | + alignItems: 'center', |
| 293 | + }, |
| 294 | + sendButtonText: { |
| 295 | + color: '#FFFFFF', |
| 296 | + fontSize: 15, |
| 297 | + fontWeight: '600', |
| 298 | + }, |
| 299 | + sendButtonDisabled: { |
| 300 | + opacity: 0.4, |
| 301 | + }, |
| 302 | + stopButton: { |
| 303 | + marginLeft: 8, |
| 304 | + height: 36, |
| 305 | + paddingHorizontal: 16, |
| 306 | + borderRadius: 18, |
| 307 | + backgroundColor: '#FF3B30', |
| 308 | + justifyContent: 'center', |
| 309 | + alignItems: 'center', |
| 310 | + }, |
| 311 | + stopButtonText: { |
| 312 | + color: '#FFFFFF', |
| 313 | + fontSize: 15, |
| 314 | + fontWeight: '600', |
19 | 315 | }, |
20 | 316 | }); |
0 commit comments