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; const protocol = window.location.protocol; return `${protocol}//${hostname}:${defaultPort}`; } return `http://localhost:${defaultPort}`; }; export const API_BASE_URL = getApiBaseUrl("3001", process.env.NEXT_PUBLIC_CMS_API_URL); export const LMS_API_BASE_URL = getApiBaseUrl("3002", process.env.NEXT_PUBLIC_LMS_API_URL); // Polyfill for crypto.randomUUID() for non-HTTPS contexts export function generateUUID(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } // Fallback for non-secure contexts return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } export const getImageUrl = (path?: string) => { if (!path) return ''; if (path.startsWith('http')) return path; // Map uploads to assets if backend stores relative paths // The main.rs serves "uploads" dir at "/assets" route let cleanPath = path; if (cleanPath.startsWith('uploads/')) cleanPath = '/' + cleanPath.replace('uploads/', 'assets/'); if (cleanPath.startsWith('/uploads/')) cleanPath = cleanPath.replace('/uploads/', '/assets/'); const finalPath = cleanPath.startsWith('/') ? cleanPath : `/${cleanPath}`; return `${API_BASE_URL}${finalPath}`; }; export interface Course { id: string; title: string; description?: string; instructor_id: string; pacing_mode: 'self_paced' | 'instructor_led'; organization_id: string; start_date?: string; end_date?: string; passing_percentage: number; certificate_template?: string; price: number; currency: string; marketing_metadata?: { objectives?: string; requirements?: string; duration?: string; modules_summary?: string; certification_info?: string; }; course_image_url?: string; created_at: string; updated_at: string; modules?: Module[]; } export interface Module { id: string; course_id: string; title: string; position: number; lessons: Lesson[]; } export interface QuizQuestion { id: string; question: string; options: string[]; correct: number[]; type?: 'multiple-choice' | 'true-false' | 'multiple-select'; } export interface Block { id: string; type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'document' | 'video_marker' | 'audio-response' | 'memory-match' | 'hotspot' | 'peer-review' | 'role-playing' | 'mermaid' | 'code-lab'; title?: string; content?: string; url?: string; description?: string; // For hotspot or general info media_type?: 'video' | 'audio'; config?: Record; quiz_data?: { questions: QuizQuestion[]; }; pairs?: { left: string; right: string; id?: string }[]; items?: string[]; prompt?: string; correctAnswers?: string[]; keywords?: string[]; timeLimit?: number; markers?: { timestamp: number; question: string; options: string[]; correctIndex: number; }[]; hotspots?: { id: string; x: number; y: number; radius: number; label: string; }[]; imageUrl?: string; reviewCriteria?: string; // Role-playing fields scenario?: string; ai_persona?: string; user_role?: string; objectives?: string; initial_message?: string; // Mermaid fields mermaid_code?: string; // Code Lab fields language?: string; instructions?: string; initial_code?: string; solution?: string; test_cases?: { description: string; expected: string }[]; } export interface Lesson { id: string; module_id: string; title: string; content_type: string; content_url?: string; metadata?: { blocks: Block[]; }; content_blocks?: Block[]; is_graded: boolean; grading_category_id: string | null; max_attempts: number | null; allow_retry: boolean; due_date?: string; important_date_type?: 'exam' | 'assignment' | 'milestone' | 'live-session'; summary?: string; transcription?: { en?: string; es?: string; cues?: { start: number; end: number; text: string }[]; } | null; transcription_status?: 'idle' | 'queued' | 'processing' | 'completed' | 'failed'; is_previewable: boolean; created_at: string; } export interface Organization { id: string; name: string; domain?: string; logo_url?: string; favicon_url?: string; platform_name?: string; primary_color?: string; secondary_color?: string; certificate_template?: string; logo_variant?: string; created_at: string; updated_at: string; } export interface BrandingPayload { name?: string; primary_color?: string; secondary_color?: string; platform_name?: string; logo_variant?: string; } export interface BrandingResponse { logo_url?: string; favicon_url?: string; platform_name?: string; logo_variant?: string; primary_color: string; secondary_color: string; } export interface User { id: string; email: string; full_name: string; role: string; organization_id?: string; avatar_url?: string; bio?: string; language?: string; } export interface OrganizationSSOConfig { organization_id: string; issuer_url: string; client_id: string; client_secret: string; enabled: boolean; created_at: string; updated_at: string; } export interface ProvisionPayload { org_name: string; org_domain?: string; admin_email: string; admin_password: string; admin_full_name: string; } export interface UserCreatePayload { email: string; password: string; full_name: string; role: string; } export interface AuthResponse { user: User; token: string; } export interface AuthPayload { email: string; password?: string; full_name?: string; role?: 'instructor' | 'admin'; organization_name?: string; } export interface UploadResponse { id: string; filename: string; url: string; mimetype?: string; size_bytes?: number; logo_url?: string; favicon_url?: string; } // ==================== AI Usage Global ==================== export interface GlobalAiSummary { total_tokens: number; total_input: number; total_output: number; total_requests: number; total_cost_usd: number; savings_vs_openai_usd: number; savings_percentage: number; openai_equivalent_cost_usd: number; total_organizations: number; total_active_users: number; } export interface StudentChatSummary { total_tokens: number; total_requests: number; total_cost_usd: number; active_students: number; } export interface DailyUsage { date: string; total_tokens: number; input_tokens: number; output_tokens: number; cost_usd: number; requests: number; } export interface UsageByEndpoint { endpoint: string; request_type: string; total_tokens: number; input_tokens: number; output_tokens: number; cost_usd: number; requests: number; } export interface UsageByOrganization { org_id: string; org_name: string; total_tokens: number; input_tokens: number; output_tokens: number; cost_usd: number; requests: number; active_users: number; } export interface UsageByRequestType { request_type: string; total_tokens: number; cost_usd: number; requests: number; } export interface TopUserUsage { user_id: string; email: string; full_name: string; role: string; org_name: string; total_tokens: number; input_tokens: number; output_tokens: number; cost_usd: number; requests: number; } export interface StudentChatUsage { user_id: string; email: string; full_name: string; org_name: string; total_tokens: number; cost_usd: number; chat_requests: number; last_chat: string; } export interface GlobalAiUsageResponse { summary: GlobalAiSummary; student_chat_summary: StudentChatSummary | null; daily_usage: DailyUsage[]; by_endpoint: UsageByEndpoint[]; by_organization: UsageByOrganization[]; by_request_type: UsageByRequestType[]; top_users: TopUserUsage[]; student_chat_usage: StudentChatUsage[]; } // ==================== Grading ==================== export interface GradingCategory { id: string; course_id: string; name: string; weight: number; drop_count: number; } // Content Libraries export interface LibraryBlock { id: string; organization_id: string; created_by: string; name: string; description?: string; block_type: string; block_data: Block; tags?: string[]; usage_count: number; created_at: string; updated_at: string; } export interface CreateLibraryBlockPayload { name: string; description?: string; block_type: string; block_data: Block; tags?: string[]; } export interface UpdateLibraryBlockPayload { name?: string; description?: string; tags?: string[]; } export interface LibraryBlockFilters { type?: string; tags?: string; search?: string; } // ==================== Advanced Grading / Rubrics ==================== export interface Rubric { id: string; organization_id: string; course_id: string | null; created_by: string; name: string; description: string | null; total_points: number; created_at: string; updated_at: string; } export interface RubricCriterion { id: string; rubric_id: string; name: string; description: string | null; max_points: number; position: number; created_at: string; } export interface RubricLevel { id: string; criterion_id: string; name: string; description: string | null; points: number; position: number; created_at: string; } export interface RubricWithDetails { id: string; organization_id: string; course_id: string | null; created_by: string; name: string; description: string | null; total_points: number; created_at: string; updated_at: string; criteria: CriterionWithLevels[]; } export interface CriterionWithLevels { id: string; rubric_id: string; name: string; description: string | null; max_points: number; position: number; created_at: string; levels: RubricLevel[]; } export interface CreateRubricPayload { name: string; description?: string; course_id?: string; } export interface UpdateRubricPayload { name?: string; description?: string; } export interface CreateCriterionPayload { name: string; description?: string; max_points: number; position?: number; } export interface UpdateCriterionPayload { name?: string; description?: string; max_points?: number; position?: number; } export interface CreateLevelPayload { name: string; description?: string; points: number; position?: number; } export interface UpdateLevelPayload { name?: string; description?: string; points?: number; position?: number; } // ==================== Learning Sequences / Dependencies ==================== export interface LessonDependency { id: string; organization_id: string; lesson_id: string; prerequisite_lesson_id: string; min_score_percentage: number | null; created_at: string; } export interface AssignDependencyPayload { prerequisite_lesson_id: string; min_score_percentage?: number; } export interface AuditLog { id: string; user_id: string; user_full_name: string | null; action: string; entity_type: string; entity_id: string; changes: Record; created_at: string; } export interface CourseAnalytics { course_id: string; total_enrollments: number; average_score: number; lessons: { lesson_id: string; lesson_title: string; average_score: number; submission_count: number; }[]; } export interface CohortData { period: string; count: number; completion_rate: number; } export interface RetentionData { lesson_id: string; lesson_title: string; student_count: number; } export interface AdvancedAnalytics { cohorts: CohortData[]; retention: RetentionData[]; } export interface DropoutRisk { id: string; organization_id: string; course_id: string; user_id: string; risk_level: 'low' | 'medium' | 'high' | 'critical'; score: number; reasons?: { metric: string, value: number, description: string }[]; last_calculated_at: string; user_full_name?: string; // Optional for UI display user_email?: string; } export interface Webhook { id: string; organization_id: string; url: string; events: string[]; secret?: string; is_active: boolean; created_at: string; updated_at: string; } export interface CreateWebhookPayload { url: string; events: string[]; secret?: string; } export interface Asset { id: string; organization_id: string; uploaded_by: string | null; course_id: string | null; filename: string; storage_path: string; mimetype: string; size_bytes: number; created_at: string; } export interface AssetFilters { mimetype?: string; course_id?: string; search?: string; page?: number; limit?: number; } export interface Cohort { id: string; organization_id: string; name: string; description?: string; created_at: string; updated_at: string; } export interface UserCohort { id: string; cohort_id: string; user_id: string; assigned_at: string; } export interface CreateCohortPayload { name: string; description?: string; } export interface AddMemberPayload { user_id: string; } export interface StudentGradeReport { user_id: string; full_name: string; email: string; progress: number; average_score: number | null; last_active_at: string | null; } export interface BulkEnrollResponse { successful_emails: string[]; failed_emails: string[]; already_enrolled_emails: string[]; } export interface SubmissionWithReviews { id: string; user_id: string; full_name: string; email: string; submitted_at: string; review_count: number; average_score: number | null; } export interface CourseAnnouncement { id: string; organization_id: string; course_id: string; author_id: string; title: string; content: string; is_pinned: boolean; created_at: string; updated_at: string; cohort_ids?: string[]; } export interface AnnouncementWithAuthor extends CourseAnnouncement { author_name: string; author_avatar?: string; } export interface CreateAnnouncementPayload { title: string; content: string; is_pinned?: boolean; cohort_ids?: string[]; } export interface UpdateAnnouncementPayload { title?: string; content?: string; is_pinned?: boolean; } export interface CourseSubmission { id: string; user_id: string; course_id: string; lesson_id: string; content: string; submitted_at: string; } export interface PeerReview { id: string; submission_id: string; reviewer_id: string; score: number; feedback: string; created_at: string; } export interface CourseInstructor { id: string; course_id: string; user_id: string; role: 'primary' | 'instructor' | 'assistant'; created_at: string; email: string; full_name: string; } const getToken = () => typeof window !== 'undefined' ? localStorage.getItem('studio_token') : null; const getSelectedOrgId = () => typeof window !== 'undefined' ? localStorage.getItem('studio_selected_org_id') : null; interface ApiFetchOptions extends RequestInit { query?: Record; } export const apiFetch = (url: string, options: ApiFetchOptions = {}, isLms: boolean = false) => { const token = getToken(); const selectedOrgId = getSelectedOrgId(); const baseUrl = isLms ? LMS_API_BASE_URL : API_BASE_URL; const isFormData = options.body instanceof FormData; const headers: Record = { ...(isFormData ? {} : { 'Content-Type': 'application/json' }), ...Object.fromEntries(Object.entries(options.headers || {}).map(([k, v]) => [k, String(v)])), ...(token ? { 'Authorization': `Bearer ${token}` } : {}), ...(selectedOrgId ? { 'X-Organization-Id': selectedOrgId } : {}) }; // Build query string const queryParams = options.query; let finalUrl = `${baseUrl}${url}`; if (queryParams) { const searchParams = new URLSearchParams(); Object.entries(queryParams).forEach(([key, value]) => { if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } }); const queryString = searchParams.toString(); if (queryString) { finalUrl += `${url.includes('?') ? '&' : '?'}${queryString}`; } } return fetch(finalUrl, { ...options, headers }).then(async res => { if (!res.ok) { const text = await res.text(); try { const json = JSON.parse(text); return Promise.reject(new Error(json.message || 'An error occurred')); } catch { return Promise.reject(new Error(text || res.statusText)); } } // Handle no-content responses if (res.status === 204) return; return res.json(); }); }; export const cmsApi = { // Organization getOrganization: (): Promise => apiFetch('/organization'), updateOrganizationBranding: (payload: BrandingPayload): Promise => apiFetch('/organization/branding', { method: 'PUT', body: JSON.stringify(payload) }), uploadOrganizationLogo: (file: File): Promise<{ logo_url: string }> => { const formData = new FormData(); formData.append('file', file); return apiFetch('/organization/logo', { method: 'POST', body: formData }); }, uploadOrganizationFavicon: (file: File): Promise<{ favicon_url: string }> => { const formData = new FormData(); formData.append('file', file); return apiFetch('/organization/favicon', { method: 'POST', body: formData }); }, getSSOConfig: (): Promise => apiFetch('/organization/sso'), updateSSOConfig: (payload: Partial): Promise => apiFetch('/organization/sso', { method: 'PUT', body: JSON.stringify(payload) }), // Auth register: (payload: AuthPayload): Promise => apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }), login: (payload: AuthPayload): Promise => apiFetch('/auth/login', { method: 'POST', body: JSON.stringify(payload) }), getMe: (): Promise => apiFetch('/auth/me'), // Branding (Public) getBranding: (): Promise => apiFetch('/branding'), // Courses getCourses: (): Promise => apiFetch('/courses'), createCourse: (title: string, organizationId?: string): Promise => apiFetch('/courses', { method: 'POST', body: JSON.stringify({ title, organization_id: organizationId }) }), getCourse: (id: string): Promise => apiFetch(`/courses/${id}`), getCourseWithFullOutline: (id: string): Promise => apiFetch(`/courses/${id}/outline`), updateCourse: (id: string, payload: Partial): Promise => apiFetch(`/courses/${id}`, { method: 'PUT', body: JSON.stringify(payload) }), publishCourse: (id: string, targetOrganizationId?: string): Promise => apiFetch(`/courses/${id}/publish`, { method: 'POST', body: JSON.stringify({ target_organization_id: targetOrganizationId }) }), getPreviewToken: (id: string): Promise<{ token: string }> => apiFetch(`/courses/${id}/preview-token`, { method: 'POST' }), // Team Management getCourseTeam: (courseId: string): Promise => apiFetch(`/courses/${courseId}/team`), addTeamMember: (courseId: string, email: string, role: string): Promise => apiFetch(`/courses/${courseId}/team`, { method: 'POST', body: JSON.stringify({ email, role }) }), removeTeamMember: (courseId: string, userId: string): Promise => apiFetch(`/courses/${courseId}/team/${userId}`, { method: 'DELETE' }), getUsers: (): Promise => apiFetch('/users'), // Modules & Lessons createModule: (course_id: string, title: string, position: number): Promise => apiFetch('/modules', { method: 'POST', body: JSON.stringify({ course_id, title, position }) }), updateModule: (id: string, payload: Partial): Promise => apiFetch(`/modules/${id}`, { method: 'PUT', body: JSON.stringify(payload) }), createLesson: (module_id: string, title: string, content_type: string, position: number): Promise => apiFetch('/lessons', { method: 'POST', body: JSON.stringify({ module_id, title, content_type, position }) }), getLesson: (id: string): Promise => apiFetch(`/lessons/${id}`), updateLesson: (id: string, payload: Partial): Promise => apiFetch(`/lessons/${id}`, { method: 'PUT', body: JSON.stringify(payload) }), summarizeLesson: (id: string): Promise => apiFetch(`/lessons/${id}/summarize`, { method: 'POST' }), async generateQuiz(lessonId: string, payload: { prompt_hint?: string, quiz_type?: string }): Promise { return apiFetch(`/lessons/${lessonId}/generate-quiz`, { method: 'POST', body: JSON.stringify(payload) }); }, async generateRolePlay(lessonId: string, payload: { prompt_hint?: string }): Promise<{ title: string; scenario: string; ai_persona: string; user_role: string; objectives: string; initial_message: string; }> { return apiFetch(`/lessons/${lessonId}/generate-role-play`, { method: 'POST', body: JSON.stringify(payload) }); }, async generateMermaidDiagram(lessonId: string, payload: { prompt_hint?: string }): Promise<{ mermaid_code: string }> { return apiFetch(`/lessons/${lessonId}/generate-mermaid`, { method: 'POST', body: JSON.stringify(payload) }); }, async generateCodeLab(lessonId: string, payload: { language?: string; prompt_hint?: string }): Promise<{ language: string; title: string; instructions: string; initial_code: string; solution: string; test_cases: { description: string; expected: string }[]; }> { return apiFetch(`/lessons/${lessonId}/generate-code-lab`, { method: 'POST', body: JSON.stringify(payload) }); }, async generateHotspots(lessonId: string, payload: { image_url: string, prompt_hint?: string }): Promise<{ label: string; description: string; x: number; y: number; }[]> { return apiFetch(`/lessons/${lessonId}/generate-hotspots`, { method: 'POST', body: JSON.stringify(payload) }); }, reviewText: (text: string): Promise<{ suggestion: string, comments: string }> => apiFetch('/api/ai/review-text', { method: 'POST', body: JSON.stringify({ text }) }), deleteModule: (id: string): Promise => apiFetch(`/modules/${id}`, { method: 'DELETE' }), deleteLesson: (id: string): Promise => apiFetch(`/lessons/${id}`, { method: 'DELETE' }), reorderModules: (payload: { items: { id: string, position: number }[] }): Promise => apiFetch('/modules/reorder', { method: 'POST', body: JSON.stringify(payload) }), reorderLessons: (payload: { items: { id: string, position: number }[] }): Promise => apiFetch('/lessons/reorder', { method: 'POST', body: JSON.stringify(payload) }), // Grading getGradingCategories: (courseId: string): Promise => apiFetch(`/courses/${courseId}/grading`), createGradingCategory: (course_id: string, name: string, weight: number): Promise => apiFetch('/grading', { method: 'POST', body: JSON.stringify({ course_id, name, weight, drop_count: 0 }) }), deleteGradingCategory: (id: string): Promise => apiFetch(`/grading/${id}`, { method: 'DELETE' }), // Admin & Analytics getAuditLogs: (): Promise => apiFetch('/audit-logs'), getCourseAnalytics: (id: string, cohortId?: string): Promise => { const query = cohortId ? `?cohort_id=${cohortId}` : ''; return apiFetch(`/courses/${id}/analytics${query}`, {}, true); }, getAdvancedAnalytics: (id: string, cohortId?: string): Promise => { const query = cohortId ? `?cohort_id=${cohortId}` : ''; return apiFetch(`/courses/${id}/analytics/advanced${query}`, {}, true); }, getLessonHeatmap: (lessonId: string): Promise<{ second: number, count: number }[]> => apiFetch(`/lessons/${lessonId}/heatmap`), exportCourse: (id: string): Promise> => apiFetch(`/courses/${id}/export`), importCourse: (data: Record): Promise => apiFetch(`/courses/import`, { method: 'POST', body: JSON.stringify(data) }), deleteCourse: (id: string): Promise => apiFetch(`/courses/${id}`, { method: 'DELETE' }), async generateCourse(prompt: string, targetOrgId?: string): Promise { return apiFetch(`/courses/generate`, { method: 'POST', body: JSON.stringify({ prompt, target_organization_id: targetOrgId }) }); }, // Users getAllUsers: (): Promise => apiFetch('/users'), createUser: (data: UserCreatePayload): Promise => apiFetch('/users', { method: 'POST', body: JSON.stringify(data) }), updateUser: (id: string, payload: { role?: string, organization_id?: string, full_name?: string, avatar_url?: string, bio?: string, language?: string }): Promise => apiFetch(`/users/${id}`, { method: 'PUT', body: JSON.stringify(payload) }), // Webhooks getWebhooks: (): Promise => apiFetch('/webhooks'), createWebhook: (payload: CreateWebhookPayload): Promise => apiFetch('/webhooks', { method: 'POST', body: JSON.stringify(payload) }), deleteWebhook: (id: string): Promise => apiFetch(`/webhooks/${id}`, { method: 'DELETE' }), // Assets getAssets: (filters?: AssetFilters): Promise => { const params = new URLSearchParams(); if (filters) { if (filters.mimetype) params.append('mimetype', filters.mimetype); if (filters.course_id) params.append('course_id', filters.course_id); if (filters.search) params.append('search', filters.search); if (filters.page) params.append('page', filters.page.toString()); if (filters.limit) params.append('limit', filters.limit.toString()); } const query = params.toString(); return apiFetch(`/api/assets${query ? `?${query}` : ''}`); }, getCourseAssets: (courseId: string): Promise => apiFetch(`/api/assets?course_id=${courseId}`), deleteAsset: (id: string): Promise => apiFetch(`/api/assets/${id}`, { method: 'DELETE' }), uploadAsset: (file: File, onProgress?: (pct: number) => void, courseId?: string): Promise => { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', file); if (courseId) formData.append('course_id', courseId); const xhr = new XMLHttpRequest(); xhr.open('POST', `${API_BASE_URL}/api/assets/upload`); const token = getToken(); if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`); const selectedOrgId = getSelectedOrgId(); if (selectedOrgId) xhr.setRequestHeader('X-Organization-Id', selectedOrgId); xhr.upload.onprogress = (event) => { if (event.lengthComputable && onProgress) { const percentComplete = Math.round((event.loaded / event.total) * 100); onProgress(percentComplete); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { let msg = 'Upload failed'; try { msg = JSON.parse(xhr.responseText).message || msg; } catch { } reject(new Error(msg)); } }; xhr.onerror = () => reject(new Error('Network error')); xhr.send(formData); }); }, initSSOLogin: (orgId: string): void => { window.location.href = `${API_BASE_URL}/auth/sso/login/${orgId}`; }, // Background Tasks getBackgroundTasks: (): Promise => apiFetch('/tasks'), retryTask: (id: string): Promise => apiFetch(`/tasks/${id}/retry`, { method: 'POST' }), cancelTask: (id: string): Promise => apiFetch(`/tasks/${id}`, { method: 'DELETE' }), // Content Libraries createLibraryBlock: (payload: CreateLibraryBlockPayload): Promise => apiFetch('/library/blocks', { method: 'POST', body: JSON.stringify(payload) }), listLibraryBlocks: (filters?: LibraryBlockFilters): Promise => { const params = new URLSearchParams(); if (filters?.type) params.append('type', filters.type); if (filters?.tags) params.append('tags', filters.tags); if (filters?.search) params.append('search', filters.search); const query = params.toString() ? `?${params.toString()}` : ''; return apiFetch(`/library/blocks${query}`); }, getLibraryBlock: (id: string): Promise => apiFetch(`/library/blocks/${id}`), updateLibraryBlock: (id: string, payload: UpdateLibraryBlockPayload): Promise => apiFetch(`/library/blocks/${id}`, { method: 'PUT', body: JSON.stringify(payload) }), deleteLibraryBlock: (id: string): Promise => apiFetch(`/library/blocks/${id}`, { method: 'DELETE' }), incrementBlockUsage: (id: string): Promise => apiFetch(`/library/blocks/${id}/increment-usage`, { method: 'POST' }), // Advanced Grading / Rubrics createRubric: (courseId: string, payload: CreateRubricPayload): Promise => apiFetch(`/courses/${courseId}/rubrics`, { method: 'POST', body: JSON.stringify(payload) }), listCourseRubrics: (courseId: string): Promise => apiFetch(`/courses/${courseId}/rubrics`), getRubricWithDetails: (rubricId: string): Promise => apiFetch(`/rubrics/${rubricId}`), updateRubric: (rubricId: string, payload: UpdateRubricPayload): Promise => apiFetch(`/rubrics/${rubricId}`, { method: 'PUT', body: JSON.stringify(payload) }), deleteRubric: (rubricId: string): Promise => apiFetch(`/rubrics/${rubricId}`, { method: 'DELETE' }), // Rubric Criteria createCriterion: (rubricId: string, payload: CreateCriterionPayload): Promise => apiFetch(`/rubrics/${rubricId}/criteria`, { method: 'POST', body: JSON.stringify(payload) }), updateCriterion: (criterionId: string, payload: UpdateCriterionPayload): Promise => apiFetch(`/criteria/${criterionId}`, { method: 'PUT', body: JSON.stringify(payload) }), deleteCriterion: (criterionId: string): Promise => apiFetch(`/criteria/${criterionId}`, { method: 'DELETE' }), // Rubric Levels createLevel: (criterionId: string, payload: CreateLevelPayload): Promise => apiFetch(`/criteria/${criterionId}/levels`, { method: 'POST', body: JSON.stringify(payload) }), updateLevel: (levelId: string, payload: UpdateLevelPayload): Promise => apiFetch(`/levels/${levelId}`, { method: 'PUT', body: JSON.stringify(payload) }), deleteLevel: (levelId: string): Promise => apiFetch(`/levels/${levelId}`, { method: 'DELETE' }), // Lesson-Rubric Association assignRubricToLesson: (lessonId: string, rubricId: string): Promise => apiFetch(`/lessons/${lessonId}/rubrics/${rubricId}`, { method: 'POST' }), unassignRubricFromLesson: (lessonId: string, rubricId: string): Promise => apiFetch(`/lessons/${lessonId}/rubrics/${rubricId}`, { method: 'DELETE' }), getLessonRubrics: (lessonId: string): Promise => apiFetch(`/lessons/${lessonId}/rubrics`), // Learning Sequences (Dependencies) listLessonDependencies: (lessonId: string): Promise => apiFetch(`/lessons/${lessonId}/dependencies`), assignDependency: (lessonId: string, payload: AssignDependencyPayload): Promise => apiFetch(`/lessons/${lessonId}/dependencies`, { method: 'POST', body: JSON.stringify(payload) }), removeDependency: (lessonId: string, prerequisiteId: string): Promise => apiFetch(`/lessons/${lessonId}/dependencies/${prerequisiteId}`, { method: 'DELETE' }), // Test Templates listTestTemplates: (filters?: TestTemplateFilters): Promise => apiFetch('/test-templates', { method: 'GET', query: filters as any }, false), getTestTemplate: (templateId: string): Promise => apiFetch(`/test-templates/${templateId}`, {}, false), createTestTemplate: (payload: CreateTestTemplatePayload): Promise => apiFetch('/test-templates', { method: 'POST', body: JSON.stringify(payload) }, false), updateTestTemplate: (templateId: string, payload: UpdateTestTemplatePayload): Promise => apiFetch(`/test-templates/${templateId}`, { method: 'PUT', body: JSON.stringify(payload) }, false), deleteTestTemplate: (templateId: string): Promise => apiFetch(`/test-templates/${templateId}`, { method: 'DELETE' }, false), createTemplateQuestion: (templateId: string, payload: CreateQuestionPayload): Promise => apiFetch(`/test-templates/${templateId}/questions`, { method: 'POST', body: JSON.stringify(payload) }, false), deleteTemplateQuestion: (templateId: string, questionId: string): Promise => apiFetch(`/test-templates/${templateId}/questions/${questionId}`, { method: 'DELETE' }, false), createTemplateSection: (templateId: string, payload: CreateSectionPayload): Promise => apiFetch(`/test-templates/${templateId}/sections`, { method: 'POST', body: JSON.stringify(payload) }, false), deleteTemplateSection: (templateId: string, sectionId: string): Promise => apiFetch(`/test-templates/${templateId}/sections/${sectionId}`, { method: 'DELETE' }, false), applyTemplateToLesson: (templateId: string, lessonId: string, gradingCategoryId?: string): Promise => apiFetch(`/test-templates/${templateId}/apply`, { method: 'POST', body: JSON.stringify({ lesson_id: lessonId, grading_category_id: gradingCategoryId }) }, false), generateQuestionsWithRAG: (courseId?: number, topic?: string, numQuestions?: number): Promise => apiFetch('/test-templates/generate-with-rag', { method: 'POST', body: JSON.stringify({ course_id: courseId, topic, num_questions: numQuestions }) }, false), // Admin - AI Usage Global getGlobalAiUsage: (startDate?: string, endDate?: string): Promise => apiFetch('/admin/ai-usage/global', { query: { start_date: startDate, end_date: endDate } }, false), }; // ==================== Question Bank ==================== export type QuestionBankType = 'multiple-choice' | 'true-false' | 'short-answer' | 'essay' | 'matching' | 'ordering' | 'fill-in-the-blanks' | 'audio-response' | 'hotspot' | 'code-lab'; export interface QuestionBank { id: string; organization_id: string; question_text: string; question_type: QuestionBankType; options?: any; correct_answer?: any; explanation?: string; audio_url?: string; audio_text?: string; audio_status?: 'pending' | 'generating' | 'ready' | 'failed'; audio_metadata?: any; media_url?: string; media_type?: string; points: number; difficulty?: 'easy' | 'medium' | 'hard'; tags?: string[]; skill_assessed?: 'reading' | 'listening' | 'speaking' | 'writing'; source?: 'manual' | 'ai-generated' | 'imported-mysql' | 'imported-csv'; source_metadata?: any; usage_count?: number; last_used_at?: string; is_active: boolean; is_archived: boolean; created_by?: string; created_at: string; updated_at: string; } export interface QuestionBankFilters { question_type?: QuestionBankType; difficulty?: string; tags?: string; source?: string; search?: string; has_audio?: boolean; } export interface CreateQuestionBankPayload { question_text: string; question_type: QuestionBankType; options?: any; correct_answer?: any; explanation?: string; points?: number; difficulty?: string; tags?: string[]; media_url?: string; media_type?: string; skill_assessed?: string; audio_url?: string; audio_text?: string; } export interface UpdateQuestionBankPayload { question_text?: string; question_type?: QuestionBankType; options?: any; correct_answer?: any; explanation?: string; points?: number; difficulty?: string; tags?: string[]; is_active?: boolean; is_archived?: boolean; skill_assessed?: string; audio_url?: string; audio_text?: string; } export const questionBankApi = { list: (filters?: QuestionBankFilters): Promise => apiFetch('/question-bank', { method: 'GET', query: filters as any }, false), get: (id: string): Promise => apiFetch(`/question-bank/${id}`, {}, false), create: (payload: CreateQuestionBankPayload): Promise => apiFetch('/question-bank', { method: 'POST', body: JSON.stringify(payload) }, false), update: (id: string, payload: UpdateQuestionBankPayload): Promise => apiFetch(`/question-bank/${id}`, { method: 'PUT', body: JSON.stringify(payload) }, false), delete: (id: string): Promise => apiFetch(`/question-bank/${id}`, { method: 'DELETE' }, false), importFromMySQL: (courseId?: number, questionIds?: number[], importAll?: boolean): Promise => apiFetch('/question-bank/import-mysql', { method: 'POST', body: JSON.stringify({ mysql_course_id: courseId, question_ids: questionIds, import_all: importAll }) }, false), getMySQLPlans: (): Promise => apiFetch('/question-bank/mysql-plans', {}, false), getMySQLCoursesByPlan: (planId: number): Promise => apiFetch(`/question-bank/mysql-courses?plan_id=${planId}`, {}, false), }; export interface MySqlPlan { idPlanDeEstudios: number; NombrePlan: string; } export interface MySqlCourse { idCursos: number; NombreCurso: string; NivelCurso?: number; idPlanDeEstudios: number; NombrePlan: string; Duracion?: number; // Duration in hours (40=regular, 80=intensive) } export const lmsApi = { getCohorts: (): Promise => apiFetch('/cohorts', {}, true), createCohort: (payload: CreateCohortPayload): Promise => apiFetch('/cohorts', { method: 'POST', body: JSON.stringify(payload) }, true), addMember: (cohortId: string, userId: string): Promise => apiFetch(`/cohorts/${cohortId}/members`, { method: 'POST', body: JSON.stringify({ user_id: userId }) }, true), removeMember: (cohortId: string, userId: string): Promise => apiFetch(`/cohorts/${cohortId}/members/${userId}`, { method: 'DELETE' }, true), getMembers: (id: string): Promise => apiFetch(`/cohorts/${id}/members`, {}, true), getCourseGrades: (id: string, cohortId?: string): Promise => { const query = cohortId ? `?cohort_id=${cohortId}` : ''; return apiFetch(`/courses/${id}/grades${query}`, {}, true); }, exportGradesUrl: (courseId: string): string => { // Since we are downloading via tag... return `${LMS_API_BASE_URL}/courses/${courseId}/export-grades`; }, bulkEnroll: (courseId: string, emails: string[]): Promise => apiFetch('/bulk-enroll', { method: 'POST', body: JSON.stringify({ course_id: courseId, emails }) }, true), // Peer Assessment submitAssignment: (courseId: string, lessonId: string, content: string): Promise => apiFetch(`/courses/${courseId}/lessons/${lessonId}/submit`, { method: 'POST', body: JSON.stringify({ content }) }, true), getPeerReviewAssignment: (courseId: string, lessonId: string): Promise => apiFetch(`/courses/${courseId}/lessons/${lessonId}/peer-review`, {}, true), submitPeerReview: (courseId: string, lessonId: string, submissionId: string, score: number, feedback: string): Promise => apiFetch(`/courses/${courseId}/lessons/${lessonId}/peer-review`, { method: 'POST', body: JSON.stringify({ submission_id: submissionId, score, feedback }) }, true), getMySubmissionFeedback: (courseId: string, lessonId: string): Promise => apiFetch(`/courses/${courseId}/lessons/${lessonId}/feedback`, {}, true), listLessonSubmissions: (courseId: string, lessonId: string): Promise => apiFetch(`/courses/${courseId}/lessons/${lessonId}/submissions`, {}, true), getSubmissionReviews: (submissionId: string): Promise => apiFetch(`/peer-reviews/submissions/${submissionId}/reviews`, {}, true), // Announcements listAnnouncements: (courseId: string): Promise => apiFetch(`/courses/${courseId}/announcements`, {}, true), createAnnouncement: (courseId: string, payload: CreateAnnouncementPayload): Promise => apiFetch(`/courses/${courseId}/announcements`, { method: 'POST', body: JSON.stringify(payload) }, true), updateAnnouncement: (announcementId: string, payload: UpdateAnnouncementPayload): Promise => apiFetch(`/announcements/${announcementId}`, { method: 'PUT', body: JSON.stringify(payload) }, true), deleteAnnouncement: (announcementId: string): Promise => apiFetch(`/announcements/${announcementId}`, { method: 'DELETE' }, true), async getDeepLinkingResponse(payload: { dl_token: string, items: LtiDeepLinkingContentItem[] }): Promise<{ jwt: string, return_url: string }> { return apiFetch('/lti/deep-linking/response', { method: 'POST', body: JSON.stringify(payload) }, true); }, getDropoutRisks: (courseId: string): Promise => apiFetch(`/courses/${courseId}/dropout-risks`, {}, true), // Live Learning getMeetings: (courseId: string): Promise => apiFetch(`/courses/${courseId}/meetings`, {}, true), createMeeting: (courseId: string, payload: Partial): Promise => apiFetch(`/courses/${courseId}/meetings`, { method: 'POST', body: JSON.stringify(payload) }, true), deleteMeeting: (courseId: string, meetingId: string): Promise => apiFetch(`/courses/${courseId}/meetings/${meetingId}`, { method: 'DELETE' }, true), // Portfolio & Badges getPublicProfile: (userId: string): Promise => apiFetch(`/profile/${userId}`, {}, true), getMyBadges: (): Promise => apiFetch(`/my/badges`, {}, true), // Audio Responses getAudioResponses: (filters?: AudioResponseFilters): Promise => { const query: Record = {}; 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 => apiFetch(`/audio-responses/${id}`, { method: 'GET' }, true), getAudioResponseAudio: (id: string): Promise => { 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 => apiFetch(`/courses/${courseId}/audio-responses/stats`, { method: 'GET' }, true), }; export interface Meeting { id: string; course_id: string; title: string; description?: string; provider: string; meeting_id: string; start_at: string; duration_minutes: number; join_url?: string; is_active: boolean; } export interface Badge { id: string; name: string; description: string; icon_url: string; criteria: any; created_at: string; } export interface PublicProfile { user_id: string; full_name: string; avatar_url?: string; bio?: string; badges: Badge[]; level: number; xp: number; completed_courses_count: number; } export interface LtiDeepLinkingContentItem { type: 'ltiResourceLink'; title?: string; text?: string; url?: string; icon?: { url: string; width?: number; height?: number }; thumbnail?: { url: string; width?: number; height?: number }; [key: string]: string | number | boolean | object | undefined | null; } export interface BackgroundTask { id: string; title: string; course_title?: string; task_type: 'lesson_transcription' | 'lesson_image' | 'course_image'; status: 'idle' | 'queued' | 'processing' | 'failed' | 'completed' | 'error'; progress: number; updated_at: string; } // ==================== Test Templates ==================== export type CourseLevel = 'beginner' | 'beginner_1' | 'beginner_2' | 'intermediate' | 'intermediate_1' | 'intermediate_2' | 'advanced' | 'advanced_1' | 'advanced_2'; export type CourseType = 'intensive' | 'regular'; export type TestType = 'CA' | 'MWT' | 'MOT' | 'FOT' | 'FWT'; export type QuestionType = 'multiple-choice' | 'true-false' | 'short-answer' | 'essay' | 'matching' | 'ordering'; export interface TestTemplate { id: string; organization_id: string; mysql_course_id?: number; // Reference to imported MySQL course name: string; description?: string; level?: CourseLevel; // Deprecated: use mysql_course_id instead course_type?: CourseType; // Deprecated: use mysql_course_id instead test_type: TestType; duration_minutes: number; passing_score: number; total_points: number; instructions?: string; template_data: any; tags?: string[]; is_active: boolean; usage_count: number; created_by: string; created_at: string; updated_at: string; } export interface TestTemplateSection { id: string; template_id: string; title: string; description?: string; section_order: number; points: number; instructions?: string; section_data?: any; created_at: string; } export interface TestTemplateQuestion { id: string; template_id: string; section_id?: string; question_order: number; question_type: QuestionType; question_text: string; options?: any; correct_answer?: any; explanation?: string; points: number; metadata?: any; created_at: string; } export interface TestTemplateWithQuestions { template: TestTemplate; sections: TestTemplateSection[]; questions: TestTemplateQuestion[]; } export interface CreateTestTemplatePayload { name: string; description?: string; mysql_course_id?: number; // Reference to imported MySQL course (preferred) level?: CourseLevel; // Fallback if mysql_course_id not provided course_type?: CourseType; // Fallback if mysql_course_id not provided test_type: TestType; duration_minutes: number; passing_score: number; total_points: number; instructions?: string; template_data: any; tags?: string[]; } export interface UpdateTestTemplatePayload { name?: string; description?: string; mysql_course_id?: number; level?: CourseLevel; course_type?: CourseType; test_type?: TestType; duration_minutes?: number; passing_score?: number; total_points?: number; instructions?: string; template_data?: any; tags?: string[]; is_active?: boolean; } export interface CreateQuestionPayload { section_id?: string; question_order: number; question_type: string; question_text: string; options?: any; correct_answer?: any; explanation?: string; points: number; metadata?: any; } export interface CreateSectionPayload { title: string; description?: string; section_order: number; points: number; instructions?: string; section_data?: any; } export interface TestTemplateFilters { level?: CourseLevel; course_type?: CourseType; 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 { const query: Record = {}; 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 { return apiFetch(`/audio-responses/${id}`, { method: 'GET' }, true); } export async function getAudioResponseAudio(id: string): Promise { 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 { return apiFetch(`/courses/${courseId}/audio-responses/stats`, { method: 'GET' }, true); }