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',
|
||||
data: bytes,
|
||||
);
|
||||
// Notify model via a user message to incorporate attachment context.
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -95,12 +100,14 @@ class DashbotApplyCurlButton extends ConsumerWidget with DashbotActionMixin {
|
||||
final ChatAction action;
|
||||
const DashbotApplyCurlButton({super.key, required this.action});
|
||||
|
||||
String _labelForField(String? field) {
|
||||
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';
|
||||
}
|
||||
@@ -108,7 +115,7 @@ class DashbotApplyCurlButton extends ConsumerWidget with DashbotActionMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final label = _labelForField(action.field);
|
||||
final label = _labelForField(action.field, action.path);
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
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
|
||||
with DashbotActionMixin {
|
||||
@override
|
||||
@@ -193,6 +246,9 @@ class DashbotActionWidgetFactory {
|
||||
static Widget? build(ChatAction action) {
|
||||
switch (action.actionType) {
|
||||
case ChatActionType.other:
|
||||
if (action.field == 'select_operation') {
|
||||
return DashbotSelectOperationButton(action: action);
|
||||
}
|
||||
if (action.targetType == ChatActionTarget.test) {
|
||||
return DashbotAddTestButton(action: action);
|
||||
}
|
||||
@@ -207,6 +263,8 @@ class DashbotActionWidgetFactory {
|
||||
break;
|
||||
case ChatActionType.applyCurl:
|
||||
return DashbotApplyCurlButton(action: action);
|
||||
case ChatActionType.applyOpenApi:
|
||||
return DashbotApplyOpenApiButton(action: action);
|
||||
case ChatActionType.updateField:
|
||||
case ChatActionType.addHeader:
|
||||
case ChatActionType.updateHeader:
|
||||
@@ -236,6 +294,9 @@ class DashbotActionWidgetFactory {
|
||||
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')) {
|
||||
|
||||
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,
|
||||
generateCode,
|
||||
importCurl,
|
||||
importOpenApi,
|
||||
general
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ enum ChatActionType {
|
||||
updateMethod,
|
||||
showLanguages,
|
||||
applyCurl,
|
||||
applyOpenApi,
|
||||
other,
|
||||
noAction,
|
||||
uploadAsset,
|
||||
@@ -139,6 +141,8 @@ ChatActionType _chatActionTypeFromString(String s) {
|
||||
return ChatActionType.showLanguages;
|
||||
case 'apply_curl':
|
||||
return ChatActionType.applyCurl;
|
||||
case 'apply_openapi':
|
||||
return ChatActionType.applyOpenApi;
|
||||
case 'upload_asset':
|
||||
return ChatActionType.uploadAsset;
|
||||
case 'no_action':
|
||||
@@ -170,6 +174,8 @@ String chatActionTypeToString(ChatActionType t) {
|
||||
return 'show_languages';
|
||||
case ChatActionType.applyCurl:
|
||||
return 'apply_curl';
|
||||
case ChatActionType.applyOpenApi:
|
||||
return 'apply_openapi';
|
||||
case ChatActionType.other:
|
||||
return 'other';
|
||||
case ChatActionType.noAction:
|
||||
|
||||
@@ -7,11 +7,13 @@ import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/models/models.dart';
|
||||
import 'package:nanoid/nanoid.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/constants/dashbot_prompts.dart' as dash;
|
||||
import '../models/chat_models.dart';
|
||||
import '../repository/chat_remote_repository.dart';
|
||||
import '../providers/attachments_provider.dart';
|
||||
|
||||
class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
ChatViewmodel(this._ref) : super(const ChatState());
|
||||
@@ -49,7 +51,9 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
'[Chat] sendMessage start: type=$type, countAsUser=$countAsUser');
|
||||
final ai = _selectedAIModel;
|
||||
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');
|
||||
_appendSystem(
|
||||
'AI model is not configured. Please set one.',
|
||||
@@ -116,6 +120,40 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
),
|
||||
);
|
||||
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 {
|
||||
systemPrompt = _composeSystemPrompt(_currentRequest, type);
|
||||
}
|
||||
@@ -275,6 +313,9 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
case ChatActionType.applyCurl:
|
||||
await _applyCurl(action);
|
||||
break;
|
||||
case ChatActionType.applyOpenApi:
|
||||
await _applyOpenApi(action);
|
||||
break;
|
||||
case ChatActionType.other:
|
||||
await _applyOtherAction(action);
|
||||
break;
|
||||
@@ -415,6 +456,11 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
await _applyCurl(action);
|
||||
break;
|
||||
}
|
||||
if (action.actionType == ChatActionType.applyOpenApi ||
|
||||
action.field == 'select_operation') {
|
||||
await _applyOpenApi(action);
|
||||
break;
|
||||
}
|
||||
// Unsupported other action
|
||||
debugPrint('[Chat] Unsupported other action target: ${action.target}');
|
||||
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 {
|
||||
final requestId = _currentRequest?.id;
|
||||
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 {
|
||||
final requestId = _currentRequest?.id;
|
||||
final collection = _ref.read(collectionStateNotifierProvider.notifier);
|
||||
@@ -719,6 +919,9 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
case ChatMessageType.importCurl:
|
||||
// No AI prompt needed; handled locally.
|
||||
return null;
|
||||
case ChatMessageType.importOpenApi:
|
||||
// No AI prompt needed; handled locally.
|
||||
return null;
|
||||
case ChatMessageType.general:
|
||||
return prompts.generalInteractionPrompt();
|
||||
}
|
||||
@@ -736,6 +939,21 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
if (t.contains('curl')) return 'cURL';
|
||||
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>((
|
||||
|
||||
@@ -67,7 +67,16 @@ class _DashbotHomePageState extends ConsumerState<DashbotHomePage> {
|
||||
},
|
||||
),
|
||||
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: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
DashbotRoutes.dashbotChat,
|
||||
|
||||
32
pubspec.lock
32
pubspec.lock
@@ -270,6 +270,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1086,6 +1094,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1221,6 +1237,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1444,6 +1468,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies:
|
||||
path: packages/apidash_core
|
||||
apidash_design_system:
|
||||
path: packages/apidash_design_system
|
||||
openapi_spec: ^0.15.0
|
||||
carousel_slider: ^5.0.0
|
||||
code_builder: ^4.10.0
|
||||
csv: ^6.0.0
|
||||
|
||||
Reference in New Issue
Block a user