> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Wieloplatformowi agenci głosowi z Expo React Native

> **Note**
>
> **Samouczek** · Zakłada, że masz już za sobą [szybki start z ElevenAgents ](/docs/pl/eleven-agents/quickstart) i skonfigurowane środowisko programistyczne Expo.

## Wprowadzenie

W tym samouczku dowiesz się, jak stworzyć agenta głosowego działającego na iOS i Androidzie z użyciem [Expo React Native](https://expo.dev/) oraz pakietu ElevenLabs [React Native SDK](/docs/pl/eleven-agents/libraries/react-native) z obsługą WebRTC.

## Wymagania

* Konto ElevenLabs z [kluczem API](https://elevenlabs.io/app/settings/api-keys).
* Node.js v18 lub nowszy zainstalowany na twoim komputerze.

## Konfiguracja

### Utwórz nowy projekt Expo

Za pomocą `create-expo-app` utwórz nowy pusty projekt Expo:

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

### Zainstaluj zależności

Zainstaluj ElevenLabs React Native SDK i jego zależności:

```bash
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
```

> **Note**
>
> Jeśli masz problem z zależnościami peer, dodaj plik `.npmrc` w katalogu głównym
> projektu z następującą zawartością: `legacy-peer-deps=true`.

### Włącz uprawnienia mikrofonu i dodaj wtyczki Expo

W pliku `app.json` dodaj następujące uprawnienia:

**`app.json`**

```json 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"]
    // ...
  }
}
```

Dzięki temu React Native poprosi o uprawnienia do mikrofonu po rozpoczęciu rozmowy.

> **Uwaga**
>
> W emulatorze Androida musisz włączyć opcję „Virtual microphone uses host audio input” w
> ustawieniach mikrofonu emulatora.

## Dodaj ElevenLabs Agents do aplikacji

Dodaj ElevenLabs Agents do aplikacji, wstawiając poniższy kod do pliku `./App.tsx`:

**`./App.tsx`**

```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",
  },
});
```

### Natywne narzędzia klienta

Ważną częścią tworzenia agentów ElevenLabs jest umożliwienie agentowi dynamicznego dostępu do funkcji i ich wykonywania. Możesz to zrobić za pomocą [narzędzi klienta](/docs/pl/eleven-agents/customization/tools/client-tools).

Utwórz nowy plik na narzędzia klienta: `./utils/tools.ts`, a następnie dodaj poniższy kod:

**`./utils/tools.ts`**

```ts ./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 };
```

### Zmienne dynamiczne

Oprócz narzędzi klienta przekazujemy też platformę (web, iOS, Android) jako [zmienną dynamiczną](https://elevenlabs.io/docs/eleven-agents/customization/personalization/dynamic-variables) — zarówno do pierwszej wiadomości, jak i promptu:

**`./App.tsx`**

```tsx ./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);
  }
};
// ...
```

## Konfiguracja agenta

#### Zaloguj się do ElevenLabs

Wejdź na [elevenlabs.io](https://elevenlabs.io/app/sign-up) i zaloguj się na swoje konto.

#### Utwórz nowego agenta

Przejdź do [Agents Platform > Agents](https://elevenlabs.io/app/agents/agents) i
utwórz nowego agenta na podstawie pustego szablonu.

#### Ustaw pierwszą wiadomość

Ustaw pierwszą wiadomość i określ zmienną dynamiczną platformy.

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

#### Ustaw prompt systemowy

Ustaw prompt systemowy. Możesz też uwzględnić tu zmienne dynamiczne.

```txt
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.
```

#### Skonfiguruj narzędzia klienta

Skonfiguruj następujące narzędzia klienta:

* Nazwa: `getBatteryLevel`
  * Opis: Pobiera poziom baterii urządzenia jako wartość dziesiętną w procentach.
  * Czekaj na odpowiedź: `true`
  * Limit czasu odpowiedzi (sekundy): 3
* Nazwa: `changeBrightness`
  * Opis: Zmienia jasność ekranu urządzenia.
  * Czekaj na odpowiedź: `true`
  * Limit czasu odpowiedzi (sekundy): 3
  * Parametry:
    * Typ danych: `number`
    * Identyfikator: `brightness`
    * Wymagany: `true`
    * Typ wartości: `LLM Prompt`
    * Opis: Liczba od 0 do 1 włącznie, określająca żądaną jasność ekranu.
* Nazwa: `flashScreen`
  * Opis: Szybko włącza i wyłącza ekran.
  * Czekaj na odpowiedź: `true`
  * Limit czasu odpowiedzi (sekundy): 3

## Uruchom aplikację

Ta aplikacja wymaga kilku natywnych zależności, których Expo Go nie obsługuje. Musisz więc wstępnie zbudować aplikację, a potem uruchomić ją na natywnym urządzeniu.

* Terminal 1:
  * Uruchom `npx expo prebuild --clean`

```bash
npx expo prebuild --clean
```

* Uruchom `npx expo start --tunnel`, aby uruchomić serwer deweloperski Expo przez HTTPS.

```bash
npx expo start --tunnel
```

* Terminal 2:
  * Uruchom `npx expo run:ios --device`, aby uruchomić aplikację na urządzeniu z iOS.

```bash
npx expo run:ios --device
```

## Kolejne kroki

#### [ElevenAgents](/docs/pl/eleven-agents/quickstart)

Twórz i wdrażaj agentów głosowych z pełną platformą ElevenAgents.

#### [Przegląd integracji ElevenAgents](/docs/pl/eleven-agents/integrate/overview)

Poznaj wszystkie opcje integracji, w tym widżety webowe, natywne SDK i telefonię.