feat: i18n full support, responsive UI, multi-model AI config, and bug fixes
Major Features:
- Internationalization (i18n) with auto-detection for ES/EN/PT
- Mobile-first responsive design for Studio and Experience
- Multi-model AI configuration (llama3.2:3b, qwen3.5:9b, gpt-oss:latest)
- Course language configuration (auto-detect or fixed per course)
Backend Changes:
- shared/common: ModelType enum for intelligent model selection
- LMS: log_ai_usage function migration (fix chat tutor 500 error)
- LMS/CMS: course language config fields (language_setting, fixed_language)
- LMS: /courses/{id}/language-config endpoint for language detection
Frontend Changes:
- Experience: Enhanced i18n with browser language detection
- Experience: Audio recording with HTTPS check and error handling
- Studio: Memory game with unique pair IDs and debug logging
- Studio: Expanded translations (250+ keys for ES, EN, PT)
- Both: Language selector in headers (mobile responsive)
Documentation:
- AI_MODELS_CONFIG.md: Multi-model configuration guide
- RESPONSIVIDAD_GUIA.md: Mobile-first design patterns
- I18N_RESPONSIVIDAD_IMPLEMENTACION.md: Implementation details
- DEBUG_AUDIO_RECORDING.md: Audio troubleshooting guide
- DEBUG_MEMORY_GAME.md: Memory game debugging steps
Bug Fixes:
- Fix chat tutor 500 error (missing log_ai_usage function)
- Fix audio recording (HTTPS check, browser compatibility)
- Fix memory game pair IDs (unique ID generation)
- Fix HotspotBlock TypeScript errors
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -66,32 +66,72 @@ export default function AudioResponsePlayer({
|
||||
}, []);
|
||||
|
||||
const startRecording = async () => {
|
||||
// Check if browser supports MediaRecorder
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
console.error('MediaRecorder API not supported');
|
||||
alert('Your browser does not support audio recording. Please use Chrome, Firefox, or Edge.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if page is served over HTTPS or localhost
|
||||
const isSecureContext = window.isSecureContext || window.location.protocol === 'https:' || window.location.hostname === 'localhost';
|
||||
if (!isSecureContext) {
|
||||
console.error('getUserMedia requires secure context (HTTPS or localhost)');
|
||||
alert('Audio recording requires HTTPS. Please access the site via HTTPS or localhost.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
console.log('[AudioResponse] Requesting microphone access...');
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
sampleRate: 44100
|
||||
}
|
||||
});
|
||||
console.log('[AudioResponse] Microphone access granted');
|
||||
|
||||
const mediaRecorder = new MediaRecorder(stream, {
|
||||
mimeType: MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'
|
||||
});
|
||||
mediaRecorderRef.current = mediaRecorder;
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
audioChunksRef.current.push(event.data);
|
||||
console.log('[AudioResponse] Data available, chunk size:', event.data.size);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
console.log('[AudioResponse] Recording stopped, creating blob...');
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||
setAudioBlob(audioBlob);
|
||||
console.log('[AudioResponse] Blob created, size:', audioBlob.size, 'bytes');
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.onerror = (event) => {
|
||||
console.error('[AudioResponse] MediaRecorder error:', event);
|
||||
alert('Recording error occurred. Please try again.');
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
setIsRecording(true);
|
||||
setRecordingTime(0);
|
||||
console.log('[AudioResponse] Recording started');
|
||||
|
||||
// Start speech recognition
|
||||
if (recognitionRef.current) {
|
||||
setTranscript("");
|
||||
recognitionRef.current.start();
|
||||
try {
|
||||
recognitionRef.current.start();
|
||||
console.log('[AudioResponse] Speech recognition started');
|
||||
} catch (err) {
|
||||
console.warn('[AudioResponse] Could not start speech recognition:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Start timer
|
||||
@@ -99,14 +139,29 @@ export default function AudioResponsePlayer({
|
||||
setRecordingTime(prev => {
|
||||
const newTime = prev + 1;
|
||||
if (timeLimit && newTime >= timeLimit) {
|
||||
console.log('[AudioResponse] Time limit reached, stopping...');
|
||||
stopRecording();
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error accessing microphone:', error);
|
||||
alert('Could not access microphone. Please check permissions.');
|
||||
} catch (error: any) {
|
||||
console.error('[AudioResponse] Error accessing microphone:', error);
|
||||
let errorMessage = 'Could not access microphone. ';
|
||||
|
||||
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
|
||||
errorMessage += 'Please allow microphone access and try again.';
|
||||
} else if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
|
||||
errorMessage += 'No microphone found. Please connect a microphone and try again.';
|
||||
} else if (error.name === 'NotReadableError' || error.name === 'TrackStartError') {
|
||||
errorMessage += 'Microphone is already in use by another application.';
|
||||
} else if (error.name === 'OverconstrainedError') {
|
||||
errorMessage += 'Microphone does not meet the required constraints.';
|
||||
} else {
|
||||
errorMessage += 'Please check permissions and try again.';
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,6 +232,31 @@ export default function AudioResponsePlayer({
|
||||
|
||||
return (
|
||||
<div className="space-y-6" id={id}>
|
||||
{/* Browser Compatibility Check */}
|
||||
{typeof window !== 'undefined' && (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) && (
|
||||
<div className="p-6 bg-red-500/10 border-2 border-red-500/20 rounded-2xl">
|
||||
<div className="flex items-center gap-3 text-red-600 dark:text-red-400 mb-2">
|
||||
<X className="w-6 h-6" />
|
||||
<h3 className="text-lg font-bold">Browser Not Supported</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Your browser does not support audio recording. Please use Chrome, Firefox, Edge, or Safari.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!window.isSecureContext && window.location.protocol !== 'https:' && window.location.hostname !== 'localhost' && (
|
||||
<div className="p-6 bg-yellow-500/10 border-2 border-yellow-500/20 rounded-2xl">
|
||||
<div className="flex items-center gap-3 text-yellow-600 dark:text-yellow-400 mb-2">
|
||||
<BrainCircuit className="w-6 h-6" />
|
||||
<h3 className="text-lg font-bold">HTTPS Required</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Audio recording requires a secure connection (HTTPS). Please access this site via HTTPS or localhost.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-8 glass border-black/5 dark:border-white/5 rounded-3xl space-y-6 bg-black/[0.02] dark:bg-black/20">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-purple-600/10 dark:bg-purple-500/20 rounded-xl">
|
||||
|
||||
@@ -7,27 +7,71 @@ import pt from '../lib/locales/pt.json';
|
||||
|
||||
const translations: Record<string, any> = { en, es, pt };
|
||||
|
||||
// Idiomas soportados
|
||||
export const SUPPORTED_LANGUAGES = ['es', 'en', 'pt'] as const;
|
||||
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||||
|
||||
interface I18nContextType {
|
||||
language: string;
|
||||
setLanguage: (lang: string) => void;
|
||||
t: (path: string) => string;
|
||||
detectBrowserLanguage: () => string;
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Detecta el idioma del navegador del usuario
|
||||
*/
|
||||
function detectBrowserLanguage(): string {
|
||||
if (typeof navigator === 'undefined') return 'es';
|
||||
|
||||
// Obtener idiomas del navegador (puede ser un array)
|
||||
const browserLanguages = navigator.languages || [navigator.language || (navigator as any).userLanguage];
|
||||
|
||||
// Buscar el primer idioma soportado
|
||||
for (const browserLang of browserLanguages) {
|
||||
// Idioma completo (ej: 'en-US')
|
||||
if (SUPPORTED_LANGUAGES.includes(browserLang as SupportedLanguage)) {
|
||||
return browserLang;
|
||||
}
|
||||
|
||||
// Solo código de idioma (ej: 'en' de 'en-US')
|
||||
const langCode = browserLang.split('-')[0].toLowerCase();
|
||||
if (SUPPORTED_LANGUAGES.includes(langCode as SupportedLanguage)) {
|
||||
return langCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback a español
|
||||
return 'es';
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const [language, setLanguageState] = useState('es'); // Default to Spanish for Experience as it's more localized by default
|
||||
const [language, setLanguageState] = useState('es');
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Primero intentar cargar de localStorage
|
||||
const savedLang = localStorage.getItem('experience_language');
|
||||
if (savedLang) {
|
||||
|
||||
if (savedLang && SUPPORTED_LANGUAGES.includes(savedLang as SupportedLanguage)) {
|
||||
setLanguageState(savedLang);
|
||||
} else {
|
||||
// 2. Si no hay guardado, detectar del navegador
|
||||
const detectedLang = detectBrowserLanguage();
|
||||
setLanguageState(detectedLang);
|
||||
localStorage.setItem('experience_language', detectedLang);
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
}, []);
|
||||
|
||||
const setLanguage = (lang: string) => {
|
||||
setLanguageState(lang);
|
||||
localStorage.setItem('experience_language', lang);
|
||||
if (SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)) {
|
||||
setLanguageState(lang);
|
||||
localStorage.setItem('experience_language', lang);
|
||||
}
|
||||
};
|
||||
|
||||
const t = (path: string): string => {
|
||||
@@ -46,7 +90,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={{ language, setLanguage, t }}>
|
||||
<I18nContext.Provider value={{ language, setLanguage, t, detectBrowserLanguage }}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
@@ -59,3 +103,5 @@ export function useTranslation() {
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { detectBrowserLanguage };
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from '@/context/I18nContext';
|
||||
|
||||
interface CourseLanguageConfig {
|
||||
language_setting: 'auto' | 'fixed';
|
||||
fixed_language: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook para manejar el idioma específico de un curso
|
||||
*
|
||||
* - Si el curso está en modo 'auto', usa el idioma del usuario
|
||||
* - Si el curso está en modo 'fixed', usa el idioma fijo del curso
|
||||
*
|
||||
* @param courseId - ID del curso actual
|
||||
* @returns El idioma que debe usarse para este curso
|
||||
*/
|
||||
export function useCourseLanguage(courseId: string | null) {
|
||||
const { language: userLanguage } = useTranslation();
|
||||
const [courseLanguage, setCourseLanguage] = useState<string>(userLanguage);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [config, setConfig] = useState<CourseLanguageConfig | null>(null);
|
||||
|
||||
// Función para cargar la configuración de idioma del curso
|
||||
const loadCourseLanguageConfig = useCallback(async () => {
|
||||
if (!courseId) {
|
||||
setCourseLanguage(userLanguage);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Importar dinámicamente para evitar circular dependencies
|
||||
const { lmsApi } = await import('@/lib/api');
|
||||
|
||||
const response = await lmsApi.get(`/courses/${courseId}/language-config`);
|
||||
const data = response.data;
|
||||
|
||||
setConfig(data);
|
||||
|
||||
// Determinar qué idioma usar
|
||||
if (data.language_setting === 'fixed' && data.fixed_language) {
|
||||
setCourseLanguage(data.fixed_language);
|
||||
} else {
|
||||
// Modo 'auto' o sin configuración: usar idioma del usuario
|
||||
setCourseLanguage(userLanguage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading course language config:', error);
|
||||
// Fallback: usar idioma del usuario
|
||||
setCourseLanguage(userLanguage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [courseId, userLanguage]);
|
||||
|
||||
// Cargar configuración cuando cambia el courseId
|
||||
useEffect(() => {
|
||||
loadCourseLanguageConfig();
|
||||
}, [loadCourseLanguageConfig]);
|
||||
|
||||
// Función para verificar si el curso usa idioma fijo
|
||||
const isFixedLanguage = useCallback(() => {
|
||||
return config?.language_setting === 'fixed';
|
||||
}, [config]);
|
||||
|
||||
// Función para obtener el idioma actual (curso o usuario)
|
||||
const getCurrentLanguage = useCallback(() => {
|
||||
return courseLanguage;
|
||||
}, [courseLanguage]);
|
||||
|
||||
return {
|
||||
courseLanguage,
|
||||
isFixedLanguage,
|
||||
getCurrentLanguage,
|
||||
isLoading,
|
||||
refreshConfig: loadCourseLanguageConfig,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook para cambiar el idioma del usuario (solo funciona si el curso está en modo 'auto')
|
||||
*
|
||||
* @param courseId - ID del curso actual
|
||||
*/
|
||||
export function useCourseLanguageSwitcher(courseId: string | null) {
|
||||
const { setLanguage, language } = useTranslation();
|
||||
const { isFixedLanguage } = useCourseLanguage(courseId);
|
||||
|
||||
const canChangeLanguage = !isFixedLanguage();
|
||||
|
||||
const changeLanguage = (newLanguage: string) => {
|
||||
if (canChangeLanguage) {
|
||||
setLanguage(newLanguage);
|
||||
} else {
|
||||
console.warn('Cannot change language: course has fixed language setting');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentLanguage: language,
|
||||
canChangeLanguage,
|
||||
changeLanguage,
|
||||
isFixed: isFixedLanguage(),
|
||||
};
|
||||
}
|
||||
@@ -5,14 +5,31 @@
|
||||
"next": "Next",
|
||||
"finish": "Finish",
|
||||
"error": "Error",
|
||||
"retry": "Retry"
|
||||
"retry": "Retry",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"search": "Search",
|
||||
"noResults": "No results",
|
||||
"viewAll": "View all",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"nav": {
|
||||
"catalog": "Catalog",
|
||||
"myLearning": "My Learning",
|
||||
"bookmarks": "Bookmarks",
|
||||
"profile": "Profile",
|
||||
"signOut": "Sign Out"
|
||||
"signOut": "Sign Out",
|
||||
"selectLanguage": "Select Language",
|
||||
"menu": "Menu",
|
||||
"notifications": "Notifications",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"course": {
|
||||
"modules": "Modules",
|
||||
@@ -20,12 +37,194 @@
|
||||
"progress": "Progress",
|
||||
"calendar": "Timeline",
|
||||
"start": "Start",
|
||||
"continue": "Continue"
|
||||
"continue": "Continue",
|
||||
"enroll": "Enroll",
|
||||
"enrolled": "Enrolled",
|
||||
"price": "Price",
|
||||
"free": "Free",
|
||||
"duration": "Duration",
|
||||
"level": "Level",
|
||||
"description": "Description",
|
||||
"objectives": "Objectives",
|
||||
"requirements": "Requirements",
|
||||
"instructor": "Instructor",
|
||||
"certificate": "Certificate",
|
||||
"aboutCourse": "About Course",
|
||||
"content": "Content",
|
||||
"reviews": "Reviews",
|
||||
"faq": "FAQ"
|
||||
},
|
||||
"lesson": {
|
||||
"summary": "AI Summary",
|
||||
"transcription": "Transcription",
|
||||
"complete": "Mark as Completed",
|
||||
"nextLesson": "Next Lesson"
|
||||
"nextLesson": "Next Lesson",
|
||||
"previousLesson": "Previous Lesson",
|
||||
"locked": "Locked",
|
||||
"lockedMessage": "Complete previous lessons to unlock",
|
||||
"markComplete": "Mark as Completed",
|
||||
"inProgress": "In Progress",
|
||||
"completed": "Completed",
|
||||
"notStarted": "Not Started",
|
||||
"dueDate": "Due Date",
|
||||
"timeRemaining": "Time Remaining",
|
||||
"overdue": "Overdue"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"fullName": "Full Name",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"resetPassword": "Reset Password",
|
||||
"noAccount": "Don't have an account?",
|
||||
"hasAccount": "Already have an account?",
|
||||
"signIn": "Sign In",
|
||||
"signUp": "Sign Up",
|
||||
"orContinueWith": "Or continue with",
|
||||
"google": "Google",
|
||||
"microsoft": "Microsoft",
|
||||
"sso": "Single Sign-On"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Welcome",
|
||||
"continueLearning": "Continue Learning",
|
||||
"recommendedCourses": "Recommended Courses",
|
||||
"recentActivity": "Recent Activity",
|
||||
"myProgress": "My Progress",
|
||||
"certificates": "Certificates",
|
||||
"badges": "Badges",
|
||||
"stats": {
|
||||
"coursesCompleted": "Courses Completed",
|
||||
"hoursLearned": "Hours Learned",
|
||||
"averageGrade": "Average Grade",
|
||||
"currentStreak": "Current Streak"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"myProfile": "My Profile",
|
||||
"editProfile": "Edit Profile",
|
||||
"avatar": "Avatar",
|
||||
"bio": "Bio",
|
||||
"language": "Language",
|
||||
"timezone": "Timezone",
|
||||
"notifications": "Notifications",
|
||||
"privacy": "Privacy",
|
||||
"account": "Account",
|
||||
"changePassword": "Change Password",
|
||||
"deleteAccount": "Delete Account",
|
||||
"portfolio": "Portfolio",
|
||||
"achievements": "Achievements",
|
||||
"publicProfile": "Public Profile"
|
||||
},
|
||||
"gamification": {
|
||||
"level": "Level",
|
||||
"xp": "Experience Points",
|
||||
"leaderboard": "Leaderboard",
|
||||
"rank": "Rank",
|
||||
"points": "Points",
|
||||
"badge": "Badge",
|
||||
"badges": "Badges",
|
||||
"earned": "Earned",
|
||||
"locked": "Locked",
|
||||
"nextLevel": "Next Level",
|
||||
"xpToNextLevel": "XP to next level"
|
||||
},
|
||||
"grading": {
|
||||
"grade": "Grade",
|
||||
"grades": "Grades",
|
||||
"score": "Score",
|
||||
"percentage": "Percentage",
|
||||
"passingGrade": "Passing Grade",
|
||||
"failed": "Failed",
|
||||
"passed": "Passed",
|
||||
"excellent": "Excellent",
|
||||
"good": "Good",
|
||||
"regular": "Regular",
|
||||
"poor": "Poor",
|
||||
"feedback": "Feedback",
|
||||
"submissionDate": "Submission Date",
|
||||
"gradedDate": "Graded Date"
|
||||
},
|
||||
"quiz": {
|
||||
"startQuiz": "Start Quiz",
|
||||
"submitAnswers": "Submit Answers",
|
||||
"questionNumber": "Question",
|
||||
"of": "of",
|
||||
"correctAnswer": "Correct Answer",
|
||||
"yourAnswer": "Your Answer",
|
||||
"explanation": "Explanation",
|
||||
"tryAgain": "Try Again",
|
||||
"attempts": "Attempts",
|
||||
"attemptsRemaining": "attempts remaining",
|
||||
"timeLimit": "Time Limit",
|
||||
"timeExpired": "Time Expired"
|
||||
},
|
||||
"forum": {
|
||||
"discussions": "Discussions",
|
||||
"newThread": "New Discussion",
|
||||
"title": "Title",
|
||||
"content": "Content",
|
||||
"post": "Post",
|
||||
"reply": "Reply",
|
||||
"replies": "Replies",
|
||||
"views": "Views",
|
||||
"lastActivity": "Last Activity",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"markAsSolved": "Mark as Solved",
|
||||
"upvote": "Upvote",
|
||||
"downvote": "Downvote"
|
||||
},
|
||||
"payments": {
|
||||
"purchase": "Purchase",
|
||||
"buyNow": "Buy Now",
|
||||
"paymentMethod": "Payment Method",
|
||||
"cardNumber": "Card Number",
|
||||
"expiryDate": "Expiry Date",
|
||||
"cvv": "CVV",
|
||||
"cardholderName": "Cardholder Name",
|
||||
"paymentSuccess": "Payment Success",
|
||||
"paymentFailed": "Payment Failed",
|
||||
"refund": "Refund",
|
||||
"invoice": "Invoice",
|
||||
"receipt": "Receipt"
|
||||
},
|
||||
"accessibility": {
|
||||
"skipToContent": "Skip to content",
|
||||
"increaseContrast": "Increase Contrast",
|
||||
"decreaseContrast": "Decrease Contrast",
|
||||
"largerText": "Larger Text",
|
||||
"smallerText": "Smaller Text",
|
||||
"screenReader": "Screen Reader"
|
||||
},
|
||||
"language": {
|
||||
"selectLanguage": "Select Language",
|
||||
"auto": "Automatic",
|
||||
"spanish": "Spanish",
|
||||
"english": "English",
|
||||
"portuguese": "Portuguese",
|
||||
"courseLanguage": "Course Language",
|
||||
"interfaceLanguage": "Interface Language"
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Not found",
|
||||
"unauthorized": "Unauthorized",
|
||||
"serverError": "Server error",
|
||||
"networkError": "Network error",
|
||||
"sessionExpired": "Session expired",
|
||||
"invalidCredentials": "Invalid credentials",
|
||||
"emailInUse": "Email already in use",
|
||||
"weakPassword": "Weak password"
|
||||
},
|
||||
"dates": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"tomorrow": "Tomorrow",
|
||||
"daysAgo": "{{days}} days ago",
|
||||
"weeksAgo": "{{weeks}} weeks ago",
|
||||
"monthsAgo": "{{months}} months ago",
|
||||
"yearsAgo": "{{years}} years ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,31 @@
|
||||
"next": "Siguiente",
|
||||
"finish": "Finalizar",
|
||||
"error": "Error",
|
||||
"retry": "Reintentar"
|
||||
"retry": "Reintentar",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"close": "Cerrar",
|
||||
"confirm": "Confirmar",
|
||||
"success": "Éxito",
|
||||
"warning": "Advertencia",
|
||||
"info": "Información",
|
||||
"search": "Buscar",
|
||||
"noResults": "Sin resultados",
|
||||
"viewAll": "Ver todo",
|
||||
"learnMore": "Más información"
|
||||
},
|
||||
"nav": {
|
||||
"catalog": "Catálogo",
|
||||
"myLearning": "Mi Aprendizaje",
|
||||
"bookmarks": "Marcadores",
|
||||
"profile": "Perfil",
|
||||
"signOut": "Cerrar Sesión"
|
||||
"signOut": "Cerrar Sesión",
|
||||
"selectLanguage": "Seleccionar Idioma",
|
||||
"menu": "Menú",
|
||||
"notifications": "Notificaciones",
|
||||
"settings": "Configuración"
|
||||
},
|
||||
"course": {
|
||||
"modules": "Módulos",
|
||||
@@ -20,12 +37,194 @@
|
||||
"progress": "Progreso",
|
||||
"calendar": "Cronología",
|
||||
"start": "Comenzar",
|
||||
"continue": "Continuar"
|
||||
"continue": "Continuar",
|
||||
"enroll": "Inscribirse",
|
||||
"enrolled": "Inscrito",
|
||||
"price": "Precio",
|
||||
"free": "Gratis",
|
||||
"duration": "Duración",
|
||||
"level": "Nivel",
|
||||
"description": "Descripción",
|
||||
"objectives": "Objetivos",
|
||||
"requirements": "Requisitos",
|
||||
"instructor": "Instructor",
|
||||
"certificate": "Certificado",
|
||||
"aboutCourse": "Acerca del Curso",
|
||||
"content": "Contenido",
|
||||
"reviews": "Reseñas",
|
||||
"faq": "Preguntas Frecuentes"
|
||||
},
|
||||
"lesson": {
|
||||
"summary": "Resumen AI",
|
||||
"transcription": "Transcripción",
|
||||
"complete": "Marcar como Completado",
|
||||
"nextLesson": "Siguiente Lección"
|
||||
"nextLesson": "Siguiente Lección",
|
||||
"previousLesson": "Lección Anterior",
|
||||
"locked": "Bloqueado",
|
||||
"lockedMessage": "Completa las lecciones anteriores para desbloquear",
|
||||
"markComplete": "Marcar como Completada",
|
||||
"inProgress": "En Progreso",
|
||||
"completed": "Completada",
|
||||
"notStarted": "No Iniciada",
|
||||
"dueDate": "Fecha Límite",
|
||||
"timeRemaining": "Tiempo Restante",
|
||||
"overdue": "Vencido"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Iniciar Sesión",
|
||||
"register": "Registrarse",
|
||||
"email": "Correo Electrónico",
|
||||
"password": "Contraseña",
|
||||
"fullName": "Nombre Completo",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"resetPassword": "Restablecer Contraseña",
|
||||
"noAccount": "¿No tienes cuenta?",
|
||||
"hasAccount": "¿Ya tienes cuenta?",
|
||||
"signIn": "Ingresar",
|
||||
"signUp": "Crear Cuenta",
|
||||
"orContinueWith": "O continúa con",
|
||||
"google": "Google",
|
||||
"microsoft": "Microsoft",
|
||||
"sso": "Inicio de Sesión Único"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenido",
|
||||
"continueLearning": "Continuar Aprendiendo",
|
||||
"recommendedCourses": "Cursos Recomendados",
|
||||
"recentActivity": "Actividad Reciente",
|
||||
"myProgress": "Mi Progreso",
|
||||
"certificates": "Certificados",
|
||||
"badges": "Insignias",
|
||||
"stats": {
|
||||
"coursesCompleted": "Cursos Completados",
|
||||
"hoursLearned": "Horas Aprendidas",
|
||||
"averageGrade": "Nota Promedio",
|
||||
"currentStreak": "Racha Actual"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"myProfile": "Mi Perfil",
|
||||
"editProfile": "Editar Perfil",
|
||||
"avatar": "Avatar",
|
||||
"bio": "Biografía",
|
||||
"language": "Idioma",
|
||||
"timezone": "Zona Horaria",
|
||||
"notifications": "Notificaciones",
|
||||
"privacy": "Privacidad",
|
||||
"account": "Cuenta",
|
||||
"changePassword": "Cambiar Contraseña",
|
||||
"deleteAccount": "Eliminar Cuenta",
|
||||
"portfolio": "Portafolio",
|
||||
"achievements": "Logros",
|
||||
"publicProfile": "Perfil Público"
|
||||
},
|
||||
"gamification": {
|
||||
"level": "Nivel",
|
||||
"xp": "Puntos de Experiencia",
|
||||
"leaderboard": "Tabla de Clasificación",
|
||||
"rank": "Rango",
|
||||
"points": "Puntos",
|
||||
"badge": "Insignia",
|
||||
"badges": "Insignias",
|
||||
"earned": "Obtenida",
|
||||
"locked": "Bloqueada",
|
||||
"nextLevel": "Siguiente Nivel",
|
||||
"xpToNextLevel": "XP para el siguiente nivel"
|
||||
},
|
||||
"grading": {
|
||||
"grade": "Nota",
|
||||
"grades": "Notas",
|
||||
"score": "Puntaje",
|
||||
"percentage": "Porcentaje",
|
||||
"passingGrade": "Nota de Aprobación",
|
||||
"failed": "Reprobado",
|
||||
"passed": "Aprobado",
|
||||
"excellent": "Excelente",
|
||||
"good": "Bueno",
|
||||
"regular": "Regular",
|
||||
"poor": "Deficiente",
|
||||
"feedback": "Retroalimentación",
|
||||
"submissionDate": "Fecha de Entrega",
|
||||
"gradedDate": "Fecha de Calificación"
|
||||
},
|
||||
"quiz": {
|
||||
"startQuiz": "Comenzar Cuestionario",
|
||||
"submitAnswers": "Enviar Respuestas",
|
||||
"questionNumber": "Pregunta",
|
||||
"of": "de",
|
||||
"correctAnswer": "Respuesta Correcta",
|
||||
"yourAnswer": "Tu Respuesta",
|
||||
"explanation": "Explicación",
|
||||
"tryAgain": "Intentar de Nuevo",
|
||||
"attempts": "Intentos",
|
||||
"attemptsRemaining": "intentos restantes",
|
||||
"timeLimit": "Límite de Tiempo",
|
||||
"timeExpired": "Tiempo Expirado"
|
||||
},
|
||||
"forum": {
|
||||
"discussions": "Discusiones",
|
||||
"newThread": "Nueva Discusión",
|
||||
"title": "Título",
|
||||
"content": "Contenido",
|
||||
"post": "Publicar",
|
||||
"reply": "Responder",
|
||||
"replies": "Respuestas",
|
||||
"views": "Vistas",
|
||||
"lastActivity": "Última Actividad",
|
||||
"subscribe": "Suscribirse",
|
||||
"unsubscribe": "Desuscribirse",
|
||||
"markAsSolved": "Marcar como Resuelto",
|
||||
"upvote": "Votar a Favor",
|
||||
"downvote": "Votar en Contra"
|
||||
},
|
||||
"payments": {
|
||||
"purchase": "Comprar",
|
||||
"buyNow": "Comprar Ahora",
|
||||
"paymentMethod": "Método de Pago",
|
||||
"cardNumber": "Número de Tarjeta",
|
||||
"expiryDate": "Fecha de Vencimiento",
|
||||
"cvv": "CVV",
|
||||
"cardholderName": "Nombre del Titular",
|
||||
"paymentSuccess": "Pago Exitoso",
|
||||
"paymentFailed": "Pago Fallido",
|
||||
"refund": "Reembolso",
|
||||
"invoice": "Factura",
|
||||
"receipt": "Recibo"
|
||||
},
|
||||
"accessibility": {
|
||||
"skipToContent": "Saltar al contenido",
|
||||
"increaseContrast": "Aumentar Contraste",
|
||||
"decreaseContrast": "Disminuir Contraste",
|
||||
"largerText": "Texto Más Grande",
|
||||
"smallerText": "Texto Más Pequeño",
|
||||
"screenReader": "Lector de Pantalla"
|
||||
},
|
||||
"language": {
|
||||
"selectLanguage": "Seleccionar Idioma",
|
||||
"auto": "Automático",
|
||||
"spanish": "Español",
|
||||
"english": "Inglés",
|
||||
"portuguese": "Portugués",
|
||||
"courseLanguage": "Idioma del Curso",
|
||||
"interfaceLanguage": "Idioma de la Interfaz"
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "No encontrado",
|
||||
"unauthorized": "No autorizado",
|
||||
"serverError": "Error del servidor",
|
||||
"networkError": "Error de red",
|
||||
"sessionExpired": "Sesión expirada",
|
||||
"invalidCredentials": "Credenciales inválidas",
|
||||
"emailInUse": "Correo ya en uso",
|
||||
"weakPassword": "Contraseña débil"
|
||||
},
|
||||
"dates": {
|
||||
"today": "Hoy",
|
||||
"yesterday": "Ayer",
|
||||
"tomorrow": "Mañana",
|
||||
"daysAgo": "hace {{days}} días",
|
||||
"weeksAgo": "hace {{weeks}} semanas",
|
||||
"monthsAgo": "hace {{months}} meses",
|
||||
"yearsAgo": "hace {{years}} años"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,228 @@
|
||||
"loading": "Carregando...",
|
||||
"back": "Voltar",
|
||||
"next": "Próximo",
|
||||
"finish": "Finalizar",
|
||||
"finish": "Concluir",
|
||||
"error": "Erro",
|
||||
"retry": "Repetir"
|
||||
"retry": "Tentar Novamente",
|
||||
"save": "Salvar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"close": "Fechar",
|
||||
"confirm": "Confirmar",
|
||||
"success": "Sucesso",
|
||||
"warning": "Aviso",
|
||||
"info": "Informação",
|
||||
"search": "Buscar",
|
||||
"noResults": "Sem resultados",
|
||||
"viewAll": "Ver tudo",
|
||||
"learnMore": "Saiba mais"
|
||||
},
|
||||
"nav": {
|
||||
"catalog": "Catálogo",
|
||||
"myLearning": "Meu Aprendizado",
|
||||
"bookmarks": "Favoritos",
|
||||
"profile": "Perfil",
|
||||
"signOut": "Sair"
|
||||
"signOut": "Sair",
|
||||
"selectLanguage": "Selecionar Idioma",
|
||||
"menu": "Menu",
|
||||
"notifications": "Notificações",
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"course": {
|
||||
"modules": "Módulos",
|
||||
"lessons": "Lições",
|
||||
"lessons": "Aulas",
|
||||
"progress": "Progresso",
|
||||
"calendar": "Cronologia",
|
||||
"calendar": "Cronograma",
|
||||
"start": "Começar",
|
||||
"continue": "Continuar"
|
||||
"continue": "Continuar",
|
||||
"enroll": "Inscrever-se",
|
||||
"enrolled": "Inscrito",
|
||||
"price": "Preço",
|
||||
"free": "Gratuito",
|
||||
"duration": "Duração",
|
||||
"level": "Nível",
|
||||
"description": "Descrição",
|
||||
"objectives": "Objetivos",
|
||||
"requirements": "Requisitos",
|
||||
"instructor": "Instrutor",
|
||||
"certificate": "Certificado",
|
||||
"aboutCourse": "Sobre o Curso",
|
||||
"content": "Conteúdo",
|
||||
"reviews": "Avaliações",
|
||||
"faq": "Perguntas Frequentes"
|
||||
},
|
||||
"lesson": {
|
||||
"summary": "Resumo IA",
|
||||
"transcription": "Transcrição",
|
||||
"complete": "Marcar como Concluído",
|
||||
"nextLesson": "Próxima Lição"
|
||||
"complete": "Marcar como Concluída",
|
||||
"nextLesson": "Próxima Aula",
|
||||
"previousLesson": "Aula Anterior",
|
||||
"locked": "Bloqueada",
|
||||
"lockedMessage": "Complete as aulas anteriores para desbloquear",
|
||||
"markComplete": "Marcar como Concluída",
|
||||
"inProgress": "Em Progresso",
|
||||
"completed": "Concluída",
|
||||
"notStarted": "Não Iniciada",
|
||||
"dueDate": "Data Limite",
|
||||
"timeRemaining": "Tempo Restante",
|
||||
"overdue": "Atrasada"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Entrar",
|
||||
"register": "Cadastrar",
|
||||
"email": "E-mail",
|
||||
"password": "Senha",
|
||||
"fullName": "Nome Completo",
|
||||
"forgotPassword": "Esqueceu a senha?",
|
||||
"resetPassword": "Redefinir Senha",
|
||||
"noAccount": "Não tem uma conta?",
|
||||
"hasAccount": "Já tem uma conta?",
|
||||
"signIn": "Entrar",
|
||||
"signUp": "Criar Conta",
|
||||
"orContinueWith": "Ou continue com",
|
||||
"google": "Google",
|
||||
"microsoft": "Microsoft",
|
||||
"sso": "Login Único"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bem-vindo",
|
||||
"continueLearning": "Continuar Aprendendo",
|
||||
"recommendedCourses": "Cursos Recomendados",
|
||||
"recentActivity": "Atividade Recente",
|
||||
"myProgress": "Meu Progresso",
|
||||
"certificates": "Certificados",
|
||||
"badges": "Distintivos",
|
||||
"stats": {
|
||||
"coursesCompleted": "Cursos Concluídos",
|
||||
"hoursLearned": "Horas Aprendidas",
|
||||
"averageGrade": "Nota Média",
|
||||
"currentStreak": "Sequência Atual"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"myProfile": "Meu Perfil",
|
||||
"editProfile": "Editar Perfil",
|
||||
"avatar": "Avatar",
|
||||
"bio": "Biografia",
|
||||
"language": "Idioma",
|
||||
"timezone": "Fuso Horário",
|
||||
"notifications": "Notificações",
|
||||
"privacy": "Privacidade",
|
||||
"account": "Conta",
|
||||
"changePassword": "Alterar Senha",
|
||||
"deleteAccount": "Excluir Conta",
|
||||
"portfolio": "Portfólio",
|
||||
"achievements": "Conquistas",
|
||||
"publicProfile": "Perfil Público"
|
||||
},
|
||||
"gamification": {
|
||||
"level": "Nível",
|
||||
"xp": "Pontos de Experiência",
|
||||
"leaderboard": "Classificação",
|
||||
"rank": "Posição",
|
||||
"points": "Pontos",
|
||||
"badge": "Distintivo",
|
||||
"badges": "Distintivos",
|
||||
"earned": "Conquistado",
|
||||
"locked": "Bloqueado",
|
||||
"nextLevel": "Próximo Nível",
|
||||
"xpToNextLevel": "XP para o próximo nível"
|
||||
},
|
||||
"grading": {
|
||||
"grade": "Nota",
|
||||
"grades": "Notas",
|
||||
"score": "Pontuação",
|
||||
"percentage": "Porcentagem",
|
||||
"passingGrade": "Nota Mínima",
|
||||
"failed": "Reprovado",
|
||||
"passed": "Aprovado",
|
||||
"excellent": "Excelente",
|
||||
"good": "Bom",
|
||||
"regular": "Regular",
|
||||
"poor": "Insuficiente",
|
||||
"feedback": "Feedback",
|
||||
"submissionDate": "Data de Entrega",
|
||||
"gradedDate": "Data de Avaliação"
|
||||
},
|
||||
"quiz": {
|
||||
"startQuiz": "Iniciar Questionário",
|
||||
"submitAnswers": "Enviar Respostas",
|
||||
"questionNumber": "Pergunta",
|
||||
"of": "de",
|
||||
"correctAnswer": "Resposta Correta",
|
||||
"yourAnswer": "Sua Resposta",
|
||||
"explanation": "Explicação",
|
||||
"tryAgain": "Tentar Novamente",
|
||||
"attempts": "Tentativas",
|
||||
"attemptsRemaining": "tentativas restantes",
|
||||
"timeLimit": "Limite de Tempo",
|
||||
"timeExpired": "Tempo Esgotado"
|
||||
},
|
||||
"forum": {
|
||||
"discussions": "Discussões",
|
||||
"newThread": "Nova Discussão",
|
||||
"title": "Título",
|
||||
"content": "Conteúdo",
|
||||
"post": "Publicar",
|
||||
"reply": "Responder",
|
||||
"replies": "Respostas",
|
||||
"views": "Visualizações",
|
||||
"lastActivity": "Última Atividade",
|
||||
"subscribe": "Inscrever-se",
|
||||
"unsubscribe": "Cancelar Inscrição",
|
||||
"markAsSolved": "Marcar como Resolvido",
|
||||
"upvote": "Votar a Favor",
|
||||
"downvote": "Votar Contra"
|
||||
},
|
||||
"payments": {
|
||||
"purchase": "Comprar",
|
||||
"buyNow": "Comprar Agora",
|
||||
"paymentMethod": "Método de Pagamento",
|
||||
"cardNumber": "Número do Cartão",
|
||||
"expiryDate": "Data de Validade",
|
||||
"cvv": "CVV",
|
||||
"cardholderName": "Nome do Titular",
|
||||
"paymentSuccess": "Pagamento Bem-Sucedido",
|
||||
"paymentFailed": "Pagamento Falhou",
|
||||
"refund": "Reembolso",
|
||||
"invoice": "Fatura",
|
||||
"receipt": "Recibo"
|
||||
},
|
||||
"accessibility": {
|
||||
"skipToContent": "Pular para o conteúdo",
|
||||
"increaseContrast": "Aumentar Contraste",
|
||||
"decreaseContrast": "Diminuir Contraste",
|
||||
"largerText": "Texto Maior",
|
||||
"smallerText": "Texto Menor",
|
||||
"screenReader": "Leitor de Tela"
|
||||
},
|
||||
"language": {
|
||||
"selectLanguage": "Selecionar Idioma",
|
||||
"auto": "Automático",
|
||||
"spanish": "Espanhol",
|
||||
"english": "Inglês",
|
||||
"portuguese": "Português",
|
||||
"courseLanguage": "Idioma do Curso",
|
||||
"interfaceLanguage": "Idioma da Interface"
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Não encontrado",
|
||||
"unauthorized": "Não autorizado",
|
||||
"serverError": "Erro do servidor",
|
||||
"networkError": "Erro de rede",
|
||||
"sessionExpired": "Sessão expirada",
|
||||
"invalidCredentials": "Credenciais inválidas",
|
||||
"emailInUse": "E-mail já em uso",
|
||||
"weakPassword": "Senha fraca"
|
||||
},
|
||||
"dates": {
|
||||
"today": "Hoje",
|
||||
"yesterday": "Ontem",
|
||||
"tomorrow": "Amanhã",
|
||||
"daysAgo": "há {{days}} dias",
|
||||
"weeksAgo": "há {{weeks}} semanas",
|
||||
"monthsAgo": "há {{months}} meses",
|
||||
"yearsAgo": "há {{years}} anos"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { cmsApi, Lesson, Block, GradingCategory, LibraryBlock, Rubric, RubricLevel, RubricCriterion, LessonDependency, getImageUrl } from '@/lib/api';
|
||||
import { cmsApi, Lesson, Block, GradingCategory, LibraryBlock, Rubric, RubricLevel, RubricCriterion, LessonDependency, getImageUrl, generateUUID } from '@/lib/api';
|
||||
import {
|
||||
Layout,
|
||||
CheckCircle2,
|
||||
@@ -103,7 +103,9 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
setImportantDateType(lessonData.important_date_type || "");
|
||||
setIsPreviewable(lessonData.is_previewable || false);
|
||||
|
||||
if (lessonData.metadata?.blocks) {
|
||||
if (lessonData.content_blocks && Array.isArray(lessonData.content_blocks)) {
|
||||
setBlocks(lessonData.content_blocks);
|
||||
} else if (lessonData.metadata?.blocks) {
|
||||
setBlocks(lessonData.metadata.blocks);
|
||||
} else {
|
||||
setBlocks([
|
||||
@@ -195,7 +197,8 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
}
|
||||
|
||||
const updated = await cmsApi.updateLesson(lesson.id, {
|
||||
metadata: { ...lesson.metadata, blocks },
|
||||
metadata: { ...lesson.metadata },
|
||||
content_blocks: blocks,
|
||||
content_url,
|
||||
content_type, // Sync type to ensure backend triggers transcription
|
||||
summary,
|
||||
@@ -218,7 +221,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
||||
|
||||
const addBlock = (type: Block['type']) => {
|
||||
const newBlock: Block = {
|
||||
id: crypto.randomUUID(),
|
||||
id: generateUUID(),
|
||||
type,
|
||||
...(type === 'description' && { content: "" }),
|
||||
...(type === 'media' && { url: "", media_type: 'video' as const, config: { maxPlays: 0 } }),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { Search, MapPin, Plus, Trash2, Image as ImageIcon, Crosshair, Wand2, Loader2 } from "lucide-react";
|
||||
import AssetPickerModal from "../AssetPickerModal";
|
||||
import { Asset, getImageUrl, cmsApi } from "@/lib/api";
|
||||
import { Asset, getImageUrl, cmsApi, generateUUID } from "@/lib/api";
|
||||
|
||||
interface Hotspot {
|
||||
id: string;
|
||||
@@ -46,12 +46,19 @@ export default function HotspotBlock({
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const data = await cmsApi.generateHotspots(lessonId, { image_url: imageUrl });
|
||||
const newHotspots = data.map(h => ({
|
||||
id: crypto.randomUUID(),
|
||||
x: h.x,
|
||||
y: h.y,
|
||||
// Handle different response formats from AI
|
||||
let hotspotsArray = Array.isArray(data) ? data : (data.hotspots || data.items || []);
|
||||
|
||||
if (!Array.isArray(hotspotsArray)) {
|
||||
throw new Error("La respuesta de la IA no es un array válido");
|
||||
}
|
||||
|
||||
const newHotspots = hotspotsArray.map((h: any) => ({
|
||||
id: generateUUID(),
|
||||
x: typeof h.x === 'number' ? h.x : 50,
|
||||
y: typeof h.y === 'number' ? h.y : 50,
|
||||
radius: 5,
|
||||
label: h.label
|
||||
label: h.label || 'Punto de interés'
|
||||
}));
|
||||
onChange({ hotspots: [...hotspots, ...newHotspots] });
|
||||
} catch (error) {
|
||||
@@ -76,7 +83,7 @@ export default function HotspotBlock({
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
const newHotspot: Hotspot = {
|
||||
id: crypto.randomUUID(),
|
||||
id: generateUUID(),
|
||||
x,
|
||||
y,
|
||||
radius: 5,
|
||||
|
||||
@@ -16,27 +16,38 @@ interface MemoryBlockProps {
|
||||
onChange: (updates: { title?: string; pairs?: MatchingPair[] }) => void;
|
||||
}
|
||||
|
||||
// Generate unique ID for pairs
|
||||
const generatePairId = () => {
|
||||
return `pair_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
export default function MemoryBlock({ id, title, pairs = [], editMode, onChange }: MemoryBlockProps) {
|
||||
const addPair = () => {
|
||||
const newPair: MatchingPair = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
id: generatePairId(),
|
||||
left: "",
|
||||
right: ""
|
||||
};
|
||||
console.log('[MemoryBlock] Adding new pair:', newPair);
|
||||
onChange({ pairs: [...pairs, newPair] });
|
||||
};
|
||||
|
||||
const updatePair = (index: number, updates: Partial<MatchingPair>) => {
|
||||
const newPairs = [...pairs];
|
||||
newPairs[index] = { ...newPairs[index], ...updates };
|
||||
console.log('[MemoryBlock] Updating pair at index', index, ':', updates);
|
||||
onChange({ pairs: newPairs });
|
||||
};
|
||||
|
||||
const removePair = (index: number) => {
|
||||
console.log('[MemoryBlock] Removing pair at index', index);
|
||||
const newPairs = pairs.filter((_, i) => i !== index);
|
||||
onChange({ pairs: newPairs });
|
||||
};
|
||||
|
||||
// Debug: Log pairs on render
|
||||
console.log('[MemoryBlock] Render with pairs:', pairs);
|
||||
|
||||
if (!editMode) {
|
||||
return (
|
||||
<div className="space-y-8" id={id}>
|
||||
|
||||
@@ -8,29 +8,64 @@ import pt from '../lib/locales/pt.json';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const translations: Record<string, any> = { en, es, pt };
|
||||
|
||||
// Idiomas soportados
|
||||
export const SUPPORTED_LANGUAGES = ['es', 'en', 'pt'] as const;
|
||||
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
|
||||
|
||||
interface I18nContextType {
|
||||
language: string;
|
||||
setLanguage: (lang: string) => void;
|
||||
t: (path: string) => string;
|
||||
detectBrowserLanguage: () => string;
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Detecta el idioma del navegador del usuario
|
||||
*/
|
||||
function detectBrowserLanguage(): string {
|
||||
if (typeof navigator === 'undefined') return 'es';
|
||||
|
||||
const browserLanguages = navigator.languages || [navigator.language || (navigator as any).userLanguage];
|
||||
|
||||
for (const browserLang of browserLanguages) {
|
||||
if (SUPPORTED_LANGUAGES.includes(browserLang as SupportedLanguage)) {
|
||||
return browserLang;
|
||||
}
|
||||
|
||||
const langCode = browserLang.split('-')[0].toLowerCase();
|
||||
if (SUPPORTED_LANGUAGES.includes(langCode as SupportedLanguage)) {
|
||||
return langCode;
|
||||
}
|
||||
}
|
||||
|
||||
return 'es';
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const [language, setLanguageState] = useState('es');
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const savedLang = localStorage.getItem('studio_language');
|
||||
if (savedLang) {
|
||||
|
||||
if (savedLang && SUPPORTED_LANGUAGES.includes(savedLang as SupportedLanguage)) {
|
||||
setLanguageState(savedLang);
|
||||
} else {
|
||||
// Try to detect from user profile if available, but for now default or localstorage
|
||||
const detectedLang = detectBrowserLanguage();
|
||||
setLanguageState(detectedLang);
|
||||
localStorage.setItem('studio_language', detectedLang);
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
}, []);
|
||||
|
||||
const setLanguage = (lang: string) => {
|
||||
setLanguageState(lang);
|
||||
localStorage.setItem('studio_language', lang);
|
||||
if (SUPPORTED_LANGUAGES.includes(lang as SupportedLanguage)) {
|
||||
setLanguageState(lang);
|
||||
localStorage.setItem('studio_language', lang);
|
||||
}
|
||||
};
|
||||
|
||||
const t = (path: string): string => {
|
||||
@@ -41,7 +76,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
if (result[key]) {
|
||||
result = result[key];
|
||||
} else {
|
||||
return path; // Fallback to path if key missing
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +84,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={{ language, setLanguage, t }}>
|
||||
<I18nContext.Provider value={{ language, setLanguage, t, detectBrowserLanguage }}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
@@ -62,3 +97,5 @@ export function useTranslation() {
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { detectBrowserLanguage };
|
||||
|
||||
@@ -11,6 +11,19 @@ const getApiBaseUrl = (defaultPort: string, envVar?: string) => {
|
||||
export const API_BASE_URL = getApiBaseUrl("3001", process.env.NEXT_PUBLIC_CMS_API_URL);
|
||||
export const LMS_API_BASE_URL = getApiBaseUrl("3002", process.env.NEXT_PUBLIC_LMS_API_URL);
|
||||
|
||||
// Polyfill for crypto.randomUUID() for non-HTTPS contexts
|
||||
export function generateUUID(): string {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// Fallback for non-secure contexts
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export const getImageUrl = (path?: string) => {
|
||||
if (!path) return '';
|
||||
if (path.startsWith('http')) return path;
|
||||
|
||||
@@ -1,39 +1,253 @@
|
||||
{
|
||||
"common": {
|
||||
"loading": "Cargando...",
|
||||
"back": "Volver",
|
||||
"next": "Siguiente",
|
||||
"finish": "Finalizar",
|
||||
"error": "Error",
|
||||
"retry": "Reintentar",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"close": "Cerrar",
|
||||
"confirm": "Confirmar",
|
||||
"success": "Éxito",
|
||||
"warning": "Advertencia",
|
||||
"create": "Crear",
|
||||
"loading": "Cargando...",
|
||||
"add": "Agregar",
|
||||
"remove": "Eliminar",
|
||||
"update": "Actualizar",
|
||||
"search": "Buscar",
|
||||
"back": "Volver",
|
||||
"next": "Siguiente",
|
||||
"filter": "Filtrar",
|
||||
"export": "Exportar",
|
||||
"import": "Importar",
|
||||
"download": "Descargar",
|
||||
"upload": "Subir",
|
||||
"preview": "Vista Previa",
|
||||
"publish": "Publicar",
|
||||
"preview": "Previsualizar"
|
||||
"draft": "Borrador",
|
||||
"archive": "Archivar",
|
||||
"duplicate": "Duplicar",
|
||||
"settings": "Configuración"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"courses": "Cursos",
|
||||
"globalControl": "Control Global",
|
||||
"webhooks": "Webhooks",
|
||||
"library": "Librería",
|
||||
"questionBank": "Banco de Preguntas",
|
||||
"testTemplates": "Plantillas de Pruebas",
|
||||
"analytics": "Analíticas",
|
||||
"students": "Estudiantes",
|
||||
"instructors": "Instructores",
|
||||
"settings": "Configuración",
|
||||
"profile": "Perfil",
|
||||
"signOut": "Cerrar Sesión"
|
||||
"signOut": "Cerrar Sesión",
|
||||
"selectLanguage": "Seleccionar Idioma",
|
||||
"controlPanel": "Panel de Control",
|
||||
"webhooks": "Webhooks",
|
||||
"globalControl": "Control Global"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel de Control",
|
||||
"newCourse": "Nuevo Curso",
|
||||
"aiBuilder": "Constructor AI",
|
||||
"noCourses": "No hay cursos disponibles."
|
||||
"course": {
|
||||
"createCourse": "Crear Curso",
|
||||
"editCourse": "Editar Curso",
|
||||
"deleteCourse": "Eliminar Curso",
|
||||
"courseTitle": "Título del Curso",
|
||||
"courseDescription": "Descripción del Curso",
|
||||
"courseLanguage": "Idioma del Curso",
|
||||
"languageAuto": "Automático (detectar del usuario)",
|
||||
"languageFixed": "Fijo (siempre este idioma)",
|
||||
"languageEnglish": "Inglés",
|
||||
"languageSpanish": "Español",
|
||||
"languagePortuguese": "Portugués",
|
||||
"modules": "Módulos",
|
||||
"addModule": "Agregar Módulo",
|
||||
"editModule": "Editar Módulo",
|
||||
"deleteModule": "Eliminar Módulo",
|
||||
"moduleName": "Nombre del Módulo",
|
||||
"moduleDescription": "Descripción del Módulo",
|
||||
"lessons": "Lecciones",
|
||||
"addLesson": "Agregar Lección",
|
||||
"editLesson": "Editar Lección",
|
||||
"deleteLesson": "Eliminar Lección",
|
||||
"lessonTitle": "Título de la Lección",
|
||||
"lessonType": "Tipo de Lección",
|
||||
"blocks": "Bloques",
|
||||
"addBlock": "Agregar Bloque",
|
||||
"blockType": "Tipo de Bloque",
|
||||
"reorderBlocks": "Reordenar Bloques",
|
||||
"moveUp": "Mover Arriba",
|
||||
"moveDown": "Mover Abajo",
|
||||
"pricing": "Precios",
|
||||
"currency": "Moneda",
|
||||
"enrollment": "Inscripciones",
|
||||
"published": "Publicado",
|
||||
"unpublished": "No Publicado",
|
||||
"visibility": "Visibilidad",
|
||||
"public": "Público",
|
||||
"private": "Privado"
|
||||
},
|
||||
"editor": {
|
||||
"outline": "Estructura",
|
||||
"settings": "Configuración",
|
||||
"newModule": "Nuevo Módulo",
|
||||
"newLesson": "Nueva Lección",
|
||||
"addBlock": "Añadir Bloque de Contenido",
|
||||
"publishToOrg": "Publicar en Organización",
|
||||
"reading": "Lectura"
|
||||
"content": {
|
||||
"video": "Video",
|
||||
"audio": "Audio",
|
||||
"text": "Texto",
|
||||
"image": "Imagen",
|
||||
"document": "Documento",
|
||||
"quiz": "Cuestionario",
|
||||
"codeExercise": "Ejercicio de Código",
|
||||
"memoryGame": "Juego de Memoria",
|
||||
"dragToCube": "Arrastrar al Cubo",
|
||||
"hotspots": "Puntos Calientes",
|
||||
"videoMarkers": "Marcadores de Video",
|
||||
"audioResponse": "Respuesta de Audio",
|
||||
"fillBlanks": "Completar Espacios",
|
||||
"matching": "Emparejamiento",
|
||||
"ordering": "Ordenamiento",
|
||||
"shortAnswer": "Respuesta Corta",
|
||||
"multipleChoice": "Opción Múltiple",
|
||||
"trueFalse": "Verdadero/Falso",
|
||||
"essay": "Ensayo",
|
||||
"uploadMedia": "Subir Archivo",
|
||||
"dropFiles": "Soltar archivos aquí",
|
||||
"browseFiles": "Examinar archivos",
|
||||
"uploading": "Subiendo...",
|
||||
"uploadComplete": "Subida Completa",
|
||||
"uploadFailed": "Subida Fallida"
|
||||
},
|
||||
"ai": {
|
||||
"generateWithAI": "Generar con IA",
|
||||
"generateCourse": "Generar Curso con IA",
|
||||
"generateQuiz": "Generar Cuestionario con IA",
|
||||
"generateSummary": "Generar Resumen con IA",
|
||||
"generateTranscription": "Transcribir con IA",
|
||||
"generateQuestions": "Generar Preguntas con IA",
|
||||
"topicPrompt": "Ingresa el tema del curso",
|
||||
"generating": "Generando...",
|
||||
"generationComplete": "Generación Completa",
|
||||
"generationFailed": "Generación Fallida",
|
||||
"aiPowered": "Potenciado por IA",
|
||||
"model": "Modelo de IA",
|
||||
"tokens": "Tokens",
|
||||
"cost": "Costo"
|
||||
},
|
||||
"grading": {
|
||||
"gradingSystem": "Sistema de Calificación",
|
||||
"categories": "Categorías",
|
||||
"weights": "Pesos",
|
||||
"passingGrade": "Nota de Aprobación",
|
||||
"percentage": "Porcentaje",
|
||||
"dropLowest": "Eliminar Notas Más Bajas",
|
||||
"maxAttempts": "Intentos Máximos",
|
||||
"autoGrade": "Calificación Automática",
|
||||
"manualGrade": "Calificación Manual",
|
||||
"rubric": "Rúbrica",
|
||||
"feedback": "Retroalimentación",
|
||||
"grades": "Notas",
|
||||
"exportGrades": "Exportar Notas",
|
||||
"importGrades": "Importar Notas"
|
||||
},
|
||||
"students": {
|
||||
"enrollments": "Inscripciones",
|
||||
"enrollStudent": "Inscribir Estudiante",
|
||||
"bulkEnroll": "Inscripción Masiva",
|
||||
"studentProgress": "Progreso del Estudiante",
|
||||
"studentGrades": "Notas del Estudiante",
|
||||
"studentActivity": "Actividad del Estudiante",
|
||||
"cohort": "Cohorte",
|
||||
"groups": "Grupos",
|
||||
"retention": "Retención",
|
||||
"dropoutRisk": "Riesgo de Abandono",
|
||||
"engagement": "Compromiso"
|
||||
},
|
||||
"analytics": {
|
||||
"overview": "Resumen",
|
||||
"enrollments": "Inscripciones",
|
||||
"completions": "Completaciones",
|
||||
"averageGrade": "Nota Promedio",
|
||||
"engagement": "Compromiso",
|
||||
"retention": "Retención",
|
||||
"revenue": "Ingresos",
|
||||
"aiUsage": "Uso de IA",
|
||||
"exportReport": "Exportar Reporte",
|
||||
"dateRange": "Rango de Fechas",
|
||||
"compareCourses": "Comparar Cursos",
|
||||
"studentPerformance": "Rendimiento Estudiantil"
|
||||
},
|
||||
"settings": {
|
||||
"general": "Configuración General",
|
||||
"branding": "Marca",
|
||||
"logo": "Logotipo",
|
||||
"favicon": "Favicon",
|
||||
"colors": "Colores",
|
||||
"platformName": "Nombre de la Plataforma",
|
||||
"notifications": "Notificaciones",
|
||||
"emailTemplates": "Plantillas de Email",
|
||||
"integrations": "Integraciones",
|
||||
"lti": "LTI",
|
||||
"sso": "SSO",
|
||||
"webhooks": "Webhooks",
|
||||
"apiKeys": "Claves API",
|
||||
"security": "Seguridad",
|
||||
"backup": "Respaldo",
|
||||
"export": "Exportar Datos",
|
||||
"import": "Importar Datos"
|
||||
},
|
||||
"user": {
|
||||
"profile": "Perfil",
|
||||
"editProfile": "Editar Perfil",
|
||||
"changePassword": "Cambiar Contraseña",
|
||||
"preferences": "Preferencias",
|
||||
"language": "Idioma",
|
||||
"timezone": "Zona Horaria",
|
||||
"notifications": "Notificaciones",
|
||||
"avatar": "Avatar",
|
||||
"bio": "Biografía",
|
||||
"role": "Rol",
|
||||
"admin": "Administrador",
|
||||
"instructor": "Instructor",
|
||||
"student": "Estudiante"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Campo requerido",
|
||||
"invalidEmail": "Email inválido",
|
||||
"minLength": "Longitud mínima: {{length}}",
|
||||
"maxLength": "Longitud máxima: {{length}}",
|
||||
"invalidFormat": "Formato inválido",
|
||||
"alreadyExists": "Ya existe",
|
||||
"notFound": "No encontrado",
|
||||
"unauthorized": "No autorizado",
|
||||
"serverError": "Error del servidor",
|
||||
"networkError": "Error de red",
|
||||
"uploadFailed": "Subida fallida",
|
||||
"generationFailed": "Generación fallida"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Este campo es requerido",
|
||||
"email": "Debe ser un email válido",
|
||||
"url": "Debe ser una URL válida",
|
||||
"number": "Debe ser un número",
|
||||
"min": "Debe ser mayor o igual a {{min}}",
|
||||
"max": "Debe ser menor o igual a {{max}}",
|
||||
"pattern": "Formato inválido"
|
||||
},
|
||||
"dates": {
|
||||
"today": "Hoy",
|
||||
"yesterday": "Ayer",
|
||||
"tomorrow": "Mañana",
|
||||
"lastWeek": "Semana Pasada",
|
||||
"lastMonth": "Mes Pasado",
|
||||
"lastYear": "Año Pasado",
|
||||
"custom": "Personalizado",
|
||||
"startDate": "Fecha de Inicio",
|
||||
"endDate": "Fecha de Fin",
|
||||
"dueDate": "Fecha Límite",
|
||||
"publishedDate": "Fecha de Publicación",
|
||||
"createdDate": "Fecha de Creación",
|
||||
"updatedDate": "Fecha de Actualización"
|
||||
},
|
||||
"accessibility": {
|
||||
"skipToContent": "Saltar al contenido",
|
||||
"increaseContrast": "Aumentar Contraste",
|
||||
"screenReader": "Lector de Pantalla"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user