Przeglądaj źródła

docs: design english speaking task hint modal

jimmylee 2 tygodni temu
rodzic
commit
282e7c2372

+ 373 - 0
docs/superpowers/specs/2026-04-25-english-speaking-task-hint-modal-design.md

@@ -0,0 +1,373 @@
+# 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()`:
+
+```ts
+{
+  topic: config.topic,
+  grade: config.grade,
+  vocabulary: config.vocabulary ?? [],
+  sentences: config.sentences ?? [],
+  totalRounds: config.totalRounds,
+  roleId: config.roleId,
+}
+```
+
+## Architecture
+
+```text
+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
+    ↓
+  Single LLM request asks for JSON text
+    ↓
+  Backend parses + validates + normalizes
+    ↓
+  if valid: return normalized TaskHint
+  if invalid: return deterministic fallback TaskHint
+```
+
+## Frontend Design
+
+### Component Extraction
+
+Create:
+
+```text
+src/views/Editor/EnglishSpeaking/preview/TaskHintModal.vue
+```
+
+`TaskHintModal.vue` is presentational. It does not know about sessions, APIs, prompts, or LLMs.
+
+Props:
+
+```ts
+interface TaskHintModalProps {
+  visible: boolean
+  loading: boolean
+  error?: string | null
+  hint?: TaskHint | null
+  aiName?: string
+}
+```
+
+Emits:
+
+```ts
+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:
+
+```ts
+const showHintModal = ref(false)
+const taskHint = ref<TaskHint | null>(null)
+const taskHintLoading = ref(false)
+const taskHintError = ref<string | null>(null)
+```
+
+Functions:
+
+```ts
+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`:
+
+```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`:
+
+```ts
+generateTaskHint(sessionId: string): Promise<TaskHint>
+```
+
+### Frontend API
+
+Add to `RealDialogueAPI`:
+
+```ts
+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:
+
+```text
+POST /api/speaking/dialogue/session/{sessionId}/task-hint
+```
+
+Response:
+
+```json
+{
+  "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.
+
+## LLM Generation Strategy
+
+MVP decision:
+
+```text
+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.
+```
+
+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:
+
+```ts
+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.
+- 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 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 and service function.
+6. Add backend prompt, parse/validate/normalize/fallback logic.
+7. Add focused frontend and backend tests.