e4866c6dee
- 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>
1517 lines
53 KiB
TypeScript
1517 lines
53 KiB
TypeScript
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, string | number | boolean | undefined | null>;
|
|
}
|
|
|
|
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<string, string> = {
|
|
...(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<Organization> => apiFetch('/organization'),
|
|
updateOrganizationBranding: (payload: BrandingPayload): Promise<Organization> => 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<OrganizationSSOConfig> => apiFetch('/organization/sso'),
|
|
updateSSOConfig: (payload: Partial<OrganizationSSOConfig>): Promise<void> => apiFetch('/organization/sso', { method: 'PUT', body: JSON.stringify(payload) }),
|
|
|
|
// Auth
|
|
register: (payload: AuthPayload): Promise<AuthResponse> => apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }),
|
|
login: (payload: AuthPayload): Promise<AuthResponse> => apiFetch('/auth/login', { method: 'POST', body: JSON.stringify(payload) }),
|
|
getMe: (): Promise<User> => apiFetch('/auth/me'),
|
|
|
|
// Branding (Public)
|
|
getBranding: (): Promise<BrandingResponse> => apiFetch('/branding'),
|
|
|
|
// Courses
|
|
getCourses: (): Promise<Course[]> => apiFetch('/courses'),
|
|
createCourse: (title: string, organizationId?: string): Promise<Course> => apiFetch('/courses', { method: 'POST', body: JSON.stringify({ title, organization_id: organizationId }) }),
|
|
getCourse: (id: string): Promise<Course> => apiFetch(`/courses/${id}`),
|
|
getCourseWithFullOutline: (id: string): Promise<Course> => apiFetch(`/courses/${id}/outline`),
|
|
updateCourse: (id: string, payload: Partial<Course>): Promise<Course> => apiFetch(`/courses/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
publishCourse: (id: string, targetOrganizationId?: string): Promise<void> => 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<CourseInstructor[]> => apiFetch(`/courses/${courseId}/team`),
|
|
addTeamMember: (courseId: string, email: string, role: string): Promise<CourseInstructor> => apiFetch(`/courses/${courseId}/team`, { method: 'POST', body: JSON.stringify({ email, role }) }),
|
|
removeTeamMember: (courseId: string, userId: string): Promise<void> => apiFetch(`/courses/${courseId}/team/${userId}`, { method: 'DELETE' }),
|
|
getUsers: (): Promise<User[]> => apiFetch('/users'),
|
|
|
|
// Modules & Lessons
|
|
createModule: (course_id: string, title: string, position: number): Promise<Module> => apiFetch('/modules', { method: 'POST', body: JSON.stringify({ course_id, title, position }) }),
|
|
updateModule: (id: string, payload: Partial<Module>): Promise<Module> => apiFetch(`/modules/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
createLesson: (module_id: string, title: string, content_type: string, position: number): Promise<Lesson> => apiFetch('/lessons', { method: 'POST', body: JSON.stringify({ module_id, title, content_type, position }) }),
|
|
getLesson: (id: string): Promise<Lesson> => apiFetch(`/lessons/${id}`),
|
|
updateLesson: (id: string, payload: Partial<Lesson>): Promise<Lesson> => apiFetch(`/lessons/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
summarizeLesson: (id: string): Promise<Lesson> => apiFetch(`/lessons/${id}/summarize`, { method: 'POST' }),
|
|
async generateQuiz(lessonId: string, payload: { prompt_hint?: string, quiz_type?: string }): Promise<any> {
|
|
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<void> => apiFetch(`/modules/${id}`, { method: 'DELETE' }),
|
|
deleteLesson: (id: string): Promise<void> => apiFetch(`/lessons/${id}`, { method: 'DELETE' }),
|
|
reorderModules: (payload: { items: { id: string, position: number }[] }): Promise<void> => apiFetch('/modules/reorder', { method: 'POST', body: JSON.stringify(payload) }),
|
|
reorderLessons: (payload: { items: { id: string, position: number }[] }): Promise<void> => apiFetch('/lessons/reorder', { method: 'POST', body: JSON.stringify(payload) }),
|
|
|
|
// Grading
|
|
getGradingCategories: (courseId: string): Promise<GradingCategory[]> => apiFetch(`/courses/${courseId}/grading`),
|
|
createGradingCategory: (course_id: string, name: string, weight: number): Promise<GradingCategory> => apiFetch('/grading', { method: 'POST', body: JSON.stringify({ course_id, name, weight, drop_count: 0 }) }),
|
|
deleteGradingCategory: (id: string): Promise<void> => apiFetch(`/grading/${id}`, { method: 'DELETE' }),
|
|
|
|
// Admin & Analytics
|
|
getAuditLogs: (): Promise<AuditLog[]> => apiFetch('/audit-logs'),
|
|
getCourseAnalytics: (id: string, cohortId?: string): Promise<CourseAnalytics> => {
|
|
const query = cohortId ? `?cohort_id=${cohortId}` : '';
|
|
return apiFetch(`/courses/${id}/analytics${query}`, {}, true);
|
|
},
|
|
getAdvancedAnalytics: (id: string, cohortId?: string): Promise<AdvancedAnalytics> => {
|
|
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<Record<string, unknown>> => apiFetch(`/courses/${id}/export`),
|
|
importCourse: (data: Record<string, unknown>): Promise<Course> => apiFetch(`/courses/import`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}),
|
|
|
|
deleteCourse: (id: string): Promise<void> => apiFetch(`/courses/${id}`, { method: 'DELETE' }),
|
|
|
|
async generateCourse(prompt: string, targetOrgId?: string): Promise<Course> {
|
|
return apiFetch(`/courses/generate`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ prompt, target_organization_id: targetOrgId })
|
|
});
|
|
},
|
|
|
|
// Users
|
|
getAllUsers: (): Promise<User[]> => apiFetch('/users'),
|
|
createUser: (data: UserCreatePayload): Promise<User> => 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<void> => apiFetch(`/users/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
|
|
// Webhooks
|
|
getWebhooks: (): Promise<Webhook[]> => apiFetch('/webhooks'),
|
|
createWebhook: (payload: CreateWebhookPayload): Promise<Webhook> => apiFetch('/webhooks', { method: 'POST', body: JSON.stringify(payload) }),
|
|
deleteWebhook: (id: string): Promise<void> => apiFetch(`/webhooks/${id}`, { method: 'DELETE' }),
|
|
|
|
// Assets
|
|
getAssets: (filters?: AssetFilters): Promise<Asset[]> => {
|
|
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<Asset[]> => apiFetch(`/api/assets?course_id=${courseId}`),
|
|
deleteAsset: (id: string): Promise<void> => apiFetch(`/api/assets/${id}`, { method: 'DELETE' }),
|
|
uploadAsset: (file: File, onProgress?: (pct: number) => void, courseId?: string): Promise<UploadResponse> => {
|
|
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<BackgroundTask[]> => apiFetch('/tasks'),
|
|
retryTask: (id: string): Promise<void> => apiFetch(`/tasks/${id}/retry`, { method: 'POST' }),
|
|
cancelTask: (id: string): Promise<void> => apiFetch(`/tasks/${id}`, { method: 'DELETE' }),
|
|
|
|
// Content Libraries
|
|
createLibraryBlock: (payload: CreateLibraryBlockPayload): Promise<LibraryBlock> =>
|
|
apiFetch('/library/blocks', { method: 'POST', body: JSON.stringify(payload) }),
|
|
listLibraryBlocks: (filters?: LibraryBlockFilters): Promise<LibraryBlock[]> => {
|
|
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<LibraryBlock> =>
|
|
apiFetch(`/library/blocks/${id}`),
|
|
updateLibraryBlock: (id: string, payload: UpdateLibraryBlockPayload): Promise<LibraryBlock> =>
|
|
apiFetch(`/library/blocks/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
deleteLibraryBlock: (id: string): Promise<void> =>
|
|
apiFetch(`/library/blocks/${id}`, { method: 'DELETE' }),
|
|
incrementBlockUsage: (id: string): Promise<void> =>
|
|
apiFetch(`/library/blocks/${id}/increment-usage`, { method: 'POST' }),
|
|
|
|
// Advanced Grading / Rubrics
|
|
createRubric: (courseId: string, payload: CreateRubricPayload): Promise<Rubric> =>
|
|
apiFetch(`/courses/${courseId}/rubrics`, { method: 'POST', body: JSON.stringify(payload) }),
|
|
listCourseRubrics: (courseId: string): Promise<Rubric[]> =>
|
|
apiFetch(`/courses/${courseId}/rubrics`),
|
|
getRubricWithDetails: (rubricId: string): Promise<RubricWithDetails> =>
|
|
apiFetch(`/rubrics/${rubricId}`),
|
|
updateRubric: (rubricId: string, payload: UpdateRubricPayload): Promise<Rubric> =>
|
|
apiFetch(`/rubrics/${rubricId}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
deleteRubric: (rubricId: string): Promise<void> =>
|
|
apiFetch(`/rubrics/${rubricId}`, { method: 'DELETE' }),
|
|
|
|
// Rubric Criteria
|
|
createCriterion: (rubricId: string, payload: CreateCriterionPayload): Promise<RubricCriterion> =>
|
|
apiFetch(`/rubrics/${rubricId}/criteria`, { method: 'POST', body: JSON.stringify(payload) }),
|
|
updateCriterion: (criterionId: string, payload: UpdateCriterionPayload): Promise<RubricCriterion> =>
|
|
apiFetch(`/criteria/${criterionId}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
deleteCriterion: (criterionId: string): Promise<void> =>
|
|
apiFetch(`/criteria/${criterionId}`, { method: 'DELETE' }),
|
|
|
|
// Rubric Levels
|
|
createLevel: (criterionId: string, payload: CreateLevelPayload): Promise<RubricLevel> =>
|
|
apiFetch(`/criteria/${criterionId}/levels`, { method: 'POST', body: JSON.stringify(payload) }),
|
|
updateLevel: (levelId: string, payload: UpdateLevelPayload): Promise<RubricLevel> =>
|
|
apiFetch(`/levels/${levelId}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
|
deleteLevel: (levelId: string): Promise<void> =>
|
|
apiFetch(`/levels/${levelId}`, { method: 'DELETE' }),
|
|
|
|
// Lesson-Rubric Association
|
|
assignRubricToLesson: (lessonId: string, rubricId: string): Promise<void> =>
|
|
apiFetch(`/lessons/${lessonId}/rubrics/${rubricId}`, { method: 'POST' }),
|
|
unassignRubricFromLesson: (lessonId: string, rubricId: string): Promise<void> =>
|
|
apiFetch(`/lessons/${lessonId}/rubrics/${rubricId}`, { method: 'DELETE' }),
|
|
getLessonRubrics: (lessonId: string): Promise<Rubric[]> =>
|
|
apiFetch(`/lessons/${lessonId}/rubrics`),
|
|
|
|
// Learning Sequences (Dependencies)
|
|
listLessonDependencies: (lessonId: string): Promise<LessonDependency[]> =>
|
|
apiFetch(`/lessons/${lessonId}/dependencies`),
|
|
assignDependency: (lessonId: string, payload: AssignDependencyPayload): Promise<LessonDependency> =>
|
|
apiFetch(`/lessons/${lessonId}/dependencies`, { method: 'POST', body: JSON.stringify(payload) }),
|
|
removeDependency: (lessonId: string, prerequisiteId: string): Promise<void> =>
|
|
apiFetch(`/lessons/${lessonId}/dependencies/${prerequisiteId}`, { method: 'DELETE' }),
|
|
|
|
// Test Templates
|
|
listTestTemplates: (filters?: TestTemplateFilters): Promise<TestTemplate[]> =>
|
|
apiFetch('/test-templates', { method: 'GET', query: filters as any }, false),
|
|
getTestTemplate: (templateId: string): Promise<TestTemplateWithQuestions> =>
|
|
apiFetch(`/test-templates/${templateId}`, {}, false),
|
|
createTestTemplate: (payload: CreateTestTemplatePayload): Promise<TestTemplate> =>
|
|
apiFetch('/test-templates', { method: 'POST', body: JSON.stringify(payload) }, false),
|
|
updateTestTemplate: (templateId: string, payload: UpdateTestTemplatePayload): Promise<TestTemplate> =>
|
|
apiFetch(`/test-templates/${templateId}`, { method: 'PUT', body: JSON.stringify(payload) }, false),
|
|
deleteTestTemplate: (templateId: string): Promise<void> =>
|
|
apiFetch(`/test-templates/${templateId}`, { method: 'DELETE' }, false),
|
|
createTemplateQuestion: (templateId: string, payload: CreateQuestionPayload): Promise<TestTemplateQuestion> =>
|
|
apiFetch(`/test-templates/${templateId}/questions`, { method: 'POST', body: JSON.stringify(payload) }, false),
|
|
deleteTemplateQuestion: (templateId: string, questionId: string): Promise<void> =>
|
|
apiFetch(`/test-templates/${templateId}/questions/${questionId}`, { method: 'DELETE' }, false),
|
|
createTemplateSection: (templateId: string, payload: CreateSectionPayload): Promise<TestTemplateSection> =>
|
|
apiFetch(`/test-templates/${templateId}/sections`, { method: 'POST', body: JSON.stringify(payload) }, false),
|
|
deleteTemplateSection: (templateId: string, sectionId: string): Promise<void> =>
|
|
apiFetch(`/test-templates/${templateId}/sections/${sectionId}`, { method: 'DELETE' }, false),
|
|
applyTemplateToLesson: (templateId: string, lessonId: string, gradingCategoryId?: string): Promise<void> =>
|
|
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<TestTemplateQuestion[]> =>
|
|
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<GlobalAiUsageResponse> =>
|
|
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<QuestionBank[]> =>
|
|
apiFetch('/question-bank', { method: 'GET', query: filters as any }, false),
|
|
get: (id: string): Promise<QuestionBank> =>
|
|
apiFetch(`/question-bank/${id}`, {}, false),
|
|
create: (payload: CreateQuestionBankPayload): Promise<QuestionBank> =>
|
|
apiFetch('/question-bank', { method: 'POST', body: JSON.stringify(payload) }, false),
|
|
update: (id: string, payload: UpdateQuestionBankPayload): Promise<QuestionBank> =>
|
|
apiFetch(`/question-bank/${id}`, { method: 'PUT', body: JSON.stringify(payload) }, false),
|
|
delete: (id: string): Promise<void> =>
|
|
apiFetch(`/question-bank/${id}`, { method: 'DELETE' }, false),
|
|
importFromMySQL: (courseId?: number, questionIds?: number[], importAll?: boolean): Promise<QuestionBank[]> =>
|
|
apiFetch('/question-bank/import-mysql', { method: 'POST', body: JSON.stringify({ mysql_course_id: courseId, question_ids: questionIds, import_all: importAll }) }, false),
|
|
getMySQLPlans: (): Promise<MySqlPlan[]> =>
|
|
apiFetch('/question-bank/mysql-plans', {}, false),
|
|
getMySQLCoursesByPlan: (planId: number): Promise<MySqlCourse[]> =>
|
|
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<Cohort[]> => apiFetch('/cohorts', {}, true),
|
|
createCohort: (payload: CreateCohortPayload): Promise<Cohort> => apiFetch('/cohorts', { method: 'POST', body: JSON.stringify(payload) }, true),
|
|
addMember: (cohortId: string, userId: string): Promise<UserCohort> => apiFetch(`/cohorts/${cohortId}/members`, { method: 'POST', body: JSON.stringify({ user_id: userId }) }, true),
|
|
removeMember: (cohortId: string, userId: string): Promise<void> => apiFetch(`/cohorts/${cohortId}/members/${userId}`, { method: 'DELETE' }, true),
|
|
getMembers: (id: string): Promise<string[]> => apiFetch(`/cohorts/${id}/members`, {}, true),
|
|
getCourseGrades: (id: string, cohortId?: string): Promise<StudentGradeReport[]> => {
|
|
const query = cohortId ? `?cohort_id=${cohortId}` : '';
|
|
return apiFetch(`/courses/${id}/grades${query}`, {}, true);
|
|
},
|
|
exportGradesUrl: (courseId: string): string => {
|
|
// Since we are downloading via <a> tag...
|
|
return `${LMS_API_BASE_URL}/courses/${courseId}/export-grades`;
|
|
},
|
|
bulkEnroll: (courseId: string, emails: string[]): Promise<BulkEnrollResponse> =>
|
|
apiFetch('/bulk-enroll', { method: 'POST', body: JSON.stringify({ course_id: courseId, emails }) }, true),
|
|
// Peer Assessment
|
|
submitAssignment: (courseId: string, lessonId: string, content: string): Promise<CourseSubmission> =>
|
|
apiFetch(`/courses/${courseId}/lessons/${lessonId}/submit`, { method: 'POST', body: JSON.stringify({ content }) }, true),
|
|
getPeerReviewAssignment: (courseId: string, lessonId: string): Promise<CourseSubmission | null> =>
|
|
apiFetch(`/courses/${courseId}/lessons/${lessonId}/peer-review`, {}, true),
|
|
submitPeerReview: (courseId: string, lessonId: string, submissionId: string, score: number, feedback: string): Promise<PeerReview> =>
|
|
apiFetch(`/courses/${courseId}/lessons/${lessonId}/peer-review`, { method: 'POST', body: JSON.stringify({ submission_id: submissionId, score, feedback }) }, true),
|
|
getMySubmissionFeedback: (courseId: string, lessonId: string): Promise<PeerReview[]> =>
|
|
apiFetch(`/courses/${courseId}/lessons/${lessonId}/feedback`, {}, true),
|
|
listLessonSubmissions: (courseId: string, lessonId: string): Promise<SubmissionWithReviews[]> =>
|
|
apiFetch(`/courses/${courseId}/lessons/${lessonId}/submissions`, {}, true),
|
|
getSubmissionReviews: (submissionId: string): Promise<PeerReview[]> =>
|
|
apiFetch(`/peer-reviews/submissions/${submissionId}/reviews`, {}, true),
|
|
|
|
// Announcements
|
|
listAnnouncements: (courseId: string): Promise<AnnouncementWithAuthor[]> =>
|
|
apiFetch(`/courses/${courseId}/announcements`, {}, true),
|
|
createAnnouncement: (courseId: string, payload: CreateAnnouncementPayload): Promise<CourseAnnouncement> =>
|
|
apiFetch(`/courses/${courseId}/announcements`, { method: 'POST', body: JSON.stringify(payload) }, true),
|
|
updateAnnouncement: (announcementId: string, payload: UpdateAnnouncementPayload): Promise<CourseAnnouncement> =>
|
|
apiFetch(`/announcements/${announcementId}`, { method: 'PUT', body: JSON.stringify(payload) }, true),
|
|
deleteAnnouncement: (announcementId: string): Promise<void> =>
|
|
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<DropoutRisk[]> =>
|
|
apiFetch(`/courses/${courseId}/dropout-risks`, {}, true),
|
|
|
|
// Live Learning
|
|
getMeetings: (courseId: string): Promise<Meeting[]> =>
|
|
apiFetch(`/courses/${courseId}/meetings`, {}, true),
|
|
|
|
createMeeting: (courseId: string, payload: Partial<Meeting>): Promise<Meeting> =>
|
|
apiFetch(`/courses/${courseId}/meetings`, { method: 'POST', body: JSON.stringify(payload) }, true),
|
|
|
|
deleteMeeting: (courseId: string, meetingId: string): Promise<void> =>
|
|
apiFetch(`/courses/${courseId}/meetings/${meetingId}`, { method: 'DELETE' }, true),
|
|
|
|
// Portfolio & Badges
|
|
getPublicProfile: (userId: string): Promise<PublicProfile> =>
|
|
apiFetch(`/profile/${userId}`, {}, true),
|
|
|
|
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 {
|
|
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<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);
|
|
} |