diff --git a/lib/dashbot/core/common/widgets/dashbot_action.dart b/lib/dashbot/core/common/widgets/dashbot_action.dart new file mode 100644 index 00000000..72bd0029 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import '../../../features/chat/models/chat_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: + return DashbotApplyOpenApiButton(action: action); + 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 == 'apply_openapi') { + return DashbotApplyOpenApiButton(action: action); + } + if (action.action.contains('update') || + action.action.contains('add') || + action.action.contains('delete')) { + return DashbotAutoFixButton(action: action); + } + return null; + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons.dart deleted file mode 100644 index 876200bd..00000000 --- a/lib/dashbot/core/common/widgets/dashbot_action_buttons.dart +++ /dev/null @@ -1,415 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../features/chat/models/chat_models.dart'; -import '../../../features/chat/viewmodel/chat_viewmodel.dart'; -import '../../providers/attachments_provider.dart'; -import 'package:file_selector/file_selector.dart'; -import '../../services/openapi_import_service.dart'; -import '../../../features/chat/view/widgets/openapi_operation_picker_dialog.dart'; -import 'package:openapi_spec/openapi_spec.dart'; -import '../../providers/dashbot_window_notifier.dart'; -import '../../../../utils/save_utils.dart'; -import '../../../../utils/utils.dart'; - -/// Base mixin for action widgets. -mixin DashbotActionMixin { - ChatAction get action; -} - -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 = []; - if (action.value is Map && action.value['accepted_types'] is List) { - final exts = (action.value['accepted_types'] as List) - .whereType() - .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, - ); - } - }, - ); - } -} - -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'), - ); - } -} - -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'), - ); - } -} - -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); - return ElevatedButton( - onPressed: () async { - await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action); - }, - child: Text(label), - ); - } -} - -class DashbotGenerateLanguagePicker extends ConsumerWidget - with DashbotActionMixin { - @override - final ChatAction action; - const DashbotGenerateLanguagePicker({super.key, required this.action}); - - List _extractLanguages(dynamic value) { - if (value is List) { - return value.whereType().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)), - ), - ], - ); - } -} - -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), - ); - } -} - -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)), - ); - } -} - -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) { - final map = action.value as Map; - 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, - ); - await chatNotifier.applyAutoFix(ChatAction.fromJson({ - 'action': 'apply_openapi', - 'actionType': 'apply_openapi', - 'target': 'httpRequestModel', - 'targetType': 'httpRequestModel', - 'field': 'apply_to_new', - 'value': payload, - })); - } - } catch (_) {} - }, - ); - } -} - -class DashbotGeneratedCodeBlock extends StatelessWidget - with DashbotActionMixin { - @override - final ChatAction action; - const DashbotGeneratedCodeBlock({super.key, required this.action}); - - @override - Widget build(BuildContext context) { - final code = (action.value is String) ? action.value as String : ''; - return Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), - child: SelectableText( - code.isEmpty ? '// No code returned' : code, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - ), - ), - ); - } -} - -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, - ); - }, - ); - } -} - -/// 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: - return DashbotApplyOpenApiButton(action: action); - 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 == 'apply_openapi') { - return DashbotApplyOpenApiButton(action: action); - } - if (action.action.contains('update') || - action.action.contains('add') || - action.action.contains('delete')) { - return DashbotAutoFixButton(action: action); - } - return null; - } -} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_actions_buttons.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_actions_buttons.dart new file mode 100644 index 00000000..0e6c0f7f --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_actions_buttons.dart @@ -0,0 +1,10 @@ +export 'dashbot_add_test_button.dart'; +export 'dashbot_apply_curl_button.dart'; +export 'dashbot_apply_openapi_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_upload_requests_button.dart'; +export 'dsahbot_select_operation_button.dart'; diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_add_test_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_add_test_button.dart new file mode 100644 index 00000000..ef623b70 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_add_test_button.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.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'), + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_apply_curl_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_apply_curl_button.dart new file mode 100644 index 00000000..7f52a4f6 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_apply_curl_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.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); + return ElevatedButton( + onPressed: () async { + await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action); + }, + child: Text(label), + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_apply_openapi_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_apply_openapi_button.dart new file mode 100644 index 00000000..2d66db86 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_apply_openapi_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.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), + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_auto_fix_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_auto_fix_button.dart new file mode 100644 index 00000000..41e11538 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_auto_fix_button.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.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'), + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_download_doc_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_download_doc_button.dart new file mode 100644 index 00000000..a22fda6b --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_download_doc_button.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:apidash/utils/utils.dart'; +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../dashbot_action.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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, + ); + }, + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_generate_codeblock.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_generate_codeblock.dart new file mode 100644 index 00000000..1980f135 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_generate_codeblock.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../dashbot_action.dart'; + +class DashbotGeneratedCodeBlock extends StatelessWidget + with DashbotActionMixin { + @override + final ChatAction action; + const DashbotGeneratedCodeBlock({super.key, required this.action}); + + @override + Widget build(BuildContext context) { + final code = (action.value is String) ? action.value as String : ''; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: SelectableText( + code.isEmpty ? '// No code returned' : code, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + ), + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_generate_language_picker_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_generate_language_picker_button.dart new file mode 100644 index 00000000..6ac85226 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_generate_language_picker_button.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.dart'; +import '../dashbot_action.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class DashbotGenerateLanguagePicker extends ConsumerWidget + with DashbotActionMixin { + @override + final ChatAction action; + const DashbotGenerateLanguagePicker({super.key, required this.action}); + + List _extractLanguages(dynamic value) { + if (value is List) { + return value.whereType().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)), + ), + ], + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_import_now_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_import_now_button.dart new file mode 100644 index 00000000..287c00b1 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_import_now_button.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:openapi_spec/openapi_spec.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/view/widgets/openapi_operation_picker_dialog.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.dart'; +import '../../../providers/dashbot_window_notifier.dart'; +import '../../../services/openapi_import_service.dart'; +import '../dashbot_action.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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) { + final map = action.value as Map; + 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, + ); + await chatNotifier.applyAutoFix(ChatAction.fromJson({ + 'action': 'apply_openapi', + 'actionType': 'apply_openapi', + 'target': 'httpRequestModel', + 'targetType': 'httpRequestModel', + 'field': 'apply_to_new', + 'value': payload, + })); + } + } catch (_) {} + }, + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_upload_requests_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_upload_requests_button.dart new file mode 100644 index 00000000..1e919262 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_upload_requests_button.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.dart'; +import '../dashbot_action.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../providers/attachments_provider.dart'; +import 'package:file_selector/file_selector.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 = []; + if (action.value is Map && action.value['accepted_types'] is List) { + final exts = (action.value['accepted_types'] as List) + .whereType() + .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, + ); + } + }, + ); + } +} diff --git a/lib/dashbot/core/common/widgets/dashbot_action_buttons/dsahbot_select_operation_button.dart b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dsahbot_select_operation_button.dart new file mode 100644 index 00000000..2c497b26 --- /dev/null +++ b/lib/dashbot/core/common/widgets/dashbot_action_buttons/dsahbot_select_operation_button.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../../../../features/chat/models/chat_models.dart'; +import '../../../../features/chat/viewmodel/chat_viewmodel.dart'; +import '../dashbot_action.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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)), + ); + } +} diff --git a/lib/dashbot/features/chat/view/widgets/chat_bubble.dart b/lib/dashbot/features/chat/view/widgets/chat_bubble.dart index 28359efc..5a3b8122 100644 --- a/lib/dashbot/features/chat/view/widgets/chat_bubble.dart +++ b/lib/dashbot/features/chat/view/widgets/chat_bubble.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/chat_models.dart'; -import '../../../../core/common/widgets/dashbot_action_buttons.dart'; +import '../../../../core/common/widgets/dashbot_action.dart'; class ChatBubble extends ConsumerWidget { final String message;