feat: Implement AI tutor functionality, add branding fields, and improve API URL handling.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { lmsApi } from "@/lib/api";
|
||||
import { Send, Bot, User, X, MessageSquare, Loader2 } from "lucide-react";
|
||||
|
||||
interface Message {
|
||||
role: 'tutor' | 'user';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function AITutor({ lessonId }: { lessonId: string }) {
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{ role: 'tutor', content: '¡Hola! Soy tu tutor de IA. ¿Tienes alguna duda sobre esta lección?' }
|
||||
]);
|
||||
const [input, setInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMessage = input.trim();
|
||||
setInput("");
|
||||
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { response } = await lmsApi.chatWithTutor(lessonId, userMessage);
|
||||
setMessages(prev => [...prev, { role: 'tutor', content: response }]);
|
||||
} catch (error) {
|
||||
console.error("Chat error:", error);
|
||||
setMessages(prev => [...prev, { role: 'tutor', content: "Lo siento, hubo un error conectando con el tutor. Por favor intenta de nuevo." }]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-24 right-6 w-14 h-14 rounded-2xl bg-blue-600 text-white shadow-lg shadow-blue-500/40 flex items-center justify-center hover:scale-110 transition-all z-[100] group"
|
||||
title="Abrir Tutor de IA"
|
||||
>
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 border-2 border-black rounded-full animate-pulse" />
|
||||
<MessageSquare className="w-6 h-6 group-hover:rotate-12 transition-transform" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-24 right-6 w-80 md:w-96 h-[500px] glass bg-black/80 backdrop-blur-2xl border border-white/10 rounded-3xl shadow-2xl flex flex-col z-[200] animate-in slide-in-from-bottom-6 duration-500 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-white/5 bg-blue-600/10 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-500 flex items-center justify-center shadow-lg shadow-blue-500/20">
|
||||
<Bot className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-white uppercase tracking-widest">Tutor de IA</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-green-500 rounded-full" />
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-tighter">En Línea</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-2 hover:bg-white/5 rounded-lg text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide"
|
||||
>
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div className={`flex gap-2 max-w-[85%] ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
|
||||
<div className={`shrink-0 w-8 h-8 rounded-lg flex items-center justify-center ${msg.role === 'user' ? 'bg-white/5' : 'bg-blue-600/20 text-blue-400'}`}>
|
||||
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
||||
</div>
|
||||
<div className={`p-3 rounded-2xl text-xs font-medium leading-relaxed ${msg.role === 'user'
|
||||
? 'bg-blue-600 text-white rounded-tr-none'
|
||||
: 'bg-white/5 text-gray-200 border border-white/5 rounded-tl-none'
|
||||
}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start animate-in fade-in duration-300">
|
||||
<div className="flex gap-2 max-w-[85%]">
|
||||
<div className="shrink-0 w-8 h-8 rounded-lg bg-blue-600/20 text-blue-400 flex items-center justify-center">
|
||||
<Bot size={16} />
|
||||
</div>
|
||||
<div className="bg-white/5 text-gray-400 border border-white/5 p-3 rounded-2xl rounded-tl-none flex items-center gap-2">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest">El tutor está pensando...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-white/5 bg-black/40">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
placeholder="Escribe tu duda aquí..."
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl py-3 px-4 pr-12 text-xs font-medium focus:outline-none focus:border-blue-500/50 transition-colors placeholder:text-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="absolute right-2 top-1.5 p-1.5 bg-blue-600 text-white rounded-lg disabled:opacity-50 disabled:bg-gray-600 transition-all hover:bg-blue-500"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-2 text-[9px] text-gray-600 font-bold uppercase tracking-widest text-center">
|
||||
IA entrenada con el contenido de esta lección
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import Image from "next/image";
|
||||
import { useBranding } from "@/context/BrandingContext";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useTranslation } from "@/context/I18nContext";
|
||||
import { LogOut, Globe } from "lucide-react";
|
||||
import { LogOut, Globe, Menu, X } from "lucide-react";
|
||||
import NotificationCenter from "./NotificationCenter";
|
||||
import { useState } from "react";
|
||||
|
||||
import { lmsApi, getImageUrl } from "@/lib/api";
|
||||
|
||||
@@ -14,14 +15,15 @@ export default function AppHeader() {
|
||||
const { t, language, setLanguage } = useTranslation();
|
||||
const { branding } = useBranding();
|
||||
const { user, logout } = useAuth();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
// Use platform_name if available, otherwise name, otherwise default
|
||||
const platformName = branding?.platform_name || branding?.name || 'OpenCCB';
|
||||
|
||||
return (
|
||||
<header className="h-16 glass sticky top-0 z-50 px-6 flex items-center justify-between backdrop-blur-xl bg-black/40 border-b border-white/5">
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center font-black text-white shadow-lg shadow-blue-500/20 group-hover:scale-105 transition-all overflow-hidden relative">
|
||||
<header className="h-16 glass sticky top-0 z-[100] px-4 md:px-6 flex items-center justify-between backdrop-blur-xl bg-black/40 border-b border-white/5">
|
||||
<Link href="/" className="flex items-center gap-2 md:gap-3 group">
|
||||
<div className="w-8 h-8 md:w-10 md:h-10 rounded-lg md:rounded-xl bg-blue-600 flex items-center justify-center font-black text-white shadow-lg shadow-blue-500/20 group-hover:scale-105 transition-all overflow-hidden relative">
|
||||
{branding?.logo_url ? (
|
||||
<Image src={getImageUrl(branding.logo_url)} alt={branding.name} fill className="object-contain" sizes="40px" />
|
||||
) : (
|
||||
@@ -31,53 +33,129 @@ export default function AppHeader() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col -gap-1">
|
||||
<span className="font-black text-lg tracking-tighter text-white leading-none">
|
||||
<span className="font-black text-sm md:text-lg tracking-tighter text-white leading-none">
|
||||
{platformName.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-[10px] font-black tracking-widest text-blue-500 uppercase">EXPERIENCIA</span>
|
||||
<span className="text-[8px] md:text-[10px] font-black tracking-widest text-blue-500 uppercase">EXPERIENCIA</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-2 md:gap-8">
|
||||
<div className="hidden md:flex items-center gap-8 mr-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<nav className="hidden md:flex items-center gap-8 mr-4">
|
||||
<Link href="/" className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 hover:text-white transition-colors">
|
||||
{t('nav.catalog')}
|
||||
</Link>
|
||||
<Link href="/my-learning" className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-400 hover:text-white transition-colors">
|
||||
{t('nav.myLearning')}
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<NotificationCenter />
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
<NotificationCenter />
|
||||
|
||||
<div className="flex items-center gap-2 border-l border-white/10 pl-4">
|
||||
<Globe size={14} className="text-gray-500" />
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
className="bg-transparent text-[10px] font-black uppercase tracking-widest text-gray-500 hover:text-white transition-colors focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="en" className="bg-[#0f1115]">EN</option>
|
||||
<option value="es" className="bg-[#0f1115]">ES</option>
|
||||
<option value="pt" className="bg-[#0f1115]">PT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-2 border-l border-white/10 pl-4">
|
||||
<Globe size={14} className="text-gray-500" />
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
className="bg-transparent text-[10px] font-black uppercase tracking-widest text-gray-500 hover:text-white transition-colors focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="en" className="bg-[#0f1115]">EN</option>
|
||||
<option value="es" className="bg-[#0f1115]">ES</option>
|
||||
<option value="pt" className="bg-[#0f1115]">PT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 md:gap-4 pl-4 border-l border-white/10">
|
||||
<Link href="/profile" className="flex items-center gap-2 group/profile">
|
||||
<div className="w-8 h-8 rounded-full bg-white/5 border border-white/10 flex items-center justify-center font-bold text-xs text-blue-400 group-hover/profile:border-blue-500/50 transition-colors">
|
||||
{user?.full_name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center gap-4 pl-4 border-l border-white/10">
|
||||
<Link href="/profile" className="flex items-center gap-2 group/profile">
|
||||
<div className="w-8 h-8 rounded-full bg-white/5 border border-white/10 flex items-center justify-center font-bold text-xs text-blue-400 group-hover/profile:border-blue-500/50 transition-colors">
|
||||
{user?.full_name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="p-2 hover:bg-red-500/10 rounded-full text-gray-400 hover:text-red-400 transition-colors"
|
||||
title={t('nav.signOut')}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
onClick={logout}
|
||||
className="p-2 hover:bg-red-500/10 rounded-full text-gray-400 hover:text-red-400 transition-colors"
|
||||
title={t('nav.signOut')}
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="md:hidden p-2 hover:bg-white/5 rounded-lg text-gray-400 transition-colors"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Mobile Sidebar Overlay */}
|
||||
{isMenuOpen && (
|
||||
<div className="fixed inset-0 z-[150] md:hidden bg-black/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="absolute right-0 top-0 bottom-0 w-64 glass border-l border-white/10 p-6 flex flex-col animate-in slide-in-from-right duration-300">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<span className="font-black text-xs uppercase tracking-[0.2em] text-gray-500">Menú</span>
|
||||
<button onClick={() => setIsMenuOpen(false)} className="p-2 hover:bg-white/5 rounded-lg">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col gap-6 flex-1">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="text-sm font-black uppercase tracking-widest text-gray-300 hover:text-white border-l-2 border-transparent hover:border-blue-500 pl-4 transition-all"
|
||||
>
|
||||
{t('nav.catalog')}
|
||||
</Link>
|
||||
<Link
|
||||
href="/my-learning"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="text-sm font-black uppercase tracking-widest text-gray-300 hover:text-white border-l-2 border-transparent hover:border-blue-500 pl-4 transition-all"
|
||||
>
|
||||
{t('nav.myLearning')}
|
||||
</Link>
|
||||
|
||||
<div className="pt-6 mt-6 border-t border-white/5 space-y-4">
|
||||
<div className="flex items-center gap-3 px-4 py-2 rounded-xl bg-white/5">
|
||||
<Globe size={16} className="text-gray-500" />
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
className="bg-transparent text-xs font-bold uppercase tracking-widest text-gray-300 focus:outline-none flex-1"
|
||||
>
|
||||
<option value="en" className="bg-[#0f1115]">English</option>
|
||||
<option value="es" className="bg-[#0f1115]">Español</option>
|
||||
<option value="pt" className="bg-[#0f1115]">Português</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="pt-6 border-t border-white/5 space-y-4">
|
||||
<Link
|
||||
href="/profile"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center font-bold text-xs text-blue-400">
|
||||
{user?.full_name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
<span className="text-sm font-bold">{user?.full_name || 'Mi Perfil'}</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
<span className="text-sm font-bold">{t('nav.signOut')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { lmsApi, UserGrade } from "@/lib/api";
|
||||
import { Trophy, Award, BookOpen, RotateCcw, Bot, Loader2, Star, Sparkles, CheckCircle2 } from "lucide-react";
|
||||
|
||||
interface LessonLockedViewProps {
|
||||
lessonId: string;
|
||||
courseId: string;
|
||||
grade: UserGrade;
|
||||
maxAttempts?: number;
|
||||
}
|
||||
|
||||
export default function LessonLockedView({ lessonId, courseId, grade, maxAttempts }: LessonLockedViewProps) {
|
||||
const [feedback, setFeedback] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFeedback = async () => {
|
||||
try {
|
||||
const res = await lmsApi.getLessonFeedback(lessonId);
|
||||
setFeedback(res.response);
|
||||
} catch (err) {
|
||||
console.error("Error fetching AI feedback:", err);
|
||||
setFeedback("¡Buen trabajo completando esta evaluación! Sigue así para mejorar tus resultados en las próximas lecciones.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchFeedback();
|
||||
}, [lessonId]);
|
||||
|
||||
const scorePct = Math.round(grade.score * 100);
|
||||
const isPassing = scorePct >= 70; // Assuming 70% is passing
|
||||
|
||||
return (
|
||||
<div className="space-y-12 animate-in fade-in slide-in-from-bottom-8 duration-1000">
|
||||
{/* Header / Score Card */}
|
||||
<div className="relative group">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-[3rem] blur opacity-25 group-hover:opacity-40 transition duration-1000"></div>
|
||||
<div className="relative glass p-10 md:p-16 rounded-[2.5rem] border border-white/10 bg-black/40 text-center space-y-8 overflow-hidden">
|
||||
{/* Background visual flair */}
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10">
|
||||
<Sparkles size={120} className="text-blue-500" />
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-2 px-6 py-2 bg-blue-600/10 border border-blue-500/20 text-blue-400 rounded-full text-[10px] font-black uppercase tracking-[0.2em]">
|
||||
<CheckCircle2 size={12} /> Evaluación Finalizada
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-5xl md:text-7xl font-black text-white tracking-tighter">
|
||||
Tu Puntuación: <span className={isPassing ? "text-blue-500" : "text-amber-500"}>{scorePct}%</span>
|
||||
</h2>
|
||||
<div className="flex justify-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map((s) => (
|
||||
<Star
|
||||
key={s}
|
||||
size={32}
|
||||
className={`${s <= Math.ceil(scorePct / 20) ? "text-yellow-500 fill-yellow-500" : "text-white/5"} transition-all`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6 pt-4">
|
||||
<div className="flex items-center gap-3 px-6 py-4 rounded-2xl bg-white/5 border border-white/5">
|
||||
<RotateCcw size={20} className="text-gray-500" />
|
||||
<div className="text-left">
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-gray-500">Intentos Usados</p>
|
||||
<p className="text-lg font-black text-white">{grade.attempts_count} {maxAttempts ? `de ${maxAttempts}` : ""}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-6 py-4 rounded-2xl bg-white/5 border border-white/5">
|
||||
<Trophy size={20} className="text-amber-500" />
|
||||
<div className="text-left">
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-gray-500">Estado</p>
|
||||
<p className={`text-lg font-black uppercase ${isPassing ? "text-green-500" : "text-amber-500"}`}>
|
||||
{isPassing ? "Aprobado" : "No Alcanzado"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Feedback Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="glass p-8 md:p-12 rounded-[2.5rem] border border-white/5 bg-blue-600/5 relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 -mr-16 -mt-16 w-64 h-64 bg-blue-600/10 blur-[100px] rounded-full group-hover:bg-blue-600/20 transition-all duration-1000"></div>
|
||||
|
||||
<h3 className="text-xs font-black uppercase tracking-[0.3em] text-blue-400 mb-8 flex items-center gap-3">
|
||||
<Bot size={20} className="animate-bounce" /> Retroalimentación de tu Tutor de IA
|
||||
</h3>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
||||
<Loader2 className="w-12 h-12 text-blue-500 animate-spin" />
|
||||
<p className="text-xs font-black uppercase tracking-widest text-gray-500">Generando análisis personalizado...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="text-xl md:text-2xl text-gray-200 leading-relaxed font-medium">
|
||||
{feedback}
|
||||
</div>
|
||||
<div className="h-px w-20 bg-blue-500/40 rounded-full"></div>
|
||||
<p className="text-xs font-bold text-gray-500 italic uppercase tracking-wider">
|
||||
Este análisis es generado automáticamente basándose en tu desempeño histórico y los contenidos de esta lección.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<div className="glass p-8 rounded-[2rem] border border-white/5 bg-white/[0.02]">
|
||||
<h3 className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-6 flex items-center gap-2">
|
||||
<BookOpen size={16} className="text-blue-500" /> Próximos Pasos
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex items-start gap-3 p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-colors border border-transparent hover:border-white/10 group cursor-pointer">
|
||||
<div className="w-8 h-8 rounded-lg bg-green-500/20 text-green-400 flex items-center justify-center shrink-0">
|
||||
<Award size={16} />
|
||||
</div>
|
||||
<p className="text-xs font-bold text-gray-300 leading-tight group-hover:text-white transition-colors">Continúa con la siguiente lección para seguir sumando XP.</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-3 p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-colors border border-transparent hover:border-white/10 group cursor-pointer">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-500/20 text-blue-400 flex items-center justify-center shrink-0">
|
||||
<BookOpen size={16} />
|
||||
</div>
|
||||
<p className="text-xs font-bold text-gray-300 leading-tight group-hover:text-white transition-colors">Revisa el glosario de términos de esta sección.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-8 rounded-[2rem] bg-amber-500/5 border border-amber-500/10">
|
||||
<p className="text-[10px] font-black text-amber-500 uppercase tracking-widest mb-2">Nota Importante</p>
|
||||
<p className="text-[11px] font-bold text-amber-500/70 leading-relaxed uppercase tracking-tight">
|
||||
Has alcanzado el máximo de intentos. Esta evaluación está bloqueada, pero puedes seguir repasando los materiales del curso.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FileText, Download, Eye, ExternalLink } from "lucide-react";
|
||||
import { CMS_API_URL } from "@/lib/api";
|
||||
import { getCmsApiUrl } from "@/lib/api";
|
||||
|
||||
interface DocumentPlayerProps {
|
||||
id: string;
|
||||
@@ -18,7 +18,7 @@ export default function DocumentPlayer({ id, title, url }: DocumentPlayerProps)
|
||||
if (path.startsWith('http')) return path;
|
||||
const cleanPath = path.startsWith('/uploads') ? path.replace('/uploads', '/assets') : path;
|
||||
const finalPath = cleanPath.startsWith('/') ? cleanPath : `/${cleanPath}`;
|
||||
return `${CMS_API_URL}${finalPath}`;
|
||||
return `${getCmsApiUrl()}${finalPath}`;
|
||||
};
|
||||
|
||||
const displayUrl = getFullUrl(url);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Play, Lock, AlertCircle } from "lucide-react";
|
||||
import { lmsApi } from "@/lib/api";
|
||||
import { lmsApi, getCmsApiUrl } from "@/lib/api";
|
||||
|
||||
interface MediaPlayerProps {
|
||||
id: string;
|
||||
@@ -46,14 +46,14 @@ export default function MediaPlayer({ id, lessonId, title, url, media_type, conf
|
||||
|
||||
const maxPlays = config?.maxPlays || 0;
|
||||
|
||||
const CMS_API_URL = process.env.NEXT_PUBLIC_CMS_API_URL || "http://localhost:3001";
|
||||
|
||||
|
||||
const getFullUrl = (path: string) => {
|
||||
if (path.startsWith('http')) return path;
|
||||
// Map /uploads to /assets for the backend
|
||||
const cleanPath = path.startsWith('/uploads') ? path.replace('/uploads', '/assets') : path;
|
||||
const finalPath = cleanPath.startsWith('/') ? cleanPath : `/${cleanPath}`;
|
||||
return `${CMS_API_URL}${finalPath}`;
|
||||
return `${getCmsApiUrl()}${finalPath}`;
|
||||
};
|
||||
|
||||
const isLocalFile = url.startsWith('/uploads') || url.startsWith('http://localhost:3001/assets') || url.includes('/assets/');
|
||||
@@ -129,8 +129,8 @@ export default function MediaPlayer({ id, lessonId, title, url, media_type, conf
|
||||
// Since browser <track> doesn't support custom headers easily,
|
||||
// we might need to handle this via a proxy or temporary signed URLs.
|
||||
// For now, we'll assume the backend allows VTT access if requested with the correct lesson ID.
|
||||
const vttEn = lessonId ? `${CMS_API_URL}/lessons/${lessonId}/vtt?lang=en` : null;
|
||||
const vttEs = lessonId ? `${CMS_API_URL}/lessons/${lessonId}/vtt?lang=es` : null;
|
||||
const vttEn = lessonId ? `${getCmsApiUrl()}/lessons/${lessonId}/vtt?lang=en` : null;
|
||||
const vttEs = lessonId ? `${getCmsApiUrl()}/lessons/${lessonId}/vtt?lang=es` : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6" id={id}>
|
||||
|
||||
Reference in New Issue
Block a user