From b286e3457820c740fd69d9f3f77515a2efdc92d9 Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Wed, 24 Sep 2025 20:31:02 +0530 Subject: [PATCH] feat: add support to fetch openapi spec from url --- .../chat/viewmodel/chat_viewmodel.dart | 154 +++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart index 3753abb0..fb100378 100644 --- a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart +++ b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/models/models.dart'; -import 'package:apidash/utils/file_utils.dart'; import 'package:apidash/utils/utils.dart'; import '../../../core/model/chat_attachment.dart'; import '../../../core/services/curl_import_service.dart'; @@ -96,6 +95,30 @@ class ChatViewmodel extends StateNotifier { return; } + // Detect OpenAPI import flow: if the last system message was an OpenAPI import prompt, + // then treat pasted URL or raw spec as part of the import flow. + final lastSystemOpenApi = existingMessages.lastWhere( + (m) => + m.role == MessageRole.system && + m.messageType == ChatMessageType.importOpenApi, + orElse: () => ChatMessage( + id: '', + content: '', + role: MessageRole.system, + timestamp: DateTime.fromMillisecondsSinceEpoch(0), + ), + ); + final openApiFlowActive = lastSystemOpenApi.id.isNotEmpty; + if ((_looksLikeOpenApi(text) || _looksLikeUrl(text)) && + (type == ChatMessageType.importOpenApi || openApiFlowActive)) { + if (_looksLikeOpenApi(text)) { + await handlePotentialOpenApiPaste(text); + } else { + await handlePotentialOpenApiUrl(text); + } + return; + } + final promptBuilder = _ref.read(promptBuilderProvider); // Prepare a substituted copy of current request for prompt context final currentReq = _currentRequest; @@ -155,7 +178,7 @@ class ChatViewmodel extends StateNotifier { ChatMessage( id: getNewUuid(), content: - '{"explnation":"Upload your OpenAPI (JSON or YAML) specification or paste it here.","actions":[${jsonEncode(uploadAction.toJson())}]}', + '{"explnation":"Upload your OpenAPI (JSON or YAML) specification, paste the full spec text, or paste a URL to a spec (e.g., https://api.example.com/openapi.json).","actions":[${jsonEncode(uploadAction.toJson())}]}', role: MessageRole.system, timestamp: DateTime.now(), messageType: ChatMessageType.importOpenApi, @@ -164,6 +187,8 @@ class ChatViewmodel extends StateNotifier { ); if (_looksLikeOpenApi(text)) { await handlePotentialOpenApiPaste(text); + } else if (_looksLikeUrl(text)) { + await handlePotentialOpenApiUrl(text); } state = state.copyWith(isGenerating: false, currentStreamingResponse: ''); return; @@ -600,6 +625,131 @@ class ChatViewmodel extends StateNotifier { } } + bool _looksLikeUrl(String input) { + final t = input.trim(); + if (t.isEmpty) return false; + return t.startsWith('http://') || t.startsWith('https://'); + } + + Future handlePotentialOpenApiUrl(String text) async { + final trimmed = text.trim(); + if (!_looksLikeUrl(trimmed)) return; + state = state.copyWith(isGenerating: true, currentStreamingResponse: ''); + try { + // Build a simple GET using existing networking stack + final httpModel = HttpRequestModel( + method: HTTPVerb.get, + url: trimmed, + headers: const [ + // Hint servers that we can accept JSON or YAML + NameValueModel( + name: 'Accept', + value: 'application/json, application/yaml, text/yaml, */*'), + ], + isHeaderEnabledList: const [true], + ); + + final (resp, _, err) = await sendHttpRequest( + getNewUuid(), + APIType.rest, + httpModel, + ); + + if (err != null) { + final safe = err.replaceAll('"', "'"); + _appendSystem( + '{"explnation":"Failed to fetch URL: $safe","actions":[]}', + ChatMessageType.importOpenApi, + ); + return; + } + if (resp == null) { + _appendSystem( + '{"explnation":"No response received when fetching the URL.","actions":[]}', + ChatMessageType.importOpenApi, + ); + return; + } + + final body = resp.body; + if (body.trim().isEmpty) { + _appendSystem( + '{"explnation":"The fetched URL returned an empty body.","actions":[]}', + ChatMessageType.importOpenApi, + ); + return; + } + + // Try to parse fetched content as OpenAPI + final spec = OpenApiImportService.tryParseSpec(body); + if (spec == null) { + _appendSystem( + '{"explnation":"The fetched content does not look like a valid OpenAPI spec (JSON or YAML).","actions":[]}', + ChatMessageType.importOpenApi, + ); + return; + } + + // Build insights and show picker (reuse local method) + String? insights; + try { + final ai = _selectedAIModel; + if (ai != null) { + final summary = OpenApiImportService.summaryForSpec(spec); + final meta = OpenApiImportService.extractSpecMeta(spec); + final sys = dash.DashbotPrompts() + .openApiInsightsPrompt(specSummary: summary, specMeta: meta); + final res = await _repo.sendChat( + request: ai.copyWith( + systemPrompt: sys, + userPrompt: + 'Provide concise, actionable insights about these endpoints.', + stream: false, + ), + ); + if (res != null && res.isNotEmpty) { + try { + final map = MessageJson.safeParse(res); + if (map['explnation'] is String) insights = map['explnation']; + } catch (_) { + insights = res; + } + } + } + } catch (e) { + debugPrint('[OpenAPI URL] insights error: $e'); + } + + final picker = OpenApiImportService.buildOperationPicker( + spec, + insights: insights, + ); + final rqId = _currentRequest?.id ?? 'global'; + _addMessage( + rqId, + ChatMessage( + id: getNewUuid(), + content: jsonEncode(picker), + role: MessageRole.system, + timestamp: DateTime.now(), + messageType: ChatMessageType.importOpenApi, + actions: (picker['actions'] as List) + .whereType>() + .map(ChatAction.fromJson) + .toList(), + ), + ); + } catch (e) { + final safe = e.toString().replaceAll('"', "'"); + _appendSystem( + '{"explnation":"Failed to fetch or parse OpenAPI from URL: $safe","actions":[]}', + ChatMessageType.importOpenApi, + ); + } finally { + state = state.copyWith(isGenerating: false, currentStreamingResponse: ''); + } + } + Future handlePotentialOpenApiPaste(String text) async { final trimmed = text.trim(); if (!_looksLikeOpenApi(trimmed)) return;