neoagent 2.4.1-beta.12 → 2.4.1-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -124,7 +124,8 @@
124
124
 
125
125
  <service
126
126
  android:name=".auto.NeoAgentCarAppService"
127
- android:exported="true">
127
+ android:exported="true"
128
+ android:permission="android.car.permission.BIND_CAR_APP_SERVICE">
128
129
  <intent-filter>
129
130
  <action android:name="androidx.car.app.CarAppService" />
130
131
  <category android:name="androidx.car.app.category.IOT" />
@@ -65,6 +65,7 @@ part 'main_recordings.dart';
65
65
  part 'main_chat.dart';
66
66
  part 'main_account_settings.dart';
67
67
  part 'main_settings.dart';
68
+ part 'main_model_picker.dart';
68
69
  part 'main_operations.dart';
69
70
  part 'main_admin.dart';
70
71
  part 'main_unified.dart';
@@ -19,6 +19,10 @@ class _ChatPanelState extends State<ChatPanel> {
19
19
  int _lastToolCount = 0;
20
20
  String _lastStream = '';
21
21
  bool _isSendingChatMessage = false;
22
+ bool _isDictating = false;
23
+ bool _isTranscribing = false;
24
+ LiveVoiceCapture? _dictationCapture;
25
+ final List<Uint8List> _dictationChunks = [];
22
26
 
23
27
  @override
24
28
  void initState() {
@@ -44,9 +48,73 @@ class _ChatPanelState extends State<ChatPanel> {
44
48
  widget.controller.removeListener(_consumeQueuedDraft);
45
49
  _composerController.dispose();
46
50
  _scrollController.dispose();
51
+ _dictationCapture?.dispose();
47
52
  super.dispose();
48
53
  }
49
54
 
55
+ Future<void> _startDictation() async {
56
+ if (_isDictating || _isTranscribing) return;
57
+ final capture = LiveVoiceCapture();
58
+ _dictationCapture = capture;
59
+ _dictationChunks.clear();
60
+ try {
61
+ await capture.start(
62
+ onChunk: (chunk) => _dictationChunks.add(chunk),
63
+ sampleRate: 16000,
64
+ channels: 1,
65
+ );
66
+ if (mounted) setState(() => _isDictating = true);
67
+ } catch (e) {
68
+ await capture.dispose();
69
+ _dictationCapture = null;
70
+ if (mounted) {
71
+ ScaffoldMessenger.of(context).showSnackBar(
72
+ SnackBar(content: Text('Microphone error: $e')),
73
+ );
74
+ }
75
+ }
76
+ }
77
+
78
+ Future<void> _stopAndTranscribe() async {
79
+ if (!_isDictating) return;
80
+ final capture = _dictationCapture;
81
+ _dictationCapture = null;
82
+ setState(() {
83
+ _isDictating = false;
84
+ _isTranscribing = true;
85
+ });
86
+ try {
87
+ await capture?.stop();
88
+ if (_dictationChunks.isEmpty) return;
89
+ final allBytes = _dictationChunks.fold<List<int>>(
90
+ <int>[],
91
+ (acc, chunk) => acc..addAll(chunk),
92
+ );
93
+ final audioBase64 = base64Encode(Uint8List.fromList(allBytes));
94
+ final transcript = await widget.controller.transcribeDictationAudio(
95
+ audioBase64: audioBase64,
96
+ );
97
+ if (mounted && transcript.isNotEmpty) {
98
+ final current = _composerController.text;
99
+ final separator = current.isNotEmpty && !current.endsWith(' ') ? ' ' : '';
100
+ _composerController.text = '$current$separator$transcript';
101
+ _composerController.selection = TextSelection.collapsed(
102
+ offset: _composerController.text.length,
103
+ );
104
+ }
105
+ } catch (e) {
106
+ if (mounted) {
107
+ ScaffoldMessenger.of(context).showSnackBar(
108
+ SnackBar(content: Text('Transcription failed: $e')),
109
+ );
110
+ }
111
+ } finally {
112
+ await capture?.dispose();
113
+ _dictationChunks.clear();
114
+ if (mounted) setState(() => _isTranscribing = false);
115
+ }
116
+ }
117
+
50
118
  void _consumeQueuedDraft() {
51
119
  final draft = widget.controller.peekPendingChatDraft();
52
120
  final attachments = widget.controller.peekPendingSharedChatAttachments();
@@ -331,6 +399,26 @@ class _ChatPanelState extends State<ChatPanel> {
331
399
  color: _textSecondary,
332
400
  ),
333
401
  const SizedBox(width: 2),
402
+ _isTranscribing
403
+ ? const SizedBox(
404
+ width: 40,
405
+ height: 40,
406
+ child: Padding(
407
+ padding: EdgeInsets.all(10),
408
+ child: CircularProgressIndicator(strokeWidth: 2),
409
+ ),
410
+ )
411
+ : IconButton(
412
+ tooltip: _isDictating ? 'Stop & transcribe' : 'Dictate',
413
+ onPressed: _isDictating ? _stopAndTranscribe : _startDictation,
414
+ icon: Icon(
415
+ _isDictating
416
+ ? Icons.stop_circle_outlined
417
+ : Icons.mic_none_rounded,
418
+ ),
419
+ color: _isDictating ? Theme.of(context).colorScheme.error : _textSecondary,
420
+ ),
421
+ const SizedBox(width: 2),
334
422
  FilledButton(
335
423
  onPressed: () => controller.setSelectedSection(
336
424
  AppSection.voiceAssistant,
@@ -4602,6 +4602,18 @@ class NeoAgentController extends ChangeNotifier {
4602
4602
  }
4603
4603
  }
4604
4604
 
4605
+ Future<String> transcribeDictationAudio({
4606
+ required String audioBase64,
4607
+ String mimeType = 'audio/pcm;rate=16000;channels=1',
4608
+ }) async {
4609
+ final result = await _backendClient.transcribeAudio(
4610
+ backendUrl,
4611
+ audioBase64: audioBase64,
4612
+ mimeType: mimeType,
4613
+ );
4614
+ return result['transcript']?.toString() ?? '';
4615
+ }
4616
+
4605
4617
  Future<void> sendMessage(
4606
4618
  String task, {
4607
4619
  List<SharedChatAttachment> sharedAttachments =
@@ -188,7 +188,9 @@ class OfficialIntegrationsTab extends StatelessWidget {
188
188
  !item.env.configured
189
189
  ? item.env.summary
190
190
  : item.hasExpiredAccounts
191
- ? 'One or more accounts expired. Reconnect the affected account to restore tool access.'
191
+ ? item.id == 'google_workspace'
192
+ ? 'One or more accounts expired. Reconnect to restore access. If this keeps happening, your Google Cloud OAuth app may be in Testing mode — publish it to Production in Google Cloud Console to get long-lived tokens.'
193
+ : 'One or more accounts expired. Reconnect the affected account to restore tool access.'
192
194
  : !item.supportsMultipleAccounts && item.isConnected
193
195
  ? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
194
196
  : item.isConnected