neoagent 2.3.1-beta.29 → 2.3.1-beta.30
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.
- package/flutter_app/lib/main_chat.dart +91 -19
- package/flutter_app/lib/main_controller.dart +26 -0
- package/flutter_app/lib/main_models.dart +29 -0
- package/flutter_app/lib/main_recordings.dart +90 -25
- package/flutter_app/lib/main_settings.dart +103 -62
- package/flutter_app/lib/main_voice_assistant.dart +168 -42
- package/package.json +3 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +53135 -52939
- package/server/routes/settings.js +75 -2
- package/server/routes/wearable.js +20 -6
- package/server/services/messaging/access_policy.js +10 -0
- package/server/services/messaging/manager.js +49 -0
- package/server/services/messaging/meshtastic.js +322 -0
- package/server/services/messaging/meshtastic_env.js +100 -0
- package/server/services/voice/openaiSpeech.js +6 -1
- package/server/services/voice/providers.js +50 -11
- package/server/services/voice/runtimeManager.js +2 -0
- package/server/services/voice/turnRunner.js +29 -9
- package/server/services/wearable/firmware_manifest.js +370 -0
- package/server/services/wearable/service.js +71 -28
|
@@ -664,7 +664,9 @@ class _MessagingPanelState extends State<MessagingPanel> {
|
|
|
664
664
|
final textControllers = <String, TextEditingController>{};
|
|
665
665
|
final boolValues = <String, bool>{};
|
|
666
666
|
for (final field in platform.configFields) {
|
|
667
|
-
final savedValue =
|
|
667
|
+
final savedValue = field.settingsKey == null
|
|
668
|
+
? saved[field.key]
|
|
669
|
+
: widget.controller.settings[field.storageKey];
|
|
668
670
|
if (field.kind == MessagingConfigFieldKind.boolean) {
|
|
669
671
|
boolValues[field.key] =
|
|
670
672
|
savedValue == true || savedValue?.toString() == 'true';
|
|
@@ -736,10 +738,22 @@ class _MessagingPanelState extends State<MessagingPanel> {
|
|
|
736
738
|
);
|
|
737
739
|
}),
|
|
738
740
|
const SizedBox(height: 8),
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
741
|
+
if (platform.id == 'meshtastic')
|
|
742
|
+
Text(
|
|
743
|
+
'Meshtastic connects directly to the device TCP API on port 4403 by default. Normal chat is limited to the configured channel.',
|
|
744
|
+
style: TextStyle(
|
|
745
|
+
color: _textSecondary,
|
|
746
|
+
fontSize: 12,
|
|
747
|
+
),
|
|
748
|
+
)
|
|
749
|
+
else
|
|
750
|
+
SelectableText(
|
|
751
|
+
'Inbound webhook: ${widget.controller.backendUrl}/api/messaging/webhook/${platform.id}',
|
|
752
|
+
style: TextStyle(
|
|
753
|
+
color: _textSecondary,
|
|
754
|
+
fontSize: 12,
|
|
755
|
+
),
|
|
756
|
+
),
|
|
743
757
|
],
|
|
744
758
|
),
|
|
745
759
|
),
|
|
@@ -752,26 +766,84 @@ class _MessagingPanelState extends State<MessagingPanel> {
|
|
|
752
766
|
FilledButton(
|
|
753
767
|
onPressed: () async {
|
|
754
768
|
final config = <String, dynamic>{};
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (
|
|
769
|
+
final snapshot = <String, dynamic>{};
|
|
770
|
+
for (final field in platform.configFields) {
|
|
771
|
+
if (field.kind == MessagingConfigFieldKind.boolean ||
|
|
772
|
+
!field.includeInConfig) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
final controller = textControllers[field.key];
|
|
776
|
+
final value = controller?.text.trim() ?? '';
|
|
777
|
+
if (value.isNotEmpty) config[field.key] = value;
|
|
758
778
|
}
|
|
759
|
-
for (final
|
|
760
|
-
|
|
779
|
+
for (final field in platform.configFields) {
|
|
780
|
+
if (field.kind == MessagingConfigFieldKind.boolean) {
|
|
781
|
+
final value = boolValues[field.key] ?? false;
|
|
782
|
+
if (field.includeInConfig) {
|
|
783
|
+
config[field.key] = value;
|
|
784
|
+
}
|
|
785
|
+
if (field.settingsKey != null) {
|
|
786
|
+
snapshot[field.storageKey] = value;
|
|
787
|
+
}
|
|
788
|
+
} else if (field.settingsKey != null) {
|
|
789
|
+
final controller = textControllers[field.key];
|
|
790
|
+
final value = controller?.text.trim() ?? '';
|
|
791
|
+
if (value.isNotEmpty) {
|
|
792
|
+
snapshot[field.storageKey] = value;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
snapshot[platform.settingsKey] = jsonEncode(config);
|
|
797
|
+
final meshtasticEnabled =
|
|
798
|
+
platform.id != 'meshtastic' ||
|
|
799
|
+
(boolValues['meshtastic_enabled'] ?? true);
|
|
800
|
+
var connected = false;
|
|
801
|
+
if (meshtasticEnabled) {
|
|
802
|
+
connected = await _connectMessagingPlatform(
|
|
803
|
+
platform: platform.id,
|
|
804
|
+
platformLabel: platform.label,
|
|
805
|
+
config: config,
|
|
806
|
+
configSnapshot: snapshot,
|
|
807
|
+
);
|
|
808
|
+
} else {
|
|
809
|
+
final messenger = ScaffoldMessenger.maybeOf(context);
|
|
810
|
+
try {
|
|
811
|
+
await widget.controller.saveSettingsPayload(snapshot);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (!mounted) return;
|
|
814
|
+
messenger?.showSnackBar(
|
|
815
|
+
SnackBar(
|
|
816
|
+
content: Text(
|
|
817
|
+
'Failed to save ${platform.label}: ${widget.controller.friendlyErrorMessage(error)}',
|
|
818
|
+
),
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
await widget.controller.refreshMessaging();
|
|
825
|
+
connected = true;
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (!mounted) return;
|
|
828
|
+
messenger?.showSnackBar(
|
|
829
|
+
SnackBar(
|
|
830
|
+
content: Text(
|
|
831
|
+
'Saved ${platform.label}, but refresh failed: ${widget.controller.friendlyErrorMessage(error)}',
|
|
832
|
+
),
|
|
833
|
+
),
|
|
834
|
+
);
|
|
835
|
+
}
|
|
761
836
|
}
|
|
762
|
-
final connected = await _connectMessagingPlatform(
|
|
763
|
-
platform: platform.id,
|
|
764
|
-
platformLabel: platform.label,
|
|
765
|
-
config: config,
|
|
766
|
-
configSnapshot: <String, dynamic>{
|
|
767
|
-
platform.settingsKey: jsonEncode(config),
|
|
768
|
-
},
|
|
769
|
-
);
|
|
770
837
|
if (connected && context.mounted) {
|
|
771
838
|
Navigator.of(context).pop();
|
|
772
839
|
}
|
|
773
840
|
},
|
|
774
|
-
child: Text(
|
|
841
|
+
child: Text(
|
|
842
|
+
platform.id == 'meshtastic' &&
|
|
843
|
+
!(boolValues['meshtastic_enabled'] ?? true)
|
|
844
|
+
? 'Save'
|
|
845
|
+
: 'Connect',
|
|
846
|
+
),
|
|
775
847
|
),
|
|
776
848
|
],
|
|
777
849
|
);
|
|
@@ -5009,6 +5009,29 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5009
5009
|
await refreshMessaging();
|
|
5010
5010
|
}
|
|
5011
5011
|
|
|
5012
|
+
Future<void> saveSettingsPayload(Map<String, dynamic> payload) async {
|
|
5013
|
+
if (isSavingSettings) {
|
|
5014
|
+
return;
|
|
5015
|
+
}
|
|
5016
|
+
isSavingSettings = true;
|
|
5017
|
+
errorMessage = null;
|
|
5018
|
+
notifyListeners();
|
|
5019
|
+
try {
|
|
5020
|
+
await _backendClient.saveSettings(
|
|
5021
|
+
backendUrl,
|
|
5022
|
+
payload,
|
|
5023
|
+
agentId: _scopedAgentId,
|
|
5024
|
+
);
|
|
5025
|
+
settings = <String, dynamic>{...settings, ...payload};
|
|
5026
|
+
} catch (error) {
|
|
5027
|
+
errorMessage = _friendlyErrorMessage(error);
|
|
5028
|
+
rethrow;
|
|
5029
|
+
} finally {
|
|
5030
|
+
isSavingSettings = false;
|
|
5031
|
+
notifyListeners();
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5012
5035
|
Future<void> disconnectMessagingPlatform(String platform) async {
|
|
5013
5036
|
await _backendClient.disconnectMessagingPlatform(
|
|
5014
5037
|
backendUrl,
|
|
@@ -6212,6 +6235,9 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
6212
6235
|
}
|
|
6213
6236
|
final content = payload['content']?.toString() ?? '';
|
|
6214
6237
|
final kind = payload['kind']?.toString() ?? 'final';
|
|
6238
|
+
if (content.trim().isEmpty) {
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6215
6241
|
voiceAssistantLiveState = voiceAssistantLiveState.copyWith(
|
|
6216
6242
|
interimAssistantText: kind == 'final'
|
|
6217
6243
|
? voiceAssistantLiveState.interimAssistantText
|
|
@@ -270,6 +270,29 @@ messagingPlatforms = <MessagingPlatformDescriptor>[
|
|
|
270
270
|
),
|
|
271
271
|
],
|
|
272
272
|
),
|
|
273
|
+
MessagingPlatformDescriptor(
|
|
274
|
+
id: 'meshtastic',
|
|
275
|
+
label: 'Meshtastic',
|
|
276
|
+
subtitle: 'TCP bridge to a local device channel',
|
|
277
|
+
accent: Color(0xFF2E7D32),
|
|
278
|
+
connectMethod: MessagingConnectMethod.config,
|
|
279
|
+
icon: Icons.router_rounded,
|
|
280
|
+
configFields: <MessagingConfigField>[
|
|
281
|
+
MessagingConfigField(
|
|
282
|
+
key: 'meshtastic_enabled',
|
|
283
|
+
label: 'Enable Meshtastic on this server',
|
|
284
|
+
kind: MessagingConfigFieldKind.boolean,
|
|
285
|
+
settingsKey: 'meshtastic_enabled',
|
|
286
|
+
includeInConfig: false,
|
|
287
|
+
),
|
|
288
|
+
MessagingConfigField(key: 'host', label: 'Device IP Address'),
|
|
289
|
+
MessagingConfigField(
|
|
290
|
+
key: 'channel',
|
|
291
|
+
label: 'Channel Index',
|
|
292
|
+
defaultValue: '0',
|
|
293
|
+
),
|
|
294
|
+
],
|
|
295
|
+
),
|
|
273
296
|
MessagingPlatformDescriptor(
|
|
274
297
|
id: 'telnyx',
|
|
275
298
|
label: 'Telnyx Voice',
|
|
@@ -413,6 +436,8 @@ class MessagingConfigField {
|
|
|
413
436
|
this.kind = MessagingConfigFieldKind.text,
|
|
414
437
|
this.obscure = false,
|
|
415
438
|
this.defaultValue,
|
|
439
|
+
this.settingsKey,
|
|
440
|
+
this.includeInConfig = true,
|
|
416
441
|
});
|
|
417
442
|
|
|
418
443
|
final String key;
|
|
@@ -420,6 +445,10 @@ class MessagingConfigField {
|
|
|
420
445
|
final MessagingConfigFieldKind kind;
|
|
421
446
|
final bool obscure;
|
|
422
447
|
final String? defaultValue;
|
|
448
|
+
final String? settingsKey;
|
|
449
|
+
final bool includeInConfig;
|
|
450
|
+
|
|
451
|
+
String get storageKey => settingsKey ?? key;
|
|
423
452
|
}
|
|
424
453
|
|
|
425
454
|
class MessagingPlatformDescriptor {
|
|
@@ -41,12 +41,25 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
41
41
|
@override
|
|
42
42
|
Widget build(BuildContext context) {
|
|
43
43
|
final runtime = widget.controller.recordingRuntime;
|
|
44
|
-
final
|
|
44
|
+
final isStarting = widget.controller.isStartingRecording;
|
|
45
|
+
final isStopping = widget.controller.isStoppingRecording;
|
|
46
|
+
final statusLabel = isStarting
|
|
47
|
+
? 'Starting'
|
|
48
|
+
: isStopping
|
|
49
|
+
? 'Stopping'
|
|
50
|
+
: runtime.active
|
|
51
|
+
? (runtime.paused ? 'Paused' : 'Recording')
|
|
52
|
+
: 'Ready';
|
|
53
|
+
final statusColor = isStarting || isStopping
|
|
54
|
+
? _accent
|
|
55
|
+
: runtime.active
|
|
56
|
+
? (runtime.paused ? _warning : _danger)
|
|
57
|
+
: _success;
|
|
45
58
|
|
|
46
59
|
return ListView(
|
|
47
60
|
padding: _pagePadding(context),
|
|
48
61
|
children: <Widget>[
|
|
49
|
-
const _SectionTitle('
|
|
62
|
+
const _SectionTitle('Recordings'),
|
|
50
63
|
const SizedBox(height: 12),
|
|
51
64
|
Card(
|
|
52
65
|
child: Padding(
|
|
@@ -60,12 +73,8 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
60
73
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
61
74
|
children: <Widget>[
|
|
62
75
|
_DotStatus(
|
|
63
|
-
label:
|
|
64
|
-
|
|
65
|
-
: 'Idle',
|
|
66
|
-
color: anyRecordingActive
|
|
67
|
-
? (runtime.paused ? _warning : _danger)
|
|
68
|
-
: _success,
|
|
76
|
+
label: statusLabel,
|
|
77
|
+
color: statusColor,
|
|
69
78
|
),
|
|
70
79
|
if (runtime.platformLabel != null &&
|
|
71
80
|
runtime.platformLabel!.isNotEmpty)
|
|
@@ -76,12 +85,57 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
76
85
|
],
|
|
77
86
|
),
|
|
78
87
|
const SizedBox(height: 16),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
if (isStarting) ...<Widget>[
|
|
89
|
+
const SizedBox(height: 14),
|
|
90
|
+
Container(
|
|
91
|
+
width: double.infinity,
|
|
92
|
+
padding: const EdgeInsets.symmetric(
|
|
93
|
+
horizontal: 14,
|
|
94
|
+
vertical: 12,
|
|
95
|
+
),
|
|
96
|
+
decoration: BoxDecoration(
|
|
97
|
+
color: _bgSecondary.withValues(alpha: 0.8),
|
|
98
|
+
borderRadius: BorderRadius.circular(16),
|
|
99
|
+
border: Border.all(color: _borderLight),
|
|
100
|
+
),
|
|
101
|
+
child: Row(
|
|
102
|
+
children: <Widget>[
|
|
103
|
+
const SizedBox.square(
|
|
104
|
+
dimension: 16,
|
|
105
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
106
|
+
),
|
|
107
|
+
const SizedBox(width: 12),
|
|
108
|
+
Expanded(
|
|
109
|
+
child: Text(
|
|
110
|
+
'Starting recording. This can take a few seconds while the session and permissions are prepared.',
|
|
111
|
+
style: TextStyle(
|
|
112
|
+
color: _textSecondary,
|
|
113
|
+
height: 1.4,
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
] else if (isStopping) ...<Widget>[
|
|
121
|
+
const SizedBox(height: 14),
|
|
122
|
+
Container(
|
|
123
|
+
width: double.infinity,
|
|
124
|
+
padding: const EdgeInsets.symmetric(
|
|
125
|
+
horizontal: 14,
|
|
126
|
+
vertical: 12,
|
|
127
|
+
),
|
|
128
|
+
decoration: BoxDecoration(
|
|
129
|
+
color: _bgSecondary.withValues(alpha: 0.8),
|
|
130
|
+
borderRadius: BorderRadius.circular(16),
|
|
131
|
+
border: Border.all(color: _borderLight),
|
|
132
|
+
),
|
|
133
|
+
child: Text(
|
|
134
|
+
'Finalizing recording...',
|
|
135
|
+
style: TextStyle(color: _textSecondary, height: 1.4),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
],
|
|
85
139
|
const SizedBox(height: 18),
|
|
86
140
|
Wrap(
|
|
87
141
|
spacing: 12,
|
|
@@ -95,7 +149,7 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
95
149
|
? null
|
|
96
150
|
: widget.controller.startWebRecording,
|
|
97
151
|
icon: Icon(Icons.desktop_windows_outlined),
|
|
98
|
-
label: Text('
|
|
152
|
+
label: Text('Screen + mic'),
|
|
99
153
|
),
|
|
100
154
|
if (runtime.supportsScreenAndMic)
|
|
101
155
|
OutlinedButton.icon(
|
|
@@ -115,7 +169,7 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
115
169
|
? null
|
|
116
170
|
: widget.controller.startBackgroundRecording,
|
|
117
171
|
icon: Icon(Icons.mic_none_outlined),
|
|
118
|
-
label: Text('
|
|
172
|
+
label: Text('Background mic'),
|
|
119
173
|
),
|
|
120
174
|
if (runtime.supportsSystemAudio)
|
|
121
175
|
FilledButton.icon(
|
|
@@ -127,7 +181,7 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
127
181
|
foregroundColor: Colors.white,
|
|
128
182
|
),
|
|
129
183
|
icon: Icon(Icons.surround_sound_outlined),
|
|
130
|
-
label: Text('
|
|
184
|
+
label: Text('Desktop studio'),
|
|
131
185
|
),
|
|
132
186
|
if (runtime.supportsBackgroundMic && runtime.active)
|
|
133
187
|
OutlinedButton.icon(
|
|
@@ -206,7 +260,7 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
206
260
|
),
|
|
207
261
|
const SizedBox(height: 6),
|
|
208
262
|
Text(
|
|
209
|
-
'Permissions
|
|
263
|
+
'Permissions and live levels stay visible while the floating bar handles quick controls.',
|
|
210
264
|
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
211
265
|
),
|
|
212
266
|
const SizedBox(height: 16),
|
|
@@ -317,13 +371,12 @@ class _RecordingsPanelState extends State<RecordingsPanel> {
|
|
|
317
371
|
),
|
|
318
372
|
),
|
|
319
373
|
const SizedBox(height: 20),
|
|
320
|
-
const _SectionTitle('
|
|
374
|
+
const _SectionTitle('Transcripts'),
|
|
321
375
|
const SizedBox(height: 12),
|
|
322
376
|
if (widget.controller.recordingSessions.isEmpty)
|
|
323
377
|
const _EmptyCard(
|
|
324
378
|
title: 'No recordings yet',
|
|
325
|
-
subtitle:
|
|
326
|
-
'Start a recording and your persisted transcripts will appear here with timestamps.',
|
|
379
|
+
subtitle: 'Start one and transcripts will appear here.',
|
|
327
380
|
)
|
|
328
381
|
else
|
|
329
382
|
...widget.controller.recordingSessions.map(
|
|
@@ -466,7 +519,7 @@ class _RecordingSessionCard extends StatelessWidget {
|
|
|
466
519
|
Icon(Icons.auto_awesome, size: 16, color: _accent),
|
|
467
520
|
const SizedBox(width: 8),
|
|
468
521
|
Text(
|
|
469
|
-
'
|
|
522
|
+
'Insights',
|
|
470
523
|
style: TextStyle(
|
|
471
524
|
color: _accent,
|
|
472
525
|
fontWeight: FontWeight.w600,
|
|
@@ -612,16 +665,28 @@ class _RecordingSessionCard extends StatelessWidget {
|
|
|
612
665
|
),
|
|
613
666
|
] else if (session.transcriptText.isNotEmpty) ...<Widget>[
|
|
614
667
|
const SizedBox(height: 16),
|
|
615
|
-
|
|
668
|
+
Container(
|
|
669
|
+
width: double.infinity,
|
|
670
|
+
padding: const EdgeInsets.all(14),
|
|
671
|
+
decoration: BoxDecoration(
|
|
672
|
+
color: _bgSecondary,
|
|
673
|
+
borderRadius: BorderRadius.circular(14),
|
|
674
|
+
border: Border.all(color: _border),
|
|
675
|
+
),
|
|
676
|
+
child: SelectableText(
|
|
677
|
+
session.transcriptText,
|
|
678
|
+
style: TextStyle(height: 1.45),
|
|
679
|
+
),
|
|
680
|
+
),
|
|
616
681
|
] else ...<Widget>[
|
|
617
682
|
const SizedBox(height: 16),
|
|
618
683
|
Text(
|
|
619
684
|
session.status == 'processing'
|
|
620
|
-
? '
|
|
685
|
+
? 'Transcribing...'
|
|
621
686
|
: session.status == 'failed'
|
|
622
687
|
? 'Transcription failed. Check the error above and retry.'
|
|
623
688
|
: session.status == 'completed'
|
|
624
|
-
? '
|
|
689
|
+
? 'No transcript text was returned. You can retry transcription.'
|
|
625
690
|
: 'Transcript is not available yet.',
|
|
626
691
|
style: TextStyle(color: _textSecondary),
|
|
627
692
|
),
|