Sfoglia il codice sorgente

feat: lazy load dialogue task hints

jimmylee 2 settimane fa
parent
commit
a3d3cc55b6
1 ha cambiato i file con 42 aggiunte e 162 eliminazioni
  1. 42 162
      src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

+ 42 - 162
src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

@@ -240,7 +240,7 @@
           class="state-layer state-idle"
           :style="stateStyle('idle')"
         >
-          <button class="hint-btn" @click="showHintModal = true">
+          <button class="hint-btn" @click="openTaskHint">
             <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
               stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
               <path d="M9 18h6" /><path d="M10 22h4" />
@@ -332,73 +332,15 @@
     <!-- ─────── OVERLAYS ─────── -->
 
     <!-- 任务提示弹窗 -->
-    <div v-if="showHintModal" class="modal-mask" @click.self="showHintModal = false">
-      <div class="modal hint-modal scale-in">
-        <div class="modal-head">
-          <h3 class="modal-title">
-            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#f97316"
-              stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-              <path d="M9 18h6" /><path d="M10 22h4" />
-              <path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14" />
-            </svg>
-            任务提示
-          </h3>
-          <button class="close-btn" @click="showHintModal = false">
-            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
-              stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-              <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
-            </svg>
-          </button>
-        </div>
-
-        <div class="hint-context">
-          <p class="context-label">当前问题</p>
-          <p class="context-body">
-            {{ aiName }} 刚才问你关于 <strong>{{ topic }}</strong> 的问题,你可以分享你的看法和经历。
-          </p>
-        </div>
-
-        <div class="hint-section">
-          <p class="section-label">参考句子</p>
-          <div class="sentences">
-            <div v-for="(h, i) in sentenceHints" :key="i" class="sentence-card">
-              <div class="sentence-main">
-                <p class="sentence-en">{{ h.en }}</p>
-                <p class="sentence-zh">{{ h.zh }}</p>
-              </div>
-              <button class="voice-icon-btn" title="朗读">
-                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
-                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                  <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
-                  <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
-                </svg>
-              </button>
-            </div>
-          </div>
-        </div>
-
-        <div class="hint-section">
-          <p class="section-label">关键词汇</p>
-          <div class="vocab-grid">
-            <div v-for="(v, i) in vocabHints" :key="i" class="vocab-item">
-              <div class="vocab-info">
-                <p class="vocab-word">{{ v.word }}</p>
-                <p class="vocab-meta">{{ v.phonetic }} · {{ v.meaning }}</p>
-              </div>
-              <button class="vocab-play">
-                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
-                  stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                  <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
-                  <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
-                </svg>
-              </button>
-            </div>
-          </div>
-        </div>
-
-        <p class="hint-footer">用自己的话表达更棒哦 ✨</p>
-      </div>
-    </div>
+    <TaskHintModal
+      :visible="showHintModal"
+      :loading="taskHintLoading"
+      :error="taskHintError"
+      :hint="taskHint"
+      :ai-name="aiName"
+      @close="showHintModal = false"
+      @retry="loadTaskHint"
+    />
 
     <!-- 音素详情弹窗 -->
     <div v-if="phonemeDetail" class="modal-mask" @click.self="phonemeDetail = null">
@@ -484,9 +426,11 @@
 <script lang="ts" setup>
 import { ref, computed, watch, onMounted, onUnmounted, nextTick, h, defineComponent } from 'vue'
 import type { PropType } from 'vue'
-import type { PreviewChatMessage, BadgeAchievement, DialogueReport, SessionStartInfo } from '@/types/englishSpeaking'
+import type { PreviewChatMessage, BadgeAchievement, DialogueReport, SessionStartInfo, TaskHint } from '@/types/englishSpeaking'
 import { useDialogueEngine } from '../composables/useDialogueEngine'
 import { useAudioRecorder } from '../composables/useAudioRecorder'
+import TaskHintModal from './TaskHintModal.vue'
+import { createDialogueApi } from '../services/llmService'
 
 // ─────────────────────────────────────────────
 // Props / Emits
@@ -539,19 +483,6 @@ const BADGE_CONFIG: Record<string, BadgeAchievement> = {
   perfect_round: { id: 'perfect_round', name: '完美一轮', nameEn: 'Perfect Round', icon: '⭐', description: '单轮四维度全优' },
 }
 
-const sentenceHints = [
-  { en: 'I like pandas best because they are very cute.', zh: '我最喜欢大熊猫,因为它们非常可爱。' },
-  { en: "My favorite animal is the elephant. It's really smart!", zh: '我最喜欢的动物是大象,它真的很聪明!' },
-  { en: 'I enjoy watching animals at the zoo with my family.', zh: '我喜欢和家人一起在动物园看动物。' },
-]
-
-const vocabHints = [
-  { word: 'favorite', phonetic: '/ˈfeɪvərɪt/', meaning: '最喜欢的' },
-  { word: 'adorable', phonetic: '/əˈdɔːrəbl/', meaning: '可爱的' },
-  { word: 'bamboo', phonetic: '/bæmˈbuː/', meaning: '竹子' },
-  { word: 'habitat', phonetic: '/ˈhæbɪtæt/', meaning: '栖息地' },
-]
-
 // ─────────────────────────────────────────────
 // Composables
 // ─────────────────────────────────────────────
@@ -567,6 +498,10 @@ const chatContainerRef = ref<HTMLDivElement>()
 const expandedMessageId = ref<string | null>(null)
 const playingMessageId = ref<string | null>(null)
 const showHintModal = ref(false)
+const taskHint = ref<TaskHint | null>(null)
+const taskHintLoading = ref(false)
+const taskHintError = ref<string | null>(null)
+const taskHintApi = createDialogueApi(props.mode)
 const showExitConfirm = ref(false)
 const phonemeDetail = ref<{
   word: string
@@ -760,6 +695,31 @@ function toggleExpand(id: string) {
   expandedMessageId.value = expandedMessageId.value === id ? null : id
 }
 
+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 taskHintApi.generateTaskHint(props.sessionInfo.sessionId)
+  } catch {
+    taskHintError.value = '生成任务提示失败,请重试'
+  } finally {
+    taskHintLoading.value = false
+  }
+}
+
 function stopCurrentPlayback() {
   if (currentAudio) {
     currentAudio.pause()
@@ -1584,7 +1544,6 @@ onUnmounted(() => {
   overflow-y: auto;
   box-shadow: 0 20px 60px rgba(0,0,0,0.15);
 }
-.hint-modal { max-width: 440px; padding: 20px; }
 .phoneme-modal { max-width: 320px; }
 .exit-modal { max-width: 320px; padding: 20px; }
 
@@ -1614,85 +1573,6 @@ onUnmounted(() => {
   &:hover { background: #e5e7eb; }
 }
 
-// 任务提示
-.hint-context {
-  background: #fff7ed;
-  border: 1px solid #fed7aa;
-  border-radius: 12px;
-  padding: 12px 14px;
-  margin-bottom: 16px;
-}
-.context-label {
-  font-size: 10px;
-  color: #f97316;
-  font-weight: 500;
-  letter-spacing: 0.03em;
-  text-transform: uppercase;
-  margin: 0 0 4px;
-}
-.context-body { font-size: 13px; color: #374151; line-height: 1.5; margin: 0; }
-
-.hint-section { margin-bottom: 16px; }
-.section-label {
-  font-size: 10px;
-  color: #9ca3af;
-  font-weight: 500;
-  letter-spacing: 0.03em;
-  text-transform: uppercase;
-  margin: 0 0 8px;
-}
-.sentences { display: flex; flex-direction: column; gap: 8px; }
-.sentence-card {
-  display: flex;
-  align-items: flex-start;
-  gap: 8px;
-  padding: 10px 12px;
-  background: #f9fafb;
-  border: 1px solid #f3f4f6;
-  border-radius: 12px;
-  transition: border-color 0.2s;
-  &:hover { border-color: #fed7aa; }
-}
-.sentence-main { flex: 1; min-width: 0; }
-.sentence-en { font-size: 12px; color: #1f2937; margin: 0; line-height: 1.5; }
-.sentence-zh { font-size: 11px; color: #9ca3af; margin: 2px 0 0; }
-.voice-icon-btn, .vocab-play {
-  width: 28px; height: 28px;
-  flex-shrink: 0;
-  background: #fff7ed;
-  border: 1px solid #fed7aa;
-  color: #f97316;
-  border-radius: 8px;
-  cursor: pointer;
-  display: flex; align-items: center; justify-content: center;
-  &:hover { background: #fed7aa; }
-}
-.vocab-grid {
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
-  gap: 8px;
-}
-.vocab-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 8px 10px;
-  background: #f9fafb;
-  border: 1px solid #f3f4f6;
-  border-radius: 10px;
-}
-.vocab-info { min-width: 0; }
-.vocab-word { font-size: 12px; font-weight: 500; color: #1f2937; margin: 0; }
-.vocab-meta { font-size: 10px; color: #9ca3af; margin: 2px 0 0; }
-.vocab-play {
-  width: 24px; height: 24px;
-  background: #fff;
-  border: 1px solid #e5e7eb;
-  color: #9ca3af;
-  &:hover { color: #f97316; border-color: #fed7aa; background: #fff; }
-}
-.hint-footer { text-align: center; font-size: 11px; color: #d1d5db; margin: 16px 0 0; }
-
 // 音素
 .phoneme-word { text-align: left; font-size: 16px; font-weight: 700; color: #111827; margin: 0; }
 .phoneme-sub { font-size: 11px; color: #9ca3af; margin: 2px 0 0; }