docs: add instruction docs for analytics, practice, activities, toolbar; adjust content-word scoring ratio (10:7)
- Add analytics-system.instructions.md - Add practice-exercises.instructions.md (with three-tier design direction) - Add conversation-activities.instructions.md - Add toolbar-reading-assistance.instructions.md - Update copilot-instructions.md header - Change content-word multiplier from 9 to 7 in practice_selection_repo.dart
This commit is contained in:
parent
75a237b90c
commit
b6f368fa1f
6 changed files with 678 additions and 83 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -1,4 +1,4 @@
|
|||
You own the docs. Three sources of truth must agree: **docs**, **code**, and **prior user guidance**. When they don't, resolve it. Update `.github/instructions/` docs when your changes shift conventions. Fix obvious factual errors (paths, class names) without asking. Flag ambiguity when sources contradict.
|
||||
Check the relevant `.github/instructions/` doc before and after coding. If it doesn't exist, create it with the user first.
|
||||
|
||||
# client - Flutter/Dart Language Learning Chat App
|
||||
|
||||
|
|
|
|||
127
.github/instructions/analytics-system.instructions.md
vendored
Normal file
127
.github/instructions/analytics-system.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
applyTo: "lib/pangea/analytics_data/**,lib/pangea/analytics_misc/**,lib/pangea/analytics_page/**,lib/pangea/analytics_summary/**,lib/pangea/analytics_practice/**,lib/pangea/analytics_settings/**,lib/pangea/analytics_downloads/**,lib/pangea/analytics_details_popup/**,lib/pangea/space_analytics/**,lib/pangea/constructs/**"
|
||||
---
|
||||
|
||||
# Analytics System
|
||||
|
||||
The analytics system tracks learning, rewards progress, visualizes growth, and guides the delivery of content like practice exercises, activity suggestions, and distractor generation. Every word a user encounters — through chatting, reading, or practicing — grows from a seed into a flower.
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Instant feedback**: Users should see XP and growth animations the moment they interact with a word, not after a server round-trip. The system is local-first.
|
||||
2. **Every interaction counts**: Every interaction, from reading a message to tapping a new word to practice exercises, contributes to the user's progress.
|
||||
3. **Engaging visuals**: The seeds→greens→flowers metaphor and per-word emoji associations make progress tangible and fun to track.
|
||||
4. **Fun, personalized practice**: Practice activities are generated from the user's actual messages, making them relevant and engaging. The system prioritizes words that need attention, not just random drills.
|
||||
5. **Teacher insights**: Teachers can view aggregate analytics for their students, helping them tailor instruction and identify who needs extra support.
|
||||
|
||||
## Constructs
|
||||
The core unit of analytics is a **construct** — either a vocabulary word, chunk, grammar pattern, or even higher-level concepts (see [`ConstructIdentifier`](lib/pangea/constructs/construct_identifier.dart)). It's basically anything you can track and is interesting for learning. Each construct has a unique identifier, a type (vocab vs morph), and tracks how many times the user has encountered it, practiced it, and mastered it.
|
||||
|
||||
### Two Kinds of Constructs
|
||||
|
||||
Defined by [`ConstructTypeEnum`](lib/pangea/analytics_misc/construct_type_enum.dart):
|
||||
|
||||
| Type | UI Label | What It Tracks | Example |
|
||||
|------|----------|----------------|--------|
|
||||
| Vocab | "Vocabulary" | Individual words identified by lemma + part of speech | "run" (verb), "bank" (noun) |
|
||||
| Morph | "Grammar" | Morphological features of words (categories from [`MorphFeaturesEnum`](lib/pangea/morphs/morph_features_enum.dart)) | Tense=Past, Number=Plural |
|
||||
|
||||
The user sees these as two tabs in their analytics view. Grammar constructs "unlock" when they reach the Green stage (50 XP), giving users a sense of discovery.
|
||||
|
||||
### What Earns XP
|
||||
|
||||
Different interactions contribute different amounts of XP, reflecting effort. Each interaction type is a value in [`ConstructUseTypeEnum`](lib/pangea/analytics_misc/construct_use_type_enum.dart), which determines how much XP it awards:
|
||||
|
||||
- **Clicking a new word** in the toolbar (first view) — small XP (passive learning)
|
||||
- **Correct practice answers** (emoji matching, meaning selection, listening) — moderate XP
|
||||
- **Wrong practice answers** — reduced or zero XP (no punishment, but less reward)
|
||||
- **Using a word in writing** (via the choreographer) — XP based on the construct use type
|
||||
|
||||
Each data point is stored as a [`OneConstructUse`](lib/pangea/analytics_misc/constructs_model.dart) which includes construct identifier, use type, timestamp, and messageId.
|
||||
|
||||
### Construct Deduplication
|
||||
|
||||
The same word can appear with different casing or slight variations across messages (e.g., "Hello" vs "hello"). [`ConstructMergeTable`](lib/pangea/analytics_data/construct_merge_table.dart) merges these transparently so the user sees one unified progress bar per word, not confusing duplicates.
|
||||
|
||||
### Blocking Constructs
|
||||
|
||||
Users can hide specific constructs they consider too easy or irrelevant (e.g., cognates, proper nouns). Blocked constructs:
|
||||
- Disappear from all analytics views
|
||||
- Stop contributing to XP totals
|
||||
- Are excluded from practice activity selection
|
||||
- Persist across sessions via the Matrix analytics room
|
||||
|
||||
## User Levels
|
||||
|
||||
Total XP across all constructs determines the user's global level, computed in [`DerivedAnalyticsDataModel`](lib/pangea/analytics_data/derived_analytics_data_model.dart). The progression is quadratic — early levels come quickly to create momentum, while later levels require sustained effort:
|
||||
|
||||
$$\text{level} = \lfloor 1 + \sqrt{\frac{1 + 8 \cdot \text{totalXP} / 300}{2}} \rfloor$$
|
||||
|
||||
Level-ups are **celebration moments**: the app shows a banner, plays a chime, and [`LevelUpAnalyticsService`](lib/pangea/analytics_data/level_up_analytics_service.dart) generates an AI summary of what the user learned since their last level-up (pulling from actual messages they sent and received).
|
||||
|
||||
> This formula is still being balanced to find the optimal sequence of effort and reward.
|
||||
|
||||
### Level Protection
|
||||
|
||||
Users should never see their level go down from routine actions. If blocking a construct or switching languages would reduce total XP below the current level threshold, the system applies an XP offset to maintain the level. This is a deliberate UX choice — level-downs feel bad and discourage experimentation.
|
||||
|
||||
## Data Architecture Principles
|
||||
|
||||
### Local-First, Sync-Later
|
||||
|
||||
All analytics computation happens against [`AnalyticsDatabase`](lib/pangea/analytics_data/analytics_database.dart) (SQLite on native, IndexedDB on web). The app never queries the server for analytics on a per-message basis. [`AnalyticsUpdateService`](lib/pangea/analytics_data/analytics_update_service.dart) syncs data to a dedicated Matrix room in the background — batched every 10 messages or 10 minutes, whichever comes first. [`AnalyticsDataService`](lib/pangea/analytics_data/analytics_data_service.dart) is the central orchestrator that wires everything together.
|
||||
|
||||
### Per-Language Isolation
|
||||
|
||||
Each target language has its own analytics room and its own local database partition. Switching languages reinitializes the analytics context cleanly. There is no cross-language XP blending.
|
||||
|
||||
### Multi-Device Sync
|
||||
|
||||
Because analytics are stored in Matrix rooms, they sync across devices automatically via the Matrix sync protocol. On login or language change, [`AnalyticsSyncController`](lib/pangea/analytics_data/analytics_sync_controller.dart) performs a bulk catch-up from the analytics room before starting real-time tracking.
|
||||
|
||||
## Celebration Moments
|
||||
|
||||
[`AnalyticsUpdateDispatcher`](lib/pangea/analytics_data/analytics_update_dispatcher.dart) emits [typed events](lib/pangea/analytics_data/analytics_update_events.dart) that the UI listens for to trigger celebratory animations:
|
||||
|
||||
| Event | UX Response |
|
||||
|-------|-------------|
|
||||
| [`XPGainedEvent`](lib/pangea/analytics_data/analytics_update_events.dart) | Floating "+N" animation anchored to the word the user interacted with |
|
||||
| [`ConstructLevelUpEvent`](lib/pangea/analytics_data/analytics_update_events.dart) | Growth animation on the word's token in the toolbar |
|
||||
| [`LevelUpEvent`](lib/pangea/analytics_data/analytics_update_events.dart) | Full-screen banner + chime + AI-generated learning summary |
|
||||
| [`MorphUnlockedEvent`](lib/pangea/analytics_data/analytics_update_events.dart) | Notification that a new grammar pattern has been discovered |
|
||||
| [`NewConstructsEvent`](lib/pangea/analytics_data/analytics_update_events.dart) | Subtle highlight (first-ever interaction with a word) |
|
||||
|
||||
These events are always anchored to a specific UI element (via a `targetID`) so the animation appears in context, not as a disconnected popup. Use the [`AnalyticsUpdater`](lib/pangea/analytics_data/analytics_updater_mixin.dart) mixin on any widget that triggers analytics and wants to show immediate XP/growth feedback.
|
||||
|
||||
## Analytics for Teachers (Space Analytics)
|
||||
|
||||
Teachers/space admins can view aggregate analytics for their students:
|
||||
- Download summaries per student or per space
|
||||
- See inactive student indicators
|
||||
- Request detailed analytics reports
|
||||
|
||||
This data flows from each student's analytics room to the teacher view — the teacher never sees raw construct data, only aggregated summaries.
|
||||
|
||||
## Key Contracts
|
||||
|
||||
- **Never fetch analytics from Synapse per-message.** The local database is the runtime source of truth.
|
||||
- **XP per construct caps at the flower threshold (100).** [`ConstructUses.cappedUses`](lib/pangea/analytics_misc/construct_use_model.dart) enforces this, preventing level inflation from repeatedly encountering familiar words.
|
||||
- **Level can never visibly decrease** from user-initiated actions (blocking, language switching). Use offsets to maintain.
|
||||
- **The "other" category is always filtered out** of aggregations and displays. It represents unclassifiable tokens.
|
||||
- **Analytics initialization must complete before any UI reads.** All public methods await an init completer.
|
||||
- **Construct uses store `eventId` and `roomId` when they originate from a message context.** Chat-originated uses (wa, ga, ta) and message-practice uses populate both fields, enabling tracing back to the source message. Standalone practice uses (e.g., from the analytics practice page) correctly set these to null — there is no originating room/message in that context.
|
||||
|
||||
## Open Issues Discussions
|
||||
*Last updated: 2026-02-15*
|
||||
|
||||
- [#5675](https://github.com/pangeachat/client/issues/5675) — Rethink grammar analytics tab: filter irrelevant morph features per language pair, simplify UI for non-linguists
|
||||
- [#5506](https://github.com/pangeachat/client/discussions/5506) — Define key outcomes to track and why (analytics strategy)
|
||||
- [#4958](https://github.com/pangeachat/client/discussions/4958) — Implement repetition decay for per-user XP scoring
|
||||
- [#4959](https://github.com/pangeachat/client/discussions/4959) — Ensure "minutes per day" is being collected in analytics
|
||||
- [#5300](https://github.com/pangeachat/client/discussions/5300) — Some grammar types have no data (construct coverage gaps)
|
||||
- [#4947](https://github.com/pangeachat/client/discussions/4947) — Hard to tell where Activity Ping is coming from (analytics event clarity)
|
||||
- [#4742](https://github.com/pangeachat/client/discussions/4742) — Way to sort vocab words in analytics
|
||||
- [#4397](https://github.com/pangeachat/client/discussions/4397) — Link vocab detail sentences to actual chat messages for practice
|
||||
- [#5252](https://github.com/pangeachat/client/discussions/5252) — Show construct emoji change when XP earning triggers it
|
||||
- [#3569](https://github.com/pangeachat/client/discussions/3569) — Practice exercises in the analytics page
|
||||
|
||||
110
.github/instructions/conversation-activities.instructions.md
vendored
Normal file
110
.github/instructions/conversation-activities.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
applyTo: "lib/pangea/activity_planner/**,lib/pangea/activity_sessions/**,lib/pangea/activity_suggestions/**,lib/pangea/activity_summary/**"
|
||||
---
|
||||
|
||||
# Activity System
|
||||
|
||||
Activities are how Pangea Chat turns passive chatting into deliberate language learning. Activities are designed according best practices in task-based language teaching and second language acquisition research, with a focus on maximizing engagement and learning outcomes while minimizing friction.
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Conversation games**: We would like user's to think of and describe these experiences as 'games'. That is how we will know we have arrived.
|
||||
2. **Minimal front-loaded instruction**: Historically, conversation practice is proceeded by learning lists of vocab and grammar, which are quickly forgotten. Instead, we want to get users practicing real conversations as quickly as possible, with any necessary instruction or feedback integrated into the experience rather than front-loaded.
|
||||
3. **AI-powered assistance**: The system should leverage AI to provide personalized assistance, feedback, and content generation to enhance the learning experience.
|
||||
4. **Data-driven**: Every interaction should generate data that can be used to improve the system, understand user behavior, and demonstrate learning outcomes.
|
||||
|
||||
---
|
||||
|
||||
## Structured Activities
|
||||
|
||||
### The User Experience
|
||||
|
||||
|
||||
|
||||
### Key Technical Design Decisions
|
||||
|
||||
- **Activities are Matrix rooms.** This means all existing chat infrastructure (message events, timelines, sync) works automatically. No separate activity backend needed.
|
||||
- **Course context is inherited.** Activity rooms know which course (space) they belong to via parent room relationships. This lets summaries reference the course's learning goals.
|
||||
|
||||
### Activity Lifecycle States
|
||||
|
||||
1. User picks an activity from their course
|
||||
2. User starts activity
|
||||
3. User chooses role
|
||||
4. User decides who to play with
|
||||
- Wait for course participants with optional "Ping" to course
|
||||
- "Play with Pangea Bot" option for solo practice
|
||||
- "Invite friends" option to share an invite link or invite specific users
|
||||
5. Activity in progress
|
||||
6. Users mark activity as finished
|
||||
7. Activity summary generated and posted in the activity room, visible to all participants
|
||||
8. Users archive activity when done reviewing the summary (removes from active view but retains in history)
|
||||
Optionally, users can review past activities in their activity history, which surfaces summaries and outcomes from previous sessions. We intend to implement an explicit practice flow here.
|
||||
|
||||
An activity is "started" when all role slots are filled. It's "finished" when all non-bot participants have marked themselves done or left. "Archived" means the user has dismissed it from their active view.
|
||||
|
||||
## Future Work
|
||||
*Last updated: 2026-02-15*
|
||||
|
||||
**Lifecycle & Session Management**
|
||||
|
||||
- [pangeachat/client#5390](https://github.com/pangeachat/client/issues/5390) — Separate activities where user has a role from ones where they do not
|
||||
- [pangeachat/client#4805](https://github.com/pangeachat/client/discussions/4805) — Open/Joined/Done Activities not sorted
|
||||
- [pangeachat/client#4955](https://github.com/pangeachat/client/discussions/4955) — Highlight ongoing and open sessions
|
||||
- [pangeachat/client#4666](https://github.com/pangeachat/client/discussions/4666) — Auto-save to completed activities
|
||||
- [pangeachat/client#4667](https://github.com/pangeachat/client/discussions/4667) — Disallow kicking activity members with roles
|
||||
- [pangeachat/client#5435](https://github.com/pangeachat/client/discussions/5435) — On activity completion, create set of multiple-choice checks
|
||||
- [pangeachat/pangea-bot#1002](https://github.com/pangeachat/pangea-bot/issues/1002) — If goals seem complete, suggest they end the activity
|
||||
- [pangeachat/pangea-bot#999](https://github.com/pangeachat/pangea-bot/issues/999) — If stale activity session in-waiting, offer to do it
|
||||
- [pangeachat/pangea-bot#967](https://github.com/pangeachat/pangea-bot/issues/967) — Invite user to activity session
|
||||
|
||||
**Roles & Bot Behavior**
|
||||
|
||||
- [pangeachat/2-step-choreographer#1709](https://github.com/pangeachat/2-step-choreographer/issues/1709) — In Activity, if Bot's role shouldn't start activity, wait before posting message
|
||||
- [pangeachat/2-step-choreographer#1639](https://github.com/pangeachat/2-step-choreographer/issues/1639) — Activity roles look strange in non-english
|
||||
|
||||
**Activity Summaries**
|
||||
|
||||
- [pangeachat/2-step-choreographer#1340](https://github.com/pangeachat/2-step-choreographer/issues/1340) — activity summary not in activity's language of instruction
|
||||
- [pangeachat/2-step-choreographer#1695](https://github.com/pangeachat/2-step-choreographer/issues/1695) — Don't make punctuation corrections on voice message transcriptions
|
||||
- [pangeachat/2-step-choreographer#1102](https://github.com/pangeachat/2-step-choreographer/issues/1102) — Duplicate superlatives
|
||||
|
||||
**Descriptions & Onboarding UX**
|
||||
|
||||
- [pangeachat/2-step-choreographer#1707](https://github.com/pangeachat/2-step-choreographer/issues/1707) — Simplify Activity Descriptions
|
||||
- [pangeachat/2-step-choreographer#1708](https://github.com/pangeachat/2-step-choreographer/issues/1708) — Include translations for activity description examples
|
||||
- [pangeachat/client#5609](https://github.com/pangeachat/client/issues/5609) — Copy/Paste single words or letters feature
|
||||
- [pangeachat/client#5613](https://github.com/pangeachat/client/discussions/5613) — Shorten the learning curve time, especially for A1 users
|
||||
- [pangeachat/client#3382](https://github.com/pangeachat/client/discussions/3382) — More interesting loading screen during activity generation?
|
||||
- [pangeachat/client#4956](https://github.com/pangeachat/client/discussions/4956) — Add help-walkthrough for Course Page
|
||||
- [pangeachat/client#4947](https://github.com/pangeachat/client/discussions/4947) — Hard to tell where Activity Ping is coming from
|
||||
|
||||
**Activity Content & Media**
|
||||
|
||||
- [pangeachat/2-step-choreographer#1106](https://github.com/pangeachat/2-step-choreographer/issues/1106) — Generate role images based on activity images
|
||||
- [pangeachat/2-step-choreographer#1118](https://github.com/pangeachat/2-step-choreographer/issues/1118) — retry with feedback on image generation content violation
|
||||
- [pangeachat/2-step-choreographer#1440](https://github.com/pangeachat/2-step-choreographer/issues/1440) — Endpoint: Add avatar image to scene
|
||||
- [pangeachat/2-step-choreographer#1209](https://github.com/pangeachat/2-step-choreographer/issues/1209) — Prototype activity video search
|
||||
- [pangeachat/2-step-choreographer#1103](https://github.com/pangeachat/2-step-choreographer/issues/1103) — Add morphs to activities
|
||||
- [pangeachat/client#4650](https://github.com/pangeachat/client/discussions/4650) — Should generated images match art style associated with region language is associated with?
|
||||
- [pangeachat/client#2837](https://github.com/pangeachat/client/discussions/2837) — Integrating YouTube videos
|
||||
- [pangeachat/client#2773](https://github.com/pangeachat/client/discussions/2773) — Turn a link to interact-able activities
|
||||
- [pangeachat/cms#128](https://github.com/pangeachat/cms/issues/128) — Course languages get mixed up when previously translated
|
||||
- [pangeachat/cms#43](https://github.com/pangeachat/cms/issues/43) — Because course image is loaded from topic image, course image is not available until topics are fully loaded
|
||||
|
||||
**Planning, Courses & Discovery**
|
||||
|
||||
- [pangeachat/client#5196](https://github.com/pangeachat/client/issues/5196) — Don't see what you need? Course request button
|
||||
- [pangeachat/client#4793](https://github.com/pangeachat/client/discussions/4793) — In-app request for course
|
||||
- [pangeachat/client#2751](https://github.com/pangeachat/client/discussions/2751) — Mapping Learning Objectives with CEFR or ACTFL
|
||||
- [pangeachat/client#2185](https://github.com/pangeachat/client/discussions/2185) — Limit activity planner mode to provided choices?
|
||||
- [pangeachat/client#1658](https://github.com/pangeachat/client/discussions/1658) — Activity Planner Ideas
|
||||
|
||||
**New Activity Types & Game Ideas**
|
||||
|
||||
- [pangeachat/client#4841](https://github.com/pangeachat/client/discussions/4841) — Other Activity Game Ideas
|
||||
- [pangeachat/client#3952](https://github.com/pangeachat/client/discussions/3952) — I Spy vocab game
|
||||
|
||||
**Teacher Experience**
|
||||
|
||||
- [pangeachat/client#1837](https://github.com/pangeachat/client/discussions/1837) — Teacher's feedback (will add more)
|
||||
239
.github/instructions/practice-exercises.instructions.md
vendored
Normal file
239
.github/instructions/practice-exercises.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
---
|
||||
applyTo: "lib/pangea/practice_activities/**,lib/pangea/analytics_practice/**,lib/pangea/toolbar/message_practice/**"
|
||||
---
|
||||
|
||||
# Practice Exercises
|
||||
|
||||
Practice exercises are multiple-choice exercises that reinforce vocabulary and grammar from real conversations. There are no disconnected flashcard decks — every practice item traces back to a message the user sent or received.
|
||||
|
||||
For **conversation activities**, see [conversation-activities.instructions.md](conversation-activities.instructions.md).
|
||||
|
||||
## Three Entry Points
|
||||
|
||||
| Entry Point | What It Is | Where It Lives | Activity Types Used |
|
||||
|---|---|---|---|
|
||||
| **Vocab Practice** | Standalone session of ~10 vocab exercises drawn from the user's weakest words | Analytics page → "Practice Vocab" button → [`AnalyticsPractice(type: vocab)`](../../lib/pangea/analytics_practice/analytics_practice_page.dart) | `lemmaMeaning`, `lemmaAudio` |
|
||||
| **Grammar Practice** | Standalone session of ~10 grammar exercises drawn from recent errors + weak morphology | Analytics page → "Practice Grammar" button → [`AnalyticsPractice(type: morph)`](../../lib/pangea/analytics_practice/analytics_practice_page.dart) | `grammarError`, `grammarCategory` |
|
||||
| **Message Practice** | Per-message practice accessed from the toolbar; exercises target words in that specific message | Toolbar → 💪 button → [`PracticeController`](../../lib/pangea/toolbar/message_practice/practice_controller.dart) | `wordMeaning`, `wordFocusListening`, `emoji`, `morphId` |
|
||||
|
||||
All three entry points produce the same [`ConstructUseModel`](../../lib/pangea/analytics_misc/constructs_model.dart) records, so practice from any source contributes equally to the user's vocabulary garden and XP.
|
||||
|
||||
---
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Spaced repetition without the UI**: Users never configure schedules. The system silently prioritizes words they haven't seen recently and content words over function words.
|
||||
2. **Multi-modal learning**: Each word can be practiced through listening, meaning, emoji association, and grammar analysis — hitting visual, auditory, and semantic learning channels.
|
||||
3. **Completion, not perfection**: Wrong answers still contribute (at reduced XP), keeping the experience encouraging.
|
||||
4. **Always from real messages**: Every practice target traces back to a token from a real conversation message.
|
||||
|
||||
---
|
||||
|
||||
## Target Prioritization: Which Words First?
|
||||
|
||||
All practice paths aim to surface words/constructs the user hasn't practiced recently, but they currently use **two separate implementations** rather than a shared service.
|
||||
|
||||
### Message Practice — scoring formula
|
||||
|
||||
[`PracticeSelectionRepo._fetchPriorityScores`](../../lib/pangea/practice_activities/practice_selection_repo.dart) computes a numeric score per token:
|
||||
|
||||
```
|
||||
score = daysSinceLastUsed × (isContentWord ? 10 : 7)
|
||||
```
|
||||
|
||||
- **`daysSinceLastUsed`**: looked up via `getConstructUses` → `lastUseByTypes` filtered to the specific activity type's associated construct-use types. If the word has never been practiced, defaults to **20 days**.
|
||||
- **Content word bonus**: nouns, verbs, and adjectives get a 10× multiplier; function words (articles, prepositions) get 7×. This meaningfully favors content words when recency is equal.
|
||||
- Tokens are sorted by score descending, top 8 taken, then shuffled.
|
||||
|
||||
### Standalone Practice — simple recency sort
|
||||
|
||||
[`AnalyticsPracticeSessionRepo._fetchVocab`](../../lib/pangea/analytics_practice/analytics_practice_session_repo.dart) and `_fetchAudio` sort by `lastUsed` ascending with nulls first (never-practiced words come first). There is **no scoring formula** and **no content-word bonus**.
|
||||
|
||||
Grammar targets use a different strategy:
|
||||
- **`_fetchErrors`**: selects recent grammar mistakes, skipping any construct practiced in the last 24 hours.
|
||||
- **`_fetchMorphs`**: sorts morph constructs by `lastUsed` ascending (same as vocab).
|
||||
|
||||
### ⚠️ Divergence note
|
||||
|
||||
These two systems evolved independently. The message-practice scorer is more nuanced (explicit formula, content-word weighting, per-activity-type recency). The standalone path is simpler but misses the content-word boost and uses aggregate recency rather than per-type recency. Unifying them into a shared prioritization service is a natural improvement — see Future Work.
|
||||
|
||||
### 🧠 Design Direction: Use-Type-Aware Spaced Repetition
|
||||
|
||||
The current scoring only considers **recency** and **content-word status**. It ignores the rich signal from [`ConstructUseTypeEnum`](../../lib/pangea/analytics_misc/construct_use_type_enum.dart) — specifically **how** the user encountered or practiced each word. The next evolution should classify words into priority tiers based on their use-type history:
|
||||
|
||||
**Three-tier model:**
|
||||
|
||||
| Tier | Who goes here | Practice priority |
|
||||
|---|---|---|
|
||||
| **Suppressed** | Lemmas whose most recent chat use is `wa` (without assistance) AND no subsequent incorrect practice | **0** — skip entirely |
|
||||
| **Active** | Lemmas encountered through `ta` (IT) or `ga` (IGC), OR lemmas with a recent incorrect practice answer (`incXX`) | **High** — prioritize these |
|
||||
| **Maintenance** | Everything else — correctly practiced but aging | **Normal** — standard recency-based |
|
||||
|
||||
**Tier transitions:**
|
||||
- A `wa` use → moves to Suppressed (user knows this word)
|
||||
- A `ta` or `ga` use → moves to Active (user needed help)
|
||||
- An incorrect practice answer → moves to Active (user struggled)
|
||||
- N consecutive correct practice answers → Active → Maintenance (learning is sticking)
|
||||
- Time passes without interaction → Maintenance words naturally bubble up via recency
|
||||
|
||||
**Within each tier**, the existing scoring formula applies: `daysSinceLastUsed × (isContentWord ? 10 : 7)`. Active-tier words get an additional multiplier (e.g., ×2) so they always appear before maintenance words of similar age.
|
||||
|
||||
**Key principle**: Words used through IT and IGC should be practiced **much more** than `wa` words. A `wa` word should only re-enter practice if the user later gets it wrong.
|
||||
|
||||
**Example scenario:**
|
||||
1. User types "gato" correctly without assistance → `wa` → Suppressed. Won't appear in practice.
|
||||
2. User uses IT to translate "mariposa" → `ta` → Active. High priority for practice.
|
||||
3. User practices "mariposa" and gets it wrong → `incLM` → stays Active, priority boosted.
|
||||
4. User practices "mariposa" correctly 3 times → Active → Maintenance.
|
||||
5. Two weeks pass with no interaction → Maintenance, but high recency score → likely to appear.
|
||||
6. User later misspells "gato" and IGC corrects it → `ga` → moves from Suppressed back to Active.
|
||||
|
||||
### ⚠️ Grammar Error Practice: Missing Message Data
|
||||
|
||||
[`_fetchErrors`](../../lib/pangea/analytics_practice/analytics_practice_session_repo.dart) finds grammar practice material by querying construct uses of type `ga` (grammar accepted), then resolving the original message via `room?.getEventById(eventID)`. While the Matrix SDK's `Room.getEventById` already falls back to a server fetch if the event isn't in the local database, the chain can still fail at an earlier step: `client.getRoomById(roomID)` returns null if the room isn't loaded in memory (e.g., the user left). When the event *is* found, additional data is needed — the `PangeaMessageEvent` wrapper requires a timeline, tokens, choreo data, and translations, all of which are only available if the full message representation events are also accessible. A similar pattern applies to `_fetchAudio` and `_fetchMorphs`, which need example messages with token data and audio. See Future Work for improvements.
|
||||
|
||||
---
|
||||
|
||||
## Activity Types
|
||||
|
||||
The [`ActivityTypeEnum`](../../lib/pangea/practice_activities/activity_type_enum.dart) defines all exercise types. They split into two groups:
|
||||
|
||||
### Message Practice Types (toolbar)
|
||||
|
||||
| Type | Enum Value | What the User Does | Learning Channel |
|
||||
|---|---|---|---|
|
||||
| Listening | `wordFocusListening` | Hears the word spoken, taps the correct token in the message | Auditory recognition |
|
||||
| Meaning | `wordMeaning` | Sees L1 translations, picks the right one for a highlighted word | Semantic retrieval |
|
||||
| Emoji | `emoji` | Chooses the best emoji for a word (associative learning) | Visual association |
|
||||
| Grammar | `morphId` | Matches morphological features (tense, number, case) to values | Analytical/structural |
|
||||
|
||||
### Standalone Practice Types (analytics page)
|
||||
|
||||
| Type | Enum Value | What the User Does | Generator |
|
||||
|---|---|---|---|
|
||||
| Vocab Meaning | `lemmaMeaning` | Picks the correct lemma definition from distractors | [`VocabMeaningActivityGenerator`](../../lib/pangea/analytics_practice/vocab_meaning_activity_generator.dart) |
|
||||
| Vocab Audio | `lemmaAudio` | Hears a word in the context of an example sentence, identifies it | [`VocabAudioActivityGenerator`](../../lib/pangea/analytics_practice/vocab_audio_activity_generator.dart) |
|
||||
| Grammar Category | `grammarCategory` | Identifies the correct morph tag (e.g., "Past Tense") for a word | [`MorphCategoryActivityGenerator`](../../lib/pangea/analytics_practice/morph_category_activity_generator.dart) |
|
||||
| Grammar Error | `grammarError` | Picks the correct replacement for a grammar error they made | [`GrammarErrorPracticeGenerator`](../../lib/pangea/analytics_practice/grammar_error_practice_generator.dart) |
|
||||
|
||||
Each activity type maps to specific [`ConstructUseTypeEnum`](../../lib/pangea/analytics_misc/construct_use_type_enum.dart) values for correct/incorrect/ignored answers (e.g., `corLM`/`incLM` for lemmaMeaning).
|
||||
|
||||
---
|
||||
|
||||
## Standalone Practice Sessions (Vocab & Grammar)
|
||||
|
||||
### Session Lifecycle
|
||||
|
||||
1. [`AnalyticsPracticeSessionRepo.get(type, language)`](../../lib/pangea/analytics_practice/analytics_practice_session_repo.dart) builds a session:
|
||||
- **Vocab**: fetches the user's weakest lemmas (by spaced-repetition score), splits ~50/50 between `lemmaAudio` (needs example messages with audio) and `lemmaMeaning` targets
|
||||
- **Grammar**: fetches recent grammar errors first (`grammarError` targets), then fills remaining slots with weak morph features (`grammarCategory` targets)
|
||||
- Session size: 10 exercises + 5 error buffer (constants in [`AnalyticsPracticeConstants`](../../lib/pangea/analytics_practice/analytics_practice_constants.dart))
|
||||
2. [`AnalyticsPracticeState`](../../lib/pangea/analytics_practice/analytics_practice_page.dart) manages the session UI — progress bar, timer, activity queue, hints
|
||||
3. For each target, a [`MessageActivityRequest`](../../lib/pangea/practice_activities/message_activity_request.dart) is sent to the appropriate generator
|
||||
4. The generator returns a [`PracticeActivityModel`](../../lib/pangea/practice_activities/practice_activity_model.dart) subclass with choices and answers
|
||||
5. On answer, a construct use is recorded and the session advances
|
||||
|
||||
### Session Completion
|
||||
|
||||
When all targets are answered, [`CompletedActivitySessionView`](../../lib/pangea/analytics_practice/completed_activity_session_view.dart) shows:
|
||||
- Total correct / incorrect / skipped
|
||||
- Time elapsed (with bonus XP if under 60 seconds)
|
||||
- Per-item review
|
||||
|
||||
### Subscription Gate
|
||||
|
||||
Standalone practice requires an active subscription. [`UnsubscribedPracticePage`](../../lib/pangea/analytics_practice/unsubscribed_practice_page.dart) is shown if the user isn't subscribed.
|
||||
|
||||
---
|
||||
|
||||
## Message Practice (Toolbar)
|
||||
|
||||
### Target Selection
|
||||
|
||||
[`PracticeSelectionRepo`](../../lib/pangea/practice_activities/practice_selection_repo.dart) determines which tokens in a message become practice targets:
|
||||
|
||||
- Only tokens with `saveVocab = true` on their lemma (filters out punctuation, numbers, etc.)
|
||||
- Only messages in the user's target language (L2)
|
||||
- Deduplicated by lemma — if "running" and "runs" appear in the same message, only one is selected
|
||||
- Capped at 5 targets per activity type per message (avoids overwhelming long messages)
|
||||
|
||||
Selections are cached per message with a 1-day TTL in [`PracticeSelection`](../../lib/pangea/practice_activities/practice_selection.dart).
|
||||
|
||||
Token priority within each message uses the scoring formula described in [Target Prioritization](#target-prioritization-which-words-first) above.
|
||||
|
||||
### Practice Modes
|
||||
|
||||
[`MessagePracticeMode`](../../lib/pangea/toolbar/message_practice/message_practice_mode_enum.dart) defines the four toolbar modes: `listening`, `wordMeaning`, `wordEmoji`, `wordMorph`. Each mode maps to an `ActivityTypeEnum` and shows per-word buttons on the message. When all words in a mode are complete, the mode's icon turns gold.
|
||||
|
||||
### Controller
|
||||
|
||||
[`PracticeController`](../../lib/pangea/toolbar/message_practice/practice_controller.dart) manages per-message practice state:
|
||||
- Fetches `PracticeSelection` on construction
|
||||
- Generates activities on demand via [`PracticeRepo`](../../lib/pangea/practice_activities/practice_generation_repo.dart)
|
||||
- Records answers via [`PracticeRecordController`](../../lib/pangea/toolbar/message_practice/practice_record_controller.dart)
|
||||
- Plays TTS on correct answers for audio reinforcement
|
||||
|
||||
---
|
||||
|
||||
## Activity Generation
|
||||
|
||||
[`PracticeRepo`](../../lib/pangea/practice_activities/practice_generation_repo.dart) is the central dispatch for generating exercises. It:
|
||||
|
||||
1. Receives a `MessageActivityRequest` with a `PracticeTarget` (tokens + activity type + optional morph feature)
|
||||
2. Routes to the correct generator based on activity type
|
||||
3. Caches results per-target with a 1-day TTL to avoid re-generating on re-render
|
||||
4. Message-practice types (`wordMeaning`, `emoji`, `morphId`, `wordFocusListening`) call the choreographer API
|
||||
5. Standalone types (`lemmaMeaning`, `lemmaAudio`, `grammarCategory`, `grammarError`) generate locally using lemma data and morph mappings
|
||||
|
||||
### Model Hierarchy
|
||||
|
||||
[`PracticeActivityModel`](../../lib/pangea/practice_activities/practice_activity_model.dart) is a sealed class with subclasses for each activity type:
|
||||
- `VocabMeaningPracticeActivityModel`, `VocabAudioPracticeActivityModel`
|
||||
- `MorphCategoryPracticeActivityModel`, `GrammarErrorPracticeActivityModel`
|
||||
- `LemmaPracticeActivityModel`, `LemmaMeaningPracticeActivityModel`
|
||||
- `EmojiPracticeActivityModel`, `MorphMatchPracticeActivityModel`
|
||||
- `WordListeningPracticeActivityModel`
|
||||
|
||||
All expose a `multipleChoiceContent` (choices + answers) and produce a `PracticeTarget` for recording.
|
||||
|
||||
---
|
||||
|
||||
## Key Contracts
|
||||
|
||||
- **Practice targets are deterministic per message.** For a given eventId + language + token set, the same targets are generated and cached. Don't introduce randomness that would change targets on re-render.
|
||||
- **Practice never blocks on network.** Selection happens locally from cached token data. Activity content fetches from choreo, but the UI shows shimmer placeholders, never a blocking spinner.
|
||||
- **Emoji and meaning choices persist beyond the practice session.** They become the user's personal annotation on that lemma, visible in word cards and analytics.
|
||||
- **All practice produces construct uses.** Whether from the toolbar or the standalone page, every answer is recorded as a `ConstructUseModel` that feeds into the analytics system.
|
||||
|
||||
## Future Work
|
||||
*Last updated: 2026-02-15*
|
||||
|
||||
**Practice Types & Modalities**
|
||||
|
||||
- [pangeachat/client#5656](https://github.com/pangeachat/client/issues/5656) — Voice practice ideas
|
||||
- [pangeachat/client#3175](https://github.com/pangeachat/client/discussions/3175) — Speaking practice for Voice/Audio message
|
||||
- [pangeachat/client#3176](https://github.com/pangeachat/client/discussions/3176) — New type of practice activity
|
||||
- [pangeachat/client#2678](https://github.com/pangeachat/client/discussions/2678) — Listening exercises
|
||||
- [pangeachat/client#5654](https://github.com/pangeachat/client/issues/5654) — Are there more places where it makes sense to use the word audio?
|
||||
|
||||
**Practice Generation & Targeting**
|
||||
|
||||
- [pangeachat/client#5700](https://github.com/pangeachat/client/issues/5700) — Unified practice target selection with use-type-aware spaced repetition and server-side message fetch (covers Parts 1 & 2: shared scorer + three-tier model)
|
||||
- [pangeachat/client#2677](https://github.com/pangeachat/client/discussions/2677) — Generate activities based on stored word forms from analytics
|
||||
- [pangeachat/2-step-choreographer#1546](https://github.com/pangeachat/2-step-choreographer/issues/1546) — Add emojis to distractor generation
|
||||
|
||||
**Practice UX & Feedback**
|
||||
|
||||
- [pangeachat/client#5436](https://github.com/pangeachat/client/discussions/5436) — If messages practice is complete, put special gold barbell reaction on it
|
||||
- Persist a completion record to the Matrix room when a user completes all 4 practice modes on a message, making practice targets deterministic across sessions and devices
|
||||
- [pangeachat/client#3569](https://github.com/pangeachat/client/discussions/3569) — Practice Exercises in the analytics page
|
||||
|
||||
**Bugs & Quality**
|
||||
|
||||
- [pangeachat/2-step-choreographer#1568](https://github.com/pangeachat/2-step-choreographer/issues/1568) — Vocab Practice in English instead of L1
|
||||
|
||||
**Server-Side & Cross-Device**
|
||||
|
||||
- Server-side practice history to enable cross-device spaced repetition
|
||||
- [pangeachat/client#5700](https://github.com/pangeachat/client/issues/5700) Part 3 — Server-side message fetch fallback for practice (room resolution, related sub-event data)
|
||||
- More activity types (fill-in-the-blank, sentence reordering, pronunciation scoring)
|
||||
178
.github/instructions/toolbar-reading-assistance.instructions.md
vendored
Normal file
178
.github/instructions/toolbar-reading-assistance.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
---
|
||||
applyTo: "lib/pangea/toolbar/**"
|
||||
---
|
||||
|
||||
# Toolbar & Reading Assistance
|
||||
|
||||
The toolbar is the primary learning surface for received messages. When a user taps any message in a chat, an overlay appears that transforms that message into an interactive language exercise. The design philosophy: **every message is a lesson**, and the toolbar is how you access it.
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Think in your target language**: The toolbar emphasizes word-level interaction over full-sentence translation. The goal is to encourage users to engage with the text and discover meanings themselves, rather than relying on passive translation. Translation is available but intentionally one step removed. Further, messages can be viewed in 'emoji mode' where known words show their emoji associations above the text.
|
||||
2. **Encourage exploration**: Word cards are simple and feature playful emoji associations and encourage exploration via the vocab collection mechanic and rewarding of the first tap with XP and visual feedback. The experience should feel like discovering hidden treasures in the text.
|
||||
3. **Opportunities for hearing the language**: On each click of a word, its audio is played. The audio mode with word-by-word highlighting provides a karaoke-like experience that helps users connect written and spoken forms.
|
||||
|
||||
## The User Experience
|
||||
|
||||
### Reading Assistance (Select Mode)
|
||||
|
||||
User taps a message → the message appears in an overlay with each word individually tappable:
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ [Word card for selected │ ← appears above message when a word is tapped
|
||||
│ token: meaning, phonetic, │
|
||||
│ emoji, feedback button] │
|
||||
├──────────────────────────────┤
|
||||
│ The cat sat on the mat │ ← message with underlined/interactive tokens
|
||||
│ ~~~ ~~~ ~~~ │ new words shimmer to attract attention
|
||||
├──────────────────────────────┤
|
||||
│ audio translate practice emoji ...│ ← standard message actions
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
**When the user taps a word:**
|
||||
|
||||
- The word card appears above with the lemma (dictionary form), meaning, phonetic transcription, and emojis.
|
||||
- TTS speaks the word aloud (unless the user is already in audio mode)
|
||||
- If this is the **first time** the user has ever tapped this word, it silently earns a small amount of XP — the word has been "planted" as a seed in their vocabulary garden
|
||||
- A green underline effect on new words draws the eye to words worth exploring.
|
||||
|
||||
### Toolbar Modes
|
||||
|
||||
The toolbar offers several assistance modes. Which ones appear depends on the message:
|
||||
|
||||
| Mode | What the user sees | When it appears |
|
||||
| ---------------------- | -------------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| **Audio** | Full sentence TTS with word-by-word highlighting (like a karaoke bar) | Text messages in user's L2 |
|
||||
| **Translate** | Full L1 translation appears below the message | Text messages in L2 or unknown languages |
|
||||
| **Practice** | Transitions to practice mode (see conversation-activities doc) | Text messages in user's L2 |
|
||||
| **Emoji** | Emoji picker appears for each word — user can assign personal emoji associations | Text messages in user's L2 |
|
||||
| **Speech Translation** | Shows STT transcript + translation for audio messages | Audio messages not in user's L1 |
|
||||
| **Regenerate** | Re-tokenizes the message (for when tokenization looks wrong) | Optional, when enabled |
|
||||
|
||||
**Mode availability logic:**
|
||||
|
||||
- Messages in the user's **L2** (target language) → all modes available
|
||||
- Messages in the user's **L1** (native language) → no modes (nothing to learn)
|
||||
- Messages in an **unknown language** → only translate
|
||||
- **Audio messages** → speech transcription and translation modes instead of text modes
|
||||
|
||||
### Practice Mode Transition
|
||||
|
||||
When the user taps the practice button:
|
||||
|
||||
1. The overlay message **animates to the center** of the screen and enlarges
|
||||
2. Practice buttons appear on each eligible word
|
||||
3. The user cycles through four practice types (listening, meaning, emoji, grammar)
|
||||
4. Completed modes turn gold; when all four are gold for all words, the message is "fully practiced"
|
||||
|
||||
This transition is a deliberate "mode shift" — the user is choosing to go deeper. The animation signals that something different is happening.
|
||||
|
||||
## Word Card
|
||||
|
||||
The word card is the detailed view for a single token. It appears above the message when a word is tapped and contains:
|
||||
|
||||
- **Lemma** — the dictionary form (e.g., "laufen" for "lief")
|
||||
- **Meaning** — either user-set or auto-generated L1 translation
|
||||
- **Phonetic transcription** — IPA or simplified pronunciation guide
|
||||
- **Emoji** — the user's personal emoji association (if set), or a picker to set one
|
||||
- **Feedback button** — lets the user report bad tokenization or incorrect meanings
|
||||
|
||||
The word card is intentionally compact — it should be glanceable, not a full dictionary entry. The goal is quick recognition, not exhaustive reference.
|
||||
|
||||
## Collection Mechanic
|
||||
|
||||
The toolbar visually distinguishes words the user has never interacted with. New words get a green underline effect that draws attention without being disruptive. The first time a user taps a new word:
|
||||
|
||||
1. The underline disappears (globally — once you've tapped that word, you understand how it works)
|
||||
2. The word earns a small "click" XP bonus
|
||||
3. The word is now tracked as a seed in the user's vocabulary garden
|
||||
|
||||
The design insiration is from Zelda's spin slash of grass to uncover rupees.
|
||||
|
||||
## Audio Playback & Highlighting
|
||||
|
||||
In audio mode, the toolbar plays the full sentence with word-by-word highlighting:
|
||||
|
||||
- Each word highlights as it's being spoken (driven by TTS timing data)
|
||||
- The user can tap individual words to hear them in isolation
|
||||
- Highlighted tokens follow the TTS playback position in real time
|
||||
|
||||
For audio messages (voice notes), the toolbar provides transcription with the same word-level interaction — tap any word in the transcript to hear it, see its meaning, or practice it.
|
||||
|
||||
## Emoji Associations
|
||||
|
||||
Users can assign a personal emoji to any word. This serves two purposes:
|
||||
|
||||
1. **Memory aid** — visual associations help with retention
|
||||
2. **Personalization** — the emoji appears on the word's token in future encounters, making familiar words instantly recognizable
|
||||
|
||||
Emoji associations persist via the Matrix analytics room and sync across devices.
|
||||
|
||||
## Layout Constraints
|
||||
|
||||
The toolbar must work within chat layout constraints:
|
||||
|
||||
- The overlay positions itself relative to the original message bubble
|
||||
- On small screens, it scrolls if the message + word card + buttons exceed available space
|
||||
- The overlay must survive screen rotation and keyboard appearance without losing state
|
||||
- On width changes (e.g., split-screen), the overlay dismisses rather than attempting to reposition (avoids jarring layout jumps)
|
||||
|
||||
## Key Contracts
|
||||
|
||||
- **Overlay, not navigation.** The toolbar never pushes a route. It's a composited overlay that lives on top of the chat. Dismissal returns to the exact same chat state.
|
||||
- **Lazy loading.** Translation, TTS, and transcription are fetched only when the user activates the corresponding mode. Nothing is prefetched on message tap.
|
||||
- **Token-centric.** All assistance features require the message to have a tokenized representation. If tokens aren't available (message still loading, unsupported language), the toolbar shows limited functionality.
|
||||
- **First-click-only analytics.** Tapping the same word repeatedly doesn't keep earning XP. Only the first interaction with a word per session counts, preventing XP gaming.
|
||||
- **Deferred setState.** Because the overlay lives in the compositing layer alongside the chat, setState calls must be phase-aware to avoid "setState during build" errors. All state updates check the scheduler phase and defer if necessary.
|
||||
|
||||
## Future Work
|
||||
|
||||
_Last updated: 2026-02-15_
|
||||
|
||||
### Overlay & Layout
|
||||
|
||||
- [pangeachat/client#3348](https://github.com/pangeachat/client/discussions/3348) — Issues with message overlay / toolbar updates
|
||||
- [pangeachat/client#3344](https://github.com/pangeachat/client/discussions/3344) — Mixed message text directionality (RTL overlay layout)
|
||||
- [pangeachat/client#2237](https://github.com/pangeachat/client/discussions/2237) — Soft cap on message length (overlay sizing)
|
||||
- [pangeachat/client#5609](https://github.com/pangeachat/client/issues/5609) — Copy/paste single words or letters from overlay
|
||||
|
||||
### Word Card
|
||||
|
||||
- [pangeachat/client#4731](https://github.com/pangeachat/client/discussions/4731) — Word card emotes are confusing
|
||||
- [pangeachat/client#4481](https://github.com/pangeachat/client/discussions/4481) — Give definitions by form in word cards (not lemma)
|
||||
- [pangeachat/client#2356](https://github.com/pangeachat/client/discussions/2356) — Simplify lemma_definition user report flow when initial definition is wrong
|
||||
- [pangeachat/client#5610](https://github.com/pangeachat/client/issues/5610) — Fix wording on the word-card emoji selection popup
|
||||
- [pangeachat/2-step-choreographer#1475](https://github.com/pangeachat/2-step-choreographer/issues/1475) — Lemma definition improvements (contractions, context, forms)
|
||||
- [pangeachat/2-step-choreographer#1505](https://github.com/pangeachat/2-step-choreographer/issues/1505) — Consume message info for lemma info endpoint
|
||||
- [pangeachat/cms#49](https://github.com/pangeachat/cms/issues/49) — Improve lemma definition flow (CMS localization + batching)
|
||||
|
||||
### Phonetic Transcription
|
||||
|
||||
- [pangeachat/client#5600](https://github.com/pangeachat/client/discussions/5600) — How should pronunciations be displayed for different learners?
|
||||
- [pangeachat/2-step-choreographer#1611](https://github.com/pangeachat/2-step-choreographer/issues/1611) — Phonetic transcription: heteronyms and TTS consistency
|
||||
- [pangeachat/2-step-choreographer#1610](https://github.com/pangeachat/2-step-choreographer/issues/1610) — Test pinyin transcription library accuracy
|
||||
- [pangeachat/2-step-choreographer#831](https://github.com/pangeachat/2-step-choreographer/issues/831) — English-Vietnamese phonetic transcription is not good
|
||||
|
||||
### Audio & TTS
|
||||
|
||||
- [pangeachat/client#5654](https://github.com/pangeachat/client/issues/5654) — Are there more places where it makes sense to use word audio?
|
||||
- [pangeachat/client#3175](https://github.com/pangeachat/client/discussions/3175) — Speaking practice for voice/audio messages
|
||||
- [pangeachat/client#2678](https://github.com/pangeachat/client/discussions/2678) — Listening exercises
|
||||
- [pangeachat/client#5656](https://github.com/pangeachat/client/issues/5656) — Voice practice ideas
|
||||
|
||||
### Translation & IT
|
||||
|
||||
- [pangeachat/client#5437](https://github.com/pangeachat/client/discussions/5437) — Audio, translation, and emoji tools, toggable
|
||||
- [pangeachat/client#5612](https://github.com/pangeachat/client/issues/5612) — Make IT popup feel less like an error
|
||||
- [pangeachat/client#1896](https://github.com/pangeachat/client/discussions/1896) — Interactive Translator needs refresh and probably skip
|
||||
- [pangeachat/client#3302](https://github.com/pangeachat/client/discussions/3302) — Changing IT
|
||||
- [pangeachat/client#4649](https://github.com/pangeachat/client/discussions/4649) — Should other users' messages in DMs be automatically translated to L2?
|
||||
- [pangeachat/2-step-choreographer#1477](https://github.com/pangeachat/2-step-choreographer/issues/1477) — Direct translation improvements (ambiguous words)
|
||||
- [pangeachat/2-step-choreographer#1311](https://github.com/pangeachat/2-step-choreographer/issues/1311) — Mixed languages carry over into translation
|
||||
|
||||
### Collection Mechanic
|
||||
|
||||
- [pangeachat/client#4387](https://github.com/pangeachat/client/discussions/4387) — Max on underlined new words in a sentence feels like a bug
|
||||
- [pangeachat/client#1754](https://github.com/pangeachat/client/discussions/1754) — Render pangea representation of user L1
|
||||
|
|
@ -1,27 +1,20 @@
|
|||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_selection.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
class _PracticeSelectionCacheEntry {
|
||||
final PracticeSelection selection;
|
||||
final DateTime timestamp;
|
||||
|
||||
_PracticeSelectionCacheEntry({
|
||||
required this.selection,
|
||||
required this.timestamp,
|
||||
});
|
||||
_PracticeSelectionCacheEntry({required this.selection, required this.timestamp});
|
||||
|
||||
bool get isExpired => DateTime.now().difference(timestamp).inDays > 1;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'selection': selection.toJson(),
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
Map<String, dynamic> toJson() => {'selection': selection.toJson(), 'timestamp': timestamp.toIso8601String()};
|
||||
|
||||
factory _PracticeSelectionCacheEntry.fromJson(Map<String, dynamic> json) {
|
||||
return _PracticeSelectionCacheEntry(
|
||||
|
|
@ -34,11 +27,7 @@ class _PracticeSelectionCacheEntry {
|
|||
class PracticeSelectionRepo {
|
||||
static final GetStorage _storage = GetStorage('practice_selection_cache');
|
||||
|
||||
static Future<PracticeSelection?> get(
|
||||
String eventId,
|
||||
String messageLanguage,
|
||||
List<PangeaToken> tokens,
|
||||
) async {
|
||||
static Future<PracticeSelection?> get(String eventId, String messageLanguage, List<PangeaToken> tokens) async {
|
||||
final userL2 = MatrixState.pangeaController.userController.userL2;
|
||||
if (userL2?.langCodeShort != messageLanguage.split("-").first) {
|
||||
return null;
|
||||
|
|
@ -53,12 +42,8 @@ class PracticeSelectionRepo {
|
|||
return newEntry;
|
||||
}
|
||||
|
||||
static Future<PracticeSelection> _fetch({
|
||||
required List<PangeaToken> tokens,
|
||||
required String langCode,
|
||||
}) async {
|
||||
if (langCode.split("-")[0] !=
|
||||
MatrixState.pangeaController.userController.userL2?.langCodeShort) {
|
||||
static Future<PracticeSelection> _fetch({required List<PangeaToken> tokens, required String langCode}) async {
|
||||
if (langCode.split("-")[0] != MatrixState.pangeaController.userController.userL2?.langCodeShort) {
|
||||
return PracticeSelection({});
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +51,7 @@ class PracticeSelectionRepo {
|
|||
if (eligibleTokens.isEmpty) {
|
||||
return PracticeSelection({});
|
||||
}
|
||||
final queue = await _fillActivityQueue(
|
||||
eligibleTokens,
|
||||
langCode.split('-')[0],
|
||||
);
|
||||
final queue = await _fillActivityQueue(eligibleTokens, langCode.split('-')[0]);
|
||||
final selection = PracticeSelection(queue);
|
||||
return selection;
|
||||
}
|
||||
|
|
@ -78,9 +60,7 @@ class PracticeSelectionRepo {
|
|||
try {
|
||||
final keys = List.from(_storage.getKeys());
|
||||
for (final String key in keys) {
|
||||
final cacheEntry = _PracticeSelectionCacheEntry.fromJson(
|
||||
_storage.read(key),
|
||||
);
|
||||
final cacheEntry = _PracticeSelectionCacheEntry.fromJson(_storage.read(key));
|
||||
if (cacheEntry.isExpired) {
|
||||
_storage.remove(key);
|
||||
}
|
||||
|
|
@ -94,9 +74,7 @@ class PracticeSelectionRepo {
|
|||
if (entry == null) return null;
|
||||
|
||||
try {
|
||||
return _PracticeSelectionCacheEntry.fromJson(
|
||||
_storage.read(eventId),
|
||||
).selection;
|
||||
return _PracticeSelectionCacheEntry.fromJson(_storage.read(eventId)).selection;
|
||||
} catch (e) {
|
||||
_storage.remove(eventId);
|
||||
return null;
|
||||
|
|
@ -104,10 +82,7 @@ class PracticeSelectionRepo {
|
|||
}
|
||||
|
||||
static void _setCached(String eventId, PracticeSelection entry) {
|
||||
final cachedEntry = _PracticeSelectionCacheEntry(
|
||||
selection: entry,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
final cachedEntry = _PracticeSelectionCacheEntry(selection: entry, timestamp: DateTime.now());
|
||||
_storage.write(eventId, cachedEntry.toJson());
|
||||
}
|
||||
|
||||
|
|
@ -122,19 +97,9 @@ class PracticeSelectionRepo {
|
|||
return queue;
|
||||
}
|
||||
|
||||
static int _sortTokens(
|
||||
PangeaToken a,
|
||||
PangeaToken b,
|
||||
int aScore,
|
||||
int bScore,
|
||||
) => bScore.compareTo(aScore);
|
||||
static int _sortTokens(PangeaToken a, PangeaToken b, int aScore, int bScore) => bScore.compareTo(aScore);
|
||||
|
||||
static int _sortMorphTargets(
|
||||
PracticeTarget a,
|
||||
PracticeTarget b,
|
||||
int aScore,
|
||||
int bScore,
|
||||
) => bScore.compareTo(aScore);
|
||||
static int _sortMorphTargets(PracticeTarget a, PracticeTarget b, int aScore, int bScore) => bScore.compareTo(aScore);
|
||||
|
||||
static List<PracticeTarget> _tokenToMorphTargets(PangeaToken t) {
|
||||
return t.morphsBasicallyEligibleForPracticeByPriority
|
||||
|
|
@ -171,11 +136,7 @@ class PracticeSelectionRepo {
|
|||
return [];
|
||||
}
|
||||
|
||||
final scores = await _fetchPriorityScores(
|
||||
practiceTokens,
|
||||
activityType,
|
||||
language,
|
||||
);
|
||||
final scores = await _fetchPriorityScores(practiceTokens, activityType, language);
|
||||
|
||||
practiceTokens.sort((a, b) => _sortTokens(a, b, scores[a]!, scores[b]!));
|
||||
practiceTokens = practiceTokens.take(8).toList();
|
||||
|
|
@ -189,25 +150,11 @@ class PracticeSelectionRepo {
|
|||
];
|
||||
}
|
||||
|
||||
static Future<List<PracticeTarget>> _buildMorphActivity(
|
||||
List<PangeaToken> tokens,
|
||||
String language,
|
||||
) async {
|
||||
static Future<List<PracticeTarget>> _buildMorphActivity(List<PangeaToken> tokens, String language) async {
|
||||
final List<PangeaToken> practiceTokens = List<PangeaToken>.from(tokens);
|
||||
final candidates = practiceTokens.expand(_tokenToMorphTargets).toList();
|
||||
final scores = await _fetchPriorityScores(
|
||||
practiceTokens,
|
||||
ActivityTypeEnum.morphId,
|
||||
language,
|
||||
);
|
||||
candidates.sort(
|
||||
(a, b) => _sortMorphTargets(
|
||||
a,
|
||||
b,
|
||||
scores[a.tokens.first]!,
|
||||
scores[b.tokens.first]!,
|
||||
),
|
||||
);
|
||||
final scores = await _fetchPriorityScores(practiceTokens, ActivityTypeEnum.morphId, language);
|
||||
candidates.sort((a, b) => _sortMorphTargets(a, b, scores[a.tokens.first]!, scores[b.tokens.first]!));
|
||||
|
||||
final seenTexts = <String>{};
|
||||
final seenLemmas = <String>{};
|
||||
|
|
@ -232,24 +179,18 @@ class PracticeSelectionRepo {
|
|||
final ids = tokens.map((t) => t.vocabConstructID).toList();
|
||||
final idMap = {for (final token in tokens) token: token.vocabConstructID};
|
||||
|
||||
final constructs = await MatrixState
|
||||
.pangeaController
|
||||
.matrixState
|
||||
.analyticsDataService
|
||||
.getConstructUses(ids, language);
|
||||
final constructs = await MatrixState.pangeaController.matrixState.analyticsDataService.getConstructUses(
|
||||
ids,
|
||||
language,
|
||||
);
|
||||
|
||||
for (final token in tokens) {
|
||||
final construct = constructs[idMap[token]];
|
||||
final lastUsed = construct?.lastUseByTypes(
|
||||
activityType.associatedUseTypes,
|
||||
);
|
||||
final lastUsed = construct?.lastUseByTypes(activityType.associatedUseTypes);
|
||||
|
||||
final daysSinceLastUsed = lastUsed == null
|
||||
? 20
|
||||
: DateTime.now().difference(lastUsed).inDays;
|
||||
final daysSinceLastUsed = lastUsed == null ? 20 : DateTime.now().difference(lastUsed).inDays;
|
||||
|
||||
scores[token] =
|
||||
daysSinceLastUsed * (token.vocabConstructID.isContentWord ? 10 : 9);
|
||||
scores[token] = daysSinceLastUsed * (token.vocabConstructID.isContentWord ? 10 : 7);
|
||||
}
|
||||
return scores;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue