Skip to navigation

Platformoverstijgende spraakagents met Expo React Native

Bouw ElevenLabs-agents die werken op iOS en Android met Expo en de ElevenLabs React Native SDK met WebRTC-ondersteuning.

Tutorial · Hierbij wordt ervan uitgegaan dat je de ElevenAgents quickstart hebt voltooid en een Expo-ontwikkelomgeving hebt ingesteld.

Inleiding

In deze tutorial leer je hoe je een spraakagent bouwt die werkt op iOS en Android met Expo React Native en de ElevenLabs React Native SDK met WebRTC-ondersteuning.

Vereisten

  • Een ElevenLabs-account met een API-sleutel.
  • Node.js v18 of hoger geïnstalleerd op je computer.

Instellen

Een nieuw Expo-project maken

Maak met create-expo-app een nieuw leeg Expo-project:

npx create-expo-app@latest --template blank-typescript

Afhankelijkheden installeren

Installeer de ElevenLabs React Native SDK en de bijbehorende afhankelijkheden:

npx expo install @elevenlabs/react-native @livekit/react-native @livekit/react-native-webrtc @config-plugins/react-native-webrtc @livekit/react-native-expo-plugin @livekit/react-native-expo-plugin livekit-client

Als je een probleem ondervindt met peer dependencies, voeg dan een .npmrc-bestand toe in de hoofdmap van het project met de volgende inhoud: legacy-peer-deps=true.

Microfoonmachtigingen inschakelen en Expo-plugins toevoegen

Voeg in het bestand app.json de volgende machtigingen toe:

app.json
{
"expo": {
"scheme": "elevenlabs",
// ...
"ios": {
"infoPlist": {
"NSMicrophoneUsageDescription": "This app uses the microphone to record audio."
},
"supportsTablet": true,
"bundleIdentifier": "YOUR.BUNDLE.ID"
},
"android": {
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.CAMERA",
"android.permission.INTERNET",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.WAKE_LOCK",
"android.permission.BLUETOOTH"
],
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "YOUR.PACKAGE.ID"
},
"plugins": ["@livekit/react-native-expo-plugin", "@config-plugins/react-native-webrtc"]
// ...
}
}

Hiermee kan React Native om microfoonmachtigingen vragen wanneer het gesprek wordt gestart.

Opmerking

Voor de Android-emulator moet je “Virtual microphone uses host audio input” inschakelen in de microfooninstellingen van de emulator.

ElevenLabs Agents aan je app toevoegen

Voeg ElevenLabs Agents aan je app toe door de volgende code toe te voegen aan je bestand ./App.tsx:

./App.tsx
import { ConversationProvider, useConversation } from "@elevenlabs/react-native";
import type { ConversationStatus, ConversationEvent, Role } from "@elevenlabs/react-native";
import React, { useState } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Keyboard,
TouchableWithoutFeedback,
Platform,
} from "react-native";
import { TextInput } from "react-native";
import { getBatteryLevel, changeBrightness, flashScreen } from "./utils/tools";
const ConversationScreen = () => {
const conversation = useConversation({
clientTools: {
getBatteryLevel,
changeBrightness,
flashScreen,
},
onConnect: ({ conversationId }: { conversationId: string }) => {
console.log("✅ Connected to conversation", conversationId);
},
onDisconnect: (details: string) => {
console.log("❌ Disconnected from conversation", details);
},
onError: (message: string, context?: Record<string, unknown>) => {
console.error("❌ Conversation error:", message, context);
},
onMessage: ({ message, source }: { message: ConversationEvent; source: Role }) => {
console.log(`💬 Message from ${source}:`, message);
},
onModeChange: ({ mode }: { mode: "speaking" | "listening" }) => {
console.log(`🔊 Mode: ${mode}`);
},
onStatusChange: ({ status }: { status: ConversationStatus }) => {
console.log(`📡 Status: ${status}`);
},
onCanSendFeedbackChange: ({ canSendFeedback }: { canSendFeedback: boolean }) => {
console.log(`🔊 Can send feedback: ${canSendFeedback}`);
},
});
const [isStarting, setIsStarting] = useState(false);
const [textInput, setTextInput] = useState("");
const handleSubmitText = () => {
if (textInput.trim()) {
conversation.sendUserMessage(textInput.trim());
setTextInput("");
Keyboard.dismiss();
}
};
const startConversation = async () => {
if (isStarting) return;
setIsStarting(true);
try {
await conversation.startSession({
agentId: process.env.EXPO_PUBLIC_AGENT_ID,
dynamicVariables: {
platform: Platform.OS,
},
});
} catch (error) {
console.error("Failed to start conversation:", error);
} finally {
setIsStarting(false);
}
};
const endConversation = async () => {
try {
await conversation.endSession();
} catch (error) {
console.error("Failed to end conversation:", error);
}
};
const getStatusColor = (status: ConversationStatus): string => {
switch (status) {
case "connected":
return "#10B981";
case "connecting":
return "#F59E0B";
case "disconnected":
return "#EF4444";
default:
return "#6B7280";
}
};
const getStatusText = (status: ConversationStatus): string => {
return status[0].toUpperCase() + status.slice(1);
};
const canStart = conversation.status === "disconnected" && !isStarting;
const canEnd = conversation.status === "connected";
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<Text style={styles.title}>ElevenLabs React Native Example</Text>
<Text style={styles.subtitle}>Remember to set the agentId in the .env file!</Text>
<View style={styles.statusContainer}>
<View
style={[styles.statusDot, { backgroundColor: getStatusColor(conversation.status) }]}
/>
<Text style={styles.statusText}>{getStatusText(conversation.status)}</Text>
</View>
{/* Speaking Indicator */}
{conversation.status === "connected" && (
<View style={styles.speakingContainer}>
<View
style={[
styles.speakingDot,
{
backgroundColor: conversation.isSpeaking ? "#8B5CF6" : "#D1D5DB",
},
]}
/>
<Text
style={[
styles.speakingText,
{ color: conversation.isSpeaking ? "#8B5CF6" : "#9CA3AF" },
]}
>
{conversation.isSpeaking ? "🎤 AI Speaking" : "👂 AI Listening"}
</Text>
</View>
)}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, styles.startButton, !canStart && styles.disabledButton]}
onPress={startConversation}
disabled={!canStart}
>
<Text style={styles.buttonText}>
{isStarting ? "Starting..." : "Start Conversation"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.endButton, !canEnd && styles.disabledButton]}
onPress={endConversation}
disabled={!canEnd}
>
<Text style={styles.buttonText}>End Conversation</Text>
</TouchableOpacity>
</View>
{/* Feedback Buttons */}
{conversation.status === "connected" && conversation.canSendFeedback && (
<View style={styles.feedbackContainer}>
<Text style={styles.feedbackLabel}>How was that response?</Text>
<View style={styles.feedbackButtons}>
<TouchableOpacity
style={[styles.button, styles.likeButton]}
onPress={() => conversation.sendFeedback(true)}
>
<Text style={styles.buttonText}>👍 Like</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.dislikeButton]}
onPress={() => conversation.sendFeedback(false)}
>
<Text style={styles.buttonText}>👎 Dislike</Text>
</TouchableOpacity>
</View>
</View>
)}
{/* Text Input and Messaging */}
{conversation.status === "connected" && (
<View style={styles.messagingContainer}>
<Text style={styles.messagingLabel}>Send Text Message</Text>
<TextInput
style={styles.textInput}
value={textInput}
onChangeText={(text) => {
setTextInput(text);
// Prevent agent from interrupting while user is typing
if (text.length > 0) {
conversation.sendUserActivity();
}
}}
placeholder="Type your message or context... (Press Enter to send)"
multiline
onSubmitEditing={handleSubmitText}
returnKeyType="send"
blurOnSubmit={true}
/>
<View style={styles.messageButtons}>
<TouchableOpacity
style={[styles.button, styles.messageButton]}
onPress={handleSubmitText}
disabled={!textInput.trim()}
>
<Text style={styles.buttonText}>💬 Send Message</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.contextButton]}
onPress={() => {
if (textInput.trim()) {
conversation.sendContextualUpdate(textInput.trim());
setTextInput("");
Keyboard.dismiss();
}
}}
disabled={!textInput.trim()}
>
<Text style={styles.buttonText}>📝 Send Context</Text>
</TouchableOpacity>
</View>
</View>
)}
</View>
</TouchableWithoutFeedback>
);
};
export default function App() {
return (
<ConversationProvider>
<ConversationScreen />
</ConversationProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F3F4F6",
padding: 20,
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 8,
color: "#1F2937",
},
subtitle: {
fontSize: 16,
color: "#6B7280",
marginBottom: 32,
},
statusContainer: {
flexDirection: "row",
alignItems: "center",
marginBottom: 24,
},
statusDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 8,
},
statusText: {
fontSize: 16,
fontWeight: "500",
color: "#374151",
},
speakingContainer: {
flexDirection: "row",
alignItems: "center",
marginBottom: 24,
},
speakingDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 8,
},
speakingText: {
fontSize: 14,
fontWeight: "500",
},
toolsContainer: {
backgroundColor: "#E5E7EB",
padding: 16,
borderRadius: 8,
marginBottom: 24,
width: "100%",
},
toolsTitle: {
fontSize: 14,
fontWeight: "600",
color: "#374151",
marginBottom: 8,
},
toolItem: {
fontSize: 12,
color: "#6B7280",
fontFamily: "monospace",
marginBottom: 4,
},
buttonContainer: {
width: "100%",
gap: 16,
},
button: {
backgroundColor: "#3B82F6",
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 8,
alignItems: "center",
},
startButton: {
backgroundColor: "#10B981",
},
endButton: {
backgroundColor: "#EF4444",
},
disabledButton: {
backgroundColor: "#9CA3AF",
},
buttonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
instructions: {
marginTop: 24,
fontSize: 14,
color: "#6B7280",
textAlign: "center",
lineHeight: 20,
},
feedbackContainer: {
marginTop: 24,
alignItems: "center",
},
feedbackLabel: {
fontSize: 16,
fontWeight: "500",
color: "#374151",
marginBottom: 12,
},
feedbackButtons: {
flexDirection: "row",
gap: 16,
},
likeButton: {
backgroundColor: "#10B981",
},
dislikeButton: {
backgroundColor: "#EF4444",
},
messagingContainer: {
marginTop: 24,
width: "100%",
},
messagingLabel: {
fontSize: 16,
fontWeight: "500",
color: "#374151",
marginBottom: 8,
},
textInput: {
backgroundColor: "#FFFFFF",
borderRadius: 8,
padding: 16,
minHeight: 100,
textAlignVertical: "top",
borderWidth: 1,
borderColor: "#D1D5DB",
marginBottom: 16,
},
messageButtons: {
flexDirection: "row",
gap: 16,
},
messageButton: {
backgroundColor: "#3B82F6",
flex: 1,
},
contextButton: {
backgroundColor: "#4F46E5",
flex: 1,
},
activityContainer: {
marginTop: 24,
alignItems: "center",
},
activityLabel: {
fontSize: 14,
color: "#6B7280",
marginBottom: 8,
textAlign: "center",
},
activityButton: {
backgroundColor: "#F59E0B",
},
});

Native clienttools

Een belangrijk onderdeel van het bouwen van ElevenLabs-agents is de agent toegang geven tot functionaliteit en deze dynamisch laten uitvoeren. Dit kan via clienttools.

Maak een nieuw bestand voor je clienttools: ./utils/tools.ts, en voeg de volgende code toe:

./utils/tools.ts
import * as Battery from "expo-battery";
import * as Brightness from "expo-brightness";
const getBatteryLevel = async () => {
const batteryLevel = await Battery.getBatteryLevelAsync();
console.log("batteryLevel", batteryLevel);
if (batteryLevel === -1) {
return "Error: Device does not support retrieving the battery level.";
}
return batteryLevel;
};
const changeBrightness = ({ brightness }: { brightness: number }) => {
console.log("changeBrightness", brightness);
Brightness.setSystemBrightnessAsync(brightness);
return brightness;
};
const flashScreen = () => {
Brightness.setSystemBrightnessAsync(1);
setTimeout(() => {
Brightness.setSystemBrightnessAsync(0);
}, 200);
return "Successfully flashed the screen.";
};
export { getBatteryLevel, changeBrightness, flashScreen };

Dynamische variabelen

Naast de clienttools voegen we ook het platform (web, iOS, Android) als dynamische variabele toe aan zowel het eerste bericht als de prompt:

./App.tsx
// ...
const startConversation = async () => {
if (isStarting) return;
setIsStarting(true);
try {
await conversation.startSession({
agentId: process.env.EXPO_PUBLIC_AGENT_ID,
dynamicVariables: {
platform: Platform.OS,
},
});
} catch (error) {
console.error("Failed to start conversation:", error);
} finally {
setIsStarting(false);
}
};
// ...

Agentconfiguratie

1

Inloggen bij ElevenLabs

Ga naar elevenlabs.io en log in op je account.

2

Een nieuwe agent maken

Ga naar Agents Platform > Agents en maak een nieuwe agent op basis van de lege template.

3

Het eerste bericht instellen

Stel het eerste bericht in en geef de dynamische variabele voor het platform op.

Hi there, woah, so cool that I'm running on {{platform}}. What can I help you with?
4

De systeemprompt instellen

Stel de systeemprompt in. Je kunt hier ook dynamische variabelen opnemen.

You are a helpful assistant running on {{platform}}. You have access to certain tools that allow you to check the user device battery level and change the display brightness. Use these tools if the user asks about them. Otherwise, just answer the question.
5

De clienttools instellen

Stel de volgende clienttools in:

  • Naam: getBatteryLevel
    • Beschrijving: Haalt het batterijniveau van het apparaat op als percentage met een decimaalteken.
    • Wachten op antwoord: true
    • Responstime-out (seconden): 3
  • Naam: changeBrightness
    • Beschrijving: Wijzigt de helderheid van het apparaatscherm.
    • Wachten op antwoord: true
    • Responstime-out (seconden): 3
    • Parameters:
      • Gegevenstype: number
      • ID: brightness
      • Vereist: true
      • Waardetype: LLM Prompt
      • Beschrijving: Een getal tussen 0 en 1, inclusief, dat de gewenste schermhelderheid aangeeft.
  • Naam: flashScreen
    • Beschrijving: Laat het scherm snel aan- en uitflitsen.
    • Wachten op antwoord: true
    • Responstime-out (seconden): 3

De app uitvoeren

Deze app vereist enkele native afhankelijkheden die niet worden ondersteund in Expo Go. Je moet de app daarom vooraf bouwen en vervolgens op een native apparaat uitvoeren.

  • Terminal 1:
    • Voer npx expo prebuild --clean uit
npx expo prebuild --clean
  • Voer npx expo start --tunnel uit om de Expo-ontwikkelserver via https te starten.
npx expo start --tunnel
  • Terminal 2:
    • Voer npx expo run:ios --device uit om de app op je iOS-apparaat uit te voeren.
npx expo run:ios --device

Volgende stappen