diff --git a/lib/main.dart b/lib/main.dart index 2b43ca94..675b0a54 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/genai.dart'; import 'models/models.dart'; import 'providers/providers.dart'; import 'services/services.dart'; @@ -9,6 +10,7 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + var settingsModel = await getSettingsFromSharedPrefs(); var onboardingStatus = await getOnboardingStatusFromSharedPrefs(); initializeJsRuntime(); @@ -23,6 +25,11 @@ void main() async { settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); } + //Load all LLM + LLMManager.fetchAvailableLLMs().then((_) { + LLMManager.loadAvailableLLMs().then((_) {}); + }); + runApp( ProviderScope( overrides: [ diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index a06b1e59..953746cf 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; +import 'package:genai/genai.dart'; @immutable class SettingsModel { @@ -18,6 +19,7 @@ class SettingsModel { this.workspaceFolderPath, this.isSSLDisabled = false, this.isDashBotEnabled = true, + this.defaultLLMSaveObject, }); final bool isDark; @@ -33,6 +35,7 @@ class SettingsModel { final String? workspaceFolderPath; final bool isSSLDisabled; final bool isDashBotEnabled; + final LLMSaveObject? defaultLLMSaveObject; SettingsModel copyWith({ bool? isDark, @@ -48,6 +51,8 @@ class SettingsModel { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + LLMSaveObject? def, + LLMSaveObject? defaultLLMSaveObject, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -65,6 +70,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject ?? this.defaultLLMSaveObject, ); } @@ -85,6 +91,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject, ); } @@ -141,6 +148,12 @@ class SettingsModel { final isSSLDisabled = data["isSSLDisabled"] as bool?; final isDashBotEnabled = data["isDashBotEnabled"] as bool?; + LLMSaveObject? defaultLLMSaveObject; + if (data["defaultLLMSaveObject"] != null) { + defaultLLMSaveObject = + LLMSaveObject.fromJSON(data["defaultLLMSaveObject"]); + } + const sm = SettingsModel(); return sm.copyWith( @@ -158,6 +171,7 @@ class SettingsModel { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject, ); } @@ -178,6 +192,7 @@ class SettingsModel { "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, "isDashBotEnabled": isDashBotEnabled, + 'defaultLLMSaveObject': defaultLLMSaveObject?.toJSON(), }; } @@ -203,7 +218,8 @@ class SettingsModel { other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && - other.isDashBotEnabled == isDashBotEnabled; + other.isDashBotEnabled == isDashBotEnabled && + other.defaultLLMSaveObject == defaultLLMSaveObject; } @override @@ -223,6 +239,7 @@ class SettingsModel { workspaceFolderPath, isSSLDisabled, isDashBotEnabled, + defaultLLMSaveObject, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index d3cb9f2f..3f9e39a8 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:genai/genai.dart'; import '../models/models.dart'; import '../services/services.dart'; import '../consts.dart'; @@ -34,6 +35,7 @@ class ThemeStateNotifier extends StateNotifier { String? workspaceFolderPath, bool? isSSLDisabled, bool? isDashBotEnabled, + LLMSaveObject? defaultLLMSaveObject, }) async { state = state.copyWith( isDark: isDark, @@ -49,6 +51,7 @@ class ThemeStateNotifier extends StateNotifier { workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, isDashBotEnabled: isDashBotEnabled, + defaultLLMSaveObject: defaultLLMSaveObject, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 606ef516..86d3a68c 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:genai/widgets/llm_selector.dart'; import '../providers/providers.dart'; import '../services/services.dart'; import '../utils/utils.dart'; @@ -114,6 +115,18 @@ class SettingsPage extends ConsumerWidget { }, ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('Default Large Language Model (LLM)'), + trailing: DefaultLLMSelectorButton( + defaultLLM: settings.defaultLLMSaveObject, + onDefaultLLMUpdated: (d) { + ref + .read(settingsProvider.notifier) + .update(defaultLLMSaveObject: d); + }, + ), + ), CheckboxListTile( title: const Text("Save Responses"), subtitle: diff --git a/packages/genai/lib/widgets/llm_selector.dart b/packages/genai/lib/widgets/llm_selector.dart new file mode 100644 index 00000000..ddb67c04 --- /dev/null +++ b/packages/genai/lib/widgets/llm_selector.dart @@ -0,0 +1,268 @@ +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 Row( + mainAxisSize: MainAxisSize.min, + children: [ + Chip(label: Text(defaultLLM?.selectedLLM.modelName ?? 'none')), + SizedBox(height: 10), + IconButton( + 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); + }, + icon: Icon(Icons.edit), + ), + ], + ); + } +} + +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, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0536f2ac..81107934 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -702,6 +702,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.32.1" + genai: + dependency: "direct main" + description: + path: "packages/genai" + relative: true + source: path + version: "0.0.1" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ef4fae7..4e40b9b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: path: packages/apidash_core apidash_design_system: path: packages/apidash_design_system + genai: + path: packages/genai carousel_slider: ^5.0.0 code_builder: ^4.10.0 csv: ^6.0.0