From e68674d610a03f956e043b32984a146e1c7ec540 Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Fri, 5 Sep 2025 19:11:51 +0530 Subject: [PATCH] feat: implement auto-fix functionality --- .../core/constants/dashbot_prompts.dart | 40 +++- .../features/chat/models/chat_models.dart | 41 ++++ .../chat/view/pages/dashbot_chat_page.dart | 3 + .../chat/view/widgets/chat_bubble.dart | 36 +++- .../chat/viewmodel/chat_viewmodel.dart | 187 +++++++++++++++++- 5 files changed, 297 insertions(+), 10 deletions(-) diff --git a/lib/dashbot/core/constants/dashbot_prompts.dart b/lib/dashbot/core/constants/dashbot_prompts.dart index 717099df..002484e6 100644 --- a/lib/dashbot/core/constants/dashbot_prompts.dart +++ b/lib/dashbot/core/constants/dashbot_prompts.dart @@ -142,21 +142,47 @@ CONTEXT TASK - Perform root-cause analysis for the error and provide a concise, stepwise debugging plan tailored to the given context. -- Include concrete checks and likely fixes. +- If you can suggest a specific fix, provide an actionable change to the request. +- When suggesting fixes, explain clearly WHAT will be changed and WHY +- Use meaningful placeholder values (like 'your_username' instead of empty strings) +- Make explanations detailed but simple for users to understand OUTPUT FORMAT (STRICT) - Return ONLY a single JSON object. No markdown, no extra text. - The JSON MUST contain both keys: { - "explnation": string, + "explnation": string, // Detailed explanation of the issue and what the fix will do "action": { - "action": "update_request" | "update_header" | "update_body" | "set_env" | "delete" | "other", - "target": "endpoint" | "header" | "body" | "env" | "collection" | "test", - "path": string, // dot path like 'headers.Authorization' or 'body.user.id' - "value": string // specific fix or instruction - } + "action": "update_field" | "add_header" | "update_header" | "delete_header" | "update_body" | "update_url" | "update_method" | "no_action", + "target": "httpRequestModel", + "field": "url" | "method" | "headers" | "body" | "params" | "auth", + "path": string, // specific path like "Authorization" for headers, or "user.id" for body fields + "value": string | object // the new value to set, use meaningful placeholders + } | null } +ACTION GUIDELINES +- Use "update_field" for simple field updates (url, method) +- Use "add_header" to add a new header with meaningful values +- Use "update_header" to modify existing header value +- Use "delete_header" to remove a header +- Use "update_body" for body modifications with proper JSON structure +- For parameters, use object format: {"param_name": "meaningful_placeholder"} +- Set action to null if no specific fix can be suggested +- Always explain WHAT will be changed and provide meaningful placeholder values + +PARAMETER EXAMPLES +- Username: "your_username" or "john_doe" +- Password: "your_password" or "secret123" +- Email: "user@example.com" +- API Key: "your_api_key_here" +- Token: "your_jwt_token" + +EXPLANATION EXAMPLES +- "I'll add the missing 'username' and 'password' query parameters with placeholder values that you can replace with your actual credentials" +- "I'll update the Authorization header to include a Bearer token placeholder" +- "I'll modify the request URL to include the correct API endpoint path" + REFUSAL TEMPLATE (when off-topic), JSON only: {"explnation":"I am Dashbot, an AI assistant focused specifically on API development tasks within API Dash. My capabilities are limited to explaining API responses, debugging requests, generating documentation, creating tests, visualizing API data, and generating integration code. Therefore, I cannot answer questions outside of this scope. How can I assist you with an API-related task?","action":null} diff --git a/lib/dashbot/features/chat/models/chat_models.dart b/lib/dashbot/features/chat/models/chat_models.dart index 194c4fa0..143a6b92 100644 --- a/lib/dashbot/features/chat/models/chat_models.dart +++ b/lib/dashbot/features/chat/models/chat_models.dart @@ -40,6 +40,7 @@ class ChatMessage { final MessageRole role; final DateTime timestamp; final ChatMessageType? messageType; + final ChatAction? action; const ChatMessage({ required this.id, @@ -47,6 +48,7 @@ class ChatMessage { required this.role, required this.timestamp, this.messageType, + this.action, }); ChatMessage copyWith({ @@ -55,6 +57,7 @@ class ChatMessage { MessageRole? role, DateTime? timestamp, ChatMessageType? messageType, + ChatAction? action, }) { return ChatMessage( id: id ?? this.id, @@ -62,6 +65,7 @@ class ChatMessage { role: role ?? this.role, timestamp: timestamp ?? this.timestamp, messageType: messageType ?? this.messageType, + action: action ?? this.action, ); } } @@ -82,6 +86,43 @@ class ChatResponse { enum ChatMessageType { explainResponse, debugError, generateTest, general } +// Action model for auto-fix functionality +class ChatAction { + final String action; + final String target; + final String field; + final String? path; + final dynamic value; + + const ChatAction({ + required this.action, + required this.target, + required this.field, + this.path, + this.value, + }); + + factory ChatAction.fromJson(Map json) { + return ChatAction( + action: json['action'] as String, + target: json['target'] as String, + field: json['field'] as String, + path: json['path'] as String?, + value: json['value'], + ); + } + + Map toJson() { + return { + 'action': action, + 'target': target, + 'field': field, + 'path': path, + 'value': value, + }; + } +} + // Failure classes using fpdart Either pattern abstract class ChatFailure implements Exception { final String message; diff --git a/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart b/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart index b3836f63..c7c1265e 100644 --- a/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart +++ b/lib/dashbot/features/chat/view/pages/dashbot_chat_page.dart @@ -65,9 +65,12 @@ class _ChatScreenState extends ConsumerState { ); } final message = msgs[index]; + debugPrint( + '[ChatPage] Message action: ${message.action?.toJson()}'); return ChatBubble( message: message.content, role: message.role, + action: message.action, ); }, ); diff --git a/lib/dashbot/features/chat/view/widgets/chat_bubble.dart b/lib/dashbot/features/chat/view/widgets/chat_bubble.dart index b3fc091f..6539d50b 100644 --- a/lib/dashbot/features/chat/view/widgets/chat_bubble.dart +++ b/lib/dashbot/features/chat/view/widgets/chat_bubble.dart @@ -4,22 +4,33 @@ import '../../../../core/utils/dashbot_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../viewmodel/chat_viewmodel.dart'; import '../../models/chat_models.dart'; -class ChatBubble extends StatelessWidget { +class ChatBubble extends ConsumerWidget { final String message; final MessageRole role; final String? promptOverride; + final ChatAction? action; const ChatBubble({ super.key, required this.message, required this.role, this.promptOverride, + this.action, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + if (action != null) { + debugPrint('[ChatBubble] Action received: ${action!.toJson()}'); + } else { + final preview = + message.length > 100 ? '${message.substring(0, 100)}...' : message; + debugPrint('[ChatBubble] No action received for message: $preview'); + } if (promptOverride != null && role == MessageRole.user && message == promptOverride) { @@ -94,6 +105,27 @@ class ChatBubble extends StatelessWidget { ), ), if (role == MessageRole.system) ...[ + if (action != null) ...[ + const SizedBox(height: 4), + ElevatedButton.icon( + onPressed: () async { + final chatViewmodel = + ref.read(chatViewmodelProvider.notifier); + await chatViewmodel.applyAutoFix(action!); + debugPrint('Auto-fix applied successfully!'); + }, + icon: const Icon(Icons.auto_fix_high, size: 16), + label: const Text('Auto Fix'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + textStyle: Theme.of(context).textTheme.labelSmall, + ), + ), + ], + const SizedBox(height: 4), IconButton( onPressed: () { Clipboard.setData(ClipboardData(text: message)); diff --git a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart index d85cf6a7..ce614173 100644 --- a/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart +++ b/lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart @@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/models/models.dart'; import 'package:nanoid/nanoid.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'; @@ -31,7 +32,10 @@ class ChatViewmodel extends StateNotifier { List get currentMessages { final id = _currentRequest?.id ?? 'global'; - return state.chatSessions[id] ?? const []; + debugPrint('[Chat] Getting messages for request ID: $id'); + final messages = state.chatSessions[id] ?? const []; + debugPrint('[Chat] Found ${messages.length} messages'); + return messages; } Future sendMessage({ @@ -102,6 +106,27 @@ class ChatViewmodel extends StateNotifier { debugPrint( '[Chat] stream done. total=${state.currentStreamingResponse.length}, anyChunk=$receivedAnyChunk'); if (state.currentStreamingResponse.isNotEmpty) { + ChatAction? parsedAction; + try { + debugPrint( + '[Chat] Attempting to parse response: ${state.currentStreamingResponse}'); + final Map parsed = + MessageJson.safeParse(state.currentStreamingResponse); + debugPrint('[Chat] Parsed JSON: $parsed'); + if (parsed.containsKey('action') && parsed['action'] != null) { + debugPrint('[Chat] Action object found: ${parsed['action']}'); + parsedAction = + ChatAction.fromJson(parsed['action'] as Map); + debugPrint('[Chat] Parsed action: ${parsedAction.toJson()}'); + } else { + debugPrint('[Chat] No action found in response'); + } + } catch (e) { + debugPrint('[Chat] Error parsing action: $e'); + debugPrint('[Chat] Error details: ${e.toString()}'); + // If parsing fails, continue without action + } + _addMessage( requestId, ChatMessage( @@ -110,6 +135,7 @@ class ChatViewmodel extends StateNotifier { role: MessageRole.system, timestamp: DateTime.now(), messageType: type, + action: parsedAction, ), ); } else if (!receivedAnyChunk) { @@ -120,6 +146,20 @@ class ChatViewmodel extends StateNotifier { final fallback = await _repo.sendChat(request: enriched.copyWith(stream: false)); if (fallback != null && fallback.isNotEmpty) { + ChatAction? fallbackAction; + try { + final Map parsed = + MessageJson.safeParse(fallback); + if (parsed.containsKey('action') && parsed['action'] != null) { + fallbackAction = ChatAction.fromJson( + parsed['action'] as Map); + debugPrint( + '[Chat] Fallback parsed action: ${fallbackAction.toJson()}'); + } + } catch (e) { + debugPrint('[Chat] Fallback error parsing action: $e'); + } + _addMessage( requestId, ChatMessage( @@ -128,6 +168,7 @@ class ChatViewmodel extends StateNotifier { role: MessageRole.system, timestamp: DateTime.now(), messageType: type, + action: fallbackAction, ), ); } else { @@ -152,8 +193,150 @@ class ChatViewmodel extends StateNotifier { state = state.copyWith(isGenerating: false); } + Future applyAutoFix(ChatAction action) async { + final requestId = _currentRequest?.id; + if (requestId == null) return; + + try { + switch (action.action) { + case 'update_field': + await _applyFieldUpdate(action); + break; + case 'add_header': + await _applyHeaderUpdate(action, isAdd: true); + break; + case 'update_header': + await _applyHeaderUpdate(action, isAdd: false); + break; + case 'delete_header': + await _applyHeaderDelete(action); + break; + case 'update_body': + await _applyBodyUpdate(action); + break; + case 'update_url': + await _applyUrlUpdate(action); + break; + case 'update_method': + await _applyMethodUpdate(action); + break; + default: + debugPrint('[Chat] Unsupported action: ${action.action}'); + } + } catch (e) { + debugPrint('[Chat] Error applying auto-fix: $e'); + _appendSystem('Failed to apply auto-fix: $e', ChatMessageType.general); + } + } + + Future _applyFieldUpdate(ChatAction action) async { + final requestId = _currentRequest?.id; + if (requestId == null) return; + + final collectionNotifier = + _ref.read(collectionStateNotifierProvider.notifier); + + switch (action.field) { + case 'url': + collectionNotifier.update(url: action.value as String, id: requestId); + break; + case 'method': + final method = HTTPVerb.values.firstWhere( + (m) => m.name.toLowerCase() == (action.value as String).toLowerCase(), + orElse: () => HTTPVerb.get, + ); + collectionNotifier.update(method: method, id: requestId); + break; + case 'params': + if (action.value is Map) { + final params = (action.value as Map) + .entries + .map( + (e) => NameValueModel(name: e.key, value: e.value.toString())) + .toList(); + collectionNotifier.update(params: params, id: requestId); + } + break; + } + } + + Future _applyHeaderUpdate(ChatAction action, + {required bool isAdd}) async { + final requestId = _currentRequest?.id; + if (requestId == null || action.path == null) return; + + final collectionNotifier = + _ref.read(collectionStateNotifierProvider.notifier); + final currentRequest = _currentRequest; + if (currentRequest?.httpRequestModel == null) return; + + final headers = List.from( + currentRequest!.httpRequestModel!.headers ?? []); + + if (isAdd) { + headers.add( + NameValueModel(name: action.path!, value: action.value as String)); + } else { + final index = headers.indexWhere((h) => h.name == action.path); + if (index != -1) { + headers[index] = headers[index].copyWith(value: action.value as String); + } + } + + collectionNotifier.update(headers: headers, id: requestId); + } + + Future _applyHeaderDelete(ChatAction action) async { + final requestId = _currentRequest?.id; + if (requestId == null || action.path == null) return; + + final collectionNotifier = + _ref.read(collectionStateNotifierProvider.notifier); + final currentRequest = _currentRequest; + if (currentRequest?.httpRequestModel == null) return; + + final headers = List.from( + currentRequest!.httpRequestModel!.headers ?? []); + headers.removeWhere((h) => h.name == action.path); + + collectionNotifier.update(headers: headers, id: requestId); + } + + Future _applyBodyUpdate(ChatAction action) async { + final requestId = _currentRequest?.id; + if (requestId == null) return; + + final collectionNotifier = + _ref.read(collectionStateNotifierProvider.notifier); + collectionNotifier.update(body: action.value as String, id: requestId); + } + + Future _applyUrlUpdate(ChatAction action) async { + final requestId = _currentRequest?.id; + if (requestId == null) return; + + final collectionNotifier = + _ref.read(collectionStateNotifierProvider.notifier); + collectionNotifier.update(url: action.value as String, id: requestId); + } + + Future _applyMethodUpdate(ChatAction action) async { + final requestId = _currentRequest?.id; + if (requestId == null) return; + + final collectionNotifier = + _ref.read(collectionStateNotifierProvider.notifier); + final method = HTTPVerb.values.firstWhere( + (m) => m.name.toLowerCase() == (action.value as String).toLowerCase(), + orElse: () => HTTPVerb.get, + ); + collectionNotifier.update(method: method, id: requestId); + } + // Helpers void _addMessage(String requestId, ChatMessage m) { + debugPrint( + '[Chat] Adding message to request ID: $requestId, action: ${m.action?.toJson()}'); final msgs = state.chatSessions[requestId] ?? const []; state = state.copyWith( chatSessions: { @@ -161,6 +344,8 @@ 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) {