fluffychat/.github/instructions/course-plans.instructions.md
wcjord 859cb78339
feat: fetch languages directly from CMS (#5764)
* feat: fetch languages directly from CMS

- Switch language_repo.dart to fetch from CMS REST API (public, no auth)
- Parse CMS paginated response format (docs[] envelope)
- Rename getLanguages URL to cmsLanguages in urls.dart
- Add 15 unit tests for CMS response parsing
- Add design docs: course-plans, layout instructions

* formatting

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
2026-02-23 10:21:10 -05:00

7.8 KiB

applyTo
lib/pangea/course_plans/**,lib/pangea/course_creation/**,lib/pangea/course_chats/**

Course Plans — Client Design

Client-side loading, caching, and display of localized course content (plans, topics, activities, locations, media).


1. Data Flow Overview

CoursePlanModel (fetched first)
  ├── topicIds: List<String>  ─── fetchTopics() ───► CourseTopicModel[]
  │     ├── activityIds       ─── fetchActivities() ► ActivityPlanModel[]
  │     └── locationIds       ─── fetchLocations() ─► CourseLocationModel[]
  └── mediaIds: List<String>  ─── fetchMediaUrls() ► CourseMediaResponse

Each level returns IDs only; the next level is fetched in a separate HTTP call. This is intentional — each level has its own cache and can be loaded independently.


2. Loading Pattern — CoursePlanProvider Mixin

CoursePlanProvider is a mixin used by course pages (CourseChatsPage, SelectedCoursePage, PublicCoursePreview). It orchestrates the multi-step loading:

Method What it fetches When called
loadCourse(courseId) Course plan + media URLs On page init
loadTopics() ALL topics for the course + location media Immediately after loadCourse completes
loadActivity(topicId) Activities for ONE topic On-demand (user taps a topic)
loadAllActivities() Activities for ALL topics in parallel Called from chat_details.dart on the course settings tab — this is the performance bottleneck

Current loading sequences

CourseChatsPage (main course view):

1. loadCourse(id) → POST /choreo/course_plans/localize
2. loadTopics()   → POST /choreo/topics/localize (ALL topic IDs)
3. _loadTopicsMedia() → location media for each topic

Activities are NOT loaded here. They display only in the course settings tab.

ChatDetailsController._loadCourseInfo() (course settings/details tab):

1. loadCourse(id)
2. loadTopics()
3. loadAllActivities() ← loads ALL activities for ALL topics at once

This is the call path that creates CMS pressure. See course-localization.instructions.md §5 for the full analysis.


3. Repos & Caching

Each content type has its own repo with GetStorage-backed caching (1-day TTL):

Repo Storage Key Cache Check
CoursePlansRepo "course_storage""${uuid}_${l1}" Single course only (TODO: batch)
CourseTopicRepo "course_topic_storage""${uuid}_${l1}" Checks cache first, fetches only missing topics
CourseActivityRepo "course_activity_storage""${id}_${l1}" Checks cache first, fetches only missing activities
CourseLocationRepo "course_location_storage" Locations for topics
CourseLocationMediaRepo "course_location_media_storage" Media URLs for locations
CourseMediaRepo "course_media_storage" Media URLs for course-level images

Deduplication

All repos use a Completer-based in-flight cache (Map<String, Completer<T>>) keyed by batch ID to prevent duplicate concurrent requests for the same data.

Cache invalidation

CoursePlansRepo.clearCache() clears ALL six storage boxes. This is called when the global 1-day TTL expires (checked on next get()).


4. Models

CoursePlanModel

Returned by CoursePlansRepo.get(). Contains:

  • Course metadata (title, description, targetLanguage, cefrLevel, etc.)
  • topicIds: List<String> — IDs only, not full topic objects
  • mediaIds: List<String> — IDs only
  • loadedTopics — reads from CourseTopicRepo cache synchronously
  • fetchTopics() — async, calls choreo API for missing topics

CourseTopicModel

Returned by CourseTopicRepo.get(). Contains:

  • Topic metadata (title, description, uuid)
  • activityIds: List<String> — IDs only
  • locationIds: List<String> — IDs only
  • loadedActivities — reads from CourseActivityRepo cache synchronously
  • fetchActivities() — async, calls choreo API for missing activities

ActivityPlanModel

Returned by CourseActivityRepo.get(). Full activity plan with all fields (title, description, learning_objective, instructions, topic, objective, roles, mode, etc.).


5. Key API Endpoints

Client Call Choreo Endpoint What's Sent
CoursePlansRepo.get() POST /choreo/course_plans/localize 1 course ID + L1
CoursePlanModel.fetchTopics() POST /choreo/topics/localize ALL topic IDs for the course + L1
CourseTopicModel.fetchActivities() POST /choreo/activity_plan/localize ALL activity IDs for one topic + L1

6. Pages That Load Courses

Page Loading Behavior
CourseChatsPage loadCourse() then loadTopics() — shows course with topic list. No activities loaded.
ChatDetailsController loadCourse() then loadTopics() then loadAllActivities() — the only call site that eagerly loads all activities
SelectedCoursePage loadCourse() then loadTopics() — course detail/preview
PublicCoursePreview loadCourse() then loadTopics() — browsing public courses

7. Topic Unlock Logic — ActivitySummariesProvider

ActivitySummariesProvider is a mixin that determines which topic is "active" for a user. Topics are sequential — a user must complete activities in topic N before topic N+1 unlocks.

currentTopicId() logic

Iterates topics in order. For each topic:

  1. Checks activityListComplete — requires ALL activities for the topic to be loaded
  2. Calls _hasCompletedTopic() — checks if the user has archived enough activity sessions
  3. Returns the first incomplete topic

_hasCompletedTopic() dependency on activity data

The unlock heuristic counts "two-person activities" via topic.loadedActivities.values.where((a) => a.req.numberOfParticipants <= 2).length. This is the only reason all activities need to be loaded for unlock computation. If numberOfParticipants were available without loading full activity objects, loadAllActivities() could be eliminated from the page load path.


8. UI Display Pattern — CourseSettings

CourseSettings renders the course details tab:

  • All topics are displayed as a vertical list — title, location pin, participant avatars
  • Locked topics are dimmed with a lock icon — activities are NOT shown for locked topics
  • Unlocked topics show activities in a horizontal scrollable TopicActivitiesList per topic
  • Activities are rendered as ActivitySuggestionCard widgets

This means even for unlocked topics, only a subset of activity cards are visible at any given time (horizontal scroll). Loading all activities across all topics upfront is unnecessary for display purposes.


Future Work

(No linked issues yet.)