feat(activity_planner): full draft done (#1542)

* feat(activity_planner): mvp done

* refactor(activity_planner): move launch icon button to file

* dev: dart formatting

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
This commit is contained in:
wcjord 2025-01-22 17:00:48 -05:00 committed by GitHub
parent cbf9bd22ee
commit b81f3841f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1043 additions and 47 deletions

View file

@ -4749,5 +4749,23 @@
},
"notInClass": "Not in a class!",
"noClassCode": "No class code!",
"chooseCorrectLabel": "Choose the correct label"
"chooseCorrectLabel": "Choose the correct label",
"activityPlannerTitle": "Activity Planner",
"topicLabel": "Topic",
"topicPlaceholder": "Choose a topic...",
"modeLabel": "Mode",
"modePlaceholder": "Choose a mode...",
"learningObjectiveLabel": "Learning Objective",
"learningObjectivePlaceholder": "Choose a learning objective...",
"mediaLabel": "Media students should share",
"languageOfInstructionsLabel": "Language of activity instructions",
"targetLanguageLabel": "Target language",
"cefrLevelLabel": "CEFR Level",
"generateActivitiesButton": "Generate Activities",
"launchActivityButton": "Launch Activity",
"image": "Image",
"video": "Video",
"voiceMessage": "Voice message",
"nan": "Not applicable",
"activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!"
}

View file

@ -16,6 +16,7 @@ import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_page_launch_icon_button.dart';
import 'package:fluffychat/pangea/analytics/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics/widgets/gain_points.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart';
@ -127,6 +128,7 @@ class ChatView extends StatelessWidget {
context.go('/rooms/${controller.room.id}/search');
},
),
ActivityPlanPageLaunchIconButton(controller: controller),
IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: L10n.of(context).chatDetails,

View file

@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../common/network/requests.dart';
class ActivityModeListRepo {
static final GetStorage _modeListStorage = GetStorage('mode_list_storage');
static void set(
ActivitySettingRequestSchema request,
List<ActivitySettingResponseSchema> response,
) {
_modeListStorage.write(
request.storageKey,
response.map((e) => e.toJson()).toList(),
);
}
static List<ActivitySettingResponseSchema> fromJson(Iterable json) {
return List<ActivitySettingResponseSchema>.from(
json.map((x) => ActivitySettingResponseSchema.fromJson(x)),
);
}
static Future<List<ActivitySettingResponseSchema>> get(
ActivitySettingRequestSchema request,
) async {
final cachedJson = _modeListStorage.read(request.storageKey);
if (cachedJson != null) {
return ActivityModeListRepo.fromJson(cachedJson);
}
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.activityModeList,
body: request.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = ActivityModeListRepo.fromJson(decodedBody);
set(request, response);
return response;
}
}

View file

@ -0,0 +1,122 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../common/network/requests.dart';
class ActivityPlanRequest {
final String topic;
final String mode;
final String objective;
final MediaEnum media;
final int cefrLevel;
final String languageOfInstructions;
final String targetLanguage;
final int count;
ActivityPlanRequest({
required this.topic,
required this.mode,
required this.objective,
required this.media,
required this.cefrLevel,
required this.languageOfInstructions,
required this.targetLanguage,
this.count = 3,
});
Map<String, dynamic> toJson() {
return {
'topic': topic,
'mode': mode,
'objective': objective,
'media': media.string,
'cefr_level': cefrLanguageLevel,
'language_of_instructions': languageOfInstructions,
'target_language': targetLanguage,
'count': count,
};
}
String get storageKey =>
'$topic-$mode-$objective-${media.string}-$cefrLevel-$languageOfInstructions-$targetLanguage';
String get cefrLanguageLevel {
switch (cefrLevel) {
case 0:
return 'Pre-A1';
case 1:
return 'A1';
case 2:
return 'A2';
case 3:
return 'B1';
case 4:
return 'B2';
case 5:
return 'C1';
case 6:
return 'C2';
default:
return 'Pre-A1';
}
}
}
class ActivityPlanResponse {
final List<String> activityPlans;
ActivityPlanResponse({required this.activityPlans});
factory ActivityPlanResponse.fromJson(Map<String, dynamic> json) {
return ActivityPlanResponse(
activityPlans: List<String>.from(json['activity_plans']),
);
}
Map<String, dynamic> toJson() {
return {
'activity_plans': activityPlans,
};
}
}
class ActivityPlanGenerationRepo {
static final GetStorage _activityPlanStorage =
GetStorage('activity_plan_storage');
static void set(ActivityPlanRequest request, ActivityPlanResponse response) {
_activityPlanStorage.write(request.storageKey, response.toJson());
}
static Future<ActivityPlanResponse> get(ActivityPlanRequest request) async {
final cachedJson = _activityPlanStorage.read(request.storageKey);
if (cachedJson != null) {
final cached = ActivityPlanResponse.fromJson(cachedJson);
return cached;
}
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.activityPlanGeneration,
body: request.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = ActivityPlanResponse.fromJson(decodedBody);
set(request, response);
return response;
}
}

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
class ActivityPlanPageLaunchIconButton extends StatelessWidget {
const ActivityPlanPageLaunchIconButton({
super.key,
required this.controller,
});
final ChatController controller;
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.event_note_outlined),
tooltip: L10n.of(context).activityPlannerTitle,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ActivityPlannerPage(room: controller.room),
),
);
},
);
}
}

View file

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ActivityPlanTile extends StatefulWidget {
final String activity;
final VoidCallback onLaunch;
final ValueChanged<String> onEdit;
const ActivityPlanTile({
super.key,
required this.activity,
required this.onLaunch,
required this.onEdit,
});
@override
ActivityPlanTileState createState() => ActivityPlanTileState();
}
class ActivityPlanTileState extends State<ActivityPlanTile> {
late TextEditingController _controller;
bool editMode = false;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.activity);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (editMode)
TextField(
controller: _controller,
onChanged: widget.onEdit,
maxLines: null,
)
else
Text(
widget.activity,
maxLines: null,
overflow: TextOverflow.visible,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () {
setState(() {
editMode = !editMode;
});
},
child: Text(!editMode ? l10n.edit : l10n.cancel),
),
const SizedBox(width: 8.0),
ElevatedButton(
onPressed: !editMode
? widget.onLaunch
: () {
setState(() {
widget.onEdit(_controller.text);
editMode = !editMode;
});
},
child: Text(
!editMode ? l10n.launchActivityButton : l10n.saveChanges,
),
),
],
),
],
),
),
);
}
}

View file

@ -0,0 +1,466 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_mode_list_repo.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_tile.dart';
import 'package:fluffychat/pangea/activity_planner/learning_objective_list_repo.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
import 'package:fluffychat/pangea/activity_planner/topic_list_repo.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
// a page to allow the user to choose settings and then generate a list of activities
// has app bar with a back button to go back to content 1 (disabled if on content 1), and a title of "Activity Planner", and close button to close the activity planner
// content 1 - settings
// content 2 - display of activities generated by the system, allowing edit and selection
// use standard flutter material widgets and theme colors/styles. all copy should be defined in intl_en.arb and used with L10n.of(context).copyKey
// content 1
// should have a maxWidth, pulled from appconfig
// a. topic input with drop-down of suggestions pulled from TopicListRepo. label text of "topic" and placeholder of some random suggestions from repo
// b. mode input with drop-down of suggestions pulled from ModeListRepo. label text of "mode" and placeholder of some random suggestions from repo
// c. objective input with drop-down of suggestions pulled from LearningObjectiveListRepo. label text of "learning objective" and placeholder of some random suggestions from repo.
// e. dropdown for media type with text "media students should share as part of the activity"
// d. dropdown for selecting "language of activity instructions" which is auto-populated with the user's l1 but can be changed with options coming from pangeaController.pLanguageStore.baseOptions
// f. dropdown for selecting "target language" which is auto-populated with the user's l2 but can be changed with options coming from pangeaController.pLanguageStore.targetOptions
// g. selection for language level
// h. button to generate activities
// content 2
// a. app bar with a back button to go back to content 1, and a title of "Activity Planner", and close button to close the activity planner
// b. display of activities generated by the system, arranged in a column. if there is enough horizontal space, the activities should be arranged in a row
// a1. each activity should have a button to "launch activity" which calls a callback. this can be blank for now.
// a2. each activity should have a button to edit the activity. upon edit, the activity should become an input form where the user can freely edit the activity content
enum _PageMode {
settings,
activities,
}
class ActivityPlannerPage extends StatefulWidget {
final Room room;
const ActivityPlannerPage({super.key, required this.room});
@override
ActivityPlannerPageState createState() => ActivityPlannerPageState();
}
class ActivityPlannerPageState extends State<ActivityPlannerPage> {
final _formKey = GlobalKey<FormState>();
/// Index of the content to display
_PageMode _pageMode = _PageMode.settings;
/// Selected values from the form
String? _selectedTopic;
String? _selectedMode;
String? _selectedObjective;
MediaEnum _selectedMedia = MediaEnum.nan;
String? _selectedLanguageOfInstructions;
String? _selectedTargetLanguage;
int? _selectedCefrLevel;
/// fetch data from repos
List<ActivitySettingResponseSchema> _topicItems = [];
List<ActivitySettingResponseSchema> _modeItems = [];
List<ActivitySettingResponseSchema> _objectiveItems = [];
/// List of activities generated by the system
List<String> _activities = [];
final _topicSearchController = TextEditingController();
final _objectiveSearchController = TextEditingController();
final List<TextEditingController> _activityControllers = [];
@override
void initState() {
super.initState();
_loadDropdownData();
_selectedLanguageOfInstructions =
MatrixState.pangeaController.languageController.userL1?.langCode;
_selectedTargetLanguage =
MatrixState.pangeaController.languageController.userL2?.langCode;
_selectedCefrLevel = 0;
// Initialize controllers for activity editing
for (final activity in _activities) {
_activityControllers.add(TextEditingController(text: activity));
}
}
@override
void dispose() {
_topicSearchController.dispose();
_objectiveSearchController.dispose();
disposeAndClearActivityControllers();
super.dispose();
}
void disposeAndClearActivityControllers() {
for (final controller in _activityControllers) {
controller.dispose();
}
_activityControllers.clear();
}
ActivitySettingRequestSchema get req => ActivitySettingRequestSchema(
langCode:
MatrixState.pangeaController.languageController.userL2?.langCode ??
LanguageKeys.defaultLanguage,
);
Future<void> _loadDropdownData() async {
final topics = await TopicListRepo.get(req);
final modes = await ActivityModeListRepo.get(req);
final objectives = await LearningObjectiveListRepo.get(req);
setState(() {
_topicItems = topics;
_modeItems = modes;
_objectiveItems = objectives;
});
}
// send the activity as a message to the room
Future<void> onLaunch(int index) => showFutureLoadingDialog(
context: context,
future: () async {
// this shouldn't often error but just in case since it's not necessary for the activity to be sent
late List<PangeaToken> tokens;
try {
tokens = await MatrixState.pangeaController.messageData.getTokens(
repEventId: null,
req: TokensRequestModel(
fullText: _activities[index],
langCode: _selectedLanguageOfInstructions!,
senderL1: _selectedLanguageOfInstructions!,
senderL2: _selectedLanguageOfInstructions!,
),
room: null,
);
} catch (e) {
debugger(when: kDebugMode);
}
final eventId = await widget.room.pangeaSendTextEvent(
_activities[index],
messageTag: ModelKey.messageTagActivityPlan,
originalSent: PangeaRepresentation(
langCode: _selectedLanguageOfInstructions!,
text: _activities[index],
originalSent: true,
originalWritten: false,
),
tokensSent: PangeaMessageTokens(tokens: tokens),
);
if (eventId == null) {
debugger(when: kDebugMode);
return;
}
await widget.room.setPinnedEvents([eventId]);
Navigator.of(context).pop();
},
);
Future<void> _generateActivities() async {
if (_formKey.currentState?.validate() ?? false) {
final request = ActivityPlanRequest(
topic: _selectedTopic!,
mode: _selectedMode!,
objective: _selectedObjective!,
media: _selectedMedia,
languageOfInstructions: _selectedLanguageOfInstructions!,
targetLanguage: _selectedTargetLanguage!,
cefrLevel: _selectedCefrLevel!,
);
await showFutureLoadingDialog(
context: context,
future: () async {
final response = await ActivityPlanGenerationRepo.get(request);
setState(() {
_activities = response.activityPlans;
disposeAndClearActivityControllers();
for (final activity in _activities) {
_activityControllers.add(TextEditingController(text: activity));
}
_pageMode = _PageMode.activities;
});
},
);
}
}
void _randomizeSelections() {
setState(() {
_selectedTopic = (_topicItems..shuffle()).first.name;
_selectedObjective = (_objectiveItems..shuffle()).first.name;
_selectedMode = (_modeItems..shuffle()).first.name;
});
}
// Add validation logic
String? _validateNotNull(String? value) {
if (value == null || value.isEmpty) {
return L10n.of(context).interactiveTranslatorRequired;
}
return null;
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return Scaffold(
appBar: AppBar(
leading: _pageMode == _PageMode.settings
? IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
)
: IconButton(
onPressed: () => setState(() => _pageMode = _PageMode.settings),
icon: const Icon(Icons.arrow_back),
),
title: Text(l10n.activityPlannerTitle),
),
body: _pageMode == _PageMode.settings
? _buildSettingsForm(l10n)
: _buildActivitiesView(l10n),
);
}
Widget _buildSettingsForm(L10n l10n) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.activityPlannerOverview,
),
DropdownButtonFormField2<String>(
hint: Text(l10n.topicPlaceholder),
value: _selectedTopic,
decoration: _selectedTopic != null
? InputDecoration(
labelText: l10n.topicLabel,
)
: null,
isExpanded: true,
validator: (value) => _validateNotNull(value),
dropdownSearchData: DropdownSearchData(
searchController: _topicSearchController,
searchInnerWidget: InnerSearchWidget(
searchController: _topicSearchController,
),
searchInnerWidgetHeight: 60,
searchMatchFn: (item, searchValue) {
return item.value
.toString()
.toLowerCase()
.contains(searchValue.toLowerCase());
},
),
items: _topicItems
.map(
(e) => DropdownMenuItem(
value: e.name,
child: Text(e.name),
),
)
.toList(),
onChanged: (value) {
_selectedTopic = value;
},
dropdownStyleData: const DropdownStyleData(maxHeight: 400),
),
const SizedBox(height: 24),
DropdownButtonFormField2<String>(
hint: Text(l10n.learningObjectivePlaceholder),
decoration: _selectedObjective != null
? InputDecoration(labelText: l10n.learningObjectiveLabel)
: null,
validator: (value) => _validateNotNull(value),
items: _objectiveItems
.map(
(e) =>
DropdownMenuItem(value: e.name, child: Text(e.name)),
)
.toList(),
onChanged: (val) => _selectedObjective = val,
dropdownStyleData: const DropdownStyleData(maxHeight: 400),
value: _selectedObjective,
dropdownSearchData: DropdownSearchData(
searchController: _objectiveSearchController,
searchInnerWidget: InnerSearchWidget(
searchController: _objectiveSearchController,
),
searchInnerWidgetHeight: 60,
searchMatchFn: (item, searchValue) {
return item.value
.toString()
.toLowerCase()
.contains(searchValue.toLowerCase());
},
),
),
const SizedBox(height: 24),
DropdownButtonFormField2<String>(
decoration: _selectedMode != null
? InputDecoration(labelText: l10n.modeLabel)
: null,
hint: Text(l10n.modePlaceholder),
validator: (value) => _validateNotNull(value),
items: _modeItems
.map(
(e) =>
DropdownMenuItem(value: e.name, child: Text(e.name)),
)
.toList(),
onChanged: (val) => _selectedMode = val,
value: _selectedMode,
),
const SizedBox(height: 24),
DropdownButtonFormField2<MediaEnum>(
decoration: InputDecoration(labelText: l10n.mediaLabel),
items: MediaEnum.values
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.toDisplayCopyUsingL10n(context)),
),
)
.toList(),
onChanged: (val) => _selectedMedia = val ?? MediaEnum.nan,
value: _selectedMedia,
),
const SizedBox(height: 24),
LanguageLevelDropdown(
initialLevel: 0,
onChanged: (val) => _selectedCefrLevel = val,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages:
MatrixState.pangeaController.pLanguageStore.baseOptions,
onChange: (val) => _selectedTargetLanguage = val.langCode,
initialLanguage:
MatrixState.pangeaController.languageController.userL1,
isL2List: false,
decorationText: L10n.of(context).languageOfInstructionsLabel,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages:
MatrixState.pangeaController.pLanguageStore.targetOptions,
onChange: (val) => _selectedTargetLanguage = val.langCode,
initialLanguage:
MatrixState.pangeaController.languageController.userL2,
decorationText: L10n.of(context).targetLanguageLabel,
isL2List: true,
),
const SizedBox(height: 24),
Row(
children: [
IconButton(
icon: const Icon(Icons.shuffle),
onPressed: _randomizeSelections,
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _generateActivities,
child: Text(l10n.generateActivitiesButton),
),
),
],
),
],
),
),
),
);
}
Widget _buildActivitiesView(L10n l10n) {
return ListView(
padding: const EdgeInsets.all(16),
children: _activities.asMap().entries.map((entry) {
final index = entry.key;
return ActivityPlanTile(
activity: _activities[index],
onLaunch: () => onLaunch(index),
onEdit: (val) {
setState(() {
_activities[index] = val;
});
},
);
}).toList(),
);
}
}
class InnerSearchWidget extends StatelessWidget {
const InnerSearchWidget({
super.key,
required TextEditingController searchController,
}) : _objectiveSearchController = searchController;
final TextEditingController _objectiveSearchController;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
controller: _objectiveSearchController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: L10n.of(context).search,
icon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
);
}
}

View file

@ -0,0 +1,57 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../common/network/requests.dart';
class LearningObjectiveListRepo {
static final GetStorage _objectiveListStorage =
GetStorage('objective_list_storage');
static void set(
ActivitySettingRequestSchema request,
List<ActivitySettingResponseSchema> response,
) {
_objectiveListStorage.write(
request.storageKey,
response.map((e) => e.toJson()).toList(),
);
}
static List<ActivitySettingResponseSchema> fromJson(Iterable json) {
return List<ActivitySettingResponseSchema>.from(
json.map((x) => ActivitySettingResponseSchema.fromJson(x)),
);
}
static Future<List<ActivitySettingResponseSchema>> get(
ActivitySettingRequestSchema request,
) async {
final cachedJson = _objectiveListStorage.read(request.storageKey);
if (cachedJson != null) {
return LearningObjectiveListRepo.fromJson(cachedJson);
}
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.objectiveList,
body: request.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = LearningObjectiveListRepo.fromJson(decodedBody);
set(request, response);
return response;
}
}

View file

@ -0,0 +1,37 @@
class ActivitySettingRequestSchema {
final String langCode;
ActivitySettingRequestSchema({required this.langCode});
Map<String, dynamic> toJson() {
return {
'lang_code': langCode,
};
}
String get storageKey => 'topic_list-$langCode';
}
class ActivitySettingResponseSchema {
final String defaultName;
final String name;
ActivitySettingResponseSchema({
required this.defaultName,
required this.name,
});
factory ActivitySettingResponseSchema.fromJson(Map<String, dynamic> json) {
return ActivitySettingResponseSchema(
defaultName: json['default_name'],
name: json['name'],
);
}
Map<String, dynamic> toJson() {
return {
'default_name': defaultName,
'name': name,
};
}
}

View file

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum MediaEnum {
images,
videos,
voiceMessages,
nan,
}
extension MediaEnumExtension on MediaEnum {
//fromString
static MediaEnum fromString(String value) {
switch (value) {
case 'images':
return MediaEnum.images;
case 'videos':
return MediaEnum.videos;
case 'voice_messages':
return MediaEnum.voiceMessages;
case 'nan':
return MediaEnum.nan;
default:
return MediaEnum.nan;
}
}
String get string {
switch (this) {
case MediaEnum.images:
return 'images';
case MediaEnum.videos:
return 'videos';
case MediaEnum.voiceMessages:
return 'voice_messages';
case MediaEnum.nan:
return 'nan';
}
}
//toDisplayCopyUsingL10n
String toDisplayCopyUsingL10n(BuildContext context) {
switch (this) {
case MediaEnum.images:
return L10n.of(context).image;
case MediaEnum.videos:
return L10n.of(context).video;
case MediaEnum.voiceMessages:
return L10n.of(context).voiceMessage;
case MediaEnum.nan:
return L10n.of(context).nan;
}
}
}

View file

@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../common/network/requests.dart';
class TopicListRepo {
static final GetStorage _topicListStorage = GetStorage('topic_list_storage');
static void set(
ActivitySettingRequestSchema request,
List<ActivitySettingResponseSchema> response,
) {
_topicListStorage.write(
request.storageKey,
response.map((e) => e.toJson()).toList(),
);
}
static List<ActivitySettingResponseSchema> fromJson(Iterable json) {
return List<ActivitySettingResponseSchema>.from(
json.map((x) => ActivitySettingResponseSchema.fromJson(x)),
);
}
static Future<List<ActivitySettingResponseSchema>> get(
ActivitySettingRequestSchema request,
) async {
final cachedJson = _topicListStorage.read(request.storageKey);
if (cachedJson != null) {
return TopicListRepo.fromJson(cachedJson);
}
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.topicList,
body: request.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = TopicListRepo.fromJson(decodedBody);
set(request, response);
return response;
}
}

View file

@ -23,6 +23,7 @@ class LanguageLevelDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DropdownButtonFormField2(
decoration: InputDecoration(labelText: L10n.of(context).cefrLevelLabel),
hint: Text(
L10n.of(context).selectLanguageLevel,
overflow: TextOverflow.clip,

View file

@ -87,8 +87,11 @@ class ModelKey {
/// This will help us know to omit the message from notifications,
/// bot responses, etc. It will also help use find the message if
/// we want to gather user edits for LLM fine-tuning.
/// @ggurdin: Maybe this not the way to do this and we should be using
/// something built in to matrix? should talk about this
static const String messageTags = "p.tag";
static const String messageTagMorphEdit = "morph_edit";
static const String messageTagActivityPlan = "activity_plan";
static const String baseDefinition = "base_definition";
static const String targetDefinition = "target_definition";

View file

@ -66,6 +66,12 @@ class PApiUrls {
static String lemmaDictionary = "${PApiUrls.choreoEndpoint}/lemma_definition";
static String activityPlanGeneration =
"${PApiUrls.choreoEndpoint}/activity_plan";
static String activityModeList = "${PApiUrls.choreoEndpoint}/modes";
static String objectiveList = "${PApiUrls.choreoEndpoint}/objectives";
static String topicList = "${PApiUrls.choreoEndpoint}/topics";
///-------------------------------- revenue cat --------------------------
static String rcAppsChoreo = "${PApiUrls.subscriptionEndpoint}/app_ids";
static String rcProductsChoreo =

View file

@ -21,6 +21,7 @@ enum InstructionsEnum {
clickBestOption,
unlockedLanguageTools,
lemmaMeaning,
activityPlannerOverview,
}
extension InstructionsEnumExtension on InstructionsEnum {
@ -36,6 +37,7 @@ extension InstructionsEnumExtension on InstructionsEnum {
return l10n.tooltipInstructionsTitle;
case InstructionsEnum.missingVoice:
return l10n.missingVoiceTitle;
case InstructionsEnum.activityPlannerOverview:
case InstructionsEnum.clickAgainToDeselect:
case InstructionsEnum.speechToText:
case InstructionsEnum.l1Translation:
@ -83,6 +85,8 @@ extension InstructionsEnumExtension on InstructionsEnum {
return l10n.unlockedLanguageTools;
case InstructionsEnum.lemmaMeaning:
return l10n.lemmaMeaningInstructionsBody;
case InstructionsEnum.activityPlannerOverview:
return l10n.activityPlannerOverviewInstructionsBody;
}
}

View file

@ -66,6 +66,7 @@ Future<void> pLanguageDialog(
selectedSourceLanguage ?? LanguageModel.unknown,
languages: pangeaController.pLanguageStore.baseOptions,
isL2List: false,
decorationText: L10n.of(context).myBaseLanguage,
),
PQuestionContainer(
title: L10n.of(context).whatLanguageYouWantToLearn,
@ -76,6 +77,7 @@ Future<void> pLanguageDialog(
initialLanguage: selectedTargetLanguage,
languages: pangeaController.pLanguageStore.targetOptions,
isL2List: true,
decorationText: L10n.of(context).iWantToLearn,
),
],
),

View file

@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart';
@ -15,6 +15,7 @@ class PLanguageDropdown extends StatefulWidget {
final Function(LanguageModel) onChange;
final bool showMultilingual;
final bool isL2List;
final String decorationText;
final String? error;
const PLanguageDropdown({
@ -23,7 +24,8 @@ class PLanguageDropdown extends StatefulWidget {
required this.onChange,
required this.initialLanguage,
this.showMultilingual = false,
required this.isL2List,
required this.decorationText,
this.isL2List = false,
this.error,
});
@ -63,52 +65,30 @@ class _PLanguageDropdownState extends State<PLanguageDropdown> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.secondary,
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(36)),
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: DropdownButton<LanguageModel>(
hint: Row(
children: [
const Icon(Icons.language_outlined),
const SizedBox(width: 10),
Text(
widget.isL2List
? L10n.of(context).iWantToLearn
: L10n.of(context).myBaseLanguage,
),
],
),
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down),
underline: Container(),
items: [
if (widget.showMultilingual)
DropdownMenuItem(
value: LanguageModel.multiLingual(context),
child: LanguageDropDownEntry(
languageModel: LanguageModel.multiLingual(context),
isL2List: widget.isL2List,
),
),
...sortedLanguages.map(
(languageModel) => DropdownMenuItem(
value: languageModel,
child: LanguageDropDownEntry(
languageModel: languageModel,
isL2List: widget.isL2List,
),
DropdownButtonFormField2<LanguageModel>(
decoration: InputDecoration(labelText: widget.decorationText),
isExpanded: true,
items: [
if (widget.showMultilingual)
DropdownMenuItem(
value: LanguageModel.multiLingual(context),
child: LanguageDropDownEntry(
languageModel: LanguageModel.multiLingual(context),
isL2List: widget.isL2List,
),
),
],
onChanged: (value) => widget.onChange(value!),
value: widget.initialLanguage,
),
...sortedLanguages.map(
(languageModel) => DropdownMenuItem(
value: languageModel,
child: LanguageDropDownEntry(
languageModel: languageModel,
isL2List: widget.isL2List,
),
),
),
],
onChanged: (value) => widget.onChange(value!),
value: widget.initialLanguage,
),
AnimatedSize(
duration: FluffyThemes.animationDuration,

View file

@ -94,6 +94,7 @@ class UserSettingsView extends StatelessWidget {
initialLanguage: controller.selectedTargetLanguage,
isL2List: true,
error: controller.selectedLanguageError,
decorationText: L10n.of(context).iWantToLearn,
),
),
if (controller.isSSOSignup)

View file

@ -239,6 +239,9 @@ class LanguageSelectionRow extends StatelessWidget {
initialLanguage: isSource
? controller.sourceLanguageSearch
: controller.targetLanguageSearch,
decorationText: isSource
? L10n.of(context).myBaseLanguage
: L10n.of(context).iWantToLearn,
),
),
],

View file

@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}