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,14 @@
import { useEffect, useState } from 'react';
export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}

View File

@@ -0,0 +1,57 @@
import { useState, useEffect } from 'react';
// 存储文件处理状态的共享对象
const fileProcessingSubscribers = {
value: false,
listeners: new Set()
};
// 存储文件任务信息的共享对象
const fileTaskSubscribers = {
value: null,
listeners: new Set()
};
/**
* 自定义hook用于在组件间共享文件处理任务的状态
*/
export default function useFileProcessingStatus() {
const [taskFileProcessing, setTaskFileProcessing] = useState(fileProcessingSubscribers.value);
const [task, setTask] = useState(fileTaskSubscribers.value);
useEffect(() => {
// 添加当前组件为订阅者
const updateProcessingState = newValue => setTaskFileProcessing(newValue);
const updateTaskState = newTask => setTask(newTask);
fileProcessingSubscribers.listeners.add(updateProcessingState);
fileTaskSubscribers.listeners.add(updateTaskState);
// 组件卸载时清理
return () => {
fileProcessingSubscribers.listeners.delete(updateProcessingState);
fileTaskSubscribers.listeners.delete(updateTaskState);
};
}, []);
// 共享的setState函数
const setSharedFileProcessing = newValue => {
fileProcessingSubscribers.value = newValue;
// 通知所有订阅者
fileProcessingSubscribers.listeners.forEach(listener => listener(newValue));
};
// 共享的setTask函数
const setSharedTask = newTask => {
fileTaskSubscribers.value = newTask;
// 通知所有订阅者
fileTaskSubscribers.listeners.forEach(listener => listener(newTask));
};
return {
taskFileProcessing,
task,
setTaskFileProcessing: setSharedFileProcessing,
setTask: setSharedTask
};
}

View File

@@ -0,0 +1,135 @@
import { useCallback } from 'react';
import { toast } from 'sonner';
import i18n from '@/lib/i18n';
import axios from 'axios';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
import { useTranslation } from 'react-i18next';
export function useGenerateDataset() {
const model = useAtomValue(selectedModelInfoAtom);
const { t } = useTranslation();
const generateSingleDataset = useCallback(
async ({ projectId, questionId, questionInfo, imageId, imageName }) => {
// 获取模型参数
if (!model) {
toast.error(t('models.configNotFound'));
return null;
}
// 判断是否为图片问题
const isImageQuestion = !!imageId;
// 调用API生成数据集
const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';
if (isImageQuestion) {
// 图片问题:调用图片数据集生成接口
toast.promise(
axios.post(`/api/projects/${projectId}/images/datasets`, {
imageName,
question: { question: questionInfo, id: questionId },
model,
language: currentLanguage
}),
{
loading: t('datasets.generating'),
description: `图片:【${imageName}\n问题:【${questionInfo}`,
position: 'top-right',
success: data => {
return '生成数据集成功';
},
error: error => {
return t('datasets.generateFailed', { error: error.response?.data?.error });
}
}
);
} else {
// 文本问题:调用普通数据集生成接口
toast.promise(
axios.post(`/api/projects/${projectId}/datasets`, {
questionId,
model,
language: currentLanguage
}),
{
loading: t('datasets.generating'),
description: `问题:【${questionInfo}`,
position: 'top-right',
success: data => {
return '生成数据集成功';
},
error: error => {
return t('datasets.generateFailed', { error: error.response?.data?.error });
}
}
);
}
},
[model, t]
);
const generateMultipleDataset = useCallback(
async (projectId, questions) => {
let completed = 0;
const total = questions.length;
// 显示带进度的Loading
const loadingToastId = toast.loading(`正在处理请求 (${completed}/${total})...`, { position: 'top-right' });
// 处理每个请求
const processRequest = async question => {
try {
const isImageQuestion = !!question.imageId;
let response;
if (isImageQuestion) {
// 图片问题
response = await axios.post(`/api/projects/${projectId}/images/datasets`, {
imageName: question.imageName,
question,
model,
language: i18n.language === 'zh-CN' ? '中文' : 'en'
});
} else {
// 文本问题
response = await axios.post(`/api/projects/${projectId}/datasets`, {
questionId: question.id,
model,
language: i18n.language === 'zh-CN' ? '中文' : 'en'
});
}
const data = response.data;
completed++;
toast.success(`${question.question} 完成`, { position: 'top-right' });
toast.loading(`正在处理请求 (${completed}/${total})...`, { id: loadingToastId });
return data;
} catch (error) {
completed++;
toast.error(`${question.question} 失败`, {
description: error.message,
position: 'top-right'
});
toast.loading(`正在处理请求 (${completed}/${total})...`, { id: loadingToastId });
throw error;
}
};
try {
const results = await Promise.allSettled(questions.map(req => processRequest(req)));
// 全部完成后更新Loading为完成状态
toast.success(`全部请求处理完成 (成功: ${results.filter(r => r.status === 'fulfilled').length}/${total})`, {
id: loadingToastId,
position: 'top-right'
});
return results;
} catch {
// Promise.allSettled不会进入catch这里只是保险
}
},
[model, t]
);
return { generateSingleDataset, generateMultipleDataset };
}

View File

@@ -0,0 +1,406 @@
'use client';
import { useState, useEffect } from 'react';
import { useAtomValue } from 'jotai/index';
import { modelConfigListAtom } from '@/lib/store';
export default function useModelPlayground(projectId, defaultModelId = null) {
// 状态管理
const [selectedModels, setSelectedModels] = useState(defaultModelId ? [defaultModelId] : []);
const [loading, setLoading] = useState({});
const [userInput, setUserInput] = useState('');
const [conversations, setConversations] = useState({});
const [error, setError] = useState(null);
const [outputMode, setOutputMode] = useState('normal'); // 'normal' 或 'streaming'
const [uploadedImage, setUploadedImage] = useState(null); // 存储上传的图片Base64
const availableModels = useAtomValue(modelConfigListAtom);
// 初始化会话状态
useEffect(() => {
if (selectedModels.length > 0) {
const initialConversations = {};
selectedModels.forEach(modelId => {
if (!conversations[modelId]) {
initialConversations[modelId] = [];
}
});
if (Object.keys(initialConversations).length > 0) {
setConversations(prev => ({
...prev,
...initialConversations
}));
}
}
}, [selectedModels]);
// 处理模型选择
const handleModelSelection = event => {
const {
target: { value }
} = event;
// 限制最多选择 3 个模型
const selectedValues = typeof value === 'string' ? value.split(',') : value;
const limitedSelection = selectedValues.slice(0, 3);
setSelectedModels(limitedSelection);
};
// 处理用户输入
const handleInputChange = e => {
setUserInput(e.target.value);
};
// 处理图片上传
const handleImageUpload = e => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setUploadedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
// 删除已上传的图片
const handleRemoveImage = () => {
setUploadedImage(null);
};
// 处理输出模式切换
const handleOutputModeChange = event => {
setOutputMode(event.target.value);
};
// 发送消息给所有选中的模型
const handleSendMessage = async () => {
if (!userInput.trim() || Object.values(loading).some(value => value) || selectedModels.length === 0) return;
// 获取用户输入
const input = userInput.trim();
setUserInput('');
// 获取图片(如果有的话)
const image = uploadedImage;
setUploadedImage(null); // 清除图片
// 更新所有选中模型的对话
const updatedConversations = { ...conversations };
selectedModels.forEach(modelId => {
if (!updatedConversations[modelId]) {
updatedConversations[modelId] = [];
}
// 检查是否有图片并且当前模型是视觉模型
const model = availableModels.find(m => m.id === modelId);
const isVisionModel = model && model.type === 'vision';
if (isVisionModel && image) {
// 如果是视觉模型并且有图片,使用复合格式
updatedConversations[modelId].push({
role: 'user',
content: [
{ type: 'text', text: input || '请描述这个图片' },
{ type: 'image_url', image_url: { url: image } }
]
});
} else {
// 其他情况使用纯文本
updatedConversations[modelId].push({
role: 'user',
content: input
});
}
});
setConversations(updatedConversations);
// 为每个模型设置独立的加载状态
const updatedLoading = {};
selectedModels.forEach(modelId => {
updatedLoading[modelId] = true;
});
setLoading(updatedLoading);
// 为每个模型单独发送请求
selectedModels.forEach(async modelId => {
const model = availableModels.find(m => m.id === modelId);
if (!model) {
// 模型配置不存在
const modelConversation = [...(updatedConversations[modelId] || [])];
// 更新对话状态
setConversations(prev => ({
...prev,
[modelId]: [...modelConversation, { role: 'error', content: '模型配置不存在' }]
}));
// 更新加载状态
setLoading(prev => ({ ...prev, [modelId]: false }));
return;
}
try {
// 检查是否是视觉模型且有图片
const isVisionModel = model.type === 'vision';
// 构建请求消息
let requestMessages = [...updatedConversations[modelId]]; // 复制当前消息历史
// 如果是vision模型并且有图片将最后一条用户消息替换为包含图片的消息
if (isVisionModel && image && requestMessages.length > 0) {
// 找到最后一条用户消息
const lastUserMsgIndex = requestMessages.length - 1;
// 替换为包含图片的消息
requestMessages[lastUserMsgIndex] = {
role: 'user',
content: [
{ type: 'text', text: input || '请描述这个图片' },
{ type: 'image_url', image_url: { url: image } }
]
};
}
// 根据输出模式选择不同的处理方式
if (outputMode === 'streaming') {
// 流式输出处理
// 先添加一个空的助手回复,用于后续流式更新
setConversations(prev => {
const modelConversation = [...(prev[modelId] || [])];
return {
...prev,
[modelId]: [
...modelConversation,
{
role: 'assistant',
content: '',
isStreaming: true,
thinking: '', // 添加推理过程字段
showThinking: true // 默认显示推理过程
}
]
};
});
const response = await fetch(`/api/projects/${projectId}/playground/chat/stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: model,
messages: requestMessages
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let accumulatedContent = '';
// 状态变量,用于跟踪是否正在处理思维链
let isInThinking = false;
let currentThinking = '';
let currentContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码收到的数据块
const chunk = decoder.decode(value, { stream: true });
// 处理当前数据块
for (let i = 0; i < chunk.length; i++) {
const char = chunk[i];
// 检测开始标签 <think>
if (i + 6 <= chunk.length && chunk.substring(i, i + 7) === '<think>') {
isInThinking = true;
i += 6; // 跳过标签
continue;
}
// 检测结束标签 </think>
if (i + 7 <= chunk.length && chunk.substring(i, i + 8) === '</think>') {
isInThinking = false;
i += 7; // 跳过标签
continue;
}
// 根据当前状态添加到对应内容中
if (isInThinking) {
currentThinking += char;
} else {
currentContent += char;
}
}
// 累积全部内容以便最终处理
accumulatedContent += chunk;
// 更新对话内容
setConversations(prev => {
const modelConversation = [...prev[modelId]];
const lastIndex = modelConversation.length - 1;
// 更新最后一条消息的内容,包括思维链
modelConversation[lastIndex] = {
...modelConversation[lastIndex],
content: currentContent,
thinking: currentThinking,
showThinking: currentThinking.length > 0 // 只要有思维链内容就显示
};
return {
...prev,
[modelId]: modelConversation
};
});
}
// 完成流式传输,移除流式标记
// 使用刚刚实时跟踪的 currentThinking 和 currentContent作为最终的思维链和内容
let finalThinking = currentThinking;
let finalAnswer = currentContent;
// 如果到流结束时还在思维链中,确保解析完整的思维链内容
if (isInThinking) {
console.log('警告: 流结束时仍在思维链中,可能有标签不完整');
isInThinking = false;
}
setConversations(prev => {
const modelConversation = [...prev[modelId]];
const lastIndex = modelConversation.length - 1;
// 更新最后一条消息,移除流式标记
modelConversation[lastIndex] = {
role: 'assistant',
content: finalAnswer,
thinking: finalThinking,
showThinking: finalThinking ? true : false,
isStreaming: false
};
return {
...prev,
[modelId]: modelConversation
};
});
} else {
// 普通输出处理
const response = await fetch(`/api/projects/${projectId}/playground/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: {
...model,
extra_body: { enable_thinking: true } // 启用思考链
},
messages: requestMessages
})
});
// 获取响应数据
const data = await response.json();
// 独立更新此模型的对话状态
setConversations(prev => {
const modelConversation = [...(prev[modelId] || [])];
if (response.ok) {
// 处理可能包含思考链的内容
let thinking = '';
let content = data.response;
// 检查是否包含思考链
if (content && content.includes('<think>')) {
const thinkParts = content.split(/<think>(.*?)<\/think>/s);
if (thinkParts.length >= 3) {
thinking = thinkParts[1] || '';
// 移除思考链部分,只保留最终回答
content = thinkParts.filter((_, i) => i % 2 === 0).join('');
}
}
return {
...prev,
[modelId]: [
...modelConversation,
{
role: 'assistant',
content: content,
thinking: thinking,
showThinking: thinking ? true : false
}
]
};
} else {
return {
...prev,
[modelId]: [...modelConversation, { role: 'error', content: `错误: ${data.error || '请求失败'}` }]
};
}
});
}
} catch (error) {
console.error(`请求模型 ${model.name} 失败:`, error);
// 独立更新此模型的对话状态 - 添加错误消息
setConversations(prev => {
const modelConversation = [...(prev[modelId] || [])];
return {
...prev,
[modelId]: [...modelConversation, { role: 'error', content: `错误: ${error.message}` }]
};
});
} finally {
// 更新此模型的加载状态
setLoading(prev => ({ ...prev, [modelId]: false }));
}
});
};
// 清空所有对话
const handleClearConversations = () => {
const clearedConversations = {};
selectedModels.forEach(modelId => {
clearedConversations[modelId] = [];
});
setConversations(clearedConversations);
setLoading({});
};
// 获取模型名称
const getModelName = modelId => {
const model = availableModels.find(m => m.id === modelId);
return model ? `${model.provider}: ${model.name}` : modelId;
};
return {
availableModels,
selectedModels,
loading,
userInput,
conversations,
error,
outputMode,
uploadedImage,
handleModelSelection,
handleInputChange,
handleImageUpload,
handleRemoveImage,
handleSendMessage,
handleClearConversations,
handleOutputModeChange,
getModelName
};
}

View File

@@ -0,0 +1,73 @@
'use client';
import { useState, useCallback } from 'react';
import { Snackbar, Alert } from '@mui/material';
export const useSnackbar = () => {
const [open, setOpen] = useState(false);
const [message, setMessage] = useState('');
const [severity, setSeverity] = useState('info');
const showMessage = useCallback((newMessage, newSeverity = 'info') => {
setMessage(newMessage);
setSeverity(newSeverity);
setOpen(true);
}, []);
const showSuccess = useCallback(
message => {
showMessage(message, 'success');
},
[showMessage]
);
const showError = useCallback(
message => {
showMessage(message, 'error');
},
[showMessage]
);
const showInfo = useCallback(
message => {
showMessage(message, 'info');
},
[showMessage]
);
const showWarning = useCallback(
message => {
showMessage(message, 'warning');
},
[showMessage]
);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
const SnackbarComponent = useCallback(
() => (
<Snackbar
open={open}
autoHideDuration={2000}
onClose={handleClose}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert onClose={handleClose} severity={severity}>
{message}
</Alert>
</Snackbar>
),
[open, message, severity, handleClose]
);
return {
showMessage,
showSuccess,
showError,
showInfo,
showWarning,
SnackbarComponent
};
};

View File

@@ -0,0 +1,63 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { DEFAULT_SETTINGS } from '@/constant/setting';
export default function useTaskSettings(projectId) {
const { t } = useTranslation();
const [taskSettings, setTaskSettings] = useState({
...DEFAULT_SETTINGS
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
useEffect(() => {
async function fetchTaskSettings() {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/tasks`);
if (!response.ok) {
throw new Error(t('settings.fetchTasksFailed'));
}
const data = await response.json();
// 如果没有配置,使用默认值
if (Object.keys(data).length === 0) {
setTaskSettings({
...DEFAULT_SETTINGS
});
} else {
// 确保所有默认值都被正确设置,特别是数字类型的字段
const mergedSettings = {
...DEFAULT_SETTINGS,
...data
};
// 确保 multiTurnRounds 是数字类型
if (mergedSettings.multiTurnRounds !== undefined) {
mergedSettings.multiTurnRounds = Number(mergedSettings.multiTurnRounds);
}
setTaskSettings(mergedSettings);
}
} catch (error) {
console.error('获取任务配置出错:', error);
setError(error.message);
} finally {
setLoading(false);
}
}
fetchTaskSettings();
}, [projectId, t]);
return {
taskSettings,
setTaskSettings,
loading,
error,
success,
setSuccess
};
}