# Speaking Student Session History Design

## Goal

Each student should have independent English speaking dialogue sessions for each configured speaking tool in the PPT. The speaking tool configuration id already exists in the PPT JSON as the type 77 frame element's `url`. Student identity already exists on the student page as the URL `userid`.

The system should use `configId + userId` to find a student's historical session for a specific speaking tool only in formal student mode. If history exists, the student view should open that historical session directly. If no history exists, the student should see the first-time start button and clicking it should create a session.

URL `mode` is the runtime discriminator:

- `mode=student`: formal student usage; read historical sessions by `configId + userId`.
- `mode=editor3`: editor preview; do not read historical sessions.
- other editor/screen modes: preview/display behavior; do not read historical sessions unless a later requirement explicitly enables it.

## Current State

Frontend:

- `TopicDiscussionPreview.vue` receives `configId` from `elementInfo.url`.
- `DialogueChatView.vue` correctly receives a prepared `sessionInfo` and does not create sessions itself.
- `mode=student` controls whether `App.vue` renders the `Student` page. It is the runtime mode for formal student usage, but it is not a session ownership field.
- The student `userid` is already read from the URL and passed into `Student`, but it is not currently passed through the slide rendering chain to `TopicDiscussionPreview`.
- `createSession` currently sends topic, grade, rounds, duration, role, vocabulary, and sentences, but not `configId` or `userId`.

Backend:

- `DialogueSession` already has `user_id`.
- `POST /api/speaking/dialogue/session` already accepts `userId` and stores it through `create_session_only`, but the frontend does not send it.
- `DialogueSession` does not yet have `config_id`.
- `SpeakingConfig.uuid` already exists and matches the frontend `configId`.

## Proposed Behavior

Student mode:

1. `TopicDiscussionPreview` receives `configId` from the frame element and reads URL `userid` internally.
2. On mount, if URL `mode=student` and both values are present, the component queries the backend for the latest session for that pair.
3. If a latest session exists:
   - active sessions open directly into chat using the returned `sessionInfo` and restored message history;
   - completed sessions open directly into the report view using `GET /report?sessionId=...`.
4. If no session exists, the ready screen shows the start button.
5. Clicking start always creates a new session and stores `configId + userId` on that session.

Editor / preview mode:

- URL `mode=editor3` is preview and must not query historical sessions.
- URL `userid` may be absent or present, but history lookup is disabled unless `mode=student`.
- The component keeps the current behavior: show ready screen, create a preview session only when start is clicked.
- Preview sessions may omit `userId` and can omit `configId` if the config id is unavailable, though passing `configId` is acceptable for traceability.

## Backend Contract

Extend create session request:

```json
{
  "configId": "speaking-config-uuid",
  "userId": "student-user-id",
  "topic": "...",
  "grade": "...",
  "vocabulary": [],
  "sentences": [],
  "totalRounds": 3,
  "durationMinutes": 5,
  "roleId": "tom"
}
```

`POST /api/speaking/dialogue/session` keeps its create semantics: every call creates a new session.

Add latest lookup:

```text
GET /api/speaking/dialogue/session/latest?configId=...&userId=...
```

If a future session detail route is added, avoid route ambiguity with path parameters. Either define `/session/latest` before `/session/{sessionId}` in FastAPI, or use an unambiguous plural route such as:

```text
GET /api/speaking/dialogue/sessions/latest?configId=...&userId=...
```

Recommended response when found:

```json
{
  "session": {
    "sessionId": "...",
    "status": "active",
    "totalRounds": 3,
    "currentRound": 1,
    "expiresAt": "2026-04-27T10:00:00",
    "createdAt": "2026-04-27T09:55:00",
    "completedAt": null,
    "messages": [
      {
        "id": "...",
        "round": 1,
        "role": "ai",
        "content": "..."
      }
    ]
  }
}
```

Recommended response when not found:

```json
{
  "session": null
}
```

Using a nullable payload avoids making normal first-time student entry look like an error path.

Active historical sessions must restore enough message history for the chat UI to match backend state. The latest lookup may return `messages` directly, or the backend may add a dedicated detail endpoint such as:

```text
GET /api/speaking/dialogue/session/{sessionId}
```

The first implementation should prefer returning `messages` from the latest lookup to keep the frontend flow simple.

## Data Model

Add `config_id` to `dialogue_session`:

```sql
config_id VARCHAR(36) NULL
```

Add an index optimized for latest lookup:

```sql
CREATE INDEX idx_dialogue_session_config_user_created
ON dialogue_session (config_id, user_id, created_at);
```

The lookup should order by `created_at DESC, id DESC` so a student who uses "practice again" later gets the latest attempt.

Existing deployments need a migration in addition to `init.sql`:

```sql
ALTER TABLE dialogue_session ADD COLUMN config_id VARCHAR(36) NULL;
CREATE INDEX idx_dialogue_session_config_user_created
ON dialogue_session (config_id, user_id, created_at);
```

## Frontend Data Flow

Read runtime context in `TopicDiscussionPreview` from the URL:

```ts
const params = new URLSearchParams(window.location.search)
const mode = params.get('mode')
const userId = params.get('userid')
```

This avoids prop drilling through `Student -> ScreenSlideList -> ScreenSlide -> ScreenElement -> BaseFrameElement`. `configId` still comes from PPT JSON via `elementInfo.url`.

For editor rendering:

```text
FrameElement.index.vue
-> TopicDiscussionPreview configId only
```

Extend the dialogue API client:

- `SessionConfig` adds optional `configId` and `userId`.
- `RealDialogueAPI.createSession` sends both fields when present.
- Add `getLatestSession(configId, userId)`.
- Use URL `userid` as backend `userId`.
- `DialogueChatView` / `useDialogueEngine` must support initial historical messages so active session history returned by the backend is visible before the student continues speaking.

## UI States

`TopicDiscussionPreview` should add a small loading state before ready/chat/report:

- `checking-history`: loading historical session only when URL `mode=student`.
- `ready`: no history; show first-time start button.
- `chatting`: active historical session or newly created session.
- `completed`: completed historical session or completed current session, including existing report statuses such as `evaluating`, `ready`, `failed`, and `incomplete`.

If the latest lookup fails because of network/server error, keep the user on ready and show a concise retryable error. Do not silently create a new session, because that would hide history and fragment student records.

## Error Handling

- Missing `configId` when `mode=student`: show ready with an error because the speaking tool cannot be associated with a configured task.
- Missing URL `userid` when `mode=student`: show ready with an error because student history cannot be isolated.
- `mode=editor3`: never query latest session, even if `userid` exists in the URL.
- Latest lookup returns null: normal first-time path.
- Latest lookup returns active session whose `expiresAt` has passed: backend should treat subsequent interaction as completed; frontend should still use the returned session and let existing session APIs enforce status.

## Testing

Backend:

- Create session stores `config_id` and `user_id`.
- Migration adds `config_id` and the `(config_id, user_id, created_at)` index for existing databases.
- Latest lookup returns null when no record exists.
- Latest lookup returns the newest session for the exact `configId + userId` pair.
- Latest lookup returns active session messages, or a detail endpoint exists to fetch them before rendering chat.
- Latest lookup does not leak sessions across students or configs.

Frontend:

- `mode=student` with `configId + userid` queries latest before showing the start button.
- `mode=editor3` with the same `configId + userid` does not query latest.
- `TopicDiscussionPreview` reads URL `mode` and `userid` directly.
- Existing student history skips first-time start and opens the returned session.
- No history shows the start button and create sends `configId + userId`.
- Editor preview remains usable without URL `userid`.

## Out of Scope

- Full historical attempt list UI.
- A "practice again" button. The backend create endpoint already supports this later because every `POST /session` creates a new session.
- Teacher dashboard/report aggregation.
- Deleting or resetting student attempts.
- Changing the existing `DialogueChatView` session ownership boundary.
- Server-side authentication or validation of URL `userid`; current implementation may trust existing URL identity behavior.
