From 8edf981628f3c95990a090ae38be15ede3c2c75f Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Wed, 12 Mar 2025 02:34:48 +0530 Subject: [PATCH 1/7] Feat: API Documentation & process general queries --- lib/dashbot/features/documentation.dart | 66 +++++++++++++++++++++++ lib/dashbot/features/general_query.dart | 45 ++++++++++++++++ lib/dashbot/services/dashbot_service.dart | 22 +++++--- lib/dashbot/widgets/dashbot_widget.dart | 8 +++ lib/screens/dashboard.dart | 22 ++++---- 5 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 lib/dashbot/features/documentation.dart create mode 100644 lib/dashbot/features/general_query.dart diff --git a/lib/dashbot/features/documentation.dart b/lib/dashbot/features/documentation.dart new file mode 100644 index 000000000..da8f19494 --- /dev/null +++ b/lib/dashbot/features/documentation.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import '../services/dashbot_service.dart'; +import 'package:apidash/models/request_model.dart'; + +class DocumentationFeature { + final DashBotService _service; + + DocumentationFeature(this._service); + + Future generateApiDocumentation({ + required RequestModel? requestModel, + required dynamic responseModel, + }) async { + if (requestModel == null || responseModel == null) { + return "No recent API requests found."; + } + + final method = requestModel.httpRequestModel?.method + .toString() + .split('.') + .last + .toUpperCase() ?? + "GET"; + final endpoint = requestModel.httpRequestModel?.url ?? "Unknown Endpoint"; + final headers = requestModel.httpRequestModel?.enabledHeadersMap ?? {}; + final parameters = requestModel.httpRequestModel?.enabledParamsMap ?? {}; + final body = requestModel.httpRequestModel?.body; + final rawResponse = responseModel.body; + final responseBody = + rawResponse is String ? rawResponse : jsonEncode(rawResponse); + final statusCode = responseModel.statusCode ?? 0; + + final prompt = """ +API DOCUMENTATION GENERATION + +**API Details:** +- Endpoint: $endpoint +- Method: $method +- Status Code: $statusCode + +**Request Components:** +- Headers: ${headers.isNotEmpty ? jsonEncode(headers) : "None"} +- Query Parameters: ${parameters.isNotEmpty ? jsonEncode(parameters) : "None"} +- Request Body: ${body != null && body.isNotEmpty ? body : "None"} + +**Response Example:** +``` +$responseBody +``` + +**Documentation Instructions:** +Create comprehensive API documentation that includes: + +1. **Overview**: A clear, concise description of what this API endpoint does +2. **Authentication**: Required authentication method based on headers +3. **Request Details**: All required and optional parameters with descriptions +4. **Response Structure**: Breakdown of response fields and their meanings +5. **Error Handling**: Possible error codes and troubleshooting +6. **Example Usage**: A complete code example showing how to call this API + +Format in clean markdown with proper sections and code blocks where appropriate. +"""; + + return _service.generateResponse(prompt); + } +} diff --git a/lib/dashbot/features/general_query.dart b/lib/dashbot/features/general_query.dart new file mode 100644 index 000000000..45f4d3b50 --- /dev/null +++ b/lib/dashbot/features/general_query.dart @@ -0,0 +1,45 @@ +import 'package:ollama_dart/ollama_dart.dart'; +import 'package:apidash/models/request_model.dart'; + +class GeneralQueryFeature { + final OllamaClient _client; + + GeneralQueryFeature(this._client); + + Future generateResponse(String prompt, {RequestModel? requestModel, dynamic responseModel}) async { + // Create a more focused prompt that incorporates request/response context if available + String enhancedPrompt = prompt; + + if (requestModel != null && responseModel != null) { + final method = requestModel.httpRequestModel?.method.toString().split('.').last.toUpperCase() ?? "GET"; + final endpoint = requestModel.httpRequestModel?.url ?? "Unknown Endpoint"; + final statusCode = responseModel.statusCode ?? 0; + + enhancedPrompt = ''' +CONTEXT-AWARE RESPONSE + +**User Question:** +$prompt + +**Related API Context:** +- Endpoint: $endpoint +- Method: $method +- Status Code: $statusCode + +**Instructions:** +1. Directly address the user's specific question +2. Provide relevant, concise information +3. Reference the API context when helpful +4. Focus on practical, actionable insights +5. Avoid generic explanations or documentation + +Respond in a helpful, direct manner that specifically answers what was asked. +'''; + } + + final response = await _client.generateCompletion( + request: GenerateCompletionRequest(model: 'llama3.2:3b', prompt: enhancedPrompt), + ); + return response.response.toString(); + } +} diff --git a/lib/dashbot/services/dashbot_service.dart b/lib/dashbot/services/dashbot_service.dart index 8eb0087c8..8a1abd1ed 100644 --- a/lib/dashbot/services/dashbot_service.dart +++ b/lib/dashbot/services/dashbot_service.dart @@ -1,24 +1,29 @@ import 'package:apidash/dashbot/features/debug.dart'; +import 'package:apidash/dashbot/features/documentation.dart'; import 'package:ollama_dart/ollama_dart.dart'; -import '../features/explain.dart'; +import 'package:apidash/dashbot/features/explain.dart'; import 'package:apidash/models/request_model.dart'; +import 'package:apidash/dashbot/features/general_query.dart'; class DashBotService { final OllamaClient _client; late final ExplainFeature _explainFeature; late final DebugFeature _debugFeature; + late final DocumentationFeature _documentationFeature; + final GeneralQueryFeature _generalQueryFeature; + DashBotService() - : _client = OllamaClient(baseUrl: 'http://127.0.0.1:11434/api') { + : _client = OllamaClient(baseUrl: 'http://127.0.0.1:11434/api'), + _generalQueryFeature = GeneralQueryFeature(OllamaClient(baseUrl: 'http://127.0.0.1:11434/api')) { + _explainFeature = ExplainFeature(this); _debugFeature = DebugFeature(this); + _documentationFeature = DocumentationFeature(this); } Future generateResponse(String prompt) async { - final response = await _client.generateCompletion( - request: GenerateCompletionRequest(model: 'llama3.2:3b', prompt: prompt), - ); - return response.response.toString(); + return _generalQueryFeature.generateResponse(prompt); } Future handleRequest( @@ -29,8 +34,11 @@ class DashBotService { } else if (input == "Debug API") { return _debugFeature.debugApi( requestModel: requestModel, responseModel: responseModel); + } else if (input == "Document API") { + return _documentationFeature.generateApiDocumentation( + requestModel: requestModel, responseModel: responseModel); } - return generateResponse(input); + return _generalQueryFeature.generateResponse(input, requestModel: requestModel, responseModel: responseModel); } } diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index 200d4c5fa..7618fb98d 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -143,6 +143,14 @@ class _DashBotWidgetState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ), + ElevatedButton.icon( + onPressed: () => _sendMessage("Document API"), + icon: const Icon(Icons.description_outlined), + label: const Text("Document"), + style: ElevatedButton.styleFrom ( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + ), ], ); } diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 428ffaebc..cc9a62670 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -126,17 +126,17 @@ class Dashboard extends ConsumerWidget { ), ), // TODO: Release DashBot - // floatingActionButton: FloatingActionButton( - // onPressed: () => showModalBottomSheet( - // context: context, - // isScrollControlled: true, - // builder: (context) => const Padding( - // padding: EdgeInsets.all(16.0), - // child: DashBotWidget(), - // ), - // ), - // child: const Icon(Icons.help_outline), - // ), + floatingActionButton: FloatingActionButton( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => const Padding( + padding: EdgeInsets.all(16.0), + child: DashBotWidget(), + ), + ), + child: const Icon(Icons.help_outline), + ), ); } } From 6fa9d221117b7ae0f3833cd9a325ce51d113e5c0 Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Thu, 13 Mar 2025 02:10:19 +0530 Subject: [PATCH 2/7] Test cases generator --- lib/dashbot/features/test_generator.dart | 97 ++++++ lib/dashbot/services/dashbot_service.dart | 8 +- lib/dashbot/widgets/dashbot_widget.dart | 101 ++++-- lib/dashbot/widgets/test_runner_widget.dart | 352 ++++++++++++++++++++ 4 files changed, 527 insertions(+), 31 deletions(-) create mode 100644 lib/dashbot/features/test_generator.dart create mode 100644 lib/dashbot/widgets/test_runner_widget.dart diff --git a/lib/dashbot/features/test_generator.dart b/lib/dashbot/features/test_generator.dart new file mode 100644 index 000000000..c638964bd --- /dev/null +++ b/lib/dashbot/features/test_generator.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import '../services/dashbot_service.dart'; +import 'package:apidash/models/request_model.dart'; + +class TestGeneratorFeature { + final DashBotService _service; + + TestGeneratorFeature(this._service); + + Future generateApiTests({ + required RequestModel? requestModel, + required dynamic responseModel, + }) async { + if (requestModel == null || responseModel == null) { + return "No recent API requests found."; + } + + final method = requestModel.httpRequestModel?.method + .toString() + .split('.') + .last + .toUpperCase() ?? + "GET"; + final endpoint = requestModel.httpRequestModel?.url ?? "Unknown Endpoint"; + final headers = requestModel.httpRequestModel?.enabledHeadersMap ?? {}; + final parameters = requestModel.httpRequestModel?.enabledParamsMap ?? {}; + final body = requestModel.httpRequestModel?.body; + final rawResponse = responseModel.body; + final responseBody = + rawResponse is String ? rawResponse : jsonEncode(rawResponse); + final statusCode = responseModel.statusCode ?? 0; + + // Extract base URL and endpoint path + Uri uri = Uri.parse(endpoint); + final baseUrl = "${uri.scheme}://${uri.host}"; + final path = uri.path; + + // Analyze parameter types and values + final parameterAnalysis = _analyzeParameters(uri.queryParameters); + + final prompt = """ +EXECUTABLE API TEST CASES GENERATOR + +**API Analysis:** +- Base URL: $baseUrl +- Endpoint: $path +- Method: $method +- Current Parameters: ${uri.queryParameters} +- Current Response: $responseBody (Status: $statusCode) +- Parameter Types: $parameterAnalysis + +**Test Generation Task:** +Generate practical, ready-to-use test cases for this API in cURL format. Each test should be executable immediately. + +Include these test categories: +1. **Valid Cases**: Different valid parameter values (use real-world examples like other country codes if this is a country API) +2. **Invalid Parameter Tests**: Missing parameters, empty values, incorrect formats +3. **Edge Cases**: Special characters, long values, unexpected inputs +4. **Validation Tests**: Test input validation and error handling + +For each test case: +1. Provide a brief description of what the test verifies +2. Include a complete, executable cURL command +3. Show the expected outcome (status code and sample response) +4. Organize tests in a way that's easy to copy and run + +Focus on creating realistic test values based on the API context (e.g., for a country flag API, use real country codes, invalid codes, etc.) +"""; + + return _service.generateResponse(prompt); + } + + String _analyzeParameters(Map parameters) { + if (parameters.isEmpty) { + return "No parameters detected"; + } + + Map analysis = {}; + + parameters.forEach((key, value) { + // Try to determine parameter type and format + if (RegExp(r'^[A-Z]{3}$').hasMatch(value)) { + analysis[key] = "Appears to be a 3-letter country code (ISO 3166-1 alpha-3)"; + } else if (RegExp(r'^[A-Z]{2}$').hasMatch(value)) { + analysis[key] = "Appears to be a 2-letter country code (ISO 3166-1 alpha-2)"; + } else if (RegExp(r'^\d+$').hasMatch(value)) { + analysis[key] = "Numeric value"; + } else if (RegExp(r'^[a-zA-Z]+$').hasMatch(value)) { + analysis[key] = "Alphabetic string"; + } else { + analysis[key] = "Unknown format: $value"; + } + }); + + return jsonEncode(analysis); + } +} diff --git a/lib/dashbot/services/dashbot_service.dart b/lib/dashbot/services/dashbot_service.dart index 8a1abd1ed..64b3ca860 100644 --- a/lib/dashbot/services/dashbot_service.dart +++ b/lib/dashbot/services/dashbot_service.dart @@ -2,6 +2,7 @@ import 'package:apidash/dashbot/features/debug.dart'; import 'package:apidash/dashbot/features/documentation.dart'; import 'package:ollama_dart/ollama_dart.dart'; import 'package:apidash/dashbot/features/explain.dart'; +import 'package:apidash/dashbot/features/test_generator.dart'; // New import import 'package:apidash/models/request_model.dart'; import 'package:apidash/dashbot/features/general_query.dart'; @@ -10,16 +11,16 @@ class DashBotService { late final ExplainFeature _explainFeature; late final DebugFeature _debugFeature; late final DocumentationFeature _documentationFeature; + late final TestGeneratorFeature _testGeneratorFeature; // New feature final GeneralQueryFeature _generalQueryFeature; - DashBotService() : _client = OllamaClient(baseUrl: 'http://127.0.0.1:11434/api'), _generalQueryFeature = GeneralQueryFeature(OllamaClient(baseUrl: 'http://127.0.0.1:11434/api')) { - _explainFeature = ExplainFeature(this); _debugFeature = DebugFeature(this); _documentationFeature = DocumentationFeature(this); + _testGeneratorFeature = TestGeneratorFeature(this); // Initialize new feature } Future generateResponse(String prompt) async { @@ -37,6 +38,9 @@ class DashBotService { } else if (input == "Document API") { return _documentationFeature.generateApiDocumentation( requestModel: requestModel, responseModel: responseModel); + } else if (input == "Test API") { // New condition + return _testGeneratorFeature.generateApiTests( + requestModel: requestModel, responseModel: responseModel); } return _generalQueryFeature.generateResponse(input, requestModel: requestModel, responseModel: responseModel); diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index 7618fb98d..a113bb9d1 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -1,8 +1,8 @@ -// lib/dashbot/widgets/dashbot_widget.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/dashbot/providers/dashbot_providers.dart'; import 'package:apidash/providers/providers.dart'; +import 'test_runner_widget.dart'; import 'chat_bubble.dart'; class DashBotWidget extends ConsumerStatefulWidget { @@ -18,6 +18,8 @@ class _DashBotWidgetState extends ConsumerState { final TextEditingController _controller = TextEditingController(); late ScrollController _scrollController; bool _isLoading = false; + bool _showTestRunner = false; + String _testCases = ''; @override void initState() { @@ -34,6 +36,12 @@ class _DashBotWidgetState extends ConsumerState { Future _sendMessage(String message) async { if (message.trim().isEmpty) return; + + // Reset test runner state when sending a new message + setState(() { + _showTestRunner = false; + }); + final dashBotService = ref.read(dashBotServiceProvider); final requestModel = ref.read(selectedRequestModelProvider); final responseModel = requestModel?.httpResponseModel; @@ -48,10 +56,19 @@ class _DashBotWidgetState extends ConsumerState { try { final response = await dashBotService.handleRequest( message, requestModel, responseModel); + ref.read(chatMessagesProvider.notifier).addMessage({ 'role': 'bot', 'message': response, }); + + // If message was "Test API", show the test runner + if (message == "Test API") { + setState(() { + _showTestRunner = true; + _testCases = response; + }); + } } catch (error, stackTrace) { debugPrint('Error in _sendMessage: $error'); debugPrint('StackTrace: $stackTrace'); @@ -78,30 +95,38 @@ class _DashBotWidgetState extends ConsumerState { final statusCode = requestModel?.httpResponseModel?.statusCode; final showDebugButton = statusCode != null && statusCode >= 400; - return Container( - height: 450, - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)) - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 12), - _buildQuickActions(showDebugButton), - const SizedBox(height: 12), - Expanded(child: _buildChatArea(messages)), - if (_isLoading) _buildLoadingIndicator(), - const SizedBox(height: 10), - _buildInputArea(context), + return Column( + children: [ + Container( + height: 450, + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 12), + _buildQuickActions(showDebugButton), + const SizedBox(height: 12), + Expanded(child: _buildChatArea(messages)), + if (_isLoading) _buildLoadingIndicator(), + const SizedBox(height: 10), + _buildInputArea(context), + ], + ), + ), + if (_showTestRunner) ...[ + const SizedBox(height: 20), + TestRunnerWidget(testCases: _testCases), ], - ), + ], ); } @@ -114,8 +139,12 @@ class _DashBotWidgetState extends ConsumerState { IconButton( icon: const Icon(Icons.delete_sweep), tooltip: 'Clear Chat', - onPressed: () => - ref.read(chatMessagesProvider.notifier).clearMessages(), + onPressed: () { + ref.read(chatMessagesProvider.notifier).clearMessages(); + setState(() { + _showTestRunner = false; + }); + }, ), ], ); @@ -147,7 +176,15 @@ class _DashBotWidgetState extends ConsumerState { onPressed: () => _sendMessage("Document API"), icon: const Icon(Icons.description_outlined), label: const Text("Document"), - style: ElevatedButton.styleFrom ( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + ), + ElevatedButton.icon( + onPressed: () => _sendMessage("Test API"), + icon: const Icon(Icons.science_outlined), + label: const Text("Test"), + style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ), @@ -193,13 +230,19 @@ class _DashBotWidgetState extends ConsumerState { hintText: 'Ask DashBot...', border: InputBorder.none, ), - onSubmitted: _sendMessage, + onSubmitted: (value) { + _sendMessage(value); + _controller.clear(); + }, maxLines: 1, ), ), IconButton( icon: const Icon(Icons.send), - onPressed: () => _sendMessage(_controller.text), + onPressed: () { + _sendMessage(_controller.text); + _controller.clear(); + }, ), ], ), diff --git a/lib/dashbot/widgets/test_runner_widget.dart b/lib/dashbot/widgets/test_runner_widget.dart new file mode 100644 index 000000000..e2a449a18 --- /dev/null +++ b/lib/dashbot/widgets/test_runner_widget.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'content_renderer.dart'; + +class TestRunnerWidget extends ConsumerStatefulWidget { + final String testCases; + + const TestRunnerWidget({Key? key, required this.testCases}) : super(key: key); + + @override + ConsumerState createState() => _TestRunnerWidgetState(); +} + +class _TestRunnerWidgetState extends ConsumerState { + List> _parsedTests = []; + Map> _results = {}; + bool _isRunning = false; + int _currentTestIndex = -1; + + @override + void initState() { + super.initState(); + _parseTestCases(); + } + + void _parseTestCases() { + // Basic parsing of cURL commands from the text + final curlRegex = RegExp(r'```(.*?)curl\s+(.*?)```', dotAll: true); + final descriptionRegex = RegExp(r'###\s*(.*?)\n', dotAll: true); + + final curlMatches = curlRegex.allMatches(widget.testCases); + final descMatches = descriptionRegex.allMatches(widget.testCases); + + List> tests = []; + int index = 0; + + for (var match in curlMatches) { + String? description = "Test case ${index + 1}"; + if (index < descMatches.length) { + description = descMatches.elementAt(index).group(1)?.trim(); + } + + final curlCommand = match.group(2)?.trim() ?? ""; + + tests.add({ + 'description': description, + 'command': curlCommand, + 'index': index, + }); + + index++; + } + + setState(() { + _parsedTests = tests; + }); + } + + Future _runTest(int index) async { + if (_isRunning) return; + + setState(() { + _isRunning = true; + _currentTestIndex = index; + }); + + final test = _parsedTests[index]; + final command = test['command']; + + try { + // Parse curl command to make HTTP request + // This is a simplified version - a real implementation would need to handle all curl options + final urlMatch = RegExp(r'"([^"]*)"').firstMatch(command); + String url = urlMatch?.group(1) ?? ""; + + if (url.isEmpty) { + final urlMatch2 = RegExp(r"'([^']*)'").firstMatch(command); + url = urlMatch2?.group(1) ?? ""; + } + + if (url.isEmpty) { + throw Exception("Could not parse URL from curl command"); + } + + // Determine HTTP method (default to GET) + String method = "GET"; + if (command.contains("-X POST") || command.contains("--request POST")) { + method = "POST"; + } else if (command.contains("-X PUT") || command.contains("--request PUT")) { + method = "PUT"; + } // Add other methods as needed + + // Make the actual request + http.Response response; + if (method == "GET") { + response = await http.get(Uri.parse(url)); + } else if (method == "POST") { + // Extract body if present + final bodyMatch = RegExp(r'-d\s+"([^"]*)"').firstMatch(command); + final body = bodyMatch?.group(1) ?? ""; + response = await http.post(Uri.parse(url), body: body); + } else { + throw Exception("Unsupported HTTP method: $method"); + } + + setState(() { + _results[index] = { + 'status': response.statusCode, + 'body': response.body, + 'headers': response.headers, + 'isSuccess': response.statusCode >= 200 && response.statusCode < 300, + }; + }); + } catch (e) { + setState(() { + _results[index] = { + 'error': e.toString(), + 'isSuccess': false, + }; + }); + } finally { + setState(() { + _isRunning = false; + _currentTestIndex = -1; + }); + } + } + + Future _runAllTests() async { + for (int i = 0; i < _parsedTests.length; i++) { + await _runTest(i); + } + } + + @override + Widget build(BuildContext context) { + return Container( + height: 500, + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 16), + Expanded( + child: _parsedTests.isEmpty + ? Center(child: Text("No test cases found")) + : _buildTestList(), + ), + const SizedBox(height: 16), + _buildActionButtons(), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'API Test Runner', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.help_outline), + tooltip: 'How to use', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('API Test Runner'), + content: const Text( + 'This tool runs the API tests generated from your request.\n\n' + '• Click "Run All" to execute all tests\n' + '• Click individual "Run" buttons to execute specific tests\n' + '• Click "Copy" to copy a curl command to clipboard', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + }, + ), + ], + ); + } + + Widget _buildTestList() { + return ListView.builder( + itemCount: _parsedTests.length, + itemBuilder: (context, index) { + final test = _parsedTests[index]; + final result = _results[index]; + final bool hasResult = result != null; + final bool isSuccess = hasResult ? result['isSuccess'] ?? false : false; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ExpansionTile( + title: Text( + test['description'] ?? "Test case ${index + 1}", + style: TextStyle( + fontWeight: FontWeight.bold, + color: hasResult + ? (isSuccess + ? Colors.green + : Colors.red) + : null, + ), + ), + subtitle: Text('Test ${index + 1} of ${_parsedTests.length}'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.copy), + tooltip: 'Copy command', + onPressed: () { + Clipboard.setData(ClipboardData(text: test['command'])); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Command copied to clipboard')), + ); + }, + ), + if (_currentTestIndex == index && _isRunning) + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + IconButton( + icon: Icon(hasResult + ? (isSuccess ? Icons.check_circle : Icons.error) + : Icons.play_arrow), + color: hasResult + ? (isSuccess ? Colors.green : Colors.red) + : null, + tooltip: hasResult ? 'Run again' : 'Run test', + onPressed: () => _runTest(index), + ), + ], + ), + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Command:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 4, bottom: 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(4), + ), + width: double.infinity, + child: SelectableText( + test['command'], + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + if (hasResult) ...[ + const Divider(), + Text( + 'Result:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: isSuccess ? Colors.green : Colors.red, + ), + ), + const SizedBox(height: 8), + if (result.containsKey('error')) + Text( + 'Error: ${result['error']}', + style: const TextStyle(color: Colors.red), + ) + else ...[ + Text('Status: ${result['status']}'), + const SizedBox(height: 8), + const Text( + 'Response:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(4), + ), + width: double.infinity, + child: renderContent( + context, + _tryFormatJson(result['body']) + ), + ), + ], + ], + ], + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildActionButtons() { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: _isRunning ? null : _runAllTests, + icon: const Icon(Icons.play_circle_outline), + label: const Text("Run All Tests"), + ), + ], + ); + } + + String _tryFormatJson(dynamic input) { + if (input == null) return "null"; + + if (input is! String) return input.toString(); + + try { + final decoded = json.decode(input); + return JsonEncoder.withIndent(' ').convert(decoded); + } catch (e) { + return input; + } + } +} From 877ea6127c64fca881271cd2e49a7a82a02ca894 Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:43:59 +0530 Subject: [PATCH 3/7] Test cases generator response with a button to run the test cases --- lib/dashbot/widgets/dashbot_widget.dart | 120 ++++++++-------- lib/dashbot/widgets/test_runner_widget.dart | 145 ++++++++------------ 2 files changed, 124 insertions(+), 141 deletions(-) diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index a113bb9d1..846aca675 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -6,9 +6,7 @@ import 'test_runner_widget.dart'; import 'chat_bubble.dart'; class DashBotWidget extends ConsumerStatefulWidget { - const DashBotWidget({ - super.key, - }); + const DashBotWidget({super.key}); @override ConsumerState createState() => _DashBotWidgetState(); @@ -18,8 +16,6 @@ class _DashBotWidgetState extends ConsumerState { final TextEditingController _controller = TextEditingController(); late ScrollController _scrollController; bool _isLoading = false; - bool _showTestRunner = false; - String _testCases = ''; @override void initState() { @@ -37,11 +33,6 @@ class _DashBotWidgetState extends ConsumerState { Future _sendMessage(String message) async { if (message.trim().isEmpty) return; - // Reset test runner state when sending a new message - setState(() { - _showTestRunner = false; - }); - final dashBotService = ref.read(dashBotServiceProvider); final requestModel = ref.read(selectedRequestModelProvider); final responseModel = requestModel?.httpResponseModel; @@ -57,18 +48,15 @@ class _DashBotWidgetState extends ConsumerState { final response = await dashBotService.handleRequest( message, requestModel, responseModel); + // If "Test API" is requested, append a button to the response + final botMessage = message == "Test API" + ? "$response\n\n**[Run Test Cases]**" + : response; + ref.read(chatMessagesProvider.notifier).addMessage({ 'role': 'bot', - 'message': response, + 'message': botMessage, }); - - // If message was "Test API", show the test runner - if (message == "Test API") { - setState(() { - _showTestRunner = true; - _testCases = response; - }); - } } catch (error, stackTrace) { debugPrint('Error in _sendMessage: $error'); debugPrint('StackTrace: $stackTrace'); @@ -88,6 +76,19 @@ class _DashBotWidgetState extends ConsumerState { } } + void _showTestRunner(String testCases) { + showDialog( + context: context, + builder: (context) => Dialog( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: 500, + child: TestRunnerWidget(testCases: testCases), + ), + ), + ); + } + @override Widget build(BuildContext context) { final messages = ref.watch(chatMessagesProvider); @@ -95,38 +96,30 @@ class _DashBotWidgetState extends ConsumerState { final statusCode = requestModel?.httpResponseModel?.statusCode; final showDebugButton = statusCode != null && statusCode >= 400; - return Column( - children: [ - Container( - height: 450, - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)) - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 12), - _buildQuickActions(showDebugButton), - const SizedBox(height: 12), - Expanded(child: _buildChatArea(messages)), - if (_isLoading) _buildLoadingIndicator(), - const SizedBox(height: 10), - _buildInputArea(context), - ], - ), - ), - if (_showTestRunner) ...[ - const SizedBox(height: 20), - TestRunnerWidget(testCases: _testCases), + return Container( + height: 450, + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)) ], - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 12), + _buildQuickActions(showDebugButton), + const SizedBox(height: 12), + Expanded(child: _buildChatArea(messages)), + if (_isLoading) _buildLoadingIndicator(), + const SizedBox(height: 10), + _buildInputArea(context), + ], + ), ); } @@ -141,9 +134,6 @@ class _DashBotWidgetState extends ConsumerState { tooltip: 'Clear Chat', onPressed: () { ref.read(chatMessagesProvider.notifier).clearMessages(); - setState(() { - _showTestRunner = false; - }); }, ), ], @@ -199,8 +189,30 @@ class _DashBotWidgetState extends ConsumerState { itemCount: messages.length, itemBuilder: (context, index) { final message = messages.reversed.toList()[index]; + final isBot = message['role'] == 'bot'; + final text = message['message'] as String; + + // Check if the message contains the "Run Test Cases" button + if (isBot && text.contains("[Run Test Cases]")) { + final testCases = text.replaceAll("\n\n**[Run Test Cases]**", ""); + return Column( + crossAxisAlignment: + isBot ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + ChatBubble(message: testCases, isUser: false), + Padding( + padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), + child: ElevatedButton( + onPressed: () => _showTestRunner(testCases), + child: const Text("Run Test Cases"), + ), + ), + ], + ); + } + return ChatBubble( - message: message['message'], + message: text, isUser: message['role'] == 'user', ); }, diff --git a/lib/dashbot/widgets/test_runner_widget.dart b/lib/dashbot/widgets/test_runner_widget.dart index e2a449a18..0057bba89 100644 --- a/lib/dashbot/widgets/test_runner_widget.dart +++ b/lib/dashbot/widgets/test_runner_widget.dart @@ -27,8 +27,7 @@ class _TestRunnerWidgetState extends ConsumerState { } void _parseTestCases() { - // Basic parsing of cURL commands from the text - final curlRegex = RegExp(r'```(.*?)curl\s+(.*?)```', dotAll: true); + final curlRegex = RegExp(r'```bash\ncurl\s+(.*?)\n```', dotAll: true); final descriptionRegex = RegExp(r'###\s*(.*?)\n', dotAll: true); final curlMatches = curlRegex.allMatches(widget.testCases); @@ -43,7 +42,7 @@ class _TestRunnerWidgetState extends ConsumerState { description = descMatches.elementAt(index).group(1)?.trim(); } - final curlCommand = match.group(2)?.trim() ?? ""; + final curlCommand = match.group(1)?.trim() ?? ""; tests.add({ 'description': description, @@ -71,34 +70,22 @@ class _TestRunnerWidgetState extends ConsumerState { final command = test['command']; try { - // Parse curl command to make HTTP request - // This is a simplified version - a real implementation would need to handle all curl options - final urlMatch = RegExp(r'"([^"]*)"').firstMatch(command); - String url = urlMatch?.group(1) ?? ""; + final urlMatch = RegExp(r'"([^"]*)"').firstMatch(command) ?? + RegExp(r"'([^']*)'").firstMatch(command); + final url = urlMatch?.group(1) ?? ""; + if (url.isEmpty) throw Exception("Could not parse URL from curl command"); - if (url.isEmpty) { - final urlMatch2 = RegExp(r"'([^']*)'").firstMatch(command); - url = urlMatch2?.group(1) ?? ""; - } - - if (url.isEmpty) { - throw Exception("Could not parse URL from curl command"); - } - - // Determine HTTP method (default to GET) String method = "GET"; if (command.contains("-X POST") || command.contains("--request POST")) { method = "POST"; } else if (command.contains("-X PUT") || command.contains("--request PUT")) { method = "PUT"; - } // Add other methods as needed + } - // Make the actual request http.Response response; if (method == "GET") { response = await http.get(Uri.parse(url)); } else if (method == "POST") { - // Extract body if present final bodyMatch = RegExp(r'-d\s+"([^"]*)"').firstMatch(command); final body = bodyMatch?.group(1) ?? ""; response = await http.post(Uri.parse(url), body: body); @@ -131,70 +118,58 @@ class _TestRunnerWidgetState extends ConsumerState { Future _runAllTests() async { for (int i = 0; i < _parsedTests.length; i++) { + if (!mounted) return; await _runTest(i); } } @override Widget build(BuildContext context) { - return Container( - height: 500, - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 16), - Expanded( - child: _parsedTests.isEmpty - ? Center(child: Text("No test cases found")) - : _buildTestList(), + return Scaffold( + appBar: AppBar( + title: const Text('API Test Runner'), + actions: [ + IconButton( + icon: const Icon(Icons.help_outline), + tooltip: 'How to use', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('API Test Runner'), + content: const Text( + 'Run generated API tests:\n\n' + '• "Run All" executes all tests\n' + '• "Run" executes a single test\n' + '• "Copy" copies the curl command', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + }, ), - const SizedBox(height: 16), - _buildActionButtons(), ], ), - ); - } - - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'API Test Runner', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - IconButton( - icon: const Icon(Icons.help_outline), - tooltip: 'How to use', - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('API Test Runner'), - content: const Text( - 'This tool runs the API tests generated from your request.\n\n' - '• Click "Run All" to execute all tests\n' - '• Click individual "Run" buttons to execute specific tests\n' - '• Click "Copy" to copy a curl command to clipboard', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Close'), - ), - ], - ), - ); - }, + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _parsedTests.isEmpty + ? const Center(child: Text("No test cases found")) + : _buildTestList(), + ), + const SizedBox(height: 16), + _buildActionButtons(), + ], ), - ], + ), ); } @@ -205,19 +180,17 @@ class _TestRunnerWidgetState extends ConsumerState { final test = _parsedTests[index]; final result = _results[index]; final bool hasResult = result != null; - final bool isSuccess = hasResult ? result['isSuccess'] ?? false : false; + final bool isSuccess = hasResult && (result['isSuccess'] ?? false); return Card( - margin: const EdgeInsets.only(bottom: 12), + margin: const EdgeInsets.symmetric(vertical: 6), child: ExpansionTile( title: Text( test['description'] ?? "Test case ${index + 1}", style: TextStyle( fontWeight: FontWeight.bold, color: hasResult - ? (isSuccess - ? Colors.green - : Colors.red) + ? (isSuccess ? Colors.green : Colors.red) : null, ), ), @@ -231,7 +204,7 @@ class _TestRunnerWidgetState extends ConsumerState { onPressed: () { Clipboard.setData(ClipboardData(text: test['command'])); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Command copied to clipboard')), + const SnackBar(content: Text('Command copied')), ); }, ), @@ -303,14 +276,14 @@ class _TestRunnerWidgetState extends ConsumerState { padding: const EdgeInsets.all(8), margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, + color: Theme.of(context) + .colorScheme + .surfaceContainerLow, borderRadius: BorderRadius.circular(4), ), width: double.infinity, child: renderContent( - context, - _tryFormatJson(result['body']) - ), + context, _tryFormatJson(result['body'])), ), ], ], @@ -339,13 +312,11 @@ class _TestRunnerWidgetState extends ConsumerState { String _tryFormatJson(dynamic input) { if (input == null) return "null"; - if (input is! String) return input.toString(); - try { final decoded = json.decode(input); return JsonEncoder.withIndent(' ').convert(decoded); - } catch (e) { + } catch (_) { return input; } } From 7582c7888089218c5c7052bd3a63d8926b38a60b Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:57:21 +0530 Subject: [PATCH 4/7] Test cases generator modification to only Run test cases feature without response --- lib/dashbot/features/test_generator.dart | 9 +++-- lib/dashbot/widgets/dashbot_widget.dart | 42 +++++++++++++++--------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/lib/dashbot/features/test_generator.dart b/lib/dashbot/features/test_generator.dart index c638964bd..301eb4750 100644 --- a/lib/dashbot/features/test_generator.dart +++ b/lib/dashbot/features/test_generator.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import '../services/dashbot_service.dart'; +import 'package:apidash/dashbot/services/dashbot_service.dart'; import 'package:apidash/models/request_model.dart'; class TestGeneratorFeature { @@ -67,7 +67,12 @@ For each test case: Focus on creating realistic test values based on the API context (e.g., for a country flag API, use real country codes, invalid codes, etc.) """; - return _service.generateResponse(prompt); + // Generate the test cases + final testCases = await _service.generateResponse(prompt); + + // Return only a button trigger message with the test cases hidden + // This will be detected in DashBotWidget to show only a button instead of the full text + return "TEST_CASES_HIDDEN\n$testCases"; } String _analyzeParameters(Map parameters) { diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index 846aca675..65e706ea4 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -48,15 +48,25 @@ class _DashBotWidgetState extends ConsumerState { final response = await dashBotService.handleRequest( message, requestModel, responseModel); - // If "Test API" is requested, append a button to the response - final botMessage = message == "Test API" - ? "$response\n\n**[Run Test Cases]**" - : response; + // Check if this is a test case response with hidden content + if (response.startsWith("TEST_CASES_HIDDEN\n")) { + // Extract the test cases but don't show them in the message + final testCases = response.replaceFirst("TEST_CASES_HIDDEN\n", ""); - ref.read(chatMessagesProvider.notifier).addMessage({ - 'role': 'bot', - 'message': botMessage, - }); + // Add a message with a marker that will trigger the button display + ref.read(chatMessagesProvider.notifier).addMessage({ + 'role': 'bot', + 'message': "Test cases generated successfully. Click the button below to run them.", + 'testCases': testCases, + 'showTestButton': true, + }); + } else { + // Normal message handling + ref.read(chatMessagesProvider.notifier).addMessage({ + 'role': 'bot', + 'message': response, + }); + } } catch (error, stackTrace) { debugPrint('Error in _sendMessage: $error'); debugPrint('StackTrace: $stackTrace'); @@ -191,20 +201,20 @@ class _DashBotWidgetState extends ConsumerState { final message = messages.reversed.toList()[index]; final isBot = message['role'] == 'bot'; final text = message['message'] as String; + final showTestButton = message['showTestButton'] == true; + final testCases = message['testCases'] as String?; - // Check if the message contains the "Run Test Cases" button - if (isBot && text.contains("[Run Test Cases]")) { - final testCases = text.replaceAll("\n\n**[Run Test Cases]**", ""); + if (isBot && showTestButton && testCases != null) { return Column( - crossAxisAlignment: - isBot ? CrossAxisAlignment.start : CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ChatBubble(message: testCases, isUser: false), + ChatBubble(message: text, isUser: false), Padding( padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), - child: ElevatedButton( + child: ElevatedButton.icon( onPressed: () => _showTestRunner(testCases), - child: const Text("Run Test Cases"), + icon: const Icon(Icons.play_arrow), + label: const Text("Run Test Cases"), ), ), ], From f44091a50c8464d8794d984acf9e7e0d6ede5274 Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Sat, 22 Mar 2025 15:18:25 +0530 Subject: [PATCH 5/7] Move DashBot to bottom-right, add close/minimize/maximize buttons and toggle FAB visibility --- lib/dashbot/dashbot.dart | 67 ++++++- lib/dashbot/widgets/dashbot_widget.dart | 237 ++++++++++++++++-------- lib/screens/dashboard.dart | 208 +++++++++++---------- 3 files changed, 328 insertions(+), 184 deletions(-) diff --git a/lib/dashbot/dashbot.dart b/lib/dashbot/dashbot.dart index 9454aa85d..e2371acdd 100644 --- a/lib/dashbot/dashbot.dart +++ b/lib/dashbot/dashbot.dart @@ -1 +1,66 @@ -export 'widgets/dashbot_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/dashbot/widgets/dashbot_widget.dart'; + +// Provider to manage DashBot visibility state +final dashBotVisibilityProvider = StateProvider((ref) => false); +final dashBotMinimizedProvider = StateProvider((ref) => false); + +// Function to show DashBot in a bottom sheet (old style) +void showDashBotBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => const Padding( + padding: EdgeInsets.all(16.0), + child: DashBotWidget(), + ), + ); +} + +// Function to toggle DashBot overlay (new style) +void toggleDashBotOverlay(WidgetRef ref) { + ref.read(dashBotVisibilityProvider.notifier).state = true; + ref.read(dashBotMinimizedProvider.notifier).state = false; +} + +// DashBot Overlay Widget +class DashBotOverlay extends ConsumerWidget { + const DashBotOverlay({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isMinimized = ref.watch(dashBotMinimizedProvider); + + return Material( + elevation: 8, + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: 400, // Fixed width for the DashBot + height: isMinimized ? 120 : 450, + child: const DashBotWidget(), + ), + ); + } +} + +// FloatingActionButton for DashBot +class DashBotFAB extends ConsumerWidget { + final bool useOverlay; + + const DashBotFAB({this.useOverlay = true, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FloatingActionButton( + onPressed: () { + if (useOverlay) { + toggleDashBotOverlay(ref); + } else { + showDashBotBottomSheet(context); + } + }, + child: const Icon(Icons.help_outline), + ); + } +} diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index 65e706ea4..2426fca84 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/dashbot/providers/dashbot_providers.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/dashbot/dashbot.dart'; import 'test_runner_widget.dart'; import 'chat_bubble.dart'; @@ -78,7 +79,7 @@ class _DashBotWidgetState extends ConsumerState { setState(() => _isLoading = false); WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.animateTo( - 0, + _scrollController.position.minScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); @@ -105,19 +106,18 @@ class _DashBotWidgetState extends ConsumerState { final requestModel = ref.read(selectedRequestModelProvider); final statusCode = requestModel?.httpResponseModel?.statusCode; final showDebugButton = statusCode != null && statusCode >= 400; + final isMinimized = ref.watch(dashBotMinimizedProvider); return Container( - height: 450, + height: double.infinity, width: double.infinity, - padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4)) - ], ), - child: Column( + child: isMinimized + ? _buildMinimizedView(context) + : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(context), @@ -134,115 +134,181 @@ class _DashBotWidgetState extends ConsumerState { } Widget _buildHeader(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + final isMinimized = ref.watch(dashBotMinimizedProvider); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'DashBot', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Row( + children: [ + // Minimize/Maximize button with proper alignment + IconButton( + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + icon: Icon( + isMinimized ? Icons.fullscreen : Icons.remove, + size: 20, + ), + tooltip: isMinimized ? 'Maximize' : 'Minimize', + onPressed: () { + ref.read(dashBotMinimizedProvider.notifier).state = !isMinimized; + }, + ), + // Close button + IconButton( + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.close, size: 20), + tooltip: 'Close', + onPressed: () { + ref.read(dashBotVisibilityProvider.notifier).state = false; + }, + ), + // Clear chat button + IconButton( + padding: const EdgeInsets.all(8), + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.delete_sweep, size: 20), + tooltip: 'Clear Chat', + onPressed: () { + ref.read(chatMessagesProvider.notifier).clearMessages(); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildMinimizedView(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('DashBot', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - IconButton( - icon: const Icon(Icons.delete_sweep), - tooltip: 'Clear Chat', - onPressed: () { - ref.read(chatMessagesProvider.notifier).clearMessages(); - }, + _buildHeader(context), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildInputArea(context), ), ], ); } Widget _buildQuickActions(bool showDebugButton) { - return Wrap( - spacing: 8, - runSpacing: 8, - children: [ - ElevatedButton.icon( - onPressed: () => _sendMessage("Explain API"), - icon: const Icon(Icons.info_outline), - label: const Text("Explain"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - ), - if (showDebugButton) + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ ElevatedButton.icon( - onPressed: () => _sendMessage("Debug API"), - icon: const Icon(Icons.bug_report_outlined), - label: const Text("Debug"), + onPressed: () => _sendMessage("Explain API"), + icon: const Icon(Icons.info_outline, size: 16), + label: const Text("Explain"), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, ), ), - ElevatedButton.icon( - onPressed: () => _sendMessage("Document API"), - icon: const Icon(Icons.description_outlined), - label: const Text("Document"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + if (showDebugButton) + ElevatedButton.icon( + onPressed: () => _sendMessage("Debug API"), + icon: const Icon(Icons.bug_report_outlined, size: 16), + label: const Text("Debug"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, + ), + ), + ElevatedButton.icon( + onPressed: () => _sendMessage("Document API"), + icon: const Icon(Icons.description_outlined, size: 16), + label: const Text("Document"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, + ), ), - ), - ElevatedButton.icon( - onPressed: () => _sendMessage("Test API"), - icon: const Icon(Icons.science_outlined), - label: const Text("Test"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ElevatedButton.icon( + onPressed: () => _sendMessage("Test API"), + icon: const Icon(Icons.science_outlined, size: 16), + label: const Text("Test"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + visualDensity: VisualDensity.compact, + ), ), - ), - ], + ], + ), ); } Widget _buildChatArea(List> messages) { - return ListView.builder( - controller: _scrollController, - reverse: true, - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages.reversed.toList()[index]; - final isBot = message['role'] == 'bot'; - final text = message['message'] as String; - final showTestButton = message['showTestButton'] == true; - final testCases = message['testCases'] as String?; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.builder( + controller: _scrollController, + reverse: true, + itemCount: messages.length, + itemBuilder: (context, index) { + final message = messages.reversed.toList()[index]; + final isBot = message['role'] == 'bot'; + final text = message['message'] as String; + final showTestButton = message['showTestButton'] == true; + final testCases = message['testCases'] as String?; - if (isBot && showTestButton && testCases != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChatBubble(message: text, isUser: false), - Padding( - padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), - child: ElevatedButton.icon( - onPressed: () => _showTestRunner(testCases), - icon: const Icon(Icons.play_arrow), - label: const Text("Run Test Cases"), + if (isBot && showTestButton && testCases != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChatBubble(message: text, isUser: false), + Padding( + padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4), + child: ElevatedButton.icon( + onPressed: () => _showTestRunner(testCases), + icon: const Icon(Icons.play_arrow, size: 16), + label: const Text("Run Test Cases"), + style: ElevatedButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + ), ), - ), - ], - ); - } + ], + ); + } - return ChatBubble( - message: text, - isUser: message['role'] == 'user', - ); - }, + return ChatBubble( + message: text, + isUser: message['role'] == 'user', + ); + }, + ), ); } Widget _buildLoadingIndicator() { return const Padding( - padding: EdgeInsets.all(8.0), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: LinearProgressIndicator(), ); } Widget _buildInputArea(BuildContext context) { + final isMinimized = ref.watch(dashBotMinimizedProvider); + return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.surfaceContainer, ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ Expanded( @@ -251,19 +317,28 @@ class _DashBotWidgetState extends ConsumerState { decoration: const InputDecoration( hintText: 'Ask DashBot...', border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 8), ), onSubmitted: (value) { _sendMessage(value); _controller.clear(); + if (isMinimized) { + ref.read(dashBotMinimizedProvider.notifier).state = false; + } }, maxLines: 1, ), ), IconButton( - icon: const Icon(Icons.send), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.send, size: 20), onPressed: () { _sendMessage(_controller.text); _controller.clear(); + if (isMinimized) { + ref.read(dashBotMinimizedProvider.notifier).state = false; + } }, ), ], diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index cc9a62670..148ff5c9d 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -17,126 +17,130 @@ class Dashboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); + final isDashBotVisible = ref.watch(dashBotVisibilityProvider); + return Scaffold( body: SafeArea( - child: Row( - children: [ - Column( - children: [ - SizedBox( - height: kIsMacOS ? 32.0 : 16.0, - width: 64, - ), + child: Stack( + children: [ + Row( + children: [ Column( - mainAxisSize: MainAxisSize.min, children: [ - IconButton( - isSelected: railIdx == 0, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 0; - }, - icon: const Icon(Icons.auto_awesome_mosaic_outlined), - selectedIcon: const Icon(Icons.auto_awesome_mosaic), - ), - Text( - 'Requests', - style: Theme.of(context).textTheme.labelSmall, - ), - kVSpacer10, - IconButton( - isSelected: railIdx == 1, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 1; - }, - icon: const Icon(Icons.laptop_windows_outlined), - selectedIcon: const Icon(Icons.laptop_windows), + SizedBox( + height: kIsMacOS ? 32.0 : 16.0, + width: 64, ), - Text( - 'Variables', - style: Theme.of(context).textTheme.labelSmall, - ), - kVSpacer10, - IconButton( - isSelected: railIdx == 2, - onPressed: () { - ref.read(navRailIndexStateProvider.notifier).state = 2; - }, - icon: const Icon(Icons.history_outlined), - selectedIcon: const Icon(Icons.history_rounded), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + isSelected: railIdx == 0, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 0; + }, + icon: const Icon(Icons.auto_awesome_mosaic_outlined), + selectedIcon: const Icon(Icons.auto_awesome_mosaic), + ), + Text( + 'Requests', + style: Theme.of(context).textTheme.labelSmall, + ), + kVSpacer10, + IconButton( + isSelected: railIdx == 1, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 1; + }, + icon: const Icon(Icons.laptop_windows_outlined), + selectedIcon: const Icon(Icons.laptop_windows), + ), + Text( + 'Variables', + style: Theme.of(context).textTheme.labelSmall, + ), + kVSpacer10, + IconButton( + isSelected: railIdx == 2, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 2; + }, + icon: const Icon(Icons.history_outlined), + selectedIcon: const Icon(Icons.history_rounded), + ), + Text( + 'History', + style: Theme.of(context).textTheme.labelSmall, + ), + ], ), - Text( - 'History', - style: Theme.of(context).textTheme.labelSmall, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: NavbarButton( + railIdx: railIdx, + selectedIcon: Icons.help, + icon: Icons.help_outline, + label: 'About', + showLabel: false, + isCompact: true, + onTap: () { + showAboutAppDialog(context); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: NavbarButton( + railIdx: railIdx, + buttonIdx: 3, + selectedIcon: Icons.settings, + icon: Icons.settings_outlined, + label: 'Settings', + showLabel: false, + isCompact: true, + ), + ), + ], + ), ), ], ), + VerticalDivider( + thickness: 1, + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: NavbarButton( - railIdx: railIdx, - selectedIcon: Icons.help, - icon: Icons.help_outline, - label: 'About', - showLabel: false, - isCompact: true, - onTap: () { - showAboutAppDialog(context); - }, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: NavbarButton( - railIdx: railIdx, - buttonIdx: 3, - selectedIcon: Icons.settings, - icon: Icons.settings_outlined, - label: 'Settings', - showLabel: false, - isCompact: true, - ), - ), + child: IndexedStack( + alignment: AlignmentDirectional.topCenter, + index: railIdx, + children: const [ + HomePage(), + EnvironmentPage(), + HistoryPage(), + SettingsPage(), ], ), - ), + ) ], ), - VerticalDivider( - thickness: 1, - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - Expanded( - child: IndexedStack( - alignment: AlignmentDirectional.topCenter, - index: railIdx, - children: const [ - HomePage(), - EnvironmentPage(), - HistoryPage(), - SettingsPage(), - ], + + // DashBot Overlay + if (isDashBotVisible) + Positioned( + bottom: 20, + right: 20, + child: const DashBotOverlay(), ), - ) ], ), ), - // TODO: Release DashBot - floatingActionButton: FloatingActionButton( - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => const Padding( - padding: EdgeInsets.all(16.0), - child: DashBotWidget(), - ), - ), - child: const Icon(Icons.help_outline), - ), + // Conditionally show FAB only when DashBot is not visible + floatingActionButton: !isDashBotVisible ? const DashBotFAB() : null, ); } } From 1c0af6ebdab96e3f464fb771881e9a6bb4a4874f Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Sun, 23 Mar 2025 22:34:15 +0530 Subject: [PATCH 6/7] Update Dashboard widget to fix DashBot overlay positioning with Stack --- lib/screens/dashboard.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 148ff5c9d..5931c7feb 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -18,7 +18,6 @@ class Dashboard extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); final isDashBotVisible = ref.watch(dashBotVisibilityProvider); - return Scaffold( body: SafeArea( child: Stack( @@ -139,7 +138,7 @@ class Dashboard extends ConsumerWidget { ], ), ), - // Conditionally show FAB only when DashBot is not visible + // TODO: Release DashBot floatingActionButton: !isDashBotVisible ? const DashBotFAB() : null, ); } From 7c5a841fb71ae463edf6ccdc410dddb89323ea4a Mon Sep 17 00:00:00 2001 From: siddu015 <116783967+siddu015@users.noreply.github.com> Date: Sun, 23 Mar 2025 23:09:27 +0530 Subject: [PATCH 7/7] Update Dashbot --- lib/dashbot/dashbot.dart | 7 +------ lib/dashbot/features/documentation.dart | 2 +- lib/dashbot/features/general_query.dart | 1 - lib/dashbot/features/test_generator.dart | 10 ---------- lib/dashbot/services/dashbot_service.dart | 8 ++++---- lib/dashbot/widgets/dashbot_widget.dart | 16 +++------------- lib/dashbot/widgets/test_runner_widget.dart | 4 ++-- 7 files changed, 11 insertions(+), 37 deletions(-) diff --git a/lib/dashbot/dashbot.dart b/lib/dashbot/dashbot.dart index e2371acdd..07bc6b7fa 100644 --- a/lib/dashbot/dashbot.dart +++ b/lib/dashbot/dashbot.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/dashbot/widgets/dashbot_widget.dart'; -// Provider to manage DashBot visibility state final dashBotVisibilityProvider = StateProvider((ref) => false); final dashBotMinimizedProvider = StateProvider((ref) => false); -// Function to show DashBot in a bottom sheet (old style) void showDashBotBottomSheet(BuildContext context) { showModalBottomSheet( context: context, @@ -18,13 +16,11 @@ void showDashBotBottomSheet(BuildContext context) { ); } -// Function to toggle DashBot overlay (new style) void toggleDashBotOverlay(WidgetRef ref) { ref.read(dashBotVisibilityProvider.notifier).state = true; ref.read(dashBotMinimizedProvider.notifier).state = false; } -// DashBot Overlay Widget class DashBotOverlay extends ConsumerWidget { const DashBotOverlay({super.key}); @@ -36,7 +32,7 @@ class DashBotOverlay extends ConsumerWidget { elevation: 8, borderRadius: BorderRadius.circular(12), child: SizedBox( - width: 400, // Fixed width for the DashBot + width: 400, height: isMinimized ? 120 : 450, child: const DashBotWidget(), ), @@ -44,7 +40,6 @@ class DashBotOverlay extends ConsumerWidget { } } -// FloatingActionButton for DashBot class DashBotFAB extends ConsumerWidget { final bool useOverlay; diff --git a/lib/dashbot/features/documentation.dart b/lib/dashbot/features/documentation.dart index da8f19494..9f6464de8 100644 --- a/lib/dashbot/features/documentation.dart +++ b/lib/dashbot/features/documentation.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import '../services/dashbot_service.dart'; +import 'package:apidash/dashbot/services/dashbot_service.dart'; import 'package:apidash/models/request_model.dart'; class DocumentationFeature { diff --git a/lib/dashbot/features/general_query.dart b/lib/dashbot/features/general_query.dart index 45f4d3b50..f5ce53eee 100644 --- a/lib/dashbot/features/general_query.dart +++ b/lib/dashbot/features/general_query.dart @@ -7,7 +7,6 @@ class GeneralQueryFeature { GeneralQueryFeature(this._client); Future generateResponse(String prompt, {RequestModel? requestModel, dynamic responseModel}) async { - // Create a more focused prompt that incorporates request/response context if available String enhancedPrompt = prompt; if (requestModel != null && responseModel != null) { diff --git a/lib/dashbot/features/test_generator.dart b/lib/dashbot/features/test_generator.dart index 301eb4750..854426328 100644 --- a/lib/dashbot/features/test_generator.dart +++ b/lib/dashbot/features/test_generator.dart @@ -22,20 +22,15 @@ class TestGeneratorFeature { .toUpperCase() ?? "GET"; final endpoint = requestModel.httpRequestModel?.url ?? "Unknown Endpoint"; - final headers = requestModel.httpRequestModel?.enabledHeadersMap ?? {}; - final parameters = requestModel.httpRequestModel?.enabledParamsMap ?? {}; - final body = requestModel.httpRequestModel?.body; final rawResponse = responseModel.body; final responseBody = rawResponse is String ? rawResponse : jsonEncode(rawResponse); final statusCode = responseModel.statusCode ?? 0; - // Extract base URL and endpoint path Uri uri = Uri.parse(endpoint); final baseUrl = "${uri.scheme}://${uri.host}"; final path = uri.path; - // Analyze parameter types and values final parameterAnalysis = _analyzeParameters(uri.queryParameters); final prompt = """ @@ -67,11 +62,7 @@ For each test case: Focus on creating realistic test values based on the API context (e.g., for a country flag API, use real country codes, invalid codes, etc.) """; - // Generate the test cases final testCases = await _service.generateResponse(prompt); - - // Return only a button trigger message with the test cases hidden - // This will be detected in DashBotWidget to show only a button instead of the full text return "TEST_CASES_HIDDEN\n$testCases"; } @@ -83,7 +74,6 @@ Focus on creating realistic test values based on the API context (e.g., for a co Map analysis = {}; parameters.forEach((key, value) { - // Try to determine parameter type and format if (RegExp(r'^[A-Z]{3}$').hasMatch(value)) { analysis[key] = "Appears to be a 3-letter country code (ISO 3166-1 alpha-3)"; } else if (RegExp(r'^[A-Z]{2}$').hasMatch(value)) { diff --git a/lib/dashbot/services/dashbot_service.dart b/lib/dashbot/services/dashbot_service.dart index 64b3ca860..4d39f8264 100644 --- a/lib/dashbot/services/dashbot_service.dart +++ b/lib/dashbot/services/dashbot_service.dart @@ -2,7 +2,7 @@ import 'package:apidash/dashbot/features/debug.dart'; import 'package:apidash/dashbot/features/documentation.dart'; import 'package:ollama_dart/ollama_dart.dart'; import 'package:apidash/dashbot/features/explain.dart'; -import 'package:apidash/dashbot/features/test_generator.dart'; // New import +import 'package:apidash/dashbot/features/test_generator.dart'; import 'package:apidash/models/request_model.dart'; import 'package:apidash/dashbot/features/general_query.dart'; @@ -11,7 +11,7 @@ class DashBotService { late final ExplainFeature _explainFeature; late final DebugFeature _debugFeature; late final DocumentationFeature _documentationFeature; - late final TestGeneratorFeature _testGeneratorFeature; // New feature + late final TestGeneratorFeature _testGeneratorFeature; final GeneralQueryFeature _generalQueryFeature; DashBotService() @@ -20,7 +20,7 @@ class DashBotService { _explainFeature = ExplainFeature(this); _debugFeature = DebugFeature(this); _documentationFeature = DocumentationFeature(this); - _testGeneratorFeature = TestGeneratorFeature(this); // Initialize new feature + _testGeneratorFeature = TestGeneratorFeature(this); } Future generateResponse(String prompt) async { @@ -38,7 +38,7 @@ class DashBotService { } else if (input == "Document API") { return _documentationFeature.generateApiDocumentation( requestModel: requestModel, responseModel: responseModel); - } else if (input == "Test API") { // New condition + } else if (input == "Test API") { return _testGeneratorFeature.generateApiTests( requestModel: requestModel, responseModel: responseModel); } diff --git a/lib/dashbot/widgets/dashbot_widget.dart b/lib/dashbot/widgets/dashbot_widget.dart index 2426fca84..98defd77c 100644 --- a/lib/dashbot/widgets/dashbot_widget.dart +++ b/lib/dashbot/widgets/dashbot_widget.dart @@ -7,7 +7,9 @@ import 'test_runner_widget.dart'; import 'chat_bubble.dart'; class DashBotWidget extends ConsumerStatefulWidget { - const DashBotWidget({super.key}); + const DashBotWidget({ + super.key, + }); @override ConsumerState createState() => _DashBotWidgetState(); @@ -33,7 +35,6 @@ class _DashBotWidgetState extends ConsumerState { Future _sendMessage(String message) async { if (message.trim().isEmpty) return; - final dashBotService = ref.read(dashBotServiceProvider); final requestModel = ref.read(selectedRequestModelProvider); final responseModel = requestModel?.httpResponseModel; @@ -48,13 +49,8 @@ class _DashBotWidgetState extends ConsumerState { try { final response = await dashBotService.handleRequest( message, requestModel, responseModel); - - // Check if this is a test case response with hidden content if (response.startsWith("TEST_CASES_HIDDEN\n")) { - // Extract the test cases but don't show them in the message final testCases = response.replaceFirst("TEST_CASES_HIDDEN\n", ""); - - // Add a message with a marker that will trigger the button display ref.read(chatMessagesProvider.notifier).addMessage({ 'role': 'bot', 'message': "Test cases generated successfully. Click the button below to run them.", @@ -62,7 +58,6 @@ class _DashBotWidgetState extends ConsumerState { 'showTestButton': true, }); } else { - // Normal message handling ref.read(chatMessagesProvider.notifier).addMessage({ 'role': 'bot', 'message': response, @@ -135,7 +130,6 @@ class _DashBotWidgetState extends ConsumerState { Widget _buildHeader(BuildContext context) { final isMinimized = ref.watch(dashBotMinimizedProvider); - return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Row( @@ -147,7 +141,6 @@ class _DashBotWidgetState extends ConsumerState { ), Row( children: [ - // Minimize/Maximize button with proper alignment IconButton( padding: const EdgeInsets.all(8), visualDensity: VisualDensity.compact, @@ -160,7 +153,6 @@ class _DashBotWidgetState extends ConsumerState { ref.read(dashBotMinimizedProvider.notifier).state = !isMinimized; }, ), - // Close button IconButton( padding: const EdgeInsets.all(8), visualDensity: VisualDensity.compact, @@ -170,7 +162,6 @@ class _DashBotWidgetState extends ConsumerState { ref.read(dashBotVisibilityProvider.notifier).state = false; }, ), - // Clear chat button IconButton( padding: const EdgeInsets.all(8), visualDensity: VisualDensity.compact, @@ -302,7 +293,6 @@ class _DashBotWidgetState extends ConsumerState { Widget _buildInputArea(BuildContext context) { final isMinimized = ref.watch(dashBotMinimizedProvider); - return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), diff --git a/lib/dashbot/widgets/test_runner_widget.dart b/lib/dashbot/widgets/test_runner_widget.dart index 0057bba89..e93f384de 100644 --- a/lib/dashbot/widgets/test_runner_widget.dart +++ b/lib/dashbot/widgets/test_runner_widget.dart @@ -1,8 +1,8 @@ +import 'dart:convert'; +import 'package:apidash_core/apidash_core.dart' as http; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:convert'; -import 'package:http/http.dart' as http; import 'content_renderer.dart'; class TestRunnerWidget extends ConsumerStatefulWidget {