neoagent 2.3.1-beta.2 → 2.3.1-beta.21
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 +39 -0
- package/README.md +2 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +13 -5
- package/docs/integrations.md +4 -1
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +109 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/main.dart +23057 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_integrations.dart +931 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3473 -0
- package/flutter_app/lib/main_shared.dart +2861 -0
- package/flutter_app/lib/main_theme.dart +204 -0
- package/flutter_app/lib/main_voice_assistant.dart +831 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1833 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1088 -0
- package/flutter_app/pubspec.yaml +53 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/manager.js +231 -7
- package/package.json +3 -1
- package/server/db/database.js +68 -0
- package/server/http/middleware.js +50 -0
- package/server/http/routes.js +3 -1
- package/server/index.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +61 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65262 -64422
- package/server/routes/integrations.js +86 -0
- package/server/routes/memory.js +11 -2
- package/server/routes/screenHistory.js +46 -0
- package/server/routes/triggers.js +81 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +35 -6
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +172 -0
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/home_assistant/provider.js +306 -26
- package/server/services/integrations/manager.js +63 -7
- package/server/services/integrations/oauth_provider.js +13 -6
- package/server/services/integrations/provider_config_store.js +76 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/trello/provider.js +744 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/memory/manager.js +39 -2
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/providers.js +2 -1
- package/server/services/widgets/service.js +49 -4
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -0,0 +1,2861 @@
|
|
|
1
|
+
part of 'main.dart';
|
|
2
|
+
|
|
3
|
+
EdgeInsets _pagePadding(BuildContext context) {
|
|
4
|
+
final width = MediaQuery.sizeOf(context).width;
|
|
5
|
+
if (width >= 1280) {
|
|
6
|
+
return const EdgeInsets.fromLTRB(40, 34, 40, 40);
|
|
7
|
+
}
|
|
8
|
+
if (width >= 900) {
|
|
9
|
+
return const EdgeInsets.fromLTRB(30, 28, 30, 32);
|
|
10
|
+
}
|
|
11
|
+
return const EdgeInsets.fromLTRB(20, 20, 20, 28);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class _AmbientBackdrop extends StatelessWidget {
|
|
15
|
+
const _AmbientBackdrop({required this.child});
|
|
16
|
+
|
|
17
|
+
final Widget child;
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
Widget build(BuildContext context) {
|
|
21
|
+
return DecoratedBox(
|
|
22
|
+
decoration: BoxDecoration(gradient: _appBackgroundGradient),
|
|
23
|
+
child: Stack(
|
|
24
|
+
children: <Widget>[
|
|
25
|
+
Positioned(
|
|
26
|
+
top: -120,
|
|
27
|
+
left: -90,
|
|
28
|
+
child: _BlurOrb(size: 340, color: _accent.withValues(alpha: 0.9)),
|
|
29
|
+
),
|
|
30
|
+
Positioned(
|
|
31
|
+
top: 90,
|
|
32
|
+
right: -120,
|
|
33
|
+
child: _BlurOrb(
|
|
34
|
+
size: 280,
|
|
35
|
+
color: _accentAlt.withValues(alpha: 0.85),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
Positioned(
|
|
39
|
+
bottom: -140,
|
|
40
|
+
left: 100,
|
|
41
|
+
child: _BlurOrb(size: 360, color: _accent.withValues(alpha: 0.45)),
|
|
42
|
+
),
|
|
43
|
+
Positioned.fill(
|
|
44
|
+
child: IgnorePointer(
|
|
45
|
+
child: DecoratedBox(
|
|
46
|
+
decoration: BoxDecoration(
|
|
47
|
+
gradient: LinearGradient(
|
|
48
|
+
colors: <Color>[
|
|
49
|
+
Colors.white.withValues(alpha: 0.02),
|
|
50
|
+
Colors.transparent,
|
|
51
|
+
Colors.black.withValues(alpha: 0.08),
|
|
52
|
+
],
|
|
53
|
+
begin: Alignment.topCenter,
|
|
54
|
+
end: Alignment.bottomCenter,
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
child,
|
|
61
|
+
],
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
List<AppSection> _mainSections(NeoAgentController controller) {
|
|
68
|
+
return <AppSection>[
|
|
69
|
+
AppSection.chat,
|
|
70
|
+
AppSection.recordings,
|
|
71
|
+
AppSection.runs,
|
|
72
|
+
AppSection.logs,
|
|
73
|
+
AppSection.devices,
|
|
74
|
+
AppSection.tasks,
|
|
75
|
+
AppSection.widgets,
|
|
76
|
+
AppSection.skills,
|
|
77
|
+
AppSection.integrations,
|
|
78
|
+
AppSection.mcp,
|
|
79
|
+
AppSection.memory,
|
|
80
|
+
if (controller.showHealthSection) AppSection.health,
|
|
81
|
+
AppSection.settings,
|
|
82
|
+
AppSection.agents,
|
|
83
|
+
AppSection.messaging,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
List<Widget> _buildSidebarItems(
|
|
88
|
+
NeoAgentController controller, {
|
|
89
|
+
required ValueChanged<AppSection> onSelect,
|
|
90
|
+
required SidebarGroup? expandedGroup,
|
|
91
|
+
required ValueChanged<SidebarGroup> onToggleGroup,
|
|
92
|
+
}) {
|
|
93
|
+
final widgets = <Widget>[];
|
|
94
|
+
final mainSections = _mainSections(controller);
|
|
95
|
+
final selectedSidebarSection = mainSections.contains(
|
|
96
|
+
controller.selectedSection,
|
|
97
|
+
);
|
|
98
|
+
for (final group in SidebarGroup.values) {
|
|
99
|
+
final sections = mainSections
|
|
100
|
+
.where((section) => section.group == group)
|
|
101
|
+
.toList();
|
|
102
|
+
if (sections.isEmpty) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
final active =
|
|
107
|
+
selectedSidebarSection && controller.selectedSection.group == group;
|
|
108
|
+
final defaultSection = sections.first;
|
|
109
|
+
final hasChildren = sections.length > 1;
|
|
110
|
+
final expanded = expandedGroup == group;
|
|
111
|
+
|
|
112
|
+
widgets.add(
|
|
113
|
+
_SidebarButton(
|
|
114
|
+
label: group.label,
|
|
115
|
+
icon: group.icon,
|
|
116
|
+
active: active,
|
|
117
|
+
trailing: hasChildren
|
|
118
|
+
? Icon(
|
|
119
|
+
expanded ? Icons.expand_less : Icons.expand_more,
|
|
120
|
+
size: 16,
|
|
121
|
+
color: active ? _accent : _textMuted,
|
|
122
|
+
)
|
|
123
|
+
: null,
|
|
124
|
+
onTap: hasChildren
|
|
125
|
+
? () => onToggleGroup(group)
|
|
126
|
+
: () => onSelect(defaultSection),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!hasChildren || !expanded) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (final section in sections) {
|
|
135
|
+
widgets.add(
|
|
136
|
+
_SidebarButton(
|
|
137
|
+
label: section.label,
|
|
138
|
+
icon: section.icon,
|
|
139
|
+
active: controller.selectedSection == section,
|
|
140
|
+
indent: 18,
|
|
141
|
+
iconSize: 16,
|
|
142
|
+
fontSize: 12,
|
|
143
|
+
onTap: () => onSelect(section),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return widgets;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Future<void> _confirmDelete(
|
|
152
|
+
BuildContext context, {
|
|
153
|
+
required String title,
|
|
154
|
+
required String message,
|
|
155
|
+
required Future<void> Function() onConfirm,
|
|
156
|
+
String confirmLabel = 'Delete',
|
|
157
|
+
}) async {
|
|
158
|
+
final confirmed = await showDialog<bool>(
|
|
159
|
+
context: context,
|
|
160
|
+
builder: (context) {
|
|
161
|
+
return AlertDialog(
|
|
162
|
+
backgroundColor: _bgCard,
|
|
163
|
+
title: Text(title),
|
|
164
|
+
content: Text(message),
|
|
165
|
+
actions: <Widget>[
|
|
166
|
+
TextButton(
|
|
167
|
+
onPressed: () => Navigator.of(context).pop(false),
|
|
168
|
+
child: Text('Cancel'),
|
|
169
|
+
),
|
|
170
|
+
FilledButton(
|
|
171
|
+
onPressed: () => Navigator.of(context).pop(true),
|
|
172
|
+
child: Text(confirmLabel),
|
|
173
|
+
),
|
|
174
|
+
],
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
if (confirmed == true) {
|
|
179
|
+
await onConfirm();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Widget _buildHealthSummaryPills(Map<String, dynamic> summary) {
|
|
184
|
+
return Wrap(
|
|
185
|
+
spacing: 10,
|
|
186
|
+
runSpacing: 10,
|
|
187
|
+
children: <Widget>[
|
|
188
|
+
_MetaPill(
|
|
189
|
+
icon: Icons.directions_walk_outlined,
|
|
190
|
+
label: 'Steps ${_asInt(summary['stepsTotal'])}',
|
|
191
|
+
),
|
|
192
|
+
_MetaPill(
|
|
193
|
+
icon: Icons.favorite_outline,
|
|
194
|
+
label: 'Heart ${_asInt(summary['heartRateRecordCount'])} records',
|
|
195
|
+
),
|
|
196
|
+
_MetaPill(
|
|
197
|
+
icon: Icons.bedtime_outlined,
|
|
198
|
+
label: 'Sleep ${_asInt(summary['sleepSessionCount'])} sessions',
|
|
199
|
+
),
|
|
200
|
+
_MetaPill(
|
|
201
|
+
icon: Icons.fitness_center_outlined,
|
|
202
|
+
label: 'Exercise ${_asInt(summary['exerciseSessionCount'])} sessions',
|
|
203
|
+
),
|
|
204
|
+
_MetaPill(
|
|
205
|
+
icon: Icons.monitor_weight_outlined,
|
|
206
|
+
label: 'Weight ${_asInt(summary['weightRecordCount'])} records',
|
|
207
|
+
),
|
|
208
|
+
],
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class _PageTitle extends StatelessWidget {
|
|
213
|
+
const _PageTitle({
|
|
214
|
+
required this.title,
|
|
215
|
+
required this.subtitle,
|
|
216
|
+
this.trailing,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
final String title;
|
|
220
|
+
final String subtitle;
|
|
221
|
+
final Widget? trailing;
|
|
222
|
+
|
|
223
|
+
@override
|
|
224
|
+
Widget build(BuildContext context) {
|
|
225
|
+
final compact = MediaQuery.sizeOf(context).width < 760;
|
|
226
|
+
return Padding(
|
|
227
|
+
padding: const EdgeInsets.only(bottom: 24),
|
|
228
|
+
child: compact
|
|
229
|
+
? Column(
|
|
230
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
231
|
+
children: <Widget>[
|
|
232
|
+
Text('CONTROL SURFACE', style: _sectionEyebrowStyle()),
|
|
233
|
+
const SizedBox(height: 8),
|
|
234
|
+
Text(title, style: _displayTitleStyle(30)),
|
|
235
|
+
const SizedBox(height: 10),
|
|
236
|
+
ConstrainedBox(
|
|
237
|
+
constraints: const BoxConstraints(maxWidth: 720),
|
|
238
|
+
child: Text(
|
|
239
|
+
subtitle,
|
|
240
|
+
style: TextStyle(color: _textSecondary, height: 1.5),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
if (trailing != null) ...<Widget>[
|
|
244
|
+
const SizedBox(height: 18),
|
|
245
|
+
trailing!,
|
|
246
|
+
],
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
: Row(
|
|
250
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
251
|
+
children: <Widget>[
|
|
252
|
+
Expanded(
|
|
253
|
+
child: Column(
|
|
254
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
255
|
+
children: <Widget>[
|
|
256
|
+
Text('CONTROL SURFACE', style: _sectionEyebrowStyle()),
|
|
257
|
+
const SizedBox(height: 8),
|
|
258
|
+
Text(title, style: _displayTitleStyle(32)),
|
|
259
|
+
const SizedBox(height: 10),
|
|
260
|
+
ConstrainedBox(
|
|
261
|
+
constraints: const BoxConstraints(maxWidth: 760),
|
|
262
|
+
child: Text(
|
|
263
|
+
subtitle,
|
|
264
|
+
style: TextStyle(color: _textSecondary, height: 1.5),
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
],
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
if (trailing != null) trailing!,
|
|
271
|
+
],
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
class _RunStatusPanel extends StatelessWidget {
|
|
278
|
+
const _RunStatusPanel({required this.run, required this.tools});
|
|
279
|
+
|
|
280
|
+
final ActiveRunState? run;
|
|
281
|
+
final List<ToolEventItem> tools;
|
|
282
|
+
|
|
283
|
+
@override
|
|
284
|
+
Widget build(BuildContext context) {
|
|
285
|
+
final runningCount = tools.where((tool) => tool.status == 'running').length;
|
|
286
|
+
final helperCount = tools.where((tool) => tool.isHelperRelated).length;
|
|
287
|
+
final webCount = tools.where((tool) => tool.isWebRelated).length;
|
|
288
|
+
|
|
289
|
+
return Card(
|
|
290
|
+
child: Padding(
|
|
291
|
+
padding: const EdgeInsets.all(18),
|
|
292
|
+
child: Column(
|
|
293
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
294
|
+
children: <Widget>[
|
|
295
|
+
Row(
|
|
296
|
+
children: <Widget>[
|
|
297
|
+
Expanded(
|
|
298
|
+
child: Column(
|
|
299
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
300
|
+
children: <Widget>[
|
|
301
|
+
Text(
|
|
302
|
+
run?.title ?? 'Live run',
|
|
303
|
+
style: TextStyle(
|
|
304
|
+
fontSize: 16,
|
|
305
|
+
fontWeight: FontWeight.w700,
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
const SizedBox(height: 6),
|
|
309
|
+
Text(
|
|
310
|
+
run == null
|
|
311
|
+
? 'Waiting for run events...'
|
|
312
|
+
: [
|
|
313
|
+
'${run!.phase}${run!.iteration > 0 ? ' · step ${run!.iteration}' : ''}',
|
|
314
|
+
if (run!.pendingSteeringCount > 0)
|
|
315
|
+
'${run!.pendingSteeringCount} steering ${run!.pendingSteeringCount == 1 ? 'update' : 'updates'} queued',
|
|
316
|
+
].join(' · '),
|
|
317
|
+
style: TextStyle(color: _textSecondary),
|
|
318
|
+
),
|
|
319
|
+
],
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
if (run != null && run!.model.isNotEmpty)
|
|
323
|
+
_MetaPill(label: run!.model, icon: Icons.memory_outlined),
|
|
324
|
+
],
|
|
325
|
+
),
|
|
326
|
+
if (tools.isNotEmpty) ...<Widget>[
|
|
327
|
+
const SizedBox(height: 14),
|
|
328
|
+
Wrap(
|
|
329
|
+
spacing: 10,
|
|
330
|
+
runSpacing: 10,
|
|
331
|
+
children: <Widget>[
|
|
332
|
+
_MetaPill(
|
|
333
|
+
label: '${tools.length} events',
|
|
334
|
+
icon: Icons.timeline_outlined,
|
|
335
|
+
),
|
|
336
|
+
if (runningCount > 0)
|
|
337
|
+
_MetaPill(
|
|
338
|
+
label: '$runningCount active',
|
|
339
|
+
icon: Icons.sync_outlined,
|
|
340
|
+
color: _warning,
|
|
341
|
+
),
|
|
342
|
+
if (webCount > 0)
|
|
343
|
+
_MetaPill(
|
|
344
|
+
label: '$webCount web',
|
|
345
|
+
icon: Icons.language_outlined,
|
|
346
|
+
),
|
|
347
|
+
if (helperCount > 0)
|
|
348
|
+
_MetaPill(
|
|
349
|
+
label: '$helperCount helpers',
|
|
350
|
+
icon: Icons.account_tree_outlined,
|
|
351
|
+
),
|
|
352
|
+
],
|
|
353
|
+
),
|
|
354
|
+
const SizedBox(height: 14),
|
|
355
|
+
...tools.asMap().entries.map(
|
|
356
|
+
(entry) => Padding(
|
|
357
|
+
padding: EdgeInsets.only(
|
|
358
|
+
bottom: entry.key == tools.length - 1 ? 0 : 12,
|
|
359
|
+
),
|
|
360
|
+
child: _ToolEventTimelineRow(
|
|
361
|
+
tool: entry.value,
|
|
362
|
+
isLast: entry.key == tools.length - 1,
|
|
363
|
+
),
|
|
364
|
+
),
|
|
365
|
+
),
|
|
366
|
+
] else
|
|
367
|
+
Text(
|
|
368
|
+
'Waiting for task events...',
|
|
369
|
+
style: TextStyle(color: _textSecondary),
|
|
370
|
+
),
|
|
371
|
+
],
|
|
372
|
+
),
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
class _ToolEventTimelineRow extends StatelessWidget {
|
|
379
|
+
const _ToolEventTimelineRow({required this.tool, required this.isLast});
|
|
380
|
+
|
|
381
|
+
final ToolEventItem tool;
|
|
382
|
+
final bool isLast;
|
|
383
|
+
|
|
384
|
+
@override
|
|
385
|
+
Widget build(BuildContext context) {
|
|
386
|
+
Color color;
|
|
387
|
+
switch (tool.status) {
|
|
388
|
+
case 'running':
|
|
389
|
+
color = _warning;
|
|
390
|
+
break;
|
|
391
|
+
case 'failed':
|
|
392
|
+
color = _danger;
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
color = _success;
|
|
396
|
+
}
|
|
397
|
+
return Row(
|
|
398
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
399
|
+
children: <Widget>[
|
|
400
|
+
SizedBox(
|
|
401
|
+
width: 28,
|
|
402
|
+
child: Column(
|
|
403
|
+
children: <Widget>[
|
|
404
|
+
Container(
|
|
405
|
+
width: 28,
|
|
406
|
+
height: 28,
|
|
407
|
+
decoration: BoxDecoration(
|
|
408
|
+
color: color.withValues(alpha: 0.14),
|
|
409
|
+
shape: BoxShape.circle,
|
|
410
|
+
),
|
|
411
|
+
child: Icon(tool.laneIcon, size: 16, color: color),
|
|
412
|
+
),
|
|
413
|
+
if (!isLast)
|
|
414
|
+
Container(
|
|
415
|
+
width: 2,
|
|
416
|
+
height: 62,
|
|
417
|
+
margin: const EdgeInsets.only(top: 6),
|
|
418
|
+
decoration: BoxDecoration(
|
|
419
|
+
color: _border,
|
|
420
|
+
borderRadius: BorderRadius.circular(999),
|
|
421
|
+
),
|
|
422
|
+
),
|
|
423
|
+
],
|
|
424
|
+
),
|
|
425
|
+
),
|
|
426
|
+
const SizedBox(width: 12),
|
|
427
|
+
Expanded(
|
|
428
|
+
child: Container(
|
|
429
|
+
padding: const EdgeInsets.all(12),
|
|
430
|
+
decoration: BoxDecoration(
|
|
431
|
+
color: _bgSecondary,
|
|
432
|
+
borderRadius: BorderRadius.circular(14),
|
|
433
|
+
border: Border.all(color: _border),
|
|
434
|
+
),
|
|
435
|
+
child: Column(
|
|
436
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
437
|
+
children: <Widget>[
|
|
438
|
+
Row(
|
|
439
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
440
|
+
children: <Widget>[
|
|
441
|
+
Expanded(
|
|
442
|
+
child: Column(
|
|
443
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
444
|
+
children: <Widget>[
|
|
445
|
+
Text(
|
|
446
|
+
tool.toolName,
|
|
447
|
+
style: TextStyle(fontWeight: FontWeight.w700),
|
|
448
|
+
),
|
|
449
|
+
const SizedBox(height: 4),
|
|
450
|
+
Text(
|
|
451
|
+
tool.laneLabel,
|
|
452
|
+
style: TextStyle(
|
|
453
|
+
color: _textSecondary,
|
|
454
|
+
fontSize: 12,
|
|
455
|
+
fontWeight: FontWeight.w600,
|
|
456
|
+
),
|
|
457
|
+
),
|
|
458
|
+
],
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
_StatusPill(label: tool.statusLabel, color: color),
|
|
462
|
+
],
|
|
463
|
+
),
|
|
464
|
+
if (tool.summary.isNotEmpty) ...<Widget>[
|
|
465
|
+
const SizedBox(height: 8),
|
|
466
|
+
Text(
|
|
467
|
+
tool.compactSummary,
|
|
468
|
+
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
469
|
+
),
|
|
470
|
+
],
|
|
471
|
+
],
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
),
|
|
475
|
+
],
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
class _SettingToggle extends StatelessWidget {
|
|
481
|
+
const _SettingToggle({
|
|
482
|
+
required this.title,
|
|
483
|
+
required this.subtitle,
|
|
484
|
+
required this.value,
|
|
485
|
+
required this.onChanged,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
final String title;
|
|
489
|
+
final String subtitle;
|
|
490
|
+
final bool value;
|
|
491
|
+
final ValueChanged<bool> onChanged;
|
|
492
|
+
|
|
493
|
+
@override
|
|
494
|
+
Widget build(BuildContext context) {
|
|
495
|
+
return SwitchListTile(
|
|
496
|
+
value: value,
|
|
497
|
+
contentPadding: EdgeInsets.zero,
|
|
498
|
+
title: Text(title),
|
|
499
|
+
subtitle: Text(subtitle),
|
|
500
|
+
onChanged: onChanged,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
class _OverviewCard extends StatelessWidget {
|
|
506
|
+
const _OverviewCard({
|
|
507
|
+
required this.title,
|
|
508
|
+
required this.value,
|
|
509
|
+
required this.helper,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
final String title;
|
|
513
|
+
final String value;
|
|
514
|
+
final String helper;
|
|
515
|
+
|
|
516
|
+
@override
|
|
517
|
+
Widget build(BuildContext context) {
|
|
518
|
+
return Card(
|
|
519
|
+
child: Padding(
|
|
520
|
+
padding: const EdgeInsets.all(22),
|
|
521
|
+
child: Column(
|
|
522
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
523
|
+
children: <Widget>[
|
|
524
|
+
Container(
|
|
525
|
+
width: 34,
|
|
526
|
+
height: 4,
|
|
527
|
+
decoration: BoxDecoration(
|
|
528
|
+
color: _accent,
|
|
529
|
+
borderRadius: BorderRadius.circular(999),
|
|
530
|
+
),
|
|
531
|
+
),
|
|
532
|
+
const SizedBox(height: 16),
|
|
533
|
+
Text(
|
|
534
|
+
title.toUpperCase(),
|
|
535
|
+
style: TextStyle(
|
|
536
|
+
color: _textSecondary,
|
|
537
|
+
fontSize: 11,
|
|
538
|
+
fontWeight: FontWeight.w700,
|
|
539
|
+
letterSpacing: 1.2,
|
|
540
|
+
),
|
|
541
|
+
),
|
|
542
|
+
const SizedBox(height: 10),
|
|
543
|
+
Text(value, style: _displayTitleStyle(28)),
|
|
544
|
+
const SizedBox(height: 12),
|
|
545
|
+
Text(helper, style: TextStyle(color: _textSecondary, height: 1.45)),
|
|
546
|
+
],
|
|
547
|
+
),
|
|
548
|
+
),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
class _EmptyCard extends StatelessWidget {
|
|
554
|
+
const _EmptyCard({required this.title, required this.subtitle});
|
|
555
|
+
|
|
556
|
+
final String title;
|
|
557
|
+
final String subtitle;
|
|
558
|
+
|
|
559
|
+
@override
|
|
560
|
+
Widget build(BuildContext context) {
|
|
561
|
+
return Card(
|
|
562
|
+
child: Padding(
|
|
563
|
+
padding: const EdgeInsets.all(34),
|
|
564
|
+
child: _EmptyState(title: title, subtitle: subtitle),
|
|
565
|
+
),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
class _SectionTitle extends StatelessWidget {
|
|
571
|
+
const _SectionTitle(this.text);
|
|
572
|
+
|
|
573
|
+
final String text;
|
|
574
|
+
|
|
575
|
+
@override
|
|
576
|
+
Widget build(BuildContext context) {
|
|
577
|
+
return Text(
|
|
578
|
+
text.toUpperCase(),
|
|
579
|
+
style: TextStyle(
|
|
580
|
+
fontSize: 12,
|
|
581
|
+
fontWeight: FontWeight.w800,
|
|
582
|
+
letterSpacing: 1.1,
|
|
583
|
+
color: _textSecondary,
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
class _DotStatus extends StatelessWidget {
|
|
590
|
+
const _DotStatus({required this.label, required this.color});
|
|
591
|
+
|
|
592
|
+
final String label;
|
|
593
|
+
final Color color;
|
|
594
|
+
|
|
595
|
+
@override
|
|
596
|
+
Widget build(BuildContext context) {
|
|
597
|
+
return Container(
|
|
598
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
599
|
+
decoration: BoxDecoration(
|
|
600
|
+
color: _bgSecondary,
|
|
601
|
+
borderRadius: BorderRadius.circular(999),
|
|
602
|
+
border: Border.all(color: _border),
|
|
603
|
+
),
|
|
604
|
+
child: Row(
|
|
605
|
+
mainAxisSize: MainAxisSize.min,
|
|
606
|
+
children: <Widget>[
|
|
607
|
+
Container(
|
|
608
|
+
width: 8,
|
|
609
|
+
height: 8,
|
|
610
|
+
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
611
|
+
),
|
|
612
|
+
const SizedBox(width: 8),
|
|
613
|
+
Text(label),
|
|
614
|
+
],
|
|
615
|
+
),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
class _SidebarButton extends StatelessWidget {
|
|
621
|
+
const _SidebarButton({
|
|
622
|
+
required this.label,
|
|
623
|
+
required this.icon,
|
|
624
|
+
this.active = false,
|
|
625
|
+
this.indent = 0,
|
|
626
|
+
this.iconSize = 18,
|
|
627
|
+
this.fontSize = 13,
|
|
628
|
+
this.trailing,
|
|
629
|
+
required this.onTap,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
final String label;
|
|
633
|
+
final IconData icon;
|
|
634
|
+
final bool active;
|
|
635
|
+
final double indent;
|
|
636
|
+
final double iconSize;
|
|
637
|
+
final double fontSize;
|
|
638
|
+
final Widget? trailing;
|
|
639
|
+
final VoidCallback? onTap;
|
|
640
|
+
|
|
641
|
+
@override
|
|
642
|
+
Widget build(BuildContext context) {
|
|
643
|
+
return Padding(
|
|
644
|
+
padding: const EdgeInsets.only(bottom: 6),
|
|
645
|
+
child: AnimatedContainer(
|
|
646
|
+
duration: const Duration(milliseconds: 180),
|
|
647
|
+
curve: Curves.easeOutCubic,
|
|
648
|
+
decoration: BoxDecoration(
|
|
649
|
+
gradient: active
|
|
650
|
+
? LinearGradient(
|
|
651
|
+
colors: <Color>[
|
|
652
|
+
_accentMuted,
|
|
653
|
+
_accentMuted.withValues(alpha: 0.06),
|
|
654
|
+
],
|
|
655
|
+
)
|
|
656
|
+
: null,
|
|
657
|
+
borderRadius: BorderRadius.circular(18),
|
|
658
|
+
border: Border.all(
|
|
659
|
+
color: active
|
|
660
|
+
? _accent.withValues(alpha: 0.35)
|
|
661
|
+
: Colors.transparent,
|
|
662
|
+
),
|
|
663
|
+
),
|
|
664
|
+
child: Material(
|
|
665
|
+
color: Colors.transparent,
|
|
666
|
+
child: InkWell(
|
|
667
|
+
borderRadius: BorderRadius.circular(18),
|
|
668
|
+
onTap: onTap,
|
|
669
|
+
child: Container(
|
|
670
|
+
width: double.infinity,
|
|
671
|
+
padding: EdgeInsets.fromLTRB(12 + indent, 12, 12, 12),
|
|
672
|
+
child: Row(
|
|
673
|
+
children: <Widget>[
|
|
674
|
+
if (active)
|
|
675
|
+
Container(
|
|
676
|
+
width: 6,
|
|
677
|
+
height: 26,
|
|
678
|
+
margin: const EdgeInsets.only(right: 10),
|
|
679
|
+
decoration: BoxDecoration(
|
|
680
|
+
color: _accent,
|
|
681
|
+
borderRadius: BorderRadius.circular(999),
|
|
682
|
+
),
|
|
683
|
+
),
|
|
684
|
+
Icon(
|
|
685
|
+
icon,
|
|
686
|
+
size: iconSize,
|
|
687
|
+
color: active ? _accentHover : _textSecondary,
|
|
688
|
+
),
|
|
689
|
+
const SizedBox(width: 10),
|
|
690
|
+
Expanded(
|
|
691
|
+
child: Text(
|
|
692
|
+
label,
|
|
693
|
+
style: TextStyle(
|
|
694
|
+
fontSize: fontSize,
|
|
695
|
+
fontWeight: active ? FontWeight.w700 : FontWeight.w600,
|
|
696
|
+
color: active ? _textPrimary : _textSecondary,
|
|
697
|
+
),
|
|
698
|
+
),
|
|
699
|
+
),
|
|
700
|
+
if (trailing != null) ...<Widget>[
|
|
701
|
+
const SizedBox(width: 8),
|
|
702
|
+
trailing!,
|
|
703
|
+
],
|
|
704
|
+
],
|
|
705
|
+
),
|
|
706
|
+
),
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
class _SidebarIconButton extends StatelessWidget {
|
|
715
|
+
const _SidebarIconButton({
|
|
716
|
+
required this.tooltip,
|
|
717
|
+
required this.icon,
|
|
718
|
+
required this.onTap,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
final String tooltip;
|
|
722
|
+
final IconData icon;
|
|
723
|
+
final VoidCallback? onTap;
|
|
724
|
+
|
|
725
|
+
@override
|
|
726
|
+
Widget build(BuildContext context) {
|
|
727
|
+
return Tooltip(
|
|
728
|
+
message: tooltip,
|
|
729
|
+
child: Material(
|
|
730
|
+
color: _bgCard.withValues(alpha: 0.8),
|
|
731
|
+
shape: CircleBorder(side: BorderSide(color: _borderLight)),
|
|
732
|
+
child: InkWell(
|
|
733
|
+
customBorder: CircleBorder(),
|
|
734
|
+
onTap: onTap,
|
|
735
|
+
child: SizedBox(
|
|
736
|
+
width: 38,
|
|
737
|
+
height: 38,
|
|
738
|
+
child: Icon(icon, size: 17, color: _textSecondary),
|
|
739
|
+
),
|
|
740
|
+
),
|
|
741
|
+
),
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
class _BlurOrb extends StatelessWidget {
|
|
747
|
+
const _BlurOrb({required this.size, required this.color});
|
|
748
|
+
|
|
749
|
+
final double size;
|
|
750
|
+
final Color color;
|
|
751
|
+
|
|
752
|
+
@override
|
|
753
|
+
Widget build(BuildContext context) {
|
|
754
|
+
return IgnorePointer(
|
|
755
|
+
child: Container(
|
|
756
|
+
width: size,
|
|
757
|
+
height: size,
|
|
758
|
+
decoration: BoxDecoration(
|
|
759
|
+
shape: BoxShape.circle,
|
|
760
|
+
boxShadow: <BoxShadow>[
|
|
761
|
+
BoxShadow(
|
|
762
|
+
color: color.withValues(alpha: 0.18),
|
|
763
|
+
blurRadius: 120,
|
|
764
|
+
spreadRadius: 30,
|
|
765
|
+
),
|
|
766
|
+
],
|
|
767
|
+
),
|
|
768
|
+
),
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
class _LogoBadge extends StatelessWidget {
|
|
774
|
+
const _LogoBadge({required this.size});
|
|
775
|
+
|
|
776
|
+
final double size;
|
|
777
|
+
|
|
778
|
+
@override
|
|
779
|
+
Widget build(BuildContext context) {
|
|
780
|
+
return Container(
|
|
781
|
+
width: size,
|
|
782
|
+
height: size,
|
|
783
|
+
decoration: BoxDecoration(
|
|
784
|
+
gradient: LinearGradient(
|
|
785
|
+
colors: <Color>[_brandAccent, _brandAccentAlt],
|
|
786
|
+
begin: Alignment.topLeft,
|
|
787
|
+
end: Alignment.bottomRight,
|
|
788
|
+
),
|
|
789
|
+
borderRadius: BorderRadius.circular(size * 0.34),
|
|
790
|
+
boxShadow: <BoxShadow>[
|
|
791
|
+
BoxShadow(
|
|
792
|
+
color: _brandAccent.withValues(alpha: 0.32),
|
|
793
|
+
blurRadius: 36,
|
|
794
|
+
offset: const Offset(0, 10),
|
|
795
|
+
),
|
|
796
|
+
],
|
|
797
|
+
border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
|
|
798
|
+
),
|
|
799
|
+
child: Padding(
|
|
800
|
+
padding: EdgeInsets.all(size * 0.18),
|
|
801
|
+
child: CustomPaint(painter: _NeoAgentLogoPainter()),
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
class _BrandLockup extends StatelessWidget {
|
|
808
|
+
const _BrandLockup({
|
|
809
|
+
required this.logoSize,
|
|
810
|
+
this.titleFontSize = 28,
|
|
811
|
+
this.direction = Axis.vertical,
|
|
812
|
+
this.spacing = 18,
|
|
813
|
+
this.alignment = CrossAxisAlignment.center,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
final double logoSize;
|
|
817
|
+
final double titleFontSize;
|
|
818
|
+
final Axis direction;
|
|
819
|
+
final double spacing;
|
|
820
|
+
final CrossAxisAlignment alignment;
|
|
821
|
+
|
|
822
|
+
@override
|
|
823
|
+
Widget build(BuildContext context) {
|
|
824
|
+
final title = Text(
|
|
825
|
+
'NeoOS',
|
|
826
|
+
style: GoogleFonts.spaceGrotesk(
|
|
827
|
+
fontSize: titleFontSize,
|
|
828
|
+
fontWeight: FontWeight.w700,
|
|
829
|
+
color: _textPrimary,
|
|
830
|
+
letterSpacing: -0.4,
|
|
831
|
+
),
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
if (direction == Axis.horizontal) {
|
|
835
|
+
return Row(
|
|
836
|
+
mainAxisSize: MainAxisSize.min,
|
|
837
|
+
children: <Widget>[
|
|
838
|
+
_LogoBadge(size: logoSize),
|
|
839
|
+
SizedBox(width: spacing),
|
|
840
|
+
Flexible(child: title),
|
|
841
|
+
],
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return Column(
|
|
846
|
+
mainAxisSize: MainAxisSize.min,
|
|
847
|
+
crossAxisAlignment: alignment,
|
|
848
|
+
children: <Widget>[
|
|
849
|
+
_LogoBadge(size: logoSize),
|
|
850
|
+
SizedBox(height: spacing),
|
|
851
|
+
title,
|
|
852
|
+
],
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
class _NeoAgentLogoPainter extends CustomPainter {
|
|
858
|
+
@override
|
|
859
|
+
void paint(Canvas canvas, Size size) {
|
|
860
|
+
final fillPaint = Paint()
|
|
861
|
+
..color = Colors.white
|
|
862
|
+
..style = PaintingStyle.fill;
|
|
863
|
+
final strokePaint = Paint()
|
|
864
|
+
..color = Colors.white
|
|
865
|
+
..style = PaintingStyle.stroke
|
|
866
|
+
..strokeWidth = size.width * 0.08
|
|
867
|
+
..strokeCap = StrokeCap.round
|
|
868
|
+
..strokeJoin = StrokeJoin.round;
|
|
869
|
+
|
|
870
|
+
final top = Path()
|
|
871
|
+
..moveTo(size.width * 0.5, size.height * 0.08)
|
|
872
|
+
..lineTo(size.width * 0.1, size.height * 0.3)
|
|
873
|
+
..lineTo(size.width * 0.5, size.height * 0.52)
|
|
874
|
+
..lineTo(size.width * 0.9, size.height * 0.3)
|
|
875
|
+
..close();
|
|
876
|
+
canvas.drawPath(top, fillPaint);
|
|
877
|
+
|
|
878
|
+
final middle = Path()
|
|
879
|
+
..moveTo(size.width * 0.1, size.height * 0.52)
|
|
880
|
+
..lineTo(size.width * 0.5, size.height * 0.74)
|
|
881
|
+
..lineTo(size.width * 0.9, size.height * 0.52);
|
|
882
|
+
canvas.drawPath(middle, strokePaint);
|
|
883
|
+
|
|
884
|
+
final bottom = Path()
|
|
885
|
+
..moveTo(size.width * 0.1, size.height * 0.72)
|
|
886
|
+
..lineTo(size.width * 0.5, size.height * 0.94)
|
|
887
|
+
..lineTo(size.width * 0.9, size.height * 0.72);
|
|
888
|
+
canvas.drawPath(bottom, strokePaint);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
@override
|
|
892
|
+
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
class _EmptyState extends StatelessWidget {
|
|
896
|
+
const _EmptyState({required this.title, required this.subtitle});
|
|
897
|
+
|
|
898
|
+
final String title;
|
|
899
|
+
final String subtitle;
|
|
900
|
+
|
|
901
|
+
@override
|
|
902
|
+
Widget build(BuildContext context) {
|
|
903
|
+
return Column(
|
|
904
|
+
mainAxisSize: MainAxisSize.min,
|
|
905
|
+
children: <Widget>[
|
|
906
|
+
const _LogoBadge(size: 52),
|
|
907
|
+
const SizedBox(height: 12),
|
|
908
|
+
Text(
|
|
909
|
+
title,
|
|
910
|
+
style: TextStyle(
|
|
911
|
+
fontSize: 17,
|
|
912
|
+
fontWeight: FontWeight.w600,
|
|
913
|
+
color: _textPrimary,
|
|
914
|
+
),
|
|
915
|
+
),
|
|
916
|
+
const SizedBox(height: 8),
|
|
917
|
+
ConstrainedBox(
|
|
918
|
+
constraints: const BoxConstraints(maxWidth: 360),
|
|
919
|
+
child: Text(
|
|
920
|
+
subtitle,
|
|
921
|
+
textAlign: TextAlign.center,
|
|
922
|
+
style: TextStyle(fontSize: 13, color: _textMuted),
|
|
923
|
+
),
|
|
924
|
+
),
|
|
925
|
+
],
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
class _ChatBubble extends StatelessWidget {
|
|
931
|
+
const _ChatBubble({required this.entry, this.onLoadRunDetail});
|
|
932
|
+
|
|
933
|
+
final ChatEntry entry;
|
|
934
|
+
final Future<RunDetailSnapshot> Function(String runId)? onLoadRunDetail;
|
|
935
|
+
|
|
936
|
+
@override
|
|
937
|
+
Widget build(BuildContext context) {
|
|
938
|
+
final isUser = entry.role == 'user';
|
|
939
|
+
final isTransient = entry.transient;
|
|
940
|
+
|
|
941
|
+
return Opacity(
|
|
942
|
+
opacity: isTransient ? 0.92 : 1,
|
|
943
|
+
child: Row(
|
|
944
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
945
|
+
mainAxisAlignment: isUser
|
|
946
|
+
? MainAxisAlignment.end
|
|
947
|
+
: MainAxisAlignment.start,
|
|
948
|
+
children: <Widget>[
|
|
949
|
+
if (!isUser) ...<Widget>[
|
|
950
|
+
const _MessageAvatar(assistant: true),
|
|
951
|
+
const SizedBox(width: 12),
|
|
952
|
+
],
|
|
953
|
+
Flexible(
|
|
954
|
+
child: Container(
|
|
955
|
+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
|
|
956
|
+
decoration: BoxDecoration(
|
|
957
|
+
color: isUser ? _accent : _bgCard,
|
|
958
|
+
borderRadius: BorderRadius.only(
|
|
959
|
+
topLeft: const Radius.circular(14),
|
|
960
|
+
topRight: const Radius.circular(14),
|
|
961
|
+
bottomLeft: Radius.circular(isUser ? 14 : 4),
|
|
962
|
+
bottomRight: Radius.circular(isUser ? 4 : 14),
|
|
963
|
+
),
|
|
964
|
+
border: isUser ? null : Border.all(color: _border),
|
|
965
|
+
boxShadow: isUser
|
|
966
|
+
? const <BoxShadow>[
|
|
967
|
+
BoxShadow(
|
|
968
|
+
color: Color(0x4D14B8A6),
|
|
969
|
+
blurRadius: 12,
|
|
970
|
+
offset: Offset(0, 2),
|
|
971
|
+
),
|
|
972
|
+
]
|
|
973
|
+
: null,
|
|
974
|
+
),
|
|
975
|
+
child: Column(
|
|
976
|
+
crossAxisAlignment: isUser
|
|
977
|
+
? CrossAxisAlignment.end
|
|
978
|
+
: CrossAxisAlignment.start,
|
|
979
|
+
children: <Widget>[
|
|
980
|
+
if (!isUser && entry.platformTag != null)
|
|
981
|
+
Padding(
|
|
982
|
+
padding: const EdgeInsets.only(bottom: 8),
|
|
983
|
+
child: _StatusPill(
|
|
984
|
+
label: entry.platformTag!,
|
|
985
|
+
color: entry.platform == 'live' ? _info : _warning,
|
|
986
|
+
),
|
|
987
|
+
),
|
|
988
|
+
MarkdownBody(
|
|
989
|
+
data: entry.content,
|
|
990
|
+
selectable: true,
|
|
991
|
+
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
|
|
992
|
+
.copyWith(
|
|
993
|
+
p: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
994
|
+
color: isUser ? Colors.white : _textPrimary,
|
|
995
|
+
height: 1.65,
|
|
996
|
+
),
|
|
997
|
+
code: Theme.of(context).textTheme.bodyMedium
|
|
998
|
+
?.copyWith(
|
|
999
|
+
fontFamily:
|
|
1000
|
+
GoogleFonts.jetBrainsMono().fontFamily,
|
|
1001
|
+
backgroundColor: _bgPrimary,
|
|
1002
|
+
color: isUser ? Colors.white : _textPrimary,
|
|
1003
|
+
),
|
|
1004
|
+
blockquoteDecoration: BoxDecoration(
|
|
1005
|
+
borderRadius: BorderRadius.circular(14),
|
|
1006
|
+
color: const Color(0x22000000),
|
|
1007
|
+
),
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
1010
|
+
if (!isUser &&
|
|
1011
|
+
entry.runId?.trim().isNotEmpty == true) ...<Widget>[
|
|
1012
|
+
const SizedBox(height: 12),
|
|
1013
|
+
_MessageRunPreview(
|
|
1014
|
+
runId: entry.runId!.trim(),
|
|
1015
|
+
onLoadRunDetail: onLoadRunDetail,
|
|
1016
|
+
),
|
|
1017
|
+
],
|
|
1018
|
+
const SizedBox(height: 10),
|
|
1019
|
+
Text(
|
|
1020
|
+
entry.createdAtLabel,
|
|
1021
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
1022
|
+
color: isUser ? const Color(0xCCFFFFFF) : _textSecondary,
|
|
1023
|
+
),
|
|
1024
|
+
),
|
|
1025
|
+
],
|
|
1026
|
+
),
|
|
1027
|
+
),
|
|
1028
|
+
),
|
|
1029
|
+
if (isUser) ...<Widget>[
|
|
1030
|
+
const SizedBox(width: 12),
|
|
1031
|
+
const _MessageAvatar(assistant: false),
|
|
1032
|
+
],
|
|
1033
|
+
],
|
|
1034
|
+
),
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
class _MessageRunPreview extends StatefulWidget {
|
|
1040
|
+
const _MessageRunPreview({
|
|
1041
|
+
required this.runId,
|
|
1042
|
+
required this.onLoadRunDetail,
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
final String runId;
|
|
1046
|
+
final Future<RunDetailSnapshot> Function(String runId)? onLoadRunDetail;
|
|
1047
|
+
|
|
1048
|
+
@override
|
|
1049
|
+
State<_MessageRunPreview> createState() => _MessageRunPreviewState();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
class _MessageRunPreviewState extends State<_MessageRunPreview> {
|
|
1053
|
+
late Future<RunDetailSnapshot>? _future;
|
|
1054
|
+
|
|
1055
|
+
@override
|
|
1056
|
+
void initState() {
|
|
1057
|
+
super.initState();
|
|
1058
|
+
_future = widget.onLoadRunDetail?.call(widget.runId);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
@override
|
|
1062
|
+
void didUpdateWidget(covariant _MessageRunPreview oldWidget) {
|
|
1063
|
+
super.didUpdateWidget(oldWidget);
|
|
1064
|
+
if (oldWidget.runId != widget.runId ||
|
|
1065
|
+
oldWidget.onLoadRunDetail != widget.onLoadRunDetail) {
|
|
1066
|
+
_future = widget.onLoadRunDetail?.call(widget.runId);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
@override
|
|
1071
|
+
Widget build(BuildContext context) {
|
|
1072
|
+
if (_future == null) {
|
|
1073
|
+
return const SizedBox.shrink();
|
|
1074
|
+
}
|
|
1075
|
+
return FutureBuilder<RunDetailSnapshot>(
|
|
1076
|
+
future: _future,
|
|
1077
|
+
builder: (context, snapshot) {
|
|
1078
|
+
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
1079
|
+
return _MessageRunCardShell(
|
|
1080
|
+
child: Row(
|
|
1081
|
+
children: <Widget>[
|
|
1082
|
+
SizedBox.square(
|
|
1083
|
+
dimension: 14,
|
|
1084
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
1085
|
+
),
|
|
1086
|
+
const SizedBox(width: 10),
|
|
1087
|
+
Expanded(
|
|
1088
|
+
child: Text(
|
|
1089
|
+
'Loading execution details...',
|
|
1090
|
+
style: TextStyle(color: _textSecondary, fontSize: 12),
|
|
1091
|
+
),
|
|
1092
|
+
),
|
|
1093
|
+
],
|
|
1094
|
+
),
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (snapshot.hasError || !snapshot.hasData) {
|
|
1098
|
+
return const SizedBox.shrink();
|
|
1099
|
+
}
|
|
1100
|
+
final detail = snapshot.data!;
|
|
1101
|
+
final previewSteps = detail.steps.take(4).toList();
|
|
1102
|
+
return _MessageRunCardShell(
|
|
1103
|
+
child: Column(
|
|
1104
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1105
|
+
children: <Widget>[
|
|
1106
|
+
Row(
|
|
1107
|
+
children: <Widget>[
|
|
1108
|
+
Expanded(
|
|
1109
|
+
child: Text(
|
|
1110
|
+
detail.run.title.ifEmpty('Execution'),
|
|
1111
|
+
style: TextStyle(
|
|
1112
|
+
fontWeight: FontWeight.w700,
|
|
1113
|
+
color: _textPrimary,
|
|
1114
|
+
),
|
|
1115
|
+
),
|
|
1116
|
+
),
|
|
1117
|
+
_StatusPill(
|
|
1118
|
+
label: detail.run.statusLabel,
|
|
1119
|
+
color: detail.run.statusColor,
|
|
1120
|
+
),
|
|
1121
|
+
],
|
|
1122
|
+
),
|
|
1123
|
+
const SizedBox(height: 10),
|
|
1124
|
+
Wrap(
|
|
1125
|
+
spacing: 8,
|
|
1126
|
+
runSpacing: 8,
|
|
1127
|
+
children: <Widget>[
|
|
1128
|
+
_MetaPill(
|
|
1129
|
+
label: '${detail.steps.length} steps',
|
|
1130
|
+
icon: Icons.timeline_outlined,
|
|
1131
|
+
),
|
|
1132
|
+
if (detail.webStepCount > 0)
|
|
1133
|
+
_MetaPill(
|
|
1134
|
+
label: '${detail.webStepCount} web',
|
|
1135
|
+
icon: Icons.language_outlined,
|
|
1136
|
+
),
|
|
1137
|
+
if (detail.helperCount > 0)
|
|
1138
|
+
_MetaPill(
|
|
1139
|
+
label: '${detail.helperCount} helpers',
|
|
1140
|
+
icon: Icons.account_tree_outlined,
|
|
1141
|
+
),
|
|
1142
|
+
if (detail.planningStepCount > 0)
|
|
1143
|
+
_MetaPill(
|
|
1144
|
+
label: '${detail.planningStepCount} planning',
|
|
1145
|
+
icon: Icons.route_outlined,
|
|
1146
|
+
),
|
|
1147
|
+
],
|
|
1148
|
+
),
|
|
1149
|
+
const SizedBox(height: 12),
|
|
1150
|
+
...previewSteps.asMap().entries.map(
|
|
1151
|
+
(entry) => Padding(
|
|
1152
|
+
padding: EdgeInsets.only(
|
|
1153
|
+
bottom: entry.key == previewSteps.length - 1 ? 0 : 10,
|
|
1154
|
+
),
|
|
1155
|
+
child: _MessageRunStepRow(
|
|
1156
|
+
step: entry.value,
|
|
1157
|
+
isLast: entry.key == previewSteps.length - 1,
|
|
1158
|
+
),
|
|
1159
|
+
),
|
|
1160
|
+
),
|
|
1161
|
+
if (detail.steps.length > previewSteps.length) ...<Widget>[
|
|
1162
|
+
const SizedBox(height: 10),
|
|
1163
|
+
Text(
|
|
1164
|
+
'${detail.steps.length - previewSteps.length} more steps in run history',
|
|
1165
|
+
style: TextStyle(
|
|
1166
|
+
color: _textSecondary,
|
|
1167
|
+
fontSize: 12,
|
|
1168
|
+
fontWeight: FontWeight.w600,
|
|
1169
|
+
),
|
|
1170
|
+
),
|
|
1171
|
+
],
|
|
1172
|
+
],
|
|
1173
|
+
),
|
|
1174
|
+
);
|
|
1175
|
+
},
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
class _MessageRunCardShell extends StatelessWidget {
|
|
1181
|
+
const _MessageRunCardShell({required this.child});
|
|
1182
|
+
|
|
1183
|
+
final Widget child;
|
|
1184
|
+
|
|
1185
|
+
@override
|
|
1186
|
+
Widget build(BuildContext context) {
|
|
1187
|
+
return Container(
|
|
1188
|
+
width: double.infinity,
|
|
1189
|
+
padding: const EdgeInsets.all(12),
|
|
1190
|
+
decoration: BoxDecoration(
|
|
1191
|
+
color: _bgPrimary,
|
|
1192
|
+
borderRadius: BorderRadius.circular(14),
|
|
1193
|
+
border: Border.all(color: _border),
|
|
1194
|
+
),
|
|
1195
|
+
child: child,
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
class _MessageRunStepRow extends StatelessWidget {
|
|
1201
|
+
const _MessageRunStepRow({required this.step, required this.isLast});
|
|
1202
|
+
|
|
1203
|
+
final RunStepItem step;
|
|
1204
|
+
final bool isLast;
|
|
1205
|
+
|
|
1206
|
+
@override
|
|
1207
|
+
Widget build(BuildContext context) {
|
|
1208
|
+
return Row(
|
|
1209
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1210
|
+
children: <Widget>[
|
|
1211
|
+
SizedBox(
|
|
1212
|
+
width: 24,
|
|
1213
|
+
child: Column(
|
|
1214
|
+
children: <Widget>[
|
|
1215
|
+
Container(
|
|
1216
|
+
width: 24,
|
|
1217
|
+
height: 24,
|
|
1218
|
+
decoration: BoxDecoration(
|
|
1219
|
+
color: step.statusColor.withValues(alpha: 0.14),
|
|
1220
|
+
shape: BoxShape.circle,
|
|
1221
|
+
),
|
|
1222
|
+
child: Icon(step.laneIcon, size: 14, color: step.statusColor),
|
|
1223
|
+
),
|
|
1224
|
+
if (!isLast)
|
|
1225
|
+
Container(
|
|
1226
|
+
width: 2,
|
|
1227
|
+
height: 34,
|
|
1228
|
+
margin: const EdgeInsets.only(top: 6),
|
|
1229
|
+
decoration: BoxDecoration(
|
|
1230
|
+
color: _border,
|
|
1231
|
+
borderRadius: BorderRadius.circular(999),
|
|
1232
|
+
),
|
|
1233
|
+
),
|
|
1234
|
+
],
|
|
1235
|
+
),
|
|
1236
|
+
),
|
|
1237
|
+
const SizedBox(width: 10),
|
|
1238
|
+
Expanded(
|
|
1239
|
+
child: Column(
|
|
1240
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1241
|
+
children: <Widget>[
|
|
1242
|
+
Row(
|
|
1243
|
+
children: <Widget>[
|
|
1244
|
+
Expanded(
|
|
1245
|
+
child: Text(
|
|
1246
|
+
step.label,
|
|
1247
|
+
style: TextStyle(
|
|
1248
|
+
fontWeight: FontWeight.w600,
|
|
1249
|
+
color: _textPrimary,
|
|
1250
|
+
),
|
|
1251
|
+
),
|
|
1252
|
+
),
|
|
1253
|
+
Text(
|
|
1254
|
+
step.laneLabel,
|
|
1255
|
+
style: TextStyle(
|
|
1256
|
+
color: _textSecondary,
|
|
1257
|
+
fontSize: 11,
|
|
1258
|
+
fontWeight: FontWeight.w700,
|
|
1259
|
+
),
|
|
1260
|
+
),
|
|
1261
|
+
],
|
|
1262
|
+
),
|
|
1263
|
+
const SizedBox(height: 3),
|
|
1264
|
+
Text(
|
|
1265
|
+
step.compactSummary,
|
|
1266
|
+
style: TextStyle(
|
|
1267
|
+
color: _textSecondary,
|
|
1268
|
+
fontSize: 12,
|
|
1269
|
+
height: 1.4,
|
|
1270
|
+
),
|
|
1271
|
+
),
|
|
1272
|
+
],
|
|
1273
|
+
),
|
|
1274
|
+
),
|
|
1275
|
+
],
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
class _MessageAvatar extends StatelessWidget {
|
|
1281
|
+
const _MessageAvatar({required this.assistant});
|
|
1282
|
+
|
|
1283
|
+
final bool assistant;
|
|
1284
|
+
|
|
1285
|
+
@override
|
|
1286
|
+
Widget build(BuildContext context) {
|
|
1287
|
+
return Container(
|
|
1288
|
+
width: 30,
|
|
1289
|
+
height: 30,
|
|
1290
|
+
decoration: BoxDecoration(
|
|
1291
|
+
borderRadius: BorderRadius.circular(8),
|
|
1292
|
+
gradient: assistant
|
|
1293
|
+
? LinearGradient(colors: <Color>[_accent, _accentAlt])
|
|
1294
|
+
: null,
|
|
1295
|
+
color: assistant ? null : _bgTertiary,
|
|
1296
|
+
boxShadow: assistant
|
|
1297
|
+
? const <BoxShadow>[
|
|
1298
|
+
BoxShadow(
|
|
1299
|
+
color: Color(0x5914B8A6),
|
|
1300
|
+
blurRadius: 10,
|
|
1301
|
+
offset: Offset(0, 2),
|
|
1302
|
+
),
|
|
1303
|
+
]
|
|
1304
|
+
: null,
|
|
1305
|
+
),
|
|
1306
|
+
child: Icon(
|
|
1307
|
+
assistant ? Icons.auto_awesome : Icons.person,
|
|
1308
|
+
size: 16,
|
|
1309
|
+
color: assistant ? Colors.white : _textSecondary,
|
|
1310
|
+
),
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
class _StatusPill extends StatelessWidget {
|
|
1316
|
+
const _StatusPill({required this.label, required this.color});
|
|
1317
|
+
|
|
1318
|
+
final String label;
|
|
1319
|
+
final Color color;
|
|
1320
|
+
|
|
1321
|
+
@override
|
|
1322
|
+
Widget build(BuildContext context) {
|
|
1323
|
+
return Container(
|
|
1324
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
1325
|
+
decoration: BoxDecoration(
|
|
1326
|
+
borderRadius: BorderRadius.circular(999),
|
|
1327
|
+
color: color.withValues(alpha: 0.12),
|
|
1328
|
+
),
|
|
1329
|
+
child: Text(
|
|
1330
|
+
label,
|
|
1331
|
+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
1332
|
+
color: color,
|
|
1333
|
+
fontWeight: FontWeight.w700,
|
|
1334
|
+
),
|
|
1335
|
+
),
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
class _MetaPill extends StatelessWidget {
|
|
1341
|
+
const _MetaPill({required this.label, required this.icon, this.color});
|
|
1342
|
+
|
|
1343
|
+
final String label;
|
|
1344
|
+
final IconData icon;
|
|
1345
|
+
final Color? color;
|
|
1346
|
+
|
|
1347
|
+
@override
|
|
1348
|
+
Widget build(BuildContext context) {
|
|
1349
|
+
final accentColor = color ?? const Color(0xFF5EEAD4);
|
|
1350
|
+
return Container(
|
|
1351
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
1352
|
+
decoration: BoxDecoration(
|
|
1353
|
+
color: _bgSecondary,
|
|
1354
|
+
borderRadius: BorderRadius.circular(999),
|
|
1355
|
+
border: Border.all(color: _border),
|
|
1356
|
+
),
|
|
1357
|
+
child: Row(
|
|
1358
|
+
mainAxisSize: MainAxisSize.min,
|
|
1359
|
+
children: <Widget>[
|
|
1360
|
+
Icon(icon, size: 14, color: accentColor),
|
|
1361
|
+
const SizedBox(width: 8),
|
|
1362
|
+
Flexible(
|
|
1363
|
+
child: Text(
|
|
1364
|
+
label,
|
|
1365
|
+
overflow: TextOverflow.ellipsis,
|
|
1366
|
+
softWrap: false,
|
|
1367
|
+
),
|
|
1368
|
+
),
|
|
1369
|
+
],
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
class _InfoChip extends StatelessWidget {
|
|
1376
|
+
const _InfoChip({required this.icon, required this.label});
|
|
1377
|
+
|
|
1378
|
+
final IconData icon;
|
|
1379
|
+
final String label;
|
|
1380
|
+
|
|
1381
|
+
@override
|
|
1382
|
+
Widget build(BuildContext context) {
|
|
1383
|
+
return Container(
|
|
1384
|
+
width: double.infinity,
|
|
1385
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
1386
|
+
decoration: BoxDecoration(
|
|
1387
|
+
color: Colors.white.withValues(alpha: 0.06),
|
|
1388
|
+
borderRadius: BorderRadius.circular(16),
|
|
1389
|
+
border: Border.all(color: Colors.white.withValues(alpha: 0.10)),
|
|
1390
|
+
),
|
|
1391
|
+
child: Row(
|
|
1392
|
+
children: <Widget>[
|
|
1393
|
+
Icon(icon, size: 16, color: Colors.white.withValues(alpha: 0.72)),
|
|
1394
|
+
const SizedBox(width: 8),
|
|
1395
|
+
Expanded(
|
|
1396
|
+
child: Text(
|
|
1397
|
+
label,
|
|
1398
|
+
style: TextStyle(
|
|
1399
|
+
color: Colors.white.withValues(alpha: 0.82),
|
|
1400
|
+
fontSize: 13,
|
|
1401
|
+
),
|
|
1402
|
+
),
|
|
1403
|
+
),
|
|
1404
|
+
],
|
|
1405
|
+
),
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
class _InlineError extends StatelessWidget {
|
|
1411
|
+
const _InlineError({required this.message});
|
|
1412
|
+
|
|
1413
|
+
final String message;
|
|
1414
|
+
|
|
1415
|
+
@override
|
|
1416
|
+
Widget build(BuildContext context) {
|
|
1417
|
+
return Container(
|
|
1418
|
+
width: double.infinity,
|
|
1419
|
+
padding: const EdgeInsets.all(12),
|
|
1420
|
+
decoration: BoxDecoration(
|
|
1421
|
+
color: const Color(0x19EF4444),
|
|
1422
|
+
borderRadius: BorderRadius.circular(8),
|
|
1423
|
+
border: Border.all(color: const Color(0x4CEF4444)),
|
|
1424
|
+
),
|
|
1425
|
+
child: Text(message, style: TextStyle(fontSize: 13)),
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
class _InlineSuccess extends StatelessWidget {
|
|
1431
|
+
const _InlineSuccess({required this.message});
|
|
1432
|
+
|
|
1433
|
+
final String message;
|
|
1434
|
+
|
|
1435
|
+
@override
|
|
1436
|
+
Widget build(BuildContext context) {
|
|
1437
|
+
return Container(
|
|
1438
|
+
width: double.infinity,
|
|
1439
|
+
padding: const EdgeInsets.all(12),
|
|
1440
|
+
decoration: BoxDecoration(
|
|
1441
|
+
color: _success.withValues(alpha: 0.10),
|
|
1442
|
+
borderRadius: BorderRadius.circular(8),
|
|
1443
|
+
border: Border.all(color: _success.withValues(alpha: 0.28)),
|
|
1444
|
+
),
|
|
1445
|
+
child: Row(
|
|
1446
|
+
children: <Widget>[
|
|
1447
|
+
Icon(Icons.check_circle_outline, color: _success, size: 18),
|
|
1448
|
+
const SizedBox(width: 8),
|
|
1449
|
+
Expanded(
|
|
1450
|
+
child: Text(
|
|
1451
|
+
message,
|
|
1452
|
+
style: TextStyle(color: _success, fontSize: 13),
|
|
1453
|
+
),
|
|
1454
|
+
),
|
|
1455
|
+
],
|
|
1456
|
+
),
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
class _GlobalNetworkBanner extends StatelessWidget {
|
|
1462
|
+
const _GlobalNetworkBanner({required this.controller});
|
|
1463
|
+
|
|
1464
|
+
final NeoAgentController controller;
|
|
1465
|
+
|
|
1466
|
+
@override
|
|
1467
|
+
Widget build(BuildContext context) {
|
|
1468
|
+
return LayoutBuilder(
|
|
1469
|
+
builder: (context, constraints) {
|
|
1470
|
+
final compact = constraints.maxWidth < 520;
|
|
1471
|
+
return Material(
|
|
1472
|
+
color: Colors.transparent,
|
|
1473
|
+
child: Container(
|
|
1474
|
+
width: double.infinity,
|
|
1475
|
+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
1476
|
+
decoration: BoxDecoration(
|
|
1477
|
+
color: _warning.withValues(alpha: 0.14),
|
|
1478
|
+
borderRadius: BorderRadius.circular(18),
|
|
1479
|
+
border: Border.all(color: _warning.withValues(alpha: 0.32)),
|
|
1480
|
+
boxShadow: <BoxShadow>[
|
|
1481
|
+
BoxShadow(
|
|
1482
|
+
color: Colors.black.withValues(alpha: 0.12),
|
|
1483
|
+
blurRadius: 18,
|
|
1484
|
+
offset: const Offset(0, 10),
|
|
1485
|
+
),
|
|
1486
|
+
],
|
|
1487
|
+
),
|
|
1488
|
+
child: compact
|
|
1489
|
+
? Column(
|
|
1490
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1491
|
+
children: <Widget>[
|
|
1492
|
+
Row(
|
|
1493
|
+
children: <Widget>[
|
|
1494
|
+
Icon(
|
|
1495
|
+
Icons.cloud_off_outlined,
|
|
1496
|
+
color: _warning,
|
|
1497
|
+
size: 18,
|
|
1498
|
+
),
|
|
1499
|
+
const SizedBox(width: 10),
|
|
1500
|
+
Expanded(
|
|
1501
|
+
child: Text(
|
|
1502
|
+
controller.offlineBannerMessage,
|
|
1503
|
+
style: TextStyle(
|
|
1504
|
+
color: _textPrimary,
|
|
1505
|
+
height: 1.35,
|
|
1506
|
+
),
|
|
1507
|
+
),
|
|
1508
|
+
),
|
|
1509
|
+
],
|
|
1510
|
+
),
|
|
1511
|
+
const SizedBox(height: 10),
|
|
1512
|
+
OutlinedButton(
|
|
1513
|
+
onPressed: controller.refreshConnectivityStatus,
|
|
1514
|
+
style: OutlinedButton.styleFrom(
|
|
1515
|
+
foregroundColor: _textPrimary,
|
|
1516
|
+
side: BorderSide(
|
|
1517
|
+
color: _warning.withValues(alpha: 0.38),
|
|
1518
|
+
),
|
|
1519
|
+
),
|
|
1520
|
+
child: const Text('Retry'),
|
|
1521
|
+
),
|
|
1522
|
+
],
|
|
1523
|
+
)
|
|
1524
|
+
: Row(
|
|
1525
|
+
children: <Widget>[
|
|
1526
|
+
Icon(Icons.cloud_off_outlined, color: _warning, size: 18),
|
|
1527
|
+
const SizedBox(width: 10),
|
|
1528
|
+
Expanded(
|
|
1529
|
+
child: Text(
|
|
1530
|
+
controller.offlineBannerMessage,
|
|
1531
|
+
style: TextStyle(color: _textPrimary, height: 1.35),
|
|
1532
|
+
),
|
|
1533
|
+
),
|
|
1534
|
+
const SizedBox(width: 12),
|
|
1535
|
+
OutlinedButton(
|
|
1536
|
+
onPressed: controller.refreshConnectivityStatus,
|
|
1537
|
+
style: OutlinedButton.styleFrom(
|
|
1538
|
+
foregroundColor: _textPrimary,
|
|
1539
|
+
side: BorderSide(
|
|
1540
|
+
color: _warning.withValues(alpha: 0.38),
|
|
1541
|
+
),
|
|
1542
|
+
),
|
|
1543
|
+
child: const Text('Retry'),
|
|
1544
|
+
),
|
|
1545
|
+
],
|
|
1546
|
+
),
|
|
1547
|
+
),
|
|
1548
|
+
);
|
|
1549
|
+
},
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
class _DesktopCloseDecision {
|
|
1555
|
+
const _DesktopCloseDecision({
|
|
1556
|
+
required this.keepRunning,
|
|
1557
|
+
required this.rememberChoice,
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
final bool keepRunning;
|
|
1561
|
+
final bool rememberChoice;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
class _RecordingPermissionBadge extends StatelessWidget {
|
|
1565
|
+
const _RecordingPermissionBadge({required this.label, required this.state});
|
|
1566
|
+
|
|
1567
|
+
final String label;
|
|
1568
|
+
final RecordingPermissionState state;
|
|
1569
|
+
|
|
1570
|
+
@override
|
|
1571
|
+
Widget build(BuildContext context) {
|
|
1572
|
+
final (color, icon, text) = switch (state) {
|
|
1573
|
+
RecordingPermissionState.granted => (
|
|
1574
|
+
_success,
|
|
1575
|
+
Icons.check_circle,
|
|
1576
|
+
'Ready',
|
|
1577
|
+
),
|
|
1578
|
+
RecordingPermissionState.denied => (
|
|
1579
|
+
_danger,
|
|
1580
|
+
Icons.lock_outline,
|
|
1581
|
+
'Blocked',
|
|
1582
|
+
),
|
|
1583
|
+
RecordingPermissionState.needsRestart => (
|
|
1584
|
+
_warning,
|
|
1585
|
+
Icons.restart_alt_rounded,
|
|
1586
|
+
'Restart needed',
|
|
1587
|
+
),
|
|
1588
|
+
RecordingPermissionState.unsupported => (
|
|
1589
|
+
_textSecondary,
|
|
1590
|
+
Icons.do_not_disturb_alt_outlined,
|
|
1591
|
+
'Unsupported',
|
|
1592
|
+
),
|
|
1593
|
+
RecordingPermissionState.unknown => (
|
|
1594
|
+
_warning,
|
|
1595
|
+
Icons.help_outline,
|
|
1596
|
+
'Check access',
|
|
1597
|
+
),
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
return Container(
|
|
1601
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
1602
|
+
decoration: BoxDecoration(
|
|
1603
|
+
color: color.withValues(alpha: 0.10),
|
|
1604
|
+
borderRadius: BorderRadius.circular(16),
|
|
1605
|
+
border: Border.all(color: color.withValues(alpha: 0.20)),
|
|
1606
|
+
),
|
|
1607
|
+
child: Row(
|
|
1608
|
+
mainAxisSize: MainAxisSize.min,
|
|
1609
|
+
children: <Widget>[
|
|
1610
|
+
Icon(icon, size: 16, color: color),
|
|
1611
|
+
const SizedBox(width: 8),
|
|
1612
|
+
Text(
|
|
1613
|
+
'$label · $text',
|
|
1614
|
+
style: TextStyle(color: color, fontWeight: FontWeight.w700),
|
|
1615
|
+
),
|
|
1616
|
+
],
|
|
1617
|
+
),
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
class _CompanionPermissionBadge extends StatelessWidget {
|
|
1623
|
+
const _CompanionPermissionBadge({required this.label, required this.state});
|
|
1624
|
+
|
|
1625
|
+
final String label;
|
|
1626
|
+
final String state;
|
|
1627
|
+
|
|
1628
|
+
@override
|
|
1629
|
+
Widget build(BuildContext context) {
|
|
1630
|
+
final normalized = state.trim().toLowerCase();
|
|
1631
|
+
final (color, icon, text) = switch (normalized) {
|
|
1632
|
+
'available' => (_success, Icons.check_circle, 'Granted'),
|
|
1633
|
+
'required' => (_warning, Icons.lock_outline, 'Needs access'),
|
|
1634
|
+
'unsupported' => (
|
|
1635
|
+
_textSecondary,
|
|
1636
|
+
Icons.do_not_disturb_alt_outlined,
|
|
1637
|
+
'Unsupported',
|
|
1638
|
+
),
|
|
1639
|
+
_ => (_warning, Icons.help_outline, 'Unknown'),
|
|
1640
|
+
};
|
|
1641
|
+
return Container(
|
|
1642
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
1643
|
+
decoration: BoxDecoration(
|
|
1644
|
+
color: color.withValues(alpha: 0.10),
|
|
1645
|
+
borderRadius: BorderRadius.circular(16),
|
|
1646
|
+
border: Border.all(color: color.withValues(alpha: 0.20)),
|
|
1647
|
+
),
|
|
1648
|
+
child: Row(
|
|
1649
|
+
mainAxisSize: MainAxisSize.min,
|
|
1650
|
+
children: <Widget>[
|
|
1651
|
+
Icon(icon, size: 16, color: color),
|
|
1652
|
+
const SizedBox(width: 8),
|
|
1653
|
+
Text(
|
|
1654
|
+
'$label · $text',
|
|
1655
|
+
style: TextStyle(color: color, fontWeight: FontWeight.w700),
|
|
1656
|
+
),
|
|
1657
|
+
],
|
|
1658
|
+
),
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
class _AudioLevelBar extends StatelessWidget {
|
|
1664
|
+
const _AudioLevelBar({
|
|
1665
|
+
required this.label,
|
|
1666
|
+
required this.valueDb,
|
|
1667
|
+
required this.color,
|
|
1668
|
+
this.compact = false,
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
final String label;
|
|
1672
|
+
final double valueDb;
|
|
1673
|
+
final Color color;
|
|
1674
|
+
final bool compact;
|
|
1675
|
+
|
|
1676
|
+
@override
|
|
1677
|
+
Widget build(BuildContext context) {
|
|
1678
|
+
final progress = ((valueDb + 72) / 72).clamp(0.0, 1.0);
|
|
1679
|
+
return SizedBox(
|
|
1680
|
+
width: compact ? 148 : 220,
|
|
1681
|
+
child: Column(
|
|
1682
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1683
|
+
children: <Widget>[
|
|
1684
|
+
Row(
|
|
1685
|
+
children: <Widget>[
|
|
1686
|
+
Text(
|
|
1687
|
+
label,
|
|
1688
|
+
style: TextStyle(
|
|
1689
|
+
color: _textSecondary,
|
|
1690
|
+
fontSize: compact ? 11 : 12,
|
|
1691
|
+
fontWeight: FontWeight.w700,
|
|
1692
|
+
letterSpacing: 0.3,
|
|
1693
|
+
),
|
|
1694
|
+
),
|
|
1695
|
+
const Spacer(),
|
|
1696
|
+
Text(
|
|
1697
|
+
valueDb <= -119 ? 'Silent' : '${valueDb.toStringAsFixed(0)} dB',
|
|
1698
|
+
style: TextStyle(
|
|
1699
|
+
color: _textSecondary,
|
|
1700
|
+
fontSize: compact ? 11 : 12,
|
|
1701
|
+
),
|
|
1702
|
+
),
|
|
1703
|
+
],
|
|
1704
|
+
),
|
|
1705
|
+
const SizedBox(height: 8),
|
|
1706
|
+
ClipRRect(
|
|
1707
|
+
borderRadius: BorderRadius.circular(999),
|
|
1708
|
+
child: LinearProgressIndicator(
|
|
1709
|
+
value: progress,
|
|
1710
|
+
minHeight: compact ? 7 : 8,
|
|
1711
|
+
color: color,
|
|
1712
|
+
backgroundColor: _borderLight,
|
|
1713
|
+
),
|
|
1714
|
+
),
|
|
1715
|
+
],
|
|
1716
|
+
),
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
class _DesktopFloatingToolbar extends StatefulWidget {
|
|
1722
|
+
const _DesktopFloatingToolbar({required this.controller});
|
|
1723
|
+
|
|
1724
|
+
final NeoAgentController controller;
|
|
1725
|
+
|
|
1726
|
+
@override
|
|
1727
|
+
State<_DesktopFloatingToolbar> createState() =>
|
|
1728
|
+
_DesktopFloatingToolbarState();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
class _DesktopFloatingToolbarState extends State<_DesktopFloatingToolbar> {
|
|
1732
|
+
Timer? _ticker;
|
|
1733
|
+
|
|
1734
|
+
@override
|
|
1735
|
+
void initState() {
|
|
1736
|
+
super.initState();
|
|
1737
|
+
_syncTicker();
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
@override
|
|
1741
|
+
void didUpdateWidget(covariant _DesktopFloatingToolbar oldWidget) {
|
|
1742
|
+
super.didUpdateWidget(oldWidget);
|
|
1743
|
+
_syncTicker();
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
@override
|
|
1747
|
+
void dispose() {
|
|
1748
|
+
_ticker?.cancel();
|
|
1749
|
+
super.dispose();
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
void _syncTicker() {
|
|
1753
|
+
final runtime = widget.controller.recordingRuntime;
|
|
1754
|
+
final shouldTick =
|
|
1755
|
+
runtime.active &&
|
|
1756
|
+
runtime.startedAt != null &&
|
|
1757
|
+
runtime.floatingToolbarVisible;
|
|
1758
|
+
if (!shouldTick) {
|
|
1759
|
+
_ticker?.cancel();
|
|
1760
|
+
_ticker = null;
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
_ticker ??= Timer.periodic(const Duration(seconds: 1), (_) {
|
|
1764
|
+
if (mounted) {
|
|
1765
|
+
setState(() {});
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
@override
|
|
1771
|
+
Widget build(BuildContext context) {
|
|
1772
|
+
final controller = widget.controller;
|
|
1773
|
+
final runtime = controller.recordingRuntime;
|
|
1774
|
+
if (!runtime.supportsFloatingToolbar ||
|
|
1775
|
+
!runtime.active ||
|
|
1776
|
+
!runtime.floatingToolbarVisible) {
|
|
1777
|
+
return const SizedBox.shrink();
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
final elapsed = runtime.startedAt == null
|
|
1781
|
+
? '00:00'
|
|
1782
|
+
: _formatDuration(
|
|
1783
|
+
DateTime.now().difference(runtime.startedAt!).inMilliseconds,
|
|
1784
|
+
);
|
|
1785
|
+
|
|
1786
|
+
return Positioned(
|
|
1787
|
+
top: 10,
|
|
1788
|
+
left: 0,
|
|
1789
|
+
right: 0,
|
|
1790
|
+
child: SafeArea(
|
|
1791
|
+
child: IgnorePointer(
|
|
1792
|
+
ignoring: false,
|
|
1793
|
+
child: Align(
|
|
1794
|
+
alignment: Alignment.topCenter,
|
|
1795
|
+
child: _DesktopFloatingToolbarSurface(
|
|
1796
|
+
controller: controller,
|
|
1797
|
+
elapsedLabel: elapsed,
|
|
1798
|
+
compactWindow: false,
|
|
1799
|
+
onOpenMainWindow: null,
|
|
1800
|
+
),
|
|
1801
|
+
),
|
|
1802
|
+
),
|
|
1803
|
+
),
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
class _DetachedDesktopFloatingToolbarShell extends StatefulWidget {
|
|
1809
|
+
const _DetachedDesktopFloatingToolbarShell({
|
|
1810
|
+
required this.controller,
|
|
1811
|
+
required this.onOpenMainWindow,
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
final NeoAgentController controller;
|
|
1815
|
+
final Future<void> Function() onOpenMainWindow;
|
|
1816
|
+
|
|
1817
|
+
@override
|
|
1818
|
+
State<_DetachedDesktopFloatingToolbarShell> createState() =>
|
|
1819
|
+
_DetachedDesktopFloatingToolbarShellState();
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
class _DetachedDesktopFloatingToolbarShellState
|
|
1823
|
+
extends State<_DetachedDesktopFloatingToolbarShell> {
|
|
1824
|
+
Timer? _ticker;
|
|
1825
|
+
|
|
1826
|
+
@override
|
|
1827
|
+
void initState() {
|
|
1828
|
+
super.initState();
|
|
1829
|
+
_syncTicker();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
@override
|
|
1833
|
+
void didUpdateWidget(
|
|
1834
|
+
covariant _DetachedDesktopFloatingToolbarShell oldWidget,
|
|
1835
|
+
) {
|
|
1836
|
+
super.didUpdateWidget(oldWidget);
|
|
1837
|
+
_syncTicker();
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
@override
|
|
1841
|
+
void dispose() {
|
|
1842
|
+
_ticker?.cancel();
|
|
1843
|
+
super.dispose();
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
void _syncTicker() {
|
|
1847
|
+
final runtime = widget.controller.recordingRuntime;
|
|
1848
|
+
final shouldTick =
|
|
1849
|
+
runtime.active &&
|
|
1850
|
+
runtime.startedAt != null &&
|
|
1851
|
+
runtime.floatingToolbarVisible;
|
|
1852
|
+
if (!shouldTick) {
|
|
1853
|
+
_ticker?.cancel();
|
|
1854
|
+
_ticker = null;
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
_ticker ??= Timer.periodic(const Duration(seconds: 1), (_) {
|
|
1858
|
+
if (mounted) {
|
|
1859
|
+
setState(() {});
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
@override
|
|
1865
|
+
Widget build(BuildContext context) {
|
|
1866
|
+
final runtime = widget.controller.recordingRuntime;
|
|
1867
|
+
if (!runtime.active || !runtime.floatingToolbarVisible) {
|
|
1868
|
+
return const SizedBox.shrink();
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
final elapsed = runtime.startedAt == null
|
|
1872
|
+
? '00:00'
|
|
1873
|
+
: _formatDuration(
|
|
1874
|
+
DateTime.now().difference(runtime.startedAt!).inMilliseconds,
|
|
1875
|
+
);
|
|
1876
|
+
|
|
1877
|
+
return DecoratedBox(
|
|
1878
|
+
decoration: const BoxDecoration(color: Colors.transparent),
|
|
1879
|
+
child: Scaffold(
|
|
1880
|
+
backgroundColor: Colors.transparent,
|
|
1881
|
+
body: SafeArea(
|
|
1882
|
+
child: Center(
|
|
1883
|
+
child: Padding(
|
|
1884
|
+
padding: const EdgeInsets.all(10),
|
|
1885
|
+
child: _DesktopFloatingToolbarSurface(
|
|
1886
|
+
controller: widget.controller,
|
|
1887
|
+
elapsedLabel: elapsed,
|
|
1888
|
+
compactWindow: true,
|
|
1889
|
+
onOpenMainWindow: widget.onOpenMainWindow,
|
|
1890
|
+
),
|
|
1891
|
+
),
|
|
1892
|
+
),
|
|
1893
|
+
),
|
|
1894
|
+
),
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
bool _desktopAssistantUsesToggleControls() {
|
|
1900
|
+
if (kIsWeb) {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
switch (defaultTargetPlatform) {
|
|
1904
|
+
case TargetPlatform.macOS:
|
|
1905
|
+
case TargetPlatform.windows:
|
|
1906
|
+
case TargetPlatform.linux:
|
|
1907
|
+
return true;
|
|
1908
|
+
case TargetPlatform.android:
|
|
1909
|
+
case TargetPlatform.iOS:
|
|
1910
|
+
case TargetPlatform.fuchsia:
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
String _desktopAssistantPrimaryLabel(bool isCapturing) {
|
|
1916
|
+
if (_desktopAssistantUsesToggleControls()) {
|
|
1917
|
+
return isCapturing ? 'Stop and send' : 'Start talking';
|
|
1918
|
+
}
|
|
1919
|
+
return isCapturing ? 'Release to send' : 'Hold to talk';
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
String _desktopAssistantPrimaryCaption(bool isCapturing) {
|
|
1923
|
+
if (_desktopAssistantUsesToggleControls()) {
|
|
1924
|
+
return isCapturing
|
|
1925
|
+
? 'Commit the active live capture'
|
|
1926
|
+
: 'Click once to begin capturing';
|
|
1927
|
+
}
|
|
1928
|
+
return isCapturing
|
|
1929
|
+
? 'Stop capture and submit'
|
|
1930
|
+
: 'Press and hold for quick capture';
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
String _desktopAssistantIdleHint() {
|
|
1934
|
+
return _desktopAssistantUsesToggleControls()
|
|
1935
|
+
? 'Click the mic to start speaking'
|
|
1936
|
+
: 'Hold Ctrl+Shift+Space to talk';
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
String _desktopAssistantScreenContextHint(bool enabled) {
|
|
1940
|
+
return enabled ? 'Current screen will be attached' : 'Audio only';
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
class _DesktopAssistantControlState {
|
|
1944
|
+
const _DesktopAssistantControlState({
|
|
1945
|
+
required this.isCapturing,
|
|
1946
|
+
required this.isBusy,
|
|
1947
|
+
required this.useToggleCapture,
|
|
1948
|
+
required this.statusLabel,
|
|
1949
|
+
required this.statusColor,
|
|
1950
|
+
required this.transcriptPreview,
|
|
1951
|
+
required this.primaryLabel,
|
|
1952
|
+
required this.primaryCaption,
|
|
1953
|
+
required this.primaryIcon,
|
|
1954
|
+
required this.primaryColor,
|
|
1955
|
+
required this.idleHint,
|
|
1956
|
+
required this.screenContextHint,
|
|
1957
|
+
required this.sourceSummary,
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
factory _DesktopAssistantControlState.fromController(
|
|
1961
|
+
NeoAgentController controller, {
|
|
1962
|
+
required bool blockedHintVisible,
|
|
1963
|
+
}) {
|
|
1964
|
+
final liveState = controller.voiceAssistantLiveState;
|
|
1965
|
+
final isCapturing = controller.isLiveVoiceCaptureEngaged;
|
|
1966
|
+
final includeScreenContext = controller.voiceAssistantIncludeScreenContext;
|
|
1967
|
+
final useToggleCapture = _desktopAssistantUsesToggleControls();
|
|
1968
|
+
final transcriptPreview = liveState.partialTranscript.trim().isEmpty
|
|
1969
|
+
? liveState.finalTranscript.trim()
|
|
1970
|
+
: liveState.partialTranscript.trim();
|
|
1971
|
+
return _DesktopAssistantControlState(
|
|
1972
|
+
isCapturing: isCapturing,
|
|
1973
|
+
isBusy: liveState.isBusy,
|
|
1974
|
+
useToggleCapture: useToggleCapture,
|
|
1975
|
+
statusLabel: blockedHintVisible
|
|
1976
|
+
? 'Assistant unavailable while recording'
|
|
1977
|
+
: (isCapturing
|
|
1978
|
+
? _desktopAssistantPrimaryLabel(true)
|
|
1979
|
+
: _desktopAssistantStatusLabel(liveState.state)),
|
|
1980
|
+
statusColor: blockedHintVisible
|
|
1981
|
+
? _warning
|
|
1982
|
+
: (isCapturing ? _success : _accent),
|
|
1983
|
+
transcriptPreview: transcriptPreview,
|
|
1984
|
+
primaryLabel: _desktopAssistantPrimaryLabel(isCapturing),
|
|
1985
|
+
primaryCaption: _desktopAssistantPrimaryCaption(isCapturing),
|
|
1986
|
+
primaryIcon: isCapturing ? Icons.stop_rounded : Icons.mic,
|
|
1987
|
+
primaryColor: isCapturing ? _warning : _success,
|
|
1988
|
+
idleHint: _desktopAssistantIdleHint(),
|
|
1989
|
+
screenContextHint: _desktopAssistantScreenContextHint(
|
|
1990
|
+
includeScreenContext,
|
|
1991
|
+
),
|
|
1992
|
+
sourceSummary: includeScreenContext ? 'Mic + screen' : 'Direct mic',
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
final bool isCapturing;
|
|
1997
|
+
final bool isBusy;
|
|
1998
|
+
final bool useToggleCapture;
|
|
1999
|
+
final String statusLabel;
|
|
2000
|
+
final Color statusColor;
|
|
2001
|
+
final String transcriptPreview;
|
|
2002
|
+
final String primaryLabel;
|
|
2003
|
+
final String primaryCaption;
|
|
2004
|
+
final IconData primaryIcon;
|
|
2005
|
+
final Color primaryColor;
|
|
2006
|
+
final String idleHint;
|
|
2007
|
+
final String screenContextHint;
|
|
2008
|
+
final String sourceSummary;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
class _VoiceAssistantScreenContextButton extends StatelessWidget {
|
|
2012
|
+
const _VoiceAssistantScreenContextButton({
|
|
2013
|
+
required this.controller,
|
|
2014
|
+
required this.compact,
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
final NeoAgentController controller;
|
|
2018
|
+
final bool compact;
|
|
2019
|
+
|
|
2020
|
+
@override
|
|
2021
|
+
Widget build(BuildContext context) {
|
|
2022
|
+
final enabled = controller.voiceAssistantIncludeScreenContext;
|
|
2023
|
+
final onPressed = controller.canCaptureVoiceAssistantScreenContext
|
|
2024
|
+
? () {
|
|
2025
|
+
unawaited(controller.toggleVoiceAssistantScreenContext());
|
|
2026
|
+
}
|
|
2027
|
+
: null;
|
|
2028
|
+
|
|
2029
|
+
if (compact) {
|
|
2030
|
+
return IconButton(
|
|
2031
|
+
tooltip: enabled
|
|
2032
|
+
? 'Stop including the current screen'
|
|
2033
|
+
: 'Include the current screen',
|
|
2034
|
+
onPressed: onPressed,
|
|
2035
|
+
style: IconButton.styleFrom(
|
|
2036
|
+
visualDensity: VisualDensity.compact,
|
|
2037
|
+
padding: const EdgeInsets.all(8),
|
|
2038
|
+
minimumSize: const Size(30, 30),
|
|
2039
|
+
backgroundColor: enabled
|
|
2040
|
+
? _accent.withValues(alpha: 0.14)
|
|
2041
|
+
: _bgSecondary.withValues(alpha: 0.9),
|
|
2042
|
+
foregroundColor: enabled ? _accent : _textSecondary,
|
|
2043
|
+
),
|
|
2044
|
+
icon: Icon(
|
|
2045
|
+
enabled
|
|
2046
|
+
? Icons.desktop_windows_rounded
|
|
2047
|
+
: Icons.desktop_windows_outlined,
|
|
2048
|
+
size: 15,
|
|
2049
|
+
),
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
return _VoiceAssistantActionButton(
|
|
2054
|
+
icon: enabled
|
|
2055
|
+
? Icons.desktop_windows_rounded
|
|
2056
|
+
: Icons.desktop_windows_outlined,
|
|
2057
|
+
label: enabled ? 'Screen on' : 'Screen off',
|
|
2058
|
+
onTap: onPressed,
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
class _DesktopAssistantPopupShell extends StatelessWidget {
|
|
2064
|
+
const _DesktopAssistantPopupShell({
|
|
2065
|
+
required this.controller,
|
|
2066
|
+
required this.blockedHintVisible,
|
|
2067
|
+
required this.onPrimaryAction,
|
|
2068
|
+
required this.onCancel,
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
final NeoAgentController controller;
|
|
2072
|
+
final bool blockedHintVisible;
|
|
2073
|
+
final Future<void> Function() onPrimaryAction;
|
|
2074
|
+
final Future<void> Function() onCancel;
|
|
2075
|
+
|
|
2076
|
+
@override
|
|
2077
|
+
Widget build(BuildContext context) {
|
|
2078
|
+
final assistantUi = _DesktopAssistantControlState.fromController(
|
|
2079
|
+
controller,
|
|
2080
|
+
blockedHintVisible: blockedHintVisible,
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
return DecoratedBox(
|
|
2084
|
+
decoration: const BoxDecoration(color: Colors.transparent),
|
|
2085
|
+
child: Scaffold(
|
|
2086
|
+
backgroundColor: Colors.transparent,
|
|
2087
|
+
body: SafeArea(
|
|
2088
|
+
child: Align(
|
|
2089
|
+
alignment: Alignment.bottomCenter,
|
|
2090
|
+
child: Padding(
|
|
2091
|
+
padding: const EdgeInsets.fromLTRB(16, 16, 16, 18),
|
|
2092
|
+
child: Material(
|
|
2093
|
+
color: Colors.transparent,
|
|
2094
|
+
child: Container(
|
|
2095
|
+
constraints: const BoxConstraints(maxWidth: 430),
|
|
2096
|
+
padding: const EdgeInsets.symmetric(
|
|
2097
|
+
horizontal: 12,
|
|
2098
|
+
vertical: 10,
|
|
2099
|
+
),
|
|
2100
|
+
decoration: BoxDecoration(
|
|
2101
|
+
gradient: LinearGradient(
|
|
2102
|
+
colors: <Color>[
|
|
2103
|
+
_bgCard.withValues(alpha: 0.99),
|
|
2104
|
+
_bgSecondary.withValues(alpha: 0.97),
|
|
2105
|
+
],
|
|
2106
|
+
),
|
|
2107
|
+
borderRadius: BorderRadius.circular(999),
|
|
2108
|
+
border: Border.all(
|
|
2109
|
+
color: _borderLight.withValues(alpha: 0.9),
|
|
2110
|
+
),
|
|
2111
|
+
boxShadow: <BoxShadow>[
|
|
2112
|
+
BoxShadow(
|
|
2113
|
+
color: Colors.black.withValues(alpha: 0.2),
|
|
2114
|
+
blurRadius: 18,
|
|
2115
|
+
offset: const Offset(0, 8),
|
|
2116
|
+
),
|
|
2117
|
+
],
|
|
2118
|
+
),
|
|
2119
|
+
child: Row(
|
|
2120
|
+
mainAxisSize: MainAxisSize.max,
|
|
2121
|
+
children: <Widget>[
|
|
2122
|
+
_DesktopAssistantPulseDots(
|
|
2123
|
+
color: assistantUi.statusColor,
|
|
2124
|
+
active: assistantUi.isCapturing || assistantUi.isBusy,
|
|
2125
|
+
),
|
|
2126
|
+
const SizedBox(width: 12),
|
|
2127
|
+
_DesktopAssistantWaveform(
|
|
2128
|
+
color: assistantUi.statusColor,
|
|
2129
|
+
active: assistantUi.isCapturing,
|
|
2130
|
+
busy: assistantUi.isBusy,
|
|
2131
|
+
),
|
|
2132
|
+
const SizedBox(width: 12),
|
|
2133
|
+
Expanded(
|
|
2134
|
+
child: Column(
|
|
2135
|
+
mainAxisSize: MainAxisSize.min,
|
|
2136
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2137
|
+
children: <Widget>[
|
|
2138
|
+
Text(
|
|
2139
|
+
assistantUi.statusLabel,
|
|
2140
|
+
maxLines: 1,
|
|
2141
|
+
overflow: TextOverflow.ellipsis,
|
|
2142
|
+
style: TextStyle(
|
|
2143
|
+
color: _textPrimary,
|
|
2144
|
+
fontWeight: FontWeight.w700,
|
|
2145
|
+
fontSize: 12.5,
|
|
2146
|
+
),
|
|
2147
|
+
),
|
|
2148
|
+
if (!blockedHintVisible)
|
|
2149
|
+
Text(
|
|
2150
|
+
assistantUi.transcriptPreview.isEmpty
|
|
2151
|
+
? '${assistantUi.idleHint} • ${assistantUi.screenContextHint}'
|
|
2152
|
+
: assistantUi.transcriptPreview,
|
|
2153
|
+
maxLines: 1,
|
|
2154
|
+
overflow: TextOverflow.ellipsis,
|
|
2155
|
+
style: TextStyle(
|
|
2156
|
+
color: _textMuted,
|
|
2157
|
+
fontSize: 11.5,
|
|
2158
|
+
height: 1.35,
|
|
2159
|
+
),
|
|
2160
|
+
),
|
|
2161
|
+
],
|
|
2162
|
+
),
|
|
2163
|
+
),
|
|
2164
|
+
const SizedBox(width: 8),
|
|
2165
|
+
_VoiceAssistantScreenContextButton(
|
|
2166
|
+
controller: controller,
|
|
2167
|
+
compact: true,
|
|
2168
|
+
),
|
|
2169
|
+
const SizedBox(width: 4),
|
|
2170
|
+
FilledButton.icon(
|
|
2171
|
+
onPressed: blockedHintVisible
|
|
2172
|
+
? null
|
|
2173
|
+
: () {
|
|
2174
|
+
unawaited(onPrimaryAction());
|
|
2175
|
+
},
|
|
2176
|
+
style: FilledButton.styleFrom(
|
|
2177
|
+
visualDensity: VisualDensity.compact,
|
|
2178
|
+
padding: const EdgeInsets.symmetric(
|
|
2179
|
+
horizontal: 12,
|
|
2180
|
+
vertical: 10,
|
|
2181
|
+
),
|
|
2182
|
+
minimumSize: const Size(0, 38),
|
|
2183
|
+
backgroundColor: assistantUi.statusColor,
|
|
2184
|
+
foregroundColor: Colors.white,
|
|
2185
|
+
),
|
|
2186
|
+
icon: Icon(
|
|
2187
|
+
assistantUi.isCapturing
|
|
2188
|
+
? Icons.stop_rounded
|
|
2189
|
+
: Icons.mic_rounded,
|
|
2190
|
+
size: 16,
|
|
2191
|
+
),
|
|
2192
|
+
label: Text(
|
|
2193
|
+
assistantUi.isCapturing ? 'Send' : 'Talk',
|
|
2194
|
+
style: const TextStyle(fontWeight: FontWeight.w700),
|
|
2195
|
+
),
|
|
2196
|
+
),
|
|
2197
|
+
IconButton(
|
|
2198
|
+
tooltip: 'Cancel',
|
|
2199
|
+
onPressed: () {
|
|
2200
|
+
unawaited(onCancel());
|
|
2201
|
+
},
|
|
2202
|
+
style: IconButton.styleFrom(
|
|
2203
|
+
visualDensity: VisualDensity.compact,
|
|
2204
|
+
padding: const EdgeInsets.all(8),
|
|
2205
|
+
minimumSize: const Size(30, 30),
|
|
2206
|
+
backgroundColor: _bgSecondary.withValues(alpha: 0.9),
|
|
2207
|
+
foregroundColor: _textSecondary,
|
|
2208
|
+
),
|
|
2209
|
+
icon: const Icon(Icons.close_rounded, size: 14),
|
|
2210
|
+
),
|
|
2211
|
+
],
|
|
2212
|
+
),
|
|
2213
|
+
),
|
|
2214
|
+
),
|
|
2215
|
+
),
|
|
2216
|
+
),
|
|
2217
|
+
),
|
|
2218
|
+
),
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
String _desktopAssistantStatusLabel(String state) {
|
|
2224
|
+
switch (state.trim().toLowerCase()) {
|
|
2225
|
+
case 'transcribing':
|
|
2226
|
+
return 'Transcribing';
|
|
2227
|
+
case 'thinking':
|
|
2228
|
+
return 'Thinking';
|
|
2229
|
+
case 'speaking':
|
|
2230
|
+
return 'Speaking';
|
|
2231
|
+
case 'listening':
|
|
2232
|
+
return 'Listening';
|
|
2233
|
+
case 'idle':
|
|
2234
|
+
default:
|
|
2235
|
+
return 'Ready';
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
class _DesktopAssistantPulseDots extends StatelessWidget {
|
|
2240
|
+
const _DesktopAssistantPulseDots({required this.color, required this.active});
|
|
2241
|
+
|
|
2242
|
+
final Color color;
|
|
2243
|
+
final bool active;
|
|
2244
|
+
|
|
2245
|
+
@override
|
|
2246
|
+
Widget build(BuildContext context) {
|
|
2247
|
+
return SizedBox(
|
|
2248
|
+
width: 26,
|
|
2249
|
+
child: Wrap(
|
|
2250
|
+
spacing: 3,
|
|
2251
|
+
runSpacing: 3,
|
|
2252
|
+
children: List<Widget>.generate(6, (index) {
|
|
2253
|
+
final opacity = active ? 0.35 + (index % 3) * 0.2 : 0.28;
|
|
2254
|
+
return Container(
|
|
2255
|
+
width: 5,
|
|
2256
|
+
height: 5,
|
|
2257
|
+
decoration: BoxDecoration(
|
|
2258
|
+
color: color.withValues(alpha: opacity),
|
|
2259
|
+
shape: BoxShape.circle,
|
|
2260
|
+
),
|
|
2261
|
+
);
|
|
2262
|
+
}),
|
|
2263
|
+
),
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
class _DesktopAssistantWaveform extends StatefulWidget {
|
|
2269
|
+
const _DesktopAssistantWaveform({
|
|
2270
|
+
required this.color,
|
|
2271
|
+
required this.active,
|
|
2272
|
+
required this.busy,
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
final Color color;
|
|
2276
|
+
final bool active;
|
|
2277
|
+
final bool busy;
|
|
2278
|
+
|
|
2279
|
+
@override
|
|
2280
|
+
State<_DesktopAssistantWaveform> createState() =>
|
|
2281
|
+
_DesktopAssistantWaveformState();
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
class _DesktopAssistantWaveformState extends State<_DesktopAssistantWaveform>
|
|
2285
|
+
with SingleTickerProviderStateMixin {
|
|
2286
|
+
late final AnimationController _controller;
|
|
2287
|
+
|
|
2288
|
+
@override
|
|
2289
|
+
void initState() {
|
|
2290
|
+
super.initState();
|
|
2291
|
+
_controller = AnimationController(
|
|
2292
|
+
vsync: this,
|
|
2293
|
+
duration: const Duration(milliseconds: 980),
|
|
2294
|
+
);
|
|
2295
|
+
_syncAnimation();
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
@override
|
|
2299
|
+
void didUpdateWidget(covariant _DesktopAssistantWaveform oldWidget) {
|
|
2300
|
+
super.didUpdateWidget(oldWidget);
|
|
2301
|
+
_syncAnimation();
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
@override
|
|
2305
|
+
void dispose() {
|
|
2306
|
+
_controller.dispose();
|
|
2307
|
+
super.dispose();
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
void _syncAnimation() {
|
|
2311
|
+
if (widget.active || widget.busy) {
|
|
2312
|
+
if (!_controller.isAnimating) {
|
|
2313
|
+
_controller.repeat();
|
|
2314
|
+
}
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
_controller.stop();
|
|
2318
|
+
_controller.value = 0;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
@override
|
|
2322
|
+
Widget build(BuildContext context) {
|
|
2323
|
+
const barCount = 18;
|
|
2324
|
+
return SizedBox(
|
|
2325
|
+
width: 116,
|
|
2326
|
+
height: 18,
|
|
2327
|
+
child: AnimatedBuilder(
|
|
2328
|
+
animation: _controller,
|
|
2329
|
+
builder: (context, child) {
|
|
2330
|
+
return Row(
|
|
2331
|
+
children: List<Widget>.generate(barCount, (index) {
|
|
2332
|
+
final phase = _controller.value * 2 * math.pi;
|
|
2333
|
+
final wave = math.sin(phase + index * 0.55);
|
|
2334
|
+
final minHeight = widget.busy ? 3.0 : 2.0;
|
|
2335
|
+
final maxHeight = widget.active
|
|
2336
|
+
? 12.0
|
|
2337
|
+
: (widget.busy ? 7.0 : 3.0);
|
|
2338
|
+
final normalized = widget.active || widget.busy
|
|
2339
|
+
? (wave + 1) / 2
|
|
2340
|
+
: 0.2;
|
|
2341
|
+
final height = minHeight + (maxHeight - minHeight) * normalized;
|
|
2342
|
+
return Padding(
|
|
2343
|
+
padding: const EdgeInsets.only(right: 2),
|
|
2344
|
+
child: Align(
|
|
2345
|
+
alignment: Alignment.bottomCenter,
|
|
2346
|
+
child: Container(
|
|
2347
|
+
width: 3,
|
|
2348
|
+
height: height,
|
|
2349
|
+
decoration: BoxDecoration(
|
|
2350
|
+
color: widget.color.withValues(
|
|
2351
|
+
alpha: widget.active ? 0.9 : (widget.busy ? 0.65 : 0.4),
|
|
2352
|
+
),
|
|
2353
|
+
borderRadius: BorderRadius.circular(99),
|
|
2354
|
+
),
|
|
2355
|
+
),
|
|
2356
|
+
),
|
|
2357
|
+
);
|
|
2358
|
+
}),
|
|
2359
|
+
);
|
|
2360
|
+
},
|
|
2361
|
+
),
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
class _DesktopFloatingToolbarSurface extends StatelessWidget {
|
|
2367
|
+
const _DesktopFloatingToolbarSurface({
|
|
2368
|
+
required this.controller,
|
|
2369
|
+
required this.elapsedLabel,
|
|
2370
|
+
required this.compactWindow,
|
|
2371
|
+
required this.onOpenMainWindow,
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
final NeoAgentController controller;
|
|
2375
|
+
final String elapsedLabel;
|
|
2376
|
+
final bool compactWindow;
|
|
2377
|
+
final Future<void> Function()? onOpenMainWindow;
|
|
2378
|
+
|
|
2379
|
+
@override
|
|
2380
|
+
Widget build(BuildContext context) {
|
|
2381
|
+
final runtime = controller.recordingRuntime;
|
|
2382
|
+
return Material(
|
|
2383
|
+
color: Colors.transparent,
|
|
2384
|
+
child: Container(
|
|
2385
|
+
constraints: BoxConstraints(
|
|
2386
|
+
maxWidth: compactWindow ? double.infinity : 680,
|
|
2387
|
+
),
|
|
2388
|
+
margin: compactWindow
|
|
2389
|
+
? EdgeInsets.zero
|
|
2390
|
+
: const EdgeInsets.symmetric(horizontal: 16),
|
|
2391
|
+
padding: EdgeInsets.symmetric(
|
|
2392
|
+
horizontal: compactWindow ? 10 : 14,
|
|
2393
|
+
vertical: compactWindow ? 8 : 12,
|
|
2394
|
+
),
|
|
2395
|
+
decoration: BoxDecoration(
|
|
2396
|
+
gradient: LinearGradient(
|
|
2397
|
+
colors: <Color>[
|
|
2398
|
+
_bgCard.withValues(alpha: 0.98),
|
|
2399
|
+
_bgCard.withValues(alpha: 0.92),
|
|
2400
|
+
],
|
|
2401
|
+
),
|
|
2402
|
+
borderRadius: BorderRadius.circular(compactWindow ? 22 : 24),
|
|
2403
|
+
border: Border.all(color: _borderLight),
|
|
2404
|
+
boxShadow: <BoxShadow>[
|
|
2405
|
+
BoxShadow(
|
|
2406
|
+
color: Colors.black.withValues(alpha: 0.24),
|
|
2407
|
+
blurRadius: 24,
|
|
2408
|
+
offset: const Offset(0, 12),
|
|
2409
|
+
),
|
|
2410
|
+
],
|
|
2411
|
+
),
|
|
2412
|
+
child: Wrap(
|
|
2413
|
+
spacing: 10,
|
|
2414
|
+
runSpacing: 10,
|
|
2415
|
+
crossAxisAlignment: WrapCrossAlignment.center,
|
|
2416
|
+
children: <Widget>[
|
|
2417
|
+
if (compactWindow)
|
|
2418
|
+
const _BrandLockup(
|
|
2419
|
+
logoSize: 34,
|
|
2420
|
+
titleFontSize: 16,
|
|
2421
|
+
direction: Axis.horizontal,
|
|
2422
|
+
spacing: 10,
|
|
2423
|
+
alignment: CrossAxisAlignment.start,
|
|
2424
|
+
),
|
|
2425
|
+
if (compactWindow)
|
|
2426
|
+
DragToMoveArea(
|
|
2427
|
+
child: Container(
|
|
2428
|
+
padding: const EdgeInsets.symmetric(
|
|
2429
|
+
horizontal: 8,
|
|
2430
|
+
vertical: 6,
|
|
2431
|
+
),
|
|
2432
|
+
decoration: BoxDecoration(
|
|
2433
|
+
color: _bgSecondary.withValues(alpha: 0.78),
|
|
2434
|
+
borderRadius: BorderRadius.circular(12),
|
|
2435
|
+
border: Border.all(color: _borderLight),
|
|
2436
|
+
),
|
|
2437
|
+
child: Icon(
|
|
2438
|
+
Icons.drag_indicator_rounded,
|
|
2439
|
+
size: 14,
|
|
2440
|
+
color: _textMuted,
|
|
2441
|
+
),
|
|
2442
|
+
),
|
|
2443
|
+
),
|
|
2444
|
+
Container(
|
|
2445
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
2446
|
+
decoration: BoxDecoration(
|
|
2447
|
+
color: (runtime.paused ? _warning : _danger).withValues(
|
|
2448
|
+
alpha: 0.10,
|
|
2449
|
+
),
|
|
2450
|
+
borderRadius: BorderRadius.circular(16),
|
|
2451
|
+
border: Border.all(
|
|
2452
|
+
color: (runtime.paused ? _warning : _danger).withValues(
|
|
2453
|
+
alpha: 0.20,
|
|
2454
|
+
),
|
|
2455
|
+
),
|
|
2456
|
+
),
|
|
2457
|
+
child: Row(
|
|
2458
|
+
mainAxisSize: MainAxisSize.min,
|
|
2459
|
+
children: <Widget>[
|
|
2460
|
+
Icon(
|
|
2461
|
+
runtime.paused
|
|
2462
|
+
? Icons.pause_circle_outline
|
|
2463
|
+
: Icons.fiber_manual_record_rounded,
|
|
2464
|
+
color: runtime.paused ? _warning : _danger,
|
|
2465
|
+
size: 18,
|
|
2466
|
+
),
|
|
2467
|
+
const SizedBox(width: 8),
|
|
2468
|
+
Text(
|
|
2469
|
+
runtime.paused ? 'Paused' : 'Recording',
|
|
2470
|
+
style: TextStyle(
|
|
2471
|
+
color: runtime.paused ? _warning : _danger,
|
|
2472
|
+
fontSize: 12,
|
|
2473
|
+
fontWeight: FontWeight.w800,
|
|
2474
|
+
),
|
|
2475
|
+
),
|
|
2476
|
+
const SizedBox(width: 10),
|
|
2477
|
+
Text(
|
|
2478
|
+
elapsedLabel,
|
|
2479
|
+
style: TextStyle(
|
|
2480
|
+
color: _textPrimary,
|
|
2481
|
+
fontSize: 12,
|
|
2482
|
+
fontWeight: FontWeight.w700,
|
|
2483
|
+
),
|
|
2484
|
+
),
|
|
2485
|
+
],
|
|
2486
|
+
),
|
|
2487
|
+
),
|
|
2488
|
+
_AudioLevelBar(
|
|
2489
|
+
label: 'MIC',
|
|
2490
|
+
valueDb: runtime.microphoneLevelDb,
|
|
2491
|
+
color: _accent,
|
|
2492
|
+
compact: true,
|
|
2493
|
+
),
|
|
2494
|
+
_AudioLevelBar(
|
|
2495
|
+
label: 'SYSTEM',
|
|
2496
|
+
valueDb: runtime.systemAudioLevelDb,
|
|
2497
|
+
color: _accentAlt,
|
|
2498
|
+
compact: true,
|
|
2499
|
+
),
|
|
2500
|
+
if (compactWindow && onOpenMainWindow != null)
|
|
2501
|
+
IconButton(
|
|
2502
|
+
tooltip: 'Open NeoAgent',
|
|
2503
|
+
onPressed: onOpenMainWindow,
|
|
2504
|
+
style: IconButton.styleFrom(
|
|
2505
|
+
backgroundColor: _bgSecondary,
|
|
2506
|
+
foregroundColor: _textPrimary,
|
|
2507
|
+
),
|
|
2508
|
+
icon: const Icon(Icons.open_in_full_rounded),
|
|
2509
|
+
),
|
|
2510
|
+
IconButton(
|
|
2511
|
+
tooltip: runtime.paused ? 'Resume recording' : 'Pause recording',
|
|
2512
|
+
onPressed: runtime.paused
|
|
2513
|
+
? controller.resumeDesktopRecording
|
|
2514
|
+
: controller.pauseDesktopRecording,
|
|
2515
|
+
style: IconButton.styleFrom(
|
|
2516
|
+
backgroundColor: _bgSecondary,
|
|
2517
|
+
foregroundColor: _textPrimary,
|
|
2518
|
+
),
|
|
2519
|
+
icon: Icon(
|
|
2520
|
+
runtime.paused ? Icons.play_arrow_rounded : Icons.pause_rounded,
|
|
2521
|
+
),
|
|
2522
|
+
),
|
|
2523
|
+
IconButton(
|
|
2524
|
+
tooltip: 'Stop recording',
|
|
2525
|
+
onPressed: controller.isStoppingRecording
|
|
2526
|
+
? null
|
|
2527
|
+
: controller.stopRecording,
|
|
2528
|
+
style: IconButton.styleFrom(
|
|
2529
|
+
backgroundColor: _danger.withValues(alpha: 0.12),
|
|
2530
|
+
foregroundColor: _danger,
|
|
2531
|
+
),
|
|
2532
|
+
icon: const Icon(Icons.stop_rounded),
|
|
2533
|
+
),
|
|
2534
|
+
IconButton(
|
|
2535
|
+
tooltip: 'Hide floating bar',
|
|
2536
|
+
onPressed: controller.hideDesktopFloatingToolbar,
|
|
2537
|
+
style: IconButton.styleFrom(
|
|
2538
|
+
backgroundColor: _bgSecondary,
|
|
2539
|
+
foregroundColor: _textSecondary,
|
|
2540
|
+
),
|
|
2541
|
+
icon: const Icon(Icons.close_rounded),
|
|
2542
|
+
),
|
|
2543
|
+
],
|
|
2544
|
+
),
|
|
2545
|
+
),
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
String _ensureModelValue(
|
|
2551
|
+
String value,
|
|
2552
|
+
List<ModelMeta> models, {
|
|
2553
|
+
required bool allowAuto,
|
|
2554
|
+
}) {
|
|
2555
|
+
if (allowAuto && value == 'auto') {
|
|
2556
|
+
return 'auto';
|
|
2557
|
+
}
|
|
2558
|
+
for (final model in models) {
|
|
2559
|
+
if (model.id == value) {
|
|
2560
|
+
return value;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (allowAuto) {
|
|
2564
|
+
return 'auto';
|
|
2565
|
+
}
|
|
2566
|
+
return models.isNotEmpty ? models.first.id : value;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
String _firstAvailableModelId(List<ModelMeta> models) {
|
|
2570
|
+
for (final model in models) {
|
|
2571
|
+
if (model.available) {
|
|
2572
|
+
return model.id;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return models.isNotEmpty ? models.first.id : 'auto';
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
String _modelLabelForValue(String value, List<ModelMeta> models) {
|
|
2579
|
+
if (value == 'auto' || value.trim().isEmpty) {
|
|
2580
|
+
return 'Auto';
|
|
2581
|
+
}
|
|
2582
|
+
for (final model in models) {
|
|
2583
|
+
if (model.id == value) {
|
|
2584
|
+
return model.label;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
return value;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
String _friendlyBaseUrlLabel(String value) {
|
|
2591
|
+
final uri = Uri.tryParse(value);
|
|
2592
|
+
if (uri == null || uri.host.trim().isEmpty) {
|
|
2593
|
+
return value;
|
|
2594
|
+
}
|
|
2595
|
+
final port = uri.hasPort ? ':${uri.port}' : '';
|
|
2596
|
+
return '${uri.host}$port';
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
String? _androidRuntimeVersionLabel(Map<String, dynamic> runtime) {
|
|
2600
|
+
final apiLevel = _asInt(runtime['apiLevel']);
|
|
2601
|
+
final systemImage = runtime['systemImage']?.toString().trim() ?? '';
|
|
2602
|
+
if (apiLevel <= 0 && systemImage.isEmpty) {
|
|
2603
|
+
return null;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
if (apiLevel > 0) {
|
|
2607
|
+
return 'Android $apiLevel';
|
|
2608
|
+
}
|
|
2609
|
+
return systemImage;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
Map<String, dynamic> _jsonMap(dynamic value) {
|
|
2613
|
+
if (value is Map<String, dynamic>) {
|
|
2614
|
+
return value;
|
|
2615
|
+
}
|
|
2616
|
+
if (value is Map) {
|
|
2617
|
+
return Map<String, dynamic>.from(value);
|
|
2618
|
+
}
|
|
2619
|
+
return const <String, dynamic>{};
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
List<dynamic> _jsonList(
|
|
2623
|
+
dynamic value, {
|
|
2624
|
+
List<String> nestedKeys = const <String>[
|
|
2625
|
+
'items',
|
|
2626
|
+
'data',
|
|
2627
|
+
'results',
|
|
2628
|
+
'rows',
|
|
2629
|
+
'values',
|
|
2630
|
+
'list',
|
|
2631
|
+
],
|
|
2632
|
+
bool fallbackToMapValues = false,
|
|
2633
|
+
}) {
|
|
2634
|
+
if (value is List) {
|
|
2635
|
+
return value;
|
|
2636
|
+
}
|
|
2637
|
+
if (value is Map) {
|
|
2638
|
+
for (final key in nestedKeys) {
|
|
2639
|
+
final nested = value[key];
|
|
2640
|
+
if (nested is List) {
|
|
2641
|
+
return nested;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (fallbackToMapValues) {
|
|
2645
|
+
return value.values.toList(growable: false);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return const <dynamic>[];
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
List<Map<String, dynamic>> _jsonMapList(
|
|
2652
|
+
dynamic value, {
|
|
2653
|
+
List<String> nestedKeys = const <String>[
|
|
2654
|
+
'items',
|
|
2655
|
+
'data',
|
|
2656
|
+
'results',
|
|
2657
|
+
'rows',
|
|
2658
|
+
'values',
|
|
2659
|
+
'list',
|
|
2660
|
+
],
|
|
2661
|
+
bool fallbackToMapValues = false,
|
|
2662
|
+
}) {
|
|
2663
|
+
return _jsonList(
|
|
2664
|
+
value,
|
|
2665
|
+
nestedKeys: nestedKeys,
|
|
2666
|
+
fallbackToMapValues: fallbackToMapValues,
|
|
2667
|
+
).whereType<Map>().map((item) => Map<String, dynamic>.from(item)).toList();
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
List<String> _jsonStringList(
|
|
2671
|
+
dynamic value, {
|
|
2672
|
+
List<String> nestedKeys = const <String>[
|
|
2673
|
+
'items',
|
|
2674
|
+
'data',
|
|
2675
|
+
'results',
|
|
2676
|
+
'rows',
|
|
2677
|
+
'values',
|
|
2678
|
+
'list',
|
|
2679
|
+
],
|
|
2680
|
+
bool fallbackToMapValues = false,
|
|
2681
|
+
}) {
|
|
2682
|
+
return _jsonList(
|
|
2683
|
+
value,
|
|
2684
|
+
nestedKeys: nestedKeys,
|
|
2685
|
+
fallbackToMapValues: fallbackToMapValues,
|
|
2686
|
+
)
|
|
2687
|
+
.map((item) => item?.toString() ?? '')
|
|
2688
|
+
.where((item) => item.isNotEmpty)
|
|
2689
|
+
.toList();
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
String _normalizeSuggestedWhitelistEntry(String platform, String entry) {
|
|
2693
|
+
final trimmed = entry.trim();
|
|
2694
|
+
if (trimmed.isEmpty) {
|
|
2695
|
+
return '';
|
|
2696
|
+
}
|
|
2697
|
+
switch (platform) {
|
|
2698
|
+
case 'whatsapp':
|
|
2699
|
+
return trimmed.replaceAll(RegExp(r'[^0-9]'), '');
|
|
2700
|
+
case 'telnyx':
|
|
2701
|
+
return trimmed.replaceAll(RegExp(r'[^0-9+]'), '');
|
|
2702
|
+
case 'discord':
|
|
2703
|
+
case 'telegram':
|
|
2704
|
+
return trimmed.replaceAll(
|
|
2705
|
+
RegExp(r'[^0-9a-z:_-]', caseSensitive: false),
|
|
2706
|
+
'',
|
|
2707
|
+
);
|
|
2708
|
+
default:
|
|
2709
|
+
return trimmed;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
int _asInt(dynamic value) {
|
|
2714
|
+
if (value is int) {
|
|
2715
|
+
return value;
|
|
2716
|
+
}
|
|
2717
|
+
if (value is double) {
|
|
2718
|
+
return value.round();
|
|
2719
|
+
}
|
|
2720
|
+
return int.tryParse(value?.toString() ?? '') ?? 0;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
DateTime _parseTimestamp(String? raw) {
|
|
2724
|
+
if (raw == null || raw.isEmpty) {
|
|
2725
|
+
return DateTime.now();
|
|
2726
|
+
}
|
|
2727
|
+
final normalized = raw.contains('T') ? raw : '${raw.replaceFirst(' ', 'T')}Z';
|
|
2728
|
+
return DateTime.tryParse(normalized)?.toLocal() ?? DateTime.now();
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
DateTime? _parseOptionalTimestamp(String? raw) {
|
|
2732
|
+
if (raw == null || raw.isEmpty) {
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
return _parseTimestamp(raw);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
String _formatTimestamp(DateTime value) {
|
|
2739
|
+
final hour = value.hour.toString().padLeft(2, '0');
|
|
2740
|
+
final minute = value.minute.toString().padLeft(2, '0');
|
|
2741
|
+
final month = value.month.toString().padLeft(2, '0');
|
|
2742
|
+
final day = value.day.toString().padLeft(2, '0');
|
|
2743
|
+
return '$month/$day $hour:$minute';
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
String _formatTimeOnly(DateTime value) {
|
|
2747
|
+
final hour = value.hour.toString().padLeft(2, '0');
|
|
2748
|
+
final minute = value.minute.toString().padLeft(2, '0');
|
|
2749
|
+
final second = value.second.toString().padLeft(2, '0');
|
|
2750
|
+
return '$hour:$minute:$second';
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
String _formatDuration(int milliseconds) {
|
|
2754
|
+
final totalSeconds = math.max(0, milliseconds ~/ 1000);
|
|
2755
|
+
final hours = totalSeconds ~/ 3600;
|
|
2756
|
+
final minutes = (totalSeconds % 3600) ~/ 60;
|
|
2757
|
+
final seconds = totalSeconds % 60;
|
|
2758
|
+
if (hours > 0) {
|
|
2759
|
+
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
2760
|
+
}
|
|
2761
|
+
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
String _formatElapsed(Duration value) {
|
|
2765
|
+
final totalSeconds = math.max(0, value.inSeconds);
|
|
2766
|
+
final hours = totalSeconds ~/ 3600;
|
|
2767
|
+
final minutes = (totalSeconds % 3600) ~/ 60;
|
|
2768
|
+
final seconds = totalSeconds % 60;
|
|
2769
|
+
if (hours > 0) {
|
|
2770
|
+
return '${hours}h ${minutes}m';
|
|
2771
|
+
}
|
|
2772
|
+
if (minutes > 0) {
|
|
2773
|
+
return '${minutes}m ${seconds}s';
|
|
2774
|
+
}
|
|
2775
|
+
return '${seconds}s';
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
String _formatNumber(int value) {
|
|
2779
|
+
final chars = value.abs().toString().split('').reversed.toList();
|
|
2780
|
+
final buffer = StringBuffer();
|
|
2781
|
+
for (var i = 0; i < chars.length; i++) {
|
|
2782
|
+
if (i > 0 && i % 3 == 0) {
|
|
2783
|
+
buffer.write('.');
|
|
2784
|
+
}
|
|
2785
|
+
buffer.write(chars[i]);
|
|
2786
|
+
}
|
|
2787
|
+
final formatted = buffer.toString().split('').reversed.join();
|
|
2788
|
+
return value < 0 ? '-$formatted' : formatted;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
String _summarizeToolArgs(dynamic raw) {
|
|
2792
|
+
if (raw is Map && raw.isNotEmpty) {
|
|
2793
|
+
final first = raw.entries.first;
|
|
2794
|
+
return '${first.key}: ${first.value}'.trim();
|
|
2795
|
+
}
|
|
2796
|
+
return '';
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
String _summarizeToolResult(dynamic raw) {
|
|
2800
|
+
if (raw == null) {
|
|
2801
|
+
return '';
|
|
2802
|
+
}
|
|
2803
|
+
if (raw is Map) {
|
|
2804
|
+
if (raw['timedOut'] == true) {
|
|
2805
|
+
final durationMs = _asInt(raw['durationMs']);
|
|
2806
|
+
final durationText = durationMs > 0
|
|
2807
|
+
? ' after ${_formatDuration(durationMs)}'
|
|
2808
|
+
: '';
|
|
2809
|
+
return 'Timed out$durationText';
|
|
2810
|
+
}
|
|
2811
|
+
if (raw['killed'] == true) {
|
|
2812
|
+
return 'Stopped before completion';
|
|
2813
|
+
}
|
|
2814
|
+
if (raw['error'] != null) {
|
|
2815
|
+
return raw['error'].toString();
|
|
2816
|
+
}
|
|
2817
|
+
if (raw['status'] != null && raw['status'].toString() == 'stopped') {
|
|
2818
|
+
return 'Stopped';
|
|
2819
|
+
}
|
|
2820
|
+
if (raw['message'] != null) {
|
|
2821
|
+
return raw['message'].toString();
|
|
2822
|
+
}
|
|
2823
|
+
if (raw['content'] != null) {
|
|
2824
|
+
return raw['content'].toString();
|
|
2825
|
+
}
|
|
2826
|
+
return raw.entries
|
|
2827
|
+
.take(2)
|
|
2828
|
+
.map((entry) => '${entry.key}: ${entry.value}')
|
|
2829
|
+
.join(' • ');
|
|
2830
|
+
}
|
|
2831
|
+
final text = raw.toString();
|
|
2832
|
+
return text.length > 140 ? '${text.substring(0, 140)}…' : text;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
String _titleCase(String value) {
|
|
2836
|
+
final normalized = value.trim();
|
|
2837
|
+
if (normalized.isEmpty) {
|
|
2838
|
+
return '';
|
|
2839
|
+
}
|
|
2840
|
+
return normalized
|
|
2841
|
+
.split(RegExp(r'\s+'))
|
|
2842
|
+
.map((part) {
|
|
2843
|
+
if (part.isEmpty) {
|
|
2844
|
+
return part;
|
|
2845
|
+
}
|
|
2846
|
+
return '${part[0].toUpperCase()}${part.substring(1)}';
|
|
2847
|
+
})
|
|
2848
|
+
.join(' ');
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
String _truncateRunText(String value, {int maxLength = 1400}) {
|
|
2852
|
+
final trimmed = value.trim();
|
|
2853
|
+
if (trimmed.length <= maxLength) {
|
|
2854
|
+
return trimmed;
|
|
2855
|
+
}
|
|
2856
|
+
return '${trimmed.substring(0, maxLength)}\n\n…truncated…';
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
extension on String {
|
|
2860
|
+
String ifEmpty(String fallback) => trim().isEmpty ? fallback : this;
|
|
2861
|
+
}
|