Files
prop-ai-hr/mobile-uni/src/services/exam.ts
T
admin 7716f0040d feat(mobile): AI question generation panel in supervisor exam editor
Hints input + preset chips + count picker; generated questions land in
the existing editor blocks for the supervisor to review before publish.
2026-07-20 10:16:40 +08:00

184 lines
5.3 KiB
TypeScript

import type { ExamDetail, ExamQuestion, ExamResultItem, ExamSubmitResult, ExamSummary, SupervisorExamDraft } from '@/types/api';
import { apiRequest } from './api';
type RawExamQuestion = {
id: number;
questionType: string;
stem: string;
options?: string[];
correctAnswers?: string[];
score: number;
explanation?: string;
sortOrder?: number;
};
type RawExamSummary = {
id: number;
title: string;
projectCode?: string;
positionCode?: string;
status: string;
totalScore: number;
passScore: number;
dueDate?: string | null;
questionCount: number;
targetCount: number;
submissionCount: number;
bestScore?: number | null;
passed?: boolean | null;
};
type RawExamDetail = Omit<RawExamSummary, 'questionCount' | 'targetCount' | 'submissionCount' | 'bestScore' | 'passed'> & {
questions: RawExamQuestion[];
targets: ExamDetail['targets'];
};
const normalizeSummary = (item: RawExamSummary): ExamSummary => ({
...item,
passingScore: item.passScore
});
const normalizeDetail = (item: RawExamDetail): ExamDetail => ({
...item,
passingScore: item.passScore,
questionCount: item.questions?.length || 0,
targetCount: item.targets?.length || 0,
submissionCount: 0,
bestScore: null,
passed: null,
questions: (item.questions || []).map((question) => ({
id: question.id,
format: question.questionType,
stem: question.stem,
options: question.options || [],
score: question.score,
answer: question.correctAnswers || [],
explanation: question.explanation || ''
})),
targets: item.targets || []
});
const serializeDraft = (draft: SupervisorExamDraft) => ({
title: draft.title,
projectCode: draft.projectCode,
positionCode: draft.positionCode,
passScore: draft.passScore,
dueDate: draft.dueDate,
targets: draft.targets,
questions: draft.questions.map((question, index) => ({
id: question.id > 0 && question.id < 1_000_000_000 ? question.id : null,
questionType: question.format,
stem: question.stem,
options: question.options,
correctAnswers: Array.isArray(question.answer) ? question.answer : [String(question.answer || '')],
score: question.score,
explanation: question.explanation || '',
sortOrder: index + 1
}))
});
export const listMyExams = async () =>
(await apiRequest<RawExamSummary[]>({
url: '/api/aihr/mobile/exams',
method: 'GET',
timeout: 15000
})).map(normalizeSummary);
export const getMyExam = async (id: number) =>
normalizeDetail(await apiRequest<RawExamDetail>({
url: `/api/aihr/mobile/exams/${id}`,
method: 'GET',
timeout: 15000
}));
export const submitMyExam = (id: number, answers: Record<string, string | string[]>, requestId: string) =>
apiRequest<ExamSubmitResult>({
url: `/api/aihr/mobile/exams/${id}/submit`,
method: 'POST',
data: {
requestId,
answers: Object.entries(answers).map(([questionId, value]) => ({
questionId: Number(questionId),
answers: Array.isArray(value) ? value : [String(value)]
}))
},
timeout: 30000
});
export const listSupervisorExams = async () =>
(await apiRequest<RawExamSummary[]>({
url: '/api/aihr/mobile/exams/supervisor',
method: 'GET',
timeout: 15000
})).map(normalizeSummary);
export const getSupervisorExam = async (id: number) =>
normalizeDetail(await apiRequest<RawExamDetail>({
url: `/api/aihr/mobile/exams/supervisor/${id}`,
method: 'GET',
timeout: 15000
}));
export const saveSupervisorExam = async (draft: SupervisorExamDraft) =>
normalizeDetail(await apiRequest<RawExamDetail>({
url: '/api/aihr/mobile/exams/supervisor',
method: 'POST',
data: serializeDraft(draft),
timeout: 30000
}));
export const updateSupervisorExam = async (id: number, draft: SupervisorExamDraft) =>
normalizeDetail(await apiRequest<RawExamDetail>({
url: `/api/aihr/mobile/exams/supervisor/${id}`,
method: 'PUT',
data: serializeDraft(draft),
timeout: 30000
}));
export const publishSupervisorExam = async (id: number, requestId: string) =>
normalizeDetail(await apiRequest<RawExamDetail>({
url: `/api/aihr/mobile/exams/supervisor/${id}/publish`,
method: 'POST',
data: { requestId },
timeout: 30000
}));
export const getSupervisorExamResults = (id: number) =>
apiRequest<ExamResultItem[]>({
url: `/api/aihr/mobile/exams/supervisor/${id}/results`,
method: 'GET',
timeout: 15000
});
type RawAiGenerateResponse = {
questions: RawExamQuestion[];
groundedFragments?: number;
};
export const generateSupervisorExamQuestions = async (data: {
positionCode: string;
hints: string;
count: number;
formats: string[];
}): Promise<{ questions: ExamQuestion[]; groundedFragments: number }> => {
const response = await apiRequest<RawAiGenerateResponse>({
url: '/api/aihr/mobile/exams/supervisor/ai-generate',
method: 'POST',
data,
timeout: 60000
});
return {
groundedFragments: response.groundedFragments || 0,
questions: (response.questions || []).map((question, index) => ({
id: Date.now() + index,
format: question.questionType,
stem: question.stem,
options: question.options || [],
score: question.score,
answer: question.questionType === 'multiple' ? (question.correctAnswers || []) : (question.correctAnswers?.[0] || ''),
explanation: question.explanation || ''
}))
};
};