first-update
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
/**
|
||||
* 数据集详情页面业务逻辑 Hook
|
||||
*/
|
||||
export default function useDatasetDetails(projectId, datasetId) {
|
||||
const router = useRouter();
|
||||
const [datasets, setDatasets] = useState([]);
|
||||
const [currentDataset, setCurrentDataset] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingAnswer, setEditingAnswer] = useState(false);
|
||||
const [editingCot, setEditingCot] = useState(false);
|
||||
const [editingQuestion, setEditingQuestion] = useState(false);
|
||||
const [answerValue, setAnswerValue] = useState('');
|
||||
const [cotValue, setCotValue] = useState('');
|
||||
const [questionValue, setQuestionValue] = useState('');
|
||||
const [snackbar, setSnackbar] = useState({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success'
|
||||
});
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [unconfirming, setUnconfirming] = useState(false);
|
||||
const [optimizeDialog, setOptimizeDialog] = useState({
|
||||
open: false,
|
||||
loading: false
|
||||
});
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false);
|
||||
const [viewChunk, setViewChunk] = useState(null);
|
||||
const [datasetsAllCount, setDatasetsAllCount] = useState(0);
|
||||
const [datasetsConfirmCount, setDatasetsConfirmCount] = useState(0);
|
||||
const [answerTokens, setAnswerTokens] = useState(0);
|
||||
const [cotTokens, setCotTokens] = useState(0);
|
||||
const model = useAtomValue(selectedModelInfoAtom);
|
||||
const [shortcutsEnabled, setShortcutsEnabled] = useState(() => {
|
||||
const storedValue = localStorage.getItem('shortcutsEnabled');
|
||||
return storedValue !== null ? storedValue === 'true' : false;
|
||||
});
|
||||
|
||||
// 输入环境判断,避免在输入框/可编辑区域误触快捷键
|
||||
const isEditableTarget = el => {
|
||||
if (!el) return false;
|
||||
const tag = el.tagName?.toLowerCase();
|
||||
if (tag && ['input', 'textarea', 'select'].includes(tag)) return true;
|
||||
if (el.isContentEditable) return true;
|
||||
// 兼容嵌套的可编辑区域与常见富文本编辑器
|
||||
return !!el.closest?.('[contenteditable="true"], .ProseMirror, .ql-editor');
|
||||
};
|
||||
|
||||
// 简单节流,避免连续触发
|
||||
const lastShortcutRef = useRef(0);
|
||||
|
||||
// 异步获取Token数量
|
||||
const fetchTokenCount = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/datasets/${datasetId}/token-count`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.answerTokens !== undefined) {
|
||||
setAnswerTokens(data.answerTokens);
|
||||
}
|
||||
if (data.cotTokens !== undefined) {
|
||||
setCotTokens(data.cotTokens);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取Token数量失败:', error);
|
||||
// Token加载失败不阻塞主界面或显示错误提示
|
||||
}
|
||||
};
|
||||
|
||||
// 获取数据集详情
|
||||
const fetchDatasets = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/datasets/${datasetId}`);
|
||||
if (!response.ok) throw new Error('获取数据集详情失败');
|
||||
const data = await response.json();
|
||||
setCurrentDataset(data.datasets);
|
||||
setCotValue(data.datasets?.cot);
|
||||
setAnswerValue(data.datasets?.answer);
|
||||
setQuestionValue(data.datasets?.question);
|
||||
setDatasetsAllCount(data.total);
|
||||
setDatasetsConfirmCount(data.confirmedCount);
|
||||
|
||||
// 数据加载完成后,异步获取Token数量
|
||||
fetchTokenCount();
|
||||
} catch (error) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 确认并保存数据集
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
setConfirming(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirmed: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('操作失败');
|
||||
}
|
||||
|
||||
setCurrentDataset(prev => ({ ...prev, confirmed: true }));
|
||||
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: '操作成功',
|
||||
severity: 'success'
|
||||
});
|
||||
|
||||
// 导航到下一个数据集
|
||||
handleNavigate('next');
|
||||
} catch (error) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message || '操作失败',
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消确认数据集
|
||||
const handleUnconfirm = async () => {
|
||||
try {
|
||||
setUnconfirming(true);
|
||||
const response = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirmed: false
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('操作失败');
|
||||
}
|
||||
|
||||
setCurrentDataset(prev => ({ ...prev, confirmed: false }));
|
||||
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: '已取消确认',
|
||||
severity: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message || '取消确认失败',
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setUnconfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 导航到其他数据集
|
||||
const handleNavigate = async direction => {
|
||||
const response = await axios.get(`/api/projects/${projectId}/datasets/${datasetId}?operateType=${direction}`);
|
||||
if (response.data) {
|
||||
router.push(`/projects/${projectId}/datasets/${response.data.id}`);
|
||||
} else {
|
||||
toast.warning(`已经是${direction === 'next' ? '最后' : '第'}一条数据了`);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const handleSave = async (field, value) => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
[field]: value
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('保存失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setCurrentDataset(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: '保存成功',
|
||||
severity: 'success'
|
||||
});
|
||||
|
||||
// 重置编辑状态
|
||||
if (field === 'answer') setEditingAnswer(false);
|
||||
if (field === 'cot') setEditingCot(false);
|
||||
if (field === 'question') setEditingQuestion(false);
|
||||
} catch (error) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message || '保存失败',
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除数据集
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('确定要删除这条数据吗?此操作不可撤销。')) return;
|
||||
|
||||
try {
|
||||
// 尝试获取下一个数据集,在删除前先确保有可导航的目标
|
||||
const nextResponse = await axios.get(`/api/projects/${projectId}/datasets/${datasetId}?operateType=next`);
|
||||
const hasNextDataset = !!nextResponse.data;
|
||||
const nextDatasetId = hasNextDataset ? nextResponse.data.id : null;
|
||||
|
||||
// 删除当前数据集
|
||||
const deleteResponse = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!deleteResponse.ok) {
|
||||
throw new Error('删除失败');
|
||||
}
|
||||
|
||||
// 导航逻辑:有下一个就跳转下一个,没有则返回列表页
|
||||
if (hasNextDataset) {
|
||||
router.push(`/projects/${projectId}/datasets/${nextDatasetId}`);
|
||||
} else {
|
||||
// 没有更多数据集,返回列表页面
|
||||
router.push(`/projects/${projectId}/datasets`);
|
||||
}
|
||||
|
||||
toast.success('删除成功');
|
||||
} catch (error) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message || '删除失败',
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 优化对话框相关操作
|
||||
const handleOpenOptimizeDialog = () => {
|
||||
setOptimizeDialog({
|
||||
open: true,
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseOptimizeDialog = () => {
|
||||
setOptimizeDialog(prev => {
|
||||
// 如果正在优化,不允许关闭
|
||||
if (prev.loading) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
open: false,
|
||||
loading: false
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 优化操作
|
||||
const handleOptimize = async advice => {
|
||||
if (!model) {
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: '请先选择模型,可以在顶部导航栏选择',
|
||||
severity: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即关闭对话框,并设置优化中状态
|
||||
setOptimizeDialog(prev => {
|
||||
const newState = {
|
||||
open: false,
|
||||
loading: true
|
||||
};
|
||||
return newState;
|
||||
});
|
||||
|
||||
toast.info('已开始优化,请稍候...');
|
||||
|
||||
// 异步后台处理,不等待结果
|
||||
(async () => {
|
||||
try {
|
||||
const language = i18n.language === 'zh-CN' ? '中文' : 'en';
|
||||
const response = await fetch(`/api/projects/${projectId}/datasets/optimize`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
datasetId,
|
||||
model,
|
||||
advice,
|
||||
language
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || '优化失败');
|
||||
}
|
||||
|
||||
// 优化成功后,重新查询数据以获取最新状态
|
||||
await fetchDatasets();
|
||||
// 优化可能改变了文本内容,重新获取Token计数
|
||||
fetchTokenCount();
|
||||
|
||||
toast.success('AI智能优化成功');
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setOptimizeDialog({
|
||||
open: false,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
// 查看文本块详情
|
||||
const handleViewChunk = async chunkContent => {
|
||||
try {
|
||||
setViewChunk(chunkContent);
|
||||
setViewDialogOpen(true);
|
||||
} catch (error) {
|
||||
console.error('查看文本块出错', error);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
setViewDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭文本块详情对话框
|
||||
const handleCloseViewDialog = () => {
|
||||
setViewDialogOpen(false);
|
||||
};
|
||||
|
||||
// 初始化和快捷键事件
|
||||
useEffect(() => {
|
||||
fetchDatasets();
|
||||
}, [projectId, datasetId]);
|
||||
|
||||
// 快捷键状态变化
|
||||
useEffect(() => {
|
||||
localStorage.setItem('shortcutsEnabled', shortcutsEnabled);
|
||||
}, [shortcutsEnabled]);
|
||||
|
||||
// 监听键盘事件
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if (!shortcutsEnabled) return;
|
||||
|
||||
// 在输入框或可编辑区域时不触发
|
||||
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
if (isEditableTarget(event.target) || isEditableTarget(activeEl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅要求 Shift 修饰键,降低误触且更简单
|
||||
if (!event.shiftKey) return;
|
||||
|
||||
// 简单节流,过滤极短时间内重复触发
|
||||
const now = Date.now();
|
||||
if (now - (lastShortcutRef.current || 0) < 250) {
|
||||
return;
|
||||
}
|
||||
lastShortcutRef.current = now;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft': // 上一个(Shift + ArrowLeft)
|
||||
event.preventDefault();
|
||||
handleNavigate('prev');
|
||||
break;
|
||||
case 'ArrowRight': // 下一个(Shift + ArrowRight)
|
||||
event.preventDefault();
|
||||
handleNavigate('next');
|
||||
break;
|
||||
case 'y': // 确认(Shift + Y)
|
||||
case 'Y':
|
||||
if (!confirming && currentDataset && !currentDataset.confirmed) {
|
||||
event.preventDefault();
|
||||
handleConfirm();
|
||||
}
|
||||
break;
|
||||
case 'd': // 删除(Shift + D)
|
||||
case 'D':
|
||||
event.preventDefault();
|
||||
handleDelete();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [shortcutsEnabled, confirming, currentDataset]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
currentDataset,
|
||||
answerValue,
|
||||
cotValue,
|
||||
questionValue,
|
||||
editingAnswer,
|
||||
editingCot,
|
||||
editingQuestion,
|
||||
confirming,
|
||||
unconfirming,
|
||||
snackbar,
|
||||
optimizeDialog,
|
||||
viewDialogOpen,
|
||||
viewChunk,
|
||||
datasetsAllCount,
|
||||
datasetsConfirmCount,
|
||||
answerTokens,
|
||||
cotTokens,
|
||||
shortcutsEnabled,
|
||||
setShortcutsEnabled,
|
||||
setSnackbar,
|
||||
setAnswerValue,
|
||||
setCotValue,
|
||||
setQuestionValue,
|
||||
setEditingAnswer,
|
||||
setEditingCot,
|
||||
setEditingQuestion,
|
||||
handleNavigate,
|
||||
handleConfirm,
|
||||
handleUnconfirm,
|
||||
handleSave,
|
||||
handleDelete,
|
||||
handleOpenOptimizeDialog,
|
||||
handleCloseOptimizeDialog,
|
||||
handleOptimize,
|
||||
handleViewChunk,
|
||||
handleCloseViewDialog
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user