Browse Source

docs(speaking): add design spec and implementation plan for grade-aware system prompt

- Spec: docs/superpowers/specs/2026-04-25-english-speaking-dialogue-prompt-design.md
- Plan: docs/superpowers/plans/2026-04-25-english-speaking-dialogue-prompt.md
jimmylee 2 tuần trước cách đây
mục cha
commit
ca5c517e4a

+ 810 - 0
docs/superpowers/plans/2026-04-25-english-speaking-dialogue-prompt.md

@@ -0,0 +1,810 @@
+# English Speaking Dialogue System Prompt — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the minimal dialogue system prompt with a structured, grade-aware template; wire the existing curriculum-grade dropdown + learning-goal vocabulary/sentences into session creation so the backend can render the full prompt at session creation time.
+
+**Architecture:** Frontend persists `grade` (= `grade.id` from `curriculum.json`, e.g. `'grade5-1'`) into the Pinia speaking store and sends it along with `vocabulary[]` and `sentences[]` to the backend's `POST /session`. Backend renders a full multi-tier prompt template into `dialogue_session.system_prompt` (no schema change). The LLM reads the literal `{年级}` value and self-selects the correct tier from the three Grade-Specific Adaptation Rules sections in the prompt.
+
+**Tech Stack:**
+- Frontend: Vue 3 + TypeScript, Pinia (`/Users/buoy/Development/gitrepo/PPT`)
+- Backend: FastAPI + SQLAlchemy async + Pydantic (`/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`)
+- Backend tests: pytest + httpx ASGITransport + in-memory aiosqlite
+
+**Spec:** `docs/superpowers/specs/2026-04-25-english-speaking-dialogue-prompt-design.md`
+
+---
+
+## File Map
+
+**Backend** (`cococlass-english-speaking-api`):
+- Modify: `app/api/dialogue.py:44-49` — extend `CreateSessionRequest`
+- Modify: `app/api/dialogue.py:52-68` — pass new fields to service
+- Modify: `app/service/speaking/dialogue_service.py:27-34` — replace `SYSTEM_PROMPT_TEMPLATE`
+- Modify: `app/service/speaking/dialogue_service.py:52-90` — extend `create_session_only`
+- Test: `tests/api/test_dialogue_greeting.py` — extend existing test or add a new file
+- Test: `tests/service/speaking/test_dialogue_service_prompt.py` — new file for prompt rendering
+
+**Frontend** (`PPT`):
+- Modify: `src/types/englishSpeaking.ts:51-57` — add `grade` to `TopicDiscussionConfig`
+- Modify: `src/types/englishSpeaking.ts:240-246` — add `grade` to `SessionConfig`
+- Modify: `src/store/speaking.ts:10-42` — add `grade` to `DEFAULT_CONFIG`, add `updateGrade`
+- Modify: `src/views/Editor/EnglishSpeaking/SpeakingPanel.vue:80-82` — bind selectedGrade to store
+- Modify: `src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue:213-218` — pass grade/sentences
+- Modify: `src/views/Editor/EnglishSpeaking/services/llmService.ts:155-176` — send grade/vocabulary/sentences
+
+**Untouched:**
+- `app/models/dialogue.py` (no schema change)
+- `src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue` (already emits `grade.id`)
+- `src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue`
+- `src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts`
+
+---
+
+## Order
+
+Backend first (Tasks 1–4) so the API accepts new fields before frontend sends them. Frontend after (Tasks 5–9). Then integration verification (Task 10).
+
+---
+
+### Task 1: Backend — Extend `CreateSessionRequest` Pydantic model
+
+**Repo:** `/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`
+
+**Files:**
+- Modify: `app/api/dialogue.py:44-49`
+
+- [ ] **Step 1: Update the Pydantic model**
+
+Edit `app/api/dialogue.py`, replace the existing `CreateSessionRequest` definition:
+
+```python
+class CreateSessionRequest(BaseModel):
+    topic: str
+    grade: str
+    vocabulary: list[str] = []
+    sentences: list[str] = []
+    totalRounds: int = 3
+    durationSeconds: int | None = None
+    roleId: str | None = None
+    userId: str | None = None
+```
+
+- [ ] **Step 2: Verify import is fine**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && python -c "from app.api.dialogue import CreateSessionRequest; r = CreateSessionRequest(topic='t', grade='g'); print(r.vocabulary, r.sentences)"`
+
+Expected output: `[] []`
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api
+git add app/api/dialogue.py
+git commit -m "feat(speaking): add grade/vocabulary/sentences to CreateSessionRequest"
+```
+
+---
+
+### Task 2: Backend — Replace `SYSTEM_PROMPT_TEMPLATE`
+
+**Repo:** `/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`
+
+**Files:**
+- Modify: `app/service/speaking/dialogue_service.py:27-34`
+
+- [ ] **Step 1: Replace the template constant**
+
+In `app/service/speaking/dialogue_service.py`, replace the `SYSTEM_PROMPT_TEMPLATE = """..."""` block (currently lines 27–34) with the full multi-tier template. Use Python `.format()` named-substitution style — placeholders are `{年级}`, `{话题主题}`, `{重点词汇}`, `{重点句型}`. Note: any literal `{` / `}` in the template body (none expected) would need doubling.
+
+```python
+SYSTEM_PROMPT_TEMPLATE = """You are a professional English speaking practice partner designed for students at the {年级}. Your goal is to create a relaxed and encouraging learning environment to help students improve their spoken English skills. All output must be in English.
+
+## Core Configuration
+- **Practice Level**: {年级}
+- **Conversation Topic**: {话题主题}
+- **Key Vocabulary**: {重点词汇}
+- **Key Sentence Patterns**: {重点句型}
+
+## Grade-Specific Adaptation Rules
+**The system must dynamically adjust all language output based on the following parameters for the specified {年级}:**
+
+### For Grades 1-3 (e.g., Grade 2)
+*   **Vocabulary Selection**: 100% CEFR A1-A2 level words. Use only the most common, concrete terms related to the topic.
+*   **Sentence Complexity**: 100% simple sentences (single subject + predicate). Use no compound or complex sentences.
+*   **Sentence Length**: Aim for 4 to 8 words per sentence.
+*   **Utterances per Turn**: As the teacher, you should speak 1 sentence per turn. Encourage the student to respond with 1 simple sentence.
+*   **Pacing & Scaffolding**: Speak slowly. Use repetition. Ask one simple, direct question at a time.
+
+### For Grades 4-6 (e.g., Grade 5)
+*   **Vocabulary Selection**: Approximately 80% CEFR A1-A2 and 20% B1-B2 level words. You may introduce a few more descriptive or topic-specific adjectives/adverbs.
+*   **Sentence Complexity**: Approximately 90% simple sentences and 10% compound sentences (using 'and', 'but', 'so', 'because').
+*   **Sentence Length**: Aim for 10 to 15 words per sentence.
+*   **Utterances per Turn**: As the teacher, you can use 1-2 sentences per turn. Encourage the student to form slightly longer responses.
+*   **Pacing & Scaffolding**: Use natural pacing. You can connect ideas within a turn using basic conjunctions.
+
+### For Middle School Grades 1-3 (e.g., Grade 8)
+*   **Vocabulary Selection**: Approximately 70% CEFR A1-A2 and 30% B1-B2 level words. Confidently use a wider range of vocabulary, including more abstract or general academic terms where appropriate.
+*   **Sentence Complexity**: Approximately 60% simple sentences and 40% compound/complex sentences. Use relative clauses, participial phrases, and a variety of conjunctions.
+*   **Sentence Length**: Aim for 15 to 25 words per sentence.
+*   **Utterances per Turn**: As the teacher, you can use 2 sentences to model richer language. Encourage the student to express opinions, reasons, and sequences of events.
+*   **Pacing & Scaffolding**: Use natural, conversational pacing. Prompt for elaboration (e.g., "Why?", "Can you tell me more?").
+
+## Conversation Rules
+1.  **Role & Tone**: You are the friendly tutor. Maintain a positive, patient, and encouraging tone. Correct errors gently and implicitly by modeling the correct form in your following response.
+2.  **Topic Adherence**: All dialogue must naturally revolve around the topic: **{话题主题}**. Gently guide the conversation back if it drifts.
+3.  **Language Focus Integration**:
+    *   Seamlessly incorporate the **{重点词汇}** into the conversation. Aim to use and elicit each target word multiple times.
+    *   Create scenarios that naturally prompt the use of the **{重点句型}**.
+    *   Provide positive, specific feedback when the student successfully uses a target word or pattern (e.g., "Great! You said 'by bus.'").
+4.  **Conversation Flow**:
+    *   **Start**: Greet, introduce yourself, and state the topic.
+    *   **Core**: Engage in 4-6 conversational turns. Adapt your follow-up questions and prompts based on the student's responses and the **Grade-Specific Rules** above.
+    *   **End**: Conclude the practice naturally with encouraging feedback.
+
+## Example Dialogue Styles
+*   **Grade 2 Example**: T: "Hello. Let's talk about school. How do you come to school?" S: "I come by car." T: "Good! By car. Is it fun?"
+*   **Grade 5 Example**: T: "Hello! Today, let's discuss how we get to school. How do you usually come to school?" S: "I usually come by bus with my friend." T: "That's nice. Do you live near the bus stop?"
+*   **Grade 8 Example**: T: "Hello there. Let's talk about our commutes to school. What's your typical way of getting to school, and how long does it usually take?" S: "I typically ride my bike because I live quite near, so it only takes about ten minutes." T: "That's efficient. Do you ever use other means of transport, like the subway?"
+
+## Start the Conversation Now
+Initiate the conversation. Your first utterance must:
+1.  Be in English.
+2.  Start with a greeting like "Hello" or "Hi".
+3.  Mention the topic **{话题主题}**.
+4.  Be crafted according to the **Grade-Specific Adaptation Rules** for the specified **{年级}**.
+"""
+```
+
+- [ ] **Step 2: Smoke check the template renders**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && python -c "from app.service.speaking.dialogue_service import SYSTEM_PROMPT_TEMPLATE; print(SYSTEM_PROMPT_TEMPLATE.format(年级='grade5-1', 话题主题='How I get to school', 重点词汇='by bus, on foot', 重点句型='- How do you come to school?')[:200])"`
+
+Expected: First 200 chars of the rendered prompt with `grade5-1` and `How I get to school` substituted in. No `KeyError`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add app/service/speaking/dialogue_service.py
+git commit -m "feat(speaking): replace system prompt with grade-aware multi-tier template"
+```
+
+---
+
+### Task 3: Backend — Extend `create_session_only` and add unit test
+
+**Repo:** `/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`
+
+**Files:**
+- Modify: `app/service/speaking/dialogue_service.py:52-90`
+- Create: `tests/service/speaking/test_dialogue_service_prompt.py`
+
+- [ ] **Step 1: Write the failing test**
+
+Create `tests/service/speaking/test_dialogue_service_prompt.py`:
+
+```python
+"""Tests for system_prompt rendering in create_session_only."""
+
+from unittest.mock import MagicMock
+
+import pytest
+import pytest_asyncio
+from fastapi import HTTPException
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
+from sqlalchemy.pool import StaticPool
+
+from app.models.dialogue import Base, DialogueSession
+from app.service.speaking.dialogue_service import DialogueService
+
+
+@pytest_asyncio.fixture
+async def db():
+    engine = create_async_engine(
+        "sqlite+aiosqlite:///:memory:",
+        poolclass=StaticPool,
+        connect_args={"check_same_thread": False},
+    )
+    async with engine.begin() as conn:
+        await conn.run_sync(Base.metadata.create_all)
+    SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
+    async with SessionLocal() as s:
+        yield s
+
+
+@pytest.fixture
+def service():
+    return DialogueService(
+        asr=MagicMock(), llm=MagicMock(), assessor=MagicMock(), storage=MagicMock(),
+    )
+
+
+@pytest.mark.asyncio
+async def test_create_session_renders_full_prompt(db, service):
+    await service.create_session_only(
+        db=db,
+        topic="How I get to school",
+        grade="grade5-1",
+        vocabulary=["by bus", "on foot"],
+        sentences=["How do you come to school?", "I come by bus."],
+    )
+
+    session = (await db.execute(select(DialogueSession))).scalar_one()
+    prompt = session.system_prompt
+
+    assert "grade5-1" in prompt
+    assert "How I get to school" in prompt
+    assert "by bus, on foot" in prompt
+    assert "- How do you come to school?" in prompt
+    assert "- I come by bus." in prompt
+    assert "Grade-Specific Adaptation Rules" in prompt
+    assert "For Grades 1-3" in prompt
+    assert "For Grades 4-6" in prompt
+    assert "For Middle School" in prompt
+
+
+@pytest.mark.asyncio
+async def test_create_session_handles_empty_arrays(db, service):
+    await service.create_session_only(
+        db=db,
+        topic="X",
+        grade="grade5-1",
+        vocabulary=[],
+        sentences=[],
+    )
+    session = (await db.execute(select(DialogueSession))).scalar_one()
+    prompt = session.system_prompt
+    # Placeholders are substituted (with empty strings) — no leftover braces
+    assert "{重点词汇}" not in prompt
+    assert "{重点句型}" not in prompt
+
+
+@pytest.mark.asyncio
+async def test_create_session_rejects_blank_grade(db, service):
+    with pytest.raises(HTTPException) as exc:
+        await service.create_session_only(
+            db=db,
+            topic="X",
+            grade="   ",
+            vocabulary=[],
+            sentences=[],
+        )
+    assert exc.value.status_code == 400
+```
+
+- [ ] **Step 2: Run test, expect failure**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest tests/service/speaking/test_dialogue_service_prompt.py -v`
+
+Expected: FAIL — `create_session_only()` doesn't accept `grade`/`vocabulary`/`sentences` yet (TypeError on unexpected kwarg).
+
+- [ ] **Step 3: Update `create_session_only` signature and rendering**
+
+In `app/service/speaking/dialogue_service.py`, replace the existing `create_session_only` method (lines 52–90) with:
+
+```python
+    async def create_session_only(
+        self,
+        db: AsyncSession,
+        topic: str,
+        grade: str,
+        vocabulary: list[str],
+        sentences: list[str],
+        total_rounds: int = 3,
+        duration_seconds: int | None = None,
+        role_config: dict | None = None,
+        user_id: str | None = None,
+    ) -> dict:
+        """只建会话记录,不调 LLM / Insert a session row without invoking the LLM.
+
+        Returns:
+            dict: {sessionId, totalRounds, currentRound, expiresAt}
+        """
+        from fastapi import HTTPException
+
+        if not grade.strip():
+            raise HTTPException(status_code=400, detail="grade is required")
+
+        rendered_vocab = ", ".join(vocabulary)
+        rendered_sentences = "\n".join(f"- {s}" for s in sentences)
+        system_prompt = SYSTEM_PROMPT_TEMPLATE.format(
+            年级=grade,
+            话题主题=topic,
+            重点词汇=rendered_vocab,
+            重点句型=rendered_sentences,
+        )
+        expires_at = None
+        if duration_seconds:
+            expires_at = datetime.now() + timedelta(seconds=duration_seconds)
+
+        session = DialogueSession(
+            user_id=user_id,
+            topic=topic,
+            role_config=role_config,
+            total_rounds=total_rounds,
+            current_round=1,
+            status="active",
+            system_prompt=system_prompt,
+            expires_at=expires_at,
+        )
+        db.add(session)
+        await db.commit()
+        await db.refresh(session)
+
+        return {
+            "sessionId": session.uuid,
+            "totalRounds": total_rounds,
+            "currentRound": session.current_round,
+            "expiresAt": expires_at.isoformat() if expires_at else None,
+        }
+```
+
+- [ ] **Step 4: Run tests, expect pass**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest tests/service/speaking/test_dialogue_service_prompt.py -v`
+
+Expected: All 3 tests PASS.
+
+- [ ] **Step 5: Run full backend suite to make sure nothing else broke**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest -v 2>&1 | tail -40`
+
+Expected: Existing tests may fail because they call `create_session_only` without the new required `grade`/`vocabulary`/`sentences` arguments. Note the failures — they're addressed in Task 4 (route handler update) and may also need fixture updates. If only the route's E2E test is broken, that's expected; we'll fix it in Task 4. If a service-level test breaks, fix the call site to pass the new required args (use defaults like `grade='grade5-1', vocabulary=[], sentences=[]`).
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add app/service/speaking/dialogue_service.py tests/service/speaking/test_dialogue_service_prompt.py
+git commit -m "feat(speaking): render grade-aware system_prompt in create_session_only"
+```
+
+---
+
+### Task 4: Backend — Wire route handler to pass new fields
+
+**Repo:** `/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`
+
+**Files:**
+- Modify: `app/api/dialogue.py:52-68` (the `/session` POST handler)
+- Modify: `tests/api/test_dialogue_greeting.py` (existing E2E test sends old payload)
+
+- [ ] **Step 1: Update the route handler to pass new fields**
+
+In `app/api/dialogue.py`, replace the body of the `create_session` route handler:
+
+```python
+@router.post("/session")
+async def create_session(
+    req: CreateSessionRequest,
+    db: AsyncSession = Depends(get_db),
+    service: DialogueService = Depends(get_dialogue_service),
+):
+    """创建对话 session(不生成开场白)。开场白由 POST /session/{id}/greeting 触发。"""
+    logger.info(f"Creating session: topic={req.topic}, grade={req.grade}, rounds={req.totalRounds}")
+    result = await service.create_session_only(
+        db=db,
+        topic=req.topic,
+        grade=req.grade,
+        vocabulary=req.vocabulary,
+        sentences=req.sentences,
+        total_rounds=req.totalRounds,
+        duration_seconds=req.durationSeconds,
+        user_id=req.userId,
+    )
+    logger.info(f"Session created: {result['sessionId']}")
+    return result
+```
+
+- [ ] **Step 2: Update the existing E2E test in `tests/api/test_dialogue_greeting.py`**
+
+Find every `client.post("/api/speaking/dialogue/session", json=...)` call site (or whatever path your test uses — search the file). For each one, add `"grade": "grade5-1"` to the JSON body. If `vocabulary` / `sentences` aren't sent, they default to `[]` per the Pydantic model — no change needed.
+
+Concretely, run: `grep -n '"topic"' /Users/buoy/Development/gitrepo/cococlass-english-speaking-api/tests/api/test_dialogue_greeting.py`
+
+Then for each match, ensure the JSON dict includes `"grade": "grade5-1"`.
+
+- [ ] **Step 3: Add a new E2E test asserting required-field validation**
+
+Append to `tests/api/test_dialogue_greeting.py` (inside the existing test module — reuse the `test_env` fixture):
+
+```python
+@pytest.mark.asyncio
+async def test_create_session_requires_grade(test_env):
+    client: AsyncClient = test_env["client"]
+    res = await client.post(
+        "/api/speaking/dialogue/session",
+        json={"topic": "Animals"},  # missing 'grade'
+    )
+    assert res.status_code == 422  # Pydantic validation error
+
+
+@pytest.mark.asyncio
+async def test_create_session_persists_grade_in_prompt(test_env):
+    client: AsyncClient = test_env["client"]
+    res = await client.post(
+        "/api/speaking/dialogue/session",
+        json={
+            "topic": "How I get to school",
+            "grade": "grade5-1",
+            "vocabulary": ["by bus", "on foot"],
+            "sentences": ["How do you come to school?"],
+        },
+    )
+    assert res.status_code == 200
+    session_id = res.json()["sessionId"]
+
+    # Inspect persisted prompt
+    SessionLocal = test_env["SessionLocal"]
+    from sqlalchemy import select
+    from app.models.dialogue import DialogueSession
+    async with SessionLocal() as s:
+        row = (await s.execute(select(DialogueSession).where(DialogueSession.uuid == session_id))).scalar_one()
+    assert "grade5-1" in row.system_prompt
+    assert "by bus, on foot" in row.system_prompt
+    assert "- How do you come to school?" in row.system_prompt
+```
+
+If the existing `test_env` fixture doesn't expose `client` and `SessionLocal` as a dict, adapt to whatever shape it returns (read the fixture body around lines 22–60). The point: have `client` for HTTP calls and a way to query the DB.
+
+- [ ] **Step 4: Run tests**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest tests/api/test_dialogue_greeting.py -v`
+
+Expected: All tests PASS, including the new ones.
+
+- [ ] **Step 5: Run full suite once more**
+
+Run: `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && pytest -v 2>&1 | tail -20`
+
+Expected: All tests PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add app/api/dialogue.py tests/api/test_dialogue_greeting.py
+git commit -m "feat(speaking): wire grade/vocabulary/sentences through /session route"
+```
+
+---
+
+### Task 5: Frontend — Extend `TopicDiscussionConfig` and `SessionConfig` types
+
+**Repo:** `/Users/buoy/Development/gitrepo/PPT`
+
+**Files:**
+- Modify: `src/types/englishSpeaking.ts:51-57`
+- Modify: `src/types/englishSpeaking.ts:240-246`
+
+- [ ] **Step 1: Add `grade` to `TopicDiscussionConfig`**
+
+In `src/types/englishSpeaking.ts`, replace the `TopicDiscussionConfig` interface (lines 51–57):
+
+```ts
+// 话题讨论配置
+export interface TopicDiscussionConfig {
+  topic: string
+  grade: string          // grade.id from curriculum.json (e.g., 'grade5-1')
+  selectedRole: string
+  learningGoals: LearningGoals
+  practice: PracticeSettings
+  evaluation: EvaluationSettings
+}
+```
+
+- [ ] **Step 2: Add `grade` to `SessionConfig`**
+
+In the same file, replace the `SessionConfig` interface (lines 240–246):
+
+```ts
+// 对话会话配置
+export interface SessionConfig {
+  topic: string
+  grade: string
+  roleId: string
+  totalRounds: number
+  vocabulary?: string[]
+  sentences?: string[]
+}
+```
+
+- [ ] **Step 3: Type-check (will fail at consumer sites — that's expected)**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | head -30`
+
+Expected: Errors at sites where `TopicDiscussionConfig` is constructed without `grade` (DEFAULT_CONFIG) and where `SessionConfig` is constructed without `grade` (TopicDiscussionPreview's `createSession` call). These are addressed in Tasks 6 and 8.
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /Users/buoy/Development/gitrepo/PPT
+git add src/types/englishSpeaking.ts
+git commit -m "feat(speaking): add grade to TopicDiscussionConfig and SessionConfig types"
+```
+
+---
+
+### Task 6: Frontend — Update Pinia store (`DEFAULT_CONFIG` + `updateGrade`)
+
+**Repo:** `/Users/buoy/Development/gitrepo/PPT`
+
+**Files:**
+- Modify: `src/store/speaking.ts:10-42` (DEFAULT_CONFIG)
+- Modify: `src/store/speaking.ts:78-82` (actions, near `updateTopic`)
+
+- [ ] **Step 1: Add `grade` to `DEFAULT_CONFIG`**
+
+In `src/store/speaking.ts`, the `DEFAULT_CONFIG` object — add `grade: 'grade5-1'` right after `topic`:
+
+```ts
+const DEFAULT_CONFIG: TopicDiscussionConfig = {
+  topic: 'How I get to school',
+  grade: 'grade5-1',
+  selectedRole: 'tom',
+  learningGoals: {
+    // …unchanged
+```
+
+- [ ] **Step 2: Add `updateGrade` action**
+
+In the `actions` block, immediately after the `updateTopic` action:
+
+```ts
+    // 年级
+    updateGrade(grade: string) {
+      this.config.grade = grade
+    },
+```
+
+- [ ] **Step 3: Type-check**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | grep -E "speaking\.ts|englishSpeaking\.ts" | head -10`
+
+Expected: No errors from `speaking.ts` or `englishSpeaking.ts`. Errors elsewhere (TopicDiscussionPreview) are still expected.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/store/speaking.ts
+git commit -m "feat(speaking): persist grade in store, add updateGrade action"
+```
+
+---
+
+### Task 7: Frontend — Bind `SpeakingPanel` selectedGrade to store
+
+**Repo:** `/Users/buoy/Development/gitrepo/PPT`
+
+**Files:**
+- Modify: `src/views/Editor/EnglishSpeaking/SpeakingPanel.vue:67-110`
+
+- [ ] **Step 1: Replace local `selectedGrade` ref with store binding**
+
+In `src/views/Editor/EnglishSpeaking/SpeakingPanel.vue`, the `<script setup>` section currently has at lines 80–82:
+
+```ts
+const selectedTextbook = ref('shep')
+const selectedGrade = ref('grade5-1')
+const selectedUnit = ref('SHEP-5-上-Unit2')
+```
+
+The store is already imported (line 71: `import { useSpeakingStore } from '@/store/speaking'`) and instantiated as `speakingStore` (line 106).
+
+Move the `useSpeakingStore` call up (right after the `const pageMode` line so it's before our binding), and replace the three refs block with:
+
+```ts
+const speakingStore = useSpeakingStore()
+
+// 课本筛选条件的默认值(上海英语·五年级上·Unit2),供开发/演示用
+const selectedTextbook = ref('shep')
+const selectedUnit = ref('SHEP-5-上-Unit2')
+// selectedGrade 持久化到 store(用于发送到后端 createSession 渲染 system_prompt)
+const selectedGrade = computed({
+  get: () => speakingStore.config.grade,
+  set: (val: string) => speakingStore.updateGrade(val),
+})
+```
+
+Add `computed` to the Vue import at line 68:
+
+```ts
+import { ref, computed, watch } from 'vue'
+```
+
+Remove the now-duplicate `const speakingStore = useSpeakingStore()` from line 106 (the watch block can use the same instance).
+
+- [ ] **Step 2: Verify the `<Layer1Home>` v-bind/v-on still works**
+
+The template binding `:selectedGrade="selectedGrade"` and `@update:selectedGrade="selectedGrade = $event"` will continue to work because `computed` with a setter is assignable. No template change needed.
+
+- [ ] **Step 3: Type-check**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | grep "SpeakingPanel" | head -5`
+
+Expected: No errors from SpeakingPanel.vue.
+
+- [ ] **Step 4: Manual smoke test**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npm run dev` (or whatever dev command this project uses — check `package.json` if unsure).
+
+Open the English Speaking panel. Change the grade in the dropdown. Open Vue devtools (or add a temporary `console.log(speakingStore.config.grade)` in the panel) and confirm `store.config.grade` updates.
+
+Stop the dev server.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/views/Editor/EnglishSpeaking/SpeakingPanel.vue
+git commit -m "feat(speaking): bind SpeakingPanel grade selector to speakingStore"
+```
+
+---
+
+### Task 8: Frontend — `TopicDiscussionPreview` passes grade/sentences in createSession
+
+**Repo:** `/Users/buoy/Development/gitrepo/PPT`
+
+**Files:**
+- Modify: `src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue:213-218`
+
+- [ ] **Step 1: Extend the `createSession` call**
+
+In `TopicDiscussionPreview.vue`, the `startDialogue` function currently calls:
+
+```ts
+const info = await api.createSession({
+  topic: speakingStore.config.topic || props.topic,
+  totalRounds: speakingStore.config.practice.rounds ?? props.totalRounds,
+  roleId: mockRole.id,
+  vocabulary: speakingStore.config.learningGoals.vocabulary,
+})
+```
+
+Replace with:
+
+```ts
+const info = await api.createSession({
+  topic: speakingStore.config.topic || props.topic,
+  grade: speakingStore.config.grade,
+  totalRounds: speakingStore.config.practice.rounds ?? props.totalRounds,
+  roleId: mockRole.id,
+  vocabulary: speakingStore.config.learningGoals.vocabulary,
+  sentences: speakingStore.config.learningGoals.sentences,
+})
+```
+
+- [ ] **Step 2: Type-check**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | grep "TopicDiscussionPreview" | head -5`
+
+Expected: No errors. (Earlier `grade` missing error from Task 5 is now resolved.)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue
+git commit -m "feat(speaking): pass grade and sentences to createSession"
+```
+
+---
+
+### Task 9: Frontend — `RealDialogueAPI.createSession` sends new fields on the wire
+
+**Repo:** `/Users/buoy/Development/gitrepo/PPT`
+
+**Files:**
+- Modify: `src/views/Editor/EnglishSpeaking/services/llmService.ts:155-176`
+
+- [ ] **Step 1: Extend the request body**
+
+In `src/views/Editor/EnglishSpeaking/services/llmService.ts`, the `RealDialogueAPI.createSession` method currently has:
+
+```ts
+body: JSON.stringify({
+  topic: config.topic,
+  totalRounds: config.totalRounds,
+  roleId: config.roleId,
+}),
+```
+
+Replace with:
+
+```ts
+body: JSON.stringify({
+  topic: config.topic,
+  grade: config.grade,
+  vocabulary: config.vocabulary ?? [],
+  sentences: config.sentences ?? [],
+  totalRounds: config.totalRounds,
+  roleId: config.roleId,
+}),
+```
+
+Also fix the `MockDialogueAPI.createSession` if it uses the rename — actually it doesn't read these fields, no change needed there.
+
+- [ ] **Step 2: Type-check the whole frontend**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npx vue-tsc --noEmit 2>&1 | tail -20`
+
+Expected: No errors related to this change. (Pre-existing unrelated errors, if any, are out of scope.)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/views/Editor/EnglishSpeaking/services/llmService.ts
+git commit -m "feat(speaking): send grade/vocabulary/sentences to backend /session"
+```
+
+---
+
+### Task 10: Integration smoke test
+
+**Both repos.**
+
+- [ ] **Step 1: Start backend**
+
+Run in a terminal (or background): `cd /Users/buoy/Development/gitrepo/cococlass-english-speaking-api && uvicorn app.main:app --reload --port 8000` (use whatever startup command the backend uses — check `pyproject.toml` / README if unsure).
+
+- [ ] **Step 2: Hit the API directly with curl**
+
+```bash
+curl -i -X POST http://localhost:8000/api/speaking/dialogue/session \
+  -H 'Content-Type: application/json' \
+  -d '{
+    "topic": "How I get to school",
+    "grade": "grade5-1",
+    "vocabulary": ["by bus", "on foot"],
+    "sentences": ["How do you come to school?", "I come by bus."],
+    "totalRounds": 3
+  }'
+```
+
+Expected: HTTP 200, response body has `sessionId`.
+
+- [ ] **Step 3: Inspect the persisted prompt**
+
+Connect to the backend's database (the connection string is in `app/config.py` or env). Query the row:
+
+```sql
+SELECT system_prompt FROM dialogue_session ORDER BY id DESC LIMIT 1;
+```
+
+(Or use `sqlite3 path/to/db.sqlite "SELECT system_prompt FROM dialogue_session ORDER BY id DESC LIMIT 1;"` if SQLite.)
+
+Expected: Output contains `grade5-1`, `How I get to school`, `by bus, on foot`, `- How do you come to school?`, and the three Grade-Specific Adaptation Rules sections.
+
+- [ ] **Step 4: Validation negative test**
+
+```bash
+curl -i -X POST http://localhost:8000/api/speaking/dialogue/session \
+  -H 'Content-Type: application/json' \
+  -d '{"topic": "X"}'
+```
+
+Expected: HTTP 422 (Pydantic validation, missing `grade`).
+
+- [ ] **Step 5: Frontend integration**
+
+Run: `cd /Users/buoy/Development/gitrepo/PPT && npm run dev`
+
+Open the editor, navigate to English Speaking → 话题讨论 config page. Change the grade in `SpeakingPanel`, edit vocabulary or sentences, click "开始对话". Watch the network tab: the `POST /session` request body should include `grade`, `vocabulary`, `sentences` matching the UI state.
+
+In the backend DB (or via repeating Step 3), confirm the latest session row's `system_prompt` reflects what the UI just sent.
+
+- [ ] **Step 6: LLM behavior check (optional)**
+
+Continue the dialogue flow in the UI — the AI's first turn should match the Grade 4–6 tier rules (10–15 word sentences, mostly simple/compound). If wildly off-tier, that's a prompt-engineering issue, not an integration issue — note for follow-up but doesn't block this plan.
+
+- [ ] **Step 7: Stop the dev server and backend**
+
+No commit for Task 10 itself (it's verification).
+
+---
+
+## Done criteria
+
+- All backend tests pass (`pytest -v`)
+- `vue-tsc --noEmit` reports no new errors
+- `curl POST /session` with `{topic, grade, vocabulary, sentences}` returns 200 and the persisted `system_prompt` contains the rendered template
+- Missing `grade` returns 422
+- UI can change grade, send a session-create request, and see the prompt reflect the UI state in the DB

+ 325 - 0
docs/superpowers/specs/2026-04-25-english-speaking-dialogue-prompt-design.md

@@ -0,0 +1,325 @@
+# English Speaking Dialogue System Prompt — Design
+
+**Date**: 2026-04-25
+**Branch**: `feat/english-speaking`
+**Repos affected**:
+- Frontend: `/Users/buoy/Development/gitrepo/PPT`
+- Backend: `/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`
+
+## Goal
+
+Replace the current minimal system prompt with a structured, grade-aware template that adapts language complexity to the student's grade level. Wire the existing curriculum-selector grade dropdown + learning-goal vocabulary/sentence lists into the session-creation request so the backend can render the full prompt at session-creation time.
+
+## Non-goals
+
+- No grade.id → label mapping in frontend
+- No tier inference (grade1-3 / 4-6 / 初中) anywhere — the LLM reads all three tier sections in the prompt and self-selects based on the literal `{年级}` value
+- No new DB columns — reuse existing `dialogue_session.system_prompt`
+- No prompt versioning / A-B testing infrastructure
+- No alembic migration
+
+## Architecture
+
+```
+Frontend (PPT)                        Backend (cococlass-english-speaking-api)
+─────────────────                     ──────────────────────────────────────────
+SpeakingPanel.vue                     POST /session/create
+  CurriculumSelector                    ↓
+   selectedGrade ──┐                  CreateSessionRequest (Pydantic)
+                   ↓                    ↓
+   store.config.grade                 dialogue_service.create_session_only()
+   store.config.learningGoals.{       ↓
+     vocabulary, sentences }          render SYSTEM_PROMPT_TEMPLATE
+                   ↓                    ↓
+   llmService.createSession()        DialogueSession.system_prompt (Text)
+   POST { topic, grade,                ↓
+          vocabulary, sentences,     LLM (greeting / round)
+          totalRounds, ... }           ↓
+                                      reads system_prompt → adapts
+                                      output to {年级} tier
+```
+
+The prompt template is hardcoded in the backend service. The frontend ships only data, not prompt fragments.
+
+---
+
+## Section 1 — Frontend config: persist grade
+
+### `src/types/englishSpeaking.ts`
+
+Add `grade: string` to `TopicDiscussionConfig`:
+
+```ts
+export interface TopicDiscussionConfig {
+  topic: string
+  grade: string          // NEW — grade.id from curriculum.json
+  selectedRole: string
+  learningGoals: LearningGoals
+  practice: PracticeSettings
+  evaluation: EvaluationSettings
+}
+```
+
+### `src/store/speaking.ts`
+
+Add to `DEFAULT_CONFIG`:
+
+```ts
+const DEFAULT_CONFIG: TopicDiscussionConfig = {
+  topic: 'How I get to school',
+  grade: 'grade5-1',     // NEW — must match a grade.id in curriculum.json
+  selectedRole: 'tom',
+  // …rest unchanged
+}
+```
+
+Add action:
+
+```ts
+updateGrade(id: string) {
+  this.config.grade = id
+},
+```
+
+### `src/views/Editor/EnglishSpeaking/SpeakingPanel.vue`
+
+Replace the local `selectedGrade` ref with a binding to the store:
+
+- Before: `const selectedGrade = ref('grade5-1')`
+- After: bind `<CurriculumSelector>`'s `selectedGrade` prop to `store.config.grade` and its `update:selectedGrade` event to `store.updateGrade()`
+
+`selectedTextbook` and `selectedUnit` stay as local refs — they're not part of the prompt contract.
+
+### `src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue`
+
+**No changes.** Already emits `update:selectedGrade` with `grade.id`.
+
+---
+
+## Section 2 — Frontend → Backend request contract
+
+`POST /session/create` body:
+
+```ts
+{
+  topic: string
+  grade: string          // REQUIRED — dropdown's selected value (= grade.id, e.g. 'grade5-1')
+  vocabulary: string[]   // array (may be empty)
+  sentences: string[]    // array (may be empty)
+  totalRounds?: number
+  durationSeconds?: number
+  roleId?: string
+  userId?: string
+}
+```
+
+### `src/views/Editor/EnglishSpeaking/services/llmService.ts`
+
+In `RealDialogueAPI.createSession()`, extend the request body:
+
+```ts
+body: JSON.stringify({
+  topic: store.config.topic,
+  grade: store.config.grade,                        // NEW
+  vocabulary: store.config.learningGoals.vocabulary, // NEW
+  sentences: store.config.learningGoals.sentences,   // NEW
+  totalRounds: store.config.practice.rounds,
+  // …existing fields
+})
+```
+
+Frontend does **not** translate `grade.id` → label. Whatever the dropdown selected goes on the wire.
+
+---
+
+## Section 3 — Backend changes
+
+### `app/api/dialogue.py`
+
+Extend `CreateSessionRequest`:
+
+```python
+class CreateSessionRequest(BaseModel):
+    topic: str
+    grade: str                                # NEW, required
+    vocabulary: list[str] = []                # NEW
+    sentences: list[str] = []                 # NEW
+    totalRounds: int = 3
+    durationSeconds: int | None = None
+    roleId: str | None = None
+    userId: str | None = None
+```
+
+In the route handler, pass the new fields into `create_session_only()`.
+
+### `app/service/speaking/dialogue_service.py`
+
+**1. Replace `SYSTEM_PROMPT_TEMPLATE`** with the full template (4 placeholders: `{年级}`, `{话题主题}`, `{重点词汇}`, `{重点句型}`):
+
+```python
+SYSTEM_PROMPT_TEMPLATE = """You are a professional English speaking practice partner designed for students at the {年级}. Your goal is to create a relaxed and encouraging learning environment to help students improve their spoken English skills. All output must be in English.
+
+## Core Configuration
+- **Practice Level**: {年级}
+- **Conversation Topic**: {话题主题}
+- **Key Vocabulary**: {重点词汇}
+- **Key Sentence Patterns**: {重点句型}
+
+## Grade-Specific Adaptation Rules
+**The system must dynamically adjust all language output based on the following parameters for the specified {年级}:**
+
+### For Grades 1-3 (e.g., Grade 2)
+*   **Vocabulary Selection**: 100% CEFR A1-A2 level words. Use only the most common, concrete terms related to the topic.
+*   **Sentence Complexity**: 100% simple sentences (single subject + predicate). Use no compound or complex sentences.
+*   **Sentence Length**: Aim for 4 to 8 words per sentence.
+*   **Utterances per Turn**: As the teacher, you should speak 1 sentence per turn. Encourage the student to respond with 1 simple sentence.
+*   **Pacing & Scaffolding**: Speak slowly. Use repetition. Ask one simple, direct question at a time.
+
+### For Grades 4-6 (e.g., Grade 5)
+*   **Vocabulary Selection**: Approximately 80% CEFR A1-A2 and 20% B1-B2 level words. You may introduce a few more descriptive or topic-specific adjectives/adverbs.
+*   **Sentence Complexity**: Approximately 90% simple sentences and 10% compound sentences (using 'and', 'but', 'so', 'because').
+*   **Sentence Length**: Aim for 10 to 15 words per sentence.
+*   **Utterances per Turn**: As the teacher, you can use 1-2 sentences per turn. Encourage the student to form slightly longer responses.
+*   **Pacing & Scaffolding**: Use natural pacing. You can connect ideas within a turn using basic conjunctions.
+
+### For Middle School Grades 1-3 (e.g., Grade 8)
+*   **Vocabulary Selection**: Approximately 70% CEFR A1-A2 and 30% B1-B2 level words. Confidently use a wider range of vocabulary, including more abstract or general academic terms where appropriate.
+*   **Sentence Complexity**: Approximately 60% simple sentences and 40% compound/complex sentences. Use relative clauses, participial phrases, and a variety of conjunctions.
+*   **Sentence Length**: Aim for 15 to 25 words per sentence.
+*   **Utterances per Turn**: As the teacher, you can use 2 sentences to model richer language. Encourage the student to express opinions, reasons, and sequences of events.
+*   **Pacing & Scaffolding**: Use natural, conversational pacing. Prompt for elaboration (e.g., "Why?", "Can you tell me more?").
+
+## Conversation Rules
+1.  **Role & Tone**: You are the friendly tutor. Maintain a positive, patient, and encouraging tone. Correct errors gently and implicitly by modeling the correct form in your following response.
+2.  **Topic Adherence**: All dialogue must naturally revolve around the topic: **{话题主题}**. Gently guide the conversation back if it drifts.
+3.  **Language Focus Integration**:
+    *   Seamlessly incorporate the **{重点词汇}** into the conversation. Aim to use and elicit each target word multiple times.
+    *   Create scenarios that naturally prompt the use of the **{重点句型}**.
+    *   Provide positive, specific feedback when the student successfully uses a target word or pattern (e.g., "Great! You said 'by bus.'").
+4.  **Conversation Flow**:
+    *   **Start**: Greet, introduce yourself, and state the topic.
+    *   **Core**: Engage in 4-6 conversational turns. Adapt your follow-up questions and prompts based on the student's responses and the **Grade-Specific Rules** above.
+    *   **End**: Conclude the practice naturally with encouraging feedback.
+
+## Example Dialogue Styles
+*   **Grade 2 Example**: T: "Hello. Let's talk about school. How do you come to school?" S: "I come by car." T: "Good! By car. Is it fun?"
+*   **Grade 5 Example**: T: "Hello! Today, let's discuss how we get to school. How do you usually come to school?" S: "I usually come by bus with my friend." T: "That's nice. Do you live near the bus stop?"
+*   **Grade 8 Example**: T: "Hello there. Let's talk about our commutes to school. What's your typical way of getting to school, and how long does it usually take?" S: "I typically ride my bike because I live quite near, so it only takes about ten minutes." T: "That's efficient. Do you ever use other means of transport, like the subway?"
+
+## Start the Conversation Now
+Initiate the conversation. Your first utterance must:
+1.  Be in English.
+2.  Start with a greeting like "Hello" or "Hi".
+3.  Mention the topic **{话题主题}**.
+4.  Be crafted according to the **Grade-Specific Adaptation Rules** for the specified **{年级}**.
+"""
+```
+
+**2. Update `create_session_only()`** signature and rendering:
+
+```python
+async def create_session_only(
+    db: AsyncSession,
+    topic: str,
+    grade: str,                            # NEW, required
+    vocabulary: list[str],                 # NEW
+    sentences: list[str],                  # NEW
+    total_rounds: int = 3,
+    duration_seconds: int | None = None,
+    role_config: dict | None = None,
+    user_id: str | None = None,
+) -> DialogueSession:
+    if not grade.strip():
+        raise HTTPException(400, "grade is required")
+
+    rendered_vocab = ", ".join(vocabulary)
+    rendered_sentences = "\n".join(f"- {s}" for s in sentences)
+
+    system_prompt = SYSTEM_PROMPT_TEMPLATE.format(
+        年级=grade,
+        话题主题=topic,
+        重点词汇=rendered_vocab,
+        重点句型=rendered_sentences,
+    )
+    # …existing persistence logic, store system_prompt on session
+```
+
+### `app/models/dialogue.py`
+
+**No changes.** `DialogueSession.system_prompt: Text` already exists.
+
+---
+
+## Section 4 — Serialization & edge cases
+
+### Vocabulary `string[]` → `{重点词汇}`
+- Render: `", ".join(vocabulary)`
+- Empty array → empty string (placeholder line stays in template, value is blank)
+- No dedup, no trim, no length cap (frontend store already manages)
+
+### Sentences `string[]` → `{重点句型}`
+- Render: `"\n".join(f"- {s}" for s in sentences)`
+- Empty array → empty string
+- Same: no preprocessing
+
+### `grade` missing / empty
+- `grade: str` has no default → Pydantic 422 if missing
+- Empty string `""` passes Pydantic but is semantically invalid → backend raises `HTTPException(400, "grade is required")`
+
+### `topic` missing
+- Status quo, existing validation applies.
+
+### Frontend defensives
+- `store.config.grade` defaults to `'grade5-1'` from `DEFAULT_CONFIG` — normal path never sends empty
+- `vocabulary` / `sentences` come from `store.config.learningGoals.{vocabulary, sentences}` — defaults are populated
+
+### Explicitly NOT done
+- No grade.id → label conversion (LLM reads `{年级}` literal, matches against the three tier sections it sees in the prompt)
+- No tier inference logic in code
+- No persistence of vocab/sentences as separate DB columns (they're already inside the rendered `system_prompt`)
+
+---
+
+## Section 5 — Compatibility & verification
+
+### DB compatibility
+- No schema change.
+- Historical `dialogue_session` rows have `system_prompt` rendered from the old short template — left as-is, only new sessions use the new template.
+- If the backend has no real session history yet, the table can be cleared without consequence.
+
+### Caller compatibility
+- `create_session_only()` signature changes (new required `grade`). Only one caller: `POST /session/create` route handler in `app/api/dialogue.py`. Update it in the same change.
+
+### Deployment order
+1. **Backend first** — accepts new fields, renders new template
+2. **Frontend after** — sends new fields
+- Window: old frontend + new backend → missing `grade` → 422 (visible error in dev). Acceptable for solo-dev environment.
+- Reverse (new frontend + old backend) → extra fields ignored, old template renders without grade adaptation. Won't crash, just won't have desired effect.
+
+### Verification
+1. **Backend smoke test**: `curl POST /session/create` with `{topic, grade, vocabulary, sentences}` → query DB for the row's `system_prompt` column → confirm it contains the three tier sections and all 4 placeholders are substituted.
+2. **Frontend integration**: change grade in `SpeakingPanel`, edit vocab/sentences, click start → DB row's `system_prompt` reflects current store state.
+3. **LLM behavior**: run a real round with `grade='grade5-1'` and inspect output — sentence length / vocabulary should track the Grade 4-6 tier rules. Repeat with a hypothetical `grade='grade2'` to confirm tier switches.
+
+### Rollback
+Revert the two commits (frontend, backend). DB stays clean since no migration. Old short prompt template returns.
+
+---
+
+## Files touched
+
+**Frontend** (`/Users/buoy/Development/gitrepo/PPT`)
+- `src/types/englishSpeaking.ts` — add `grade` to `TopicDiscussionConfig`
+- `src/store/speaking.ts` — add `grade` default + `updateGrade` action
+- `src/views/Editor/EnglishSpeaking/SpeakingPanel.vue` — bind grade to store
+- `src/views/Editor/EnglishSpeaking/services/llmService.ts` — extend createSession body
+
+**Backend** (`/Users/buoy/Development/gitrepo/cococlass-english-speaking-api`)
+- `app/api/dialogue.py` — extend `CreateSessionRequest`, pass new fields to service
+- `app/service/speaking/dialogue_service.py` — replace `SYSTEM_PROMPT_TEMPLATE`, extend `create_session_only` signature
+
+**Untouched**
+- `app/models/dialogue.py`
+- `src/views/Editor/EnglishSpeaking/components/CurriculumSelector.vue`
+- `src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue`
+- `src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts`