Refactor AI model selection and config handling

This commit is contained in:
Ankit Mahato
2025-08-28 06:42:32 +05:30
parent 8fa3433cf8
commit 6e1f2b4773
56 changed files with 1074 additions and 1390 deletions

View File

@@ -0,0 +1,61 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
class BoundedTextField extends StatefulWidget {
const BoundedTextField({
super.key,
required this.value,
required this.onChanged,
});
final String value;
final void Function(String value) onChanged;
@override
State<BoundedTextField> createState() => _BoundedTextFieldState();
}
class _BoundedTextFieldState extends State<BoundedTextField> {
TextEditingController controller = TextEditingController();
@override
void initState() {
controller.text = widget.value;
super.initState();
}
@override
void didUpdateWidget(covariant BoundedTextField oldWidget) {
//Assisting in Resetting on Change
if (widget.value == '') {
controller.text = widget.value;
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
// final double width = context.isCompactWindow ? 150 : 220;
return Container(
height: 40,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
borderRadius: kBorderRadius8,
),
width: double.infinity,
child: Container(
transform: Matrix4.translationValues(0, -5, 0),
child: TextField(
controller: controller,
// obscureText: true,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.only(left: 10),
),
onChanged: widget.onChanged,
),
),
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash/models/models.dart';
@@ -19,7 +17,6 @@ class ResponseBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final responseModel = selectedRequestModel?.httpResponseModel;
if (responseModel == null) {
return const ErrorMessage(
message: '$kNullResponseModelError $kUnexpectedRaiseIssue');
@@ -28,7 +25,6 @@ class ResponseBody extends StatelessWidget {
final isSSE = responseModel.sseOutput?.isNotEmpty ?? false;
var body = responseModel.body;
var formattedBody = responseModel.formattedBody;
if (body == null) {
return const ErrorMessage(
message: '$kMsgNullBody $kUnexpectedRaiseIssue');
@@ -46,7 +42,6 @@ class ResponseBody extends StatelessWidget {
final mediaType =
responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain);
// Fix #415: Treat null Content-type as plain text instead of Error message
// if (mediaType == null) {
// return ErrorMessage(
@@ -54,8 +49,9 @@ class ResponseBody extends StatelessWidget {
// '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue');
// }
var responseBodyView = selectedRequestModel?.apiType == APIType.ai
? ([ResponseBodyView.answer, ResponseBodyView.raw], 'text')
var responseBodyView = (selectedRequestModel?.apiType == APIType.ai &&
(responseModel.sseOutput?.isNotEmpty ?? false))
? (kAnswerRawBodyViewOptions, kSubTypePlain)
: getResponseBodyViewOptions(mediaType);
var options = responseBodyView.$1;
var highlightLanguage = responseBodyView.$2;
@@ -65,18 +61,6 @@ class ResponseBody extends StatelessWidget {
options.remove(ResponseBodyView.code);
}
if (responseModel.sseOutput?.isNotEmpty ?? false) {
return ResponseBodySuccess(
key: Key("${selectedRequestModel!.id}-response"),
mediaType: MediaType('text', 'event-stream'),
options: [ResponseBodyView.sse, ResponseBodyView.raw],
bytes: utf8.encode((responseModel.sseOutput!).toString()),
body: jsonEncode(responseModel.sseOutput!),
formattedBody: responseModel.sseOutput!.join('\n'),
selectedModel: selectedRequestModel?.aiRequestModel?.model,
);
}
return ResponseBodySuccess(
key: Key("${selectedRequestModel!.id}-response"),
mediaType: mediaType,
@@ -85,7 +69,8 @@ class ResponseBody extends StatelessWidget {
body: body,
formattedBody: formattedBody,
highlightLanguage: highlightLanguage,
selectedModel: selectedRequestModel?.aiRequestModel?.model,
sseOutput: responseModel.sseOutput,
aiRequestModel: selectedRequestModel?.aiRequestModel,
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/foundation.dart';
@@ -7,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:apidash/utils/utils.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'package:genai/genai.dart';
import 'button_share.dart';
class ResponseBodySuccess extends StatefulWidget {
@@ -19,15 +16,17 @@ class ResponseBodySuccess extends StatefulWidget {
required this.bytes,
this.formattedBody,
this.highlightLanguage,
this.selectedModel,
this.sseOutput,
this.aiRequestModel,
});
final MediaType mediaType;
final List<ResponseBodyView> options;
final String body;
final Uint8List bytes;
final String? formattedBody;
final List<String>? sseOutput;
final String? highlightLanguage;
final LLMModel? selectedModel; //ONLY FOR AI-REQUESTS
final AIRequestModel? aiRequestModel;
@override
State<ResponseBodySuccess> createState() => _ResponseBodySuccessState();
@@ -50,8 +49,6 @@ class _ResponseBodySuccessState extends State<ResponseBodySuccess> {
borderRadius: kBorderRadius8,
);
final isAIRequest = widget.options.contains(ResponseBodyView.answer);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
var showLabel = showButtonLabelsInBodySuccess(
@@ -93,33 +90,20 @@ class _ResponseBodySuccessState extends State<ResponseBodySuccess> {
),
const Spacer(),
((widget.options == kPreviewRawBodyViewOptions) ||
kCodeRawBodyViewOptions.contains(currentSeg) ||
isAIRequest)
kCodeRawBodyViewOptions.contains(currentSeg))
? CopyButton(
toCopy: (currentSeg == ResponseBodyView.answer)
? widget.formattedBody!
: isAIRequest
? formatBody(widget.body, widget.mediaType)!
: (widget.formattedBody ?? widget.body),
toCopy: widget.formattedBody ?? widget.body,
showLabel: showLabel,
)
: const SizedBox(),
kIsMobile
? ShareButton(
toShare: (currentSeg == ResponseBodyView.answer)
? widget.formattedBody!
: isAIRequest
? formatBody(widget.body, widget.mediaType)!
: (widget.formattedBody ?? widget.body),
toShare: widget.formattedBody ?? widget.body,
showLabel: showLabel,
)
: SaveInDownloadsButton(
content: (currentSeg == ResponseBodyView.answer)
? utf8.encode(widget.formattedBody!)
: widget.bytes,
mimeType: (currentSeg == ResponseBodyView.answer)
? 'text/plain'
: widget.mediaType.mimeType,
content: widget.bytes,
mimeType: widget.mediaType.mimeType,
showLabel: showLabel,
),
],
@@ -153,25 +137,7 @@ class _ResponseBodySuccessState extends State<ResponseBodySuccess> {
),
),
),
ResponseBodyView.raw => Expanded(
child: Container(
width: double.maxFinite,
padding: kP8,
decoration: textContainerdecoration,
child: SingleChildScrollView(
child: SelectableText(
widget.options.contains(ResponseBodyView.answer)
? formatBody(
widget.body,
MediaType(kTypeApplication, kSubTypeJson),
)!
: (widget.formattedBody ?? widget.body),
style: kCodeStyle,
),
),
),
),
ResponseBodyView.answer => Expanded(
ResponseBodyView.raw || ResponseBodyView.answer => Expanded(
child: Container(
width: double.maxFinite,
padding: kP8,
@@ -190,8 +156,8 @@ class _ResponseBodySuccessState extends State<ResponseBodySuccess> {
padding: kP8,
decoration: textContainerdecoration,
child: SSEDisplay(
sseOutput: widget.formattedBody?.split('\n') ?? [],
selectedLLModel: widget.selectedModel,
sseOutput: widget.sseOutput,
aiRequestModel: widget.aiRequestModel,
),
),
),

View File

@@ -1,28 +1,23 @@
import 'dart:convert';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:genai/genai.dart';
class SSEDisplay extends StatefulWidget {
final LLMModel? selectedLLModel;
final List<String> sseOutput;
class SSEDisplay extends StatelessWidget {
final AIRequestModel? aiRequestModel;
final List<String>? sseOutput;
const SSEDisplay({
super.key,
required this.sseOutput,
this.selectedLLModel,
this.sseOutput,
this.aiRequestModel,
});
@override
State<SSEDisplay> createState() => _SSEDisplayState();
}
class _SSEDisplayState extends State<SSEDisplay> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final fontSizeMedium = theme.textTheme.bodyMedium?.fontSize;
final isDark = theme.brightness == Brightness.dark;
if (widget.sseOutput.isEmpty) {
if (sseOutput == null || sseOutput!.isEmpty) {
return Text(
'No content',
style: kCodeStyle.copyWith(
@@ -32,11 +27,10 @@ class _SSEDisplayState extends State<SSEDisplay> {
);
}
if (widget.selectedLLModel != null) {
if (aiRequestModel != null) {
// For RAW Text output (only AI Requests)
String out = "";
final mc = widget.selectedLLModel!.provider.modelController;
for (String x in widget.sseOutput) {
for (String x in sseOutput!) {
x = x.trim();
if (x.isEmpty || x.contains('[DONE]')) {
continue;
@@ -50,10 +44,10 @@ class _SSEDisplayState extends State<SSEDisplay> {
Map? dec;
try {
dec = jsonDecode(x);
final z = mc.streamOutputFormatter(dec!);
final z = aiRequestModel?.getFormattedStreamOutput(dec!);
out += z ?? '<?>';
} catch (e) {
print("Error in JSONDEC $e");
debugPrint("SSEDisplay -> Error in JSONDEC $e");
}
}
return SingleChildScrollView(
@@ -63,9 +57,8 @@ class _SSEDisplayState extends State<SSEDisplay> {
return ListView(
padding: kP1,
children: widget.sseOutput.reversed
.where((e) => e.trim() != '')
.map<Widget>((chunk) {
children:
sseOutput!.reversed.where((e) => e.trim() != '').map<Widget>((chunk) {
Map<String, dynamic>? parsedJson;
try {
parsedJson = jsonDecode(chunk);

View File

@@ -32,6 +32,7 @@ export 'field_cell_obscurable.dart';
export 'field_cell.dart';
export 'field_json_search.dart';
export 'field_read_only.dart';
export 'field_text_bounded.dart';
export 'field_url.dart';
export 'intro_message.dart';
export 'markdown.dart';