feat: create a dedicated provider for dashbot navigation

This commit is contained in:
Udhay-Adithya
2025-09-27 17:36:02 +05:30
parent ec02eb640f
commit b840ba44de
7 changed files with 141 additions and 99 deletions

View File

@@ -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<String> {
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, String>(
() => DashbotActiveRouteNotifier(),
name: 'dashbotActiveRouteProvider',
);

View File

@@ -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;
}

View File

@@ -9,8 +9,7 @@ import 'core/routes/dashbot_router.dart';
import 'core/routes/dashbot_routes.dart'; import 'core/routes/dashbot_routes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
// Removed navigation persistence provider. import 'core/providers/dashbot_active_route_provider.dart';
import 'features/chat/viewmodel/chat_viewmodel.dart';
class DashbotWindow extends ConsumerWidget { class DashbotWindow extends ConsumerWidget {
final VoidCallback onClose; final VoidCallback onClose;
@@ -25,8 +24,7 @@ class DashbotWindow extends ConsumerWidget {
final windowState = ref.watch(dashbotWindowNotifierProvider); final windowState = ref.watch(dashbotWindowNotifierProvider);
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier); final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
final settings = ref.watch(settingsProvider); final settings = ref.watch(settingsProvider);
final currentRequest = ref.watch(selectedRequestModelProvider); final activeRoute = ref.watch(dashbotActiveRouteProvider);
final chatState = ref.watch(chatViewmodelProvider);
// Close the overlay when the window is not popped anymore // Close the overlay when the window is not popped anymore
ref.listen( ref.listen(
@@ -38,43 +36,26 @@ class DashbotWindow extends ConsumerWidget {
}, },
); );
void _maybeNavigate() { void navigateTo(String route) {
final req = ref.read(selectedRequestModelProvider); final nav = _dashbotNavigatorKey.currentState;
final hasResponse = (req?.httpResponseModel?.statusCode != null) || if (nav == null) return;
(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; Route? top;
navigator.popUntil((route) { nav.popUntil((r) {
top = route; top = r;
return true; return true;
}); });
return top?.settings.name == r; if (top?.settings.name == route) return;
} if (route == DashbotRoutes.dashbotDefault) {
nav.popUntil((r) => r.isFirst);
if (isOn(desired)) return; } else {
if (desired == DashbotRoutes.dashbotDefault && canPop) { nav.pushNamed(route);
navigator.popUntil((route) => route.isFirst);
return;
}
if (desired != DashbotRoutes.dashbotDefault) {
navigator.pushNamed(desired);
} }
} }
ref.listen(chatViewmodelProvider, (_, __) => _maybeNavigate()); ref.listen<String>(dashbotActiveRouteProvider, (prev, next) {
ref.listen(selectedRequestModelProvider, (_, __) => _maybeNavigate()); if (prev == next || next.isEmpty) return;
navigateTo(next);
});
return Stack( return Stack(
children: [ children: [
@@ -188,17 +169,7 @@ class DashbotWindow extends ConsumerWidget {
Expanded( Expanded(
child: Navigator( child: Navigator(
key: _dashbotNavigatorKey, key: _dashbotNavigatorKey,
initialRoute: (chatState initialRoute: activeRoute,
.chatSessions[
(currentRequest?.id ?? 'global')]
?.isNotEmpty ??
false)
? DashbotRoutes.dashbotChat
: (currentRequest
?.httpResponseModel?.statusCode !=
null)
? DashbotRoutes.dashbotHome
: DashbotRoutes.dashbotDefault,
onGenerateRoute: generateRoute, onGenerateRoute: generateRoute,
), ),
), ),

View File

@@ -1,4 +1,3 @@
import 'package:apidash/providers/providers.dart';
import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:apidash_design_system/apidash_design_system.dart';
import 'core/routes/dashbot_router.dart'; import 'core/routes/dashbot_router.dart';
import 'core/routes/dashbot_routes.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/providers/dashbot_window_notifier.dart';
import 'core/utils/show_dashbot.dart'; import 'core/utils/show_dashbot.dart';
import 'package:apidash/consts.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 { class DashbotTab extends ConsumerStatefulWidget {
const DashbotTab({super.key}); const DashbotTab({super.key});
@@ -26,47 +25,31 @@ class _DashbotTabState extends ConsumerState<DashbotTab>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final currentRequest = ref.watch(selectedRequestModelProvider); final activeRoute = ref.watch(dashbotActiveRouteProvider);
final chatState = ref.watch(chatViewmodelProvider);
void maybeNavigate() { void navigateTo(String route) {
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 = _navKey.currentState; final navigator = _navKey.currentState;
if (navigator == null) return; if (navigator == null) return;
final canPop = navigator.canPop(); // Determine current top
final desired = isChatActive
? DashbotRoutes.dashbotChat
: hasResponse
? DashbotRoutes.dashbotHome
: DashbotRoutes.dashbotDefault;
bool isOn(String r) {
Route? top; Route? top;
navigator.popUntil((route) { navigator.popUntil((r) {
top = route; top = r;
return true; return true;
}); });
return top?.settings.name == r; final topName = top?.settings.name;
} if (topName == route) return; // already there
if (route == DashbotRoutes.dashbotDefault) {
if (isOn(desired)) return; navigator.popUntil((r) => r.isFirst);
if (desired == DashbotRoutes.dashbotDefault && canPop) { } else {
navigator.popUntil((route) => route.isFirst); navigator.pushNamed(route);
return;
}
if (desired != DashbotRoutes.dashbotDefault) {
navigator.pushNamed(desired);
} }
} }
ref.listen(chatViewmodelProvider, (_, __) => maybeNavigate()); // React to route provider changes.
ref.listen(selectedRequestModelProvider, (_, __) => maybeNavigate()); ref.listen<String>(dashbotActiveRouteProvider, (prev, next) {
if (prev == next || next.isEmpty) return;
navigateTo(next);
});
return PopScope( return PopScope(
canPop: true, canPop: true,
@@ -135,16 +118,7 @@ class _DashbotTabState extends ConsumerState<DashbotTab>
Expanded( Expanded(
child: Navigator( child: Navigator(
key: _navKey, key: _navKey,
initialRoute: (chatState initialRoute: activeRoute,
.chatSessions[(currentRequest?.id ?? 'global')]
?.isNotEmpty ??
false)
? DashbotRoutes.dashbotChat
: (currentRequest?.httpResponseModel?.statusCode !=
null ||
currentRequest?.responseStatus != null)
? DashbotRoutes.dashbotHome
: DashbotRoutes.dashbotDefault,
onGenerateRoute: generateRoute, onGenerateRoute: generateRoute,
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:apidash/dashbot/features/chat/view/widgets/dashbot_task_buttons.dart'; 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 'package:apidash_design_system/apidash_design_system.dart';
import '../../../../core/constants/constants.dart'; import '../../../../core/constants/constants.dart';
@@ -66,6 +67,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
_scrollToBottom(); _scrollToBottom();
} }
}); });
ref.watch(selectedRequestModelProvider.select((value) => value?.id));
return Scaffold( return Scaffold(
body: Column( body: Column(

View File

@@ -17,6 +17,7 @@ import '../models/chat_action.dart';
import '../models/chat_state.dart'; import '../models/chat_state.dart';
import '../repository/chat_remote_repository.dart'; import '../repository/chat_remote_repository.dart';
import '../providers/service_providers.dart'; import '../providers/service_providers.dart';
import '../../../core/providers/dashbot_active_route_provider.dart';
class ChatViewmodel extends StateNotifier<ChatState> { class ChatViewmodel extends StateNotifier<ChatState> {
ChatViewmodel(this._ref) : super(const ChatState()); ChatViewmodel(this._ref) : super(const ChatState());
@@ -270,6 +271,8 @@ class ChatViewmodel extends StateNotifier<ChatState> {
isGenerating: false, isGenerating: false,
currentStreamingResponse: '', currentStreamingResponse: '',
); );
// Reset to base route (unpins chat) after clearing messages.
_ref.read(dashbotActiveRouteProvider.notifier).resetToBaseRoute();
} }
Future<void> sendTaskMessage(ChatMessageType type) async { Future<void> sendTaskMessage(ChatMessageType type) async {

View File

@@ -8,6 +8,7 @@ import '../../../../core/utils/dashbot_icons.dart';
import '../../../../core/providers/dashbot_window_notifier.dart'; import '../../../../core/providers/dashbot_window_notifier.dart';
import '../../../../core/routes/dashbot_routes.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:apidash_design_system/tokens/measurements.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -46,15 +47,15 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "🤖 Chat with Dashbot", label: "🤖 Chat with Dashbot",
onPressed: () { onPressed: () {
Navigator.of(context).pushNamed( ref.read(dashbotActiveRouteProvider.notifier).goToChat();
DashbotRoutes.dashbotChat, Navigator.of(context).pushNamed(DashbotRoutes.dashbotChat);
);
}, },
), ),
], ],
HomeScreenTaskButton( HomeScreenTaskButton(
label: "🔎 Explain me this response", label: "🔎 Explain me this response",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.explainResponse, arguments: ChatMessageType.explainResponse,
@@ -64,6 +65,7 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "🐞 Help me debug this error", label: "🐞 Help me debug this error",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.debugError, arguments: ChatMessageType.debugError,
@@ -73,6 +75,7 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "📄 Generate documentation", label: "📄 Generate documentation",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.generateDoc, arguments: ChatMessageType.generateDoc,
@@ -82,6 +85,7 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "📝 Generate Tests", label: "📝 Generate Tests",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.generateTest, arguments: ChatMessageType.generateTest,
@@ -91,6 +95,7 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "🧩 Generate Code", label: "🧩 Generate Code",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.generateCode, arguments: ChatMessageType.generateCode,
@@ -100,6 +105,7 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "📥 Import cURL", label: "📥 Import cURL",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.importCurl, arguments: ChatMessageType.importCurl,
@@ -109,6 +115,7 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
HomeScreenTaskButton( HomeScreenTaskButton(
label: "📄 Import OpenAPI", label: "📄 Import OpenAPI",
onPressed: () { onPressed: () {
ref.read(dashbotActiveRouteProvider.notifier).goToChat();
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat, DashbotRoutes.dashbotChat,
arguments: ChatMessageType.importOpenApi, arguments: ChatMessageType.importOpenApi,