Просмотр исходного кода

feat(speaking): TopicDiscussionPreview creates session on start button click

- 'Start dialogue' button now triggers POST /session before navigating
- Button shows loading spinner while creating; disabled during in-flight
- On failure: error text below button, button re-enabled for retry
- On success: passes sessionInfo prop to DialogueChatView so it can attach
  and trigger greeting immediately
- Listens to child 'restart' emit to return to ready stage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 2 недель назад
Родитель
Сommit
39f6b8286f
1 измененных файлов с 64 добавлено и 6 удалено
  1. 64 6
      src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue

+ 64 - 6
src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue

@@ -16,16 +16,18 @@
       </div>
       </div>
 
 
       <div class="ready-footer">
       <div class="ready-footer">
-        <button class="start-btn" @click="startDialogue">
-          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+        <button class="start-btn" :disabled="sessionCreating" @click="startDialogue">
+          <svg v-if="!sessionCreating" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
             stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
             <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
             <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
             <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
             <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
             <line x1="12" y1="19" x2="12" y2="23" />
             <line x1="12" y1="19" x2="12" y2="23" />
             <line x1="8" y1="23" x2="16" y2="23" />
             <line x1="8" y1="23" x2="16" y2="23" />
           </svg>
           </svg>
-          开始对话
+          <span v-else class="start-btn-spinner" />
+          {{ sessionCreating ? '创建中…' : '开始对话' }}
         </button>
         </button>
+        <p v-if="sessionError" class="session-error-text">{{ sessionError }}</p>
       </div>
       </div>
     </div>
     </div>
 
 
@@ -38,7 +40,9 @@
       :ai-avatar="mockRole.avatar"
       :ai-avatar="mockRole.avatar"
       :total-rounds="speakingStore.config.practice.rounds || totalRounds"
       :total-rounds="speakingStore.config.practice.rounds || totalRounds"
       :mode="mode"
       :mode="mode"
+      :session-info="preparedSession"
       @complete="handleDialogueComplete"
       @complete="handleDialogueComplete"
+      @restart="resetPreview"
     />
     />
 
 
     <!-- Completed 阶段:直接渲染报告 -->
     <!-- Completed 阶段:直接渲染报告 -->
@@ -61,9 +65,10 @@
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
 import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
-import type { DialogueReport, OverallEvaluation, PreviewAIRole, PreviewDialogueState } from '@/types/englishSpeaking'
+import type { DialogueReport, OverallEvaluation, PreviewAIRole, PreviewDialogueState, SessionStartInfo } from '@/types/englishSpeaking'
 import { useSpeakingStore } from '@/store/speaking'
 import { useSpeakingStore } from '@/store/speaking'
 import { getSpeakingConfig } from '@/services/speaking'
 import { getSpeakingConfig } from '@/services/speaking'
+import { createDialogueApi, DialogueApiError } from '../services/llmService'
 
 
 import DialogueChatView from './DialogueChatView.vue'
 import DialogueChatView from './DialogueChatView.vue'
 import OverallReport from './OverallReport.vue'
 import OverallReport from './OverallReport.vue'
@@ -87,6 +92,9 @@ const props = withDefaults(defineProps<Props>(), {
 })
 })
 
 
 const dialogueState = ref<PreviewDialogueState>('ready')
 const dialogueState = ref<PreviewDialogueState>('ready')
+const sessionCreating = ref(false)
+const sessionError = ref<string | null>(null)
+const preparedSession = ref<SessionStartInfo | null>(null)
 
 
 const mockRole: PreviewAIRole = {
 const mockRole: PreviewAIRole = {
   id: 'tom',
   id: 'tom',
@@ -195,8 +203,32 @@ const displayEvaluation = computed<OverallEvaluation>(() => {
   return mockEvaluation
   return mockEvaluation
 })
 })
 
 
-function startDialogue() {
-  dialogueState.value = 'chatting'
+async function startDialogue() {
+  if (sessionCreating.value) return
+  sessionCreating.value = true
+  sessionError.value = null
+  try {
+    const api = createDialogueApi(props.mode)
+    const info = await api.createSession({
+      topic: speakingStore.config.topic || props.topic,
+      totalRounds: speakingStore.config.practice.rounds || props.totalRounds,
+      roleId: 'tom',
+      vocabulary: speakingStore.config.learningGoals.vocabulary,
+    })
+    preparedSession.value = {
+      sessionId: info.sessionId,
+      expiresAt: info.expiresAt,
+    }
+    dialogueState.value = 'chatting'
+  } catch (err: unknown) {
+    if (err instanceof DialogueApiError) {
+      sessionError.value = `创建会话失败(${err.status}),请重试`
+    } else {
+      sessionError.value = '创建会话失败,请重试'
+    }
+  } finally {
+    sessionCreating.value = false
+  }
 }
 }
 
 
 function handleDialogueComplete(report: DialogueReport | null) {
 function handleDialogueComplete(report: DialogueReport | null) {
@@ -207,6 +239,9 @@ function handleDialogueComplete(report: DialogueReport | null) {
 function resetPreview() {
 function resetPreview() {
   dialogueState.value = 'ready'
   dialogueState.value = 'ready'
   realEvaluation.value = null
   realEvaluation.value = null
+  preparedSession.value = null
+  sessionError.value = null
+  sessionCreating.value = false
 }
 }
 
 
 // ── Sync with speakingStore (让 CanvasTool 可以驱动"重置预览") ──
 // ── Sync with speakingStore (让 CanvasTool 可以驱动"重置预览") ──
@@ -326,6 +361,29 @@ onUnmounted(() => { speakingStore.setPreviewState('ready') })
   &:hover { background: #ea580c; }
   &:hover { background: #ea580c; }
   &:active { transform: scale(0.97); }
   &:active { transform: scale(0.97); }
 }
 }
+.start-btn:disabled {
+  opacity: 0.6;
+  cursor: wait;
+  background: #fb923c;
+}
+.start-btn-spinner {
+  width: 14px;
+  height: 14px;
+  border: 2px solid rgba(255, 255, 255, 0.4);
+  border-top-color: #fff;
+  border-radius: 50%;
+  animation: start-btn-spin 0.8s linear infinite;
+  display: inline-block;
+}
+@keyframes start-btn-spin {
+  to { transform: rotate(360deg); }
+}
+.session-error-text {
+  margin: 8px 0 0;
+  font-size: 12px;
+  color: #dc2626;
+  text-align: center;
+}
 
 
 // ─── Completed 阶段 ───
 // ─── Completed 阶段 ───
 .report-stage {
 .report-stage {