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

feat(speaking): blank manual-create config and require topic before start

- add EMPTY_CONFIG + resetConfigEmpty so manual creation gets a blank slate
  (empty topic / vocabulary / sentences; rounds=60, duration=1)
- Layer2Speaking manual branch uses resetConfigEmpty
- TopicDiscussionPreview disables 开始对话 and shows "至少设置topic" when topic is empty
- comment out the AI生成 entry in CreationModeSwitch (kept for later re-enable)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jimmylee 1 неделя назад
Родитель
Сommit
9fa950978c

+ 1 - 1
.env

@@ -3,4 +3,4 @@ VITE_AZURE_SPEECH_REGION=eastasia
 # VITE_SPEAKING_API_HOST=http://localhost:8000
 
 # 学生录音上送方式:websocket(默认,流式) | http(单次 POST /speak)
-VITE_SPEAKING_TRANSPORT=http
+# VITE_SPEAKING_TRANSPORT=http

+ 34 - 0
src/store/speaking.ts

@@ -42,6 +42,35 @@ const DEFAULT_CONFIG: TopicDiscussionConfig = {
   },
 }
 
+// 手动创建用:结构与 DEFAULT_CONFIG 完全一致,按需手动调整为空值
+const EMPTY_CONFIG: TopicDiscussionConfig = {
+  topic: '',
+  grade: 'grade5-1',
+  selectedRole: 'tom',
+  learningGoals: {
+    vocabulary: [],
+    sentences: [],
+  },
+  practice: {
+    mode: 'time',
+    rounds: 60,
+    duration: 1,
+    showEnglish: true,
+    taskHint: true,
+    stutterHint: true,
+  },
+  evaluation: {
+    showReport: true,
+    dimensions: {
+      accuracy: true,
+      fluency: true,
+      completeness: true,
+      rhythm: true,
+    },
+    scoreMode: 'letter',
+  },
+}
+
 export const useSpeakingStore = defineStore('speaking', {
   state: () => ({
     config: { ...DEFAULT_CONFIG } as TopicDiscussionConfig,
@@ -59,6 +88,11 @@ export const useSpeakingStore = defineStore('speaking', {
       this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG))
     },
 
+    // 重置为空白配置(手动创建用)
+    resetConfigEmpty() {
+      this.config = JSON.parse(JSON.stringify(EMPTY_CONFIG))
+    },
+
     setPreviewState(state: PreviewDialogueState) {
       this.previewState = state
     },

+ 2 - 0
src/views/Editor/EnglishSpeaking/components/CreationModeSwitch.vue

@@ -6,12 +6,14 @@
     <div class="mode-actions">
       <!-- 当前是 smart 模式时,显示 AI生成 和 手动创建 按钮 -->
       <template v-if="modelValue === 'smart'">
+        <!-- AI生成按钮(暂时隐藏,保留以便后续启用)
         <button class="mode-btn" @click="$emit('update:modelValue', 'ai')">
           <svg class="mode-btn-icon sparkle" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
           </svg>
           <span>AI生成</span>
         </button>
+        -->
         <button class="mode-btn" @click="$emit('update:modelValue', 'manual')">
           <svg class="mode-btn-icon clipboard" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" />

+ 2 - 2
src/views/Editor/EnglishSpeaking/layers/Layer2Speaking.vue

@@ -162,9 +162,9 @@ const handleSelectTask = (task: TopicDiscussionTask) => {
   insertSpeakingToolToCanvas('select')
 }
 
-// 手动创建 → 重置 store → 立即创建配置 + 插入元素
+// 手动创建 → 清空 store → 立即创建配置 + 插入元素
 const handleManualCreate = (_taskTypeId: string) => {
-  speakingStore.resetConfig()
+  speakingStore.resetConfigEmpty()
   insertSpeakingToolToCanvas('manual')
 }
 </script>

+ 7 - 4
src/views/Editor/EnglishSpeaking/preview/TopicDiscussionPreview.vue

@@ -29,7 +29,7 @@
       </div>
 
       <div class="ready-footer">
-        <button class="start-btn" :disabled="sessionCreating" @click="startDialogue">
+        <button class="start-btn" :disabled="sessionCreating || topicMissing" @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">
             <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
@@ -40,7 +40,8 @@
           <span v-else class="start-btn-spinner" />
           {{ sessionCreating ? '创建中…' : '开始对话' }}
         </button>
-        <p v-if="sessionError" class="session-error-text">{{ sessionError }}</p>
+        <p v-if="topicMissing" class="session-error-text">至少设置topic</p>
+        <p v-else-if="sessionError" class="session-error-text">{{ sessionError }}</p>
       </div>
     </div>
 
@@ -105,6 +106,8 @@ const props = withDefaults(defineProps<Props>(), {
   configId: '',
 })
 
+const speakingStore = useSpeakingStore()
+
 const dialogueState = ref<PreviewDialogueState>('ready')
 const sessionCreating = ref(false)
 const sessionError = ref<string | null>(null)
@@ -112,6 +115,8 @@ const preparedSession = ref<SessionStartInfo | null>(null)
 const historyChecked = ref(false)
 const historyLoadToken = ref(0)
 
+const topicMissing = computed(() => !(speakingStore.config.topic ?? '').trim())
+
 const runtimeParams = computed(() => {
   const params = new URLSearchParams(window.location.search)
   return {
@@ -409,8 +414,6 @@ function resetPreview() {
 }
 
 // ── Sync with speakingStore (让 CanvasTool 可以驱动"重置预览") ──
-const speakingStore = useSpeakingStore()
-
 watch(dialogueState, (s) => { speakingStore.setPreviewState(s) }, { immediate: true })
 
 watch(