Files
YG-Datasets/easy-dataset-main/app/api/projects/[projectId]/eval-datasets/import/route.js

381 lines
11 KiB
JavaScript
Raw Normal View History

2026-03-17 14:36:31 +08:00
import { NextResponse } from 'next/server';
import { db } from '@/lib/db/index';
import { nanoid } from 'nanoid';
import * as XLSX from 'xlsx';
/**
* Validate true/false item schema
*/
function validateTrueFalse(item, index) {
const errors = [];
if (!item.question || typeof item.question !== 'string') {
errors.push(`Item ${index + 1}: missing or invalid "question"`);
}
if (!item.correctAnswer || (item.correctAnswer !== '✅' && item.correctAnswer !== '❌')) {
errors.push(`Item ${index + 1}: "correctAnswer" must be "✅" or "❌"`);
}
return errors;
}
/**
* Validate single-choice item schema
*/
function validateSingleChoice(item, index) {
const errors = [];
if (!item.question || typeof item.question !== 'string') {
errors.push(`Item ${index + 1}: missing or invalid "question"`);
}
// Normalize options
let options = item.options;
if (typeof options === 'string') {
try {
options = JSON.parse(options);
} catch (e) {
errors.push(`Item ${index + 1}: invalid "options" format; unable to parse`);
return errors;
}
}
if (!options || !Array.isArray(options) || options.length < 2) {
errors.push(`Item ${index + 1}: "options" must be an array with at least 2 items`);
}
if (!item.correctAnswer || !/^[A-Z]$/.test(item.correctAnswer)) {
errors.push(`Item ${index + 1}: "correctAnswer" must be a single uppercase letter (A-Z)`);
}
return errors;
}
/**
* Validate multiple-choice item schema
*/
function validateMultipleChoice(item, index) {
const errors = [];
if (!item.question || typeof item.question !== 'string') {
errors.push(`Item ${index + 1}: missing or invalid "question"`);
}
// Normalize options
let options = item.options;
if (typeof options === 'string') {
try {
options = JSON.parse(options);
} catch (e) {
errors.push(`Item ${index + 1}: invalid "options" format; unable to parse`);
return errors;
}
}
if (!options || !Array.isArray(options) || options.length < 2) {
errors.push(`Item ${index + 1}: "options" must be an array with at least 2 items`);
}
// Normalize correctAnswer
let correctAnswer = item.correctAnswer;
if (typeof correctAnswer === 'string') {
try {
correctAnswer = JSON.parse(correctAnswer);
} catch (e) {
errors.push(`Item ${index + 1}: invalid "correctAnswer" format; unable to parse`);
return errors;
}
}
if (!correctAnswer || !Array.isArray(correctAnswer) || correctAnswer.length < 1) {
errors.push(`Item ${index + 1}: "correctAnswer" must be an array with at least 1 item`);
}
// Validate each answer token
if (Array.isArray(correctAnswer)) {
for (const ans of correctAnswer) {
if (!/^[A-Z]$/.test(ans)) {
errors.push(`Item ${index + 1}: "${ans}" is not a valid option letter in "correctAnswer"`);
}
}
}
return errors;
}
/**
* Validate QA item schema (short_answer and open_ended)
*/
function validateQA(item, index) {
const errors = [];
if (!item.question || typeof item.question !== 'string') {
errors.push(`Item ${index + 1}: missing or invalid "question"`);
}
if (!item.correctAnswer || typeof item.correctAnswer !== 'string') {
errors.push(`Item ${index + 1}: missing or invalid "correctAnswer"`);
}
return errors;
}
/**
* Validate data by question type
*/
function validateData(data, questionType) {
const allErrors = [];
for (let i = 0; i < data.length; i++) {
let errors = [];
switch (questionType) {
case 'true_false':
errors = validateTrueFalse(data[i], i);
break;
case 'single_choice':
errors = validateSingleChoice(data[i], i);
break;
case 'multiple_choice':
errors = validateMultipleChoice(data[i], i);
break;
case 'short_answer':
case 'open_ended':
errors = validateQA(data[i], i);
break;
default:
errors = [`Unsupported question type: ${questionType}`];
}
allErrors.push(...errors);
}
return allErrors;
}
/**
* Parse an Excel file
*/
function parseExcel(buffer, questionType) {
const excelHeaders = {
question: '\u9898\u76ee',
correctAnswer: '\u6b63\u786e\u7b54\u6848',
answer: '\u7b54\u6848',
options: '\u9009\u9879'
};
const workbook = XLSX.read(buffer, { type: 'buffer' });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const rawData = XLSX.utils.sheet_to_json(sheet, { defval: '' });
// Convert to normalized schema
const data = rawData.map(row => {
const item = {
question: row.question || row[excelHeaders.question] || '',
correctAnswer: row.correctAnswer || row[excelHeaders.correctAnswer] || row[excelHeaders.answer] || ''
};
// Handle options (choice questions)
if (questionType === 'single_choice' || questionType === 'multiple_choice') {
// Try to parse from options column
if (row.options || row[excelHeaders.options]) {
let optionsStr = (row.options || row[excelHeaders.options]).trim();
// Replace single quotes so it becomes valid JSON
if (optionsStr.startsWith('[') && optionsStr.includes("'")) {
optionsStr = optionsStr.replace(/'/g, '"');
}
try {
// Try JSON parsing
item.options = JSON.parse(optionsStr);
} catch {
// Fallback: split by separators
item.options = optionsStr
.split(/[,;|]/)
.map(o => o.trim())
.filter(Boolean);
}
}
}
// Handle multiple-choice correctAnswer
if (questionType === 'multiple_choice') {
if (typeof item.correctAnswer === 'string') {
let answerStr = item.correctAnswer.trim();
// Replace single quotes so it becomes valid JSON
if (answerStr.startsWith('[') && answerStr.includes("'")) {
answerStr = answerStr.replace(/'/g, '"');
}
// Try JSON parsing
try {
item.correctAnswer = JSON.parse(answerStr);
} catch {
// Split string such as "A,B,C" or "ABC"
if (answerStr.includes(',') || answerStr.includes('')) {
item.correctAnswer = answerStr.split(/[,]/).map(a => a.trim().toUpperCase());
} else {
// Split characters such as "ABC" -> ["A", "B", "C"]
item.correctAnswer = answerStr
.toUpperCase()
.split('')
.filter(c => /[A-Z]/.test(c));
}
}
}
}
return item;
});
return data;
}
/**
* Parse a JSON file
*/
function parseJSON(content) {
return JSON.parse(content);
}
/**
* POST - Import evaluation datasets
*/
export async function POST(request, { params }) {
try {
const { projectId } = params;
const formData = await request.formData();
const file = formData.get('file');
const questionType = formData.get('questionType');
const tags = formData.get('tags') || '';
console.log(`[Import] Start processing. Project: ${projectId}, questionType: ${questionType}, tags: ${tags}`);
if (!file) {
return NextResponse.json({ code: 400, error: 'Please upload a file' }, { status: 400 });
}
if (!questionType) {
return NextResponse.json({ code: 400, error: 'Please select a question type' }, { status: 400 });
}
// Validate question type
const validTypes = ['true_false', 'single_choice', 'multiple_choice', 'short_answer', 'open_ended'];
if (!validTypes.includes(questionType)) {
return NextResponse.json({ code: 400, error: `Unsupported question type: ${questionType}` }, { status: 400 });
}
// Get file extension
const fileName = file.name;
const fileExt = fileName.split('.').pop().toLowerCase();
console.log(`[Import] File name: ${fileName}, extension: ${fileExt}`);
// Validate file type
if (!['json', 'xls', 'xlsx'].includes(fileExt)) {
return NextResponse.json(
{ code: 400, error: 'Unsupported file format. Please upload a json, xls, or xlsx file' },
{ status: 400 }
);
}
// Read file content
const buffer = await file.arrayBuffer();
let data = [];
// Parse file
console.log('[Import] Parsing file...');
if (fileExt === 'json') {
const content = new TextDecoder().decode(buffer);
data = parseJSON(content);
} else {
data = parseExcel(Buffer.from(buffer), questionType);
}
console.log(`[Import] Parsing completed. Total items: ${data.length}`);
if (!Array.isArray(data) || data.length === 0) {
return NextResponse.json({ code: 400, error: 'File is empty or has an invalid format' }, { status: 400 });
}
// Validate data
console.log('[Import] Validating data...');
const errors = validateData(data, questionType);
if (errors.length > 0) {
console.log(`[Import] Validation failed. Error count: ${errors.length}`);
return NextResponse.json(
{
code: 400,
error: 'Data validation failed',
details: errors.slice(0, 10),
totalErrors: errors.length
},
{ status: 400 }
);
}
console.log('[Import] Validation passed. Writing to database...');
// Prepare data
const now = new Date();
const evalDatasets = data.map(item => {
// Normalize options
let options = item.options;
if (typeof options === 'string') {
try {
options = JSON.parse(options);
} catch (e) {
// Keep original on parse failure
}
}
// Normalize correctAnswer
let correctAnswer = item.correctAnswer;
if (typeof correctAnswer === 'string' && questionType === 'multiple_choice') {
try {
correctAnswer = JSON.parse(correctAnswer);
} catch (e) {
// Keep original on parse failure
}
}
return {
id: nanoid(),
projectId,
question: item.question,
questionType,
options: options ? JSON.stringify(options) : '',
// For multiple_choice, store correctAnswer as JSON array string
correctAnswer: Array.isArray(correctAnswer) ? JSON.stringify(correctAnswer) : correctAnswer,
tags: tags || '',
note: '',
createAt: now,
updateAt: now
};
});
// Batch insert
const batchSize = 100;
let insertedCount = 0;
for (let i = 0; i < evalDatasets.length; i += batchSize) {
const batch = evalDatasets.slice(i, i + batchSize);
await db.evalDatasets.createMany({ data: batch });
insertedCount += batch.length;
console.log(`[Import] Inserted ${insertedCount}/${evalDatasets.length} items`);
}
console.log(`[Import] Import completed. Total inserted: ${insertedCount}`);
return NextResponse.json({
code: 0,
data: {
total: insertedCount,
questionType,
tags
},
message: `Successfully imported ${insertedCount} evaluation items`
});
} catch (error) {
console.error('[Import] Import failed:', error);
return NextResponse.json(
{
code: 500,
error: 'Import failed',
message: error.message
},
{ status: 500 }
);
}
}