refactor: remove unused old dashbot features and related files

This commit is contained in:
Udhay-Adithya
2025-08-31 12:16:43 +05:30
parent b9cc42b019
commit 15e3856242
15 changed files with 0 additions and 1306 deletions

View File

@@ -1,2 +0,0 @@
const kModel = 'llama3.2:3b';
const kOllamaEndpoint = 'http://127.0.0.1:11434/api';

View File

@@ -1 +0,0 @@
export 'widgets/dashbot_widget.dart';

View File

@@ -1,63 +0,0 @@
import 'dart:convert';
import '../services/services.dart';
import '../../models/models.dart';
class DebugFeature {
final DashBotService _service;
DebugFeature(this._service);
Future<String> debugApi({
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 = """
URGENT API DEBUG ANALYSIS
**Request Overview:**
- Endpoint: $endpoint
- Method: $method
- Status Code: $statusCode
**Debugging Instructions:**
Provide a PRECISE, TEXT-ONLY explanation that:
1. Identifies the EXACT problem
2. Explains WHY the request failed
3. Describes SPECIFIC steps to resolve the issue
4. NO CODE SNIPPETS ALLOWED
**Request Details:**
- Headers: ${headers.isNotEmpty ? jsonEncode(headers) : "No headers"}
- Parameters: ${parameters.isNotEmpty ? jsonEncode(parameters) : "No parameters"}
- Request Body: ${body ?? "Empty body"}
**Response Context:**
```
$responseBody
```
Provide a CLEAR, ACTIONABLE solution in the SIMPLEST possible language.
""";
return _service.generateResponse(prompt);
}
}

View File

@@ -1,66 +0,0 @@
import 'dart:convert';
import '../services/services.dart';
import '../../models/models.dart';
class DocumentationFeature {
final DashBotService _service;
DocumentationFeature(this._service);
Future<String> 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);
}
}

View File

@@ -1,67 +0,0 @@
import '../services/services.dart';
import '../../models/models.dart';
class ExplainFeature {
final DashBotService _service;
ExplainFeature(this._service);
Future<String> explainLatestApi({
required RequestModel? requestModel,
required dynamic responseModel,
}) async {
if (requestModel == null || responseModel == null) {
return "No recent API requests found.";
}
if (requestModel.httpRequestModel?.url == null) {
return "Error: Invalid API request (missing endpoint).";
}
final method =
requestModel.httpRequestModel?.method.name.toUpperCase() ?? "GET";
final url = requestModel.httpRequestModel!.url;
final headers = requestModel.httpRequestModel?.enabledHeadersMap ?? {};
final parameters = requestModel.httpRequestModel?.enabledParamsMap ?? {};
final body = requestModel.httpRequestModel?.body ?? '';
final responseBody = responseModel.body;
final statusCode = responseModel.statusCode;
final prompt = '''
FOCUSED API INTERACTION BREAKDOWN
**Essential Request Details:**
- Endpoint Purpose: What is this API endpoint designed to do?
- Interaction Type: Describe the core purpose of this specific request
**Request Details:**
- Endpoint: $url
- HTTP Method: $method
- Request Headers: ${headers.isEmpty ? "None" : headers}
- URL Parameters: ${parameters.isEmpty ? "None" : parameters}
- Request Body: ${body.isEmpty ? "None" : body}
**Response Details**
- Status Code: $statusCode
- Content: $responseBody
**Response CORE Insights:**
- Status: Success or Failure?
- Key Data Extracted: What CRITICAL information does the response contain?
**Precise Analysis Requirements:**
1. Explain the API's PRIMARY function in ONE clear sentence
2. Identify the MOST IMPORTANT piece of information returned
3. Describe the PRACTICAL significance of this API call
AVOID:
- Technical jargon
- Unnecessary details
- Verbose explanations
Deliver a CRYSTAL CLEAR, CONCISE explanation that anyone can understand.
''';
return _service.generateResponse(prompt);
}
}

View File

@@ -1,5 +0,0 @@
export 'debug.dart';
export 'documentation.dart';
export 'explain.dart';
export 'general_query.dart';
export 'test_generator.dart';

View File

@@ -1,54 +0,0 @@
import 'package:ollama_dart/ollama_dart.dart';
import '../../models/models.dart';
import '../consts.dart';
class GeneralQueryFeature {
final OllamaClient _client;
GeneralQueryFeature(this._client);
Future<String> generateResponse(String prompt,
{RequestModel? requestModel, dynamic responseModel}) async {
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: kModel,
prompt: enhancedPrompt,
),
);
return response.response.toString();
}
}

View File

@@ -1,94 +0,0 @@
import 'dart:convert';
import '../services/services.dart';
import '../../models/models.dart';
class TestGeneratorFeature {
final DashBotService _service;
TestGeneratorFeature(this._service);
Future<String> 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 rawResponse = responseModel.body;
final responseBody =
rawResponse is String ? rawResponse : jsonEncode(rawResponse);
final statusCode = responseModel.statusCode ?? 0;
Uri uri = Uri.parse(endpoint);
final baseUrl = "${uri.scheme}://${uri.host}";
final path = uri.path;
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.)
""";
final testCases = await _service.generateResponse(prompt);
return "TEST_CASES_HIDDEN\n$testCases";
}
String _analyzeParameters(Map<String, String> parameters) {
if (parameters.isEmpty) {
return "No parameters detected";
}
Map<String, String> analysis = {};
parameters.forEach((key, value) {
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);
}
}

View File

@@ -1,45 +0,0 @@
import 'dart:convert';
import 'package:apidash/services/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/services.dart';
final dashBotMinimizedProvider = StateProvider<bool>((ref) {
return true;
});
final chatMessagesProvider =
StateNotifierProvider<ChatMessagesNotifier, List<Map<String, dynamic>>>(
(ref) => ChatMessagesNotifier(),
);
final dashBotServiceProvider = Provider<DashBotService>((ref) {
return DashBotService();
});
class ChatMessagesNotifier extends StateNotifier<List<Map<String, dynamic>>> {
ChatMessagesNotifier() : super([]) {
_loadMessages();
}
Future<void> _loadMessages() async {
final messages = await hiveHandler.getDashbotMessages();
if (messages != null) {
state = List<Map<String, dynamic>>.from(json.decode(messages));
}
}
Future<void> _saveMessages() async {
final messages = json.encode(state);
await hiveHandler.saveDashbotMessages(messages);
}
void addMessage(Map<String, dynamic> message) {
state = [...state, message];
_saveMessages();
}
void clearMessages() {
state = [];
_saveMessages();
}
}

View File

@@ -1,61 +0,0 @@
import 'package:ollama_dart/ollama_dart.dart';
import 'package:apidash/models/request_model.dart';
import '../consts.dart';
import '../features/features.dart';
class DashBotService {
final OllamaClient _client;
late final ExplainFeature _explainFeature;
late final DebugFeature _debugFeature;
late final DocumentationFeature _documentationFeature;
late final TestGeneratorFeature _testGeneratorFeature;
final GeneralQueryFeature _generalQueryFeature;
DashBotService()
: _client = OllamaClient(baseUrl: kOllamaEndpoint),
_generalQueryFeature =
GeneralQueryFeature(OllamaClient(baseUrl: kOllamaEndpoint)) {
_explainFeature = ExplainFeature(this);
_debugFeature = DebugFeature(this);
_documentationFeature = DocumentationFeature(this);
_testGeneratorFeature = TestGeneratorFeature(this);
}
Future<String> generateResponse(String prompt) async {
return _generalQueryFeature.generateResponse(prompt);
}
Future<String> handleRequest(
String input,
RequestModel? requestModel,
dynamic responseModel,
) async {
if (input == "Explain API") {
return _explainFeature.explainLatestApi(
requestModel: requestModel,
responseModel: responseModel,
);
} else if (input == "Debug API") {
return _debugFeature.debugApi(
requestModel: requestModel,
responseModel: responseModel,
);
} else if (input == "Document API") {
return _documentationFeature.generateApiDocumentation(
requestModel: requestModel,
responseModel: responseModel,
);
} else if (input == "Test API") {
return _testGeneratorFeature.generateApiTests(
requestModel: requestModel,
responseModel: responseModel,
);
}
return _generalQueryFeature.generateResponse(
input,
requestModel: requestModel,
responseModel: responseModel,
);
}
}

View File

@@ -1 +0,0 @@
export 'dashbot_service.dart';

View File

@@ -1,54 +0,0 @@
// lib/dashbot/widgets/chat_bubble.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'content_renderer.dart';
class ChatBubble extends StatelessWidget {
final String message;
final bool isUser;
const ChatBubble({
super.key,
required this.message,
this.isUser = false,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isUser
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: renderContent(context, message),
),
if (!isUser) ...[
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.copy, size: 20),
tooltip: 'Copy Response',
onPressed: () {
Clipboard.setData(ClipboardData(text: message));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
},
),
],
],
),
),
);
}
}

View File

@@ -1,125 +0,0 @@
// lib/dashbot/widgets/content_renderer.dart
import 'dart:convert';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_highlighter/flutter_highlighter.dart';
import 'package:flutter_highlighter/themes/monokai-sublime.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
Widget renderContent(
BuildContext context,
String text,
) {
if (text.isEmpty) {
return const Text("No content to display.");
}
final codeBlockPattern = RegExp(
r'```(\w+)?\n([\s\S]*?)```',
multiLine: true,
);
final matches = codeBlockPattern.allMatches(text);
if (matches.isEmpty) {
return _renderMarkdown(context, text);
}
List<Widget> children = [];
int lastEnd = 0;
for (var match in matches) {
if (match.start > lastEnd) {
children.add(_renderMarkdown(
context,
text.substring(lastEnd, match.start),
));
}
final language = match.group(1) ?? 'text';
final code = match.group(2)!.trim();
children.add(_renderCodeBlock(context, language, code));
lastEnd = match.end;
}
if (lastEnd < text.length) {
children.add(_renderMarkdown(context, text.substring(lastEnd)));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
Widget _renderMarkdown(
BuildContext context,
String markdown,
) {
return MarkdownBody(
data: markdown,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
);
}
Widget _renderCodeBlock(
BuildContext context,
String language,
String code,
) {
if (language == 'json') {
try {
final prettyJson =
const JsonEncoder.withIndent(' ').convert(jsonDecode(code));
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: SelectableText(
prettyJson,
style: kCodeStyle.copyWith(
fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
),
);
} catch (e) {
return _renderFallbackCode(context, code);
}
} else {
try {
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: HighlightView(
code,
language: language,
theme: monokaiSublimeTheme,
textStyle: kCodeStyle.copyWith(
fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
),
);
} catch (e) {
return _renderFallbackCode(context, code);
}
}
}
Widget _renderFallbackCode(
BuildContext context,
String code,
) {
return Container(
padding: const EdgeInsets.all(8),
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: SelectableText(
code,
style: kCodeStyle.copyWith(
fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize,
color: Colors.red,
),
),
);
}

View File

@@ -1,340 +0,0 @@
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 {
const DashBotWidget({
super.key,
});
@override
ConsumerState<DashBotWidget> createState() => _DashBotWidgetState();
}
class _DashBotWidgetState extends ConsumerState<DashBotWidget> {
final TextEditingController _controller = TextEditingController();
late ScrollController _scrollController;
bool _isLoading = false;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
_controller.dispose();
super.dispose();
}
Future<void> _sendMessage(String message) async {
if (message.trim().isEmpty) return;
final dashBotService = ref.read(dashBotServiceProvider);
final requestModel = ref.read(selectedRequestModelProvider);
final responseModel = requestModel?.httpResponseModel;
setState(() => _isLoading = true);
ref.read(chatMessagesProvider.notifier).addMessage({
'role': 'user',
'message': message,
});
try {
final response = await dashBotService.handleRequest(
message, requestModel, responseModel);
if (response.startsWith("TEST_CASES_HIDDEN\n")) {
final testCases = response.replaceFirst("TEST_CASES_HIDDEN\n", "");
ref.read(chatMessagesProvider.notifier).addMessage({
'role': 'bot',
'message':
"Test cases generated successfully. Click the button below to run them.",
'testCases': testCases,
'showTestButton': true,
});
} else {
ref.read(chatMessagesProvider.notifier).addMessage({
'role': 'bot',
'message': response,
});
}
} catch (error, stackTrace) {
debugPrint('Error in _sendMessage: $error');
debugPrint('StackTrace: $stackTrace');
ref.read(chatMessagesProvider.notifier).addMessage({
'role': 'bot',
'message': "Error: ${error.toString()}",
});
} finally {
setState(() => _isLoading = false);
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.minScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
}
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);
final requestModel = ref.read(selectedRequestModelProvider);
final statusCode = requestModel?.httpResponseModel?.statusCode;
final showDebugButton = statusCode != null && statusCode >= 400;
final isMinimized = ref.watch(dashBotMinimizedProvider);
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: isMinimized
? _buildMinimizedView(context)
: 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),
],
),
);
}
Widget _buildHeader(BuildContext context) {
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: [
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;
},
),
IconButton(
padding: const EdgeInsets.all(8),
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.close, size: 20),
tooltip: 'Close',
onPressed: () {
Navigator.pop(context);
},
),
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,
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(context),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildInputArea(context),
),
],
);
}
Widget _buildQuickActions(bool showDebugButton) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
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,
),
),
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, size: 16),
label: const Text("Test"),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
visualDensity: VisualDensity.compact,
),
),
],
),
);
}
Widget _buildChatArea(List<Map<String, dynamic>> messages) {
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, size: 16),
label: const Text("Run Test Cases"),
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.compact,
),
),
),
],
);
}
return ChatBubble(
message: text,
isUser: message['role'] == 'user',
);
},
),
);
}
Widget _buildLoadingIndicator() {
return const Padding(
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: 4),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
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(
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;
}
},
),
],
),
);
}
}

View File

@@ -1,328 +0,0 @@
import 'dart:convert';
import 'package:apidash_core/apidash_core.dart' as http;
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'content_renderer.dart';
class TestRunnerWidget extends ConsumerStatefulWidget {
final String testCases;
const TestRunnerWidget({
super.key,
required this.testCases,
});
@override
ConsumerState<TestRunnerWidget> createState() => _TestRunnerWidgetState();
}
class _TestRunnerWidgetState extends ConsumerState<TestRunnerWidget> {
List<Map<String, dynamic>> _parsedTests = [];
Map<int, Map<String, dynamic>> _results = {};
bool _isRunning = false;
int _currentTestIndex = -1;
@override
void initState() {
super.initState();
_parseTestCases();
}
void _parseTestCases() {
final curlRegex = RegExp(r'```bash\ncurl\s+(.*?)\n```', dotAll: true);
final descriptionRegex = RegExp(r'###\s*(.*?)\n', dotAll: true);
final curlMatches = curlRegex.allMatches(widget.testCases);
final descMatches = descriptionRegex.allMatches(widget.testCases);
List<Map<String, dynamic>> 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(1)?.trim() ?? "";
tests.add({
'description': description,
'command': curlCommand,
'index': index,
});
index++;
}
setState(() {
_parsedTests = tests;
});
}
Future<void> _runTest(int index) async {
if (_isRunning) return;
setState(() {
_isRunning = true;
_currentTestIndex = index;
});
final test = _parsedTests[index];
final command = test['command'];
try {
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");
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";
}
http.Response response;
if (method == "GET") {
response = await http.get(Uri.parse(url));
} else if (method == "POST") {
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<void> _runAllTests() async {
for (int i = 0; i < _parsedTests.length; i++) {
if (!mounted) return;
await _runTest(i);
}
}
@override
Widget build(BuildContext context) {
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'),
),
],
),
);
},
),
],
),
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(),
],
),
),
);
}
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);
return Card(
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) : 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')),
);
},
),
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: kCodeStyle,
),
),
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 (_) {
return input;
}
}
}