Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5d26805
Differentiate lessons within a curriculum unit
arqo123 Aug 19, 2026
7e63c62
Make the voice end-of-turn pause longer and configurable
arqo123 Aug 19, 2026
a1a283a
Let learners decline a placement question instead of guessing
arqo123 Aug 19, 2026
0469a99
Configure Anthropic output tokens and handle truncation
artcc Aug 20, 2026
68a8c6b
Merge pull request #303 from artcc/feature/issue-297
artcc Aug 20, 2026
37a4f1f
Make the placement gap answer readable and tappable
arqo123 Aug 24, 2026
fdb685f
Wrap the speech-pause selector on narrow screens
arqo123 Aug 24, 2026
05e428b
Address review of the sibling-lesson context
arqo123 Aug 24, 2026
cd63a06
Describe the sibling-summary caps precisely in the prompt spec
arqo123 Aug 24, 2026
34e2030
Merge branch 'develop' into feat/lesson-variety-within-unit
artcc Aug 24, 2026
95c89d1
Merge branch 'develop' into fix/configurable-end-of-speech-window
artcc Aug 24, 2026
5e3645c
Merge branch 'develop' into feat/placement-test-dont-know
artcc Aug 24, 2026
06655f7
Merge pull request #300 from arqo123/feat/lesson-variety-within-unit
artcc Aug 24, 2026
4bf0d8c
Merge branch 'develop' into feat/placement-test-dont-know
artcc Aug 24, 2026
75a0753
Merge branch 'develop' into fix/configurable-end-of-speech-window
artcc Aug 24, 2026
32048f8
Merge pull request #301 from arqo123/fix/configurable-end-of-speech-w…
artcc Aug 24, 2026
c20b40a
Merge branch 'develop' into feat/placement-test-dont-know
artcc Aug 24, 2026
6255ef4
Add speaking lesson generation support
artcc Aug 24, 2026
4b815bd
Merge pull request #306 from artcc/feature/issue-305
artcc Aug 24, 2026
d4c9aae
Merge branch 'develop' into feat/placement-test-dont-know
artcc Aug 24, 2026
2169fe4
Merge pull request #302 from arqo123/feat/placement-test-dont-know
artcc Aug 24, 2026
cd7485b
Propagate study-plan context through STT and flashcard flows
artcc Aug 24, 2026
8cef453
Merge pull request #307 from artcc/feature/issue-304
artcc Aug 24, 2026
6a37d5a
Hide admin subscription UI when Stripe is disabled
artcc Aug 26, 2026
917ae85
Merge pull request #308 from artcc/feature/settings
artcc Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ OPENAI_MODEL=gpt-5.4-mini
# Anthropic (if LLM_PROVIDER=anthropic)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-4-5-haiku
ANTHROPIC_MAX_TOKENS=8192

# DeepSeek (if LLM_PROVIDER=deepseek)
DEEPSEEK_API_KEY=
Expand Down
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ OPENAI_MODEL=gpt-5.4-mini
# Anthropic (if LLM_PROVIDER=anthropic)
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-4-5-haiku
ANTHROPIC_MAX_TOKENS=8192

# DeepSeek (if LLM_PROVIDER=deepseek)
DEEPSEEK_API_KEY=
Expand Down
6 changes: 3 additions & 3 deletions .opencode/skills/pre-push/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Fixes what can be fixed automatically: Python lint + format, JS/TS lint + format
source .venv/bin/activate && cd backend && pytest -v
```

973 backend tests, >= 70% backend coverage required. Current backend coverage is 85.03% last measured. SQLite in-memory, no Docker needed.
1019 backend tests, >= 70% backend coverage required. Current backend coverage is 85.56% last measured. SQLite in-memory, no Docker needed.

Use a command timeout of at least 600 seconds for the full backend suite; the current run takes about 4 minutes and can exceed shorter 120-second tool defaults.

Expand All @@ -40,10 +40,10 @@ cd frontend && npm run lint && npx tsc --noEmit && npm run test:run
- Step 0: `./scripts/format.sh` runs the canonical formatter from the repository root.
- Step 0: backend formatting uses `ruff --fix` and `black` from `backend/pyproject.toml`.
- Step 0: frontend formatting uses `eslint --fix` plus `prettier --write` from `frontend/.prettierrc` and `frontend/eslint.config.mjs`.
- Step 1: `pytest` runs 973 backend tests with SQLite in-memory.
- Step 1: `pytest` runs 1019 backend tests with SQLite in-memory.
- Step 2: `eslint` verifies no remaining JS/TS lint errors.
- Step 2: `tsc --noEmit` runs TypeScript type checking.
- Step 2: `vitest` runs 446 frontend tests; frontend coverage is not configured.
- Step 2: `vitest` runs 474 frontend tests; frontend coverage is not configured.

**Note:** CI does not run ruff/black checks — only pytest, eslint, tsc, and vitest. Backend lint (ruff/black) is kept in step 0 as auto-format only.

Expand Down
2 changes: 1 addition & 1 deletion .opencode/skills/run-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Options:
cd frontend && npm run test:run
```

440 tests covering stores, components, hooks, app pages, lib, i18n, middleware, review UI, review prompt triggers, billing UI, freemium state, memory management, SSE parsing, chat stream resets, toasts, and API interceptor. Frontend coverage is not configured/reported:
474 tests covering stores, components, hooks, app pages, lib, i18n, middleware, review UI, review prompt triggers, billing UI, freemium state, memory management, SSE parsing, chat stream resets, toasts, and API interceptor. Frontend coverage is not configured/reported:

- `lib/api.ts` — auth interceptor, 401 refresh, retry
- `store/auth.ts` — isSubscribed(), logout
Expand Down
16 changes: 12 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

## Project state

**v1.8.40 — Localized listening lesson labels.** Phase 1 (platform), Phase 1+ (resources hub), Phase 2 (TTS/STT), Phase 3 (voice conversation), Phase 4 (multi-language support), Phase 5 (Stripe subscriptions), Phase 6 (Listening exercises), Phase 7 (Reading exercises), Phase 8 (Feedback board), Phase 9 (LLM Memory), Phase 10 (Multi-Language), and Phase 11 (User Reviews) are complete. Administrators can compose a dashboard announcement in any supported UI language, generate and edit all ten translations, save it active or inactive, and publish revisions that authenticated users dismiss persistently per account. Memories are global per user across learning languages, can be saved by Lingu through one native tool round in text or voice, and can be manually added, listed, deleted, or cleared by every authenticated user. Deleting a learning language preserves memories by setting nullable study-plan provenance to `NULL`. Paginated application lists show 10 results per page; Listening and Reading histories expose every attempt through the shared pagination controls, and Feedback uses deterministic tie-breaking between pages. The public landing review carousel requests up to 100 approved positive reviews and remains unpaginated. Public pages use the dot-grid background, while the authenticated application and its loading states use a solid background. The authenticated Feedback section shows per-user unread thread counters in the sidebar, red unread labels on specific feedback list items, and a gold `ADMIN` badge beside administrator-authored suggestions, bug reports, and replies without sending comment emails. Administration exposes maintenance mode and dashboard announcement management in the System section. Japanese (`ja-JP`), Korean (`ko-KR`), and Mainland Chinese (`zh-CN`) have backend curriculum, grammar, vocabulary, phrasebook, and assessment data. Static grammar, phrasebook, vocabulary resources, lessons, newly generated lesson exercises, exercise hints, and newly generated lesson vocabulary include native-language learning support. Email verification and password reset are also included. Unsubscribed hosted users get one one-time post-assessment voice conversation demo, configurable via `ASSESSMENT_VOICE_TRIAL_DURATION_SECONDS` and defaulting to 5 minutes. Voice conversations are persisted as text transcripts alongside chat conversations. The AI tutor persona is named Lingu. The repo contains `backend/`, `frontend/`, `docker-compose.yml`, `.env.example`, and CI/CD via GitHub Actions. See [CHANGELOG.md](CHANGELOG.md) for the full version history.
**v1.8.45 — Speaking lesson generation support.** Phase 1 (platform), Phase 1+ (resources hub), Phase 2 (TTS/STT), Phase 3 (voice conversation), Phase 4 (multi-language support), Phase 5 (Stripe subscriptions), Phase 6 (Listening exercises), Phase 7 (Reading exercises), Phase 8 (Feedback board), Phase 9 (LLM Memory), Phase 10 (Multi-Language), and Phase 11 (User Reviews) are complete. Administrators can compose a dashboard announcement in any supported UI language, generate and edit all ten translations, save it active or inactive, and publish revisions that authenticated users dismiss persistently per account. Memories are global per user across learning languages, can be saved by Lingu through one native tool round in text or voice, and can be manually added, listed, deleted, or cleared by every authenticated user. Deleting a learning language preserves memories by setting nullable study-plan provenance to `NULL`. Paginated application lists show 10 results per page; Listening and Reading histories expose every attempt through the shared pagination controls, and Feedback uses deterministic tie-breaking between pages. The public landing review carousel requests up to 100 approved positive reviews and remains unpaginated. Public pages use the dot-grid background, while the authenticated application and its loading states use a solid background. The authenticated Feedback section shows per-user unread thread counters in the sidebar, red unread labels on specific feedback list items, and a gold `ADMIN` badge beside administrator-authored suggestions, bug reports, and replies without sending comment emails. Administration exposes maintenance mode and dashboard announcement management in the System section. Japanese (`ja-JP`), Korean (`ko-KR`), and Mainland Chinese (`zh-CN`) have backend curriculum, grammar, vocabulary, phrasebook, and assessment data. Static grammar, phrasebook, vocabulary resources, lessons, newly generated lesson exercises, exercise hints, and newly generated lesson vocabulary include native-language learning support. Email verification and password reset are also included. Unsubscribed hosted users get one one-time post-assessment voice conversation demo, configurable via `ASSESSMENT_VOICE_TRIAL_DURATION_SECONDS` and defaulting to 5 minutes. Voice conversations are persisted as text transcripts alongside chat conversations. The AI tutor persona is named Lingu. The repo contains `backend/`, `frontend/`, `docker-compose.yml`, `.env.example`, and CI/CD via GitHub Actions. See [CHANGELOG.md](CHANGELOG.md) for the full version history.

## Architecture at a glance

Administrative subscription UI follows the public Stripe runtime flag. When Stripe is disabled, the overview hides paid-access and past-due subscription signals, the user list hides and ignores subscription filtering and values, and user detail hides subscription status and override controls; quota administration remains available.

Pronunciation exercises and flashcard speaking mode capture their resource-owned `study_plan_id` when recording starts and include it in every STT upload. The frontend stops late microphone streams, propagates request cancellation through its STT proxy, and serializes flashcard reviews while voice-result handling is pending. The backend verifies plan ownership, derives the target language from that plan, converts it to the provider's ISO code, and requires every STT service call to declare a language explicitly; there is no implicit English fallback. Generated flashcards likewise derive their target language from the active persisted plan rather than client state, and reviews credit progress to the persisted card plan rather than whichever language is currently active.

Active Reading and Listening exercises let users select and save one word from question prompts through the shared flashcard lookup flow; answer options are not selectable vocabulary surfaces.

My Plan unit drawers offer Start for the current lesson, Resume for skipped pending lessons, and read-only Review for completed lessons without awarding progress again. Drawer actions share the solid primary button treatment used by the plan overview.
Expand All @@ -14,18 +18,22 @@ Lesson completion locks the lesson row, commits completion/progress/competencies

Automatic LLM memory is best-effort: text and voice continue without user-visible memory errors, only confirmed saves emit the memory toast, at most one memory tool call executes per turn, and explicit tool incompatibility is remembered only for the current voice WebSocket session. Tool-free retries omit memory-tool instructions, replace rather than append to any invalid partial response, and reject an empty fallback instead of persisting it as a successful answer.

Anthropic requests use the deployment-configurable `ANTHROPIC_MAX_TOKENS` output budget, defaulting to 8192, and non-streaming truncation is reported explicitly before structured JSON parsing.

Every lesson type used by the static curricula has an explicit generation policy. Mainland Chinese B2-C2 `speaking` lessons use oral-production guidance and a 30% grammar-exercise minimum instead of the generic 70% fallback.

The dashboard announcement is a global singleton. Public config exposes only active translations and the server revision; authenticated dismissal stores that revision on the user, and content or source-language edits increment it so a changed announcement reappears while active-state-only changes do not.

Monorepo: `backend/` (Python 3.14 FastAPI) + `frontend/` (Next.js 16 App Router) deployed via Docker Compose with PostgreSQL 16 and Redis 7. The backend proxies all external services (Ollama, Kokoro, Whisper) — the frontend never calls them directly.

## Key constraints

- **Users can learn multiple languages simultaneously** — each language gets an isolated study plan, progress, flashcards, conversations, and competencies. Supported target languages: `en-US`, `en-GB`, `es-ES`, `it-IT`, `pt-PT`, `de-DE`, `fr-FR`, `ja-JP`, `ko-KR`, `zh-CN`. User's native language (asked at registration) is used for flashcard translations, tutor feedback, lesson and exercise `native_explanation` content, and cached native-language help in static grammar, phrasebook, and vocabulary resources.
- **User settings and memories are global (per user), not per language.** Profile (avatar, bio, display name, email, password, native language, UI locale), conversation limits (max duration, inactivity timeout, daily/weekly minutes, weekly sessions), token quota, subscription, and LLM memories are stored on the `users` table or keyed by `user_id` only — they do not change when switching the active study language. The nullable `study_plan_id` column on `memories` is creation provenance only; all text, voice, and Settings retrieval is global by `user_id`, and deleting a language preserves linked memories through `SET NULL`. Authenticated memory management is not subscription-gated.
- **User settings and memories are global (per user), not per language.** Profile (avatar, bio, display name, email, password, native language, UI locale), conversation limits (max duration, inactivity timeout, daily/weekly minutes, weekly sessions), the voice end-of-turn pause, token quota, subscription, and LLM memories are stored on the `users` table or keyed by `user_id` only — they do not change when switching the active study language. The nullable `study_plan_id` column on `memories` is creation provenance only; all text, voice, and Settings retrieval is global by `user_id`, and deleting a language preserves linked memories through `SET NULL`. Authenticated memory management is not subscription-gated.
- **First registered user becomes admin automatically** when `FIRST_USER_IS_ADMIN=true` (default).
- **Registration gating**: `ALLOW_REGISTRATION=false` blocks public signups; admin creates users or generates single-use invite links (48h expiry in Redis).
- **Ollama should run on the host for GPU access**, accessed via `host.docker.internal:11434`. On Linux, the backend service needs `extra_hosts: ["host.docker.internal:host-gateway"]`.
- **Default target language is `en-GB`** — all fallback defaults across backend (service params, Query params, model column defaults, chat context, onboarding form) and frontend (`DEFAULT_TARGET_LANGUAGE` in `target-languages.ts`) use `en-GB`. `en-US` remains a supported language but is never used as a fallback default.
- **Default target language is `en-GB`** — all valid target-language fallbacks across backend (service params, Query params, model column defaults, chat context, onboarding form) and frontend (`DEFAULT_TARGET_LANGUAGE` in `target-languages.ts`) use `en-GB`. `en-US` remains a supported language but is never used as a fallback default. Resource-owned STT calls are intentionally stricter: they require a user-owned study plan and never fall back to any language.

## Documentation maintenance (MANDATORY)

Expand Down Expand Up @@ -62,7 +70,7 @@ Files most commonly affected by code changes:
These describe what was built — they are the reference documentation:

- `specs/architecture.instructions.md` — Repository structure, data flows, auth design, test summary
- `specs/architecture-backend.instructions.md` — Backend architecture: models (21), services (20), routers (23), schemas (15), env vars (56), Python code standards
- `specs/architecture-backend.instructions.md` — Backend architecture: models (21), services (20), routers (23), schemas (15), env vars (57), Python code standards
- `specs/architecture-frontend.instructions.md` — Frontend architecture: pages, components, stores (6), lib modules (9), TypeScript code standards
- `specs/add-target-language.instructions.md` — Canonical checklist for adding new target languages, based on the British English (`en-GB`) data package structure and current dispatchers
- `specs/database-models.instructions.md` — **22 SQLAlchemy ORM models**: full schema details, relationships, constraints, business rules
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.8.45] - 2026-08-26

### Added

- **Configurable Anthropic output budget**: `ANTHROPIC_MAX_TOKENS` controls the maximum output tokens per request and defaults to 8192, including in production and development Compose deployments.

### Fixed

- **Stripe-disabled administration**: self-hosted deployments now hide subscription metrics and overdue alerts from the admin overview, hide and ignore subscription filters and values in the user list, and omit subscription status and override controls from user detail while keeping quota management available.
- **Target-language speech transcription**: pronunciation exercises and flashcard speaking mode now capture and send their owning study plan with each recording, so the backend validates that immutable context and passes the plan's ISO language code to STT instead of silently treating every recording as English. Late microphone permissions and in-flight proxy requests are cleaned up safely, flashcard reviews stay serialized until voice-result handling completes, and each review credits progress to the card's owning plan even if the active language changed. Flashcard generation also derives its language from the active persisted plan rather than a client-supplied language value.
- **Anthropic lesson generation truncation**: Anthropic no longer uses the previous 4096-token output ceiling that could cut bilingual lesson JSON mid-response, and `stop_reason=max_tokens` now produces an explicit `LLMResponseError` retaining the partial output instead of a misleading JSON decode failure.
- **Lesson variety within curriculum units**: New lessons receive a capped summary of already generated siblings and type-specific guidance for grammar, vocabulary, reading, writing, listening, and review, reducing repeated explanations, examples, vocabulary, and common traps while keeping the exercise grammar ratio aligned with each lesson's focus.
- **Voice hesitation cut-offs**: Automatic end-of-turn silence windows are longer at every CEFR level, and learners can now choose automatic, 1-second, 2-second, or 3-second pauses from Conversation settings before a spoken turn is submitted.
- **Speaking lesson generation**: Mainland Chinese B2-C2 `speaking` curriculum slots now use dedicated oral-production guidance and a production-compatible grammar exercise ratio instead of the generic lesson fallback. Curriculum lesson types are validated against the prompt policies to prevent future mismatches.

## [1.8.40] - 2026-08-17

### Changed
Expand Down
Loading