Date: 2026-04-25
Branch: feat/english-speaking
Repos affected:
/Users/buoy/Development/gitrepo/PPT/Users/buoy/Development/gitrepo/cococlass-english-speaking-apiReplace the hardcoded task hint modal inside DialogueChatView.vue with a dedicated component and a lazy-loaded backend endpoint. The hint should use the same session inputs as the dialogue prompt: grade, topic, key vocabulary, and key sentence patterns.
The MVP should keep the user-facing flow fast and robust:
TaskHint object, using deterministic fallback content when LLM output cannot be parsed or validated.POST /session.DialogueChatView.vue currently owns all task hint UI and content:
sentenceHints and vocabHints are static frontend arrays.aiName and topic.showHintModal.Session creation already sends the required source inputs through RealDialogueAPI.createSession():
{
topic: config.topic,
grade: config.grade,
vocabulary: config.vocabulary ?? [],
sentences: config.sentences ?? [],
totalRounds: config.totalRounds,
roleId: config.roleId,
}
User clicks "提示"
↓
DialogueChatView.openTaskHint()
↓
show TaskHintModal immediately
↓
if cached hint exists for this mounted session:
render cached hint
else:
POST /api/speaking/dialogue/session/{sessionId}/task-hint
↓
Backend reads session grade/topic/vocabulary/sentences
↓
if cached TaskHint exists for this session:
return cached TaskHint without calling the LLM
else:
Single LLM request asks for JSON text
↓
Backend parses + validates + normalizes
↓
if valid: persist and return normalized TaskHint
if invalid: persist and return deterministic fallback TaskHint
Create:
src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue
TaskHintModal.vue is presentational. It does not know about sessions, APIs, prompts, or LLMs.
Props:
interface TaskHintModalProps {
visible: boolean
loading: boolean
error?: string | null
hint?: TaskHint | null
aiName?: string
}
Emits:
close
retry
The component renders four states:
visible === falsehint is availableBackend parse or schema failure should not normally produce the error state because the backend returns fallback content. The frontend error state is for network, auth, 5xx, or aborted request failures.
DialogueChatView.vue should own the API call and cache for the mounted session:
const showHintModal = ref(false)
const taskHint = ref<TaskHint | null>(null)
const taskHintLoading = ref(false)
const taskHintError = ref<string | null>(null)
Functions:
function openTaskHint() {
showHintModal.value = true
if (!taskHint.value && !taskHintLoading.value) {
loadTaskHint()
}
}
async function loadTaskHint() {
if (!props.sessionInfo?.sessionId) {
taskHintError.value = '当前会话未准备好,请稍后重试'
return
}
taskHintLoading.value = true
taskHintError.value = null
try {
taskHint.value = await api.generateTaskHint(props.sessionInfo.sessionId)
} catch {
taskHintError.value = '生成任务提示失败,请重试'
} finally {
taskHintLoading.value = false
}
}
The hint button should call openTaskHint() instead of assigning showHintModal = true.
Add to src/types/englishSpeaking.ts:
export interface TaskHint {
practice_level: string
conversation_topic: string
current_question: string
example_sentences: {
english: string
chinese: string
}[]
key_vocabulary: {
word: string
meaning: string
}[]
}
Extend DialogueAPI:
generateTaskHint(sessionId: string): Promise<TaskHint>
Add to RealDialogueAPI:
async generateTaskHint(sessionId: string): Promise<TaskHint> {
const res = await fetch(`${API_BASE}/session/${sessionId}/task-hint`, {
method: 'POST',
credentials: 'include',
})
if (!res.ok) {
throw new DialogueApiError(`task hint failed: ${res.status}`, res.status)
}
return await res.json()
}
MockDialogueAPI.generateTaskHint() should return a local mock object shaped like TaskHint.
Add:
POST /api/speaking/dialogue/session/{sessionId}/task-hint
Response:
{
"practice_level": "grade5-1",
"conversation_topic": "How I get to school",
"current_question": "和 Tom 聊一聊 How I get to school,试着用今天的重点词汇和句型表达你的想法。",
"example_sentences": [
{
"english": "I come to school by bus.",
"chinese": "我乘公交车来学校。"
},
{
"english": "I live near my school.",
"chinese": "我住得离学校很近。"
},
{
"english": "It takes me ten minutes to get there.",
"chinese": "我到那里需要十分钟。"
}
],
"key_vocabulary": [
{
"word": "by bus",
"meaning": "乘公交车"
},
{
"word": "on foot",
"meaning": "步行"
},
{
"word": "near",
"meaning": "附近的"
},
{
"word": "far from",
"meaning": "离……远"
}
]
}
The endpoint reads course variables from the existing session record. The frontend should not resend grade, topic, vocabulary, or sentences for this endpoint.
The endpoint is idempotent per dialogue session:
TaskHint directly.Storage can be implemented with a JSON column on the dialogue session, a separate one-row-per-session table, or an existing session metadata store. For MVP, prefer the smallest storage change that fits the current backend schema.
MVP decision:
Use one LLM request that asks for JSON text.
Do not use a retry/repair request.
Do not require provider-specific structured output.
If the provider supports JSON mode, the backend may enable it.
Regardless of provider support, always parse and validate server-side.
On parse or validation failure, return deterministic fallback content.
Persist whichever valid `TaskHint` is returned, including fallback.
This keeps the MVP provider-agnostic while still benefiting from JSON mode when available in providers such as Qwen, DeepSeek, Kimi, or GLM.
The prompt should ask for exactly one JSON object and no Markdown code block. It should include:
practice_level: direct session gradeconversation_topic: direct session topiccurrent_question: 1-2 Chinese sentences, friendly and encouragingexample_sentences: exactly 3 objects with english and chinesekey_vocabulary: exactly 4 objects with word and meaningCorrect the original placeholder typos:
{{重点词汇)}} becomes {{重点词汇}}{{重点句型)}} becomes {{重点句型}}The example in the prompt must also contain exactly 3 example sentences and 4 vocabulary items so the example does not contradict the rules.
The backend validates the parsed object:
example_sentences is an array.english and chinese.key_vocabulary is an array.word and meaning.Normalization:
Fallback is local code, not another LLM request.
Use session values:
current_question =
`和 Tom 聊一聊「${topic}」,试着用今天的重点词汇和句型表达你的想法。`
For example_sentences:
sentences as the English side."参考译文待补充" as a safe MVP Chinese placeholder if translation is unavailable.For key_vocabulary:
vocabulary."重点词汇" as a safe MVP meaning when no local dictionary is available.The fallback does not need to be as polished as LLM output. It only needs to keep the modal usable and the response contract valid.
Backend:
TaskHint with 200 unless the service cannot read the session.TaskHint with 200.Frontend:
taskHint.Frontend:
TaskHintModal renders loading, error, and content states.DialogueChatView calls generateTaskHint() only on first hint open.generateTaskHint() again after transport failure.Backend:
TaskHint types and DialogueAPI.generateTaskHint().TaskHintModal.vue from the inline modal markup/styles.DialogueChatView.vue lazy-load state and button behavior.