diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index b808bc3..07a99b6 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -1447,6 +1447,15 @@ Future sendMessageFromService( await _sendMessageInternal(ref, message, attachments, toolIds); } +Future sendMessageWithContainer( + ProviderContainer container, + String message, + List? attachments, [ + List? toolIds, +]) async { + await _sendMessageInternal(container, message, attachments, toolIds); +} + // Internal send message implementation Future _sendMessageInternal( dynamic ref, diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index e3a7ec8..8ea9834 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -372,10 +372,9 @@ class _ChatPageState extends ConsumerState { // Scroll to bottom after enqueuing (only if user was near bottom) WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { - final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; // Only auto-scroll if user was already near the bottom (within 300px) - if (maxScroll - currentScroll < 300) { + if (currentScroll <= 300) { _scrollToBottom(); } } @@ -543,26 +542,18 @@ class _ChatPageState extends ConsumerState { _scrollDebounceTimer = Timer(const Duration(milliseconds: 80), () { if (!mounted || _isDeactivated || !_scrollController.hasClients) return; - final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; + final maxScroll = _scrollController.position.maxScrollExtent; - // Hysteresis thresholds to avoid flicker - const double showThreshold = - 300.0; // show when farther than this from bottom - const double hideThreshold = - 150.0; // hide when within this distance of bottom + const double showThreshold = 300.0; + const double hideThreshold = 150.0; - final bool farFromBottom = currentScroll < (maxScroll - showThreshold); - final bool nearBottom = currentScroll >= (maxScroll - hideThreshold); + final bool farFromBottom = currentScroll > showThreshold; + final bool nearBottom = currentScroll <= hideThreshold; - bool showButton; - if (_showScrollToBottom) { - // Currently shown: keep it until we are near the bottom - showButton = !nearBottom && maxScroll > showThreshold; - } else { - // Currently hidden: only show when far from bottom - showButton = farFromBottom && maxScroll > showThreshold; - } + final bool showButton = _showScrollToBottom + ? !nearBottom && maxScroll > showThreshold + : farFromBottom && maxScroll > showThreshold; if (showButton != _showScrollToBottom && mounted && !_isDeactivated) { setState(() { @@ -575,17 +566,15 @@ class _ChatPageState extends ConsumerState { void _scrollToBottom({bool smooth = true}) { if (!_scrollController.hasClients) return; - final maxScroll = _scrollController.position.maxScrollExtent; - if (maxScroll <= 0) return; - + final target = 0.0; if (smooth) { _scrollController.animateTo( - maxScroll, + target, duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, ); } else { - _scrollController.jumpTo(maxScroll); + _scrollController.jumpTo(target); } } @@ -742,6 +731,7 @@ class _ChatPageState extends ConsumerState { scrollController: _scrollController, physics: const AlwaysScrollableScrollPhysics(), items: messages, + reverse: true, padding: const EdgeInsets.fromLTRB( Spacing.lg, Spacing.md, @@ -1029,9 +1019,8 @@ class _ChatPageState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (_scrollController.hasClients) { - final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; - if (maxScroll - currentScroll < 300) { + if (currentScroll <= 300) { _scrollToBottom(smooth: true); } } diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index b9ec5ae..d3c23ad 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -18,7 +18,7 @@ import 'enhanced_attachment.dart'; import 'package:conduit/shared/widgets/chat_action_button.dart'; import '../../../shared/widgets/model_avatar.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../providers/chat_providers.dart' show sendMessage; +import '../providers/chat_providers.dart' show sendMessageWithContainer; import '../../../core/utils/debug_logger.dart'; import 'sources/openwebui_sources.dart'; import '../providers/assistant_response_builder_provider.dart'; @@ -70,7 +70,8 @@ class _AssistantMessageWidgetState extends ConsumerState return; } try { - await sendMessage(ref, trimmed, null); + final container = ProviderScope.containerOf(context, listen: false); + await sendMessageWithContainer(container, trimmed, null); } catch (err, stack) { DebugLogger.log( 'Failed to send follow-up: $err', diff --git a/lib/shared/widgets/optimized_list.dart b/lib/shared/widgets/optimized_list.dart index 6635f68..8df72e7 100644 --- a/lib/shared/widgets/optimized_list.dart +++ b/lib/shared/widgets/optimized_list.dart @@ -146,6 +146,8 @@ class _OptimizedListState extends ConsumerState> { ? const AlwaysScrollableScrollPhysics() : const ClampingScrollPhysics()); + final reverse = widget.reverse; + if (widget.separatorBuilder != null) { listWidget = ListView.separated( controller: _scrollController, @@ -154,7 +156,7 @@ class _OptimizedListState extends ConsumerState> { keyboardDismissBehavior: widget.keyboardDismissBehavior, shrinkWrap: widget.shrinkWrap, scrollDirection: widget.scrollDirection, - reverse: widget.reverse, + reverse: reverse, cacheExtent: widget.cacheExtent ?? 250.0, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, addRepaintBoundaries: widget.addRepaintBoundaries, @@ -165,7 +167,7 @@ class _OptimizedListState extends ConsumerState> { return _buildLoadMoreIndicator(); } - return _buildOptimizedItem(context, index); + return _buildOptimizedItem(context, index, reverse: reverse); }, ); } else { @@ -176,7 +178,7 @@ class _OptimizedListState extends ConsumerState> { keyboardDismissBehavior: widget.keyboardDismissBehavior, shrinkWrap: widget.shrinkWrap, scrollDirection: widget.scrollDirection, - reverse: widget.reverse, + reverse: reverse, cacheExtent: widget.cacheExtent ?? 250.0, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, addRepaintBoundaries: widget.addRepaintBoundaries, @@ -187,7 +189,7 @@ class _OptimizedListState extends ConsumerState> { return _buildLoadMoreIndicator(); } - return _buildOptimizedItem(context, index); + return _buildOptimizedItem(context, index, reverse: reverse); }, ); } @@ -200,15 +202,21 @@ class _OptimizedListState extends ConsumerState> { return listWidget; } - Widget _buildOptimizedItem(BuildContext context, int index) { - final item = widget.items[index]; + Widget _buildOptimizedItem( + BuildContext context, + int index, { + required bool reverse, + }) { + final effectiveIndex = reverse ? widget.items.length - index - 1 : index; + final item = widget.items[effectiveIndex]; - // Wrap in repaint boundary for performance if (widget.addRepaintBoundaries) { - return RepaintBoundary(child: widget.itemBuilder(context, item, index)); + return RepaintBoundary( + child: widget.itemBuilder(context, item, effectiveIndex), + ); } - return widget.itemBuilder(context, item, index); + return widget.itemBuilder(context, item, effectiveIndex); } Widget _buildLoadMoreIndicator() {