neoagent 3.1.1-beta.9 → 3.2.1-beta.0
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 +2 -2
- package/README.md +5 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +68 -87
- package/docs/capabilities.md +1 -1
- package/docs/clients-and-devices.md +3 -4
- package/docs/configuration.md +1 -1
- package/docs/devices.md +1 -2
- package/docs/health.md +24 -0
- package/docs/index.md +2 -2
- package/docs/integrations-architecture.md +6 -0
- package/docs/integrations.md +15 -1
- package/docs/models.md +1 -1
- package/docs/persistence.md +1 -1
- package/docs/why-neoagent.md +1 -1
- package/flutter_app/README.md +0 -5
- package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
- package/flutter_app/lib/main.dart +0 -6
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_controller.dart +2 -867
- package/flutter_app/lib/main_devices.dart +0 -73
- package/flutter_app/lib/main_integrations.dart +339 -187
- package/flutter_app/lib/main_launcher.dart +2 -212
- package/flutter_app/lib/main_models.dart +0 -259
- package/flutter_app/lib/main_navigation.dart +2 -14
- package/flutter_app/lib/main_operations.dart +875 -383
- package/flutter_app/lib/main_runtime.dart +9 -208
- package/flutter_app/lib/main_settings.dart +17 -199
- package/flutter_app/lib/main_shared.dart +20 -591
- package/flutter_app/lib/main_timeline.dart +26 -122
- package/flutter_app/lib/main_voice_assistant.dart +0 -5
- package/flutter_app/lib/src/backend_client.dart +0 -102
- package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
- package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
- package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/macos/Podfile.lock +56 -5
- package/flutter_app/pubspec.lock +0 -7
- package/flutter_app/pubspec.yaml +0 -2
- package/flutter_app/web/index.html +174 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
- package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
- package/landing/index.html +0 -2
- package/lib/manager.js +1 -1
- package/lib/schema_migrations.js +75 -0
- package/package.json +1 -4
- package/server/db/database.js +1 -133
- package/server/http/middleware.js +2 -16
- package/server/http/routes.js +0 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +0 -25
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/index.html +174 -0
- package/server/public/main.dart.js +92036 -91145
- package/server/routes/auth.js +11 -2
- package/server/routes/settings.js +0 -5
- package/server/routes/voice_assistant.js +1 -128
- package/server/services/account/erasure.js +0 -3
- package/server/services/ai/deliverables/artifact_helpers.js +1 -1
- package/server/services/ai/loop/conversation_loop.js +6 -2
- package/server/services/ai/loop/tool_dispatch.js +0 -3
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/settings.js +0 -6
- package/server/services/ai/toolEvidence.js +1 -4
- package/server/services/ai/tools.js +0 -234
- package/server/services/desktop/auth.js +0 -3
- package/server/services/desktop/registry.js +2 -50
- package/server/services/integrations/neoarchive/provider.js +616 -0
- package/server/services/integrations/neorecall/client.js +140 -0
- package/server/services/integrations/neorecall/constants.js +87 -0
- package/server/services/integrations/neorecall/provider.js +163 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/manager.js +0 -22
- package/server/services/memory/manager.js +44 -19
- package/server/services/memory/retrieval_reasoning.js +36 -14
- package/server/services/messaging/formatting_guides.js +1 -0
- package/server/services/messaging/whatsapp.js +18 -0
- package/server/services/security/tool_categories.js +0 -5
- package/server/services/social_video/service.js +2 -20
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/integration_runtime.js +3 -67
- package/server/services/tasks/runtime.js +1 -6
- package/server/services/tasks/task_repository.js +6 -13
- package/server/services/timeline/service.js +0 -224
- package/server/services/voice/message.js +0 -24
- package/server/services/voice/providers.js +42 -1
- package/server/services/voice/runtimeManager.js +9 -5
- package/server/services/voice/turnRunner.js +0 -22
- package/server/services/wearable/service.js +0 -5
- package/docs/recordings-and-health.md +0 -41
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
- package/flutter_app/lib/main_recordings.dart +0 -920
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
- package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
- package/flutter_app/lib/src/recording_bridge.dart +0 -232
- package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
- package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
- package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
- package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
- package/flutter_app/lib/src/recording_payloads.dart +0 -95
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
- package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
- package/server/routes/recordings.js +0 -335
- package/server/routes/screenHistory.js +0 -190
- package/server/services/ai/recordingInsights.js +0 -105
- package/server/services/integrations/neomail/provider.js +0 -993
- package/server/services/recordings/deepgram.js +0 -58
- package/server/services/recordings/manager.js +0 -1273
- package/server/services/tasks/adapters/neomail_email_received.js +0 -46
- package/server/services/voice/screenshotContext.js +0 -73
- package/server/services/voice/turnCoordinator.js +0 -51
|
@@ -505,10 +505,8 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
505
505
|
|
|
506
506
|
// Installed tab search & filter state
|
|
507
507
|
String _installedQuery = '';
|
|
508
|
-
String _installedStatusFilter =
|
|
509
|
-
|
|
510
|
-
String _installedSourceFilter =
|
|
511
|
-
'all'; // 'all' | 'built-in' | 'learned' | 'user' | 'store'
|
|
508
|
+
String _installedStatusFilter = 'all'; // 'all' | 'active' | 'draft' | 'disabled'
|
|
509
|
+
String _installedSourceFilter = 'all'; // 'all' | 'built-in' | 'learned' | 'user' | 'store'
|
|
512
510
|
late final TextEditingController _installedSearchController;
|
|
513
511
|
|
|
514
512
|
@override
|
|
@@ -647,36 +645,18 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
647
645
|
final q = _installedQuery;
|
|
648
646
|
if (q.isNotEmpty &&
|
|
649
647
|
!skill.name.toLowerCase().contains(q) &&
|
|
650
|
-
!skill.description.toLowerCase().contains(q))
|
|
651
|
-
return false;
|
|
652
|
-
}
|
|
648
|
+
!skill.description.toLowerCase().contains(q)) return false;
|
|
653
649
|
if (_installedStatusFilter != 'all') {
|
|
654
|
-
if (_installedStatusFilter == 'active' &&
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
if (_installedStatusFilter == 'draft' && !skill.draft) {
|
|
659
|
-
return false;
|
|
660
|
-
}
|
|
661
|
-
if (_installedStatusFilter == 'disabled' && skill.enabled) {
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
if (_installedSourceFilter != 'all' &&
|
|
666
|
-
skill.source != _installedSourceFilter) {
|
|
667
|
-
return false;
|
|
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;
|
|
668
653
|
}
|
|
654
|
+
if (_installedSourceFilter != 'all' && skill.source != _installedSourceFilter) return false;
|
|
669
655
|
return true;
|
|
670
656
|
}).toList();
|
|
671
657
|
|
|
672
658
|
final statusFilters = <String>['all', 'active', 'draft', 'disabled'];
|
|
673
|
-
final sourceFilters = <String>[
|
|
674
|
-
'all',
|
|
675
|
-
'built-in',
|
|
676
|
-
'learned',
|
|
677
|
-
'user',
|
|
678
|
-
'store',
|
|
679
|
-
];
|
|
659
|
+
final sourceFilters = <String>['all', 'built-in', 'learned', 'user', 'store'];
|
|
680
660
|
|
|
681
661
|
return Card(
|
|
682
662
|
child: Column(
|
|
@@ -777,174 +757,157 @@ class _SkillsPanelState extends State<SkillsPanel>
|
|
|
777
757
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
778
758
|
itemBuilder: (context, index) {
|
|
779
759
|
final skill = filteredSkills[index];
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
),
|
|
881
|
-
style: TextStyle(
|
|
882
|
-
color: _textSecondary,
|
|
883
|
-
),
|
|
884
|
-
),
|
|
885
|
-
const SizedBox(height: 10),
|
|
886
|
-
Wrap(
|
|
887
|
-
spacing: 8,
|
|
888
|
-
runSpacing: 8,
|
|
889
|
-
children: <Widget>[
|
|
890
|
-
_MetaPill(
|
|
891
|
-
label: skill.category,
|
|
892
|
-
icon: Icons.folder_outlined,
|
|
893
|
-
),
|
|
894
|
-
_MetaPill(
|
|
895
|
-
label: skill.source,
|
|
896
|
-
icon: Icons.source_outlined,
|
|
897
|
-
),
|
|
898
|
-
if (skill.draft)
|
|
899
|
-
const _MetaPill(
|
|
900
|
-
label: 'Draft',
|
|
901
|
-
icon: Icons.edit_note_outlined,
|
|
902
|
-
),
|
|
903
|
-
],
|
|
904
|
-
),
|
|
905
|
-
],
|
|
760
|
+
return LayoutBuilder(
|
|
761
|
+
builder: (context, constraints) {
|
|
762
|
+
final compact = constraints.maxWidth < 760;
|
|
763
|
+
return Container(
|
|
764
|
+
padding: const EdgeInsets.all(14),
|
|
765
|
+
decoration: BoxDecoration(
|
|
766
|
+
color: _bgSecondary,
|
|
767
|
+
borderRadius: BorderRadius.circular(14),
|
|
768
|
+
border: Border.all(color: _border),
|
|
769
|
+
),
|
|
770
|
+
child: compact
|
|
771
|
+
? Column(
|
|
772
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
773
|
+
children: <Widget>[
|
|
774
|
+
Row(
|
|
775
|
+
children: <Widget>[
|
|
776
|
+
Expanded(
|
|
777
|
+
child: Text(
|
|
778
|
+
skill.name,
|
|
779
|
+
style: TextStyle(fontWeight: FontWeight.w700),
|
|
780
|
+
),
|
|
781
|
+
),
|
|
782
|
+
Switch(
|
|
783
|
+
value: skill.enabled,
|
|
784
|
+
onChanged: (value) => controller
|
|
785
|
+
.setSkillEnabled(skill.name, value),
|
|
786
|
+
),
|
|
787
|
+
],
|
|
788
|
+
),
|
|
789
|
+
Text(
|
|
790
|
+
skill.description.ifEmpty('No description'),
|
|
791
|
+
style: TextStyle(color: _textSecondary),
|
|
792
|
+
),
|
|
793
|
+
const SizedBox(height: 10),
|
|
794
|
+
Wrap(
|
|
795
|
+
spacing: 8,
|
|
796
|
+
runSpacing: 8,
|
|
797
|
+
children: <Widget>[
|
|
798
|
+
_MetaPill(
|
|
799
|
+
label: skill.category,
|
|
800
|
+
icon: Icons.folder_outlined,
|
|
801
|
+
),
|
|
802
|
+
_MetaPill(
|
|
803
|
+
label: skill.source,
|
|
804
|
+
icon: Icons.source_outlined,
|
|
805
|
+
),
|
|
806
|
+
if (skill.draft)
|
|
807
|
+
const _MetaPill(
|
|
808
|
+
label: 'Draft',
|
|
809
|
+
icon: Icons.edit_note_outlined,
|
|
810
|
+
),
|
|
811
|
+
],
|
|
812
|
+
),
|
|
813
|
+
const SizedBox(height: 10),
|
|
814
|
+
Row(
|
|
815
|
+
children: <Widget>[
|
|
816
|
+
const Spacer(),
|
|
817
|
+
OutlinedButton(
|
|
818
|
+
onPressed: () =>
|
|
819
|
+
_openSkillEditor(context, skill.name),
|
|
820
|
+
child: Text('Open'),
|
|
821
|
+
),
|
|
822
|
+
const SizedBox(width: 8),
|
|
823
|
+
TextButton.icon(
|
|
824
|
+
onPressed: () =>
|
|
825
|
+
_confirmDeleteSkill(context, skill.name),
|
|
826
|
+
icon: Icon(Icons.delete_outline),
|
|
827
|
+
style: TextButton.styleFrom(
|
|
828
|
+
foregroundColor: _danger,
|
|
829
|
+
),
|
|
830
|
+
label: Text('Delete'),
|
|
831
|
+
),
|
|
832
|
+
],
|
|
833
|
+
),
|
|
834
|
+
],
|
|
835
|
+
)
|
|
836
|
+
: Row(
|
|
837
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
838
|
+
children: <Widget>[
|
|
839
|
+
Expanded(
|
|
840
|
+
child: Column(
|
|
841
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
842
|
+
children: <Widget>[
|
|
843
|
+
Text(
|
|
844
|
+
skill.name,
|
|
845
|
+
style: TextStyle(fontWeight: FontWeight.w700),
|
|
846
|
+
),
|
|
847
|
+
const SizedBox(height: 6),
|
|
848
|
+
Text(
|
|
849
|
+
skill.description.ifEmpty('No description'),
|
|
850
|
+
style: TextStyle(color: _textSecondary),
|
|
851
|
+
),
|
|
852
|
+
const SizedBox(height: 10),
|
|
853
|
+
Wrap(
|
|
854
|
+
spacing: 8,
|
|
855
|
+
runSpacing: 8,
|
|
856
|
+
children: <Widget>[
|
|
857
|
+
_MetaPill(
|
|
858
|
+
label: skill.category,
|
|
859
|
+
icon: Icons.folder_outlined,
|
|
906
860
|
),
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
),
|
|
916
|
-
OutlinedButton(
|
|
917
|
-
onPressed: () => _openSkillEditor(
|
|
918
|
-
context,
|
|
919
|
-
skill.name,
|
|
920
|
-
),
|
|
921
|
-
child: Text('Open'),
|
|
922
|
-
),
|
|
923
|
-
const SizedBox(height: 6),
|
|
924
|
-
TextButton.icon(
|
|
925
|
-
onPressed: () => _confirmDeleteSkill(
|
|
926
|
-
context,
|
|
927
|
-
skill.name,
|
|
928
|
-
),
|
|
929
|
-
icon: Icon(Icons.delete_outline),
|
|
930
|
-
style: TextButton.styleFrom(
|
|
931
|
-
foregroundColor: _danger,
|
|
932
|
-
),
|
|
933
|
-
label: Text('Delete'),
|
|
861
|
+
_MetaPill(
|
|
862
|
+
label: skill.source,
|
|
863
|
+
icon: Icons.source_outlined,
|
|
864
|
+
),
|
|
865
|
+
if (skill.draft)
|
|
866
|
+
const _MetaPill(
|
|
867
|
+
label: 'Draft',
|
|
868
|
+
icon: Icons.edit_note_outlined,
|
|
934
869
|
),
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
870
|
+
],
|
|
871
|
+
),
|
|
872
|
+
],
|
|
873
|
+
),
|
|
874
|
+
),
|
|
875
|
+
const SizedBox(width: 10),
|
|
876
|
+
Column(
|
|
877
|
+
children: <Widget>[
|
|
878
|
+
Switch(
|
|
879
|
+
value: skill.enabled,
|
|
880
|
+
onChanged: (value) => controller
|
|
881
|
+
.setSkillEnabled(skill.name, value),
|
|
938
882
|
),
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
883
|
+
OutlinedButton(
|
|
884
|
+
onPressed: () =>
|
|
885
|
+
_openSkillEditor(context, skill.name),
|
|
886
|
+
child: Text('Open'),
|
|
887
|
+
),
|
|
888
|
+
const SizedBox(height: 6),
|
|
889
|
+
TextButton.icon(
|
|
890
|
+
onPressed: () =>
|
|
891
|
+
_confirmDeleteSkill(context, skill.name),
|
|
892
|
+
icon: Icon(Icons.delete_outline),
|
|
893
|
+
style: TextButton.styleFrom(
|
|
894
|
+
foregroundColor: _danger,
|
|
895
|
+
),
|
|
896
|
+
label: Text('Delete'),
|
|
897
|
+
),
|
|
898
|
+
],
|
|
899
|
+
),
|
|
900
|
+
],
|
|
901
|
+
),
|
|
902
|
+
);
|
|
903
|
+
},
|
|
904
|
+
);
|
|
942
905
|
},
|
|
943
906
|
),
|
|
944
907
|
),
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
908
|
+
],
|
|
909
|
+
),
|
|
910
|
+
);
|
|
948
911
|
}
|
|
949
912
|
|
|
950
913
|
Widget _buildStoreTab(
|
|
@@ -1502,9 +1465,8 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1502
1465
|
);
|
|
1503
1466
|
if (!mounted) return;
|
|
1504
1467
|
_llmImportController.clear();
|
|
1505
|
-
final warningText =
|
|
1506
|
-
? ''
|
|
1507
|
-
: ' ${result.warnings.join(' ')}';
|
|
1468
|
+
final warningText =
|
|
1469
|
+
result.warnings.isEmpty ? '' : ' ${result.warnings.join(' ')}';
|
|
1508
1470
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
1509
1471
|
SnackBar(
|
|
1510
1472
|
content: Text(
|
|
@@ -1532,7 +1494,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1532
1494
|
: controller.memories;
|
|
1533
1495
|
if (_entityFilter == null) return base;
|
|
1534
1496
|
return base
|
|
1535
|
-
.where(
|
|
1497
|
+
.where(
|
|
1498
|
+
(m) => m.entities.any((e) => e.name == _entityFilter),
|
|
1499
|
+
)
|
|
1536
1500
|
.toList();
|
|
1537
1501
|
}
|
|
1538
1502
|
|
|
@@ -1624,10 +1588,7 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1624
1588
|
});
|
|
1625
1589
|
}
|
|
1626
1590
|
|
|
1627
|
-
void _openRetrievalInspector(
|
|
1628
|
-
BuildContext context,
|
|
1629
|
-
NeoAgentController controller,
|
|
1630
|
-
) {
|
|
1591
|
+
void _openRetrievalInspector(BuildContext context, NeoAgentController controller) {
|
|
1631
1592
|
Navigator.of(context).push(
|
|
1632
1593
|
MaterialPageRoute(
|
|
1633
1594
|
builder: (context) => RetrievalInspectorView(controller: controller),
|
|
@@ -1642,8 +1603,7 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1642
1603
|
final memoriesToShow = _visibleMemories;
|
|
1643
1604
|
final selectedIds = _selectedVisibleMemoryIds.toSet();
|
|
1644
1605
|
final selectedCount = selectedIds.length;
|
|
1645
|
-
final allVisibleSelected =
|
|
1646
|
-
memoriesToShow.isNotEmpty &&
|
|
1606
|
+
final allVisibleSelected = memoriesToShow.isNotEmpty &&
|
|
1647
1607
|
memoriesToShow.every((m) => selectedIds.contains(m.id));
|
|
1648
1608
|
final showingSearchResults = controller.memoryRecallResults.isNotEmpty;
|
|
1649
1609
|
final compact = MediaQuery.sizeOf(context).width < 760;
|
|
@@ -1684,7 +1644,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1684
1644
|
padding: const EdgeInsets.all(18),
|
|
1685
1645
|
child: Row(
|
|
1686
1646
|
children: <Widget>[
|
|
1687
|
-
_MemoryConfidenceGauge(
|
|
1647
|
+
_MemoryConfidenceGauge(
|
|
1648
|
+
confidence: stats.averageConfidence,
|
|
1649
|
+
),
|
|
1688
1650
|
const SizedBox(width: 18),
|
|
1689
1651
|
Expanded(
|
|
1690
1652
|
child: Wrap(
|
|
@@ -1743,7 +1705,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1743
1705
|
children: <Widget>[
|
|
1744
1706
|
Row(
|
|
1745
1707
|
children: <Widget>[
|
|
1746
|
-
Expanded(
|
|
1708
|
+
Expanded(
|
|
1709
|
+
child: const _SectionTitle('Knowledge Graph'),
|
|
1710
|
+
),
|
|
1747
1711
|
if (_entityFilter != null)
|
|
1748
1712
|
TextButton.icon(
|
|
1749
1713
|
onPressed: () =>
|
|
@@ -1756,7 +1720,10 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1756
1720
|
const SizedBox(height: 4),
|
|
1757
1721
|
Text(
|
|
1758
1722
|
'Tap an entity to filter memories by it.',
|
|
1759
|
-
style: TextStyle(
|
|
1723
|
+
style: TextStyle(
|
|
1724
|
+
color: _textSecondary,
|
|
1725
|
+
fontSize: 12,
|
|
1726
|
+
),
|
|
1760
1727
|
),
|
|
1761
1728
|
const SizedBox(height: 14),
|
|
1762
1729
|
SizedBox(
|
|
@@ -1890,16 +1857,15 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1890
1857
|
Wrap(
|
|
1891
1858
|
spacing: 8,
|
|
1892
1859
|
runSpacing: 8,
|
|
1893
|
-
crossAxisAlignment:
|
|
1860
|
+
crossAxisAlignment:
|
|
1861
|
+
WrapCrossAlignment.center,
|
|
1894
1862
|
children: <Widget>[
|
|
1895
1863
|
OutlinedButton.icon(
|
|
1896
|
-
onPressed:
|
|
1897
|
-
allVisibleSelected ||
|
|
1864
|
+
onPressed: allVisibleSelected ||
|
|
1898
1865
|
_bulkActionInFlight
|
|
1899
1866
|
? null
|
|
1900
1867
|
: () => _selectAllVisibleMemories(
|
|
1901
|
-
memoriesToShow,
|
|
1902
|
-
),
|
|
1868
|
+
memoriesToShow),
|
|
1903
1869
|
icon: Icon(
|
|
1904
1870
|
Icons.done_all_outlined,
|
|
1905
1871
|
size: 16,
|
|
@@ -1937,7 +1903,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1937
1903
|
Icons.archive_outlined,
|
|
1938
1904
|
size: 16,
|
|
1939
1905
|
),
|
|
1940
|
-
label: Text(
|
|
1906
|
+
label: Text(
|
|
1907
|
+
'Archive ($selectedCount)',
|
|
1908
|
+
),
|
|
1941
1909
|
),
|
|
1942
1910
|
OutlinedButton.icon(
|
|
1943
1911
|
onPressed: _bulkActionInFlight
|
|
@@ -1955,7 +1923,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1955
1923
|
Icons.delete_sweep_outlined,
|
|
1956
1924
|
size: 16,
|
|
1957
1925
|
),
|
|
1958
|
-
label: Text(
|
|
1926
|
+
label: Text(
|
|
1927
|
+
'Delete ($selectedCount)',
|
|
1928
|
+
),
|
|
1959
1929
|
),
|
|
1960
1930
|
],
|
|
1961
1931
|
],
|
|
@@ -1971,9 +1941,8 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1971
1941
|
)
|
|
1972
1942
|
else
|
|
1973
1943
|
...memoriesToShow.map((memory) {
|
|
1974
|
-
final isSelected =
|
|
1975
|
-
|
|
1976
|
-
);
|
|
1944
|
+
final isSelected =
|
|
1945
|
+
selectedIds.contains(memory.id);
|
|
1977
1946
|
return _MemoryRow(
|
|
1978
1947
|
memory: memory,
|
|
1979
1948
|
isSelected: isSelected,
|
|
@@ -1981,7 +1950,8 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1981
1950
|
memory.id,
|
|
1982
1951
|
!isSelected,
|
|
1983
1952
|
),
|
|
1984
|
-
onCheck: (value) =>
|
|
1953
|
+
onCheck: (value) =>
|
|
1954
|
+
_toggleMemorySelection(
|
|
1985
1955
|
memory.id,
|
|
1986
1956
|
value ?? false,
|
|
1987
1957
|
),
|
|
@@ -1994,9 +1964,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
1994
1964
|
'This memory will be removed permanently.',
|
|
1995
1965
|
onConfirm: () =>
|
|
1996
1966
|
_deleteSingleMemory(
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
1967
|
+
controller,
|
|
1968
|
+
memory.id,
|
|
1969
|
+
),
|
|
2000
1970
|
),
|
|
2001
1971
|
);
|
|
2002
1972
|
}),
|
|
@@ -2015,7 +1985,9 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2015
1985
|
Expanded(
|
|
2016
1986
|
child: Text(
|
|
2017
1987
|
'Key-value pairs that persist across conversations.',
|
|
2018
|
-
style: TextStyle(
|
|
1988
|
+
style: TextStyle(
|
|
1989
|
+
color: _textSecondary,
|
|
1990
|
+
),
|
|
2019
1991
|
),
|
|
2020
1992
|
),
|
|
2021
1993
|
TextButton.icon(
|
|
@@ -2029,75 +2001,75 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2029
2001
|
],
|
|
2030
2002
|
),
|
|
2031
2003
|
const SizedBox(height: 10),
|
|
2032
|
-
if (controller
|
|
2004
|
+
if (controller
|
|
2005
|
+
.memoryOverview.coreEntries.isEmpty)
|
|
2033
2006
|
Text(
|
|
2034
2007
|
'No core memory entries yet.',
|
|
2035
2008
|
style: TextStyle(color: _textSecondary),
|
|
2036
2009
|
)
|
|
2037
2010
|
else
|
|
2038
|
-
...controller.memoryOverview.coreEntries
|
|
2011
|
+
...controller.memoryOverview.coreEntries
|
|
2012
|
+
.entries
|
|
2039
2013
|
.map((entry) {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2014
|
+
return Container(
|
|
2015
|
+
width: double.infinity,
|
|
2016
|
+
margin: const EdgeInsets.only(bottom: 10),
|
|
2017
|
+
padding: const EdgeInsets.all(12),
|
|
2018
|
+
decoration: BoxDecoration(
|
|
2019
|
+
color: _bgSecondary,
|
|
2020
|
+
borderRadius:
|
|
2021
|
+
BorderRadius.circular(12),
|
|
2022
|
+
border: Border.all(color: _border),
|
|
2023
|
+
),
|
|
2024
|
+
child: Row(
|
|
2025
|
+
crossAxisAlignment:
|
|
2026
|
+
CrossAxisAlignment.start,
|
|
2027
|
+
children: <Widget>[
|
|
2028
|
+
Expanded(
|
|
2029
|
+
child: Column(
|
|
2030
|
+
crossAxisAlignment:
|
|
2031
|
+
CrossAxisAlignment.start,
|
|
2032
|
+
children: <Widget>[
|
|
2033
|
+
Text(
|
|
2034
|
+
entry.key,
|
|
2035
|
+
style: TextStyle(
|
|
2036
|
+
fontWeight: FontWeight.w700,
|
|
2037
|
+
),
|
|
2038
|
+
),
|
|
2039
|
+
const SizedBox(height: 6),
|
|
2040
|
+
Text(
|
|
2041
|
+
entry.value.toString(),
|
|
2042
|
+
),
|
|
2043
|
+
],
|
|
2044
|
+
),
|
|
2044
2045
|
),
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2046
|
+
IconButton(
|
|
2047
|
+
onPressed: () =>
|
|
2048
|
+
_openCoreMemoryEditor(
|
|
2049
|
+
context,
|
|
2050
|
+
controller,
|
|
2051
|
+
keyValue: entry,
|
|
2050
2052
|
),
|
|
2051
|
-
|
|
2053
|
+
icon: Icon(Icons.edit_outlined),
|
|
2052
2054
|
),
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
entry.key,
|
|
2064
|
-
style: TextStyle(
|
|
2065
|
-
fontWeight:
|
|
2066
|
-
FontWeight.w700,
|
|
2067
|
-
),
|
|
2068
|
-
),
|
|
2069
|
-
const SizedBox(height: 6),
|
|
2070
|
-
Text(entry.value.toString()),
|
|
2071
|
-
],
|
|
2072
|
-
),
|
|
2073
|
-
),
|
|
2074
|
-
IconButton(
|
|
2075
|
-
onPressed: () =>
|
|
2076
|
-
_openCoreMemoryEditor(
|
|
2077
|
-
context,
|
|
2078
|
-
controller,
|
|
2079
|
-
keyValue: entry,
|
|
2080
|
-
),
|
|
2081
|
-
icon: Icon(Icons.edit_outlined),
|
|
2082
|
-
),
|
|
2083
|
-
IconButton(
|
|
2084
|
-
onPressed: () => _confirmDelete(
|
|
2085
|
-
context,
|
|
2086
|
-
title:
|
|
2087
|
-
'Delete core memory entry?',
|
|
2088
|
-
message:
|
|
2089
|
-
'Remove "${entry.key}" from core memory.',
|
|
2090
|
-
onConfirm: () =>
|
|
2091
|
-
controller.deleteCoreMemory(
|
|
2092
|
-
entry.key,
|
|
2093
|
-
),
|
|
2094
|
-
),
|
|
2095
|
-
icon: Icon(Icons.delete_outline),
|
|
2055
|
+
IconButton(
|
|
2056
|
+
onPressed: () => _confirmDelete(
|
|
2057
|
+
context,
|
|
2058
|
+
title:
|
|
2059
|
+
'Delete core memory entry?',
|
|
2060
|
+
message:
|
|
2061
|
+
'Remove "${entry.key}" from core memory.',
|
|
2062
|
+
onConfirm: () => controller
|
|
2063
|
+
.deleteCoreMemory(
|
|
2064
|
+
entry.key,
|
|
2096
2065
|
),
|
|
2097
|
-
|
|
2066
|
+
),
|
|
2067
|
+
icon: Icon(Icons.delete_outline),
|
|
2098
2068
|
),
|
|
2099
|
-
|
|
2100
|
-
|
|
2069
|
+
],
|
|
2070
|
+
),
|
|
2071
|
+
);
|
|
2072
|
+
}),
|
|
2101
2073
|
],
|
|
2102
2074
|
),
|
|
2103
2075
|
),
|
|
@@ -2120,8 +2092,11 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2120
2092
|
FilledButton.icon(
|
|
2121
2093
|
onPressed: _llmPromptLoading
|
|
2122
2094
|
? null
|
|
2123
|
-
: () =>
|
|
2124
|
-
|
|
2095
|
+
: () =>
|
|
2096
|
+
_loadLlmPrompt(controller),
|
|
2097
|
+
icon: Icon(
|
|
2098
|
+
Icons.auto_awesome_outlined,
|
|
2099
|
+
),
|
|
2125
2100
|
label: Text(
|
|
2126
2101
|
_llmPromptLoading
|
|
2127
2102
|
? 'Generating...'
|
|
@@ -2130,9 +2105,11 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2130
2105
|
),
|
|
2131
2106
|
OutlinedButton.icon(
|
|
2132
2107
|
onPressed:
|
|
2133
|
-
_llmPromptController.text
|
|
2134
|
-
|
|
2135
|
-
|
|
2108
|
+
_llmPromptController.text
|
|
2109
|
+
.trim()
|
|
2110
|
+
.isEmpty
|
|
2111
|
+
? null
|
|
2112
|
+
: _copyLlmPrompt,
|
|
2136
2113
|
icon: Icon(Icons.copy_all_outlined),
|
|
2137
2114
|
label: Text('Copy Prompt'),
|
|
2138
2115
|
),
|
|
@@ -2145,7 +2122,8 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2145
2122
|
maxLines: 8,
|
|
2146
2123
|
readOnly: true,
|
|
2147
2124
|
decoration: const InputDecoration(
|
|
2148
|
-
labelText:
|
|
2125
|
+
labelText:
|
|
2126
|
+
'Prompt to paste into another AI',
|
|
2149
2127
|
),
|
|
2150
2128
|
),
|
|
2151
2129
|
const SizedBox(height: 16),
|
|
@@ -2155,7 +2133,8 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2155
2133
|
onChanged: _llmImporting
|
|
2156
2134
|
? null
|
|
2157
2135
|
: (value) => setState(
|
|
2158
|
-
() =>
|
|
2136
|
+
() =>
|
|
2137
|
+
_llmApplyBehaviorNotes = value,
|
|
2159
2138
|
),
|
|
2160
2139
|
title: Text('Apply behavior notes'),
|
|
2161
2140
|
subtitle: Text(
|
|
@@ -2168,7 +2147,8 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2168
2147
|
onChanged: _llmImporting
|
|
2169
2148
|
? null
|
|
2170
2149
|
: (value) => setState(
|
|
2171
|
-
() =>
|
|
2150
|
+
() =>
|
|
2151
|
+
_llmApplyCoreMemory = value,
|
|
2172
2152
|
),
|
|
2173
2153
|
title: Text('Apply core memory'),
|
|
2174
2154
|
subtitle: Text(
|
|
@@ -2188,10 +2168,13 @@ class _MemoryPanelState extends State<MemoryPanel>
|
|
|
2188
2168
|
FilledButton.icon(
|
|
2189
2169
|
onPressed: _llmImporting
|
|
2190
2170
|
? null
|
|
2191
|
-
: () =>
|
|
2171
|
+
: () =>
|
|
2172
|
+
_importLlmMemories(controller),
|
|
2192
2173
|
icon: Icon(Icons.file_download_outlined),
|
|
2193
2174
|
label: Text(
|
|
2194
|
-
_llmImporting
|
|
2175
|
+
_llmImporting
|
|
2176
|
+
? 'Importing...'
|
|
2177
|
+
: 'Import',
|
|
2195
2178
|
),
|
|
2196
2179
|
),
|
|
2197
2180
|
],
|
|
@@ -2470,10 +2453,9 @@ class _MemoryConfidenceGaugeState extends State<_MemoryConfidenceGauge>
|
|
|
2470
2453
|
vsync: this,
|
|
2471
2454
|
duration: const Duration(milliseconds: 1200),
|
|
2472
2455
|
);
|
|
2473
|
-
_progress = Tween<double>(
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
|
|
2456
|
+
_progress = Tween<double>(begin: 0, end: widget.confidence).animate(
|
|
2457
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
|
2458
|
+
);
|
|
2477
2459
|
_controller.forward();
|
|
2478
2460
|
}
|
|
2479
2461
|
|
|
@@ -2481,10 +2463,12 @@ class _MemoryConfidenceGaugeState extends State<_MemoryConfidenceGauge>
|
|
|
2481
2463
|
void didUpdateWidget(_MemoryConfidenceGauge oldWidget) {
|
|
2482
2464
|
super.didUpdateWidget(oldWidget);
|
|
2483
2465
|
if (oldWidget.confidence != widget.confidence) {
|
|
2484
|
-
_progress = Tween<double>(
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2466
|
+
_progress = Tween<double>(
|
|
2467
|
+
begin: _progress.value,
|
|
2468
|
+
end: widget.confidence,
|
|
2469
|
+
).animate(
|
|
2470
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
|
2471
|
+
);
|
|
2488
2472
|
_controller
|
|
2489
2473
|
..reset()
|
|
2490
2474
|
..forward();
|
|
@@ -2674,7 +2658,10 @@ class _MemoryRow extends StatelessWidget {
|
|
|
2674
2658
|
if (onDelete != null)
|
|
2675
2659
|
IconButton(
|
|
2676
2660
|
onPressed: onDelete,
|
|
2677
|
-
icon: Icon(
|
|
2661
|
+
icon: Icon(
|
|
2662
|
+
Icons.delete_outline,
|
|
2663
|
+
size: 18,
|
|
2664
|
+
),
|
|
2678
2665
|
),
|
|
2679
2666
|
],
|
|
2680
2667
|
),
|
|
@@ -2699,7 +2686,10 @@ class _MemoryRow extends StatelessWidget {
|
|
|
2699
2686
|
const SizedBox(height: 6),
|
|
2700
2687
|
Text(
|
|
2701
2688
|
memory.createdAtLabel,
|
|
2702
|
-
style: TextStyle(
|
|
2689
|
+
style: TextStyle(
|
|
2690
|
+
fontSize: 11,
|
|
2691
|
+
color: _textMuted,
|
|
2692
|
+
),
|
|
2703
2693
|
),
|
|
2704
2694
|
],
|
|
2705
2695
|
),
|
|
@@ -2788,31 +2778,27 @@ class _EntityGraphViewState extends State<_EntityGraphView>
|
|
|
2788
2778
|
for (int i = 0; i < entities.length; i++) {
|
|
2789
2779
|
final entity = entities[i];
|
|
2790
2780
|
final sizeFactor = 0.4 + 0.6 * (entity.mentionCount / maxMention);
|
|
2791
|
-
_nodes.add(
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
),
|
|
2801
|
-
);
|
|
2781
|
+
_nodes.add(_GraphNode(
|
|
2782
|
+
id: entity.name,
|
|
2783
|
+
label: entity.name,
|
|
2784
|
+
radius: 18 + 20 * sizeFactor,
|
|
2785
|
+
color: _kindColors[entity.kind] ?? _kindColors['concept']!,
|
|
2786
|
+
kind: entity.kind,
|
|
2787
|
+
isReflection: false,
|
|
2788
|
+
offsetPhase: i * 0.7,
|
|
2789
|
+
));
|
|
2802
2790
|
}
|
|
2803
2791
|
|
|
2804
2792
|
for (int i = 0; i < views.length && i < 6; i++) {
|
|
2805
|
-
_nodes.add(
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
),
|
|
2815
|
-
);
|
|
2793
|
+
_nodes.add(_GraphNode(
|
|
2794
|
+
id: 'kv_${views[i].title}',
|
|
2795
|
+
label: views[i].title,
|
|
2796
|
+
radius: 14,
|
|
2797
|
+
color: const Color(0xFF8B7EC8),
|
|
2798
|
+
kind: views[i].viewType,
|
|
2799
|
+
isReflection: true,
|
|
2800
|
+
offsetPhase: (entities.length + i) * 0.9,
|
|
2801
|
+
));
|
|
2816
2802
|
}
|
|
2817
2803
|
|
|
2818
2804
|
_layoutDone = false;
|
|
@@ -2953,12 +2939,10 @@ class _EntityGraphPainter extends CustomPainter {
|
|
|
2953
2939
|
void paint(Canvas canvas, Size size) {
|
|
2954
2940
|
if (nodes.isEmpty) return;
|
|
2955
2941
|
|
|
2956
|
-
final entityNodes =
|
|
2957
|
-
.where((n) => !n.isReflection)
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
.where((n) => n.isReflection)
|
|
2961
|
-
.toList(growable: false);
|
|
2942
|
+
final entityNodes =
|
|
2943
|
+
nodes.where((n) => !n.isReflection).toList(growable: false);
|
|
2944
|
+
final reflectionNodes =
|
|
2945
|
+
nodes.where((n) => n.isReflection).toList(growable: false);
|
|
2962
2946
|
|
|
2963
2947
|
// Draw connections between entity nodes (subtle web)
|
|
2964
2948
|
final linePaint = Paint()
|
|
@@ -2990,8 +2974,8 @@ class _EntityGraphPainter extends CustomPainter {
|
|
|
2990
2974
|
var closest = entityNodes.first;
|
|
2991
2975
|
var minDist = double.infinity;
|
|
2992
2976
|
for (final en in entityNodes) {
|
|
2993
|
-
final d =
|
|
2994
|
-
(en.
|
|
2977
|
+
final d = (en.x - rn.x) * (en.x - rn.x) +
|
|
2978
|
+
(en.y - rn.y) * (en.y - rn.y);
|
|
2995
2979
|
if (d < minDist) {
|
|
2996
2980
|
minDist = d;
|
|
2997
2981
|
closest = en;
|
|
@@ -3018,9 +3002,8 @@ class _EntityGraphPainter extends CustomPainter {
|
|
|
3018
3002
|
// Glow
|
|
3019
3003
|
if (isSelected || isHovered) {
|
|
3020
3004
|
final glowPaint = Paint()
|
|
3021
|
-
..color = (isSelected ? accentColor : node.color)
|
|
3022
|
-
|
|
3023
|
-
)
|
|
3005
|
+
..color = (isSelected ? accentColor : node.color)
|
|
3006
|
+
.withValues(alpha: 0.22)
|
|
3024
3007
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
|
|
3025
3008
|
canvas.drawCircle(Offset(cx, cy), r + 6, glowPaint);
|
|
3026
3009
|
}
|
|
@@ -3040,8 +3023,8 @@ class _EntityGraphPainter extends CustomPainter {
|
|
|
3040
3023
|
..color = isSelected
|
|
3041
3024
|
? accentColor
|
|
3042
3025
|
: (isHovered
|
|
3043
|
-
|
|
3044
|
-
|
|
3026
|
+
? node.color.withValues(alpha: 0.8)
|
|
3027
|
+
: node.color.withValues(alpha: 0.35))
|
|
3045
3028
|
..style = PaintingStyle.stroke
|
|
3046
3029
|
..strokeWidth = isSelected ? 2.5 : 1.5;
|
|
3047
3030
|
canvas.drawCircle(Offset(cx, cy), r, borderPaint);
|
|
@@ -3058,7 +3041,10 @@ class _EntityGraphPainter extends CustomPainter {
|
|
|
3058
3041
|
maxLines: 1,
|
|
3059
3042
|
ellipsis: '…',
|
|
3060
3043
|
)..layout(maxWidth: r * 3);
|
|
3061
|
-
tp.paint(
|
|
3044
|
+
tp.paint(
|
|
3045
|
+
canvas,
|
|
3046
|
+
Offset(cx - tp.width / 2, cy + r + 5),
|
|
3047
|
+
);
|
|
3062
3048
|
}
|
|
3063
3049
|
}
|
|
3064
3050
|
|
|
@@ -5039,15 +5025,6 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
5039
5025
|
providerKey: 'google_workspace',
|
|
5040
5026
|
appKey: 'gmail',
|
|
5041
5027
|
),
|
|
5042
|
-
_TaskTriggerOption(
|
|
5043
|
-
type: 'neomail_email_received',
|
|
5044
|
-
section: 'Email',
|
|
5045
|
-
label: 'NeoMail Email Received',
|
|
5046
|
-
description: 'Run when a matching message is synced into NeoMail.',
|
|
5047
|
-
icon: Icons.mark_email_unread_rounded,
|
|
5048
|
-
providerKey: 'neomail',
|
|
5049
|
-
appKey: 'mailbox',
|
|
5050
|
-
),
|
|
5051
5028
|
_TaskTriggerOption(
|
|
5052
5029
|
type: 'outlook_email_received',
|
|
5053
5030
|
section: 'Email',
|
|
@@ -5110,6 +5087,284 @@ _TaskTriggerOption _taskTriggerOptionForType(String type) {
|
|
|
5110
5087
|
);
|
|
5111
5088
|
}
|
|
5112
5089
|
|
|
5090
|
+
class _TaskSchedulePreset {
|
|
5091
|
+
const _TaskSchedulePreset({
|
|
5092
|
+
required this.id,
|
|
5093
|
+
required this.label,
|
|
5094
|
+
required this.description,
|
|
5095
|
+
required this.icon,
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
final String id;
|
|
5099
|
+
final String label;
|
|
5100
|
+
final String description;
|
|
5101
|
+
final IconData icon;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
const List<_TaskSchedulePreset> _taskSchedulePresets = <_TaskSchedulePreset>[
|
|
5105
|
+
_TaskSchedulePreset(
|
|
5106
|
+
id: 'every_15_minutes',
|
|
5107
|
+
label: 'Every 15 minutes',
|
|
5108
|
+
description: 'Runs four times per hour.',
|
|
5109
|
+
icon: Icons.timer_outlined,
|
|
5110
|
+
),
|
|
5111
|
+
_TaskSchedulePreset(
|
|
5112
|
+
id: 'every_30_minutes',
|
|
5113
|
+
label: 'Every 30 minutes',
|
|
5114
|
+
description: 'Runs twice per hour.',
|
|
5115
|
+
icon: Icons.timelapse_rounded,
|
|
5116
|
+
),
|
|
5117
|
+
_TaskSchedulePreset(
|
|
5118
|
+
id: 'hourly',
|
|
5119
|
+
label: 'Hourly',
|
|
5120
|
+
description: 'Runs once per hour.',
|
|
5121
|
+
icon: Icons.schedule_rounded,
|
|
5122
|
+
),
|
|
5123
|
+
_TaskSchedulePreset(
|
|
5124
|
+
id: 'daily',
|
|
5125
|
+
label: 'Daily',
|
|
5126
|
+
description: 'Runs every day at the selected time.',
|
|
5127
|
+
icon: Icons.today_rounded,
|
|
5128
|
+
),
|
|
5129
|
+
_TaskSchedulePreset(
|
|
5130
|
+
id: 'weekdays',
|
|
5131
|
+
label: 'Weekdays',
|
|
5132
|
+
description: 'Runs Monday through Friday.',
|
|
5133
|
+
icon: Icons.work_outline_rounded,
|
|
5134
|
+
),
|
|
5135
|
+
_TaskSchedulePreset(
|
|
5136
|
+
id: 'weekly',
|
|
5137
|
+
label: 'Weekly',
|
|
5138
|
+
description: 'Runs on selected weekdays.',
|
|
5139
|
+
icon: Icons.view_week_rounded,
|
|
5140
|
+
),
|
|
5141
|
+
_TaskSchedulePreset(
|
|
5142
|
+
id: 'monthly',
|
|
5143
|
+
label: 'Monthly',
|
|
5144
|
+
description: 'Runs once per month on the selected day.',
|
|
5145
|
+
icon: Icons.calendar_month_rounded,
|
|
5146
|
+
),
|
|
5147
|
+
_TaskSchedulePreset(
|
|
5148
|
+
id: 'custom',
|
|
5149
|
+
label: 'Custom Cron',
|
|
5150
|
+
description: 'Advanced manual schedule for special cases.',
|
|
5151
|
+
icon: Icons.tune_rounded,
|
|
5152
|
+
),
|
|
5153
|
+
];
|
|
5154
|
+
|
|
5155
|
+
const List<String> _taskWeekdayLabels = <String>[
|
|
5156
|
+
'Mon',
|
|
5157
|
+
'Tue',
|
|
5158
|
+
'Wed',
|
|
5159
|
+
'Thu',
|
|
5160
|
+
'Fri',
|
|
5161
|
+
'Sat',
|
|
5162
|
+
'Sun',
|
|
5163
|
+
];
|
|
5164
|
+
|
|
5165
|
+
_TaskSchedulePreset _taskSchedulePresetForId(String id) {
|
|
5166
|
+
return _taskSchedulePresets.firstWhere(
|
|
5167
|
+
(entry) => entry.id == id,
|
|
5168
|
+
orElse: () => _taskSchedulePresets[1],
|
|
5169
|
+
);
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
class _TaskScheduleDraft {
|
|
5173
|
+
_TaskScheduleDraft({
|
|
5174
|
+
required this.mode,
|
|
5175
|
+
required this.presetId,
|
|
5176
|
+
required this.time,
|
|
5177
|
+
required this.weekdays,
|
|
5178
|
+
required this.monthDay,
|
|
5179
|
+
required this.customCronExpression,
|
|
5180
|
+
});
|
|
5181
|
+
|
|
5182
|
+
factory _TaskScheduleDraft.fromTask(TaskItem? task) {
|
|
5183
|
+
final runAt = task?.triggerConfig['runAt']?.toString().trim() ?? '';
|
|
5184
|
+
if (runAt.isNotEmpty) {
|
|
5185
|
+
return _TaskScheduleDraft(
|
|
5186
|
+
mode: 'one_time',
|
|
5187
|
+
presetId: 'daily',
|
|
5188
|
+
time: const TimeOfDay(hour: 9, minute: 0),
|
|
5189
|
+
weekdays: <int>{1},
|
|
5190
|
+
monthDay: 1,
|
|
5191
|
+
customCronExpression: '',
|
|
5192
|
+
);
|
|
5193
|
+
}
|
|
5194
|
+
final cron =
|
|
5195
|
+
task?.triggerConfig['cronExpression']?.toString().trim() ??
|
|
5196
|
+
'*/30 * * * *';
|
|
5197
|
+
final parsed = _parseCronExpression(cron);
|
|
5198
|
+
if (parsed != null) return parsed;
|
|
5199
|
+
return _TaskScheduleDraft(
|
|
5200
|
+
mode: 'recurring',
|
|
5201
|
+
presetId: 'custom',
|
|
5202
|
+
time: const TimeOfDay(hour: 9, minute: 0),
|
|
5203
|
+
weekdays: <int>{1},
|
|
5204
|
+
monthDay: 1,
|
|
5205
|
+
customCronExpression: cron,
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
|
|
5209
|
+
String mode;
|
|
5210
|
+
String presetId;
|
|
5211
|
+
TimeOfDay time;
|
|
5212
|
+
Set<int> weekdays;
|
|
5213
|
+
int monthDay;
|
|
5214
|
+
String customCronExpression;
|
|
5215
|
+
|
|
5216
|
+
bool get usesTime =>
|
|
5217
|
+
presetId == 'daily' ||
|
|
5218
|
+
presetId == 'weekdays' ||
|
|
5219
|
+
presetId == 'weekly' ||
|
|
5220
|
+
presetId == 'monthly';
|
|
5221
|
+
|
|
5222
|
+
String get cronExpression {
|
|
5223
|
+
if (presetId == 'custom') return customCronExpression;
|
|
5224
|
+
final minute = time.minute.toString();
|
|
5225
|
+
final hour = time.hour.toString();
|
|
5226
|
+
return switch (presetId) {
|
|
5227
|
+
'every_15_minutes' => '*/15 * * * *',
|
|
5228
|
+
'every_30_minutes' => '*/30 * * * *',
|
|
5229
|
+
'hourly' => '0 * * * *',
|
|
5230
|
+
'daily' => '$minute $hour * * *',
|
|
5231
|
+
'weekdays' => '$minute $hour * * 1-5',
|
|
5232
|
+
'weekly' => '$minute $hour * * ${_cronWeekdays(weekdays)}',
|
|
5233
|
+
'monthly' => '$minute $hour $monthDay * *',
|
|
5234
|
+
'custom' => customCronExpression,
|
|
5235
|
+
_ => '*/30 * * * *',
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
String get summary {
|
|
5240
|
+
if (mode == 'one_time') return 'One-time run';
|
|
5241
|
+
if (presetId == 'custom') return 'Custom Cron';
|
|
5242
|
+
final preset = _taskSchedulePresetForId(presetId);
|
|
5243
|
+
if (!usesTime) return preset.label;
|
|
5244
|
+
final timeLabel = _formatTaskScheduleTime(time);
|
|
5245
|
+
if (presetId == 'weekly') {
|
|
5246
|
+
return '${preset.label} ${_formatTaskWeekdays(weekdays)} at $timeLabel';
|
|
5247
|
+
}
|
|
5248
|
+
if (presetId == 'monthly') {
|
|
5249
|
+
return '${preset.label} on day $monthDay at $timeLabel';
|
|
5250
|
+
}
|
|
5251
|
+
return '${preset.label} at $timeLabel';
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
_TaskScheduleDraft? _parseCronExpression(String cron) {
|
|
5256
|
+
final fields = cron.trim().split(RegExp(r'\s+'));
|
|
5257
|
+
if (fields.length != 5) return null;
|
|
5258
|
+
final minute = fields[0];
|
|
5259
|
+
final hour = fields[1];
|
|
5260
|
+
final dayOfMonth = fields[2];
|
|
5261
|
+
final month = fields[3];
|
|
5262
|
+
final dayOfWeek = fields[4];
|
|
5263
|
+
if (cron == '*/15 * * * *') {
|
|
5264
|
+
return _recurringScheduleDraft('every_15_minutes');
|
|
5265
|
+
}
|
|
5266
|
+
if (cron == '*/30 * * * *') {
|
|
5267
|
+
return _recurringScheduleDraft('every_30_minutes');
|
|
5268
|
+
}
|
|
5269
|
+
if (cron == '0 * * * *') return _recurringScheduleDraft('hourly');
|
|
5270
|
+
final parsedMinute = int.tryParse(minute);
|
|
5271
|
+
final parsedHour = int.tryParse(hour);
|
|
5272
|
+
if (parsedMinute == null ||
|
|
5273
|
+
parsedMinute < 0 ||
|
|
5274
|
+
parsedMinute > 59 ||
|
|
5275
|
+
parsedHour == null ||
|
|
5276
|
+
parsedHour < 0 ||
|
|
5277
|
+
parsedHour > 23 ||
|
|
5278
|
+
month != '*') {
|
|
5279
|
+
return null;
|
|
5280
|
+
}
|
|
5281
|
+
final time = TimeOfDay(hour: parsedHour, minute: parsedMinute);
|
|
5282
|
+
if (dayOfMonth == '*' && dayOfWeek == '*') {
|
|
5283
|
+
return _recurringScheduleDraft('daily', time: time);
|
|
5284
|
+
}
|
|
5285
|
+
if (dayOfMonth == '*' && dayOfWeek == '1-5') {
|
|
5286
|
+
return _recurringScheduleDraft('weekdays', time: time);
|
|
5287
|
+
}
|
|
5288
|
+
if (dayOfMonth == '*') {
|
|
5289
|
+
final weekdays = _parseCronWeekdays(dayOfWeek);
|
|
5290
|
+
if (weekdays == null || weekdays.isEmpty) return null;
|
|
5291
|
+
return _recurringScheduleDraft('weekly', time: time, weekdays: weekdays);
|
|
5292
|
+
}
|
|
5293
|
+
if (dayOfWeek == '*') {
|
|
5294
|
+
final parsedDay = int.tryParse(dayOfMonth);
|
|
5295
|
+
if (parsedDay == null || parsedDay < 1 || parsedDay > 31) return null;
|
|
5296
|
+
return _recurringScheduleDraft('monthly', time: time, monthDay: parsedDay);
|
|
5297
|
+
}
|
|
5298
|
+
return null;
|
|
5299
|
+
}
|
|
5300
|
+
|
|
5301
|
+
bool _looksLikeCronExpression(String cron) {
|
|
5302
|
+
return cron.trim().split(RegExp(r'\s+')).length == 5;
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
_TaskScheduleDraft _recurringScheduleDraft(
|
|
5306
|
+
String presetId, {
|
|
5307
|
+
TimeOfDay time = const TimeOfDay(hour: 9, minute: 0),
|
|
5308
|
+
Set<int> weekdays = const <int>{1},
|
|
5309
|
+
int monthDay = 1,
|
|
5310
|
+
}) {
|
|
5311
|
+
return _TaskScheduleDraft(
|
|
5312
|
+
mode: 'recurring',
|
|
5313
|
+
presetId: presetId,
|
|
5314
|
+
time: time,
|
|
5315
|
+
weekdays: Set<int>.from(weekdays),
|
|
5316
|
+
monthDay: monthDay,
|
|
5317
|
+
customCronExpression: '',
|
|
5318
|
+
);
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
Set<int>? _parseCronWeekdays(String value) {
|
|
5322
|
+
final result = <int>{};
|
|
5323
|
+
for (final part in value.split(',')) {
|
|
5324
|
+
final trimmed = part.trim();
|
|
5325
|
+
if (trimmed.isEmpty) return null;
|
|
5326
|
+
final rangeParts = trimmed.split('-');
|
|
5327
|
+
if (rangeParts.length == 2) {
|
|
5328
|
+
final start = _parseCronWeekday(rangeParts[0]);
|
|
5329
|
+
final end = _parseCronWeekday(rangeParts[1]);
|
|
5330
|
+
if (start == null || end == null || start > end) return null;
|
|
5331
|
+
for (var day = start; day <= end; day += 1) {
|
|
5332
|
+
result.add(day);
|
|
5333
|
+
}
|
|
5334
|
+
} else if (rangeParts.length == 1) {
|
|
5335
|
+
final day = _parseCronWeekday(trimmed);
|
|
5336
|
+
if (day == null) return null;
|
|
5337
|
+
result.add(day);
|
|
5338
|
+
} else {
|
|
5339
|
+
return null;
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
return result;
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
int? _parseCronWeekday(String value) {
|
|
5346
|
+
final parsed = int.tryParse(value.trim());
|
|
5347
|
+
if (parsed == null || parsed < 0 || parsed > 7) return null;
|
|
5348
|
+
return parsed == 0 ? 7 : parsed;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
String _cronWeekdays(Set<int> weekdays) {
|
|
5352
|
+
final sorted = weekdays.where((day) => day >= 1 && day <= 7).toList()..sort();
|
|
5353
|
+
if (sorted.isEmpty) return '1';
|
|
5354
|
+
return sorted.join(',');
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
String _formatTaskScheduleTime(TimeOfDay time) {
|
|
5358
|
+
final hour = time.hour.toString().padLeft(2, '0');
|
|
5359
|
+
final minute = time.minute.toString().padLeft(2, '0');
|
|
5360
|
+
return '$hour:$minute';
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
String _formatTaskWeekdays(Set<int> weekdays) {
|
|
5364
|
+
final sorted = weekdays.where((day) => day >= 1 && day <= 7).toList()..sort();
|
|
5365
|
+
if (sorted.isEmpty) return 'Monday';
|
|
5366
|
+
return sorted.map((day) => _taskWeekdayLabels[day - 1]).join(', ');
|
|
5367
|
+
}
|
|
5113
5368
|
Future<String?> _pickTaskTriggerType(
|
|
5114
5369
|
BuildContext context,
|
|
5115
5370
|
String selectedType,
|
|
@@ -6277,8 +6532,9 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6277
6532
|
}) async {
|
|
6278
6533
|
final nameController = TextEditingController(text: task?.name ?? '');
|
|
6279
6534
|
final triggerType = ValueNotifier<String>(task?.triggerType ?? 'schedule');
|
|
6280
|
-
final
|
|
6281
|
-
|
|
6535
|
+
final scheduleDraft = _TaskScheduleDraft.fromTask(task);
|
|
6536
|
+
final customCronController = TextEditingController(
|
|
6537
|
+
text: scheduleDraft.customCronExpression,
|
|
6282
6538
|
);
|
|
6283
6539
|
final runAtController = TextEditingController(
|
|
6284
6540
|
text: task?.triggerConfig['runAt']?.toString() ?? '',
|
|
@@ -6289,7 +6545,9 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6289
6545
|
final selectedConnectionId = ValueNotifier<int?>(
|
|
6290
6546
|
task?.triggerConfig['connectionId'] is int
|
|
6291
6547
|
? task!.triggerConfig['connectionId'] as int
|
|
6292
|
-
: int.tryParse(
|
|
6548
|
+
: int.tryParse(
|
|
6549
|
+
task?.triggerConfig['connectionId']?.toString() ?? '',
|
|
6550
|
+
),
|
|
6293
6551
|
);
|
|
6294
6552
|
final selectedDeliveryTarget = ValueNotifier<TaskDeliveryTarget?>(
|
|
6295
6553
|
_taskDeliveryTargetFromTask(task),
|
|
@@ -6312,8 +6570,6 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6312
6570
|
);
|
|
6313
6571
|
final channelController = TextEditingController(
|
|
6314
6572
|
text:
|
|
6315
|
-
task?.triggerConfig['folder']?.toString() ??
|
|
6316
|
-
task?.triggerConfig['folderId']?.toString() ??
|
|
6317
6573
|
task?.triggerConfig['channel']?.toString() ??
|
|
6318
6574
|
task?.triggerConfig['chatId']?.toString() ??
|
|
6319
6575
|
'',
|
|
@@ -6485,21 +6741,246 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6485
6741
|
if (selectedTriggerType == 'schedule') {
|
|
6486
6742
|
return Column(
|
|
6487
6743
|
children: <Widget>[
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6744
|
+
SegmentedButton<String>(
|
|
6745
|
+
segments: const <ButtonSegment<String>>[
|
|
6746
|
+
ButtonSegment<String>(
|
|
6747
|
+
value: 'recurring',
|
|
6748
|
+
icon: Icon(Icons.repeat_rounded),
|
|
6749
|
+
label: Text('Recurring'),
|
|
6750
|
+
),
|
|
6751
|
+
ButtonSegment<String>(
|
|
6752
|
+
value: 'one_time',
|
|
6753
|
+
icon: Icon(Icons.event_rounded),
|
|
6754
|
+
label: Text('Once'),
|
|
6755
|
+
),
|
|
6756
|
+
],
|
|
6757
|
+
selected: <String>{scheduleDraft.mode},
|
|
6758
|
+
onSelectionChanged: (selection) {
|
|
6759
|
+
setLocalState(() {
|
|
6760
|
+
scheduleDraft.mode = selection.first;
|
|
6761
|
+
if (scheduleDraft.mode == 'recurring') {
|
|
6762
|
+
runAtController.clear();
|
|
6763
|
+
}
|
|
6764
|
+
});
|
|
6765
|
+
},
|
|
6495
6766
|
),
|
|
6496
6767
|
const SizedBox(height: 12),
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6768
|
+
if (scheduleDraft.mode == 'one_time')
|
|
6769
|
+
TextField(
|
|
6770
|
+
controller: runAtController,
|
|
6771
|
+
decoration: const InputDecoration(
|
|
6772
|
+
labelText: 'Run At',
|
|
6773
|
+
helperText:
|
|
6774
|
+
'Use a date and time, for example 2026-07-03T09:00:00.',
|
|
6775
|
+
),
|
|
6776
|
+
)
|
|
6777
|
+
else ...<Widget>[
|
|
6778
|
+
DropdownButtonFormField<String>(
|
|
6779
|
+
initialValue: scheduleDraft.presetId,
|
|
6780
|
+
isExpanded: true,
|
|
6781
|
+
decoration: const InputDecoration(
|
|
6782
|
+
labelText: 'Repeat',
|
|
6783
|
+
),
|
|
6784
|
+
items: <DropdownMenuItem<String>>[
|
|
6785
|
+
..._taskSchedulePresets.map(
|
|
6786
|
+
(preset) => DropdownMenuItem<String>(
|
|
6787
|
+
value: preset.id,
|
|
6788
|
+
child: Text(preset.label),
|
|
6789
|
+
),
|
|
6790
|
+
),
|
|
6791
|
+
],
|
|
6792
|
+
onChanged: (value) {
|
|
6793
|
+
if (value == null) return;
|
|
6794
|
+
setLocalState(() {
|
|
6795
|
+
final currentCron =
|
|
6796
|
+
scheduleDraft.cronExpression;
|
|
6797
|
+
scheduleDraft.presetId = value;
|
|
6798
|
+
if (value == 'custom' &&
|
|
6799
|
+
customCronController.text
|
|
6800
|
+
.trim()
|
|
6801
|
+
.isEmpty) {
|
|
6802
|
+
customCronController.text =
|
|
6803
|
+
currentCron;
|
|
6804
|
+
scheduleDraft.customCronExpression =
|
|
6805
|
+
currentCron;
|
|
6806
|
+
}
|
|
6807
|
+
});
|
|
6808
|
+
},
|
|
6501
6809
|
),
|
|
6502
|
-
|
|
6810
|
+
const SizedBox(height: 12),
|
|
6811
|
+
if (scheduleDraft.presetId ==
|
|
6812
|
+
'custom') ...<Widget>[
|
|
6813
|
+
TextField(
|
|
6814
|
+
controller: customCronController,
|
|
6815
|
+
onChanged: (value) =>
|
|
6816
|
+
scheduleDraft.customCronExpression =
|
|
6817
|
+
value,
|
|
6818
|
+
decoration: const InputDecoration(
|
|
6819
|
+
labelText: 'Cron expression',
|
|
6820
|
+
helperText:
|
|
6821
|
+
'Advanced: minute hour day month weekday.',
|
|
6822
|
+
),
|
|
6823
|
+
),
|
|
6824
|
+
const SizedBox(height: 12),
|
|
6825
|
+
],
|
|
6826
|
+
if (scheduleDraft.usesTime) ...<Widget>[
|
|
6827
|
+
InkWell(
|
|
6828
|
+
borderRadius: BorderRadius.circular(14),
|
|
6829
|
+
onTap: () async {
|
|
6830
|
+
final picked = await showTimePicker(
|
|
6831
|
+
context: context,
|
|
6832
|
+
initialTime: scheduleDraft.time,
|
|
6833
|
+
builder: (context, child) {
|
|
6834
|
+
return MediaQuery(
|
|
6835
|
+
data: MediaQuery.of(context)
|
|
6836
|
+
.copyWith(
|
|
6837
|
+
alwaysUse24HourFormat: true,
|
|
6838
|
+
),
|
|
6839
|
+
child: child!,
|
|
6840
|
+
);
|
|
6841
|
+
},
|
|
6842
|
+
);
|
|
6843
|
+
if (picked == null) return;
|
|
6844
|
+
setLocalState(() {
|
|
6845
|
+
scheduleDraft.time = picked;
|
|
6846
|
+
});
|
|
6847
|
+
},
|
|
6848
|
+
child: InputDecorator(
|
|
6849
|
+
decoration: const InputDecoration(
|
|
6850
|
+
labelText: 'Time',
|
|
6851
|
+
),
|
|
6852
|
+
child: Row(
|
|
6853
|
+
children: <Widget>[
|
|
6854
|
+
const Icon(
|
|
6855
|
+
Icons.access_time_rounded,
|
|
6856
|
+
),
|
|
6857
|
+
const SizedBox(width: 12),
|
|
6858
|
+
Expanded(
|
|
6859
|
+
child: Text(
|
|
6860
|
+
_formatTaskScheduleTime(
|
|
6861
|
+
scheduleDraft.time,
|
|
6862
|
+
),
|
|
6863
|
+
style: const TextStyle(
|
|
6864
|
+
fontWeight: FontWeight.w700,
|
|
6865
|
+
),
|
|
6866
|
+
),
|
|
6867
|
+
),
|
|
6868
|
+
Icon(
|
|
6869
|
+
Icons.unfold_more_rounded,
|
|
6870
|
+
color: _textSecondary,
|
|
6871
|
+
),
|
|
6872
|
+
],
|
|
6873
|
+
),
|
|
6874
|
+
),
|
|
6875
|
+
),
|
|
6876
|
+
const SizedBox(height: 12),
|
|
6877
|
+
],
|
|
6878
|
+
if (scheduleDraft.presetId ==
|
|
6879
|
+
'weekly') ...<Widget>[
|
|
6880
|
+
Align(
|
|
6881
|
+
alignment: Alignment.centerLeft,
|
|
6882
|
+
child: Wrap(
|
|
6883
|
+
spacing: 8,
|
|
6884
|
+
runSpacing: 8,
|
|
6885
|
+
children: List<Widget>.generate(7, (
|
|
6886
|
+
index,
|
|
6887
|
+
) {
|
|
6888
|
+
final day = index + 1;
|
|
6889
|
+
return ChoiceChip(
|
|
6890
|
+
label: Text(
|
|
6891
|
+
_taskWeekdayLabels[index],
|
|
6892
|
+
),
|
|
6893
|
+
selected: scheduleDraft.weekdays
|
|
6894
|
+
.contains(day),
|
|
6895
|
+
onSelected: (selected) {
|
|
6896
|
+
setLocalState(() {
|
|
6897
|
+
if (selected) {
|
|
6898
|
+
scheduleDraft.weekdays.add(
|
|
6899
|
+
day,
|
|
6900
|
+
);
|
|
6901
|
+
} else if (scheduleDraft
|
|
6902
|
+
.weekdays
|
|
6903
|
+
.length >
|
|
6904
|
+
1) {
|
|
6905
|
+
scheduleDraft.weekdays.remove(
|
|
6906
|
+
day,
|
|
6907
|
+
);
|
|
6908
|
+
}
|
|
6909
|
+
});
|
|
6910
|
+
},
|
|
6911
|
+
);
|
|
6912
|
+
}),
|
|
6913
|
+
),
|
|
6914
|
+
),
|
|
6915
|
+
const SizedBox(height: 12),
|
|
6916
|
+
],
|
|
6917
|
+
if (scheduleDraft.presetId ==
|
|
6918
|
+
'monthly') ...<Widget>[
|
|
6919
|
+
DropdownButtonFormField<int>(
|
|
6920
|
+
initialValue: scheduleDraft.monthDay,
|
|
6921
|
+
decoration: const InputDecoration(
|
|
6922
|
+
labelText: 'Day of month',
|
|
6923
|
+
),
|
|
6924
|
+
items:
|
|
6925
|
+
List<DropdownMenuItem<int>>.generate(
|
|
6926
|
+
31,
|
|
6927
|
+
(index) => DropdownMenuItem<int>(
|
|
6928
|
+
value: index + 1,
|
|
6929
|
+
child: Text('Day ${index + 1}'),
|
|
6930
|
+
),
|
|
6931
|
+
),
|
|
6932
|
+
onChanged: (value) {
|
|
6933
|
+
if (value == null) return;
|
|
6934
|
+
setLocalState(() {
|
|
6935
|
+
scheduleDraft.monthDay = value;
|
|
6936
|
+
});
|
|
6937
|
+
},
|
|
6938
|
+
),
|
|
6939
|
+
const SizedBox(height: 12),
|
|
6940
|
+
],
|
|
6941
|
+
InputDecorator(
|
|
6942
|
+
decoration: const InputDecoration(
|
|
6943
|
+
labelText: 'Schedule',
|
|
6944
|
+
),
|
|
6945
|
+
child: Builder(
|
|
6946
|
+
builder: (context) {
|
|
6947
|
+
final preset = _taskSchedulePresetForId(
|
|
6948
|
+
scheduleDraft.presetId,
|
|
6949
|
+
);
|
|
6950
|
+
return Row(
|
|
6951
|
+
children: <Widget>[
|
|
6952
|
+
Icon(preset.icon, color: _accent),
|
|
6953
|
+
const SizedBox(width: 12),
|
|
6954
|
+
Expanded(
|
|
6955
|
+
child: Column(
|
|
6956
|
+
crossAxisAlignment:
|
|
6957
|
+
CrossAxisAlignment.start,
|
|
6958
|
+
mainAxisSize: MainAxisSize.min,
|
|
6959
|
+
children: <Widget>[
|
|
6960
|
+
Text(
|
|
6961
|
+
scheduleDraft.summary,
|
|
6962
|
+
style: const TextStyle(
|
|
6963
|
+
fontWeight:
|
|
6964
|
+
FontWeight.w700,
|
|
6965
|
+
),
|
|
6966
|
+
),
|
|
6967
|
+
const SizedBox(height: 3),
|
|
6968
|
+
Text(
|
|
6969
|
+
preset.description,
|
|
6970
|
+
style: TextStyle(
|
|
6971
|
+
color: _textSecondary,
|
|
6972
|
+
fontSize: 12,
|
|
6973
|
+
),
|
|
6974
|
+
),
|
|
6975
|
+
],
|
|
6976
|
+
),
|
|
6977
|
+
),
|
|
6978
|
+
],
|
|
6979
|
+
);
|
|
6980
|
+
},
|
|
6981
|
+
),
|
|
6982
|
+
),
|
|
6983
|
+
],
|
|
6503
6984
|
],
|
|
6504
6985
|
);
|
|
6505
6986
|
}
|
|
@@ -6534,8 +7015,6 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6534
7015
|
],
|
|
6535
7016
|
if (selectedTriggerType ==
|
|
6536
7017
|
'gmail_message_received' ||
|
|
6537
|
-
selectedTriggerType ==
|
|
6538
|
-
'neomail_email_received' ||
|
|
6539
7018
|
selectedTriggerType ==
|
|
6540
7019
|
'outlook_email_received') ...<Widget>[
|
|
6541
7020
|
TextField(
|
|
@@ -6553,18 +7032,6 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6553
7032
|
setLocalState(() => unreadOnly = value),
|
|
6554
7033
|
),
|
|
6555
7034
|
],
|
|
6556
|
-
if (selectedTriggerType ==
|
|
6557
|
-
'neomail_email_received') ...<Widget>[
|
|
6558
|
-
TextField(
|
|
6559
|
-
controller: channelController,
|
|
6560
|
-
decoration: const InputDecoration(
|
|
6561
|
-
labelText: 'Folder (optional)',
|
|
6562
|
-
helperText:
|
|
6563
|
-
'Example: INBOX, archive, or a custom folder path.',
|
|
6564
|
-
),
|
|
6565
|
-
),
|
|
6566
|
-
const SizedBox(height: 12),
|
|
6567
|
-
],
|
|
6568
7035
|
if (selectedTriggerType ==
|
|
6569
7036
|
'outlook_email_received') ...<Widget>[
|
|
6570
7037
|
TextField(
|
|
@@ -6871,13 +7338,44 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6871
7338
|
// Manual trigger uses no trigger-specific config.
|
|
6872
7339
|
} else if (selectedTriggerType == 'schedule') {
|
|
6873
7340
|
final runAt = runAtController.text.trim();
|
|
6874
|
-
triggerConfig['mode'] =
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
7341
|
+
triggerConfig['mode'] = scheduleDraft.mode;
|
|
7342
|
+
if (scheduleDraft.mode == 'recurring') {
|
|
7343
|
+
scheduleDraft.customCronExpression =
|
|
7344
|
+
customCronController.text.trim();
|
|
7345
|
+
final cronExpression = scheduleDraft.cronExpression
|
|
6879
7346
|
.trim();
|
|
7347
|
+
if (cronExpression.isEmpty) {
|
|
7348
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
7349
|
+
const SnackBar(
|
|
7350
|
+
content: Text('Please choose a schedule.'),
|
|
7351
|
+
backgroundColor: Colors.red,
|
|
7352
|
+
),
|
|
7353
|
+
);
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
if (scheduleDraft.presetId == 'custom' &&
|
|
7357
|
+
!_looksLikeCronExpression(cronExpression)) {
|
|
7358
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
7359
|
+
const SnackBar(
|
|
7360
|
+
content: Text('Custom Cron must have 5 fields.'),
|
|
7361
|
+
backgroundColor: Colors.red,
|
|
7362
|
+
),
|
|
7363
|
+
);
|
|
7364
|
+
return;
|
|
7365
|
+
}
|
|
7366
|
+
triggerConfig['cronExpression'] = cronExpression;
|
|
6880
7367
|
} else {
|
|
7368
|
+
if (runAt.isEmpty) {
|
|
7369
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
7370
|
+
const SnackBar(
|
|
7371
|
+
content: Text(
|
|
7372
|
+
'Please enter when the task should run.',
|
|
7373
|
+
),
|
|
7374
|
+
backgroundColor: Colors.red,
|
|
7375
|
+
),
|
|
7376
|
+
);
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
6881
7379
|
triggerConfig['runAt'] = runAt;
|
|
6882
7380
|
}
|
|
6883
7381
|
} else {
|
|
@@ -6918,17 +7416,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6918
7416
|
triggerConfig['eventTypes'] = eventTypes;
|
|
6919
7417
|
}
|
|
6920
7418
|
if (selectedTriggerType == 'gmail_message_received' ||
|
|
6921
|
-
selectedTriggerType == 'neomail_email_received' ||
|
|
6922
7419
|
selectedTriggerType == 'outlook_email_received') {
|
|
6923
7420
|
if (queryController.text.trim().isNotEmpty) {
|
|
6924
7421
|
triggerConfig['query'] = queryController.text.trim();
|
|
6925
7422
|
}
|
|
6926
7423
|
triggerConfig['unreadOnly'] = unreadOnly;
|
|
6927
|
-
if (selectedTriggerType == 'neomail_email_received' &&
|
|
6928
|
-
channelController.text.trim().isNotEmpty) {
|
|
6929
|
-
triggerConfig['folder'] = channelController.text
|
|
6930
|
-
.trim();
|
|
6931
|
-
}
|
|
6932
7424
|
if (selectedTriggerType == 'outlook_email_received' &&
|
|
6933
7425
|
channelController.text.trim().isNotEmpty) {
|
|
6934
7426
|
triggerConfig['folderId'] = channelController.text
|