Merge pull request #52 from pangeachat/bot-options

Conversation Bot UI
pull/1011/head
ggurdin 2 years ago committed by GitHub
commit 314be3898e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3873,5 +3873,14 @@
"type": "text",
"placeholders": {}
},
"addConversationBot": "Enable Conversation Bot",
"addConversationBotDesc": "Add a bot to this group chat that will ask questions on a specific topic",
"convoBotSettingsTitle": "Conversation Bot Settings",
"convoBotSettingsDescription": "Edit conversation topic and difficulty",
"enterAConversationTopic": "Enter a conversation topic",
"conversationTopic": "Conversation topic",
"enableModeration": "Enable moderation",
"enableModerationDesc": "Enable automatic moderation to review messages before they are sent",
"conversationLanguageLevel": "What is the language level of this conversation?",
"showDefinition": "Show Definition"
}

@ -4534,5 +4534,14 @@
"placeholders": {
"roomName": {}
}
}
}
},
"addConversationBot": "Añadir bot de conversación",
"addConversationBotDesc": "Añadir un bot de conversación para enviar mensajes automáticos a este chat",
"convoBotSettingsTitle": "Configuración del bot de conversación",
"convoBotSettingsDescription": "Editar tema de conversación y dificultad",
"enterAConversationTopic": "Introducir un tema de conversación",
"conversationTopic": "Tema de conversación",
"enableModeration": "Activar la moderación",
"enableModerationDesc": "Activar la moderación automática para revisar los mensajes antes de enviarlos",
"conversationLanguageLevel": "¿Cuál es el nivel lingüístico de esta conversación?"
}

@ -1,25 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_details/chat_details_view.dart';
import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/utils/set_class_name.dart';
import 'package:fluffychat/pangea/utils/set_class_topic.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
enum AliasActions { copy, delete, setCanonical }
@ -45,6 +44,8 @@ class ChatDetailsController extends State<ChatDetails> {
// #Pangea
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
final GlobalKey<ConversationBotSettingsState> addConversationBotKey =
GlobalKey<ConversationBotSettingsState>();
bool displayAddStudentOptions = false;
void toggleAddStudentOptions() =>

@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/utils/archive_space.dart';
import 'package:fluffychat/pangea/utils/lock_room.dart';
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/pangea/widgets/space/class_settings.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
@ -489,6 +490,13 @@ class ChatDetailsView extends StatelessWidget {
if (controller.displayAddStudentOptions &&
room.showClassEditOptions)
ClassInvitationButtons(roomId: controller.roomId!),
const Divider(height: 1),
if (!room.isSpace && room.canInvite)
ConversationBotSettings(
key: controller.addConversationBotKey,
room: room,
),
const Divider(height: 1),
if (!room.isPangeaClass)
AddToSpaceToggles(
roomId: room.id,

@ -1,20 +1,20 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:fluffychat/pages/new_group/new_group_view.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/chat_topic_model.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as sdk;
class NewGroup extends StatefulWidget {
const NewGroup({super.key});
@ -42,6 +42,8 @@ class NewGroupController extends State<NewGroup> {
// #Pangea
PangeaController pangeaController = MatrixState.pangeaController;
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
final GlobalKey<ConversationBotSettingsState> addConversationBotKey =
GlobalKey<ConversationBotSettingsState>();
ChatTopic chatTopic = ChatTopic.empty;
@ -121,6 +123,10 @@ class NewGroupController extends State<NewGroup> {
.map((suggestionStatus) => suggestionStatus.room)
.toList(),
),
invite: [
if (addConversationBotKey.currentState?.addBot ?? false)
BotName.byEnvironment,
],
// Pangea#
);
if (!mounted) return;

@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/new_group/new_group.dart';
import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart';
import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class NewGroupView extends StatelessWidget {
final NewGroupController controller;
@ -85,6 +84,10 @@ class NewGroupView extends StatelessWidget {
// ),
// ),
// ),
ConversationBotSettings(
key: controller.addConversationBotKey,
),
const Divider(height: 1),
AddToSpaceToggles(
key: controller.addToSpaceKey,
startOpen: false,

@ -87,4 +87,10 @@ class ModelKey {
static const String currentText = "current";
static const String bestContinuance = "best_continuance";
static const String feedbackLang = "feedback_lang";
// bot options
static const String languageLevel = "langauge_level";
static const String conversationTopic = "conversation_topic";
static const String keywords = "keywords";
static const String safetyModeration = "safety_moderation";
}

@ -13,4 +13,5 @@ class PangeaEventTypes {
static const vocab = "p.vocab";
static const roomInfo = "pangea.roomtopic";
static const botOptions = "pangea.bot_options";
}

@ -4,6 +4,7 @@ import 'dart:developer';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
@ -981,4 +982,18 @@ extension PangeaRoom on Room {
if (!isSpace) return null;
return pangeaRoomRulesStateEvent?.originServerTs ?? creationTime;
}
Future<bool> get isBotRoom async {
final List<User> participants = await requestParticipants();
return participants.any(
(User user) => user.id == BotName.byEnvironment,
);
}
BotOptionsModel? get botOptions {
if (isSpace) return null;
return BotOptionsModel.fromJson(
getState(PangeaEventTypes.botOptions)?.content ?? {},
);
}
}

@ -0,0 +1,72 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../constants/pangea_event_types.dart';
class BotOptionsModel {
int? languageLevel;
String topic;
List<String> keywords;
bool safetyModeration;
BotOptionsModel({
this.languageLevel,
this.topic = "General Conversation",
this.keywords = const [],
this.safetyModeration = true,
});
factory BotOptionsModel.fromJson(json) {
return BotOptionsModel(
languageLevel: json[ModelKey.languageLevel],
topic: json[ModelKey.conversationTopic] ?? "General Conversation",
keywords: (json[ModelKey.keywords] ?? []).cast<String>(),
safetyModeration: json[ModelKey.safetyModeration] ?? true,
);
}
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
try {
// data[ModelKey.isConversationBotChat] = isConversationBotChat;
data[ModelKey.languageLevel] = languageLevel;
data[ModelKey.conversationTopic] = topic;
data[ModelKey.keywords] = keywords;
data[ModelKey.safetyModeration] = safetyModeration;
return data;
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s);
return data;
}
}
//TODO: define enum with all possible values
updateBotOption(String key, dynamic value) {
switch (key) {
case ModelKey.languageLevel:
languageLevel = value;
break;
case ModelKey.conversationTopic:
topic = value;
break;
case ModelKey.keywords:
keywords = value;
break;
case ModelKey.safetyModeration:
safetyModeration = value;
break;
default:
throw Exception('Invalid key for bot options - $key');
}
}
StateEvent get toStateEvent => StateEvent(
content: toJson(),
type: PangeaEventTypes.botOptions,
);
}

@ -0,0 +1,245 @@
import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import '../../../widgets/matrix.dart';
import '../../constants/pangea_event_types.dart';
import '../../extensions/pangea_room_extension.dart';
import '../../utils/error_handler.dart';
class ConversationBotSettings extends StatefulWidget {
final Room? room;
final bool startOpen;
// final ClassSettingsModel? initialSettings;
const ConversationBotSettings({
super.key,
this.room,
this.startOpen = false,
// this.initialSettings,
});
@override
ConversationBotSettingsState createState() => ConversationBotSettingsState();
}
class ConversationBotSettingsState extends State<ConversationBotSettings> {
late BotOptionsModel botOptions;
late bool isOpen;
bool addBot = false;
ConversationBotSettingsState({Key? key});
@override
void initState() {
super.initState();
isOpen = widget.startOpen;
botOptions = widget.room?.botOptions ?? BotOptionsModel();
widget.room?.isBotRoom.then((bool isBotRoom) {
setState(() {
addBot = isBotRoom;
});
});
}
Future<void> updateBotOption(void Function() makeLocalChange) async {
makeLocalChange();
await showFutureLoadingDialog(
context: context,
future: () async {
try {
await setBotOption();
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
setState(() {});
},
);
}
Future<void> setBotOption() async {
if (widget.room == null) return;
try {
await Matrix.of(context).client.setRoomStateWithKey(
widget.room!.id,
PangeaEventTypes.botOptions,
'',
botOptions.toJson(),
);
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
}
@override
Widget build(BuildContext context) => Column(
children: [
ListTile(
title: Text(
L10n.of(context)!.convoBotSettingsTitle,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.convoBotSettingsDescription),
leading: CircleAvatar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).textTheme.bodyLarge!.color,
child: const Icon(Icons.psychology_outlined),
),
trailing: Icon(
isOpen
? Icons.keyboard_arrow_down_outlined
: Icons.keyboard_arrow_right_outlined,
),
onTap: () => setState(() => isOpen = !isOpen),
),
if (isOpen)
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: isOpen ? null : 0,
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16),
child: SwitchListTile.adaptive(
title: Text(
L10n.of(context)!.addConversationBot,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.addConversationBotDesc),
secondary: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).textTheme.bodyLarge!.color,
child: const BotFace(
width: 30.0,
expression: BotExpression.right,
),
),
activeColor: AppConfig.activeToggleColor,
value: addBot,
onChanged: (bool add) {
setState(() => addBot = add);
add
? widget.room?.invite(BotName.byEnvironment)
: widget.room?.kick(BotName.byEnvironment);
},
),
),
if (addBot) ...[
Padding(
padding: const EdgeInsets.only(left: 16),
child: ListTile(
onTap: () async {
final topic = await showTextInputDialog(
context: context,
textFields: [
DialogTextField(
initialText: botOptions.topic.isEmpty
? ""
: botOptions.topic,
hintText:
L10n.of(context)!.enterAConversationTopic,
),
],
title: L10n.of(context)!.conversationTopic,
);
if (topic == null) return;
updateBotOption(() {
botOptions.topic = topic.single;
});
},
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).textTheme.bodyLarge!.color,
child: const Icon(Icons.topic_outlined),
),
subtitle: Text(
botOptions.topic.isEmpty
? L10n.of(context)!.enterAConversationTopic
: botOptions.topic,
),
title: Text(
L10n.of(context)!.conversationTopic,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16),
child: SwitchListTile.adaptive(
title: Text(
L10n.of(context)!.enableModeration,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.enableModerationDesc),
secondary: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).textTheme.bodyLarge!.color,
child: const Icon(Icons.shield_outlined),
),
activeColor: AppConfig.activeToggleColor,
value: botOptions.safetyModeration,
onChanged: (bool newValue) => updateBotOption(() {
botOptions.safetyModeration = newValue;
}),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(32, 16, 0, 0),
child: Text(
L10n.of(context)!.conversationLanguageLevel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16),
child: LanguageLevelDropdown(
initialLevel: botOptions.languageLevel,
onChanged: (int? newValue) => updateBotOption(() {
botOptions.languageLevel = newValue!;
}),
),
),
const SizedBox(height: 16),
],
],
),
),
],
);
}

@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:fluffychat/pangea/models/class_model.dart';
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -9,14 +10,12 @@ import 'package:matrix/matrix.dart';
import '../../../widgets/matrix.dart';
import '../../constants/language_keys.dart';
import '../../constants/language_level_type.dart';
import '../../constants/pangea_event_types.dart';
import '../../controllers/language_list_controller.dart';
import '../../controllers/pangea_controller.dart';
import '../../extensions/pangea_room_extension.dart';
import '../../models/language_model.dart';
import '../../utils/error_handler.dart';
import '../../utils/language_level_copy.dart';
import '../user_settings/p_language_dropdown.dart';
import '../user_settings/p_question_container.dart';
@ -169,72 +168,11 @@ class ClassSettingsState extends State<ClassSettings> {
PQuestionContainer(
title: L10n.of(context)!.whatIsYourClassLanguageLevel,
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.secondary,
width: 0.5,
),
borderRadius:
const BorderRadius.all(Radius.circular(10)),
),
child: DropdownButton(
// Initial Value
hint: Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
classSettings.languageLevel == null
? L10n.of(context)!.selectLanguageLevel
: LanguageLevelTextPicker.languageLevelText(
context,
classSettings.languageLevel!,
),
style: const TextStyle().copyWith(
color: Theme.of(context)
.textTheme
.bodyLarge!
.color,
fontSize: 14,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
),
isExpanded: true,
underline: Container(),
// Down Arrow Icon
icon: const Icon(Icons.keyboard_arrow_down),
// Array list of items
items:
LanguageLevelType.allInts.map((int levelOption) {
return DropdownMenuItem(
value: levelOption,
child: Text(
LanguageLevelTextPicker.languageLevelText(
context,
levelOption,
),
style: const TextStyle().copyWith(
color: Theme.of(context)
.textTheme
.bodyLarge!
.color,
fontSize: 14,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
);
}).toList(),
// After selecting the desired option,it will
// change button value to selected value
onChanged: (int? newValue) => updatePermission(() {
classSettings.languageLevel = newValue!;
}),
),
),
LanguageLevelDropdown(
initialLevel: classSettings.languageLevel,
onChanged: (int? newValue) => updatePermission(() {
classSettings.languageLevel = newValue!;
}),
),
],
),

@ -0,0 +1,76 @@
import 'package:fluffychat/pangea/constants/language_level_type.dart';
import 'package:fluffychat/pangea/utils/language_level_copy.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class LanguageLevelDropdown extends StatelessWidget {
final int? initialLevel;
final void Function(int?)? onChanged;
const LanguageLevelDropdown({
super.key,
this.initialLevel,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.secondary,
width: 0.5,
),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
child: DropdownButton(
// Initial Value
hint: Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
initialLevel == null
? L10n.of(context)!.selectLanguageLevel
: LanguageLevelTextPicker.languageLevelText(
context,
initialLevel!,
),
style: const TextStyle().copyWith(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 14,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
),
isExpanded: true,
underline: Container(),
// Down Arrow Icon
icon: const Icon(Icons.keyboard_arrow_down),
// Array list of items
items: LanguageLevelType.allInts.map((int levelOption) {
return DropdownMenuItem(
value: levelOption,
child: Text(
LanguageLevelTextPicker.languageLevelText(
context,
levelOption,
),
style: const TextStyle().copyWith(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 14,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
);
}).toList(),
// After selecting the desired option,it will
// change button value to selected value
onChanged: onChanged,
),
),
);
}
}

@ -115,6 +115,7 @@ abstract class ClientManager {
PangeaEventTypes.classSettings,
PangeaEventTypes.rules,
PangeaEventTypes.vocab,
PangeaEventTypes.botOptions,
EventTypes.RoomTopic,
EventTypes.RoomAvatar,
// Pangea#

Loading…
Cancel
Save