|
|
@@ -1,7 +1,28 @@
|
|
|
-import type { DialogueAPI, SSEEvent, SessionConfig, SessionInfo, DialogueReport, SentenceEvaluation } from '@/types/englishSpeaking'
|
|
|
+import type {
|
|
|
+ DialogueAPI,
|
|
|
+ SSEEvent,
|
|
|
+ SessionConfig,
|
|
|
+ SessionInfo,
|
|
|
+ GreetingInfo,
|
|
|
+ DialogueReport,
|
|
|
+ SentenceEvaluation,
|
|
|
+} from '@/types/englishSpeaking'
|
|
|
|
|
|
const API_BASE = 'http://localhost:8000/api/speaking/dialogue'
|
|
|
|
|
|
+export class DialogueApiError extends Error {
|
|
|
+ status: number
|
|
|
+ constructor(message: string, status: number) {
|
|
|
+ super(message)
|
|
|
+ this.status = status
|
|
|
+ this.name = 'DialogueApiError'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function createDialogueApi(mode: 'preview' | 'real'): DialogueAPI {
|
|
|
+ return mode === 'real' ? new RealDialogueAPI() : new MockDialogueAPI()
|
|
|
+}
|
|
|
+
|
|
|
// ==================== SSE 解析 ====================
|
|
|
|
|
|
async function* parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<SSEEvent> {
|
|
|
@@ -146,7 +167,31 @@ export class RealDialogueAPI implements DialogueAPI {
|
|
|
roleId: config.roleId,
|
|
|
}),
|
|
|
})
|
|
|
- if (!res.ok) throw new Error(`createSession failed: ${res.status}`)
|
|
|
+ if (!res.ok) {
|
|
|
+ throw new DialogueApiError(`createSession failed: ${res.status}`, res.status)
|
|
|
+ }
|
|
|
+ const body = await res.json()
|
|
|
+ return {
|
|
|
+ sessionId: body.sessionId,
|
|
|
+ totalRounds: body.totalRounds,
|
|
|
+ currentRound: body.currentRound,
|
|
|
+ expiresAt: body.expiresAt ?? null,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async generateGreeting(sessionId: string, signal?: AbortSignal): Promise<GreetingInfo> {
|
|
|
+ const res = await fetch(`${API_BASE}/session/${sessionId}/greeting`, {
|
|
|
+ method: 'POST',
|
|
|
+ credentials: 'include',
|
|
|
+ signal,
|
|
|
+ })
|
|
|
+ if (!res.ok) {
|
|
|
+ const text = await res.text().catch(() => '')
|
|
|
+ throw new DialogueApiError(
|
|
|
+ `greeting failed: ${res.status}${text ? ` (${text.slice(0, 100)})` : ''}`,
|
|
|
+ res.status,
|
|
|
+ )
|
|
|
+ }
|
|
|
return res.json()
|
|
|
}
|
|
|
|
|
|
@@ -192,10 +237,18 @@ export class MockDialogueAPI implements DialogueAPI {
|
|
|
this.roundIndex = 0
|
|
|
return {
|
|
|
sessionId: 'mock-session-' + Date.now(),
|
|
|
- aiMessage: "Hi! What's your favorite animal?",
|
|
|
+ totalRounds: _config.totalRounds,
|
|
|
+ currentRound: 1,
|
|
|
+ expiresAt: null,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ async generateGreeting(_sessionId: string, _signal?: AbortSignal): Promise<GreetingInfo> {
|
|
|
+ // 模拟 300ms 延迟
|
|
|
+ await new Promise(r => setTimeout(r, 300))
|
|
|
+ return { aiMessage: "Hi! What's your favorite animal?" }
|
|
|
+ }
|
|
|
+
|
|
|
async *speak(_sessionId: string, _audioBlob: Blob, signal: AbortSignal): AsyncGenerator<SSEEvent> {
|
|
|
const mockStudentTexts = [
|
|
|
'I like pandas. They are very cute!',
|