Files
openccb/web/studio/src/app/admin/audio-evaluations/page.tsx
T
Nurfog e4866c6dee feat: SAM integration, deployment scripts, and audio response enhancements
- Add SAM (Sistema de Administración Académica) integration with sync endpoints
- Add deployment automation (deploy.sh, remote-setup.sh, setup-nginx-ssl.sh)
- Add nginx proxy configuration for SSL with Let's Encrypt
- Add audio response support for student lessons (migrations, handlers)
- Add audio evaluations admin page
- Update CORS to support wildcard subdomains for norteamericano.cl
- Add comprehensive deployment documentation (DESPLIEGUE.md, ManualDeConfiguracion.md)
- Update docker-compose.yml with nginx-proxy and acme-companion services
- Remove outdated documentation files

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-27 09:20:23 -03:00

407 lines
26 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import {
Mic,
Play,
Pause,
CheckCircle,
AlertCircle,
BrainCircuit,
User,
Calendar,
BookOpen,
Filter,
Download,
RefreshCcw
} from "lucide-react";
import { lmsApi, type AudioResponse, type AudioResponseFilters } from "@/lib/api";
import PageLayout from "@/components/PageLayout";
import AuthGuard from "@/components/AuthGuard";
export default function AudioEvaluationsPage() {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [evaluations, setEvaluations] = useState<AudioResponse[]>([]);
const [selectedEvaluation, setSelectedEvaluation] = useState<AudioResponse | null>(null);
const [teacherScore, setTeacherScore] = useState<number>(50);
const [teacherFeedback, setTeacherFeedback] = useState<string>("");
const [filters, setFilters] = useState<AudioResponseFilters>({});
const [playingId, setPlayingId] = useState<string | null>(null);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const fetchEvaluations = async () => {
setLoading(true);
try {
const data = await lmsApi.getAudioResponses(filters);
setEvaluations(data);
} catch (error) {
console.error("Error fetching audio evaluations:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchEvaluations();
}, [filters]);
const handlePlayAudio = async (id: string) => {
if (playingId === id) {
// Stop playing
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
}
setPlayingId(null);
setAudioUrl(null);
} else {
try {
const audioBlob = await lmsApi.getAudioResponseAudio(id);
const url = URL.createObjectURL(audioBlob);
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
}
setAudioUrl(url);
setPlayingId(id);
const audio = new Audio(url);
audio.onended = () => {
setPlayingId(null);
setAudioUrl(null);
};
audio.play();
} catch (error) {
console.error("Error playing audio:", error);
alert("Error al reproducir el audio");
}
}
};
const handleSubmitEvaluation = async () => {
if (!selectedEvaluation) return;
try {
await lmsApi.evaluateAudioResponse(
selectedEvaluation.id,
teacherScore,
teacherFeedback
);
alert("Evaluación guardada exitosamente");
setSelectedEvaluation(null);
fetchEvaluations();
} catch (error) {
console.error("Error submitting evaluation:", error);
alert("Error al guardar la evaluación");
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <span className="px-3 py-1 bg-yellow-500/10 border border-yellow-500/20 rounded-full text-xs font-bold text-yellow-600 dark:text-yellow-400">Pendiente</span>;
case 'ai_evaluated':
return <span className="px-3 py-1 bg-blue-500/10 border border-blue-500/20 rounded-full text-xs font-bold text-blue-600 dark:text-blue-400">IA Evaluada</span>;
case 'teacher_evaluated':
return <span className="px-3 py-1 bg-purple-500/10 border border-purple-500/20 rounded-full text-xs font-bold text-purple-600 dark:text-purple-400">Profesor Evaluó</span>;
case 'both_evaluated':
return <span className="px-3 py-1 bg-green-500/10 border border-green-500/20 rounded-full text-xs font-bold text-green-600 dark:text-green-400">Completa</span>;
default:
return null;
}
};
const getScoreColor = (score: number | null) => {
if (score === null) return 'text-gray-400';
if (score >= 70) return 'text-green-600 dark:text-green-400';
if (score >= 40) return 'text-yellow-600 dark:text-yellow-400';
return 'text-red-600 dark:text-red-400';
};
return (
<PageLayout title="Evaluaciones de Audio">
<div className="space-y-6">
{/* Filters */}
<div className="p-6 glass border-black/5 dark:border-white/5 rounded-3xl bg-black/[0.02] dark:bg-black/20">
<div className="flex items-center gap-3 mb-4">
<Filter className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100">Filtros</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Estado</label>
<select
value={filters.status || ''}
onChange={(e) => setFilters({ ...filters, status: e.target.value || undefined })}
className="w-full mt-1 px-4 py-2 bg-white dark:bg-black/40 border border-gray-200 dark:border-white/10 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500/20"
>
<option value="">Todos</option>
<option value="pending">Pendiente</option>
<option value="ai_evaluated">Solo IA</option>
<option value="teacher_evaluated">Solo Profesor</option>
<option value="both_evaluated">Completa</option>
</select>
</div>
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Curso ID</label>
<input
type="text"
value={filters.course_id || ''}
onChange={(e) => setFilters({ ...filters, course_id: e.target.value || undefined })}
placeholder="UUID del curso"
className="w-full mt-1 px-4 py-2 bg-white dark:bg-black/40 border border-gray-200 dark:border-white/10 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500/20"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Lección ID</label>
<input
type="text"
value={filters.lesson_id || ''}
onChange={(e) => setFilters({ ...filters, lesson_id: e.target.value || undefined })}
placeholder="UUID de la lección"
className="w-full mt-1 px-4 py-2 bg-white dark:bg-black/40 border border-gray-200 dark:border-white/10 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500/20"
/>
</div>
</div>
<button
onClick={() => {
setFilters({});
fetchEvaluations();
}}
className="mt-4 flex items-center gap-2 px-4 py-2 glass hover:bg-black/5 dark:hover:bg-white/10 rounded-xl text-sm font-bold text-purple-600 dark:text-purple-400 transition-all"
>
<RefreshCcw className="w-4 h-4" />
Limpiar filtros
</button>
</div>
{/* Evaluations Table */}
<div className="glass border-black/5 dark:border-white/5 rounded-3xl bg-black/[0.02] dark:bg-black/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gradient-to-r from-purple-600/10 to-pink-600/10 border-b border-black/5 dark:border-white/5">
<tr>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Estudiante</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Curso / Lección</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Prompt</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Transcripción</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">IA</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Profesor</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Estado</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Fecha</th>
<th className="px-6 py-4 text-left text-xs font-black uppercase tracking-widest text-gray-500 dark:text-gray-400">Acciones</th>
</tr>
</thead>
<tbody className="divide-y divide-black/5 dark:divide-white/5">
{loading ? (
<tr>
<td colSpan={9} className="px-6 py-12 text-center">
<div className="flex items-center justify-center gap-3 text-gray-400">
<RefreshCcw className="w-6 h-6 animate-spin" />
<span className="text-sm font-medium">Cargando evaluaciones...</span>
</div>
</td>
</tr>
) : evaluations.length === 0 ? (
<tr>
<td colSpan={9} className="px-6 py-12 text-center text-gray-400">
<Mic className="w-12 h-12 mx-auto mb-3 opacity-20" />
<p className="text-sm font-medium">No hay evaluaciones de audio</p>
</td>
</tr>
) : (
evaluations.map((eval_) => (
<tr key={eval_.id} className="hover:bg-black/5 dark:hover:bg-white/5 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-purple-500 to-pink-500 rounded-full flex items-center justify-center text-white text-xs font-bold">
{eval_.student_name.charAt(0).toUpperCase()}
</div>
<div>
<p className="text-sm font-bold text-gray-900 dark:text-gray-100">{eval_.student_name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{eval_.student_email}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="max-w-[200px]">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate" title={eval_.course_title}>{eval_.course_title}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate" title={eval_.lesson_title}>{eval_.lesson_title}</p>
</div>
</td>
<td className="px-6 py-4">
<p className="text-sm text-gray-700 dark:text-gray-300 max-w-[200px] truncate" title={eval_.prompt}>{eval_.prompt}</p>
</td>
<td className="px-6 py-4">
<p className="text-sm text-gray-700 dark:text-gray-300 max-w-[200px] truncate" title={eval_.transcript || ''}>
{eval_.transcript || <span className="text-gray-400 italic">Sin transcripción</span>}
</p>
</td>
<td className="px-6 py-4">
{eval_.ai_score !== null ? (
<div className="space-y-1">
<p className={`text-lg font-black ${getScoreColor(eval_.ai_score)}`}>{eval_.ai_score}%</p>
{eval_.ai_found_keywords && eval_.ai_found_keywords.length > 0 && (
<p className="text-xs text-gray-500 dark:text-gray-400">
Keywords: {eval_.ai_found_keywords.join(', ')}
</p>
)}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="px-6 py-4">
{eval_.teacher_score !== null ? (
<p className={`text-lg font-black ${getScoreColor(eval_.teacher_score)}`}>{eval_.teacher_score}%</p>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="px-6 py-4">
{getStatusBadge(eval_.status)}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<Calendar className="w-3 h-3" />
{new Date(eval_.created_at).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button
onClick={() => handlePlayAudio(eval_.id)}
className="p-2 glass hover:bg-black/5 dark:hover:bg-white/10 rounded-lg transition-all"
title="Reproducir audio"
>
{playingId === eval_.id ? (
<Pause className="w-4 h-4 text-purple-600 dark:text-purple-400" />
) : (
<Play className="w-4 h-4 text-purple-600 dark:text-purple-400" />
)}
</button>
<button
onClick={() => setSelectedEvaluation(eval_)}
className="p-2 glass hover:bg-purple-500/10 rounded-lg transition-all"
title="Evaluar"
>
<BrainCircuit className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Evaluation Modal */}
{selectedEvaluation && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="glass border-black/5 dark:border-white/5 rounded-3xl bg-white dark:bg-gray-900 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-8 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-2xl font-black text-gray-900 dark:text-white">Evaluar Respuesta de Audio</h3>
<button
onClick={() => setSelectedEvaluation(null)}
className="p-2 glass hover:bg-black/5 dark:hover:bg-white/10 rounded-xl transition-all"
>
<AlertCircle className="w-5 h-5 text-gray-600 dark:text-gray-400" />
</button>
</div>
{/* Student Info */}
<div className="p-4 bg-gradient-to-r from-purple-500/10 to-pink-500/10 rounded-2xl border border-purple-500/20">
<div className="flex items-center gap-3 mb-2">
<User className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<p className="text-sm font-bold text-gray-900 dark:text-gray-100">{selectedEvaluation.student_name}</p>
</div>
<div className="flex items-center gap-3">
<BookOpen className="w-4 h-4 text-gray-500" />
<p className="text-xs text-gray-600 dark:text-gray-400">{selectedEvaluation.course_title} {selectedEvaluation.lesson_title}</p>
</div>
</div>
{/* Prompt */}
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Prompt</label>
<p className="mt-1 text-lg font-medium text-gray-900 dark:text-gray-100">{selectedEvaluation.prompt}</p>
</div>
{/* Transcript */}
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Transcripción</label>
<p className="mt-1 p-4 bg-black/5 dark:bg-white/5 rounded-xl text-gray-700 dark:text-gray-300">
{selectedEvaluation.transcript || <span className="italic text-gray-400">Sin transcripción disponible</span>}
</p>
</div>
{/* AI Evaluation */}
{selectedEvaluation.ai_score !== null && (
<div className="p-4 bg-blue-500/10 border border-blue-500/20 rounded-2xl space-y-3">
<div className="flex items-center gap-2">
<BrainCircuit className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<label className="text-xs font-bold text-blue-600 dark:text-blue-400 uppercase tracking-wider">Evaluación de IA</label>
</div>
<div className="flex items-center gap-4">
<p className="text-3xl font-black text-blue-600 dark:text-blue-400">{selectedEvaluation.ai_score}%</p>
</div>
{selectedEvaluation.ai_feedback && (
<p className="text-sm text-gray-700 dark:text-gray-300 italic">"{selectedEvaluation.ai_feedback}"</p>
)}
</div>
)}
{/* Teacher Evaluation */}
<div className="space-y-4">
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Puntuación (0-100)</label>
<input
type="number"
min="0"
max="100"
value={teacherScore}
onChange={(e) => setTeacherScore(parseInt(e.target.value))}
className="w-full mt-1 px-4 py-3 bg-white dark:bg-black/40 border border-gray-200 dark:border-white/10 rounded-xl text-lg font-bold outline-none focus:ring-2 focus:ring-purple-500/20"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Feedback</label>
<textarea
value={teacherFeedback}
onChange={(e) => setTeacherFeedback(e.target.value)}
placeholder="Escribe tu feedback para el estudiante..."
rows={4}
className="w-full mt-1 px-4 py-3 bg-white dark:bg-black/40 border border-gray-200 dark:border-white/10 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500/20 resize-none"
/>
</div>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4">
<button
onClick={() => setSelectedEvaluation(null)}
className="flex-1 py-3 glass hover:bg-black/5 dark:hover:bg-white/10 rounded-xl font-bold text-gray-600 dark:text-gray-300 transition-all"
>
Cancelar
</button>
<button
onClick={handleSubmitEvaluation}
className="flex-1 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 rounded-xl font-bold text-white shadow-lg shadow-purple-500/30 transition-all"
>
Guardar Evaluación
</button>
</div>
</div>
</div>
</div>
)}
</div>
</PageLayout>
);
}