|
@@ -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 {
|