From 3f0331120cdf891609b9f6c56d2b2170fe0ef814 Mon Sep 17 00:00:00 2001 From: Patrick Niemeyer Date: Tue, 17 Dec 2024 16:11:44 -0600 Subject: [PATCH] gai: UI supports scripting extensions. --- .../lib/chat/api/user_preferences_chat.dart | 148 +++++++++ gai-frontend/lib/chat/auth_dialog.dart | 74 +++-- gai-frontend/lib/chat/chat.dart | 107 ++++--- .../lib/chat/chat_settings_button.dart | 295 +++++++++--------- .../lib/chat/identicon_options_menu_item.dart | 87 ++++++ .../lib/chat/scripting/chat_scripting.dart | 38 ++- .../code_viewer/script_code_box.dart | 78 +++++ .../code_viewer/scripts_menu_item.dart | 99 ++++++ .../code_viewer/test_code_field.dart | 112 +++++++ .../chat/scripting/code_viewer/themes.dart | 187 +++++++++++ .../code_viewer/user_script_dialog.dart | 144 +++++++++ gai-frontend/pubspec.lock | 40 +++ gai-frontend/pubspec.yaml | 4 + .../menu/orchid_popup_menu_item_utils.dart | 2 +- gui-orchid/pubspec.lock | 32 ++ 15 files changed, 1192 insertions(+), 255 deletions(-) create mode 100644 gai-frontend/lib/chat/api/user_preferences_chat.dart create mode 100644 gai-frontend/lib/chat/identicon_options_menu_item.dart create mode 100644 gai-frontend/lib/chat/scripting/code_viewer/script_code_box.dart create mode 100644 gai-frontend/lib/chat/scripting/code_viewer/scripts_menu_item.dart create mode 100644 gai-frontend/lib/chat/scripting/code_viewer/test_code_field.dart create mode 100644 gai-frontend/lib/chat/scripting/code_viewer/themes.dart create mode 100644 gai-frontend/lib/chat/scripting/code_viewer/user_script_dialog.dart diff --git a/gai-frontend/lib/chat/api/user_preferences_chat.dart b/gai-frontend/lib/chat/api/user_preferences_chat.dart new file mode 100644 index 000000000..506e923d4 --- /dev/null +++ b/gai-frontend/lib/chat/api/user_preferences_chat.dart @@ -0,0 +1,148 @@ +import 'package:orchid/api/preferences/observable_preference.dart'; +import 'package:orchid/api/preferences/user_preferences.dart'; + +class UserScript { + // A user-entered name for the script entry. + final String name; + + // The script body, or, if script begins with 'https' a URL from which to fetch the script. + final String script; + + // Whether the script is selected for use. + final bool selected; + + UserScript(this.name, this.script, this.selected); + + UserScript.fromJson(Map json) + : name = json['name'], + script = json['script'], + selected = json['selected']; + + Map toJson() => { + 'name': name, + 'script': script, + 'selected': selected, + }; + + @override + String toString() { + return 'UserScript{name: $name, script: $script, selected: $selected}'; + } +} + +class UserPreferencesScripts { + static final UserPreferencesScripts _singleton = UserPreferencesScripts._internal(); + + factory UserPreferencesScripts() { + return _singleton; + } + + UserPreferencesScripts._internal(); + + // Temporary implementation for single script management. + final userScript = ObservableStringPreference(_UserPreferenceKeys.Script); + final userScriptEnabled = ObservableBoolPreference(_UserPreferenceKeys.ScriptEnabled); + + /* + ObservablePreference> keys = ObservablePreference( + key: _UserPreferenceKeys.Scripts, + getValue: (key) { + return _getScripts(); + }, + putValue: (key, keys) { + return _setKeys(keys); + }); + + static List _getScripts() { + + String? value = + UserPreferences().getStringForKey(_UserPreferenceKeyKeys.Keys); + if (value == null) { + return []; + } + try { + var jsonList = jsonDecode(value) as List; + return jsonList + .map((el) { + try { + return StoredEthereumKey.fromJson(el); + } catch (err) { + log("Error decoding key: $err"); + return null; + } + }) + .whereType() + .toList(); + } catch (err) { + log("Error retrieving keys!: $value, $err"); + return []; + } + } + + static Future _setKeys(List? keys) async { + print("setKeys: storing keys: ${jsonEncode(keys)}"); + if (keys == null) { + return UserPreferences() + .sharedPreferences() + .remove(_UserPreferenceKeyKeys.Keys.toString()); + } + try { + var value = jsonEncode(keys); + return await UserPreferences() + .putStringForKey(_UserPreferenceKeyKeys.Keys, value); + } catch (err) { + log("Error storing keys!: $err"); + return false; + } + } + + /// Remove a key from the user's keystore. + Future removeKey(StoredEthereumKeyRef keyRef) async { + var keysList = ((keys.get()) ?? []); + try { + keysList.removeWhere((key) => key.uid == keyRef.keyUid); + } catch (err) { + log("account: error removing key: $keyRef"); + return false; + } + await keys.set(keysList); + return true; + } + + /// Add a key to the user's keystore if it does not already exist. + Future addKey(StoredEthereumKey key) async { + return addKeyIfNeeded(key); + } + + /// Add a key to the user's keystore if it does not already exist. + Future addKeyIfNeeded(StoredEthereumKey key) async { + log("XXX: addKeyIfNeeded: add key if needed: $key"); + var curKeys = keys.get() ?? []; + if (!curKeys.contains(key)) { + log("XXX: addKeyIfNeeded: adding key"); + await keys.set(curKeys + [key]); + } else { + log("XXX: addKeyIfNeeded: duplicate key"); + } + } + + /// Add a list of keys to the user's keystore. + Future addKeys(List newKeys) async { + var allKeys = ((keys.get()) ?? []) + newKeys; + await keys.set(allKeys); + } + + */ + +/// +/// End: Keys +/// + + +} + +enum _UserPreferenceKeys implements UserPreferenceKey { + Script, + ScriptEnabled, + // Scripts, +} diff --git a/gai-frontend/lib/chat/auth_dialog.dart b/gai-frontend/lib/chat/auth_dialog.dart index 80d283515..bff68fe48 100644 --- a/gai-frontend/lib/chat/auth_dialog.dart +++ b/gai-frontend/lib/chat/auth_dialog.dart @@ -109,47 +109,45 @@ class _AuthDialogState extends State { return SizedBox( key: ValueKey(widget.accountDetailNotifier?.hashCode ?? 'key'), width: 500, - child: IntrinsicHeight( - child: ValueListenableBuilder( - valueListenable: widget.accountDetailNotifier, - builder: (BuildContext context, AccountDetail? accountDetail, - Widget? child) { - return OrchidTitledPanel( - highlight: false, - opaque: true, - titleText: "Connect to Provider", - onDismiss: () { - Navigator.pop(context); - }, - body: DefaultTabController( - length: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const TabBar( - tabs: [ - Tab(text: 'Orchid Account'), - Tab(text: 'Auth Token'), + child: ValueListenableBuilder( + valueListenable: widget.accountDetailNotifier, + builder: (BuildContext context, AccountDetail? accountDetail, + Widget? child) { + return OrchidTitledPanel( + highlight: false, + opaque: true, + titleText: "Connect to Provider", + onDismiss: () { + Navigator.pop(context); + }, + body: DefaultTabController( + length: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const TabBar( + tabs: [ + Tab(text: 'Orchid Account'), + Tab(text: 'Auth Token'), + ], + labelColor: Colors.white, + unselectedLabelColor: Colors.white60, + indicatorColor: Colors.white, + ), + SizedBox( + height: 560, + child: TabBarView( + children: [ + _buildOrchidAccountTab(accountDetail), + _buildAuthTokenTab(), ], - labelColor: Colors.white, - unselectedLabelColor: Colors.white60, - indicatorColor: Colors.white, ), - SizedBox( - height: 500, - child: TabBarView( - children: [ - _buildOrchidAccountTab(accountDetail), - _buildAuthTokenTab(), - ], - ), - ), - ], - ), + ), + ], ), - ); - }), - ), + ), + ); + }), ); } diff --git a/gai-frontend/lib/chat/chat.dart b/gai-frontend/lib/chat/chat.dart index d5c4c371a..6e17a0d90 100644 --- a/gai-frontend/lib/chat/chat.dart +++ b/gai-frontend/lib/chat/chat.dart @@ -2,9 +2,11 @@ import 'package:orchid/api/orchid_eth/chains.dart'; import 'package:orchid/api/orchid_eth/orchid_account.dart'; import 'package:orchid/api/orchid_eth/orchid_account_detail.dart'; import 'package:orchid/api/orchid_keys.dart'; +import 'package:orchid/chat/api/user_preferences_chat.dart'; import 'package:orchid/chat/model.dart'; import 'package:orchid/chat/provider_connection.dart'; import 'package:orchid/chat/scripting/chat_scripting.dart'; +import 'package:orchid/chat/scripting/code_viewer/user_script_dialog.dart'; import 'package:orchid/common/app_sizes.dart'; import 'package:orchid/chat/chat_settings_button.dart'; import 'package:orchid/orchid/field/orchid_labeled_numeric_field.dart'; @@ -87,20 +89,21 @@ class _ChatViewState extends State { log('Error initializing from params: $e, $stack'); } - // Initialize scripting extension - /* + // final script = UserPreferencesScripts().userScript.get(); + // log('User script on start: $script'); + + // Initialize the scripting extension mechanism ChatScripting.init( - // url: 'lib/extensions/test.js', - // url: 'lib/extensions/party_mode.js', - url: 'lib/extensions/filter_example.js', - debugMode: true, + // url: 'lib/extensions/filter_example.js', + // debugMode: true, + script: UserPreferencesScripts().userScript.get(), + // Not: script overrides url providerManager: _providerManager, modelManager: _modelManager, getUserSelectedModels: () => _userSelectedModels, chatHistory: _chatHistory, addChatMessageToUI: _addChatMessage, ); - */ } bool get _connected { @@ -317,6 +320,7 @@ class _ChatViewState extends State { // Debug hack if (_userSelectedModelIds.isEmpty && ChatScripting.enabled && + (UserPreferencesScripts().userScriptEnabled.get() ?? false) && ChatScripting.instance.debugMode) { setState(() { _userSelectedModelIds = ['gpt-4o']; @@ -338,7 +342,8 @@ class _ChatViewState extends State { // FocusManager.instance.primaryFocus?.unfocus(); // ? // If we have a script selected allow it to handle the prompt - if (ChatScripting.enabled) { + if (ChatScripting.enabled && + (UserPreferencesScripts().userScriptEnabled.get() ?? false)) { ChatScripting.instance.sendUserPrompt(msg, _userSelectedModels); } else { _sendUserPromptDefaultBehavior(msg); @@ -404,15 +409,6 @@ class _ChatViewState extends State { }); } - List> buildMenu(BuildContext context) { - return >[ - const PopupMenuItem( - value: OrchataMenuItem.debug, - child: Text('Debug'), - ) - ]; - } - void _clearChat() { setState(() { _chatHistory.clear(); @@ -554,7 +550,6 @@ class _ChatViewState extends State { Widget _buildHeaderRow({required bool showIcons}) { const buttonHeight = 40.0; - const settingsIconSize = buttonHeight * 1.5; return Row( children: [ @@ -596,41 +591,49 @@ class _ChatViewState extends State { ).left(8), // Settings button - SizedBox( - width: settingsIconSize, - height: buttonHeight, - child: Center( - child: ChatSettingsButton( - debugMode: _debugMode, - multiSelectMode: _multiSelectMode, - partyMode: _partyMode, - onDebugModeChanged: () { - setState(() { - _debugMode = !_debugMode; - }); - }, - onMultiSelectModeChanged: () { - setState(() { - _multiSelectMode = !_multiSelectMode; - // Reset selections when toggling modes - _userSelectedModelIds = []; - }); - }, - onPartyModeChanged: () { - setState(() { - _partyMode = !_partyMode; - if (_partyMode) { - _multiSelectMode = true; - } - }); - }, - onClearChat: _clearChat, - ), - ), - ).left(8), + _buildSettingsButton(buttonHeight).left(8), ], ); } + + SizedBox _buildSettingsButton(double buttonHeight) { + final settingsIconSize = buttonHeight * 1.5; + return SizedBox( + width: settingsIconSize, + height: buttonHeight, + child: Center( + child: ChatSettingsButton( + debugMode: _debugMode, + multiSelectMode: _multiSelectMode, + partyMode: _partyMode, + onDebugModeChanged: () { + setState(() { + _debugMode = !_debugMode; + }); + }, + onMultiSelectModeChanged: () { + setState(() { + _multiSelectMode = !_multiSelectMode; + // Reset selections when toggling modes + _userSelectedModelIds = []; + }); + }, + onPartyModeChanged: () { + setState(() { + _partyMode = !_partyMode; + if (_partyMode) { + _multiSelectMode = true; + } + }); + }, + onClearChat: _clearChat, + editUserScript: () { + UserScriptDialog.show(context); + }, + ), + ), + ); + } } Future _launchURL(String urlString) async { @@ -639,7 +642,3 @@ Future _launchURL(String urlString) async { throw 'Could not launch $url'; } } - -enum AuthTokenMethod { manual, walletConnect } - -enum OrchataMenuItem { debug } diff --git a/gai-frontend/lib/chat/chat_settings_button.dart b/gai-frontend/lib/chat/chat_settings_button.dart index 95d8aadd6..15a3a76c5 100644 --- a/gai-frontend/lib/chat/chat_settings_button.dart +++ b/gai-frontend/lib/chat/chat_settings_button.dart @@ -1,7 +1,8 @@ +import 'package:orchid/chat/identicon_options_menu_item.dart'; +import 'package:orchid/chat/scripting/code_viewer/scripts_menu_item.dart'; import 'package:orchid/orchid/orchid.dart'; import 'package:orchid/api/orchid_language.dart'; import 'package:orchid/api/preferences/user_preferences_ui.dart'; -import 'package:orchid/orchid/menu/expanding_popup_menu_item.dart'; import 'package:orchid/orchid/menu/orchid_popup_menu_item_utils.dart'; import 'package:orchid/orchid/menu/submenu_popup_menu_item.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -15,6 +16,7 @@ class ChatSettingsButton extends StatefulWidget { final VoidCallback onMultiSelectModeChanged; final VoidCallback onPartyModeChanged; final VoidCallback onClearChat; + final VoidCallback editUserScript; const ChatSettingsButton({ Key? key, @@ -25,6 +27,7 @@ class ChatSettingsButton extends StatefulWidget { required this.onMultiSelectModeChanged, required this.onPartyModeChanged, required this.onClearChat, + required this.editUserScript, }) : super(key: key); @override @@ -44,130 +47,159 @@ class _ChatSettingsButtonState extends State { final githubUrl = 'https://github.com/OrchidTechnologies/orchid/tree/$buildCommit/web-ethereum/dapp2'; - return Center( - child: OrchidPopupMenuButton( - width: 30, - height: 30, - selected: _buttonSelected, - backgroundColor: Colors.transparent, - onSelected: (item) { - setState(() { - _buttonSelected = false; - }); - }, - onCanceled: () { - setState(() { - _buttonSelected = false; - }); - }, - itemBuilder: (itemBuilderContext) { - setState(() { - _buttonSelected = true; - }); + return OrchidPopupMenuButton( + width: 30, + height: 30, + selected: _buttonSelected, + backgroundColor: Colors.transparent, + onSelected: (item) { + setState(() { + _buttonSelected = false; + }); + }, + onCanceled: () { + setState(() { + _buttonSelected = false; + }); + }, + itemBuilder: (itemBuilderContext) { + setState(() { + _buttonSelected = true; + }); - const div = PopupMenuDivider(height: 1.0); - return [ - // Clear chat - PopupMenuItem( - onTap: widget.onClearChat, - height: _height, - child: SizedBox( - width: _width, - child: Text("Clear Chat", style: _textStyle), + const div = PopupMenuDivider(height: 1.0); + return [ + // Clear chat + PopupMenuItem( + onTap: widget.onClearChat, + height: _height, + child: SizedBox( + width: _width, + child: Text("Clear Chat", style: _textStyle), + ), + ), + div, + + // debug mode + PopupMenuItem( + onTap: widget.onDebugModeChanged, + height: _height, + child: SizedBox( + width: _width, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Debug Mode", style: _textStyle), + Icon( + widget.debugMode + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.white, + ), + ], ), ), - div, - // debug mode - PopupMenuItem( - onTap: widget.onDebugModeChanged, - height: _height, - child: SizedBox( - width: _width, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Debug Mode", style: _textStyle), - Icon( - widget.debugMode - ? Icons.check_box_outlined - : Icons.check_box_outline_blank, - color: Colors.white, - ), - ], - ), + ), + div, + + // multi-select mode + PopupMenuItem( + onTap: widget.onMultiSelectModeChanged, + height: _height, + child: SizedBox( + width: _width, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Multi-Model Mode", style: _textStyle), + Icon( + widget.multiSelectMode + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.white, + ), + ], ), ), - div, - // multi-select mode - PopupMenuItem( - onTap: widget.onMultiSelectModeChanged, - height: _height, - child: SizedBox( - width: _width, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Multi-Model Mode", style: _textStyle), - Icon( - widget.multiSelectMode - ? Icons.check_box_outlined - : Icons.check_box_outline_blank, - color: Colors.white, - ), - ], - ), + ), + div, + + /* + // party mode + PopupMenuItem( + onTap: widget.onPartyModeChanged, + height: _height, + child: SizedBox( + width: _width, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Party Mode", style: _textStyle), + Icon( + widget.partyMode + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.white, + ), + ], ), ), - div, - // party mode - PopupMenuItem( - onTap: widget.onPartyModeChanged, + ), + div, + */ + + // scripts + SubmenuPopopMenuItemBuilder( + builder: (bool expanded) => ScriptsMenuItem( height: _height, - child: SizedBox( - width: _width, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("Party Mode", style: _textStyle), - Icon( - widget.partyMode - ? Icons.check_box_outlined - : Icons.check_box_outline_blank, - color: Colors.white, - ), - ], - ), - ), + width: _width, + expanded: expanded, + textStyle: _textStyle, + editScript: () { + log('edit script'); + Navigator.pop(context); + widget.editUserScript(); + }, ), - div, - SubmenuPopopMenuItemBuilder( - builder: _buildIdenticonsPref, + ), + div, + + // identicon style menu + SubmenuPopopMenuItemBuilder( + builder: (bool expanded) => IdenticonOptionsMenuItem( + expanded: expanded, + textStyle: _textStyle, ), - div, - PopupMenuItem( - onTap: () async { - launchUrlString(githubUrl); - }, - height: _height, - child: SizedBox( - width: _width, - child: Text('Version: $buildCommit', style: _textStyle), - ), + ), + div, + + // build version + PopupMenuItem( + onTap: () async { + launchUrlString(githubUrl); + }, + height: _height, + child: SizedBox( + width: _width, + child: Text('Version: $buildCommit', style: _textStyle), ), - ]; - }, - child: SizedBox( - width: 30, - height: 30, - child: FittedBox( - fit: BoxFit.contain, - child: OrchidAsset.svg.settings_gear, ), + ]; + }, + + // settings icon + child: SizedBox( + width: 30, + height: 30, + child: FittedBox( + fit: BoxFit.contain, + child: OrchidAsset.svg.settings_gear, ), ), ); } + /* Future _openLicensePage(BuildContext context) { // TODO: return Future.delayed(millis(100), () async {}); @@ -185,6 +217,7 @@ class _ChatSettingsButtonState extends State { ); }); } + */ Widget _languageOptions(String? selected) { var items = OrchidLanguage.languages.keys @@ -200,7 +233,7 @@ class _ChatSettingsButtonState extends State { .toList() .cast() .separatedWith( - PopupMenuDivider(height: 1.0), + const PopupMenuDivider(height: 1.0), ); items.insert( @@ -219,50 +252,6 @@ class _ChatSettingsButtonState extends State { ); } - Widget _buildIdenticonsPref(bool expanded) { - return UserPreferencesUI().useBlockiesIdenticons.builder( - (useBlockies) { - if (useBlockies == null) { - return Container(); - } - return ExpandingPopupMenuItem( - expanded: expanded, - title: s.identiconStyle, - currentSelectionText: - (!expanded ? (useBlockies ? s.blockies : s.jazzicon) : ''), - expandedContent: _identiconOptions(useBlockies), - expandedHeight: 108, - textStyle: _textStyle, - ); - }, - ); - } - - Widget _identiconOptions(bool useBlockies) { - final pref = UserPreferencesUI().useBlockiesIdenticons; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - PopupMenuDivider(height: 1.0), - _listMenuItem( - selected: useBlockies, - title: s.blockies, - onTap: () async { - await pref.set(true); - }, - ), - PopupMenuDivider(height: 1.0), - _listMenuItem( - selected: !useBlockies, - title: s.jazzicon, - onTap: () async { - await pref.set(false); - }, - ), - ], - ); - } - PopupMenuItem _listMenuItem({ required bool selected, required String title, diff --git a/gai-frontend/lib/chat/identicon_options_menu_item.dart b/gai-frontend/lib/chat/identicon_options_menu_item.dart new file mode 100644 index 000000000..da462454f --- /dev/null +++ b/gai-frontend/lib/chat/identicon_options_menu_item.dart @@ -0,0 +1,87 @@ +import 'package:orchid/orchid/menu/submenu_popup_menu_item.dart'; +import 'package:orchid/orchid/orchid.dart'; +import 'package:orchid/api/preferences/user_preferences_ui.dart'; +import 'package:orchid/orchid/menu/expanding_popup_menu_item.dart'; +import 'package:orchid/orchid/menu/orchid_popup_menu_item_utils.dart'; + +// TODO: Port this back to the orchid lib for the account dapp, etc. +// Note: so much boilerplate for these.. +class IdenticonOptionsMenuItem extends StatelessWidget { + const IdenticonOptionsMenuItem({ + super.key, + required TextStyle textStyle, + required this.expanded, + }) : _textStyle = textStyle; + + final TextStyle _textStyle; + final bool expanded; + + @override + Widget build(BuildContext context) { + return UserPreferencesUI().useBlockiesIdenticons.builder( + (useBlockies) { + if (useBlockies == null) { + return Container(); + } + return ExpandingPopupMenuItem( + expanded: expanded, + title: context.s.identiconStyle, + currentSelectionText: + (!expanded ? (useBlockies ? context.s.blockies : context.s.jazzicon) : ''), + expandedContent: _identiconOptions(context, useBlockies), + expandedHeight: 108, + textStyle: _textStyle, + ); + }, + ); + } + + Widget _identiconOptions(BuildContext context, bool useBlockies) { + final pref = UserPreferencesUI().useBlockiesIdenticons; + // const checkmark = '\u2713'; + // String check(bool checked) => checked ? ' $checkmark ' : ' '; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const PopupMenuDivider(height: 1.0), + _listMenuItem( + context: context, + selected: useBlockies, + title: context.s.blockies, + // selected: false, + // title: check(useBlockies) + context.s.blockies, + onTap: () async { + await pref.set(true); + }, + ), + const PopupMenuDivider(height: 1.0), + _listMenuItem( + context: context, + selected: !useBlockies, + title: context.s.jazzicon, + // selected: false, + // title: check(!useBlockies) + context.s.jazzicon, + onTap: () async { + await pref.set(false); + }, + ), + ], + ); + } + + PopupMenuItem _listMenuItem({ + required BuildContext context, + required bool selected, + required String title, + required VoidCallback onTap, + }) { + return OrchidPopupMenuItemUtils.listMenuItem( + context: context, + selected: selected, + title: title, + onTap: onTap, + textStyle: _textStyle, + ); + } +} diff --git a/gai-frontend/lib/chat/scripting/chat_scripting.dart b/gai-frontend/lib/chat/scripting/chat_scripting.dart index 41dcf9304..b4f3606da 100644 --- a/gai-frontend/lib/chat/scripting/chat_scripting.dart +++ b/gai-frontend/lib/chat/scripting/chat_scripting.dart @@ -25,7 +25,7 @@ class ChatScripting { static bool get enabled => _instance != null; // Scripting State - late String script; + String? script; late ProviderManager providerManager; late ModelManager modelManager; late List Function() getUserSelectedModels; @@ -34,8 +34,11 @@ class ChatScripting { late bool debugMode; static Future init({ + // A script + String? script, + // The URL from which to load the script - required String url, + String? url, // If debugMode is true, the script will be re-loaded before each invocation bool debugMode = false, @@ -57,15 +60,29 @@ class ChatScripting { instance.getUserSelectedModels = getUserSelectedModels; instance.addChatMessageToUI = addChatMessageToUI; - // Install persistent callback functions - addGlobalBindings(); + if (url != null) { + // Install persistent callback functions + final script = await instance.loadScriptFromURL(url); + instance.setScript(script); + } - await instance.loadExtensionScript(url); + if (script != null) { + instance.setScript(script); + } + } + + // Set the script into the environment + void setScript(String newScript) { + // init the global bindings once, when we have a script + if (script == null) { + addGlobalBindings(); + } + script = newScript; // Do one setup and evaluation of the script now - instance.evalExtensionScript(); + evalExtensionScript(); } - Future loadExtensionScript(String url) async { + Future loadScriptFromURL(String url) async { // Load the script from the URL as a string log("Loading script from $url"); final response = await http.get(Uri.parse(url)); @@ -81,15 +98,18 @@ class ChatScripting { "Failed to load script from $url: HTML response: ${response.body.truncate(64)}"); } - script = response.body; // log("Loaded script: $script"); + return response.body; } void evalExtensionScript() { // Wrap the script in an async function to allow top level await without messing with modules. // final wrappedScript = "(async () => {$script})();"; try { - evaluateJS(script); // We could get a result back async here if needed + if (script == null) { + throw Exception("No script to evaluate."); + } + evaluateJS(script!); // We could get a result back async here if needed } catch (e, stack) { log("Failed to evaluate script: $e"); log(stack.toString()); diff --git a/gai-frontend/lib/chat/scripting/code_viewer/script_code_box.dart b/gai-frontend/lib/chat/scripting/code_viewer/script_code_box.dart new file mode 100644 index 000000000..16cfa9929 --- /dev/null +++ b/gai-frontend/lib/chat/scripting/code_viewer/script_code_box.dart @@ -0,0 +1,78 @@ +import 'package:orchid/orchid/orchid.dart'; +import 'themes.dart'; +import 'package:code_text_field/code_text_field.dart'; +import 'package:highlight/languages/all.dart'; + +class UserScriptEditor extends StatefulWidget { + final String language; + final String theme; + final String? initialScript; + final Function(String?) onScriptChanged; + + const UserScriptEditor({ + Key? key, + required this.language, + required this.theme, + required this.initialScript, + required this.onScriptChanged, + }) : super(key: key); + + @override + _UserScriptEditorState createState() => _UserScriptEditorState(); +} + +class _UserScriptEditorState extends State { + CodeController? _codeController; + + @override + void initState() { + super.initState(); + + _codeController = CodeController( + text: widget.initialScript, + patternMap: { + r"\B#[a-zA-Z0-9]+\b": const TextStyle(color: Colors.red), + r"\B@[a-zA-Z0-9]+\b": const TextStyle( + fontWeight: FontWeight.w800, + color: Colors.blue, + ), + r"\B![a-zA-Z0-9]+\b": + const TextStyle(color: Colors.yellow, fontStyle: FontStyle.italic), + }, + stringMap: { + "bev": const TextStyle(color: Colors.indigo), + }, + language: allLanguages[widget.language], + ); + } + + @override + void dispose() { + _codeController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final styles = THEMES[widget.theme]; + + if (styles == null) { + return _buildCodeField(); + } + + return CodeTheme( + data: CodeThemeData(styles: styles), + child: _buildCodeField(), + ); + } + + Widget _buildCodeField() { + return CodeField( + controller: _codeController!, + textStyle: const TextStyle(fontFamily: 'SourceCode'), + onChanged: (text) { + widget.onScriptChanged(text); + }, + ); + } +} diff --git a/gai-frontend/lib/chat/scripting/code_viewer/scripts_menu_item.dart b/gai-frontend/lib/chat/scripting/code_viewer/scripts_menu_item.dart new file mode 100644 index 000000000..7b432d583 --- /dev/null +++ b/gai-frontend/lib/chat/scripting/code_viewer/scripts_menu_item.dart @@ -0,0 +1,99 @@ +import 'package:orchid/chat/api/user_preferences_chat.dart'; +import 'package:orchid/common/app_text.dart'; +import 'package:orchid/common/rounded_rect.dart'; +import 'package:orchid/orchid/orchid.dart'; +import 'package:orchid/orchid/menu/expanding_popup_menu_item.dart'; + +class ScriptsMenuItem extends StatelessWidget { + final double height; + final double width; + final TextStyle _textStyle; + final bool expanded; + final VoidCallback editScript; + + const ScriptsMenuItem({ + super.key, + required TextStyle textStyle, + required this.expanded, + required this.height, + required this.width, + required this.editScript, + }) : _textStyle = textStyle; + + @override + Widget build(BuildContext context) { + return UserPreferencesScripts().userScriptEnabled.builder( + (bool? scriptEnabled) { + if (scriptEnabled == null) { + return Container(); + } + final scriptInitialized = + UserPreferencesScripts().userScript.get() != null; + return ExpandingPopupMenuItem( + expanded: expanded, + title: "User Script", + currentSelectionText: + (!expanded ? (scriptEnabled ? "Enabled" : '') : ''), + expandedContent: _scriptOptions(context, scriptEnabled), + expandedHeight: (scriptInitialized ? 120 : height), + textStyle: _textStyle, + ); + }, + ); + } + + Widget _scriptOptions(BuildContext context, bool scriptEnabled) { + final enabledPref = UserPreferencesScripts().userScriptEnabled; + final script = UserPreferencesScripts().userScript.get(); + final bool hasScript = script != null && script.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // show one line of the script + if (hasScript) + Container( + color: OrchidColors.dark_background, + child: Text( + script.trim().truncate(36), + textAlign: TextAlign.left, + maxLines: 1, + style: AppText.logStyle.white, + ).pad(4), + ), + + // TODO: Make a checked item widget for this and backport to the other dapps + // Script enabled checkbox + if (hasScript) + PopupMenuItem( + onTap: () { + enabledPref.set(!scriptEnabled); + }, + height: height, + child: SizedBox( + width: width, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Enabled", style: _textStyle), + Icon( + scriptEnabled + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.white, + ), + ], + ), + ), + ), + // div, + + // Edit link + Text(hasScript ? "Edit Script" : "Add Script").linkButton( + onTapped: editScript, + style: OrchidText.body1.blueHightlight, + ), + ], + ); + } +} diff --git a/gai-frontend/lib/chat/scripting/code_viewer/test_code_field.dart b/gai-frontend/lib/chat/scripting/code_viewer/test_code_field.dart new file mode 100644 index 000000000..f10db4cbf --- /dev/null +++ b/gai-frontend/lib/chat/scripting/code_viewer/test_code_field.dart @@ -0,0 +1,112 @@ +import 'package:orchid/chat/scripting/code_viewer/script_code_box.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Code field', + theme: ThemeData( + primarySwatch: Colors.blueGrey, + ), + home: HomePage(), + ); + } +} + +class HomePage extends StatefulWidget { + HomePage({Key? key}) : super(key: key); + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Future _launchInBrowser(String url) async { + if (await canLaunch(url)) { + await launch( + url, + forceSafariVC: false, + forceWebView: false, + headers: {'my_header_key': 'my_header_value'}, + ); + } else { + throw 'Could not launch $url'; + } + } + + @override + Widget build(BuildContext context) { + final preset = [ + "dart|monokai-sublime", + "python|atom-one-dark", + "cpp|an-old-hope", + "java|a11y-dark", + "javascript|vs", + ]; + const exampleCode = "x: int = 42;"; + List children = preset.map((e) { + final parts = e.split('|'); + print(parts); + final box = UserScriptEditor( + language: parts[0], + theme: parts[1], + initialScript: exampleCode, + onScriptChanged: (script) { }, + ); + return Padding( + padding: const EdgeInsets.only(bottom: 32.0), + child: box, + ); + }).toList(); + final page = Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 900), + padding: const EdgeInsets.symmetric(vertical: 32.0), + child: Column(children: children), + ), + ); + + return Scaffold( + backgroundColor: const Color(0xFF363636), + appBar: AppBar( + backgroundColor: const Color(0xff23241f), + title: const Text("CodeField demo"), + // title: Text("Recursive Fibonacci"), + centerTitle: false, + actions: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + // primary: Colors.white, + ), + icon: const Icon(FontAwesomeIcons.github), + onPressed: () => + _launchInBrowser("https://github.com/BertrandBev/code_field"), + label: const Text("GITHUB"), + ), + const SizedBox(width: 8.0), + ], + ), + body: SingleChildScrollView(child: page), + // body: CodeEditor5(), + ); + } +} \ No newline at end of file diff --git a/gai-frontend/lib/chat/scripting/code_viewer/themes.dart b/gai-frontend/lib/chat/scripting/code_viewer/themes.dart new file mode 100644 index 000000000..76602d6ad --- /dev/null +++ b/gai-frontend/lib/chat/scripting/code_viewer/themes.dart @@ -0,0 +1,187 @@ + +// +// Source: https://github.com/BertrandBev/code_field/tree/master/example/lib +// +import 'package:flutter_highlight/themes/a11y-dark.dart'; +import 'package:flutter_highlight/themes/a11y-light.dart'; +import 'package:flutter_highlight/themes/agate.dart'; +import 'package:flutter_highlight/themes/an-old-hope.dart'; +import 'package:flutter_highlight/themes/androidstudio.dart'; +import 'package:flutter_highlight/themes/arduino-light.dart'; +import 'package:flutter_highlight/themes/arta.dart'; +import 'package:flutter_highlight/themes/ascetic.dart'; +import 'package:flutter_highlight/themes/atelier-cave-dark.dart'; +import 'package:flutter_highlight/themes/atelier-cave-light.dart'; +import 'package:flutter_highlight/themes/atelier-dune-dark.dart'; +import 'package:flutter_highlight/themes/atelier-dune-light.dart'; +import 'package:flutter_highlight/themes/atelier-estuary-dark.dart'; +import 'package:flutter_highlight/themes/atelier-estuary-light.dart'; +import 'package:flutter_highlight/themes/atelier-forest-dark.dart'; +import 'package:flutter_highlight/themes/atelier-forest-light.dart'; +import 'package:flutter_highlight/themes/atelier-heath-dark.dart'; +import 'package:flutter_highlight/themes/atelier-heath-light.dart'; +import 'package:flutter_highlight/themes/atelier-lakeside-dark.dart'; +import 'package:flutter_highlight/themes/atelier-lakeside-light.dart'; +import 'package:flutter_highlight/themes/atelier-plateau-dark.dart'; +import 'package:flutter_highlight/themes/atelier-plateau-light.dart'; +import 'package:flutter_highlight/themes/atelier-savanna-dark.dart'; +import 'package:flutter_highlight/themes/atelier-savanna-light.dart'; +import 'package:flutter_highlight/themes/atelier-seaside-dark.dart'; +import 'package:flutter_highlight/themes/atelier-seaside-light.dart'; +import 'package:flutter_highlight/themes/atelier-sulphurpool-dark.dart'; +import 'package:flutter_highlight/themes/atelier-sulphurpool-light.dart'; +import 'package:flutter_highlight/themes/atom-one-dark-reasonable.dart'; +import 'package:flutter_highlight/themes/atom-one-dark.dart'; +import 'package:flutter_highlight/themes/atom-one-light.dart'; +import 'package:flutter_highlight/themes/brown-paper.dart'; +import 'package:flutter_highlight/themes/codepen-embed.dart'; +import 'package:flutter_highlight/themes/color-brewer.dart'; +import 'package:flutter_highlight/themes/darcula.dart'; +import 'package:flutter_highlight/themes/dark.dart'; +import 'package:flutter_highlight/themes/default.dart'; +import 'package:flutter_highlight/themes/docco.dart'; +import 'package:flutter_highlight/themes/dracula.dart'; +import 'package:flutter_highlight/themes/far.dart'; +import 'package:flutter_highlight/themes/foundation.dart'; +import 'package:flutter_highlight/themes/github-gist.dart'; +import 'package:flutter_highlight/themes/github.dart'; +import 'package:flutter_highlight/themes/gml.dart'; +import 'package:flutter_highlight/themes/googlecode.dart'; +import 'package:flutter_highlight/themes/gradient-dark.dart'; +import 'package:flutter_highlight/themes/grayscale.dart'; +import 'package:flutter_highlight/themes/gruvbox-dark.dart'; +import 'package:flutter_highlight/themes/gruvbox-light.dart'; +import 'package:flutter_highlight/themes/hopscotch.dart'; +import 'package:flutter_highlight/themes/hybrid.dart'; +import 'package:flutter_highlight/themes/idea.dart'; +import 'package:flutter_highlight/themes/ir-black.dart'; +import 'package:flutter_highlight/themes/isbl-editor-dark.dart'; +import 'package:flutter_highlight/themes/isbl-editor-light.dart'; +import 'package:flutter_highlight/themes/kimbie.dark.dart'; +import 'package:flutter_highlight/themes/kimbie.light.dart'; +import 'package:flutter_highlight/themes/lightfair.dart'; +import 'package:flutter_highlight/themes/magula.dart'; +import 'package:flutter_highlight/themes/mono-blue.dart'; +import 'package:flutter_highlight/themes/monokai-sublime.dart'; +import 'package:flutter_highlight/themes/monokai.dart'; +import 'package:flutter_highlight/themes/night-owl.dart'; +import 'package:flutter_highlight/themes/nord.dart'; +import 'package:flutter_highlight/themes/obsidian.dart'; +import 'package:flutter_highlight/themes/ocean.dart'; +import 'package:flutter_highlight/themes/paraiso-dark.dart'; +import 'package:flutter_highlight/themes/paraiso-light.dart'; +import 'package:flutter_highlight/themes/pojoaque.dart'; +import 'package:flutter_highlight/themes/purebasic.dart'; +import 'package:flutter_highlight/themes/qtcreator_dark.dart'; +import 'package:flutter_highlight/themes/qtcreator_light.dart'; +import 'package:flutter_highlight/themes/railscasts.dart'; +import 'package:flutter_highlight/themes/rainbow.dart'; +import 'package:flutter_highlight/themes/routeros.dart'; +import 'package:flutter_highlight/themes/school-book.dart'; +import 'package:flutter_highlight/themes/shades-of-purple.dart'; +import 'package:flutter_highlight/themes/solarized-dark.dart'; +import 'package:flutter_highlight/themes/solarized-light.dart'; +import 'package:flutter_highlight/themes/sunburst.dart'; +import 'package:flutter_highlight/themes/tomorrow-night-blue.dart'; +import 'package:flutter_highlight/themes/tomorrow-night-bright.dart'; +import 'package:flutter_highlight/themes/tomorrow-night-eighties.dart'; +import 'package:flutter_highlight/themes/tomorrow-night.dart'; +import 'package:flutter_highlight/themes/tomorrow.dart'; +import 'package:flutter_highlight/themes/vs.dart'; +import 'package:flutter_highlight/themes/vs2015.dart'; +import 'package:flutter_highlight/themes/xcode.dart'; +import 'package:flutter_highlight/themes/xt256.dart'; +import 'package:flutter_highlight/themes/zenburn.dart'; + +const THEMES = { + 'a11y-dark': a11yDarkTheme, + 'a11y-light': a11yLightTheme, + 'agate': agateTheme, + 'an-old-hope': anOldHopeTheme, + 'androidstudio': androidstudioTheme, + 'arduino-light': arduinoLightTheme, + 'arta': artaTheme, + 'ascetic': asceticTheme, + 'atelier-cave-dark': atelierCaveDarkTheme, + 'atelier-cave-light': atelierCaveLightTheme, + 'atelier-dune-dark': atelierDuneDarkTheme, + 'atelier-dune-light': atelierDuneLightTheme, + 'atelier-estuary-dark': atelierEstuaryDarkTheme, + 'atelier-estuary-light': atelierEstuaryLightTheme, + 'atelier-forest-dark': atelierForestDarkTheme, + 'atelier-forest-light': atelierForestLightTheme, + 'atelier-heath-dark': atelierHeathDarkTheme, + 'atelier-heath-light': atelierHeathLightTheme, + 'atelier-lakeside-dark': atelierLakesideDarkTheme, + 'atelier-lakeside-light': atelierLakesideLightTheme, + 'atelier-plateau-dark': atelierPlateauDarkTheme, + 'atelier-plateau-light': atelierPlateauLightTheme, + 'atelier-savanna-dark': atelierSavannaDarkTheme, + 'atelier-savanna-light': atelierSavannaLightTheme, + 'atelier-seaside-dark': atelierSeasideDarkTheme, + 'atelier-seaside-light': atelierSeasideLightTheme, + 'atelier-sulphurpool-dark': atelierSulphurpoolDarkTheme, + 'atelier-sulphurpool-light': atelierSulphurpoolLightTheme, + 'atom-one-dark-reasonable': atomOneDarkReasonableTheme, + 'atom-one-dark': atomOneDarkTheme, + 'atom-one-light': atomOneLightTheme, + 'brown-paper': brownPaperTheme, + 'codepen-embed': codepenEmbedTheme, + 'color-brewer': colorBrewerTheme, + 'darcula': darculaTheme, + 'dark': darkTheme, + 'default': defaultTheme, + 'docco': doccoTheme, + 'dracula': draculaTheme, + 'far': farTheme, + 'foundation': foundationTheme, + 'github-gist': githubGistTheme, + 'github': githubTheme, + 'gml': gmlTheme, + 'googlecode': googlecodeTheme, + 'gradient-dark': gradientDarkTheme, + 'grayscale': grayscaleTheme, + 'gruvbox-dark': gruvboxDarkTheme, + 'gruvbox-light': gruvboxLightTheme, + 'hopscotch': hopscotchTheme, + 'hybrid': hybridTheme, + 'idea': ideaTheme, + 'ir-black': irBlackTheme, + 'isbl-editor-dark': isblEditorDarkTheme, + 'isbl-editor-light': isblEditorLightTheme, + 'kimbie.dark': kimbieDarkTheme, + 'kimbie.light': kimbieLightTheme, + 'lightfair': lightfairTheme, + 'magula': magulaTheme, + 'mono-blue': monoBlueTheme, + 'monokai-sublime': monokaiSublimeTheme, + 'monokai': monokaiTheme, + 'night-owl': nightOwlTheme, + 'nord': nordTheme, + 'obsidian': obsidianTheme, + 'ocean': oceanTheme, + 'paraiso-dark': paraisoDarkTheme, + 'paraiso-light': paraisoLightTheme, + 'pojoaque': pojoaqueTheme, + 'purebasic': purebasicTheme, + 'qtcreator_dark': qtcreatorDarkTheme, + 'qtcreator_light': qtcreatorLightTheme, + 'railscasts': railscastsTheme, + 'rainbow': rainbowTheme, + 'routeros': routerosTheme, + 'school-book': schoolBookTheme, + 'shades-of-purple': shadesOfPurpleTheme, + 'solarized-dark': solarizedDarkTheme, + 'solarized-light': solarizedLightTheme, + 'sunburst': sunburstTheme, + 'tomorrow-night-blue': tomorrowNightBlueTheme, + 'tomorrow-night-bright': tomorrowNightBrightTheme, + 'tomorrow-night-eighties': tomorrowNightEightiesTheme, + 'tomorrow-night': tomorrowNightTheme, + 'tomorrow': tomorrowTheme, + 'vs': vsTheme, + 'vs2015': vs2015Theme, + 'xcode': xcodeTheme, + 'xt256': xt256Theme, + 'zenburn': zenburnTheme, +}; \ No newline at end of file diff --git a/gai-frontend/lib/chat/scripting/code_viewer/user_script_dialog.dart b/gai-frontend/lib/chat/scripting/code_viewer/user_script_dialog.dart new file mode 100644 index 000000000..bc3e7c0a9 --- /dev/null +++ b/gai-frontend/lib/chat/scripting/code_viewer/user_script_dialog.dart @@ -0,0 +1,144 @@ +import 'package:orchid/chat/api/user_preferences_chat.dart'; +import 'package:orchid/chat/scripting/chat_scripting.dart'; +import 'package:orchid/chat/scripting/code_viewer/script_code_box.dart'; +import 'package:orchid/orchid/orchid.dart'; +import 'package:orchid/common/app_dialogs.dart'; +import 'package:orchid/orchid/orchid_action_button.dart'; +import 'package:orchid/orchid/orchid_titled_panel.dart'; + +class UserScriptDialog extends StatefulWidget { + const UserScriptDialog({ + super.key, + }); + + static void show(BuildContext context) { + AppDialogs.showAppDialog( + context: context, + showActions: false, + contentPadding: EdgeInsets.zero, + body: const UserScriptDialog(), + ); + } + + static const exampleScript = ''' +/// This is an example user script that demonstrates how to use the chat API to interact with the chat. +function onUserPrompt(userPrompt) { + (async () => { + // Log a system message to the chat + chatSystemMessage('Extension: Example'); + + // Add the user prompt to the chat + let userMessage = new ChatMessage(ChatMessageSource.CLIENT, userPrompt, {}); + addChatMessage(userMessage); + + // Send it to the first user-selected model + const modelId = getUserSelectedModels()[0].id; + addChatMessage(await sendMessagesToModel([userMessage], modelId, null)); + })(); +} +'''; + + @override + State createState() => _UserScriptDialogState(); +} + +class _UserScriptDialogState extends State { + String? savedScript; + String? editedScript; + + bool get hasUnsavedChanges { + return editedScript != null && (savedScript != editedScript); + } + + @override + void initState() { + super.initState(); + + // Default the script on load once + savedScript = UserPreferencesScripts().userScript.get(); + if (savedScript == null) { + editedScript = UserScriptDialog.exampleScript; + } + + UserPreferencesScripts().userScript.stream().listen((script) { + // guard against the case where this widget is disposed before the stream emits + if (!mounted) { + return; + } + + if (script != null) { + setState(() { + savedScript = script; + editedScript = null; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + // Calculate the width of the dialog + final width = MediaQuery.of(context).size.width; + final dialogWidth = width < 500 ? width - 80 : width * 0.7; + final height = MediaQuery.of(context).size.height; + final double dialogHeight = height < 500 ? height - 80 : height * 0.7; + + return SizedBox( + child: OrchidTitledPanel( + titleText: 'User Script', + highlight: false, + opaque: true, + onDismiss: _dismiss, + body: SizedBox( + width: dialogWidth, + height: dialogHeight, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: UserScriptEditor( + language: 'typescript', + theme: 'a11y-dark', + // Allow the edited script to be initialized if no saved script + initialScript: savedScript ?? editedScript, + onScriptChanged: (script) { + log('User script changed: $script'); + setState(() { + editedScript = script; + }); + }, + ), + ).pad(24), + ), + + // Save button + OrchidActionButton( + text: 'Save', + onPressed: hasUnsavedChanges + ? () { + final emptyScript = + editedScript?.trim().isEmpty ?? false; + if (emptyScript) { + UserPreferencesScripts().userScript.clear(); + UserPreferencesScripts().userScriptEnabled.set(false); + } else { + UserPreferencesScripts().userScript.set(editedScript?.trim()); + UserPreferencesScripts().userScriptEnabled.set(true); + savedScript = editedScript; + ChatScripting.instance.setScript(editedScript!); + } + _dismiss(); + } + : null, + ).bottom(24), + ], + ), + ), + ).show, + ); + } + + void _dismiss() { + Navigator.pop(context); + } +} diff --git a/gai-frontend/pubspec.lock b/gai-frontend/pubspec.lock index dc26a272f..42b5c9732 100644 --- a/gai-frontend/pubspec.lock +++ b/gai-frontend/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.10.0" + code_text_field: + dependency: "direct main" + description: + name: code_text_field + sha256: "0cbffbb2932cf82e1d022996388041de3493a476acad3fbb13e5917cac0fc5f2" + url: "https://pub.dev" + source: hosted + version: "1.1.0" collection: dependency: transitive description: @@ -206,6 +214,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_js: dependency: "direct main" description: @@ -253,6 +269,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a + url: "https://pub.dev" + source: hosted + version: "10.8.0" glob: dependency: transitive description: @@ -261,6 +285,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" http: dependency: "direct main" description: @@ -341,6 +373,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + linked_scroll_controller: + dependency: transitive + description: + name: linked_scroll_controller + sha256: e6020062bcf4ffc907ee7fd090fa971e65d8dfaac3c62baf601a3ced0b37986a + url: "https://pub.dev" + source: hosted + version: "0.2.0" lints: dependency: transitive description: diff --git a/gai-frontend/pubspec.yaml b/gai-frontend/pubspec.yaml index 1a299ff65..08c9ada16 100644 --- a/gai-frontend/pubspec.yaml +++ b/gai-frontend/pubspec.yaml @@ -37,6 +37,10 @@ dependencies: decimal: ^3.0.2 http: ^0.13.4 + code_text_field: ^1.1.0 + font_awesome_flutter: ^10.8.0 + + dev_dependencies: flutter_test: sdk: flutter diff --git a/gui-orchid/lib/orchid/menu/orchid_popup_menu_item_utils.dart b/gui-orchid/lib/orchid/menu/orchid_popup_menu_item_utils.dart index 8d94612eb..2b9057dae 100644 --- a/gui-orchid/lib/orchid/menu/orchid_popup_menu_item_utils.dart +++ b/gui-orchid/lib/orchid/menu/orchid_popup_menu_item_utils.dart @@ -14,7 +14,7 @@ class OrchidPopupMenuItemUtils { child: ListTile( selected: selected, selectedTileColor: OrchidColors.selected_color_dark, - title: body ?? Text(title ?? '', style: textStyle), + title: body ?? Text(title ?? '', style: textStyle).left(8), onTap: () { // Close the menu item Navigator.pop(context); diff --git a/gui-orchid/pubspec.lock b/gui-orchid/pubspec.lock index 9b1fff511..65e4a4403 100644 --- a/gui-orchid/pubspec.lock +++ b/gui-orchid/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_text_field: + dependency: "direct main" + description: + name: code_text_field + sha256: "0cbffbb2932cf82e1d022996388041de3493a476acad3fbb13e5917cac0fc5f2" + url: "https://pub.dev" + source: hosted + version: "1.1.0" collection: dependency: transitive description: @@ -166,6 +174,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_html: dependency: "direct main" description: @@ -213,6 +229,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.0" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" html: dependency: transitive description: @@ -325,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + linked_scroll_controller: + dependency: transitive + description: + name: linked_scroll_controller + sha256: e6020062bcf4ffc907ee7fd090fa971e65d8dfaac3c62baf601a3ced0b37986a + url: "https://pub.dev" + source: hosted + version: "0.2.0" list_counter: dependency: transitive description: