feat: SAM integration, deployment scripts, and audio response enhancements

- Add SAM (Sistema de Administración Académica) integration with sync endpoints
- Add deployment automation (deploy.sh, remote-setup.sh, setup-nginx-ssl.sh)
- Add nginx proxy configuration for SSL with Let's Encrypt
- Add audio response support for student lessons (migrations, handlers)
- Add audio evaluations admin page
- Update CORS to support wildcard subdomains for norteamericano.cl
- Add comprehensive deployment documentation (DESPLIEGUE.md, ManualDeConfiguracion.md)
- Update docker-compose.yml with nginx-proxy and acme-companion services
- Remove outdated documentation files

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-27 09:20:23 -03:00
parent 995065df4f
commit e4866c6dee
50 changed files with 4866 additions and 5371 deletions
+133 -3
View File
@@ -1,11 +1,17 @@
const getApiBaseUrl = (defaultPort: string, envVar?: string) => {
// Si hay una variable de entorno definida, usarla siempre
if (envVar && envVar.trim() !== '') {
return envVar;
}
// Fallback para desarrollo local
if (typeof window !== 'undefined') {
const hostname = window.location.hostname;
// Detect if we are on a custom domain or IP
const protocol = window.location.protocol;
return `${protocol}//${hostname}:${defaultPort}`;
}
return envVar || `http://localhost:${defaultPort}`;
return `http://localhost:${defaultPort}`;
};
export const API_BASE_URL = getApiBaseUrl("3001", process.env.NEXT_PUBLIC_CMS_API_URL);
@@ -763,7 +769,6 @@ export const cmsApi = {
},
getSSOConfig: (): Promise<OrganizationSSOConfig> => apiFetch('/organization/sso'),
updateSSOConfig: (payload: Partial<OrganizationSSOConfig>): Promise<void> => apiFetch('/organization/sso', { method: 'PUT', body: JSON.stringify(payload) }),
getOrganization: (): Promise<Organization> => apiFetch('/organization'),
// Auth
register: (payload: AuthPayload): Promise<AuthResponse> => apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }),
@@ -1209,6 +1214,37 @@ export const lmsApi = {
getMyBadges: (): Promise<Badge[]> =>
apiFetch(`/my/badges`, {}, true),
// Audio Responses
getAudioResponses: (filters?: AudioResponseFilters): Promise<AudioResponse[]> => {
const query: Record<string, string> = {};
if (filters?.course_id) query.course_id = filters.course_id;
if (filters?.lesson_id) query.lesson_id = filters.lesson_id;
if (filters?.status) query.status = filters.status;
if (filters?.user_id) query.user_id = filters.user_id;
return apiFetch('/audio-responses', { method: 'GET', query }, true);
},
getAudioResponseDetail: (id: string): Promise<AudioResponse> =>
apiFetch(`/audio-responses/${id}`, { method: 'GET' }, true),
getAudioResponseAudio: (id: string): Promise<Blob> => {
const token = getToken();
return fetch(`${LMS_API_BASE_URL}/audio-responses/${id}/audio`, {
method: 'GET',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
}).then(async res => {
if (!res.ok) throw new Error('Failed to fetch audio');
return res.blob();
});
},
evaluateAudioResponse: (id: string, teacherScore: number, teacherFeedback?: string): Promise<{ success: boolean; message: string }> =>
apiFetch(`/audio-responses/${id}/evaluate`, {
method: 'POST',
body: JSON.stringify({ teacher_score: teacherScore, teacher_feedback: teacherFeedback })
}, true),
getCourseAudioResponseStats: (courseId: string): Promise<AudioResponseStats> =>
apiFetch(`/courses/${courseId}/audio-responses/stats`, { method: 'GET' }, true),
};
export interface Meeting {
@@ -1384,4 +1420,98 @@ export interface TestTemplateFilters {
test_type?: TestType;
tags?: string;
search?: string;
}
// ==================== AUDIO RESPONSE INTERFACES ====================
export interface AudioResponse {
id: string;
user_id: string;
student_name: string;
student_email: string;
course_id: string;
course_title: string;
lesson_id: string;
lesson_title: string;
block_id: string;
prompt: string;
transcript: string | null;
ai_score: number | null;
ai_found_keywords: string[] | null;
ai_feedback: string | null;
teacher_score: number | null;
teacher_feedback: string | null;
status: 'pending' | 'ai_evaluated' | 'teacher_evaluated' | 'both_evaluated';
created_at: string;
attempt_number: number;
}
export interface AudioResponseStats {
organization_id: string;
course_id: string;
lesson_id: string;
total_responses: number;
ai_evaluated: number;
teacher_evaluated: number;
fully_evaluated: number;
pending: number;
avg_ai_score: number | null;
avg_teacher_score: number | null;
}
export interface AudioResponseFilters {
course_id?: string;
lesson_id?: string;
status?: string;
user_id?: string;
}
// ==================== AUDIO RESPONSE API ====================
export async function getAudioResponses(filters?: AudioResponseFilters): Promise<AudioResponse[]> {
const query: Record<string, string> = {};
if (filters?.course_id) query.course_id = filters.course_id;
if (filters?.lesson_id) query.lesson_id = filters.lesson_id;
if (filters?.status) query.status = filters.status;
if (filters?.user_id) query.user_id = filters.user_id;
return apiFetch('/audio-responses', { method: 'GET', query }, true);
}
export async function getAudioResponseDetail(id: string): Promise<AudioResponse> {
return apiFetch(`/audio-responses/${id}`, { method: 'GET' }, true);
}
export async function getAudioResponseAudio(id: string): Promise<Blob> {
const token = getToken();
const response = await fetch(`${LMS_API_BASE_URL}/audio-responses/${id}/audio`, {
method: 'GET',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
});
if (!response.ok) {
throw new Error('Failed to fetch audio');
}
return response.blob();
}
export async function evaluateAudioResponse(
id: string,
teacherScore: number,
teacherFeedback?: string
): Promise<{ success: boolean; message: string }> {
return apiFetch(`/audio-responses/${id}/evaluate`, {
method: 'POST',
body: JSON.stringify({
teacher_score: teacherScore,
teacher_feedback: teacherFeedback
})
}, true);
}
export async function getCourseAudioResponseStats(courseId: string): Promise<AudioResponseStats> {
return apiFetch(`/courses/${courseId}/audio-responses/stats`, { method: 'GET' }, true);
}