feat: enhance dashbot integration with context providers and ui updates

This commit is contained in:
Udhay-Adithya
2025-09-01 16:00:01 +05:30
parent 0def6c1713
commit 9e8c3b9887
11 changed files with 165 additions and 141 deletions

View File

@@ -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<DashbotRequestContext?>((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,
);
});

View File

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

View File

@@ -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,

View File

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

View File

@@ -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<DashbotRequestContext?>(
(ref) => null,
);

View File

@@ -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<dynamic>? generateRoute(RouteSettings settings) {
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;
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());

View File

@@ -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<Override>? overrides,
}) {
final isDashbotActive = ref.read(dashbotWindowNotifierProvider).isActive;
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
if (isDashbotActive) return;
@@ -13,12 +19,15 @@ void showDashbotWindow(BuildContext context, WidgetRef ref) {
entry = OverlayEntry(
builder:
(context) => DashbotWindow(
(context) => ProviderScope(
overrides: overrides ?? const [],
child: DashbotWindow(
onClose: () {
entry?.remove();
windowNotifier.toggleActive();
},
),
),
);
windowNotifier.toggleActive();
overlay.insert(entry);

View File

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

View File

@@ -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,

View File

@@ -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<ChatScreen> createState() => _ChatScreenState();
@@ -17,110 +15,19 @@ class ChatScreen extends ConsumerStatefulWidget {
class _ChatScreenState extends ConsumerState<ChatScreen> {
final TextEditingController _textController = TextEditingController();
final List<ChatMessage> _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);
}
});
}
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;
});
},
);
final task = widget.initialTask;
if (task != null) {
final vm = ref.read(chatViewmodelProvider.notifier);
vm.sendMessage(text: '', type: task, countAsUser: false);
}
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
@@ -129,26 +36,32 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
body: Column(
children: [
Expanded(
child:
_messages.isEmpty && !_isGenerating
? const Center(child: Text("Ask me anything!"))
: ListView.builder(
itemCount: _messages.length + (_isGenerating ? 1 : 0),
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),
reverse: false,
itemBuilder: (context, index) {
if (_isGenerating && index == _messages.length) {
if (state.isGenerating && index == msgs.length) {
return ChatBubble(
message: _currentStreamingResponse,
message: state.currentStreamingResponse,
role: MessageRole.system,
);
}
final message = _messages[index];
final message = msgs[index];
return ChatBubble(
message: message.content,
role: message.role,
);
},
);
},
),
),
Divider(
@@ -165,7 +78,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
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<ChatScreen> {
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',
),
],

View File

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