# French Coach — Custom UI API contract All endpoints are mounted on the same Gradio app object (`gr.Server` / FastAPI instance) returned by `demo.launch(prevent_thread_lock=True)` in `app_custom.py`, under the `/api/...` prefix. The built React app (`frontend/dist`) is served as static assets from the same server, so the whole thing is one Gradio-SDK process (no separate API host). User identity: single-user dev mode. Every endpoint operates on a fixed `USER_ID = "dev_user"` (same default `app.py` uses when not running on a Space). Hugging Face OAuth for the custom UI on the public Space is **not** wired up yet — flagged as a follow-up, not a blocker (does not affect Gradio-SDK / model-size eligibility). All responses are JSON unless noted. Errors: `{"error": ""}` with a non-200 status code. --- ## Lessons (Notebook + Lessons browser) ### `GET /api/lessons` List saved lesson pages (excludes `page_type == "resource"`), newest first. Response: ```json { "lessons": [ {"id": "uuid", "title": "Class 4 — Le passé composé", "date": "2026-04-30", "category": "Grammar", "page_type": "lesson", "preview": "Aujourd'hui on a vu…"} ] } ``` ### `GET /api/lessons/{id}` Load one page for editing. Response: ```json {"id": "uuid", "title": "Class 4", "raw_text": "Le petit chat...", "annotations": {"tokens": [...], "meanings": {...}}} ``` 404 -> `{"error": "not found"}` if missing or not owned by user. ### `POST /api/lessons` Save the current editor text as a **new** page (curator auto-titles it). Awards `saved_lesson` points. Body: `{"text": "...", "annotations": {...}}` Response: `{"id": "uuid", "title": "Auto-generated title"}` ### `PUT /api/lessons/{id}` Update an existing page's text/annotations in place (title unchanged). Body: `{"text": "...", "annotations": {...}}` Response: `{"title": "Existing title"}` ### `PATCH /api/lessons/{id}/title` Rename a page (user override of the auto title). Body: `{"title": "New title"}` Response: `{"title": "New title"}` ### `DELETE /api/lessons/{id}` Delete a page (exercises cascade). Response: `{"deleted": true}` --- ## Resources tab ### `GET /api/resources` Pages curated as `page_type == "resource"` that contain links and/or books. Response: ```json { "resources": [ {"id": "uuid", "title": "Online Resources", "links": [{"url": "https://...", "label": "TV5 Monde", "domain": "tv5monde.com"}], "books": [{"title": "Le Petit Prince", "author": "Saint-Exupéry", "note": "easy reader"}]} ] } ``` --- ## Annotation / gender coloring / word card (Notebook + Tools screens) ### `POST /api/annotate` Run spaCy annotation on arbitrary text (used by Notebook "Annotate" and the Gender Checker tool). Body: `{"text": "...", "colors_on": true}` Response: ```json {"html": "Le ...", "tokens": [{"text": "Le", "lemma": "le", "pos": "DET", "gender": "Masc", "whitespace": " "}], "meanings": {}} ``` `html` is the same gender-colored markup `nlp.render_html` already produces (with `data-token`/`data-text`/`data-gender`/`data-pos`/`data-lemma` attributes) — the React Notebook/Tools screens render it with `dangerouslySetInnerHTML` and use click-event delegation, exactly like the existing Blocks `PAGE_JS`, so gender colors + click behavior stay identical. ### `POST /api/render` Re-render cached annotations to gender-colored HTML without re-running spaCy/LLM (used when loading a saved lesson, or toggling the gender-colors checkbox, so cached `meanings` survive). Body: `{"annotations": {"tokens": [...], "meanings": {...}}, "colors_on": true}` Response: `{"html": "... ..."}` ### `POST /api/word-card` Get (and cache) the LLM meaning/grammar note for one clicked word. Awards `word_explored` points the first time a given lemma is looked up. Body: ```json {"text": "femme", "lemma": "femme", "pos": "NOUN", "gender": "Fem", "meanings": {"...cached meanings dict from annotate/lessons..."}} ``` Response: ```json {"text": "femme", "lemma": "femme", "pos": "NOUN", "gender": "Fem", "meaning": "woman", "grammar": "feminine noun", "meanings": {"femme": {"meaning": "woman", "grammar": "feminine noun"}, "...": "..."}} ``` The client merges the returned `meanings` dict back into its local annotations object (so a later "Save"/"Update" persists the cache). --- ## Chat coach ### `POST /api/chat` Non-streaming reply (the custom UI does one round trip per message instead of token-streaming). Body: ```json {"message": "Comment dit-on 'thank you'?", "history": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}], "lesson_text": "...current notebook text, used as context, optional..."} ``` Response: `{"reply": "On dit « merci »..."}` --- ## Exercises ### `POST /api/exercises/coach` **Coach Agent** (Day 3): plans a balanced 5-7 item practice set from the current lesson, grounded against the A1/A2 CEFR syllabus (`syllabus_full_a1_c2.json`), generates each item, critiques it, and regenerates anything that fails review (max 2 attempts/item). Identified concepts are upserted into `concepts` (`covered_on = today`) for the Summary tab's "next focus". Body: `{"lesson_text": "...", "page_id": "uuid (optional)"}` Response: ```json { "concepts": [{"id": "verb_etre_present", "name": "Verb: Être (Present Tense)", "cefr_level": "A1", "family": "verb_tenses"}], "exercises": [ {"type": "fill_blank", "instruction": "Fill in the blank:", "sentence_with_blank": "...", "answer": "...", "hint": "...", "explanation": "..."}, {"type": "multiple_choice", "instruction": "Choose the correct answer:", "question": "...", "options": ["...", "...", "...", "..."], "answer": "...", "explanation": "..."}, {"type": "error_detection", "instruction": "Find and fix the mistake:", "sentence": "...", "answer": "...", "explanation": "..."}, {"type": "reorder", "instruction": "Put the words in the correct order:", "words": ["...", "..."], "answer": "...", "explanation": "..."}, {"type": "translation", "instruction": "Translate to French:", "prompt": "...", "answer": "...", "explanation": "..."} ] } ``` The frontend shows `exercises` one at a time (see `Exercises.jsx` `CoachExercises`). ### `POST /api/exercises/coach/check` Check one item's answer. `fill_blank`/`multiple_choice` are checked by exact match; `error_detection`/`reorder`/`translation` are graded leniently by the LLM (accepts spelling/accent/punctuation variation). Always awards `exercise_done` points — participation, not correctness; never red/shaming. Body: `{"exercise": {...}, "answer": "..."}` Response: `{"correct": true, "feedback": "encouraging message", "answer": "model answer"}` ### `POST /api/exercises/dialogue` Start a new dialogue scene from the lesson. Body: `{"lesson_text": "..."}` Response: ```json {"dialogue": {"scene": "...", "agent_role": "...", "user_role": "...", "turns": [...]}, "replies": [], "hint": "Your turn: ...", "transcript_html": "
...
"} ``` ### `POST /api/exercises/dialogue/reply` Send the user's next line. Awards `dialogue_turn` points. Body: `{"dialogue": {...}, "replies": ["..."], "reply": "Bonjour !"}` Response: ```json {"replies": ["Bonjour !"], "transcript_html": "
...
", "hint": "Your turn: ..." , "feedback_html": "
...
"} ``` (`hint` is `"🎉 Dialogue complete! Great work!"` once finished.) ### `POST /api/exercises/visual/sample` Matched-image visual exercise (Day 4): no upload needed. Picks one of ~15 pre-generated images (`frontend/public/sample_images/`, generated once via `generate_sample_images.py`) matching the lesson's detected topic (`nlp.detect_category`), avoiding images this user has already seen (`user_image_usage`) until the set cycles. Builds 3-5 exercises (with hints) grounded in the image's hand-written description — no vision call at request time. Awards `photo_exercise` points. Body: `{"lesson_text": "..."}` Response: ```json { "image_url": "/custom/sample_images/food_dining.jpg", "topic": "Food & Dining", "html": "
...
" } ``` (`html` = `exercises.render_visual_exercises(result)`, includes a `hint` line per exercise.) ### `POST /api/exercises/pronunciation/target` Body: `{"lesson_text": "..."}` Response: ```json {"target": {"phrase": "...", "translation": "...", "tip": "..."}, "html": "
...
"} ``` ### `POST /api/exercises/pronunciation/check` Awards `pronunciation` points. Body: `{"target": {"phrase": "..."}, "transcription": "..."}` Response: `{"html": "
...
"}` --- ## Gender Checker + Translator (Tools) ### `POST /api/gender-check` Look up a single French noun. spaCy (`nlp.word_info`) gives a lemma/POS hint, but gender itself is **not** reliable from spaCy on an isolated word (no determiner context to disambiguate — e.g. "pomme" alone tags `Masc` though it's feminine), so the LLM is authoritative for gender/articles/example/pattern note. Body: `{"word": "pomme"}` Response: ```json { "word": "pomme", "lemma": "pomme", "pos": "NOUN", "gender": "Fem", "article": "la", "indefinite_article": "une", "example": "J'achète une pomme pour le goûter.", "example_translation": "I'm buying an apple for the snack.", "pattern_note": "Words ending in -e are often feminine." } ``` ### `POST /api/translate` Translate a word/phrase with alternatives and a bilingual example. If `lesson_text` is given, it's used as register/vocabulary context only. Body: `{"text": "good morning", "direction": "en_fr"|"fr_en", "lesson_text": "..."}` Response: ```json { "translation": "bonjour", "alternatives": [], "example_fr": "Bonjour, comment allez-vous ?", "example_en": "Good morning, how are you?" } ``` `example_fr`/`example_en` are always in their named language regardless of `direction` (the LLM was inconsistent about which side of `example`/ `example_translation` was French, so the schema is now language-explicit). --- ## Summary / gamification ### `GET /api/summary` Calls `gamify.try_daily_open` (idempotent per day, awards `daily_open` points once/day) then returns the encouraging daily summary, total points, today's activity stats, and A1-A2 concept progress for the dashboard. Response: ```json { "summary": "You've covered 6 concepts...", "total_points": 142, "daily_stats": { "pages_today": 1, "exercises_today": 5, "dialogue_turns": 0, "words_clicked": 3, "total_points": 142 }, "concepts": { "covered": ["Personal Subject Pronouns", "Regular -ER Verbs (Present)"], "next": "French Pronunciation & Sound System", "covered_count": 8, "total_count": 49 } } ``` --- ## Screens -> endpoints map (for Phase 3 ordering) 1. **Notebook** — `/api/lessons/{id}` (load), `/api/annotate`, `/api/word-card`, `/api/lessons` (save new), `/api/lessons/{id}` PUT (update), `/api/lessons/{id}/title` PATCH (rename), `/api/lessons/{id}` DELETE. 2. **Lessons browser** — `/api/lessons` (list, grouped client-side by date/category). 3. **Exercises** — the four `/api/exercises/...` groups. 4. **Chat coach** — `/api/chat`. 5. **Summary dashboard** — `/api/summary`. 6. **Tools** — `/api/annotate`/`/api/word-card` (Text Checker), `/api/gender-check` (Gender Checker), `/api/translate` (Translator) — all standalone utilities, separate from the saved notebook.