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: When a student clicks "开始对话" on a tool-77 (English-Speaking) frame and a fresh sessionId is created, fire addCourseWorks_workPage once with content = sessionId so the teacher dashboard sees the student has begun this slide.
Architecture: Add a new recordSpeakingStart(sessionId) provider in Student/index.vue (which has access to props.userid, props.courseid, and slideIndex). TopicDiscussionPreview.vue injects it and calls it from the fresh-start path inside startDialogue() only — never on resume, completion, or in teacher mode. Failure is fire-and-forget; idempotency is enforced purely by the call-site rule.
Tech Stack: Vue 3 (Composition API, <script setup>), TypeScript, Pinia, existing api.submitWork wrapper at src/services/course.ts:45.
Spec: docs/superpowers/specs/2026-05-08-speaking-record-start-work-design.md
| File | Change |
|---|---|
src/views/Student/index.vue |
Add provide('recordSpeakingStart', ...) next to existing provide('notifySpeakingProgress', ...) (around line 583). |
src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue |
Add inject('recordSpeakingStart', ...) next to existing inject('notifySpeakingProgress', ...) (around line 114), then call it inside startDialogue() immediately after the existing notifySpeakingProgress('active', ...) (around line 333). |
src/views/Student/index2.vue does not need changes — verified that it does not provide notifySpeakingProgress and the speaking flow does not route through it.
No new files, no test files (this codebase has no unit-test infrastructure for the affected Vue components — verification is manual; see Task 3).
recordSpeakingStart in Student/index.vueFiles:
src/views/Student/index.vue (insert a new provide(...) block immediately after the existing notifySpeakingProgress block at lines 583-593)Context for the engineer:
api is the default import from @/services/course (already imported at line 422). It exposes api.submitWork({uid, cid, stage, task, tool, atool, content, type}) which POSTs to addCourseWorks_workPage.slideIndex is from storeToRefs(slidesStore) (already destructured at line 485). Use slideIndex.value to read the current value.props.userid, props.courseid, and props.type exist on this component. props.type === '2' means student client (type === '1' is teacher).The existing pattern at Student/index.vue:1883 (submitWork(slideIndex, atool, content, type)) shows the convention: stage='0', tool='0', task=String(slideIndex). Follow it exactly.
[ ] Step 1: Insert the new provider
Find the block at src/views/Student/index.vue:583-593:
provide('notifySpeakingProgress', (status: 'active' | 'completed', payload: { configId: string; sessionId: string }) => {
if (props.type !== '2') return // 只有学生客户端发广播
sendMessage({
type: 'speaking_session_updated',
courseid: props.courseid,
slideIndex: slideIndex.value,
userid: props.userid,
status,
...payload,
})
})
Insert this immediately after that block (still at the top level of <script setup>, same indentation):
provide('recordSpeakingStart', async (sessionId: string) => {
if (props.type !== '2') return // 只有学生客户端写作业记录
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)
}
})
Run: cd /Users/buoy/Development/gitrepo/PPT && npm run type-check
Expected: PASS (no new TS errors). If the codebase already has pre-existing errors, confirm none of them are inside Student/index.vue and none reference recordSpeakingStart or submitWork.
git -C /Users/buoy/Development/gitrepo/PPT add src/views/Student/index.vue
git -C /Users/buoy/Development/gitrepo/PPT commit -m "$(cat <<'EOF'
feat(speaking): provide recordSpeakingStart for course-work entry
Adds a provider that posts addCourseWorks_workPage once with
content=sessionId, atool='77', type='21' for the student client only.
Failure is logged and swallowed; the dialogue is not blocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
startDialogue() in TopicDiscussionPreview.vueFiles:
src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue (add inject around line 114; add call inside startDialogue() around line 333)Context for the engineer:
const notifySpeakingProgress = inject<SpeakingNotify>('notifySpeakingProgress', () => {}) at line 114 — model the new inject after it.startDialogue() at line 299. After createSession returns, preparedSession.value is set with { sessionId, expiresAt, currentRound } (line 327-331), then dialogueState.value = 'chatting' (line 332), then notifySpeakingProgress('active', { configId, sessionId }) (line 333-336).loadLatestStudentSession() at line 375. It also fires notifySpeakingProgress('active', ...) at line 426. DO NOT call recordSpeakingStart from this path — that would write a duplicate work record on every page refresh.The completion path handleDialogueComplete at line 442 fires notifySpeakingProgress('completed', ...). DO NOT call recordSpeakingStart from this path either.
[ ] Step 1: Add the inject
Find the existing line at src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue:114:
const notifySpeakingProgress = inject<SpeakingNotify>('notifySpeakingProgress', () => {})
Insert immediately after it:
const recordSpeakingStart = inject<(sessionId: string) => void>('recordSpeakingStart', () => {})
startDialogue() onlyFind the block at src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue:332-336:
dialogueState.value = 'chatting'
notifySpeakingProgress('active', {
configId: props.configId || '',
sessionId: preparedSession.value?.sessionId || '',
})
Add a new line immediately after the closing }) of notifySpeakingProgress, still inside the try block, still at the same indentation as notifySpeakingProgress:
recordSpeakingStart(preparedSession.value?.sessionId || '')
After the edit, that block should read:
dialogueState.value = 'chatting'
notifySpeakingProgress('active', {
configId: props.configId || '',
sessionId: preparedSession.value?.sessionId || '',
})
recordSpeakingStart(preparedSession.value?.sessionId || '')
Run: grep -n "recordSpeakingStart" /Users/buoy/Development/gitrepo/PPT/src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue
Expected output: exactly 2 lines — one inject(...) line near line 115, one recordSpeakingStart(...) call inside startDialogue near line 337. Not 3 lines, not any line near 426 (resume) or 452 (completion).
If grep shows more than 2 lines, remove the extras.
Run: cd /Users/buoy/Development/gitrepo/PPT && npm run type-check
Expected: PASS (no new TS errors).
git -C /Users/buoy/Development/gitrepo/PPT add src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue
git -C /Users/buoy/Development/gitrepo/PPT commit -m "$(cat <<'EOF'
feat(speaking): record start work entry on fresh "开始对话" click
Injects recordSpeakingStart and fires it once inside startDialogue(),
right after the existing notifySpeakingProgress('active', ...) broadcast.
The resume path (loadLatestStudentSession) and completion path
(handleDialogueComplete) deliberately do NOT call it — idempotency is
enforced purely at the call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Context for the engineer:
You need student-mode access to a course slide containing a tool-77 (English-Speaking) frame. The student-mode URL has ?mode=student&userid=<id> query params, and the parent Student/index.vue must have been loaded with props.courseid and props.userid set. If you don't have such a course set up, ask the user for a test URL before proceeding.
[ ] Step 1: Start the dev server
Run: cd /Users/buoy/Development/gitrepo/PPT && npm run dev
Expected: Vite dev server starts, prints a local URL.
addCourseWorks_workPage.POST .../addCourseWorks_workPage request. Inspect the request body — the single-element array should contain:
uid = the student id (matches userid query param)cid = the course idstage = "0"task = the current slide index as a stringtool = "0"atool = "77"content = the same sessionId returned by the immediately preceding createSession calltype = "21"'chatting' (the chat view appears).Expected: all of the above. If any field mismatches, fix the implementation before continuing.
'chatting' (do not finish), refresh the page.'chatting' (via loadLatestStudentSession finding an active session).POST .../addCourseWorks_workPage request should appear in the Network tab during this resume.Expected: zero new addCourseWorks_workPage calls. If one fires, the call accidentally landed in the resume path — review Task 2 Step 3.
mode=student query param, or mode=teacher — whatever sets props.type === '1' on Student/index.vue).POST .../addCourseWorks_workPage request should appear.Expected: zero addCourseWorks_workPage calls.
addCourseWorks_workPage → Block request URL (or use Override > "Failure response").console.error shows [speaking] recordSpeakingStart failed: ..., and the dialogue still proceeds to 'chatting' normally.Expected: dialogue UI is uninterrupted; only a console error.
If any of Steps 2-5 fails, do not commit further. Report which step failed, the actual observed behavior, and the diff between expected and actual. The user will decide whether to adjust the spec or fix the implementation.
This task produces no code changes. If all checks passed, mark the plan complete and report back to the user.
docs/superpowers/specs/2026-05-08-speaking-record-start-work-design.md is covered:
Student/index.vue → Task 1TopicDiscussionPreview → Task 2index2.vue open question → resolved during planning (no change needed)recordSpeakingStart signature is (sessionId: string) => void in both the provide (returns Promise<void>, callers use void because they don't await) and the inject. The mismatch (provide returns Promise, inject types as void) is intentional — the call site fire-and-forgets.