1572-feedback-for-activity-planner (#1586)
* feat(activity_planner): major UI and functionality updates * fix(intl_en): did not merge
This commit is contained in:
parent
a377200f5f
commit
3dedcf5bf5
20 changed files with 1025 additions and 576 deletions
|
|
@ -4768,5 +4768,7 @@
|
|||
"voiceMessage": "Voice message",
|
||||
"nan": "Not applicable",
|
||||
"activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!",
|
||||
"completeActivitiesToUnlock": "Complete the highlighted word activities to unlock"
|
||||
"completeActivitiesToUnlock": "Complete the highlighted word activities to unlock",
|
||||
"myBookmarkedActivities": "My Bookmarked Activities",
|
||||
"noBookmarkedActivities": "No bookmarked activities"
|
||||
}
|
||||
|
|
@ -1,12 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/learning_settings/utils/language_list_util.dar
|
|||
import 'package:fluffychat/utils/client_manager.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/error_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'config/setting_keys.dart';
|
||||
import 'utils/background_push.dart';
|
||||
import 'widgets/fluffy_chat_app.dart';
|
||||
|
|
|
|||
147
lib/pangea/activity_planner/activity_list_view.dart
Normal file
147
lib/pangea/activity_planner/activity_list_view.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'activity_plan_card.dart';
|
||||
|
||||
class ActivityListView extends StatefulWidget {
|
||||
final Room? room;
|
||||
|
||||
/// if null, show saved activities
|
||||
final ActivityPlanRequest? activityPlanRequest;
|
||||
|
||||
const ActivityListView({
|
||||
super.key,
|
||||
required this.room,
|
||||
required this.activityPlanRequest,
|
||||
});
|
||||
|
||||
@override
|
||||
_ActivityListViewState createState() => _ActivityListViewState();
|
||||
}
|
||||
|
||||
class _ActivityListViewState extends State<ActivityListView> {
|
||||
bool get showBookmarkedActivities => widget.activityPlanRequest == null;
|
||||
|
||||
Future<List<ActivityPlanModel>> get savedActivities =>
|
||||
Future.value(BookmarkedActivitiesRepo.get());
|
||||
|
||||
Future<List<ActivityPlanModel>> get activities async =>
|
||||
showBookmarkedActivities
|
||||
? await savedActivities
|
||||
: (await ActivityPlanGenerationRepo.get(widget.activityPlanRequest!))
|
||||
.activityPlans;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onEdit(int index, ActivityPlanModel updatedActivity) async {
|
||||
// in this case we're editing an activity plan that was generated recently
|
||||
// via the repo and should be updated in the cached response
|
||||
if (widget.activityPlanRequest != null) {
|
||||
final activities = await this.activities;
|
||||
activities[index] = updatedActivity;
|
||||
ActivityPlanGenerationRepo.set(
|
||||
widget.activityPlanRequest!,
|
||||
ActivityPlanResponse(activityPlans: activities),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _onLaunch(int index) => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final activity = (await activities)[index];
|
||||
|
||||
final eventId = await widget.room?.pangeaSendTextEvent(
|
||||
activity.markdown,
|
||||
messageTag: ModelKey.messageTagActivityPlan,
|
||||
//include full model or should we move to a state event for this?
|
||||
);
|
||||
|
||||
if (eventId == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.room?.setPinnedEvents([eventId]);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return FutureBuilder<List<ActivityPlanModel>>(
|
||||
future: activities,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError) {
|
||||
print(snapshot.error);
|
||||
print(snapshot.stackTrace);
|
||||
// debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: snapshot.error,
|
||||
s: snapshot.stackTrace,
|
||||
data: {
|
||||
'room': widget.room,
|
||||
'activityPlanRequest': widget.activityPlanRequest,
|
||||
'snapshot.data': snapshot.data,
|
||||
'snapshot.error': snapshot.error,
|
||||
},
|
||||
);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(l10n.oopsSomethingWentWrong),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(l10n.tryAgain),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
if (showBookmarkedActivities) {
|
||||
return Center(child: Text(l10n.noBookmarkedActivities));
|
||||
}
|
||||
return Center(child: Text(l10n.noDataFound));
|
||||
} else {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ActivityPlanCard(
|
||||
activity: snapshot.data![index],
|
||||
onLaunch: () => _onLaunch(index),
|
||||
onEdit: (updatedActivity) => _onEdit(index, updatedActivity),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
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 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../common/network/requests.dart';
|
||||
|
||||
class ActivityModeListRepo {
|
||||
|
|
|
|||
305
lib/pangea/activity_planner/activity_plan_card.dart
Normal file
305
lib/pangea/activity_planner/activity_plan_card.dart
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ActivityPlanCard extends StatefulWidget {
|
||||
final ActivityPlanModel activity;
|
||||
final VoidCallback onLaunch;
|
||||
final ValueChanged<ActivityPlanModel> onEdit;
|
||||
final double maxWidth;
|
||||
|
||||
const ActivityPlanCard({
|
||||
super.key,
|
||||
required this.activity,
|
||||
required this.onLaunch,
|
||||
required this.onEdit,
|
||||
this.maxWidth = 400,
|
||||
});
|
||||
|
||||
@override
|
||||
ActivityPlanCardState createState() => ActivityPlanCardState();
|
||||
}
|
||||
|
||||
class ActivityPlanCardState extends State<ActivityPlanCard> {
|
||||
bool _isEditing = false;
|
||||
late ActivityPlanModel _tempActivity;
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _learningObjectiveController;
|
||||
late TextEditingController _instructionsController;
|
||||
final TextEditingController _newVocabController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempActivity = widget.activity;
|
||||
_titleController = TextEditingController(text: _tempActivity.title);
|
||||
_learningObjectiveController =
|
||||
TextEditingController(text: _tempActivity.learningObjective);
|
||||
_instructionsController =
|
||||
TextEditingController(text: _tempActivity.instructions);
|
||||
}
|
||||
|
||||
static const double itemPadding = 8;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_learningObjectiveController.dispose();
|
||||
_instructionsController.dispose();
|
||||
_newVocabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveEdits() async {
|
||||
final updatedActivity = ActivityPlanModel(
|
||||
req: _tempActivity.req,
|
||||
title: _titleController.text,
|
||||
learningObjective: _learningObjectiveController.text,
|
||||
instructions: _instructionsController.text,
|
||||
vocab: _tempActivity.vocab,
|
||||
);
|
||||
|
||||
final activityWithBookmarkId = await _addBookmark(updatedActivity);
|
||||
|
||||
// need to save in the repo as well
|
||||
widget.onEdit(activityWithBookmarkId);
|
||||
|
||||
setState(() {
|
||||
_isEditing = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<ActivityPlanModel> _addBookmark(ActivityPlanModel activity) =>
|
||||
BookmarkedActivitiesRepo.save(activity).catchError((e, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: stack, data: activity.toJson());
|
||||
}).whenComplete(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
Future<void> _removeBookmark() =>
|
||||
BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId!)
|
||||
.catchError((e, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson());
|
||||
}).whenComplete(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
void _addVocab() {
|
||||
setState(() {
|
||||
_tempActivity.vocab.add(Vocab(lemma: _newVocabController.text, pos: ''));
|
||||
_newVocabController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _removeVocab(int index) {
|
||||
setState(() {
|
||||
_tempActivity.vocab.removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
bool get isBookmarked =>
|
||||
BookmarkedActivitiesRepo.isBookmarked(widget.activity);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: widget.maxWidth),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: itemPadding),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.event_note_outlined),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: _isEditing
|
||||
? TextField(
|
||||
controller: _titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).title,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.activity.title,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (!_isEditing)
|
||||
IconButton(
|
||||
onPressed: isBookmarked
|
||||
? () => _removeBookmark()
|
||||
: () => _addBookmark(widget.activity),
|
||||
icon: Icon(
|
||||
isBookmarked ? Icons.bookmark : Icons.bookmark_border,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.target,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: _isEditing
|
||||
? TextField(
|
||||
controller: _learningObjectiveController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Learning Objective',
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.activity.learningObjective,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.steps_rounded,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: _isEditing
|
||||
? TextField(
|
||||
controller: _instructionsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Instructions',
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.activity.instructions,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
if (widget.activity.vocab.isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.dictionary,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: List<Widget>.generate(
|
||||
_tempActivity.vocab.length, (int index) {
|
||||
return _isEditing
|
||||
? Chip(
|
||||
label:
|
||||
Text(_tempActivity.vocab[index].lemma),
|
||||
onDeleted: () => _removeVocab(index),
|
||||
backgroundColor: Colors.transparent,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side:
|
||||
BorderSide(color: Colors.transparent),
|
||||
),
|
||||
)
|
||||
: Chip(
|
||||
label:
|
||||
Text(_tempActivity.vocab[index].lemma),
|
||||
backgroundColor: Colors.transparent,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side:
|
||||
BorderSide(color: Colors.transparent),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isEditing)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: itemPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _newVocabController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Add Vocabulary',
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: _addVocab,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(!_isEditing ? Icons.edit : Icons.save),
|
||||
onPressed: () => !_isEditing
|
||||
? setState(() {
|
||||
_isEditing = true;
|
||||
})
|
||||
: _saveEdits(),
|
||||
isSelected: _isEditing,
|
||||
),
|
||||
if (_isEditing)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cancel),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: !_isEditing ? widget.onLaunch : null,
|
||||
icon: const Icon(Icons.send),
|
||||
label: Text(l10n.launchActivityButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
|
|
@ -9,83 +11,6 @@ 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');
|
||||
|
|
|
|||
90
lib/pangea/activity_planner/activity_plan_model.dart
Normal file
90
lib/pangea/activity_planner/activity_plan_model.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
|
||||
class ActivityPlanModel {
|
||||
final ActivityPlanRequest req;
|
||||
final String title;
|
||||
final String learningObjective;
|
||||
final String instructions;
|
||||
final List<Vocab> vocab;
|
||||
String? bookmarkId;
|
||||
|
||||
ActivityPlanModel({
|
||||
required this.req,
|
||||
required this.title,
|
||||
required this.learningObjective,
|
||||
required this.instructions,
|
||||
required this.vocab,
|
||||
this.bookmarkId,
|
||||
});
|
||||
|
||||
factory ActivityPlanModel.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityPlanModel(
|
||||
req: ActivityPlanRequest.fromJson(json['req']),
|
||||
title: json['title'],
|
||||
learningObjective: json['learning_objective'],
|
||||
instructions: json['instructions'],
|
||||
vocab: List<Vocab>.from(
|
||||
json['vocab'].map((vocab) => Vocab.fromJson(vocab)),
|
||||
),
|
||||
bookmarkId: json['bookmark_id'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'req': req.toJson(),
|
||||
'title': title,
|
||||
'learning_objective': learningObjective,
|
||||
'instructions': instructions,
|
||||
'vocab': vocab.map((vocab) => vocab.toJson()).toList(),
|
||||
'bookmark_id': bookmarkId,
|
||||
};
|
||||
}
|
||||
|
||||
/// activity content displayed nicely in markdown
|
||||
/// use target emoji for learning objective
|
||||
/// use step emoji for instructions
|
||||
String get markdown {
|
||||
String markdown =
|
||||
''' **$title** \n🎯 $learningObjective \n🪜 $instructions \n📖 ''';
|
||||
// cycle through vocab with index
|
||||
for (var i = 0; i < vocab.length; i++) {
|
||||
// if the lemma appears more than once in the vocab list, show the pos
|
||||
// vocab is a wrapped list of string, separated by commas
|
||||
final v = vocab[i];
|
||||
final bool showPos =
|
||||
vocab.where((vocab) => vocab.lemma == v.lemma).length > 1;
|
||||
markdown +=
|
||||
'${v.lemma}${showPos ? ' (${v.pos})' : ''}${i + 1 < vocab.length ? ', ' : ''}';
|
||||
}
|
||||
return markdown;
|
||||
}
|
||||
|
||||
bool get isBookmarked {
|
||||
return bookmarkId != null;
|
||||
}
|
||||
}
|
||||
|
||||
class Vocab {
|
||||
final String lemma;
|
||||
final String pos;
|
||||
|
||||
Vocab({
|
||||
required this.lemma,
|
||||
required this.pos,
|
||||
});
|
||||
|
||||
factory Vocab.fromJson(Map<String, dynamic> json) {
|
||||
return Vocab(
|
||||
lemma: json['lemma'],
|
||||
pos: json['pos'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lemma': lemma,
|
||||
'pos': pos,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
|
||||
class ActivityPlanPageLaunchIconButton extends StatelessWidget {
|
||||
const ActivityPlanPageLaunchIconButton({
|
||||
super.key,
|
||||
|
|
@ -15,6 +14,9 @@ class ActivityPlanPageLaunchIconButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.room.isBotDM) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.event_note_outlined),
|
||||
tooltip: L10n.of(context).activityPlannerTitle,
|
||||
|
|
|
|||
97
lib/pangea/activity_planner/activity_plan_request.dart
Normal file
97
lib/pangea/activity_planner/activity_plan_request.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/media_enum.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,
|
||||
};
|
||||
}
|
||||
|
||||
factory ActivityPlanRequest.fromJson(Map<String, dynamic> json) {
|
||||
int cefrLevel = 0;
|
||||
switch (json['cefr_level']) {
|
||||
case 'Pre-A1':
|
||||
cefrLevel = 0;
|
||||
break;
|
||||
case 'A1':
|
||||
cefrLevel = 1;
|
||||
break;
|
||||
case 'A2':
|
||||
cefrLevel = 2;
|
||||
break;
|
||||
case 'B1':
|
||||
cefrLevel = 3;
|
||||
break;
|
||||
case 'B2':
|
||||
cefrLevel = 4;
|
||||
break;
|
||||
case 'C1':
|
||||
cefrLevel = 5;
|
||||
break;
|
||||
case 'C2':
|
||||
cefrLevel = 6;
|
||||
break;
|
||||
}
|
||||
return ActivityPlanRequest(
|
||||
topic: json['topic'],
|
||||
mode: json['mode'],
|
||||
objective: json['objective'],
|
||||
media: MediaEnum.nan.fromString(json['media']),
|
||||
cefrLevel: cefrLevel,
|
||||
languageOfInstructions: json['language_of_instructions'],
|
||||
targetLanguage: json['target_language'],
|
||||
count: json['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';
|
||||
}
|
||||
}
|
||||
}
|
||||
21
lib/pangea/activity_planner/activity_plan_response.dart
Normal file
21
lib/pangea/activity_planner/activity_plan_response.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
|
||||
class ActivityPlanResponse {
|
||||
final List<ActivityPlanModel> activityPlans;
|
||||
|
||||
ActivityPlanResponse({required this.activityPlans});
|
||||
|
||||
factory ActivityPlanResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityPlanResponse(
|
||||
activityPlans: (json['activity_plans'] as List)
|
||||
.map((e) => ActivityPlanModel.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity_plans': activityPlans.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +1,26 @@
|
|||
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_list_view.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/activity_plan_request.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/suggestion_form_field.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
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
enum _PageMode {
|
||||
settings,
|
||||
activities,
|
||||
generatedActivities,
|
||||
savedActivities,
|
||||
}
|
||||
|
||||
class ActivityPlannerPage extends StatefulWidget {
|
||||
|
|
@ -80,18 +46,8 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
String? _selectedTargetLanguage;
|
||||
int? _selectedCefrLevel;
|
||||
|
||||
/// fetch data from repos
|
||||
List<ActivitySettingResponseSchema> _topicItems = [];
|
||||
List<ActivitySettingResponseSchema> _modeItems = [];
|
||||
List<ActivitySettingResponseSchema> _objectiveItems = [];
|
||||
List<String> activities = [];
|
||||
|
||||
/// List of activities generated by the system
|
||||
List<String> _activities = [];
|
||||
|
||||
final _topicSearchController = TextEditingController();
|
||||
final _objectiveSearchController = TextEditingController();
|
||||
|
||||
final List<TextEditingController> _activityControllers = [];
|
||||
Room? get room => Matrix.of(context).client.getRoomById(widget.roomID);
|
||||
|
||||
@override
|
||||
|
|
@ -102,138 +58,69 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
return;
|
||||
}
|
||||
|
||||
_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));
|
||||
}
|
||||
}
|
||||
|
||||
final _topicController = TextEditingController();
|
||||
final _objectiveController = TextEditingController();
|
||||
final _modeController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_topicSearchController.dispose();
|
||||
_objectiveSearchController.dispose();
|
||||
disposeAndClearActivityControllers();
|
||||
_topicController.dispose();
|
||||
_objectiveController.dispose();
|
||||
_modeController.dispose();
|
||||
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;
|
||||
});
|
||||
}
|
||||
Future<List<ActivitySettingResponseSchema>> get _topicItems =>
|
||||
TopicListRepo.get(req);
|
||||
|
||||
// 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
|
||||
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);
|
||||
}
|
||||
Future<List<ActivitySettingResponseSchema>> get _modeItems =>
|
||||
ActivityModeListRepo.get(req);
|
||||
|
||||
final eventId = await room?.pangeaSendTextEvent(
|
||||
_activities[index],
|
||||
messageTag: ModelKey.messageTagActivityPlan,
|
||||
originalSent: PangeaRepresentation(
|
||||
langCode: _selectedLanguageOfInstructions!,
|
||||
text: _activities[index],
|
||||
originalSent: true,
|
||||
originalWritten: false,
|
||||
),
|
||||
tokensSent:
|
||||
tokens != null ? PangeaMessageTokens(tokens: tokens) : null,
|
||||
);
|
||||
|
||||
if (eventId == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
await room?.setPinnedEvents([eventId]);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
Future<List<ActivitySettingResponseSchema>> get _objectiveItems =>
|
||||
LearningObjectiveListRepo.get(req);
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
_pageMode = _PageMode.generatedActivities;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool get _canRandomizeSelections =>
|
||||
_topicItems.isNotEmpty &&
|
||||
_objectiveItems.isNotEmpty &&
|
||||
_modeItems.isNotEmpty;
|
||||
Future<String> _randomTopic() async {
|
||||
final topics = await _topicItems;
|
||||
return (topics..shuffle()).first.name;
|
||||
}
|
||||
|
||||
void _randomizeSelections() {
|
||||
if (!_canRandomizeSelections) return;
|
||||
Future<String> _randomObjective() async {
|
||||
final objectives = await _objectiveItems;
|
||||
return (objectives..shuffle()).first.name;
|
||||
}
|
||||
|
||||
Future<String> _randomMode() async {
|
||||
final modes = await _modeItems;
|
||||
return (modes..shuffle()).first.name;
|
||||
}
|
||||
|
||||
void _randomizeSelections() async {
|
||||
_selectedTopic = await _randomTopic();
|
||||
_selectedObjective = await _randomObjective();
|
||||
_selectedMode = await _randomMode();
|
||||
|
||||
setState(() {
|
||||
_selectedTopic = (_topicItems..shuffle()).first.name;
|
||||
_selectedObjective = (_objectiveItems..shuffle()).first.name;
|
||||
_selectedMode = (_modeItems..shuffle()).first.name;
|
||||
_topicController.text = _selectedTopic!;
|
||||
_objectiveController.text = _selectedObjective!;
|
||||
_modeController.text = _selectedMode!;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -259,223 +146,157 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
onPressed: () => setState(() => _pageMode = _PageMode.settings),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
title: Text(l10n.activityPlannerTitle),
|
||||
title: _pageMode == _PageMode.savedActivities
|
||||
? Text(l10n.myBookmarkedActivities)
|
||||
: Text(l10n.activityPlannerTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
setState(() => _pageMode = _PageMode.savedActivities),
|
||||
icon: const Icon(Icons.bookmarks),
|
||||
),
|
||||
],
|
||||
),
|
||||
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:
|
||||
_canRandomizeSelections ? _randomizeSelections : null,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _generateActivities,
|
||||
child: Text(l10n.generateActivitiesButton),
|
||||
body: _pageMode != _PageMode.settings
|
||||
? ActivityListView(
|
||||
room: room,
|
||||
activityPlanRequest: _PageMode.savedActivities == _pageMode
|
||||
? null
|
||||
: ActivityPlanRequest(
|
||||
topic: _selectedTopic!,
|
||||
mode: _selectedMode!,
|
||||
objective: _selectedObjective!,
|
||||
media: _selectedMedia,
|
||||
languageOfInstructions: _selectedLanguageOfInstructions!,
|
||||
targetLanguage: _selectedTargetLanguage!,
|
||||
cefrLevel: _selectedCefrLevel!,
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
SuggestionFormField(
|
||||
suggestions: _topicItems,
|
||||
validator: _validateNotNull,
|
||||
label: l10n.topicLabel,
|
||||
placeholder: l10n.topicPlaceholder,
|
||||
onSelected: (val) => _selectedTopic = val,
|
||||
initialValue: _selectedTopic,
|
||||
controller: _topicController,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SuggestionFormField(
|
||||
suggestions: _objectiveItems,
|
||||
validator: _validateNotNull,
|
||||
label: l10n.learningObjectiveLabel,
|
||||
placeholder:
|
||||
l10n.learningObjectivePlaceholder,
|
||||
onSelected: (val) => _selectedObjective = val,
|
||||
initialValue: _selectedObjective,
|
||||
controller: _objectiveController,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SuggestionFormField(
|
||||
suggestions: _modeItems,
|
||||
validator: _validateNotNull,
|
||||
label: l10n.modeLabel,
|
||||
placeholder: l10n.modePlaceholder,
|
||||
onSelected: (val) => _selectedMode = val,
|
||||
initialValue: _selectedMode,
|
||||
controller: _modeController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.shuffle),
|
||||
onPressed: _randomizeSelections,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
_generateActivities();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.lightbulb_outline),
|
||||
const SizedBox(width: 8),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ActivityPlannerSettingsSearchWidget extends StatelessWidget {
|
||||
const ActivityPlannerSettingsSearchWidget({
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/pangea/activity_planner/bookmarked_activities_repo.dart
Normal file
39
lib/pangea/activity_planner/bookmarked_activities_repo.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class BookmarkedActivitiesRepo {
|
||||
static final GetStorage _storage = GetStorage('bookmarked_activities');
|
||||
static const Uuid _uuid = Uuid();
|
||||
|
||||
/// save an activity to the list of bookmarked activities
|
||||
/// returns the activity with a bookmarkId
|
||||
static Future<ActivityPlanModel> save(ActivityPlanModel activity) async {
|
||||
activity.bookmarkId ??= _uuid.v4();
|
||||
|
||||
await _storage.write(
|
||||
activity.bookmarkId!,
|
||||
activity.toJson(),
|
||||
);
|
||||
|
||||
//now it has a bookmarkId
|
||||
return activity;
|
||||
}
|
||||
|
||||
static Future<void> remove(String bookmarkId) => _storage.remove(bookmarkId);
|
||||
|
||||
static bool isBookmarked(ActivityPlanModel activity) {
|
||||
return activity.bookmarkId != null &&
|
||||
_storage.read(activity.bookmarkId!) != null;
|
||||
}
|
||||
|
||||
static List<ActivityPlanModel> get() {
|
||||
final list = _storage.getValues();
|
||||
|
||||
if (list == null) return [];
|
||||
|
||||
return (list as Iterable)
|
||||
.map((json) => ActivityPlanModel.fromJson(json))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum MediaEnum {
|
||||
|
|
@ -11,7 +10,7 @@ enum MediaEnum {
|
|||
|
||||
extension MediaEnumExtension on MediaEnum {
|
||||
//fromString
|
||||
static MediaEnum fromString(String value) {
|
||||
MediaEnum fromString(String value) {
|
||||
switch (value) {
|
||||
case 'images':
|
||||
return MediaEnum.images;
|
||||
|
|
|
|||
57
lib/pangea/activity_planner/suggestion_form_field.dart
Normal file
57
lib/pangea/activity_planner/suggestion_form_field.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SuggestionFormField extends StatelessWidget {
|
||||
final Future<List<ActivitySettingResponseSchema>> suggestions;
|
||||
final String? Function(String?)? validator;
|
||||
final String label;
|
||||
final String placeholder;
|
||||
final void Function(String) onSelected;
|
||||
final String? initialValue;
|
||||
final TextEditingController controller;
|
||||
|
||||
const SuggestionFormField({
|
||||
super.key,
|
||||
required this.suggestions,
|
||||
required this.placeholder,
|
||||
this.validator,
|
||||
required this.label,
|
||||
required this.onSelected,
|
||||
required this.initialValue,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Autocomplete<String>(
|
||||
initialValue:
|
||||
initialValue != null ? TextEditingValue(text: initialValue!) : null,
|
||||
optionsBuilder: (TextEditingValue textEditingValue) async {
|
||||
return (await suggestions)
|
||||
.where((ActivitySettingResponseSchema option) {
|
||||
return option.name
|
||||
.toLowerCase()
|
||||
.contains(textEditingValue.text.toLowerCase());
|
||||
}).map((ActivitySettingResponseSchema e) => e.name);
|
||||
},
|
||||
onSelected: onSelected,
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
TextEditingController textEditingController,
|
||||
FocusNode focusNode,
|
||||
VoidCallback onFieldSubmitted,
|
||||
) {
|
||||
textEditingController.value = controller.value;
|
||||
return TextFormField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: placeholder,
|
||||
),
|
||||
validator: validator,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,6 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics/controllers/get_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/analytics/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
|
|
@ -30,6 +25,10 @@ import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.
|
|||
import 'package:fluffychat/pangea/user/controllers/permissions_controller.dart';
|
||||
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../choreographer/controllers/it_feedback_controller.dart';
|
||||
import '../utils/firebase_analytics.dart';
|
||||
|
|
@ -186,7 +185,7 @@ class PangeaController {
|
|||
|
||||
final List<Room> botDMs = [];
|
||||
for (final room in matrixState.client.rooms) {
|
||||
if (await room.isBotDM) {
|
||||
if (room.isBotDM) {
|
||||
botDMs.add(room);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,8 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:matrix/matrix.dart' as matrix;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics/models/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/analytics/models/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
|
|
@ -29,6 +19,15 @@ import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
|||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.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:html_unescape/html_unescape.dart';
|
||||
import 'package:matrix/matrix.dart' as matrix;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../choreographer/models/choreo_record.dart';
|
||||
import '../events/constants/pangea_event_types.dart';
|
||||
import '../events/models/representation_content_model.dart';
|
||||
|
|
@ -195,7 +194,7 @@ extension PangeaRoom on Room {
|
|||
|
||||
Future<bool> get botIsInRoom async => await _botIsInRoom;
|
||||
|
||||
Future<bool> get isBotDM async => await _isBotDM;
|
||||
bool get isBotDM => _isBotDM;
|
||||
|
||||
// bool get isLocked => _isLocked;
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ extension RoomInformationRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Future<bool> get _isBotDM async => botOptions?.mode == BotMode.directChat;
|
||||
bool get _isBotDM => botOptions?.mode == BotMode.directChat;
|
||||
|
||||
bool _isAnalyticsRoomOfUser(String userId) =>
|
||||
isAnalyticsRoom && isMadeByUser(userId);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue