From f60836e4b27b224f7e7396f06d3c8b7c24d4ff9a Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Fri, 25 Jul 2025 19:03:23 +0530 Subject: [PATCH] Added SSE(Streaming) to Native AI Requests --- lib/widgets/response_body_success.dart | 4 +- lib/widgets/sse_display.dart | 135 +++++---- .../genai/lib/models/ai_request_model.dart | 2 +- packages/genai/lib/widgets/llm_selector.dart | 261 ------------------ 4 files changed, 86 insertions(+), 316 deletions(-) delete mode 100644 packages/genai/lib/widgets/llm_selector.dart diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index b4c75dfa..6836abac 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/foundation.dart'; @@ -186,7 +188,7 @@ class _ResponseBodySuccessState extends State { padding: kP8, decoration: textContainerdecoration, child: SSEDisplay( - sseOutput: widget.sseOutput, + sseOutput: widget.sseOutput ?? [], ), ), ), diff --git a/lib/widgets/sse_display.dart b/lib/widgets/sse_display.dart index efa65f43..b1655a70 100644 --- a/lib/widgets/sse_display.dart +++ b/lib/widgets/sse_display.dart @@ -1,20 +1,31 @@ import 'dart:convert'; +import 'package:apidash/providers/collection_providers.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class SSEDisplay extends StatelessWidget { - final List? sseOutput; +class SSEDisplay extends ConsumerStatefulWidget { + final List sseOutput; const SSEDisplay({ super.key, - this.sseOutput, + required this.sseOutput, }); + @override + ConsumerState createState() => _SSEDisplayState(); +} + +class _SSEDisplayState extends ConsumerState { @override Widget build(BuildContext context) { + final requestModel = ref.read(selectedRequestModelProvider); + final aiRequestModel = requestModel?.aiRequestModel; + final isAIOutput = (aiRequestModel != null); + final theme = Theme.of(context); final fontSizeMedium = theme.textTheme.bodyMedium?.fontSize; final isDark = theme.brightness == Brightness.dark; - if (sseOutput == null || sseOutput!.isEmpty) { + if (widget.sseOutput.isEmpty) { return Text( 'No content', style: kCodeStyle.copyWith( @@ -24,59 +35,77 @@ class SSEDisplay extends StatelessWidget { ); } - return ListView( - padding: kP1, - children: sseOutput!.reversed.where((e) => e != '').map((chunk) { - Map? parsedJson; - try { - parsedJson = jsonDecode(chunk); - } catch (_) {} + if (isAIOutput) { + String out = ""; + for (String x in widget.sseOutput) { + x = x.substring(6); + out += aiRequestModel.model.provider.modelController + .streamOutputFormatter(jsonDecode(x)) ?? + ""; + } + return SingleChildScrollView( + child: Text(out), + ); + } - return Card( - color: theme.colorScheme.surfaceContainerLowest, - shape: RoundedRectangleBorder( - borderRadius: kBorderRadius6, - ), - child: Padding( - padding: kP8, - child: parsedJson != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: parsedJson.entries.map((entry) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${entry.key}: ', - style: kCodeStyle.copyWith( - fontSize: fontSizeMedium, - color: isDark ? kColorGQL.toDark : kColorGQL, - fontWeight: FontWeight.bold, + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: (widget.sseOutput) + .reversed + .where((e) => e != '') + .map((chunk) { + Map? parsedJson; + try { + parsedJson = jsonDecode(chunk); + } catch (_) {} + + return Card( + color: theme.colorScheme.surfaceContainerLowest, + shape: RoundedRectangleBorder( + borderRadius: kBorderRadius6, + ), + child: Padding( + padding: kP8, + child: parsedJson != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: parsedJson.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${entry.key}: ', + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + color: isDark ? kColorGQL.toDark : kColorGQL, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(width: 4), - Expanded( - child: Text( - entry.value.toString(), - style: kCodeStyle, + const SizedBox(width: 4), + Expanded( + child: Text( + entry.value.toString(), + style: kCodeStyle, + ), ), - ), - ], - ), - ); - }).toList(), - ) - : Text( - chunk.toString().trim(), - style: kCodeStyle.copyWith( - fontSize: fontSizeMedium, + ], + ), + ); + }).toList(), + ) + : Text( + chunk.toString().trim(), + style: kCodeStyle.copyWith( + fontSize: fontSizeMedium, + ), ), - ), - ), - ); - }).toList(), + ), + ); + }).toList(), + ), ); } } diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index f093dfc5..11c79d3d 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -32,7 +32,7 @@ class AIRequestModel with _$AIRequestModel { LLMRequestDetails createRequest() { final controller = model.provider.modelController; - return controller.createRequest(model, payload); + return controller.createRequest(model, payload, stream: true); } factory AIRequestModel.fromDefaultSaveObject(LLMSaveObject? defaultLLMSO) { diff --git a/packages/genai/lib/widgets/llm_selector.dart b/packages/genai/lib/widgets/llm_selector.dart deleted file mode 100644 index f53670d5..00000000 --- a/packages/genai/lib/widgets/llm_selector.dart +++ /dev/null @@ -1,261 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:genai/llm_provider.dart'; -import 'package:genai/llm_saveobject.dart'; -import 'package:genai/providers/ollama.dart'; - -class DefaultLLMSelectorButton extends StatelessWidget { - final LLMSaveObject? defaultLLM; - final Function(LLMSaveObject) onDefaultLLMUpdated; - const DefaultLLMSelectorButton({ - super.key, - this.defaultLLM, - required this.onDefaultLLMUpdated, - }); - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: () async { - final saveObject = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - scrollable: true, - content: DefaultLLMSelectorDialog(defaultLLM: defaultLLM), - contentPadding: EdgeInsets.all(10), - ); - }, - ); - if (saveObject == null) return; - onDefaultLLMUpdated(saveObject); - }, - child: Text(defaultLLM?.selectedLLM.modelName ?? 'Select Model'), - ); - } -} - -class DefaultLLMSelectorDialog extends StatefulWidget { - final LLMSaveObject? defaultLLM; - - const DefaultLLMSelectorDialog({super.key, this.defaultLLM}); - - @override - State createState() => - _DefaultLLMSelectorDialogState(); -} - -class _DefaultLLMSelectorDialogState extends State { - late LLMProvider selectedLLMProvider; - late LLMSaveObject llmSaveObject; - - @override - void initState() { - super.initState(); - - final oC = OllamaModelController().inputPayload; - - llmSaveObject = - widget.defaultLLM ?? - LLMSaveObject( - endpoint: oC.endpoint, - credential: '', - configMap: oC.configMap, - selectedLLM: LLMProvider.gemini.getLLMByIdentifier( - 'gemini-2.0-flash', - ), - provider: LLMProvider.ollama, - ); - - selectedLLMProvider = llmSaveObject.provider; - } - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - width: MediaQuery.of(context).size.width * 0.8, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Left panel - Provider List - Container( - width: 300, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Providers'), - const SizedBox(height: 10), - ...LLMProvider.values.map( - (provider) => ListTile( - title: Text(provider.displayName), - trailing: llmSaveObject.provider == provider - ? const CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ) - : null, - onTap: () { - final input = provider.modelController.inputPayload; - setState(() { - selectedLLMProvider = provider; - llmSaveObject = LLMSaveObject( - endpoint: input.endpoint, - credential: '', - configMap: input.configMap, - selectedLLM: provider.models.first, - provider: provider, - ); - }); - }, - ), - ), - ], - ), - ), - - const SizedBox(width: 40), - - // Right panel - Configuration and Save - Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - selectedLLMProvider.displayName, - style: const TextStyle(fontSize: 28), - ), - const SizedBox(height: 20), - - if (selectedLLMProvider != LLMProvider.ollama) ...[ - const Text('API Key / Credential'), - const SizedBox(height: 10), - BoundedTextField( - onChanged: (x) { - llmSaveObject.credential = x; - }, - value: llmSaveObject.credential, - ), - const SizedBox(height: 10), - ], - - const Text('Endpoint'), - const SizedBox(height: 10), - BoundedTextField( - key: ValueKey(llmSaveObject.provider), - onChanged: (x) => llmSaveObject.endpoint = x, - value: llmSaveObject.endpoint, - ), - - const SizedBox(height: 20), - const Text('Models'), - const SizedBox(height: 8), - - Container( - height: 300, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: const Color.fromARGB(27, 0, 0, 0), - ), - child: SingleChildScrollView( - child: Column( - children: selectedLLMProvider.models - .map( - (model) => ListTile( - title: Text(model.modelName), - subtitle: Text(model.identifier), - trailing: llmSaveObject.selectedLLM == model - ? const CircleAvatar( - radius: 5, - backgroundColor: Colors.green, - ) - : null, - onTap: () { - setState(() { - llmSaveObject.selectedLLM = model; - }); - }, - ), - ) - .toList(), - ), - ), - ), - const SizedBox(height: 10), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () { - llmSaveObject.provider = selectedLLMProvider; - Navigator.of(context).pop(llmSaveObject); - }, - child: const Text('Save Changes'), - ), - ), - ], - ), - ), - ], - ), - ); - } -} - -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 createState() => _BoundedTextFieldState(); -} - -class _BoundedTextFieldState extends State { - 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: BorderRadius.circular(8), - ), - 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, - ), - ), - ); - } -}