neoagent 3.0.1-beta.15 → 3.0.1-beta.16
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/flutter_app/lib/main_chat.dart +64 -0
- package/flutter_app/lib/main_controller.dart +113 -8
- package/flutter_app/lib/src/backend_client.dart +14 -1
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +53459 -53320
- package/server/routes/agents.js +99 -21
|
@@ -11,6 +11,7 @@ class ChatPanel extends StatefulWidget {
|
|
|
11
11
|
|
|
12
12
|
class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
13
13
|
static const double _autoScrollBottomThreshold = 120;
|
|
14
|
+
static const double _olderHistoryLoadThreshold = 180;
|
|
14
15
|
static const int _autoScrollSettlePasses = 4;
|
|
15
16
|
|
|
16
17
|
late final TextEditingController _composerController;
|
|
@@ -21,6 +22,7 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
21
22
|
String _lastScrollContentSignature = '';
|
|
22
23
|
bool _stickToBottom = true;
|
|
23
24
|
bool _ignoreScrollUpdates = false;
|
|
25
|
+
bool _loadingOlderHistory = false;
|
|
24
26
|
int _scrollGeneration = 0;
|
|
25
27
|
bool _isSendingChatMessage = false;
|
|
26
28
|
bool _isDictating = false;
|
|
@@ -267,8 +269,18 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
267
269
|
return pos.pixels >= pos.maxScrollExtent - _autoScrollBottomThreshold;
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
bool get _isNearTop {
|
|
273
|
+
if (!_scrollController.hasClients) return false;
|
|
274
|
+
final pos = _scrollController.position;
|
|
275
|
+
if (!pos.hasContentDimensions) return false;
|
|
276
|
+
return pos.pixels <= _olderHistoryLoadThreshold;
|
|
277
|
+
}
|
|
278
|
+
|
|
270
279
|
void _handleScrollPositionChanged() {
|
|
271
280
|
if (_ignoreScrollUpdates || !_scrollController.hasClients) return;
|
|
281
|
+
if (_isNearTop) {
|
|
282
|
+
unawaited(_maybeLoadOlderHistory());
|
|
283
|
+
}
|
|
272
284
|
final nearBottom = _isNearBottom;
|
|
273
285
|
if (_stickToBottom && !nearBottom) {
|
|
274
286
|
_scrollGeneration++;
|
|
@@ -278,6 +290,46 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
|
|
293
|
+
Future<void> _maybeLoadOlderHistory() async {
|
|
294
|
+
final controller = widget.controller;
|
|
295
|
+
if (_loadingOlderHistory ||
|
|
296
|
+
controller.isLoadingOlderChatHistory ||
|
|
297
|
+
!controller.chatHistoryHasMore ||
|
|
298
|
+
!_isNearTop) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
_loadingOlderHistory = true;
|
|
302
|
+
final hasClients = _scrollController.hasClients;
|
|
303
|
+
final previousPixels = hasClients ? _scrollController.position.pixels : 0.0;
|
|
304
|
+
final previousMaxExtent = hasClients
|
|
305
|
+
? _scrollController.position.maxScrollExtent
|
|
306
|
+
: 0.0;
|
|
307
|
+
final loaded = await controller.loadOlderChatHistory();
|
|
308
|
+
if (!mounted || !loaded) {
|
|
309
|
+
_loadingOlderHistory = false;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
313
|
+
if (!mounted || !_scrollController.hasClients) {
|
|
314
|
+
_loadingOlderHistory = false;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
final position = _scrollController.position;
|
|
318
|
+
final extentDelta = position.maxScrollExtent - previousMaxExtent;
|
|
319
|
+
final targetOffset = (previousPixels + extentDelta).clamp(
|
|
320
|
+
0.0,
|
|
321
|
+
position.maxScrollExtent,
|
|
322
|
+
);
|
|
323
|
+
_ignoreScrollUpdates = true;
|
|
324
|
+
_scrollController.jumpTo(targetOffset.toDouble());
|
|
325
|
+
_ignoreScrollUpdates = false;
|
|
326
|
+
_loadingOlderHistory = false;
|
|
327
|
+
if (_isNearTop) {
|
|
328
|
+
unawaited(_maybeLoadOlderHistory());
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
281
333
|
void _openModelPicker() {
|
|
282
334
|
final controller = widget.controller;
|
|
283
335
|
if (controller.hasLiveRun) return;
|
|
@@ -396,6 +448,18 @@ class _ChatPanelState extends State<ChatPanel> with WidgetsBindingObserver {
|
|
|
396
448
|
_maybeFollowChatContent(messages, controller);
|
|
397
449
|
|
|
398
450
|
final threadChildren = <Widget>[
|
|
451
|
+
if (controller.isLoadingOlderChatHistory) ...<Widget>[
|
|
452
|
+
const Padding(
|
|
453
|
+
padding: EdgeInsets.only(bottom: 16),
|
|
454
|
+
child: Center(
|
|
455
|
+
child: SizedBox(
|
|
456
|
+
width: 20,
|
|
457
|
+
height: 20,
|
|
458
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
),
|
|
462
|
+
],
|
|
399
463
|
if (controller.usageAndLimits case final usage?
|
|
400
464
|
when usage.hasLimits) ...<Widget>[
|
|
401
465
|
_RateLimitStatusCard(usage: usage),
|
|
@@ -66,6 +66,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
66
66
|
final Map<String, DateTime> _manualRunCooldowns = <String, DateTime>{};
|
|
67
67
|
static const Duration _manualRunCooldownDuration = Duration(seconds: 10);
|
|
68
68
|
static const Duration _homeWidgetSyncCooldown = Duration(seconds: 5);
|
|
69
|
+
static const int _chatHistoryPageSize = 40;
|
|
69
70
|
DateTime? _lastHomeWidgetSyncAt;
|
|
70
71
|
int _authCycle = 0;
|
|
71
72
|
bool _isPollingQrLogin = false;
|
|
@@ -144,6 +145,8 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
144
145
|
List<RecordingSessionItem> recordingSessions = const <RecordingSessionItem>[];
|
|
145
146
|
|
|
146
147
|
List<ChatEntry> chatMessages = const <ChatEntry>[];
|
|
148
|
+
bool chatHistoryHasMore = false;
|
|
149
|
+
bool isLoadingOlderChatHistory = false;
|
|
147
150
|
List<AgentProfile> agentProfiles = const <AgentProfile>[];
|
|
148
151
|
String? selectedAgentId;
|
|
149
152
|
List<ModelMeta> supportedModels = const <ModelMeta>[];
|
|
@@ -205,6 +208,9 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
205
208
|
String? _pendingChatDraft;
|
|
206
209
|
List<SharedChatAttachment> _pendingSharedChatAttachments =
|
|
207
210
|
const <SharedChatAttachment>[];
|
|
211
|
+
String? _chatHistoryBeforeCreatedAt;
|
|
212
|
+
String? _chatHistoryBeforeSource;
|
|
213
|
+
String? _chatHistoryBeforeId;
|
|
208
214
|
|
|
209
215
|
ActiveRunState? activeRun;
|
|
210
216
|
List<ToolEventItem> toolEvents = const <ToolEventItem>[];
|
|
@@ -500,6 +506,103 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
500
506
|
];
|
|
501
507
|
}
|
|
502
508
|
|
|
509
|
+
void _resetChatHistoryPagination() {
|
|
510
|
+
chatHistoryHasMore = false;
|
|
511
|
+
isLoadingOlderChatHistory = false;
|
|
512
|
+
_chatHistoryBeforeCreatedAt = null;
|
|
513
|
+
_chatHistoryBeforeSource = null;
|
|
514
|
+
_chatHistoryBeforeId = null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
void _applyChatHistoryCursor(Map<String, dynamic> history) {
|
|
518
|
+
chatHistoryHasMore = history['hasMore'] == true;
|
|
519
|
+
_chatHistoryBeforeCreatedAt = _optionalIdFrom(
|
|
520
|
+
history['nextBeforeCreatedAt'],
|
|
521
|
+
);
|
|
522
|
+
_chatHistoryBeforeSource = _optionalIdFrom(history['nextBeforeSource']);
|
|
523
|
+
_chatHistoryBeforeId = _optionalIdFrom(history['nextBeforeId']);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
String _chatEntryKey(ChatEntry entry) {
|
|
527
|
+
final stableId = entry.id.trim();
|
|
528
|
+
if (stableId.isNotEmpty) {
|
|
529
|
+
return [
|
|
530
|
+
stableId,
|
|
531
|
+
entry.platform,
|
|
532
|
+
entry.role,
|
|
533
|
+
entry.createdAt.toIso8601String(),
|
|
534
|
+
].join('|');
|
|
535
|
+
}
|
|
536
|
+
return [
|
|
537
|
+
entry.role,
|
|
538
|
+
entry.platform,
|
|
539
|
+
entry.createdAt.toIso8601String(),
|
|
540
|
+
entry.content,
|
|
541
|
+
].join('|');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
List<ChatEntry> _chatHistoryEntriesFromResponse(
|
|
545
|
+
Map<String, dynamic> history,
|
|
546
|
+
) {
|
|
547
|
+
return _decodeModelList(
|
|
548
|
+
'chat_history',
|
|
549
|
+
history['messages'],
|
|
550
|
+
ChatEntry.fromJson,
|
|
551
|
+
fallbackToMapValues: true,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
Future<bool> loadOlderChatHistory() async {
|
|
556
|
+
if (!isAuthenticated ||
|
|
557
|
+
isLoadingOlderChatHistory ||
|
|
558
|
+
!chatHistoryHasMore ||
|
|
559
|
+
_chatHistoryBeforeCreatedAt == null ||
|
|
560
|
+
_chatHistoryBeforeSource == null ||
|
|
561
|
+
_chatHistoryBeforeId == null) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
final agentId = _scopedAgentId;
|
|
566
|
+
final beforeCreatedAt = _chatHistoryBeforeCreatedAt;
|
|
567
|
+
final beforeSource = _chatHistoryBeforeSource;
|
|
568
|
+
final beforeId = _chatHistoryBeforeId;
|
|
569
|
+
isLoadingOlderChatHistory = true;
|
|
570
|
+
notifyListeners();
|
|
571
|
+
try {
|
|
572
|
+
final history = await _backendClient.fetchChatHistory(
|
|
573
|
+
backendUrl,
|
|
574
|
+
agentId: agentId,
|
|
575
|
+
limit: _chatHistoryPageSize,
|
|
576
|
+
beforeCreatedAt: beforeCreatedAt,
|
|
577
|
+
beforeSource: beforeSource,
|
|
578
|
+
beforeId: beforeId,
|
|
579
|
+
);
|
|
580
|
+
if (agentId != _scopedAgentId) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
final olderMessages = _chatHistoryEntriesFromResponse(history);
|
|
584
|
+
_applyChatHistoryCursor(history);
|
|
585
|
+
if (olderMessages.isEmpty) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
final existingKeys = chatMessages.map(_chatEntryKey).toSet();
|
|
589
|
+
final prepended = olderMessages
|
|
590
|
+
.where((entry) => existingKeys.add(_chatEntryKey(entry)))
|
|
591
|
+
.toList(growable: false);
|
|
592
|
+
if (prepended.isEmpty) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
chatMessages = <ChatEntry>[...prepended, ...chatMessages];
|
|
596
|
+
return true;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
errorMessage = _friendlyErrorMessage(error);
|
|
599
|
+
return false;
|
|
600
|
+
} finally {
|
|
601
|
+
isLoadingOlderChatHistory = false;
|
|
602
|
+
notifyListeners();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
503
606
|
String _settingString(String key, String fallback, {bool lowercase = false}) {
|
|
504
607
|
final value = settings[key]?.toString().trim() ?? '';
|
|
505
608
|
if (value.isEmpty) {
|
|
@@ -1561,6 +1664,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1561
1664
|
linkedAuthProviders = const <LinkedAuthProviderItem>[];
|
|
1562
1665
|
settings = const <String, dynamic>{};
|
|
1563
1666
|
chatMessages = const <ChatEntry>[];
|
|
1667
|
+
_resetChatHistoryPagination();
|
|
1564
1668
|
agentProfiles = const <AgentProfile>[];
|
|
1565
1669
|
selectedAgentId = null;
|
|
1566
1670
|
supportedModels = const <ModelMeta>[];
|
|
@@ -1723,6 +1827,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1723
1827
|
}
|
|
1724
1828
|
selectedAgentId = id;
|
|
1725
1829
|
chatMessages = const <ChatEntry>[];
|
|
1830
|
+
_resetChatHistoryPagination();
|
|
1726
1831
|
recentRuns = const <RunSummary>[];
|
|
1727
1832
|
messagingStatuses = const <String, MessagingPlatformStatus>{};
|
|
1728
1833
|
messagingMessages = const <MessagingMessage>[];
|
|
@@ -2045,8 +2150,12 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2045
2150
|
|
|
2046
2151
|
final historyFuture = _softRefreshLoad<Map<String, dynamic>>(
|
|
2047
2152
|
'chat_history',
|
|
2048
|
-
_backendClient.fetchChatHistory(
|
|
2049
|
-
|
|
2153
|
+
_backendClient.fetchChatHistory(
|
|
2154
|
+
backendUrl,
|
|
2155
|
+
agentId: agentId,
|
|
2156
|
+
limit: _chatHistoryPageSize,
|
|
2157
|
+
),
|
|
2158
|
+
const <String, dynamic>{'messages': <dynamic>[], 'hasMore': false},
|
|
2050
2159
|
);
|
|
2051
2160
|
final modelsFuture = _softRefreshLoad<Map<String, dynamic>>(
|
|
2052
2161
|
'supported_models',
|
|
@@ -2214,12 +2323,8 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2214
2323
|
return;
|
|
2215
2324
|
}
|
|
2216
2325
|
|
|
2217
|
-
chatMessages =
|
|
2218
|
-
|
|
2219
|
-
history['messages'],
|
|
2220
|
-
ChatEntry.fromJson,
|
|
2221
|
-
fallbackToMapValues: true,
|
|
2222
|
-
);
|
|
2326
|
+
chatMessages = _chatHistoryEntriesFromResponse(history);
|
|
2327
|
+
_applyChatHistoryCursor(history);
|
|
2223
2328
|
|
|
2224
2329
|
supportedModels = _decodeModelList(
|
|
2225
2330
|
'supported_models',
|
|
@@ -426,10 +426,23 @@ class BackendClient {
|
|
|
426
426
|
Future<Map<String, dynamic>> fetchChatHistory(
|
|
427
427
|
String baseUrl, {
|
|
428
428
|
String? agentId,
|
|
429
|
+
int limit = 40,
|
|
430
|
+
String? beforeCreatedAt,
|
|
431
|
+
String? beforeSource,
|
|
432
|
+
String? beforeId,
|
|
429
433
|
}) async {
|
|
434
|
+
final params = <String>[
|
|
435
|
+
'limit=$limit',
|
|
436
|
+
if (beforeCreatedAt != null && beforeCreatedAt.trim().isNotEmpty)
|
|
437
|
+
'beforeCreatedAt=${Uri.encodeQueryComponent(beforeCreatedAt.trim())}',
|
|
438
|
+
if (beforeSource != null && beforeSource.trim().isNotEmpty)
|
|
439
|
+
'beforeSource=${Uri.encodeQueryComponent(beforeSource.trim())}',
|
|
440
|
+
if (beforeId != null && beforeId.trim().isNotEmpty)
|
|
441
|
+
'beforeId=${Uri.encodeQueryComponent(beforeId.trim())}',
|
|
442
|
+
];
|
|
430
443
|
return getMap(
|
|
431
444
|
baseUrl,
|
|
432
|
-
_withAgentQuery('/api/agents/chat-history
|
|
445
|
+
_withAgentQuery('/api/agents/chat-history?${params.join('&')}', agentId),
|
|
433
446
|
);
|
|
434
447
|
}
|
|
435
448
|
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7a01035360d9803433d2977561ec9305
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "2846866146" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|