mirror of
https://github.com/foss42/apidash.git
synced 2025-12-08 05:59:15 +08:00
feat: enhance dashbot integration with context providers and ui updates
This commit is contained in:
22
lib/providers/dashbot_context_provider.dart
Normal file
22
lib/providers/dashbot_context_provider.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
29
packages/dashbot/lib/core/model/dashbot_request_context.dart
Normal file
29
packages/dashbot/lib/core/model/dashbot_request_context.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user