From 1c55cc4ae7b14eddeaf6ab16896a18dea0a23acf Mon Sep 17 00:00:00 2001 From: Nurfog Date: Mon, 19 Jan 2026 13:02:11 -0300 Subject: [PATCH] feat: Introduce My Learning page with user progress and gamification, standardize LLM models on Llama 3, and optimize branding logo image loading. --- docker-compose.yml | 14 +- install.sh | 8 +- roadmap.md | 8 +- services/cms-service/src/handlers.rs | 108 +------- web/experience/package-lock.json | 12 - web/experience/src/app/my-learning/page.tsx | 232 ++++++++++++++++++ web/experience/src/app/profile/page.tsx | 9 +- web/experience/src/components/AppHeader.tsx | 3 +- .../src/components/blocks/HotspotPlayer.tsx | 11 +- web/experience/src/lib/api.ts | 2 +- 10 files changed, 267 insertions(+), 140 deletions(-) create mode 100644 web/experience/src/app/my-learning/page.tsx diff --git a/docker-compose.yml b/docker-compose.yml index 6314cef..3cd31a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,13 +53,13 @@ services: - "11434:11434" volumes: - ollama_data:/root/.ollama - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 1 - capabilities: [ gpu ] + #deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [ gpu ] e2e: build: diff --git a/install.sh b/install.sh index f54a7be..b769854 100755 --- a/install.sh +++ b/install.sh @@ -109,8 +109,6 @@ update_env() { # Auto-configure AI variables based on hardware if [ "$HAS_NVIDIA" = true ]; then - update_env "WHISPER_IMAGE" "fedirz/faster-whisper-server:latest-cuda" - update_env "WHISPER_DEVICE" "cuda" update_env "LOCAL_LLM_MODEL" "llama3.2:1b" # Uncomment GPU deploy section in docker-compose.yml while preserving indentation sed -i 's/^ #deploy:/ deploy:/' docker-compose.yml @@ -121,8 +119,6 @@ if [ "$HAS_NVIDIA" = true ]; then sed -i 's/^ # count: 1/ count: 1/' docker-compose.yml sed -i 's/^ # capabilities: \[ gpu \]/ capabilities: [ gpu ]/' docker-compose.yml else - update_env "WHISPER_IMAGE" "fedirz/faster-whisper-server:latest-cpu" - update_env "WHISPER_DEVICE" "cpu" update_env "LOCAL_LLM_MODEL" "phi3:mini" # Comment GPU deploy section in docker-compose.yml sed -i 's/^ deploy:/ #deploy:/' docker-compose.yml @@ -157,9 +153,9 @@ until docker exec openccb-ollama-1 ollama list &> /dev/null; do sleep 2; done echo "馃摜 Downloading models..." if [ "$HAS_NVIDIA" = true ]; then - docker exec openccb-ollama-1 ollama pull llama3:8b + docker exec openccb-ollama-1 ollama pull llama3 else - docker exec openccb-ollama-1 ollama pull phi3:mini + docker exec openccb-ollama-1 ollama pull llama3:8b fi # 6. Database Initialization (Integrated db-mgmt.sh) diff --git a/roadmap.md b/roadmap.md index e21839f..083e05d 100644 --- a/roadmap.md +++ b/roadmap.md @@ -92,10 +92,10 @@ - [x] M茅tricas de retenci贸n (Implementado) - [x] Mapas de calor de participaci贸n (Heatmaps) (Implementado) - [x] **Integraci贸n de IA**: - - [x] Res煤menes de lecciones generados por IA (Implementado) - - [x] Transcripci贸n y traducci贸n de video en tiempo real (IA Local) (Implementado) - - [x] Generaci贸n autom谩tica de quices (Implementado) -- [x] **Rutas de Aprendizaje Personalizadas**: Recomendaciones impulsadas por IA + - [x] Res煤menes de lecciones generados por IA (Llama 3) + - [x] Generaci贸n autom谩tica de quices (Llama 3) + - [ ] Transcripci贸n y traducci贸n de video en tiempo real (Postpuesto - Reemplazado por Llama 3 para otras funciones) +- [x] **Rutas de Aprendizaje Personalizadas**: Recomendaciones impulsadas por Llama 3 (Implementado) - [x] **Gamificaci贸n Base**: (Implementada a nivel de sistema) - [x] Medallas y logros - [x] Tablas de clasificaci贸n (Leaderboards) diff --git a/services/cms-service/src/handlers.rs b/services/cms-service/src/handlers.rs index ea82fe2..5cfb25f 100644 --- a/services/cms-service/src/handlers.rs +++ b/services/cms-service/src/handlers.rs @@ -730,114 +730,18 @@ pub async fn run_transcription_task(pool: PgPool, lesson_id: Uuid) -> Result<(), .await .map_err(|e| format!("File read failed ({}): {}", file_path, e))?; - // 4. Configuration - let provider = env::var("AI_PROVIDER").unwrap_or_else(|_| "openai".to_string()); - let client = reqwest::Client::new(); - - let (url, auth_header, model) = if provider == "local" { - let base_url = - env::var("LOCAL_WHISPER_URL").unwrap_or_else(|_| "http://localhost:8000".to_string()); - ( - format!("{}/v1/audio/transcriptions", base_url), - "".to_string(), - "medium".to_string(), - ) - } else { - let api_key = env::var("OPENAI_API_KEY").map_err(|_| "Missing OPENAI_API_KEY")?; - ( - "https://api.openai.com/v1/audio/transcriptions".to_string(), - format!("Bearer {}", api_key), - "whisper-1".to_string(), - ) - }; - - let part = reqwest::multipart::Part::bytes(file_data) - .file_name(filename.to_string()) - .mime_str("application/octet-stream") - .map_err(|e| format!("Multipart part creation failed: {}", e))?; - - let form = reqwest::multipart::Form::new() - .part("file", part) - .text("model", model) - .text("response_format", "verbose_json"); - - let mut request = client.post(&url).multipart(form); - if !auth_header.is_empty() { - request = request.header("Authorization", auth_header); - } - - let response = request - .send() - .await - .map_err(|e| format!("Transcription request failed: {}", e))?; - - if !response.status().is_success() { - let err_body = response.text().await.unwrap_or_default(); - return Err(format!("Transcription API error: {}", err_body)); - } - - let whisper_data: serde_json::Value = response - .json() - .await - .map_err(|e| format!("Whisper JSON parse failed: {}", e))?; - - // Extract text and segments (cues) - let text = whisper_data["text"].as_str().unwrap_or_default(); - let segments = whisper_data["segments"].as_array(); - - let mut cues = Vec::new(); - if let Some(segments) = segments { - for s in segments { - cues.push(json!({ - "start": s["start"], - "end": s["end"], - "text": s["text"] - })); - } - } - - let transcription = json!({ - "en": text, - "es": "", - "cues": cues - }); - - // 5. Update initial transcription + // 4. Transcription service is currently disabled in favor of Llama 3 + tracing::warn!("Transcription service is disabled for lesson {}. Using Whisper removal policy.", lesson_id); + sqlx::query( - "UPDATE lessons SET transcription = $1, transcription_status = 'processing' WHERE id = $2", + "UPDATE lessons SET transcription_status = 'failed' WHERE id = $1", ) - .bind(&transcription) .bind(lesson_id) .execute(&pool) .await - .map_err(|e| format!("Initial database update failed: {}", e))?; + .map_err(|e| format!("Failed to update status to failed: {}", e))?; - // 6. Translation (Optional/Background within the task) - let es_text = match translate_text(text, "es").await { - Ok(t) => t, - Err(e) => { - tracing::error!("Translation failed for lesson {}: {}", lesson_id, e); - "".to_string() - } - }; - - let final_transcription = json!({ - "en": text, - "es": es_text, - "cues": cues - }); - - // 7. Final Update - sqlx::query( - "UPDATE lessons SET transcription = $1, transcription_status = 'completed' WHERE id = $2", - ) - .bind(final_transcription) - .bind(lesson_id) - .execute(&pool) - .await - .map_err(|e| format!("Final database update failed: {}", e))?; - - Ok(()) + return Err("Transcription service is currently disabled. All AI features now use Llama 3 directly.".to_string()); } pub async fn get_lesson_vtt( diff --git a/web/experience/package-lock.json b/web/experience/package-lock.json index bee89bd..30ad925 100644 --- a/web/experience/package-lock.json +++ b/web/experience/package-lock.json @@ -568,7 +568,6 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -631,7 +630,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -1105,7 +1103,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1516,7 +1513,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2215,7 +2211,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2378,7 +2373,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3803,7 +3797,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5145,7 +5138,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5346,7 +5338,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5358,7 +5349,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6301,7 +6291,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -6479,7 +6468,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/web/experience/src/app/my-learning/page.tsx b/web/experience/src/app/my-learning/page.tsx new file mode 100644 index 0000000..562d96c --- /dev/null +++ b/web/experience/src/app/my-learning/page.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { lmsApi, Course, Module } from "@/lib/api"; +import Link from "next/link"; +import { useAuth } from "@/context/AuthContext"; +import { BookOpen, TrendingUp, Clock, CheckCircle2, Award, Target } from "lucide-react"; + +interface CourseWithModules extends Course { + modules?: Module[]; +} + +interface EnrollmentWithProgress { + course: CourseWithModules; + progress: number; + lastAccessed?: string; +} + +export default function MyLearningPage() { + const [enrollments, setEnrollments] = useState([]); + const [loading, setLoading] = useState(true); + const [gamification, setGamification] = useState<{ points: number, level: number, badges: any[] } | null>(null); + const { user } = useAuth(); + + useEffect(() => { + const fetchData = async () => { + if (!user) { + setLoading(false); + return; + } + + try { + const enrollmentData = await lmsApi.getEnrollments(user.id); + const gamificationData = await lmsApi.getGamification(user.id); + setGamification(gamificationData); + + // Fetch course details for each enrollment + const enrichedEnrollments: EnrollmentWithProgress[] = []; + for (const enrollment of enrollmentData) { + try { + const outline = await lmsApi.getCourseOutline(enrollment.course_id); + + // TODO: Implement actual progress tracking + // For now, show 0% progress for all courses + const progress = 0; + + enrichedEnrollments.push({ + course: outline, + progress, + lastAccessed: enrollment.enroled_at + }); + } catch (err) { + console.error(`Error loading course ${enrollment.course_id}`, err); + } + } + + setEnrollments(enrichedEnrollments); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + fetchData(); + }, [user]); + + if (loading) { + return ( +
+
+ {[1, 2, 3].map(i => ( +
+ ))} +
+
+ ); + } + + if (!user) { + return ( +
+

Inicia Sesi贸n

+

Debes iniciar sesi贸n para ver tus cursos.

+ + Ir a Login + +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ + Mi Aprendizaje +
+

+ Mis Cursos +

+

+ Contin煤a tu viaje de aprendizaje donde lo dejaste. +

+
+ + {/* Gamification Stats */} + {gamification && ( +
+
+
+
+ {gamification.level} +
+
+

Nivel

+

{gamification.points} XP

+
+
+
+ {gamification.badges.length > 0 && ( +
+
+ +
+

Medallas

+

{gamification.badges.length}

+
+
+
+ )} +
+ )} +
+ + {/* Enrolled Courses */} + {enrollments.length === 0 ? ( +
+
+ +
+

No est谩s inscrito en ning煤n curso

+

+ Explora nuestro cat谩logo y comienza tu viaje de aprendizaje hoy. +

+ + + Explorar Cat谩logo + +
+ ) : ( +
+ {enrollments.map(({ course, progress }) => ( + +
+ {/* Progress Ring */} +
+
+ + + + +
+ {Math.round(progress)}% +
+
+ {progress === 100 && ( + + )} +
+ + {/* Course Info */} +
+

+ {course?.title || 'Curso sin t铆tulo'} +

+

+ {course?.description || "Contin煤a aprendiendo..."} +

+
+ + {/* Stats */} +
+
+ + {course.modules?.length || 0} m贸dulos +
+
+ + {progress < 100 ? 'En progreso' : 'Completado'} +
+
+ + {/* Continue Button */} + +
+ + ))} +
+ )} +
+ ); +} diff --git a/web/experience/src/app/profile/page.tsx b/web/experience/src/app/profile/page.tsx index 26487a5..d418c80 100644 --- a/web/experience/src/app/profile/page.tsx +++ b/web/experience/src/app/profile/page.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import { useState, useEffect, useRef, useCallback } from "react"; import { useAuth } from "@/context/AuthContext"; import { useTranslation } from "@/context/I18nContext"; @@ -124,10 +125,12 @@ export default function ProfilePage() {
{avatarUrl ? ( - {fullName} ) : ( @@ -196,7 +199,7 @@ export default function ProfilePage() {
{badge.icon_url ? ( - {badge.name} + {badge.name} ) : ( )} diff --git a/web/experience/src/components/AppHeader.tsx b/web/experience/src/components/AppHeader.tsx index ca0c257..12434dd 100644 --- a/web/experience/src/components/AppHeader.tsx +++ b/web/experience/src/components/AppHeader.tsx @@ -1,6 +1,7 @@ 'use client'; import Link from "next/link"; +import Image from "next/image"; import { useBranding } from "@/context/BrandingContext"; import { useAuth } from "@/context/AuthContext"; import { useTranslation } from "@/context/I18nContext"; @@ -17,7 +18,7 @@ export default function AppHeader() {
{branding?.logo_url ? ( - {branding.name} + {branding.name} ) : (
L
)} diff --git a/web/experience/src/components/blocks/HotspotPlayer.tsx b/web/experience/src/components/blocks/HotspotPlayer.tsx index 6899ac9..7aac35f 100644 --- a/web/experience/src/components/blocks/HotspotPlayer.tsx +++ b/web/experience/src/components/blocks/HotspotPlayer.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, useRef } from "react"; +import Image from "next/image"; import { Search, CheckCircle, XCircle, MousePointer2 } from "lucide-react"; interface Hotspot { @@ -88,10 +89,12 @@ export default function HotspotPlayer({ onClick={handleClick} className="relative aspect-video rounded-3xl overflow-hidden border-4 border-white/10 bg-black cursor-crosshair group select-none shadow-2xl" > - {title} {/* Overlay found hotspots */} @@ -144,8 +147,8 @@ export default function HotspotPlayer({
{found.includes(h.id) ? :
} diff --git a/web/experience/src/lib/api.ts b/web/experience/src/lib/api.ts index 27b1b08..d0b57d5 100644 --- a/web/experience/src/lib/api.ts +++ b/web/experience/src/lib/api.ts @@ -297,6 +297,6 @@ export const lmsApi = { }); }, async getRecommendations(courseId: string): Promise { - return apiFetch(`/courses/${courseId}/recommendations`, {}, true); + return apiFetch(`/courses/${courseId}/recommendations`); } };