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.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.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.Student mode:
TopicDiscussionPreview receives configId from the frame element and reads URL userid internally.mode=student and both values are present, the component queries the backend for the latest session for that pair.sessionInfo and restored message history;GET /report?sessionId=....configId + userId on that session.Editor / preview mode:
mode=editor3 is preview and must not query historical sessions.userid may be absent or present, but history lookup is disabled unless mode=student.userId and can omit configId if the config id is unavailable, though passing configId is acceptable for traceability.Extend create session request:
{
"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:
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:
GET /api/speaking/dialogue/sessions/latest?configId=...&userId=...
Recommended response when found:
{
"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:
{
"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:
GET /api/speaking/dialogue/session/{sessionId}
The first implementation should prefer returning messages from the latest lookup to keep the frontend flow simple.
Add config_id to dialogue_session:
config_id VARCHAR(36) NULL
Add an index optimized for latest lookup:
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:
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);
Read runtime context in TopicDiscussionPreview from the URL:
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:
FrameElement.index.vue
-> TopicDiscussionPreview configId only
Extend the dialogue API client:
SessionConfig adds optional configId and userId.RealDialogueAPI.createSession sends both fields when present.getLatestSession(configId, userId).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.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.
configId when mode=student: show ready with an error because the speaking tool cannot be associated with a configured task.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.expiresAt has passed: backend should treat subsequent interaction as completed; frontend should still use the returned session and let existing session APIs enforce status.Backend:
config_id and user_id.config_id and the (config_id, user_id, created_at) index for existing databases.configId + userId pair.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.configId + userId.userid.POST /session creates a new session.DialogueChatView session ownership boundary.userid; current implementation may trust existing URL identity behavior.