Files

472 lines
13 KiB
JavaScript
Raw Permalink 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, 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
};
}