first-update
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
import LLMClient from '@/lib/llm/core/index';
|
||||
import { getModelConfigById } from '@/lib/db/model-config';
|
||||
|
||||
/**
|
||||
* Get current question and generate answers from two models
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId, taskId } = params;
|
||||
|
||||
const task = await db.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ code: 404, error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (task.status !== 0) {
|
||||
return NextResponse.json({ code: 400, error: 'Task has ended' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Parse task detail
|
||||
let detail = {};
|
||||
let modelInfo = {};
|
||||
try {
|
||||
detail = task.detail ? JSON.parse(task.detail) : {};
|
||||
modelInfo = task.modelInfo ? JSON.parse(task.modelInfo) : {};
|
||||
} catch (e) {
|
||||
console.error('Failed to parse task detail:', e);
|
||||
}
|
||||
|
||||
const questionIds = detail.questionIds || detail.evalDatasetIds || [];
|
||||
const currentIndex = detail.currentIndex || 0;
|
||||
|
||||
// Check if all questions are completed
|
||||
if (questionIds.length === 0 || currentIndex >= questionIds.length) {
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
completed: true,
|
||||
message: 'All questions completed'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch current question
|
||||
const currentQuestionId = questionIds[currentIndex];
|
||||
const currentQuestion = await db.evalDatasets.findUnique({
|
||||
where: { id: currentQuestionId },
|
||||
select: {
|
||||
id: true,
|
||||
question: true,
|
||||
questionType: true,
|
||||
correctAnswer: true,
|
||||
tags: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentQuestion) {
|
||||
return NextResponse.json({ code: 404, error: 'Question not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Fetch both model configs
|
||||
const [modelConfigA, modelConfigB] = await Promise.all([
|
||||
getModelConfigById(modelInfo.modelA.providerId),
|
||||
getModelConfigById(modelInfo.modelB.providerId)
|
||||
]);
|
||||
|
||||
if (!modelConfigA || !modelConfigB) {
|
||||
return NextResponse.json({ code: 400, error: 'Model configuration not found' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build prompts
|
||||
const systemPrompt = "You are a helpful assistant. Provide detailed and accurate answers to the user's question.";
|
||||
const userPrompt = currentQuestion.question;
|
||||
|
||||
// Call both models in parallel
|
||||
const startTimeA = Date.now();
|
||||
const startTimeB = Date.now();
|
||||
|
||||
let answerA = '';
|
||||
let answerB = '';
|
||||
let errorA = null;
|
||||
let errorB = null;
|
||||
let durationA = 0;
|
||||
let durationB = 0;
|
||||
|
||||
try {
|
||||
// Call model A
|
||||
const clientA = new LLMClient(modelConfigA);
|
||||
|
||||
const resultA = await clientA.chat([
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]);
|
||||
|
||||
answerA = resultA.text || '';
|
||||
durationA = Date.now() - startTimeA;
|
||||
} catch (err) {
|
||||
console.error('Model A call failed:', err);
|
||||
errorA = err.message;
|
||||
durationA = Date.now() - startTimeA;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call model B
|
||||
const clientB = new LLMClient(modelConfigB);
|
||||
|
||||
const resultB = await clientB.chat([
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]);
|
||||
|
||||
answerB = resultB.text || '';
|
||||
durationB = Date.now() - startTimeB;
|
||||
} catch (err) {
|
||||
console.error('Model B call failed:', err);
|
||||
errorB = err.message;
|
||||
durationB = Date.now() - startTimeB;
|
||||
}
|
||||
|
||||
// Randomly swap positions (core blind-test behavior)
|
||||
const isSwapped = Math.random() > 0.5;
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
completed: false,
|
||||
currentIndex,
|
||||
totalCount: evalDatasetIds.length,
|
||||
question: currentQuestion,
|
||||
// Blind test: do not reveal which model is which
|
||||
leftAnswer: {
|
||||
content: isSwapped ? answerB : answerA,
|
||||
error: isSwapped ? errorB : errorA,
|
||||
duration: isSwapped ? durationB : durationA
|
||||
},
|
||||
rightAnswer: {
|
||||
content: isSwapped ? answerA : answerB,
|
||||
error: isSwapped ? errorA : errorB,
|
||||
duration: isSwapped ? durationA : durationB
|
||||
},
|
||||
// Server stores the actual mapping for scoring
|
||||
_swap: isSwapped
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch current question:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to fetch current question', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* Get current question info (including random swap info)
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
const { projectId, taskId } = params;
|
||||
|
||||
try {
|
||||
if (!projectId || !taskId) {
|
||||
return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch task
|
||||
const task = await db.task.findUnique({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
if (!task || task.taskType !== 'blind-test') {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Parse task detail
|
||||
const detail = JSON.parse(task.detail || '{}');
|
||||
// Support both evalDatasetIds and questionIds
|
||||
const questionIds = detail.questionIds || detail.evalDatasetIds || [];
|
||||
const currentIndex = detail.currentIndex || 0;
|
||||
|
||||
// Check if task is completed
|
||||
if (questionIds.length === 0 || currentIndex >= questionIds.length) {
|
||||
return NextResponse.json({
|
||||
completed: true,
|
||||
currentIndex,
|
||||
totalQuestions: questionIds.length
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch current question
|
||||
const currentQuestionId = questionIds[currentIndex];
|
||||
const currentQuestion = await db.evalDatasets.findUnique({
|
||||
where: { id: currentQuestionId }
|
||||
});
|
||||
|
||||
if (!currentQuestion) {
|
||||
return NextResponse.json({ error: 'Question not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Randomly decide whether to swap (core blind-test behavior)
|
||||
const isSwapped = Math.random() > 0.5;
|
||||
|
||||
return NextResponse.json({
|
||||
questionId: currentQuestion.id,
|
||||
question: currentQuestion.question,
|
||||
answer: currentQuestion.correctAnswer || '',
|
||||
questionIndex: currentIndex + 1,
|
||||
totalQuestions: questionIds.length,
|
||||
isSwapped
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch question info:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* Get blind-test task details
|
||||
* Results are fetched from EvalResults table
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId, taskId } = params;
|
||||
|
||||
const task = await db.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ code: 404, error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
let detail = {};
|
||||
let modelInfo = {};
|
||||
try {
|
||||
detail = task.detail ? JSON.parse(task.detail) : {};
|
||||
modelInfo = task.modelInfo ? JSON.parse(task.modelInfo) : {};
|
||||
} catch (e) {
|
||||
console.error('Failed to parse task detail:', e);
|
||||
}
|
||||
|
||||
// Fetch all related evaluation questions
|
||||
const evalDatasetIds = detail.evalDatasetIds || [];
|
||||
const evalDatasets = await db.evalDatasets.findMany({
|
||||
where: {
|
||||
id: { in: evalDatasetIds }
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
question: true,
|
||||
questionType: true,
|
||||
correctAnswer: true,
|
||||
tags: true
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by evalDatasetIds order
|
||||
const orderedDatasets = evalDatasetIds.map(id => evalDatasets.find(d => d.id === id)).filter(Boolean);
|
||||
|
||||
// Fetch results from EvalResults table
|
||||
const evalResults = await db.evalResults.findMany({
|
||||
where: { taskId },
|
||||
orderBy: { createAt: 'asc' }
|
||||
});
|
||||
|
||||
// Parse results into the format expected by frontend
|
||||
const results = evalResults.map(r => {
|
||||
let modelAnswer = {};
|
||||
let judgeData = {};
|
||||
try {
|
||||
modelAnswer = JSON.parse(r.modelAnswer || '{}');
|
||||
judgeData = JSON.parse(r.judgeResponse || '{}');
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
return {
|
||||
questionId: r.evalDatasetId,
|
||||
vote: judgeData.vote,
|
||||
isSwapped: judgeData.isSwapped,
|
||||
modelAScore: judgeData.modelAScore || 0,
|
||||
modelBScore: judgeData.modelBScore || 0,
|
||||
leftAnswer: modelAnswer.leftAnswer || '',
|
||||
rightAnswer: modelAnswer.rightAnswer || '',
|
||||
timestamp: r.createAt
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
...task,
|
||||
detail: {
|
||||
...detail,
|
||||
results // Include results from EvalResults table
|
||||
},
|
||||
modelInfo,
|
||||
evalDatasets: orderedDatasets
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch blind-test task details:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to fetch blind-test task details', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update blind-test task (interrupt/stop)
|
||||
*/
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { projectId, taskId } = params;
|
||||
const { action } = await request.json();
|
||||
|
||||
const task = await db.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ code: 404, error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (action === 'interrupt') {
|
||||
if (task.status !== 0) {
|
||||
return NextResponse.json({ code: 400, error: 'Only running tasks can be interrupted' }, { status: 400 });
|
||||
}
|
||||
|
||||
const updatedTask = await db.task.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
status: 3, // Interrupted
|
||||
endTime: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: updatedTask,
|
||||
message: 'Task interrupted'
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ code: 400, error: 'Unknown action' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Failed to update blind-test task:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to update blind-test task', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete blind-test task and its results
|
||||
*/
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { projectId, taskId } = params;
|
||||
|
||||
const task = await db.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ code: 404, error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete related EvalResults first
|
||||
await db.evalResults.deleteMany({
|
||||
where: { taskId }
|
||||
});
|
||||
|
||||
// Then delete the task
|
||||
await db.task.delete({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
message: 'Task deleted'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to delete blind-test task:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to delete blind-test task', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
import LLMClient from '@/lib/llm/core/index';
|
||||
import { getModelConfigById } from '@/lib/db/model-config';
|
||||
|
||||
/**
|
||||
* Stream answer for a specified model
|
||||
* Query param: model=A or model=B
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
const { projectId, taskId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const modelType = searchParams.get('model'); // 'A' or 'B'
|
||||
|
||||
try {
|
||||
if (!projectId || !taskId) {
|
||||
return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!modelType || !['A', 'B'].includes(modelType)) {
|
||||
return NextResponse.json({ error: 'Model type must be specified (A or B)' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch task
|
||||
const task = await db.task.findUnique({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
if (!task || task.taskType !== 'blind-test') {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Parse task detail
|
||||
const detail = JSON.parse(task.detail || '{}');
|
||||
const modelInfo = JSON.parse(task.modelInfo || '{}');
|
||||
// Support both evalDatasetIds and questionIds
|
||||
const questionIds = detail.questionIds || detail.evalDatasetIds || [];
|
||||
const currentIndex = detail.currentIndex || 0;
|
||||
|
||||
// Check if task is completed
|
||||
if (questionIds.length === 0 || currentIndex >= questionIds.length) {
|
||||
return NextResponse.json({ completed: true });
|
||||
}
|
||||
|
||||
// Fetch current question
|
||||
const currentQuestionId = questionIds[currentIndex];
|
||||
const currentQuestion = await db.evalDatasets.findUnique({
|
||||
where: { id: currentQuestionId }
|
||||
});
|
||||
|
||||
if (!currentQuestion) {
|
||||
return NextResponse.json({ error: 'Question not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Resolve model config based on modelType
|
||||
const modelConfigKey = modelType === 'A' ? 'modelA' : 'modelB';
|
||||
const modelConfig = await getModelConfigById(modelInfo[modelConfigKey].id);
|
||||
|
||||
if (!modelConfig) {
|
||||
return NextResponse.json({ error: 'Model configuration not found' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Prepare messages
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: "You are a helpful assistant. Provide detailed and accurate answers to the user's question."
|
||||
},
|
||||
{ role: 'user', content: currentQuestion.question }
|
||||
];
|
||||
|
||||
// Create LLM client
|
||||
const client = new LLMClient({
|
||||
projectId,
|
||||
...modelConfig
|
||||
});
|
||||
|
||||
// Call streaming API and return response directly
|
||||
const response = await client.chatStreamAPI(messages);
|
||||
|
||||
return new Response(response.body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Model ${modelType} streaming call failed:`, error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
import LLMClient from '@/lib/llm/core/index';
|
||||
import { getModelConfigById } from '@/lib/db/model-config';
|
||||
|
||||
/**
|
||||
* Stream answers from two models for the current question
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
const { projectId, taskId } = params;
|
||||
|
||||
try {
|
||||
if (!projectId || !taskId) {
|
||||
return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch task
|
||||
const task = await db.task.findUnique({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
if (!task || task.taskType !== 'blind-test') {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Parse task detail
|
||||
const detail = JSON.parse(task.detail || '{}');
|
||||
const modelInfo = JSON.parse(task.modelInfo || '{}');
|
||||
const { questionIds = [], currentIndex = 0 } = detail;
|
||||
|
||||
// Check if task is completed
|
||||
if (currentIndex >= questionIds.length) {
|
||||
return NextResponse.json({ completed: true });
|
||||
}
|
||||
|
||||
// Fetch current question
|
||||
const currentQuestionId = questionIds[currentIndex];
|
||||
const currentQuestion = await db.evalDatasets.findUnique({
|
||||
where: { id: currentQuestionId }
|
||||
});
|
||||
|
||||
if (!currentQuestion) {
|
||||
return NextResponse.json({ error: 'Question not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Fetch model configs
|
||||
const [modelConfigA, modelConfigB] = await Promise.all([
|
||||
getModelConfigById(modelInfo.modelA.providerId),
|
||||
getModelConfigById(modelInfo.modelB.providerId)
|
||||
]);
|
||||
|
||||
if (!modelConfigA || !modelConfigB) {
|
||||
return NextResponse.json({ error: 'Model configuration not found' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Randomly swap positions (core blind-test behavior)
|
||||
const isSwapped = Math.random() > 0.5;
|
||||
|
||||
// Create streaming response
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
// Send init message
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'init',
|
||||
question: currentQuestion.question,
|
||||
questionId: currentQuestion.id,
|
||||
questionIndex: currentIndex + 1,
|
||||
totalQuestions: questionIds.length,
|
||||
isSwapped
|
||||
}) + '\n'
|
||||
)
|
||||
);
|
||||
|
||||
// Prepare messages
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: "You are a helpful assistant. Provide detailed and accurate answers to the user's question."
|
||||
},
|
||||
{ role: 'user', content: currentQuestion.question }
|
||||
];
|
||||
|
||||
// Create LLM clients
|
||||
const clientA = new LLMClient({
|
||||
projectId,
|
||||
...modelConfigA
|
||||
});
|
||||
|
||||
const clientB = new LLMClient({
|
||||
projectId,
|
||||
...modelConfigB
|
||||
});
|
||||
|
||||
let answerA = '';
|
||||
let answerB = '';
|
||||
const startTime = Date.now();
|
||||
|
||||
// Call both models in parallel (streaming)
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
const response = await clientA.chatStreamAPI(messages);
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
answerA += chunk;
|
||||
|
||||
// Send chunk update
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'chunk',
|
||||
model: isSwapped ? 'B' : 'A',
|
||||
content: chunk
|
||||
}) + '\n'
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Model A call failed:', err);
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
model: isSwapped ? 'B' : 'A',
|
||||
error: err.message
|
||||
}) + '\n'
|
||||
)
|
||||
);
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
const response = await clientB.chatStreamAPI(messages);
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
answerB += chunk;
|
||||
|
||||
// Send chunk update
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'chunk',
|
||||
model: isSwapped ? 'A' : 'B',
|
||||
content: chunk
|
||||
}) + '\n'
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Model B call failed:', err);
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
model: isSwapped ? 'A' : 'B',
|
||||
error: err.message
|
||||
}) + '\n'
|
||||
)
|
||||
);
|
||||
}
|
||||
})()
|
||||
]);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Send done message
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
type: 'done',
|
||||
duration,
|
||||
answerA: isSwapped ? answerB : answerA,
|
||||
answerB: isSwapped ? answerA : answerB
|
||||
}) + '\n'
|
||||
)
|
||||
);
|
||||
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
console.error('Streaming handler failed:', error);
|
||||
controller.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('API error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* Submit vote result
|
||||
* vote: 'left' | 'right' | 'both_good' | 'both_bad'
|
||||
* Results are stored in EvalResults table
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId, taskId } = params;
|
||||
const { vote, questionId, isSwapped, leftAnswer, rightAnswer } = await request.json();
|
||||
|
||||
// Validate vote option
|
||||
const validVotes = ['left', 'right', 'both_good', 'both_bad'];
|
||||
if (!validVotes.includes(vote)) {
|
||||
return NextResponse.json({ code: 400, error: 'Invalid vote option' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!questionId) {
|
||||
return NextResponse.json({ code: 400, error: 'Question ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const task = await db.task.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
}
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ code: 404, error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (task.status !== 0) {
|
||||
return NextResponse.json({ code: 400, error: 'Task has ended' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Parse task details
|
||||
let detail = {};
|
||||
try {
|
||||
detail = task.detail ? JSON.parse(task.detail) : {};
|
||||
} catch (e) {
|
||||
console.error('Failed to parse task detail:', e);
|
||||
}
|
||||
|
||||
// Calculate scores
|
||||
// isSwapped: true means left is model B and right is model A
|
||||
// isSwapped: false means left is model A and right is model B
|
||||
let modelAScore = 0;
|
||||
let modelBScore = 0;
|
||||
|
||||
if (vote === 'left') {
|
||||
if (isSwapped) {
|
||||
modelBScore = 1; // Left is B
|
||||
} else {
|
||||
modelAScore = 1; // Left is A
|
||||
}
|
||||
} else if (vote === 'right') {
|
||||
if (isSwapped) {
|
||||
modelAScore = 1; // Right is A
|
||||
} else {
|
||||
modelBScore = 1; // Right is B
|
||||
}
|
||||
} else if (vote === 'both_good') {
|
||||
modelAScore = 0.5;
|
||||
modelBScore = 0.5;
|
||||
}
|
||||
// both_bad: both scores remain 0
|
||||
|
||||
// Store result in EvalResults table
|
||||
const evalResult = await db.evalResults.create({
|
||||
data: {
|
||||
projectId,
|
||||
taskId,
|
||||
evalDatasetId: questionId,
|
||||
modelAnswer: JSON.stringify({
|
||||
leftAnswer: leftAnswer || '',
|
||||
rightAnswer: rightAnswer || ''
|
||||
}),
|
||||
score: modelAScore, // Store modelA score for sorting/aggregation
|
||||
isCorrect: false, // Not applicable for blind-test
|
||||
judgeResponse: JSON.stringify({
|
||||
vote,
|
||||
isSwapped,
|
||||
modelAScore,
|
||||
modelBScore
|
||||
}),
|
||||
duration: 0,
|
||||
status: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Update task progress
|
||||
const evalDatasetIds = detail.evalDatasetIds || [];
|
||||
const newCurrentIndex = (detail.currentIndex || 0) + 1;
|
||||
const isCompleted = newCurrentIndex >= evalDatasetIds.length;
|
||||
|
||||
const updatedDetail = {
|
||||
...detail,
|
||||
currentIndex: newCurrentIndex
|
||||
};
|
||||
|
||||
await db.task.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
detail: JSON.stringify(updatedDetail),
|
||||
completedCount: newCurrentIndex,
|
||||
status: isCompleted ? 1 : 0, // 1-completed, 0-running
|
||||
endTime: isCompleted ? new Date() : null
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate current total scores from EvalResults
|
||||
const allResults = await db.evalResults.findMany({
|
||||
where: { taskId },
|
||||
select: { judgeResponse: true }
|
||||
});
|
||||
|
||||
let totalModelAScore = 0;
|
||||
let totalModelBScore = 0;
|
||||
for (const r of allResults) {
|
||||
try {
|
||||
const judge = JSON.parse(r.judgeResponse || '{}');
|
||||
totalModelAScore += judge.modelAScore || 0;
|
||||
totalModelBScore += judge.modelBScore || 0;
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
success: true,
|
||||
isCompleted,
|
||||
currentIndex: newCurrentIndex,
|
||||
totalCount: evalDatasetIds.length,
|
||||
scores: {
|
||||
modelA: totalModelAScore,
|
||||
modelB: totalModelBScore
|
||||
}
|
||||
},
|
||||
message: isCompleted ? 'Blind-test task completed' : 'Vote recorded'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to submit vote result:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to submit vote result', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* Get all blind-test tasks for a project
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
||||
|
||||
if (!projectId) {
|
||||
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
// Fetch task list and total count
|
||||
const [tasks, total] = await Promise.all([
|
||||
db.task.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
},
|
||||
orderBy: { createAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize
|
||||
}),
|
||||
db.task.count({
|
||||
where: {
|
||||
projectId,
|
||||
taskType: 'blind-test'
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
// Fetch evaluation results for all tasks to calculate scores
|
||||
const taskIds = tasks.map(t => t.id);
|
||||
const allEvalResults = await db.evalResults.findMany({
|
||||
where: { taskId: { in: taskIds } },
|
||||
select: {
|
||||
taskId: true,
|
||||
judgeResponse: true
|
||||
}
|
||||
});
|
||||
|
||||
// Group results by taskId and calculate scores
|
||||
const taskScores = {};
|
||||
for (const result of allEvalResults) {
|
||||
if (!taskScores[result.taskId]) {
|
||||
taskScores[result.taskId] = { modelAScore: 0, modelBScore: 0 };
|
||||
}
|
||||
try {
|
||||
const judge = JSON.parse(result.judgeResponse || '{}');
|
||||
taskScores[result.taskId].modelAScore += judge.modelAScore || 0;
|
||||
taskScores[result.taskId].modelBScore += judge.modelBScore || 0;
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Parse task detail fields and attach scores
|
||||
const tasksWithDetails = tasks.map(task => {
|
||||
let detail = {};
|
||||
let modelInfo = {};
|
||||
try {
|
||||
detail = task.detail ? JSON.parse(task.detail) : {};
|
||||
modelInfo = task.modelInfo ? JSON.parse(task.modelInfo) : {};
|
||||
} catch (e) {
|
||||
console.error('Failed to parse task detail:', e);
|
||||
}
|
||||
|
||||
// Attach calculated scores as results array
|
||||
const scores = taskScores[task.id] || { modelAScore: 0, modelBScore: 0 };
|
||||
const results = [
|
||||
{
|
||||
modelAScore: scores.modelAScore,
|
||||
modelBScore: scores.modelBScore
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
...task,
|
||||
detail: {
|
||||
...detail,
|
||||
results // Attach results for display in task card
|
||||
},
|
||||
modelInfo
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
items: tasksWithDetails,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch blind-test task list:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to fetch blind-test task list', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blind-test task
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const data = await request.json();
|
||||
|
||||
const { modelA, modelB, evalDatasetIds, language = 'zh-CN' } = data;
|
||||
|
||||
if (!modelA || !modelA.modelId || !modelA.providerId) {
|
||||
return NextResponse.json({ code: 400, error: 'Please select model A' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!modelB || !modelB.modelId || !modelB.providerId) {
|
||||
return NextResponse.json({ code: 400, error: 'Please select model B' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (modelA.modelId === modelB.modelId && modelA.providerId === modelB.providerId) {
|
||||
return NextResponse.json({ code: 400, error: 'The two models must be different' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!evalDatasetIds || evalDatasetIds.length === 0) {
|
||||
return NextResponse.json({ code: 400, error: 'Please select questions to evaluate' }, { status: 400 });
|
||||
}
|
||||
|
||||
const evalDatasets = await db.evalDatasets.findMany({
|
||||
where: {
|
||||
id: { in: evalDatasetIds },
|
||||
projectId
|
||||
},
|
||||
select: { id: true, questionType: true }
|
||||
});
|
||||
|
||||
const invalidQuestions = evalDatasets.filter(
|
||||
q => q.questionType !== 'short_answer' && q.questionType !== 'open_ended'
|
||||
);
|
||||
|
||||
if (invalidQuestions.length > 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 400,
|
||||
error: 'Blind-test tasks only support short-answer and open-ended questions'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch model config info
|
||||
const [modelConfigA, modelConfigB] = await Promise.all([
|
||||
db.modelConfig.findFirst({
|
||||
where: { projectId, providerId: modelA.providerId, modelId: modelA.modelId }
|
||||
}),
|
||||
db.modelConfig.findFirst({
|
||||
where: { projectId, providerId: modelB.providerId, modelId: modelB.modelId }
|
||||
})
|
||||
]);
|
||||
|
||||
// Build model info (two models)
|
||||
const modelInfo = {
|
||||
modelA: {
|
||||
id: modelConfigA?.id,
|
||||
modelId: modelA.modelId,
|
||||
modelName: modelConfigA?.modelName || modelA.modelId,
|
||||
providerId: modelA.providerId,
|
||||
providerName: modelConfigA?.providerName || modelA.providerId
|
||||
},
|
||||
modelB: {
|
||||
id: modelConfigB?.id,
|
||||
modelId: modelB.modelId,
|
||||
modelName: modelConfigB?.modelName || modelB.modelId,
|
||||
providerId: modelB.providerId,
|
||||
providerName: modelConfigB?.providerName || modelB.providerId
|
||||
}
|
||||
};
|
||||
|
||||
// Build task detail (only store evalDatasetIds and currentIndex)
|
||||
const taskDetail = {
|
||||
evalDatasetIds,
|
||||
currentIndex: 0 // Current question index
|
||||
};
|
||||
|
||||
// Create task
|
||||
const newTask = await db.task.create({
|
||||
data: {
|
||||
projectId,
|
||||
taskType: 'blind-test',
|
||||
status: 0, // Running
|
||||
modelInfo: JSON.stringify(modelInfo),
|
||||
language,
|
||||
detail: JSON.stringify(taskDetail),
|
||||
totalCount: evalDatasetIds.length,
|
||||
completedCount: 0,
|
||||
note: ''
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
...newTask,
|
||||
detail: taskDetail,
|
||||
modelInfo
|
||||
},
|
||||
message: 'Blind-test task created'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create blind-test task:', error);
|
||||
return NextResponse.json(
|
||||
{ code: 500, error: 'Failed to create blind-test task', message: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user