diff --git a/lib/providers/dashbot_context_provider.dart b/lib/providers/dashbot_context_provider.dart new file mode 100644 index 00000000..6c432b36 --- /dev/null +++ b/lib/providers/dashbot_context_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:dashbot/dashbot.dart'; + +import 'providers.dart'; + +/// Derives the DashbotRequestContext from the app's current selection. +final appDashbotRequestContextProvider = + Provider((ref) { + final req = ref.watch(selectedRequestModelProvider); + if (req == null) return null; + return DashbotRequestContext( + apiType: req.apiType, + requestId: req.id, + requestName: req.name, + requestDescription: req.description, + aiRequestModel: req.aiRequestModel, + httpRequestModel: req.httpRequestModel, + responseStatus: req.responseStatus, + responseMessage: req.message, + httpResponseModel: req.httpResponseModel, + ); +}); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 1a906ce5..0747fc02 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -4,3 +4,4 @@ export 'environment_providers.dart'; export 'history_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; +export 'dashbot_context_provider.dart'; diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index b0dc140f..9fdf6357 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -130,7 +130,15 @@ class Dashboard extends ConsumerWidget { floatingActionButton: isDashBotEnabled ? FloatingActionButton( backgroundColor: Theme.of(context).colorScheme.primaryContainer, - onPressed: () => showDashbotWindow(context, ref), + onPressed: () => showDashbotWindow( + context, + ref, + overrides: [ + dashbotRequestContextProvider.overrideWith( + (ref) => ref.watch(appDashbotRequestContextProvider), + ), + ], + ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 6.0, diff --git a/packages/dashbot/lib/core/model/dashbot_request_context.dart b/packages/dashbot/lib/core/model/dashbot_request_context.dart new file mode 100644 index 00000000..4f97cf28 --- /dev/null +++ b/packages/dashbot/lib/core/model/dashbot_request_context.dart @@ -0,0 +1,29 @@ +import 'package:apidash_core/apidash_core.dart'; + +/// Context object that Dashbot needs from the host app. +/// +/// Host apps should create/override a provider that returns this object +/// so Dashbot can react to changes in the current request selection. +class DashbotRequestContext { + final String? requestId; + final String? requestName; + final String? requestDescription; + final APIType apiType; + final AIRequestModel? aiRequestModel; + final HttpRequestModel? httpRequestModel; + final int? responseStatus; + final String? responseMessage; + final HttpResponseModel? httpResponseModel; + + const DashbotRequestContext({ + required this.apiType, + this.requestId, + this.requestName, + this.requestDescription, + this.aiRequestModel, + this.httpRequestModel, + this.responseStatus, + this.responseMessage, + this.httpResponseModel, + }); +} diff --git a/packages/dashbot/lib/core/providers/dashbot_request_provider.dart b/packages/dashbot/lib/core/providers/dashbot_request_provider.dart new file mode 100644 index 00000000..9c550a9f --- /dev/null +++ b/packages/dashbot/lib/core/providers/dashbot_request_provider.dart @@ -0,0 +1,9 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../model/dashbot_request_context.dart'; + +/// Default provider for Dashbot's external request context. +/// The host app should override this provider at the Dashbot subtree. +final dashbotRequestContextProvider = Provider( + (ref) => null, +); diff --git a/packages/dashbot/lib/core/routes/dashbot_router.dart b/packages/dashbot/lib/core/routes/dashbot_router.dart index 43952276..2d481948 100644 --- a/packages/dashbot/lib/core/routes/dashbot_router.dart +++ b/packages/dashbot/lib/core/routes/dashbot_router.dart @@ -1,4 +1,5 @@ import 'package:dashbot/features/chat/view/pages/dashbot_chat_page.dart'; +import 'package:dashbot/features/chat/models/chat_models.dart'; import 'dashbot_routes.dart'; import '../common/pages/dashbot_default_page.dart'; @@ -12,10 +13,11 @@ Route? generateRoute(RouteSettings settings) { case (DashbotRoutes.dashbotDefault): return MaterialPageRoute(builder: (context) => DashbotDefaultPage()); case (DashbotRoutes.dashbotChat): - final args = settings.arguments as Map?; - final initialPrompt = args?['initialPrompt'] as String; + final arg = settings.arguments; + ChatMessageType? initialTask; + if (arg is ChatMessageType) initialTask = arg; return MaterialPageRoute( - builder: (context) => ChatScreen(initialPrompt: initialPrompt), + builder: (context) => ChatScreen(initialTask: initialTask), ); default: return MaterialPageRoute(builder: (context) => DashbotDefaultPage()); diff --git a/packages/dashbot/lib/core/utils/show_dashbot.dart b/packages/dashbot/lib/core/utils/show_dashbot.dart index 68d291af..db90e88d 100644 --- a/packages/dashbot/lib/core/utils/show_dashbot.dart +++ b/packages/dashbot/lib/core/utils/show_dashbot.dart @@ -4,7 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../dashbot_dashboard.dart'; import '../providers/dashbot_window_notifier.dart'; -void showDashbotWindow(BuildContext context, WidgetRef ref) { +/// Optionally pass provider overrides (e.g., dashbotRequestContextProvider) +/// so the host app can feed live context into Dashbot. +void showDashbotWindow( + BuildContext context, + WidgetRef ref, { + List? overrides, +}) { final isDashbotActive = ref.read(dashbotWindowNotifierProvider).isActive; final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier); if (isDashbotActive) return; @@ -13,11 +19,14 @@ void showDashbotWindow(BuildContext context, WidgetRef ref) { entry = OverlayEntry( builder: - (context) => DashbotWindow( - onClose: () { - entry?.remove(); - windowNotifier.toggleActive(); - }, + (context) => ProviderScope( + overrides: overrides ?? const [], + child: DashbotWindow( + onClose: () { + entry?.remove(); + windowNotifier.toggleActive(); + }, + ), ), ); windowNotifier.toggleActive(); diff --git a/packages/dashbot/lib/dashbot.dart b/packages/dashbot/lib/dashbot.dart index 76a71656..0c969383 100644 --- a/packages/dashbot/lib/dashbot.dart +++ b/packages/dashbot/lib/dashbot.dart @@ -1,3 +1,5 @@ export 'dashbot_dashboard.dart'; export 'core/providers/dashbot_window_notifier.dart'; export 'core/utils/utils.dart'; +export 'core/model/dashbot_request_context.dart'; +export 'core/providers/dashbot_request_provider.dart'; diff --git a/packages/dashbot/lib/dashbot_dashboard.dart b/packages/dashbot/lib/dashbot_dashboard.dart index 6ca95c20..00630a16 100644 --- a/packages/dashbot/lib/dashbot_dashboard.dart +++ b/packages/dashbot/lib/dashbot_dashboard.dart @@ -2,6 +2,7 @@ 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/providers/dashbot_request_provider.dart'; import 'core/routes/dashbot_router.dart'; import 'core/routes/dashbot_routes.dart'; import 'package:flutter/material.dart'; @@ -16,9 +17,7 @@ class DashbotWindow extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final windowState = ref.watch(dashbotWindowNotifierProvider); final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier); - // final RequestModel? currentRequest = ref.watch( - // selectedRequestModelProvider, - // ); + final dashbotCtx = ref.watch(dashbotRequestContextProvider); return Stack( children: [ @@ -64,10 +63,16 @@ class DashbotWindow extends ConsumerWidget { children: [ kHSpacer20, DashbotIcons.getDashbotIcon1(width: 38), - + // TODO: remove the show active request name/model in prod kHSpacer12, Text( - 'DashBot', + dashbotCtx + ?.aiRequestModel + ?.modelApiProvider + ?.name == + null + ? 'DashBot' + : 'DashBot ยท ${dashbotCtx?.aiRequestModel?.modelApiProvider?.name}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, diff --git a/packages/dashbot/lib/features/chat/view/pages/dashbot_chat_page.dart b/packages/dashbot/lib/features/chat/view/pages/dashbot_chat_page.dart index 629bf81a..5eb99970 100644 --- a/packages/dashbot/lib/features/chat/view/pages/dashbot_chat_page.dart +++ b/packages/dashbot/lib/features/chat/view/pages/dashbot_chat_page.dart @@ -1,15 +1,13 @@ -import 'dart:developer'; import 'package:dashbot/features/chat/models/chat_models.dart'; import 'package:dashbot/features/chat/view/widgets/chat_bubble.dart'; import 'package:dashbot/features/chat/viewmodel/chat_viewmodel.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:nanoid/nanoid.dart'; class ChatScreen extends ConsumerStatefulWidget { - final String initialPrompt; - const ChatScreen({super.key, required this.initialPrompt}); + final ChatMessageType? initialTask; + const ChatScreen({super.key, this.initialTask}); @override ConsumerState createState() => _ChatScreenState(); @@ -17,139 +15,54 @@ class ChatScreen extends ConsumerStatefulWidget { class _ChatScreenState extends ConsumerState { final TextEditingController _textController = TextEditingController(); - final List _messages = []; - bool _isGenerating = false; - String _currentStreamingResponse = ''; @override void initState() { super.initState(); + // Kick off task-specific prompt after first frame SchedulerBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _sendMessage(promptOverride: widget.initialPrompt); + if (!mounted) return; + final task = widget.initialTask; + if (task != null) { + final vm = ref.read(chatViewmodelProvider.notifier); + vm.sendMessage(text: '', type: task, countAsUser: false); } }); } - void _sendMessage({String? promptOverride}) async { - final messageContent = promptOverride ?? _textController.text; - - if (messageContent.trim().isEmpty) return; - - final userChatMessage = ChatMessage( - id: nanoid(), - content: messageContent, - role: MessageRole.user, - timestamp: DateTime.now(), - messageType: ChatMessageType.general, - ); - - if (promptOverride == null) { - _textController.clear(); - } - - setState(() { - _messages.add(userChatMessage); - _isGenerating = true; - _currentStreamingResponse = ''; - }); - - log("Sending message: $messageContent"); - - final stream = ref - .read(chatViewmodelProvider.notifier) - .sendMessage(messageContent, ChatMessageType.general); - - try { - await for (final result in stream) { - if (!mounted) return; - - result.fold( - (failure) { - log("Error: ${failure.message}"); - if (!mounted) return; - final errorChatMessage = ChatMessage( - id: nanoid(), - content: "Error: ${failure.message}", - role: MessageRole.system, - timestamp: DateTime.now(), - messageType: ChatMessageType.general, - ); - setState(() { - _messages.add(errorChatMessage); - _isGenerating = false; - _currentStreamingResponse = ''; - }); - }, - (response) { - setState(() { - _currentStreamingResponse += response.content; - }); - }, - ); - } - - if (!mounted) return; - - if (_currentStreamingResponse.isNotEmpty) { - final assistantChatMessage = ChatMessage( - id: nanoid(), - content: _currentStreamingResponse, - role: MessageRole.system, - timestamp: DateTime.now(), - messageType: ChatMessageType.general, - ); - _messages.add(assistantChatMessage); - } - - setState(() { - _isGenerating = false; - }); - } catch (e) { - log("Error receiving stream: $e"); - if (!mounted) return; - final errorChatMessage = ChatMessage( - id: nanoid(), - content: "Error: $e", - role: MessageRole.system, - timestamp: DateTime.now(), - messageType: ChatMessageType.general, - ); - setState(() { - _messages.add(errorChatMessage); - _isGenerating = false; - _currentStreamingResponse = ''; - }); - } - } - @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Expanded( - child: - _messages.isEmpty && !_isGenerating - ? const Center(child: Text("Ask me anything!")) - : ListView.builder( - itemCount: _messages.length + (_isGenerating ? 1 : 0), - padding: const EdgeInsets.all(16.0), - reverse: false, - itemBuilder: (context, index) { - if (_isGenerating && index == _messages.length) { - return ChatBubble( - message: _currentStreamingResponse, - role: MessageRole.system, - ); - } - final message = _messages[index]; - return ChatBubble( - message: message.content, - role: message.role, - ); - }, - ), + child: Consumer( + builder: (context, ref, _) { + final state = ref.watch(chatViewmodelProvider); + final vm = ref.read(chatViewmodelProvider.notifier); + final msgs = vm.currentMessages; + if (msgs.isEmpty && !state.isGenerating) { + return const Center(child: Text('Ask me anything!')); + } + return ListView.builder( + itemCount: msgs.length + (state.isGenerating ? 1 : 0), + padding: const EdgeInsets.all(16.0), + itemBuilder: (context, index) { + if (state.isGenerating && index == msgs.length) { + return ChatBubble( + message: state.currentStreamingResponse, + role: MessageRole.system, + ); + } + final message = msgs[index]; + return ChatBubble( + message: message.content, + role: message.role, + ); + }, + ); + }, + ), ), Divider( color: Theme.of(context).colorScheme.surfaceContainerHigh, @@ -165,7 +78,9 @@ class _ChatScreenState extends ConsumerState { controller: _textController, decoration: InputDecoration( hintText: - _isGenerating ? 'Generating...' : 'Ask anything', + ref.watch(chatViewmodelProvider).isGenerating + ? 'Generating...' + : 'Ask anything', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, @@ -173,14 +88,35 @@ class _ChatScreenState extends ConsumerState { filled: true, fillColor: Theme.of(context).colorScheme.surface, ), - enabled: !_isGenerating, - onSubmitted: (_) => _isGenerating ? null : _sendMessage(), + enabled: !ref.watch(chatViewmodelProvider).isGenerating, + onSubmitted: (_) { + final vm = ref.read(chatViewmodelProvider.notifier); + if (!ref.read(chatViewmodelProvider).isGenerating) { + final text = _textController.text; + _textController.clear(); + vm.sendMessage( + text: text, + type: ChatMessageType.general, + ); + } + }, ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.send_rounded), - onPressed: _isGenerating ? null : _sendMessage, + onPressed: + ref.watch(chatViewmodelProvider).isGenerating + ? null + : () { + final vm = ref.read(chatViewmodelProvider.notifier); + final text = _textController.text; + _textController.clear(); + vm.sendMessage( + text: text, + type: ChatMessageType.general, + ); + }, tooltip: 'Send message', ), ], diff --git a/packages/dashbot/pubspec.yaml b/packages/dashbot/pubspec.yaml index b6896193..1d1e67f0 100644 --- a/packages/dashbot/pubspec.yaml +++ b/packages/dashbot/pubspec.yaml @@ -30,6 +30,7 @@ dev_dependencies: riverpod_lint: ^2.5.1 riverpod_generator: ^2.5.1 custom_lint: ^0.7.3 + build_runner: ^2.4.12 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec