first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -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 });
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 });
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 });
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 });
}
}