diff --git a/lib/dashbot/core/providers/dashbot_active_route_provider.dart b/lib/dashbot/core/providers/dashbot_active_route_provider.dart new file mode 100644 index 00000000..23c4ea3d --- /dev/null +++ b/lib/dashbot/core/providers/dashbot_active_route_provider.dart @@ -0,0 +1,63 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:apidash/providers/providers.dart'; + +import '../routes/dashbot_routes.dart'; +import '../utils/dashbot_route_utils.dart'; + +/// A Notifier that exposes the current Dashbot active route. +/// +/// Behavior: +/// - Default state is computed from the currently selected request using +/// [computeDashbotBaseRoute]. +/// - Automatically updates when the selected request changes, *unless* the +/// route has been manually set to Chat (Chat acts as a user override). +/// - Public method [goToChat] pins the route to Chat. +/// - Public method [resetToBaseRoute] recalculates from the current request +/// (used after clearing chat, etc.). +class DashbotActiveRouteNotifier extends Notifier { + bool _chatPinned = false; + + @override + String build() { + // Watch current request for automatic base route computation. + final req = ref.watch(selectedRequestModelProvider); + ref.keepAlive(); + + // If chat is pinned we always stay on chat regardless of request changes. + if (_chatPinned) { + return DashbotRoutes.dashbotChat; + } + // Otherwise compute the base route from the current request. + return computeDashbotBaseRoute(req); + } + + void goToChat() { + if (state == DashbotRoutes.dashbotChat) return; + _chatPinned = true; + state = DashbotRoutes.dashbotChat; + } + + void resetToBaseRoute() { + _chatPinned = false; + final req = ref.read(selectedRequestModelProvider); + final target = computeDashbotBaseRoute(req); + state = target; + } + + /// Force set a specific route (used rarely; prefers semantic helpers). + void setRoute(String route) { + if (route == DashbotRoutes.dashbotChat) { + goToChat(); + return; + } + _chatPinned = false; + state = route; + } +} + +final dashbotActiveRouteProvider = + NotifierProvider( + () => DashbotActiveRouteNotifier(), + name: 'dashbotActiveRouteProvider', +); diff --git a/lib/dashbot/core/utils/dashbot_route_utils.dart b/lib/dashbot/core/utils/dashbot_route_utils.dart new file mode 100644 index 00000000..9db32741 --- /dev/null +++ b/lib/dashbot/core/utils/dashbot_route_utils.dart @@ -0,0 +1,22 @@ +import 'package:apidash/models/models.dart'; + +import '../routes/dashbot_routes.dart'; + +/// Computes the base Dashbot route for a given request based on whether a +/// response exists. +/// - Returns [DashbotRoutes.dashbotHome] if the request has a response (either +/// statusCode or responseStatus present). +/// - Otherwise returns [DashbotRoutes.dashbotDefault]. +String computeDashbotBaseRoute(RequestModel? req) { + final hasResponse = (req?.httpResponseModel?.statusCode != null) || + (req?.responseStatus != null); + return hasResponse ? DashbotRoutes.dashbotHome : DashbotRoutes.dashbotDefault; +} + +/// Returns true if the route that should be shown for [req] differs from the +/// currently active [currentRoute]. +/// This helper is pure and does not perform any side effects. +bool needsDashbotRouteChange(RequestModel? req, String currentRoute) { + final target = computeDashbotBaseRoute(req); + return target != currentRoute; +} diff --git a/lib/dashbot/dashbot_dashboard.dart b/lib/dashbot/dashbot_dashboard.dart index f18b1579..1a393404 100644 --- a/lib/dashbot/dashbot_dashboard.dart +++ b/lib/dashbot/dashbot_dashboard.dart @@ -9,8 +9,7 @@ import 'core/routes/dashbot_router.dart'; import 'core/routes/dashbot_routes.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -// Removed navigation persistence provider. -import 'features/chat/viewmodel/chat_viewmodel.dart'; +import 'core/providers/dashbot_active_route_provider.dart'; class DashbotWindow extends ConsumerWidget { final VoidCallback onClose; @@ -25,8 +24,7 @@ class DashbotWindow extends ConsumerWidget { final windowState = ref.watch(dashbotWindowNotifierProvider); final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier); final settings = ref.watch(settingsProvider); - final currentRequest = ref.watch(selectedRequestModelProvider); - final chatState = ref.watch(chatViewmodelProvider); + final activeRoute = ref.watch(dashbotActiveRouteProvider); // Close the overlay when the window is not popped anymore ref.listen( @@ -38,43 +36,26 @@ class DashbotWindow extends ConsumerWidget { }, ); - void _maybeNavigate() { - final req = ref.read(selectedRequestModelProvider); - final hasResponse = (req?.httpResponseModel?.statusCode != null) || - (req?.responseStatus != null); - final requestId = req?.id ?? 'global'; - final messages = chatState.chatSessions[requestId] ?? const []; - final isChatActive = messages.isNotEmpty; - final navigator = _dashbotNavigatorKey.currentState; - if (navigator == null) return; - final canPop = navigator.canPop(); - - final desired = isChatActive - ? DashbotRoutes.dashbotChat - : hasResponse - ? DashbotRoutes.dashbotHome - : DashbotRoutes.dashbotDefault; - bool isOn(String r) { - Route? top; - navigator.popUntil((route) { - top = route; - return true; - }); - return top?.settings.name == r; - } - - if (isOn(desired)) return; - if (desired == DashbotRoutes.dashbotDefault && canPop) { - navigator.popUntil((route) => route.isFirst); - return; - } - if (desired != DashbotRoutes.dashbotDefault) { - navigator.pushNamed(desired); + void navigateTo(String route) { + final nav = _dashbotNavigatorKey.currentState; + if (nav == null) return; + Route? top; + nav.popUntil((r) { + top = r; + return true; + }); + if (top?.settings.name == route) return; + if (route == DashbotRoutes.dashbotDefault) { + nav.popUntil((r) => r.isFirst); + } else { + nav.pushNamed(route); } } - ref.listen(chatViewmodelProvider, (_, __) => _maybeNavigate()); - ref.listen(selectedRequestModelProvider, (_, __) => _maybeNavigate()); + ref.listen(dashbotActiveRouteProvider, (prev, next) { + if (prev == next || next.isEmpty) return; + navigateTo(next); + }); return Stack( children: [ @@ -188,17 +169,7 @@ class DashbotWindow extends ConsumerWidget { Expanded( child: Navigator( key: _dashbotNavigatorKey, - initialRoute: (chatState - .chatSessions[ - (currentRequest?.id ?? 'global')] - ?.isNotEmpty ?? - false) - ? DashbotRoutes.dashbotChat - : (currentRequest - ?.httpResponseModel?.statusCode != - null) - ? DashbotRoutes.dashbotHome - : DashbotRoutes.dashbotDefault, + initialRoute: activeRoute, onGenerateRoute: generateRoute, ), ), diff --git a/lib/dashbot/dashbot_tab.dart b/lib/dashbot/dashbot_tab.dart index 634af42c..26d41695 100644 --- a/lib/dashbot/dashbot_tab.dart +++ b/lib/dashbot/dashbot_tab.dart @@ -1,4 +1,3 @@ -import 'package:apidash/providers/providers.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'core/routes/dashbot_router.dart'; import 'core/routes/dashbot_routes.dart'; @@ -7,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/providers/dashbot_window_notifier.dart'; import 'core/utils/show_dashbot.dart'; import 'package:apidash/consts.dart'; -import 'features/chat/viewmodel/chat_viewmodel.dart'; +import 'core/providers/dashbot_active_route_provider.dart'; class DashbotTab extends ConsumerStatefulWidget { const DashbotTab({super.key}); @@ -26,47 +25,31 @@ class _DashbotTabState extends ConsumerState @override Widget build(BuildContext context) { super.build(context); - final currentRequest = ref.watch(selectedRequestModelProvider); - final chatState = ref.watch(chatViewmodelProvider); + final activeRoute = ref.watch(dashbotActiveRouteProvider); - void maybeNavigate() { - final req = ref.read(selectedRequestModelProvider); - final hasResponse = (req?.httpResponseModel?.statusCode != null) || - (req?.responseStatus != null); - final requestId = req?.id ?? 'global'; - final messages = chatState.chatSessions[requestId] ?? const []; - final isChatActive = messages.isNotEmpty; + void navigateTo(String route) { final navigator = _navKey.currentState; if (navigator == null) return; - final canPop = navigator.canPop(); - - final desired = isChatActive - ? DashbotRoutes.dashbotChat - : hasResponse - ? DashbotRoutes.dashbotHome - : DashbotRoutes.dashbotDefault; - - bool isOn(String r) { - Route? top; - navigator.popUntil((route) { - top = route; - return true; - }); - return top?.settings.name == r; - } - - if (isOn(desired)) return; - if (desired == DashbotRoutes.dashbotDefault && canPop) { - navigator.popUntil((route) => route.isFirst); - return; - } - if (desired != DashbotRoutes.dashbotDefault) { - navigator.pushNamed(desired); + // Determine current top + Route? top; + navigator.popUntil((r) { + top = r; + return true; + }); + final topName = top?.settings.name; + if (topName == route) return; // already there + if (route == DashbotRoutes.dashbotDefault) { + navigator.popUntil((r) => r.isFirst); + } else { + navigator.pushNamed(route); } } - ref.listen(chatViewmodelProvider, (_, __) => maybeNavigate()); - ref.listen(selectedRequestModelProvider, (_, __) => maybeNavigate()); + // React to route provider changes. + ref.listen(dashbotActiveRouteProvider, (prev, next) { + if (prev == next || next.isEmpty) return; + navigateTo(next); + }); return PopScope( canPop: true, @@ -135,16 +118,7 @@ class _DashbotTabState extends ConsumerState Expanded( child: Navigator( key: _navKey, - initialRoute: (chatState - .chatSessions[(currentRequest?.id ?? 'global')] - ?.isNotEmpty ?? - false) - ? DashbotRoutes.dashbotChat - : (currentRequest?.httpResponseModel?.statusCode != - null || - currentRequest?.responseStatus != null) - ? DashbotRoutes.dashbotHome - : DashbotRoutes.dashbotDefault, + initialRoute: activeRoute, onGenerateRoute: generateRoute, ), ), diff --git a/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart b/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart index 7d00d98e..5f2af62b 100644 --- a/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart +++ b/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart @@ -1,4 +1,5 @@ import 'package:apidash/dashbot/features/chat/view/widgets/dashbot_task_buttons.dart'; +import 'package:apidash/providers/providers.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import '../../../../core/constants/constants.dart'; @@ -66,6 +67,7 @@ class _ChatScreenState extends ConsumerState { _scrollToBottom(); } }); + ref.watch(selectedRequestModelProvider.select((value) => value?.id)); return Scaffold( body: Column( diff --git a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart index 310dbdb2..34112866 100644 --- a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart +++ b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart @@ -17,6 +17,7 @@ import '../models/chat_action.dart'; import '../models/chat_state.dart'; import '../repository/chat_remote_repository.dart'; import '../providers/service_providers.dart'; +import '../../../core/providers/dashbot_active_route_provider.dart'; class ChatViewmodel extends StateNotifier { ChatViewmodel(this._ref) : super(const ChatState()); @@ -270,6 +271,8 @@ class ChatViewmodel extends StateNotifier { isGenerating: false, currentStreamingResponse: '', ); + // Reset to base route (unpins chat) after clearing messages. + _ref.read(dashbotActiveRouteProvider.notifier).resetToBaseRoute(); } Future sendTaskMessage(ChatMessageType type) async { diff --git a/lib/dashbot/features/home/view/pages/dashbot_home_page.dart b/lib/dashbot/features/home/view/pages/dashbot_home_page.dart index 926325d7..798df4c9 100644 --- a/lib/dashbot/features/home/view/pages/dashbot_home_page.dart +++ b/lib/dashbot/features/home/view/pages/dashbot_home_page.dart @@ -8,6 +8,7 @@ import '../../../../core/utils/dashbot_icons.dart'; import '../../../../core/providers/dashbot_window_notifier.dart'; import '../../../../core/routes/dashbot_routes.dart'; +import '../../../../core/providers/dashbot_active_route_provider.dart'; import 'package:apidash_design_system/tokens/measurements.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -46,15 +47,15 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "🤖 Chat with Dashbot", onPressed: () { - Navigator.of(context).pushNamed( - DashbotRoutes.dashbotChat, - ); + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); + Navigator.of(context).pushNamed(DashbotRoutes.dashbotChat); }, ), ], HomeScreenTaskButton( label: "🔎 Explain me this response", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.explainResponse, @@ -64,6 +65,7 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "🐞 Help me debug this error", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.debugError, @@ -73,6 +75,7 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "📄 Generate documentation", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.generateDoc, @@ -82,6 +85,7 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "📝 Generate Tests", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.generateTest, @@ -91,6 +95,7 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "🧩 Generate Code", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.generateCode, @@ -100,6 +105,7 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "📥 Import cURL", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.importCurl, @@ -109,6 +115,7 @@ class _DashbotHomePageState extends ConsumerState { HomeScreenTaskButton( label: "📄 Import OpenAPI", onPressed: () { + ref.read(dashbotActiveRouteProvider.notifier).goToChat(); Navigator.of(context).pushNamed( DashbotRoutes.dashbotChat, arguments: ChatMessageType.importOpenApi,