Move DashBot to bottom-right, add close/minimize/maximize buttons and toggle FAB visibility

This commit is contained in:
siddu015
2025-03-22 15:18:25 +05:30
parent 7582c78880
commit f44091a50c
3 changed files with 328 additions and 184 deletions

View File

@@ -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<bool>((ref) => false);
final dashBotMinimizedProvider = StateProvider<bool>((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),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/dashbot/providers/dashbot_providers.dart'; import 'package:apidash/dashbot/providers/dashbot_providers.dart';
import 'package:apidash/providers/providers.dart'; import 'package:apidash/providers/providers.dart';
import 'package:apidash/dashbot/dashbot.dart';
import 'test_runner_widget.dart'; import 'test_runner_widget.dart';
import 'chat_bubble.dart'; import 'chat_bubble.dart';
@@ -78,7 +79,7 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
setState(() => _isLoading = false); setState(() => _isLoading = false);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo( _scrollController.animateTo(
0, _scrollController.position.minScrollExtent,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOut, curve: Curves.easeOut,
); );
@@ -105,19 +106,18 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
final requestModel = ref.read(selectedRequestModelProvider); final requestModel = ref.read(selectedRequestModelProvider);
final statusCode = requestModel?.httpResponseModel?.statusCode; final statusCode = requestModel?.httpResponseModel?.statusCode;
final showDebugButton = statusCode != null && statusCode >= 400; final showDebugButton = statusCode != null && statusCode >= 400;
final isMinimized = ref.watch(dashBotMinimizedProvider);
return Container( return Container(
height: 450, height: double.infinity,
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12), 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildHeader(context), _buildHeader(context),
@@ -134,115 +134,181 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
} }
Widget _buildHeader(BuildContext context) { Widget _buildHeader(BuildContext context) {
return Row( final isMinimized = ref.watch(dashBotMinimizedProvider);
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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: [ children: [
const Text('DashBot', _buildHeader(context),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8),
IconButton( Padding(
icon: const Icon(Icons.delete_sweep), padding: const EdgeInsets.symmetric(horizontal: 16),
tooltip: 'Clear Chat', child: _buildInputArea(context),
onPressed: () {
ref.read(chatMessagesProvider.notifier).clearMessages();
},
), ),
], ],
); );
} }
Widget _buildQuickActions(bool showDebugButton) { Widget _buildQuickActions(bool showDebugButton) {
return Wrap( return Padding(
spacing: 8, padding: const EdgeInsets.symmetric(horizontal: 16),
runSpacing: 8, child: Wrap(
children: [ spacing: 8,
ElevatedButton.icon( runSpacing: 8,
onPressed: () => _sendMessage("Explain API"), children: [
icon: const Icon(Icons.info_outline),
label: const Text("Explain"),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
if (showDebugButton)
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => _sendMessage("Debug API"), onPressed: () => _sendMessage("Explain API"),
icon: const Icon(Icons.bug_report_outlined), icon: const Icon(Icons.info_outline, size: 16),
label: const Text("Debug"), label: const Text("Explain"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
visualDensity: VisualDensity.compact,
), ),
), ),
ElevatedButton.icon( if (showDebugButton)
onPressed: () => _sendMessage("Document API"), ElevatedButton.icon(
icon: const Icon(Icons.description_outlined), onPressed: () => _sendMessage("Debug API"),
label: const Text("Document"), icon: const Icon(Icons.bug_report_outlined, size: 16),
style: ElevatedButton.styleFrom( label: const Text("Debug"),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), 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(
ElevatedButton.icon( onPressed: () => _sendMessage("Test API"),
onPressed: () => _sendMessage("Test API"), icon: const Icon(Icons.science_outlined, size: 16),
icon: const Icon(Icons.science_outlined), label: const Text("Test"),
label: const Text("Test"), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), visualDensity: VisualDensity.compact,
),
), ),
), ],
], ),
); );
} }
Widget _buildChatArea(List<Map<String, dynamic>> messages) { Widget _buildChatArea(List<Map<String, dynamic>> messages) {
return ListView.builder( return Padding(
controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16),
reverse: true, child: ListView.builder(
itemCount: messages.length, controller: _scrollController,
itemBuilder: (context, index) { reverse: true,
final message = messages.reversed.toList()[index]; itemCount: messages.length,
final isBot = message['role'] == 'bot'; itemBuilder: (context, index) {
final text = message['message'] as String; final message = messages.reversed.toList()[index];
final showTestButton = message['showTestButton'] == true; final isBot = message['role'] == 'bot';
final testCases = message['testCases'] as String?; final text = message['message'] as String;
final showTestButton = message['showTestButton'] == true;
final testCases = message['testCases'] as String?;
if (isBot && showTestButton && testCases != null) { if (isBot && showTestButton && testCases != null) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ChatBubble(message: text, isUser: false), ChatBubble(message: text, isUser: false),
Padding( Padding(
padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => _showTestRunner(testCases), onPressed: () => _showTestRunner(testCases),
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.play_arrow, size: 16),
label: const Text("Run Test Cases"), label: const Text("Run Test Cases"),
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.compact,
),
),
), ),
), ],
], );
); }
}
return ChatBubble( return ChatBubble(
message: text, message: text,
isUser: message['role'] == 'user', isUser: message['role'] == 'user',
); );
}, },
),
); );
} }
Widget _buildLoadingIndicator() { Widget _buildLoadingIndicator() {
return const Padding( return const Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: LinearProgressIndicator(), child: LinearProgressIndicator(),
); );
} }
Widget _buildInputArea(BuildContext context) { Widget _buildInputArea(BuildContext context) {
final isMinimized = ref.watch(dashBotMinimizedProvider);
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
), ),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@@ -251,19 +317,28 @@ class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Ask DashBot...', hintText: 'Ask DashBot...',
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 8),
), ),
onSubmitted: (value) { onSubmitted: (value) {
_sendMessage(value); _sendMessage(value);
_controller.clear(); _controller.clear();
if (isMinimized) {
ref.read(dashBotMinimizedProvider.notifier).state = false;
}
}, },
maxLines: 1, maxLines: 1,
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.send), padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(Icons.send, size: 20),
onPressed: () { onPressed: () {
_sendMessage(_controller.text); _sendMessage(_controller.text);
_controller.clear(); _controller.clear();
if (isMinimized) {
ref.read(dashBotMinimizedProvider.notifier).state = false;
}
}, },
), ),
], ],

View File

@@ -17,126 +17,130 @@ class Dashboard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final railIdx = ref.watch(navRailIndexStateProvider); final railIdx = ref.watch(navRailIndexStateProvider);
final isDashBotVisible = ref.watch(dashBotVisibilityProvider);
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: Row( child: Stack(
children: <Widget>[ children: [
Column( Row(
children: [ children: <Widget>[
SizedBox(
height: kIsMacOS ? 32.0 : 16.0,
width: 64,
),
Column( Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( SizedBox(
isSelected: railIdx == 0, height: kIsMacOS ? 32.0 : 16.0,
onPressed: () { width: 64,
ref.read(navRailIndexStateProvider.notifier).state = 0;
},
icon: const Icon(Icons.auto_awesome_mosaic_outlined),
selectedIcon: const Icon(Icons.auto_awesome_mosaic),
), ),
Text( Column(
'Requests', mainAxisSize: MainAxisSize.min,
style: Theme.of(context).textTheme.labelSmall, 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, Expanded(
IconButton( child: Column(
isSelected: railIdx == 1, mainAxisAlignment: MainAxisAlignment.end,
onPressed: () { children: [
ref.read(navRailIndexStateProvider.notifier).state = 1; Padding(
}, padding: const EdgeInsets.only(bottom: 16.0),
icon: const Icon(Icons.laptop_windows_outlined), child: NavbarButton(
selectedIcon: const Icon(Icons.laptop_windows), railIdx: railIdx,
), selectedIcon: Icons.help,
Text( icon: Icons.help_outline,
'Variables', label: 'About',
style: Theme.of(context).textTheme.labelSmall, showLabel: false,
), isCompact: true,
kVSpacer10, onTap: () {
IconButton( showAboutAppDialog(context);
isSelected: railIdx == 2, },
onPressed: () { ),
ref.read(navRailIndexStateProvider.notifier).state = 2; ),
}, Padding(
icon: const Icon(Icons.history_outlined), padding: const EdgeInsets.only(bottom: 16.0),
selectedIcon: const Icon(Icons.history_rounded), child: NavbarButton(
), railIdx: railIdx,
Text( buttonIdx: 3,
'History', selectedIcon: Icons.settings,
style: Theme.of(context).textTheme.labelSmall, icon: Icons.settings_outlined,
label: 'Settings',
showLabel: false,
isCompact: true,
),
),
],
),
), ),
], ],
), ),
VerticalDivider(
thickness: 1,
width: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
Expanded( Expanded(
child: Column( child: IndexedStack(
mainAxisAlignment: MainAxisAlignment.end, alignment: AlignmentDirectional.topCenter,
children: [ index: railIdx,
Padding( children: const [
padding: const EdgeInsets.only(bottom: 16.0), HomePage(),
child: NavbarButton( EnvironmentPage(),
railIdx: railIdx, HistoryPage(),
selectedIcon: Icons.help, SettingsPage(),
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, // DashBot Overlay
width: 1, if (isDashBotVisible)
color: Theme.of(context).colorScheme.surfaceContainerHigh, Positioned(
), bottom: 20,
Expanded( right: 20,
child: IndexedStack( child: const DashBotOverlay(),
alignment: AlignmentDirectional.topCenter,
index: railIdx,
children: const [
HomePage(),
EnvironmentPage(),
HistoryPage(),
SettingsPage(),
],
), ),
)
], ],
), ),
), ),
// TODO: Release DashBot // Conditionally show FAB only when DashBot is not visible
floatingActionButton: FloatingActionButton( floatingActionButton: !isDashBotVisible ? const DashBotFAB() : null,
onPressed: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const Padding(
padding: EdgeInsets.all(16.0),
child: DashBotWidget(),
),
),
child: const Icon(Icons.help_outline),
),
); );
} }
} }