mirror of
https://github.com/foss42/apidash.git
synced 2025-12-08 14:08:22 +08:00
Refactor DashBot
This commit is contained in:
136
lib/dashbot/widgets/chat_bubble.dart
Normal file
136
lib/dashbot/widgets/chat_bubble.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:apidash_design_system/apidash_design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../constants.dart';
|
||||
import '../models/models.dart';
|
||||
import '../utils/utils.dart';
|
||||
import 'dashbot_action.dart';
|
||||
|
||||
class ChatBubble extends ConsumerWidget {
|
||||
final String message;
|
||||
final MessageRole role;
|
||||
final String? promptOverride;
|
||||
final List<ChatAction>? actions;
|
||||
|
||||
const ChatBubble({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.role,
|
||||
this.promptOverride,
|
||||
this.actions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final preview =
|
||||
message.length > 100 ? '${message.substring(0, 100)}...' : message;
|
||||
debugPrint(
|
||||
'[ChatBubble] Actions count: ${actions?.length ?? 0} | msg: $preview');
|
||||
if (promptOverride != null &&
|
||||
role == MessageRole.user &&
|
||||
message == promptOverride) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
if (message.isEmpty) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Column(
|
||||
children: [
|
||||
kVSpacer8,
|
||||
DashbotIcons.getDashbotIcon1(width: 42),
|
||||
kVSpacer8,
|
||||
CircularProgressIndicator.adaptive(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Parse agent JSON when role is system and show only the "explanation" field.
|
||||
String renderedMessage = message;
|
||||
if (role == MessageRole.system) {
|
||||
try {
|
||||
final Map<String, dynamic> parsed = MessageJson.safeParse(message);
|
||||
if (parsed.containsKey('explanation')) {
|
||||
final exp = parsed['explanation'];
|
||||
if (exp is String && exp.isNotEmpty) {
|
||||
renderedMessage = exp;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Fallback to raw message
|
||||
}
|
||||
}
|
||||
|
||||
final effectiveActions = actions ?? const [];
|
||||
|
||||
return Align(
|
||||
alignment: role == MessageRole.user
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (role == MessageRole.system) ...[
|
||||
kVSpacer6,
|
||||
DashbotIcons.getDashbotIcon1(width: 42),
|
||||
kVSpacer8,
|
||||
],
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: MarkdownBody(
|
||||
data: renderedMessage.isEmpty ? " " : renderedMessage,
|
||||
selectable: true,
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).copyWith(
|
||||
p: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: role == MessageRole.user
|
||||
? Theme.of(context).colorScheme.inverseSurface
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (role == MessageRole.system) ...[
|
||||
if (effectiveActions.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final a in effectiveActions)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final w = DashbotActionWidgetFactory.build(a);
|
||||
if (w != null) return w;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
ADIconButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: renderedMessage));
|
||||
},
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
icon: Icons.copy_rounded,
|
||||
tooltip: "Copy",
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
83
lib/dashbot/widgets/dashbot_action.dart
Normal file
83
lib/dashbot/widgets/dashbot_action.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../constants.dart';
|
||||
import '../models/models.dart';
|
||||
import 'dashbot_action_buttons/dashbot_actions_buttons.dart';
|
||||
|
||||
/// Base mixin for action widgets.
|
||||
mixin DashbotActionMixin {
|
||||
ChatAction get action;
|
||||
}
|
||||
|
||||
/// Factory to map an action to a widget.
|
||||
class DashbotActionWidgetFactory {
|
||||
static Widget? build(ChatAction action) {
|
||||
switch (action.actionType) {
|
||||
case ChatActionType.other:
|
||||
if (action.action == 'import_now_openapi') {
|
||||
return DashbotImportNowButton(action: action);
|
||||
}
|
||||
if (action.field == 'select_operation') {
|
||||
return DashbotSelectOperationButton(action: action);
|
||||
}
|
||||
if (action.targetType == ChatActionTarget.test) {
|
||||
return DashbotAddTestButton(action: action);
|
||||
}
|
||||
if (action.targetType == ChatActionTarget.code) {
|
||||
return DashbotGeneratedCodeBlock(action: action);
|
||||
}
|
||||
break;
|
||||
case ChatActionType.showLanguages:
|
||||
if (action.targetType == ChatActionTarget.codegen) {
|
||||
return DashbotGenerateLanguagePicker(action: action);
|
||||
}
|
||||
break;
|
||||
case ChatActionType.applyCurl:
|
||||
return DashbotApplyCurlButton(action: action);
|
||||
case ChatActionType.applyOpenApi:
|
||||
if (action.action == 'import_now_openapi') {
|
||||
return DashbotImportNowButton(action: action);
|
||||
}
|
||||
return null;
|
||||
case ChatActionType.downloadDoc:
|
||||
return DashbotDownloadDocButton(action: action);
|
||||
case ChatActionType.noAction:
|
||||
if (action.action == 'import_now_openapi') {
|
||||
return DashbotImportNowButton(action: action);
|
||||
}
|
||||
return null;
|
||||
case ChatActionType.updateField:
|
||||
case ChatActionType.addHeader:
|
||||
case ChatActionType.updateHeader:
|
||||
case ChatActionType.deleteHeader:
|
||||
case ChatActionType.updateBody:
|
||||
case ChatActionType.updateUrl:
|
||||
case ChatActionType.updateMethod:
|
||||
return DashbotAutoFixButton(action: action);
|
||||
|
||||
case ChatActionType.uploadAsset:
|
||||
if (action.targetType == ChatActionTarget.attachment) {
|
||||
return DashbotUploadRequestButton(action: action);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action.action == 'other' && action.target == 'test') {
|
||||
return DashbotAddTestButton(action: action);
|
||||
}
|
||||
if (action.action == 'other' && action.target == 'code') {
|
||||
return DashbotGeneratedCodeBlock(action: action);
|
||||
}
|
||||
if (action.action == 'show_languages' && action.target == 'codegen') {
|
||||
return DashbotGenerateLanguagePicker(action: action);
|
||||
}
|
||||
if (action.action == 'apply_curl') {
|
||||
return DashbotApplyCurlButton(action: action);
|
||||
}
|
||||
if (action.action.contains('update') ||
|
||||
action.action.contains('add') ||
|
||||
action.action.contains('delete')) {
|
||||
return DashbotAutoFixButton(action: action);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export 'dashbot_add_test_button.dart';
|
||||
export 'dashbot_apply_curl_button.dart';
|
||||
export 'dashbot_auto_fix_button.dart';
|
||||
export 'dashbot_download_doc_button.dart';
|
||||
export 'dashbot_generate_codeblock.dart';
|
||||
export 'dashbot_generate_language_picker_button.dart';
|
||||
export 'dashbot_import_now_button.dart';
|
||||
export 'dashbot_select_operation_button.dart';
|
||||
export 'dashbot_upload_requests_button.dart';
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class DashbotAddTestButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotAddTestButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
||||
},
|
||||
icon: const Icon(Icons.playlist_add_check, size: 16),
|
||||
label: const Text('Add Test'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class DashbotApplyCurlButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotApplyCurlButton({super.key, required this.action});
|
||||
|
||||
String _labelForField(String? field, String? path) {
|
||||
switch (field) {
|
||||
case 'apply_to_selected':
|
||||
return 'Apply to Selected';
|
||||
case 'apply_to_new':
|
||||
return 'Create New Request';
|
||||
case 'select_operation':
|
||||
return path == null || path.isEmpty ? 'Select Operation' : path;
|
||||
default:
|
||||
return 'Apply';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final label = _labelForField(action.field, action.path);
|
||||
final isDestructive = action.field == 'apply_to_selected';
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
||||
},
|
||||
child: Text(
|
||||
label,
|
||||
// Destructive action: highlight with error color
|
||||
style: isDestructive
|
||||
? TextStyle(color: Theme.of(context).colorScheme.error)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
|
||||
class DashbotApplyOpenApiButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotApplyOpenApiButton({super.key, required this.action});
|
||||
|
||||
String _labelForField(String? field) {
|
||||
switch (field) {
|
||||
case 'apply_to_selected':
|
||||
return 'Apply to Selected';
|
||||
case 'apply_to_new':
|
||||
return 'Create New Request';
|
||||
default:
|
||||
return 'Apply';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final label = _labelForField(action.field);
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
||||
},
|
||||
child: Text(label),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class DashbotAutoFixButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotAutoFixButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
||||
},
|
||||
icon: const Icon(Icons.auto_fix_high, size: 16),
|
||||
label: const Text('Auto Fix'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:apidash/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../dashbot_action.dart';
|
||||
|
||||
class DashbotDownloadDocButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotDownloadDocButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final docContent = (action.value is String) ? action.value as String : '';
|
||||
final filename = action.path ?? 'api-documentation';
|
||||
|
||||
return ElevatedButton.icon(
|
||||
icon: const Icon(Icons.download, size: 16),
|
||||
label: const Text('Download Documentation'),
|
||||
onPressed: docContent.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final contentBytes = Uint8List.fromList(docContent.codeUnits);
|
||||
|
||||
await saveToDownloads(
|
||||
scaffoldMessenger,
|
||||
content: contentBytes,
|
||||
mimeType: 'text/markdown',
|
||||
ext: 'md',
|
||||
name: filename,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:apidash_design_system/apidash_design_system.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../dashbot_action.dart';
|
||||
|
||||
class DashbotGeneratedCodeBlock extends StatefulWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotGeneratedCodeBlock({super.key, required this.action});
|
||||
|
||||
@override
|
||||
State<DashbotGeneratedCodeBlock> createState() =>
|
||||
_DashbotGeneratedCodeBlockState();
|
||||
}
|
||||
|
||||
class _DashbotGeneratedCodeBlockState extends State<DashbotGeneratedCodeBlock> {
|
||||
bool _isCopied = false;
|
||||
|
||||
Future<void> _copyCode(String code) async {
|
||||
await Clipboard.setData(ClipboardData(text: code));
|
||||
setState(() {
|
||||
_isCopied = true;
|
||||
});
|
||||
|
||||
// Reset the icon back to copy after 1.5 seconds
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCopied = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final code =
|
||||
(widget.action.value is String) ? widget.action.value as String : '';
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final codeTheme = isDark ? kDarkCodeTheme : kLightCodeTheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: codeTheme['root']?.backgroundColor ??
|
||||
Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: code.isNotEmpty ? () => _copyCode(code) : null,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
code.isEmpty ? '// No code returned' : code,
|
||||
style: kCodeStyle.copyWith(
|
||||
fontSize: Theme.of(context).textTheme.bodySmall?.fontSize,
|
||||
color: codeTheme['root']?.color ??
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (code.isNotEmpty)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: ADIconButton(
|
||||
key: ValueKey(_isCopied),
|
||||
icon: _isCopied ? Icons.check : Icons.content_copy,
|
||||
iconSize: 16,
|
||||
tooltip: _isCopied ? 'Copied!' : 'Copy',
|
||||
color: _isCopied
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: (codeTheme['root']?.color ??
|
||||
Theme.of(context).colorScheme.onSurface)
|
||||
.withValues(alpha: 0.6),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () => _copyCode(code),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../constants.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
|
||||
class DashbotGenerateLanguagePicker extends ConsumerWidget
|
||||
with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotGenerateLanguagePicker({super.key, required this.action});
|
||||
|
||||
List<String> _extractLanguages(dynamic value) {
|
||||
if (value is List) {
|
||||
return value.whereType<String>().toList();
|
||||
}
|
||||
return const [
|
||||
'JavaScript (fetch)',
|
||||
'Python (requests)',
|
||||
'Dart (http)',
|
||||
'Go (net/http)',
|
||||
'cURL',
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final langs = _extractLanguages(action.value);
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
for (final l in langs)
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
ref.read(chatViewmodelProvider.notifier).sendMessage(
|
||||
text: 'Please generate code in $l',
|
||||
type: ChatMessageType.generateCode,
|
||||
);
|
||||
},
|
||||
child: Text(l, style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'dart:developer';
|
||||
import 'package:apidash_core/apidash_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../../services/services.dart';
|
||||
import '../dashbot_action.dart';
|
||||
import '../openapi_operation_picker_dialog.dart';
|
||||
|
||||
class DashbotImportNowButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotImportNowButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return FilledButton.icon(
|
||||
icon: const Icon(Icons.playlist_add_check, size: 16),
|
||||
label: const Text('Import Now'),
|
||||
onPressed: () async {
|
||||
try {
|
||||
OpenApi? spec;
|
||||
String? sourceName;
|
||||
final overlayNotifier =
|
||||
ref.read(dashbotWindowNotifierProvider.notifier);
|
||||
final chatNotifier = ref.read(chatViewmodelProvider.notifier);
|
||||
if (action.value is Map<String, dynamic>) {
|
||||
final map = action.value as Map<String, dynamic>;
|
||||
sourceName = map['sourceName'] as String?;
|
||||
if (map['spec'] is OpenApi) {
|
||||
spec = map['spec'] as OpenApi;
|
||||
} else if (map['content'] is String) {
|
||||
spec =
|
||||
OpenApiImportService.tryParseSpec(map['content'] as String);
|
||||
}
|
||||
}
|
||||
if (spec == null) return;
|
||||
|
||||
final servers = spec.servers ?? const [];
|
||||
final baseUrl = servers.isNotEmpty ? (servers.first.url ?? '/') : '/';
|
||||
overlayNotifier.hide();
|
||||
final selected = await showOpenApiOperationPickerDialog(
|
||||
context: context,
|
||||
spec: spec,
|
||||
sourceName: sourceName,
|
||||
);
|
||||
overlayNotifier.show();
|
||||
if (selected == null || selected.isEmpty) return;
|
||||
for (final s in selected) {
|
||||
final payload = OpenApiImportService.payloadForOperation(
|
||||
baseUrl: baseUrl,
|
||||
path: s.path,
|
||||
method: s.method,
|
||||
op: s.op,
|
||||
);
|
||||
log("SorceName: $sourceName");
|
||||
payload['sourceName'] =
|
||||
(sourceName != null && sourceName.trim().isNotEmpty)
|
||||
? sourceName
|
||||
: spec.info.title;
|
||||
await chatNotifier.applyAutoFix(ChatAction.fromJson({
|
||||
'action': 'apply_openapi',
|
||||
'actionType': 'apply_openapi',
|
||||
'target': 'httpRequestModel',
|
||||
'targetType': 'httpRequestModel',
|
||||
'field': 'apply_to_new',
|
||||
'value': payload,
|
||||
}));
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
|
||||
class DashbotSelectOperationButton extends ConsumerWidget
|
||||
with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotSelectOperationButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final operationName = action.path ?? 'Unknown';
|
||||
return OutlinedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
||||
},
|
||||
child: Text(operationName, style: const TextStyle(fontSize: 12)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import '../../constants.dart';
|
||||
import '../../models/models.dart';
|
||||
import '../../providers/providers.dart';
|
||||
import '../dashbot_action.dart';
|
||||
|
||||
class DashbotUploadRequestButton extends ConsumerWidget
|
||||
with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotUploadRequestButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final label = action.value is Map && (action.value['purpose'] is String)
|
||||
? 'Upload: ${action.value['purpose'] as String}'
|
||||
: 'Upload Attachment';
|
||||
return OutlinedButton.icon(
|
||||
icon: const Icon(Icons.upload_file, size: 16),
|
||||
label: Text(label, overflow: TextOverflow.ellipsis),
|
||||
onPressed: () async {
|
||||
final types = <XTypeGroup>[];
|
||||
if (action.value is Map && action.value['accepted_types'] is List) {
|
||||
final exts = (action.value['accepted_types'] as List)
|
||||
.whereType<String>()
|
||||
.map((e) => e.trim())
|
||||
.toList();
|
||||
if (exts.isNotEmpty) {
|
||||
types.add(XTypeGroup(label: 'Allowed', mimeTypes: exts));
|
||||
}
|
||||
}
|
||||
final file = await openFile(
|
||||
acceptedTypeGroups:
|
||||
types.isEmpty ? [const XTypeGroup(label: 'Any')] : types);
|
||||
if (file == null) return;
|
||||
final bytes = await file.readAsBytes();
|
||||
final att = ref.read(attachmentsProvider.notifier).add(
|
||||
name: file.name,
|
||||
mimeType: file.mimeType ?? 'application/octet-stream',
|
||||
data: bytes,
|
||||
);
|
||||
if (action.field == 'openapi_spec') {
|
||||
await ref
|
||||
.read(chatViewmodelProvider.notifier)
|
||||
.handleOpenApiAttachment(att);
|
||||
} else {
|
||||
ref.read(chatViewmodelProvider.notifier).sendMessage(
|
||||
text:
|
||||
'Attached file ${att.name} (id=${att.id}, mime=${att.mimeType}, size=${att.sizeBytes}). You can request its content if needed.',
|
||||
type: ChatMessageType.general,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/dashbot/widgets/dashbot_task_buttons.dart
Normal file
126
lib/dashbot/widgets/dashbot_task_buttons.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:apidash/providers/collection_providers.dart';
|
||||
import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart';
|
||||
import 'package:apidash/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart';
|
||||
import '../constants.dart';
|
||||
import '../providers/providers.dart';
|
||||
import 'home_screen_task_button.dart';
|
||||
|
||||
class DashbotTaskButtons extends ConsumerWidget {
|
||||
final VoidCallback? onTaskSelected;
|
||||
|
||||
const DashbotTaskButtons({super.key, this.onTaskSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(chatViewmodelProvider.notifier);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Do you want assistance with any of these tasks?',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
HomeScreenTaskButton(
|
||||
label: '🔎 Explain me this response',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.explainResponse);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '🐞 Help me debug this error',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.debugError);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '📄 Generate documentation',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.generateDoc);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '📝 Generate Tests',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.generateTest);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '🧩 Generate Code',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.generateCode);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '📥 Import cURL',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.importCurl);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '📄 Import OpenAPI',
|
||||
onPressed: () {
|
||||
vm.sendTaskMessage(ChatMessageType.importOpenApi);
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '🛠️ Generate Tool',
|
||||
onPressed: () async {
|
||||
final notifier =
|
||||
ref.read(dashbotWindowNotifierProvider.notifier);
|
||||
notifier.hide();
|
||||
await GenerateToolDialog.show(context, ref);
|
||||
notifier.show();
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
HomeScreenTaskButton(
|
||||
label: '📱 Generate UI',
|
||||
onPressed: () async {
|
||||
final notifier =
|
||||
ref.read(dashbotWindowNotifierProvider.notifier);
|
||||
notifier.hide();
|
||||
final model = ref.watch(selectedRequestModelProvider
|
||||
.select((value) => value?.httpResponseModel));
|
||||
if (model != null) {
|
||||
String data = '';
|
||||
if (model.sseOutput != null) {
|
||||
data = model.sseOutput!.join('');
|
||||
} else {
|
||||
data = model.formattedBody ?? '<>';
|
||||
}
|
||||
await showCustomDialog(
|
||||
context,
|
||||
GenerateUIDialog(content: data),
|
||||
useRootNavigator: true,
|
||||
);
|
||||
}
|
||||
notifier.show();
|
||||
onTaskSelected?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
lib/dashbot/widgets/home_screen_task_button.dart
Normal file
34
lib/dashbot/widgets/home_screen_task_button.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeScreenTaskButton extends StatelessWidget {
|
||||
const HomeScreenTaskButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.textAlign = TextAlign.center,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final TextAlign textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 16,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: textAlign,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
lib/dashbot/widgets/openapi_operation_picker_dialog.dart
Normal file
161
lib/dashbot/widgets/openapi_operation_picker_dialog.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'package:apidash_core/apidash_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef OpenApiOperationItem = ({String method, String path, Operation op});
|
||||
|
||||
Future<List<OpenApiOperationItem>?> showOpenApiOperationPickerDialog({
|
||||
required BuildContext context,
|
||||
required OpenApi spec,
|
||||
String? sourceName,
|
||||
}) async {
|
||||
final title = (spec.info.title.trim().isNotEmpty
|
||||
? spec.info.title.trim()
|
||||
: (sourceName ?? 'OpenAPI'))
|
||||
.trim();
|
||||
|
||||
final ops = <OpenApiOperationItem>[];
|
||||
(spec.paths ?? const {}).forEach((path, item) {
|
||||
final map = <String, Operation?>{
|
||||
'GET': item.get,
|
||||
'POST': item.post,
|
||||
'PUT': item.put,
|
||||
'DELETE': item.delete,
|
||||
'PATCH': item.patch,
|
||||
'HEAD': item.head,
|
||||
'OPTIONS': item.options,
|
||||
'TRACE': item.trace,
|
||||
};
|
||||
map.forEach((method, op) {
|
||||
if (op != null) {
|
||||
ops.add((method: method, path: path, op: op));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (ops.isEmpty) {
|
||||
// Nothing to select; return empty selection.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Multi-select: default select all
|
||||
final selected = <int>{for (var i = 0; i < ops.length; i++) i};
|
||||
bool selectAll = ops.isNotEmpty;
|
||||
String searchQuery = '';
|
||||
|
||||
final scrollController = ScrollController();
|
||||
try {
|
||||
return await showDialog<List<OpenApiOperationItem>>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (ctx) {
|
||||
return StatefulBuilder(builder: (ctx, setState) {
|
||||
// Filter operations based on search query
|
||||
final filteredOps = <int>[];
|
||||
for (int i = 0; i < ops.length; i++) {
|
||||
final o = ops[i];
|
||||
final label = '${o.method} ${o.path}'.toLowerCase();
|
||||
if (searchQuery.isEmpty ||
|
||||
label.contains(searchQuery.toLowerCase())) {
|
||||
filteredOps.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Import from $title'),
|
||||
content: SizedBox(
|
||||
width: 520,
|
||||
height: 420,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// TODO: Create a common Search field widget
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
searchQuery = value;
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Search routes',
|
||||
hintText: 'Type to filter routes...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Select all checkbox
|
||||
CheckboxListTile(
|
||||
value: selectAll,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
selectAll = v ?? false;
|
||||
selected
|
||||
..clear()
|
||||
..addAll(selectAll
|
||||
? List<int>.generate(ops.length, (i) => i)
|
||||
: const <int>{});
|
||||
});
|
||||
},
|
||||
title: const Text('Select all'),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
// Routes list
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: filteredOps.length,
|
||||
itemBuilder: (c, index) {
|
||||
final i = filteredOps[index];
|
||||
final o = ops[i];
|
||||
final label = '${o.method} ${o.path}';
|
||||
final checked = selected.contains(i);
|
||||
return CheckboxListTile(
|
||||
value: checked,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
if (v == true) {
|
||||
selected.add(i);
|
||||
} else {
|
||||
selected.remove(i);
|
||||
}
|
||||
selectAll = selected.length == ops.length;
|
||||
});
|
||||
},
|
||||
title: Text(label),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(null),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: selected.isEmpty
|
||||
? null
|
||||
: () {
|
||||
final result = selected.map((i) => ops[i]).toList();
|
||||
Navigator.of(ctx).pop(result);
|
||||
},
|
||||
child: const Text('Import'),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
scrollController.dispose();
|
||||
}
|
||||
}
|
||||
6
lib/dashbot/widgets/widgets.dart
Normal file
6
lib/dashbot/widgets/widgets.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
export 'chat_bubble.dart';
|
||||
export 'dashbot_action_buttons/dashbot_actions_buttons.dart';
|
||||
export 'dashbot_action.dart';
|
||||
export 'dashbot_task_buttons.dart';
|
||||
export 'home_screen_task_button.dart';
|
||||
export 'openapi_operation_picker_dialog.dart';
|
||||
Reference in New Issue
Block a user