mirror of
https://github.com/foss42/apidash.git
synced 2025-12-05 12:34:26 +08:00
feat: improve ui for openapi specs feature
This commit is contained in:
@@ -4,6 +4,9 @@ import '../../../features/chat/models/chat_models.dart';
|
||||
import '../../../features/chat/viewmodel/chat_viewmodel.dart';
|
||||
import '../../../features/chat/providers/attachments_provider.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import '../../services/openapi_import_service.dart';
|
||||
import '../../../features/chat/view/widgets/openapi_operation_picker_dialog.dart';
|
||||
import 'package:openapi_spec/openapi_spec.dart';
|
||||
|
||||
/// Base mixin for action widgets.
|
||||
mixin DashbotActionMixin {
|
||||
@@ -212,6 +215,65 @@ class DashbotSelectOperationButton extends ConsumerWidget
|
||||
}
|
||||
}
|
||||
|
||||
class DashbotImportNowButton extends ConsumerWidget with DashbotActionMixin {
|
||||
@override
|
||||
final ChatAction action;
|
||||
const DashbotImportNowButton({super.key, required this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return FilledButton.icon(
|
||||
icon: const Icon(Icons.playlist_add_check, size: 16),
|
||||
label: const Text('Import Now'),
|
||||
onPressed: () async {
|
||||
try {
|
||||
OpenApi? spec;
|
||||
String? sourceName;
|
||||
if (action.value is Map<String, dynamic>) {
|
||||
final map = action.value as Map<String, dynamic>;
|
||||
sourceName = map['sourceName'] as String?;
|
||||
if (map['spec'] is OpenApi) {
|
||||
spec = map['spec'] as OpenApi;
|
||||
} else if (map['content'] is String) {
|
||||
spec =
|
||||
OpenApiImportService.tryParseSpec(map['content'] as String);
|
||||
}
|
||||
}
|
||||
if (spec == null) return;
|
||||
|
||||
final servers = spec.servers ?? const [];
|
||||
final baseUrl = servers.isNotEmpty ? (servers.first.url ?? '/') : '/';
|
||||
|
||||
final selected = await showOpenApiOperationPickerDialog(
|
||||
context: context,
|
||||
spec: spec,
|
||||
sourceName: sourceName,
|
||||
);
|
||||
if (selected == null || selected.isEmpty) return;
|
||||
|
||||
final notifier = ref.read(chatViewmodelProvider.notifier);
|
||||
for (final s in selected) {
|
||||
final payload = OpenApiImportService.payloadForOperation(
|
||||
baseUrl: baseUrl,
|
||||
path: s.path,
|
||||
method: s.method,
|
||||
op: s.op,
|
||||
);
|
||||
await notifier.applyAutoFix(ChatAction.fromJson({
|
||||
'action': 'apply_openapi',
|
||||
'actionType': 'apply_openapi',
|
||||
'target': 'httpRequestModel',
|
||||
'targetType': 'httpRequestModel',
|
||||
'field': 'apply_to_new',
|
||||
'value': payload,
|
||||
}));
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashbotGeneratedCodeBlock extends StatelessWidget
|
||||
with DashbotActionMixin {
|
||||
@override
|
||||
@@ -246,6 +308,9 @@ class DashbotActionWidgetFactory {
|
||||
static Widget? build(ChatAction action) {
|
||||
switch (action.actionType) {
|
||||
case ChatActionType.other:
|
||||
if (action.action == 'import_now_openapi') {
|
||||
return DashbotImportNowButton(action: action);
|
||||
}
|
||||
if (action.field == 'select_operation') {
|
||||
return DashbotSelectOperationButton(action: action);
|
||||
}
|
||||
@@ -265,6 +330,12 @@ class DashbotActionWidgetFactory {
|
||||
return DashbotApplyCurlButton(action: action);
|
||||
case ChatActionType.applyOpenApi:
|
||||
return DashbotApplyOpenApiButton(action: action);
|
||||
case ChatActionType.noAction:
|
||||
// If downstream requests, render an Import Now for OpenAPI contexts
|
||||
if (action.action == 'import_now_openapi') {
|
||||
return DashbotImportNowButton(action: action);
|
||||
}
|
||||
return null;
|
||||
case ChatActionType.updateField:
|
||||
case ChatActionType.addHeader:
|
||||
case ChatActionType.updateHeader:
|
||||
@@ -273,8 +344,7 @@ class DashbotActionWidgetFactory {
|
||||
case ChatActionType.updateUrl:
|
||||
case ChatActionType.updateMethod:
|
||||
return DashbotAutoFixButton(action: action);
|
||||
case ChatActionType.noAction:
|
||||
return null;
|
||||
|
||||
case ChatActionType.uploadAsset:
|
||||
if (action.targetType == ChatActionTarget.attachment) {
|
||||
return DashbotUploadRequestButton(action: action);
|
||||
|
||||
@@ -445,6 +445,40 @@ Where "explnation" must include:
|
||||
REFUSAL TEMPLATE (when off-topic), JSON only:
|
||||
{"explnation":"I am Dashbot, an AI assistant focused specifically on API development tasks within API Dash. My capabilities are limited to explaining API responses, debugging requests, generating documentation, creating tests, visualizing API data, and generating integration code. Therefore, I cannot answer questions outside of this scope. How can I assist you with an API-related task?","actions":[]}
|
||||
|
||||
RETURN THE JSON ONLY.
|
||||
</system_prompt>
|
||||
""";
|
||||
}
|
||||
|
||||
// Provide insights and suggestions after importing an OpenAPI spec
|
||||
String openApiInsightsPrompt({
|
||||
required String specSummary,
|
||||
}) {
|
||||
return """
|
||||
<system_prompt>
|
||||
YOU ARE Dashbot, an API Insights Assistant specialized in analyzing OpenAPI specifications within API Dash.
|
||||
|
||||
STRICT OFF-TOPIC POLICY
|
||||
- If a request is unrelated to APIs, refuse. Return JSON with only "explnation" and an empty "actions": [].
|
||||
|
||||
CONTEXT (OPENAPI SUMMARY)
|
||||
${specSummary.trim()}
|
||||
|
||||
TASK
|
||||
- Provide practical, user-friendly insights based on the API spec:
|
||||
- Identify noteworthy endpoints (e.g., CRUD sets, auth/login, health/status) and common patterns.
|
||||
- Point out authentication/security requirements (e.g., API keys, OAuth scopes) if present.
|
||||
- Suggest a few starter calls (e.g., list/search) and a short onboarding path.
|
||||
- Call out potential pitfalls (rate limits, pagination, required headers, content types).
|
||||
- Keep it concise and actionable: 1–2 line summary → 4–6 bullets → 2–3 next steps.
|
||||
|
||||
OUTPUT FORMAT (STRICT)
|
||||
- Return ONLY a single JSON object.
|
||||
- Keys: {"explnation": string, "actions": []}
|
||||
|
||||
REFUSAL TEMPLATE (when off-topic), JSON only:
|
||||
{"explnation":"I am Dashbot, an AI assistant focused specifically on API development tasks within API Dash. My capabilities are limited to explaining API responses, debugging requests, generating documentation, creating tests, visualizing API data, and generating integration code. Therefore, I cannot answer questions outside of this scope. How can I assist you with an API-related task?","actions":[]}
|
||||
|
||||
RETURN THE JSON ONLY.
|
||||
</system_prompt>
|
||||
""";
|
||||
|
||||
@@ -85,6 +85,7 @@ class OpenApiImportService {
|
||||
return {
|
||||
'method': method.toUpperCase(),
|
||||
'url': url,
|
||||
'baseUrl': baseUrl,
|
||||
'headers': headers,
|
||||
'body': body,
|
||||
'form': isForm,
|
||||
@@ -92,6 +93,20 @@ class OpenApiImportService {
|
||||
};
|
||||
}
|
||||
|
||||
/// Public wrapper to build a request payload for a given operation.
|
||||
static Map<String, dynamic> payloadForOperation({
|
||||
required String baseUrl,
|
||||
required String path,
|
||||
required String method,
|
||||
required Operation op,
|
||||
}) =>
|
||||
_payloadForOperation(
|
||||
baseUrl: baseUrl,
|
||||
path: path,
|
||||
method: method,
|
||||
op: op,
|
||||
);
|
||||
|
||||
/// Build an action message asking whether to apply to selected/new
|
||||
/// for a single chosen operation.
|
||||
static Map<String, dynamic> buildActionMessageFromPayload(
|
||||
@@ -121,15 +136,12 @@ class OpenApiImportService {
|
||||
}
|
||||
|
||||
/// Build a list of operations from the spec, and if multiple are found,
|
||||
/// return a JSON with explnation and an actions array of type "other"
|
||||
/// where each action value holds an actionPayload for that operation and
|
||||
/// path/method in the path field for UI display. The Chat model will emit
|
||||
/// a follow-up message once the user picks one.
|
||||
/// return a JSON with a single "Import Now" style action to open
|
||||
/// a selection dialog in the UI, avoiding rendering dozens of buttons.
|
||||
static Map<String, dynamic> buildOperationPicker(OpenApi spec) {
|
||||
final servers = spec.servers ?? const [];
|
||||
final baseUrl = servers.isNotEmpty ? (servers.first.url ?? '/') : '/';
|
||||
final actions = <Map<String, dynamic>>[];
|
||||
|
||||
int endpointsCount = 0;
|
||||
final methods = <String>{};
|
||||
(spec.paths ?? const {}).forEach((path, item) {
|
||||
final ops = <String, Operation?>{
|
||||
'GET': item.get,
|
||||
@@ -143,23 +155,12 @@ class OpenApiImportService {
|
||||
};
|
||||
ops.forEach((method, op) {
|
||||
if (op == null) return;
|
||||
final payload = _payloadForOperation(
|
||||
baseUrl: baseUrl,
|
||||
path: path,
|
||||
method: method,
|
||||
op: op,
|
||||
);
|
||||
actions.add({
|
||||
'action': 'other',
|
||||
'target': 'httpRequestModel',
|
||||
'field': 'select_operation',
|
||||
'path': '$method $path',
|
||||
'value': payload,
|
||||
});
|
||||
endpointsCount += 1;
|
||||
methods.add(method);
|
||||
});
|
||||
});
|
||||
|
||||
if (actions.isEmpty) {
|
||||
if (endpointsCount == 0) {
|
||||
return {
|
||||
'explnation':
|
||||
'No operations found in the OpenAPI spec. Please check the file.',
|
||||
@@ -167,10 +168,33 @@ class OpenApiImportService {
|
||||
};
|
||||
}
|
||||
|
||||
// Build a short spec summary for downstream insights prompt
|
||||
final title = spec.info.title.isNotEmpty ? spec.info.title : 'Untitled API';
|
||||
final version = spec.info.version;
|
||||
final server = servers.isNotEmpty ? servers.first.url : null;
|
||||
final summary = StringBuffer()
|
||||
..writeln('- Title: $title (v$version)')
|
||||
..writeln('- Server: ${server ?? '/'}')
|
||||
..writeln('- Endpoints discovered: $endpointsCount')
|
||||
..writeln('- Methods: ${methods.join(', ')}');
|
||||
|
||||
return {
|
||||
'explnation':
|
||||
'OpenAPI parsed. Select an operation to import as a request:',
|
||||
'actions': actions,
|
||||
'explnation': 'OpenAPI parsed. Click Import Now to choose operations.',
|
||||
'actions': [
|
||||
{
|
||||
'action': 'import_now_openapi',
|
||||
'target': 'httpRequestModel',
|
||||
'field': '',
|
||||
'path': null,
|
||||
'value': {
|
||||
'spec': spec,
|
||||
'sourceName': title,
|
||||
}
|
||||
}
|
||||
],
|
||||
'meta': {
|
||||
'openapi_summary': summary.toString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:openapi_spec/openapi_spec.dart';
|
||||
|
||||
|
||||
typedef OpenApiOperationItem = ({String method, String path, Operation op});
|
||||
|
||||
Future<List<OpenApiOperationItem>?> showOpenApiOperationPickerDialog({
|
||||
required BuildContext context,
|
||||
required OpenApi spec,
|
||||
String? sourceName,
|
||||
}) async {
|
||||
final title = (spec.info.title.trim().isNotEmpty
|
||||
? spec.info.title.trim()
|
||||
: (sourceName ?? 'OpenAPI'))
|
||||
.trim();
|
||||
|
||||
final ops = <OpenApiOperationItem>[];
|
||||
(spec.paths ?? const {}).forEach((path, item) {
|
||||
final map = <String, Operation?>{
|
||||
'GET': item.get,
|
||||
'POST': item.post,
|
||||
'PUT': item.put,
|
||||
'DELETE': item.delete,
|
||||
'PATCH': item.patch,
|
||||
'HEAD': item.head,
|
||||
'OPTIONS': item.options,
|
||||
'TRACE': item.trace,
|
||||
};
|
||||
map.forEach((method, op) {
|
||||
if (op != null) {
|
||||
ops.add((method: method, path: path, op: op));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (ops.isEmpty) {
|
||||
// Nothing to select; return empty selection.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Multi-select: default select all
|
||||
final selected = <int>{for (var i = 0; i < ops.length; i++) i};
|
||||
bool selectAll = ops.isNotEmpty;
|
||||
|
||||
return showDialog<List<OpenApiOperationItem>>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return StatefulBuilder(builder: (ctx, setState) {
|
||||
return AlertDialog(
|
||||
title: Text('Import from $title'),
|
||||
content: SizedBox(
|
||||
width: 520,
|
||||
height: 420,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ExpansionTile(
|
||||
initiallyExpanded: true,
|
||||
title: const Text('Available operations'),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: CheckboxListTile(
|
||||
value: selectAll,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
selectAll = v ?? false;
|
||||
selected
|
||||
..clear()
|
||||
..addAll(selectAll
|
||||
? List<int>.generate(ops.length, (i) => i)
|
||||
: const <int>{});
|
||||
});
|
||||
},
|
||||
title: const Text('Select all'),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
itemCount: ops.length,
|
||||
itemBuilder: (c, i) {
|
||||
final o = ops[i];
|
||||
final label = '${o.method} ${o.path}';
|
||||
final checked = selected.contains(i);
|
||||
return CheckboxListTile(
|
||||
value: checked,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
if (v == true) {
|
||||
selected.add(i);
|
||||
} else {
|
||||
selected.remove(i);
|
||||
}
|
||||
selectAll = selected.length == ops.length;
|
||||
});
|
||||
},
|
||||
title: Text(label),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(null),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: selected.isEmpty
|
||||
? null
|
||||
: () {
|
||||
final result = selected.map((i) => ops[i]).toList();
|
||||
Navigator.of(ctx).pop(result);
|
||||
},
|
||||
child: const Text('Import'),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:apidash_core/apidash_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/models/models.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
import '../../../core/services/curl_import_service.dart';
|
||||
@@ -482,6 +483,20 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
orElse: () => HTTPVerb.get,
|
||||
);
|
||||
final url = payload['url'] as String? ?? '';
|
||||
final baseUrl = payload['baseUrl'] as String? ?? _inferBaseUrl(url);
|
||||
// Derive a human-readable route path for naming
|
||||
String routePath;
|
||||
if (baseUrl.isNotEmpty && url.startsWith(baseUrl)) {
|
||||
routePath = url.substring(baseUrl.length);
|
||||
} else {
|
||||
try {
|
||||
final u = Uri.parse(url);
|
||||
routePath = u.path.isEmpty ? '/' : u.path;
|
||||
} catch (_) {
|
||||
routePath = url;
|
||||
}
|
||||
}
|
||||
if (!routePath.startsWith('/')) routePath = '/$routePath';
|
||||
|
||||
final headersMap =
|
||||
(payload['headers'] as Map?)?.cast<String, dynamic>() ?? {};
|
||||
@@ -525,11 +540,12 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
final withEnvUrl = await _maybeSubstituteBaseUrl(url, baseUrl);
|
||||
if (action.field == 'apply_to_selected') {
|
||||
if (requestId == null) return;
|
||||
collection.update(
|
||||
method: method,
|
||||
url: url,
|
||||
url: withEnvUrl,
|
||||
headers: headers,
|
||||
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
||||
body: body,
|
||||
@@ -541,14 +557,15 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
} else if (action.field == 'apply_to_new') {
|
||||
final model = HttpRequestModel(
|
||||
method: method,
|
||||
url: url,
|
||||
url: withEnvUrl,
|
||||
headers: headers,
|
||||
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
||||
body: body,
|
||||
bodyContentType: bodyContentType,
|
||||
formData: formData.isEmpty ? null : formData,
|
||||
);
|
||||
collection.addRequestModel(model, name: 'Imported OpenAPI');
|
||||
final displayName = '${method.name.toUpperCase()} $routePath';
|
||||
collection.addRequestModel(model, name: displayName);
|
||||
_appendSystem('Created a new request from the OpenAPI operation.',
|
||||
ChatMessageType.importOpenApi);
|
||||
} else if (action.field == 'select_operation') {
|
||||
@@ -674,6 +691,16 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
// If meta summary is present, generate insights via AI
|
||||
try {
|
||||
final meta = picker['meta'];
|
||||
final summary = (meta is Map && meta['openapi_summary'] is String)
|
||||
? meta['openapi_summary'] as String
|
||||
: '';
|
||||
if (summary.isNotEmpty) {
|
||||
await _generateOpenApiInsights(summary);
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
debugPrint('[OpenAPI] Exception: $e');
|
||||
final safe = e.toString().replaceAll('"', "'");
|
||||
@@ -695,6 +722,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
orElse: () => HTTPVerb.get,
|
||||
);
|
||||
final url = payload['url'] as String? ?? '';
|
||||
final baseUrl = _inferBaseUrl(url);
|
||||
|
||||
final headersMap =
|
||||
(payload['headers'] as Map?)?.cast<String, dynamic>() ?? {};
|
||||
@@ -739,11 +767,12 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
final withEnvUrl = await _maybeSubstituteBaseUrl(url, baseUrl);
|
||||
if (action.field == 'apply_to_selected') {
|
||||
if (requestId == null) return;
|
||||
collection.update(
|
||||
method: method,
|
||||
url: url,
|
||||
url: withEnvUrl,
|
||||
headers: headers,
|
||||
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
||||
body: body,
|
||||
@@ -755,7 +784,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
} else if (action.field == 'apply_to_new') {
|
||||
final model = HttpRequestModel(
|
||||
method: method,
|
||||
url: url,
|
||||
url: withEnvUrl,
|
||||
headers: headers,
|
||||
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
||||
body: body,
|
||||
@@ -954,6 +983,81 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
||||
}
|
||||
return t.contains('openapi:') || t.contains('swagger:');
|
||||
}
|
||||
|
||||
String _inferBaseUrl(String url) {
|
||||
try {
|
||||
final u = Uri.parse(url);
|
||||
if (u.hasScheme && u.host.isNotEmpty) {
|
||||
final portPart = (u.hasPort && u.port != 0) ? ':${u.port}' : '';
|
||||
return '${u.scheme}://${u.host}$portPart';
|
||||
}
|
||||
} catch (_) {}
|
||||
final m = RegExp(r'^(https?:\/\/[^\/]+)').firstMatch(url);
|
||||
return m?.group(1) ?? '';
|
||||
}
|
||||
|
||||
Future<String> _ensureBaseUrlEnv(String baseUrl) async {
|
||||
if (baseUrl.isEmpty) return 'BASE_URL';
|
||||
String host = 'API';
|
||||
try {
|
||||
final u = Uri.parse(baseUrl);
|
||||
if (u.hasAuthority && u.host.isNotEmpty) host = u.host;
|
||||
} catch (_) {}
|
||||
final slug = host
|
||||
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
|
||||
.replaceAll(RegExp(r'_+'), '_')
|
||||
.replaceAll(RegExp(r'^_|_$'), '')
|
||||
.toUpperCase();
|
||||
final key = 'BASE_URL_$slug';
|
||||
|
||||
final envNotifier = _ref.read(environmentsStateNotifierProvider.notifier);
|
||||
final envs = _ref.read(environmentsStateNotifierProvider);
|
||||
String? activeId = _ref.read(activeEnvironmentIdStateProvider);
|
||||
activeId ??= kGlobalEnvironmentId;
|
||||
final envModel = envs?[activeId];
|
||||
|
||||
if (envModel != null) {
|
||||
final exists = envModel.values.any((v) => v.key == key);
|
||||
if (!exists) {
|
||||
final values = [...envModel.values];
|
||||
values.add(
|
||||
EnvironmentVariableModel(key: key, value: baseUrl, enabled: true));
|
||||
envNotifier.updateEnvironment(activeId, values: values);
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
Future<String> _maybeSubstituteBaseUrl(String url, String baseUrl) async {
|
||||
if (baseUrl.isEmpty || !url.startsWith(baseUrl)) return url;
|
||||
final key = await _ensureBaseUrlEnv(baseUrl);
|
||||
final path = url.substring(baseUrl.length);
|
||||
final normalized = path.startsWith('/') ? path : '/$path';
|
||||
return '{{$key}}$normalized';
|
||||
}
|
||||
|
||||
Future<void> _generateOpenApiInsights(String summary) async {
|
||||
final ai = _selectedAIModel;
|
||||
if (ai == null) return;
|
||||
try {
|
||||
final sys = dash.DashbotPrompts().openApiInsightsPrompt(
|
||||
specSummary: summary,
|
||||
);
|
||||
final res = await _repo.sendChat(
|
||||
request: ai.copyWith(
|
||||
systemPrompt: sys,
|
||||
userPrompt:
|
||||
'Provide concise, actionable insights about these endpoints.',
|
||||
stream: false,
|
||||
),
|
||||
);
|
||||
if (res != null && res.isNotEmpty) {
|
||||
_appendSystem(res, ChatMessageType.importOpenApi);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[Chat] Insights error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final chatViewmodelProvider = StateNotifierProvider<ChatViewmodel, ChatState>((
|
||||
|
||||
Reference in New Issue
Block a user