feat: fix hotspot test
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '3001',
|
||||
pathname: '/assets/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -22,12 +22,12 @@ export default function StudentCalendarPage({ params }: { params: { id: string }
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const courseData = await lmsApi.getCourseOutline(params.id);
|
||||
setCourse(courseData);
|
||||
const { course, modules } = await lmsApi.getCourseOutline(params.id);
|
||||
setCourse({ ...course, modules });
|
||||
|
||||
// Flatten lessons from modules
|
||||
const allLessons: Lesson[] = [];
|
||||
courseData.modules?.forEach(mod => {
|
||||
modules?.forEach(mod => {
|
||||
mod.lessons.forEach(lesson => {
|
||||
allLessons.push(lesson);
|
||||
});
|
||||
@@ -74,8 +74,8 @@ export default function StudentCalendarPage({ params }: { params: { id: string }
|
||||
<Link key={lesson.id} href={`/courses/${params.id}/lessons/${lesson.id}`}>
|
||||
<div
|
||||
className={`text-[9px] p-1 rounded truncate flex items-center gap-1 mb-1 border transition-all hover:scale-[1.02] ${lesson.important_date_type === 'exam' ? 'bg-red-500/10 text-red-400 border-red-500/20' :
|
||||
lesson.important_date_type === 'assignment' ? 'bg-blue-500/10 text-blue-400 border-blue-500/20' :
|
||||
'bg-green-500/10 text-green-400 border-green-500/20'
|
||||
lesson.important_date_type === 'assignment' ? 'bg-blue-500/10 text-blue-400 border-blue-500/20' :
|
||||
'bg-green-500/10 text-green-400 border-green-500/20'
|
||||
}`}
|
||||
>
|
||||
<span className="w-1 h-1 rounded-full bg-current"></span>
|
||||
|
||||
@@ -34,12 +34,12 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
|
||||
useEffect(() => {
|
||||
const fetchAll = async () => {
|
||||
try {
|
||||
const [lessonData, courseData] = await Promise.all([
|
||||
const [lessonData, outlineData] = await Promise.all([
|
||||
lmsApi.getLesson(params.lessonId),
|
||||
lmsApi.getCourseOutline(params.id)
|
||||
]);
|
||||
setLesson(lessonData);
|
||||
setCourse(courseData);
|
||||
setCourse({ ...outlineData.course, modules: outlineData.modules });
|
||||
|
||||
if (user) {
|
||||
const grades = await lmsApi.getUserGrades(user.id, params.id);
|
||||
@@ -84,11 +84,25 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
|
||||
[blockId]: score
|
||||
};
|
||||
|
||||
// Calculate overall lesson score as average of block scores
|
||||
const blocks = lesson.metadata?.blocks || [];
|
||||
const interactiveBlocks = blocks.filter((b: any) =>
|
||||
!['description', 'media', 'document'].includes(b.type)
|
||||
);
|
||||
|
||||
const scores = interactiveBlocks.map((b: any) =>
|
||||
b.id === blockId ? score : currentBlockScores[b.id] || 0
|
||||
);
|
||||
|
||||
const newOverallScore = scores.length > 0
|
||||
? (scores.reduce((a: number, b: number) => a + b, 0) / scores.length) * 100
|
||||
: 100;
|
||||
|
||||
const res = await lmsApi.submitScore(
|
||||
user.id,
|
||||
params.id,
|
||||
params.lessonId,
|
||||
userGrade?.score || 0, // Keep overall score for now, or calculate average/sum
|
||||
newOverallScore,
|
||||
{ ...userGrade?.metadata, block_scores: newBlockScores }
|
||||
);
|
||||
setUserGrade(res);
|
||||
@@ -220,6 +234,7 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
|
||||
}
|
||||
}}
|
||||
isGraded={lesson.is_graded}
|
||||
hasTranscription={!!lesson.transcription}
|
||||
/>
|
||||
);
|
||||
case 'document':
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
|
||||
useEffect(() => {
|
||||
lmsApi.getCourseOutline(params.id)
|
||||
.then(setCourseData)
|
||||
.then(data => setCourseData({ ...data.course, modules: data.modules }))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
@@ -129,8 +129,8 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-widest ${rec.priority === 'high' ? 'bg-red-500/10 text-red-400 border border-red-500/20' :
|
||||
rec.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20' :
|
||||
'bg-green-500/10 text-green-400 border border-green-500/20'
|
||||
rec.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20' :
|
||||
'bg-green-500/10 text-green-400 border border-green-500/20'
|
||||
}`}>
|
||||
Prioridad {rec.priority}
|
||||
</div>
|
||||
|
||||
@@ -32,8 +32,8 @@ export default function StudentProgressPage() {
|
||||
|
||||
const loadData = React.useCallback(async () => {
|
||||
try {
|
||||
const courseData = await lmsApi.getCourseOutline(id);
|
||||
setCourse(courseData);
|
||||
const { course, modules, grading_categories } = await lmsApi.getCourseOutline(id);
|
||||
setCourse({ ...course, modules, grading_categories });
|
||||
|
||||
if (user) {
|
||||
const grades = await lmsApi.getUserGrades(user.id, id);
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function RootLayout({
|
||||
</main>
|
||||
<footer className="py-12 px-6 border-t border-white/5 text-center bg-black/20">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-600">
|
||||
Desarrollado por OpenCCB © 2023. Codificación Agente Avanzada.
|
||||
Desarrollado Por el Departamento de Informática © 2026. OpenCCB.
|
||||
</p>
|
||||
</footer>
|
||||
</AuthGuard>
|
||||
|
||||
@@ -38,14 +38,14 @@ export default function MyLearningPage() {
|
||||
const enrichedEnrollments: EnrollmentWithProgress[] = [];
|
||||
for (const enrollment of enrollmentData) {
|
||||
try {
|
||||
const outline = await lmsApi.getCourseOutline(enrollment.course_id);
|
||||
const { course, modules } = await lmsApi.getCourseOutline(enrollment.course_id);
|
||||
|
||||
// TODO: Implement actual progress tracking
|
||||
// For now, show 0% progress for all courses
|
||||
const progress = 0;
|
||||
|
||||
enrichedEnrollments.push({
|
||||
course: outline,
|
||||
course: { ...course, modules },
|
||||
progress,
|
||||
lastAccessed: enrollment.enroled_at
|
||||
});
|
||||
|
||||
@@ -35,11 +35,11 @@ export default function CatalogPage() {
|
||||
const deadlines: { lesson: Lesson, courseTitle: string, courseId: string }[] = [];
|
||||
for (const enrollment of enrollmentData) {
|
||||
try {
|
||||
const outline = await lmsApi.getCourseOutline(enrollment.course_id);
|
||||
outline.modules.forEach(mod => {
|
||||
const { course, modules } = await lmsApi.getCourseOutline(enrollment.course_id);
|
||||
modules.forEach(mod => {
|
||||
mod.lessons.forEach(l => {
|
||||
if (l.due_date && new Date(l.due_date) >= new Date()) {
|
||||
deadlines.push({ lesson: l, courseTitle: outline.title, courseId: enrollment.course_id });
|
||||
deadlines.push({ lesson: l, courseTitle: course.title, courseId: enrollment.course_id });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,14 +135,14 @@ export default function AudioResponsePlayer({
|
||||
};
|
||||
|
||||
const evaluateResponse = async () => {
|
||||
if (!transcript.trim()) {
|
||||
alert("No speech detected. Please try recording again.");
|
||||
if (!audioBlob) {
|
||||
alert("No recording found. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTranscribing(true);
|
||||
try {
|
||||
const result = await lmsApi.evaluateAudio(transcript, prompt, keywords);
|
||||
const result = await lmsApi.evaluateAudioFile(audioBlob, prompt, keywords);
|
||||
setEvaluation({
|
||||
score: result.score,
|
||||
foundKeywords: result.found_keywords,
|
||||
@@ -153,9 +153,9 @@ export default function AudioResponsePlayer({
|
||||
if (onComplete) {
|
||||
onComplete(result.score, transcript);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("Evaluation failed", err);
|
||||
alert("Evaluation failed. Please try again.");
|
||||
alert(err.message || "Evaluation failed. Please try again.");
|
||||
} finally {
|
||||
setIsTranscribing(false);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function HotspotPlayer({
|
||||
src={getImageUrl(imageUrl)}
|
||||
alt={title}
|
||||
fill
|
||||
unoptimized
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-[1.02]"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ interface MediaPlayerProps {
|
||||
media_type: 'video' | 'audio';
|
||||
config?: {
|
||||
maxPlays?: number;
|
||||
show_transcript?: boolean;
|
||||
markers?: {
|
||||
timestamp: number;
|
||||
question: string;
|
||||
@@ -176,10 +177,10 @@ export default function MediaPlayer({ id, lessonId, title, url, media_type, conf
|
||||
}
|
||||
}}
|
||||
>
|
||||
{hasTranscription && vttEn && (
|
||||
{hasTranscription && config?.show_transcript !== false && vttEn && (
|
||||
<track kind="subtitles" src={vttEn} srcLang="en" label="English" />
|
||||
)}
|
||||
{hasTranscription && vttEs && (
|
||||
{hasTranscription && config?.show_transcript !== false && vttEs && (
|
||||
<track kind="subtitles" src={vttEs} srcLang="es" label="Español" />
|
||||
)}
|
||||
</video>
|
||||
|
||||
@@ -209,7 +209,7 @@ export const lmsApi = {
|
||||
return apiFetch(`/catalog${query}`);
|
||||
},
|
||||
|
||||
async getCourseOutline(courseId: string): Promise<Course & { modules: Module[], grading_categories: GradingCategory[] }> {
|
||||
async getCourseOutline(courseId: string): Promise<{ course: Course, modules: Module[], grading_categories: GradingCategory[], organization: Organization }> {
|
||||
return apiFetch(`/courses/${courseId}/outline`);
|
||||
},
|
||||
|
||||
@@ -321,5 +321,26 @@ export const lmsApi = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ transcript, prompt, keywords })
|
||||
});
|
||||
},
|
||||
async evaluateAudioFile(file: Blob, prompt: string, keywords: string[]): Promise<AudioGradingResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, 'recorded_audio.webm');
|
||||
formData.append('prompt', prompt);
|
||||
formData.append('keywords', JSON.stringify(keywords));
|
||||
|
||||
const token = getToken();
|
||||
return fetch(`${API_BASE_URL}/audio/evaluate-file`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
},
|
||||
body: formData
|
||||
}).then(async res => {
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ message: 'Audio evaluation failed' }));
|
||||
throw new Error(err.message || 'Audio evaluation failed');
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function HotspotBlock({
|
||||
</div>
|
||||
<div className="relative aspect-video rounded-2xl overflow-hidden border border-white/5 bg-black/40">
|
||||
{imageUrl ? (
|
||||
<Image src={getImageUrl(imageUrl)} alt={title || ""} fill className="object-cover opacity-50" />
|
||||
<Image src={getImageUrl(imageUrl)} alt={title || ""} fill unoptimized className="object-cover opacity-50" />
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-600 italic text-sm">No image provided.</div>
|
||||
)}
|
||||
@@ -140,7 +140,7 @@ export default function HotspotBlock({
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative aspect-video rounded-2xl overflow-hidden group">
|
||||
<Image src={getImageUrl(imageUrl)} alt="Hotspot base" fill className="object-cover" />
|
||||
<Image src={getImageUrl(imageUrl)} alt="Hotspot base" fill unoptimized className="object-cover" />
|
||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
|
||||
<button onClick={() => setIsAssetPickerOpen(true)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-all"><ImageIcon size={18} /></button>
|
||||
<button onClick={() => onChange({ imageUrl: undefined })} className="p-2 bg-red-500/20 hover:bg-red-500/40 rounded-lg text-red-400 transition-all"><Trash2 size={18} /></button>
|
||||
@@ -164,8 +164,8 @@ export default function HotspotBlock({
|
||||
onClick={handleImageClick}
|
||||
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-white/10 cursor-crosshair shadow-2xl"
|
||||
>
|
||||
<Image src={getImageUrl(imageUrl)} alt="Define Hotspots" fill className="object-cover select-none" />
|
||||
{hotspots.map((h, idx) => (
|
||||
<Image src={getImageUrl(imageUrl)} alt="Define Hotspots" fill unoptimized className="object-cover select-none" />
|
||||
{hotspots.map((h) => (
|
||||
<div
|
||||
key={h.id}
|
||||
className="absolute group/pin"
|
||||
@@ -246,6 +246,6 @@ export default function HotspotBlock({
|
||||
filterType="image"
|
||||
onSelect={handleImageSelect}
|
||||
/>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Brain, Plus, Trash2, HelpCircle } from "lucide-react";
|
||||
|
||||
interface MatchingPair {
|
||||
|
||||
Reference in New Issue
Block a user