291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
/**
|
||
* API модуль для домашних заданий
|
||
*/
|
||
|
||
import apiClient from '@/lib/api-client';
|
||
|
||
export interface HomeworkMentor {
|
||
id: number;
|
||
email: string;
|
||
first_name: string;
|
||
last_name: string;
|
||
}
|
||
|
||
/** Файл задания/решения (ментор прикрепляет к заданию, ученик видит и скачивает). */
|
||
export interface HomeworkFileItem {
|
||
id: number;
|
||
file_type: 'assignment' | 'submission' | 'feedback';
|
||
file: string;
|
||
filename: string;
|
||
file_size: number;
|
||
/** Признак изображения по расширению (с бэкенда) — показывать превью и открывать в модалке. */
|
||
is_image?: boolean;
|
||
uploaded_by?: { id: number; first_name: string; last_name: string } | null;
|
||
created_at: string;
|
||
}
|
||
|
||
export interface Homework {
|
||
id: number;
|
||
title: string;
|
||
description?: string;
|
||
mentor: HomeworkMentor;
|
||
lesson: number | null;
|
||
deadline: string | null;
|
||
max_score: number;
|
||
passing_score: number;
|
||
status: 'draft' | 'published' | 'archived';
|
||
/** Черновик «заполнить позже» — создан при завершении урока, нужно дописать задание. */
|
||
fill_later?: boolean;
|
||
total_submissions: number;
|
||
checked_submissions: number;
|
||
returned_submissions: number;
|
||
average_score: number;
|
||
is_overdue: boolean;
|
||
created_at: string;
|
||
published_at: string | null;
|
||
/** Файл задания (один), URL для скачивания. */
|
||
attachment?: string | null;
|
||
/** Ссылка на материал (внешняя). */
|
||
attachment_url?: string | null;
|
||
/** Дополнительные файлы задания (ментор прикрепляет несколько). */
|
||
files?: HomeworkFileItem[] | null;
|
||
students?: { id: number; first_name: string; last_name: string; score: number | null; status: string }[] | null;
|
||
student_score?: { score: number | null; max_score: number; status: string } | null;
|
||
/** Только для ментора: количество решений с черновиком от ИИ (status=pending, ai_checked_at задано). */
|
||
ai_draft_count?: number;
|
||
}
|
||
|
||
export interface HomeworkSubmission {
|
||
id: number;
|
||
homework: { id: number; title: string; description?: string; max_score: number };
|
||
student: { id: number; first_name: string; last_name: string; email: string };
|
||
status: string;
|
||
content?: string;
|
||
/** Основной файл решения (URL для скачивания). */
|
||
attachment?: string | null;
|
||
attachment_url?: string | null;
|
||
/** Доп. файлы решения (студент прикрепляет несколько). */
|
||
files?: HomeworkFileItem[] | null;
|
||
score?: number | null;
|
||
feedback?: string;
|
||
/** HTML комментария проверки (markdown → HTML). */
|
||
feedback_html?: string;
|
||
submitted_at: string;
|
||
checked_at?: string | null;
|
||
ai_score?: number | null;
|
||
ai_feedback?: string;
|
||
/** HTML превью черновика ИИ (markdown → HTML). */
|
||
ai_feedback_html?: string;
|
||
ai_checked_at?: string | null;
|
||
/** True, если оценка опубликована автоматически через ИИ. */
|
||
graded_by_ai?: boolean;
|
||
checked_by?: { id: number; first_name: string; last_name: string } | null;
|
||
}
|
||
|
||
export async function getHomework(params?: {
|
||
status?: string;
|
||
page_size?: number;
|
||
child_id?: string;
|
||
}): Promise<{ results: Homework[]; count: number }> {
|
||
const q = new URLSearchParams();
|
||
if (params?.status) q.append('status', params.status);
|
||
if (params?.page_size) q.append('page_size', String(params.page_size || 1000));
|
||
if (params?.child_id) q.append('child_id', params.child_id);
|
||
const query = q.toString();
|
||
const url = `/homework/homeworks/${query ? `?${query}` : ''}`;
|
||
const res = await apiClient.get<{ results: Homework[]; count: number } | Homework[]>(url);
|
||
const data = res.data;
|
||
if (Array.isArray(data)) {
|
||
return { results: data, count: data.length };
|
||
}
|
||
return {
|
||
results: data?.results ?? [],
|
||
count: data?.count ?? 0,
|
||
};
|
||
}
|
||
|
||
export async function getHomeworkById(id: string | number): Promise<Homework> {
|
||
const res = await apiClient.get<Homework>(`/homework/homeworks/${id}/`);
|
||
return res.data;
|
||
}
|
||
|
||
/** Создать домашнее задание (в т.ч. черновик для «заполнить позже»). По умолчанию макс. балл 5, проходной 1 (не учитывается). */
|
||
export async function createHomework(data: {
|
||
title: string;
|
||
description?: string;
|
||
lesson_id?: number;
|
||
status?: 'draft' | 'published';
|
||
/** Пометить как «заполнить позже» — отображается в колонке «Ожидают заполнения» у ментора. */
|
||
fill_later?: boolean;
|
||
/** Максимальный балл (1–5 по умолчанию). По умолчанию 5. */
|
||
max_score?: number;
|
||
/** Проходной балл (по умолчанию 1, не учитывается). */
|
||
passing_score?: number;
|
||
}): Promise<Homework> {
|
||
const payload = {
|
||
...data,
|
||
max_score: data.max_score ?? 5,
|
||
passing_score: data.passing_score ?? 1,
|
||
};
|
||
const res = await apiClient.post<Homework>('/homework/homeworks/', payload);
|
||
return res.data;
|
||
}
|
||
|
||
/** Опции запроса списка решений (например, отключить кэш для актуальных данных). */
|
||
export interface GetHomeworkSubmissionsOptions {
|
||
cache?: boolean;
|
||
/** Для родителя: user_id ребёнка — вернуть решения этого ребёнка. */
|
||
child_id?: string | null;
|
||
}
|
||
|
||
export async function getHomeworkSubmissions(
|
||
homeworkId: string | number,
|
||
options?: GetHomeworkSubmissionsOptions
|
||
): Promise<HomeworkSubmission[]> {
|
||
const params = new URLSearchParams({ homework_id: String(homeworkId) });
|
||
if (options?.child_id) params.append('child_id', options.child_id);
|
||
const res = await apiClient.get<{ results: HomeworkSubmission[] } | HomeworkSubmission[]>(
|
||
`/homework/submissions/?${params.toString()}`,
|
||
{ cache: options?.cache ?? false }
|
||
);
|
||
const data = res.data;
|
||
if (Array.isArray(data)) return data;
|
||
return data?.results ?? [];
|
||
}
|
||
|
||
export async function getMySubmission(
|
||
homeworkId: string | number,
|
||
options?: GetHomeworkSubmissionsOptions
|
||
): Promise<HomeworkSubmission | null> {
|
||
const list = await getHomeworkSubmissions(homeworkId, options);
|
||
return list.length > 0 ? list[0] : null;
|
||
}
|
||
|
||
/** Получить одно решение по ID (для детального просмотра). */
|
||
export async function getHomeworkSubmission(
|
||
submissionId: string | number
|
||
): Promise<HomeworkSubmission> {
|
||
const res = await apiClient.get<HomeworkSubmission>(
|
||
`/homework/submissions/${submissionId}/`
|
||
);
|
||
return res.data;
|
||
}
|
||
|
||
/**
|
||
* ДЗ с оценками по предмету для графика прогресса.
|
||
* GET /api/homework/submissions/by_subject/
|
||
*/
|
||
export async function getHomeworkSubmissionsBySubject(params: {
|
||
subject: string;
|
||
start_date?: string;
|
||
end_date?: string;
|
||
child_id?: string;
|
||
}): Promise<{ count: number; results: HomeworkSubmission[] }> {
|
||
const q = new URLSearchParams();
|
||
q.append('subject', params.subject);
|
||
if (params.start_date) q.append('start_date', params.start_date);
|
||
if (params.end_date) q.append('end_date', params.end_date);
|
||
if (params.child_id) q.append('child_id', params.child_id);
|
||
const res = await apiClient.get<{ count: number; results: HomeworkSubmission[] }>(
|
||
`/homework/submissions/by_subject/?${q}`
|
||
);
|
||
return res.data;
|
||
}
|
||
|
||
export async function gradeSubmission(
|
||
submissionId: string | number,
|
||
data: { score: number; feedback?: string }
|
||
): Promise<unknown> {
|
||
const res = await apiClient.post(`/homework/submissions/${submissionId}/grade/`, data);
|
||
return res.data;
|
||
}
|
||
|
||
/** Использование токенов за один запрос (если API вернул). */
|
||
export interface TokenUsage {
|
||
prompt_tokens: number;
|
||
completion_tokens: number;
|
||
total_tokens: number;
|
||
}
|
||
|
||
export interface CheckWithAiResponse {
|
||
success: boolean;
|
||
ai_score: number;
|
||
ai_feedback: string;
|
||
/** HTML для отображения комментария ИИ (markdown + LaTeX → HTML). */
|
||
ai_feedback_html?: string;
|
||
ai_checked_at?: string;
|
||
message?: string;
|
||
/** Токены за эту проверку (потрачено). Остаток лимита — в кабинете Timeweb. */
|
||
usage?: TokenUsage;
|
||
}
|
||
|
||
/** Проверить решение через ИИ. Ментор: задание + решение → комментарий и оценка 1–5. */
|
||
export async function checkSubmissionWithAi(
|
||
submissionId: string | number
|
||
): Promise<CheckWithAiResponse> {
|
||
const res = await apiClient.post<CheckWithAiResponse>(
|
||
`/homework/submissions/${submissionId}/check_with_ai/`
|
||
);
|
||
return res.data;
|
||
}
|
||
|
||
export async function returnSubmissionForRevision(
|
||
submissionId: string | number,
|
||
feedback: string
|
||
): Promise<unknown> {
|
||
const res = await apiClient.post(`/homework/submissions/${submissionId}/return_for_revision/`, { feedback });
|
||
return res.data;
|
||
}
|
||
|
||
/** Удалить своё решение (студент). Задание снова переходит в ожидание загрузки. */
|
||
export async function deleteSubmission(submissionId: string | number): Promise<void> {
|
||
await apiClient.delete(`/homework/submissions/${submissionId}/`);
|
||
}
|
||
|
||
const MAX_HOMEWORK_FILE_SIZE = 50 * 1024 * 1024; // 50 МБ
|
||
const MAX_HOMEWORK_FILES = 10;
|
||
|
||
export function validateHomeworkFiles(files: File[]): { valid: boolean; error?: string } {
|
||
if (files.length === 0) return { valid: true };
|
||
if (files.length > MAX_HOMEWORK_FILES) {
|
||
return { valid: false, error: `Максимум ${MAX_HOMEWORK_FILES} файлов` };
|
||
}
|
||
for (const f of files) {
|
||
if (f.size > MAX_HOMEWORK_FILE_SIZE) {
|
||
return { valid: false, error: `Файл "${f.name}" больше 50 МБ` };
|
||
}
|
||
}
|
||
return { valid: true };
|
||
}
|
||
|
||
export async function submitHomework(
|
||
homeworkId: string | number,
|
||
data: { content?: string; text?: string; files?: File[] },
|
||
onUploadProgress?: (percent: number) => void
|
||
): Promise<unknown> {
|
||
const hasFiles = data.files && data.files.length > 0;
|
||
if (hasFiles) {
|
||
const formData = new FormData();
|
||
formData.append('homework_id', String(homeworkId));
|
||
if (data.content) formData.append('content', data.content);
|
||
if (data.text) formData.append('content', data.text);
|
||
data.files!.forEach((f) => formData.append('attachment', f));
|
||
const res = await apiClient.post(`/homework/submissions/`, formData, {
|
||
onUploadProgress:
|
||
onUploadProgress &&
|
||
(function (event: { loaded: number; total?: number }) {
|
||
if (event.total && event.total > 0) {
|
||
const percent = Math.round((event.loaded / event.total) * 100);
|
||
onUploadProgress(Math.min(percent, 100));
|
||
}
|
||
}),
|
||
});
|
||
return res.data;
|
||
}
|
||
const res = await apiClient.post(`/homework/submissions/`, {
|
||
homework_id: homeworkId,
|
||
content: data.content || data.text || '',
|
||
});
|
||
return res.data;
|
||
}
|