name: Speaking — record start as a course-work entry description: After clicking "开始对话" on the English-Speaking tool (toolType=77), call addCourseWorks_workPage once with content=sessionId so the teacher dashboard sees the student has begun this slide. type: design
After a student clicks 开始对话 on the English-Speaking tool (toolType=77), call the existing pbl-api endpoint addCourseWorks_workPage exactly once, with content = sessionId, so the teacher-side course-works query can see that the student has begun this slide.
content field stays as the sessionId for the lifetime of the record. The dialogue report is fetched/displayed by the existing createSession / getReport flow and is not pushed back into addCourseWorks_workPage.api.submitWork(params) — wrapper at src/services/course.ts:45, posts to ${API_URL}addCourseWorks_workPage. Required fields: {uid, cid, stage, task, tool, atool, content, type}.Student/index.vue:1883 — submitWork(slideIndex, atool, content, type) helper for tools 72/73 already follows this pattern with stage='0', tool='0', task=String(slideIndex).TopicDiscussionPreview.vue:299 — startDialogue() calls api.createSession(...) then sets dialogueState='chatting' (which mounts DialogueChatView.vue).TopicDiscussionPreview.vue:375 — loadLatestStudentSession(...) is the resume path: when an unfinished active session exists, the user is dropped straight into 'chatting' without going through startDialogue().Student/index.vue:583 — already provides notifySpeakingProgress (Yjs broadcast only, no HTTP).pbl-teacher-table calls addCourseWorks_workPage only on submit (Q&A type 15, MC type 45) — not on start. The "call on start" pattern is new for tool 77.
TopicDiscussionPreview does not know cid (courseid) or task (slideIndex). Those live in Student/index.vue. So the call cannot live entirely inside TopicDiscussionPreview — the cleanest seam is provide from Student/index.vue, inject in TopicDiscussionPreview.
recordSpeakingStart(sessionId)Three approaches were considered:
notifySpeakingProgress provider with a fresh: boolean flag. Rejected: overloads one provider with two responsibilities (Yjs broadcast + HTTP) and the name doesn't hint at the HTTP side-effect.recordSpeakingStart(sessionId). The fresh-vs-resume distinction lives at the call site (only startDialogue() calls it; the resume path doesn't). Single responsibility, clear name.cid + slideIndex into TopicDiscussionPreview and call api.submitWork directly there. Rejected: more inject churn for no benefit; pushes pbl-api IO into a preview component.1. src/views/Student/index.vue — provide a new function alongside notifySpeakingProgress:
provide('recordSpeakingStart', async (sessionId: string) => {
if (props.type !== '2') return // student client only
if (!sessionId || !props.courseid || !props.userid) return
try {
await api.submitWork({
uid: props.userid as string,
cid: props.courseid as string,
stage: '0',
task: String(slideIndex.value),
tool: '0',
atool: '77',
content: sessionId,
type: '21',
})
} catch (err) {
console.error('[speaking] recordSpeakingStart failed:', err)
}
})
The same provide should also be added to src/views/Student/index2.vue (mirror file with the same submitWork helper at line 1654).
2. src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue — inject and call from startDialogue() only:
const recordSpeakingStart = inject<(sessionId: string) => void>(
'recordSpeakingStart',
() => {},
)
Inside startDialogue(), after preparedSession.value is set and immediately after the existing notifySpeakingProgress('active', ...) call (line 333) — so the Yjs broadcast fires first, then the HTTP record-start kicks off without blocking it:
recordSpeakingStart(preparedSession.value.sessionId)
The resume path (loadLatestStudentSession, around line 425) must not call recordSpeakingStart.
| Field | Value | Source |
|---|---|---|
uid |
current student id | Student/index.vue props.userid |
cid |
course id | Student/index.vue props.courseid |
stage |
'0' |
fixed (existing convention) |
task |
String(slideIndex) |
slideIndex from slidesStore |
tool |
'0' |
fixed (existing convention) |
atool |
'77' |
new — speaking tool |
content |
sessionId | preparedSession.sessionId |
type |
'21' |
new — speaking tool |
props.type === '2' (student client)startDialogue() succeeded — createSession() returned and preparedSession.value.sessionId is setprops.courseid and props.userid are non-emptyloadLatestStudentSession finds an active session and jumps to 'chatting')handleDialogueComplete)props.type !== '2')console.error-logged and never block the dialogue. The speaking session on speaking-api is already created at this point and continues normally.mode=student&userid=... query params).createSession followed by a POST .../addCourseWorks_workPage in DevTools network tab with content = the new sessionId, atool='77', type='21', task = current slide index.'chatting' regardless.'chatting' (via loadLatestStudentSession).addCourseWorks_workPage call should be observed in this case.props.type !== '2').addCourseWorks_workPage call should fire even if "开始对话" is clicked.addCourseWorks_workPage in DevTools (override response with 500).'chatting' and console.error is logged.provide change needs to be mirrored in src/views/Student/index2.vue (parallel file). Confirm whether index2.vue is reachable in the speaking flow before merging.atool='77' and type='21' are agreed on with the user; if the teacher-side dashboard uses an existing filter that does not include these values, the work record may not surface. Worth a quick smoke check on the teacher view after deploy.