- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
Guía de Integración - Ejercicios Módulos 4 y 5
Resumen de Implementación
Se ha completado la capa frontend para los módulos 4 y 5 de GAMILIT con los siguientes componentes:
1. Hooks Creados
useVideoRecorder.ts
Hook para grabación de video con funcionalidades:
- Permiso de cámara/micrófono
- Grabación con preview en vivo
- Pause/Resume
- Configuración de resolución y framerate
- Manejo completo de errores
- Ubicación:
/apps/frontend/src/shared/hooks/useVideoRecorder.ts
Uso:
import { useVideoRecorder } from '@/shared/hooks/useVideoRecorder';
const {
videoBlob,
previewUrl,
startRecording,
stopRecording,
isRecording
} = useVideoRecorder();
// Iniciar grabación con opciones
await startRecording({
width: 1280,
height: 720,
frameRate: 30
});
2. API Clients Creados
manualReviewApi.ts
Cliente para el servicio de revisiones manuales del backend.
- Ubicación:
/apps/frontend/src/shared/api/manualReviewApi.ts
Endpoints:
getPendingReviews()- Lista de revisiones pendientesgetReviewById(id)- Obtener revisión específicastartReview(submissionId)- Iniciar revisiónupdateReview(id, updates)- Guardar progresocompleteReview(id, completion)- Completar y enviar
Uso:
import { manualReviewApi } from '@/shared/api/manualReviewApi';
// Obtener revisiones pendientes
const reviews = await manualReviewApi.getPendingReviews({
moduleId: 'module-4',
exerciseId: 'verificador-fake-news'
});
// Completar revisión
await manualReviewApi.completeReview(reviewId, {
evaluations: [...],
generalFeedback: 'Excelente trabajo',
notifyStudent: true
});
mediaApi.ts
Cliente para upload de archivos multimedia.
- Ubicación:
/apps/frontend/src/shared/api/mediaApi.ts
Funcionalidades:
- Upload de imágenes, audio, video
- Validación de tipos y tamaños
- Progress tracking
- Multi-upload
Uso:
import { mediaApi } from '@/shared/api/mediaApi';
// Upload con progress
const response = await mediaApi.uploadMedia(file, {
type: 'video',
exerciseId: exerciseId,
onProgress: (progress) => {
console.log(`Upload: ${progress}%`);
}
});
console.log('URL:', response.url);
3. Componentes Creados
MediaUploader.tsx
Componente reutilizable para upload de archivos.
- Ubicación:
/apps/frontend/src/shared/components/mechanics/MediaUploader.tsx
Features:
- Drag & drop
- Preview de archivos (imagen, audio, video)
- Progress bar
- Validación automática
- Multi-archivo
Uso:
import { MediaUploader } from '@/shared/components/mechanics/MediaUploader';
<MediaUploader
acceptedTypes={['image', 'video']}
maxSize={10 * 1024 * 1024} // 10MB
maxFiles={3}
exerciseId={exerciseId}
onUpload={(media) => {
console.log('Uploaded:', media);
}}
onError={(error) => {
console.error('Error:', error);
}}
/>
RubricEvaluator.tsx
Componente para calificar según rúbricas.
- Ubicación:
/apps/frontend/src/shared/components/mechanics/RubricEvaluator.tsx
Features:
- Sliders para cada criterio
- Cálculo automático de puntaje total
- Feedback por criterio
- Feedback general
- Validación
Uso:
import { RubricEvaluator } from '@/shared/components/mechanics/RubricEvaluator';
<RubricEvaluator
rubric={review.rubric}
initialEvaluations={review.evaluations}
onChange={(evaluations, feedback, score) => {
console.log('Total score:', score);
}}
onValidation={(valid, errors) => {
console.log('Valid:', valid);
}}
/>
4. Panel de Revisión para Docentes
Ubicación: /apps/frontend/src/apps/teacher/pages/ReviewPanel/
Componentes:
ReviewPanelPage.tsx- Página principalReviewList.tsx- Lista de revisiones pendientesReviewDetail.tsx- Vista detallada con evaluación
Integración en routing:
import { ReviewPanelPage } from '@/apps/teacher/pages/ReviewPanel';
// En teacher routes
<Route path="/teacher/reviews" element={<ReviewPanelPage />} />
Cómo Conectar Ejercicios Módulo 4 a API Real
Patrón General
Los ejercicios del módulo 4 ya tienen frontend completo pero usan mock data. Para conectarlos:
1. Importar el hook de submission
import { useExerciseSubmission } from '@/features/mechanics/shared/hooks/useExerciseSubmission';
2. Usar el hook en el componente
const { submissionState, submitAnswers } = useExerciseSubmission({
onSuccess: (response) => {
setFeedback({
score: response.score,
correctAnswers: response.correctAnswers,
totalQuestions: response.totalQuestions,
xpEarned: response.xpEarned,
mlCoinsEarned: response.mlCoinsEarned,
explanations: response.feedback.explanations,
});
setShowFeedback(true);
onComplete?.(response);
},
onError: (error) => {
console.error('Submission error:', error);
alert('Error al enviar respuestas: ' + error);
}
});
3. Reemplazar mock submission con API real
// ANTES (mock):
const handleSubmit = async () => {
const mockScore = calculateScore();
setFeedback({ score: mockScore, ... });
};
// DESPUÉS (API real):
const handleSubmit = async () => {
if (!exerciseId) return;
await submitAnswers(exerciseId, {
// Estructura específica del ejercicio
selectedArticleId,
claims,
results,
});
};
Ejemplo Específico: VerificadorFakeNews
// En VerificadorFakeNewsExercise.tsx
import { useExerciseSubmission } from '@/features/mechanics/shared/hooks/useExerciseSubmission';
export const VerificadorFakeNewsExercise: React.FC<ExerciseProps> = ({
exerciseId,
onComplete,
onProgressUpdate,
initialData,
exercise,
}) => {
// ... existing state ...
// ADD: Submission hook
const { submissionState, submitAnswers } = useExerciseSubmission({
onSuccess: (response) => {
setFeedback({
score: response.score,
correct: response.correctAnswers,
total: response.totalQuestions,
xpEarned: response.xpEarned,
mlCoinsEarned: response.mlCoinsEarned,
bonuses: response.bonuses,
feedback: response.feedback,
});
setShowFeedback(true);
onComplete?.(response);
},
onError: (error) => {
alert('Error al enviar: ' + error);
}
});
// MODIFY: Submit handler
const handleSubmit = async () => {
if (!exerciseId) return;
await submitAnswers(exerciseId, {
selectedArticleId,
claims,
verificationResults: results,
}, 0); // hintsUsed
};
// En el JSX, agregar loading state
return (
<>
{/* ... existing UI ... */}
<button
onClick={handleSubmit}
disabled={submissionState.loading || results.length === 0}
className="..."
>
{submissionState.loading ? 'Enviando...' : 'Enviar Verificación'}
</button>
{submissionState.error && (
<div className="error">
{submissionState.error}
</div>
)}
</>
);
};
Ejercicios a Actualizar (Módulo 4)
- VerificadorFakeNews - Patrón mostrado arriba
- QuizTikTok - Similar, enviar
selectedOptionsyswipeHistory - NavegacionHipertextual - Enviar
navigationPathydocumentsVisited - AnalisisMemes - Enviar
annotationsyanalysisText - InfografiaInteractiva - Enviar
interactedElementsyanswers
Cómo Conectar Ejercicios Módulo 5 a API Real
Los ejercicios del módulo 5 requieren upload de multimedia:
Patrón General
1. Importar componentes necesarios
import { MediaUploader } from '@/shared/components/mechanics/MediaUploader';
import { useExerciseSubmission } from '@/features/mechanics/shared/hooks/useExerciseSubmission';
import { MediaAttachmentResponse } from '@/shared/api/mediaApi';
2. State para media attachments
const [uploadedMedia, setUploadedMedia] = useState<MediaAttachmentResponse[]>([]);
3. Usar MediaUploader en UI
<MediaUploader
acceptedTypes={['image', 'audio']} // o ['video']
maxFiles={5}
exerciseId={exerciseId}
onUpload={(media) => {
setUploadedMedia(media);
}}
onError={(error) => {
console.error('Upload error:', error);
}}
/>
4. Incluir media IDs en submission
const handleSubmit = async () => {
await submitAnswers(exerciseId, {
textContent: diaryContent,
mediaIds: uploadedMedia.map(m => m.id),
// ... otros datos específicos
});
};
Ejemplo Específico: DiarioMultimedia
import React, { useState } from 'react';
import { MediaUploader } from '@/shared/components/mechanics/MediaUploader';
import { useExerciseSubmission } from '@/features/mechanics/shared/hooks/useExerciseSubmission';
import { MediaAttachmentResponse } from '@/shared/api/mediaApi';
export const DiarioMultimediaExercise: React.FC<ExerciseProps> = ({
exerciseId,
onComplete,
}) => {
const [entries, setEntries] = useState<DiaryEntry[]>([]);
const [currentTitle, setCurrentTitle] = useState('');
const [currentContent, setCurrentContent] = useState('');
const [uploadedMedia, setUploadedMedia] = useState<MediaAttachmentResponse[]>([]);
const { submissionState, submitAnswers } = useExerciseSubmission({
onSuccess: (response) => {
alert(`Diario guardado! Puntaje: ${response.score}`);
onComplete?.(response);
}
});
const handleSaveEntry = async () => {
if (!currentTitle || !currentContent) return;
const newEntry: DiaryEntry = {
id: Date.now().toString(),
date: new Date(),
title: currentTitle,
content: currentContent,
mediaIds: uploadedMedia.map(m => m.id),
};
const updatedEntries = [newEntry, ...entries];
setEntries(updatedEntries);
// Clear form
setCurrentTitle('');
setCurrentContent('');
setUploadedMedia([]);
};
const handleSubmitDiary = async () => {
if (!exerciseId) return;
await submitAnswers(exerciseId, {
entries: entries.map(e => ({
title: e.title,
content: e.content,
mediaIds: e.mediaIds,
date: e.date,
}))
});
};
return (
<div>
{/* Entry Form */}
<input
value={currentTitle}
onChange={(e) => setCurrentTitle(e.target.value)}
placeholder="Título"
/>
<textarea
value={currentContent}
onChange={(e) => setCurrentContent(e.target.value)}
placeholder="Contenido"
/>
{/* Media Upload */}
<MediaUploader
acceptedTypes={['image', 'audio']}
maxFiles={3}
exerciseId={exerciseId}
onUpload={setUploadedMedia}
onError={(err) => alert(err)}
/>
<button onClick={handleSaveEntry}>
Guardar Entrada
</button>
{/* Submit All */}
<button
onClick={handleSubmitDiary}
disabled={submissionState.loading || entries.length === 0}
>
{submissionState.loading ? 'Enviando...' : 'Enviar Diario'}
</button>
</div>
);
};
Ejemplo Específico: VideoCarta
import { useVideoRecorder } from '@/shared/hooks/useVideoRecorder';
import { mediaApi } from '@/shared/api/mediaApi';
import { useExerciseSubmission } from '@/features/mechanics/shared/hooks/useExerciseSubmission';
export const VideoCartaExercise: React.FC<ExerciseProps> = ({
exerciseId,
onComplete,
}) => {
const [videoId, setVideoId] = useState<string | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const {
videoBlob,
videoUrl,
previewUrl,
startRecording,
stopRecording,
isRecording,
resetRecording,
} = useVideoRecorder();
const { submissionState, submitAnswers } = useExerciseSubmission({
onSuccess: (response) => {
alert(`Video enviado! Puntaje: ${response.score}`);
onComplete?.(response);
}
});
const handleUploadVideo = async () => {
if (!videoBlob) return;
try {
// Create File from Blob
const file = new File([videoBlob], 'video-carta.webm', {
type: 'video/webm'
});
// Upload
const response = await mediaApi.uploadMedia(file, {
type: 'video',
exerciseId: exerciseId,
onProgress: setUploadProgress
});
setVideoId(response.id);
alert('Video subido exitosamente!');
} catch (error) {
alert('Error al subir video: ' + error);
}
};
const handleSubmit = async () => {
if (!exerciseId || !videoId) return;
await submitAnswers(exerciseId, {
videoId: videoId,
duration: Math.floor(videoBlob?.size || 0 / 1000), // estimate
});
};
return (
<div>
{/* Preview */}
{previewUrl && isRecording && (
<video src={previewUrl} autoPlay muted />
)}
{/* Playback */}
{videoUrl && !isRecording && (
<video src={videoUrl} controls />
)}
{/* Controls */}
{!isRecording && !videoUrl && (
<button onClick={() => startRecording({ width: 1280, height: 720 })}>
Iniciar Grabación
</button>
)}
{isRecording && (
<button onClick={stopRecording}>
Detener Grabación
</button>
)}
{videoUrl && !videoId && (
<button onClick={handleUploadVideo}>
Subir Video ({uploadProgress}%)
</button>
)}
{videoUrl && (
<button onClick={resetRecording}>
Grabar Nuevo Video
</button>
)}
{videoId && (
<button
onClick={handleSubmit}
disabled={submissionState.loading}
>
{submissionState.loading ? 'Enviando...' : 'Enviar Video Carta'}
</button>
)}
</div>
);
};
Ejercicios a Actualizar (Módulo 5)
- DiarioMultimedia - Texto + audio/imágenes (patrón mostrado arriba)
- ComicDigital - Imágenes + texto en viñetas
- VideoCarta - Video grabado (patrón mostrado arriba)
Integración de Notificaciones
Para notificar cuando se completa una revisión:
1. En el componente ReviewDetail
// Ya implementado en ReviewDetail.tsx
const handleCompleteReview = async () => {
await manualReviewApi.completeReview(review.id, {
evaluations,
generalFeedback,
notifyStudent: true, // ← Activa notificación
});
};
2. El backend enviará notificación automáticamente
El ManualReviewService del backend ya maneja:
- Crear notificación para el estudiante
- Actualizar el score en la submission
- Marcar la revisión como completada
3. Para mostrar notificaciones en el frontend
Si existe un sistema de notificaciones toast:
import { useToast } from '@/shared/hooks/useToast'; // si existe
const { showToast } = useToast();
// En onSuccess del submission
onSuccess: (response) => {
showToast({
type: 'success',
title: 'Ejercicio enviado',
message: `Has obtenido ${response.score} puntos!`
});
}
Si no existe, crear un componente Toast básico o usar el componente Toast existente en:
/apps/frontend/src/shared/components/base/Toast.tsx
Configuración de API Endpoints
Los endpoints ya están configurados en /apps/frontend/src/config/api.config.ts:
teacher: {
reviews: {
pending: '/teacher/reviews/pending',
get: (id: string) => `/teacher/reviews/${id}`,
start: (submissionId: string) => `/teacher/reviews/${submissionId}/start`,
update: (id: string) => `/teacher/reviews/${id}`,
complete: (id: string) => `/teacher/reviews/${id}/complete`,
},
},
media: {
upload: '/educational/media/upload',
get: (id: string) => `/educational/media/${id}`,
delete: (id: string) => `/educational/media/${id}`,
validate: '/educational/media/validate',
},
Testing
Probar MediaUploader
// En un componente de prueba
<MediaUploader
acceptedTypes={['image']}
maxSize={5 * 1024 * 1024}
maxFiles={3}
onUpload={(media) => console.log(media)}
onError={(err) => console.error(err)}
/>
Probar useVideoRecorder
const TestVideo = () => {
const { videoUrl, startRecording, stopRecording } = useVideoRecorder();
return (
<div>
<button onClick={startRecording}>Record</button>
<button onClick={stopRecording}>Stop</button>
{videoUrl && <video src={videoUrl} controls />}
</div>
);
};
Próximos Pasos
- Actualizar cada ejercicio individualmente siguiendo los patrones mostrados
- Probar upload de media con archivos reales
- Probar flujo completo de revisión desde teacher panel
- Agregar validaciones adicionales según necesidades específicas
- Implementar sistema de notificaciones más robusto si es necesario
Notas Importantes
- Todos los archivos creados usan TypeScript estricto
- Se sigue el patrón de estilos detective theme existente
- Los componentes son reutilizables y están bien documentados
- El manejo de errores es completo con mensajes user-friendly
- No se crearon archivos de test (como se solicitó)