neoagent 3.0.1-beta.2 → 3.0.1-beta.20
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/.env.example +19 -0
- package/README.md +16 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +224 -0
- package/docs/configuration.md +22 -0
- package/docs/index.md +1 -0
- package/flutter_app/lib/main.dart +4 -1
- package/flutter_app/lib/main_app_shell.dart +316 -0
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1568 -231
- package/flutter_app/lib/main_controller.dart +263 -26
- package/flutter_app/lib/main_devices.dart +1 -1
- package/flutter_app/lib/main_install.dart +1147 -0
- package/flutter_app/lib/main_navigation.dart +12 -0
- package/flutter_app/lib/main_operations.dart +134 -9
- package/flutter_app/lib/main_settings.dart +148 -52
- package/flutter_app/lib/main_shared.dart +1 -0
- package/flutter_app/lib/src/backend_client.dart +69 -1
- package/landing/index.html +2 -2
- package/lib/manager.js +156 -0
- package/lib/schema_migrations.js +84 -0
- package/package.json +10 -4
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +511 -23
- package/server/admin/billing.js +415 -0
- package/server/admin/index.html +120 -13
- package/server/admin/users.js +15 -15
- package/server/db/database.js +13 -21
- package/server/db/sessions_db.js +8 -0
- package/server/http/middleware.js +4 -4
- package/server/http/routes.js +9 -0
- package/server/http/static.js +4 -2
- package/server/middleware/requireBilling.js +12 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +89120 -85431
- package/server/routes/admin.js +524 -29
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +127 -0
- package/server/routes/billing_webhook.js +43 -0
- package/server/routes/memory.js +1 -4
- package/server/routes/settings.js +1 -2
- package/server/routes/skills.js +1 -1
- package/server/routes/triggers.js +2 -2
- package/server/services/account/sessions.js +1 -9
- package/server/services/ai/loop/agent_engine_core.js +42 -0
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/completion_judge.js +42 -0
- package/server/services/ai/loop/conversation_loop.js +248 -34
- package/server/services/ai/loop/progress_monitor.js +6 -6
- package/server/services/ai/loopPolicy.js +37 -44
- package/server/services/ai/messagingFallback.js +25 -1
- package/server/services/ai/models.js +18 -0
- package/server/services/ai/preModelCompaction.js +25 -5
- package/server/services/ai/rate_limits.js +28 -4
- package/server/services/ai/systemPrompt.js +23 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/tools.js +231 -20
- package/server/services/billing/billing_email.js +106 -0
- package/server/services/billing/config.js +17 -0
- package/server/services/billing/plans.js +121 -0
- package/server/services/billing/stripe_client.js +21 -0
- package/server/services/billing/subscriptions.js +462 -0
- package/server/services/billing/trial_guard.js +111 -0
- package/server/services/browser/contentExtractor.js +629 -0
- package/server/services/browser/controller.js +4 -8
- package/server/services/desktop/gateway.js +7 -4
- package/server/services/integrations/google/calendar.js +30 -2
- package/server/services/integrations/secrets.js +7 -4
- package/server/services/manager.js +11 -0
- package/server/services/messaging/automation.js +1 -1
- package/server/services/messaging/telnyx.js +12 -11
- package/server/services/messaging/whatsapp.js +1 -1
- package/server/services/recordings/manager.js +13 -7
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/tasks/runtime.js +103 -15
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/voice/runtimeManager.js +19 -10
- package/server/services/wearable/gateway.js +8 -5
- package/server/services/websocket.js +26 -8
- package/server/services/workspace/manager.js +37 -3
|
@@ -29,6 +29,8 @@ enum AppSection {
|
|
|
29
29
|
widgets,
|
|
30
30
|
mcp,
|
|
31
31
|
health,
|
|
32
|
+
server,
|
|
33
|
+
billing,
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
enum SidebarGroup { chat, recordings, automation, settings }
|
|
@@ -96,6 +98,10 @@ extension AppSectionX on AppSection {
|
|
|
96
98
|
return 'MCP';
|
|
97
99
|
case AppSection.health:
|
|
98
100
|
return 'Health';
|
|
101
|
+
case AppSection.server:
|
|
102
|
+
return 'Server';
|
|
103
|
+
case AppSection.billing:
|
|
104
|
+
return 'Billing';
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
|
|
@@ -133,6 +139,10 @@ extension AppSectionX on AppSection {
|
|
|
133
139
|
return Icons.hub_outlined;
|
|
134
140
|
case AppSection.health:
|
|
135
141
|
return Icons.favorite_border;
|
|
142
|
+
case AppSection.server:
|
|
143
|
+
return Icons.dns_outlined;
|
|
144
|
+
case AppSection.billing:
|
|
145
|
+
return Icons.credit_card;
|
|
136
146
|
}
|
|
137
147
|
}
|
|
138
148
|
|
|
@@ -157,6 +167,8 @@ extension AppSectionX on AppSection {
|
|
|
157
167
|
case AppSection.accountSettings:
|
|
158
168
|
case AppSection.messaging:
|
|
159
169
|
case AppSection.agents:
|
|
170
|
+
case AppSection.server:
|
|
171
|
+
case AppSection.billing:
|
|
160
172
|
return SidebarGroup.settings;
|
|
161
173
|
}
|
|
162
174
|
}
|
|
@@ -503,17 +503,30 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
503
503
|
late final TabController _tabController;
|
|
504
504
|
String _selectedCategory = 'all';
|
|
505
505
|
|
|
506
|
+
// Installed tab search & filter state
|
|
507
|
+
String _installedQuery = '';
|
|
508
|
+
String _installedStatusFilter = 'all'; // 'all' | 'active' | 'draft' | 'disabled'
|
|
509
|
+
String _installedSourceFilter = 'all'; // 'all' | 'built-in' | 'learned' | 'user' | 'store'
|
|
510
|
+
late final TextEditingController _installedSearchController;
|
|
511
|
+
|
|
506
512
|
@override
|
|
507
513
|
void initState() {
|
|
508
514
|
super.initState();
|
|
509
515
|
_searchController = TextEditingController();
|
|
510
516
|
_tabController = TabController(length: 2, vsync: this);
|
|
517
|
+
_installedSearchController = TextEditingController();
|
|
518
|
+
_installedSearchController.addListener(() {
|
|
519
|
+
setState(() {
|
|
520
|
+
_installedQuery = _installedSearchController.text.trim().toLowerCase();
|
|
521
|
+
});
|
|
522
|
+
});
|
|
511
523
|
}
|
|
512
524
|
|
|
513
525
|
@override
|
|
514
526
|
void dispose() {
|
|
515
527
|
_tabController.dispose();
|
|
516
528
|
_searchController.dispose();
|
|
529
|
+
_installedSearchController.dispose();
|
|
517
530
|
super.dispose();
|
|
518
531
|
}
|
|
519
532
|
|
|
@@ -628,13 +641,122 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
628
641
|
);
|
|
629
642
|
}
|
|
630
643
|
|
|
644
|
+
final filteredSkills = controller.skills.where((skill) {
|
|
645
|
+
final q = _installedQuery;
|
|
646
|
+
if (q.isNotEmpty &&
|
|
647
|
+
!skill.name.toLowerCase().contains(q) &&
|
|
648
|
+
!skill.description.toLowerCase().contains(q)) return false;
|
|
649
|
+
if (_installedStatusFilter != 'all') {
|
|
650
|
+
if (_installedStatusFilter == 'active' && (!skill.enabled || skill.draft)) return false;
|
|
651
|
+
if (_installedStatusFilter == 'draft' && !skill.draft) return false;
|
|
652
|
+
if (_installedStatusFilter == 'disabled' && skill.enabled) return false;
|
|
653
|
+
}
|
|
654
|
+
if (_installedSourceFilter != 'all' && skill.source != _installedSourceFilter) return false;
|
|
655
|
+
return true;
|
|
656
|
+
}).toList();
|
|
657
|
+
|
|
658
|
+
final statusFilters = <String>['all', 'active', 'draft', 'disabled'];
|
|
659
|
+
final sourceFilters = <String>['all', 'built-in', 'learned', 'user', 'store'];
|
|
660
|
+
|
|
631
661
|
return Card(
|
|
632
|
-
child:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
662
|
+
child: Column(
|
|
663
|
+
children: <Widget>[
|
|
664
|
+
Padding(
|
|
665
|
+
padding: const EdgeInsets.fromLTRB(14, 14, 14, 0),
|
|
666
|
+
child: Column(
|
|
667
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
668
|
+
children: <Widget>[
|
|
669
|
+
TextField(
|
|
670
|
+
controller: _installedSearchController,
|
|
671
|
+
decoration: InputDecoration(
|
|
672
|
+
labelText: 'Search by name or description',
|
|
673
|
+
prefixIcon: Icon(Icons.search),
|
|
674
|
+
suffixIcon: _installedSearchController.text.isEmpty
|
|
675
|
+
? null
|
|
676
|
+
: IconButton(
|
|
677
|
+
onPressed: () {
|
|
678
|
+
_installedSearchController.clear();
|
|
679
|
+
},
|
|
680
|
+
icon: Icon(Icons.close),
|
|
681
|
+
),
|
|
682
|
+
),
|
|
683
|
+
),
|
|
684
|
+
const SizedBox(height: 10),
|
|
685
|
+
SizedBox(
|
|
686
|
+
height: 38,
|
|
687
|
+
child: ListView.separated(
|
|
688
|
+
scrollDirection: Axis.horizontal,
|
|
689
|
+
itemCount: statusFilters.length,
|
|
690
|
+
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
|
691
|
+
itemBuilder: (context, index) {
|
|
692
|
+
final filter = statusFilters[index];
|
|
693
|
+
final selected = filter == _installedStatusFilter;
|
|
694
|
+
return FilterChip(
|
|
695
|
+
selected: selected,
|
|
696
|
+
label: Text(
|
|
697
|
+
filter == 'all'
|
|
698
|
+
? 'All'
|
|
699
|
+
: filter[0].toUpperCase() + filter.substring(1),
|
|
700
|
+
),
|
|
701
|
+
selectedColor: _accentMuted,
|
|
702
|
+
checkmarkColor: _accent,
|
|
703
|
+
backgroundColor: _bgSecondary,
|
|
704
|
+
side: BorderSide(color: _border),
|
|
705
|
+
onSelected: (_) =>
|
|
706
|
+
setState(() => _installedStatusFilter = filter),
|
|
707
|
+
);
|
|
708
|
+
},
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
const SizedBox(height: 8),
|
|
712
|
+
SizedBox(
|
|
713
|
+
height: 38,
|
|
714
|
+
child: ListView.separated(
|
|
715
|
+
scrollDirection: Axis.horizontal,
|
|
716
|
+
itemCount: sourceFilters.length,
|
|
717
|
+
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
|
718
|
+
itemBuilder: (context, index) {
|
|
719
|
+
final filter = sourceFilters[index];
|
|
720
|
+
final selected = filter == _installedSourceFilter;
|
|
721
|
+
return FilterChip(
|
|
722
|
+
selected: selected,
|
|
723
|
+
label: Text(filter == 'all' ? 'All' : filter),
|
|
724
|
+
selectedColor: _accentMuted,
|
|
725
|
+
checkmarkColor: _accent,
|
|
726
|
+
backgroundColor: _bgSecondary,
|
|
727
|
+
side: BorderSide(color: _border),
|
|
728
|
+
onSelected: (_) =>
|
|
729
|
+
setState(() => _installedSourceFilter = filter),
|
|
730
|
+
);
|
|
731
|
+
},
|
|
732
|
+
),
|
|
733
|
+
),
|
|
734
|
+
const SizedBox(height: 8),
|
|
735
|
+
Text(
|
|
736
|
+
'${filteredSkills.length} skill${filteredSkills.length == 1 ? '' : 's'}',
|
|
737
|
+
style: TextStyle(color: _textSecondary),
|
|
738
|
+
),
|
|
739
|
+
const SizedBox(height: 8),
|
|
740
|
+
],
|
|
741
|
+
),
|
|
742
|
+
),
|
|
743
|
+
if (filteredSkills.isEmpty)
|
|
744
|
+
Expanded(
|
|
745
|
+
child: Center(
|
|
746
|
+
child: Text(
|
|
747
|
+
'No skills match your filters',
|
|
748
|
+
style: TextStyle(color: _textSecondary),
|
|
749
|
+
),
|
|
750
|
+
),
|
|
751
|
+
)
|
|
752
|
+
else
|
|
753
|
+
Expanded(
|
|
754
|
+
child: ListView.separated(
|
|
755
|
+
padding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
|
|
756
|
+
itemCount: filteredSkills.length,
|
|
757
|
+
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
758
|
+
itemBuilder: (context, index) {
|
|
759
|
+
final skill = filteredSkills[index];
|
|
638
760
|
return LayoutBuilder(
|
|
639
761
|
builder: (context, constraints) {
|
|
640
762
|
final compact = constraints.maxWidth < 760;
|
|
@@ -780,9 +902,12 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
780
902
|
);
|
|
781
903
|
},
|
|
782
904
|
);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
905
|
+
},
|
|
906
|
+
),
|
|
907
|
+
),
|
|
908
|
+
],
|
|
909
|
+
),
|
|
910
|
+
);
|
|
786
911
|
}
|
|
787
912
|
|
|
788
913
|
Widget _buildStoreTab(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
part of 'main.dart';
|
|
2
2
|
|
|
3
|
+
enum _LeaveAction { save, discard, cancel }
|
|
4
|
+
|
|
3
5
|
class SettingsPanel extends StatefulWidget {
|
|
4
6
|
const SettingsPanel({
|
|
5
7
|
super.key,
|
|
@@ -190,6 +192,8 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
190
192
|
final Map<String, TextEditingController> _providerBaseUrlControllers =
|
|
191
193
|
<String, TextEditingController>{};
|
|
192
194
|
|
|
195
|
+
bool _hasUnsavedChanges = false;
|
|
196
|
+
|
|
193
197
|
// Inline runtime test state — ephemeral, not stored in controller.
|
|
194
198
|
bool _cliTestRunning = false;
|
|
195
199
|
Map<String, dynamic>? _cliTestResult;
|
|
@@ -323,7 +327,22 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
323
327
|
.where((section) => !section.requiresDesktop || _supportsDesktopShell)
|
|
324
328
|
.toSet();
|
|
325
329
|
|
|
326
|
-
return
|
|
330
|
+
return PopScope(
|
|
331
|
+
canPop: !_hasUnsavedChanges,
|
|
332
|
+
onPopInvokedWithResult: (didPop, result) async {
|
|
333
|
+
if (didPop) return;
|
|
334
|
+
final action = await _showLeaveDialog(context);
|
|
335
|
+
if (action == _LeaveAction.save && mounted) {
|
|
336
|
+
await _doSave();
|
|
337
|
+
if (mounted) Navigator.of(context).pop();
|
|
338
|
+
} else if (action == _LeaveAction.discard && mounted) {
|
|
339
|
+
_hydrate();
|
|
340
|
+
setState(() => _hasUnsavedChanges = false);
|
|
341
|
+
Navigator.of(context).pop();
|
|
342
|
+
}
|
|
343
|
+
// cancel: do nothing
|
|
344
|
+
},
|
|
345
|
+
child: ListView(
|
|
327
346
|
padding: widget.embedded ? EdgeInsets.zero : _pagePadding(context),
|
|
328
347
|
children: <Widget>[
|
|
329
348
|
if (!widget.embedded)
|
|
@@ -430,6 +449,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
430
449
|
),
|
|
431
450
|
],
|
|
432
451
|
],
|
|
452
|
+
),
|
|
433
453
|
);
|
|
434
454
|
}
|
|
435
455
|
|
|
@@ -456,44 +476,44 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
456
476
|
);
|
|
457
477
|
}
|
|
458
478
|
|
|
479
|
+
Future<void> _doSave() async {
|
|
480
|
+
final controller = widget.controller;
|
|
481
|
+
await controller.saveSettings(
|
|
482
|
+
browserBackend: _browserBackend == 'extension' ? 'extension' : 'vm',
|
|
483
|
+
browserExtensionTokenId:
|
|
484
|
+
_browserBackend == 'extension' ? _browserExtensionTokenId : null,
|
|
485
|
+
cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
|
|
486
|
+
cliDesktopDeviceId: _cliDesktopDeviceId,
|
|
487
|
+
smarterSelector: _smarterSelector,
|
|
488
|
+
enabledModels: _enabledModels.toList(),
|
|
489
|
+
defaultChatModel: _defaultChatModel,
|
|
490
|
+
defaultSubagentModel: _defaultSubagentModel,
|
|
491
|
+
defaultRecordingTranscriptionProvider: 'deepgram',
|
|
492
|
+
defaultRecordingTranscriptionModel: _defaultRecordingTranscriptionModel,
|
|
493
|
+
defaultRecordingSummaryProvider: _providerForSelectedModel(
|
|
494
|
+
_defaultRecordingSummaryModel,
|
|
495
|
+
controller.supportedModels,
|
|
496
|
+
),
|
|
497
|
+
defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
|
|
498
|
+
fallbackModel: _fallbackModel,
|
|
499
|
+
defaultSpeechModel: _defaultSpeechModel,
|
|
500
|
+
voiceSttProvider: controller.voiceSttProvider,
|
|
501
|
+
voiceSttModel: controller.voiceSttModel,
|
|
502
|
+
voiceTtsProvider: controller.voiceTtsProvider,
|
|
503
|
+
voiceTtsModel: controller.voiceTtsModel,
|
|
504
|
+
voiceTtsVoice: controller.voiceTtsVoice,
|
|
505
|
+
voiceRuntimeMode: 'live',
|
|
506
|
+
voiceLiveProvider: _voiceLiveProvider,
|
|
507
|
+
voiceLiveModel: _voiceLiveModel,
|
|
508
|
+
voiceLiveVoice: _voiceLiveVoice,
|
|
509
|
+
aiProviderConfigs: _buildProviderPayload(),
|
|
510
|
+
);
|
|
511
|
+
if (mounted) setState(() => _hasUnsavedChanges = false);
|
|
512
|
+
}
|
|
513
|
+
|
|
459
514
|
Widget _settingsSaveButton(NeoAgentController controller) {
|
|
460
|
-
|
|
461
|
-
onPressed: controller.isSavingSettings
|
|
462
|
-
? null
|
|
463
|
-
: () => controller.saveSettings(
|
|
464
|
-
browserBackend: _browserBackend == 'extension'
|
|
465
|
-
? 'extension'
|
|
466
|
-
: 'vm',
|
|
467
|
-
browserExtensionTokenId: _browserBackend == 'extension'
|
|
468
|
-
? _browserExtensionTokenId
|
|
469
|
-
: null,
|
|
470
|
-
cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
|
|
471
|
-
cliDesktopDeviceId: _cliDesktopDeviceId,
|
|
472
|
-
smarterSelector: _smarterSelector,
|
|
473
|
-
enabledModels: _enabledModels.toList(),
|
|
474
|
-
defaultChatModel: _defaultChatModel,
|
|
475
|
-
defaultSubagentModel: _defaultSubagentModel,
|
|
476
|
-
defaultRecordingTranscriptionProvider: 'deepgram',
|
|
477
|
-
defaultRecordingTranscriptionModel:
|
|
478
|
-
_defaultRecordingTranscriptionModel,
|
|
479
|
-
defaultRecordingSummaryProvider: _providerForSelectedModel(
|
|
480
|
-
_defaultRecordingSummaryModel,
|
|
481
|
-
controller.supportedModels,
|
|
482
|
-
),
|
|
483
|
-
defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
|
|
484
|
-
fallbackModel: _fallbackModel,
|
|
485
|
-
defaultSpeechModel: _defaultSpeechModel,
|
|
486
|
-
voiceSttProvider: controller.voiceSttProvider,
|
|
487
|
-
voiceSttModel: controller.voiceSttModel,
|
|
488
|
-
voiceTtsProvider: controller.voiceTtsProvider,
|
|
489
|
-
voiceTtsModel: controller.voiceTtsModel,
|
|
490
|
-
voiceTtsVoice: controller.voiceTtsVoice,
|
|
491
|
-
voiceRuntimeMode: 'live',
|
|
492
|
-
voiceLiveProvider: _voiceLiveProvider,
|
|
493
|
-
voiceLiveModel: _voiceLiveModel,
|
|
494
|
-
voiceLiveVoice: _voiceLiveVoice,
|
|
495
|
-
aiProviderConfigs: _buildProviderPayload(),
|
|
496
|
-
),
|
|
515
|
+
final button = FilledButton.icon(
|
|
516
|
+
onPressed: controller.isSavingSettings ? null : _doSave,
|
|
497
517
|
style: FilledButton.styleFrom(backgroundColor: _accent),
|
|
498
518
|
icon: controller.isSavingSettings
|
|
499
519
|
? const SizedBox.square(
|
|
@@ -506,6 +526,19 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
506
526
|
: Icon(Icons.save_outlined),
|
|
507
527
|
label: Text('Save'),
|
|
508
528
|
);
|
|
529
|
+
if (!_hasUnsavedChanges) return button;
|
|
530
|
+
return Column(
|
|
531
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
532
|
+
mainAxisSize: MainAxisSize.min,
|
|
533
|
+
children: <Widget>[
|
|
534
|
+
Text(
|
|
535
|
+
'Unsaved changes',
|
|
536
|
+
style: TextStyle(color: Colors.orange, fontSize: 12),
|
|
537
|
+
),
|
|
538
|
+
const SizedBox(height: 4),
|
|
539
|
+
button,
|
|
540
|
+
],
|
|
541
|
+
);
|
|
509
542
|
}
|
|
510
543
|
|
|
511
544
|
Widget _buildSettingsOverview(
|
|
@@ -623,6 +656,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
623
656
|
_browserBackend = value;
|
|
624
657
|
_browserExtensionTokenId ??=
|
|
625
658
|
controller.selectedBrowserExtensionTokenId;
|
|
659
|
+
_hasUnsavedChanges = true;
|
|
626
660
|
});
|
|
627
661
|
}
|
|
628
662
|
},
|
|
@@ -669,7 +703,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
669
703
|
}).toList(),
|
|
670
704
|
onChanged: (value) {
|
|
671
705
|
if (value != null) {
|
|
672
|
-
setState(()
|
|
706
|
+
setState(() {
|
|
707
|
+
_browserExtensionTokenId = value;
|
|
708
|
+
_hasUnsavedChanges = true;
|
|
709
|
+
});
|
|
673
710
|
}
|
|
674
711
|
},
|
|
675
712
|
),
|
|
@@ -746,7 +783,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
746
783
|
],
|
|
747
784
|
onChanged: (value) {
|
|
748
785
|
if (value != null) {
|
|
749
|
-
setState(()
|
|
786
|
+
setState(() {
|
|
787
|
+
_cliBackend = value;
|
|
788
|
+
_hasUnsavedChanges = true;
|
|
789
|
+
});
|
|
750
790
|
}
|
|
751
791
|
},
|
|
752
792
|
),
|
|
@@ -785,7 +825,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
785
825
|
}).toList(),
|
|
786
826
|
onChanged: (value) {
|
|
787
827
|
if (value != null) {
|
|
788
|
-
setState(()
|
|
828
|
+
setState(() {
|
|
829
|
+
_cliDesktopDeviceId = value;
|
|
830
|
+
_hasUnsavedChanges = true;
|
|
831
|
+
});
|
|
789
832
|
}
|
|
790
833
|
},
|
|
791
834
|
),
|
|
@@ -832,7 +875,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
832
875
|
subtitle:
|
|
833
876
|
'Automatically choose the best enabled model for each task type.',
|
|
834
877
|
value: _smarterSelector,
|
|
835
|
-
onChanged: (value) => setState(()
|
|
878
|
+
onChanged: (value) => setState(() {
|
|
879
|
+
_smarterSelector = value;
|
|
880
|
+
_hasUnsavedChanges = true;
|
|
881
|
+
}),
|
|
836
882
|
),
|
|
837
883
|
],
|
|
838
884
|
),
|
|
@@ -893,7 +939,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
893
939
|
options: modelChoices,
|
|
894
940
|
onChanged: (value) {
|
|
895
941
|
if (value != null) {
|
|
896
|
-
setState(()
|
|
942
|
+
setState(() {
|
|
943
|
+
_defaultChatModel = value;
|
|
944
|
+
_hasUnsavedChanges = true;
|
|
945
|
+
});
|
|
897
946
|
}
|
|
898
947
|
},
|
|
899
948
|
),
|
|
@@ -911,7 +960,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
911
960
|
options: modelChoices,
|
|
912
961
|
onChanged: (value) {
|
|
913
962
|
if (value != null) {
|
|
914
|
-
setState(()
|
|
963
|
+
setState(() {
|
|
964
|
+
_defaultSubagentModel = value;
|
|
965
|
+
_hasUnsavedChanges = true;
|
|
966
|
+
});
|
|
915
967
|
}
|
|
916
968
|
},
|
|
917
969
|
),
|
|
@@ -929,7 +981,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
929
981
|
options: _modelPickerOptions(routingModels),
|
|
930
982
|
onChanged: (value) {
|
|
931
983
|
if (value != null) {
|
|
932
|
-
setState(()
|
|
984
|
+
setState(() {
|
|
985
|
+
_fallbackModel = value;
|
|
986
|
+
_hasUnsavedChanges = true;
|
|
987
|
+
});
|
|
933
988
|
}
|
|
934
989
|
},
|
|
935
990
|
),
|
|
@@ -977,7 +1032,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
977
1032
|
selectedIds: _enabledModels,
|
|
978
1033
|
),
|
|
979
1034
|
);
|
|
980
|
-
if (result != null) setState(()
|
|
1035
|
+
if (result != null) setState(() {
|
|
1036
|
+
_enabledModels = result;
|
|
1037
|
+
_hasUnsavedChanges = true;
|
|
1038
|
+
});
|
|
981
1039
|
},
|
|
982
1040
|
),
|
|
983
1041
|
],
|
|
@@ -1037,9 +1095,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1037
1095
|
options: modelChoices,
|
|
1038
1096
|
onChanged: (value) {
|
|
1039
1097
|
if (value != null) {
|
|
1040
|
-
setState(
|
|
1041
|
-
|
|
1042
|
-
|
|
1098
|
+
setState(() {
|
|
1099
|
+
_defaultRecordingSummaryModel = value;
|
|
1100
|
+
_hasUnsavedChanges = true;
|
|
1101
|
+
});
|
|
1043
1102
|
}
|
|
1044
1103
|
},
|
|
1045
1104
|
),
|
|
@@ -1057,6 +1116,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1057
1116
|
if (value != null) {
|
|
1058
1117
|
setState(() {
|
|
1059
1118
|
_defaultRecordingTranscriptionModel = value;
|
|
1119
|
+
_hasUnsavedChanges = true;
|
|
1060
1120
|
});
|
|
1061
1121
|
}
|
|
1062
1122
|
},
|
|
@@ -1098,7 +1158,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1098
1158
|
options: modelChoices,
|
|
1099
1159
|
onChanged: (value) {
|
|
1100
1160
|
if (value != null) {
|
|
1101
|
-
setState(()
|
|
1161
|
+
setState(() {
|
|
1162
|
+
_defaultSpeechModel = value;
|
|
1163
|
+
_hasUnsavedChanges = true;
|
|
1164
|
+
});
|
|
1102
1165
|
}
|
|
1103
1166
|
},
|
|
1104
1167
|
),
|
|
@@ -1158,6 +1221,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1158
1221
|
!voiceOptions.contains(_voiceLiveVoice)) {
|
|
1159
1222
|
_voiceLiveVoice = voiceOptions.first;
|
|
1160
1223
|
}
|
|
1224
|
+
_hasUnsavedChanges = true;
|
|
1161
1225
|
});
|
|
1162
1226
|
},
|
|
1163
1227
|
),
|
|
@@ -1174,7 +1238,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1174
1238
|
),
|
|
1175
1239
|
onChanged: (value) {
|
|
1176
1240
|
if (value != null) {
|
|
1177
|
-
setState(()
|
|
1241
|
+
setState(() {
|
|
1242
|
+
_voiceLiveModel = value;
|
|
1243
|
+
_hasUnsavedChanges = true;
|
|
1244
|
+
});
|
|
1178
1245
|
}
|
|
1179
1246
|
},
|
|
1180
1247
|
),
|
|
@@ -1189,7 +1256,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1189
1256
|
options: _simplePickerOptions(liveVoiceOptions),
|
|
1190
1257
|
onChanged: (value) {
|
|
1191
1258
|
if (value != null) {
|
|
1192
|
-
setState(()
|
|
1259
|
+
setState(() {
|
|
1260
|
+
_voiceLiveVoice = value;
|
|
1261
|
+
_hasUnsavedChanges = true;
|
|
1262
|
+
});
|
|
1193
1263
|
}
|
|
1194
1264
|
},
|
|
1195
1265
|
),
|
|
@@ -1661,6 +1731,32 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1661
1731
|
}
|
|
1662
1732
|
}
|
|
1663
1733
|
|
|
1734
|
+
Future<_LeaveAction?> _showLeaveDialog(BuildContext context) {
|
|
1735
|
+
return showDialog<_LeaveAction>(
|
|
1736
|
+
context: context,
|
|
1737
|
+
builder: (ctx) => AlertDialog(
|
|
1738
|
+
title: const Text('Unsaved changes'),
|
|
1739
|
+
content: const Text(
|
|
1740
|
+
'You have unsaved settings. What would you like to do?',
|
|
1741
|
+
),
|
|
1742
|
+
actions: <Widget>[
|
|
1743
|
+
TextButton(
|
|
1744
|
+
onPressed: () => Navigator.pop(ctx, _LeaveAction.cancel),
|
|
1745
|
+
child: const Text('Cancel'),
|
|
1746
|
+
),
|
|
1747
|
+
TextButton(
|
|
1748
|
+
onPressed: () => Navigator.pop(ctx, _LeaveAction.discard),
|
|
1749
|
+
child: const Text('Discard'),
|
|
1750
|
+
),
|
|
1751
|
+
FilledButton(
|
|
1752
|
+
onPressed: () => Navigator.pop(ctx, _LeaveAction.save),
|
|
1753
|
+
child: const Text('Save'),
|
|
1754
|
+
),
|
|
1755
|
+
],
|
|
1756
|
+
),
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1664
1760
|
List<_ModelPickerOption> _recordingTranscriptionOptions(String current) {
|
|
1665
1761
|
const List<String> defaults = <String>['nova-3', 'nova-2-general'];
|
|
1666
1762
|
final String normalizedCurrent = current.trim();
|
|
@@ -425,6 +425,7 @@ List<AppSection> _mainSections(NeoAgentController controller) {
|
|
|
425
425
|
AppSection.memory,
|
|
426
426
|
if (controller.showHealthSection) AppSection.health,
|
|
427
427
|
AppSection.settings,
|
|
428
|
+
if (controller.showBillingSection) AppSection.billing,
|
|
428
429
|
AppSection.runs,
|
|
429
430
|
AppSection.agents,
|
|
430
431
|
AppSection.messaging,
|
|
@@ -342,6 +342,61 @@ class BackendClient {
|
|
|
342
342
|
return deleteMap(baseUrl, '/api/account/sessions/$sessionId');
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
// ── Billing ──────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
Future<Map<String, dynamic>> getBillingPlans(String baseUrl) async {
|
|
348
|
+
return getMap(baseUrl, '/api/billing/plans', allowUnauthorized: true);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
Future<Map<String, dynamic>> getBillingInfo(String baseUrl) async {
|
|
352
|
+
return getMap(baseUrl, '/api/billing/');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
Future<Map<String, dynamic>> getBillingInvoices(String baseUrl) async {
|
|
356
|
+
return getMap(baseUrl, '/api/billing/invoices');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Future<Map<String, dynamic>> createCheckoutSession({
|
|
360
|
+
required String baseUrl,
|
|
361
|
+
required String planId,
|
|
362
|
+
required String successUrl,
|
|
363
|
+
required String cancelUrl,
|
|
364
|
+
}) async {
|
|
365
|
+
return postMap(baseUrl, '/api/billing/checkout', <String, dynamic>{
|
|
366
|
+
'planId': planId,
|
|
367
|
+
'successUrl': successUrl,
|
|
368
|
+
'cancelUrl': cancelUrl,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
Future<Map<String, dynamic>> createPortalSession({
|
|
373
|
+
required String baseUrl,
|
|
374
|
+
required String returnUrl,
|
|
375
|
+
}) async {
|
|
376
|
+
return postMap(baseUrl, '/api/billing/portal', <String, dynamic>{
|
|
377
|
+
'returnUrl': returnUrl,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
Future<Map<String, dynamic>> cancelBillingSubscription(
|
|
382
|
+
String baseUrl,
|
|
383
|
+
) async {
|
|
384
|
+
return postMap(baseUrl, '/api/billing/cancel', const <String, dynamic>{});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
Future<Map<String, dynamic>> startBillingTrial({
|
|
388
|
+
required String baseUrl,
|
|
389
|
+
required String planId,
|
|
390
|
+
String? deviceFingerprint,
|
|
391
|
+
}) async {
|
|
392
|
+
return postMap(baseUrl, '/api/billing/trial', <String, dynamic>{
|
|
393
|
+
'planId': planId,
|
|
394
|
+
if (deviceFingerprint != null) 'deviceFingerprint': deviceFingerprint,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ── Agent profiles ────────────────────────────────────────────────────────
|
|
399
|
+
|
|
345
400
|
Future<Map<String, dynamic>> createAgentProfile(
|
|
346
401
|
String baseUrl,
|
|
347
402
|
Map<String, dynamic> payload,
|
|
@@ -371,10 +426,23 @@ class BackendClient {
|
|
|
371
426
|
Future<Map<String, dynamic>> fetchChatHistory(
|
|
372
427
|
String baseUrl, {
|
|
373
428
|
String? agentId,
|
|
429
|
+
int limit = 40,
|
|
430
|
+
String? beforeCreatedAt,
|
|
431
|
+
String? beforeSource,
|
|
432
|
+
String? beforeId,
|
|
374
433
|
}) async {
|
|
434
|
+
final params = <String>[
|
|
435
|
+
'limit=$limit',
|
|
436
|
+
if (beforeCreatedAt != null && beforeCreatedAt.trim().isNotEmpty)
|
|
437
|
+
'beforeCreatedAt=${Uri.encodeQueryComponent(beforeCreatedAt.trim())}',
|
|
438
|
+
if (beforeSource != null && beforeSource.trim().isNotEmpty)
|
|
439
|
+
'beforeSource=${Uri.encodeQueryComponent(beforeSource.trim())}',
|
|
440
|
+
if (beforeId != null && beforeId.trim().isNotEmpty)
|
|
441
|
+
'beforeId=${Uri.encodeQueryComponent(beforeId.trim())}',
|
|
442
|
+
];
|
|
375
443
|
return getMap(
|
|
376
444
|
baseUrl,
|
|
377
|
-
_withAgentQuery('/api/agents/chat-history
|
|
445
|
+
_withAgentQuery('/api/agents/chat-history?${params.join('&')}', agentId),
|
|
378
446
|
);
|
|
379
447
|
}
|
|
380
448
|
|
package/landing/index.html
CHANGED
|
@@ -910,7 +910,7 @@ p { margin: 0; text-wrap: pretty; }
|
|
|
910
910
|
<div class="wrap">
|
|
911
911
|
<div class="hero-grid">
|
|
912
912
|
<div class="hero-copy">
|
|
913
|
-
<div class="pill"><span class="tag">New</span> <b>NeoAgent is now in
|
|
913
|
+
<div class="pill"><span class="tag">New</span> <b>NeoAgent is now in beta</b></div>
|
|
914
914
|
<h1>
|
|
915
915
|
<span class="h1-line"><span class="w" style="--i:0">One</span> <span class="w" style="--i:1">agent</span> <span class="w" style="--i:2">for</span> <span class="w" style="--i:3">your</span></span>
|
|
916
916
|
<span class="h1-line"><span class="w" style="--i:4"><em>whole</em></span> <span class="w" style="--i:5"><em>digital</em></span> <span class="w" style="--i:6"><em>life</em></span></span>
|
|
@@ -1194,7 +1194,7 @@ p { margin: 0; text-wrap: pretty; }
|
|
|
1194
1194
|
<section class="section-tight" id="cta" data-screen-label="cta">
|
|
1195
1195
|
<div class="wrap">
|
|
1196
1196
|
<div class="final reveal">
|
|
1197
|
-
<span class="eyebrow" style="display:block;margin-bottom:20px;">
|
|
1197
|
+
<span class="eyebrow" style="display:block;margin-bottom:20px;">Beta · Limited spots</span>
|
|
1198
1198
|
<h2>The agent that just <em>handles it.</em></h2>
|
|
1199
1199
|
<p>Be first to put NeoAgent to work across your devices, channels, and life. We'll send your invite as spots open up.</p>
|
|
1200
1200
|
<div class="hero-actions" style="justify-content:center;">
|