onde 1.1.0

On-device inference engine for Apple silicon.
Documentation
import React, { useState, useCallback } from "react";
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  ActivityIndicator,
  SafeAreaView,
  KeyboardAvoidingView,
  Platform,
  Alert,
  StyleSheet,
} from "react-native";
import {
  OndeChatEngine,
  userMessage,
  type ChatMessage,
  type EngineStatus,
} from "@ondeinference/react-native";

// ── Onde app credentials ─────────────────────────────────────────────────
// Register your app at https://ondeinference.com to get these.
// The dashboard lets you assign a model to your app — the SDK fetches
// it automatically. If no model is assigned, the platform default is used.
const ONDE_APP_ID = "";
const ONDE_APP_SECRET = "";

interface DisplayMessage {
  id: string;
  role: "user" | "assistant";
  content: string;
  duration?: string;
}

const STATUS_COLORS: Record<EngineStatus, string> = {
  Unloaded: "#999",
  Loading: "#FF9500",
  Ready: "#34C759",
  Generating: "#007AFF",
  Error: "#FF3B30",
};

const App: React.FC = () => {
  const [messages, setMessages] = useState<DisplayMessage[]>([]);
  const [inputText, setInputText] = useState("");
  const [engineStatus, setEngineStatus] = useState<EngineStatus>("Unloaded");
  const [modelName, setModelName] = useState<string | null>(null);
  const [approxMemory, setApproxMemory] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [isGenerating, setIsGenerating] = useState(false);

  const normalizeEngineStatus = useCallback((status: string): EngineStatus => {
    switch (status.toLowerCase()) {
      case "loading":
        return "Loading";
      case "ready":
        return "Ready";
      case "generating":
        return "Generating";
      case "error":
        return "Error";
      case "unloaded":
      default:
        return "Unloaded";
    }
  }, []);

  const refreshStatus = useCallback(async () => {
    try {
      const info = await OndeChatEngine.info();
      setEngineStatus(normalizeEngineStatus(info.status));
      setModelName(info.modelName ?? null);
      setApproxMemory(info.approxMemory ?? null);
    } catch {
      /* status refresh is best-effort */
    }
  }, [normalizeEngineStatus]);

  const handleLoadModel = useCallback(async () => {
    setIsLoading(true);
    setEngineStatus("Loading");
    try {
      let seconds: number;
      if (ONDE_APP_ID && ONDE_APP_SECRET) {
        seconds = await OndeChatEngine.loadAssignedModel(
          ONDE_APP_ID,
          ONDE_APP_SECRET,
          "You are a helpful, concise assistant.",
        );
      } else {
        seconds = await OndeChatEngine.loadDefaultModel(
          "You are a helpful, concise assistant.",
        );
      }
      await refreshStatus();
      Alert.alert("Model Loaded", `Ready in ${seconds.toFixed(1)}s`);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : "Unknown error";
      Alert.alert("Load Failed", message);
      setEngineStatus("Unloaded");
    } finally {
      setIsLoading(false);
    }
  }, [refreshStatus]);

  const handleUnloadModel = useCallback(async () => {
    try {
      const name = await OndeChatEngine.unloadModel();
      setMessages([]);
      setEngineStatus("Unloaded");
      setModelName(null);
      setApproxMemory(null);
      if (name) {
        Alert.alert("Unloaded", `${name} has been unloaded.`);
      }
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : "Unknown error";
      Alert.alert("Unload Failed", message);
    }
  }, []);

  const handleSendMessage = useCallback(async () => {
    const text = inputText.trim();
    if (!text || !OndeChatEngine.isLoaded()) return;

    const userMsg: DisplayMessage = {
      id: Date.now().toString(),
      role: "user",
      content: text,
    };
    setMessages((previous) => [...previous, userMsg]);
    setInputText("");
    setIsGenerating(true);
    setEngineStatus("Generating");

    try {
      const result = await OndeChatEngine.sendMessage(text);
      const assistantMsg: DisplayMessage = {
        id: (Date.now() + 1).toString(),
        role: "assistant",
        content: result.text,
        duration: result.durationDisplay,
      };
      setMessages((previous) => [...previous, assistantMsg]);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : "Unknown error";
      Alert.alert("Inference Error", message);
    } finally {
      setIsGenerating(false);
      await refreshStatus();
    }
  }, [inputText, refreshStatus]);

  const handleClearHistory = useCallback(() => {
    const removed = OndeChatEngine.clearHistory();
    setMessages([]);
    Alert.alert("History Cleared", `Removed ${removed} message(s).`);
  }, []);

  const renderMessage = useCallback(({ item }: { item: DisplayMessage }) => {
    const isUser = item.role === "user";
    return (
      <View
        style={[
          styles.messageBubble,
          isUser ? styles.userBubble : styles.assistantBubble,
        ]}
      >
        <Text style={isUser ? styles.userText : styles.assistantText}>
          {item.content}
        </Text>
        {item.duration && (
          <Text style={styles.durationText}>{item.duration}</Text>
        )}
      </View>
    );
  }, []);

  const isLoaded = engineStatus === "Ready" || engineStatus === "Generating";
  const canSend = isLoaded && inputText.trim().length > 0 && !isGenerating;

  return (
    <SafeAreaView style={styles.container}>
      <KeyboardAvoidingView
        style={styles.flex}
        behavior={Platform.OS === "ios" ? "padding" : undefined}
      >
        {/* Header */}
        <View style={styles.header}>
          <View style={styles.headerRow}>
            <Text style={styles.title}>Onde</Text>
            <View style={styles.statusBadge}>
              <View
                style={[
                  styles.statusDot,
                  { backgroundColor: STATUS_COLORS[engineStatus] },
                ]}
              />
              <Text style={styles.statusText}>{engineStatus}</Text>
            </View>
          </View>
          {modelName && (
            <Text style={styles.modelInfo}>
              {modelName}
              {approxMemory ? ` · ${approxMemory}` : ""}
            </Text>
          )}
        </View>

        {/* Message List */}
        <FlatList
          data={messages}
          renderItem={renderMessage}
          keyExtractor={(item) => item.id}
          style={styles.messageList}
          contentContainerStyle={styles.messageListContent}
          keyboardShouldPersistTaps="handled"
        />

        {/* Generating Indicator */}
        {isGenerating && (
          <View style={styles.generatingRow}>
            <ActivityIndicator size="small" color="#007AFF" />
            <Text style={styles.generatingText}>Generating…</Text>
          </View>
        )}

        {/* Bottom Controls */}
        <View style={styles.bottomArea}>
          <TouchableOpacity
            style={[styles.loadButton, isLoaded && styles.unloadButton]}
            onPress={isLoaded ? handleUnloadModel : handleLoadModel}
            disabled={isLoading}
          >
            {isLoading ? (
              <ActivityIndicator size="small" color="#fff" />
            ) : (
              <Text style={styles.loadButtonText}>
                {isLoaded ? "Unload" : "Load Model"}
              </Text>
            )}
          </TouchableOpacity>

          <View style={styles.inputRow}>
            <TextInput
              style={styles.textInput}
              value={inputText}
              onChangeText={setInputText}
              placeholder={isLoaded ? "Type a message…" : "Load a model first"}
              placeholderTextColor="#999"
              editable={isLoaded}
              returnKeyType="send"
              onSubmitEditing={canSend ? handleSendMessage : undefined}
            />
            <TouchableOpacity
              style={[styles.sendButton, !canSend && styles.sendButtonDisabled]}
              onPress={handleSendMessage}
              disabled={!canSend}
            >
              <Text style={styles.sendButtonText}>Send</Text>
            </TouchableOpacity>
          </View>

          {messages.length > 0 && (
            <TouchableOpacity
              style={styles.clearButton}
              onPress={handleClearHistory}
            >
              <Text style={styles.clearButtonText}>Clear History</Text>
            </TouchableOpacity>
          )}
        </View>
      </KeyboardAvoidingView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#fff" },
  flex: { flex: 1 },

  header: {
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: "#ddd",
  },
  headerRow: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
  },
  title: { fontSize: 20, fontWeight: "700", color: "#000" },
  statusBadge: { flexDirection: "row", alignItems: "center", gap: 6 },
  statusDot: { width: 8, height: 8, borderRadius: 4 },
  statusText: { fontSize: 13, color: "#666" },
  modelInfo: { fontSize: 12, color: "#999", marginTop: 4 },

  messageList: { flex: 1 },
  messageListContent: { padding: 16, gap: 10 },
  messageBubble: {
    maxWidth: "80%",
    paddingHorizontal: 14,
    paddingVertical: 10,
    borderRadius: 16,
  },
  userBubble: {
    alignSelf: "flex-end",
    backgroundColor: "#007AFF",
    borderBottomRightRadius: 4,
  },
  assistantBubble: {
    alignSelf: "flex-start",
    backgroundColor: "#F0F0F0",
    borderBottomLeftRadius: 4,
  },
  userText: { color: "#fff", fontSize: 15 },
  assistantText: { color: "#000", fontSize: 15 },
  durationText: { fontSize: 11, color: "#888", marginTop: 4 },

  generatingRow: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
    paddingHorizontal: 16,
    paddingVertical: 6,
  },
  generatingText: { fontSize: 13, color: "#007AFF" },

  bottomArea: {
    padding: 12,
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: "#ddd",
    gap: 10,
  },
  loadButton: {
    backgroundColor: "#007AFF",
    paddingVertical: 12,
    borderRadius: 10,
    alignItems: "center",
  },
  unloadButton: { backgroundColor: "#FF3B30" },
  loadButtonText: { color: "#fff", fontSize: 16, fontWeight: "600" },

  inputRow: { flexDirection: "row", gap: 8 },
  textInput: {
    flex: 1,
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: "#ccc",
    borderRadius: 10,
    paddingHorizontal: 14,
    paddingVertical: 10,
    fontSize: 15,
    color: "#000",
    backgroundColor: "#FAFAFA",
  },
  sendButton: {
    backgroundColor: "#007AFF",
    paddingHorizontal: 18,
    borderRadius: 10,
    justifyContent: "center",
  },
  sendButtonDisabled: { backgroundColor: "#B0D4FF" },
  sendButtonText: { color: "#fff", fontSize: 15, fontWeight: "600" },

  clearButton: { alignSelf: "center", paddingVertical: 4 },
  clearButtonText: { fontSize: 13, color: "#999" },
});

export default App;