mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 18:57:05 +08:00
feat: add openapi spec import functionality
This commit is contained in:
@@ -45,12 +45,17 @@ class DashbotUploadRequestButton extends ConsumerWidget
|
|||||||
mimeType: file.mimeType ?? 'application/octet-stream',
|
mimeType: file.mimeType ?? 'application/octet-stream',
|
||||||
data: bytes,
|
data: bytes,
|
||||||
);
|
);
|
||||||
// Notify model via a user message to incorporate attachment context.
|
if (action.field == 'openapi_spec') {
|
||||||
ref.read(chatViewmodelProvider.notifier).sendMessage(
|
await ref
|
||||||
text:
|
.read(chatViewmodelProvider.notifier)
|
||||||
'Attached file ${att.name} (id=${att.id}, mime=${att.mimeType}, size=${att.sizeBytes}). You can request its content if needed.',
|
.handleOpenApiAttachment(att);
|
||||||
type: ChatMessageType.general,
|
} 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -95,12 +100,14 @@ class DashbotApplyCurlButton extends ConsumerWidget with DashbotActionMixin {
|
|||||||
final ChatAction action;
|
final ChatAction action;
|
||||||
const DashbotApplyCurlButton({super.key, required this.action});
|
const DashbotApplyCurlButton({super.key, required this.action});
|
||||||
|
|
||||||
String _labelForField(String? field) {
|
String _labelForField(String? field, String? path) {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'apply_to_selected':
|
case 'apply_to_selected':
|
||||||
return 'Apply to Selected';
|
return 'Apply to Selected';
|
||||||
case 'apply_to_new':
|
case 'apply_to_new':
|
||||||
return 'Create New Request';
|
return 'Create New Request';
|
||||||
|
case 'select_operation':
|
||||||
|
return path == null || path.isEmpty ? 'Select Operation' : path;
|
||||||
default:
|
default:
|
||||||
return 'Apply';
|
return 'Apply';
|
||||||
}
|
}
|
||||||
@@ -108,7 +115,7 @@ class DashbotApplyCurlButton extends ConsumerWidget with DashbotActionMixin {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final label = _labelForField(action.field);
|
final label = _labelForField(action.field, action.path);
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
await ref.read(chatViewmodelProvider.notifier).applyAutoFix(action);
|
||||||
@@ -159,6 +166,52 @@ class DashbotGenerateLanguagePicker extends ConsumerWidget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 DashbotGeneratedCodeBlock extends StatelessWidget
|
class DashbotGeneratedCodeBlock extends StatelessWidget
|
||||||
with DashbotActionMixin {
|
with DashbotActionMixin {
|
||||||
@override
|
@override
|
||||||
@@ -193,6 +246,9 @@ class DashbotActionWidgetFactory {
|
|||||||
static Widget? build(ChatAction action) {
|
static Widget? build(ChatAction action) {
|
||||||
switch (action.actionType) {
|
switch (action.actionType) {
|
||||||
case ChatActionType.other:
|
case ChatActionType.other:
|
||||||
|
if (action.field == 'select_operation') {
|
||||||
|
return DashbotSelectOperationButton(action: action);
|
||||||
|
}
|
||||||
if (action.targetType == ChatActionTarget.test) {
|
if (action.targetType == ChatActionTarget.test) {
|
||||||
return DashbotAddTestButton(action: action);
|
return DashbotAddTestButton(action: action);
|
||||||
}
|
}
|
||||||
@@ -207,6 +263,8 @@ class DashbotActionWidgetFactory {
|
|||||||
break;
|
break;
|
||||||
case ChatActionType.applyCurl:
|
case ChatActionType.applyCurl:
|
||||||
return DashbotApplyCurlButton(action: action);
|
return DashbotApplyCurlButton(action: action);
|
||||||
|
case ChatActionType.applyOpenApi:
|
||||||
|
return DashbotApplyOpenApiButton(action: action);
|
||||||
case ChatActionType.updateField:
|
case ChatActionType.updateField:
|
||||||
case ChatActionType.addHeader:
|
case ChatActionType.addHeader:
|
||||||
case ChatActionType.updateHeader:
|
case ChatActionType.updateHeader:
|
||||||
@@ -236,6 +294,9 @@ class DashbotActionWidgetFactory {
|
|||||||
if (action.action == 'apply_curl') {
|
if (action.action == 'apply_curl') {
|
||||||
return DashbotApplyCurlButton(action: action);
|
return DashbotApplyCurlButton(action: action);
|
||||||
}
|
}
|
||||||
|
if (action.action == 'apply_openapi') {
|
||||||
|
return DashbotApplyOpenApiButton(action: action);
|
||||||
|
}
|
||||||
if (action.action.contains('update') ||
|
if (action.action.contains('update') ||
|
||||||
action.action.contains('add') ||
|
action.action.contains('add') ||
|
||||||
action.action.contains('delete')) {
|
action.action.contains('delete')) {
|
||||||
|
|||||||
176
lib/dashbot/core/services/openapi_import_service.dart
Normal file
176
lib/dashbot/core/services/openapi_import_service.dart
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:openapi_spec/openapi_spec.dart';
|
||||||
|
|
||||||
|
/// Service to parse OpenAPI specifications and produce
|
||||||
|
/// a standard action message map understood by Dashbot.
|
||||||
|
class OpenApiImportService {
|
||||||
|
/// Try to parse a JSON or YAML OpenAPI spec string.
|
||||||
|
/// Returns null if parsing fails.
|
||||||
|
static OpenApi? tryParseSpec(String source) {
|
||||||
|
try {
|
||||||
|
// Let the library infer JSON/YAML
|
||||||
|
return OpenApi.fromString(source: source, format: null);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a single request payload from a path + method operation.
|
||||||
|
/// The payload mirrors CurlImportService payload shape for reuse.
|
||||||
|
static Map<String, dynamic> _payloadForOperation({
|
||||||
|
required String baseUrl,
|
||||||
|
required String path,
|
||||||
|
required String method,
|
||||||
|
required Operation op,
|
||||||
|
}) {
|
||||||
|
// Resolve URL (server may include variables; keep as-is if any)
|
||||||
|
final url = baseUrl.endsWith('/')
|
||||||
|
? '${baseUrl.substring(0, baseUrl.length - 1)}$path'
|
||||||
|
: '$baseUrl$path';
|
||||||
|
|
||||||
|
// Headers from parameters in header "in": "header"
|
||||||
|
final headers = <String, String>{};
|
||||||
|
for (final p in op.parameters ?? const []) {
|
||||||
|
// Use direct type checking since the parameter objects are union types
|
||||||
|
if (p is ParameterHeader && p.name != null) {
|
||||||
|
headers[p.name!] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request body and content-type heuristic
|
||||||
|
String? body;
|
||||||
|
bool isForm = false;
|
||||||
|
final formData = <Map<String, String>>[];
|
||||||
|
if (op.requestBody != null) {
|
||||||
|
final content = op.requestBody!.content;
|
||||||
|
// Prefer application/json
|
||||||
|
if (content != null && content.isNotEmpty) {
|
||||||
|
if (content.containsKey('application/json')) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
// Use example if any
|
||||||
|
final media = content['application/json'];
|
||||||
|
final ex = media?.example;
|
||||||
|
if (ex != null) {
|
||||||
|
body = jsonEncode(ex);
|
||||||
|
} else {
|
||||||
|
// Try schema default/example
|
||||||
|
// final schema = media?.schema;
|
||||||
|
// final example = schema?.example;
|
||||||
|
// if (example != null) {
|
||||||
|
// body = jsonEncode(example);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
} else if (content.containsKey('application/x-www-form-urlencoded') ||
|
||||||
|
content.containsKey('multipart/form-data')) {
|
||||||
|
isForm = true;
|
||||||
|
headers['Content-Type'] = content.containsKey('multipart/form-data')
|
||||||
|
? 'multipart/form-data'
|
||||||
|
: 'application/x-www-form-urlencoded';
|
||||||
|
// Populate fields from schema properties if available
|
||||||
|
// final key = content.containsKey('multipart/form-data')
|
||||||
|
// ? 'multipart/form-data'
|
||||||
|
// : 'application/x-www-form-urlencoded';
|
||||||
|
// TODO: Extract form field names from schema if available
|
||||||
|
// if (props != null && props.isNotEmpty) {
|
||||||
|
// for (final entry in props.entries) {
|
||||||
|
// final n = entry.key;
|
||||||
|
// // Using empty placeholder values
|
||||||
|
// formData.add({'name': n, 'value': '', 'type': 'text'});
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'method': method.toUpperCase(),
|
||||||
|
'url': url,
|
||||||
|
'headers': headers,
|
||||||
|
'body': body,
|
||||||
|
'form': isForm,
|
||||||
|
'formData': formData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an action message asking whether to apply to selected/new
|
||||||
|
/// for a single chosen operation.
|
||||||
|
static Map<String, dynamic> buildActionMessageFromPayload(
|
||||||
|
Map<String, dynamic> actionPayload,
|
||||||
|
{String? title}) {
|
||||||
|
final buf = StringBuffer(
|
||||||
|
title ?? 'Parsed the OpenAPI operation. Where should I apply it?');
|
||||||
|
return {
|
||||||
|
'explnation': buf.toString(),
|
||||||
|
'actions': [
|
||||||
|
{
|
||||||
|
'action': 'apply_openapi',
|
||||||
|
'target': 'httpRequestModel',
|
||||||
|
'field': 'apply_to_selected',
|
||||||
|
'path': null,
|
||||||
|
'value': actionPayload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'action': 'apply_openapi',
|
||||||
|
'target': 'httpRequestModel',
|
||||||
|
'field': 'apply_to_new',
|
||||||
|
'path': null,
|
||||||
|
'value': actionPayload,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a list of operations from the spec, and if multiple are found,
|
||||||
|
/// return a JSON with explnation and an actions array of type "other"
|
||||||
|
/// where each action value holds an actionPayload for that operation and
|
||||||
|
/// path/method in the path field for UI display. The Chat model will emit
|
||||||
|
/// a follow-up message once the user picks one.
|
||||||
|
static Map<String, dynamic> buildOperationPicker(OpenApi spec) {
|
||||||
|
final servers = spec.servers ?? const [];
|
||||||
|
final baseUrl = servers.isNotEmpty ? (servers.first.url ?? '/') : '/';
|
||||||
|
final actions = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
(spec.paths ?? const {}).forEach((path, item) {
|
||||||
|
final ops = <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,
|
||||||
|
};
|
||||||
|
ops.forEach((method, op) {
|
||||||
|
if (op == null) return;
|
||||||
|
final payload = _payloadForOperation(
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
op: op,
|
||||||
|
);
|
||||||
|
actions.add({
|
||||||
|
'action': 'other',
|
||||||
|
'target': 'httpRequestModel',
|
||||||
|
'field': 'select_operation',
|
||||||
|
'path': '$method $path',
|
||||||
|
'value': payload,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (actions.isEmpty) {
|
||||||
|
return {
|
||||||
|
'explnation':
|
||||||
|
'No operations found in the OpenAPI spec. Please check the file.',
|
||||||
|
'actions': []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'explnation':
|
||||||
|
'OpenAPI parsed. Select an operation to import as a request:',
|
||||||
|
'actions': actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ enum ChatMessageType {
|
|||||||
generateDoc,
|
generateDoc,
|
||||||
generateCode,
|
generateCode,
|
||||||
importCurl,
|
importCurl,
|
||||||
|
importOpenApi,
|
||||||
general
|
general
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ enum ChatActionType {
|
|||||||
updateMethod,
|
updateMethod,
|
||||||
showLanguages,
|
showLanguages,
|
||||||
applyCurl,
|
applyCurl,
|
||||||
|
applyOpenApi,
|
||||||
other,
|
other,
|
||||||
noAction,
|
noAction,
|
||||||
uploadAsset,
|
uploadAsset,
|
||||||
@@ -139,6 +141,8 @@ ChatActionType _chatActionTypeFromString(String s) {
|
|||||||
return ChatActionType.showLanguages;
|
return ChatActionType.showLanguages;
|
||||||
case 'apply_curl':
|
case 'apply_curl':
|
||||||
return ChatActionType.applyCurl;
|
return ChatActionType.applyCurl;
|
||||||
|
case 'apply_openapi':
|
||||||
|
return ChatActionType.applyOpenApi;
|
||||||
case 'upload_asset':
|
case 'upload_asset':
|
||||||
return ChatActionType.uploadAsset;
|
return ChatActionType.uploadAsset;
|
||||||
case 'no_action':
|
case 'no_action':
|
||||||
@@ -170,6 +174,8 @@ String chatActionTypeToString(ChatActionType t) {
|
|||||||
return 'show_languages';
|
return 'show_languages';
|
||||||
case ChatActionType.applyCurl:
|
case ChatActionType.applyCurl:
|
||||||
return 'apply_curl';
|
return 'apply_curl';
|
||||||
|
case ChatActionType.applyOpenApi:
|
||||||
|
return 'apply_openapi';
|
||||||
case ChatActionType.other:
|
case ChatActionType.other:
|
||||||
return 'other';
|
return 'other';
|
||||||
case ChatActionType.noAction:
|
case ChatActionType.noAction:
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import 'package:apidash/providers/providers.dart';
|
|||||||
import 'package:apidash/models/models.dart';
|
import 'package:apidash/models/models.dart';
|
||||||
import 'package:nanoid/nanoid.dart';
|
import 'package:nanoid/nanoid.dart';
|
||||||
import '../../../core/services/curl_import_service.dart';
|
import '../../../core/services/curl_import_service.dart';
|
||||||
|
import '../../../core/services/openapi_import_service.dart';
|
||||||
|
|
||||||
import '../../../core/utils/safe_parse_json_message.dart';
|
import '../../../core/utils/safe_parse_json_message.dart';
|
||||||
import '../../../core/constants/dashbot_prompts.dart' as dash;
|
import '../../../core/constants/dashbot_prompts.dart' as dash;
|
||||||
import '../models/chat_models.dart';
|
import '../models/chat_models.dart';
|
||||||
import '../repository/chat_remote_repository.dart';
|
import '../repository/chat_remote_repository.dart';
|
||||||
|
import '../providers/attachments_provider.dart';
|
||||||
|
|
||||||
class ChatViewmodel extends StateNotifier<ChatState> {
|
class ChatViewmodel extends StateNotifier<ChatState> {
|
||||||
ChatViewmodel(this._ref) : super(const ChatState());
|
ChatViewmodel(this._ref) : super(const ChatState());
|
||||||
@@ -49,7 +51,9 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
'[Chat] sendMessage start: type=$type, countAsUser=$countAsUser');
|
'[Chat] sendMessage start: type=$type, countAsUser=$countAsUser');
|
||||||
final ai = _selectedAIModel;
|
final ai = _selectedAIModel;
|
||||||
if (text.trim().isEmpty && countAsUser) return;
|
if (text.trim().isEmpty && countAsUser) return;
|
||||||
if (ai == null && type != ChatMessageType.importCurl) {
|
if (ai == null &&
|
||||||
|
type != ChatMessageType.importCurl &&
|
||||||
|
type != ChatMessageType.importOpenApi) {
|
||||||
debugPrint('[Chat] No AI model configured');
|
debugPrint('[Chat] No AI model configured');
|
||||||
_appendSystem(
|
_appendSystem(
|
||||||
'AI model is not configured. Please set one.',
|
'AI model is not configured. Please set one.',
|
||||||
@@ -116,6 +120,40 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
} else if (type == ChatMessageType.importOpenApi) {
|
||||||
|
final rqId = _currentRequest?.id ?? 'global';
|
||||||
|
final uploadAction = ChatAction.fromJson({
|
||||||
|
'action': 'upload_asset',
|
||||||
|
'target': 'attachment',
|
||||||
|
'field': 'openapi_spec',
|
||||||
|
'path': null,
|
||||||
|
'value': {
|
||||||
|
'purpose': 'OpenAPI specification',
|
||||||
|
'accepted_types': [
|
||||||
|
'application/json',
|
||||||
|
'application/yaml',
|
||||||
|
'application/x-yaml',
|
||||||
|
'text/yaml',
|
||||||
|
'text/x-yaml'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
_addMessage(
|
||||||
|
rqId,
|
||||||
|
ChatMessage(
|
||||||
|
id: nanoid(),
|
||||||
|
content:
|
||||||
|
'{"explnation":"Upload your OpenAPI (JSON or YAML) specification or paste it here.","actions":[${jsonEncode(uploadAction.toJson())}]}',
|
||||||
|
role: MessageRole.system,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
messageType: ChatMessageType.importOpenApi,
|
||||||
|
actions: [uploadAction],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (_looksLikeOpenApi(text)) {
|
||||||
|
await handlePotentialOpenApiPaste(text);
|
||||||
|
}
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
systemPrompt = _composeSystemPrompt(_currentRequest, type);
|
systemPrompt = _composeSystemPrompt(_currentRequest, type);
|
||||||
}
|
}
|
||||||
@@ -275,6 +313,9 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
case ChatActionType.applyCurl:
|
case ChatActionType.applyCurl:
|
||||||
await _applyCurl(action);
|
await _applyCurl(action);
|
||||||
break;
|
break;
|
||||||
|
case ChatActionType.applyOpenApi:
|
||||||
|
await _applyOpenApi(action);
|
||||||
|
break;
|
||||||
case ChatActionType.other:
|
case ChatActionType.other:
|
||||||
await _applyOtherAction(action);
|
await _applyOtherAction(action);
|
||||||
break;
|
break;
|
||||||
@@ -415,6 +456,11 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
await _applyCurl(action);
|
await _applyCurl(action);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (action.actionType == ChatActionType.applyOpenApi ||
|
||||||
|
action.field == 'select_operation') {
|
||||||
|
await _applyOpenApi(action);
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Unsupported other action
|
// Unsupported other action
|
||||||
debugPrint('[Chat] Unsupported other action target: ${action.target}');
|
debugPrint('[Chat] Unsupported other action target: ${action.target}');
|
||||||
break;
|
break;
|
||||||
@@ -423,6 +469,112 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _applyOpenApi(ChatAction action) async {
|
||||||
|
final requestId = _currentRequest?.id;
|
||||||
|
final collection = _ref.read(collectionStateNotifierProvider.notifier);
|
||||||
|
final payload = action.value is Map<String, dynamic>
|
||||||
|
? (action.value as Map<String, dynamic>)
|
||||||
|
: <String, dynamic>{};
|
||||||
|
|
||||||
|
String methodStr = (payload['method'] as String?)?.toLowerCase() ?? 'get';
|
||||||
|
final method = HTTPVerb.values.firstWhere(
|
||||||
|
(m) => m.name == methodStr,
|
||||||
|
orElse: () => HTTPVerb.get,
|
||||||
|
);
|
||||||
|
final url = payload['url'] as String? ?? '';
|
||||||
|
|
||||||
|
final headersMap =
|
||||||
|
(payload['headers'] as Map?)?.cast<String, dynamic>() ?? {};
|
||||||
|
final headers = headersMap.entries
|
||||||
|
.map((e) => NameValueModel(name: e.key, value: e.value.toString()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final body = payload['body'] as String?;
|
||||||
|
final formFlag = payload['form'] == true;
|
||||||
|
final formDataListRaw = (payload['formData'] as List?)?.cast<dynamic>();
|
||||||
|
final formData = formDataListRaw == null
|
||||||
|
? <FormDataModel>[]
|
||||||
|
: formDataListRaw
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((e) => FormDataModel(
|
||||||
|
name: (e['name'] as String?) ?? '',
|
||||||
|
value: (e['value'] as String?) ?? '',
|
||||||
|
type: (() {
|
||||||
|
final t = (e['type'] as String?) ?? 'text';
|
||||||
|
try {
|
||||||
|
return FormDataType.values
|
||||||
|
.firstWhere((ft) => ft.name == t);
|
||||||
|
} catch (_) {
|
||||||
|
return FormDataType.text;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
ContentType bodyContentType;
|
||||||
|
if (formFlag || formData.isNotEmpty) {
|
||||||
|
bodyContentType = ContentType.formdata;
|
||||||
|
} else if ((body ?? '').trim().isEmpty) {
|
||||||
|
bodyContentType = ContentType.text;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
jsonDecode(body!);
|
||||||
|
bodyContentType = ContentType.json;
|
||||||
|
} catch (_) {
|
||||||
|
bodyContentType = ContentType.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.field == 'apply_to_selected') {
|
||||||
|
if (requestId == null) return;
|
||||||
|
collection.update(
|
||||||
|
method: method,
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
||||||
|
body: body,
|
||||||
|
bodyContentType: bodyContentType,
|
||||||
|
formData: formData.isEmpty ? null : formData,
|
||||||
|
);
|
||||||
|
_appendSystem('Applied OpenAPI operation to the selected request.',
|
||||||
|
ChatMessageType.importOpenApi);
|
||||||
|
} else if (action.field == 'apply_to_new') {
|
||||||
|
final model = HttpRequestModel(
|
||||||
|
method: method,
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
||||||
|
body: body,
|
||||||
|
bodyContentType: bodyContentType,
|
||||||
|
formData: formData.isEmpty ? null : formData,
|
||||||
|
);
|
||||||
|
collection.addRequestModel(model, name: 'Imported OpenAPI');
|
||||||
|
_appendSystem('Created a new request from the OpenAPI operation.',
|
||||||
|
ChatMessageType.importOpenApi);
|
||||||
|
} else if (action.field == 'select_operation') {
|
||||||
|
// Present apply options for the selected operation
|
||||||
|
final applyMsg = OpenApiImportService.buildActionMessageFromPayload(
|
||||||
|
payload,
|
||||||
|
title: 'Selected ${action.path}. Where should I apply it?',
|
||||||
|
);
|
||||||
|
final rqId = _currentRequest?.id ?? 'global';
|
||||||
|
_addMessage(
|
||||||
|
rqId,
|
||||||
|
ChatMessage(
|
||||||
|
id: nanoid(),
|
||||||
|
content: jsonEncode(applyMsg),
|
||||||
|
role: MessageRole.system,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
messageType: ChatMessageType.importOpenApi,
|
||||||
|
actions: (applyMsg['actions'] as List)
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(ChatAction.fromJson)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _applyTestToPostScript(ChatAction action) async {
|
Future<void> _applyTestToPostScript(ChatAction action) async {
|
||||||
final requestId = _currentRequest?.id;
|
final requestId = _currentRequest?.id;
|
||||||
if (requestId == null) return;
|
if (requestId == null) return;
|
||||||
@@ -482,6 +634,54 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleOpenApiAttachment(ChatAttachment att) async {
|
||||||
|
try {
|
||||||
|
final content = utf8.decode(att.data);
|
||||||
|
await handlePotentialOpenApiPaste(content);
|
||||||
|
} catch (e) {
|
||||||
|
final safe = e.toString().replaceAll('"', "'");
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"Failed to read attachment: $safe","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handlePotentialOpenApiPaste(String text) async {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
if (!_looksLikeOpenApi(trimmed)) return;
|
||||||
|
try {
|
||||||
|
debugPrint('[OpenAPI] Original length: ${trimmed.length}');
|
||||||
|
final spec = OpenApiImportService.tryParseSpec(trimmed);
|
||||||
|
if (spec == null) {
|
||||||
|
_appendSystem(
|
||||||
|
'{"explnation":"Sorry, I couldn\'t parse that OpenAPI spec. Ensure it\'s valid JSON or YAML.","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final picker = OpenApiImportService.buildOperationPicker(spec);
|
||||||
|
final rqId = _currentRequest?.id ?? 'global';
|
||||||
|
_addMessage(
|
||||||
|
rqId,
|
||||||
|
ChatMessage(
|
||||||
|
id: nanoid(),
|
||||||
|
content: jsonEncode(picker),
|
||||||
|
role: MessageRole.system,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
messageType: ChatMessageType.importOpenApi,
|
||||||
|
actions: (picker['actions'] as List)
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(ChatAction.fromJson)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[OpenAPI] Exception: $e');
|
||||||
|
final safe = e.toString().replaceAll('"', "'");
|
||||||
|
_appendSystem('{"explnation":"Parsing failed: $safe","actions":[]}',
|
||||||
|
ChatMessageType.importOpenApi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _applyCurl(ChatAction action) async {
|
Future<void> _applyCurl(ChatAction action) async {
|
||||||
final requestId = _currentRequest?.id;
|
final requestId = _currentRequest?.id;
|
||||||
final collection = _ref.read(collectionStateNotifierProvider.notifier);
|
final collection = _ref.read(collectionStateNotifierProvider.notifier);
|
||||||
@@ -719,6 +919,9 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
case ChatMessageType.importCurl:
|
case ChatMessageType.importCurl:
|
||||||
// No AI prompt needed; handled locally.
|
// No AI prompt needed; handled locally.
|
||||||
return null;
|
return null;
|
||||||
|
case ChatMessageType.importOpenApi:
|
||||||
|
// No AI prompt needed; handled locally.
|
||||||
|
return null;
|
||||||
case ChatMessageType.general:
|
case ChatMessageType.general:
|
||||||
return prompts.generalInteractionPrompt();
|
return prompts.generalInteractionPrompt();
|
||||||
}
|
}
|
||||||
@@ -736,6 +939,21 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
if (t.contains('curl')) return 'cURL';
|
if (t.contains('curl')) return 'cURL';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _looksLikeOpenApi(String text) {
|
||||||
|
final t = text.trim();
|
||||||
|
if (t.isEmpty) return false;
|
||||||
|
if (t.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
final m = jsonDecode(t);
|
||||||
|
if (m is Map &&
|
||||||
|
(m.containsKey('openapi') || m.containsKey('swagger'))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return t.contains('openapi:') || t.contains('swagger:');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final chatViewmodelProvider = StateNotifierProvider<ChatViewmodel, ChatState>((
|
final chatViewmodelProvider = StateNotifierProvider<ChatViewmodel, ChatState>((
|
||||||
|
|||||||
@@ -67,7 +67,16 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
HomeScreenTaskButton(
|
HomeScreenTaskButton(
|
||||||
label: "<EFBFBD>🔎 Explain me this response",
|
label: "📄 Import OpenAPI",
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
DashbotRoutes.dashbotChat,
|
||||||
|
arguments: ChatMessageType.importOpenApi,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
HomeScreenTaskButton(
|
||||||
|
label: "🔎 Explain me this response",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
DashbotRoutes.dashbotChat,
|
DashbotRoutes.dashbotChat,
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@@ -270,6 +270,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
cli_completion:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_completion
|
||||||
|
sha256: "72e8ccc4545f24efa7bbdf3bff7257dc9d62b072dee77513cc54295575bc9220"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.1"
|
||||||
cli_config:
|
cli_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1086,6 +1094,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
version: "7.3.0"
|
||||||
|
mason_logger:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mason_logger
|
||||||
|
sha256: "6d5a989ff41157915cb5162ed6e41196d5e31b070d2f86e1c2edf216996a158c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1221,6 +1237,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.3"
|
version: "0.2.3"
|
||||||
|
openapi_spec:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: openapi_spec
|
||||||
|
sha256: "0de980914cafaab7e2086f5541cefba1fb6a64510ab136bd3828bdf02e26c09d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1444,6 +1468,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
recase:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: recase
|
||||||
|
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ dependencies:
|
|||||||
path: packages/apidash_core
|
path: packages/apidash_core
|
||||||
apidash_design_system:
|
apidash_design_system:
|
||||||
path: packages/apidash_design_system
|
path: packages/apidash_design_system
|
||||||
|
openapi_spec: ^0.15.0
|
||||||
carousel_slider: ^5.0.0
|
carousel_slider: ^5.0.0
|
||||||
code_builder: ^4.10.0
|
code_builder: ^4.10.0
|
||||||
csv: ^6.0.0
|
csv: ^6.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user