neoagent 2.3.1-beta.62 → 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/lib/main.dart +1 -0
- package/flutter_app/lib/main_controller.dart +163 -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/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 +76078 -75311
- 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.
|
|
@@ -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();
|
|
@@ -98,6 +99,10 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
98
99
|
bool networkStatusKnown = false;
|
|
99
100
|
bool _desktopFloatingToolbarPopupRequested = false;
|
|
100
101
|
bool _voiceAssistantIncludeScreenContext = false;
|
|
102
|
+
bool _didTrackAppOpen = false;
|
|
103
|
+
bool _analyticsConfigured = false;
|
|
104
|
+
bool _analyticsConsentResolved = false;
|
|
105
|
+
bool _analyticsConsentGranted = false;
|
|
101
106
|
|
|
102
107
|
bool hasUser = true;
|
|
103
108
|
bool registrationOpen = false;
|
|
@@ -319,6 +324,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
319
324
|
_diagnosticLogSubscription?.cancel();
|
|
320
325
|
_connectivitySubscription?.cancel();
|
|
321
326
|
_appReleaseUpdater.dispose();
|
|
327
|
+
unawaited(_analytics.dispose());
|
|
322
328
|
_desktopCompanion.removeListener(notifyListeners);
|
|
323
329
|
unawaited(_desktopCompanion.disconnect());
|
|
324
330
|
_recordingBridge.removeListener(_handleRecordingBridgeChanged);
|
|
@@ -349,6 +355,12 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
349
355
|
bool get canCaptureVoiceAssistantScreenContext =>
|
|
350
356
|
_desktopScreenCapture.isSupported;
|
|
351
357
|
|
|
358
|
+
bool get showAnalyticsConsentBanner =>
|
|
359
|
+
kIsWeb && _analyticsConfigured && !_analyticsConsentResolved;
|
|
360
|
+
|
|
361
|
+
bool get analyticsConsentGranted =>
|
|
362
|
+
_analyticsConsentResolved && _analyticsConsentGranted;
|
|
363
|
+
|
|
352
364
|
bool get isLiveVoiceCaptureEngaged =>
|
|
353
365
|
_isStartingLiveVoice || _liveVoiceCaptureActive;
|
|
354
366
|
|
|
@@ -701,6 +713,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
701
713
|
appUpdateAutoCheckEnabled =
|
|
702
714
|
_prefs?.getBool('app.update.autoCheckEnabled') ?? true;
|
|
703
715
|
installedAppVersion = await _safeLoadInstalledAppVersion();
|
|
716
|
+
await _initializeAnalytics();
|
|
704
717
|
await refreshConnectivityStatus();
|
|
705
718
|
if (_connectivityPluginAvailable && _connectivitySubscription == null) {
|
|
706
719
|
try {
|
|
@@ -862,6 +875,101 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
862
875
|
}
|
|
863
876
|
}
|
|
864
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
|
+
|
|
865
973
|
Future<void> setAppUpdateChannel(String channel) async {
|
|
866
974
|
final normalized = channel.trim().toLowerCase() == 'beta'
|
|
867
975
|
? 'beta'
|
|
@@ -886,6 +994,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
886
994
|
if (isCheckingAppUpdate) {
|
|
887
995
|
return;
|
|
888
996
|
}
|
|
997
|
+
unawaited(_analytics.trackAppUpdateCheck(silent: silent));
|
|
889
998
|
if (!appUpdaterConfigured) {
|
|
890
999
|
appUpdateErrorMessage = kIsWeb
|
|
891
1000
|
? 'Client app update checks are unavailable in the web app.'
|
|
@@ -975,6 +1084,11 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
975
1084
|
isBooting = true;
|
|
976
1085
|
notifyListeners();
|
|
977
1086
|
await bootstrap();
|
|
1087
|
+
unawaited(
|
|
1088
|
+
_analytics.trackBackendUrlSaved(
|
|
1089
|
+
backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
|
|
1090
|
+
),
|
|
1091
|
+
);
|
|
978
1092
|
return true;
|
|
979
1093
|
} catch (error) {
|
|
980
1094
|
errorMessage = _friendlyErrorMessage(error);
|
|
@@ -1147,6 +1261,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1147
1261
|
response,
|
|
1148
1262
|
retentionErrorMessage:
|
|
1149
1263
|
'QR login completed, but NeoAgent could not keep the session. Please try again.',
|
|
1264
|
+
authMethod: 'qr',
|
|
1150
1265
|
);
|
|
1151
1266
|
} catch (error) {
|
|
1152
1267
|
final message = _friendlyErrorMessage(error);
|
|
@@ -1199,6 +1314,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1199
1314
|
String? fallbackUsername,
|
|
1200
1315
|
String? retentionErrorMessage,
|
|
1201
1316
|
bool isRegistration = false,
|
|
1317
|
+
String authMethod = 'password',
|
|
1202
1318
|
}) async {
|
|
1203
1319
|
user = Map<String, dynamic>.from(
|
|
1204
1320
|
response['user'] as Map<dynamic, dynamic>? ??
|
|
@@ -1220,6 +1336,14 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1220
1336
|
_clearQrLoginChallenge();
|
|
1221
1337
|
await _persistCredentials();
|
|
1222
1338
|
await refresh();
|
|
1339
|
+
if (isAuthenticated) {
|
|
1340
|
+
unawaited(
|
|
1341
|
+
_analytics.trackSignedIn(
|
|
1342
|
+
authMethod: authMethod,
|
|
1343
|
+
isRegistration: isRegistration,
|
|
1344
|
+
),
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1223
1347
|
if (!isAuthenticated && retentionErrorMessage != null) {
|
|
1224
1348
|
errorMessage = retentionErrorMessage;
|
|
1225
1349
|
}
|
|
@@ -1289,6 +1413,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1289
1413
|
await _completeAuthenticatedResponse(
|
|
1290
1414
|
response,
|
|
1291
1415
|
isRegistration: register,
|
|
1416
|
+
authMethod: 'oauth',
|
|
1292
1417
|
retentionErrorMessage:
|
|
1293
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.',
|
|
1294
1419
|
);
|
|
@@ -1315,6 +1440,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1315
1440
|
await _completeAuthenticatedResponse(
|
|
1316
1441
|
response,
|
|
1317
1442
|
fallbackUsername: pendingTwoFactorUsername,
|
|
1443
|
+
authMethod: 'two_factor',
|
|
1318
1444
|
retentionErrorMessage:
|
|
1319
1445
|
'Two-factor sign-in completed, but NeoAgent could not keep the browser session. Please sign in again.',
|
|
1320
1446
|
);
|
|
@@ -1405,6 +1531,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1405
1531
|
response,
|
|
1406
1532
|
fallbackUsername: username,
|
|
1407
1533
|
isRegistration: register,
|
|
1534
|
+
authMethod: 'password',
|
|
1408
1535
|
retentionErrorMessage:
|
|
1409
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.',
|
|
1410
1537
|
);
|
|
@@ -1435,6 +1562,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1435
1562
|
final logoutFuture = _backendClient.logout(backendUrl);
|
|
1436
1563
|
_authCycle += 1;
|
|
1437
1564
|
_clearAuthenticatedState();
|
|
1565
|
+
unawaited(_analytics.trackSignedOut());
|
|
1438
1566
|
isAuthenticating = true;
|
|
1439
1567
|
notifyListeners();
|
|
1440
1568
|
|
|
@@ -1451,6 +1579,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1451
1579
|
notifyListeners();
|
|
1452
1580
|
try {
|
|
1453
1581
|
await _backendClient.completeOnboarding(backendUrl);
|
|
1582
|
+
unawaited(_analytics.trackOnboardingDismissed());
|
|
1454
1583
|
if (isAuthenticated && user != null) {
|
|
1455
1584
|
user!['hasCompletedOnboarding'] = true;
|
|
1456
1585
|
}
|
|
@@ -1604,6 +1733,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1604
1733
|
}
|
|
1605
1734
|
|
|
1606
1735
|
void setSelectedSection(AppSection section) {
|
|
1736
|
+
final previousSection = selectedSection;
|
|
1607
1737
|
selectedSection = section;
|
|
1608
1738
|
unawaited(_prefs?.setString(_selectedSectionPrefsKey, section.name));
|
|
1609
1739
|
if (section == AppSection.devices) {
|
|
@@ -1612,6 +1742,14 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1612
1742
|
if (section == AppSection.accountSettings) {
|
|
1613
1743
|
unawaited(refreshAccountSettings());
|
|
1614
1744
|
}
|
|
1745
|
+
if (previousSection != section) {
|
|
1746
|
+
unawaited(
|
|
1747
|
+
_analytics.trackSectionChanged(
|
|
1748
|
+
section: section.label,
|
|
1749
|
+
previousSection: previousSection.label,
|
|
1750
|
+
),
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1615
1753
|
notifyListeners();
|
|
1616
1754
|
}
|
|
1617
1755
|
|
|
@@ -2422,6 +2560,9 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2422
2560
|
}
|
|
2423
2561
|
|
|
2424
2562
|
Future<void> refreshWidgets({bool all = false}) async {
|
|
2563
|
+
if (all) {
|
|
2564
|
+
unawaited(_analytics.trackWidgetRefreshRequested(all: all));
|
|
2565
|
+
}
|
|
2425
2566
|
widgets = _decodeModelList(
|
|
2426
2567
|
'widgets',
|
|
2427
2568
|
await _backendClient.fetchWidgets(
|
|
@@ -3224,6 +3365,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3224
3365
|
}
|
|
3225
3366
|
|
|
3226
3367
|
Future<void> startBackgroundRecording() async {
|
|
3368
|
+
unawaited(_analytics.trackRecordingStarted(kind: 'background'));
|
|
3227
3369
|
await _startRecordingCapture(
|
|
3228
3370
|
logKey: 'start_background',
|
|
3229
3371
|
payload: buildAndroidBackgroundRecordingPayload(),
|
|
@@ -3250,6 +3392,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3250
3392
|
}
|
|
3251
3393
|
return;
|
|
3252
3394
|
}
|
|
3395
|
+
unawaited(_analytics.trackRecordingStarted(kind: 'desktop'));
|
|
3253
3396
|
await _startRecordingCapture(
|
|
3254
3397
|
logKey: 'start_desktop',
|
|
3255
3398
|
payload: buildDesktopRecordingPayload(),
|
|
@@ -3633,6 +3776,15 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3633
3776
|
notifyListeners();
|
|
3634
3777
|
try {
|
|
3635
3778
|
_desktopFloatingToolbarPopupRequested = false;
|
|
3779
|
+
unawaited(
|
|
3780
|
+
_analytics.trackRecordingStopped(
|
|
3781
|
+
kind: recordingRuntime.supportsBackgroundMic &&
|
|
3782
|
+
!recordingRuntime.supportsScreenAndMic
|
|
3783
|
+
? 'background'
|
|
3784
|
+
: 'desktop',
|
|
3785
|
+
stopReason: stopReason,
|
|
3786
|
+
),
|
|
3787
|
+
);
|
|
3636
3788
|
await _recordingBridge.stopActiveRecording();
|
|
3637
3789
|
if (isAndroidBackgroundStop) {
|
|
3638
3790
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
|
@@ -4261,6 +4413,12 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4261
4413
|
if (trimmed.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
|
|
4262
4414
|
return;
|
|
4263
4415
|
}
|
|
4416
|
+
unawaited(
|
|
4417
|
+
_analytics.trackChatMessageSent(
|
|
4418
|
+
length: trimmed.length,
|
|
4419
|
+
steeringLiveRun: canSteerLiveRun,
|
|
4420
|
+
),
|
|
4421
|
+
);
|
|
4264
4422
|
|
|
4265
4423
|
final optimistic = ChatEntry(
|
|
4266
4424
|
id: '',
|
|
@@ -4682,6 +4840,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4682
4840
|
errorMessage = null;
|
|
4683
4841
|
notifyListeners();
|
|
4684
4842
|
try {
|
|
4843
|
+
unawaited(_analytics.trackAppUpdateTriggered());
|
|
4685
4844
|
await _backendClient.triggerUpdate(backendUrl);
|
|
4686
4845
|
await refreshUpdateStatus();
|
|
4687
4846
|
} catch (error) {
|
|
@@ -5412,8 +5571,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5412
5571
|
return;
|
|
5413
5572
|
}
|
|
5414
5573
|
_pendingChatDraft = normalized;
|
|
5415
|
-
|
|
5416
|
-
notifyListeners();
|
|
5574
|
+
setSelectedSection(AppSection.chat);
|
|
5417
5575
|
}
|
|
5418
5576
|
|
|
5419
5577
|
String? takePendingChatDraft() {
|
|
@@ -5432,17 +5590,15 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5432
5590
|
|
|
5433
5591
|
void openWidgetSurface(String widgetId) {
|
|
5434
5592
|
final normalized = widgetId.trim();
|
|
5435
|
-
|
|
5593
|
+
setSelectedSection(AppSection.widgets);
|
|
5436
5594
|
if (normalized.isNotEmpty) {
|
|
5437
5595
|
_selectedWidgetId = normalized;
|
|
5438
5596
|
unawaited(refreshWidgets(all: true));
|
|
5439
5597
|
}
|
|
5440
|
-
notifyListeners();
|
|
5441
5598
|
}
|
|
5442
5599
|
|
|
5443
5600
|
void openVoiceAssistantSurface() {
|
|
5444
|
-
|
|
5445
|
-
notifyListeners();
|
|
5601
|
+
setSelectedSection(AppSection.voiceAssistant);
|
|
5446
5602
|
}
|
|
5447
5603
|
|
|
5448
5604
|
String? get selectedWidgetId => _selectedWidgetId;
|
|
@@ -5475,6 +5631,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5475
5631
|
return;
|
|
5476
5632
|
}
|
|
5477
5633
|
_startManualRunCooldown('task', '$id');
|
|
5634
|
+
unawaited(_analytics.trackTaskRunRequested(taskId: id));
|
|
5478
5635
|
await _backendClient.runSavedTask(backendUrl, id);
|
|
5479
5636
|
await refreshTasks();
|
|
5480
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,
|