You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
18 KiB
Dart
467 lines
18 KiB
Dart
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),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|