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

381 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }
);
}
}