examforge / src /services /DataService.ts
Benjahmin's picture
feat(offline): implement offline resilience for exams
8a790fb
Raw
History Blame Contribute Delete
12 kB
import { UserProfile, Exam, Question, Attempt, StudentGroup } from '../types';
export class DataService {
private static authFetch(url: string, init: RequestInit = {}): Promise<Response> {
const token = localStorage.getItem('local_user_token');
const headers = {
...(init.headers || {}),
} as Record<string, string>;
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return fetch(url, {
...init,
headers
});
}
// --- User Operations ---
static async getProfile(): Promise<UserProfile | null> {
try {
const response = await this.authFetch('/api/auth/me');
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
static async getAllStudents(): Promise<UserProfile[]> {
try {
const response = await this.authFetch('/api/students');
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
// --- Question Bank Operations ---
static async createQuestion(question: Partial<Question>): Promise<Question> {
const response = await this.authFetch('/api/questions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(question)
});
if (!response.ok) {
throw new Error("Failed to create question");
}
return response.json();
}
// Orchestrator: Launches are asynchronous background document parsing job
static async startAiCompilationJob(text: string): Promise<{ jobId: string }> {
const response = await this.authFetch('/api/ai/jobs/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || "Failed to start AI compilation workspace");
}
return response.json();
}
// Orchestrator: Polls the current state of an outstanding compilation job
static async getAiJobStatus(jobId: string): Promise<any> {
const response = await this.authFetch(`/api/ai/jobs/status/${jobId}`, {
method: 'GET'
});
if (!response.ok) throw new Error("Failed to consult background job telemetry");
return response.json();
}
// Legacy Parser Wrapper: Calls background parsing job and polls to completion, preserving existing workflows safely
static async parseDocument(text: string, model?: string): Promise<{ questions: Question[], auditReport?: any, modelUsed?: string }> {
const { jobId } = await this.startAiCompilationJob(text);
while (true) {
await new Promise(resolve => setTimeout(resolve, 1000));
const job = await this.getAiJobStatus(jobId);
if (job.status === 'completed') {
return {
questions: job.result.questions,
auditReport: job.result.auditReport,
modelUsed: 'meta/llama-3.1-8b-instruct + meta/llama-3.1-70b-instruct'
};
}
if (job.status === 'failed') {
throw new Error(job.error || "Background parsing pipeline returned a fatal error");
}
}
}
// Deep Validation Audit: Verifies exam questions, duplicates, option errors on the fly
static async validateQuestions(questions: Question[]): Promise<{ examQualityScore: number; warnings: any[]; duplicates: any[] }> {
const response = await this.authFetch('/api/ai/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ questions })
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || "Academic validation report generation failed");
}
return response.json();
}
// Academic Analyzer: Translates scoring metrics, pass-rates, and category metrics to professional reviews
static async explainAnalytics(metrics: any): Promise<{ summary: string; strengths: string[]; weaknesses: string[]; recommendations: string[] }> {
const response = await this.authFetch('/api/ai/analytics-explain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metrics })
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || "Analytics explain compiler failed");
}
return response.json();
}
// Contextual Assistant: Real-time queries regarding analytics, warnings, or syllabus alignments
static async askAiAssistant(prompt: string, context?: any): Promise<{ response: string; modelUsed: string }> {
const response = await this.authFetch('/api/ai/assistant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, context })
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || "Academic Assistant is currently unresponsive");
}
return response.json();
}
static async getTeacherQuestions(): Promise<Question[]> {
try {
const response = await this.authFetch('/api/questions/list');
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
static async deleteQuestion(id: string): Promise<void> {
const response = await this.authFetch(`/api/questions/delete/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error("Failed to delete question");
}
}
// --- Group Operations ---
static async createGroup(group: Partial<StudentGroup>): Promise<StudentGroup> {
const response = await this.authFetch('/api/groups/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(group)
});
if (!response.ok) {
throw new Error("Failed to create group");
}
return response.json();
}
static async deleteGroup(id: string): Promise<void> {
const response = await this.authFetch(`/api/groups/delete/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error("Failed to delete group");
}
}
static async getTeacherGroups(): Promise<StudentGroup[]> {
try {
const response = await this.authFetch('/api/groups/list');
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
static async getTeacherStats() {
try {
const response = await this.authFetch('/api/teacher/stats');
if (!response.ok) throw new Error("Failed to consult teacher dashboard stats");
return response.json();
} catch (e) {
console.error(e);
return { totalExams: 0, totalQuestions: 0, totalSubmissions: 0, avgScore: 0 };
}
}
static async getStudentGroups(): Promise<StudentGroup[]> {
try {
const response = await this.authFetch('/api/student/groups');
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
static async getAttempt(id: string): Promise<Attempt | null> {
try {
const response = await this.authFetch(`/api/attempts/get/${id}`);
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
static async getQuestionsByIds(ids: string[]): Promise<Question[]> {
if (ids.length === 0) return [];
try {
const response = await this.authFetch('/api/questions/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
});
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
// --- Exam Operations ---
static async createExam(exam: Partial<Exam>): Promise<string> {
const response = await this.authFetch('/api/exams/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(exam)
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || "Failed to create exam");
}
const data = await response.json();
return data.id;
}
static async getExam(id: string): Promise<Exam | null> {
try {
const response = await this.authFetch(`/api/exams/get/${id}`);
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
static async getTeacherExams(): Promise<Exam[]> {
try {
const response = await this.authFetch('/api/exams/list');
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
static async updateExam(id: string, data: Partial<Exam>): Promise<void> {
const response = await this.authFetch(`/api/exams/update/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error("Failed to update exam");
}
}
static async getExamAttempts(examId: string): Promise<Attempt[]> {
try {
const response = await this.authFetch(`/api/exams/attempts/${examId}`);
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
// --- Draft Synchronization Cache Operations ---
static async syncDraft(draftData: any): Promise<any> {
try {
const response = await this.authFetch('/api/drafts/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(draftData)
});
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
static async getDraft(): Promise<any | null> {
try {
const response = await this.authFetch('/api/drafts/get');
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
static async clearDraft(): Promise<void> {
try {
await this.authFetch('/api/drafts/clear', { method: 'POST' });
} catch (e) {
console.warn("Clear draft failed:", e);
}
}
// --- Attempt Operations ---
static async startAttempt(examId: string, student: UserProfile): Promise<string> {
const response = await this.authFetch('/api/attempts/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ examId, student })
});
if (!response.ok) {
throw new Error("Failed to start exam attempt");
}
const data = await response.json();
return data.attemptId;
}
static async updateAttemptProgress(id: string, answers: any, timeSpent: number): Promise<void> {
try {
await this.authFetch(`/api/attempts/progress/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers, timeSpent })
});
} catch (e) {
console.warn("Progress sync failed", e);
}
}
static async submitAttempt(
attemptId: string,
examId: string,
answers: any,
timeSpent: number,
offlineDuration: number = 0,
submissionTimestamp?: string
): Promise<any> {
const response = await this.authFetch('/api/exams/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attemptId,
examId,
answers,
timeSpent,
offlineDuration,
submissionTimestamp
})
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || "Failed to submit exam attempt");
}
return response.json();
}
static async getStudentAttempts(): Promise<Attempt[]> {
try {
const response = await this.authFetch('/api/student/attempts');
if (!response.ok) return [];
return response.json();
} catch {
return [];
}
}
}