const getApiBaseUrl = (defaultPort: string, envVar?: string) => { if (typeof window !== 'undefined') { const hostname = window.location.hostname; const protocol = window.location.protocol; return `${protocol}//${hostname}:${defaultPort}`; } return envVar || `http://localhost:${defaultPort}`; }; export const getLmsApiUrl = () => getApiBaseUrl("3002", process.env.NEXT_PUBLIC_LMS_API_URL); export const getCmsApiUrl = () => getApiBaseUrl("3001", process.env.NEXT_PUBLIC_CMS_API_URL); export const getImageUrl = (path?: string) => { if (!path) return ''; if (path.startsWith('http')) return path; const cleanPath = path.startsWith('/uploads') ? path.replace('/uploads', '/assets') : path; const finalPath = cleanPath.startsWith('/') ? cleanPath : `/${cleanPath}`; return `${getCmsApiUrl()}${finalPath}`; }; export interface Organization { id: string; name: string; logo_url?: string; favicon_url?: string; platform_name?: string; primary_color?: string; secondary_color?: string; } export interface Recommendation { title: string; description: string; lesson_id: string | null; priority: "high" | "medium" | "low"; reason: string; } export interface RecommendationResponse { recommendations: Recommendation[]; } export interface AudioGradingResponse { score: number; found_keywords: string[]; feedback: string; } export interface Course { id: string; title: string; description?: string; instructor_id: string; passing_percentage: number; certificate_template?: string; pacing_mode: string; start_date?: string; end_date?: string; created_at: string; } 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' | 'code' | 'hotspot' | 'memory-match' | 'document' | 'audio-response' | 'video_marker'; title: string; content?: string; url?: string; media_type?: 'video' | 'audio'; config?: Record; quiz_data?: { questions: QuizQuestion[]; }; pairs?: { left: string; right: string }[]; items?: string[]; prompt?: string; correctAnswers?: string[]; instructions?: string; initialCode?: string; keywords?: string[]; timeLimit?: number; description?: string; imageUrl?: string; hotspots?: { id: string; x: number; y: number; radius: number; label: string; }[]; metadata?: any; } export interface Lesson { id: string; module_id: string; title: string; content_type: string; content_url?: string; summary?: string; transcription?: { en?: string; es?: string; cues?: { start: number; end: number; text: string }[]; } | null; metadata?: { blocks: Block[]; }; is_graded: boolean; grading_category_id: string | null; max_attempts: number | null; allow_retry: boolean; position: number; due_date?: string; important_date_type?: 'exam' | 'assignment' | 'milestone' | 'live-session'; created_at: string; } export interface GradingCategory { id: string; course_id: string; name: string; weight: number; drop_count: number; } export interface UserGrade { id: string; user_id: string; course_id: string; lesson_id: string; score: number; attempts_count: number; metadata: Record; created_at: string; } export interface User { id: string; email: string; full_name: string; role: string; organization_id: string; xp?: number; level?: number; avatar_url?: string; bio?: string; language?: string; } export interface Notification { id: string; title: string; message: string; notification_type: string; is_read: boolean; link_url?: string; created_at: string; } export interface AuthResponse { user: User; token: string; } export interface AuthPayload { email: string; password?: string; full_name?: string; organization_name?: string; } export interface Enrollment { id: string; user_id: string; course_id: string; enrolled_at: string; } export interface Module { id: string; course_id: string; title: string; position: number; lessons: Lesson[]; } // Discussion Forums Types export interface DiscussionThread { id: string; organization_id: string; course_id: string; lesson_id?: string; author_id: string; title: string; content: string; is_pinned: boolean; is_locked: boolean; view_count: number; created_at: string; updated_at: string; } export interface ThreadWithAuthor { id: string; organization_id: string; course_id: string; lesson_id?: string; author_id: string; title: string; content: string; is_pinned: boolean; is_locked: boolean; view_count: number; created_at: string; updated_at: string; author_name: string; author_avatar?: string; post_count: number; has_endorsed_answer: boolean; } export interface DiscussionPost { id: string; organization_id: string; thread_id: string; parent_post_id?: string; author_id: string; content: string; upvotes: number; is_endorsed: boolean; created_at: string; updated_at: string; } export interface PostWithAuthor { id: string; organization_id: string; thread_id: string; parent_post_id?: string; author_id: string; content: string; upvotes: number; is_endorsed: boolean; created_at: string; updated_at: string; author_name: string; author_avatar?: string; user_vote?: 'upvote' | 'downvote'; replies: PostWithAuthor[]; } export interface CreateThreadPayload { title: string; content: string; lesson_id?: string; } export interface CreatePostPayload { content: string; parent_post_id?: string; } export interface VotePayload { vote_type: 'upvote' | 'downvote'; } const getToken = () => typeof window !== 'undefined' ? localStorage.getItem('experience_token') : null; const apiFetch = async (url: string, options: RequestInit = {}, isCMS: boolean = false) => { const token = getToken(); const baseUrl = isCMS ? getCmsApiUrl() : getLmsApiUrl(); const headers = { 'Content-Type': 'application/json', ...options.headers, ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }; const response = await fetch(`${baseUrl}${url}`, { ...options, headers }); if (!response.ok) { const error = await response.json().catch(() => ({ message: response.statusText })); throw new Error(error.message || 'An error occurred'); } if (response.status === 204) return; return response.json(); }; export const lmsApi = { async getCatalog(orgId?: string, userId?: string): Promise { const params = new URLSearchParams(); if (orgId) params.append('organization_id', orgId); if (userId) params.append('user_id', userId); const query = params.toString() ? `?${params.toString()}` : ''; return apiFetch(`/catalog${query}`); }, async getCourseOutline(courseId: string): Promise<{ course: Course, modules: Module[], grading_categories: GradingCategory[], organization: Organization }> { return apiFetch(`/courses/${courseId}/outline`); }, async getLesson(id: string): Promise { return apiFetch(`/lessons/${id}`); }, async register(payload: AuthPayload): Promise { return apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }); }, async login(payload: AuthPayload): Promise { return apiFetch('/auth/login', { method: 'POST', body: JSON.stringify(payload) }); }, async getMe(): Promise { return apiFetch('/auth/me', {}, true); // isCMS = true }, initSSOLogin(orgId: string): void { window.location.href = `${getCmsApiUrl()}/auth/sso/login/${orgId}`; }, async enroll(courseId: string, userId: string): Promise { return apiFetch('/enroll', { method: 'POST', body: JSON.stringify({ course_id: courseId, user_id: userId }) }); }, async getEnrollments(userId: string): Promise { return apiFetch(`/enrollments/${userId}`); }, async submitScore(userId: string, course_id: string, lessonId: string, score: number, metadata: Record = {}): Promise { return apiFetch('/grades', { method: 'POST', body: JSON.stringify({ user_id: userId, course_id, lesson_id: lessonId, score, metadata }) }); }, async getUserGrades(userId: string, courseId: string): Promise { return apiFetch(`/users/${userId}/courses/${courseId}/grades`); }, async getGamification(userId: string): Promise<{ points: number, level: number, badges: { id: string, name: string, description: string, earned_at: string }[] }> { return apiFetch(`/users/${userId}/gamification`); }, async getLeaderboard(): Promise { return apiFetch('/analytics/leaderboard'); }, async getBranding(orgId: string): Promise { return apiFetch(`/organizations/${orgId}/branding`, {}, true); }, async updateUser(userId: string, payload: { full_name?: string, avatar_url?: string, bio?: string, language?: string }): Promise { return apiFetch(`/users/${userId}`, { method: 'POST', body: JSON.stringify(payload) }); }, async uploadAsset(file: File): Promise<{ id: string, filename: string, url: string }> { const formData = new FormData(); formData.append('file', file); const token = getToken(); return fetch(`${getCmsApiUrl()}/assets/upload`, { method: 'POST', headers: { ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, body: formData }).then(res => res.json()); }, async recordInteraction(lessonId: string, payload: { video_timestamp?: number, event_type: string, metadata?: any }): Promise { return apiFetch(`/lessons/${lessonId}/interactions`, { method: 'POST', body: JSON.stringify(payload) }); }, async getHeatmap(lessonId: string): Promise<{ second: number, count: number }[]> { return apiFetch(`/lessons/${lessonId}/heatmap`); }, async getNotifications(): Promise { return apiFetch('/notifications'); }, async markNotificationAsRead(id: string): Promise { return apiFetch(`/notifications/${id}/read`, { method: 'POST' }); }, async getRecommendations(courseId: string): Promise { return apiFetch(`/courses/${courseId}/recommendations`); }, async evaluateAudio(transcript: string, prompt: string, keywords: string[]): Promise { return apiFetch('/audio/evaluate', { method: 'POST', body: JSON.stringify({ transcript, prompt, keywords }) }); }, async evaluateAudioFile(file: Blob, prompt: string, keywords: string[]): Promise { const formData = new FormData(); formData.append('file', file, 'recorded_audio.webm'); formData.append('prompt', prompt); formData.append('keywords', JSON.stringify(keywords)); const token = getToken(); return fetch(`${getLmsApiUrl()}/audio/evaluate-file`, { method: 'POST', headers: { ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }, body: formData }).then(async res => { if (!res.ok) { const err = await res.json().catch(() => ({ message: 'Audio evaluation failed' })); throw new Error(err.message || 'Audio evaluation failed'); } return res.json(); }); }, async chatWithTutor(lessonId: string, message: string, sessionId?: string): Promise<{ response: string, session_id: string }> { return apiFetch(`/lessons/${lessonId}/chat`, { method: 'POST', body: JSON.stringify({ message, session_id: sessionId }) }); }, async getLessonFeedback(lessonId: string): Promise<{ response: string, session_id: string }> { return apiFetch(`/lessons/${lessonId}/feedback`); }, // Discussion Forums API async getDiscussions(courseId: string, filter?: string, lessonId?: string, page?: number): Promise { const params = new URLSearchParams(); if (filter) params.append('filter', filter); if (lessonId) params.append('lesson_id', lessonId); if (page) params.append('page', page.toString()); const query = params.toString() ? `?${params.toString()}` : ''; return apiFetch(`/courses/${courseId}/discussions${query}`); }, async createThread(courseId: string, payload: CreateThreadPayload): Promise { return apiFetch(`/courses/${courseId}/discussions`, { method: 'POST', body: JSON.stringify(payload) }); }, async getThreadDetail(threadId: string): Promise<{ thread: ThreadWithAuthor, posts: PostWithAuthor[] }> { return apiFetch(`/discussions/${threadId}`); }, async createPost(threadId: string, payload: CreatePostPayload): Promise { return apiFetch(`/discussions/${threadId}/posts`, { method: 'POST', body: JSON.stringify(payload) }); }, async votePost(postId: string, voteType: 'upvote' | 'downvote'): Promise { return apiFetch(`/posts/${postId}/vote`, { method: 'POST', body: JSON.stringify({ vote_type: voteType }) }); }, async endorsePost(postId: string): Promise { return apiFetch(`/posts/${postId}/endorse`, { method: 'POST' }); }, async pinThread(threadId: string): Promise { return apiFetch(`/discussions/${threadId}/pin`, { method: 'POST' }); }, async lockThread(threadId: string): Promise { return apiFetch(`/discussions/${threadId}/lock`, { method: 'POST' }); }, async subscribeThread(threadId: string): Promise { return apiFetch(`/discussions/${threadId}/subscribe`, { method: 'POST' }); }, async unsubscribeThread(threadId: string): Promise { return apiFetch(`/discussions/${threadId}/unsubscribe`, { method: 'POST' }); } };