mirror of
https://github.com/foss42/apidash.git
synced 2025-12-09 14:40:20 +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 'history_providers.dart';
|
||||||
export 'settings_providers.dart';
|
export 'settings_providers.dart';
|
||||||
export 'ui_providers.dart';
|
export 'ui_providers.dart';
|
||||||
|
export 'dashbot_context_provider.dart';
|
||||||
|
|||||||
@@ -130,7 +130,15 @@ class Dashboard extends ConsumerWidget {
|
|||||||
floatingActionButton: isDashBotEnabled
|
floatingActionButton: isDashBotEnabled
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
onPressed: () => showDashbotWindow(context, ref),
|
onPressed: () => showDashbotWindow(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
overrides: [
|
||||||
|
dashbotRequestContextProvider.overrideWith(
|
||||||
|
(ref) => ref.watch(appDashbotRequestContextProvider),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 6.0,
|
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/view/pages/dashbot_chat_page.dart';
|
||||||
|
import 'package:dashbot/features/chat/models/chat_models.dart';
|
||||||
|
|
||||||
import 'dashbot_routes.dart';
|
import 'dashbot_routes.dart';
|
||||||
import '../common/pages/dashbot_default_page.dart';
|
import '../common/pages/dashbot_default_page.dart';
|
||||||
@@ -12,10 +13,11 @@ Route<dynamic>? generateRoute(RouteSettings settings) {
|
|||||||
case (DashbotRoutes.dashbotDefault):
|
case (DashbotRoutes.dashbotDefault):
|
||||||
return MaterialPageRoute(builder: (context) => DashbotDefaultPage());
|
return MaterialPageRoute(builder: (context) => DashbotDefaultPage());
|
||||||
case (DashbotRoutes.dashbotChat):
|
case (DashbotRoutes.dashbotChat):
|
||||||
final args = settings.arguments as Map<String, dynamic>?;
|
final arg = settings.arguments;
|
||||||
final initialPrompt = args?['initialPrompt'] as String;
|
ChatMessageType? initialTask;
|
||||||
|
if (arg is ChatMessageType) initialTask = arg;
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (context) => ChatScreen(initialPrompt: initialPrompt),
|
builder: (context) => ChatScreen(initialTask: initialTask),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return MaterialPageRoute(builder: (context) => DashbotDefaultPage());
|
return MaterialPageRoute(builder: (context) => DashbotDefaultPage());
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../dashbot_dashboard.dart';
|
import '../../dashbot_dashboard.dart';
|
||||||
import '../providers/dashbot_window_notifier.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 isDashbotActive = ref.read(dashbotWindowNotifierProvider).isActive;
|
||||||
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
|
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
|
||||||
if (isDashbotActive) return;
|
if (isDashbotActive) return;
|
||||||
@@ -13,12 +19,15 @@ void showDashbotWindow(BuildContext context, WidgetRef ref) {
|
|||||||
|
|
||||||
entry = OverlayEntry(
|
entry = OverlayEntry(
|
||||||
builder:
|
builder:
|
||||||
(context) => DashbotWindow(
|
(context) => ProviderScope(
|
||||||
|
overrides: overrides ?? const [],
|
||||||
|
child: DashbotWindow(
|
||||||
onClose: () {
|
onClose: () {
|
||||||
entry?.remove();
|
entry?.remove();
|
||||||
windowNotifier.toggleActive();
|
windowNotifier.toggleActive();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
windowNotifier.toggleActive();
|
windowNotifier.toggleActive();
|
||||||
overlay.insert(entry);
|
overlay.insert(entry);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export 'dashbot_dashboard.dart';
|
export 'dashbot_dashboard.dart';
|
||||||
export 'core/providers/dashbot_window_notifier.dart';
|
export 'core/providers/dashbot_window_notifier.dart';
|
||||||
export 'core/utils/utils.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 'package:dashbot/core/utils/dashbot_icons.dart';
|
||||||
|
|
||||||
import 'core/providers/dashbot_window_notifier.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_router.dart';
|
||||||
import 'core/routes/dashbot_routes.dart';
|
import 'core/routes/dashbot_routes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -16,9 +17,7 @@ class DashbotWindow extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final windowState = ref.watch(dashbotWindowNotifierProvider);
|
final windowState = ref.watch(dashbotWindowNotifierProvider);
|
||||||
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
|
final windowNotifier = ref.read(dashbotWindowNotifierProvider.notifier);
|
||||||
// final RequestModel? currentRequest = ref.watch(
|
final dashbotCtx = ref.watch(dashbotRequestContextProvider);
|
||||||
// selectedRequestModelProvider,
|
|
||||||
// );
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -64,10 +63,16 @@ class DashbotWindow extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
kHSpacer20,
|
kHSpacer20,
|
||||||
DashbotIcons.getDashbotIcon1(width: 38),
|
DashbotIcons.getDashbotIcon1(width: 38),
|
||||||
|
// TODO: remove the show active request name/model in prod
|
||||||
kHSpacer12,
|
kHSpacer12,
|
||||||
Text(
|
Text(
|
||||||
'DashBot',
|
dashbotCtx
|
||||||
|
?.aiRequestModel
|
||||||
|
?.modelApiProvider
|
||||||
|
?.name ==
|
||||||
|
null
|
||||||
|
? 'DashBot'
|
||||||
|
: 'DashBot · ${dashbotCtx?.aiRequestModel?.modelApiProvider?.name}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'package:dashbot/features/chat/models/chat_models.dart';
|
import 'package:dashbot/features/chat/models/chat_models.dart';
|
||||||
import 'package:dashbot/features/chat/view/widgets/chat_bubble.dart';
|
import 'package:dashbot/features/chat/view/widgets/chat_bubble.dart';
|
||||||
import 'package:dashbot/features/chat/viewmodel/chat_viewmodel.dart';
|
import 'package:dashbot/features/chat/viewmodel/chat_viewmodel.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
|
||||||
|
|
||||||
class ChatScreen extends ConsumerStatefulWidget {
|
class ChatScreen extends ConsumerStatefulWidget {
|
||||||
final String initialPrompt;
|
final ChatMessageType? initialTask;
|
||||||
const ChatScreen({super.key, required this.initialPrompt});
|
const ChatScreen({super.key, this.initialTask});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ChatScreen> createState() => _ChatScreenState();
|
ConsumerState<ChatScreen> createState() => _ChatScreenState();
|
||||||
@@ -17,110 +15,19 @@ class ChatScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _ChatScreenState extends ConsumerState<ChatScreen> {
|
class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||||
final TextEditingController _textController = TextEditingController();
|
final TextEditingController _textController = TextEditingController();
|
||||||
final List<ChatMessage> _messages = [];
|
|
||||||
bool _isGenerating = false;
|
|
||||||
String _currentStreamingResponse = '';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// Kick off task-specific prompt after first frame
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
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;
|
if (!mounted) return;
|
||||||
|
final task = widget.initialTask;
|
||||||
result.fold(
|
if (task != null) {
|
||||||
(failure) {
|
final vm = ref.read(chatViewmodelProvider.notifier);
|
||||||
log("Error: ${failure.message}");
|
vm.sendMessage(text: '', type: task, countAsUser: false);
|
||||||
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
|
@override
|
||||||
@@ -129,26 +36,32 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: Consumer(
|
||||||
_messages.isEmpty && !_isGenerating
|
builder: (context, ref, _) {
|
||||||
? const Center(child: Text("Ask me anything!"))
|
final state = ref.watch(chatViewmodelProvider);
|
||||||
: ListView.builder(
|
final vm = ref.read(chatViewmodelProvider.notifier);
|
||||||
itemCount: _messages.length + (_isGenerating ? 1 : 0),
|
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),
|
padding: const EdgeInsets.all(16.0),
|
||||||
reverse: false,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (_isGenerating && index == _messages.length) {
|
if (state.isGenerating && index == msgs.length) {
|
||||||
return ChatBubble(
|
return ChatBubble(
|
||||||
message: _currentStreamingResponse,
|
message: state.currentStreamingResponse,
|
||||||
role: MessageRole.system,
|
role: MessageRole.system,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final message = _messages[index];
|
final message = msgs[index];
|
||||||
return ChatBubble(
|
return ChatBubble(
|
||||||
message: message.content,
|
message: message.content,
|
||||||
role: message.role,
|
role: message.role,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Divider(
|
Divider(
|
||||||
@@ -165,7 +78,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
controller: _textController,
|
controller: _textController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
_isGenerating ? 'Generating...' : 'Ask anything',
|
ref.watch(chatViewmodelProvider).isGenerating
|
||||||
|
? 'Generating...'
|
||||||
|
: 'Ask anything',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
@@ -173,14 +88,35 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
|||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).colorScheme.surface,
|
fillColor: Theme.of(context).colorScheme.surface,
|
||||||
),
|
),
|
||||||
enabled: !_isGenerating,
|
enabled: !ref.watch(chatViewmodelProvider).isGenerating,
|
||||||
onSubmitted: (_) => _isGenerating ? null : _sendMessage(),
|
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),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.send_rounded),
|
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',
|
tooltip: 'Send message',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ dev_dependencies:
|
|||||||
riverpod_lint: ^2.5.1
|
riverpod_lint: ^2.5.1
|
||||||
riverpod_generator: ^2.5.1
|
riverpod_generator: ^2.5.1
|
||||||
custom_lint: ^0.7.3
|
custom_lint: ^0.7.3
|
||||||
|
build_runner: ^2.4.12
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
Reference in New Issue
Block a user