This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
|
||||
export const generateStudentReply = async (studentComment: string, courseContext: string): Promise<string> => {
|
||||
try {
|
||||
const apiKey = process.env.API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.warn("EduClean: API Key is missing. AI features are disabled.");
|
||||
return "Peço desculpas, mas não consigo gerar uma resposta no momento. Por favor, contate o professor diretamente.";
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
|
||||
const prompt = `
|
||||
Você é um assistente de professor útil, encorajador e claro.
|
||||
Contexto: O aluno está fazendo uma pergunta sobre o curso: "${courseContext}".
|
||||
Pergunta do Aluno: "${studentComment}"
|
||||
|
||||
Tarefa: Escreva uma resposta concisa, didática e amigável (máximo de 2 frases) em Português do Brasil.
|
||||
Responda diretamente ao ponto do aluno.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-flash-preview',
|
||||
contents: prompt,
|
||||
});
|
||||
|
||||
return response.text || "";
|
||||
} catch (error) {
|
||||
console.error("Gemini API Error:", error);
|
||||
return "Desculpe, não consegui gerar uma resposta automática agora. Por favor, tente novamente.";
|
||||
}
|
||||
};
|
||||
69
services/api.ts
Normal file
69
services/api.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api/v1';
|
||||
const SESSION_KEY = 'compor_hub_session';
|
||||
|
||||
export type ApiRole = 'student' | 'instructor' | 'admin';
|
||||
|
||||
export interface ApiUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: ApiRole;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
token: string;
|
||||
user: ApiUser;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message: string, public status: number) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSession(): Session | null {
|
||||
const value = localStorage.getItem(SESSION_KEY);
|
||||
if (!value) return null;
|
||||
try {
|
||||
return JSON.parse(value) as Session;
|
||||
} catch {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSession(session: Session) {
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const session = getSession();
|
||||
const headers = new Headers(options.headers);
|
||||
if (options.body) headers.set('Content-Type', 'application/json');
|
||||
if (session) headers.set('Authorization', `Bearer ${session.token}`);
|
||||
|
||||
const response = await fetch(`${API_URL}${path}`, { ...options, headers });
|
||||
if (response.status === 204) return undefined as T;
|
||||
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new ApiError(body.error || 'The request could not be completed', response.status);
|
||||
}
|
||||
return body as T;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
async login(email: string, password: string) {
|
||||
return apiRequest<Session>('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) });
|
||||
},
|
||||
async register(name: string, email: string, password: string) {
|
||||
return apiRequest<Session>('/auth/register', { method: 'POST', body: JSON.stringify({ name, email, password }) });
|
||||
},
|
||||
async me() {
|
||||
return apiRequest<{ user: ApiUser }>('/auth/me');
|
||||
},
|
||||
};
|
||||
189
services/db.ts
189
services/db.ts
@@ -1,81 +1,166 @@
|
||||
import localforage from 'localforage';
|
||||
import { Course, Comment, Section } from '../types';
|
||||
import { INITIAL_COURSES, generateMockSections } from '../constants';
|
||||
import { Attachment, AttachmentType, Comment, Course, Lesson } from '../types';
|
||||
import { apiRequest } from './api';
|
||||
|
||||
const COURSES_KEY = 'compor_hub_courses_v1';
|
||||
const COMMENTS_KEY = 'compor_hub_comments_v1';
|
||||
const PROGRESS_KEY = 'compor_hub_progress_v1';
|
||||
|
||||
// Initialize DB with seed courses
|
||||
export const initDB = async (): Promise<void> => {
|
||||
const existingCourses = await localforage.getItem<Course[]>(COURSES_KEY);
|
||||
if (!existingCourses || existingCourses.length === 0) {
|
||||
await localforage.setItem(COURSES_KEY, INITIAL_COURSES);
|
||||
}
|
||||
export const initDB = async (): Promise<void> => undefined;
|
||||
|
||||
type ApiAsset = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: 'document' | 'spreadsheet' | 'archive' | 'image' | 'link';
|
||||
url: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type ApiCourse = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
coverImageUrl: string | null;
|
||||
instructor: { name: string };
|
||||
lessons: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
durationSeconds: number | null;
|
||||
accessLevel: 'public' | 'enrolled';
|
||||
media: Array<{ provider: string; playbackUrl: string | null; embedUrl: string | null }>;
|
||||
assets: ApiAsset[];
|
||||
}>;
|
||||
assets: ApiAsset[];
|
||||
};
|
||||
|
||||
const toAttachmentType = (kind: ApiAsset['kind']): AttachmentType => {
|
||||
if (kind === 'document') return 'doc';
|
||||
if (kind === 'archive') return 'zip';
|
||||
return kind;
|
||||
};
|
||||
|
||||
const toAttachment = (asset: ApiAsset): Attachment => ({
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
type: toAttachmentType(asset.kind),
|
||||
url: asset.url,
|
||||
description: asset.description,
|
||||
});
|
||||
|
||||
const toDuration = (seconds: number | null) => {
|
||||
if (!seconds) return '—';
|
||||
const minutes = Math.round(seconds / 60);
|
||||
return minutes >= 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${minutes}m`;
|
||||
};
|
||||
|
||||
const toCourse = (course: ApiCourse): Course => ({
|
||||
id: course.id,
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
category: course.category,
|
||||
thumbnail: course.coverImageUrl || 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80',
|
||||
instructor: course.instructor.name,
|
||||
duration: `${course.lessons.length} Aulas`,
|
||||
lessons: course.lessons.map((lesson): Lesson => ({
|
||||
id: lesson.id,
|
||||
title: lesson.title,
|
||||
description: lesson.description,
|
||||
duration: toDuration(lesson.durationSeconds),
|
||||
isFree: lesson.accessLevel === 'public',
|
||||
videoUrl: lesson.media[0]?.playbackUrl || lesson.media[0]?.embedUrl || '',
|
||||
mediaProvider: lesson.media[0]?.provider,
|
||||
attachments: lesson.assets.map(toAttachment),
|
||||
})),
|
||||
attachments: course.assets.map(toAttachment),
|
||||
});
|
||||
|
||||
const toSeconds = (duration: string) => {
|
||||
const hours = Number(duration.match(/(\d+)h/)?.[1] || 0);
|
||||
const minutes = Number(duration.match(/(\d+)m/)?.[1] || 0);
|
||||
const seconds = hours * 3600 + minutes * 60;
|
||||
return seconds || null;
|
||||
};
|
||||
|
||||
const toAssetKind = (type: AttachmentType): ApiAsset['kind'] => {
|
||||
if (type === 'doc' || type === 'pdf') return 'document';
|
||||
if (type === 'zip') return 'archive';
|
||||
if (type === 'video') return 'link';
|
||||
return type;
|
||||
};
|
||||
|
||||
const toAssetPayload = (attachment: Attachment) => ({
|
||||
name: attachment.name,
|
||||
kind: toAssetKind(attachment.type),
|
||||
url: attachment.url,
|
||||
description: attachment.description || '',
|
||||
accessLevel: 'enrolled' as const,
|
||||
});
|
||||
|
||||
const toCoursePayload = (course: Course) => ({
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
category: course.category,
|
||||
coverImageUrl: course.thumbnail && !course.thumbnail.startsWith('blob:') ? course.thumbnail : null,
|
||||
status: 'published' as const,
|
||||
assets: (course.attachments || []).filter((attachment) => attachment.url && attachment.url !== '#').map(toAssetPayload),
|
||||
lessons: course.lessons.map((lesson) => ({
|
||||
title: lesson.title,
|
||||
description: lesson.description || '',
|
||||
durationSeconds: toSeconds(lesson.duration),
|
||||
accessLevel: lesson.isFree ? 'public' as const : 'enrolled' as const,
|
||||
media: lesson.videoUrl && !lesson.videoUrl.startsWith('blob:')
|
||||
? [{ provider: lesson.mediaProvider || 'external', externalId: lesson.videoUrl, playbackUrl: lesson.videoUrl, status: 'ready' as const }]
|
||||
: [],
|
||||
assets: (lesson.attachments || []).filter((attachment) => attachment.url && attachment.url !== '#').map(toAssetPayload),
|
||||
})),
|
||||
});
|
||||
|
||||
export const getCourses = async (): Promise<Course[]> => {
|
||||
const courses = await localforage.getItem<Course[]>(COURSES_KEY);
|
||||
if (!courses || courses.length === 0) {
|
||||
await initDB();
|
||||
return INITIAL_COURSES;
|
||||
}
|
||||
return courses;
|
||||
const response = await apiRequest<{ data: ApiCourse[] }>('/courses');
|
||||
return response.data.map(toCourse);
|
||||
};
|
||||
|
||||
export const getManagedCourses = async (): Promise<Course[]> => {
|
||||
const response = await apiRequest<{ data: ApiCourse[] }>('/manage/courses');
|
||||
return response.data.map(toCourse);
|
||||
};
|
||||
|
||||
export const getCourseById = async (id: string): Promise<Course | null> => {
|
||||
const courses = await getCourses();
|
||||
return courses.find(c => c.id === id) || null;
|
||||
try {
|
||||
const response = await apiRequest<{ data: ApiCourse }>(`/courses/${id}`);
|
||||
return toCourse(response.data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveCourse = async (course: Course): Promise<void> => {
|
||||
const courses = await getCourses();
|
||||
const index = courses.findIndex(c => c.id === course.id);
|
||||
if (index >= 0) {
|
||||
courses[index] = course;
|
||||
const payload = toCoursePayload(course);
|
||||
const isExisting = /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(course.id);
|
||||
if (isExisting) {
|
||||
await apiRequest(`/manage/courses/${course.id}`, { method: 'PATCH', body: JSON.stringify(payload) });
|
||||
} else {
|
||||
courses.unshift(course);
|
||||
await apiRequest('/manage/courses', { method: 'POST', body: JSON.stringify(payload) });
|
||||
}
|
||||
await localforage.setItem(COURSES_KEY, courses);
|
||||
};
|
||||
|
||||
export const deleteCourse = async (id: string): Promise<void> => {
|
||||
const courses = await getCourses();
|
||||
const filtered = courses.filter(c => c.id !== id);
|
||||
await localforage.setItem(COURSES_KEY, filtered);
|
||||
await apiRequest(`/manage/courses/${id}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
// Comments and learner progress move to PostgreSQL in the next milestone.
|
||||
export const getComments = async (courseId: string): Promise<Comment[]> => {
|
||||
const allComments = (await localforage.getItem<Comment[]>(COMMENTS_KEY)) || [];
|
||||
return allComments.filter(c => c.courseId === courseId);
|
||||
return allComments.filter((comment) => comment.courseId === courseId);
|
||||
};
|
||||
|
||||
export const saveComment = async (comment: Comment): Promise<void> => {
|
||||
const allComments = (await localforage.getItem<Comment[]>(COMMENTS_KEY)) || [];
|
||||
const index = allComments.findIndex(c => c.id === comment.id);
|
||||
|
||||
if (index >= 0) {
|
||||
allComments[index] = comment;
|
||||
} else {
|
||||
allComments.unshift(comment);
|
||||
}
|
||||
const index = allComments.findIndex((item) => item.id === comment.id);
|
||||
if (index >= 0) allComments[index] = comment;
|
||||
else allComments.unshift(comment);
|
||||
await localforage.setItem(COMMENTS_KEY, allComments);
|
||||
};
|
||||
|
||||
export const saveProgress = async (courseId: string, progress: number): Promise<void> => {
|
||||
const courses = await getCourses();
|
||||
const course = courses.find(c => c.id === courseId);
|
||||
if (course) {
|
||||
course.progress = progress;
|
||||
await saveCourse(course);
|
||||
}
|
||||
};
|
||||
|
||||
export const incrementViews = async (courseId: string): Promise<void> => {
|
||||
const courses = await getCourses();
|
||||
const course = courses.find(c => c.id === courseId);
|
||||
if (course) {
|
||||
course.views = (course.views || 0) + 1;
|
||||
await saveCourse(course);
|
||||
}
|
||||
};
|
||||
export const saveProgress = async (_courseId: string, _progress: number): Promise<void> => undefined;
|
||||
export const incrementViews = async (_courseId: string): Promise<void> => undefined;
|
||||
|
||||
Reference in New Issue
Block a user