From f44091a50c8464d8794d984acf9e7e0d6ede5274 Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Sat, 22 Mar 2025 15:18:25 +0530 Subject: [PATCH] Move DashBot to bottom-right, add close/minimize/maximize buttons and toggle FAB visibility --- lib/dashbot/dashbot.dart | 67 ++++++- lib/dashbot/widgets/dashbot_widget.dart | 237 ++++++++++++++++-------- lib/screens/dashboard.dart | 208 +++++++++++---------- 3 files changed, 328 insertions(+), 184 deletions(-) diff --git a/lib/dashbot/dashbot.dart b/lib/dashbot/dashbot.dart index 9454aa85..e2371acd 100644 --- a/lib/dashbot/dashbot.dart +++ b/lib/dashbot/dashbot.dart @@ -1 +1,66 @@ -export 'widgets/dashbot_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/dashbot/widgets/dashbot_widget.dart'; + +// Provider to manage DashBot visibility state +final dashBotVisibilityProvider = StateProvider((ref) => false); +final dashBotMinimizedProvider = StateProvider((ref) => false); + +// Function to show DashBot in a bottom sheet (old style) +void showDashBotBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => const Padding( + padding: EdgeInsets.all(16.0), + child: DashBotWidget(), + ), + ); +} + +// Function to toggle DashBot overlay (new style) +void toggleDashBotOverlay(WidgetRef ref) { + ref.read(dashBotVisibilityProvider.notifier).state = true; + ref.read(dashBotMinimizedProvider.notifier).state = false; +} + +// DashBot Overlay Widget +class DashBotOverlay extends ConsumerWidget { + const DashBotOverlay({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isMinimized = ref.watch(dashBotMinimizedProvider); + + return Material( + elevation: 8, + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: 400, // Fixed width for the DashBot + height: isMinimized ? 120 : 450, + child: const DashBotWidget(), + ), + ); + } +} + +// FloatingActionButton for DashBot +class DashBotFAB extends ConsumerWidget { + final bool useOverlay; + + const DashBotFAB({this.useOverlay = true, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FloatingActionButton( + onPressed: () { + if (useOverlay) { + toggleDashBotOverlay(ref); + } else { + showDashBotBottomSheet(context); + } + }, + child: const Icon(Icons.help_outline), + ); + } +} diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index 65e706ea..2426fca8 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/dashbot/providers/dashbot_providers.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/dashbot/dashbot.dart'; import 'test_runner_widget.dart'; import 'chat_bubble.dart'; @@ -78,7 +79,7 @@ class _DashBotWidgetState extends ConsumerState { setState(() => _isLoading = false); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.animateTo( - 0, + _scrollController.position.minScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); @@ -105,19 +106,18 @@ class _DashBotWidgetState extends ConsumerState { final requestModel = ref.read(selectedRequestModelProvider); final statusCode = requestModel?.httpResponseModel?.statusCode; final showDebugButton = statusCode != null && statusCode >= 400; + final isMinimized = ref.watch(dashBotMinimizedProvider); return Container( - height: 450, + height: double.infinity, width: double.infinity, - padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)) - ], ), - child: Column( + child: isMinimized + ? _buildMinimizedView(context) + : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(context), @@ -134,115 +134,181 @@ class _DashBotWidgetState extends ConsumerState { } Widget _buildHeader(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + final isMinimized = ref.watch(dashBotMinimizedProvider); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'DashBot', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Row( + children: [ + // Minimize/Maximize button with proper alignment + IconButton( + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + icon: Icon( + isMinimized ? Icons.fullscreen : Icons.remove, + size: 20, + ), + tooltip: isMinimized ? 'Maximize' : 'Minimize', + onPressed: () { + ref.read(dashBotMinimizedProvider.notifier).state = !isMinimized; + }, + ), + // Close button + IconButton( + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.close, size: 20), + tooltip: 'Close', + onPressed: () { + ref.read(dashBotVisibilityProvider.notifier).state = false; + }, + ), + // Clear chat button + IconButton( + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.delete_sweep, size: 20), + tooltip: 'Clear Chat', + onPressed: () { + ref.read(chatMessagesProvider.notifier).clearMessages(); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildMinimizedView(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('DashBot', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - IconButton( - icon: const Icon(Icons.delete_sweep), - tooltip: 'Clear Chat', - onPressed: () { - ref.read(chatMessagesProvider.notifier).clearMessages(); - }, + _buildHeader(context), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildInputArea(context), ), ], ); } Widget _buildQuickActions(bool showDebugButton) { - return Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ElevatedButton.icon( - onPressed: () => _sendMessage("Explain API"), - icon: const Icon(Icons.info_outline), - label: const Text("Explain"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - ), - if (showDebugButton) + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ ElevatedButton.icon( - onPressed: () => _sendMessage("Debug API"), - icon: const Icon(Icons.bug_report_outlined), - label: const Text("Debug"), + onPressed: () => _sendMessage("Explain API"), + icon: const Icon(Icons.info_outline, size: 16), + label: const Text("Explain"), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, ), ), - ElevatedButton.icon( - onPressed: () => _sendMessage("Document API"), - icon: const Icon(Icons.description_outlined), - label: const Text("Document"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + if (showDebugButton) + ElevatedButton.icon( + onPressed: () => _sendMessage("Debug API"), + icon: const Icon(Icons.bug_report_outlined, size: 16), + label: const Text("Debug"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, + ), + ), + ElevatedButton.icon( + onPressed: () => _sendMessage("Document API"), + icon: const Icon(Icons.description_outlined, size: 16), + label: const Text("Document"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, + ), ), - ), - ElevatedButton.icon( - onPressed: () => _sendMessage("Test API"), - icon: const Icon(Icons.science_outlined), - label: const Text("Test"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ElevatedButton.icon( + onPressed: () => _sendMessage("Test API"), + icon: const Icon(Icons.science_outlined, size: 16), + label: const Text("Test"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, + ), ), - ), - ], + ], + ), ); } Widget _buildChatArea(List> messages) { - return ListView.builder( - controller: _scrollController, - reverse: true, - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages.reversed.toList()[index]; - final isBot = message['role'] == 'bot'; - final text = message['message'] as String; - final showTestButton = message['showTestButton'] == true; - final testCases = message['testCases'] as String?; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + controller: _scrollController, + reverse: true, + itemCount: messages.length, + itemBuilder: (context, index) { + final message = messages.reversed.toList()[index]; + final isBot = message['role'] == 'bot'; + final text = message['message'] as String; + final showTestButton = message['showTestButton'] == true; + final testCases = message['testCases'] as String?; - if (isBot && showTestButton && testCases != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChatBubble(message: text, isUser: false), - Padding( - padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), - child: ElevatedButton.icon( - onPressed: () => _showTestRunner(testCases), - icon: const Icon(Icons.play_arrow), - label: const Text("Run Test Cases"), + if (isBot && showTestButton && testCases != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChatBubble(message: text, isUser: false), + Padding( + padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), + child: ElevatedButton.icon( + onPressed: () => _showTestRunner(testCases), + icon: const Icon(Icons.play_arrow, size: 16), + label: const Text("Run Test Cases"), + style: ElevatedButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + ), ), - ), - ], - ); - } + ], + ); + } - return ChatBubble( - message: text, - isUser: message['role'] == 'user', - ); - }, + return ChatBubble( + message: text, + isUser: message['role'] == 'user', + ); + }, + ), ); } Widget _buildLoadingIndicator() { return const Padding( - padding: EdgeInsets.all(8.0), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: LinearProgressIndicator(), ); } Widget _buildInputArea(BuildContext context) { + final isMinimized = ref.watch(dashBotMinimizedProvider); + return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.surfaceContainer, ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ Expanded( @@ -251,19 +317,28 @@ class _DashBotWidgetState extends ConsumerState { decoration: const InputDecoration( hintText: 'Ask DashBot...', border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 8), ), onSubmitted: (value) { _sendMessage(value); _controller.clear(); + if (isMinimized) { + ref.read(dashBotMinimizedProvider.notifier).state = false; + } }, maxLines: 1, ), ), IconButton( - icon: const Icon(Icons.send), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.send, size: 20), onPressed: () { _sendMessage(_controller.text); _controller.clear(); + if (isMinimized) { + ref.read(dashBotMinimizedProvider.notifier).state = false; + } }, ), ], diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index cc9a6267..148ff5c9 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -17,126 +17,130 @@ class Dashboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); + final isDashBotVisible = ref.watch(dashBotVisibilityProvider); + return Scaffold( body: SafeArea( - child: Row( - children: [ - Column( - children: [ - SizedBox( - height: kIsMacOS ? 32.0 : 16.0, - width: 64, - ), + child: Stack( + children: [ + Row( + children: [ Column( - mainAxisSize: MainAxisSize.min, children: [ - IconButton( - isSelected: railIdx == 0, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 0; - }, - icon: const Icon(Icons.auto_awesome_mosaic_outlined), - selectedIcon: const Icon(Icons.auto_awesome_mosaic), + SizedBox( + height: kIsMacOS ? 32.0 : 16.0, + width: 64, ), - Text( - 'Requests', - style: Theme.of(context).textTheme.labelSmall, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + isSelected: railIdx == 0, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 0; + }, + icon: const Icon(Icons.auto_awesome_mosaic_outlined), + selectedIcon: const Icon(Icons.auto_awesome_mosaic), + ), + Text( + 'Requests', + style: Theme.of(context).textTheme.labelSmall, + ), + kVSpacer10, + IconButton( + isSelected: railIdx == 1, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 1; + }, + icon: const Icon(Icons.laptop_windows_outlined), + selectedIcon: const Icon(Icons.laptop_windows), + ), + Text( + 'Variables', + style: Theme.of(context).textTheme.labelSmall, + ), + kVSpacer10, + IconButton( + isSelected: railIdx == 2, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 2; + }, + icon: const Icon(Icons.history_outlined), + selectedIcon: const Icon(Icons.history_rounded), + ), + Text( + 'History', + style: Theme.of(context).textTheme.labelSmall, + ), + ], ), - kVSpacer10, - IconButton( - isSelected: railIdx == 1, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 1; - }, - icon: const Icon(Icons.laptop_windows_outlined), - selectedIcon: const Icon(Icons.laptop_windows), - ), - Text( - 'Variables', - style: Theme.of(context).textTheme.labelSmall, - ), - kVSpacer10, - IconButton( - isSelected: railIdx == 2, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 2; - }, - icon: const Icon(Icons.history_outlined), - selectedIcon: const Icon(Icons.history_rounded), - ), - Text( - 'History', - style: Theme.of(context).textTheme.labelSmall, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: NavbarButton( + railIdx: railIdx, + selectedIcon: Icons.help, + icon: Icons.help_outline, + label: 'About', + showLabel: false, + isCompact: true, + onTap: () { + showAboutAppDialog(context); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: NavbarButton( + railIdx: railIdx, + buttonIdx: 3, + selectedIcon: Icons.settings, + icon: Icons.settings_outlined, + label: 'Settings', + showLabel: false, + isCompact: true, + ), + ), + ], + ), ), ], ), + VerticalDivider( + thickness: 1, + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: NavbarButton( - railIdx: railIdx, - selectedIcon: Icons.help, - icon: Icons.help_outline, - label: 'About', - showLabel: false, - isCompact: true, - onTap: () { - showAboutAppDialog(context); - }, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: NavbarButton( - railIdx: railIdx, - buttonIdx: 3, - selectedIcon: Icons.settings, - icon: Icons.settings_outlined, - label: 'Settings', - showLabel: false, - isCompact: true, - ), - ), + child: IndexedStack( + alignment: AlignmentDirectional.topCenter, + index: railIdx, + children: const [ + HomePage(), + EnvironmentPage(), + HistoryPage(), + SettingsPage(), ], ), - ), + ) ], ), - VerticalDivider( - thickness: 1, - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - Expanded( - child: IndexedStack( - alignment: AlignmentDirectional.topCenter, - index: railIdx, - children: const [ - HomePage(), - EnvironmentPage(), - HistoryPage(), - SettingsPage(), - ], + + // DashBot Overlay + if (isDashBotVisible) + Positioned( + bottom: 20, + right: 20, + child: const DashBotOverlay(), ), - ) ], ), ), - // TODO: Release DashBot - floatingActionButton: FloatingActionButton( - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => const Padding( - padding: EdgeInsets.all(16.0), - child: DashBotWidget(), - ), - ), - child: const Icon(Icons.help_outline), - ), + // Conditionally show FAB only when DashBot is not visible + floatingActionButton: !isDashBotVisible ? const DashBotFAB() : null, ); } }