French-Coach / frontend /API_CONTRACT.md
Asma-F's picture
Deploy: fixed React routing + OpenBMB backend
cc13fc9 verified
|
Raw
History Blame Contribute Delete
11.4 kB
# 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": "<message>"}` 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": "<span data-token=\"1\" data-gender=\"Masc\" ...>Le</span> ...",
"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": "<span data-token=...>...</span> ..."}`
### `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": "<div ...>...</div>"}
```
### `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": "<div ...>...</div>",
"hint": "Your turn: ..." , "feedback_html": "<div ...>...</div>"}
```
(`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": "<div ...>...</div>"
}
```
(`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": "<div ...>...</div>"}
```
### `POST /api/exercises/pronunciation/check`
Awards `pronunciation` points.
Body: `{"target": {"phrase": "..."}, "transcription": "..."}`
Response: `{"html": "<div ...>...</div>"}`
---
## 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.