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

406 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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
};
}