# 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
