1 import React, { useState, useCallback } from "react";
2 import {
3 View,
4 Text,
5 TextInput,
6 TouchableOpacity,
7 FlatList,
8 ActivityIndicator,
9 SafeAreaView,
10 KeyboardAvoidingView,
11 Platform,
12 Alert,
13 StyleSheet,
14 } from "react-native";
15 import {
16 OndeChatEngine,
17 userMessage,
18 type ChatMessage,
19 type EngineStatus,
20 } from "@ondeinference/react-native";
21
22 // ── Onde app credentials ─────────────────────────────────────────────────
23 // Register your app at https://ondeinference.com to get these.
24 // The dashboard lets you assign a model to your app — the SDK fetches
25 // it automatically. If no model is assigned, the platform default is used.
26 const ONDE_APP_ID = "";
27 const ONDE_APP_SECRET = "";
28
29 interface DisplayMessage {
30 id: string;
31 role: "user" | "assistant";
32 content: string;
33 duration?: string;
34 }
35
36 const STATUS_COLORS: Record<EngineStatus, string> = {
37 Unloaded: "#999",
38 Loading: "#FF9500",
39 Ready: "#34C759",
40 Generating: "#007AFF",
41 Error: "#FF3B30",
42 };
43
44 const App: React.FC = () => {
45 const [messages, setMessages] = useState<DisplayMessage[]>([]);
46 const [inputText, setInputText] = useState("");
47 const [engineStatus, setEngineStatus] = useState<EngineStatus>("Unloaded");
48 const [modelName, setModelName] = useState<string | null>(null);
49 const [approxMemory, setApproxMemory] = useState<string | null>(null);
50 const [isLoading, setIsLoading] = useState(false);
51 const [isGenerating, setIsGenerating] = useState(false);
52
53 const normalizeEngineStatus = useCallback((status: string): EngineStatus => {
54 switch (status.toLowerCase()) {
55 case "loading":
56 return "Loading";
57 case "ready":
58 return "Ready";
59 case "generating":
60 return "Generating";
61 case "error":
62 return "Error";
63 case "unloaded":
64 default:
65 return "Unloaded";
66 }
67 }, []);
68
69 const refreshStatus = useCallback(async () => {
70 try {
71 const info = await OndeChatEngine.info();
72 setEngineStatus(normalizeEngineStatus(info.status));
73 setModelName(info.modelName ?? null);
74 setApproxMemory(info.approxMemory ?? null);
75 } catch {
76 /* status refresh is best-effort */
77 }
78 }, [normalizeEngineStatus]);
79
80 const handleLoadModel = useCallback(async () => {
81 setIsLoading(true);
82 setEngineStatus("Loading");
83 try {
84 let seconds: number;
85 if (ONDE_APP_ID && ONDE_APP_SECRET) {
86 seconds = await OndeChatEngine.loadAssignedModel(
87 ONDE_APP_ID,
88 ONDE_APP_SECRET,
89 "You are a helpful, concise assistant.",
90 );
91 } else {
92 seconds = await OndeChatEngine.loadDefaultModel(
93 "You are a helpful, concise assistant.",
94 );
95 }
96 await refreshStatus();
97 Alert.alert("Model Loaded", `Ready in ${seconds.toFixed(1)}s`);
98 } catch (error: unknown) {
99 const message = error instanceof Error ? error.message : "Unknown error";
100 Alert.alert("Load Failed", message);
101 setEngineStatus("Unloaded");
102 } finally {
103 setIsLoading(false);
104 }
105 }, [refreshStatus]);
106
107 const handleUnloadModel = useCallback(async () => {
108 try {
109 const name = await OndeChatEngine.unloadModel();
110 setMessages([]);
111 setEngineStatus("Unloaded");
112 setModelName(null);
113 setApproxMemory(null);
114 if (name) {
115 Alert.alert("Unloaded", `${name} has been unloaded.`);
116 }
117 } catch (error: unknown) {
118 const message = error instanceof Error ? error.message : "Unknown error";
119 Alert.alert("Unload Failed", message);
120 }
121 }, []);
122
123 const handleSendMessage = useCallback(async () => {
124 const text = inputText.trim();
125 if (!text || !OndeChatEngine.isLoaded()) return;
126
127 const userMsg: DisplayMessage = {
128 id: Date.now().toString(),
129 role: "user",
130 content: text,
131 };
132 setMessages((previous) => [...previous, userMsg]);
133 setInputText("");
134 setIsGenerating(true);
135 setEngineStatus("Generating");
136
137 try {
138 const result = await OndeChatEngine.sendMessage(text);
139 const assistantMsg: DisplayMessage = {
140 id: (Date.now() + 1).toString(),
141 role: "assistant",
142 content: result.text,
143 duration: result.durationDisplay,
144 };
145 setMessages((previous) => [...previous, assistantMsg]);
146 } catch (error: unknown) {
147 const message = error instanceof Error ? error.message : "Unknown error";
148 Alert.alert("Inference Error", message);
149 } finally {
150 setIsGenerating(false);
151 await refreshStatus();
152 }
153 }, [inputText, refreshStatus]);
154
155 const handleClearHistory = useCallback(() => {
156 const removed = OndeChatEngine.clearHistory();
157 setMessages([]);
158 Alert.alert("History Cleared", `Removed ${removed} message(s).`);
159 }, []);
160
161 const renderMessage = useCallback(({ item }: { item: DisplayMessage }) => {
162 const isUser = item.role === "user";
163 return (
164 <View
165 style={[
166 styles.messageBubble,
167 isUser ? styles.userBubble : styles.assistantBubble,
168 ]}
169 >
170 <Text style={isUser ? styles.userText : styles.assistantText}>
171 {item.content}
172 </Text>
173 {item.duration && (
174 <Text style={styles.durationText}>{item.duration}</Text>
175 )}
176 </View>
177 );
178 }, []);
179
180 const isLoaded = engineStatus === "Ready" || engineStatus === "Generating";
181 const canSend = isLoaded && inputText.trim().length > 0 && !isGenerating;
182
183 return (
184 <SafeAreaView style={styles.container}>
185 <KeyboardAvoidingView
186 style={styles.flex}
187 behavior={Platform.OS === "ios" ? "padding" : undefined}
188 >
189 {/* Header */}
190 <View style={styles.header}>
191 <View style={styles.headerRow}>
192 <Text style={styles.title}>Onde</Text>
193 <View style={styles.statusBadge}>
194 <View
195 style={[
196 styles.statusDot,
197 { backgroundColor: STATUS_COLORS[engineStatus] },
198 ]}
199 />
200 <Text style={styles.statusText}>{engineStatus}</Text>
201 </View>
202 </View>
203 {modelName && (
204 <Text style={styles.modelInfo}>
205 {modelName}
206 {approxMemory ? ` · ${approxMemory}` : ""}
207 </Text>
208 )}
209 </View>
210
211 {/* Message List */}
212 <FlatList
213 data={messages}
214 renderItem={renderMessage}
215 keyExtractor={(item) => item.id}
216 style={styles.messageList}
217 contentContainerStyle={styles.messageListContent}
218 keyboardShouldPersistTaps="handled"
219 />
220
221 {/* Generating Indicator */}
222 {isGenerating && (
223 <View style={styles.generatingRow}>
224 <ActivityIndicator size="small" color="#007AFF" />
225 <Text style={styles.generatingText}>Generating…</Text>
226 </View>
227 )}
228
229 {/* Bottom Controls */}
230 <View style={styles.bottomArea}>
231 <TouchableOpacity
232 style={[styles.loadButton, isLoaded && styles.unloadButton]}
233 onPress={isLoaded ? handleUnloadModel : handleLoadModel}
234 disabled={isLoading}
235 >
236 {isLoading ? (
237 <ActivityIndicator size="small" color="#fff" />
238 ) : (
239 <Text style={styles.loadButtonText}>
240 {isLoaded ? "Unload" : "Load Model"}
241 </Text>
242 )}
243 </TouchableOpacity>
244
245 <View style={styles.inputRow}>
246 <TextInput
247 style={styles.textInput}
248 value={inputText}
249 onChangeText={setInputText}
250 placeholder={isLoaded ? "Type a message…" : "Load a model first"}
251 placeholderTextColor="#999"
252 editable={isLoaded}
253 returnKeyType="send"
254 onSubmitEditing={canSend ? handleSendMessage : undefined}
255 />
256 <TouchableOpacity
257 style={[styles.sendButton, !canSend && styles.sendButtonDisabled]}
258 onPress={handleSendMessage}
259 disabled={!canSend}
260 >
261 <Text style={styles.sendButtonText}>Send</Text>
262 </TouchableOpacity>
263 </View>
264
265 {messages.length > 0 && (
266 <TouchableOpacity
267 style={styles.clearButton}
268 onPress={handleClearHistory}
269 >
270 <Text style={styles.clearButtonText}>Clear History</Text>
271 </TouchableOpacity>
272 )}
273 </View>
274 </KeyboardAvoidingView>
275 </SafeAreaView>
276 );
277 };
278
279 const styles = StyleSheet.create({
280 container: { flex: 1, backgroundColor: "#fff" },
281 flex: { flex: 1 },
282
283 header: {
284 paddingHorizontal: 16,
285 paddingVertical: 12,
286 borderBottomWidth: StyleSheet.hairlineWidth,
287 borderBottomColor: "#ddd",
288 },
289 headerRow: {
290 flexDirection: "row",
291 justifyContent: "space-between",
292 alignItems: "center",
293 },
294 title: { fontSize: 20, fontWeight: "700", color: "#000" },
295 statusBadge: { flexDirection: "row", alignItems: "center", gap: 6 },
296 statusDot: { width: 8, height: 8, borderRadius: 4 },
297 statusText: { fontSize: 13, color: "#666" },
298 modelInfo: { fontSize: 12, color: "#999", marginTop: 4 },
299
300 messageList: { flex: 1 },
301 messageListContent: { padding: 16, gap: 10 },
302 messageBubble: {
303 maxWidth: "80%",
304 paddingHorizontal: 14,
305 paddingVertical: 10,
306 borderRadius: 16,
307 },
308 userBubble: {
309 alignSelf: "flex-end",
310 backgroundColor: "#007AFF",
311 borderBottomRightRadius: 4,
312 },
313 assistantBubble: {
314 alignSelf: "flex-start",
315 backgroundColor: "#F0F0F0",
316 borderBottomLeftRadius: 4,
317 },
318 userText: { color: "#fff", fontSize: 15 },
319 assistantText: { color: "#000", fontSize: 15 },
320 durationText: { fontSize: 11, color: "#888", marginTop: 4 },
321
322 generatingRow: {
323 flexDirection: "row",
324 alignItems: "center",
325 gap: 8,
326 paddingHorizontal: 16,
327 paddingVertical: 6,
328 },
329 generatingText: { fontSize: 13, color: "#007AFF" },
330
331 bottomArea: {
332 padding: 12,
333 borderTopWidth: StyleSheet.hairlineWidth,
334 borderTopColor: "#ddd",
335 gap: 10,
336 },
337 loadButton: {
338 backgroundColor: "#007AFF",
339 paddingVertical: 12,
340 borderRadius: 10,
341 alignItems: "center",
342 },
343 unloadButton: { backgroundColor: "#FF3B30" },
344 loadButtonText: { color: "#fff", fontSize: 16, fontWeight: "600" },
345
346 inputRow: { flexDirection: "row", gap: 8 },
347 textInput: {
348 flex: 1,
349 borderWidth: StyleSheet.hairlineWidth,
350 borderColor: "#ccc",
351 borderRadius: 10,
352 paddingHorizontal: 14,
353 paddingVertical: 10,
354 fontSize: 15,
355 color: "#000",
356 backgroundColor: "#FAFAFA",
357 },
358 sendButton: {
359 backgroundColor: "#007AFF",
360 paddingHorizontal: 18,
361 borderRadius: 10,
362 justifyContent: "center",
363 },
364 sendButtonDisabled: { backgroundColor: "#B0D4FF" },
365 sendButtonText: { color: "#fff", fontSize: 15, fontWeight: "600" },
366
367 clearButton: { alignSelf: "center", paddingVertical: 4 },
368 clearButtonText: { fontSize: 13, color: "#999" },
369 });
370
371 export default App;