Jelajahi Sumber

feat(speaking): attachSession takes totalRounds; add classifyError helper

Engine now tracks totalRounds and exposes isFinalRound (used in Task 12
to skip aiMsg push on the final turn). New module-level classifyError
maps backend error strings + HTTP status to one of three recovery
kinds (retry / rerecord / restart) — used in Tasks 12-14 to populate
PreviewChatMessage.recovery on every error path.
jimmylee 2 minggu lalu
induk
melakukan
f0cf94e70a

+ 23 - 1
src/views/Editor/EnglishSpeaking/composables/useDialogueEngine.ts

@@ -7,6 +7,7 @@ export function useDialogueEngine() {
   const sessionId = ref<string | null>(null)
   const expiresAt = ref<string | null>(null)
   const currentRound = ref(1)
+  const totalRounds = ref<number>(3)
   const isComplete = ref(false)
   const countdownSeconds = ref<number | null>(null)
 
@@ -16,6 +17,7 @@ export function useDialogueEngine() {
 
   const isProcessing = computed(() => messages.value.some(m => m.status === 'loading'))
   const canRecord = computed(() => !isProcessing.value && !isComplete.value)
+  const isFinalRound = computed(() => currentRound.value >= totalRounds.value)
 
   // ==================== Session Attach ====================
 
@@ -27,9 +29,14 @@ export function useDialogueEngine() {
    *  expiresAt 由后端在 createSession 时基于 durationMinutes 计算下发,前端只读 ——
    *  保证关页面/刷新/换设备重进都能接续同一个截止时刻,倒计时不暂停。
    */
-  function attachSession(info: { sessionId: string; expiresAt?: string | null }) {
+  function attachSession(info: {
+    sessionId: string
+    expiresAt?: string | null
+    totalRounds: number
+  }) {
     sessionId.value = info.sessionId
     expiresAt.value = info.expiresAt ?? null
+    totalRounds.value = info.totalRounds
     if (info.expiresAt) startCountdown(info.expiresAt)
   }
 
@@ -419,11 +426,13 @@ export function useDialogueEngine() {
     messages,
     sessionId,
     currentRound,
+    totalRounds,
     isComplete,
     isProcessing,
     canRecord,
     countdownSeconds,
     greetingInflight,
+    isFinalRound,
 
     attachSession,
     generateGreeting,
@@ -466,3 +475,16 @@ function friendlyErrorMessage(raw: string | undefined): string {
   if (!raw) return '请求失败,请重试'
   return map[raw] || raw
 }
+
+export type Recovery = 'retry' | 'rerecord' | 'restart'
+
+export function classifyError(
+  raw: string | undefined,
+  status: number | undefined,
+  role: 'student' | 'ai',
+): Recovery {
+  if (status === 404 || status === 409) return 'restart'
+  if (raw === 'Session not found' || raw === 'Session is not active') return 'restart'
+  if (raw === 'No speech detected' && role === 'student') return 'rerecord'
+  return 'retry'
+}

+ 5 - 1
src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

@@ -967,7 +967,11 @@ watch(
 
 onMounted(() => {
   if (props.sessionInfo) {
-    engine.attachSession(props.sessionInfo)
+    engine.attachSession({
+      sessionId: props.sessionInfo.sessionId,
+      expiresAt: props.sessionInfo.expiresAt,
+      totalRounds: props.totalRounds,
+    })
     engine.generateGreeting()
   } else {
     console.warn('[DialogueChatView] mounted without sessionInfo; chat is inert. Parent must createSession before mounting.')