vocall_sdk 0.1.0
vocall_sdk: ^0.1.0 copied to clipboard
Flutter SDK for the Vocall Agent-Application Protocol (AAP). Let an AI agent see and control your app's UI — navigate screens, fill forms, click buttons, and interact via voice or text.
example/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vocall_sdk/vocall_sdk.dart';
void main() {
runApp(const VoiceTestApp());
}
class VoiceTestApp extends StatelessWidget {
const VoiceTestApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Jarvis Voice Test',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: const VoiceTestPage(),
);
}
}
class VoiceTestPage extends StatefulWidget {
const VoiceTestPage({super.key});
@override
State<VoiceTestPage> createState() => _VoiceTestPageState();
}
class _VoiceTestPageState extends State<VoiceTestPage> {
late final JarvisClient _jarvis;
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _serverUrlController = TextEditingController(text: 'ws://localhost:12900/connect');
final List<String> _log = [];
double _audioLevel = 0.0;
StreamSubscription<double>? _levelSub;
bool _connected = false;
@override
void initState() {
super.initState();
_jarvis = JarvisClient(serverUrl: _serverUrlController.text);
_jarvis.addListener(_onClientChanged);
_levelSub = _jarvis.audioLevelStream.listen((level) {
setState(() => _audioLevel = level);
});
}
@override
void dispose() {
_levelSub?.cancel();
_jarvis.removeListener(_onClientChanged);
_jarvis.dispose();
_nameController.dispose();
_emailController.dispose();
_serverUrlController.dispose();
super.dispose();
}
void _onClientChanged() {
setState(() {});
}
void _addLog(String msg) {
setState(() {
_log.add('[${DateTime.now().toIso8601String().substring(11, 23)}] $msg');
if (_log.length > 200) _log.removeAt(0);
});
}
void _doConnect() {
_jarvis.disconnect();
// Create a new client with the URL from the text field
setState(() {
_connected = false;
_log.clear();
});
_jarvis.fieldRegistry.registerField('home', 'name', _nameController);
_jarvis.fieldRegistry.registerField('home', 'email', _emailController);
_jarvis.connect(ManifestMessage(
app: 'voice-test',
screens: {
'home': const ScreenDescriptor(
id: 'home',
label: 'Home',
fields: [
FieldDescriptor(id: 'name', type: FieldType.text, label: 'Name'),
FieldDescriptor(id: 'email', type: FieldType.email, label: 'Email'),
],
actions: [
ActionDescriptor(id: 'submit', label: 'Submit'),
],
),
},
));
_connected = true;
_addLog('Connecting to ${_serverUrlController.text}');
}
void _toggleAlwaysListening() {
if (_jarvis.alwaysListening) {
_jarvis.stopAlwaysListening();
_addLog('Stopped always-listening');
} else {
_jarvis.startAlwaysListening();
_addLog('Started always-listening');
}
}
void _doInterrupt() {
_jarvis.interrupt();
_addLog('Interrupt sent');
}
Color _statusColor(JarvisStatus status) {
return switch (status) {
JarvisStatus.disconnected => Colors.grey,
JarvisStatus.idle => Colors.blue,
JarvisStatus.listening => Colors.green,
JarvisStatus.recording => Colors.orange,
JarvisStatus.thinking => Colors.purple,
JarvisStatus.speaking => Colors.red,
JarvisStatus.executing => Colors.teal,
};
}
@override
Widget build(BuildContext context) {
final status = _jarvis.status;
final partial = _jarvis.partialTranscription;
return Scaffold(
appBar: AppBar(
title: const Text('Jarvis Voice Test'),
actions: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _statusColor(status),
borderRadius: BorderRadius.circular(16),
),
child: Text(
status.name.toUpperCase(),
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
),
),
],
),
body: Column(
children: [
// Connection bar
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _serverUrlController,
decoration: const InputDecoration(
labelText: 'Server URL',
border: OutlineInputBorder(),
isDense: true,
),
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _doConnect,
child: Text(_connected ? 'Reconnect' : 'Connect'),
),
],
),
),
// Controls bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
// Mic button with level indicator
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 56,
height: 56,
child: CircularProgressIndicator(
value: _audioLevel,
strokeWidth: 4,
color: _jarvis.alwaysListening ? Colors.green : Colors.grey,
backgroundColor: Colors.grey.shade200,
),
),
IconButton(
iconSize: 28,
icon: Icon(
_jarvis.alwaysListening ? Icons.mic : Icons.mic_off,
color: _jarvis.alwaysListening ? Colors.green : Colors.grey,
),
onPressed: _connected ? _toggleAlwaysListening : null,
),
],
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: status == JarvisStatus.speaking || status == JarvisStatus.thinking
? _doInterrupt
: null,
icon: const Icon(Icons.stop),
label: const Text('Interrupt'),
),
const SizedBox(width: 16),
if (partial != null)
Expanded(
child: Text(
'STT: $partial',
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.orange.shade800,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const Divider(),
// Test fields
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
isDense: true,
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
isDense: true,
),
),
),
],
),
),
const Divider(),
// Chat messages
Expanded(
flex: 2,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _jarvis.messages.length,
itemBuilder: (context, index) {
final msg = _jarvis.messages[index];
final isUser = msg.role == ChatRole.user;
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isUser ? Colors.indigo.shade100 : Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Text(msg.text, style: const TextStyle(fontSize: 14)),
),
);
},
),
),
const Divider(height: 1),
// Protocol log
Expanded(
flex: 1,
child: Container(
color: Colors.grey.shade900,
child: ListView.builder(
padding: const EdgeInsets.all(4),
reverse: true,
itemCount: _log.length,
itemBuilder: (context, index) {
return Text(
_log[_log.length - 1 - index],
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Colors.green,
),
);
},
),
),
),
],
),
);
}
}