diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart index ba27c19b2..d284557db 100644 --- a/lib/pangea/activity_planner/activity_plan_model.dart +++ b/lib/pangea/activity_planner/activity_plan_model.dart @@ -220,7 +220,7 @@ class Vocab { class ActivityRole { final String id; final String name; - final String goal; + final String? goal; final String? avatarUrl; ActivityRole({ @@ -238,9 +238,9 @@ class ActivityRole { } return ActivityRole( - id: json['id'], - name: json['name'], - goal: json['goal'], + id: json['id'] as String, + name: json['name'] as String, + goal: json['goal'] as String?, avatarUrl: avatarUrl, ); } diff --git a/lib/pangea/activity_sessions/activity_finished_status_message.dart b/lib/pangea/activity_sessions/activity_finished_status_message.dart index 7f7fe200d..521064b00 100644 --- a/lib/pangea/activity_sessions/activity_finished_status_message.dart +++ b/lib/pangea/activity_sessions/activity_finished_status_message.dart @@ -14,8 +14,8 @@ import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; -import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart'; -import 'package:fluffychat/pangea/courses/course_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -38,7 +38,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget { final courseParent = controller.room.courseParent; if (courseParent?.coursePlan == null) return; - final coursePlan = await CourseRepo.get( + final coursePlan = await CoursePlansRepo.get( courseParent!.coursePlan!.uuid, ); diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index 33b436a1c..ffea66ebf 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -15,7 +15,7 @@ import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; diff --git a/lib/pangea/chat_settings/pages/space_details_content.dart b/lib/pangea/chat_settings/pages/space_details_content.dart index 200a36c9d..6c5c1039b 100644 --- a/lib/pangea/chat_settings/pages/space_details_content.dart +++ b/lib/pangea/chat_settings/pages/space_details_content.dart @@ -17,9 +17,9 @@ import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_settings/course_settings.dart'; -import 'package:fluffychat/pangea/courses/course_plan_builder.dart'; -import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/space_analytics/space_analytics.dart'; diff --git a/lib/pangea/common/config/environment.dart b/lib/pangea/common/config/environment.dart index 26b0ce898..30318bcbf 100644 --- a/lib/pangea/common/config/environment.dart +++ b/lib/pangea/common/config/environment.dart @@ -64,6 +64,14 @@ class Environment { return envEntry; } + static String get cmsApi { + final envEntry = appConfigOverride?.choreoApi ?? dotenv.env['CHOREO_API']; + if (envEntry == null) { + return "Not found"; + } + return envEntry; + } + static String get choreoApiKey { return appConfigOverride?.choreoApiKey ?? dotenv.env['CHOREO_API_KEY'] ?? diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index d2f7ec379..f1fb7ab5e 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -14,8 +14,8 @@ import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart'; import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_view.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; -import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; diff --git a/lib/pangea/course_chats/course_chats_view.dart b/lib/pangea/course_chats/course_chats_view.dart index c68b36b13..4eaaf61d4 100644 --- a/lib/pangea/course_chats/course_chats_view.dart +++ b/lib/pangea/course_chats/course_chats_view.dart @@ -12,9 +12,9 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.dart'; -import 'package:fluffychat/pangea/courses/course_plan_builder.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; -import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/space_analytics/analytics_request_indicator.dart'; import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart'; diff --git a/lib/pangea/course_creation/course_image_widget.dart b/lib/pangea/course_creation/course_image_widget.dart new file mode 100644 index 000000000..f9c40602a --- /dev/null +++ b/lib/pangea/course_creation/course_image_widget.dart @@ -0,0 +1,51 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; + +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseImage extends StatelessWidget { + final String? imageUrl; + final double width; + final Widget? replacement; + final BorderRadius borderRadius; + + const CourseImage({ + super.key, + required this.imageUrl, + required this.width, + this.replacement, + this.borderRadius = const BorderRadius.all(Radius.circular(20.0)), + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: borderRadius, + child: imageUrl != null + ? CachedNetworkImage( + width: width, + height: width, + fit: BoxFit.cover, + imageUrl: imageUrl!, + httpHeaders: { + 'Authorization': + 'Bearer ${MatrixState.pangeaController.userController.accessToken}', + }, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + placeholder: (context, url) { + return const Center( + child: CircularProgressIndicator(), + ); + }, + errorWidget: (context, url, error) { + return replacement ?? const SizedBox(); + }, + ) + : replacement ?? const SizedBox(), + ); + } +} diff --git a/lib/pangea/course_creation/course_info_chip_widget.dart b/lib/pangea/course_creation/course_info_chip_widget.dart index 2a5b6b9ff..3f0efaf76 100644 --- a/lib/pangea/course_creation/course_info_chip_widget.dart +++ b/lib/pangea/course_creation/course_info_chip_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; class CourseInfoChip extends StatelessWidget { diff --git a/lib/pangea/course_creation/course_plan_tile_widget.dart b/lib/pangea/course_creation/course_plan_tile_widget.dart index ef6342215..114d6bde2 100644 --- a/lib/pangea/course_creation/course_plan_tile_widget.dart +++ b/lib/pangea/course_creation/course_plan_tile_widget.dart @@ -1,9 +1,10 @@ +// ignore_for_file: depend_on_referenced_packages + import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; - +import 'package:fluffychat/pangea/course_creation/course_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; class CoursePlanTile extends StatelessWidget { @@ -40,36 +41,16 @@ class CoursePlanTile extends StatelessWidget { child: Row( spacing: 4.0, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: course.imageUrl != null - ? CachedNetworkImage( - width: 40.0, - height: 40.0, - fit: BoxFit.cover, - imageUrl: course.imageUrl!, - placeholder: (context, url) { - return const Center( - child: CircularProgressIndicator(), - ); - }, - errorWidget: (context, url, error) { - return Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - color: theme.colorScheme.secondary, - ), - ); - }, - ) - : Container( - width: 40.0, - height: 40.0, - decoration: BoxDecoration( - color: theme.colorScheme.secondary, - ), - ), + CourseImage( + imageUrl: course.imageUrl, + width: 40.0, + replacement: Container( + width: 40.0, + height: 40.0, + decoration: BoxDecoration( + color: theme.colorScheme.secondary, + ), + ), ), Flexible( child: Column( diff --git a/lib/pangea/course_creation/new_course_page.dart b/lib/pangea/course_creation/new_course_page.dart index 5bdea6781..52634d021 100644 --- a/lib/pangea/course_creation/new_course_page.dart +++ b/lib/pangea/course_creation/new_course_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/course_creation/new_course_view.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; -import 'package:fluffychat/pangea/courses/course_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; @@ -55,7 +55,7 @@ class NewCourseController extends State { Future _loadCourses() async { try { setState(() => loading = true); - courses = await CourseRepo.search(filter: _filter); + courses = await CoursePlansRepo.search(filter: _filter); } catch (e) { error = e; } finally { diff --git a/lib/pangea/course_creation/selected_course_page.dart b/lib/pangea/course_creation/selected_course_page.dart index 2e087a161..acf17c858 100644 --- a/lib/pangea/course_creation/selected_course_page.dart +++ b/lib/pangea/course_creation/selected_course_page.dart @@ -9,7 +9,7 @@ import 'package:matrix/matrix.dart' as sdk; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/course_creation/selected_course_view.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -29,7 +29,13 @@ class SelectedCourseController extends State { Uri? avatarUrl; if (course.imageUrl != null) { try { - final Response response = await http.get(Uri.parse(course.imageUrl!)); + final Response response = await http.get( + Uri.parse(course.imageUrl!), + headers: { + 'Authorization': + 'Bearer ${MatrixState.pangeaController.userController.accessToken}', + }, + ); avatar = response.bodyBytes; avatarUrl = await client.uploadContent(avatar); } catch (e) { diff --git a/lib/pangea/course_creation/selected_course_view.dart b/lib/pangea/course_creation/selected_course_view.dart index 2541d6d93..951ed3034 100644 --- a/lib/pangea/course_creation/selected_course_view.dart +++ b/lib/pangea/course_creation/selected_course_view.dart @@ -4,9 +4,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:go_router/go_router.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/course_creation/course_image_widget.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; -import 'package:fluffychat/pangea/courses/course_plan_builder.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -57,39 +58,16 @@ class SelectedCourseView extends StatelessWidget { return Column( spacing: 8.0, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(20.0), - child: course.imageUrl != null - ? CachedNetworkImage( - width: 100.0, - height: 100.0, - fit: BoxFit.cover, - imageUrl: course.imageUrl!, - placeholder: (context, url) { - return const Center( - child: - CircularProgressIndicator(), - ); - }, - errorWidget: (context, url, error) { - return Container( - width: 100.0, - height: 100.0, - decoration: BoxDecoration( - color: theme - .colorScheme.secondary, - ), - ); - }, - ) - : Container( - width: 100.0, - height: 100.0, - decoration: BoxDecoration( - color: - theme.colorScheme.secondary, - ), - ), + CourseImage( + imageUrl: course.imageUrl, + width: 100.0, + replacement: Container( + width: 100.0, + height: 100.0, + decoration: BoxDecoration( + color: theme.colorScheme.secondary, + ), + ), ), Text( course.title, diff --git a/lib/pangea/courses/course_plan_builder.dart b/lib/pangea/course_plans/course_plan_builder.dart similarity index 88% rename from lib/pangea/courses/course_plan_builder.dart rename to lib/pangea/course_plans/course_plan_builder.dart index 0976748b2..c0f9ee343 100644 --- a/lib/pangea/courses/course_plan_builder.dart +++ b/lib/pangea/course_plans/course_plan_builder.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; -import 'package:fluffychat/pangea/courses/course_repo.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_plans_repo.dart'; class CoursePlanBuilder extends StatefulWidget { final String? courseId; @@ -60,7 +60,7 @@ class CoursePlanController extends State { error = null; }); - course = await CourseRepo.get(widget.courseId!); + course = await CoursePlansRepo.get(widget.courseId!); course == null ? widget.onNotFound?.call() : widget.onFound?.call(course!); diff --git a/lib/pangea/courses/course_plan_event.dart b/lib/pangea/course_plans/course_plan_event.dart similarity index 100% rename from lib/pangea/courses/course_plan_event.dart rename to lib/pangea/course_plans/course_plan_event.dart diff --git a/lib/pangea/course_plans/course_plan_model.dart b/lib/pangea/course_plans/course_plan_model.dart new file mode 100644 index 000000000..054b24133 --- /dev/null +++ b/lib/pangea/course_plans/course_plan_model.dart @@ -0,0 +1,267 @@ +import 'package:fluffychat/pangea/activity_generator/media_enum.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/common/config/environment.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart'; + +/// Represents a topic in the course planner response. +class Topic { + final String title; + final String description; + final String location; + final String uuid; + final String? imageUrl; + + final List activities; + + Topic({ + required this.title, + required this.description, + this.location = "Unknown", + required this.uuid, + List? activities, + this.imageUrl, + }) : activities = activities ?? []; + + /// Deserialize from JSON + factory Topic.fromJson(Map json) { + return Topic( + title: json['title'] as String, + description: json['description'] as String, + location: json['location'] as String? ?? "Unknown", + uuid: json['uuid'] as String, + activities: (json['activities'] as List?) + ?.map( + (e) => ActivityPlanModel.fromJson(e as Map), + ) + .toList() ?? + [], + imageUrl: json['image_url'] as String?, + ); + } + + /// Serialize to JSON + Map toJson() { + return { + 'title': title, + 'description': description, + 'location': location, + 'uuid': uuid, + 'activities': activities.map((e) => e.toJson()).toList(), + 'image_url': imageUrl, + }; + } + + List get activityIds => activities.map((e) => e.activityId).toList(); +} + +/// Represents a course plan in the course planner response. +class CoursePlanModel { + final String targetLanguage; + final String languageOfInstructions; + final LanguageLevelTypeEnum cefrLevel; + + final String title; + final String description; + + final String uuid; + + final List topics; + final String? imageUrl; + + CoursePlanModel({ + required this.targetLanguage, + required this.languageOfInstructions, + required this.cefrLevel, + required this.title, + required this.description, + required this.uuid, + List? topics, + this.imageUrl, + }) : topics = topics ?? []; + + int get activities => + topics.map((t) => t.activities.length).reduce((a, b) => a + b); + + LanguageModel? get targetLanguageModel => + PLanguageStore.byLangCode(targetLanguage); + + LanguageModel? get baseLanguageModel => + PLanguageStore.byLangCode(languageOfInstructions); + + String get targetLanguageDisplay => + targetLanguageModel?.langCode.toUpperCase() ?? + targetLanguage.toUpperCase(); + + String get baseLanguageDisplay => + baseLanguageModel?.langCode.toUpperCase() ?? + languageOfInstructions.toUpperCase(); + + String? topicID(String activityID) { + for (final topic in topics) { + for (final activity in topic.activities) { + if (activity.activityId == activityID) { + return topic.uuid; + } + } + } + return null; + } + + /// Deserialize from JSON + factory CoursePlanModel.fromJson(Map json) { + return CoursePlanModel( + targetLanguage: json['target_language'] as String, + languageOfInstructions: json['language_of_instructions'] as String, + cefrLevel: LanguageLevelTypeEnumExtension.fromString(json['cefr_level']), + title: json['title'] as String, + description: json['description'] as String, + uuid: json['uuid'] as String, + topics: (json['topics'] as List?) + ?.map((e) => Topic.fromJson(e as Map)) + .toList() ?? + [], + imageUrl: json['image_url'] as String?, + ); + } + + /// Serialize to JSON + Map toJson() { + return { + 'target_language': targetLanguage, + 'language_of_instructions': languageOfInstructions, + 'cefr_level': cefrLevel.string, + 'title': title, + 'description': description, + 'uuid': uuid, + 'topics': topics.map((e) => e.toJson()).toList(), + 'image_url': imageUrl, + }; + } + + factory CoursePlanModel.fromCmsDocs( + CmsCoursePlan cmsCoursePlan, + List? cmsCoursePlanMedias, + List? cmsCoursePlanModules, + List? cmsCoursePlanModuleLocations, + List? cmsCoursePlanActivities, + List? cmsCoursePlanActivityMedias, + ) { + // fetch topics + List? topics; + if (cmsCoursePlanModules != null) { + for (final module in cmsCoursePlanModules) { + // select locations of current module + List? moduleLocations; + if (cmsCoursePlanModuleLocations != null) { + for (final location in cmsCoursePlanModuleLocations) { + if (location.coursePlanModules.contains(module.id)) { + moduleLocations ??= []; + moduleLocations.add(location); + } + } + } + + // select activities of current module + List? moduleActivities; + if (cmsCoursePlanActivities != null) { + for (final activity in cmsCoursePlanActivities) { + if (activity.coursePlanModules.contains(module.id)) { + moduleActivities ??= []; + moduleActivities.add(activity); + } + } + } + + List? activityPlans; + if (moduleActivities != null) { + for (final activity in moduleActivities) { + // select media of current activity + List? activityMedias; + if (cmsCoursePlanActivityMedias != null) { + for (final media in cmsCoursePlanActivityMedias) { + if (media.coursePlanActivities.contains(activity.id)) { + activityMedias ??= []; + activityMedias.add(media); + } + } + } + + activityPlans ??= []; + activityPlans.add( + ActivityPlanModel( + req: ActivityPlanRequest( + topic: "", + mode: "", + objective: "", + media: MediaEnum.nan, + cefrLevel: activity.cefrLevel, + languageOfInstructions: activity.l1, + targetLanguage: activity.l2, + numberOfParticipants: activity.roles.length, + ), + activityId: activity.id, + title: activity.title, + description: activity.description, + learningObjective: activity.learningObjective, + instructions: activity.instructions, + vocab: activity.vocabs + .map((v) => Vocab(lemma: v.lemma, pos: v.pos)) + .toList(), + roles: activity.roles.asMap().map( + (index, v) => MapEntry( + index.toString(), + ActivityRole( + id: v.id, + name: v.name, + avatarUrl: v.avatarUrl, + goal: v.goal, + ), + ), + ), + imageURL: activityMedias != null && activityMedias.isNotEmpty + ? '${Environment.cmsApi}${activityMedias.first.url}' + : null, + ), + ); + } + } + + topics ??= []; + topics.add( + Topic( + uuid: module.id, + title: module.title, + description: module.description, + location: moduleLocations != null && moduleLocations.isNotEmpty + ? moduleLocations.first.name + : "Any", + activities: activityPlans, + ), + ); + } + } + + return CoursePlanModel( + uuid: cmsCoursePlan.id, + title: cmsCoursePlan.title, + description: cmsCoursePlan.description, + cefrLevel: + LanguageLevelTypeEnumExtension.fromString(cmsCoursePlan.cefrLevel), + languageOfInstructions: cmsCoursePlan.l1, + targetLanguage: cmsCoursePlan.l2, + topics: topics, + imageUrl: cmsCoursePlanMedias != null && cmsCoursePlanMedias.isNotEmpty + ? '${Environment.cmsApi}${cmsCoursePlanMedias.first.url}' + : null, + ); + } +} diff --git a/lib/pangea/courses/course_plan_room_extension.dart b/lib/pangea/course_plans/course_plan_room_extension.dart similarity index 93% rename from lib/pangea/courses/course_plan_room_extension.dart rename to lib/pangea/course_plans/course_plan_room_extension.dart index 607bd47e6..8de621e8b 100644 --- a/lib/pangea/courses/course_plan_room_extension.dart +++ b/lib/pangea/course_plans/course_plan_room_extension.dart @@ -1,9 +1,9 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; -import 'package:fluffychat/pangea/courses/course_plan_event.dart'; -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; -import 'package:fluffychat/pangea/courses/course_user_event.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_event.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/course_plans/course_user_event.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; extension CoursePlanRoomExtension on Room { diff --git a/lib/pangea/course_plans/course_plans_repo.dart b/lib/pangea/course_plans/course_plans_repo.dart new file mode 100644 index 000000000..4e2a997bd --- /dev/null +++ b/lib/pangea/course_plans/course_plans_repo.dart @@ -0,0 +1,333 @@ +import 'package:get_storage/get_storage.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_model.dart'; +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_media.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module.dart'; +import 'package:fluffychat/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart'; +import 'package:fluffychat/pangea/payload_client/payload_client.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class CourseFilter { + final LanguageModel? targetLanguage; + final LanguageModel? languageOfInstructions; + final LanguageLevelTypeEnum? cefrLevel; + + CourseFilter({ + this.targetLanguage, + this.languageOfInstructions, + this.cefrLevel, + }); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CourseFilter && + other.targetLanguage == targetLanguage && + other.languageOfInstructions == languageOfInstructions && + other.cefrLevel == cefrLevel; + } + + @override + int get hashCode => + targetLanguage.hashCode ^ + languageOfInstructions.hashCode ^ + cefrLevel.hashCode; +} + +class CoursePlansRepo { + static final GetStorage _courseStorage = GetStorage("course_storage"); + + static final PayloadClient payload = PayloadClient( + baseUrl: Environment.cmsApi, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + static CoursePlanModel? _getCached(String id) { + final json = _courseStorage.read(id); + if (json != null) { + try { + return CoursePlanModel.fromJson(json); + } catch (e) { + _courseStorage.remove(id); + } + } + return null; + } + + static Future _setCached(CoursePlanModel coursePlan) async { + await _courseStorage.write(coursePlan.uuid, coursePlan.toJson()); + } + + static String _searchKey(CourseFilter filter) { + return "search_${filter.hashCode.toString()}"; + } + + static List? _getCachedSearchResults( + CourseFilter filter, + ) { + final jsonList = _courseStorage.read(_searchKey(filter)); + if (jsonList != null) { + try { + final ids = List.from(jsonList); + final coursePlans = ids + .map((id) => _getCached(id)) + .whereType() + .toList(); + + return coursePlans; + } catch (e) { + _courseStorage.remove(_searchKey(filter)); + } + } + return null; + } + + static Future _setCachedSearchResults( + CourseFilter filter, + List coursePlans, + ) async { + final jsonList = coursePlans.map((e) => e.uuid).toList(); + for (final plan in coursePlans) { + _setCached(plan); + } + await _courseStorage.write(_searchKey(filter), jsonList); + } + + static Future get(String id) async { + final cached = _getCached(id); + if (cached != null) { + return cached; + } + + final cmsCoursePlan = await payload.findById( + "course-plans", + id, + CmsCoursePlan.fromJson, + ); + + final coursePlan = await _fromCmsCoursePlan(cmsCoursePlan); + await _setCached(coursePlan); + return coursePlan; + } + + static Future> search({CourseFilter? filter}) async { + final cached = _getCachedSearchResults(filter ?? CourseFilter()); + if (cached != null && cached.isNotEmpty) { + return cached; + } + + final Map where = {}; + if (filter != null) { + int numberOfFilter = 0; + if (filter.cefrLevel != null) { + numberOfFilter += 1; + } + if (filter.languageOfInstructions != null) { + numberOfFilter += 1; + } + if (filter.targetLanguage != null) { + numberOfFilter += 1; + } + if (numberOfFilter > 1) { + where["and"] = []; + if (filter.cefrLevel != null) { + where["and"].add({ + "cefrLevel": {"equals": filter.cefrLevel!.string}, + }); + } + if (filter.languageOfInstructions != null) { + where["and"].add({ + "languageOfInstructions": { + "equals": filter.languageOfInstructions!.langCode, + }, + }); + } + if (filter.targetLanguage != null) { + where["and"].add({ + "targetLanguage": {"equals": filter.targetLanguage!.langCode}, + }); + } + } else if (numberOfFilter == 1) { + if (filter.cefrLevel != null) { + where["cefrLevel"] = {"equals": filter.cefrLevel!.string}; + } + if (filter.languageOfInstructions != null) { + where["languageOfInstructions"] = { + "equals": filter.languageOfInstructions!.langCode, + }; + } + if (filter.targetLanguage != null) { + where["targetLanguage"] = {"equals": filter.targetLanguage!.langCode}; + } + } + } + + final result = await payload.find( + "course-plans", + CmsCoursePlan.fromJson, + page: 1, + limit: 10, + where: where, + ); + + final coursePlans = await Future.wait( + result.docs.map( + (cmsCoursePlan) => _fromCmsCoursePlan( + cmsCoursePlan, + ), + ), + ); + + await _setCachedSearchResults( + filter ?? CourseFilter(), + coursePlans, + ); + + return coursePlans; + } + + static Future _fromCmsCoursePlan( + CmsCoursePlan cmsCoursePlan, + ) async { + final medias = await _getMedia(cmsCoursePlan); + final modules = await _getModules(cmsCoursePlan); + final locations = await _getModuleLocations(modules ?? []); + final activities = await _getModuleActivities(modules ?? []); + final activityMedias = await _getActivityMedia(activities ?? []); + return CoursePlanModel.fromCmsDocs( + cmsCoursePlan, + medias, + modules, + locations, + activities, + activityMedias, + ); + } + + static Future?> _getMedia( + CmsCoursePlan cmsCoursePlan, + ) async { + final docs = cmsCoursePlan.coursePlanMedia?.docs; + if (docs == null || docs.isEmpty) return null; + + final where = { + "id": {"in": docs.join(",")}, + }; + final limit = docs.length; + final cmsCoursePlanMediaResult = await payload.find( + "course-plan-media", + CmsCoursePlanMedia.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + return cmsCoursePlanMediaResult.docs; + } + + static Future?> _getModules( + CmsCoursePlan cmsCoursePlan, + ) async { + final docs = cmsCoursePlan.coursePlanModules?.docs; + if (docs == null || docs.isEmpty) return null; + + final where = { + "id": {"in": docs.join(",")}, + }; + final limit = docs.length; + final cmsCourseModulesResult = await payload.find( + "course-plan-modules", + CmsCoursePlanModule.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + return cmsCourseModulesResult.docs; + } + + static Future?> _getModuleLocations( + List modules, + ) async { + final List locations = []; + for (final module in modules) { + if (module.coursePlanModuleLocations?.docs != null) { + locations.addAll(module.coursePlanModuleLocations!.docs!); + } + } + if (locations.isEmpty) return null; + + final where = { + "id": {"in": locations.join(",")}, + }; + final limit = locations.length; + final cmsCoursePlanModuleLocationsResult = await payload.find( + "course-plan-module-locations", + CmsCoursePlanModuleLocation.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + return cmsCoursePlanModuleLocationsResult.docs; + } + + static Future?> _getModuleActivities( + List module, + ) async { + final List activities = []; + for (final mod in module) { + if (mod.coursePlanActivities?.docs != null) { + activities.addAll(mod.coursePlanActivities!.docs!); + } + } + if (activities.isEmpty) return null; + + final where = { + "id": {"in": activities.join(",")}, + }; + final limit = activities.length; + final cmsCoursePlanActivitiesResult = await payload.find( + "course-plan-activities", + CmsCoursePlanActivity.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + return cmsCoursePlanActivitiesResult.docs; + } + + static Future?> _getActivityMedia( + List activity, + ) async { + final List mediaIds = []; + for (final act in activity) { + if (act.coursePlanActivityMedia?.docs != null) { + mediaIds.addAll(act.coursePlanActivityMedia!.docs!); + } + } + if (mediaIds.isEmpty) return null; + + final where = { + "id": {"in": mediaIds.join(",")}, + }; + final limit = mediaIds.length; + final cmsCoursePlanActivityMediasResult = await payload.find( + "course-plan-activity-medias", + CmsCoursePlanActivityMedia.fromJson, + where: where, + limit: limit, + page: 1, + sort: "createdAt", + ); + return cmsCoursePlanActivityMediasResult.docs; + } +} diff --git a/lib/pangea/courses/course_user_event.dart b/lib/pangea/course_plans/course_user_event.dart similarity index 100% rename from lib/pangea/courses/course_user_event.dart rename to lib/pangea/course_plans/course_user_event.dart diff --git a/lib/pangea/course_settings/course_settings.dart b/lib/pangea/course_settings/course_settings.dart index 5e8a11261..7ee84c419 100644 --- a/lib/pangea/course_settings/course_settings.dart +++ b/lib/pangea/course_settings/course_settings.dart @@ -11,10 +11,10 @@ import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card. import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_builder.dart'; +import 'package:fluffychat/pangea/course_plans/course_plan_room_extension.dart'; import 'package:fluffychat/pangea/course_settings/pin_clipper.dart'; import 'package:fluffychat/pangea/course_settings/topic_participant_list.dart'; -import 'package:fluffychat/pangea/courses/course_plan_builder.dart'; -import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart'; class CourseSettings extends StatelessWidget { final Room room; diff --git a/lib/pangea/courses/course_plan_model.dart b/lib/pangea/courses/course_plan_model.dart deleted file mode 100644 index a6eb1ec71..000000000 --- a/lib/pangea/courses/course_plan_model.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; -import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; - -/// Represents a topic in the course planner response. -class Topic { - final String title; - final String description; - final String location; - final String uuid; - final String? imageUrl; - - final List activities; - - Topic({ - required this.title, - required this.description, - this.location = "Unknown", - required this.uuid, - List? activities, - this.imageUrl, - }) : activities = activities ?? []; - - /// Deserialize from JSON - factory Topic.fromJson(Map json) { - return Topic( - title: json['title'] as String, - description: json['description'] as String, - location: json['location'] as String? ?? "Unknown", - uuid: json['id'] as String, - activities: (json['activities'] as List?) - ?.map( - (e) => ActivityPlanModel.fromJson(e as Map), - ) - .toList() ?? - [], - imageUrl: json['image_url'] as String?, - ); - } - - /// Serialize to JSON - Map toJson() { - return { - 'title': title, - 'description': description, - 'location': location, - 'id': uuid, - 'activities': activities.map((e) => e.toJson()).toList(), - 'image_url': imageUrl, - }; - } - - List get activityIds => activities.map((e) => e.activityId).toList(); -} - -/// Represents a course plan in the course planner response. -class CoursePlanModel { - final String targetLanguage; - final String languageOfInstructions; - final LanguageLevelTypeEnum cefrLevel; - - final String title; - final String description; - - final String uuid; - - final List topics; - final String? imageUrl; - - CoursePlanModel({ - required this.targetLanguage, - required this.languageOfInstructions, - required this.cefrLevel, - required this.title, - required this.description, - required this.uuid, - List? topics, - this.imageUrl, - }) : topics = topics ?? []; - - int get activities => - topics.map((t) => t.activities.length).reduce((a, b) => a + b); - - LanguageModel? get targetLanguageModel => - PLanguageStore.byLangCode(targetLanguage); - - LanguageModel? get baseLanguageModel => - PLanguageStore.byLangCode(languageOfInstructions); - - String get targetLanguageDisplay => - targetLanguageModel?.langCode.toUpperCase() ?? - targetLanguage.toUpperCase(); - - String get baseLanguageDisplay => - baseLanguageModel?.langCode.toUpperCase() ?? - languageOfInstructions.toUpperCase(); - - String? topicID(String activityID) { - for (final topic in topics) { - for (final activity in topic.activities) { - if (activity.activityId == activityID) { - return topic.uuid; - } - } - } - return null; - } - - /// Deserialize from JSON - factory CoursePlanModel.fromJson(Map json) { - return CoursePlanModel( - targetLanguage: json['target_language'] as String, - languageOfInstructions: json['language_of_instructions'] as String, - cefrLevel: LanguageLevelTypeEnumExtension.fromString(json['cefr_level']), - title: json['title'] as String, - description: json['description'] as String, - uuid: json['id'] as String, - topics: (json['topics'] as List?) - ?.map((e) => Topic.fromJson(e as Map)) - .toList() ?? - [], - imageUrl: json['image_url'] as String?, - ); - } - - /// Serialize to JSON - Map toJson() { - return { - 'target_language': targetLanguage, - 'language_of_instructions': languageOfInstructions, - 'cefr_level': cefrLevel.string, - 'title': title, - 'description': description, - 'id': uuid, - 'topics': topics.map((e) => e.toJson()).toList(), - 'image_url': imageUrl, - }; - } -} diff --git a/lib/pangea/courses/course_repo.dart b/lib/pangea/courses/course_repo.dart deleted file mode 100644 index d15722949..000000000 --- a/lib/pangea/courses/course_repo.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:get_storage/get_storage.dart'; - -import 'package:fluffychat/pangea/courses/course_plan_model.dart'; -import 'package:fluffychat/pangea/courses/test_courses_json.dart'; -import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; -import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; - -class CourseFilter { - final LanguageModel? targetLanguage; - final LanguageModel? languageOfInstructions; - final LanguageLevelTypeEnum? cefrLevel; - - CourseFilter({ - this.targetLanguage, - this.languageOfInstructions, - this.cefrLevel, - }); -} - -class CourseRepo { - static final GetStorage _courseStorage = GetStorage("course_storage"); - - static CoursePlanModel? _getCached(String id) { - final json = _courseStorage.read(id); - if (json != null) { - try { - return CoursePlanModel.fromJson(json); - } catch (e) { - _courseStorage.remove(id); - } - } - return null; - } - - static List _getAllCached() { - final keys = _courseStorage.getKeys(); - return keys - .map((key) => _getCached(key)) - .whereType() - .toList(); - } - - static Future set(CoursePlanModel coursePlan) async { - await _courseStorage.write(coursePlan.uuid, coursePlan.toJson()); - } - - static Future get(String id) async { - final cached = _getCached(id); - if (cached != null) { - return cached; - } - - final resp = await search(); - return resp.firstWhereOrNull((course) => course.uuid == id); - } - - static Future> search({CourseFilter? filter}) async { - final cached = _getAllCached(); - if (cached.isNotEmpty) { - return cached.filtered(filter); - } - - final resp = (courseJson["courses"] as List) - .map((json) => CoursePlanModel.fromJson(json)) - .whereType() - .toList(); - - for (final plan in resp) { - set(plan); - } - - return resp.filtered(filter); - } -} - -extension on List { - List filtered(CourseFilter? filter) { - return where((course) { - final matchesTargetLanguage = filter?.targetLanguage == null || - course.targetLanguage.split("-").first == - filter?.targetLanguage?.langCodeShort; - - final matchesLanguageOfInstructions = - filter?.languageOfInstructions == null || - course.languageOfInstructions.split("-").first == - filter?.languageOfInstructions?.langCodeShort; - - final matchesCefrLevel = - filter?.cefrLevel == null || course.cefrLevel == filter?.cefrLevel; - - return matchesTargetLanguage && - matchesLanguageOfInstructions && - matchesCefrLevel; - }).toList(); - } -} diff --git a/lib/pangea/courses/test_courses_json.dart b/lib/pangea/courses/test_courses_json.dart deleted file mode 100644 index 4b0cdfb63..000000000 --- a/lib/pangea/courses/test_courses_json.dart +++ /dev/null @@ -1,3690 +0,0 @@ -final courseJson = { - "courses": [ - { - "target_language": "es", - "language_of_instructions": "en", - "cefr_level": "A1", - "title": "Josh Otusanya's Spanish Learning Journey", - "description": - "Embark on an exciting adventure through the Spanish-speaking world with Josh Otusanya! This A1-level course will take you on a virtual tour of eight vibrant cities, from the familiar streets of New York to the sun-soaked plazas of Seville. As you progress through each unit, you'll not only learn essential Spanish language skills but also immerse yourself in the rich cultures of these diverse locations. Get ready to build a strong foundation in Spanish while exploring the unique flavors of each city's Spanish-speaking community.", - "id": "a58a249e-0625-4bfb-8c37-1b7e7ab722bf", - "topics": [ - { - "activity_id": "e79eb174-048e-4cbc-a70c-79f11782eeeb", - "title": "¡Bienvenidos a Nueva York!", - "description": - "Start your Spanish journey in the Big Apple! You'll learn basic greetings, introduce yourself, and discover how to navigate the city's diverse Hispanic neighborhoods. Practice saying '¡Hola!' to new amigos in Washington Heights and Little Spain.", - "location": "New York City", - "id": "be65f331-86a1-4f67-bca7-7e09c7709bb6", - "activities": [ - { - "activity_id": "f8a1d2b3-4c5e-6789-0abc-def123456789", - "title": "Meet & Greet at the Washington Heights Café", - "description": - "You’ll roleplay meeting at a café in Washington Heights and introduce yourselves!", - "learning_objective": - "Introduce yourself and greet others using basic Spanish expressions like '¡Hola!' and '¿Cómo estás?'.", - "instructions": - "**Instructions:**\n\n1. Imagine you both just arrived at a bustling café in Washington Heights.\n2. Each of you will play your role and greet the other in Spanish.\n3. Use basic greeting phrases and introduce yourself (your name and how you are).\n4. Example phrases to help you:\n - \"¡Hola! ¿Cómo te llamas?\"\n - \"Me llamo [your name]. ¿Y tú?\"\n - \"¿Cómo estás?\"\n - \"Estoy bien, gracias.\"\n5. Respond to your partner’s greeting and introduction.\n6. Keep the conversation simple and friendly, as if you’re meeting for the first time!", - "vocab": [ - { - "lemma": "hola", - "pos": "INTJ", - }, - { - "lemma": "llamar", - "pos": "VERB", - }, - { - "lemma": "nombre", - "pos": "NOUN", - }, - { - "lemma": "cómo", - "pos": "ADV", - }, - { - "lemma": "estar", - "pos": "VERB", - }, - { - "lemma": "bien", - "pos": "ADV", - }, - { - "lemma": "gracias", - "pos": "NOUN", - }, - { - "lemma": "tú", - "pos": "PRON", - } - ], - "roles": { - "ca11d2f0-07d2-4574-bc29-aacd0e6b2a18": { - "name": "Tourist", - "goal": - "Greet a local and introduce yourself, asking how they are.", - "id": "ca11d2f0-07d2-4574-bc29-aacd0e6b2a18", - }, - "e0a52f77-7c85-4655-b19f-a5ef90b9e554": { - "name": "Local", - "goal": - "Welcome the tourist, introduce yourself, and ask how they are.", - "id": "e0a52f77-7c85-4655-b19f-a5ef90b9e554", - }, - }, - "req": { - "topic": "Greetings and Introductions", - "mode": "Roleplay", - "objective": - "Introduce themselves and greet others using basic Spanish expressions like '¡Hola!' and '¿Cómo estás?'.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Washington Heights", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "e2b3c4d5-6f78-9abc-def0-123456789abc", - "title": "Guess the Landmark in Little Spain", - "description": - "Explore Little Spain and guess famous landmarks using yes/no questions!", - "learning_objective": - "Use yes/no questions to guess famous Spanish landmarks and practice question formation in Spanish.", - "instructions": - "**How to Play:**\n\n1. One of you is the *Picture Holder* and selects an image of a famous Spanish landmark (from internet or device) and sends it in the chat, but does NOT say the name.\n2. The other is the *Guesser*. Your goal is to guess the landmark by asking up to 20 yes/no questions in Spanish. Use simple questions like:\n - ¿Está en España?\n - ¿Es un edificio?\n - ¿Es famoso?\n - ¿Es antiguo?\n - ¿Está en Madrid?\n3. The Picture Holder only answers with \"sí\" or \"no\".\n4. The Guesser can guess the landmark at any time. If correct, celebrate! If not, keep going until 20 questions are used or the answer is found.\n\n**Tips:**\n- Use the vocab list to help form questions.\n- Stay in Spanish as much as possible.\n\n¡Buena suerte en Little Spain!", - "vocab": [ - { - "lemma": "pregunta", - "pos": "NOUN", - }, - { - "lemma": "respuesta", - "pos": "NOUN", - }, - { - "lemma": "sí", - "pos": "ADV", - }, - { - "lemma": "no", - "pos": "ADV", - }, - { - "lemma": "lugar", - "pos": "NOUN", - }, - { - "lemma": "famoso", - "pos": "ADJ", - }, - { - "lemma": "antiguo", - "pos": "ADJ", - }, - { - "lemma": "edificio", - "pos": "NOUN", - }, - { - "lemma": "ciudad", - "pos": "NOUN", - }, - { - "lemma": "foto", - "pos": "NOUN", - }, - { - "lemma": "es", - "pos": "VERB", - }, - { - "lemma": "está", - "pos": "VERB", - } - ], - "roles": { - "ef560248-4894-4933-9681-5a8496ad2178": { - "name": "Picture Holder", - "goal": - "Send an image of a Spanish landmark and answer yes/no questions.", - "id": "ef560248-4894-4933-9681-5a8496ad2178", - }, - "ff212a5a-98b8-4e96-bf6d-f05d0c163e57": { - "name": "Guesser", - "goal": - "Ask yes/no questions in Spanish to guess the landmark.", - "id": "ff212a5a-98b8-4e96-bf6d-f05d0c163e57", - }, - }, - "req": { - "topic": "Hispanic Landmarks and Yes/No Questions", - "mode": "20-Question Game", - "objective": - "Use yes/no questions to guess famous Spanish landmarks and practice question formation in Spanish.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Little Spain", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "c4d5e6f7-8901-2345-6789-abcdef012345", - "title": "Meet at the East Harlem Café", - "description": - "Exchange personal info in Spanish as you meet at a cozy East Harlem café!", - "learning_objective": - "Ask and answer basic personal questions about name, origin, and age using complete Spanish sentences.", - "instructions": - "You are meeting at a café in East Harlem. Use voice messages to have a short conversation in Spanish, asking and answering about your name, where you are from, and your age.\n\n**Example questions:**\n- ¿Cómo te llamas?\n- ¿De dónde eres?\n- ¿Cuántos años tienes?\n\n**Example answers:**\n- Me llamo Ana.\n- Soy de México.\n- Tengo 21 años.\n\n1. Start by greeting your partner and asking their name.\n2. Ask where they are from and how old they are.\n3. Answer their questions about yourself.\n4. Use complete sentences in your responses.\n5. Each of you should send at least two voice messages.\n\nTry to sound friendly, as if you are meeting for the first time!", - "vocab": [ - { - "lemma": "llamar", - "pos": "VERB", - }, - { - "lemma": "ser", - "pos": "VERB", - }, - { - "lemma": "tener", - "pos": "VERB", - }, - { - "lemma": "nombre", - "pos": "NOUN", - }, - { - "lemma": "edad", - "pos": "NOUN", - }, - { - "lemma": "de", - "pos": "ADP", - }, - { - "lemma": "dónde", - "pos": "ADV", - }, - { - "lemma": "cuántos", - "pos": "DET", - }, - { - "lemma": "años", - "pos": "NOUN", - } - ], - "roles": { - "d78a9dd2-4284-4b9b-92ac-c4eba93e0f89": { - "name": "Café Visitor 1", - "goal": - "Ask and answer questions about name, origin, and age.", - "id": "d78a9dd2-4284-4b9b-92ac-c4eba93e0f89", - }, - "01a1f0a3-4530-4756-9332-3fbb0cc8e5e6": { - "name": "Café Visitor 2", - "goal": - "Ask and answer questions about name, origin, and age.", - "id": "01a1f0a3-4530-4756-9332-3fbb0cc8e5e6", - }, - }, - "req": { - "topic": "Personal Information Exchange", - "mode": "Conversation", - "objective": - "Ask and answer basic personal questions about name, origin, and age using complete Spanish sentences.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "East Harlem", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "a1b2c3d4-5678-90ef-ghij-klmnop123456", - "title": "Jackson Heights Food Preferences Debate", - "description": - "Debate your favorite Hispanic dishes at a lively Jackson Heights food market!", - "learning_objective": - "Express and defend preferences for traditional versus modern Hispanic dishes using 'prefiero…' and 'me gusta más…'.", - "instructions": - "**Instructions:**\n1. Each of you takes a role below. Imagine you are in a food market in Jackson Heights.\n2. Use Spanish to say which type of dish you prefer (traditional or modern) and why. Use phrases like:\n - \"Prefiero la comida tradicional/moderna porque…\"\n - \"Me gusta más la comida tradicional/moderna.\"\n - \"No me gusta mucho…\"\n3. Respond to each other with short questions or comments. Example:\n - \"¿Por qué prefieres la comida tradicional?\"\n - \"¡Interesante! A mí me gusta más la comida moderna.\"\n4. Try to use at least two of the new words from the vocab list.\n5. Keep the conversation going for at least two rounds each.", - "vocab": [ - { - "lemma": "comida", - "pos": "NOUN", - }, - { - "lemma": "tradicional", - "pos": "ADJ", - }, - { - "lemma": "moderno", - "pos": "ADJ", - }, - { - "lemma": "prefiero", - "pos": "VERB", - }, - { - "lemma": "gusta", - "pos": "VERB", - }, - { - "lemma": "más", - "pos": "ADV", - }, - { - "lemma": "porque", - "pos": "SCONJ", - }, - { - "lemma": "mercado", - "pos": "NOUN", - } - ], - "roles": { - "22f2d9a5-2334-43f6-a57f-fb177b94607e": { - "name": "Traditional Dish Fan", - "goal": - "Express and defend a preference for traditional Hispanic dishes.", - "id": "22f2d9a5-2334-43f6-a57f-fb177b94607e", - }, - "e00b1fe1-2728-416c-9826-2ff06096a089": { - "name": "Modern Dish Fan", - "goal": - "Express and defend a preference for modern Hispanic dishes.", - "id": "e00b1fe1-2728-416c-9826-2ff06096a089", - }, - "74c8ece1-236a-48e7-b895-8a73eabbfeaa": { - "name": "Curious Foodie", - "goal": - "Ask questions and comment on both sides, showing interest in both types of dishes.", - "id": "74c8ece1-236a-48e7-b895-8a73eabbfeaa", - }, - }, - "req": { - "topic": "Food Preferences Debate", - "mode": "Debate", - "objective": - "Express and defend preferences for traditional versus modern Hispanic dishes using phrases like 'prefiero…' and 'me gusta más…'.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Jackson Heights", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "b3006f33-bac5-4640-acb5-bf5203ce6bd4", - "title": "Find the Hidden Gems of Sunset Park", - "description": - "Explore Sunset Park by giving and following Spanish directions to discover cultural spots together!", - "learning_objective": - "Follow and give basic Spanish directions (e.g., 'gira a la izquierda', 'sigue recto') to locate key cultural places in Sunset Park.", - "instructions": - "## Instructions\n1. Each of you will play a different role. Stay in your role throughout the activity.\n2. The Guide describes a cultural spot in Sunset Park (like a bakery, park, or mural) using simple Spanish directions (e.g., \"Sigue recto dos calles y gira a la derecha\").\n3. The Tourist listens and repeats the directions in Spanish, then guesses the spot.\n4. The Local gives a hint about the spot (in English), based on local knowledge.\n5. The Guesser tries to name the spot in Spanish.\n6. Rotate the Guide role to the next person for a new spot.\n\n### Example Spanish phrases:\n- \"Gira a la izquierda.\"\n- \"Sigue recto.\"\n- \"Está cerca del parque.\"\n- \"Cruza la calle.\"\n\nWork together in the chat to find all the hidden gems!", - "vocab": [ - { - "lemma": "girar", - "pos": "VERB", - }, - { - "lemma": "izquierda", - "pos": "NOUN", - }, - { - "lemma": "derecha", - "pos": "NOUN", - }, - { - "lemma": "seguir", - "pos": "VERB", - }, - { - "lemma": "recto", - "pos": "ADJECTIVE", - }, - { - "lemma": "cruzar", - "pos": "VERB", - }, - { - "lemma": "calle", - "pos": "NOUN", - }, - { - "lemma": "cerca", - "pos": "ADVERB", - }, - { - "lemma": "parque", - "pos": "NOUN", - }, - { - "lemma": "panadería", - "pos": "NOUN", - } - ], - "roles": { - "5ae8b69d-5a20-46c3-baca-6f3d3e96a6e0": { - "name": "Guide", - "goal": - "Give Spanish directions to a cultural spot in Sunset Park.", - "id": "5ae8b69d-5a20-46c3-baca-6f3d3e96a6e0", - }, - "2d1c8520-0eaf-4a02-b1e5-3d816b9fb243": { - "name": "Tourist", - "goal": - "Follow and repeat the directions in Spanish, then guess the spot.", - "id": "2d1c8520-0eaf-4a02-b1e5-3d816b9fb243", - }, - "8dc4c251-cc3f-4ba5-989c-feaa8a75e868": { - "name": "Local", - "goal": - "Provide a helpful English hint about the spot based on local knowledge.", - "id": "8dc4c251-cc3f-4ba5-989c-feaa8a75e868", - }, - "ad0ee057-cbbe-4e77-8a65-1e849c440604": { - "name": "Guesser", - "goal": - "Try to name the spot in Spanish based on clues and directions.", - "id": "ad0ee057-cbbe-4e77-8a65-1e849c440604", - }, - }, - "req": { - "topic": "Navigating Hispanic Neighborhoods", - "mode": "Scavenger Hunt", - "objective": - "Follow and give Spanish directions (e.g., 'gira a la izquierda', 'sigue recto') to find key cultural spots.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Sunset Park", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "e93b41dc-e026-4da1-b0b5-12bfe6b78291", - "title": "Saludos desde Miami", - "description": - "Dive into numbers and basic phrases as you explore Miami's vibrant Cuban-American culture. Learn to order a 'cafecito' in Little Havana and count your way through a day at the beach.", - "location": "Miami", - "id": "01e23852-5f04-435a-92cb-f1d9e8783ce2", - "activities": [ - { - "activity_id": "c969ed03-0b3c-469f-942f-af32ef7ff26f", - "title": "Cafecito Chat at Versailles Café", - "description": - "Order a classic cafecito and ask for the price at the famous Versailles Café in Little Havana!", - "learning_objective": - "I can order a 'cafecito' and ask for the price using numbers 1–10.", - "instructions": - "**1. Read your role and goal.**\n\n**2. Start the chat! Use Spanish to interact.**\n\n**If you are the Tourist:**\n- Greet the server: _\"¡Hola!\"_\n- Order a cafecito: _\"Quisiera un cafecito, por favor.\"_\n- Ask for the price: _\"¿Cuánto cuesta?\"_\n- Listen to the price and respond: _\"Gracias.\"_\n\n**If you are the Server:**\n- Greet the tourist: _\"¡Bienvenido a Versailles!\"_\n- Take the order: _\"¿Desea algo más?\"_\n- Give a price from 1–10 euros (e.g., _\"Cuesta tres euros.\"_)\n- Respond politely: _\"Gracias a usted.\"_", - "vocab": [ - { - "lemma": "cafecito", - "pos": "NOUN", - }, - { - "lemma": "cuánto", - "pos": "ADV", - }, - { - "lemma": "costar", - "pos": "VERB", - }, - { - "lemma": "uno", - "pos": "NUM", - }, - { - "lemma": "dos", - "pos": "NUM", - }, - { - "lemma": "tres", - "pos": "NUM", - }, - { - "lemma": "cuatro", - "pos": "NUM", - }, - { - "lemma": "cinco", - "pos": "NUM", - }, - { - "lemma": "seis", - "pos": "NUM", - }, - { - "lemma": "siete", - "pos": "NUM", - }, - { - "lemma": "ocho", - "pos": "NUM", - }, - { - "lemma": "nueve", - "pos": "NUM", - }, - { - "lemma": "diez", - "pos": "NUM", - }, - { - "lemma": "por favor", - "pos": "ADV", - }, - { - "lemma": "gracias", - "pos": "NOUN", - } - ], - "roles": { - "14799595-81b3-4460-8da6-2360dfd6d29d": { - "name": "Tourist", - "goal": - "Order a cafecito and ask for the price using numbers 1–10.", - "id": "14799595-81b3-4460-8da6-2360dfd6d29d", - }, - "56e2e34e-e879-4e41-9b2b-60e2bc5e3b0f": { - "name": "Server", - "goal": - "Respond to the order, state the price (1–10), and thank the tourist.", - "id": "56e2e34e-e879-4e41-9b2b-60e2bc5e3b0f", - }, - }, - "req": { - "topic": "Ordering a cafecito at the café", - "mode": "Roleplay", - "objective": - "I can order a 'cafecito' and ask for the price using numbers 1–10.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Versailles Café, Little Havana", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "8fd29ce3-8422-47b1-b17b-f8a86a754980", - "title": "Numbers in the Park: Photo Scavenger Hunt", - "description": - "Explore Domino Park by finding and sharing numbered objects!", - "learning_objective": - "I can identify and pronounce numbers while finding specific objects around the park.", - "instructions": - "1. Each of you will have a special role in this scavenger hunt.\n2. The Guide chooses a number (1-10) and says it in Spanish. Example: \"Busquen el número cinco.\"\n3. The Photographer searches for an object in Domino Park that matches the number (e.g., 5 dominos, 3 benches) and sends a photo to the group chat.\n4. The Counter checks the photo and counts the items in Spanish, confirming the number. Example: \"Hay cinco dominos.\"\n5. Rotate turns so everyone practices each task, but keep your roles for the whole activity.\n6. Use Spanish numbers and object names as much as possible!\n\nExample phrases:\n- \"¿Cuántos bancos ves?\"\n- \"Veo tres bancos.\"\n- \"Correcto, hay tres bancos.\"", - "vocab": [ - { - "lemma": "número", - "pos": "NOUN", - }, - { - "lemma": "uno", - "pos": "NUM", - }, - { - "lemma": "dos", - "pos": "NUM", - }, - { - "lemma": "tres", - "pos": "NUM", - }, - { - "lemma": "cuatro", - "pos": "NUM", - }, - { - "lemma": "cinco", - "pos": "NUM", - }, - { - "lemma": "seis", - "pos": "NUM", - }, - { - "lemma": "siete", - "pos": "NUM", - }, - { - "lemma": "ocho", - "pos": "NUM", - }, - { - "lemma": "nueve", - "pos": "NUM", - }, - { - "lemma": "diez", - "pos": "NUM", - }, - { - "lemma": "domino", - "pos": "NOUN", - }, - { - "lemma": "banco", - "pos": "NOUN", - }, - { - "lemma": "persona", - "pos": "NOUN", - }, - { - "lemma": "árbol", - "pos": "NOUN", - }, - { - "lemma": "mesa", - "pos": "NOUN", - }, - { - "lemma": "ver", - "pos": "VERB", - }, - { - "lemma": "buscar", - "pos": "VERB", - }, - { - "lemma": "contar", - "pos": "VERB", - } - ], - "roles": { - "644d85e5-cb6d-40a9-bd71-36366b5f9e1b": { - "name": "Guide", - "goal": - "Give a number in Spanish and ask the group to find objects with that number.", - "id": "644d85e5-cb6d-40a9-bd71-36366b5f9e1b", - }, - "595160cf-30a9-4aff-8510-c77af6cc0a1c": { - "name": "Photographer", - "goal": - "Find and share images of objects in the park that match the number given.", - "id": "595160cf-30a9-4aff-8510-c77af6cc0a1c", - }, - "a0462373-77c6-48af-857d-c57cc64c4e8f": { - "name": "Counter", - "goal": - "Count the objects in the photo and confirm the number in Spanish.", - "id": "a0462373-77c6-48af-857d-c57cc64c4e8f", - }, - }, - "req": { - "topic": "Number scavenger hunt", - "mode": "Scavenger Hunt", - "objective": - "I can identify and pronounce numbers while finding specific objects around the park.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Domino Park (Parque Máximo Gómez)", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "5daed96c-bc63-4fad-a740-25fa87dfe2cc", - "title": "Guess the Favorite Ice Cream Flavor!", - "description": - "Play 20 Questions with your partner at Little Havana Cultural Center to guess their favorite ice cream flavor!", - "learning_objective": - "I can use yes/no questions and numbers to guess my partner’s favorite ice cream flavor.", - "instructions": - "1. Imagine you are at the Little Havana Cultural Center.\n2. One of you chooses your favorite ice cream flavor (in secret!).\n3. The other will send voice messages in Spanish to ask yes/no questions to guess the flavor. Use numbers to narrow down options (e.g., ¿Hay más de tres sabores de helado aquí?).\n4. The answerer replies with voice messages using \"sí\" or \"no\".\n5. The questioner can ask up to 20 questions. Try to guess the flavor before reaching 20!\n\n**Useful phrases:**\n- ¿Es de chocolate? (Is it chocolate?)\n- ¿Es un sabor de fruta? (Is it a fruit flavor?)\n- ¿Tiene más de dos colores? (Does it have more than two colors?)\n- ¿Es tu sabor favorito? (Is it your favorite flavor?)\n- ¿Hay más de cinco sabores en la lista? (Are there more than five flavors on the list?)", - "vocab": [ - { - "lemma": "helado", - "pos": "NOUN", - }, - { - "lemma": "sabor", - "pos": "NOUN", - }, - { - "lemma": "favorito", - "pos": "ADJECTIVE", - }, - { - "lemma": "pregunta", - "pos": "NOUN", - }, - { - "lemma": "sí", - "pos": "ADV", - }, - { - "lemma": "no", - "pos": "ADV", - }, - { - "lemma": "cuántos", - "pos": "PRON", - }, - { - "lemma": "hay", - "pos": "VERB", - }, - { - "lemma": "ser", - "pos": "VERB", - }, - { - "lemma": "tener", - "pos": "VERB", - }, - { - "lemma": "color", - "pos": "NOUN", - }, - { - "lemma": "fruta", - "pos": "NOUN", - }, - { - "lemma": "chocolate", - "pos": "NOUN", - }, - { - "lemma": "número", - "pos": "NOUN", - } - ], - "roles": { - "200a955c-a0e1-44a3-a47c-0169be34044c": { - "name": "Guesser", - "goal": - "Ask yes/no questions in Spanish and use numbers to discover your partner’s favorite ice cream flavor.", - "id": "200a955c-a0e1-44a3-a47c-0169be34044c", - }, - "c0975251-e70e-4a8f-9063-898287ab8889": { - "name": "Answerer", - "goal": - "Answer yes/no questions in Spanish to help your partner guess your favorite ice cream flavor.", - "id": "c0975251-e70e-4a8f-9063-898287ab8889", - }, - }, - "req": { - "topic": "20-Question Game: Ice Cream Flavors", - "mode": "20-Question Game", - "objective": - "I can use yes/no questions and numbers to guess my partner’s favorite ice cream flavor.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Little Havana Cultural Center", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "4bd5533b-6134-474f-b6fa-3caad2d12eea", - "title": "Ice Cream Showdown at Azúcar", - "description": - "Compare prices and flavors at Azúcar Ice Cream to pick your favorite!", - "learning_objective": - "I can compare prices and flavors to choose the best ice cream option using numbers and basic adjectives.", - "instructions": - "1. Imagine you are at Azúcar Ice Cream in Little Havana.\n2. Each of you chooses 2 ice cream flavors from this list: chocolate, vainilla, mango, coco, café, guayaba.\n3. Assign a price (in euros) to each flavor (e.g., chocolate: 3€, mango: 4€).\n4. In the chat, describe your flavors using adjectives (e.g., \"El chocolate es dulce y barato. El mango es caro y delicioso.\")\n5. Ask your partner about their choices: \"¿Cuál es más barato? ¿Cuál es más rico?\"\n6. Together, compare all options and decide which ice cream is the best choice. Use phrases like: \"El mango es más caro que el chocolate.\" \"La guayaba es la mejor.\"\n7. Share your final decision: \"Elegimos... porque es...\"", - "vocab": [ - { - "lemma": "chocolate", - "pos": "NOUN", - }, - { - "lemma": "vainilla", - "pos": "NOUN", - }, - { - "lemma": "mango", - "pos": "NOUN", - }, - { - "lemma": "coco", - "pos": "NOUN", - }, - { - "lemma": "café", - "pos": "NOUN", - }, - { - "lemma": "guayaba", - "pos": "NOUN", - }, - { - "lemma": "caro", - "pos": "ADJECTIVE", - }, - { - "lemma": "barato", - "pos": "ADJECTIVE", - }, - { - "lemma": "dulce", - "pos": "ADJECTIVE", - }, - { - "lemma": "rico", - "pos": "ADJECTIVE", - }, - { - "lemma": "mejor", - "pos": "ADJECTIVE", - }, - { - "lemma": "elegir", - "pos": "VERB", - }, - { - "lemma": "comparar", - "pos": "VERB", - }, - { - "lemma": "precio", - "pos": "NOUN", - }, - { - "lemma": "sabor", - "pos": "NOUN", - } - ], - "roles": { - "ce949bad-6dfe-483d-aa86-f8a1f948180b": { - "name": "Customer 1", - "goal": - "Present your ice cream choices and compare them to find the best option.", - "id": "ce949bad-6dfe-483d-aa86-f8a1f948180b", - }, - "f985387c-c75b-4180-aa77-ae75a409ce10": { - "name": "Customer 2", - "goal": - "Present your ice cream choices and compare them to find the best option.", - "id": "f985387c-c75b-4180-aa77-ae75a409ce10", - }, - }, - "req": { - "topic": "Decision Making: Choosing an ice cream", - "mode": "Decision Making", - "objective": - "I can compare prices and flavors to choose the best ice cream option using numbers and basic adjectives.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Azúcar Ice Cream, Little Havana", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "a790942d-24c6-4984-9ee3-feac8223131b", - "title": "Bayside Market Bargain Chat", - "description": - "Chat your way through Bayside Marketplace, asking and answering about prices and quantities!", - "learning_objective": - "I can ask and answer basic questions about prices and quantities in a market setting.", - "instructions": - "You are in a group chat at Bayside Marketplace. Each person has a role. Use Spanish to ask and answer about prices and quantities. Use simple phrases like:\n\n- ¿Cuánto cuesta...? (How much does ... cost?)\n- ¿Cuántos/quántas...? (How many ...?)\n- ¿Tiene(n) ...? (Do you have ...?)\n- Quiero ... (I want ...)\n- Me da ... por favor. (Can you give me ... please?)\n\n**Example conversation:**\n- Tourist: ¿Cuánto cuesta una camiseta?\n- Vendor: Cuesta diez dólares.\n- Shopper: Quiero dos camisetas, por favor.\n- Vendor: Son veinte dólares.\n\nKeep the conversation going, asking about different items, prices, and amounts. Respond in Spanish!", - "vocab": [ - { - "lemma": "cuánto", - "pos": "ADV", - }, - { - "lemma": "cuesta", - "pos": "VERB", - }, - { - "lemma": "cuántos", - "pos": "ADV", - }, - { - "lemma": "tener", - "pos": "VERB", - }, - { - "lemma": "querer", - "pos": "VERB", - }, - { - "lemma": "camiseta", - "pos": "NOUN", - }, - { - "lemma": "dólar", - "pos": "NOUN", - }, - { - "lemma": "mercado", - "pos": "NOUN", - }, - { - "lemma": "vender", - "pos": "VERB", - }, - { - "lemma": "precio", - "pos": "NOUN", - } - ], - "roles": { - "5933c983-6517-47a3-bd0c-e1f021cdb772": { - "name": "Tourist", - "goal": - "Ask about prices and quantities of souvenirs and local items.", - "id": "5933c983-6517-47a3-bd0c-e1f021cdb772", - }, - "e5442a64-3152-4d0a-92fe-7191beed8f34": { - "name": "Vendor", - "goal": - "Answer questions about prices and quantities, and suggest products.", - "id": "e5442a64-3152-4d0a-92fe-7191beed8f34", - }, - "48701f2b-770b-409e-ae99-34b413266f1f": { - "name": "Shopper", - "goal": "Ask for and buy different quantities of items.", - "id": "48701f2b-770b-409e-ae99-34b413266f1f", - }, - "ace68567-d096-4eef-8453-50f9a9795c9e": { - "name": "Local Friend", - "goal": - "Help the Tourist and Shopper with questions and give advice on good deals.", - "id": "ace68567-d096-4eef-8453-50f9a9795c9e", - }, - }, - "req": { - "topic": "Market conversation practice", - "mode": "Conversation", - "objective": - "I can ask and answer basic questions about prices and quantities in a market setting.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Bayside Marketplace", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "1fc73b7b-44a1-4fb5-9e57-e423fba22c2f", - "title": "¡Vamos a la Ciudad de México!", - "description": - "Expand your vocabulary with colors and common objects as you virtually wander through Mexico City's colorful markets. Practice describing the vibrant murals and iconic landmarks of this bustling metropolis.", - "location": "Mexico City", - "id": "8fdd6ab3-60c0-475c-b156-ace929e93dd2", - "activities": [ - { - "activity_id": "7d58d106-519d-4447-a0e4-18ebd2de9185", - "title": "Find the Colorful Market Items!", - "description": - "Explore Mercado de la Merced by asking and answering about colorful items!", - "learning_objective": - "I can ask and answer questions about colors and objects to locate specific items in a market.", - "instructions": - "1. Each of you will take a role: one is the Tourist, the other is the Local.\n2. The Tourist wants to buy colorful items in the market and must ask about different objects and their colors in Spanish. Use questions like:\n - ¿Dónde está la manzana roja?\n - ¿Tienes algo azul?\n - ¿Qué color es el plátano?\n3. The Local will answer, describing where to find the items or what color they are. Use answers like:\n - La manzana roja está en la mesa.\n - Sí, tengo una bolsa azul.\n - El plátano es amarillo.\n4. Take turns asking and answering at least three questions each.\n5. Use the vocab list to help you!", - "vocab": [ - { - "lemma": "color", - "pos": "NOUN", - }, - { - "lemma": "rojo", - "pos": "ADJ", - }, - { - "lemma": "azul", - "pos": "ADJ", - }, - { - "lemma": "verde", - "pos": "ADJ", - }, - { - "lemma": "amarillo", - "pos": "ADJ", - }, - { - "lemma": "manzana", - "pos": "NOUN", - }, - { - "lemma": "plátano", - "pos": "NOUN", - }, - { - "lemma": "bolsa", - "pos": "NOUN", - }, - { - "lemma": "mesa", - "pos": "NOUN", - }, - { - "lemma": "buscar", - "pos": "VERB", - }, - { - "lemma": "tener", - "pos": "VERB", - }, - { - "lemma": "dónde", - "pos": "ADV", - } - ], - "roles": { - "c1332cea-148b-45f1-963a-f2117d465b9f": { - "name": "Tourist", - "goal": - "Ask about the colors and locations of different market items to find them.", - "id": "c1332cea-148b-45f1-963a-f2117d465b9f", - }, - "31ae3b30-a341-498f-8fbd-c04394a7e511": { - "name": "Local", - "goal": - "Answer questions about items' colors and locations in the market.", - "id": "31ae3b30-a341-498f-8fbd-c04394a7e511", - }, - }, - "req": { - "topic": "Exploring Market Colors", - "mode": "Scavenger Hunt", - "objective": - "I can ask and answer questions about colors and objects to locate specific items in a market.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Mercado de la Merced", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "a80a9d54-bbc1-45d0-9799-b4c8e23a0d01", - "title": "Describe the Murals at Bellas Artes", - "description": - "Explore Palacio de Bellas Artes by sharing and describing mural images!", - "learning_objective": - "I can describe the colors and shapes I see in murals using complete sentences.", - "instructions": - "1. Each of you will receive or find an image of a mural from the Palacio de Bellas Artes.\n2. In Spanish, describe the mural you see to your partner. Use colors and shapes. For example:\n - \"Veo un círculo azul y un triángulo rojo.\"\n - \"Hay un cuadrado verde.\"\n3. Ask your partner questions about their mural, such as:\n - \"¿De qué color es el círculo?\"\n - \"¿Cuántos triángulos hay?\"\n4. Respond to your partner's questions in Spanish.\n5. Take turns so both of you describe and ask/answer questions.", - "vocab": [ - { - "lemma": "color", - "pos": "NOUN", - }, - { - "lemma": "forma", - "pos": "NOUN", - }, - { - "lemma": "círculo", - "pos": "NOUN", - }, - { - "lemma": "cuadrado", - "pos": "NOUN", - }, - { - "lemma": "triángulo", - "pos": "NOUN", - }, - { - "lemma": "azul", - "pos": "ADJ", - }, - { - "lemma": "rojo", - "pos": "ADJ", - }, - { - "lemma": "verde", - "pos": "ADJ", - }, - { - "lemma": "amarillo", - "pos": "ADJ", - }, - { - "lemma": "ver", - "pos": "VERB", - }, - { - "lemma": "hay", - "pos": "VERB", - } - ], - "roles": { - "32aa8f4d-93cf-4256-99da-bc2a917fd120": { - "name": "Tourist", - "goal": - "Describe the colors and shapes you see in the mural image.", - "id": "32aa8f4d-93cf-4256-99da-bc2a917fd120", - }, - "6c04698e-4b9a-41a6-9618-8eb75a1b4985": { - "name": "Local", - "goal": - "Ask questions about the mural and respond to your partner's description.", - "id": "6c04698e-4b9a-41a6-9618-8eb75a1b4985", - }, - }, - "req": { - "topic": "Admiring Murals", - "mode": "Conversation", - "objective": - "I can describe the colors and shapes I see in murals using complete sentences.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Palacio de Bellas Artes", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "c9d06b4e-2002-4497-b9f2-0b0f54e35a12", - "title": "Landmark Directions at the Zócalo", - "description": - "Explore the Zócalo by giving and following simple directions in Spanish!", - "learning_objective": - "I can give and follow simple directions and describe landmarks using color and object vocabulary.", - "instructions": - "You will send voice messages in Spanish as your role. Use simple phrases to give or follow directions and describe landmarks at the Zócalo.\n\n**Example phrases:**\n- \"Sigue derecho hasta la iglesia blanca.\"\n- \"A la izquierda está una fuente azul.\"\n- \"Busca el edificio grande y rojo.\"\n\n**Steps:**\n1. The Guide describes a route to a landmark using colors and objects (e.g., \"Camina hasta la estatua negra\").\n2. The Tourist repeats the directions in their own words and asks a clarification question if needed.\n3. The Local answers the Tourist's question and adds a detail about a landmark (e.g., \"La catedral es muy grande y gris\").\n4. Each person sends their message in Spanish as a voice message.", - "vocab": [ - { - "lemma": "caminar", - "pos": "VERB", - }, - { - "lemma": "seguir", - "pos": "VERB", - }, - { - "lemma": "derecho", - "pos": "ADVERB", - }, - { - "lemma": "izquierda", - "pos": "NOUN", - }, - { - "lemma": "derecha", - "pos": "NOUN", - }, - { - "lemma": "buscar", - "pos": "VERB", - }, - { - "lemma": "estatua", - "pos": "NOUN", - }, - { - "lemma": "fuente", - "pos": "NOUN", - }, - { - "lemma": "iglesia", - "pos": "NOUN", - }, - { - "lemma": "edificio", - "pos": "NOUN", - }, - { - "lemma": "catedral", - "pos": "NOUN", - }, - { - "lemma": "blanco", - "pos": "ADJECTIVE", - }, - { - "lemma": "rojo", - "pos": "ADJECTIVE", - }, - { - "lemma": "azul", - "pos": "ADJECTIVE", - }, - { - "lemma": "negro", - "pos": "ADJECTIVE", - }, - { - "lemma": "grande", - "pos": "ADJECTIVE", - } - ], - "roles": { - "b4c2e826-8067-4db2-8457-d6fc6a18cc19": { - "name": "Guide", - "goal": - "Give simple directions to a landmark using color and object words.", - "id": "b4c2e826-8067-4db2-8457-d6fc6a18cc19", - }, - "029b216f-df93-4359-baab-05d117d3f396": { - "name": "Tourist", - "goal": - "Follow the directions and ask for clarification or more details.", - "id": "029b216f-df93-4359-baab-05d117d3f396", - }, - "93f96737-5481-4e0a-b583-2be977987ca4": { - "name": "Local", - "goal": - "Answer the Tourist's question and describe a landmark using colors and objects.", - "id": "93f96737-5481-4e0a-b583-2be977987ca4", - }, - }, - "req": { - "topic": "Guided Tour at the Zócalo", - "mode": "Roleplay", - "objective": - "I can give and follow simple directions and describe landmarks using color and object vocabulary.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Zócalo", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "69e6d1c5-2b36-410c-b8f8-fd7841c24df6", - "title": "Colorful Souvenirs of Coyoacán", - "description": - "Choose the best objects and colors to represent Coyoacán’s famous spots!", - "learning_objective": - "I can discuss and decide which objects and colors best represent Coyoacán’s attractions.", - "instructions": - "1. Imagine you are planning a visit to Barrio de Coyoacán.\n2. You will see or talk about famous places like the Frida Kahlo Museum, the main plaza, and local markets.\n3. Each of you chooses an object (for example: sombrero, flor, libro) and a color (for example: azul, rojo, verde) that you think represents a Coyoacán attraction.\n4. Share your ideas in Spanish. Use phrases like:\n - \"Yo elijo una flor azul para el museo.\"\n - \"Prefiero un libro rojo para la plaza.\"\n5. Discuss together and decide which object and color is the best for each place. Use phrases like:\n - \"¿Qué piensas?\"\n - \"Me gusta tu idea.\"\n - \"Prefiero...\"\n6. Agree on the final choices for each attraction!", - "vocab": [ - { - "lemma": "flor", - "pos": "NOUN", - }, - { - "lemma": "sombrero", - "pos": "NOUN", - }, - { - "lemma": "libro", - "pos": "NOUN", - }, - { - "lemma": "azul", - "pos": "ADJECTIVE", - }, - { - "lemma": "rojo", - "pos": "ADJECTIVE", - }, - { - "lemma": "verde", - "pos": "ADJECTIVE", - }, - { - "lemma": "elegir", - "pos": "VERB", - }, - { - "lemma": "preferir", - "pos": "VERB", - }, - { - "lemma": "museo", - "pos": "NOUN", - }, - { - "lemma": "plaza", - "pos": "NOUN", - } - ], - "roles": { - "eaaadf1b-9e16-4daa-bf6f-001820e7b1ac": { - "name": "Tourist", - "goal": - "Share and discuss objects and colors to represent Coyoacán’s attractions.", - "id": "eaaadf1b-9e16-4daa-bf6f-001820e7b1ac", - }, - "b1420a7c-2876-4dee-b13c-28a2be8d8267": { - "name": "Local", - "goal": - "Suggest and discuss objects and colors that best symbolize Coyoacán’s attractions.", - "id": "b1420a7c-2876-4dee-b13c-28a2be8d8267", - }, - }, - "req": { - "topic": "Planning a Visit to Coyoacán", - "mode": "Decision Making", - "objective": - "I can discuss and decide which objects and colors best represent Coyoacán’s attractions.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Barrio de Coyoacán", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "45f607be-348e-4e36-8e13-c6a580a7a98c", - "title": "Guess the Chapultepec Mystery Object", - "description": - "Use yes/no questions about colors and objects to guess what’s hidden in Chapultepec!", - "learning_objective": - "I can ask yes/no questions using color and object vocabulary to guess unfamiliar items.", - "instructions": - "**How to play:**\n\n1. One of you will choose a secret object found in Bosque de Chapultepec (for example: árbol, banco, lago, flor, estatua, pájaro, etc.).\n2. The other three will take turns asking yes/no questions in Spanish to guess the object. Use color and object words!\n - Ejemplo: ¿Es verde? ¿Es una flor? ¿Es grande?\n3. The chooser only answers with \"sí\" or \"no\".\n4. You have up to 20 questions to guess the object. Work together!\n5. When you think you know, ask: \"¿Es [objeto]?\"", - "vocab": [ - { - "lemma": "árbol", - "pos": "NOUN", - }, - { - "lemma": "flor", - "pos": "NOUN", - }, - { - "lemma": "lago", - "pos": "NOUN", - }, - { - "lemma": "banco", - "pos": "NOUN", - }, - { - "lemma": "estatua", - "pos": "NOUN", - }, - { - "lemma": "pájaro", - "pos": "NOUN", - }, - { - "lemma": "verde", - "pos": "ADJ", - }, - { - "lemma": "rojo", - "pos": "ADJ", - }, - { - "lemma": "azul", - "pos": "ADJ", - }, - { - "lemma": "grande", - "pos": "ADJ", - }, - { - "lemma": "pequeño", - "pos": "ADJ", - }, - { - "lemma": "es", - "pos": "VERB", - }, - { - "lemma": "tiene", - "pos": "VERB", - }, - { - "lemma": "hay", - "pos": "VERB", - } - ], - "roles": { - "1808fe9d-c0ef-46ba-b298-5691942a68e7": { - "name": "Object Chooser", - "goal": - "Think of a secret object in Chapultepec and answer yes/no questions.", - "id": "1808fe9d-c0ef-46ba-b298-5691942a68e7", - }, - "db5e7dd9-f4b5-4988-8fd5-9f2ff763f7b7": { - "name": "Questioner 1", - "goal": - "Ask yes/no questions about color and object to guess the secret item.", - "id": "db5e7dd9-f4b5-4988-8fd5-9f2ff763f7b7", - }, - "9691c1d5-d207-445c-996c-dd2ed594edc2": { - "name": "Questioner 2", - "goal": - "Ask yes/no questions about color and object to guess the secret item.", - "id": "9691c1d5-d207-445c-996c-dd2ed594edc2", - }, - "d007621c-4c58-4ca0-9bb3-a725b8682549": { - "name": "Questioner 3", - "goal": - "Ask yes/no questions about color and object to guess the secret item.", - "id": "d007621c-4c58-4ca0-9bb3-a725b8682549", - }, - }, - "req": { - "topic": "Guessing Objects at Chapultepec", - "mode": "20-Question Game", - "objective": - "I can ask yes/no questions using color and object vocabulary to guess unfamiliar items.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Bosque de Chapultepec", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "c37b7d32-3401-4580-afaf-c91c3c2f388f", - "title": "Descubriendo Bogotá", - "description": - "Learn about family members and professions while exploring Colombia's capital. You'll practice introducing your family and talking about jobs as you 'visit' the historic La Candelaria neighborhood.", - "location": "Bogota", - "id": "dd0d3cc9-a314-46aa-b988-3fd2be9af7bb", - "activities": [ - { - "activity_id": "283e35e0-161f-4370-a6b1-b77c3eff63df", - "title": "Family Word Scavenger Hunt in Plaza de Bolívar", - "description": - "Explore Plaza de Bolívar by sharing images that represent family members!", - "learning_objective": - "I can identify and name at least five family members in Spanish by finding images or objects representing them around the site.", - "instructions": - "1. Each of you will take on a role. One will be the Tourist, the other the Local.\n2. Look for images online or use photos you have that could represent family members (for example, a photo of a couple for 'padres', a child for 'hermano', etc.).\n3. Share the images in the group chat and, using Spanish, name the family member you are representing. For example: \"Este es mi hermano\" or \"Aquí están los abuelos\".\n4. Try to find and name at least five different family members from the vocab list.\n5. Respond to each other's images with a short Spanish comment or question, such as \"¿Es tu hermana?\" or \"¡Qué bonita familia!\".", - "vocab": [ - { - "lemma": "madre", - "pos": "NOUN", - }, - { - "lemma": "padre", - "pos": "NOUN", - }, - { - "lemma": "hermano", - "pos": "NOUN", - }, - { - "lemma": "hermana", - "pos": "NOUN", - }, - { - "lemma": "abuelo", - "pos": "NOUN", - }, - { - "lemma": "abuela", - "pos": "NOUN", - }, - { - "lemma": "tío", - "pos": "NOUN", - }, - { - "lemma": "tía", - "pos": "NOUN", - }, - { - "lemma": "primo", - "pos": "NOUN", - }, - { - "lemma": "prima", - "pos": "NOUN", - } - ], - "roles": { - "36ffd38d-0ba3-4a42-a1e0-3437c350627c": { - "name": "Tourist", - "goal": - "Find and share images representing at least five different family members in Spanish, and use Spanish to name them.", - "id": "36ffd38d-0ba3-4a42-a1e0-3437c350627c", - }, - "a2ab19fe-83fc-4ca7-8887-b0c4bc46b77b": { - "name": "Local", - "goal": - "Find and share images representing at least five different family members in Spanish, and use Spanish to name them. Respond to the Tourist's images with comments or questions in Spanish.", - "id": "a2ab19fe-83fc-4ca7-8887-b0c4bc46b77b", - }, - }, - "req": { - "topic": "Family Vocabulary Scavenger Hunt", - "mode": "Scavenger Hunt", - "objective": - "I can identify and name at least five family members in Spanish by finding images or objects representing them around the site.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Plaza de Bolívar", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "c861a437-19d6-4a68-8ce4-ba6bc26fedf6", - "title": "Family Introductions at the Café", - "description": - "Meet at Café de la Candelaria and introduce your family in Spanish!", - "learning_objective": - "I can introduce my family members, stating their names and relationships, and ask similar questions.", - "instructions": - "**Instructions:**\n\n1. Imagine you are friends meeting at Café de la Candelaria. Send a voice message introducing 2-3 family members. Say their names and your relationship. For example:\n - \"Esta es mi madre. Se llama Ana.\"\n - \"Este es mi hermano. Se llama Pablo.\"\n2. Listen to your partner's voice message. Then, send a voice message asking about one of their family members. For example:\n - \"¿Cómo se llama tu padre?\"\n - \"¿Tienes hermanos?\"\n3. Respond to your partner’s question with a short voice message.\n\nTry to use full sentences and the vocabulary below!", - "vocab": [ - { - "lemma": "madre", - "pos": "NOUN", - }, - { - "lemma": "padre", - "pos": "NOUN", - }, - { - "lemma": "hermano", - "pos": "NOUN", - }, - { - "lemma": "hermana", - "pos": "NOUN", - }, - { - "lemma": "abuelo", - "pos": "NOUN", - }, - { - "lemma": "abuela", - "pos": "NOUN", - }, - { - "lemma": "llamar", - "pos": "VERB", - }, - { - "lemma": "ser", - "pos": "VERB", - }, - { - "lemma": "nombre", - "pos": "NOUN", - }, - { - "lemma": "familia", - "pos": "NOUN", - }, - { - "lemma": "tener", - "pos": "VERB", - } - ], - "roles": { - "eb4379a7-3afe-4022-9fd4-be37ff774b67": { - "name": "Introducer", - "goal": - "Introduce your family members and answer questions about them.", - "id": "eb4379a7-3afe-4022-9fd4-be37ff774b67", - }, - "701f9498-3187-4fb6-b5ff-1ff0b7b601ce": { - "name": "Questioner", - "goal": - "Ask questions about your partner's family and share about your own family.", - "id": "701f9498-3187-4fb6-b5ff-1ff0b7b601ce", - }, - }, - "req": { - "topic": "Introducing Your Family", - "mode": "Conversation", - "objective": - "I can introduce my family members, stating their names and relationships, and ask similar questions.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Café de la Candelaria", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "79d141b7-878f-4a52-aea7-f88ac2826638", - "title": "Job Interview at Museo Botero", - "description": - "Roleplay a job interview in the famous Museo Botero art museum!", - "learning_objective": - "I can roleplay a job interview, describing a candidate’s profession, skills, and asking about their work experience.", - "instructions": - "You are at Museo Botero. Each person has a role. Use simple Spanish to ask and answer questions about jobs and experience.\n\n**1. The Interviewer:**\n- Greet the candidate: \"Hola, ¿cómo estás?\"\n- Ask about their profession: \"¿Cuál es tu profesión?\"\n- Ask about skills: \"¿Qué habilidades tienes?\"\n- Ask about experience: \"¿Dónde has trabajado antes?\"\n\n**2. The Candidate:**\n- Respond to questions. Example:\n - \"Soy guía de museo.\"\n - \"Tengo habilidades en arte y comunicación.\"\n - \"He trabajado en el Museo Nacional.\"\n\n**3. The Museum Visitor:**\n- Ask a question about the candidate’s job: \"¿Por qué quieres trabajar aquí?\"\n- Say something about the museum: \"El museo es muy bonito.\"\n\n*Take turns chatting in your roles. Use the example phrases to help you.*", - "vocab": [ - { - "lemma": "profesión", - "pos": "NOUN", - }, - { - "lemma": "habilidad", - "pos": "NOUN", - }, - { - "lemma": "experiencia", - "pos": "NOUN", - }, - { - "lemma": "trabajar", - "pos": "VERB", - }, - { - "lemma": "museo", - "pos": "NOUN", - }, - { - "lemma": "guía", - "pos": "NOUN", - }, - { - "lemma": "arte", - "pos": "NOUN", - }, - { - "lemma": "comunicación", - "pos": "NOUN", - }, - { - "lemma": "preguntar", - "pos": "VERB", - }, - { - "lemma": "responder", - "pos": "VERB", - } - ], - "roles": { - "9e586ba9-1098-4736-993b-abcb89c94872": { - "name": "Interviewer", - "goal": - "Ask about the candidate’s profession, skills, and experience for a job at Museo Botero.", - "id": "9e586ba9-1098-4736-993b-abcb89c94872", - }, - "d15f86a2-c390-481c-8341-6e6bb42e3196": { - "name": "Candidate", - "goal": - "Describe your profession, skills, and work experience in Spanish, applying for a museum job.", - "id": "d15f86a2-c390-481c-8341-6e6bb42e3196", - }, - "7e112533-0e43-457e-8ad8-4bad8b4a4e90": { - "name": "Museum Visitor", - "goal": - "Ask the candidate a question about their job and comment on the museum.", - "id": "7e112533-0e43-457e-8ad8-4bad8b4a4e90", - }, - }, - "req": { - "topic": "Job Interview Roleplay", - "mode": "Roleplay", - "objective": - "I can roleplay a job interview, describing a candidate’s profession, skills, and asking about their work experience.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Museo Botero", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "62b2cc6e-27df-47b0-b66c-421927498b03", - "title": "Professions Debate in Parque de Usaquén", - "description": - "Debate which job is most important for society while chatting in the park!", - "learning_objective": - "I can argue for and against different professions, providing reasons why a particular job is important to society.", - "instructions": - "1. Imagine you are in Parque de Usaquén. Each of you chooses one profession from: médico, maestro, policía, artista, or bombero.\n2. Take your role and, in Spanish, say why your profession is important. Use phrases like:\n - \"Mi profesión es importante porque...\"\n - \"Ayudo a las personas cuando...\"\n - \"Sin mi trabajo, la sociedad...\"\n3. Listen to your partner and respond with one reason why their profession is also important, using:\n - \"También es importante porque...\"\n4. Finish by saying which profession you think is the most important and why, using:\n - \"Creo que el/la [profesión] es más importante porque...\"", - "vocab": [ - { - "lemma": "profesión", - "pos": "NOUN", - }, - { - "lemma": "importante", - "pos": "ADJ", - }, - { - "lemma": "sociedad", - "pos": "NOUN", - }, - { - "lemma": "trabajo", - "pos": "NOUN", - }, - { - "lemma": "ayudar", - "pos": "VERB", - }, - { - "lemma": "persona", - "pos": "NOUN", - }, - { - "lemma": "porque", - "pos": "SCONJ", - }, - { - "lemma": "también", - "pos": "ADV", - }, - { - "lemma": "creer", - "pos": "VERB", - } - ], - "roles": { - "f8f45dc0-d498-401f-b16e-c9f1649b7b4a": { - "name": "Debater 1", - "goal": - "Argue why your chosen profession is important to society.", - "id": "f8f45dc0-d498-401f-b16e-c9f1649b7b4a", - }, - "6fb88f31-2f34-4e2f-9302-c9ce32104106": { - "name": "Debater 2", - "goal": - "Argue why your chosen profession is important to society.", - "id": "6fb88f31-2f34-4e2f-9302-c9ce32104106", - }, - }, - "req": { - "topic": "Debating Professions", - "mode": "Debate", - "objective": - "I can argue for and against different professions, providing reasons why a particular job is important to society.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Parque de Usaquén", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "4d32b70e-e116-4925-bed6-c786dd803fbe", - "title": "Choose the Festival Team at Mercado de Paloquemao", - "description": - "Work together at the lively Mercado de Paloquemao to pick the best professions for your community festival!", - "learning_objective": - "I can collaborate with peers to decide which professions to involve in organizing a community festival, explaining my choices.", - "instructions": - "You are planning a community festival at Mercado de Paloquemao. Each of you has a different role. Together, decide which three professions (from a list) are most important to invite for the festival. Explain your choices in simple Spanish. \n\n**Professions list:** cocinero/a (cook), músico/a (musician), florista (florist), fotógrafo/a (photographer), panadero/a (baker), policía (police officer), artista (artist), guía (guide)\n\n**How to participate:**\n1. Read your role and goal.\n2. Suggest one profession you think is important. Use phrases like:\n- \"Yo pienso que necesitamos un/una [profesión].\"\n- \"Porque [explicación simple].\"\n3. Listen to others' suggestions. Respond with:\n- \"Sí, estoy de acuerdo.\"\n- \"No estoy de acuerdo, prefiero [profesión].\"\n4. As a group, agree on three professions. \n\n**Example:**\n- \"Yo pienso que necesitamos un músico porque la música es divertida.\"\n- \"Sí, estoy de acuerdo.\"\n- \"No estoy de acuerdo, prefiero un panadero porque necesitamos comida.\"\n\n¡Trabajen juntos y elijan las mejores profesiones para el festival!", - "vocab": [ - { - "lemma": "cocinero", - "pos": "NOUN", - }, - { - "lemma": "músico", - "pos": "NOUN", - }, - { - "lemma": "florista", - "pos": "NOUN", - }, - { - "lemma": "fotógrafo", - "pos": "NOUN", - }, - { - "lemma": "panadero", - "pos": "NOUN", - }, - { - "lemma": "policía", - "pos": "NOUN", - }, - { - "lemma": "artista", - "pos": "NOUN", - }, - { - "lemma": "guía", - "pos": "NOUN", - }, - { - "lemma": "necesitar", - "pos": "VERB", - }, - { - "lemma": "porque", - "pos": "SCONJ", - }, - { - "lemma": "acuerdo", - "pos": "NOUN", - }, - { - "lemma": "preferir", - "pos": "VERB", - } - ], - "roles": { - "355d7d89-ace7-47ab-bf42-0728d8984700": { - "name": "Local Vendor", - "goal": - "Suggest professions that help with food and market logistics.", - "id": "355d7d89-ace7-47ab-bf42-0728d8984700", - }, - "f467d37b-f161-458c-b292-8b86ebfdd0fd": { - "name": "Festival Visitor", - "goal": - "Support professions that make the festival fun and interesting.", - "id": "f467d37b-f161-458c-b292-8b86ebfdd0fd", - }, - "e8974915-cbc7-49d7-93a6-bf676d5d4d4f": { - "name": "Security Officer", - "goal": "Advocate for safety and organization at the event.", - "id": "e8974915-cbc7-49d7-93a6-bf676d5d4d4f", - }, - "fe7e6439-d8d6-4b64-8e84-e1a9716a71b2": { - "name": "Community Organizer", - "goal": - "Encourage teamwork and balance in the final decisions.", - "id": "fe7e6439-d8d6-4b64-8e84-e1a9716a71b2", - }, - }, - "req": { - "topic": "Planning a Community Event", - "mode": "Decision Making", - "objective": - "I can collaborate with peers to decide which professions to involve in organizing a community festival, explaining my choices.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Mercado de Paloquemao", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "45497ed0-7ba3-4b93-889e-a7b492d20c39", - "title": "Sabores de Lima", - "description": - "Dive into food vocabulary and simple preferences as you explore Lima's world-renowned culinary scene. Learn to order ceviche, discuss your favorite Peruvian dishes, and express likes and dislikes.", - "location": "Lima", - "id": "235c54c1-ebcd-4f6c-be27-e809ec316e42", - "activities": [ - { - "activity_id": "70f5b7b9-2375-4168-a3c8-2203dff7cb04", - "title": "Order Like a Local at La Mar Cebichería", - "description": - "Step into La Mar Cebichería and order ceviche, drinks, and the bill in Spanish!", - "learning_objective": - "I can order ceviche, ask for a drink, and request the bill politely in Spanish.", - "instructions": - "**1. Imagine you are at La Mar Cebichería in Miraflores.**\n\n**2. Each of you has a role. Use the example phrases to help you.**\n\n**Tourist:**\n- Greet the waiter politely: \"¡Hola! Buenas tardes.\"\n- Order a type of ceviche: \"Quisiera un ceviche clásico, por favor.\"\n- Ask for a drink: \"¿Me puede traer una limonada, por favor?\"\n- Request the bill: \"¿Me trae la cuenta, por favor?\"\n\n**Waiter:**\n- Greet the tourist: \"¡Bienvenidos a La Mar! ¿En qué puedo ayudarle?\"\n- Confirm the order: \"¿Desea algo más?\"\n- Respond to the drink request: \"Enseguida le traigo su limonada.\"\n- Bring the bill: \"Aquí tiene la cuenta. Muchas gracias.\"\n\n**3. Exchange your messages in the chat, following your roles.**\n\n**4. Try to use the Spanish phrases and vocabulary provided!**", - "vocab": [ - { - "lemma": "ceviche", - "pos": "NOUN", - }, - { - "lemma": "cuenta", - "pos": "NOUN", - }, - { - "lemma": "bebida", - "pos": "NOUN", - }, - { - "lemma": "por favor", - "pos": "ADV", - }, - { - "lemma": "traer", - "pos": "VERB", - }, - { - "lemma": "quisiera", - "pos": "VERB", - }, - { - "lemma": "limonada", - "pos": "NOUN", - }, - { - "lemma": "clásico", - "pos": "ADJ", - }, - { - "lemma": "hola", - "pos": "INTJ", - }, - { - "lemma": "gracias", - "pos": "NOUN", - } - ], - "roles": { - "4ef3de22-fec7-472d-bc80-74fcbf2c9211": { - "name": "Tourist", - "goal": - "Order ceviche, ask for a drink, and request the bill politely in Spanish.", - "id": "4ef3de22-fec7-472d-bc80-74fcbf2c9211", - }, - "c5f4b272-3ccd-4fd2-b510-dcef352e873a": { - "name": "Waiter", - "goal": - "Take the order, respond politely, and bring the bill in Spanish.", - "id": "c5f4b272-3ccd-4fd2-b510-dcef352e873a", - }, - }, - "req": { - "topic": "Ordering at a cevichería", - "mode": "Roleplay", - "objective": - "I can order ceviche, ask for a drink, and request the bill politely in Spanish.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "La Mar Cebichería (Miraflores)", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "a57d23fa-1c97-43ce-bd0f-016c00aa78c4", - "title": "Peruvian Market Ingredient Hunt", - "description": - "Explore Mercado de Surquillo by finding and asking about Peruvian ingredients!", - "learning_objective": - "I can identify key Peruvian ingredients and ask vendors for prices and quantities at the market.", - "instructions": - "1. Look at the images of fresh ingredients from Mercado de Surquillo shared in the chat.\n2. **Tourist:** Choose an ingredient you see and ask about it in Spanish. Use phrases like:\n - ¿Qué es esto?\n - ¿Cuánto cuesta el/la [ingrediente]?\n - ¿Cuánto es por un kilo?\n3. **Vendor:** Respond in Spanish. For example:\n - Es [nombre del ingrediente].\n - Cuesta 5 soles el kilo.\n - Tenemos papas, ají amarillo, maíz.\n4. Switch images and repeat so you practice with different ingredients.\n5. Try to use the new words from the vocab list!", - "vocab": [ - { - "lemma": "papa", - "pos": "NOUN", - }, - { - "lemma": "ají amarillo", - "pos": "NOUN", - }, - { - "lemma": "maíz", - "pos": "NOUN", - }, - { - "lemma": "cebolla", - "pos": "NOUN", - }, - { - "lemma": "tomate", - "pos": "NOUN", - }, - { - "lemma": "cuánto", - "pos": "ADV", - }, - { - "lemma": "kilo", - "pos": "NOUN", - }, - { - "lemma": "sol", - "pos": "NOUN", - }, - { - "lemma": "ingrediente", - "pos": "NOUN", - }, - { - "lemma": "mercado", - "pos": "NOUN", - }, - { - "lemma": "precio", - "pos": "NOUN", - }, - { - "lemma": "vender", - "pos": "VERB", - }, - { - "lemma": "comprar", - "pos": "VERB", - } - ], - "roles": { - "fdfd49b1-bb22-408c-a532-f29e76ecb88b": { - "name": "Tourist", - "goal": - "Ask about Peruvian ingredients and their prices using Spanish phrases.", - "id": "fdfd49b1-bb22-408c-a532-f29e76ecb88b", - }, - "da3c99fa-ad1b-4b8e-a515-57c83561ff64": { - "name": "Vendor", - "goal": - "Answer questions about ingredients and prices in Spanish, using simple vocabulary.", - "id": "da3c99fa-ad1b-4b8e-a515-57c83561ff64", - }, - }, - "req": { - "topic": "Exploring fresh ingredients", - "mode": "Scavenger Hunt", - "objective": - "I can identify key Peruvian ingredients and ask vendors for prices and quantities at the market.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Mercado de Surquillo", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "d0b432bd-1312-4842-90e2-14b832434604", - "title": "Peruvian Food Favorites Chat", - "description": - "Send voice messages about your favorite Peruvian dishes at Parque Kennedy!", - "learning_objective": - "I can discuss my favorite Peruvian dishes and ask classmates about their preferences using gusta and encantar.", - "instructions": - "**Instructions:**\n\n1. Imagine you are at the food stalls in Parque Kennedy, Miraflores.\n2. Send a voice message introducing yourself and saying which Peruvian dish you like or love using \"me gusta\" or \"me encanta\". For example:\n - \"¡Hola! Me gusta el ceviche. ¿Y a ti?\"\n - \"Me encanta el lomo saltado. ¿Cuál es tu plato favorito?\"\n3. Listen to your partner's message. Reply with a voice message asking about their preferences or sharing another dish you like.\n4. Use these phrases:\n - \"¿Te gusta...?\"\n - \"¿Te encanta...?\"\n - \"Me gusta...\"\n - \"No me gusta...\"\n - \"Mi plato favorito es...\"\n5. Continue exchanging at least 2 voice messages each. Try to mention at least 2 different dishes.", - "vocab": [ - { - "lemma": "gustar", - "pos": "VERB", - }, - { - "lemma": "encantar", - "pos": "VERB", - }, - { - "lemma": "plato", - "pos": "NOUN", - }, - { - "lemma": "favorito", - "pos": "ADJ", - }, - { - "lemma": "ceviche", - "pos": "NOUN", - }, - { - "lemma": "lomo saltado", - "pos": "NOUN", - }, - { - "lemma": "anticucho", - "pos": "NOUN", - }, - { - "lemma": "ají de gallina", - "pos": "NOUN", - }, - { - "lemma": "pollo a la brasa", - "pos": "NOUN", - }, - { - "lemma": "picarones", - "pos": "NOUN", - } - ], - "roles": { - "b5442a62-2179-4a6a-8235-adddbbe03216": { - "name": "Tourist", - "goal": - "Share your favorite Peruvian dishes and ask about your partner's preferences.", - "id": "b5442a62-2179-4a6a-8235-adddbbe03216", - }, - "6d6b2e97-7803-42b5-9734-ace832cbeb42": { - "name": "Local", - "goal": - "Talk about your favorite Peruvian dishes and ask the tourist about their likes and dislikes.", - "id": "6d6b2e97-7803-42b5-9734-ace832cbeb42", - }, - }, - "req": { - "topic": "Expressing likes and dislikes", - "mode": "Conversation", - "objective": - "I can discuss my favorite Peruvian dishes and ask classmates about their preferences using gusta and encantar.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Parque Kennedy food stalls (Miraflores)", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "316ccc7b-a785-491d-943f-383b8b89280e", - "title": "Barrio Chino Menu Debate", - "description": - "Pick your perfect three-course meal in Jirón Capón and explain your choices!", - "learning_objective": - "I can choose a three-course meal and explain my choices using vocabulary for starters, mains, and desserts.", - "instructions": - "**Instructions:**\n\n1. Read your role and goal.\n2. Look at the menu options below (starters, mains, desserts).\n3. In Spanish, choose one dish from each section and explain your choices to the group using simple phrases. For example:\n - \"De primero, quiero sopa porque me gusta.\"\n - \"De segundo, el arroz chaufa porque es delicioso.\"\n - \"De postre, el flan porque es dulce.\"\n4. Listen to the others and ask one question about their choices. For example: \"¿Por qué te gusta el arroz chaufa?\"\n5. Discuss as a group: Which three-course meal sounds best for visiting Barrio Chino?\n\n**Menu Example:**\n- Entradas: sopa, ensalada, rollo primavera\n- Platos principales: arroz chaufa, tallarines, pollo con verduras\n- Postres: flan, helado, frutas\n\nRemember to use Spanish words from the menu and simple sentences!", - "vocab": [ - { - "lemma": "sopa", - "pos": "NOUN", - }, - { - "lemma": "ensalada", - "pos": "NOUN", - }, - { - "lemma": "rollo primavera", - "pos": "NOUN", - }, - { - "lemma": "arroz chaufa", - "pos": "NOUN", - }, - { - "lemma": "tallarines", - "pos": "NOUN", - }, - { - "lemma": "pollo", - "pos": "NOUN", - }, - { - "lemma": "verdura", - "pos": "NOUN", - }, - { - "lemma": "flan", - "pos": "NOUN", - }, - { - "lemma": "helado", - "pos": "NOUN", - }, - { - "lemma": "fruta", - "pos": "NOUN", - }, - { - "lemma": "elegir", - "pos": "VERB", - }, - { - "lemma": "gustar", - "pos": "VERB", - }, - { - "lemma": "querer", - "pos": "VERB", - }, - { - "lemma": "delicioso", - "pos": "ADJ", - }, - { - "lemma": "dulce", - "pos": "ADJ", - } - ], - "roles": { - "39437a84-9374-44d6-809a-5aea4bc5a61a": { - "name": "Tourist", - "goal": - "Choose a three-course meal and explain your choices as a visitor to Barrio Chino.", - "id": "39437a84-9374-44d6-809a-5aea4bc5a61a", - }, - "61774f83-cf19-4a2f-8257-cf66b181564d": { - "name": "Local", - "goal": - "Select your favorite local dishes for a three-course meal and share why you recommend them.", - "id": "61774f83-cf19-4a2f-8257-cf66b181564d", - }, - "ea8558db-733a-4d26-a38d-7914a2c0e351": { - "name": "Chef", - "goal": - "Suggest a balanced three-course meal from the menu and explain your choices to the others.", - "id": "ea8558db-733a-4d26-a38d-7914a2c0e351", - }, - }, - "req": { - "topic": "Making menu choices", - "mode": "Decision Making", - "objective": - "I can choose a three-course meal and explain my choices using vocabulary for starters, mains, and desserts.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Barrio Chino (Jirón Capón)", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "5854f811-bd10-45b0-bbbb-764ef6e1cc9a", - "title": "Guess the Peruvian Dish!", - "description": - "Challenge each other to guess famous Peruvian dishes at the food museum.", - "learning_objective": - "I can ask and answer yes/no questions to guess traditional Peruvian dishes by name.", - "instructions": - "1. You are in the Museo de la Gastronomía Peruana in Barranco. One of you thinks of a traditional Peruvian dish (for example: ceviche, lomo saltado, ají de gallina).\n2. The other asks yes/no questions in Spanish to guess the dish. Use questions like:\n - ¿Es de pescado? (Is it made with fish?)\n - ¿Es caliente? (Is it hot?)\n - ¿Tiene arroz? (Does it have rice?)\n - ¿Es picante? (Is it spicy?)\n3. Only answer with “sí” (yes) or “no” (no).\n4. You have up to 20 questions to guess the dish!\n5. When you think you know, ask: ¿Es [nombre del plato]? (Is it [dish name]?)\n\n¡Diviértanse y aprendan sobre la comida peruana!", - "vocab": [ - { - "lemma": "preguntar", - "pos": "VERB", - }, - { - "lemma": "responder", - "pos": "VERB", - }, - { - "lemma": "plato", - "pos": "NOUN", - }, - { - "lemma": "comida", - "pos": "NOUN", - }, - { - "lemma": "sí", - "pos": "ADV", - }, - { - "lemma": "no", - "pos": "ADV", - }, - { - "lemma": "pescado", - "pos": "NOUN", - }, - { - "lemma": "arroz", - "pos": "NOUN", - }, - { - "lemma": "picante", - "pos": "ADJ", - }, - { - "lemma": "caliente", - "pos": "ADJ", - } - ], - "roles": { - "10380c55-1cb5-47f1-b13c-b4252d9eba74": { - "name": "Guesser", - "goal": - "Ask yes/no questions in Spanish to identify the Peruvian dish.", - "id": "10380c55-1cb5-47f1-b13c-b4252d9eba74", - }, - "c06407a5-a608-43f6-90e4-7592df4f1e6f": { - "name": "Dish Thinker", - "goal": - "Secretly choose a Peruvian dish and answer yes/no questions in Spanish.", - "id": "c06407a5-a608-43f6-90e4-7592df4f1e6f", - }, - }, - "req": { - "topic": "Peruvian food trivia", - "mode": "20-Question Game", - "objective": - "I can ask and answer yes/no questions to guess traditional Peruvian dishes by name.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Museo de la Gastronomía Peruana (Barranco)", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "17d975b6-6986-4f0a-a125-84e193ee5416", - "title": "Barcelona: Arte y Cultura", - "description": - "Discover how to talk about hobbies and daily routines while exploring Barcelona's artistic heritage. Practice describing Gaudí's architecture and expressing your daily activities in the context of this vibrant Catalan city.", - "location": "Barcelona", - "id": "60a5dc92-3137-4dec-b978-972df1c56b0a", - "activities": [ - { - "activity_id": "bfa84016-e22b-40bc-af24-52eb49930fee", - "title": "Daily Routines at Park Güell", - "description": - "Share and discover daily routines while imagining a visit to Park Güell!", - "learning_objective": - "I can talk about my daily routine and ask about others' routines using reflexive verbs and routine vocabulary.", - "instructions": - "1. Imagine you are at Park Güell in Barcelona.\n2. Each person takes their role. Use the example questions and answers to guide your conversation.\n3. Use Spanish reflexive verbs and daily routine vocabulary. \n\n**Example questions:**\n- ¿A qué hora te despiertas normalmente?\n- ¿Te duchas antes de salir de casa?\n- ¿Desayunas en casa o fuera?\n- ¿Qué haces después de visitar Park Güell?\n\n**Example answers:**\n- Me despierto a las siete.\n- Me ducho por la mañana.\n- Desayuno en una cafetería.\n- Después, me relajo en el parque.\n\nTake turns asking and answering about your routines!", - "vocab": [ - { - "lemma": "despertarse", - "pos": "VERB", - }, - { - "lemma": "ducharse", - "pos": "VERB", - }, - { - "lemma": "desayunar", - "pos": "VERB", - }, - { - "lemma": "salir", - "pos": "VERB", - }, - { - "lemma": "visitar", - "pos": "VERB", - }, - { - "lemma": "relajarse", - "pos": "VERB", - }, - { - "lemma": "parque", - "pos": "NOUN", - }, - { - "lemma": "mañana", - "pos": "NOUN", - }, - { - "lemma": "casa", - "pos": "NOUN", - }, - { - "lemma": "cafetería", - "pos": "NOUN", - } - ], - "roles": { - "71e9a0ca-f5cf-49ea-abb8-9b5200e577b0": { - "name": "Tourist", - "goal": - "Share your daily routine and ask about your partner's routine while visiting Park Güell.", - "id": "71e9a0ca-f5cf-49ea-abb8-9b5200e577b0", - }, - "65d1b2d7-eb19-4626-8526-adeb8801f82f": { - "name": "Local", - "goal": - "Describe your daily routine and answer questions about routines, especially related to Park Güell.", - "id": "65d1b2d7-eb19-4626-8526-adeb8801f82f", - }, - }, - "req": { - "topic": "Daily Routines at Park Güell", - "mode": "Conversation", - "objective": - "I can talk about my daily routine and ask about others' routines using reflexive verbs and routine vocabulary.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Park Güell", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "2f031287-db0d-4f6e-95f5-0fa5346db052", - "title": "Tour Guide Voice Roleplay: Sagrada Família", - "description": - "Explore Sagrada Família together! Describe its features and share your preferences in Spanish voice messages.", - "learning_objective": - "I can describe architectural features of Sagrada Família and express my preferences using descriptive adjectives.", - "instructions": - "1. **Guide:**\n - Send a voice message welcoming your friend to Sagrada Família.\n - Describe 2-3 parts of the church using simple adjectives. For example:\n - \"La iglesia es grande y bonita. Las torres son altas.\"\n - \"La puerta es interesante.\"\n - Ask: \"¿Qué parte te gusta más?\"\n\n2. **Tourist:**\n - Listen to the guide's message.\n - Reply with a voice message saying which part you like, using an adjective. For example:\n - \"Me gusta la puerta. Es bonita.\"\n - \"Prefiero las torres. Son impresionantes.\"\n - Thank the guide!\n\n*Use the vocab list to help you describe and express your preferences.*", - "vocab": [ - { - "lemma": "torre", - "pos": "NOUN", - }, - { - "lemma": "puerta", - "pos": "NOUN", - }, - { - "lemma": "iglesia", - "pos": "NOUN", - }, - { - "lemma": "grande", - "pos": "ADJECTIVE", - }, - { - "lemma": "bonito", - "pos": "ADJECTIVE", - }, - { - "lemma": "alto", - "pos": "ADJECTIVE", - }, - { - "lemma": "interesante", - "pos": "ADJECTIVE", - }, - { - "lemma": "impresionante", - "pos": "ADJECTIVE", - }, - { - "lemma": "gustar", - "pos": "VERB", - }, - { - "lemma": "preferir", - "pos": "VERB", - } - ], - "roles": { - "09b27c5f-81bf-4bf1-aaae-616827ccf1f2": { - "name": "Guide", - "goal": - "Describe parts of Sagrada Família and ask about preferences.", - "id": "09b27c5f-81bf-4bf1-aaae-616827ccf1f2", - }, - "29951901-5197-409b-b73b-bc80353b3d3a": { - "name": "Tourist", - "goal": - "Express which part you like and describe it with an adjective.", - "id": "29951901-5197-409b-b73b-bc80353b3d3a", - }, - }, - "req": { - "topic": "Tour Guide at Sagrada Família", - "mode": "Roleplay", - "objective": - "I can describe architectural features of Sagrada Família and express my preferences using descriptive adjectives.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Sagrada Família", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "184b72b4-bbd8-41a8-95e9-3797868be3b2", - "title": "Hobbies Hunt in Barri Gòtic", - "description": - "Explore Barri Gòtic by asking about hobbies, sharing images, and finding famous spots!", - "learning_objective": - "I can ask and answer questions about hobbies using question words and frequency adverbs while finding cultural landmarks.", - "instructions": - "**Step 1:** Each of you is assigned a role (see below).\n\n**Step 2:** Tourist: Choose a famous place in Barri Gòtic (e.g., Catedral de Barcelona). Send an image of it (can be from the internet) and ask about hobbies using Spanish question words and frequency adverbs. Example: _¿Qué te gusta hacer los fines de semana? ¿Siempre visitas museos?_\n\n**Step 3:** Local: Reply to the Tourist’s questions about hobbies. Use frequency adverbs in your answers (e.g., siempre, a veces, nunca). Example: _Me gusta leer libros y a veces paseo por el barrio._ Then, ask a new question about hobbies or the landmark.\n\n**Step 4:** Guide: Add a fun fact about the landmark and ask both the Tourist and Local a question about their hobbies or favorite activities in the area. Example: _¿Qué hobby te gustaría practicar aquí?_ \n\n**Step 5:** Continue the conversation, each sending at least one image and two questions/answers using the target vocab. Try to use all the question words and frequency adverbs!\n\n**Useful Spanish question words:**\n- ¿Qué?\n- ¿Dónde?\n- ¿Cuándo?\n- ¿Por qué?\n- ¿Cómo?\n\n**Frequency adverbs:**\n- siempre, a veces, nunca, normalmente, todos los días", - "vocab": [ - { - "lemma": "hobby", - "pos": "NOUN", - }, - { - "lemma": "preguntar", - "pos": "VERB", - }, - { - "lemma": "responder", - "pos": "VERB", - }, - { - "lemma": "siempre", - "pos": "ADV", - }, - { - "lemma": "a veces", - "pos": "ADV", - }, - { - "lemma": "nunca", - "pos": "ADV", - }, - { - "lemma": "normalmente", - "pos": "ADV", - }, - { - "lemma": "todos los días", - "pos": "ADV", - }, - { - "lemma": "qué", - "pos": "PRON", - }, - { - "lemma": "dónde", - "pos": "PRON", - }, - { - "lemma": "cuándo", - "pos": "PRON", - }, - { - "lemma": "por qué", - "pos": "PRON", - }, - { - "lemma": "cómo", - "pos": "PRON", - }, - { - "lemma": "gustar", - "pos": "VERB", - }, - { - "lemma": "hacer", - "pos": "VERB", - }, - { - "lemma": "visitar", - "pos": "VERB", - }, - { - "lemma": "leer", - "pos": "VERB", - }, - { - "lemma": "pasear", - "pos": "VERB", - } - ], - "roles": { - "f63819ee-d7f8-4792-b091-a825521f87da": { - "name": "Tourist", - "goal": - "Ask about hobbies and share images of Barri Gòtic landmarks.", - "id": "f63819ee-d7f8-4792-b091-a825521f87da", - }, - "57b8f2a2-ff87-4881-849e-18bf8432d74f": { - "name": "Local", - "goal": - "Answer questions about hobbies using frequency adverbs and ask follow-up questions.", - "id": "57b8f2a2-ff87-4881-849e-18bf8432d74f", - }, - "74f48e40-fe9e-4c5b-bc5e-88c710e7b510": { - "name": "Guide", - "goal": - "Share fun facts about landmarks and ask everyone about their hobbies or activities at the site.", - "id": "74f48e40-fe9e-4c5b-bc5e-88c710e7b510", - }, - }, - "req": { - "topic": "Hobbies Scavenger Hunt in Barri Gòtic", - "mode": "Scavenger Hunt", - "objective": - "I can ask and answer questions about hobbies using question words and frequency adverbs while finding cultural landmarks.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Barri Gòtic", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "8e9b875e-cc9d-4cf6-92e7-cea5e629feb2", - "title": "Planning a Visit to Museu Picasso", - "description": - "Work together to plan your perfect visit to Museu Picasso in Barcelona!", - "learning_objective": - "I can suggest and decide on a museum visit plan using time expressions and modal verbs.", - "instructions": - "**Instructions:**\n\n1. Each of you has a special role. Read your role and goal.\n2. In the group chat, discuss and suggest a plan to visit Museu Picasso using simple Spanish phrases.\n3. Use time expressions (por la mañana, a las 10, después) and modal verbs (poder, querer) to suggest and decide.\n4. Example phrases:\n - ¿Podemos ir a las diez?\n - Yo quiero visitar la exposición de arte moderno.\n - Después, podemos tomar un café.\n - ¿Qué opináis?\n5. Agree on a plan together. Everyone should share their ideas!\n\n¡Diviértanse planeando su visita!", - "vocab": [ - { - "lemma": "poder", - "pos": "VERB", - }, - { - "lemma": "querer", - "pos": "VERB", - }, - { - "lemma": "visitar", - "pos": "VERB", - }, - { - "lemma": "museo", - "pos": "NOUN", - }, - { - "lemma": "mañana", - "pos": "NOUN", - }, - { - "lemma": "tarde", - "pos": "NOUN", - }, - { - "lemma": "después", - "pos": "ADV", - }, - { - "lemma": "exposición", - "pos": "NOUN", - }, - { - "lemma": "arte", - "pos": "NOUN", - }, - { - "lemma": "café", - "pos": "NOUN", - } - ], - "roles": { - "e1435e91-c85c-4353-aae9-577d6c983b42": { - "name": "Tourist 1", - "goal": - "Suggest a time to visit and what you want to see first.", - "id": "e1435e91-c85c-4353-aae9-577d6c983b42", - }, - "a5585272-523f-4fe7-beef-150372033778": { - "name": "Tourist 2", - "goal": "Recommend a break and suggest a time for it.", - "id": "a5585272-523f-4fe7-beef-150372033778", - }, - "a16ad45f-7a48-46c8-989d-4570271a5e61": { - "name": "Tourist 3", - "goal": - "Propose visiting a special exhibition and suggest when.", - "id": "a16ad45f-7a48-46c8-989d-4570271a5e61", - }, - "af8faa0c-2b6f-4419-9c63-99e9aba69526": { - "name": "Tourist 4", - "goal": - "Help decide the final plan and confirm the group's choices.", - "id": "af8faa0c-2b6f-4419-9c63-99e9aba69526", - }, - }, - "req": { - "topic": "Deciding a Museum Itinerary at Museu Picasso", - "mode": "Decision Making", - "objective": - "I can suggest and decide on a museum visit plan using time expressions and modal verbs.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Museu Picasso", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "04358a9c-4e03-45cd-9eeb-5967b964d6d5", - "title": "Gaudí Debate: Which Building Wins?", - "description": - "Debate your favorite Gaudí building at Casa Batlló and compare opinions!", - "learning_objective": - "I can argue my opinion and compare options using comparative structures and expressions of preference.", - "instructions": - "1. Each of you chooses a famous Gaudí building (for example: Sagrada Familia, Park Güell, Casa Milà, Casa Batlló).\n2. Take turns presenting your building using simple Spanish sentences. Use phrases like:\n - \"Me gusta más [nombre del edificio] porque es más bonito.\"\n - \"Prefiero [nombre del edificio] porque es más grande/pequeño/interesante.\"\n - \"[Nombre del edificio] es mejor que [otro edificio].\"\n3. Listen to your partner and respond with your opinion. Try to compare the buildings using comparatives (más... que, menos... que).\n4. At the end, try to agree on which building is the best and say why, using Spanish phrases.\n\nUseful phrases:\n- \"Me gusta más...\"\n- \"Prefiero... porque...\"\n- \"Es más/menos... que...\"\n- \"Para mí, es mejor porque...\"", - "vocab": [ - { - "lemma": "más", - "pos": "ADV", - }, - { - "lemma": "menos", - "pos": "ADV", - }, - { - "lemma": "mejor", - "pos": "ADJ", - }, - { - "lemma": "peor", - "pos": "ADJ", - }, - { - "lemma": "bonito", - "pos": "ADJ", - }, - { - "lemma": "grande", - "pos": "ADJ", - }, - { - "lemma": "pequeño", - "pos": "ADJ", - }, - { - "lemma": "interesante", - "pos": "ADJ", - }, - { - "lemma": "gustar", - "pos": "VERB", - }, - { - "lemma": "preferir", - "pos": "VERB", - }, - { - "lemma": "porque", - "pos": "SCONJ", - }, - { - "lemma": "edificio", - "pos": "NOUN", - } - ], - "roles": { - "edc611e1-4637-483d-bac3-d52b09678ef0": { - "name": "Debater 1", - "goal": - "Present and defend your favorite Gaudí building, comparing it to others.", - "id": "edc611e1-4637-483d-bac3-d52b09678ef0", - }, - "174cd595-aea3-4345-a21e-e7bb11622eaf": { - "name": "Debater 2", - "goal": - "Present and defend your favorite Gaudí building, comparing it to others.", - "id": "174cd595-aea3-4345-a21e-e7bb11622eaf", - }, - }, - "req": { - "topic": "Debate on Favorite Gaudí Building at Casa Batlló", - "mode": "Debate", - "objective": - "I can argue my opinion and compare options using comparative structures and expressions of preference.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Casa Batlló", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "de60fbda-afed-43e7-87b1-62b51449414a", - "title": "Madrid: La Vida en la Ciudad", - "description": - "Learn to navigate urban life in Spanish as you explore Madrid. Master telling time, asking for directions, and using public transportation vocabulary while virtually visiting iconic locations like Puerta del Sol and Retiro Park.", - "location": "Madrid", - "id": "bba8fe62-6d0d-4e39-b416-c2e908ffc4b3", - "activities": [ - { - "activity_id": "419f13ba-0c6f-4f2e-a46b-4cb0dba79efb", - "title": "Lost in Puerta del Sol", - "description": - "Help a lost tourist find famous spots in Madrid's Puerta del Sol!", - "learning_objective": - "Can ask for and give directions using prepositions of location and imperative forms to help a peer navigate the city.", - "instructions": - "1. **Tourist:** You are lost in Puerta del Sol and want to find the *Museo del Jamón*. Ask for directions in Spanish. Example: _¿Dónde está el Museo del Jamón?_\n2. **Local:** You know the area. Give clear directions in Spanish using prepositions and imperatives. Example: _Sigue recto, gira a la derecha, está al lado de la plaza._\n3. Use phrases like:\n - _Sigue recto_ (Go straight)\n - _Gira a la izquierda/derecha_ (Turn left/right)\n - _Está enfrente de..._ (It's in front of...)\n - _Al lado de..._ (Next to...)\n4. Continue until the Tourist reaches the destination!", - "vocab": [ - { - "lemma": "dónde", - "pos": "ADV", - }, - { - "lemma": "estar", - "pos": "VERB", - }, - { - "lemma": "girar", - "pos": "VERB", - }, - { - "lemma": "seguir", - "pos": "VERB", - }, - { - "lemma": "recto", - "pos": "ADJ", - }, - { - "lemma": "izquierda", - "pos": "NOUN", - }, - { - "lemma": "derecha", - "pos": "NOUN", - }, - { - "lemma": "al lado de", - "pos": "ADP", - }, - { - "lemma": "enfrente de", - "pos": "ADP", - }, - { - "lemma": "plaza", - "pos": "NOUN", - } - ], - "roles": { - "7bf0f263-6bef-4e4d-af9a-46c957fa3c9e": { - "name": "Tourist", - "goal": - "Ask for directions to a famous place in Puerta del Sol.", - "id": "7bf0f263-6bef-4e4d-af9a-46c957fa3c9e", - }, - "6c680534-3a24-4da3-9b7b-a88633618bbb": { - "name": "Local", - "goal": - "Give clear directions using prepositions and imperatives.", - "id": "6c680534-3a24-4da3-9b7b-a88633618bbb", - }, - }, - "req": { - "topic": "Asking for Directions", - "mode": "Roleplay", - "objective": - "Can ask for and give directions using prepositions of location and imperative forms to help a peer navigate the city.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Puerta del Sol", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "04ad049f-3d44-4010-b6eb-bd75ab955f7b", - "title": "Find Your Way in Retiro!", - "description": - "Explore Parque del Retiro together using Spanish transport words!", - "learning_objective": - "Can identify and use key transportation terms (metro, autobús, parada) to navigate to specific checkpoints.", - "instructions": - "**Step 1:** Each of you receives an image of a famous spot in Parque del Retiro (e.g., the lake, Crystal Palace, or a statue).\n\n**Step 2:** In the chat, describe your image using at least two transportation words from the vocab list. For example: \"Para llegar al lago, toma el metro y baja en la parada Retiro.\"\n\n**Step 3:** Work together to guess which checkpoint each person has, using Spanish phrases like:\n- \"¿Es una parada de autobús cerca del palacio?\"\n- \"¿Dónde está el metro más cercano?\"\n\n**Step 4:** Once all checkpoints are guessed, discuss which transport you would use to visit them and why. Use words from the vocab list!", - "vocab": [ - { - "lemma": "metro", - "pos": "NOUN", - }, - { - "lemma": "autobús", - "pos": "NOUN", - }, - { - "lemma": "parada", - "pos": "NOUN", - }, - { - "lemma": "billete", - "pos": "NOUN", - }, - { - "lemma": "estación", - "pos": "NOUN", - }, - { - "lemma": "subir", - "pos": "VERB", - }, - { - "lemma": "bajar", - "pos": "VERB", - }, - { - "lemma": "línea", - "pos": "NOUN", - } - ], - "roles": { - "e1e60f6a-ff4e-4501-9154-c6e7c7317bbb": { - "name": "Tourist", - "goal": - "Describe your checkpoint image using transport words and ask for help navigating.", - "id": "e1e60f6a-ff4e-4501-9154-c6e7c7317bbb", - }, - "3a21e010-b173-40cc-ae54-63e50d748c93": { - "name": "Local", - "goal": - "Guess the checkpoint from descriptions and suggest best transport routes.", - "id": "3a21e010-b173-40cc-ae54-63e50d748c93", - }, - "73b38fa7-3f67-4e0f-a3f2-4b4f129b2f56": { - "name": "Guide", - "goal": - "Help both Tourist and Local use correct transport vocabulary and clarify directions.", - "id": "73b38fa7-3f67-4e0f-a3f2-4b4f129b2f56", - }, - }, - "req": { - "topic": "Public Transportation Vocabulary", - "mode": "Scavenger Hunt", - "objective": - "Can identify and use key transportation terms (metro, autobús, parada) to navigate to specific checkpoints.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Parque del Retiro", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "0b06e5a3-5bdc-47ff-9c67-ea0b274c0e8f", - "title": "Choose the Best Train Ticket at Atocha", - "description": - "Work together at Estación de Atocha to find the best train ticket!", - "learning_objective": - "Can read train and metro schedules, ask about departure times, and choose the best ticket option for a journey.", - "instructions": - "**Instructions:**\n\n1. Listen to the train schedule (provided below).\n2. \n**Tourist**: Record a voice message in Spanish asking about train departure times and ticket options. Use phrases like:\n- \"¿A qué hora sale el tren a Barcelona?\"\n- \"¿Cuánto cuesta el billete?\"\n- \"¿Hay billete de ida y vuelta?\"\n\n**Ticket Seller**: Reply with a voice message in Spanish giving information about departure times and ticket prices. Use phrases like:\n- \"El próximo tren sale a las diez.\"\n- \"El billete cuesta veinte euros.\"\n- \"Tenemos billete sencillo y de ida y vuelta.\"\n\n3. Tourist, record a final voice message saying which ticket you want and why. For example:\n- \"Quiero el billete de ida y vuelta porque es más barato.\"\n\n4. Discuss together (in Spanish) which ticket is best for your journey.", - "vocab": [ - { - "lemma": "tren", - "pos": "NOUN", - }, - { - "lemma": "billete", - "pos": "NOUN", - }, - { - "lemma": "horario", - "pos": "NOUN", - }, - { - "lemma": "salir", - "pos": "VERB", - }, - { - "lemma": "comprar", - "pos": "VERB", - }, - { - "lemma": "cuánto", - "pos": "PRON", - }, - { - "lemma": "ida y vuelta", - "pos": "NOUN", - }, - { - "lemma": "sencillo", - "pos": "ADJECTIVE", - }, - { - "lemma": "euros", - "pos": "NOUN", - }, - { - "lemma": "próximo", - "pos": "ADJECTIVE", - } - ], - "roles": { - "db11f973-935f-4e37-ad40-a6bdc75d3aa6": { - "name": "Tourist", - "goal": - "Ask about train times and ticket options, then choose the best ticket.", - "id": "db11f973-935f-4e37-ad40-a6bdc75d3aa6", - }, - "111451f4-9600-4799-80c9-d2bcb35be926": { - "name": "Ticket Seller", - "goal": - "Answer questions about schedule and tickets, provide prices and options.", - "id": "111451f4-9600-4799-80c9-d2bcb35be926", - }, - }, - "req": { - "topic": "Purchasing Tickets and Reading Schedules", - "mode": "Decision Making", - "objective": - "Can read train and metro schedules, ask about departure times, and choose the best ticket option for a journey.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Estación de Atocha", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "9287acf8-54cb-4893-8d75-e85ddb422cdd", - "title": "Market Hours & Meal Times Chat", - "description": - "Chat in Mercado de San Miguel to ask about opening hours and meal times!", - "learning_objective": - "Can inquire about and state opening hours and meal times using time expressions in a market context.", - "instructions": - "**Instructions:**\n\n1. Each of you has a role. Use the chat to talk as your character in Mercado de San Miguel.\n2. Use Spanish to ask and answer about:\n - When the market opens and closes\n - When breakfast, lunch, and dinner are served\n3. Example questions:\n - ¿A qué hora abre el mercado?\n - ¿A qué hora sirven la comida?\n - ¿Hasta qué hora está abierto?\n4. Example answers:\n - El mercado abre a las diez.\n - Servimos el desayuno a las nueve.\n - Cerramos a las ocho.\n5. Try to use these time expressions: a las..., hasta las..., desde las...\n6. Exchange at least 3 questions and answers each.", - "vocab": [ - { - "lemma": "mercado", - "pos": "NOUN", - }, - { - "lemma": "hora", - "pos": "NOUN", - }, - { - "lemma": "abrir", - "pos": "VERB", - }, - { - "lemma": "cerrar", - "pos": "VERB", - }, - { - "lemma": "servir", - "pos": "VERB", - }, - { - "lemma": "desayuno", - "pos": "NOUN", - }, - { - "lemma": "comida", - "pos": "NOUN", - }, - { - "lemma": "cena", - "pos": "NOUN", - }, - { - "lemma": "a", - "pos": "ADP", - }, - { - "lemma": "hasta", - "pos": "ADP", - }, - { - "lemma": "desde", - "pos": "ADP", - } - ], - "roles": { - "1a9a539d-e4d2-4003-9f32-6804fdb7772d": { - "name": "Tourist", - "goal": - "Ask about the market's opening hours and meal times to plan your visit.", - "id": "1a9a539d-e4d2-4003-9f32-6804fdb7772d", - }, - "277c77b1-c2c3-48e6-9741-06669095509b": { - "name": "Vendor", - "goal": - "Provide information about the market's hours and when meals are served.", - "id": "277c77b1-c2c3-48e6-9741-06669095509b", - }, - }, - "req": { - "topic": "Telling Time for Meals and Visits", - "mode": "Conversation", - "objective": - "Can inquire about and state opening hours and meal times using time expressions in a market context.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Mercado de San Miguel", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "fd8b0c06-ff7b-428b-8d56-a6940864fc87", - "title": "Guess the Tour Time at Plaza Mayor", - "description": - "Play a guessing game to discover when city tour events happen at Plaza Mayor!", - "learning_objective": - "Can formulate and answer yes/no questions to deduce event times and practice telling the time in Spanish.", - "instructions": - "### How to Play\n1. One of you is the Tour Guide and secretly chooses an event time (for example, \"El tour empieza a las dos.\").\n2. The other three are Tourists. Your goal is to guess the event time by asking yes/no questions in Spanish.\n3. Ask questions about the hour, such as:\n - \"¿Es a las tres?\"\n - \"¿Es antes de las cinco?\"\n - \"¿Es después de la una?\"\n4. The Tour Guide answers only with \"sí\" or \"no\".\n5. You have 20 questions in total to guess the exact time!\n6. When you think you know, make your final guess: \"¿Es a las [hora]?\"\n\n**Tip:** Use phrases like \"¿Es antes de...?\" (Is it before...?), \"¿Es después de...?\" (Is it after...?), and \"¿Es a las...?\" (Is it at...?).", - "vocab": [ - { - "lemma": "hora", - "pos": "NOUN", - }, - { - "lemma": "tour", - "pos": "NOUN", - }, - { - "lemma": "Plaza Mayor", - "pos": "PROPN", - }, - { - "lemma": "pregunta", - "pos": "NOUN", - }, - { - "lemma": "sí", - "pos": "ADV", - }, - { - "lemma": "no", - "pos": "ADV", - }, - { - "lemma": "antes", - "pos": "ADV", - }, - { - "lemma": "después", - "pos": "ADV", - }, - { - "lemma": "empezar", - "pos": "VERB", - }, - { - "lemma": "ser", - "pos": "VERB", - } - ], - "roles": { - "5c83f94f-a981-4837-ae25-f20ac411e6e7": { - "name": "Tour Guide", - "goal": - "Secretly select an event time and answer yes/no questions to help others guess it.", - "id": "5c83f94f-a981-4837-ae25-f20ac411e6e7", - }, - "f540b99c-e042-4aa8-82d7-05b3c7683e23": { - "name": "Tourist 1", - "goal": - "Ask yes/no questions in Spanish to guess the event time.", - "id": "f540b99c-e042-4aa8-82d7-05b3c7683e23", - }, - "46e6bbcb-31e9-424e-9cfa-907b14b6f63e": { - "name": "Tourist 2", - "goal": - "Ask yes/no questions in Spanish to guess the event time.", - "id": "46e6bbcb-31e9-424e-9cfa-907b14b6f63e", - }, - "6efc49be-a6f9-442c-beaf-eaa6d3fded05": { - "name": "Tourist 3", - "goal": - "Ask yes/no questions in Spanish to guess the event time.", - "id": "6efc49be-a6f9-442c-beaf-eaa6d3fded05", - }, - }, - "req": { - "topic": "Planning a City Tour Schedule", - "mode": "20-Question Game", - "objective": - "Can formulate and answer yes/no questions to deduce event times and practice telling time in a group.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Plaza Mayor", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - }, - { - "activity_id": "864a6b09-efc8-442a-877b-cad5297f2cd4", - "title": "Fiesta en Sevilla", - "description": - "Conclude your journey in the heart of Andalusia! Learn to talk about celebrations and traditions while exploring Seville's famous Feria de Abril. Practice using the near future tense to make plans for experiencing the city's flamenco and tapas culture.", - "location": "Seville", - "id": "8fb15feb-6bbe-4ed4-95dd-68d80dd7b7b6", - "activities": [ - { - "activity_id": "ccad4542-0d88-43db-8fee-249b92aa463a", - "title": "Plan Your Feria de Abril Day", - "description": - "Team up to decide your perfect Feria de Abril day at the Recinto Ferial!", - "learning_objective": - "I can plan my visit to the Feria de Abril using the near future tense to decide daily activities.", - "instructions": - "1. Imagine you are together at the Feria de Abril in Seville.\n2. Each of you has a different role. Use Spanish to discuss and decide what you will do during the day.\n3. Use the near future tense (ir + a + infinitive) to talk about your plans. For example: \"Voy a bailar sevillanas\", \"Vamos a comer churros\".\n4. Take turns suggesting activities and respond to your partner’s ideas.\n5. Agree on a full day itinerary (at least 3 activities) and write your final plan in Spanish.\n\nUseful phrases:\n- ¿Qué vamos a hacer?\n- Vamos a visitar las casetas.\n- Voy a ver los caballos.\n- ¿Quieres comer pescaito frito?\n- Sí, vamos a comerlo.", - "vocab": [ - { - "lemma": "ir", - "pos": "VERB", - }, - { - "lemma": "bailar", - "pos": "VERB", - }, - { - "lemma": "comer", - "pos": "VERB", - }, - { - "lemma": "caseta", - "pos": "NOUN", - }, - { - "lemma": "caballo", - "pos": "NOUN", - }, - { - "lemma": "churro", - "pos": "NOUN", - }, - { - "lemma": "feria", - "pos": "NOUN", - }, - { - "lemma": "pescaito", - "pos": "NOUN", - }, - { - "lemma": "ver", - "pos": "VERB", - }, - { - "lemma": "beber", - "pos": "VERB", - }, - { - "lemma": "flamenco", - "pos": "NOUN", - } - ], - "roles": { - "36fa322d-0929-433d-963a-9734d7191bb8": { - "name": "Tourist", - "goal": - "Suggest fun activities and food to try at the Feria de Abril.", - "id": "36fa322d-0929-433d-963a-9734d7191bb8", - }, - "13efeebc-e5a3-4348-a951-a818d98ebdd8": { - "name": "Local", - "goal": - "Recommend traditional Feria experiences and help make decisions for the best day.", - "id": "13efeebc-e5a3-4348-a951-a818d98ebdd8", - }, - }, - "req": { - "topic": "Planning your Feria de Abril itinerary", - "mode": "Decision Making", - "objective": - "I can plan my visit to the Feria de Abril using the near future tense to decide daily activities.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Recinto Ferial de Abril", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "e3cbc605-62a8-4e01-93f2-a732ff4abf43", - "title": "Book Flamenco Tickets at Casa de la Memoria", - "description": - "Experience booking tickets for a flamenco show at Casa de la Memoria!", - "learning_objective": - "I can use the near future tense and transactional language to secure tickets for a flamenco performance.", - "instructions": - "**Step 1:**\n- Read your role and goal.\n\n**Step 2:**\n- Send voice messages in Spanish to complete the ticket booking.\n\n**Example phrases:**\n- \"Hola, quiero reservar dos entradas para el flamenco.\"\n- \"¿Cuánto cuestan las entradas?\"\n- \"Voy a pagar con tarjeta.\"\n- \"¿A qué hora empieza el espectáculo?\"\n\n**Tips:**\n- Use the near future tense: \"Voy a...\" (I am going to...)\n- Ask and answer simple questions about the tickets.\n- Be polite and friendly!", - "vocab": [ - { - "lemma": "reservar", - "pos": "VERB", - }, - { - "lemma": "entrada", - "pos": "NOUN", - }, - { - "lemma": "espectáculo", - "pos": "NOUN", - }, - { - "lemma": "pagar", - "pos": "VERB", - }, - { - "lemma": "cuánto", - "pos": "PRON", - }, - { - "lemma": "hora", - "pos": "NOUN", - }, - { - "lemma": "tarjeta", - "pos": "NOUN", - }, - { - "lemma": "flamenco", - "pos": "NOUN", - }, - { - "lemma": "empezar", - "pos": "VERB", - }, - { - "lemma": "querer", - "pos": "VERB", - } - ], - "roles": { - "896c45c5-bc66-4bd6-afa8-29f977bf8313": { - "name": "Tourist", - "goal": - "Book two tickets for tonight's flamenco show, ask for the price, and confirm the time.", - "id": "896c45c5-bc66-4bd6-afa8-29f977bf8313", - }, - "b25ff31c-4ad6-449c-9c46-b01f5269c8b7": { - "name": "Ticket Seller", - "goal": - "Answer questions about the tickets, give the price, and confirm the booking politely.", - "id": "b25ff31c-4ad6-449c-9c46-b01f5269c8b7", - }, - }, - "req": { - "topic": "Booking flamenco show tickets", - "mode": "Roleplay", - "objective": - "I can use the near future tense and transactional language to secure tickets for a flamenco performance.", - "media": "voice_messages", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Casa de la Memoria", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "08bf5b85-e0c7-4f33-b0a3-1421a0ccf192", - "title": "Tapas Tasting Route at Mercado de Triana", - "description": - "Plan a delicious tapas tasting route together in a lively Seville market!", - "learning_objective": - "I can describe and recommend different tapas using the near future tense to plan a tasting route.", - "instructions": - "**Instructions:**\n\n1. Each of you will receive 2-3 images of different tapas (for example: tortilla, croquetas, jamón, aceitunas, gambas).\n2. In Spanish, describe the tapas in your images using simple adjectives (rico, salado, famoso, etc.).\n - Example: *La tortilla es muy rica y famosa.*\n3. Use the near future tense (ir + a + infinitive) to recommend and plan which tapas to try next.\n - Example: *Vamos a probar las croquetas. Son deliciosas.*\n4. Ask and answer questions about preferences.\n - Example: *¿Vas a comer jamón?* / *Sí, voy a comer jamón.*\n5. Together, make a list (in Spanish) of 3 tapas you are going to try on your route.\n\n**Tip:** Use the images to help describe and recommend the tapas!", - "vocab": [ - { - "lemma": "tapa", - "pos": "NOUN", - }, - { - "lemma": "probar", - "pos": "VERB", - }, - { - "lemma": "comer", - "pos": "VERB", - }, - { - "lemma": "rico", - "pos": "ADJ", - }, - { - "lemma": "famoso", - "pos": "ADJ", - }, - { - "lemma": "salado", - "pos": "ADJ", - }, - { - "lemma": "delicioso", - "pos": "ADJ", - }, - { - "lemma": "ir", - "pos": "VERB", - }, - { - "lemma": "aceituna", - "pos": "NOUN", - }, - { - "lemma": "croqueta", - "pos": "NOUN", - }, - { - "lemma": "tortilla", - "pos": "NOUN", - }, - { - "lemma": "jamón", - "pos": "NOUN", - }, - { - "lemma": "gamba", - "pos": "NOUN", - } - ], - "roles": { - "02cef1d3-e3c6-4c6c-8dd5-7d6b4048f898": { - "name": "Tourist", - "goal": - "Describe and recommend tapas you want to try, planning a tasting route.", - "id": "02cef1d3-e3c6-4c6c-8dd5-7d6b4048f898", - }, - "372ca8e9-230f-4434-ab57-76ec2b9473cf": { - "name": "Local", - "goal": - "Recommend typical tapas and help plan a tasting route using the near future tense.", - "id": "372ca8e9-230f-4434-ab57-76ec2b9473cf", - }, - }, - "req": { - "topic": "Tapas hopping conversation", - "mode": "Conversation", - "objective": - "I can describe and recommend different tapas using the near future tense to plan a tasting route.", - "media": "images", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 2, - "location": "Mercado de Triana", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "33e45902-c9b7-4f46-b9dc-99df2a4afb82", - "title": "Feria de Abril Directions Quest", - "description": - "Explore Barrio de Triana in a group chat, asking for and giving directions to find Feria objects!", - "learning_objective": - "I can ask for and give directions using the near future tense while searching for traditional Feria objects.", - "instructions": - "**Step 1:** Each of you has a role (see below). Stay in character!\n\n**Step 2:** The Tourist will ask for directions to find a traditional Feria object in Barrio de Triana, using the near future tense (\"Voy a buscar...\", \"¿Dónde voy a encontrar...?\").\n\n**Step 3:** The Local will answer, giving directions using the near future tense (\"Vas a ir...\", \"Vas a ver...\", \"Vas a doblar...\").\n\n**Step 4:** The Guide will give extra hints or suggestions, also using the near future tense (\"Vas a necesitar buscar cerca de...\", \"Vas a encontrarlo al lado de...\").\n\n**Example Phrases:**\n- \"¿Dónde voy a encontrar un abanico?\"\n- \"Vas a ir a la plaza y vas a ver una caseta.\"\n- \"Vas a necesitar buscar cerca del puente.\"\n\n**Step 5:** Swap objects and repeat for at least two different Feria items (e.g., abanico, farolillo, traje de flamenca).", - "vocab": [ - { - "lemma": "buscar", - "pos": "VERB", - }, - { - "lemma": "ir", - "pos": "VERB", - }, - { - "lemma": "ver", - "pos": "VERB", - }, - { - "lemma": "encontrar", - "pos": "VERB", - }, - { - "lemma": "necesitar", - "pos": "VERB", - }, - { - "lemma": "caseta", - "pos": "NOUN", - }, - { - "lemma": "puente", - "pos": "NOUN", - }, - { - "lemma": "plaza", - "pos": "NOUN", - }, - { - "lemma": "abanico", - "pos": "NOUN", - }, - { - "lemma": "farolillo", - "pos": "NOUN", - }, - { - "lemma": "traje de flamenca", - "pos": "NOUN", - } - ], - "roles": { - "9f86f3ba-f8c9-4052-be0b-21704367af8e": { - "name": "Tourist", - "goal": - "Ask for directions to find Feria objects using the near future tense.", - "id": "9f86f3ba-f8c9-4052-be0b-21704367af8e", - }, - "21430332-e8c6-4d0e-a781-e4e3b55f6100": { - "name": "Local", - "goal": - "Give clear directions to Feria objects in the near future tense.", - "id": "21430332-e8c6-4d0e-a781-e4e3b55f6100", - }, - "322c7129-8eeb-4557-9958-d08e595aec9b": { - "name": "Guide", - "goal": - "Offer extra hints or suggestions using the near future tense.", - "id": "322c7129-8eeb-4557-9958-d08e595aec9b", - }, - }, - "req": { - "topic": "Feria de Abril scavenger hunt", - "mode": "Scavenger Hunt", - "objective": - "I can ask for and give directions using the near future tense while searching for traditional Feria objects.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 3, - "location": "Barrio de Triana", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - }, - { - "activity_id": "603c5942-3712-4b04-a244-db98c327c10e", - "title": "Rent or Buy? Sevillana Dress Debate", - "description": - "Debate with friends on Calle Sierpes about renting or buying a traje de sevillana!", - "learning_objective": - "I can argue pros and cons using the near future tense to explain my intentions about renting or buying a traje de sevillana.", - "instructions": - "You are on Calle Sierpes, Sevilla. You will debate if you should rent or buy a traje de sevillana for the feria. Use the near future tense (ir + a + infinitive) to explain your ideas.\n\n**Step 1:** Each person introduces their opinion: rent or buy? Use phrases like:\n- \"Voy a alquilar un traje porque...\"\n- \"Voy a comprar un traje porque...\"\n\n**Step 2:** Each person gives one reason for their choice. Use simple pros and cons, for example:\n- \"Es más barato.\"\n- \"Voy a usar el traje muchas veces.\"\n\n**Step 3:** Ask someone else in the group a question about their choice:\n- \"¿Por qué vas a alquilar/comprar el traje?\"\n\n**Step 4:** Respond to a question using the near future tense.\n\n**Useful phrases:**\n- \"Voy a...\" (I am going to...)\n- \"Porque...\" (Because...)\n- \"¿Y tú?\" (And you?)\n\nHave fun and listen to everyone's ideas!", - "vocab": [ - { - "lemma": "alquilar", - "pos": "VERB", - }, - { - "lemma": "comprar", - "pos": "VERB", - }, - { - "lemma": "traje", - "pos": "NOUN", - }, - { - "lemma": "sevillana", - "pos": "ADJECTIVE", - }, - { - "lemma": "barato", - "pos": "ADJECTIVE", - }, - { - "lemma": "usar", - "pos": "VERB", - }, - { - "lemma": "feria", - "pos": "NOUN", - }, - { - "lemma": "dinero", - "pos": "NOUN", - }, - { - "lemma": "nuevo", - "pos": "ADJECTIVE", - }, - { - "lemma": "bonito", - "pos": "ADJECTIVE", - } - ], - "roles": { - "2b80a33c-4474-450e-98bd-eefbee485753": { - "name": "Debater for Renting", - "goal": "Argue in favor of renting a traje de sevillana.", - "id": "2b80a33c-4474-450e-98bd-eefbee485753", - }, - "44f179c4-ca4e-4e19-9597-7f7bbd0d1c42": { - "name": "Debater for Buying", - "goal": "Argue in favor of buying a traje de sevillana.", - "id": "44f179c4-ca4e-4e19-9597-7f7bbd0d1c42", - }, - "72c2b90e-de58-45d8-bb02-02ec73c52563": { - "name": "Questioner", - "goal": - "Ask questions to understand the reasons for renting or buying.", - "id": "72c2b90e-de58-45d8-bb02-02ec73c52563", - }, - "9d2583c1-2f26-4895-a444-a8629ac56322": { - "name": "Judge", - "goal": - "Listen to both sides and decide which has stronger reasons.", - "id": "9d2583c1-2f26-4895-a444-a8629ac56322", - }, - }, - "req": { - "topic": "Debate: rent or buy a traje de sevillana?", - "mode": "Debate", - "objective": - "I can argue pros and cons using the near future tense to explain my intentions about renting or buying a traje de sevillana.", - "media": "nan", - "activity_cefr_level": "A1", - "language_of_instructions": "en", - "target_language": "es", - "number_of_participants": 4, - "location": "Calle Sierpes", - "include_image": false, - "save_to_db": false, - "count": 1, - }, - } - ], - } - ], - } - ], -}; diff --git a/lib/pangea/payload_client/join_field.dart b/lib/pangea/payload_client/join_field.dart new file mode 100644 index 000000000..133482281 --- /dev/null +++ b/lib/pangea/payload_client/join_field.dart @@ -0,0 +1,32 @@ +class JoinField { + final List? docs; + final bool? hasNextPage; + final int? totalDocs; + + const JoinField({ + this.docs, + this.hasNextPage, + this.totalDocs, + }); + + factory JoinField.fromJson( + Map json, + ) { + final raw = json['docs']; + final list = (raw is List) ? raw.map((e) => e as String).toList() : null; + + return JoinField( + docs: list, + hasNextPage: json['hasNextPage'] as bool?, + totalDocs: json['totalDocs'] as int?, + ); + } + + Map toJson() { + return { + 'docs': docs, + 'hasNextPage': hasNextPage, + 'totalDocs': totalDocs, + }; + } +} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan.dart new file mode 100644 index 000000000..c82bd414d --- /dev/null +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan.dart @@ -0,0 +1,67 @@ +import 'package:fluffychat/pangea/payload_client/join_field.dart'; +import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; + +/// Represents a course plan from the CMS API +class CmsCoursePlan { + final String id; + final String title; + final String description; + final String cefrLevel; + final String l1; // Language of instruction + final String l2; // Target language + final JoinField? coursePlanMedia; + final JoinField? coursePlanModules; + final PolymorphicRelationship? createdBy; + final PolymorphicRelationship? updatedBy; + final String updatedAt; + final String createdAt; + + CmsCoursePlan({ + required this.id, + required this.title, + required this.description, + required this.cefrLevel, + required this.l1, + required this.l2, + this.coursePlanMedia, + this.coursePlanModules, + this.createdBy, + this.updatedBy, + required this.updatedAt, + required this.createdAt, + }); + + factory CmsCoursePlan.fromJson(Map json) { + return CmsCoursePlan( + id: json['id'], + title: json['title'], + description: json['description'], + cefrLevel: json['cefrLevel'], + l1: json['l1'], + l2: json['l2'], + coursePlanMedia: JoinField.fromJson(json['coursePlanMedia']), + coursePlanModules: JoinField.fromJson(json['coursePlanModules']), + createdBy: PolymorphicRelationship.fromJson(json['createdBy']), + updatedBy: PolymorphicRelationship.fromJson(json['updatedBy']), + updatedAt: json['updatedAt'], + createdAt: json['createdAt'], + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'cefrLevel': cefrLevel, + 'l1': l1, + 'l2': l2, + 'coursePlanMedia': coursePlanMedia?.toJson(), + 'coursePlanModules': coursePlanModules?.toJson(), + 'createdBy': createdBy?.toJson(), + 'updatedBy': updatedBy?.toJson(), + 'updatedAt': updatedAt, + 'createdAt': createdAt, + }; + } +} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart new file mode 100644 index 000000000..a34a2d238 --- /dev/null +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity.dart @@ -0,0 +1,164 @@ +import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; +import 'package:fluffychat/pangea/payload_client/join_field.dart'; +import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; + +/// Represents a course plan activity role +class CmsCoursePlanActivityRole { + final String id; + final String name; + final String goal; + final String? avatarUrl; + + CmsCoursePlanActivityRole({ + required this.id, + required this.name, + required this.goal, + this.avatarUrl, + }); + + factory CmsCoursePlanActivityRole.fromJson(Map json) { + return CmsCoursePlanActivityRole( + id: json['id'] as String, + name: json['name'] as String, + goal: json['goal'] as String, + avatarUrl: json['avatarUrl'] as String?, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'goal': goal, + 'avatarUrl': avatarUrl, + }; + } +} + +/// Represents vocabulary in a course plan activity +class CmsCoursePlanVocab { + final String lemma; + final String pos; + final String? id; + + CmsCoursePlanVocab({ + required this.lemma, + required this.pos, + this.id, + }); + + factory CmsCoursePlanVocab.fromJson(Map json) { + return CmsCoursePlanVocab( + lemma: json['lemma'] as String, + pos: json['pos'] as String, + id: json['id'] as String?, + ); + } + + Map toJson() { + return { + 'lemma': lemma, + 'pos': pos, + 'id': id, + }; + } +} + +/// Represents a course plan activity from the CMS API +class CmsCoursePlanActivity { + final String id; + final String title; + final String description; + final String learningObjective; + final String instructions; + final String l1; // Language of instruction + final String l2; // Target language + final LanguageLevelTypeEnum cefrLevel; + final List roles; + final List vocabs; + final JoinField? coursePlanActivityMedia; + final List coursePlanModules; + final PolymorphicRelationship? createdBy; + final PolymorphicRelationship? updatedBy; + final String updatedAt; + final String createdAt; + + CmsCoursePlanActivity({ + required this.id, + required this.title, + required this.description, + required this.learningObjective, + required this.instructions, + required this.l1, + required this.l2, + required this.cefrLevel, + required this.roles, + required this.vocabs, + required this.coursePlanActivityMedia, + required this.coursePlanModules, + this.createdBy, + this.updatedBy, + required this.updatedAt, + required this.createdAt, + }); + + factory CmsCoursePlanActivity.fromJson(Map json) { + return CmsCoursePlanActivity( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String, + learningObjective: json['learningObjective'] as String, + instructions: json['instructions'] as String, + l1: json['l1'] as String, + l2: json['l2'] as String, + cefrLevel: LanguageLevelTypeEnumExtension.fromString( + json['cefrLevel'] as String, + ), + roles: (json['roles'] as List) + .map( + (role) => CmsCoursePlanActivityRole.fromJson( + role as Map, + ), + ) + .toList(), + vocabs: (json['vocabs'] as List) + .map( + (vocab) => + CmsCoursePlanVocab.fromJson(vocab as Map), + ) + .toList(), + coursePlanActivityMedia: + JoinField.fromJson(json['coursePlanActivityMedia']), + coursePlanModules: List.from(json['coursePlanModules']), + createdBy: json['createdBy'] != null + ? PolymorphicRelationship.fromJson(json['createdBy']) + : null, + updatedBy: json['updatedBy'] != null + ? PolymorphicRelationship.fromJson(json['updatedBy']) + : null, + updatedAt: json['updatedAt'] as String, + createdAt: json['createdAt'] as String, + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'learningObjective': learningObjective, + 'instructions': instructions, + 'l1': l1, + 'l2': l2, + 'cefrLevel': cefrLevel.string, + 'roles': roles.map((role) => role.toJson()).toList(), + 'vocabs': vocabs.map((vocab) => vocab.toJson()).toList(), + 'coursePlanActivityMedia': coursePlanActivityMedia?.toJson(), + 'coursePlanModules': coursePlanModules, + 'createdBy': createdBy?.toJson(), + 'updatedBy': updatedBy?.toJson(), + 'updatedAt': updatedAt, + 'createdAt': createdAt, + }; + } +} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart new file mode 100644 index 000000000..c54c4cd85 --- /dev/null +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan_activity_media.dart @@ -0,0 +1,90 @@ +import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; + +/// Represents course plan activity media from the CMS API +class CmsCoursePlanActivityMedia { + final String id; + final String? alt; + final List coursePlanActivities; + final PolymorphicRelationship? createdBy; + final PolymorphicRelationship? updatedBy; + final String? prefix; + final String updatedAt; + final String createdAt; + final String? url; + final String? thumbnailURL; + final String? filename; + final String? mimeType; + final int? filesize; + final int? width; + final int? height; + final double? focalX; + final double? focalY; + + CmsCoursePlanActivityMedia({ + required this.id, + this.alt, + required this.coursePlanActivities, + this.createdBy, + this.updatedBy, + this.prefix, + required this.updatedAt, + required this.createdAt, + this.url, + this.thumbnailURL, + this.filename, + this.mimeType, + this.filesize, + this.width, + this.height, + this.focalX, + this.focalY, + }); + + factory CmsCoursePlanActivityMedia.fromJson(Map json) { + return CmsCoursePlanActivityMedia( + id: json['id'] as String, + alt: json['alt'] as String?, + coursePlanActivities: List.from(json['coursePlanActivities']), + createdBy: json['createdBy'] != null + ? PolymorphicRelationship.fromJson(json['createdBy']) + : null, + updatedBy: json['updatedBy'] != null + ? PolymorphicRelationship.fromJson(json['updatedBy']) + : null, + prefix: json['prefix'] as String?, + updatedAt: json['updatedAt'] as String, + createdAt: json['createdAt'] as String, + url: json['url'] as String?, + thumbnailURL: json['thumbnailURL'] as String?, + filename: json['filename'] as String?, + mimeType: json['mimeType'] as String?, + filesize: json['filesize'] as int?, + width: json['width'] as int?, + height: json['height'] as int?, + focalX: (json['focalX'] as num?)?.toDouble(), + focalY: (json['focalY'] as num?)?.toDouble(), + ); + } + + Map toJson() { + return { + 'id': id, + 'alt': alt, + 'coursePlanActivities': coursePlanActivities, + 'createdBy': createdBy?.toJson(), + 'updatedBy': updatedBy?.toJson(), + 'prefix': prefix, + 'updatedAt': updatedAt, + 'createdAt': createdAt, + 'url': url, + 'thumbnailURL': thumbnailURL, + 'filename': filename, + 'mimeType': mimeType, + 'filesize': filesize, + 'width': width, + 'height': height, + 'focalX': focalX, + 'focalY': focalY, + }; + } +} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_media.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_media.dart new file mode 100644 index 000000000..6765739d0 --- /dev/null +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan_media.dart @@ -0,0 +1,90 @@ +import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; + +/// Represents course plan media from the CMS API +class CmsCoursePlanMedia { + final String id; + final String? alt; + final List coursePlans; + final PolymorphicRelationship? createdBy; + final PolymorphicRelationship? updatedBy; + final String? prefix; + final String updatedAt; + final String createdAt; + final String? url; + final String? thumbnailURL; + final String? filename; + final String? mimeType; + final int? filesize; + final int? width; + final int? height; + final double? focalX; + final double? focalY; + + CmsCoursePlanMedia({ + required this.id, + this.alt, + required this.coursePlans, + this.createdBy, + this.updatedBy, + this.prefix, + required this.updatedAt, + required this.createdAt, + this.url, + this.thumbnailURL, + this.filename, + this.mimeType, + this.filesize, + this.width, + this.height, + this.focalX, + this.focalY, + }); + + factory CmsCoursePlanMedia.fromJson(Map json) { + return CmsCoursePlanMedia( + id: json['id'], + alt: json['alt'], + coursePlans: List.from(json['coursePlans'] as List), + createdBy: json['createdBy'] != null + ? PolymorphicRelationship.fromJson(json['createdBy']) + : null, + updatedBy: json['updatedBy'] != null + ? PolymorphicRelationship.fromJson(json['updatedBy']) + : null, + prefix: json['prefix'], + updatedAt: json['updatedAt'], + createdAt: json['createdAt'], + url: json['url'], + thumbnailURL: json['thumbnailURL'], + filename: json['filename'], + mimeType: json['mimeType'], + filesize: json['filesize'], + width: json['width'], + height: json['height'], + focalX: json['focalX']?.toDouble(), + focalY: json['focalY']?.toDouble(), + ); + } + + Map toJson() { + return { + 'id': id, + 'alt': alt, + 'coursePlans': coursePlans, + 'createdBy': createdBy?.toJson(), + 'updatedBy': updatedBy?.toJson(), + 'prefix': prefix, + 'updatedAt': updatedAt, + 'createdAt': createdAt, + 'url': url, + 'thumbnailURL': thumbnailURL, + 'filename': filename, + 'mimeType': mimeType, + 'filesize': filesize, + 'width': width, + 'height': height, + 'focalX': focalX, + 'focalY': focalY, + }; + } +} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_module.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_module.dart new file mode 100644 index 000000000..f79542781 --- /dev/null +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan_module.dart @@ -0,0 +1,67 @@ +import 'package:fluffychat/pangea/payload_client/join_field.dart'; +import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; + +/// Represents a course plan module from the CMS API +class CmsCoursePlanModule { + final String id; + final String title; + final String description; + final JoinField? coursePlanActivities; + final JoinField? coursePlanModuleLocations; + final List coursePlans; + final PolymorphicRelationship? createdBy; + final PolymorphicRelationship? updatedBy; + final String updatedAt; + final String createdAt; + + CmsCoursePlanModule({ + required this.id, + required this.title, + required this.description, + required this.coursePlanActivities, + required this.coursePlanModuleLocations, + required this.coursePlans, + this.createdBy, + this.updatedBy, + required this.updatedAt, + required this.createdAt, + }); + + factory CmsCoursePlanModule.fromJson(Map json) { + return CmsCoursePlanModule( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String, + coursePlanActivities: JoinField.fromJson( + json['coursePlanActivities'], + ), + coursePlanModuleLocations: JoinField.fromJson( + json['coursePlanModuleLocations'], + ), + coursePlans: List.from(json['coursePlans']), + createdBy: json['createdBy'] != null + ? PolymorphicRelationship.fromJson(json['createdBy']) + : null, + updatedBy: json['updatedBy'] != null + ? PolymorphicRelationship.fromJson(json['updatedBy']) + : null, + updatedAt: json['updatedAt'] as String, + createdAt: json['createdAt'] as String, + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'coursePlanActivities': coursePlanActivities?.toJson(), + 'coursePlanModuleLocations': coursePlanModuleLocations?.toJson(), + 'coursePlans': coursePlans, + 'createdBy': createdBy?.toJson(), + 'updatedBy': updatedBy?.toJson(), + 'updatedAt': updatedAt, + 'createdAt': createdAt, + }; + } +} diff --git a/lib/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart b/lib/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart new file mode 100644 index 000000000..77ca79c82 --- /dev/null +++ b/lib/pangea/payload_client/models/course_plan/cms_course_plan_module_location.dart @@ -0,0 +1,57 @@ +import 'package:fluffychat/pangea/payload_client/polymorphic_relationship.dart'; + +/// Represents a course plan module location from the CMS API +class CmsCoursePlanModuleLocation { + final String id; + final String name; + // [longitude, latitude] - minItems: 2, maxItems: 2 + final List? coordinates; + final List coursePlanModules; + final PolymorphicRelationship? createdBy; + final PolymorphicRelationship? updatedBy; + final String updatedAt; + final String createdAt; + + CmsCoursePlanModuleLocation({ + required this.id, + required this.name, + this.coordinates, + required this.coursePlanModules, + this.createdBy, + this.updatedBy, + required this.updatedAt, + required this.createdAt, + }); + + factory CmsCoursePlanModuleLocation.fromJson(Map json) { + return CmsCoursePlanModuleLocation( + id: json['id'] as String, + name: json['name'] as String, + coordinates: (json['coordinates'] as List?) + ?.map((coord) => (coord as num).toDouble()) + .toList(), + coursePlanModules: List.from(json['coursePlanModules']), + createdBy: json['createdBy'] != null + ? PolymorphicRelationship.fromJson(json['createdBy']) + : null, + updatedBy: json['updatedBy'] != null + ? PolymorphicRelationship.fromJson(json['updatedBy']) + : null, + updatedAt: json['updatedAt'] as String, + createdAt: json['createdAt'] as String, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'coordinates': coordinates, + 'coursePlanModules': coursePlanModules, + 'createdBy': createdBy?.toJson(), + 'updatedBy': updatedBy?.toJson(), + 'updatedAt': updatedAt, + 'createdAt': createdAt, + }; + } +} diff --git a/lib/pangea/payload_client/payload_client.dart b/lib/pangea/payload_client/payload_client.dart new file mode 100644 index 000000000..476ba7156 --- /dev/null +++ b/lib/pangea/payload_client/payload_client.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +/// Response model for paginated results from PayloadCMS +class PayloadPaginatedResponse { + final List docs; + final int totalDocs; + final int limit; + final int page; + final int totalPages; + final bool hasNextPage; + final bool hasPrevPage; + final int? nextPage; + final int? prevPage; + + PayloadPaginatedResponse({ + required this.docs, + required this.totalDocs, + required this.limit, + required this.page, + required this.totalPages, + required this.hasNextPage, + required this.hasPrevPage, + this.nextPage, + this.prevPage, + }); + + factory PayloadPaginatedResponse.fromJson( + Map json, + T Function(Map) fromJsonT, + ) { + return PayloadPaginatedResponse( + docs: (json['docs'] as List) + .map((e) => fromJsonT(e as Map)) + .toList(), + totalDocs: json['totalDocs'] as int, + limit: json['limit'] as int, + page: json['page'] as int, + totalPages: json['totalPages'] as int, + hasNextPage: json['hasNextPage'] as bool, + hasPrevPage: json['hasPrevPage'] as bool, + nextPage: json['nextPage'] as int?, + prevPage: json['prevPage'] as int?, + ); + } +} + +/// Generic PayloadCMS client for CRUD operations +class PayloadClient { + final String baseUrl; + final String accessToken; + final String basePath = "/cms/api"; + + PayloadClient({ + required this.baseUrl, + required this.accessToken, + }); + + Map get _headers { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + headers['Authorization'] = 'Bearer $accessToken'; + return headers; + } + + /// Generic GET request + Future get(String endpoint) async { + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.get(url, headers: _headers); + return response; + } + + /// Generic POST request + Future post(String endpoint, Map body) async { + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.post( + url, + headers: _headers, + body: jsonEncode(body), + ); + return response; + } + + /// Generic PATCH request + Future patch( + String endpoint, + Map body, + ) async { + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.patch( + url, + headers: _headers, + body: jsonEncode(body), + ); + return response; + } + + /// Generic DELETE request + Future delete(String endpoint) async { + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.delete(url, headers: _headers); + return response; + } + + /// Find documents with pagination + Future> find( + String collection, + T Function(Map) fromJson, { + int? page, + int? limit, + Map? where, + String? sort, + }) async { + final queryParams = {}; + + if (page != null) queryParams['page'] = page.toString(); + if (limit != null) queryParams['limit'] = limit.toString(); + if (where != null) queryParams['where'] = jsonEncode(where); + if (sort != null) queryParams['sort'] = sort; + + final endpoint = + '$basePath/$collection${queryParams.isNotEmpty ? '?${Uri(queryParameters: queryParams).query}' : ''}'; + + final response = await get(endpoint); + final json = jsonDecode(response.body) as Map; + + return PayloadPaginatedResponse.fromJson(json, fromJson); + } + + /// Find a single document by ID + Future findById( + String collection, + String id, + T Function(Map) fromJson, + ) async { + final endpoint = '$basePath/$collection/$id'; + final response = await get(endpoint); + final json = jsonDecode(response.body) as Map; + return fromJson(json); + } + + /// Create a new document + Future createDocument( + String collection, + Map data, + T Function(Map) fromJson, + ) async { + final endpoint = '$basePath/$collection'; + final response = await post(endpoint, data); + final json = jsonDecode(response.body) as Map; + return fromJson(json); + } + + /// Update an existing document + Future updateDocument( + String collection, + String id, + Map data, + T Function(Map) fromJson, + ) async { + final endpoint = '$basePath/$collection/$id'; + final response = await patch(endpoint, data); + final json = jsonDecode(response.body) as Map; + return fromJson(json); + } + + /// Delete a document + Future deleteDocument( + String collection, + String id, + T Function(Map) fromJson, + ) async { + final endpoint = '$basePath/$collection/$id'; + final response = await delete(endpoint); + final json = jsonDecode(response.body) as Map; + return fromJson(json); + } +} diff --git a/lib/pangea/payload_client/polymorphic_relationship.dart b/lib/pangea/payload_client/polymorphic_relationship.dart new file mode 100644 index 000000000..5f786ec0f --- /dev/null +++ b/lib/pangea/payload_client/polymorphic_relationship.dart @@ -0,0 +1,23 @@ +class PolymorphicRelationship { + final String relationTo; + final String value; + + PolymorphicRelationship({ + required this.relationTo, + required this.value, + }); + + factory PolymorphicRelationship.fromJson(Map json) { + return PolymorphicRelationship( + relationTo: json['relationTo'] as String, + value: json['value'] as String, + ); + } + + Map toJson() { + return { + 'relationTo': relationTo, + 'value': value, + }; + } +}