Commit Graph

116 Commits

Author SHA1 Message Date
Trey T 32395bac5d Book reader — speed as a 5-option dropdown with multiplier labels
Replace the 3-way segmented speed control with a dropdown menu offering
0.5× / 0.75× / 1× / 1.25× / 1.5×, with evened-out underlying AVSpeech
rates anchored at 1× = 0.50. Align the default saved rate to 0.50 so 1×
is selected on a fresh install (was 0.45, which matched no option).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:20:58 -05:00
Trey T b97da5e85e Book reader — choose read-aloud start + play/pause/resume
- Long-press a word to mark where read-aloud begins (session-only). A
  distinct indigo marker shows the spot; long-pressing it again clears it.
  Play honors the marker on a fresh start.
- BookSpeechController can start mid-paragraph by speaking a substring;
  a per-entry wordIndexOffset keeps word highlighting aligned to the full
  paragraph's coordinates.
- The main button is now Play / Pause / Resume — it resumes in place
  instead of restarting, so pausing, flipping to English and back, then
  resuming continues from the same word. A separate Stop button ends the
  session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:08:07 -05:00
Trey T aab64116b3 Vocab study — per-type session sizes + Review Learned multiple choice
- Settings: split the single session-size picker into separate Verbs /
  Nouns / Adjectives pickers. Nouns and adjectives previously shared one
  hidden limit; they now use nounSessionCardLimit / adjectiveSessionCardLimit.
- LexemePool.sessionCardLimit is now per part-of-speech.
- Multiple-choice views (verb/noun/adjective) gained a kind param so
  Review Learned can run as multiple choice, not just flashcards. The
  cram pass drives the in-session queue only and leaves the long-term
  SRS schedule untouched.
- PracticeView: each section now offers Review Learned — Flashcards and
  Review Learned — Multiple Choice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 23:54:10 -05:00
Trey T 179400b90d Course — Review Course Material row with bundled weekly PDFs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:40:02 -05:00
Trey T 696eafa64f Noun & adjective practice — Multiple Choice, Review Learned, Review
Mirror the four-entry Vocabulary section for nouns and adjectives, so each
POS gets the same set of practice modes the verb flow already had:

- Noun/Adjective Flashcards (existing) — English → Spanish reveal with
  article for nouns. Now accepts `kind:` to share the view with the
  Review-Learned cram pass.
- Noun/Adjective Multiple Choice — English prompt, 4 Spanish options
  drawn from the current session pool (1 correct + 3 random distractors).
  Same SRS rating writes as Flashcards.
- Review Learned — `NounFlashcardPracticeView(kind: .reviewLearned)` and
  the adjective equivalent. Cycles through already-studied lexemes with
  no schedule changes; mirrors `VocabFlashcardPracticeView`'s
  reviewLearned kind.
- Noun/Adjective Review — fetches due `LexemeReviewCard` rows by POS,
  Spanish-front / English-reveal flashcards rated directly against the
  SRS schedule. Each exposes a static `dueCount(context:)` used by the
  practice-row badge.

Wiring:
- New `LexemeSessionKind` enum (standard / reviewLearned) in
  LexemeSessionQueue.swift, mirroring `VocabSessionKind`.
- Noun + Adjective Flashcard views branch load/persist/answer on `kind`
  so Review Learned doesn't touch the persisted study group or reschedule
  cross-session SRS.
- Practice screen gets dedicated "Nouns" and "Adjectives" sections
  (between Vocabulary and Reading), each with 4 NavigationLinks shaped
  exactly like the Vocabulary section. The previous single-link Noun and
  Adjective entries in the Reading section are removed.
- PracticeView caches `nounDueCount` / `adjectiveDueCount` in @State and
  refreshes on appear + after sessions end, so the badge doesn't trigger
  LexemeReviewCard fetchCount on every body re-evaluation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:59:42 -05:00
Trey T 7da98d786c Vocab study — noun & adjective flashcards with CEFR level toggles
Add SRS-driven noun and adjective flashcards modeled on the existing verb
flashcard flow:

- SharedModels/Lexeme — catalog of non-verb vocab, frequency-ranked, with
  gender for nouns and optional example sentences. Seeded from a bundled
  vocab_lexemes.json built by Scripts/vocab/build_lexemes.py, which joins
  frequency.csv + es-en.data from a pinned doozan/spanish_data commit
  (CC-BY-SA: hermitdave/FrequencyWords + Wiktionary). 1,449 nouns and 600
  adjectives, each with Wiktionary-sourced gender and (where available)
  an example sentence with English translation.
- LexemeReviewCard + LexemeReviewStore — cloud-synced SM-2 SRS, keyed by
  partOfSpeech + lexemeId + drillMode so future drill modes can coexist.
- LexemeSessionQueue + LexemePool — parallel to VocabSessionQueue; fresh
  cards sort by frequency rank.
- LexemeStudyGroup — cloud-synced resumable session per
  (partOfSpeech, drillMode).
- NounFlashcardPracticeView + AdjectiveFlashcardPracticeView — same flow
  as VocabFlashcardPracticeView: English prompt → tap to reveal Spanish
  → Again/Hard/Good/Easy. Nouns reveal with their article (la taza, el
  problema) so gender is taught alongside meaning, not as a separate
  quiz. Example sentence shown when present.

CEFR-style level toggles:
- LexemeLevel enum (A1/A2/B1/B2/C1+) derived from frequencyRank with
  standard Spanish-frequency-dictionary cutoffs (250/500/1000/2000).
- UserProgress.selectedLexemeLevels — cloud-synced multi-select, defaults
  to A1+A2 on first launch.
- SettingsView gains a "Vocabulary Levels" section with five toggles; the
  existing "Levels" section is renamed "Verb Levels" for clarity.
- Due SRS cards always surface regardless of toggles. Disabling a level
  only stops new cards from that band entering the pool.

PracticeView gets "Nouns" and "Adjectives" rows under "Books".

DataLoader: new lexemeDataVersion gate that re-seeds the Lexeme table
from vocab_lexemes.json independent of book seeding. project.yml lists
the new JSON resource and the existing book_olly-vol2.json (which the
previous build was silently excluding because xcodegen rewrote the
project from project.yml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:16:55 -05:00
Trey T ac84b22977 Books — fix infinite render loop via value-based navigation
Opening a book chapter froze the app in an infinite render loop. Root
cause: the books screens used the eager `NavigationLink { destination }`
form inside `List`/`LazyVStack`. That form keeps the destination view
structurally parented to the source row, so `BookReaderView`'s ScrollView
got trapped inside a `List` row — a row sizes to intrinsic height, a
ScrollView has none, so the two never converge and re-measure forever.

Switch the whole books navigation chain to value-based navigation:
- practiceHomeView, BookLibraryView, BookChapterListView use
  NavigationLink(value:).
- PracticeView's NavigationStack declares the BooksRoute, Book, and
  BookChapter destinations once, at the stack root (mixing eager and
  value-based pushes in one path caused pushed screens to pop back).
- BookReaderView is built from just a BookChapter; it resolves its Book
  by slug via @Query.

Also:
- BookChapter gains a stored paragraphCount so the chapter list no longer
  decodes the full paragraph JSON on every render (bookDataVersion -> 6
  to re-seed).
- BookSpeechController builds its AVSpeechSynthesizer lazily.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:08:36 -05:00
Trey T 3ee1563cb0 Books — pre-computed per-book glossary for context-correct word lookup
The book reader's word lookup used DictionaryService, a verb-conjugation
index plus ~200 hand-typed words: ordinary nouns like "taza" returned
nothing, and homographs always lost (tapping "como" in "como siempre"
gave the verb "comer" because the verb index is checked first).

Add a glossary phase to the books pipeline (build_glossary.py): every
distinct Spanish word is translated once, in its sentence context, by
the same Claude-Code-subagent LLM step the pipeline already uses for
chapter translation. English front matter is excluded by an ES==EN
paragraph-ratio heuristic. The glossary is bundled into book_<slug>.json
and is now part of the pipeline for every book.

In the app, Book carries the decoded glossary and BookReaderView resolves
each tap automatically through cache -> glossary -> DictionaryService ->
on-device LLM, citing which source answered so a curated glossary hit
reads differently from a best-effort AI guess.

book_olly-vol2.json regenerated with a 3,658-word glossary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:44:32 -05:00
Trey T d0582c4ce7 Full Table — vary tense and verb family between consecutive prompts
randomFullTablePrompt now takes the previous prompt's tense and verb
ending and picks the next one to avoid an immediate repeat: when more
than one tense is selected the just-shown tense is excluded, and when
both -ar and -er/-ir verbs are available the next verb switches family.

Both constraints are best-effort — if honouring one would leave no
eligible fully-regular combo it is dropped, and the exhaustive
"anything eligible at all" guarantee is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:41:18 -05:00
Trey T 26ce662c60 Fix widget wiping the Books tables on every refresh
The widget extension opened the shared local SwiftData store with a
7-entity schema while the app's store has 10. SwiftData treats the
smaller schema as a migration and destructively drops the unlisted
tables — so every widget refresh deleted the bundled Book/BookChapter
rows (and DownloadedVideo), which is why books vanished after reinstalls.

Introduce SharedStore.localSchemaModels as the single source of truth
for the local schema and build the app and both widget containers from
it, so app and widget can no longer drift apart. The same class of bug
hit TextbookChapter previously; a shared list prevents a third recurrence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:12:51 -05:00
Trey T dd08a09860 Vocab Practice — apply code-review fixes for the SRS session work
Addresses the lead-review findings on the new vocab SRS/persistence code:

- Resume a persisted study group only when every stored verb resolves,
  else rebuild fresh — a partial resume desynced learnedCount from a
  shrunken queue and then persisted the loss.
- studyAgain() clears the finished group before building a new one.
- VocabStudyGroupStore.persist() collapses duplicate group records
  (cross-device sync races) down to the newest.
- Learn mode now derives its verb list from the live quiz queue, so it
  browses exactly what's left to learn instead of a stale frozen pool.
- VerbExampleGenerator retries when the first batch throws, not only
  when it returns bad data.
- Per-verb example generation tracks in-flight verbs in a Set, so the
  loading spinner clears reliably and duplicate generations are skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:59:33 -05:00
Trey T c794c013f0 Vocab Flashcards — persist the study group across launches and devices
The session queue was in-memory only: leaving the flashcard screen or
killing the app rebuilt it from scratch. The set of verbs you were
working through wasn't sticky.

VocabStudyGroup (new cloud-synced @Model) — the active standard study
set: the in-session queue (un-graduated verbs + each card's
learning-step state) as a JSON blob, plus the learned count. One active
group at a time, keyed "active-standard". CloudKit-synced, so the same
group follows you from iPad to iPhone.

VocabStudyGroupStore — fetch-or-create / persist / clear. activeGroup()
returns the newest if duplicates exist (two devices racing before sync
settles); clear() removes all duplicates.

VocabSessionQueue — CardState is now String-backed; added
init(entries:learnedCount:) to resume a saved queue and snapshot() to
export it.

VocabFlashcardPracticeView (standard kind):
  - On load, resume the active group if one exists; otherwise build a
    fresh set and persist it immediately.
  - Every answer writes the updated queue to the group.
  - Finishing the set clears the group; the completion button is now
    "Next Set" and builds a fresh group.
Review Learned (reviewLearned kind) is unaffected — it's an ad-hoc cram
and intentionally isn't persisted; its button stays "Study Again".

Registered VocabStudyGroup in the cloud container schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:42:17 -05:00
Trey T eec0fb56d5 Vocab Practice — add "Review Learned" consolidation mode
After a few Vocab Flashcards sessions, the verbs you've learned have
future SM-2 due dates, so they're excluded from new sessions (which
only pull due + new). There was no way to deliberately re-review a set
you'd already memorised.

Review Learned (new row in Practice → Vocabulary) fixes that — a cram
pass over every verb that has a VerbReviewCard, most-recently-studied
first, uncapped.

  VocabVerbPool.reviewLearnedVerbs — studied verbs sorted by
  lastReviewDate desc. Ignores due dates and the Level filter; it's a
  deliberate "review everything I've learned" pass.

  VocabFlashcardPracticeView gains a `kind` parameter
  (.standard / .reviewLearned). reviewLearned uses the new pool and —
  crucially — does NOT call VerbReviewStore.rate on graduation. Ratings
  drive the in-session learning queue for the session feel, but the
  long-term schedule is left untouched (Anki's reschedule-off cram
  behaviour), so a consolidation pass can't shove your real due dates
  weeks out.

  Header shows "Practice pass — your review schedule won't change" in
  this mode. Quiz/Learn toggle and Study Again work the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:24:47 -05:00
Trey T 209602eaad Docs — refresh feature reference + app_features for this session's work
Both the in-app Feature Reference and app_features.md were last updated
2026-04-21, before Books, the verb-table vocab SRS, Extra Study,
read-along, the guide enrichment, and the Practice-tab restructure.

FeatureReferenceView (Settings → How Features Work):
  - Regrouped to match the live Practice tab: Conjugation / Vocabulary
    / Reading sections.
  - Added Vocab Flashcards (Quiz + Learn modes, in-session learning
    queue), Vocab Multiple Choice, Books + read-aloud, Extra Study,
    and Guide cross-links.
  - Tense/grammar counts corrected (36 grammar notes, enriched guides).
  - "Settings That Affect Practice" now lists Cards per session and
    notes the Verbs-tab ↔ Level sync.

app_features.md (Conjuga section of the comparison doc):
  - Practice modes split into Conjugation / Vocabulary / Reading.
  - Documented the two-layer vocab SRS, Books + books pipeline,
    Extra Study, guide enrichment + cross-links, configurable session
    size, and the VerbReviewCard cloud model.
  - Data table updated (36 grammar notes, bundled books row).

Docs only — no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:57:25 -05:00
Trey T cee962c0e0 Vocab Flashcards — make session size configurable in Settings
VocabVerbPool.sessionCardLimit was hardcoded at 20. Settings now has a
"Vocab Flashcards → Cards per session" picker (10 / 15 / 20 / 25 / 30 /
50 / All) backed by the vocabSessionCardLimit @AppStorage key.

VocabVerbPool.sessionCardLimit became a computed property reading that
key (0/unset → default 20; 999 → "All"). Applies to both Quiz and Learn
modes since they share the same session pool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 13:06:07 -05:00
Trey T f14008f96f Vocab Flashcards — add no-pressure Learn mode alongside the SRS quiz
A Quiz/Learn toggle now sits in the flashcard toolbar (Menu, persisted
via @AppStorage):

  Quiz — unchanged. The VocabSessionQueue SRS path: tap to reveal, rate
  Again/Hard/Good/Easy, requeue, graduation feeds VerbReviewStore.

  Learn — browsing, not testing. Both sides show at once (English +
  Spanish + example sentence + speaker button), Next/Previous step
  through the session pool, looping (wraps at both ends). No rating
  buttons, no SRS writes, no requeue — just repeated exposure.

Both modes draw from the same fetched session pool (due-first + new,
capped 20) and the same lazily-generated example cache. Switching
modes is instant — the quiz queue keeps its state, the learn cursor
keeps its position; they're independent. The header shows the SRS
progress in Quiz and a plain "3 of 20" position in Learn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:53:22 -05:00
Trey T 5c0fc8ee2d Vocab Practice — proper SRS session queue with in-session learning steps
Vocab Flashcards / Multiple Choice were a flat linear walk through a
shuffled list — rating a card "Hard" or "Again" did nothing to bring it
back. Replaced with a real two-layer SRS, matching how Anki separates
in-session learning steps from the long-term schedule.

VocabSessionQueue (new) — the in-session layer. Position-based learning
steps:
  - Again → card reappears 5–8 cards later (state: learning)
  - Hard  → reappears 7–10 cards later (state: learning)
  - Good  → first time: → review state, reappears 16–24 cards later
            already in review: graduates, leaves the session
  - Easy  → graduates immediately
A card you keep failing keeps cycling until you mark it Good twice or
Easy once. answer() returns a ReviewQuality only on graduation — that's
the single rating handed to the long-term VerbReviewStore, so
intermediate Again/Hard presses no longer thrash the cross-session
SM-2 schedule.

VocabVerbPool.sessionVerbs (rewritten) — due-first ordering + a 20-card
session cap. Overdue verbs (per VerbReviewCard.dueDate) come first,
most-overdue leading; then never-reviewed verbs by frequency rank.
Not-yet-due verbs are intentionally skipped — that's the SRS schedule
doing its job. A single sitting is now bounded instead of a 100+ card
slog.

Study Again — the completion screen gets a "Study Again" button that
rebuilds the queue from the same verb set (re-shuffled), so you can run
the whole set again after finishing.

Progress display switched from "1 of 110" to "N learned · M to go",
which reflects the live queue as cards requeue and graduate.

Both vocab views now share VocabSessionQueue + VocabVerbPool; the queue
struct is pure value-type logic, easy to reason about and test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:44:43 -05:00
Trey T d61f9e50b1 Vocab Practice — speaker button to hear the Spanish word
Both vocab session reveal panes now show a speaker button next to the
Spanish infinitive. Tapping it speaks the word via the existing
SpeechService (Spanish voice), same TTS the Course flashcards use.

Flashcard mode: button beside the infinitive in the reveal pane.
Multiple choice: button beside the infinitive in the answer feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:15:12 -05:00
Trey T 900a927f95 Fixes #34 — drop AI-generated images from verb vocab practice
Image Playground's .illustration style can't depict abstract verbs —
"dar" (to give) generated a Shiba Inu in a meadow, "saber" a cake.
Verbs are actions, not objects; a static illustration rarely helps and
the irrelevant output was pure noise.

Removed:
  - VerbIllustration view (was in VocabFlashcardPracticeView).
  - VocabImageService.swift entirely — VerbIllustration was its only
    consumer.
  - vocabImageService from the app environment in ConjugaApp.
  - The illustration row from both vocab session reveal panes.

Vocab card reveal is now: Spanish infinitive + example sentence +
rating buttons. The example sentence already carries the learning
context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:38:01 -05:00
Trey T f0eb75a28a Fixes #33 — verb examples must actually use the verb
The example sentences in VerbDetailView (and the new vocab practice
modes) frequently used the wrong verb — a "tener" set would show
"Él estaba leyendo un libro" (estar), "Nosotros vamos a viajar" (ir),
"Tú debes estudiar" (deber). The model drifted off the target verb
partway through generating the 6-example batch, and nothing checked
the output.

Two defenses:

  Prompt grounding — VerbExampleGenerator.generate now takes a
  formsByTense map (tenseId → conjugated forms, from the new
  ReferenceStore.conjugatedForms). Each tense line in the prompt
  lists the verb's exact conjugated forms and instructs the model to
  use one of them. The model echoes a real form instead of recalling
  (and mis-recalling) the conjugation.

  Output validation — every generated sentence is checked against the
  conjugation table via accent/case-folded whole-word matching. Any
  sentence that doesn't contain a real conjugated form of the verb is
  rejected. Failures trigger one regeneration pass; anything still
  wrong is dropped rather than displayed. Better to show 4 correct
  examples than 6 with 2 wrong.

  Cache invalidation — VerbExampleCache now persists a versioned
  wrapper (version 2). Pre-fix cached example sets — which may contain
  wrong-verb sentences — fail the version check and are discarded, so
  they regenerate cleanly under the new path.

Callers updated: VerbDetailView, VocabFlashcardPracticeView,
VocabMultipleChoicePracticeView all build formsByTense from
ReferenceStore and pass it through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:57:57 -05:00
Trey T f4c139aed0 Fixes #35 — vocab practice no longer doubles the verb's "to"
VocabFlashcardPracticeView and VocabMultipleChoicePracticeView rendered
the English prompt as "to \(verb.english)". Every verb in conjuga_data
already stores english with a leading "to " (all 1750 of them), so the
prompt came out as "to to be", "to to run", etc.

Dropped the prefix — verb.english is shown verbatim. Same fix applied
to the Image Playground concept string so generated illustrations are
prompted with "to be" rather than "to to be".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:43:34 -05:00
Trey T 0af8e648fe Vocab Practice crash — defensive Dictionary init + correct tense count
Crash: Swift/NativeDictionary.swift:792 Fatal error: Duplicate values
for key: 'imp_tú', triggered the first time a user rated a vocab card
and the in-flight example generation tried to materialise.

Root cause: VerbExampleGenerator.generate() builds a [tenseId: example]
dictionary from the model's output via Dictionary(uniqueKeysWithValues:),
which traps on duplicates. The generator's @Generable schema declares
@Guide(.count(6)) on the examples array, so the LLM is forced to return
exactly 6. The new Vocab Flashcards / Multiple Choice views called
generate(... tenseIds: ["ind_presente"]) — only one tense — which left
the model to invent the other 5 tenseIds; it duplicated 'imp_tú' and
the dictionary init trapped.

Three fixes:

  Services/VerbExampleGenerator.swift — use Dictionary(_:uniquingKeysWith:)
  with first-wins so the generator can never crash regardless of caller
  shape.

  Views/Practice/Vocab/VocabFlashcardPracticeView.swift and
  Views/Practice/Vocab/VocabMultipleChoicePracticeView.swift — pass
  the canonical 6-tense set (VocabExampleTenseIds.canonical, same as
  VerbDetailView uses), then pick the ind_presente example for the
  card. Caches all six in VerbExampleCache as a side effect.

  Views/Verbs/VerbListView.swift — replace empty-string systemImage
  on the Level menu Labels with "circle" so the device console isn't
  spammed with "No symbol named '' found in system symbol set" every
  time the user opens the filter menu.

The crash analysis I gave earlier (CloudKit schema migration of
VerbReviewCard) was wrong — device console shows the real culprit. No
CloudKit-side changes needed; the new model stays in the cloud
container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:02:34 -05:00
Trey T c890095610 Vocab Practice — verb pool, Settings level filter, per-verb SRS
Vocab Practice used a deck picker over VocabCard rows. That meant it
ignored the Settings level toggles entirely and operated on a totally
separate vocabulary universe than the conjugation modes. Rewired
end-to-end:

Pool source
  Replaced VocabCard with the Verb table. The pool is now
  ReferenceStore.fetchVerbs(selectedLevels: UserProgress.selectedVerbLevels)
  — the same call PracticeSessionService uses for conjugation. Changes
  to level toggles in Settings (or the Verbs tab, which also writes to
  this field) immediately affect Vocab Practice.

Entry flow
  Deleted VocabPracticeEntryView. Practice → Vocabulary now has two
  direct entries:
    • Vocab Flashcards — verb.english → tap → verb.infinitive
    • Vocab Multiple Choice — verb.english → pick from 4 infinitives
  Both pull from the same level-filtered pool, shuffled.

Per-verb SRS
  New VerbReviewCard @Model (cloud-synced, mirrors CourseReviewCard's
  SM-2 fields but keyed by verbId). VerbReviewStore.rate(verbId:quality:)
  applies the existing SRSEngine. Registered in cloud container schema
  in ConjugaApp.swift.

Example sentences
  Lazy-generated via VerbExampleGenerator on first reveal, cached
  through VerbExampleCache (same path VerbDetailView uses). Empty until
  the example arrives — block hides itself if Apple Intelligence isn't
  available.

AI illustration
  VerbIllustration replaces VocabIllustration; same Image Playground
  pipeline. Cache key uses ("verb", infinitive, english) so verbs and
  course-deck vocab never collide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:31:12 -05:00
Trey T 164a0a1bb7 Verbs — share level filter with Practice via UserProgress
VerbListView used a local @State selectedLevel that reset whenever
the user left the tab and was disconnected from Practice's verb
sourcing. PracticeSessionService reads UserProgress.selectedVerbLevels;
the Verbs tab now does the same.

  Level menu becomes multi-select (toggle each level in/out, mirrors
  SettingsView's pattern). "All Levels" enables every level; tapping
  again on a level removes it from the active set. Picking "All"
  while everything is already on is a no-op.

  Filter logic loops over the active level set and admits a verb if
  any active level matches via VerbLevelGroup.matches.

  Active-filter chip shows "Basic", "Elementary", etc. when one level
  is on, or "N levels" when multiple are on. Tapping the chip resets
  to all-levels.

  Empty set is treated as "no filter applied" on the Verbs list, but
  setAllLevels(enabled: false) falls back to [.basic] rather than an
  empty set — Practice treats empty as "no verbs", so we guard against
  leaving the user with an empty pool.

Practice already reads UserProgress.selectedVerbLevels, so changes
made in the Verbs tab now flow into Practice automatically. No
Practice-side code changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:18:23 -05:00
Trey T d49eb38a6d Books — bump bookDataVersion to 4 to force re-seed
A device install was showing the Books row but no books inside. The
seed code is intact (bundle resource present, schema registered,
explicit-slug fallback in place). Bumping the version flips
versionCurrent to false on every existing install, forcing
refreshBooksDataIfNeeded to wipe + re-seed on next launch. The
diagnostic prints added earlier ([DataLoader] bundledBookJSONURLs
found N files / Book seeding complete) will tell us if it still
fails this time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:09:00 -05:00
Trey T 0b7d4a73ad Add Vocab Practice — English-first flashcards + multiple choice, with AI illustrations
Practice tab restructured into three sections:
- Conjugation: the 6 conjugation modes + the Common Tenses / Weak
  Verbs / Irregularity Drills focus buttons.
- Vocabulary: new "Vocab Practice" entry (flashcard or multiple
  choice, deck-picker on entry) + the existing Vocab Review.
- Reading: Stories, Books, Lyrics, Conversation, Listening, Cloze
  (moved here from the flat list).

VocabPracticeEntryView lets the user pick any course/textbook deck
(or "All decks") and a mode (Flashcard / Multiple Choice). Last-used
choice is remembered via @AppStorage.

VocabFlashcardPracticeView:
  Front shows the English meaning. Tap to reveal the Spanish word,
  example sentences from the card, an AI-generated illustration of
  the concept, and Again/Hard/Good/Easy rating buttons. SRS updates
  via the existing CourseReviewStore.rate() path.

VocabMultipleChoicePracticeView:
  English prompt, 4 Spanish options. Distractors come from the same
  deck and prefer matching part-of-speech (via
  DictionaryService.lookup) — falls back to random when POS is
  unknown or the deck has fewer than 4 cards. After answer: reveal
  correct/incorrect, the Spanish word, examples, illustration, and
  the same rating buttons.

VocabImageService wraps Apple Intelligence's ImageCreator
(iOS 18.2+) for on-device illustration generation. Caches PNG
results to disk under Caches/VocabImages keyed by
SHA256(deck+ES+EN). In-flight dedup keeps concurrent requests for
the same key sharing one task. Falls back to a placeholder UI when
Apple Intelligence isn't available (older devices / disabled in
Settings) — detected lazily on the first failed ImageCreator init.

EN-first direction is enforced regardless of the underlying deck's
isReversed flag, so the user sees the English-to-Spanish recall
direction they asked for even when practising a reversed course
deck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:02:02 -05:00
Trey T 9aa4d0836d Guide — bidirectional cross-link chips between tense guides & grammar notes
After the full enrichment pass, both the Tenses and Grammar surfaces of
the Guide tab cover overlapping material — WEIRDO appears in both
"Subjuntivo Presente" and "Subjunctive Triggers", preterite↔imperfect
contrast in three places, etc. Instead of trimming either body and
losing content, add a small chip row at the top of each detail view
linking directly across.

GuideCrossLinks.swift (new) — curated tense→[noteId] map covering 18 of
the 20 tenses. The two without aligned notes (ind_pluscuamperfecto,
ind_preterito_anterior) don't show chips. The reverse map (noteId→
[tenseId]) is derived once at static init and sorted by canonical tense
order so chips appear in conjugation-table order.

GuideDetailView — "Related grammar" indigo chip row directly under the
header. Tap a chip → switch to the Grammar segment with that note
selected.

GrammarNoteDetailView — "Used in tenses" orange chip row directly under
the title. Tap a chip → switch to the Tenses segment with that tense
selected.

The GuideView segment-change handler now only clears the *other* tab's
selection so programmatic jumps keep their destination intact; manual
segment swipes still feel "fresh" like before.

No content is removed. Users get a deeper-dive path one tap away in
either direction, and the redundancy becomes a feature instead of a
maintenance hazard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:59:46 -05:00
Trey T 5db4b014a9 Guide — full enrichment pass: 19 tense guides + 36 grammar notes
Brings every tense guide and grammar note in the app up to teacher-
handout depth via parallel research subagents drafting against a
shared "thorough" checklist (TL;DR, usages, conjugation table, common
irregulars, mnemonic, top pitfalls, contrast with neighbour topic,
real-world dialogue example).

Tense guides — Conjuga/conjuga_data.json (tenseGuides[].body)
  All 19 remaining guides rewritten. The 20th (subj_presente) was
  enriched in the prior commit. Each body now ~4-5.5K chars (vs the
  500-1500 chars of the pre-pass reference cards), covering:
  - All five indicative tenses, both conditionals, both imperatives.
  - Full subjunctive set including the archaic futuro / futuro
    perfecto, framed honestly with "recognise, don't produce" guidance.
  - Per-tense conjugation patterns and the top 5-15 irregular verbs.
  - Tense-vs-tense contrasts (preterite↔imperfect, future↔ir-a,
    -ra↔-se past subjunctive, etc.).
  - Pitfalls that English speakers actually make.

Grammar notes — Conjuga/Conjuga/Models/GrammarNote.swift
  All 36 notes audited and rewritten where the existing body was
  missing one of: explicit mnemonic, contrast pair, pitfalls section,
  or coverage of a key sub-topic. None copied verbatim — every note
  got at least one of those slotted in. Notable additions:
  - DOCTOR/PLACE, WEIRDO, ESCAPA, RID, PRODDS, BANGS, RRPIA mnemonics
    where missing.
  - commands-imperative: nosotros + vosotros forms were entirely
    absent; both added with the -d/-os and present-subjunctive rules.
  - relative-pronouns: el que/el cual distinction, cuyo, lo que/lo
    cual, donde/adonde.
  - se-constructions: all 6 uses including the le→se substitution.
  - irregular-yo-verbs: impact on subjunctive and negative tú command.
  - Plus 5-item pitfalls sections on every note that lacked one.

Tooling — Conjuga/Scripts/guide-enrichment/
  - PLAN.md (prior commit) — the audit, checklist, and priority order
    that drove this pass.
  - apply_drafts.py (new) — reads drafts/out/*.md, swaps tense guides
    into the JSON and grammar notes into the Swift source via regex on
    the GrammarNote(...) declarations. Handles multi-block `#` comment
    headers some agents emitted. drafts/in/ and drafts/out/ are
    gitignored — regeneratable from current state.

DataLoader.swift — courseDataVersion 8 → 9 so existing installs re-
seed all guides on next launch.

Verification:
  - `swift -frontend -parse` on GrammarNote.swift succeeds (exit 0).
  - JSON validates (python3 json.load round-trip).
  - Triple-quote count is even (72 = 36 pairs, matching 36 notes).
  - Full xcodebuild verify deferred — local SDK install was disrupted
    by an Xcode update; will retest as part of the next ad-hoc deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:50:35 -05:00
Trey T de446b2301 Guide — enrich present-subjunctive entry with WEIRDO + ESCAPA + plan
The present-subjunctive guide was surface-level: two numbered usages
and a handful of examples, no mnemonic and no structural trigger cue.
That's the recurring problem with the tense guides — they're reference
cards, not teaching materials.

This commit fixes the immediate gap and lays out a plan to fix the
rest:

  Conjuga/conjuga_data.json — subj_presente body expanded from 794 to
  3670 chars. Adds the WEIRDO mnemonic with per-letter triggers and
  examples (Wishes, Emotions, Impersonal, Recommendations, Doubt,
  Ojalá), the ESCAPA adverbial-conjunction set, the "que + change of
  subject" structural rule, adjectival clauses with unknown
  antecedents, and the future-time-clause rule (cuando / hasta que /
  en cuanto).

  Scripts/guide-enrichment/PLAN.md (new) — audit of all 20 tense
  guides and 36 grammar notes, tier-1/2/3 prioritisation, "thorough"
  checklist (TL;DR, usages, conjugation, irregulars, mnemonic,
  pitfalls, contrast, dialogue example), research sources, per-topic
  workflow, effort estimate.

  DataLoader.swift — courseDataVersion 7 → 8 so existing installs
  re-seed the new body on next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:33:09 -05:00
Trey T a416233a2d Books — read-aloud mode with active-word highlight and tap-to-define
Adds a TTS read-along to the book reader. Tap the play button in the
toolbar; AVSpeechSynthesizer reads the chapter paragraph-by-paragraph
with the current word highlighted in yellow, auto-scrolling the active
paragraph to centre. Tap any word during read-along to pause and open
the definition sheet; reading resumes when the sheet dismisses.

Behavior per spec:
- Tap-to-define interrupts the synth (pauseSpeaking at: .immediate) and
  resumes on sheet dismiss.
- Voice picker sheet (waveform.circle toolbar button) lists installed
  Spanish voices grouped by Premium / Enhanced / Default quality, with
  a "Download more voices…" row that opens iOS Settings (no public
  deep-link to Accessibility → Spoken Content exists; the footer spells
  out the path).
- Speed picker (Slow / Normal / Fast) drives AVSpeechUtterance.rate.
- Stops at chapter end, no auto-advance to the next chapter.
- Vocabulary lines shaped `palabra = meaning` are skipped — the synth
  would otherwise say "palabra equals meaning" and they're reference
  material, not prose.

Audio session uses .playback + .spokenAudio mode and is properly
deactivated with .notifyOthersOnDeactivation on stop() so music apps
resume cleanly after reading ends.

Voice/rate persisted via @AppStorage; controller picks them up
onAppear and writes back through Bindings the picker mutates.

Word-index space in BookSpeechController.wordRanges(in:) matches
BookReaderView's split(separator: " ") rendering exactly — both split
on ASCII U+0020 only, so willSpeakRange callbacks resolve to the right
visible word.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:42:09 -05:00
Trey T 70d8299df8 Books — robust bundle lookup so ad-hoc device installs always seed
bundledBookJSONURLs() was relying solely on
Bundle.urls(forResourcesWithExtension:subdirectory:), the directory-
enumeration API. That API is observed to return empty in some on-device
configurations even when the resource is present, which would cause
seedBooks() to silently no-op and leave BookLibraryView showing "No
Books" despite the JSON being bundled correctly.

Switched to the same pattern textbook seeding uses: explicit per-slug
Bundle.url(forResource:withExtension:) with a bundleURL.appending-
PathComponent fallback. Directory enumeration is kept as a secondary so
future books bundled without code changes still get picked up.

Also added diagnostic prints to refreshBooksDataIfNeeded and the URL
discovery step so device logs reveal what happened if seeding still
falls through.

Bumped bookDataVersion to 3 so existing installs re-trigger
refreshBooksDataIfNeeded → seedBooks on next launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:36:16 -05:00
Trey T 51067e23fd Fix Books navigation — tapping a book no longer re-pushes the library
BookLibraryView is itself pushed from PracticeView's NavigationStack,
so the .navigationDestination(for: Book.self) it declared was a
non-root registration. Combined with NavigationLink(value: book), that
resolved the push to *both* the destination handler and the closure
that produced BookLibraryView originally — pushing the chapter list
underneath, then re-pushing the library on top. Hitting back popped
the library and revealed the chapter list, in the wrong order.

Switched both Library→ChapterList and ChapterList→Reader to closure-
based NavigationLinks. Destinations attach directly to the link, no
type-keyed registry involved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:18:04 -05:00
Trey T 05a367fdbe Books — capture <li> vocab bullets the extractor was silently dropping
extract_epub.py was walking <p> only, but every "Vocabulario" section in
the Olly Richards EPUB lives inside <ul><li>...</li></ul>. That meant
the heading made it through but the entries didn't — 680 vocab lines
across 24 sections in this book were missing from the bundled JSON.

Audit (text-node owner by closest block ancestor) confirmed <li> is the
only silent drop: 5,260 nodes in <p>, 1,960 in <li>, 0 anywhere else.
No <h1>-<h6>, tables, or blockquotes in this EPUB at all.

Fix: walk find_all(["p", "li"]) in document order so bullet entries
slot in right after their "Vocabulario" / list heading. Re-extracted
(2,646 → 3,326 paragraphs), re-translated all 118 jobs in parallel
Claude Code subagents. translate_chapters.py prompt template now tells
subagents to keep bilingual `palabra = meaning` lines verbatim — both
sides already coexist on the line.

Bumped bookDataVersion to 2 so refreshBooksDataIfNeeded re-seeds.
Verified in simulator: all 13 chapter row sizes grew (e.g. ch6
18,295→20,951 chars).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:10:34 -05:00
Trey T 09e49bda2c Add Books — read EPUB-imported books in Practice with tap-to-define
New "Books" row in the Practice tab opens a library of bundled bilingual
books. Each chapter renders Spanish paragraph-by-paragraph; tap any
word for a definition sheet (DictionaryService with on-device AI
fallback), or toggle the toolbar button to swap to the pre-computed
English translation inline.

Local-only Book + BookChapter SwiftData models added to the local
container schema (reset version bumped to 5). DataLoader.seedBooks
walks the bundle for `book_*.json` resources, so future books drop in
without touching app code — just bundle a new JSON and bump
bookDataVersion.

First book: Olly Richards' "Spanish Short Stories For Beginners
Vol 2" — 13 chapters, 2,646 paragraphs, bilingual.

Scripts/books/ is the repeatable pipeline for future EPUBs:
extract_epub.py → translate_chapters.py (per-chapter resumable jobs) →
bundle_book.py. Translation is done by parallel Claude Code subagents
reading per-job input files and writing output files — no API key
required, matching the pattern used for the textbook vocab vision
pass. See Scripts/books/README.md for the full how-to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:21:44 -05:00
Trey T ade091f108 Add Extra Study — star cards during review, study just the marked ones
Per-week "Extra Study (N)" row appears at the end of each LanGo week
section when at least one card is marked. Cards are marked from inside
VocabFlashcardView via a star next to the speaker on reveal. Marks are
keyed by the same SHA256 hash CourseReviewCard uses, so a mark and its
SRS state describe the same logical card.

ExtraStudyMark is CloudKit-synced (private DB), with uniqueness enforced
by fetch-or-create on id since CloudKit forbids @Attribute(.unique).
Skipped for textbook courses: DeckStudyView nils out the mark context
when the deck's courseName matches a TextbookChapter, and CourseView
hides the row when the active course is a textbook — so there are no
orphan marks the user can't reach.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:38:39 -05:00
Trey T 05a0cc0d17 Issue #32 cleanup — drop the last 5 mis-oriented vocab pairs
Two small fixes after the LLM-vision pass:

1. merge_pdf_into_book.py — when the LLM classifies an image as 'hybrid'
   but extracts zero pairs (e.g., a conjugation table whose only English
   text is on the section header that was excluded by the prompt rules),
   respect that decision instead of falling through to the bbox/heuristic
   pipeline. Previously: 1 chapter-2 estar conjugation table generated
   4 bad pairs from the heuristic fallback.

2. fix_vocab.py language_score — recognize Spanish present-perfect
   ('he tenido', 'He andado por este pueblo') as Spanish. The classifier
   was treating the auxiliary 'he'/'has'/'ha' as English subject pronouns,
   producing false-positive mis-orientation flags on 4 chapter-15/20/23
   present-perfect example tables.

Result: mis-oriented vocab pairs across the book go from 5 → 0.
textbookDataVersion bumped to 14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:52:53 -05:00
Trey T f368c24ad6 Fixes #32 — LLM vision pass for vocab pairs, fixes scrambled English/Spanish
The bbox-OCR pipeline mis-paired ~114 vocab tables across the book — the
chapter 7 "Other Idioms" image (issue #32) being the most visible.
Three failure modes were collapsing the data:
  1) classifier blind to subject pronouns ("yo", "I", etc.)
  2) right-then-left OCR reads on 2-col tables
  3) Y-cluster drift on multi-line cells in 4-col layouts

Replaced the entire vocab-extraction tier with a Claude vision pass over
all 931 vocab images. Output is keyed by image with three classifications:
  - pair_table       (extract all Spanish↔English pairs)
  - reference_only   (Spanish-only conjugation tables — no pairs, UI shows
                      the flat OCR lines as a reference list instead)
  - hybrid           (some header pairs + reference content beneath; only
                      the genuine pairs become cards)

merge_pdf_into_book.py now picks pair source by priority:
  llm-vision → bounding-box OCR → block-alternation heuristic.

Numbers (across the whole book):
  - mis-oriented tables: 114 → 5
  - quarantined cards:   250 → 2
  - extracted pairs:     2832 → 4569

textbookDataVersion bumped to 13. Per-batch agent outputs gitignored
under Conjuga/Scripts/textbook/paired_vocab_llm/ — only the merged
paired_vocab_llm.json (also gitignored) is needed to rebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:48:04 -05:00
Trey T 90aea92fba Fix Full Table eligibility — accept ordinary verbs, reject orto
The eligibility filter required every form's regularity tag to equal
"regular", but the data uses four labels:
  - regular   (179 forms — curated paradigm verbs)
  - ordinary  (50,992 forms — pattern-following verbs like hablar, comer)
  - irregular (8,653)
  - orto      (176 — orthographic spelling changes like busqué)

Result was a 27-combo eligible pool, ~26 of which were -ir verbs in
present tense — every Full Table prompt landed on the same handful of
verbs.

Pulled the rule into a SharedModels function (FullTableEligibility) so
it's testable in isolation. Accepts "regular" + "ordinary" (both mean
"follows the pattern"); rejects "irregular" and "orto". 9 unit tests
cover the matrix including edge cases (incomplete forms, mixed labels,
unknown values).

PracticeSessionService.makePromptIfFullyRegular now delegates to
FullTableEligibility, sorting forms by personIndex so the regularity
array lines up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:26:34 -05:00
Trey t dce2cc1f51 Make Full Table level-agnostic, fix the streak system end-to-end
Full Table (issue from chat): drop the level filter — Full Table tests
regular conjugation patterns, not vocabulary recognition, so restricting
to Basic-level verbs collapsed the eligible pool to two combos
(vivir present, ir future). Pool now draws from all 1,750 verbs. Random
sampling first; if 40 attempts fail we fall through to a deterministic
shuffled scan that guarantees finding any eligible (verb, tense) combo
when one exists. Returning nil now happens only when the user's filters
genuinely produce zero eligible prompts. The view replaces its silent
blank screen with a ContentUnavailableView pointing at the settings
that need adjusting. FeatureReferenceView documents the level exception.

Streak (issue #31 follow-up): activity recording was scoped to flashcard
and Full Table reviews only, so spending an hour on textbook work,
guides, videos, or AI chat could break a "streak" that the dashboard
kept displaying as if it were intact. Three fixes:

  1. Extract ReviewStore.recordActivity(context:) — a streak-only entry
     point that any user-initiated learning action can call.
  2. Add UserProgress.validateStreakIfStale(today:context:) — resets a
     broken currentStreak to 0 immediately, called from app launch and
     dashboard appear so the displayed number is never a lie.
  3. DailyLog formatter pins POSIX locale + current timezone so the
     yyyy-MM-dd strings can't drift across locales.

Wired recordActivity into every previously-silent learning action: chat
send, story-quiz completion, textbook exercise submit, grammar exercise
completion, course-deck study finish, week test / checkpoint save,
listening + pronunciation check, cloze quiz completion, lyrics word
lookup, video stream / play / download success, sentence-builder check,
and course-vocab SRS rate (which was bypassing ReviewStore entirely).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:24:27 -05:00
Trey T 06b47d37cf Fix video action button wrapping + bundle textbook seed JSONs
- VideoActionsView: stack icon over title via a custom label style so
  the Stream/Download/Play row fits iPhone width without wrapping
  mid-word ("Stre am", "Down load").
- project.yml: add textbook_data.json and textbook_vocab.json to the
  Conjuga target's Copy Bundle Resources. They were missing, so
  DataLoader.seedTextbookData early-returned every launch and the
  textbook never appeared in the Book tab. Regenerated Conjuga.xcodeproj.
2026-04-24 17:32:36 -05:00
Trey t f993bfbb96 Refresh README with recent features
Add YouTube video integration, reflexive-verb list + practice-pool
filters, verb-detail example sentences, lyrics word lookup. Bump iOS /
Xcode requirements to 26. Correct grammar-note count and document the
YouTubeKit + AVFoundation muxing architecture plus the video-curation
script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:33:41 -05:00
Trey t fcb907718a Re-curate videos toward preferred channels
Swap 24 tense-guide / grammar-note videos to The Language Tutor's
numbered lesson series where a matching lesson exists, filling the two
remaining gaps (ind_preterito_anterior → Lesson 65, estar-gerund-
progressive → Lesson 113). All 32 TLT picks preserved on this pass.

For the non-TLT slots, prefer BaseLang's beginner lesson series where a
topic-specific video exists: ser-vs-estar, preterite-vs-imperfect,
subjunctive-triggers, object-pronouns, conditional-if-clauses,
tener-expressions, future-vs-ir-a, possessive-adjectives,
irregular-yo-verbs, and stem-changing-verbs.

Retire both Tell Me In Spanish videos (personal-a → castellano4U,
types-of-irregular-verbs → Master IRREGULAR VERBS Complete Lesson).

Generator header note clarifies that "not available on this app" rows
are a transient yt-dlp extraction limit — videos still play when tapped
in the app via the Stream button, which opens youtube.com externally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:24:53 -05:00
Trey t 9c7033d1b4 Add curated-videos markdown report + generator script
youtube_videos.md lists every entry in youtube_videos.json with its
tense-guide / grammar-note id, title, channel, upload date, duration,
views, and likes (where public). Also flags the two topics with no
curated video so the gap is auditable in one place.

generate_videos_markdown.py queries yt-dlp in parallel for each unique
videoId and writes the markdown. Rerun when curation changes. One
current entry (saber-vs-conocer → j87i7MVCvIE) is now marked Private
Video — needs re-curation as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:07:41 -05:00
Trey t 0a099c3fc9 Fixes #30 — Downloaded videos include audio via adaptive-stream muxing
YouTubeKit returns stream URLs only; its own README punts on the
progressive-vs-adaptive problem. Most modern YouTube videos above 360p
serve video and audio as separate DASH tracks, so the previous "any MP4"
fallback silently grabbed a video-only track and the downloaded file had
no audio.

VideoDownloadService now resolves a DownloadPlan up front. Progressive
MP4 (when one exists) still streams straight to the final destination.
Otherwise the service downloads the best MP4 video track + best M4A
audio track to temp files, then combines them into a single MP4 via
AVMutableComposition + AVAssetExportSession. Passthrough preset first
(lossless container rewrite) with a fallback to highestQuality if the
codec combination requires re-encoding. Temp files are cleaned up on
both success and failure.

DownloadStatus replaces the bare Double progress so the UI can show
per-phase labels (Video %, Audio %, Finalizing…). Muxing progress is
rendered as an indeterminate spinner since AVAssetExportSession's
progress property doesn't cross actor boundaries cleanly.

Also retires the deprecated Stream.subtype comparison in favor of
Stream.fileExtension, matching YouTubeKit's current API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:00:16 -05:00
Trey t 57f945a4d3 Fixes #29 — Delete downloaded video inline on the guide detail view
The middle button in the Stream/Download/Play row now cycles through the
full download lifecycle instead of ending at a disabled "Downloaded"
checkmark. Once a video is on disk the button becomes a red, destructive
"Delete" with a trash icon; tapping presents a confirmation dialog, and
confirming removes the file + SwiftData row, flipping the button back to
"Download" and disabling Play.

Settings → Downloaded Videos retains the swipe-delete and "Delete all"
affordances for bulk management.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 06:46:51 -05:00
Trey t 5777a210cd Fixes #21 — Curated YouTube videos per guide + grammar item
Each of the 56 tense guides and grammar notes gets a curated YouTube video
attached (54 with picks, 2 silent nulls on rare / hard-to-find topics).
Users can stream in YouTube/Safari, download via YouTubeKit for offline
viewing, or play the local MP4 full-screen via AVPlayer.

YouTubeVideoStore loads the bundled youtube_videos.json at launch and serves
lookups by tense id or grammar note id. VideoDownloadService resolves the
best progressive MP4 stream off the main actor (YouTubeKit isn't Sendable),
writes to documents/videos/<videoId>.mp4, and records a DownloadedVideo row
in the local SwiftData container so the app knows what's on disk across
launches.

VideoActionsButtonRow is the unified UI for both detail views: three large
buttons — Stream (red, always enabled), Download (blue, disabled while in
flight and after completion, shows progress), Play (green, enabled only
when downloaded). Full-screen cover on tap. Settings gains a Downloaded
Videos list with swipe-delete, total-size summary, and a 500 MB warning.

Local store reset version bumped to 4 for the new DownloadedVideo schema.

Known fragility: YouTubeKit scrapes YouTube's private stream API and will
break when YouTube changes their internal format. Streaming keeps working
regardless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:51:19 -05:00
Trey t 98badc98ad Fixes #28 — Curated reflexive verb list on detail + practice filter
Bundles the 100 most common reflexive verbs from spanishwithdaniel.com as a
canonical list and wires it through the UI. Compound list entries (recibirse
/ graduarse, equivocarse / confundirse) are split. Trailing prepositions and
set-phrase completions are captured as usageHint (e.g. acordarse "de",
ponerse "de acuerdo").

ReflexiveVerbStore loads the JSON at launch and exposes lookups by base
infinitive, both via @Environment for SwiftUI and a static shared instance
for services. Verbs whose bare infinitive isn't in the list skip the UI
treatment silently.

VerbDetailView shows a new Reflexive section with the reflexive infinitive,
usage hint, and English meaning when there is a match. VerbListView gains a
"Reflexive verbs only" filter alongside the existing Level and Irregularity
filters. Settings adds the same flag so it also constrains the practice
pool; PracticeSessionService applies the reflexive filter in all six pick
paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:56:04 -05:00
Trey t 4093b5a7f3 Fixes #27 — AI-generated example sentences on verb detail
Tapping a verb now shows six example sentences beneath the conjugation table,
one per core tense (Present, Preterite, Imperfect, Future, Present Subjunctive,
Imperative). Each example renders the tense label, Spanish sentence, and
English translation in the DeckStudyView style.

Generation uses Foundation Models with a @Generable schema that pins each
response to the requested tenseId and forces tú/nosotros subjects for
imperatives. Results are cached as JSON in the Caches directory keyed by
verb id (DictionaryService pattern); cache misses regenerate on demand.

Devices without Apple Intelligence see an inline notice instead of the
loading state. No network dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:57:32 -05:00
Trey t 5d3accb2c0 Fixes #26 — Multi-select levels and irregular-type practice filter
UserProgress gains selectedLevelsBlob and enabledIrregularCategoriesBlob
(mirrors the existing tense-blob pattern). The multi-level setter keeps the
legacy selectedLevel String in sync with the highest-ranked selection, so
widget sync, AI scenarios, and achievement checks keep working unchanged.
Legacy single-level users are migrated on first read.

Settings replaces the level Picker with per-level toggles and adds an
Irregular Types section with three toggles. Practice pool is the literal
intersection: empty levels means zero results, empty irregular categories
means no irregularity constraint.

Pure filter logic lives in SharedModels (PracticeFilter, VerbLevel.highest)
and is covered by 20 Swift Testing cases. ReferenceStore delegates so the
intersection behavior is unit-tested without a ModelContainer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:36:25 -05:00
Trey t 3d8cbccc4e Fixes #25 — Long-press lyric words for definition and tense
Tokenize Spanish lyric lines into a flow layout of underlined, long-pressable
words. Long-press (0.35s) opens a sheet with base form, English, part of
speech, and a Tense · person row for verbs. Unknown words silently no-op.
English gloss lines remain untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:18:15 -05:00