elevenlabs_agents 0.3.0 copy "elevenlabs_agents: ^0.3.0" to clipboard
elevenlabs_agents: ^0.3.0 copied to clipboard

Flutter SDK for ElevenLabs Agent Platform. Build conversational AI applications with real-time audio communication.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:elevenlabs_agents/elevenlabs_agents.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  // Load environment variables
  await dotenv.load(fileName: ".env");
  runApp(const MyApp());
}

/// Simple client tool that logs a message to the console
class LogMessageTool implements ClientTool {
  @override
  Future<ClientToolResult?> execute(Map<String, dynamic> parameters) async {
    final message = parameters['message'] as String?;

    if (message == null || message.isEmpty) {
      return ClientToolResult.failure('Missing or empty message parameter');
    }

    // Log the message to console
    debugPrint('📢 Agent Tool Call - Log Message: $message');

    // Fire-and-forget tool - no response needed
    return null;
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ElevenLabs Flutter Example',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.black,
          brightness: Brightness.light,
        ),
        scaffoldBackgroundColor: Colors.white,
      ),
      home: const ConversationScreen(),
    );
  }
}

class ConversationScreen extends StatefulWidget {
  const ConversationScreen({super.key});

  @override
  State<ConversationScreen> createState() => _ConversationScreenState();
}

class _ConversationScreenState extends State<ConversationScreen> {
  late ConversationClient _client;
  final _agentIdController = TextEditingController(
    text: dotenv.env['AGENT_ID'] ?? '',
  );
  final _messageController = TextEditingController();

  void Function()? _clientListener;

  @override
  void initState() {
    super.initState();
    _requestMicrophonePermission();
    _initializeClient();
  }

  Future<void> _requestMicrophonePermission() async {
    final status = await Permission.microphone.request();
    if (!status.isGranted) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Microphone permission is required'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  void _initializeClient() {
    _client = ConversationClient(
      clientTools: {'logMessage': LogMessageTool()},
      callbacks: ConversationCallbacks(
        onConnect: ({required conversationId}) {
          debugPrint('✅ Connected: $conversationId');
          _showSnackBar('Connected: $conversationId', Colors.green);
        },
        onDisconnect: (details) {
          debugPrint('❌ Disconnected: ${details.reason}');
        },
        onMessage: ({required message, required source}) {
          debugPrint('💬 ${source.name}: $message');
        },
        onModeChange: ({required mode}) {
          debugPrint('🔊 Mode: ${mode.name}');
        },
        onStatusChange: ({required status}) {
          debugPrint('📡 Status: ${status.name}');
        },
        onError: (message, [context]) {
          debugPrint('❌ Error: $message');
          _showSnackBar('Error: $message', Colors.red);
        },
        onVadScore: ({required vadScore}) {
          // Voice activity detection score
          // Can be used for visualization
        },
        onInterruption: (event) {
          debugPrint('⚡ Interruption detected');
        },
        onCanSendFeedbackChange: ({required canSendFeedback}) {
          setState(() {});
        },
        onTentativeUserTranscript: ({required transcript, required eventId}) {
          debugPrint('🎤 User speaking (live): "$transcript" [#$eventId]');
        },
        onUserTranscript: ({required transcript, required eventId}) {
          debugPrint('✅ User said: "$transcript" [#$eventId]');
        },
        onTentativeAgentResponse: ({required response}) {
          debugPrint('💭 Agent composing: "$response"');
        },
        onAgentResponseCorrection: (correction) {
          debugPrint('🔧 Agent correction: $correction');
        },
        onAgentChatResponsePart: (part) {
          debugPrint('📝 Agent text part [${part.type}]: "${part.text}"');
        },
        onDebug: (data) {
          debugPrint('🐛 Debug: $data');
        },
        onUnhandledClientToolCall: (toolCall) {
          debugPrint('⚠️ Unhandled tool call: ${toolCall.toolName}');
          _showSnackBar(
            'Tool not implemented: ${toolCall.toolName}',
            Colors.orange,
          );
        },
      ),
    );

    _clientListener = () {
      setState(() {});
    };
    _client.addListener(_clientListener!);
  }

  void _showSnackBar(String message, Color backgroundColor) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: backgroundColor,
        duration: const Duration(seconds: 2),
      ),
    );
  }

  @override
  void dispose() {
    if (_clientListener != null) {
      _client.removeListener(_clientListener!);
    }
    _client.dispose();
    _agentIdController.dispose();
    _messageController.dispose();
    super.dispose();
  }

  Future<void> _startConversation() async {
    final agentId = _agentIdController.text.trim();
    if (agentId.isEmpty) {
      _showSnackBar('Please enter an agent ID', Colors.red);
      return;
    }

    try {
      await _client.startSession(agentId: agentId, userId: 'demo-user');
    } catch (e) {
      _showSnackBar('Failed to start: $e', Colors.red);
    }
  }

  Future<void> _endConversation() async {
    try {
      await _client.endSession();
    } catch (e) {
      _showSnackBar('Failed to end: $e', Colors.red);
    }
  }

  Future<void> _toggleMute() async {
    await _client.toggleMute();
    setState(() {});
  }

  void _sendMessage() {
    final text = _messageController.text.trim();
    if (text.isEmpty) {
      _showSnackBar('Please enter a message', Colors.orange);
      return;
    }

    _client.sendUserMessage(text);
    _messageController.clear();
    _showSnackBar('Message sent', Colors.green);
  }

  void _sendContextualMessage() {
    final text = _messageController.text.trim();
    if (text.isEmpty) {
      _showSnackBar('Please enter a contextual message', Colors.orange);
      return;
    }

    _client.sendContextualUpdate(text);
    _messageController.clear();
    _showSnackBar('Contextual message sent', Colors.blue);
  }

  @override
  Widget build(BuildContext context) {
    final isConnected = _client.status == ConversationStatus.connected;
    final isDisconnected = _client.status == ConversationStatus.disconnected;

    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // ElevenLabs Logo
                Image.asset(
                  'assets/elevenlabs_logo.png',
                  height: 40,
                  fit: BoxFit.contain,
                ),
                const SizedBox(height: 16),
                Text(
                  'Flutter Example App',
                  style: TextStyle(
                    fontSize: 16,
                    color: Colors.grey[700],
                    letterSpacing: 1,
                  ),
                ),
                const SizedBox(height: 60),

                // Agent ID Input (only when disconnected)
                if (isDisconnected) ...[
                  Container(
                    constraints: const BoxConstraints(maxWidth: 400),
                    child: TextField(
                      controller: _agentIdController,
                      style: const TextStyle(fontSize: 14, color: Colors.black),
                      decoration: InputDecoration(
                        labelText: 'Agent ID',
                        hintText: 'Enter your agent ID',
                        filled: true,
                        fillColor: Colors.grey[100],
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                          borderSide: BorderSide.none,
                        ),
                        enabledBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                          borderSide: BorderSide(color: Colors.grey[300]!),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                          borderSide: const BorderSide(
                            color: Colors.black,
                            width: 2,
                          ),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 32),
                ],

                // Speaking Indicator
                if (isConnected) ...[
                  Container(
                    padding: const EdgeInsets.all(22),
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      gradient: RadialGradient(
                        colors: [
                          (_client.isSpeaking
                                  ? Colors.black
                                  : Colors.grey[400]!)
                              .withValues(alpha: 0.2),
                          Colors.transparent,
                        ],
                      ),
                    ),
                    child: Container(
                      padding: const EdgeInsets.all(20),
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: _client.isSpeaking
                            ? Colors.black
                            : Colors.grey[300],
                      ),
                      child: Icon(
                        _client.isSpeaking ? Icons.graphic_eq : Icons.mic,
                        size: 43,
                        color: _client.isSpeaking
                            ? Colors.white
                            : Colors.grey[700],
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  Text(
                    _client.isSpeaking ? 'Agent Speaking...' : 'Listening...',
                    style: TextStyle(
                      fontSize: 18,
                      color: Colors.grey[700],
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                  const SizedBox(height: 32),
                ],

                // Main Action Button
                Container(
                  constraints: const BoxConstraints(maxWidth: 400),
                  height: 56,
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: isDisconnected
                        ? _startConversation
                        : isConnected
                            ? _endConversation
                            : null,
                    style: ElevatedButton.styleFrom(
                      backgroundColor:
                          isConnected ? Colors.red[600] : Colors.black,
                      foregroundColor: Colors.white,
                      elevation: 0,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                      disabledBackgroundColor: Colors.grey[300],
                    ),
                    child: Text(
                      isConnected
                          ? 'Disconnect'
                          : isDisconnected
                              ? 'Connect'
                              : 'Connecting...',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.w600,
                        letterSpacing: 0.5,
                      ),
                    ),
                  ),
                ),

                // Mute Button (only when connected)
                if (isConnected) ...[
                  const SizedBox(height: 16),
                  Container(
                    constraints: const BoxConstraints(maxWidth: 400),
                    height: 56,
                    width: double.infinity,
                    child: OutlinedButton.icon(
                      onPressed: _toggleMute,
                      icon: Icon(_client.isMuted ? Icons.mic_off : Icons.mic),
                      label: Text(_client.isMuted ? 'Unmute' : 'Mute'),
                      style: OutlinedButton.styleFrom(
                        foregroundColor: _client.isMuted
                            ? Colors.red[600]
                            : Colors.grey[700],
                        side: BorderSide(
                          color: (_client.isMuted
                              ? Colors.red[600]
                              : Colors.grey[400])!,
                        ),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                    ),
                  ),

                  // Feedback Buttons (only when feedback can be sent)
                  if (_client.canSendFeedback) ...[
                    const SizedBox(height: 16),
                    Container(
                      constraints: const BoxConstraints(maxWidth: 400),
                      child: Column(
                        children: [
                          Text(
                            'Rate the last response',
                            style: TextStyle(
                              fontSize: 14,
                              color: Colors.grey[600],
                              fontWeight: FontWeight.w500,
                            ),
                          ),
                          const SizedBox(height: 8),
                          Row(
                            children: [
                              Expanded(
                                child: OutlinedButton.icon(
                                  onPressed: () =>
                                      _client.sendFeedback(isPositive: true),
                                  icon: const Icon(
                                    Icons.thumb_up_outlined,
                                    size: 20,
                                  ),
                                  label: const Text('Good'),
                                  style: OutlinedButton.styleFrom(
                                    foregroundColor: Colors.green[700],
                                    side: BorderSide(color: Colors.green[400]!),
                                    padding: const EdgeInsets.symmetric(
                                      vertical: 12,
                                    ),
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(10),
                                    ),
                                  ),
                                ),
                              ),
                              const SizedBox(width: 12),
                              Expanded(
                                child: OutlinedButton.icon(
                                  onPressed: () =>
                                      _client.sendFeedback(isPositive: false),
                                  icon: const Icon(
                                    Icons.thumb_down_outlined,
                                    size: 20,
                                  ),
                                  label: const Text('Bad'),
                                  style: OutlinedButton.styleFrom(
                                    foregroundColor: Colors.red[700],
                                    side: BorderSide(color: Colors.red[400]!),
                                    padding: const EdgeInsets.symmetric(
                                      vertical: 12,
                                    ),
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(10),
                                    ),
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ],

                  // Message Input Section (only when connected)
                  const SizedBox(height: 32),
                  Container(
                    constraints: const BoxConstraints(maxWidth: 400),
                    child: Column(
                      children: [
                        // Text Input Field
                        TextField(
                          controller: _messageController,
                          maxLines: 3,
                          style: const TextStyle(
                            fontSize: 14,
                            color: Colors.black,
                          ),
                          onChanged: (_) => _client.sendUserActivity(),
                          onSubmitted: (_) => _sendMessage(),
                          textInputAction: TextInputAction.send,
                          decoration: InputDecoration(
                            hintText: 'Type your message here...',
                            filled: true,
                            fillColor: Colors.grey[100],
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: BorderSide.none,
                            ),
                            enabledBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: BorderSide(color: Colors.grey[300]!),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: const BorderSide(
                                color: Colors.black,
                                width: 2,
                              ),
                            ),
                          ),
                        ),
                        const SizedBox(height: 12),
                        // Action Buttons Row
                        Row(
                          children: [
                            Expanded(
                              child: ElevatedButton(
                                onPressed: _sendMessage,
                                style: ElevatedButton.styleFrom(
                                  backgroundColor: Colors.black,
                                  foregroundColor: Colors.white,
                                  elevation: 0,
                                  padding: const EdgeInsets.symmetric(
                                    vertical: 14,
                                  ),
                                  shape: RoundedRectangleBorder(
                                    borderRadius: BorderRadius.circular(10),
                                  ),
                                ),
                                child: const Text(
                                  'Send',
                                  style: TextStyle(
                                    fontSize: 14,
                                    fontWeight: FontWeight.w600,
                                  ),
                                ),
                              ),
                            ),
                            const SizedBox(width: 12),
                            Expanded(
                              child: OutlinedButton(
                                onPressed: _sendContextualMessage,
                                style: OutlinedButton.styleFrom(
                                  foregroundColor: Colors.grey[700],
                                  side: BorderSide(color: Colors.grey[400]!),
                                  padding: const EdgeInsets.symmetric(
                                    vertical: 14,
                                  ),
                                  shape: RoundedRectangleBorder(
                                    borderRadius: BorderRadius.circular(10),
                                  ),
                                ),
                                child: const Text(
                                  'Send contextual',
                                  style: TextStyle(
                                    fontSize: 14,
                                    fontWeight: FontWeight.w600,
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ],

                // Status Indicator
                const SizedBox(height: 32),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 8,
                  ),
                  decoration: BoxDecoration(
                    color: _getStatusColor(
                      _client.status,
                    ).withValues(alpha: 0.1),
                    borderRadius: BorderRadius.circular(20),
                    border: Border.all(
                      color: _getStatusColor(
                        _client.status,
                      ).withValues(alpha: 0.3),
                    ),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Container(
                        width: 8,
                        height: 8,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: _getStatusColor(_client.status),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Text(
                        _client.status.name.toUpperCase(),
                        style: TextStyle(
                          color: _getStatusColor(_client.status),
                          fontSize: 12,
                          fontWeight: FontWeight.w600,
                          letterSpacing: 1,
                        ),
                      ),
                    ],
                  ),
                ),

                // Conversation ID
                if (_client.conversationId != null) ...[
                  const SizedBox(height: 12),
                  Text(
                    'ID: ${_client.conversationId}',
                    style: TextStyle(
                      fontSize: 11,
                      color: Colors.grey[500],
                      fontFamily: 'monospace',
                    ),
                  ),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }

  Color _getStatusColor(ConversationStatus status) {
    switch (status) {
      case ConversationStatus.connected:
        return Colors.green;
      case ConversationStatus.connecting:
        return Colors.orange;
      case ConversationStatus.disconnecting:
        return Colors.orange;
      case ConversationStatus.disconnected:
        return Colors.grey;
    }
  }
}
0
likes
160
points
412
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for ElevenLabs Agent Platform. Build conversational AI applications with real-time audio communication.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, http, livekit_client

More

Packages that depend on elevenlabs_agents