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

@@ -0,0 +1,48 @@
import SwiftData
import Foundation
@Model
public final class Conversation {
public var id: String = ""
public var scenario: String = ""
public var level: String = ""
public var messages: String = "[]"
public var createdDate: Date = Date()
public init(scenario: String, level: String) {
self.id = UUID().uuidString
self.scenario = scenario
self.level = level
self.messages = "[]"
self.createdDate = Date()
}
}
public struct ChatMessage: Codable, Identifiable, Hashable {
public var id: String
public let role: String // "assistant" or "user"
public let content: String
public let correction: String?
public init(role: String, content: String, correction: String? = nil) {
self.id = UUID().uuidString
self.role = role
self.content = content
self.correction = correction
}
}
extension Conversation {
public var decodedMessages: [ChatMessage] {
guard let data = messages.data(using: .utf8) else { return [] }
return (try? JSONDecoder().decode([ChatMessage].self, from: data)) ?? []
}
public func appendMessage(_ message: ChatMessage) {
var msgs = decodedMessages
msgs.append(message)
if let data = try? JSONEncoder().encode(msgs), let str = String(data: data, encoding: .utf8) {
messages = str
}
}
}