AI Request History & Duplication Feature

This commit is contained in:
Manas Hejmadi
2025-06-08 22:34:26 +05:30
parent 97d4a7a45b
commit 452020f720
11 changed files with 331 additions and 37 deletions

View File

@@ -1,4 +1,5 @@
import 'package:apidash_core/apidash_core.dart';
import 'package:genai/genai.dart';
import 'models.dart';
part 'history_request_model.freezed.dart';
@@ -14,7 +15,8 @@ class HistoryRequestModel with _$HistoryRequestModel {
const factory HistoryRequestModel({
required String historyId,
required HistoryMetaModel metaData,
required HttpRequestModel httpRequestModel,
HttpRequestModel? httpRequestModel,
AIRequestModel? aiRequestModel,
required HttpResponseModel httpResponseModel,
String? preRequestScript,
String? postRequestScript,

View File

@@ -22,7 +22,8 @@ HistoryRequestModel _$HistoryRequestModelFromJson(Map<String, dynamic> json) {
mixin _$HistoryRequestModel {
String get historyId => throw _privateConstructorUsedError;
HistoryMetaModel get metaData => throw _privateConstructorUsedError;
HttpRequestModel get httpRequestModel => throw _privateConstructorUsedError;
HttpRequestModel? get httpRequestModel => throw _privateConstructorUsedError;
AIRequestModel? get aiRequestModel => throw _privateConstructorUsedError;
HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError;
String? get preRequestScript => throw _privateConstructorUsedError;
String? get postRequestScript => throw _privateConstructorUsedError;
@@ -47,14 +48,16 @@ abstract class $HistoryRequestModelCopyWith<$Res> {
$Res call(
{String historyId,
HistoryMetaModel metaData,
HttpRequestModel httpRequestModel,
HttpRequestModel? httpRequestModel,
AIRequestModel? aiRequestModel,
HttpResponseModel httpResponseModel,
String? preRequestScript,
String? postRequestScript,
AuthModel? authModel});
$HistoryMetaModelCopyWith<$Res> get metaData;
$HttpRequestModelCopyWith<$Res> get httpRequestModel;
$HttpRequestModelCopyWith<$Res>? get httpRequestModel;
$AIRequestModelCopyWith<$Res>? get aiRequestModel;
$HttpResponseModelCopyWith<$Res> get httpResponseModel;
$AuthModelCopyWith<$Res>? get authModel;
}
@@ -76,7 +79,8 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel>
$Res call({
Object? historyId = null,
Object? metaData = null,
Object? httpRequestModel = null,
Object? httpRequestModel = freezed,
Object? aiRequestModel = freezed,
Object? httpResponseModel = null,
Object? preRequestScript = freezed,
Object? postRequestScript = freezed,
@@ -91,10 +95,14 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel>
? _value.metaData
: metaData // ignore: cast_nullable_to_non_nullable
as HistoryMetaModel,
httpRequestModel: null == httpRequestModel
httpRequestModel: freezed == httpRequestModel
? _value.httpRequestModel
: httpRequestModel // ignore: cast_nullable_to_non_nullable
as HttpRequestModel,
as HttpRequestModel?,
aiRequestModel: freezed == aiRequestModel
? _value.aiRequestModel
: aiRequestModel // ignore: cast_nullable_to_non_nullable
as AIRequestModel?,
httpResponseModel: null == httpResponseModel
? _value.httpResponseModel
: httpResponseModel // ignore: cast_nullable_to_non_nullable
@@ -128,12 +136,30 @@ class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel>
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$HttpRequestModelCopyWith<$Res> get httpRequestModel {
return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel, (value) {
$HttpRequestModelCopyWith<$Res>? get httpRequestModel {
if (_value.httpRequestModel == null) {
return null;
}
return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel!, (value) {
return _then(_value.copyWith(httpRequestModel: value) as $Val);
});
}
/// Create a copy of HistoryRequestModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$AIRequestModelCopyWith<$Res>? get aiRequestModel {
if (_value.aiRequestModel == null) {
return null;
}
return $AIRequestModelCopyWith<$Res>(_value.aiRequestModel!, (value) {
return _then(_value.copyWith(aiRequestModel: value) as $Val);
});
}
/// Create a copy of HistoryRequestModel
/// with the given fields replaced by the non-null parameter values.
@override
@@ -170,7 +196,8 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res>
$Res call(
{String historyId,
HistoryMetaModel metaData,
HttpRequestModel httpRequestModel,
HttpRequestModel? httpRequestModel,
AIRequestModel? aiRequestModel,
HttpResponseModel httpResponseModel,
String? preRequestScript,
String? postRequestScript,
@@ -179,7 +206,9 @@ abstract class _$$HistoryRequestModelImplCopyWith<$Res>
@override
$HistoryMetaModelCopyWith<$Res> get metaData;
@override
$HttpRequestModelCopyWith<$Res> get httpRequestModel;
$HttpRequestModelCopyWith<$Res>? get httpRequestModel;
@override
$AIRequestModelCopyWith<$Res>? get aiRequestModel;
@override
$HttpResponseModelCopyWith<$Res> get httpResponseModel;
@override
@@ -201,7 +230,8 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res>
$Res call({
Object? historyId = null,
Object? metaData = null,
Object? httpRequestModel = null,
Object? httpRequestModel = freezed,
Object? aiRequestModel = freezed,
Object? httpResponseModel = null,
Object? preRequestScript = freezed,
Object? postRequestScript = freezed,
@@ -216,10 +246,14 @@ class __$$HistoryRequestModelImplCopyWithImpl<$Res>
? _value.metaData
: metaData // ignore: cast_nullable_to_non_nullable
as HistoryMetaModel,
httpRequestModel: null == httpRequestModel
httpRequestModel: freezed == httpRequestModel
? _value.httpRequestModel
: httpRequestModel // ignore: cast_nullable_to_non_nullable
as HttpRequestModel,
as HttpRequestModel?,
aiRequestModel: freezed == aiRequestModel
? _value.aiRequestModel
: aiRequestModel // ignore: cast_nullable_to_non_nullable
as AIRequestModel?,
httpResponseModel: null == httpResponseModel
? _value.httpResponseModel
: httpResponseModel // ignore: cast_nullable_to_non_nullable
@@ -247,7 +281,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel {
const _$HistoryRequestModelImpl(
{required this.historyId,
required this.metaData,
required this.httpRequestModel,
this.httpRequestModel,
this.aiRequestModel,
required this.httpResponseModel,
this.preRequestScript,
this.postRequestScript,
@@ -261,7 +296,9 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel {
@override
final HistoryMetaModel metaData;
@override
final HttpRequestModel httpRequestModel;
final HttpRequestModel? httpRequestModel;
@override
final AIRequestModel? aiRequestModel;
@override
final HttpResponseModel httpResponseModel;
@override
@@ -273,7 +310,7 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel {
@override
String toString() {
return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)';
return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, aiRequestModel: $aiRequestModel, httpResponseModel: $httpResponseModel, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, authModel: $authModel)';
}
@override
@@ -287,6 +324,8 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel {
other.metaData == metaData) &&
(identical(other.httpRequestModel, httpRequestModel) ||
other.httpRequestModel == httpRequestModel) &&
(identical(other.aiRequestModel, aiRequestModel) ||
other.aiRequestModel == aiRequestModel) &&
(identical(other.httpResponseModel, httpResponseModel) ||
other.httpResponseModel == httpResponseModel) &&
(identical(other.preRequestScript, preRequestScript) ||
@@ -304,6 +343,7 @@ class _$HistoryRequestModelImpl implements _HistoryRequestModel {
historyId,
metaData,
httpRequestModel,
aiRequestModel,
httpResponseModel,
preRequestScript,
postRequestScript,
@@ -330,7 +370,8 @@ abstract class _HistoryRequestModel implements HistoryRequestModel {
const factory _HistoryRequestModel(
{required final String historyId,
required final HistoryMetaModel metaData,
required final HttpRequestModel httpRequestModel,
final HttpRequestModel? httpRequestModel,
final AIRequestModel? aiRequestModel,
required final HttpResponseModel httpResponseModel,
final String? preRequestScript,
final String? postRequestScript,
@@ -344,7 +385,9 @@ abstract class _HistoryRequestModel implements HistoryRequestModel {
@override
HistoryMetaModel get metaData;
@override
HttpRequestModel get httpRequestModel;
HttpRequestModel? get httpRequestModel;
@override
AIRequestModel? get aiRequestModel;
@override
HttpResponseModel get httpResponseModel;
@override

View File

@@ -11,8 +11,14 @@ _$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) =>
historyId: json['historyId'] as String,
metaData: HistoryMetaModel.fromJson(
Map<String, Object?>.from(json['metaData'] as Map)),
httpRequestModel: HttpRequestModel.fromJson(
httpRequestModel: json['httpRequestModel'] == null
? null
: HttpRequestModel.fromJson(
Map<String, Object?>.from(json['httpRequestModel'] as Map)),
aiRequestModel: json['aiRequestModel'] == null
? null
: AIRequestModel.fromJson(
Map<String, Object?>.from(json['aiRequestModel'] as Map)),
httpResponseModel: HttpResponseModel.fromJson(
Map<String, Object?>.from(json['httpResponseModel'] as Map)),
preRequestScript: json['preRequestScript'] as String?,
@@ -28,7 +34,8 @@ Map<String, dynamic> _$$HistoryRequestModelImplToJson(
<String, dynamic>{
'historyId': instance.historyId,
'metaData': instance.metaData.toJson(),
'httpRequestModel': instance.httpRequestModel.toJson(),
'httpRequestModel': instance.httpRequestModel?.toJson(),
'aiRequestModel': instance.aiRequestModel?.toJson(),
'httpResponseModel': instance.httpResponseModel.toJson(),
'preRequestScript': instance.preRequestScript,
'postRequestScript': instance.postRequestScript,

View File

@@ -187,10 +187,15 @@ class CollectionStateNotifier
var itemIds = ref.read(requestSequenceProvider);
var currentModel = historyRequestModel;
final aT = currentModel.aiRequestModel != null ? APIType.ai : APIType.rest;
final newModel = RequestModel(
apiType: aT,
id: newId,
name: "${currentModel.metaData.name} (history)",
httpRequestModel: currentModel.httpRequestModel,
aiRequestModel: currentModel.aiRequestModel,
httpRequestModel: currentModel.httpRequestModel ?? HttpRequestModel(),
responseStatus: currentModel.metaData.responseStatus,
message: kResponseCodeReasons[currentModel.metaData.responseStatus],
httpResponseModel: currentModel.httpResponseModel,
@@ -461,6 +466,7 @@ class CollectionStateNotifier
timeStamp: DateTime.now(),
),
httpRequestModel: substitutedHttpRequestModel,
aiRequestModel: aiRequestModel,
httpResponseModel: httpResponseModel!,
preRequestScript: requestModel.preRequestScript,
postRequestScript: requestModel.postRequestScript,

View File

@@ -28,6 +28,13 @@ class CodePane extends ConsumerWidget {
final selectedRequestModel = isHistoryRequest
? getRequestModelFromHistoryModel(selectedHistoryRequestModel!)
: ref.watch(selectedRequestModelProvider);
if (selectedRequestModel!.apiType == APIType.ai) {
return const ErrorMessage(
message: "Code generation for AI Requests is currently not available.",
);
}
final defaultUriScheme =
ref.watch(settingsProvider.select((value) => value.defaultUriScheme));

View File

@@ -0,0 +1,178 @@
import 'package:apidash/providers/collection_providers.dart';
import 'package:apidash/providers/history_providers.dart';
import 'package:apidash/widgets/editor.dart';
import 'package:apidash_design_system/tokens/measurements.dart';
import 'package:apidash_design_system/widgets/textfield_outlined.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:genai/llm_config.dart';
import 'package:genai/widgets/ai_config_widgets.dart';
class HisAIRequestPromptSection extends ConsumerWidget {
const HisAIRequestPromptSection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedHistoryModel =
ref.watch(selectedHistoryRequestModelProvider)!;
final aiReqM = selectedHistoryModel.aiRequestModel!;
final payload = aiReqM.payload;
final systemPrompt = payload.systemPrompt;
final userPrompt = payload.userPrompt;
return Container(
padding: EdgeInsets.symmetric(vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 25.0),
child: Text(
'System Prompt',
),
),
kVSpacer10,
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: TextFieldEditor(
key: Key(
"${selectedHistoryModel.historyId}-aireq-sysprompt-body"),
fieldKey:
"${selectedHistoryModel.historyId}-aireq-sysprompt-body",
initialValue: systemPrompt,
readOnly: true,
),
),
),
SizedBox(height: 10),
Padding(
padding: const EdgeInsets.only(left: 25.0),
child: Text(
'User Prompt / Input',
),
),
kVSpacer10,
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: TextFieldEditor(
key: Key(
"${selectedHistoryModel.historyId}-aireq-userprompt-body"),
fieldKey:
"${selectedHistoryModel.historyId}-aireq-userprompt-body",
initialValue: userPrompt,
readOnly: true,
),
),
),
],
),
);
}
}
class HisAIRequestAuthorizationSection extends ConsumerWidget {
const HisAIRequestAuthorizationSection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedHistoryModel =
ref.watch(selectedHistoryRequestModelProvider)!;
final aiReqM = selectedHistoryModel.aiRequestModel!;
final payload = aiReqM.payload;
final cred = payload.credential;
return Container(
padding: EdgeInsets.symmetric(vertical: 20),
child: Column(
children: [
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: TextFieldEditor(
key: Key(
"${selectedHistoryModel.historyId}-aireq-authvalue-body"),
fieldKey:
"${selectedHistoryModel.historyId}-aireq-authvalue-body",
initialValue: cred,
readOnly: true,
),
),
),
],
),
);
}
}
class HisAIRequestConfigSection extends ConsumerWidget {
const HisAIRequestConfigSection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedHistoryModel =
ref.watch(selectedHistoryRequestModelProvider)!;
final aiReqM = selectedHistoryModel.aiRequestModel!;
final payload = aiReqM.payload;
return SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 20),
child: Column(
key: ValueKey(selectedHistoryModel.historyId),
children: [
...payload.configMap.values.map(
(el) => ListTile(
title: Text(el.configName),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
el.configDescription,
),
SizedBox(height: 5),
if (el.configType == LLMModelConfigurationType.boolean) ...[
BooleanAIConfig(
readonly: true,
configuration: el,
onConfigUpdated: (x) {},
),
] else if (el.configType ==
LLMModelConfigurationType.numeric) ...[
WritableAIConfig(
configuration: el,
onConfigUpdated: (x) {},
readonly: true,
numeric: true,
),
] else if (el.configType ==
LLMModelConfigurationType.text) ...[
WritableAIConfig(
configuration: el,
onConfigUpdated: (x) {},
readonly: true,
),
] else if (el.configType ==
LLMModelConfigurationType.slider) ...[
SliderAIConfig(
configuration: el,
onSliderUpdated: (x) {},
readonly: true,
),
],
SizedBox(height: 10),
],
),
),
),
],
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:apidash/screens/history/history_widgets/ai_history_page.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
@@ -22,22 +23,38 @@ class HistoryRequestPane extends ConsumerWidget {
final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider);
final apiType = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.metaData.apiType));
final headersMap = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.headersMap)) ??
final headersMap = ref.watch(selectedHistoryRequestModelProvider.select(
(value) {
if (apiType == APIType.ai) return <String, String>{};
return value?.httpRequestModel!.headersMap;
},
)) ??
{};
final headerLength = headersMap.length;
final paramsMap = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.paramsMap)) ??
final paramsMap = ref.watch(selectedHistoryRequestModelProvider.select(
(value) {
if (apiType == APIType.ai) return <String, String>{};
return value?.httpRequestModel!.paramsMap;
},
)) ??
{};
final paramLength = paramsMap.length;
final hasBody = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.hasBody)) ??
final hasBody = ref.watch(selectedHistoryRequestModelProvider.select(
(value) {
if (apiType == APIType.ai) return false;
return value?.httpRequestModel!.hasBody;
},
)) ??
false;
final hasQuery = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.hasQuery)) ??
final hasQuery =
ref.watch(selectedHistoryRequestModelProvider.select((value) {
if (apiType == APIType.ai) return false;
return value?.httpRequestModel!.hasQuery;
})) ??
false;
final scriptsLength = ref.watch(selectedHistoryRequestModelProvider
@@ -127,7 +144,27 @@ class HistoryRequestPane extends ConsumerWidget {
const HistoryScriptsTab(),
],
),
APIType.ai => FlutterLogo(),
APIType.ai => RequestPane(
key: const Key("history-request-pane-ai"),
selectedId: selectedId,
codePaneVisible: codePaneVisible,
onPressedCodeButton: () {
ref.read(historyCodePaneVisibleStateProvider.notifier).state =
!codePaneVisible;
},
showViewCodeButton: !isCompact,
showIndicators: [
false,
false,
false,
],
tabLabels: const ["Prompts", "Authorization", "Configuration"],
children: [
const HisAIRequestPromptSection(),
const HisAIRequestAuthorizationSection(),
const HisAIRequestConfigSection(),
],
),
_ => kSizedBoxEmpty,
};
}

View File

@@ -13,16 +13,20 @@ class HistoryResponsePane extends ConsumerWidget {
final selectedId = ref.watch(selectedHistoryIdStateProvider);
final selectedHistoryRequest =
ref.watch(selectedHistoryRequestModelProvider);
final historyHttpResponseModel = selectedHistoryRequest?.httpResponseModel;
if (selectedId != null) {
final requestModel =
getRequestModelFromHistoryModel(selectedHistoryRequest!);
final statusCode = historyHttpResponseModel?.statusCode;
return Column(
children: [
ResponsePaneHeader(
responseStatus: historyHttpResponseModel?.statusCode,
message: kResponseCodeReasons[historyHttpResponseModel?.statusCode],
responseStatus: statusCode,
message: kResponseCodeReasons[statusCode],
time: historyHttpResponseModel?.time,
),
Expanded(

View File

@@ -13,6 +13,7 @@ RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) {
name: model.metaData.name,
responseStatus: model.httpResponseModel.statusCode,
message: kResponseCodeReasons[model.httpResponseModel.statusCode],
aiRequestModel: model.aiRequestModel,
httpRequestModel: model.httpRequestModel,
httpResponseModel: model.httpResponseModel,
);

View File

@@ -4,10 +4,12 @@ import 'package:genai/llm_config.dart';
class SliderAIConfig extends StatelessWidget {
final LLMModelConfiguration configuration;
final Function(LLMModelConfiguration) onSliderUpdated;
final bool readonly;
const SliderAIConfig({
super.key,
required this.configuration,
required this.onSliderUpdated,
this.readonly = false,
});
@override
@@ -23,6 +25,7 @@ class SliderAIConfig extends StatelessWidget {
max: (configuration.configValue.value as (double, double, double))
.$3,
onChanged: (x) {
if (readonly) return;
final z =
configuration.configValue.value as (double, double, double);
configuration.configValue.value = (z.$1, x, z.$3);
@@ -43,11 +46,13 @@ class WritableAIConfig extends StatelessWidget {
final bool numeric;
final LLMModelConfiguration configuration;
final Function(LLMModelConfiguration) onConfigUpdated;
final bool readonly;
const WritableAIConfig({
super.key,
this.numeric = false,
required this.configuration,
required this.onConfigUpdated,
this.readonly = false,
});
@override
@@ -55,6 +60,7 @@ class WritableAIConfig extends StatelessWidget {
return TextFormField(
initialValue: configuration.configValue.value.toString(),
onChanged: (x) {
if (readonly) return;
if (numeric) {
if (x.isEmpty) x = '0';
if (num.tryParse(x) == null) return;
@@ -71,10 +77,12 @@ class WritableAIConfig extends StatelessWidget {
class BooleanAIConfig extends StatelessWidget {
final LLMModelConfiguration configuration;
final Function(LLMModelConfiguration) onConfigUpdated;
final bool readonly;
const BooleanAIConfig({
super.key,
required this.configuration,
required this.onConfigUpdated,
this.readonly = false,
});
@override
@@ -82,6 +90,7 @@ class BooleanAIConfig extends StatelessWidget {
return Switch(
value: configuration.configValue.value as bool,
onChanged: (x) {
if (readonly) return;
configuration.configValue.value = x;
onConfigUpdated(configuration);
},

View File

@@ -31,7 +31,7 @@ void main() {
expect(
find.text(
historyRequestModel.httpRequestModel.method.name.toUpperCase()),
historyRequestModel.httpRequestModel!.method.name.toUpperCase()),
findsOneWidget);
});
@@ -45,7 +45,7 @@ void main() {
);
expect(
find.text(historyRequestModel.httpRequestModel.url), findsOneWidget);
find.text(historyRequestModel.httpRequestModel!.url), findsOneWidget);
});
});
}