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 index 8de744fe..c4728598 100644 --- 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 @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:openapi_spec/openapi_spec.dart'; @@ -55,6 +57,11 @@ class DashbotImportNowButton extends ConsumerWidget with DashbotActionMixin { method: s.method, op: s.op, ); + log("SorceName: $sourceName"); + payload['sourceName'] = + (sourceName != null && sourceName.trim().isNotEmpty) + ? sourceName + : spec.info.title; await chatNotifier.applyAutoFix(ChatAction.fromJson({ 'action': 'apply_openapi', 'actionType': 'apply_openapi', diff --git a/lib/dashbot/core/services/base/url_env_service.dart b/lib/dashbot/core/services/base/url_env_service.dart index 4901dccf..dd9eb333 100644 --- a/lib/dashbot/core/services/base/url_env_service.dart +++ b/lib/dashbot/core/services/base/url_env_service.dart @@ -65,4 +65,110 @@ class UrlEnvService { final normalized = path.startsWith('/') ? path : '/$path'; return '{{$key}}$normalized'; } + + /// Ensure (or create) an environment variable for a base URL coming from an + /// OpenAPI spec import. If the spec had no concrete host (thus parsing the + /// base URL yields no host) we derive a key using the first word of the spec + /// title to avoid every unrelated spec collapsing to BASE_URL_API. + /// + /// Behaviour: + /// - If [baseUrl] is empty: returns a derived key `BASE_URL_` but + /// does NOT create an env var (there is no value to store yet). + /// - If [baseUrl] has a host: behaves like [ensureBaseUrlEnv]. If the host + /// itself cannot be determined, it substitutes the first title word slug. + Future ensureBaseUrlEnvForOpenApi( + String baseUrl, { + required String title, + required Map? Function() readEnvs, + required String? Function() readActiveEnvId, + required void Function(String id, {List? values}) + updateEnv, + }) async { + // Derive slug from title's first word upfront (used as fallback) + final titleSlug = _slugFromOpenApiTitleFirstWord(title); + final trimmedBase = baseUrl.trim(); + final isTrivial = trimmedBase.isEmpty || + trimmedBase == '/' || + // path-only or variable server (no scheme and no host component) + (!trimmedBase.startsWith('http://') && + !trimmedBase.startsWith('https://') && + !trimmedBase.contains('://')); + if (isTrivial) { + final key = 'BASE_URL_$titleSlug'; + + final envs = readEnvs(); + String? activeId = readActiveEnvId(); + activeId ??= kGlobalEnvironmentId; + final envModel = envs?[activeId]; + if (envModel != null) { + final exists = envModel.values.any((v) => v.key == key); + if (!exists) { + final values = [...envModel.values]; + values.add( + EnvironmentVariableModel( + key: key, + value: trimmedBase == '/' ? '' : trimmedBase, + enabled: true, + ), + ); + updateEnv(activeId, values: values); + } + } + return key; + } + + String host = 'API'; + try { + final u = Uri.parse(baseUrl); + if (u.hasAuthority && u.host.isNotEmpty) host = u.host; + } catch (_) {} + + // If host could not be determined (remains 'API'), use title-based slug. + final slug = (host == 'API') + ? titleSlug + : host + .replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_') + .replaceAll(RegExp(r'_+'), '_') + .replaceAll(RegExp(r'^_|_$'), '') + .toUpperCase(); + final key = 'BASE_URL_$slug'; + + final envs = readEnvs(); + String? activeId = readActiveEnvId(); + activeId ??= kGlobalEnvironmentId; + final envModel = envs?[activeId]; + + if (envModel != null) { + final exists = envModel.values.any((v) => v.key == key); + if (!exists) { + final values = [...envModel.values]; + values.add(EnvironmentVariableModel( + key: key, + value: baseUrl, + enabled: true, + )); + updateEnv(activeId, values: values); + } + } + return key; + } + + /// Build a slug from the first word of an OpenAPI spec title. + /// Example: "Pet Store API" -> "PET"; " My-Orders Service" -> "MY". + /// Falls back to 'API' if no alphanumeric characters are present. + String _slugFromOpenApiTitleFirstWord(String title) { + final trimmed = title.trim(); + if (trimmed.isEmpty) return 'API'; + // Split on whitespace, take first non-empty token + final firstToken = trimmed.split(RegExp(r'\s+')).firstWhere( + (t) => t.trim().isNotEmpty, + orElse: () => 'API', + ); + final cleaned = firstToken + .replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_') + .replaceAll(RegExp(r'_+'), '_') + .replaceAll(RegExp(r'^_|_$'), '') + .toUpperCase(); + return cleaned.isEmpty ? 'API' : cleaned; + } } diff --git a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart index 2e48b82f..d590c2e7 100644 --- a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart +++ b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:openapi_spec/openapi_spec.dart'; import 'package:apidash/dashbot/features/chat/models/chat_message.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/foundation.dart'; @@ -38,9 +39,7 @@ class ChatViewmodel extends StateNotifier { List get currentMessages { final id = _currentRequest?.id ?? 'global'; - debugPrint('[Chat] Getting messages for request ID: $id'); final messages = state.chatSessions[id] ?? const []; - debugPrint('[Chat] Found ${messages.length} messages'); return messages; } @@ -49,8 +48,6 @@ class ChatViewmodel extends StateNotifier { ChatMessageType type = ChatMessageType.general, bool countAsUser = true, }) async { - debugPrint( - '[Chat] sendMessage start: type=$type, countAsUser=$countAsUser'); final ai = _selectedAIModel; if (text.trim().isEmpty && countAsUser) return; if (ai == null && @@ -66,7 +63,6 @@ class ChatViewmodel extends StateNotifier { final requestId = _currentRequest?.id ?? 'global'; final existingMessages = state.chatSessions[requestId] ?? const []; - debugPrint('[Chat] using requestId=$requestId'); if (countAsUser) { _addMessage( @@ -211,8 +207,6 @@ class ChatViewmodel extends StateNotifier { userPrompt: userPrompt, stream: false, ); - debugPrint( - '[Chat] prompts prepared: system=${systemPrompt.length} chars, user=${userPrompt.length} chars'); state = state.copyWith(isGenerating: true, currentStreamingResponse: ''); try { @@ -297,17 +291,20 @@ class ChatViewmodel extends StateNotifier { Future applyAutoFix(ChatAction action) async { try { + if (action.actionType == ChatActionType.applyOpenApi) { + await _applyOpenApi(action); + return; + } + if (action.actionType == ChatActionType.applyCurl) { + await _applyCurl(action); + return; + } + final msg = await _ref.read(autoFixServiceProvider).apply(action); if (msg != null && msg.isNotEmpty) { - // Message type depends on action context; choose sensible defaults - final t = (action.actionType == ChatActionType.applyCurl) - ? ChatMessageType.importCurl - : (action.actionType == ChatActionType.applyOpenApi) - ? ChatMessageType.importOpenApi - : ChatMessageType.general; + final t = ChatMessageType.general; _appendSystem(msg, t); } - // Only target-specific 'other' actions remain here if (action.actionType == ChatActionType.other) { await _applyOtherAction(action); } @@ -344,7 +341,6 @@ class ChatViewmodel extends StateNotifier { } Future _applyOpenApi(ChatAction action) async { - final requestId = _currentRequest?.id; final collection = _ref.read(collectionStateNotifierProvider.notifier); final payload = action.value is Map ? (action.value as Map) @@ -413,28 +409,26 @@ class ChatViewmodel extends StateNotifier { } } - final withEnvUrl = await _maybeSubstituteBaseUrl(url, baseUrl); - if (action.field == 'apply_to_selected') { - if (requestId == null) return; - final replacingBody = - (formFlag || formData.isNotEmpty) ? '' : (body ?? ''); - final replacingFormData = - formData.isEmpty ? const [] : formData; - collection.update( - method: method, - url: withEnvUrl, - headers: headers, - isHeaderEnabledList: List.filled(headers.length, true), - body: replacingBody, - bodyContentType: bodyContentType, - formData: replacingFormData, - params: const [], - isParamEnabledList: const [], - authModel: null, - ); - _appendSystem('Applied OpenAPI operation to the selected request.', - ChatMessageType.importOpenApi); - } else if (action.field == 'apply_to_new') { + + String sourceTitle = (payload['sourceName'] as String?) ?? ''; + if (sourceTitle.trim().isEmpty) { + final specObj = payload['spec']; + if (specObj is OpenApi) { + try { + final t = specObj.info.title.trim(); + if (t.isNotEmpty) sourceTitle = t; + } catch (_) {} + } + } + debugPrint('[OpenAPI] baseUrl="$baseUrl" title="$sourceTitle" url="$url"'); + final withEnvUrl = await _maybeSubstituteBaseUrlForOpenApi( + url, + baseUrl, + sourceTitle, + ); + debugPrint('[OpenAPI] withEnvUrl="$withEnvUrl'); + if (action.field == 'apply_to_new') { + debugPrint('[OpenAPI] withEnvUrl="$withEnvUrl'); final model = HttpRequestModel( method: method, url: withEnvUrl, @@ -936,8 +930,6 @@ class ChatViewmodel extends StateNotifier { // Helpers void _addMessage(String requestId, ChatMessage m) { - debugPrint( - '[Chat] Adding message to request ID: $requestId, actions: ${m.actions?.map((e) => e.toJson()).toList()}'); final msgs = state.chatSessions[requestId] ?? const []; state = state.copyWith( chatSessions: { @@ -945,8 +937,6 @@ class ChatViewmodel extends StateNotifier { requestId: [...msgs, m], }, ); - debugPrint( - '[Chat] Message added, total messages for $requestId: ${(state.chatSessions[requestId]?.length ?? 0)}'); } void _appendSystem(String text, ChatMessageType type) { @@ -1004,6 +994,24 @@ class ChatViewmodel extends StateNotifier { ); } + Future _maybeSubstituteBaseUrlForOpenApi( + String url, String baseUrl, String title) async { + final svc = _ref.read(urlEnvServiceProvider); + return svc.maybeSubstituteBaseUrl( + url, + baseUrl, + ensure: (b) => svc.ensureBaseUrlEnvForOpenApi( + b, + title: title, + readEnvs: () => _ref.read(environmentsStateNotifierProvider), + readActiveEnvId: () => _ref.read(activeEnvironmentIdStateProvider), + updateEnv: (id, {values}) => _ref + .read(environmentsStateNotifierProvider.notifier) + .updateEnvironment(id, values: values), + ), + ); + } + HttpRequestModel _getSubstitutedHttpRequestModel( HttpRequestModel httpRequestModel) { final envMap = _ref.read(availableEnvironmentVariablesStateProvider);