232 lines
6.7 KiB
JavaScript
232 lines
6.7 KiB
JavaScript
|
|
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 });
|
||
|
|
}
|
||
|
|
}
|