neoagent 2.3.1-beta.61 → 2.3.1-beta.63
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 +9 -0
- package/docs/configuration.md +11 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_controller.dart +183 -6
- package/flutter_app/lib/main_runtime.dart +22 -0
- package/flutter_app/lib/main_settings.dart +19 -4
- package/flutter_app/lib/main_shared.dart +161 -0
- package/flutter_app/lib/src/analytics_service.dart +294 -0
- package/flutter_app/lib/src/backend_client.dart +4 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
- package/flutter_app/pubspec.lock +8 -0
- package/flutter_app/pubspec.yaml +1 -0
- package/flutter_app/web/index.html +1 -0
- package/package.json +1 -1
- package/server/config/analytics.js +30 -0
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +183 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/index.html +1 -0
- package/server/public/main.dart.js +76037 -75248
- package/server/routes/runtime.js +14 -0
package/.env.example
CHANGED
|
@@ -51,6 +51,15 @@ NEOAGENT_RELEASE_CHANNEL=stable
|
|
|
51
51
|
# Example: http://localhost:3333,https://yourdomain.com
|
|
52
52
|
ALLOWED_ORIGINS=
|
|
53
53
|
|
|
54
|
+
########################################
|
|
55
|
+
# Analytics
|
|
56
|
+
########################################
|
|
57
|
+
|
|
58
|
+
# Mixpanel project token used by the Flutter app.
|
|
59
|
+
# Leave this blank to disable analytics, or set your own public project token.
|
|
60
|
+
# Keep the token below if you want to support the development of NeoAgent with anonymous usage analytics.
|
|
61
|
+
NEOAGENT_MIXPANEL_TOKEN=4a47ae6a05cf39a8faf0438a1200dde6
|
|
62
|
+
|
|
54
63
|
########################################
|
|
55
64
|
# Service email
|
|
56
65
|
########################################
|
package/docs/configuration.md
CHANGED
|
@@ -18,6 +18,17 @@ AI provider credentials, OAuth client secrets, and deployment controls are not c
|
|
|
18
18
|
| `NEOAGENT_DEPLOYMENT_MODE` | `self_hosted` | `self_hosted` enables in-app update controls; `managed` hides operator-only controls for SaaS deployments. |
|
|
19
19
|
| `NEOAGENT_RELEASE_CHANNEL` | `stable` | Release track used by the self-hosted updater. |
|
|
20
20
|
|
|
21
|
+
## Analytics
|
|
22
|
+
|
|
23
|
+
NeoAgent can send anonymous Mixpanel usage events from the Flutter client. The events cover app startup, section navigation, auth success, backend setup, onboarding dismissal, chat sends, recording start/stop, task runs, update checks, and update triggers. They do not include message content, credentials, or other app data.
|
|
24
|
+
|
|
25
|
+
Set `NEOAGENT_MIXPANEL_TOKEN` to your own public Mixpanel project token to enable analytics. Leave it blank to disable analytics entirely.
|
|
26
|
+
When analytics is enabled, the web client shows a dismissible cookie-consent banner until the user accepts or declines.
|
|
27
|
+
|
|
28
|
+
| Variable | Default | Description |
|
|
29
|
+
|---|---:|---|
|
|
30
|
+
| `NEOAGENT_MIXPANEL_TOKEN` | optional | Public Mixpanel project token used by the Flutter client. |
|
|
31
|
+
|
|
21
32
|
## Service Email
|
|
22
33
|
|
|
23
34
|
Service email is optional. When `NEOAGENT_EMAIL_FROM` and `NEOAGENT_EMAIL_SMTP_HOST` are set, NeoAgent uses SMTP for account security flows: signup confirmation, password reset, unusual login notifications, password change notifications, and email change notifications. Confirmation and reset links use the same `PUBLIC_URL` base as the other server-generated links.
|
|
Binary file
|
|
@@ -26,6 +26,7 @@ import 'src/android_apk_drop_zone.dart';
|
|
|
26
26
|
import 'src/android_launcher_bridge.dart';
|
|
27
27
|
import 'src/app_launch_bridge.dart';
|
|
28
28
|
import 'src/app_release_updater.dart' as app_release_updater;
|
|
29
|
+
import 'src/analytics_service.dart';
|
|
29
30
|
import 'src/backend_client.dart';
|
|
30
31
|
import 'src/desktop_companion.dart';
|
|
31
32
|
import 'src/desktop_screen_capture.dart';
|
|
@@ -37,6 +37,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
37
37
|
final OAuthLauncher _oauthLauncher;
|
|
38
38
|
final app_release_updater.AppReleaseUpdater _appReleaseUpdater =
|
|
39
39
|
app_release_updater.AppReleaseUpdater();
|
|
40
|
+
final AppAnalytics _analytics = AppAnalytics();
|
|
40
41
|
final LiveVoiceCapture _liveVoiceCapture = LiveVoiceCapture();
|
|
41
42
|
final DesktopScreenCapture _desktopScreenCapture =
|
|
42
43
|
createDesktopScreenCapture();
|
|
@@ -51,6 +52,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
51
52
|
static const String _configuredBackendUrl = String.fromEnvironment(
|
|
52
53
|
'NEOAGENT_BACKEND_URL',
|
|
53
54
|
);
|
|
55
|
+
static const String _selectedSectionPrefsKey = 'ui.selectedSection';
|
|
54
56
|
|
|
55
57
|
SharedPreferences? _prefs;
|
|
56
58
|
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
|
@@ -97,6 +99,10 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
97
99
|
bool networkStatusKnown = false;
|
|
98
100
|
bool _desktopFloatingToolbarPopupRequested = false;
|
|
99
101
|
bool _voiceAssistantIncludeScreenContext = false;
|
|
102
|
+
bool _didTrackAppOpen = false;
|
|
103
|
+
bool _analyticsConfigured = false;
|
|
104
|
+
bool _analyticsConsentResolved = false;
|
|
105
|
+
bool _analyticsConsentGranted = false;
|
|
100
106
|
|
|
101
107
|
bool hasUser = true;
|
|
102
108
|
bool registrationOpen = false;
|
|
@@ -318,6 +324,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
318
324
|
_diagnosticLogSubscription?.cancel();
|
|
319
325
|
_connectivitySubscription?.cancel();
|
|
320
326
|
_appReleaseUpdater.dispose();
|
|
327
|
+
unawaited(_analytics.dispose());
|
|
321
328
|
_desktopCompanion.removeListener(notifyListeners);
|
|
322
329
|
unawaited(_desktopCompanion.disconnect());
|
|
323
330
|
_recordingBridge.removeListener(_handleRecordingBridgeChanged);
|
|
@@ -348,6 +355,12 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
348
355
|
bool get canCaptureVoiceAssistantScreenContext =>
|
|
349
356
|
_desktopScreenCapture.isSupported;
|
|
350
357
|
|
|
358
|
+
bool get showAnalyticsConsentBanner =>
|
|
359
|
+
kIsWeb && _analyticsConfigured && !_analyticsConsentResolved;
|
|
360
|
+
|
|
361
|
+
bool get analyticsConsentGranted =>
|
|
362
|
+
_analyticsConsentResolved && _analyticsConsentGranted;
|
|
363
|
+
|
|
351
364
|
bool get isLiveVoiceCaptureEngaged =>
|
|
352
365
|
_isStartingLiveVoice || _liveVoiceCaptureActive;
|
|
353
366
|
|
|
@@ -689,6 +702,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
689
702
|
_prefs?.getBool('desktop.autoShowFloatingToolbar') ?? true;
|
|
690
703
|
_desktopAssistantHotkeyEnabled =
|
|
691
704
|
_prefs?.getBool('desktop.assistantHotkeyEnabled') ?? true;
|
|
705
|
+
_restoreSelectedSectionFromPrefs();
|
|
692
706
|
_voiceAssistantIncludeScreenContext =
|
|
693
707
|
(_prefs?.getBool('voiceAssistant.includeScreenContext') ?? false) &&
|
|
694
708
|
canCaptureVoiceAssistantScreenContext;
|
|
@@ -699,6 +713,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
699
713
|
appUpdateAutoCheckEnabled =
|
|
700
714
|
_prefs?.getBool('app.update.autoCheckEnabled') ?? true;
|
|
701
715
|
installedAppVersion = await _safeLoadInstalledAppVersion();
|
|
716
|
+
await _initializeAnalytics();
|
|
702
717
|
await refreshConnectivityStatus();
|
|
703
718
|
if (_connectivityPluginAvailable && _connectivitySubscription == null) {
|
|
704
719
|
try {
|
|
@@ -860,6 +875,101 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
860
875
|
}
|
|
861
876
|
}
|
|
862
877
|
|
|
878
|
+
Future<void> _initializeAnalytics() async {
|
|
879
|
+
try {
|
|
880
|
+
final runtimeConfig = await _backendClient.fetchRuntimeConfig(backendUrl);
|
|
881
|
+
final analyticsConfig = runtimeConfig['analytics'];
|
|
882
|
+
final mixpanelConfig = analyticsConfig is Map
|
|
883
|
+
? analyticsConfig['mixpanel']
|
|
884
|
+
: null;
|
|
885
|
+
final token = mixpanelConfig is Map
|
|
886
|
+
? mixpanelConfig['token']?.toString()
|
|
887
|
+
: null;
|
|
888
|
+
_analyticsConfigured = token != null && token.trim().isNotEmpty;
|
|
889
|
+
final consentState = _prefs?.getBool('analytics.cookieConsent');
|
|
890
|
+
_analyticsConsentResolved = consentState != null;
|
|
891
|
+
_analyticsConsentGranted = consentState == true;
|
|
892
|
+
await _analytics.initialize(
|
|
893
|
+
token: token,
|
|
894
|
+
consentGranted: _analyticsConsentGranted,
|
|
895
|
+
);
|
|
896
|
+
_trackAppOpenedIfNeeded();
|
|
897
|
+
} catch (_) {
|
|
898
|
+
_analyticsConfigured = false;
|
|
899
|
+
_analyticsConsentResolved = false;
|
|
900
|
+
_analyticsConsentGranted = false;
|
|
901
|
+
await _analytics.initialize(token: null, consentGranted: false);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
void _trackAppOpenedIfNeeded({String? consentSource}) {
|
|
906
|
+
if (!_analytics.enabled || _didTrackAppOpen) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
_didTrackAppOpen = true;
|
|
910
|
+
unawaited(
|
|
911
|
+
_analytics.trackAppOpened(
|
|
912
|
+
appMode: appMode.name,
|
|
913
|
+
platform: _analyticsPlatformLabel(),
|
|
914
|
+
backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
|
|
915
|
+
selectedSection: selectedSection.label,
|
|
916
|
+
deploymentProfile: deploymentProfile,
|
|
917
|
+
authenticated: isAuthenticated,
|
|
918
|
+
),
|
|
919
|
+
);
|
|
920
|
+
if (consentSource != null) {
|
|
921
|
+
unawaited(
|
|
922
|
+
_analytics.track(
|
|
923
|
+
'analytics_consent_granted',
|
|
924
|
+
properties: <String, Object?>{'consent_source': consentSource},
|
|
925
|
+
),
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
String _analyticsPlatformLabel() {
|
|
931
|
+
if (kIsWeb) {
|
|
932
|
+
return 'web';
|
|
933
|
+
}
|
|
934
|
+
switch (defaultTargetPlatform) {
|
|
935
|
+
case TargetPlatform.android:
|
|
936
|
+
return 'android';
|
|
937
|
+
case TargetPlatform.iOS:
|
|
938
|
+
return 'ios';
|
|
939
|
+
case TargetPlatform.macOS:
|
|
940
|
+
return 'macos';
|
|
941
|
+
case TargetPlatform.windows:
|
|
942
|
+
return 'windows';
|
|
943
|
+
case TargetPlatform.linux:
|
|
944
|
+
return 'linux';
|
|
945
|
+
case TargetPlatform.fuchsia:
|
|
946
|
+
return 'fuchsia';
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
Future<void> acceptAnalyticsConsent() async {
|
|
951
|
+
if (!_analyticsConfigured) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
_analyticsConsentResolved = true;
|
|
955
|
+
_analyticsConsentGranted = true;
|
|
956
|
+
await _prefs?.setBool('analytics.cookieConsent', true);
|
|
957
|
+
await _analytics.setConsentGranted(true);
|
|
958
|
+
_trackAppOpenedIfNeeded(consentSource: 'banner');
|
|
959
|
+
notifyListeners();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
Future<void> declineAnalyticsConsent() async {
|
|
963
|
+
if (!_analyticsConfigured) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
_analyticsConsentResolved = true;
|
|
967
|
+
_analyticsConsentGranted = false;
|
|
968
|
+
await _prefs?.setBool('analytics.cookieConsent', false);
|
|
969
|
+
await _analytics.setConsentGranted(false);
|
|
970
|
+
notifyListeners();
|
|
971
|
+
}
|
|
972
|
+
|
|
863
973
|
Future<void> setAppUpdateChannel(String channel) async {
|
|
864
974
|
final normalized = channel.trim().toLowerCase() == 'beta'
|
|
865
975
|
? 'beta'
|
|
@@ -884,6 +994,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
884
994
|
if (isCheckingAppUpdate) {
|
|
885
995
|
return;
|
|
886
996
|
}
|
|
997
|
+
unawaited(_analytics.trackAppUpdateCheck(silent: silent));
|
|
887
998
|
if (!appUpdaterConfigured) {
|
|
888
999
|
appUpdateErrorMessage = kIsWeb
|
|
889
1000
|
? 'Client app update checks are unavailable in the web app.'
|
|
@@ -973,6 +1084,11 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
973
1084
|
isBooting = true;
|
|
974
1085
|
notifyListeners();
|
|
975
1086
|
await bootstrap();
|
|
1087
|
+
unawaited(
|
|
1088
|
+
_analytics.trackBackendUrlSaved(
|
|
1089
|
+
backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
|
|
1090
|
+
),
|
|
1091
|
+
);
|
|
976
1092
|
return true;
|
|
977
1093
|
} catch (error) {
|
|
978
1094
|
errorMessage = _friendlyErrorMessage(error);
|
|
@@ -1145,6 +1261,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1145
1261
|
response,
|
|
1146
1262
|
retentionErrorMessage:
|
|
1147
1263
|
'QR login completed, but NeoAgent could not keep the session. Please try again.',
|
|
1264
|
+
authMethod: 'qr',
|
|
1148
1265
|
);
|
|
1149
1266
|
} catch (error) {
|
|
1150
1267
|
final message = _friendlyErrorMessage(error);
|
|
@@ -1197,6 +1314,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1197
1314
|
String? fallbackUsername,
|
|
1198
1315
|
String? retentionErrorMessage,
|
|
1199
1316
|
bool isRegistration = false,
|
|
1317
|
+
String authMethod = 'password',
|
|
1200
1318
|
}) async {
|
|
1201
1319
|
user = Map<String, dynamic>.from(
|
|
1202
1320
|
response['user'] as Map<dynamic, dynamic>? ??
|
|
@@ -1218,6 +1336,14 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1218
1336
|
_clearQrLoginChallenge();
|
|
1219
1337
|
await _persistCredentials();
|
|
1220
1338
|
await refresh();
|
|
1339
|
+
if (isAuthenticated) {
|
|
1340
|
+
unawaited(
|
|
1341
|
+
_analytics.trackSignedIn(
|
|
1342
|
+
authMethod: authMethod,
|
|
1343
|
+
isRegistration: isRegistration,
|
|
1344
|
+
),
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1221
1347
|
if (!isAuthenticated && retentionErrorMessage != null) {
|
|
1222
1348
|
errorMessage = retentionErrorMessage;
|
|
1223
1349
|
}
|
|
@@ -1287,6 +1413,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1287
1413
|
await _completeAuthenticatedResponse(
|
|
1288
1414
|
response,
|
|
1289
1415
|
isRegistration: register,
|
|
1416
|
+
authMethod: 'oauth',
|
|
1290
1417
|
retentionErrorMessage:
|
|
1291
1418
|
'Sign-in completed, but NeoAgent could not keep the browser session. Please sign in again. If this keeps happening, check backend session cookie settings.',
|
|
1292
1419
|
);
|
|
@@ -1313,6 +1440,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1313
1440
|
await _completeAuthenticatedResponse(
|
|
1314
1441
|
response,
|
|
1315
1442
|
fallbackUsername: pendingTwoFactorUsername,
|
|
1443
|
+
authMethod: 'two_factor',
|
|
1316
1444
|
retentionErrorMessage:
|
|
1317
1445
|
'Two-factor sign-in completed, but NeoAgent could not keep the browser session. Please sign in again.',
|
|
1318
1446
|
);
|
|
@@ -1403,6 +1531,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1403
1531
|
response,
|
|
1404
1532
|
fallbackUsername: username,
|
|
1405
1533
|
isRegistration: register,
|
|
1534
|
+
authMethod: 'password',
|
|
1406
1535
|
retentionErrorMessage:
|
|
1407
1536
|
'Sign-in completed, but NeoAgent could not keep the browser session. Please sign in again. If this keeps happening, the backend session cookie is likely not being retained.',
|
|
1408
1537
|
);
|
|
@@ -1433,6 +1562,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1433
1562
|
final logoutFuture = _backendClient.logout(backendUrl);
|
|
1434
1563
|
_authCycle += 1;
|
|
1435
1564
|
_clearAuthenticatedState();
|
|
1565
|
+
unawaited(_analytics.trackSignedOut());
|
|
1436
1566
|
isAuthenticating = true;
|
|
1437
1567
|
notifyListeners();
|
|
1438
1568
|
|
|
@@ -1449,6 +1579,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1449
1579
|
notifyListeners();
|
|
1450
1580
|
try {
|
|
1451
1581
|
await _backendClient.completeOnboarding(backendUrl);
|
|
1582
|
+
unawaited(_analytics.trackOnboardingDismissed());
|
|
1452
1583
|
if (isAuthenticated && user != null) {
|
|
1453
1584
|
user!['hasCompletedOnboarding'] = true;
|
|
1454
1585
|
}
|
|
@@ -1529,6 +1660,9 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1529
1660
|
toolEvents = const <ToolEventItem>[];
|
|
1530
1661
|
streamingAssistant = '';
|
|
1531
1662
|
selectedSection = AppSection.chat;
|
|
1663
|
+
unawaited(
|
|
1664
|
+
_prefs?.setString(_selectedSectionPrefsKey, AppSection.chat.name),
|
|
1665
|
+
);
|
|
1532
1666
|
_selectedWidgetId = null;
|
|
1533
1667
|
_pendingChatDraft = null;
|
|
1534
1668
|
_runDetailsCache.clear();
|
|
@@ -1576,6 +1710,20 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1576
1710
|
unawaited(_syncHomeWidgetConfig());
|
|
1577
1711
|
}
|
|
1578
1712
|
|
|
1713
|
+
void _restoreSelectedSectionFromPrefs() {
|
|
1714
|
+
final rawSection =
|
|
1715
|
+
_prefs?.getString(_selectedSectionPrefsKey)?.trim() ?? '';
|
|
1716
|
+
if (rawSection.isEmpty) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
final restoredSection = AppSection.values.firstWhere(
|
|
1721
|
+
(section) => section.name == rawSection,
|
|
1722
|
+
orElse: () => AppSection.chat,
|
|
1723
|
+
);
|
|
1724
|
+
selectedSection = restoredSection;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1579
1727
|
Future<void> _syncDesktopCompanionSession() {
|
|
1580
1728
|
return _desktopCompanion.updateSession(
|
|
1581
1729
|
backendUrl: backendUrl,
|
|
@@ -1585,13 +1733,23 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1585
1733
|
}
|
|
1586
1734
|
|
|
1587
1735
|
void setSelectedSection(AppSection section) {
|
|
1736
|
+
final previousSection = selectedSection;
|
|
1588
1737
|
selectedSection = section;
|
|
1738
|
+
unawaited(_prefs?.setString(_selectedSectionPrefsKey, section.name));
|
|
1589
1739
|
if (section == AppSection.devices) {
|
|
1590
1740
|
unawaited(refreshDevices());
|
|
1591
1741
|
}
|
|
1592
1742
|
if (section == AppSection.accountSettings) {
|
|
1593
1743
|
unawaited(refreshAccountSettings());
|
|
1594
1744
|
}
|
|
1745
|
+
if (previousSection != section) {
|
|
1746
|
+
unawaited(
|
|
1747
|
+
_analytics.trackSectionChanged(
|
|
1748
|
+
section: section.label,
|
|
1749
|
+
previousSection: previousSection.label,
|
|
1750
|
+
),
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1595
1753
|
notifyListeners();
|
|
1596
1754
|
}
|
|
1597
1755
|
|
|
@@ -2402,6 +2560,9 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2402
2560
|
}
|
|
2403
2561
|
|
|
2404
2562
|
Future<void> refreshWidgets({bool all = false}) async {
|
|
2563
|
+
if (all) {
|
|
2564
|
+
unawaited(_analytics.trackWidgetRefreshRequested(all: all));
|
|
2565
|
+
}
|
|
2405
2566
|
widgets = _decodeModelList(
|
|
2406
2567
|
'widgets',
|
|
2407
2568
|
await _backendClient.fetchWidgets(
|
|
@@ -3204,6 +3365,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3204
3365
|
}
|
|
3205
3366
|
|
|
3206
3367
|
Future<void> startBackgroundRecording() async {
|
|
3368
|
+
unawaited(_analytics.trackRecordingStarted(kind: 'background'));
|
|
3207
3369
|
await _startRecordingCapture(
|
|
3208
3370
|
logKey: 'start_background',
|
|
3209
3371
|
payload: buildAndroidBackgroundRecordingPayload(),
|
|
@@ -3230,6 +3392,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3230
3392
|
}
|
|
3231
3393
|
return;
|
|
3232
3394
|
}
|
|
3395
|
+
unawaited(_analytics.trackRecordingStarted(kind: 'desktop'));
|
|
3233
3396
|
await _startRecordingCapture(
|
|
3234
3397
|
logKey: 'start_desktop',
|
|
3235
3398
|
payload: buildDesktopRecordingPayload(),
|
|
@@ -3613,6 +3776,15 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3613
3776
|
notifyListeners();
|
|
3614
3777
|
try {
|
|
3615
3778
|
_desktopFloatingToolbarPopupRequested = false;
|
|
3779
|
+
unawaited(
|
|
3780
|
+
_analytics.trackRecordingStopped(
|
|
3781
|
+
kind: recordingRuntime.supportsBackgroundMic &&
|
|
3782
|
+
!recordingRuntime.supportsScreenAndMic
|
|
3783
|
+
? 'background'
|
|
3784
|
+
: 'desktop',
|
|
3785
|
+
stopReason: stopReason,
|
|
3786
|
+
),
|
|
3787
|
+
);
|
|
3616
3788
|
await _recordingBridge.stopActiveRecording();
|
|
3617
3789
|
if (isAndroidBackgroundStop) {
|
|
3618
3790
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
|
@@ -4241,6 +4413,12 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4241
4413
|
if (trimmed.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
|
|
4242
4414
|
return;
|
|
4243
4415
|
}
|
|
4416
|
+
unawaited(
|
|
4417
|
+
_analytics.trackChatMessageSent(
|
|
4418
|
+
length: trimmed.length,
|
|
4419
|
+
steeringLiveRun: canSteerLiveRun,
|
|
4420
|
+
),
|
|
4421
|
+
);
|
|
4244
4422
|
|
|
4245
4423
|
final optimistic = ChatEntry(
|
|
4246
4424
|
id: '',
|
|
@@ -4662,6 +4840,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4662
4840
|
errorMessage = null;
|
|
4663
4841
|
notifyListeners();
|
|
4664
4842
|
try {
|
|
4843
|
+
unawaited(_analytics.trackAppUpdateTriggered());
|
|
4665
4844
|
await _backendClient.triggerUpdate(backendUrl);
|
|
4666
4845
|
await refreshUpdateStatus();
|
|
4667
4846
|
} catch (error) {
|
|
@@ -5392,8 +5571,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5392
5571
|
return;
|
|
5393
5572
|
}
|
|
5394
5573
|
_pendingChatDraft = normalized;
|
|
5395
|
-
|
|
5396
|
-
notifyListeners();
|
|
5574
|
+
setSelectedSection(AppSection.chat);
|
|
5397
5575
|
}
|
|
5398
5576
|
|
|
5399
5577
|
String? takePendingChatDraft() {
|
|
@@ -5412,17 +5590,15 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5412
5590
|
|
|
5413
5591
|
void openWidgetSurface(String widgetId) {
|
|
5414
5592
|
final normalized = widgetId.trim();
|
|
5415
|
-
|
|
5593
|
+
setSelectedSection(AppSection.widgets);
|
|
5416
5594
|
if (normalized.isNotEmpty) {
|
|
5417
5595
|
_selectedWidgetId = normalized;
|
|
5418
5596
|
unawaited(refreshWidgets(all: true));
|
|
5419
5597
|
}
|
|
5420
|
-
notifyListeners();
|
|
5421
5598
|
}
|
|
5422
5599
|
|
|
5423
5600
|
void openVoiceAssistantSurface() {
|
|
5424
|
-
|
|
5425
|
-
notifyListeners();
|
|
5601
|
+
setSelectedSection(AppSection.voiceAssistant);
|
|
5426
5602
|
}
|
|
5427
5603
|
|
|
5428
5604
|
String? get selectedWidgetId => _selectedWidgetId;
|
|
@@ -5455,6 +5631,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5455
5631
|
return;
|
|
5456
5632
|
}
|
|
5457
5633
|
_startManualRunCooldown('task', '$id');
|
|
5634
|
+
unawaited(_analytics.trackTaskRunRequested(taskId: id));
|
|
5458
5635
|
await _backendClient.runSavedTask(backendUrl, id);
|
|
5459
5636
|
await refreshTasks();
|
|
5460
5637
|
await refreshRunsOnly();
|
|
@@ -627,6 +627,28 @@ class _NeoAgentAppState extends State<NeoAgentApp>
|
|
|
627
627
|
),
|
|
628
628
|
),
|
|
629
629
|
),
|
|
630
|
+
if (!_desktopToolbarWindowMode &&
|
|
631
|
+
!_desktopAssistantPopupWindowMode &&
|
|
632
|
+
_controller.showAnalyticsConsentBanner)
|
|
633
|
+
Positioned(
|
|
634
|
+
left: 0,
|
|
635
|
+
right: 0,
|
|
636
|
+
bottom: 0,
|
|
637
|
+
child: SafeArea(
|
|
638
|
+
top: false,
|
|
639
|
+
child: Padding(
|
|
640
|
+
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
|
641
|
+
child: Center(
|
|
642
|
+
child: ConstrainedBox(
|
|
643
|
+
constraints: const BoxConstraints(maxWidth: 980),
|
|
644
|
+
child: _GlobalAnalyticsConsentBanner(
|
|
645
|
+
controller: _controller,
|
|
646
|
+
),
|
|
647
|
+
),
|
|
648
|
+
),
|
|
649
|
+
),
|
|
650
|
+
),
|
|
651
|
+
),
|
|
630
652
|
if (_supportsDesktopShell &&
|
|
631
653
|
!_desktopToolbarWindowMode &&
|
|
632
654
|
!_desktopAssistantPopupWindowMode)
|
|
@@ -195,11 +195,21 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
195
195
|
final modelChoices = <DropdownMenuItem<String>>[
|
|
196
196
|
const DropdownMenuItem<String>(
|
|
197
197
|
value: 'auto',
|
|
198
|
-
child: Text(
|
|
198
|
+
child: Text(
|
|
199
|
+
'Smart Selector (Auto)',
|
|
200
|
+
maxLines: 1,
|
|
201
|
+
overflow: TextOverflow.ellipsis,
|
|
202
|
+
),
|
|
199
203
|
),
|
|
200
204
|
...routingModels.map(
|
|
201
|
-
(model) =>
|
|
202
|
-
|
|
205
|
+
(model) => DropdownMenuItem<String>(
|
|
206
|
+
value: model.id,
|
|
207
|
+
child: Text(
|
|
208
|
+
model.label,
|
|
209
|
+
maxLines: 1,
|
|
210
|
+
overflow: TextOverflow.ellipsis,
|
|
211
|
+
),
|
|
212
|
+
),
|
|
203
213
|
),
|
|
204
214
|
];
|
|
205
215
|
final enabledSmartModels = _enabledModels
|
|
@@ -634,7 +644,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
634
644
|
.map(
|
|
635
645
|
(model) => DropdownMenuItem<String>(
|
|
636
646
|
value: model.id,
|
|
637
|
-
child: Text(
|
|
647
|
+
child: Text(
|
|
648
|
+
model.label,
|
|
649
|
+
maxLines: 1,
|
|
650
|
+
overflow: TextOverflow.ellipsis,
|
|
651
|
+
),
|
|
638
652
|
),
|
|
639
653
|
)
|
|
640
654
|
.toList(),
|
|
@@ -2030,6 +2044,7 @@ class _RoutingSelectCard extends StatelessWidget {
|
|
|
2030
2044
|
DropdownButtonFormField<String>(
|
|
2031
2045
|
initialValue: value,
|
|
2032
2046
|
items: items,
|
|
2047
|
+
isExpanded: true,
|
|
2033
2048
|
decoration: const InputDecoration(isDense: true),
|
|
2034
2049
|
onChanged: onChanged,
|
|
2035
2050
|
),
|
|
@@ -1826,6 +1826,167 @@ class _GlobalWebUpdateBanner extends StatelessWidget {
|
|
|
1826
1826
|
}
|
|
1827
1827
|
}
|
|
1828
1828
|
|
|
1829
|
+
class _GlobalAnalyticsConsentBanner extends StatelessWidget {
|
|
1830
|
+
const _GlobalAnalyticsConsentBanner({required this.controller});
|
|
1831
|
+
|
|
1832
|
+
final NeoAgentController controller;
|
|
1833
|
+
|
|
1834
|
+
@override
|
|
1835
|
+
Widget build(BuildContext context) {
|
|
1836
|
+
return LayoutBuilder(
|
|
1837
|
+
builder: (context, constraints) {
|
|
1838
|
+
final compact = constraints.maxWidth < 700;
|
|
1839
|
+
final body = Text(
|
|
1840
|
+
'NeoAgent uses anonymous analytics to understand startup, navigation, and setup flows. No messages, credentials, or personal content are collected.',
|
|
1841
|
+
style: TextStyle(color: _textSecondary, height: 1.35),
|
|
1842
|
+
);
|
|
1843
|
+
return Material(
|
|
1844
|
+
color: Colors.transparent,
|
|
1845
|
+
child: _GlassSurface(
|
|
1846
|
+
borderRadius: BorderRadius.circular(22),
|
|
1847
|
+
blurSigma: 26,
|
|
1848
|
+
fillColor: _bgCard.withValues(alpha: 0.94),
|
|
1849
|
+
borderColor: _accent.withValues(alpha: 0.18),
|
|
1850
|
+
overlayGradient: LinearGradient(
|
|
1851
|
+
colors: <Color>[
|
|
1852
|
+
_accent.withValues(alpha: 0.12),
|
|
1853
|
+
_bgCard.withValues(alpha: 0.88),
|
|
1854
|
+
_bgSecondary.withValues(alpha: 0.86),
|
|
1855
|
+
],
|
|
1856
|
+
stops: const <double>[0, 0.22, 1],
|
|
1857
|
+
begin: const Alignment(-1, -1),
|
|
1858
|
+
end: const Alignment(1, 1),
|
|
1859
|
+
),
|
|
1860
|
+
boxShadow: <BoxShadow>[
|
|
1861
|
+
BoxShadow(
|
|
1862
|
+
color: Colors.black.withValues(alpha: 0.22),
|
|
1863
|
+
blurRadius: 34,
|
|
1864
|
+
offset: const Offset(0, 18),
|
|
1865
|
+
),
|
|
1866
|
+
BoxShadow(
|
|
1867
|
+
color: _accent.withValues(alpha: 0.1),
|
|
1868
|
+
blurRadius: 24,
|
|
1869
|
+
offset: const Offset(0, 8),
|
|
1870
|
+
),
|
|
1871
|
+
],
|
|
1872
|
+
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
|
|
1873
|
+
child: compact
|
|
1874
|
+
? Column(
|
|
1875
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1876
|
+
children: <Widget>[
|
|
1877
|
+
Row(
|
|
1878
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1879
|
+
children: <Widget>[
|
|
1880
|
+
Container(
|
|
1881
|
+
padding: const EdgeInsets.all(10),
|
|
1882
|
+
decoration: BoxDecoration(
|
|
1883
|
+
color: _accent.withValues(alpha: 0.16),
|
|
1884
|
+
shape: BoxShape.circle,
|
|
1885
|
+
border: Border.all(
|
|
1886
|
+
color: _accent.withValues(alpha: 0.24),
|
|
1887
|
+
),
|
|
1888
|
+
),
|
|
1889
|
+
child: Icon(
|
|
1890
|
+
Icons.cookie_outlined,
|
|
1891
|
+
color: _accent,
|
|
1892
|
+
size: 22,
|
|
1893
|
+
),
|
|
1894
|
+
),
|
|
1895
|
+
const SizedBox(width: 12),
|
|
1896
|
+
Expanded(
|
|
1897
|
+
child: Column(
|
|
1898
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1899
|
+
children: <Widget>[
|
|
1900
|
+
Text(
|
|
1901
|
+
'Analytics cookies',
|
|
1902
|
+
style: TextStyle(
|
|
1903
|
+
color: _textPrimary,
|
|
1904
|
+
fontSize: 15,
|
|
1905
|
+
fontWeight: FontWeight.w700,
|
|
1906
|
+
),
|
|
1907
|
+
),
|
|
1908
|
+
const SizedBox(height: 6),
|
|
1909
|
+
body,
|
|
1910
|
+
],
|
|
1911
|
+
),
|
|
1912
|
+
),
|
|
1913
|
+
],
|
|
1914
|
+
),
|
|
1915
|
+
const SizedBox(height: 14),
|
|
1916
|
+
Row(
|
|
1917
|
+
children: <Widget>[
|
|
1918
|
+
Expanded(
|
|
1919
|
+
child: OutlinedButton(
|
|
1920
|
+
onPressed: controller.declineAnalyticsConsent,
|
|
1921
|
+
child: const Text('Decline'),
|
|
1922
|
+
),
|
|
1923
|
+
),
|
|
1924
|
+
const SizedBox(width: 10),
|
|
1925
|
+
Expanded(
|
|
1926
|
+
child: FilledButton(
|
|
1927
|
+
onPressed: controller.acceptAnalyticsConsent,
|
|
1928
|
+
child: const Text('Allow analytics'),
|
|
1929
|
+
),
|
|
1930
|
+
),
|
|
1931
|
+
],
|
|
1932
|
+
),
|
|
1933
|
+
],
|
|
1934
|
+
)
|
|
1935
|
+
: Row(
|
|
1936
|
+
crossAxisAlignment: CrossAxisAlignment.center,
|
|
1937
|
+
children: <Widget>[
|
|
1938
|
+
Container(
|
|
1939
|
+
padding: const EdgeInsets.all(10),
|
|
1940
|
+
decoration: BoxDecoration(
|
|
1941
|
+
color: _accent.withValues(alpha: 0.16),
|
|
1942
|
+
shape: BoxShape.circle,
|
|
1943
|
+
border: Border.all(
|
|
1944
|
+
color: _accent.withValues(alpha: 0.24),
|
|
1945
|
+
),
|
|
1946
|
+
),
|
|
1947
|
+
child: Icon(
|
|
1948
|
+
Icons.cookie_outlined,
|
|
1949
|
+
color: _accent,
|
|
1950
|
+
size: 22,
|
|
1951
|
+
),
|
|
1952
|
+
),
|
|
1953
|
+
const SizedBox(width: 14),
|
|
1954
|
+
Expanded(
|
|
1955
|
+
child: Column(
|
|
1956
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1957
|
+
children: <Widget>[
|
|
1958
|
+
Text(
|
|
1959
|
+
'Analytics cookies',
|
|
1960
|
+
style: TextStyle(
|
|
1961
|
+
color: _textPrimary,
|
|
1962
|
+
fontSize: 15,
|
|
1963
|
+
fontWeight: FontWeight.w700,
|
|
1964
|
+
),
|
|
1965
|
+
),
|
|
1966
|
+
const SizedBox(height: 6),
|
|
1967
|
+
body,
|
|
1968
|
+
],
|
|
1969
|
+
),
|
|
1970
|
+
),
|
|
1971
|
+
const SizedBox(width: 16),
|
|
1972
|
+
OutlinedButton(
|
|
1973
|
+
onPressed: controller.declineAnalyticsConsent,
|
|
1974
|
+
child: const Text('Decline'),
|
|
1975
|
+
),
|
|
1976
|
+
const SizedBox(width: 10),
|
|
1977
|
+
FilledButton(
|
|
1978
|
+
onPressed: controller.acceptAnalyticsConsent,
|
|
1979
|
+
child: const Text('Allow analytics'),
|
|
1980
|
+
),
|
|
1981
|
+
],
|
|
1982
|
+
),
|
|
1983
|
+
),
|
|
1984
|
+
);
|
|
1985
|
+
},
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1829
1990
|
class _DesktopCloseDecision {
|
|
1830
1991
|
const _DesktopCloseDecision({
|
|
1831
1992
|
required this.keepRunning,
|