workspace/projects/gamilit/apps/frontend/INTEGRATION_GUIDE.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- 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>
2025-12-08 10:44:23 -06:00

675 lines
17 KiB
Markdown

# 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:**
```tsx
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 pendientes
- `getReviewById(id)` - Obtener revisión específica
- `startReview(submissionId)` - Iniciar revisión
- `updateReview(id, updates)` - Guardar progreso
- `completeReview(id, completion)` - Completar y enviar
**Uso:**
```tsx
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:**
```tsx
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:**
```tsx
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:**
```tsx
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 principal
- `ReviewList.tsx` - Lista de revisiones pendientes
- `ReviewDetail.tsx` - Vista detallada con evaluación
**Integración en routing:**
```tsx
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
```tsx
import { useExerciseSubmission } from '@/features/mechanics/shared/hooks/useExerciseSubmission';
```
#### 2. Usar el hook en el componente
```tsx
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
```tsx
// 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
```tsx
// 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)
1. **VerificadorFakeNews** - Patrón mostrado arriba
2. **QuizTikTok** - Similar, enviar `selectedOptions` y `swipeHistory`
3. **NavegacionHipertextual** - Enviar `navigationPath` y `documentsVisited`
4. **AnalisisMemes** - Enviar `annotations` y `analysisText`
5. **InfografiaInteractiva** - Enviar `interactedElements` y `answers`
## 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
```tsx
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
```tsx
const [uploadedMedia, setUploadedMedia] = useState<MediaAttachmentResponse[]>([]);
```
#### 3. Usar MediaUploader en UI
```tsx
<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
```tsx
const handleSubmit = async () => {
await submitAnswers(exerciseId, {
textContent: diaryContent,
mediaIds: uploadedMedia.map(m => m.id),
// ... otros datos específicos
});
};
```
### Ejemplo Específico: DiarioMultimedia
```tsx
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
```tsx
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)
1. **DiarioMultimedia** - Texto + audio/imágenes (patrón mostrado arriba)
2. **ComicDigital** - Imágenes + texto en viñetas
3. **VideoCarta** - Video grabado (patrón mostrado arriba)
## Integración de Notificaciones
Para notificar cuando se completa una revisión:
### 1. En el componente ReviewDetail
```tsx
// 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:
```tsx
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`:
```typescript
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
```tsx
// 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
```tsx
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
1. **Actualizar cada ejercicio individualmente** siguiendo los patrones mostrados
2. **Probar upload de media** con archivos reales
3. **Probar flujo completo de revisión** desde teacher panel
4. **Agregar validaciones adicionales** según necesidades específicas
5. **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ó)