feat: add initial dashbot package implementation

- Created a new Dashbot package with core functionalities.
- Implemented Dashbot window model and notifier for state management.
- Added UI components including Dashbot default page and home page.
- Integrated routing for Dashbot with initial routes defined.
- Included assets for Dashbot icons and set up pubspec.yaml.
- Added tests for Dashbot window notifier to ensure functionality.
- Established a basic README and CHANGELOG for the package.
This commit is contained in:
Udhay-Adithya
2025-08-31 12:15:43 +05:30
parent a3669b1685
commit b9cc42b019
25 changed files with 802 additions and 8 deletions

View File

@@ -1,10 +1,10 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:dashbot/dashbot.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/dashbot/dashbot.dart';
import 'common_widgets/common_widgets.dart';
import 'envvar/environment_page.dart';
import 'home_page/home_page.dart';
@@ -129,15 +129,15 @@ class Dashboard extends ConsumerWidget {
),
floatingActionButton: isDashBotEnabled
? FloatingActionButton(
onPressed: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => const Padding(
padding: EdgeInsets.all(16.0),
child: DashBotWidget(),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
onPressed: () => showDashbotWindow(context, ref),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 10,
),
child: DashbotIcons.getDashbotIcon1(),
),
child: const Icon(Icons.help_outline),
)
: null,
);

31
packages/dashbot/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

View File

@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
packages/dashbot/LICENSE Normal file
View File

@@ -0,0 +1 @@
TODO: Add your license here.

View File

@@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View File

@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,40 @@
import 'package:apidash_design_system/apidash_design_system.dart'
show kVSpacer20, kVSpacer16, kVSpacer10;
import 'package:dashbot/core/utils/dashbot_icons.dart';
import 'package:flutter/material.dart';
class DashbotDefaultPage extends StatelessWidget {
const DashbotDefaultPage({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
kVSpacer16,
DashbotIcons.getDashbotIcon1(width: 60),
kVSpacer20,
Text(
'Hello there!',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800),
),
kVSpacer10,
Text(
'Request not made yet',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
"Why not go ahead and make one?",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w200),
),
],
);
}
}

View File

@@ -0,0 +1,31 @@
class DashbotWindowModel {
final double width;
final double height;
final double right;
final double bottom;
final bool isActive;
const DashbotWindowModel({
this.width = 350,
this.height = 450,
this.right = 50,
this.bottom = 100,
this.isActive = false,
});
DashbotWindowModel copyWith({
double? width,
double? height,
double? right,
double? bottom,
bool? isActive,
}) {
return DashbotWindowModel(
width: width ?? this.width,
height: height ?? this.height,
right: right ?? this.right,
bottom: bottom ?? this.bottom,
isActive: isActive ?? this.isActive,
);
}
}

View File

@@ -0,0 +1,36 @@
import '../model/dashbot_window_model.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'dashbot_window_notifier.g.dart';
@Riverpod(keepAlive: true)
class DashbotWindowNotifier extends _$DashbotWindowNotifier {
@override
DashbotWindowModel build() {
return const DashbotWindowModel();
}
void updatePosition(double dx, double dy, Size screenSize) {
state = state.copyWith(
right: (state.right - dx).clamp(0, screenSize.width - state.width),
bottom: (state.bottom - dy).clamp(0, screenSize.height - state.height),
);
}
void updateSize(double dx, double dy, Size screenSize) {
final newWidth =
(state.width - dx).clamp(300, screenSize.width - state.right);
final newHeight =
(state.height - dy).clamp(350, screenSize.height - state.bottom);
state = state.copyWith(
width: newWidth.toDouble(),
height: newHeight.toDouble(),
);
}
void toggleActive() {
state = state.copyWith(isActive: !state.isActive);
}
}

View File

@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dashbot_window_notifier.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$dashbotWindowNotifierHash() =>
r'239915bec100bc33e5533291ff10233ea197d556';
/// See also [DashbotWindowNotifier].
@ProviderFor(DashbotWindowNotifier)
final dashbotWindowNotifierProvider =
NotifierProvider<DashbotWindowNotifier, DashbotWindowModel>.internal(
DashbotWindowNotifier.new,
name: r'dashbotWindowNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$dashbotWindowNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DashbotWindowNotifier = Notifier<DashbotWindowModel>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,31 @@
import 'dashbot_routes.dart';
import '../common/pages/dashbot_default_page.dart';
import '../../features/home/view/pages/home_page.dart';
import 'package:flutter/material.dart';
Route<dynamic>? generateRoute(
RouteSettings settings,
) {
switch (settings.name) {
case (DashbotRoutes.dashbotHome):
return MaterialPageRoute(
builder: (context) => DashbotHomePage(),
);
case (DashbotRoutes.dashbotDefault):
return MaterialPageRoute(
builder: (context) => DashbotDefaultPage(),
);
// case (DashbotRoutes.dashbotChat):
// final args = settings.arguments as Map<String, dynamic>?;
// final initialPrompt = args?['initialPrompt'] as String;
// return MaterialPageRoute(
// builder: (context) => ChatScreen(
// initialPrompt: initialPrompt,
// ),
// );
default:
return MaterialPageRoute(
builder: (context) => DashbotDefaultPage(),
);
}
}

View File

@@ -0,0 +1,6 @@
class DashbotRoutes {
static const String dashbotHome = '/dashbothome';
static const String dashbotDefault = '/dashbotdefault';
static const String dashbotChat = '/dashbotchat';
static const String dashbotUnknown = '/dashbotunknown';
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/widgets.dart';
class DashbotIcons {
DashbotIcons._();
static String get dashbotIcon1 =>
'packages/dashbot/assets/dashbot_icon_1.png';
static String get dashbotIcon2 =>
'packages/dashbot/assets/dashbot_icon_2.png';
static Image getDashbotIcon1({double? width, double? height, BoxFit? fit}) {
return Image.asset(dashbotIcon1, width: width, height: height, fit: fit);
}
static Image getDashbotIcon2({double? width, double? height, BoxFit? fit}) {
return Image.asset(dashbotIcon2, width: width, height: height, fit: fit);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../dashbot_dashboard.dart';
import '../providers/dashbot_window_notifier.dart';
void showDashbotWindow(BuildContext context, WidgetRef ref) {
final isDashbotActive = ref.read(dashbotWindowNotifierProvider).isActive;
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
if (isDashbotActive) return;
final overlay = Overlay.of(context);
OverlayEntry? entry;
entry = OverlayEntry(
builder:
(context) => DashbotWindow(
onClose: () {
entry?.remove();
windowNotifier.toggleActive();
},
),
);
windowNotifier.toggleActive();
overlay.insert(entry);
}

View File

@@ -0,0 +1,2 @@
export 'show_dashbot.dart';
export 'dashbot_icons.dart';

View File

@@ -0,0 +1,3 @@
export 'dashbot_dashboard.dart';
export 'core/providers/dashbot_window_notifier.dart';
export 'core/utils/utils.dart';

View File

@@ -0,0 +1,147 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:dashbot/core/utils/dashbot_icons.dart';
import 'core/providers/dashbot_window_notifier.dart';
import 'core/routes/dashbot_router.dart';
import 'core/routes/dashbot_routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class DashbotWindow extends ConsumerWidget {
final VoidCallback onClose;
const DashbotWindow({super.key, required this.onClose});
@override
Widget build(BuildContext context, WidgetRef ref) {
final windowState = ref.watch(dashbotWindowNotifierProvider);
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
// final RequestModel? currentRequest = ref.watch(
// selectedRequestModelProvider,
// );
return Stack(
children: [
Positioned(
right: windowState.right,
bottom: windowState.bottom,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(8),
child: Container(
width: windowState.width,
height: windowState.height,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Column(
children: [
// This is to update position
GestureDetector(
onPanUpdate: (details) {
windowNotifier.updatePosition(
details.delta.dx,
details.delta.dy,
MediaQuery.of(context).size,
);
},
child: Container(
height: 50,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.primaryContainer,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(10),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
kHSpacer20,
DashbotIcons.getDashbotIcon1(width: 38),
kHSpacer12,
Text(
'DashBot',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color:
Theme.of(context).colorScheme.surface,
),
),
],
),
IconButton(
icon: Icon(
Icons.close,
color:
Theme.of(context).colorScheme.onPrimary,
),
onPressed: onClose,
),
],
),
),
),
Expanded(
child: Navigator(
initialRoute: DashbotRoutes.dashbotHome,
// currentRequest?.responseStatus == null
// ? DashbotRoutes.dashbotDefault
// : DashbotRoutes.dashbotHome,
onGenerateRoute: generateRoute,
),
),
],
),
// This is to update size
Positioned(
left: 0,
top: 0,
child: GestureDetector(
onPanUpdate: (details) {
windowNotifier.updateSize(
details.delta.dx,
details.delta.dy,
MediaQuery.of(context).size,
);
},
child: MouseRegion(
cursor: SystemMouseCursors.resizeUpLeft,
child: Container(
padding: EdgeInsets.only(top: 6, left: 1),
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
),
color: Theme.of(context)
.colorScheme
.primaryContainer
.withValues(alpha: 0.7),
),
child: Icon(
Icons.drag_indicator_rounded,
size: 16,
color: Theme.of(context).colorScheme.surfaceBright,
),
),
),
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,135 @@
import 'package:dashbot/core/utils/dashbot_icons.dart';
import '../../../../core/routes/dashbot_routes.dart';
import 'package:apidash_design_system/tokens/measurements.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class DashbotHomePage extends ConsumerStatefulWidget {
const DashbotHomePage({super.key});
@override
ConsumerState<DashbotHomePage> createState() => _DashbotHomePageState();
}
class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
void navigateToChat(String prompt) {
Navigator.of(context).pushNamed(
DashbotRoutes.dashbotChat,
arguments: {'initialPrompt': prompt},
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
kVSpacer16,
DashbotIcons.getDashbotIcon1(width: 60),
kVSpacer16,
Text(
'Hello there,',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800),
),
Text('How can I help you today?'),
kVSpacer16,
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
// TextButton(
// onPressed: () {
// Navigator.of(context).pushNamed(
// DashbotRoutes.dashbotChat,
// );
// },
// style: TextButton.styleFrom(
// side: BorderSide(
// color: Theme.of(context).colorScheme.primary,
// ),
// padding: const EdgeInsets.symmetric(
// vertical: 0,
// horizontal: 16,
// ),
// ),
// child: const Text("🤖 Chat with Dashbot"),
// ),
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
side: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
padding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 16,
),
),
child: const Text("🔎 Explain me this response"),
),
// TextButton(
// onPressed: () {},
// style: TextButton.styleFrom(
// side: BorderSide(
// color: Theme.of(context).colorScheme.primary,
// ),
// padding: const EdgeInsets.symmetric(
// vertical: 0,
// horizontal: 16,
// ),
// ),
// child: const Text("🐞 Help me debug this error"),
// ),
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
side: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
padding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 16,
),
),
child: const Text(
"📄 Generate documentation",
textAlign: TextAlign.center,
),
),
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
side: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
padding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 16,
),
),
child: const Text("📝 Generate Tests"),
),
// TextButton(
// onPressed: () {},
// style: TextButton.styleFrom(
// side: BorderSide(
// color: Theme.of(context).colorScheme.primary,
// ),
// padding: const EdgeInsets.symmetric(
// vertical: 0,
// horizontal: 16,
// ),
// ),
// child: const Text("📊 Generate Visualizations"),
// ),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,63 @@
name: dashbot
description: "A new Flutter package project."
version: 0.0.1
publish_to: none
environment:
sdk: ^3.7.2
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
http: ^1.3.0
apidash_design_system:
path: ../apidash_design_system
apidash_core:
path: ../apidash_core
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.5.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
riverpod_lint: ^2.5.1
riverpod_generator: ^2.5.1
custom_lint: ^0.7.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
assets:
- assets/dashbot_icon_1.png
- assets/dashbot_icon_2.png
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
test('adds one to input values', () {});
}

View File

@@ -0,0 +1,131 @@
import 'dart:ui';
import 'package:dashbot/core/model/dashbot_window_model.dart';
import 'package:dashbot/core/providers/dashbot_window_notifier.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../test/providers/helpers.dart';
void main() {
const testScreenSize = Size(1200, 800);
group("DashbotWindowNotifier - ", () {
test(
'given dashbot window model when instatiated then initial values must match the default values',
() {
final container = createContainer();
final initialState = container.read(dashbotWindowNotifierProvider);
expect(initialState, const DashbotWindowModel());
expect(initialState.width, 350);
expect(initialState.height, 450);
expect(initialState.right, 50);
expect(initialState.bottom, 100);
expect(initialState.isActive, false);
});
test('Toggle active state', () {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Initial state is false
expect(container.read(dashbotWindowNotifierProvider).isActive, false);
// First toggle
notifier.toggleActive();
expect(container.read(dashbotWindowNotifierProvider).isActive, true);
// Second toggle
notifier.toggleActive();
expect(container.read(dashbotWindowNotifierProvider).isActive, false);
});
group('Position updates', () {
test(
'given dashbot window notifier when position is updated 100px to the left and 50px down then then values must be 150 and 50',
() {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Drag 100px left and 50px down
notifier.updatePosition(-100, 50, testScreenSize);
final state = container.read(dashbotWindowNotifierProvider);
expect(state.right, 50 - (-100)); // 50 - (-100) = 150
expect(state.bottom, 100 - 50); // 100 - 50 = 50
});
test(
'given dashbot window notifier when position is updated beyond the left boundary then the value must be clamped to the upper boundary',
() {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Try to drag 1200px left (dx positive when moving right in coordinates)
notifier.updatePosition(-1200, 0, testScreenSize);
final state = container.read(dashbotWindowNotifierProvider);
expect(state.right,
850); // 50 - (-1200) = 1250 → not within bounds, max is screen width(1200) - width(350) = 850
});
test(
'given dashbot window notifier when position is updated beyond bottom boundary then the value must be clamped to the upper boundary',
() {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Move to bottom edge
notifier.updatePosition(0, -700, testScreenSize);
final state = container.read(dashbotWindowNotifierProvider);
// 100 - (-700) = 800 → but max is screenHeight(800) - height(450) = 350
expect(state.bottom, 350);
});
});
group('Size updates', () {
test('Normal resize within bounds', () {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Increase width by 100px, height by 50px
notifier.updateSize(-100, -50, testScreenSize);
final state = container.read(dashbotWindowNotifierProvider);
expect(state.width, 350 - (-100)); // = 450
expect(state.height, 450 - (-50)); // = 500
});
test(
'given dashbot window notifier when tried to resize below the minimum limit then the value must be clamped to the lower boundary',
() {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Try to shrink too much
notifier.updateSize(100, 100, testScreenSize);
final state = container.read(dashbotWindowNotifierProvider);
expect(state.width, 300); // Clamped to minimum
expect(state.height, 350); // Clamped to minimum
});
test(
'given dashbot window notifier when tried to resize above the maximum limit then the value must be clamped to the upper boundary',
() {
final container = createContainer();
final notifier = container.read(dashbotWindowNotifierProvider.notifier);
// Try to expand beyond screen
notifier.updateSize(-1200, -900, testScreenSize);
final state = container.read(dashbotWindowNotifierProvider);
// Max width = screenWidth(1200) - right(50) = 1150
expect(state.width, 1150);
// Max height = screenHeight(800) - bottom(100) = 700
expect(state.height, 700);
});
});
});
}

View File

@@ -381,6 +381,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
dashbot:
dependency: "direct main"
description:
path: "packages/dashbot"
relative: true
source: path
version: "0.0.1"
data_table_2:
dependency: "direct main"
description:
@@ -1388,6 +1395,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_annotation:
dependency: transitive
description:
name: riverpod_annotation
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
url: "https://pub.dev"
source: hosted
version: "2.6.1"
rxdart:
dependency: transitive
description:

View File

@@ -14,6 +14,8 @@ dependencies:
path: packages/apidash_core
apidash_design_system:
path: packages/apidash_design_system
dashbot:
path: packages/dashbot
carousel_slider: ^5.0.0
code_builder: ^4.10.0
csv: ^6.0.0