|
@@ -1,4 +1,4 @@
|
|
|
-import type { DialogueAPI, SSEEvent, SessionConfig, SessionInfo, DialogueReport } from '@/types/englishSpeaking'
|
|
|
|
|
|
|
+import type { DialogueAPI, SSEEvent, SessionConfig, SessionInfo, DialogueReport, SentenceEvaluation } from '@/types/englishSpeaking'
|
|
|
|
|
|
|
|
const API_BASE = 'http://localhost:8000/api/speaking/dialogue'
|
|
const API_BASE = 'http://localhost:8000/api/speaking/dialogue'
|
|
|
|
|
|
|
@@ -48,6 +48,90 @@ async function* parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>):
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ==================== Backend shape types ====================
|
|
|
|
|
+
|
|
|
|
|
+interface BackendEvaluation {
|
|
|
|
|
+ status: 'pending' | 'completed' | 'failed'
|
|
|
|
|
+ accuracyScore: number | null
|
|
|
|
|
+ fluencyScore: number | null
|
|
|
|
|
+ completenessScore: number | null
|
|
|
|
|
+ prosodyScore: number | null
|
|
|
|
|
+ wordAnalysis: unknown
|
|
|
|
|
+ contentFeedback: {
|
|
|
|
|
+ highlights: string[]
|
|
|
|
|
+ corrections: { original: string; corrected: string; explanation: string }[]
|
|
|
|
|
+ suggestions: string[]
|
|
|
|
|
+ } | null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface BackendRound {
|
|
|
|
|
+ round: number
|
|
|
|
|
+ role: 'ai' | 'student'
|
|
|
|
|
+ content: string
|
|
|
|
|
+ audioUrl: string | null
|
|
|
|
|
+ evaluation?: BackendEvaluation
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface BackendReportResponse {
|
|
|
|
|
+ sessionId: string
|
|
|
|
|
+ topic: string
|
|
|
|
|
+ status: 'evaluating' | 'ready'
|
|
|
|
|
+ rounds: BackendRound[]
|
|
|
|
|
+ summary: string | null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function adaptReport(raw: BackendReportResponse): DialogueReport {
|
|
|
|
|
+ const sentenceEvaluations: SentenceEvaluation[] = raw.rounds.map((r, idx) => ({
|
|
|
|
|
+ id: `${raw.sessionId}-${idx}`,
|
|
|
|
|
+ round: r.round,
|
|
|
|
|
+ role: r.role,
|
|
|
|
|
+ content: r.content,
|
|
|
|
|
+ audioUrl: r.audioUrl ?? undefined,
|
|
|
|
|
+ pronunciation: r.evaluation && r.role === 'student'
|
|
|
|
|
+ ? {
|
|
|
|
|
+ accuracy: r.evaluation.accuracyScore ?? 0,
|
|
|
|
|
+ fluency: r.evaluation.fluencyScore ?? 0,
|
|
|
|
|
+ intonation: r.evaluation.prosodyScore ?? 0,
|
|
|
|
|
+ stress: r.evaluation.completenessScore ?? 0,
|
|
|
|
|
+ }
|
|
|
|
|
+ : undefined,
|
|
|
|
|
+ feedback: r.evaluation?.contentFeedback ?? undefined,
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ const studentEvals = sentenceEvaluations.filter(s => s.role === 'student' && s.pronunciation)
|
|
|
|
|
+ const avg = studentEvals.length > 0
|
|
|
|
|
+ ? Math.round(
|
|
|
|
|
+ studentEvals.reduce(
|
|
|
|
|
+ (sum, s) => sum + (s.pronunciation!.accuracy + s.pronunciation!.fluency + s.pronunciation!.intonation + s.pronunciation!.stress) / 4,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ ) / studentEvals.length,
|
|
|
|
|
+ )
|
|
|
|
|
+ : 0
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ evaluation: {
|
|
|
|
|
+ overallScore: avg,
|
|
|
|
|
+ scoreLevel: avg >= 85 ? 'excellent' : avg >= 70 ? 'good' : avg >= 60 ? 'fair' : 'needsWork',
|
|
|
|
|
+ percentile: 0,
|
|
|
|
|
+ dimensions: { fluency: 0, interaction: 0, vocabulary: 0, grammar: 0 },
|
|
|
|
|
+ aiComment: raw.summary ?? '',
|
|
|
|
|
+ highlights: [],
|
|
|
|
|
+ improvements: [],
|
|
|
|
|
+ nextChallenge: {},
|
|
|
|
|
+ statistics: {
|
|
|
|
|
+ totalRounds: Math.max(...sentenceEvaluations.map(s => s.round), 0),
|
|
|
|
|
+ averageScore: avg,
|
|
|
|
|
+ highestScore: 0,
|
|
|
|
|
+ highestRound: 0,
|
|
|
|
|
+ grammarErrors: 0,
|
|
|
|
|
+ excellentExpressions: 0,
|
|
|
|
|
+ totalDuration: 0,
|
|
|
|
|
+ },
|
|
|
|
|
+ sentenceEvaluations,
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ==================== Real API ====================
|
|
// ==================== Real API ====================
|
|
|
|
|
|
|
|
export class RealDialogueAPI implements DialogueAPI {
|
|
export class RealDialogueAPI implements DialogueAPI {
|
|
@@ -88,7 +172,8 @@ export class RealDialogueAPI implements DialogueAPI {
|
|
|
credentials: 'include',
|
|
credentials: 'include',
|
|
|
})
|
|
})
|
|
|
if (!res.ok) throw new Error(`getReport failed: ${res.status}`)
|
|
if (!res.ok) throw new Error(`getReport failed: ${res.status}`)
|
|
|
- return res.json()
|
|
|
|
|
|
|
+ const raw: BackendReportResponse = await res.json()
|
|
|
|
|
+ return adaptReport(raw)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|