Przeglądaj źródła

feat(speaking): wire DialogueChatView to new sessionInfo prop + greeting flow

- New sessionInfo prop (from parent) replaces engine.initSession auto-trigger
- On mount: attachSession + generateGreeting → user sees typing bubble immediately
- Error retry button: three branches (retryGreeting / restart / regenerateAi)
  driven by message.unrecoverable flag and presence of prior student message
- handleRestart emits 'restart' instead of recreating session internally;
  parent returns to ready stage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 2 tygodni temu
rodzic
commit
5eb84faa8f

+ 35 - 27
src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

@@ -84,7 +84,11 @@
             <!-- AI 错误 -->
             <div v-if="message.status === 'error'" class="error-card">
               <span class="error-text">{{ message.error || '生成失败' }}</span>
-              <button class="retry-btn" @click="engine.regenerateAiMessage(message.id)">重新生成</button>
+              <button
+                class="retry-btn"
+                :disabled="engine.greetingInflight.value"
+                @click="retryAiMessage(message)"
+              >{{ message.unrecoverable ? '返回重开' : '重新生成' }}</button>
             </div>
           </div>
         </div>
@@ -497,6 +501,7 @@ interface Props {
   mode?: 'preview' | 'real'
   showEnglishText?: boolean
   showChineseText?: boolean
+  sessionInfo?: { sessionId: string; expiresAt: string | null } | null
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -508,9 +513,13 @@ const props = withDefaults(defineProps<Props>(), {
   mode: 'preview',
   showEnglishText: true,
   showChineseText: false,
+  sessionInfo: null,
 })
 
-const emit = defineEmits<{ complete: [report: DialogueReport | null] }>()
+const emit = defineEmits<{
+  complete: [report: DialogueReport | null]
+  restart: []
+}>()
 
 // ─────────────────────────────────────────────
 // Config
@@ -726,6 +735,24 @@ function handleRetry() {
   else engine.regenerateAiMessage(last.id)
 }
 
+function retryAiMessage(message: PreviewChatMessage) {
+  const idx = engine.messages.value.indexOf(message)
+  const hasPrevStudent = engine.messages.value.slice(0, idx).some(m => m.role === 'student')
+
+  // 第一条 AI 消息(前面没有学生消息)= greeting
+  if (!hasPrevStudent) {
+    if (message.unrecoverable) {
+      emit('restart')
+      return
+    }
+    engine.retryGreeting()
+    return
+  }
+
+  // 非首条:沿用原 regenerate
+  engine.regenerateAiMessage(message.id)
+}
+
 function toggleExpand(id: string) {
   expandedMessageId.value = expandedMessageId.value === id ? null : id
 }
@@ -818,24 +845,7 @@ function handleRestart() {
   showExitConfirm.value = false
   engine.abort()
   engine.cancelTTS()
-  engine.messages.value = []
-  engine.currentRound.value = 1
-  engine.isComplete.value = false
-  expandedMessageId.value = null
-  playingMessageId.value = null
-  phonemeDetail.value = null
-  silenceHintText.value = ''
-  showBadge.value = null
-  consecutiveFluent.value = 0
-  consecutiveAccurate.value = 0
-  totalSeconds.value = 0
-
-  engine.initSession({
-    topic: props.topic,
-    roleId: 'tom',
-    totalRounds: props.totalRounds,
-    vocabulary: props.keywords,
-  })
+  emit('restart')
 }
 
 // ─────────────────────────────────────────────
@@ -948,15 +958,13 @@ watch(
 // ─────────────────────────────────────────────
 
 onMounted(() => {
-  engine.initSession({
-    topic: props.topic,
-    roleId: 'tom',
-    totalRounds: props.totalRounds,
-    vocabulary: props.keywords,
-  })
+  if (props.sessionInfo) {
+    engine.attachSession(props.sessionInfo)
+    engine.generateGreeting()
+  }
+  // 无 sessionInfo 时聊天区保持空(父组件应当先创建 session 再挂载本组件)
 
   totalTimer = setInterval(() => {
-    // 仅当 engine 未接管倒计时时显示正计时
     if (engine.countdownSeconds.value == null) totalSeconds.value += 1
   }, 1000)
 })