Files
YG-Datasets/easy-dataset-main/app/projects/[projectId]/blind-test-tasks/hooks/useBlindTestDetail.js

406 lines
13 KiB
JavaScript
Raw Normal View History

2026-03-17 14:36:31 +08:00
'use client';
import { useState, useCallback, useEffect, useRef } from 'react';
/**
* 盲测任务详情和盲测过程管理 Hook
*/
export default function useBlindTestDetail(projectId, taskId) {
// 任务详情
const [task, setTask] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// 当前题目状态
const [currentQuestion, setCurrentQuestion] = useState(null);
const [leftAnswer, setLeftAnswer] = useState(null);
const [rightAnswer, setRightAnswer] = useState(null);
const [isSwapped, setIsSwapped] = useState(false);
const [answersLoading, setAnswersLoading] = useState(false);
// 流式输出状态
const [streamingA, setStreamingA] = useState(false);
const [streamingB, setStreamingB] = useState(false);
const abortControllerRef = useRef(null);
const hasAutoLoadedRef = useRef(false);
// 投票状态
const [voting, setVoting] = useState(false);
const [completed, setCompleted] = useState(false);
// 加载任务详情
const loadTask = useCallback(
async (silent = false) => {
if (!projectId || !taskId) return;
try {
if (!silent) setLoading(true);
setError('');
// 添加时间戳防止缓存
const response = await fetch(`/api/projects/${projectId}/blind-test-tasks/${taskId}?t=${Date.now()}`, {
cache: 'no-store',
headers: {
Pragma: 'no-cache',
'Cache-Control': 'no-cache'
}
});
const result = await response.json();
if (result.code === 0) {
console.log('任务状态更新:', result.data.completedCount, '/', result.data.totalCount);
setTask(result.data);
// 检查任务是否已完成 (0=进行中, 1=已完成, 2=失败, 3=已中断)
if (result.data.status !== 0) {
setCompleted(true);
}
} else {
if (!silent) setError(result.error || '加载任务详情失败');
}
} catch (err) {
console.error('加载任务详情失败:', err);
if (!silent) setError('加载任务详情失败');
} finally {
if (!silent) setLoading(false);
}
},
[projectId, taskId]
);
// 流式获取当前题目和模型回答
const fetchCurrentQuestion = useCallback(async () => {
if (!projectId || !taskId) return;
// 取消上一次的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
try {
setAnswersLoading(true);
setError('');
setCurrentQuestion(null);
setLeftAnswer({ fullContent: '', content: '', thinking: '', isThinking: false, duration: 0, error: null });
setRightAnswer({ fullContent: '', content: '', thinking: '', isThinking: false, duration: 0, error: null });
// 1. 先获取题目信息
const questionRes = await fetch(`/api/projects/${projectId}/blind-test-tasks/${taskId}/question`, {
signal: controller.signal,
cache: 'no-store'
});
if (!questionRes.ok) throw new Error('获取题目失败');
const questionData = await questionRes.json();
if (questionData.completed) {
setCompleted(true);
return;
}
setCurrentQuestion({
id: questionData.questionId,
question: questionData.question,
answer: questionData.answer,
index: questionData.questionIndex,
total: questionData.totalQuestions
});
setIsSwapped(questionData.isSwapped);
setCompleted(false);
// 2. 并行调用两个模型的流式接口
setStreamingA(true);
setStreamingB(true);
const processStream = async (modelType, setAnswer, setStreaming) => {
const modelStartTime = Date.now();
try {
const streamUrl = `/api/projects/${projectId}/blind-test-tasks/${taskId}/stream-model?model=${modelType}`;
const response = await fetch(streamUrl, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`模型${modelType}调用失败: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullContent = '';
let currentContent = '';
let currentThinking = '';
let isInThinking = false;
let pendingBuffer = ''; // 用于处理跨 chunk 的标签识别
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
pendingBuffer += chunk;
// 处理缓冲区中的内容
while (pendingBuffer.length > 0) {
// 如果正在思考中,寻找结束标签
if (isInThinking) {
const endTagIndex = pendingBuffer.indexOf('</think>');
if (endTagIndex !== -1) {
const thinkingPart = pendingBuffer.substring(0, endTagIndex);
currentThinking += thinkingPart;
fullContent += thinkingPart + '</think>';
isInThinking = false;
pendingBuffer = pendingBuffer.substring(endTagIndex + 8);
continue;
} else {
// 没有找到结束标签,但可能缓冲区末尾包含了部分结束标签
// 保留最后 7 个字符("</think>" 长度为 8以防被截断
const safeLength = Math.max(0, pendingBuffer.length - 7);
const processingPart = pendingBuffer.substring(0, safeLength);
currentThinking += processingPart;
fullContent += processingPart;
pendingBuffer = pendingBuffer.substring(safeLength);
break; // 等待下一个 chunk
}
} else {
// 不在思考中,寻找开始标签
const startTagIndex = pendingBuffer.indexOf('<think>');
if (startTagIndex !== -1) {
const contentPart = pendingBuffer.substring(0, startTagIndex);
currentContent += contentPart;
fullContent += contentPart + '<think>';
isInThinking = true;
pendingBuffer = pendingBuffer.substring(startTagIndex + 7);
continue;
} else {
// 没有找到开始标签,保留最后 6 个字符以防开始标签被截断
const safeLength = Math.max(0, pendingBuffer.length - 6);
const processingPart = pendingBuffer.substring(0, safeLength);
currentContent += processingPart;
fullContent += processingPart;
pendingBuffer = pendingBuffer.substring(safeLength);
break; // 等待下一个 chunk
}
}
}
setAnswer(prev => ({
...prev,
fullContent,
content: currentContent,
thinking: currentThinking,
isThinking: isInThinking
}));
}
const modelDuration = Date.now() - modelStartTime;
setAnswer(prev => ({ ...prev, duration: modelDuration }));
setStreaming(false);
} catch (err) {
if (err.name === 'AbortError') return;
console.error(`模型${modelType}错误:`, err);
const modelDuration = Date.now() - modelStartTime;
setAnswer(prev => ({
...prev,
error: err.message,
duration: modelDuration
}));
setStreaming(false);
}
};
// 根据是否交换决定左右对应的模型
const leftModel = questionData.isSwapped ? 'B' : 'A';
const rightModel = questionData.isSwapped ? 'A' : 'B';
await Promise.all([
processStream(leftModel, setLeftAnswer, setStreamingA),
processStream(rightModel, setRightAnswer, setStreamingB)
]);
} catch (err) {
if (err.name === 'AbortError') return;
console.error('获取题目失败:', err);
setError(err.message || '获取当前题目失败');
setStreamingA(false);
setStreamingB(false);
} finally {
// 只有当前请求未被取消时才重置loading
if (abortControllerRef.current === controller) {
setAnswersLoading(false);
}
}
}, [projectId, taskId]);
// 提交投票
const submitVote = useCallback(
async vote => {
if (!projectId || !taskId || !currentQuestion) return { success: false };
try {
setVoting(true);
setError('');
const response = await fetch(`/api/projects/${projectId}/blind-test-tasks/${taskId}/vote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vote,
questionId: currentQuestion.id,
isSwapped,
// 使用 fullContent 提交,包含思考过程
leftAnswer: leftAnswer?.fullContent || leftAnswer?.content || '',
rightAnswer: rightAnswer?.fullContent || rightAnswer?.content || ''
})
});
const result = await response.json();
if (result.code === 0) {
// 等待任务状态更新(进度条)
await loadTask(true);
if (result.data.isCompleted) {
setCompleted(true);
} else {
// 获取下一题
await fetchCurrentQuestion();
}
return { success: true, data: result.data };
} else {
setError(result.error || '提交投票失败');
return { success: false, error: result.error };
}
} catch (err) {
console.error('提交投票失败:', err);
setError('提交投票失败');
return { success: false, error: '提交投票失败' };
} finally {
setVoting(false);
}
},
[projectId, taskId, currentQuestion, isSwapped, leftAnswer, rightAnswer, loadTask, fetchCurrentQuestion]
);
// 中断任务
const interruptTask = useCallback(async () => {
if (!projectId || !taskId) return false;
try {
const response = await fetch(`/api/projects/${projectId}/blind-test-tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'interrupt' })
});
const result = await response.json();
if (result.code === 0) {
setCompleted(true);
loadTask();
return true;
} else {
setError(result.error || '中断任务失败');
return false;
}
} catch (err) {
console.error('中断任务失败:', err);
setError('中断任务失败');
return false;
}
}, [projectId, taskId, loadTask]);
// 初始加载
useEffect(() => {
loadTask();
}, [loadTask]);
// 任务加载完成后,如果任务进行中,自动获取当前题目(只执行一次)
useEffect(() => {
if (task && task.status === 0 && !completed && !hasAutoLoadedRef.current && projectId && taskId) {
hasAutoLoadedRef.current = true;
fetchCurrentQuestion();
}
}, [task, completed, projectId, taskId, fetchCurrentQuestion]);
// 计算结果统计
const getResultStats = useCallback(() => {
if (!task?.detail?.results) return null;
const results = task.detail.results;
const totalModelAScore = results.reduce((sum, r) => sum + (r.modelAScore || 0), 0);
const totalModelBScore = results.reduce((sum, r) => sum + (r.modelBScore || 0), 0);
const leftWins = results.filter(r => r.vote === 'left').length;
const rightWins = results.filter(r => r.vote === 'right').length;
const bothGood = results.filter(r => r.vote === 'both_good').length;
const bothBad = results.filter(r => r.vote === 'both_bad').length;
// 计算实际模型胜出次数(需要考虑 swap
const modelAWins = results.filter(r => {
if (r.vote === 'left' && !r.isSwapped) return true;
if (r.vote === 'right' && r.isSwapped) return true;
return false;
}).length;
const modelBWins = results.filter(r => {
if (r.vote === 'left' && r.isSwapped) return true;
if (r.vote === 'right' && !r.isSwapped) return true;
return false;
}).length;
return {
totalQuestions: results.length,
modelAScore: totalModelAScore,
modelBScore: totalModelBScore,
modelAWins,
modelBWins,
ties: bothGood + bothBad,
bothGood,
bothBad,
leftWins,
rightWins
};
}, [task]);
// 组件卸载时取消请求
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return {
// 任务详情
task,
loading,
error,
setError,
loadTask,
// 当前题目状态
currentQuestion,
leftAnswer,
rightAnswer,
answersLoading,
// 流式状态
streamingA,
streamingB,
// 投票状态
voting,
completed,
// 操作
fetchCurrentQuestion,
submitVote,
interruptTask,
// 结果统计
getResultStats
};
}