first-update
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getEvalQuestionById, updateEvalQuestion, deleteEvalQuestion } from '@/lib/db/evalDatasets';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* Get evaluation dataset details by ID
|
||||
* Supports operateType=prev|next to navigate neighbors
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId, evalId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const operateType = searchParams.get('operateType');
|
||||
|
||||
// Navigation request (prev/next)
|
||||
if (operateType) {
|
||||
const current = await db.evalDatasets.findUnique({
|
||||
where: { id: evalId },
|
||||
select: { createAt: true }
|
||||
});
|
||||
|
||||
if (!current) {
|
||||
return NextResponse.json(null);
|
||||
}
|
||||
|
||||
let neighbor = null;
|
||||
|
||||
if (operateType === 'prev') {
|
||||
// Get previous item (newer createAt when list is sorted desc)
|
||||
neighbor = await db.evalDatasets.findFirst({
|
||||
where: {
|
||||
projectId,
|
||||
createAt: { gt: current.createAt }
|
||||
},
|
||||
orderBy: { createAt: 'asc' },
|
||||
select: { id: true }
|
||||
});
|
||||
} else if (operateType === 'next') {
|
||||
// Get next item (older createAt)
|
||||
neighbor = await db.evalDatasets.findFirst({
|
||||
where: {
|
||||
projectId,
|
||||
createAt: { lt: current.createAt }
|
||||
},
|
||||
orderBy: { createAt: 'desc' },
|
||||
select: { id: true }
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(neighbor || null);
|
||||
}
|
||||
|
||||
// Regular detail request
|
||||
const evalQuestion = await getEvalQuestionById(evalId);
|
||||
|
||||
if (!evalQuestion) {
|
||||
return NextResponse.json({ error: 'Eval question not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(evalQuestion);
|
||||
} catch (error) {
|
||||
console.error('Failed to get eval question:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to get eval question' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update evaluation dataset
|
||||
*/
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { evalId } = params;
|
||||
const data = await request.json();
|
||||
|
||||
// Only allow specific fields
|
||||
const allowedFields = ['question', 'options', 'correctAnswer', 'tags', 'note'];
|
||||
const updateData = {};
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (data[field] !== undefined) {
|
||||
updateData[field] = data[field];
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await updateEvalQuestion(evalId, updateData);
|
||||
|
||||
return NextResponse.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Failed to update eval question:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to update eval question' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete evaluation dataset
|
||||
*/
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { evalId } = params;
|
||||
|
||||
await deleteEvalQuestion(evalId);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete eval question:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to delete eval question' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { buildEvalQuestionWhere } from '@/lib/db/evalDatasets';
|
||||
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const questionType = searchParams.get('questionType') || '';
|
||||
const keyword = searchParams.get('keyword') || '';
|
||||
const chunkId = searchParams.get('chunkId') || '';
|
||||
|
||||
const questionTypes = searchParams.getAll('questionTypes') || [];
|
||||
|
||||
const tags =
|
||||
searchParams.getAll('tags').length > 0
|
||||
? searchParams.getAll('tags')
|
||||
: searchParams.get('tag')
|
||||
? searchParams.get('tag').split(',')
|
||||
: [];
|
||||
|
||||
const where = buildEvalQuestionWhere(projectId, {
|
||||
questionType: questionType || undefined,
|
||||
questionTypes: questionTypes.length > 0 ? questionTypes : undefined,
|
||||
keyword: keyword || undefined,
|
||||
chunkId: chunkId || undefined,
|
||||
tags: tags.length > 0 ? tags : undefined
|
||||
});
|
||||
|
||||
const [total, byTypeRaw] = await Promise.all([
|
||||
db.evalDatasets.count({ where }),
|
||||
db.evalDatasets.groupBy({
|
||||
by: ['questionType'],
|
||||
where,
|
||||
_count: { id: true }
|
||||
})
|
||||
]);
|
||||
|
||||
const byType = {};
|
||||
byTypeRaw.forEach(item => {
|
||||
byType[item.questionType] = item._count.id;
|
||||
});
|
||||
|
||||
const hasShortAnswer = (byType.short_answer || 0) > 0;
|
||||
const hasOpenEnded = (byType.open_ended || 0) > 0;
|
||||
const hasSubjective = hasShortAnswer || hasOpenEnded;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 0,
|
||||
data: { total, byType, hasSubjective, hasShortAnswer, hasOpenEnded }
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to count eval datasets:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to count eval datasets', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
import { buildEvalQuestionWhere } from '@/lib/db/evalDatasets';
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* Convert an evaluation item to a CSV row
|
||||
*/
|
||||
function convertToCSVRow(item, isHeader = false) {
|
||||
if (isHeader) {
|
||||
return ['questionType', 'question', 'options', 'correctAnswer', 'tags'].join(',');
|
||||
}
|
||||
|
||||
const escapeCSV = str => {
|
||||
if (str === null || str === undefined) return '';
|
||||
const strValue = String(str);
|
||||
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
||||
return `"${strValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return strValue;
|
||||
};
|
||||
|
||||
return [
|
||||
escapeCSV(item.questionType),
|
||||
escapeCSV(item.question),
|
||||
escapeCSV(item.options),
|
||||
escapeCSV(item.correctAnswer),
|
||||
escapeCSV(item.tags)
|
||||
].join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an evaluation item to export format
|
||||
*/
|
||||
function formatExportItem(item) {
|
||||
return {
|
||||
questionType: item.questionType,
|
||||
question: item.question,
|
||||
options: item.options,
|
||||
correctAnswer: item.correctAnswer,
|
||||
tags: item.tags
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export evaluation datasets
|
||||
* Supports JSON, JSONL, and CSV
|
||||
* Uses batched streaming for large datasets
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const body = await request.json();
|
||||
|
||||
const {
|
||||
format = 'json', // json | jsonl | csv
|
||||
questionTypes = [],
|
||||
tags = [],
|
||||
keyword = ''
|
||||
} = body;
|
||||
|
||||
// Validate format
|
||||
if (!['json', 'jsonl', 'csv'].includes(format)) {
|
||||
return NextResponse.json({ code: 400, error: 'Unsupported export format' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build query conditions
|
||||
const where = buildEvalQuestionWhere(projectId, {
|
||||
questionTypes: questionTypes.length > 0 ? questionTypes : undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
keyword: keyword || undefined
|
||||
});
|
||||
|
||||
// Fetch total count
|
||||
const total = await db.evalDatasets.count({ where });
|
||||
|
||||
if (total === 0) {
|
||||
return NextResponse.json({ code: 400, error: 'No data matches the criteria' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Return directly for small datasets
|
||||
if (total <= 1000) {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
orderBy: { createAt: 'desc' }
|
||||
});
|
||||
|
||||
const formattedItems = items.map(formatExportItem);
|
||||
|
||||
if (format === 'json') {
|
||||
return new Response(JSON.stringify(formattedItems, null, 2), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.json"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (format === 'jsonl') {
|
||||
const jsonlContent = formattedItems.map(item => JSON.stringify(item)).join('\n');
|
||||
return new Response(jsonlContent, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.jsonl"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
const csvContent = [convertToCSVRow(null, true), ...items.map(item => convertToCSVRow(item))].join('\n');
|
||||
return new Response('\uFEFF' + csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.csv"`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stream export for large datasets
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
let isFirst = true;
|
||||
|
||||
// CSV outputs header row first
|
||||
if (format === 'csv') {
|
||||
controller.enqueue(encoder.encode('\uFEFF' + convertToCSVRow(null, true) + '\n'));
|
||||
}
|
||||
|
||||
// JSON outputs opening bracket
|
||||
if (format === 'json') {
|
||||
controller.enqueue(encoder.encode('[\n'));
|
||||
}
|
||||
|
||||
// Fetch data in batches
|
||||
const totalBatches = Math.ceil(total / BATCH_SIZE);
|
||||
|
||||
for (let batch = 0; batch < totalBatches; batch++) {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
orderBy: { createAt: 'desc' },
|
||||
skip: batch * BATCH_SIZE,
|
||||
take: BATCH_SIZE
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
const formattedItem = formatExportItem(item);
|
||||
|
||||
if (format === 'json') {
|
||||
const prefix = isFirst ? '' : ',\n';
|
||||
controller.enqueue(encoder.encode(prefix + JSON.stringify(formattedItem)));
|
||||
isFirst = false;
|
||||
} else if (format === 'jsonl') {
|
||||
controller.enqueue(encoder.encode(JSON.stringify(formattedItem) + '\n'));
|
||||
} else if (format === 'csv') {
|
||||
controller.enqueue(encoder.encode(convertToCSVRow(item) + '\n'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON outputs closing bracket
|
||||
if (format === 'json') {
|
||||
controller.enqueue(encoder.encode('\n]'));
|
||||
}
|
||||
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const contentTypes = {
|
||||
json: 'application/json',
|
||||
jsonl: 'application/x-ndjson',
|
||||
csv: 'text/csv; charset=utf-8'
|
||||
};
|
||||
|
||||
const extensions = {
|
||||
json: 'json',
|
||||
jsonl: 'jsonl',
|
||||
csv: 'csv'
|
||||
};
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': contentTypes[format],
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.${extensions[format]}"`,
|
||||
'Transfer-Encoding': 'chunked'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to export eval datasets:', error);
|
||||
return NextResponse.json({ code: 500, error: error.message || 'Export failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get export preview (count only)
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Parse query params
|
||||
const questionTypes = searchParams.getAll('questionTypes');
|
||||
const tags = searchParams.getAll('tags');
|
||||
const keyword = searchParams.get('keyword') || '';
|
||||
|
||||
// Build query conditions
|
||||
const where = buildEvalQuestionWhere(projectId, {
|
||||
questionTypes: questionTypes.length > 0 ? questionTypes : undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
keyword: keyword || undefined
|
||||
});
|
||||
|
||||
// Count rows
|
||||
const total = await db.evalDatasets.count({ where });
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
total,
|
||||
isLargeDataset: total > 1000
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get export preview:', error);
|
||||
return NextResponse.json({ code: 500, error: error.message || 'Failed to get export preview' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getEvalQuestionsWithPagination, getEvalQuestionsStats, deleteEvalQuestion } from '@/lib/db/evalDatasets';
|
||||
|
||||
/**
|
||||
* Get project's evaluation dataset list (paginated)
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Parse query params
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20', 10);
|
||||
const questionType = searchParams.get('questionType') || '';
|
||||
const questionTypes = searchParams.getAll('questionTypes');
|
||||
const keyword = searchParams.get('keyword') || '';
|
||||
const chunkId = searchParams.get('chunkId') || '';
|
||||
// Support multiple tags params or comma-separated tag
|
||||
const tags =
|
||||
searchParams.getAll('tags').length > 0
|
||||
? searchParams.getAll('tags')
|
||||
: searchParams.get('tag')
|
||||
? searchParams.get('tag').split(',')
|
||||
: [];
|
||||
|
||||
const includeStats = searchParams.get('includeStats') === 'true';
|
||||
|
||||
const queryOptions = {
|
||||
page,
|
||||
pageSize,
|
||||
questionType: questionType || undefined,
|
||||
questionTypes: questionTypes.length > 0 ? questionTypes : undefined,
|
||||
keyword: keyword || undefined,
|
||||
chunkId: chunkId || undefined,
|
||||
tags: tags.length > 0 ? tags : undefined
|
||||
};
|
||||
|
||||
if (includeStats) {
|
||||
const [result, stats] = await Promise.all([
|
||||
getEvalQuestionsWithPagination(projectId, queryOptions),
|
||||
getEvalQuestionsStats(projectId)
|
||||
]);
|
||||
result.stats = stats;
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
const result = await getEvalQuestionsWithPagination(projectId, queryOptions);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to get eval datasets:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to get eval datasets' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch delete evaluation datasets
|
||||
*/
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { ids } = await request.json();
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return NextResponse.json({ error: 'Invalid request: ids array is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const results = await Promise.all(ids.map(id => deleteEvalQuestion(id).catch(err => ({ error: err.message, id }))));
|
||||
const deleted = results.filter(r => !r.error).length;
|
||||
const failed = results.filter(r => r.error).length;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
deleted,
|
||||
failed,
|
||||
message: `Successfully deleted ${deleted} items${failed > 0 ? `, ${failed} failed` : ''}`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to delete eval datasets:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to delete eval datasets' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new evaluation dataset (or batch create)
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const body = await request.json();
|
||||
|
||||
const { createEvalQuestion, createManyEvalQuestions } = require('@/lib/db/evalDatasets');
|
||||
|
||||
// Handle batch creation
|
||||
if (Array.isArray(body) || (body.items && Array.isArray(body.items))) {
|
||||
const items = Array.isArray(body) ? body : body.items;
|
||||
|
||||
if (items.length === 0) {
|
||||
return NextResponse.json({ success: true, count: 0 });
|
||||
}
|
||||
|
||||
// Validate items
|
||||
const validItems = items
|
||||
.map(item => {
|
||||
// 确保标签格式正确: 数组转为逗号分隔字符串
|
||||
let tagsStr = item.tags || '';
|
||||
if (Array.isArray(tagsStr)) {
|
||||
tagsStr = tagsStr.join(',');
|
||||
}
|
||||
return {
|
||||
projectId,
|
||||
question: item.question,
|
||||
questionType: item.questionType || 'open_ended',
|
||||
correctAnswer:
|
||||
typeof item.correctAnswer === 'object' ? JSON.stringify(item.correctAnswer) : item.correctAnswer,
|
||||
tags: tagsStr,
|
||||
note: item.note || '',
|
||||
chunkId: item.chunkId || null,
|
||||
options: item.options
|
||||
? typeof item.options === 'object'
|
||||
? JSON.stringify(item.options)
|
||||
: item.options
|
||||
: ''
|
||||
};
|
||||
})
|
||||
.filter(item => item.question && item.correctAnswer);
|
||||
|
||||
if (validItems.length === 0) {
|
||||
return NextResponse.json({ error: 'No valid items to create' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await createManyEvalQuestions(validItems);
|
||||
return NextResponse.json({ success: true, count: result.count });
|
||||
}
|
||||
|
||||
// Handle single creation
|
||||
const { question, correctAnswer, questionType = 'open_ended', tags, note, chunkId, options } = body;
|
||||
|
||||
if (!question || !correctAnswer) {
|
||||
return NextResponse.json({ error: 'Question and Correct Answer are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 确保标签格式正确: 数组转为逗号分隔字符串
|
||||
let tagsStr = tags || '';
|
||||
if (Array.isArray(tagsStr)) {
|
||||
tagsStr = tagsStr.join(',');
|
||||
}
|
||||
|
||||
const evalDataset = await createEvalQuestion({
|
||||
projectId,
|
||||
question,
|
||||
questionType,
|
||||
correctAnswer: typeof correctAnswer === 'object' ? JSON.stringify(correctAnswer) : correctAnswer,
|
||||
tags: tagsStr,
|
||||
note: note || '',
|
||||
chunkId: chunkId || null,
|
||||
options: options ? (typeof options === 'object' ? JSON.stringify(options) : options) : ''
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, evalDataset });
|
||||
} catch (error) {
|
||||
console.error('Failed to create eval dataset:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to create eval dataset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { buildEvalQuestionWhere } from '@/lib/db/evalDatasets';
|
||||
|
||||
const SMALL_TOTAL_THRESHOLD = 5000;
|
||||
const HARD_LIMIT = 50000;
|
||||
|
||||
function shuffleArray(arr) {
|
||||
const result = [...arr];
|
||||
for (let i = result.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[result[i], result[j]] = [result[j], result[i]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const body = await request.json();
|
||||
|
||||
const {
|
||||
questionType = '',
|
||||
questionTypes = [],
|
||||
keyword = '',
|
||||
chunkId = '',
|
||||
tags = [],
|
||||
limit = 0,
|
||||
strategy = 'random'
|
||||
} = body || {};
|
||||
|
||||
const where = buildEvalQuestionWhere(projectId, {
|
||||
questionType: questionType || undefined,
|
||||
questionTypes: Array.isArray(questionTypes) && questionTypes.length > 0 ? questionTypes : undefined,
|
||||
keyword: keyword || undefined,
|
||||
chunkId: chunkId || undefined,
|
||||
tags: Array.isArray(tags) && tags.length > 0 ? tags : undefined
|
||||
});
|
||||
|
||||
const total = await db.evalDatasets.count({ where });
|
||||
|
||||
if (total === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 0,
|
||||
data: {
|
||||
total: 0,
|
||||
selectedCount: 0,
|
||||
ids: [],
|
||||
strategyUsed: strategy
|
||||
}
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
let normalizedLimit = typeof limit === 'number' && limit > 0 ? Math.min(limit, HARD_LIMIT) : HARD_LIMIT;
|
||||
|
||||
if (normalizedLimit >= total) {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
orderBy: { createAt: 'desc' }
|
||||
});
|
||||
|
||||
const ids = items.map(item => item.id);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 0,
|
||||
data: {
|
||||
total,
|
||||
selectedCount: ids.length,
|
||||
ids,
|
||||
strategyUsed: total > HARD_LIMIT ? 'top' : strategy
|
||||
}
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
let ids = [];
|
||||
let strategyUsed = strategy;
|
||||
|
||||
if (total <= SMALL_TOTAL_THRESHOLD) {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
orderBy: { createAt: 'desc' }
|
||||
});
|
||||
const shuffled = shuffleArray(items);
|
||||
ids = shuffled.slice(0, normalizedLimit).map(item => item.id);
|
||||
strategyUsed = 'random-small';
|
||||
} else {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
orderBy: { createAt: 'desc' },
|
||||
take: normalizedLimit
|
||||
});
|
||||
ids = items.map(item => item.id);
|
||||
strategyUsed = 'top-latest';
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 0,
|
||||
data: {
|
||||
total,
|
||||
selectedCount: ids.length,
|
||||
ids,
|
||||
strategyUsed
|
||||
}
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to sample eval datasets:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to sample eval datasets', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* Get all evaluation dataset tags in the project
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
|
||||
// Fetch tags for all datasets in the project
|
||||
const datasets = await db.evalDatasets.findMany({
|
||||
where: { projectId },
|
||||
select: { tags: true }
|
||||
});
|
||||
|
||||
// Extract and de-duplicate tags
|
||||
const tagsSet = new Set();
|
||||
datasets.forEach(dataset => {
|
||||
if (dataset.tags) {
|
||||
// Support both English and Chinese commas
|
||||
const tags = dataset.tags
|
||||
.split(/[,,]/)
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
tags.forEach(tag => tagsSet.add(tag));
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ tags: Array.from(tagsSet).sort() });
|
||||
} catch (error) {
|
||||
console.error('Failed to get tags:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to get tags' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user