Merge branch 'main' of https://github.com/pangeachat/client into toolbar-selection

pull/1384/head
Kelrap 1 year ago
commit ed57c340d1

@ -3879,7 +3879,7 @@
"define": "Define",
"listen": "Listen",
"addConversationBot": "Enable Conversation Bot",
"addConversationBotDesc": "Add a bot to this group chat that will ask questions on a specific topic",
"addConversationBotDesc": "Add a bot to this group chat",
"convoBotSettingsTitle": "Conversation Bot Settings",
"convoBotSettingsDescription": "Edit conversation topic and difficulty",
"enterAConversationTopic": "Enter a conversation topic",
@ -4003,13 +4003,21 @@
"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",
"addConversationBotButtonInvite": "Invite",
"addConversationBotDialogInviteConfirmation": "Invite",
"addConversationBotButtonTitleRemove": "Confirm removing conversation bot",
"addConversationBotButtonRemove": "Remove",
"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.",

@ -331,7 +331,6 @@ class ChatController extends State<ChatPageWithRoom>
);
}
await Matrix.of(context).client.roomsLoading;
choreographer.setRoomId(roomId);
});
// Pangea#
_tryLoadTimeline();

@ -463,7 +463,7 @@ class InputBar extends StatelessWidget {
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
// #Pangea
key: controller!.choreographer.inputLayerLinkAndKey.key,
key: controller?.choreographer.inputLayerLinkAndKey.key,
// builder: (context, controller, focusNode) => TextField(
builder: (context, _, focusNode) => TextField(
// Pangea#
@ -504,11 +504,11 @@ class InputBar extends StatelessWidget {
onSubmitted!(text);
},
// #Pangea
style: controller?.isMaxLength ?? false
style: controller?.exceededMaxLength ?? false
? const TextStyle(color: Colors.red)
: null,
onTap: () {
controller!.onInputTap(
controller?.onInputTap(
context,
fNode: focusNode,
);

@ -6,7 +6,6 @@ import 'package:fluffychat/pages/settings/settings.dart';
import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart';
import 'package:fluffychat/pangea/utils/set_class_name.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/platform_infos.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -43,8 +42,9 @@ class ChatDetailsController extends State<ChatDetails> {
// #Pangea
final GlobalKey<AddToSpaceState> addToSpaceKey = GlobalKey<AddToSpaceState>();
final GlobalKey<ConversationBotSettingsState> addConversationBotKey =
GlobalKey<ConversationBotSettingsState>();
final GlobalKey<ChatDetailsController>
addConversationBotKey =
GlobalKey<ChatDetailsController>();
bool displayAddStudentOptions = false;
void toggleAddStudentOptions() =>

@ -105,6 +105,34 @@ class NewGroupController extends State<NewGroup> {
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:

@ -91,7 +91,7 @@ class AlternativeTranslator {
final FullTextTranslationResponseModel results =
await FullTextTranslationRepo.translate(
accessToken: await choreographer.accessToken,
accessToken: choreographer.accessToken,
request: FullTextTranslationRequestModel(
text: choreographer.itController.sourceText!,
tgtLang: choreographer.l2LangCode!,
@ -117,7 +117,7 @@ class AlternativeTranslator {
}
similarityResponse = await SimilarityRepo.get(
accessToken: await choreographer.accessToken,
accessToken: choreographer.accessToken,
request: SimilarityRequestModel(
benchmark: results.bestTranslation,
toCompare: [userTranslation!],

@ -42,7 +42,6 @@ class Choreographer {
bool isFetching = false;
Timer? debounceTimer;
String? _roomId;
ChoreoRecord choreoRecord = ChoreoRecord.newRecord;
// last checked by IGC or translation
String? _lastChecked;
@ -403,7 +402,7 @@ class Choreographer {
PangeaTextController get textController => _textController;
Future<String> get accessToken => pangeaController.userController.accessToken;
String get accessToken => pangeaController.userController.accessToken;
clear() {
choreoMode = ChoreoMode.igc;
@ -464,10 +463,7 @@ class Choreographer {
setState();
}
get roomId => _roomId;
void setRoomId(String? roomId) {
_roomId = roomId ?? '';
}
get roomId => chatController.roomId;
bool get _useCustomInput => [
EditType.keyboard,

@ -46,7 +46,7 @@ class IgcController {
);
final IGCTextData igcTextDataResponse = await IgcRepo.getIGC(
await choreographer.accessToken,
choreographer.accessToken,
igcRequest: reqBody,
);

@ -65,7 +65,7 @@ class SpanDataController {
response = _cache[cacheKey]!.data;
} else {
response = SpanDataRepo.getSpanDetails(
await choreographer.accessToken,
choreographer.accessToken,
request: SpanDetailsRepoReqAndRes(
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,

@ -73,7 +73,7 @@ class ITFeedbackCardController extends State<ITFeedbackCard> {
isTranslating = true;
});
FullTextTranslationRepo.translate(
accessToken: await controller.userController.accessToken,
accessToken: controller.userController.accessToken,
request: FullTextTranslationRequestModel(
text: res!.text,
tgtLang: controller.languageController.userL1?.langCode ??

@ -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";
}

@ -51,7 +51,7 @@ class ContextualDefinitionController {
try {
final ContextualDefinitionResponseModel res =
await _ContextualDefinitionRepo.define(
await _pangeaController.userController.accessToken,
_pangeaController.userController.accessToken,
request,
);
return res;

@ -52,7 +52,7 @@ class ITFeedbackController {
) async {
try {
final ITFeedbackResponseModel res = await _ITFeedbackRepo.get(
await _pangeaController.userController.accessToken,
_pangeaController.userController.accessToken,
request,
);
return res;

@ -145,7 +145,7 @@ class LanguageDetectionController {
return _cache[params]!.data;
} else {
final Future<LanguageDetectionResponse> response = _fetchResponse(
await _pangeaController.userController.accessToken,
_pangeaController.userController.accessToken,
params,
);
_cache[params] = _LanguageDetectionCacheItem(data: response);

@ -42,7 +42,7 @@ class MessageDataController extends BaseController {
Future<PangeaMessageTokens?> _getTokens(
TokensRequestModel req,
) async {
final accessToken = await _pangeaController.userController.accessToken;
final accessToken = _pangeaController.userController.accessToken;
final TokensResponseModel igcTextData =
await TokensRepo.tokenize(accessToken, req);
@ -195,7 +195,7 @@ class MessageDataController extends BaseController {
try {
final FullTextTranslationResponseModel res =
await FullTextTranslationRepo.translate(
accessToken: await _pangeaController.userController.accessToken,
accessToken: _pangeaController.userController.accessToken,
request: req,
);

@ -277,7 +277,11 @@ class MyAnalyticsController {
// get the timelines for each chat
final List<Future<Timeline>> timelineFutures = [];
for (final chat in chats) {
timelineFutures.add(chat.getTimeline());
timelineFutures.add(
chat.timeline == null
? chat.getTimeline()
: Future.value(chat.timeline),
);
}
final List<Timeline> timelines = await Future.wait(timelineFutures);
final Map<String, Timeline> timelineMap =

@ -53,7 +53,7 @@ class SpeechToTextController {
return _cache[cacheKey]!.data;
} else {
final Future<SpeechToTextModel> response = _fetchResponse(
accessToken: await _pangeaController.userController.accessToken,
accessToken: _pangeaController.userController.accessToken,
requestModel: requestModel,
);
_cache[cacheKey] = _SpeechToTextCacheItem(data: response);

@ -100,7 +100,7 @@ class TextToSpeechController {
return _cache[params]!.data;
} else {
final Future<TextToSpeechResponse> response = _fetchResponse(
await _pangeaController.userController.accessToken,
_pangeaController.userController.accessToken,
params,
);
_cache[params] = _TextToSpeechCacheItem(data: response);

@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:matrix/matrix.dart' as matrix;
import '../constants/local.key.dart';
import '../models/user_model.dart';
import '../repo/user_repo.dart';
@ -83,15 +82,6 @@ class UserController extends BaseController {
createdAt: DateTime.now(),
);
final newProfile = Profile(userSettings: userSettings);
// we don't use the pangea profile anymore, but we still need
// it to get access token for the choreographer, so create one
await PUserRepo.repoCreatePangeaUser(
userID: userId!,
dob: dob.toIso8601String(),
fullName: fullname!,
matrixAccessToken: _matrixAccessToken!,
);
await newProfile.saveProfileData(waitForDataInSync: true);
}
@ -157,41 +147,13 @@ class UserController extends BaseController {
/// Returns a boolean value indicating whether a new JWT (JSON Web Token) is needed.
bool needNewJWT(String token) => Jwt.isExpired(token);
/// Retrieves the access token for the user. Looks for it locally,
/// and if it's not found or expired, fetches it from the server.
Future<String> get accessToken async {
final localAccessToken =
_pangeaController.pStoreService.read(PLocalKey.access);
if (localAccessToken == null || needNewJWT(localAccessToken)) {
PangeaProfileResponse? userModel = await PUserRepo.fetchPangeaUserInfo(
userID: userId!,
matrixAccessToken: _matrixAccessToken!,
);
// Oops, some accounts were made without creating pangea profiles, so they
// don't have access to an access token yet. In that case, create a pangea profile.
if (userModel?.access == null) {
final dob = profile.userSettings.dateOfBirth;
if (dob != null) {
userModel = await PUserRepo.repoCreatePangeaUser(
userID: userId!,
dob: dob.toIso8601String(),
fullName: fullname!,
matrixAccessToken: _matrixAccessToken!,
);
if (userModel?.access == null) {
throw ("Trying to get accessToken with null userModel");
}
}
}
_pangeaController.pStoreService.save(
PLocalKey.access,
userModel!.access,
);
return userModel.access;
/// Retrieves matrix access token.
String get accessToken {
final token = _pangeaController.matrixState.client.accessToken;
if (token == null) {
throw ("Trying to get accessToken with null token. User is not logged in.");
}
return localAccessToken;
return token;
}
/// Returns the full name of the user.

@ -54,7 +54,7 @@ class WordController extends BaseController {
if (local != null) return local;
final WordData remote = await WordRepo.getWordNetData(
accessToken: await _pangeaController.userController.accessToken,
accessToken: _pangeaController.userController.accessToken,
fullText: fullText,
word: word,
userL1: userL1,

@ -3,7 +3,7 @@ part of "client_extension.dart";
extension GeneralInfoClientExtension on Client {
Future<List<String>> get _teacherRoomIds async {
final List<String> adminRoomIds = [];
for (final Room adminSpace in (await _spacesImTeaching)) {
for (final Room adminSpace in (_spacesImTeaching)) {
adminRoomIds.add(adminSpace.id);
final List<String> adminSpaceRooms = adminSpace.allSpaceChildRoomIds;
adminRoomIds.addAll(adminSpaceRooms);
@ -59,7 +59,7 @@ extension GeneralInfoClientExtension on Client {
final Event? originalEvent = await room!.getEventById(edittedEventId);
if (originalEvent == null) return [];
final Timeline timeline = await room.getTimeline();
final Timeline timeline = room.timeline ?? await room.getTimeline();
final List<Event> editEvents = originalEvent
.aggregatedEvents(
timeline,

@ -320,7 +320,7 @@ extension EventsRoomExtension on Room {
"In messageListForChat with room that is not a chat",
);
}
final Timeline timeline = await getTimeline();
final Timeline timeline = this.timeline ?? await getTimeline();
while (timeline.canRequestHistory && numberOfSearches < 50) {
await timeline.requestHistory(historyCount: 100);
@ -433,7 +433,7 @@ extension EventsRoomExtension on Room {
}) async {
try {
int numberOfSearches = 0;
final Timeline timeline = await getTimeline();
final Timeline timeline = this.timeline ?? await getTimeline();
List<Event> relevantEvents() => timeline.events
.where((event) => event.senderId == sender && event.type == type)

@ -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');
}

@ -166,7 +166,7 @@ class ConstructListViewState extends State<ConstructListView> {
if (_timelinesCache.containsKey(use.chatId)) {
timeline = _timelinesCache[use.chatId];
} else {
timeline = await msgRoom.getTimeline();
timeline = msgRoom.timeline ?? await msgRoom.getTimeline();
_timelinesCache[use.chatId] = timeline;
}

@ -107,8 +107,7 @@ class FindPartnerController extends State<FindPartner> {
UserProfileSearchResponse response;
try {
final String accessToken =
await pangeaController.userController.accessToken;
final String accessToken = pangeaController.userController.accessToken;
response = await PUserRepo.searchUserProfiles(
accessToken: accessToken,
targetLanguage: targetLanguageSearch.langCode,

@ -1,4 +1,3 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart';
@ -32,15 +31,15 @@ class SettingsLearningView extends StatelessWidget {
const SizedBox(height: 8),
const Divider(height: 1),
const SizedBox(height: 8),
if (controller.pangeaController.permissionsController.isUser18())
SwitchListTile.adaptive(
activeColor: AppConfig.activeToggleColor,
title: Text(L10n.of(context)!.publicProfileTitle),
subtitle: Text(L10n.of(context)!.publicProfileDesc),
value: controller.pangeaController.userController.isPublic,
onChanged: (bool isPublicProfile) =>
controller.setPublicProfile(isPublicProfile),
),
// if (controller.pangeaController.permissionsController.isUser18())
// SwitchListTile.adaptive(
// activeColor: AppConfig.activeToggleColor,
// title: Text(L10n.of(context)!.publicProfileTitle),
// subtitle: Text(L10n.of(context)!.publicProfileDesc),
// value: controller.pangeaController.userController.isPublic,
// onChanged: (bool isPublicProfile) =>
// controller.setPublicProfile(isPublicProfile),
// ),
ListTile(
subtitle: Text(L10n.of(context)!.toggleToolSettingsDescription),
),

@ -2,7 +2,6 @@ import 'dart:convert';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:http/http.dart';
import '../models/user_model.dart';
@ -11,33 +10,33 @@ import '../network/requests.dart';
import '../network/urls.dart';
class PUserRepo {
static Future<PangeaProfileResponse?> repoCreatePangeaUser({
required String userID,
required String dob,
required fullName,
required String matrixAccessToken,
}) async {
try {
final Requests req = Requests(
baseUrl: PApiUrls.baseAPI,
matrixAccessToken: matrixAccessToken,
);
// static Future<PangeaProfileResponse?> repoCreatePangeaUser({
// required String userID,
// required String dob,
// required fullName,
// required String matrixAccessToken,
// }) async {
// try {
// final Requests req = Requests(
// baseUrl: PApiUrls.baseAPI,
// matrixAccessToken: matrixAccessToken,
// );
final Map<String, dynamic> body = {
ModelKey.userFullName: fullName,
ModelKey.userPangeaUserId: userID,
ModelKey.userDateOfBirth: dob,
};
final resp = await req.post(
url: PApiUrls.createUser,
body: body,
);
return PangeaProfileResponse.fromJson(jsonDecode(resp.body));
} catch (err, s) {
ErrorHandler.logError(e: err, s: s);
return null;
}
}
// final Map<String, dynamic> body = {
// ModelKey.userFullName: fullName,
// ModelKey.userPangeaUserId: userID,
// ModelKey.userDateOfBirth: dob,
// };
// final resp = await req.post(
// url: PApiUrls.createUser,
// body: body,
// );
// return PangeaProfileResponse.fromJson(jsonDecode(resp.body));
// } catch (err, s) {
// ErrorHandler.logError(e: err, s: s);
// return null;
// }
// }
static Future<PangeaProfileResponse?> fetchPangeaUserInfo({
required String userID,

@ -36,9 +36,10 @@ class GetChatListItemSubtitle {
eventContextId = null;
}
final Timeline timeline = await event.room.getTimeline(
eventContextId: eventContextId,
);
final Timeline timeline = event.room.timeline != null &&
event.room.timeline!.chunk.eventsMap.containsKey(eventContextId)
? event.room.timeline!
: await event.room.getTimeline(eventContextId: eventContextId);
if (moveBackInTimeline(event)) {
event = timeline.events.firstWhereOrNull((e) => !moveBackInTimeline(e));

@ -59,7 +59,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
oldSelectedText = widget.selection.selectedText;
final String accessToken =
await MatrixState.pangeaController.userController.accessToken;
MatrixState.pangeaController.userController.accessToken;
final resp = await FullTextTranslationRepo.translate(
accessToken: accessToken,

@ -20,6 +20,9 @@ class ConversationBotCustomSystemPromptInput extends StatelessWidget {
final TextEditingController textFieldController =
TextEditingController(text: customSystemPrompt);
final GlobalKey<FormState> customSystemPromptFormKey =
GlobalKey<FormState>();
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,
);
}
}

@ -1,5 +1,7 @@
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_system_prompt_input.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -16,35 +18,14 @@ class ConversationBotCustomZone extends StatelessWidget {
@override
Widget build(BuildContext context) {
print(initialBotOptions.toJson());
return Column(
children: [
const SizedBox(height: 12),
Text(
L10n.of(context)!.conversationBotCustomZone_title,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
const Divider(
color: Colors.grey,
thickness: 1,
ConversationBotDynamicZoneTitle(
title: L10n.of(context)!.conversationBotCustomZone_title,
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 0, 0),
child: Text(
L10n.of(context)!
.conversationBotCustomZone_customSystemPromptLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
ConversationBotDynamicZoneLabel(
label: L10n.of(context)!
.conversationBotCustomZone_customSystemPromptLabel,
),
Padding(
padding: const EdgeInsets.all(8),

@ -1,6 +1,8 @@
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_keywords_input.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_discussion_topic_input.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -19,32 +21,12 @@ class ConversationBotDiscussionZone extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 12),
Text(
L10n.of(context)!.conversationBotDiscussionZone_title,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
const Divider(
color: Colors.grey,
thickness: 1,
ConversationBotDynamicZoneTitle(
title: L10n.of(context)!.conversationBotDiscussionZone_title,
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 0, 0),
child: Text(
L10n.of(context)!
.conversationBotDiscussionZone_discussionTopicLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
ConversationBotDynamicZoneLabel(
label: L10n.of(context)!
.conversationBotDiscussionZone_discussionTopicLabel,
),
Padding(
padding: const EdgeInsets.all(8),
@ -54,19 +36,9 @@ class ConversationBotDiscussionZone extends StatelessWidget {
),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 0, 0),
child: Text(
L10n.of(context)!
.conversationBotDiscussionZone_discussionKeywordsLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
ConversationBotDynamicZoneLabel(
label: L10n.of(context)!
.conversationBotDiscussionZone_discussionKeywordsLabel,
),
Padding(
padding: const EdgeInsets.all(8),

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class ConversationBotDynamicZoneLabel extends StatelessWidget {
final String label;
const ConversationBotDynamicZoneLabel({
super.key,
required this.label,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 0, 0),
child: Text(
label,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
);
}
}

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
class ConversationBotDynamicZoneTitle extends StatelessWidget {
final String title;
const ConversationBotDynamicZoneTitle({
super.key,
required this.title,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 12),
Text(
title,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
const Divider(
color: Colors.grey,
thickness: 1,
),
const SizedBox(height: 12),
],
);
}
}

@ -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(

@ -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(

@ -1,22 +1,19 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/utils/bot_name.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart';
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_settings_form.dart';
import 'package:fluffychat/widgets/matrix.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/pangea_room_extension.dart';
import '../../utils/error_handler.dart';
class ConversationBotSettings extends StatefulWidget {
final Room? room;
final bool startOpen;
@ -36,6 +33,7 @@ class ConversationBotSettings extends StatefulWidget {
class ConversationBotSettingsState extends State<ConversationBotSettings> {
late BotOptionsModel botOptions;
late bool isOpen;
late bool isCreating;
bool addBot = false;
Room? parentSpace;
@ -56,6 +54,22 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
parentSpace = widget.activeSpaceId != null
? Matrix.of(context).client.getRoomById(widget.activeSpaceId!)
: null;
isCreating = widget.room == null;
}
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);
}
}
Future<void> updateBotOption(void Function() makeLocalChange) async {
@ -74,196 +88,191 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
);
}
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(
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(
L10n.of(context)!.convoBotSettingsTitle,
isCreating
? L10n.of(context)!.addConversationBot
: L10n.of(context)!.botConfig,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.convoBotSettingsDescription),
subtitle: isCreating
? Text(L10n.of(context)!.addConversationBotDesc)
: null,
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,
child: const BotFace(
width: 30.0,
expression: BotExpression.idle,
),
),
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: ListTile(
title: Text(
L10n.of(context)!.addConversationBot,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(L10n.of(context)!.addConversationBotDesc),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor:
Theme.of(context).textTheme.bodyLarge!.color,
child: const BotFace(
width: 30.0,
expression: BotExpression.idle,
),
),
trailing: ElevatedButton(
onPressed: () async {
final bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: addBot
trailing: isCreating
? ElevatedButton(
onPressed: () async {
final bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: addBot
? Text(
L10n.of(context)!
.addConversationBotButtonTitleRemove,
)
: Text(
L10n.of(context)!
.addConversationBotDialogTitleInvite,
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(L10n.of(context)!.cancel),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(!addBot);
},
child: addBot
? Text(
L10n.of(context)!
.addConversationBotButtonTitleRemove,
.addConversationBotDialogRemoveConfirmation,
)
: Text(
L10n.of(context)!
.addConversationBotDialogTitleInvite,
.addConversationBotDialogInviteConfirmation,
),
),
],
);
},
);
if (confirm == true) {
setState(() => addBot = true);
widget.room?.invite(BotName.byEnvironment);
} else {
setState(() => addBot = false);
widget.room?.kick(BotName.byEnvironment);
}
},
child: addBot
? Text(
L10n.of(context)!.addConversationBotButtonRemove,
)
: Text(
L10n.of(context)!.addConversationBotButtonInvite,
),
)
: const Icon(Icons.settings),
onTap: isCreating
? null
: () async {
final bool? confirm = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Text(
L10n.of(context)!.botConfig,
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.fromLTRB(0, 0, 0, 12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
L10n.of(context)!.conversationBotStatus,
),
Switch(
value: addBot,
onChanged: (value) {
setState(
() => addBot = value,
);
},
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(L10n.of(context)!.cancel),
],
),
TextButton(
onPressed: () {
Navigator.of(context).pop(!addBot);
},
child: addBot
? Text(
L10n.of(context)!
.addConversationBotDialogRemoveConfirmation,
)
: Text(
L10n.of(context)!
.addConversationBotDialogInviteConfirmation,
),
if (addBot)
Flexible(
child: SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context)
.colorScheme
.secondary,
width: 0.5,
),
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
child: ConversationBotSettingsForm(
botOptions: botOptions,
),
),
),
),
],
);
},
);
if (confirm == true) {
setState(() => addBot = true);
widget.room?.invite(BotName.byEnvironment);
} else {
setState(() => addBot = false);
widget.room?.kick(BotName.byEnvironment);
}
},
child: addBot
? Text(
L10n.of(context)!
.addConversationBotButtonRemove,
)
: Text(
L10n.of(context)!
.addConversationBotButtonInvite,
],
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(L10n.of(context)!.cancel),
),
),
),
),
if (addBot) ...[
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!;
}),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(32, 16, 0, 0),
child: Text(
L10n.of(context)!.conversationBotModeSelectDescription,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16),
child: ConversationBotModeSelect(
initialMode: botOptions.mode,
onChanged: (String? mode) => updateBotOption(
() {
botOptions.mode = mode ?? "discussion";
},
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(28, 0, 12, 0),
child: ConversationBotModeDynamicZone(
initialBotOptions: botOptions,
onChanged: (BotOptionsModel? newOptions) {
updateBotOption(() {
if (newOptions != null) {
botOptions = newOptions;
}
});
},
),
),
const SizedBox(height: 16),
],
],
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(
L10n.of(context)!
.conversationBotConfigConfirmChange,
),
),
],
),
);
},
);
if (confirm == true) {
if (addBot) {
await widget.room?.invite(BotName.byEnvironment);
} else {
await widget.room?.kick(BotName.byEnvironment);
}
updateBotOption(() {
botOptions = botOptions;
});
}
},
),
if (isCreating && addBot)
ConversationBotSettingsForm(
botOptions: botOptions,
),
],
);
),
);
}
}

@ -0,0 +1,88 @@
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart';
import 'package:fluffychat/pangea/widgets/space/language_level_dropdown.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ConversationBotSettingsForm extends StatefulWidget {
final BotOptionsModel botOptions;
const ConversationBotSettingsForm({
super.key,
required this.botOptions,
});
@override
ConversationBotSettingsFormState createState() =>
ConversationBotSettingsFormState();
}
class ConversationBotSettingsFormState
extends State<ConversationBotSettingsForm> {
final formKey = GlobalKey<FormState>();
late BotOptionsModel botOptions;
@override
void initState() {
super.initState();
botOptions = widget.botOptions;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.conversationLanguageLevel,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
LanguageLevelDropdown(
initialLevel: botOptions.languageLevel,
onChanged: (int? newValue) => {
setState(() {
botOptions.languageLevel = newValue!;
}),
},
),
Text(
L10n.of(context)!.conversationBotModeSelectDescription,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
ConversationBotModeSelect(
initialMode: botOptions.mode,
onChanged: (String? mode) => {
setState(() {
botOptions.mode = mode ?? "discussion";
}),
},
),
Padding(
padding: const EdgeInsets.all(12),
child: ConversationBotModeDynamicZone(
initialBotOptions: botOptions,
onChanged: (BotOptionsModel? newOptions) {
if (newOptions != null) {
setState(() {
botOptions = newOptions;
});
}
},
),
),
],
);
}
}

@ -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<FormState> gameMasterInstructionsFormKey =
GlobalKey<FormState>();
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,
),
);
}
}

@ -1,15 +1,39 @@
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_label.dart';
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_dynamic_zone_title.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'),
ConversationBotDynamicZoneTitle(
title: L10n.of(context)!.conversationBotTextAdventureZone_title,
),
ConversationBotDynamicZoneLabel(
label: L10n.of(context)!
.conversationBotTextAdventureZone_instructionLabel,
),
Padding(
padding: const EdgeInsets.all(8),
child: ConversationBotGameMasterInstructionsInput(
initialBotOptions: initialBotOptions,
onChanged: onChanged,
),
),
],
);
}

@ -27,7 +27,7 @@ class PangeaTextController extends TextEditingController {
}
static const int maxLength = 1000;
bool get isMaxLength => text.length == 1000;
bool get exceededMaxLength => text.length >= maxLength;
bool forceKeepOpen = false;

@ -312,7 +312,7 @@ class GenerateVocabButtonState extends State<GenerateVocabButton> {
Future<List<Lemma>> _getWords() async {
final ChatTopic topic = await TopicDataRepo.generate(
await _pangeaController.userController.accessToken,
_pangeaController.userController.accessToken,
request: TopicDataRequest(
topicInfo: widget.topic,
numWords: 10,
@ -514,7 +514,7 @@ class PromptsFieldState extends State<PromptsField> {
Future<List<DiscussionPrompt>> _getPrompts() async {
final ChatTopic res = await TopicDataRepo.generate(
await _pangeaController.userController.accessToken,
_pangeaController.userController.accessToken,
request: TopicDataRequest(
topicInfo: widget.topic,
numPrompts: 10,

Loading…
Cancel
Save