Add 6 new practice features, offline dictionary, and feature reference

New features:
- Offline Dictionary: reverse index of 175K verb forms + 200 common
  words, cached to disk, powers instant word lookups in Stories
- Vocab SRS Review: spaced repetition for course vocabulary cards
  with due count badge and Again/Hard/Good/Easy rating
- Cloze Practice: fill-in-the-blank using SentenceQuizEngine with
  distractor generation from vocabulary pool
- Grammar Exercises: interactive quizzes for 5 grammar topics
  (ser/estar, por/para, preterite/imperfect, subjunctive, personal a)
  with "Practice This" button on grammar note detail
- Listening Practice: listen-and-type + pronunciation check modes
  using Speech framework with word-by-word match scoring
- Conversational Practice: AI chat partner via Foundation Models
  with 10 scenario types, saved to cloud container

Other changes:
- Add Conversation model to SharedModels and cloud container
- Add Info.plist keys for speech recognition and microphone
- Skip speech auth on simulator to prevent crash
- Fix preparing data screen to only show during seed/migration
- Extract courseDataVersion to static property on DataLoader
- Add "How Features Work" reference page in Settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-13 16:12:36 -05:00
parent b13f58ec81
commit a663bc03cd
20 changed files with 2253 additions and 19 deletions

View File

@@ -5,6 +5,7 @@ import FoundationModels
struct StoryReaderView: View {
let story: Story
@Environment(DictionaryService.self) private var dictionary
@State private var selectedWord: WordAnnotation?
@State private var showTranslation = false
@State private var lookupCache: [String: WordAnnotation] = [:]
@@ -105,7 +106,20 @@ struct StoryReaderView: View {
}
private func lookupWord(_ word: String, inContext sentence: String) {
// Show immediately with loading state
// Try offline dictionary first
if let entry = dictionary.lookup(word) {
let annotation = WordAnnotation(
word: word,
baseForm: entry.baseForm,
english: entry.english,
partOfSpeech: entry.partOfSpeech
)
lookupCache[word] = annotation
selectedWord = annotation
return
}
// Fall back to on-device AI lookup
selectedWord = WordAnnotation(word: word, baseForm: word, english: "Looking up...", partOfSpeech: "")
Task {