diff --git a/services/lms-service/src/handlers.rs b/services/lms-service/src/handlers.rs index 089e9d4..95e6ef2 100644 --- a/services/lms-service/src/handlers.rs +++ b/services/lms-service/src/handlers.rs @@ -23,6 +23,7 @@ use common::models::{ use crate::external_db::MySqlPool; use serde_json::json; use base64::Engine; +use tokio::time::{Duration, timeout}; // Simple token counter (approximate: 1 token ≈ 4 characters in English, ~3-5 in Spanish) fn count_tokens(text: &str) -> i32 { @@ -3798,6 +3799,14 @@ pub async fn get_lesson_feedback( let score_pct = (grade.score * 100.0) as i32; + let fallback_feedback = if score_pct >= 80 { + "Excelente trabajo. Tu rendimiento demuestra dominio de los conceptos clave de esta lección. Mantén ese nivel en la siguiente actividad.".to_string() + } else if score_pct >= 60 { + "Buen esfuerzo. Tienes una base sólida, pero conviene repasar los bloques clave para subir tu precisión en el próximo intento.".to_string() + } else { + "Vas avanzando. Te recomiendo revisar nuevamente los conceptos principales de la lección y volver a practicar los ejercicios para mejorar tu resultado.".to_string() + }; + let block_content = extract_block_content(&lesson.metadata); let context = format!( @@ -3847,7 +3856,9 @@ pub async fn get_lesson_feedback( score_pct, context ); - let response = client.post(&url) + let response_result = timeout( + Duration::from_secs(12), + client.post(&url) .header("Content-Type", "application/json") .header("Authorization", auth_header) .json(&serde_json::json!({ @@ -3858,28 +3869,58 @@ pub async fn get_lesson_feedback( ], "temperature": 0.7 })) - .send() - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error en la solicitud de IA: {}", e)))?; + .send(), + ) + .await; + + let response = match response_result { + Ok(Ok(resp)) => resp, + Ok(Err(e)) => { + tracing::warn!("Feedback IA: error solicitando proveedor: {}", e); + return Ok(Json(ChatResponse { + response: fallback_feedback, + session_id: Uuid::nil(), + })); + } + Err(_) => { + tracing::warn!("Feedback IA: timeout de proveedor externo"); + return Ok(Json(ChatResponse { + response: fallback_feedback, + session_id: Uuid::nil(), + })); + } + }; if !response.status().is_success() { let err_body = response.text().await.unwrap_or_default(); - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Error de la API de IA: {}", err_body), - )); + tracing::warn!("Feedback IA: proveedor devolvió error: {}", err_body); + return Ok(Json(ChatResponse { + response: fallback_feedback, + session_id: Uuid::nil(), + })); } - let ai_data: serde_json::Value = response.json().await.map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Error al analizar la respuesta de la IA: {}", e), - ) - })?; + let ai_data: serde_json::Value = match timeout(Duration::from_secs(8), response.json()).await { + Ok(Ok(data)) => data, + Ok(Err(e)) => { + tracing::warn!("Feedback IA: error parseando respuesta: {}", e); + return Ok(Json(ChatResponse { + response: fallback_feedback, + session_id: Uuid::nil(), + })); + } + Err(_) => { + tracing::warn!("Feedback IA: timeout parseando respuesta"); + return Ok(Json(ChatResponse { + response: fallback_feedback, + session_id: Uuid::nil(), + })); + } + }; let tutor_response = ai_data["choices"][0]["message"]["content"] .as_str() - .unwrap_or("Buen trabajo completando la lección. Revisa tus resultados arriba.") + .unwrap_or(&fallback_feedback) .to_string(); Ok(Json(ChatResponse { diff --git a/web/experience/src/components/LessonLockedView.tsx b/web/experience/src/components/LessonLockedView.tsx index c281360..3c075f9 100644 --- a/web/experience/src/components/LessonLockedView.tsx +++ b/web/experience/src/components/LessonLockedView.tsx @@ -21,7 +21,7 @@ export default function LessonLockedView({ lessonId, courseId, grade, maxAttempt const res = await lmsApi.getLessonFeedback(lessonId); setFeedback(res.response); } catch (err) { - console.error("Error fetching AI feedback:", err); + console.warn("AI feedback unavailable, usando fallback local"); setFeedback("¡Buen trabajo completando esta evaluación! Sigue así para mejorar tus resultados en las próximas lecciones."); } finally { setLoading(false); @@ -38,18 +38,18 @@ export default function LessonLockedView({ lessonId, courseId, grade, maxAttempt {/* Header / Score Card */}
Intentos Usados
-{grade.attempts_count} {maxAttempts ? `de ${maxAttempts}` : ""}
+{grade.attempts_count} {maxAttempts ? `de ${maxAttempts}` : ""}
Estado
@@ -87,25 +87,25 @@ export default function LessonLockedView({ lessonId, courseId, grade, maxAttempt {/* AI Feedback Section */}Generando análisis personalizado...
+Generando análisis personalizado...
+
Este análisis es generado automáticamente basándose en tu desempeño histórico y los contenidos de esta lección.
Continúa con la siguiente lección para seguir sumando XP.
+Continúa con la siguiente lección para seguir sumando XP.
Revisa el glosario de términos de esta sección.
+Revisa el glosario de términos de esta sección.