From bd4d9e43ed314c2f37db41b264be79dabd787892 Mon Sep 17 00:00:00 2001 From: WilsonLe Date: Tue, 6 Aug 2024 14:41:50 -0400 Subject: [PATCH] add create group chat validation for custom and game master instructions --- assets/l10n/intl_en.arb | 5 + lib/pages/new_group/new_group.dart | 28 ++++++ lib/pangea/constants/model_keys.dart | 3 + lib/pangea/models/bot_options_model.dart | 17 ++++ ...sation_bot_custom_system_prompt_input.dart | 48 +++++++--- .../conversation_bot_custom_zone.dart | 1 - .../conversation_bot_mode_dynamic_zone.dart | 6 +- .../conversation_bot_mode_select.dart | 4 +- ...venture_game_master_instruction_input.dart | 91 +++++++++++++++++++ .../conversation_bot_text_adventure_zone.dart | 45 ++++++++- 10 files changed, 229 insertions(+), 19 deletions(-) create mode 100644 lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 981a36238..b8ad9121b 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4003,6 +4003,7 @@ "conversationBotCustomZone_title": "Custom Settings", "conversationBotCustomZone_customSystemPromptLabel": "System prompt", "conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt", + "conversationBotCustomZone_customSystemPromptEmptyError": "Missing custom system prompt", "conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction", "botConfig": "Conversation Bot Settings", "addConversationBotDialogTitleInvite": "Confirm inviting conversation bot", @@ -4013,6 +4014,10 @@ "addConversationBotDialogRemoveConfirmation": "Remove", "conversationBotConfigConfirmChange": "Confirm", "conversationBotStatus": "Bot Status", + "conversationBotTextAdventureZone_title": "Text Adventure", + "conversationBotTextAdventureZone_instructionLabel": "Game Master Instructions", + "conversationBotTextAdventureZone_instructionPlaceholder": "Set game master instructions", + "conversationBotCustomZone_instructionSystemPromptEmptyError": "Missing game master instructions", "studentAnalyticsNotAvailable": "Student data not currently available", "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "updatePhoneOS": "You may need to update your device's OS version.", diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 94684e442..cb16a83a7 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -106,6 +106,34 @@ class NewGroupController extends State { if (!mounted) return; + // #Pangea + // validate init bot options + final addBot = addConversationBotKey.currentState?.addBot ?? false; + if (addBot) { + final botOptions = addConversationBotKey.currentState!.botOptions; + if (botOptions.mode == "custom") { + if (botOptions.customSystemPrompt == null || + botOptions.customSystemPrompt!.isEmpty) { + setState(() { + error = L10n.of(context)! + .conversationBotCustomZone_customSystemPromptEmptyError; + loading = false; + }); + return; + } + } else if (botOptions.mode == "text_adventure") { + if (botOptions.textAdventureGameMasterInstructions == null || + botOptions.textAdventureGameMasterInstructions!.isEmpty) { + setState(() { + error = L10n.of(context)! + .conversationBotCustomZone_instructionSystemPromptEmptyError; + loading = false; + }); + return; + } + } + } + final roomId = await client.createGroupChat( // #Pangea // visibility: diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index 7c60b98d7..b42061446 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -114,6 +114,9 @@ class ModelKey { "custom_trigger_reaction_enabled"; static const String customTriggerReactionKey = "custom_trigger_reaction_key"; + static const String textAdventureGameMasterInstructions = + "text_adventure_game_master_instructions"; + static const String prevEventId = "prev_event_id"; static const String prevLastUpdated = "prev_last_updated"; } diff --git a/lib/pangea/models/bot_options_model.dart b/lib/pangea/models/bot_options_model.dart index 179461a34..5c61eef5a 100644 --- a/lib/pangea/models/bot_options_model.dart +++ b/lib/pangea/models/bot_options_model.dart @@ -20,6 +20,7 @@ class BotOptionsModel { String? customSystemPrompt; bool? customTriggerReactionEnabled; String? customTriggerReactionKey; + String? textAdventureGameMasterInstructions; BotOptionsModel({ //////////////////////////////////////////////////////////////////////////// @@ -45,6 +46,11 @@ class BotOptionsModel { this.customSystemPrompt, this.customTriggerReactionEnabled = true, this.customTriggerReactionKey = "⏩", + + //////////////////////////////////////////////////////////////////////////// + // Text Adventure Mode Options + //////////////////////////////////////////////////////////////////////////// + this.textAdventureGameMasterInstructions, }); factory BotOptionsModel.fromJson(json) { @@ -73,6 +79,12 @@ class BotOptionsModel { customTriggerReactionEnabled: json[ModelKey.customTriggerReactionEnabled] ?? true, customTriggerReactionKey: json[ModelKey.customTriggerReactionKey] ?? "⏩", + + ////////////////////////////////////////////////////////////////////////// + // Text Adventure Mode Options + ////////////////////////////////////////////////////////////////////////// + textAdventureGameMasterInstructions: + json[ModelKey.textAdventureGameMasterInstructions], ); } @@ -93,6 +105,8 @@ class BotOptionsModel { data[ModelKey.customTriggerReactionEnabled] = customTriggerReactionEnabled ?? true; data[ModelKey.customTriggerReactionKey] = customTriggerReactionKey ?? "⏩"; + data[ModelKey.textAdventureGameMasterInstructions] = + textAdventureGameMasterInstructions; return data; } catch (e, s) { debugger(when: kDebugMode); @@ -134,6 +148,9 @@ class BotOptionsModel { case ModelKey.customTriggerReactionKey: customTriggerReactionKey = value; break; + case ModelKey.textAdventureGameMasterInstructions: + textAdventureGameMasterInstructions = value; + break; default: throw Exception('Invalid key for bot options - $key'); } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart index 55ec1493d..2e79e0677 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart @@ -20,6 +20,9 @@ class ConversationBotCustomSystemPromptInput extends StatelessWidget { final TextEditingController textFieldController = TextEditingController(text: customSystemPrompt); + final GlobalKey customSystemPromptFormKey = + GlobalKey(); + void setBotCustomSystemPromptAction() async { showDialog( context: context, @@ -28,14 +31,25 @@ class ConversationBotCustomSystemPromptInput extends StatelessWidget { title: Text( L10n.of(context)!.conversationBotCustomZone_customSystemPromptLabel, ), - content: TextField( - minLines: 1, - maxLines: 10, - maxLength: 1000, - controller: textFieldController, - onChanged: (value) { - customSystemPrompt = value; - }, + content: Form( + key: customSystemPromptFormKey, + child: TextFormField( + minLines: 1, + maxLines: 10, + maxLength: 1000, + controller: textFieldController, + onChanged: (value) { + if (value.isNotEmpty) { + customSystemPrompt = value; + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'This field cannot be empty'; + } + return null; + }, + ), ), actions: [ TextButton( @@ -47,11 +61,12 @@ class ConversationBotCustomSystemPromptInput extends StatelessWidget { TextButton( child: Text(L10n.of(context)!.ok), onPressed: () { - if (customSystemPrompt == "") return; - if (customSystemPrompt != - initialBotOptions.customSystemPrompt) { - initialBotOptions.customSystemPrompt = customSystemPrompt; - onChanged.call(initialBotOptions); + if (customSystemPromptFormKey.currentState!.validate()) { + if (customSystemPrompt != + initialBotOptions.customSystemPrompt) { + initialBotOptions.customSystemPrompt = customSystemPrompt; + onChanged.call(initialBotOptions); + } Navigator.of(context).pop(); } }, @@ -68,6 +83,13 @@ class ConversationBotCustomSystemPromptInput extends StatelessWidget { L10n.of(context)! .conversationBotCustomZone_customSystemPromptPlaceholder, ), + subtitle: customSystemPrompt.isEmpty + ? Text( + L10n.of(context)! + .conversationBotCustomZone_customSystemPromptEmptyError, + style: const TextStyle(color: Colors.red), + ) + : null, ); } } diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart index 553de7182..d5002ce2f 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart @@ -16,7 +16,6 @@ class ConversationBotCustomZone extends StatelessWidget { @override Widget build(BuildContext context) { - print(initialBotOptions.toJson()); return Column( children: [ const SizedBox(height: 12), diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart index b15689357..e7d8f55a9 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pangea/models/bot_options_model.dart'; import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart'; +import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart'; import 'package:flutter/material.dart'; import 'conversation_bot_discussion_zone.dart'; @@ -26,7 +27,10 @@ class ConversationBotModeDynamicZone extends StatelessWidget { onChanged: onChanged, ), // "conversation": const ConversationBotConversationZone(), - // "text_adventure": const ConversationBotTextAdventureZone(), + "text_adventure": ConversationBotTextAdventureZone( + initialBotOptions: initialBotOptions, + onChanged: onChanged, + ), }; return Container( decoration: BoxDecoration( diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart index ba60f038c..5ec435112 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart @@ -19,8 +19,8 @@ class ConversationBotModeSelect extends StatelessWidget { "custom": L10n.of(context)!.conversationBotModeSelectOption_custom, // "conversation": // L10n.of(context)!.conversationBotModeSelectOption_conversation, - // "text_adventure": - // L10n.of(context)!.conversationBotModeSelectOption_textAdventure, + "text_adventure": + L10n.of(context)!.conversationBotModeSelectOption_textAdventure, }; return Padding( diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart new file mode 100644 index 000000000..fc5aa0a08 --- /dev/null +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart @@ -0,0 +1,91 @@ +import 'package:fluffychat/pangea/models/bot_options_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class ConversationBotGameMasterInstructionsInput extends StatelessWidget { + final BotOptionsModel initialBotOptions; + // call this to update propagate changes to parents + final void Function(BotOptionsModel) onChanged; + + const ConversationBotGameMasterInstructionsInput({ + super.key, + required this.initialBotOptions, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + String gameMasterInstructions = + initialBotOptions.textAdventureGameMasterInstructions ?? ""; + + final TextEditingController textFieldController = + TextEditingController(text: gameMasterInstructions); + + final GlobalKey gameMasterInstructionsFormKey = + GlobalKey(); + + void setBotTextAdventureGameMasterInstructionsAction() async { + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => AlertDialog( + title: Text( + L10n.of(context)! + .conversationBotTextAdventureZone_instructionPlaceholder, + ), + content: Form( + key: gameMasterInstructionsFormKey, + child: TextFormField( + minLines: 1, + maxLines: 10, + maxLength: 1000, + controller: textFieldController, + onChanged: (value) { + if (value.isNotEmpty) { + gameMasterInstructions = value; + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'This field cannot be empty'; + } + return null; + }, + ), + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () { + if (gameMasterInstructionsFormKey.currentState!.validate()) { + if (gameMasterInstructions != + initialBotOptions.textAdventureGameMasterInstructions) { + initialBotOptions.textAdventureGameMasterInstructions = + gameMasterInstructions; + onChanged.call(initialBotOptions); + } + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ); + } + + return ListTile( + onTap: setBotTextAdventureGameMasterInstructionsAction, + title: Text( + initialBotOptions.textAdventureGameMasterInstructions ?? + L10n.of(context)! + .conversationBotTextAdventureZone_instructionPlaceholder, + ), + ); + } +} diff --git a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart index 2f65348cf..7f47bd018 100644 --- a/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart +++ b/lib/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart @@ -1,15 +1,56 @@ +import 'package:fluffychat/pangea/models/bot_options_model.dart'; +import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_text_adventure_game_master_instruction_input.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class ConversationBotTextAdventureZone extends StatelessWidget { + final BotOptionsModel initialBotOptions; + // call this to update propagate changes to parents + + final void Function(BotOptionsModel) onChanged; const ConversationBotTextAdventureZone({ super.key, + required this.initialBotOptions, + required this.onChanged, }); @override Widget build(BuildContext context) { - return const Column( + return Column( children: [ - Text('Text Adventure Zone'), + Text( + L10n.of(context)!.conversationBotTextAdventureZone_title, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + const Divider( + color: Colors.grey, + thickness: 1, + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 0, 0), + child: Text( + L10n.of(context)! + .conversationBotTextAdventureZone_instructionLabel, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: ConversationBotGameMasterInstructionsInput( + initialBotOptions: initialBotOptions, + onChanged: onChanged, + ), + ), ], ); }