voicebot_flutter 0.2.0 copy "voicebot_flutter: ^0.2.0" to clipboard
voicebot_flutter: ^0.2.0 copied to clipboard

Drop-in voice and text store assistant for Flutter apps. The same VoiceBot assistant that runs as the web widget, native in your app over WebSocket + PCM16.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:voicebot_flutter/voicebot_flutter.dart';

void main() => runApp(const ExampleApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'VoiceBot SDK Example',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const SessionScreen(),
    );
  }
}

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

  @override
  State<SessionScreen> createState() => _SessionScreenState();
}

class _SessionScreenState extends State<SessionScreen> {
  final _baseUrlController =
      TextEditingController(text: 'https://api.voicebot.app');
  final _tokenController = TextEditingController();
  final _textController = TextEditingController();

  VoicebotClient? _client;
  VoicebotSession? _session;
  final _subs = <StreamSubscription<Object?>>[];
  final _lines = <Transcript>[];

  SessionStatus _status = SessionStatus.ended;
  AudioState _audioState = AudioState.inactive;
  String? _lastError;
  bool _muted = false;

  @override
  void dispose() {
    unawaited(_disposeSession());
    _baseUrlController.dispose();
    _tokenController.dispose();
    _textController.dispose();
    super.dispose();
  }

  Future<void> _disposeSession() async {
    for (final s in _subs) {
      await s.cancel();
    }
    _subs.clear();
    await _session?.dispose();
    await _client?.destroy();
    _session = null;
    _client = null;
  }

  void _start({required bool withVoice}) {
    unawaited(_disposeSession());
    setState(() {
      _lines.clear();
      _lastError = null;
      _muted = false;
    });

    final client = VoicebotClient.init(
      baseUrl: _baseUrlController.text.trim(),
      tokenProvider: StaticTokenProvider.value(_tokenController.text.trim()),
    );
    final session = withVoice
        ? client.startVoiceSession(config: const SessionConfig(lang: 'uk'))
        : client.startTextSession(lang: 'uk');

    _subs.add(session.status.listen((s) => setState(() => _status = s)));
    _subs.add(session.audioState.listen((a) => setState(() => _audioState = a)));
    _subs.add(session.transcripts.listen((t) {
      setState(() {
        // Replace a trailing partial of the same role, else append.
        if (_lines.isNotEmpty &&
            _lines.last.role == t.role &&
            _lines.last.isPartial) {
          _lines[_lines.length - 1] = t;
        } else {
          _lines.add(t);
        }
      });
    }));
    _subs.add(session.errors.listen((e) => setState(() => _lastError = e.message)));
    _subs.add(session.toolCalls.listen((c) => debugPrint('tool_call: ${c.name}')));
    _subs.add(session.actions.listen((a) =>
        debugPrint('action: ${a.action} handled=${a.handled} err=${a.error}')));
    _subs.add(session.deepLinks.listen((d) => debugPrint('deep link: ${d.uri}')));

    setState(() {
      _client = client;
      _session = session;
    });
  }

  void _sendText() {
    final text = _textController.text.trim();
    if (text.isEmpty) return;
    final ok = _session?.sendText(text) ?? false;
    if (ok) _textController.clear();
  }

  Future<void> _toggleMute() async {
    final next = !_muted;
    await _session?.setMuted(next);
    setState(() => _muted = next);
  }

  @override
  Widget build(BuildContext context) {
    final connected = _session != null;
    return Scaffold(
      appBar: AppBar(title: const Text('VoiceBot SDK Example')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            TextField(
              controller: _baseUrlController,
              decoration: const InputDecoration(
                labelText: 'baseUrl',
                hintText: 'https://api.voicebot.app',
              ),
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _tokenController,
              decoration: const InputDecoration(
                labelText: 'session token (dev JWT)',
              ),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () => _start(withVoice: true),
                    icon: const Icon(Icons.mic),
                    label: const Text('Voice'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () => _start(withVoice: false),
                    icon: const Icon(Icons.chat),
                    label: const Text('Text only'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            _StatusBar(
              status: _status,
              audioState: _audioState,
              error: _lastError,
            ),
            const Divider(),
            Expanded(
              child: _lines.isEmpty
                  ? const Center(child: Text('No transcript yet.'))
                  : ListView.builder(
                      itemCount: _lines.length,
                      itemBuilder: (context, i) {
                        final line = _lines[i];
                        final isUser = line.role == TranscriptRole.user;
                        return Align(
                          alignment: isUser
                              ? Alignment.centerRight
                              : Alignment.centerLeft,
                          child: Container(
                            margin: const EdgeInsets.symmetric(vertical: 4),
                            padding: const EdgeInsets.all(10),
                            decoration: BoxDecoration(
                              color: isUser
                                  ? Colors.indigo.shade100
                                  : Colors.grey.shade200,
                              borderRadius: BorderRadius.circular(12),
                            ),
                            child: Text(line.text + (line.isPartial ? ' …' : '')),
                          ),
                        );
                      },
                    ),
            ),
            if (connected) ...[
              Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: _textController,
                      onSubmitted: (_) => _sendText(),
                      decoration: const InputDecoration(
                        hintText: 'Type a message…',
                      ),
                    ),
                  ),
                  IconButton(
                    onPressed: _sendText,
                    icon: const Icon(Icons.send),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: OutlinedButton.icon(
                      onPressed: _toggleMute,
                      icon: Icon(_muted ? Icons.mic_off : Icons.mic),
                      label: Text(_muted ? 'Unmute' : 'Mute'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: OutlinedButton.icon(
                      // Minimize keeps the session alive (sends no `end`).
                      onPressed: () => _session?.minimize(),
                      icon: const Icon(Icons.remove),
                      label: const Text('Minimize'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: FilledButton.icon(
                      style: FilledButton.styleFrom(backgroundColor: Colors.red),
                      // Close ends the session (sends `end`).
                      onPressed: () async {
                        await _session?.endSession();
                        await _disposeSession();
                        if (mounted) setState(() {});
                      },
                      icon: const Icon(Icons.close),
                      label: const Text('Close'),
                    ),
                  ),
                ],
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _StatusBar extends StatelessWidget {
  const _StatusBar({
    required this.status,
    required this.audioState,
    required this.error,
  });

  final SessionStatus status;
  final AudioState audioState;
  final String? error;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Status: ${status.name}  ·  Audio: ${audioState.name}'),
        if (error != null)
          Padding(
            padding: const EdgeInsets.only(top: 4),
            child: Text(
              'Error: $error',
              style: const TextStyle(color: Colors.red),
            ),
          ),
      ],
    );
  }
}
1
likes
0
points
18
downloads

Publisher

verified publishermonoverse.tech

Weekly Downloads

Drop-in voice and text store assistant for Flutter apps. The same VoiceBot assistant that runs as the web widget, native in your app over WebSocket + PCM16.

Repository (GitHub)
View/report issues

Topics

#voice #assistant #websocket #ecommerce #chatbot

License

unknown (license)

Dependencies

crypto, flutter, flutter_secure_storage, plugin_platform_interface, web_socket_channel

More

Packages that depend on voicebot_flutter

Packages that implement voicebot_flutter