2026-04-25-english-speaking-task-hint-modal-design.md 11 KB

English Speaking Task Hint Modal — Design

Date: 2026-04-25 Branch: feat/english-speaking Repos affected:

  • Frontend: /Users/buoy/Development/gitrepo/PPT
  • Backend: /Users/buoy/Development/gitrepo/cococlass-english-speaking-api

Goal

Replace 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:

  • Creating a dialogue session must not wait for task-hint generation.
  • The task hint is generated only when the student clicks the hint button.
  • The frontend never consumes raw LLM output.
  • The backend always returns a valid TaskHint object, using deterministic fallback content when LLM output cannot be parsed or validated.

Non-goals

  • No task-hint generation during POST /session.
  • No multi-request LLM repair loop.
  • No model-specific dependency on structured-output APIs.
  • No strict JSON Schema mode requirement for MVP.
  • No frontend prompt construction.
  • No frontend parsing of LLM text.
  • No redesign of the modal visual style beyond moving it into a component.

Current State

DialogueChatView.vue currently owns all task hint UI and content:

  • The hint modal template is inline.
  • sentenceHints and vocabHints are static frontend arrays.
  • The current question is a frontend template using aiName and topic.
  • The hint button directly toggles 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,
}

Architecture

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

Frontend Design

Component Extraction

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:

  • Hidden when visible === false
  • Loading while the request is in flight
  • Error with retry action when generation fails at the transport layer
  • Content when hint is available

Backend 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 State

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.

Frontend Types

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>

Frontend API

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.

Backend API

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:

  • If a task hint has already been generated or fallback-created for the session, return the stored TaskHint directly.
  • Do not call the LLM again for the same session after the first successful endpoint completion.
  • This backend cache is required in addition to the frontend's mounted-component cache, so refreshing the page or remounting the chat view still reuses the existing hint.

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.

LLM Generation Strategy

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.

Prompt Requirements

The prompt should ask for exactly one JSON object and no Markdown code block. It should include:

  • practice_level: direct session grade
  • conversation_topic: direct session topic
  • current_question: 1-2 Chinese sentences, friendly and encouraging
  • example_sentences: exactly 3 objects with english and chinese
  • key_vocabulary: exactly 4 objects with word and meaning

Correct 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.

Validation Rules

The backend validates the parsed object:

  • Root is an object.
  • Required string fields exist and are non-empty.
  • example_sentences is an array.
  • Each example sentence has non-empty english and chinese.
  • key_vocabulary is an array.
  • Each vocabulary item has non-empty word and meaning.

Normalization:

  • Trim strings.
  • Keep the first 3 valid example sentences.
  • Keep the first 4 valid vocabulary items.
  • If too few valid items remain, fill from deterministic fallback.
  • Ignore unknown fields.

Fallback Strategy

Fallback is local code, not another LLM request.

Use session values:

current_question =
  `和 Tom 聊一聊「${topic}」,试着用今天的重点词汇和句型表达你的想法。`

For example_sentences:

  • Prefer session sentences as the English side.
  • Use "参考译文待补充" as a safe MVP Chinese placeholder if translation is unavailable.
  • Fill to 3 items with simple topic-safe sentence templates.

For key_vocabulary:

  • Prefer session vocabulary.
  • Fill to 4 items with topic-safe common words only if needed.
  • Use "重点词汇" 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.

Error Handling

Backend:

  • Missing session: return 404.
  • Existing cached task hint: return it with 200 and skip LLM generation.
  • LLM provider/network failure: return fallback TaskHint with 200 unless the service cannot read the session.
  • LLM parse/schema failure: return fallback TaskHint with 200.
  • Unexpected server error before fallback construction: return 500.

Frontend:

  • 404 or 5xx: show modal error state with retry.
  • Successful fallback response: render content normally.
  • Reopening the modal in the same mounted session uses cached taskHint.

Testing

Frontend:

  • TaskHintModal renders loading, error, and content states.
  • DialogueChatView calls generateTaskHint() only on first hint open.
  • Retry calls generateTaskHint() again after transport failure.
  • Reopen after success does not call the API again.

Backend:

  • Endpoint returns cached task hint without calling the LLM when one already exists for the session.
  • Endpoint returns generated valid task hint for valid LLM JSON.
  • Endpoint returns fallback when LLM returns malformed JSON.
  • Endpoint returns fallback when LLM returns valid JSON with missing fields.
  • Endpoint normalizes arrays to 3 example sentences and 4 vocabulary items.
  • Endpoint returns 404 for missing session.

Implementation Order

  1. Add TaskHint types and DialogueAPI.generateTaskHint().
  2. Add real and mock API implementations.
  3. Extract TaskHintModal.vue from the inline modal markup/styles.
  4. Wire DialogueChatView.vue lazy-load state and button behavior.
  5. Add backend endpoint, service function, and per-session task-hint storage/cache.
  6. Add backend prompt, parse/validate/normalize/fallback logic.
  7. Add focused frontend and backend tests.