|
|
@@ -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; }
|