mugib_voice 0.2.1
mugib_voice: ^0.2.1 copied to clipboard
Official Flutter SDK for Mugib voice agents. One client for VAPI and Google Gemini Live — pass apiKey and agentId only.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:mugib_voice/mugib_voice.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await MugibVoice.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Mugib Voice Example',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const CallScreen(),
);
}
}
class CallScreen extends StatefulWidget {
const CallScreen({super.key});
@override
State<CallScreen> createState() => _CallScreenState();
}
class _CallScreenState extends State<CallScreen> {
static const _apiKey = 'YOUR_PROJECT_API_KEY';
List<MugibAgentSummary> _agents = [];
MugibAgentSummary? _selected;
MugibVoiceClient? _call;
MugibCallState _state = MugibCallState.idle;
final List<MugibTranscript> _transcript = [];
bool _muted = false;
bool _loadingAgents = false;
String? _agentError;
@override
void initState() {
super.initState();
_loadAgents();
}
Future<void> _loadAgents() async {
setState(() {
_loadingAgents = true;
_agentError = null;
});
try {
final list = await MugibVoice.listAgents(apiKey: _apiKey);
setState(() {
_agents = list;
_selected = list.isNotEmpty ? list.first : null;
});
} catch (e) {
setState(() => _agentError = '$e');
} finally {
setState(() => _loadingAgents = false);
}
}
Future<void> _start() async {
final agent = _selected;
if (agent == null) return;
setState(() => _transcript.clear());
final call = MugibVoiceClient(apiKey: _apiKey, agentId: agent.id);
_call = call;
call.state.listen((s) => setState(() => _state = s));
call.transcripts.listen((t) => setState(() => _transcript.add(t)));
call.errors.listen((e) {
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error: $e')));
});
try {
await call.start();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('$e')));
}
}
}
Future<void> _end() async {
await _call?.end();
await _call?.dispose();
_call = null;
setState(() => _muted = false);
}
bool get _live =>
_state == MugibCallState.connected ||
_state == MugibCallState.listening ||
_state == MugibCallState.speaking ||
_state == MugibCallState.connecting;
@override
void dispose() {
_call?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mugib Voice')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: _loadingAgents
? const LinearProgressIndicator()
: _agentError != null
? Text(_agentError!, style: const TextStyle(color: Colors.red))
: DropdownButtonFormField<MugibAgentSummary>(
value: _selected,
decoration: const InputDecoration(
labelText: 'Voice agent',
border: OutlineInputBorder(),
),
items: _agents
.map((a) => DropdownMenuItem(
value: a,
child: Text(
'${a.name} (${a.provider}) — ${a.language}'),
))
.toList(),
onChanged: _live
? null
: (v) => setState(() => _selected = v),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: _colorFor(_state).withOpacity(0.1),
child: Text(
_labelFor(_state),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: _colorFor(_state)),
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _transcript.length,
itemBuilder: (_, i) {
final t = _transcript[i];
return Align(
alignment:
t.isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: t.isUser
? Colors.indigo.shade100
: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Text(t.text),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_live) ...[
FloatingActionButton(
heroTag: 'mute',
backgroundColor: _muted ? Colors.orange : Colors.grey,
onPressed: () =>
setState(() => _muted = _call?.toggleMute() ?? false),
child: Icon(_muted ? Icons.mic_off : Icons.mic),
),
const SizedBox(width: 24),
FloatingActionButton.extended(
heroTag: 'end',
backgroundColor: Colors.red,
onPressed: _end,
icon: const Icon(Icons.call_end),
label: const Text('End'),
),
] else
FloatingActionButton.extended(
heroTag: 'start',
onPressed: _selected == null ? null : _start,
icon: const Icon(Icons.call),
label: const Text('Start Call'),
),
],
),
),
],
),
);
}
String _labelFor(MugibCallState s) => switch (s) {
MugibCallState.idle => 'Tap to start',
MugibCallState.connecting => 'Connecting…',
MugibCallState.connected => 'Connected',
MugibCallState.listening => 'Listening…',
MugibCallState.speaking => 'Agent speaking…',
MugibCallState.ended => 'Call ended',
MugibCallState.error => 'Error',
};
Color _colorFor(MugibCallState s) => switch (s) {
MugibCallState.speaking => Colors.indigo,
MugibCallState.listening => Colors.green,
MugibCallState.error => Colors.red,
MugibCallState.ended => Colors.grey,
_ => Colors.blueGrey,
};
}