feat: Implement AI-powered audio response evaluation with score, keywords, and feedback, integrating it into the AudioResponsePlayer component.
This commit is contained in:
@@ -16,7 +16,8 @@ import ShortAnswerPlayer from "@/components/blocks/ShortAnswerPlayer";
|
||||
import CodeExercisePlayer from "@/components/blocks/CodeExercisePlayer";
|
||||
import HotspotPlayer from "@/components/blocks/HotspotPlayer";
|
||||
import MemoryPlayer from "@/components/blocks/MemoryPlayer";
|
||||
import DocumentPlayer from "@/components/blocks/DocumentPlayer"; // Added import
|
||||
import DocumentPlayer from "@/components/blocks/DocumentPlayer";
|
||||
import AudioResponsePlayer from "@/components/blocks/AudioResponsePlayer";
|
||||
import InteractiveTranscript from "@/components/InteractiveTranscript";
|
||||
import { ListMusic } from "lucide-react";
|
||||
|
||||
@@ -276,6 +277,17 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
|
||||
allowRetry={lesson.allow_retry}
|
||||
/>
|
||||
);
|
||||
case 'audio-response':
|
||||
return (
|
||||
<AudioResponsePlayer
|
||||
id={block.id}
|
||||
prompt={block.prompt || ""}
|
||||
keywords={block.keywords}
|
||||
timeLimit={block.timeLimit}
|
||||
isGraded={lesson.is_graded}
|
||||
onComplete={(score) => handleBlockComplete(block.id, score)}
|
||||
/>
|
||||
);
|
||||
case 'code':
|
||||
return (
|
||||
<CodeExercisePlayer
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Mic, Square, Play, RotateCcw, Check, X, Clock } from "lucide-react";
|
||||
import { lmsApi } from "@/lib/api";
|
||||
import { Mic, Square, Play, RotateCcw, Check, X, Clock, BrainCircuit } from "lucide-react";
|
||||
|
||||
interface AudioResponsePlayerProps {
|
||||
id: string;
|
||||
@@ -26,7 +27,7 @@ export default function AudioResponsePlayer({
|
||||
const [isTranscribing, setIsTranscribing] = useState(false);
|
||||
const [recordingTime, setRecordingTime] = useState(0);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [evaluation, setEvaluation] = useState<{ score: number; foundKeywords: string[] } | null>(null);
|
||||
const [evaluation, setEvaluation] = useState<{ score: number; foundKeywords: string[]; feedback: string } | null>(null);
|
||||
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const audioChunksRef = useRef<Blob[]>([]);
|
||||
@@ -133,26 +134,30 @@ export default function AudioResponsePlayer({
|
||||
}
|
||||
};
|
||||
|
||||
const evaluateResponse = () => {
|
||||
const evaluateResponse = async () => {
|
||||
if (!transcript.trim()) {
|
||||
alert("No speech detected. Please try recording again.");
|
||||
return;
|
||||
}
|
||||
|
||||
const transcriptLower = transcript.toLowerCase();
|
||||
const foundKeywords = keywords.filter(kw =>
|
||||
transcriptLower.includes(kw.toLowerCase())
|
||||
);
|
||||
setIsTranscribing(true);
|
||||
try {
|
||||
const result = await lmsApi.evaluateAudio(transcript, prompt, keywords);
|
||||
setEvaluation({
|
||||
score: result.score,
|
||||
foundKeywords: result.found_keywords,
|
||||
feedback: result.feedback
|
||||
});
|
||||
setSubmitted(true);
|
||||
|
||||
const score = keywords.length > 0
|
||||
? Math.round((foundKeywords.length / keywords.length) * 100)
|
||||
: 100; // If no keywords specified, give full credit for any response
|
||||
|
||||
setEvaluation({ score, foundKeywords });
|
||||
setSubmitted(true);
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(score, transcript);
|
||||
if (onComplete) {
|
||||
onComplete(result.score, transcript);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Evaluation failed", err);
|
||||
alert("Evaluation failed. Please try again.");
|
||||
} finally {
|
||||
setIsTranscribing(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,10 +274,10 @@ export default function AudioResponsePlayer({
|
||||
{submitted && evaluation && (
|
||||
<div className="space-y-4">
|
||||
<div className={`p-6 rounded-2xl border-2 ${evaluation.score >= 70
|
||||
? 'bg-green-500/10 border-green-500'
|
||||
: evaluation.score >= 40
|
||||
? 'bg-yellow-500/10 border-yellow-500'
|
||||
: 'bg-red-500/10 border-red-500'
|
||||
? 'bg-green-500/10 border-green-500'
|
||||
: evaluation.score >= 40
|
||||
? 'bg-yellow-500/10 border-yellow-500'
|
||||
: 'bg-red-500/10 border-red-500'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm font-bold uppercase tracking-wider text-gray-400">Your Score</span>
|
||||
@@ -289,8 +294,8 @@ export default function AudioResponsePlayer({
|
||||
<span
|
||||
key={i}
|
||||
className={`px-3 py-1 rounded-lg text-sm font-medium flex items-center gap-1 ${found
|
||||
? 'bg-green-500/20 border border-green-500/50 text-green-300'
|
||||
: 'bg-gray-500/20 border border-gray-500/50 text-gray-400'
|
||||
? 'bg-green-500/20 border border-green-500/50 text-green-300'
|
||||
: 'bg-gray-500/20 border border-gray-500/50 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{found ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
||||
@@ -303,6 +308,21 @@ export default function AudioResponsePlayer({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{evaluation.feedback && (
|
||||
<div className="p-6 bg-blue-500/10 border-2 border-blue-500/20 rounded-2xl space-y-3 relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<BrainCircuit className="w-20 h-20 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs font-black uppercase tracking-widest text-blue-400">
|
||||
<BrainCircuit className="w-4 h-4" />
|
||||
AI Teacher Feedback
|
||||
</div>
|
||||
<p className="text-gray-200 leading-relaxed italic text-lg relative z-10">
|
||||
"{evaluation.feedback}"
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 bg-white/5 border border-white/10 rounded-xl">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-2">Your Transcript:</p>
|
||||
<p className="text-sm text-gray-300">{transcript}</p>
|
||||
|
||||
@@ -28,6 +28,12 @@ export interface RecommendationResponse {
|
||||
recommendations: Recommendation[];
|
||||
}
|
||||
|
||||
export interface AudioGradingResponse {
|
||||
score: number;
|
||||
found_keywords: string[];
|
||||
feedback: string;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -51,7 +57,7 @@ export interface QuizQuestion {
|
||||
|
||||
export interface Block {
|
||||
id: string;
|
||||
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'code' | 'hotspot' | 'memory-match' | 'document';
|
||||
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'code' | 'hotspot' | 'memory-match' | 'document' | 'audio-response' | 'video_marker';
|
||||
title: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
@@ -66,6 +72,8 @@ export interface Block {
|
||||
correctAnswers?: string[];
|
||||
instructions?: string;
|
||||
initialCode?: string;
|
||||
keywords?: string[];
|
||||
timeLimit?: number;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
@@ -298,5 +306,11 @@ export const lmsApi = {
|
||||
},
|
||||
async getRecommendations(courseId: string): Promise<RecommendationResponse> {
|
||||
return apiFetch(`/courses/${courseId}/recommendations`);
|
||||
},
|
||||
async evaluateAudio(transcript: string, prompt: string, keywords: string[]): Promise<AudioGradingResponse> {
|
||||
return apiFetch('/audio/evaluate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ transcript, prompt, keywords })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user