neoagent 2.4.4-beta.0 → 2.4.4-beta.11
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 +17 -0
- package/README.md +9 -3
- package/docs/capabilities.md +16 -7
- package/docs/configuration.md +1 -0
- package/docs/getting-started.md +6 -0
- package/docs/index.md +1 -0
- package/docs/security-boundaries.md +122 -0
- package/docs/supermemory-memory-review.md +852 -0
- package/flutter_app/lib/features/memory/views/retrieval_inspector_view.dart +128 -0
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +79 -15
- package/flutter_app/lib/main_app_shell.dart +22 -0
- package/flutter_app/lib/main_chat.dart +155 -8
- package/flutter_app/lib/main_controller.dart +74 -5
- package/flutter_app/lib/main_devices.dart +9 -3
- package/flutter_app/lib/main_models.dart +32 -0
- package/flutter_app/lib/main_operations.dart +13 -0
- package/flutter_app/lib/main_security.dart +967 -0
- package/flutter_app/lib/main_settings.dart +56 -0
- package/flutter_app/lib/src/backend_client.dart +60 -3
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
- package/flutter_app/pubspec.lock +32 -0
- package/flutter_app/pubspec.yaml +1 -0
- package/lib/install_helpers.js +1 -0
- package/lib/manager.js +63 -1
- package/lib/schema_migrations.js +262 -0
- package/package.json +4 -2
- package/server/admin/admin.js +151 -0
- package/server/admin/index.html +55 -3
- package/server/db/database.js +3 -0
- package/server/http/routes.js +2 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +86 -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 +82243 -80308
- package/server/routes/account.js +2 -23
- package/server/routes/admin.js +18 -2
- package/server/routes/agents.js +5 -1
- package/server/routes/memory.js +41 -4
- package/server/routes/security.js +112 -0
- package/server/services/account/service_email_settings.js +167 -0
- package/server/services/ai/engine.js +269 -27
- package/server/services/ai/rate_limits.js +150 -0
- package/server/services/ai/systemPrompt.js +26 -3
- package/server/services/ai/tools.js +11 -8
- package/server/services/cli/shell_worker.js +135 -0
- package/server/services/cli/shell_worker_pool.js +125 -0
- package/server/services/integrations/google/gmail.js +7 -7
- package/server/services/manager.js +20 -1
- package/server/services/memory/consolidation.js +111 -0
- package/server/services/memory/embedding_index.js +175 -0
- package/server/services/memory/embeddings.js +22 -2
- package/server/services/memory/evaluation.js +187 -0
- package/server/services/memory/ingestion_chunking.js +191 -0
- package/server/services/memory/ingestion_documents.js +96 -26
- package/server/services/memory/intelligence.js +3 -1
- package/server/services/memory/manager.js +855 -43
- package/server/services/memory/policy.js +0 -40
- package/server/services/memory/retrieval_reasoning.js +191 -0
- package/server/services/runtime/manager.js +7 -0
- package/server/services/security/approval_gate_service.js +93 -0
- package/server/services/security/tool_categories.js +105 -0
- package/server/services/security/tool_policy_service.js +92 -0
- package/server/services/security/tool_security_hook.js +77 -0
- package/server/services/websocket.js +5 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../../../main.dart'; // To access NeoAgentController
|
|
3
|
+
|
|
4
|
+
class RetrievalInspectorView extends StatefulWidget {
|
|
5
|
+
final NeoAgentController controller;
|
|
6
|
+
|
|
7
|
+
const RetrievalInspectorView({super.key, required this.controller});
|
|
8
|
+
|
|
9
|
+
@override
|
|
10
|
+
State<RetrievalInspectorView> createState() => _RetrievalInspectorViewState();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class _RetrievalInspectorViewState extends State<RetrievalInspectorView> {
|
|
14
|
+
final _queryController = TextEditingController();
|
|
15
|
+
bool _isLoading = false;
|
|
16
|
+
Map<String, dynamic>? _results;
|
|
17
|
+
String? _error;
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
void dispose() {
|
|
21
|
+
_queryController.dispose();
|
|
22
|
+
super.dispose();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Future<void> _inspect() async {
|
|
26
|
+
final query = _queryController.text.trim();
|
|
27
|
+
if (query.isEmpty) return;
|
|
28
|
+
|
|
29
|
+
setState(() {
|
|
30
|
+
_isLoading = true;
|
|
31
|
+
_error = null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
final res = await widget.controller.inspectMemory(query);
|
|
36
|
+
if (!mounted) return;
|
|
37
|
+
setState(() {
|
|
38
|
+
_results = res;
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (!mounted) return;
|
|
42
|
+
setState(() {
|
|
43
|
+
_error = e.toString();
|
|
44
|
+
});
|
|
45
|
+
} finally {
|
|
46
|
+
if (mounted) {
|
|
47
|
+
setState(() {
|
|
48
|
+
_isLoading = false;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
Widget build(BuildContext context) {
|
|
56
|
+
return Scaffold(
|
|
57
|
+
appBar: AppBar(
|
|
58
|
+
title: const Text('Retrieval Inspector'),
|
|
59
|
+
),
|
|
60
|
+
body: Padding(
|
|
61
|
+
padding: const EdgeInsets.all(16.0),
|
|
62
|
+
child: Column(
|
|
63
|
+
children: [
|
|
64
|
+
Row(
|
|
65
|
+
children: [
|
|
66
|
+
Expanded(
|
|
67
|
+
child: TextField(
|
|
68
|
+
controller: _queryController,
|
|
69
|
+
decoration: const InputDecoration(
|
|
70
|
+
labelText: 'Query',
|
|
71
|
+
border: OutlineInputBorder(),
|
|
72
|
+
),
|
|
73
|
+
onSubmitted: (_) => _inspect(),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
const SizedBox(width: 8),
|
|
77
|
+
ElevatedButton(
|
|
78
|
+
onPressed: _isLoading ? null : _inspect,
|
|
79
|
+
child: const Text('Inspect'),
|
|
80
|
+
),
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
const SizedBox(height: 16),
|
|
84
|
+
if (_isLoading)
|
|
85
|
+
const CircularProgressIndicator()
|
|
86
|
+
else if (_error != null)
|
|
87
|
+
Text('Error: $_error', style: const TextStyle(color: Colors.red))
|
|
88
|
+
else if (_results != null)
|
|
89
|
+
Expanded(
|
|
90
|
+
child: ListView.builder(
|
|
91
|
+
itemCount: (_results!['results'] as List?)?.length ?? 0,
|
|
92
|
+
itemBuilder: (context, index) {
|
|
93
|
+
final item = _results!['results'][index];
|
|
94
|
+
final breakdown = item['scoreBreakdown'] ?? {};
|
|
95
|
+
return Card(
|
|
96
|
+
margin: const EdgeInsets.only(bottom: 8),
|
|
97
|
+
child: Padding(
|
|
98
|
+
padding: const EdgeInsets.all(12.0),
|
|
99
|
+
child: Column(
|
|
100
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
101
|
+
children: [
|
|
102
|
+
Text(
|
|
103
|
+
'Score: ${item['score']?.toStringAsFixed(3) ?? '?'}',
|
|
104
|
+
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
105
|
+
),
|
|
106
|
+
Text('Content: ${item['content']}'),
|
|
107
|
+
const SizedBox(height: 8),
|
|
108
|
+
const Text('Breakdown:', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
109
|
+
Text('Semantic: ${breakdown['semantic']?.toStringAsFixed(3)}'),
|
|
110
|
+
Text('Lexical: ${breakdown['lexical']?.toStringAsFixed(3)}'),
|
|
111
|
+
Text('Full Text (FTS): ${breakdown['fullText']?.toStringAsFixed(3)}'),
|
|
112
|
+
Text('Entity: ${breakdown['entity']?.toStringAsFixed(3)}'),
|
|
113
|
+
Text('Relation: ${breakdown['relation']?.toStringAsFixed(3)}'),
|
|
114
|
+
Text('Candidate Count: ${breakdown['candidateCount']}'),
|
|
115
|
+
Text('Vector Rank: ${breakdown['vectorCandidateRank']}'),
|
|
116
|
+
],
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
],
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart';
|
|
|
11
11
|
import 'package:flutter/gestures.dart';
|
|
12
12
|
import 'package:flutter/material.dart';
|
|
13
13
|
import 'package:flutter/services.dart';
|
|
14
|
+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
14
15
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
|
15
16
|
import 'package:file_picker/file_picker.dart';
|
|
16
17
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
@@ -48,6 +49,7 @@ import 'src/android_auto_bridge.dart';
|
|
|
48
49
|
import 'features/location/location_service.dart';
|
|
49
50
|
import 'features/notifications/notification_interceptor.dart';
|
|
50
51
|
import 'features/onboarding/onboarding_shell.dart';
|
|
52
|
+
import 'features/memory/views/retrieval_inspector_view.dart';
|
|
51
53
|
|
|
52
54
|
part 'main_spacing.dart';
|
|
53
55
|
part 'main_theme.dart';
|
|
@@ -65,6 +67,7 @@ part 'main_recordings.dart';
|
|
|
65
67
|
part 'main_chat.dart';
|
|
66
68
|
part 'main_account_settings.dart';
|
|
67
69
|
part 'main_settings.dart';
|
|
70
|
+
part 'main_security.dart';
|
|
68
71
|
part 'main_model_picker.dart';
|
|
69
72
|
part 'main_operations.dart';
|
|
70
73
|
part 'main_admin.dart';
|
|
@@ -403,7 +403,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
403
403
|
controller: _displayNameController,
|
|
404
404
|
decoration: const InputDecoration(
|
|
405
405
|
labelText: 'Display name',
|
|
406
|
-
helperText:
|
|
406
|
+
helperText:
|
|
407
|
+
'Shown in the sidebar. Leave blank to use your username.',
|
|
407
408
|
),
|
|
408
409
|
),
|
|
409
410
|
if (_displayNameInlineError != null) ...<Widget>[
|
|
@@ -607,6 +608,16 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
607
608
|
return amount.toString();
|
|
608
609
|
}
|
|
609
610
|
|
|
611
|
+
String? _nextUsageDropLabel(DateTime? value) {
|
|
612
|
+
if (value == null) return null;
|
|
613
|
+
final remaining = value.difference(DateTime.now());
|
|
614
|
+
if (remaining.isNegative) return 'Usage updates shortly';
|
|
615
|
+
if (remaining.inHours > 0) {
|
|
616
|
+
return 'Next usage drop in ${remaining.inHours}h ${remaining.inMinutes.remainder(60)}m';
|
|
617
|
+
}
|
|
618
|
+
return 'Next usage drop in ${remaining.inMinutes + 1}m';
|
|
619
|
+
}
|
|
620
|
+
|
|
610
621
|
Widget _buildUsagePanel() {
|
|
611
622
|
if (widget.controller.isLoadingAccountSettings) {
|
|
612
623
|
return const Center(
|
|
@@ -616,7 +627,7 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
616
627
|
),
|
|
617
628
|
);
|
|
618
629
|
}
|
|
619
|
-
|
|
630
|
+
|
|
620
631
|
final usage = widget.controller.usageAndLimits;
|
|
621
632
|
if (usage == null) {
|
|
622
633
|
return Center(
|
|
@@ -639,12 +650,21 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
639
650
|
);
|
|
640
651
|
}
|
|
641
652
|
|
|
642
|
-
Widget buildStatBox(
|
|
653
|
+
Widget buildStatBox(
|
|
654
|
+
String label,
|
|
655
|
+
int current,
|
|
656
|
+
int? limit, {
|
|
657
|
+
required int remaining,
|
|
658
|
+
required bool reached,
|
|
659
|
+
required DateTime? nextDecreaseAt,
|
|
660
|
+
bool isCustom = false,
|
|
661
|
+
}) {
|
|
643
662
|
final double progress = limit != null
|
|
644
663
|
? (limit <= 0 ? 1.0 : (current / limit).clamp(0.0, 1.0))
|
|
645
664
|
: 0.0;
|
|
646
665
|
final bool nearLimit = progress > 0.8;
|
|
647
|
-
final bool atLimit = progress >= 1.0;
|
|
666
|
+
final bool atLimit = reached || progress >= 1.0;
|
|
667
|
+
final nextDrop = _nextUsageDropLabel(nextDecreaseAt);
|
|
648
668
|
|
|
649
669
|
return Container(
|
|
650
670
|
width: double.infinity,
|
|
@@ -656,8 +676,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
656
676
|
color: atLimit
|
|
657
677
|
? _danger.withValues(alpha: 0.6)
|
|
658
678
|
: nearLimit
|
|
659
|
-
|
|
660
|
-
|
|
679
|
+
? _warning.withValues(alpha: 0.5)
|
|
680
|
+
: _borderLight,
|
|
661
681
|
),
|
|
662
682
|
),
|
|
663
683
|
child: Column(
|
|
@@ -665,11 +685,20 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
665
685
|
children: <Widget>[
|
|
666
686
|
Row(
|
|
667
687
|
children: <Widget>[
|
|
668
|
-
Text(
|
|
688
|
+
Text(
|
|
689
|
+
label,
|
|
690
|
+
style: const TextStyle(
|
|
691
|
+
fontSize: 16,
|
|
692
|
+
fontWeight: FontWeight.w600,
|
|
693
|
+
),
|
|
694
|
+
),
|
|
669
695
|
const Spacer(),
|
|
670
696
|
if (limit != null)
|
|
671
697
|
Container(
|
|
672
|
-
padding: const EdgeInsets.symmetric(
|
|
698
|
+
padding: const EdgeInsets.symmetric(
|
|
699
|
+
horizontal: 8,
|
|
700
|
+
vertical: 3,
|
|
701
|
+
),
|
|
673
702
|
decoration: BoxDecoration(
|
|
674
703
|
color: isCustom
|
|
675
704
|
? _warning.withValues(alpha: 0.12)
|
|
@@ -696,7 +725,11 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
696
725
|
style: TextStyle(
|
|
697
726
|
fontSize: 28,
|
|
698
727
|
fontWeight: FontWeight.w800,
|
|
699
|
-
color: atLimit
|
|
728
|
+
color: atLimit
|
|
729
|
+
? _danger
|
|
730
|
+
: nearLimit
|
|
731
|
+
? _warning
|
|
732
|
+
: null,
|
|
700
733
|
),
|
|
701
734
|
),
|
|
702
735
|
Padding(
|
|
@@ -728,16 +761,27 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
728
761
|
value: progress,
|
|
729
762
|
backgroundColor: _border,
|
|
730
763
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
731
|
-
atLimit
|
|
764
|
+
atLimit
|
|
765
|
+
? _danger
|
|
766
|
+
: nearLimit
|
|
767
|
+
? _warning
|
|
768
|
+
: _accent,
|
|
732
769
|
),
|
|
733
770
|
),
|
|
734
771
|
),
|
|
735
772
|
const SizedBox(height: 6),
|
|
736
773
|
Text(
|
|
737
|
-
|
|
774
|
+
atLimit
|
|
775
|
+
? 'Limit reached${nextDrop == null ? '' : ' · $nextDrop'}'
|
|
776
|
+
: '${(progress * 100).toStringAsFixed(0)}% used · ${_formatTokens(remaining)} remaining'
|
|
777
|
+
'${nextDrop == null ? '' : ' · $nextDrop'}',
|
|
738
778
|
style: TextStyle(
|
|
739
779
|
fontSize: 11,
|
|
740
|
-
color: atLimit
|
|
780
|
+
color: atLimit
|
|
781
|
+
? _danger
|
|
782
|
+
: nearLimit
|
|
783
|
+
? _warning
|
|
784
|
+
: _textMuted,
|
|
741
785
|
),
|
|
742
786
|
),
|
|
743
787
|
],
|
|
@@ -756,9 +800,25 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
|
|
|
756
800
|
style: TextStyle(color: _textSecondary, height: 1.4),
|
|
757
801
|
),
|
|
758
802
|
const SizedBox(height: 24),
|
|
759
|
-
buildStatBox(
|
|
803
|
+
buildStatBox(
|
|
804
|
+
'Recent Usage (4 Hours)',
|
|
805
|
+
usage.fourHourUsage,
|
|
806
|
+
usage.fourHourLimit,
|
|
807
|
+
remaining: usage.fourHourRemaining,
|
|
808
|
+
reached: usage.fourHourReached,
|
|
809
|
+
nextDecreaseAt: usage.fourHourNextDecreaseAt,
|
|
810
|
+
isCustom: usage.fourHourIsCustom,
|
|
811
|
+
),
|
|
760
812
|
const SizedBox(height: 16),
|
|
761
|
-
buildStatBox(
|
|
813
|
+
buildStatBox(
|
|
814
|
+
'Weekly Usage',
|
|
815
|
+
usage.weeklyUsage,
|
|
816
|
+
usage.weeklyLimit,
|
|
817
|
+
remaining: usage.weeklyRemaining,
|
|
818
|
+
reached: usage.weeklyReached,
|
|
819
|
+
nextDecreaseAt: usage.weeklyNextDecreaseAt,
|
|
820
|
+
isCustom: usage.weeklyIsCustom,
|
|
821
|
+
),
|
|
762
822
|
],
|
|
763
823
|
);
|
|
764
824
|
}
|
|
@@ -1128,7 +1188,11 @@ class _AccountSettingsTabs extends StatelessWidget {
|
|
|
1128
1188
|
Widget build(BuildContext context) {
|
|
1129
1189
|
final buttons = <Widget>[
|
|
1130
1190
|
_tabButton(AccountSettingsTab.account, Icons.person_outline, 'Account'),
|
|
1131
|
-
_tabButton(
|
|
1191
|
+
_tabButton(
|
|
1192
|
+
AccountSettingsTab.usage,
|
|
1193
|
+
Icons.data_usage_outlined,
|
|
1194
|
+
'Usage & Limits',
|
|
1195
|
+
),
|
|
1132
1196
|
_tabButton(
|
|
1133
1197
|
AccountSettingsTab.security,
|
|
1134
1198
|
Icons.security_outlined,
|
|
@@ -1086,6 +1086,7 @@ class HomeView extends StatefulWidget {
|
|
|
1086
1086
|
|
|
1087
1087
|
class _HomeViewState extends State<HomeView> {
|
|
1088
1088
|
bool _blockedDialogOpen = false;
|
|
1089
|
+
bool _approvalSheetOpen = false;
|
|
1089
1090
|
SidebarGroup? _expandedSidebarGroup;
|
|
1090
1091
|
AppSection? _lastSelectedSection;
|
|
1091
1092
|
final GlobalKey _devicesPanelKey = GlobalKey();
|
|
@@ -1198,6 +1199,27 @@ class _HomeViewState extends State<HomeView> {
|
|
|
1198
1199
|
_showBlockedSenderDialog(pendingBlockedSender);
|
|
1199
1200
|
});
|
|
1200
1201
|
}
|
|
1202
|
+
final pendingApproval = controller.pendingApproval;
|
|
1203
|
+
if (!_approvalSheetOpen && pendingApproval != null) {
|
|
1204
|
+
_approvalSheetOpen = true;
|
|
1205
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
1206
|
+
if (!mounted) return;
|
|
1207
|
+
showModalBottomSheet<void>(
|
|
1208
|
+
context: context,
|
|
1209
|
+
isDismissible: false,
|
|
1210
|
+
enableDrag: false,
|
|
1211
|
+
shape: const RoundedRectangleBorder(
|
|
1212
|
+
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
1213
|
+
),
|
|
1214
|
+
builder: (_) => ToolApprovalSheet(
|
|
1215
|
+
request: pendingApproval,
|
|
1216
|
+
controller: controller,
|
|
1217
|
+
),
|
|
1218
|
+
).whenComplete(() {
|
|
1219
|
+
if (mounted) setState(() => _approvalSheetOpen = false);
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1201
1223
|
|
|
1202
1224
|
final wide = MediaQuery.sizeOf(context).width >= 1080;
|
|
1203
1225
|
|
|
@@ -295,12 +295,13 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
295
295
|
transitionBuilder: (ctx, animation, secondary, child) => FadeTransition(
|
|
296
296
|
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
|
297
297
|
child: SlideTransition(
|
|
298
|
-
position:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
298
|
+
position:
|
|
299
|
+
Tween<Offset>(
|
|
300
|
+
begin: const Offset(0, 0.04),
|
|
301
|
+
end: Offset.zero,
|
|
302
|
+
).animate(
|
|
303
|
+
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic),
|
|
304
|
+
),
|
|
304
305
|
child: child,
|
|
305
306
|
),
|
|
306
307
|
),
|
|
@@ -398,6 +399,11 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
398
399
|
_maybeFollowChatContent(messages, controller);
|
|
399
400
|
|
|
400
401
|
final threadChildren = <Widget>[
|
|
402
|
+
if (controller.usageAndLimits case final usage?
|
|
403
|
+
when usage.hasLimits) ...<Widget>[
|
|
404
|
+
_RateLimitStatusCard(usage: usage),
|
|
405
|
+
const SizedBox(height: 16),
|
|
406
|
+
],
|
|
401
407
|
if (controller.errorMessage != null) ...<Widget>[
|
|
402
408
|
_InlineError(message: controller.errorMessage!),
|
|
403
409
|
const SizedBox(height: 16),
|
|
@@ -471,8 +477,12 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
471
477
|
SelectionArea(
|
|
472
478
|
child: ListView(
|
|
473
479
|
controller: _scrollController,
|
|
474
|
-
padding:
|
|
475
|
-
|
|
480
|
+
padding: EdgeInsets.fromLTRB(
|
|
481
|
+
sidePadding,
|
|
482
|
+
30,
|
|
483
|
+
sidePadding,
|
|
484
|
+
18,
|
|
485
|
+
),
|
|
476
486
|
children: <Widget>[
|
|
477
487
|
Center(
|
|
478
488
|
child: ConstrainedBox(
|
|
@@ -715,6 +725,143 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
715
725
|
}
|
|
716
726
|
}
|
|
717
727
|
|
|
728
|
+
class _RateLimitStatusCard extends StatelessWidget {
|
|
729
|
+
const _RateLimitStatusCard({required this.usage});
|
|
730
|
+
|
|
731
|
+
final AccountUsageAndLimits usage;
|
|
732
|
+
|
|
733
|
+
String _formatTokens(int amount) {
|
|
734
|
+
if (amount >= 1000000) {
|
|
735
|
+
final value = amount / 1000000;
|
|
736
|
+
return '${value.toStringAsFixed(value == value.roundToDouble() ? 0 : 1)}M';
|
|
737
|
+
}
|
|
738
|
+
if (amount >= 1000) {
|
|
739
|
+
final value = amount / 1000;
|
|
740
|
+
return '${value.toStringAsFixed(value == value.roundToDouble() ? 0 : 1)}k';
|
|
741
|
+
}
|
|
742
|
+
return amount.toString();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
String? _nextDropLabel(DateTime? value) {
|
|
746
|
+
if (value == null) return null;
|
|
747
|
+
final remaining = value.difference(DateTime.now());
|
|
748
|
+
if (remaining.isNegative) return 'Usage updates shortly';
|
|
749
|
+
if (remaining.inHours > 0) {
|
|
750
|
+
return 'Next usage drop in ${remaining.inHours}h ${remaining.inMinutes.remainder(60)}m';
|
|
751
|
+
}
|
|
752
|
+
return 'Next usage drop in ${remaining.inMinutes + 1}m';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
Widget _buildWindow({
|
|
756
|
+
required String label,
|
|
757
|
+
required int usageAmount,
|
|
758
|
+
required int? limit,
|
|
759
|
+
required int remaining,
|
|
760
|
+
required bool reached,
|
|
761
|
+
required DateTime? nextDecreaseAt,
|
|
762
|
+
}) {
|
|
763
|
+
if (limit == null || limit <= 0) return const SizedBox.shrink();
|
|
764
|
+
final progress = (usageAmount / limit).clamp(0.0, 1.0);
|
|
765
|
+
final color = reached
|
|
766
|
+
? _danger
|
|
767
|
+
: progress >= 0.8
|
|
768
|
+
? _warning
|
|
769
|
+
: _accent;
|
|
770
|
+
final nextDrop = _nextDropLabel(nextDecreaseAt);
|
|
771
|
+
return Column(
|
|
772
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
773
|
+
children: <Widget>[
|
|
774
|
+
Row(
|
|
775
|
+
children: <Widget>[
|
|
776
|
+
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
777
|
+
const Spacer(),
|
|
778
|
+
Text(
|
|
779
|
+
reached ? 'Limit reached' : '${_formatTokens(remaining)} left',
|
|
780
|
+
style: TextStyle(
|
|
781
|
+
color: color,
|
|
782
|
+
fontSize: 12,
|
|
783
|
+
fontWeight: FontWeight.w600,
|
|
784
|
+
),
|
|
785
|
+
),
|
|
786
|
+
],
|
|
787
|
+
),
|
|
788
|
+
const SizedBox(height: 8),
|
|
789
|
+
ClipRRect(
|
|
790
|
+
borderRadius: BorderRadius.circular(999),
|
|
791
|
+
child: LinearProgressIndicator(
|
|
792
|
+
minHeight: 7,
|
|
793
|
+
value: progress,
|
|
794
|
+
backgroundColor: _border,
|
|
795
|
+
valueColor: AlwaysStoppedAnimation<Color>(color),
|
|
796
|
+
),
|
|
797
|
+
),
|
|
798
|
+
const SizedBox(height: 6),
|
|
799
|
+
Text(
|
|
800
|
+
'${_formatTokens(usageAmount)} / ${_formatTokens(limit)} tokens'
|
|
801
|
+
'${nextDrop == null ? '' : ' · $nextDrop'}',
|
|
802
|
+
style: TextStyle(color: _textMuted, fontSize: 11),
|
|
803
|
+
),
|
|
804
|
+
],
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
@override
|
|
809
|
+
Widget build(BuildContext context) {
|
|
810
|
+
final windows = <Widget>[
|
|
811
|
+
_buildWindow(
|
|
812
|
+
label: '4-hour usage',
|
|
813
|
+
usageAmount: usage.fourHourUsage,
|
|
814
|
+
limit: usage.fourHourLimit,
|
|
815
|
+
remaining: usage.fourHourRemaining,
|
|
816
|
+
reached: usage.fourHourReached,
|
|
817
|
+
nextDecreaseAt: usage.fourHourNextDecreaseAt,
|
|
818
|
+
),
|
|
819
|
+
_buildWindow(
|
|
820
|
+
label: '7-day usage',
|
|
821
|
+
usageAmount: usage.weeklyUsage,
|
|
822
|
+
limit: usage.weeklyLimit,
|
|
823
|
+
remaining: usage.weeklyRemaining,
|
|
824
|
+
reached: usage.weeklyReached,
|
|
825
|
+
nextDecreaseAt: usage.weeklyNextDecreaseAt,
|
|
826
|
+
),
|
|
827
|
+
];
|
|
828
|
+
return Container(
|
|
829
|
+
width: double.infinity,
|
|
830
|
+
padding: const EdgeInsets.all(14),
|
|
831
|
+
decoration: BoxDecoration(
|
|
832
|
+
color: usage.isReached ? _danger.withValues(alpha: 0.08) : _bgSecondary,
|
|
833
|
+
borderRadius: BorderRadius.circular(16),
|
|
834
|
+
border: Border.all(
|
|
835
|
+
color: usage.isReached
|
|
836
|
+
? _danger.withValues(alpha: 0.55)
|
|
837
|
+
: _borderLight,
|
|
838
|
+
),
|
|
839
|
+
),
|
|
840
|
+
child: LayoutBuilder(
|
|
841
|
+
builder: (context, constraints) {
|
|
842
|
+
if (constraints.maxWidth < 620) {
|
|
843
|
+
return Column(
|
|
844
|
+
children: <Widget>[
|
|
845
|
+
windows.first,
|
|
846
|
+
const SizedBox(height: 14),
|
|
847
|
+
windows.last,
|
|
848
|
+
],
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
return Row(
|
|
852
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
853
|
+
children: <Widget>[
|
|
854
|
+
Expanded(child: windows.first),
|
|
855
|
+
const SizedBox(width: 24),
|
|
856
|
+
Expanded(child: windows.last),
|
|
857
|
+
],
|
|
858
|
+
);
|
|
859
|
+
},
|
|
860
|
+
),
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
718
865
|
double _chatSidePadding(BuildContext context) {
|
|
719
866
|
final width = MediaQuery.sizeOf(context).width;
|
|
720
867
|
if (width >= 1280) return 40;
|