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,202 @@
'use client';
import { Container, Box, Typography, Alert, Snackbar, Paper } from '@mui/material';
import { useEffect } from 'react';
import ChunkViewDialog from '@/components/text-split/ChunkViewDialog';
import DatasetHeader from '@/components/datasets/DatasetHeader';
import DatasetMetadata from '@/components/datasets/DatasetMetadata';
import EditableField from '@/components/datasets/EditableField';
import OptimizeDialog from '@/components/datasets/OptimizeDialog';
import DatasetRatingSection from '@/components/datasets/DatasetRatingSection';
import useDatasetDetails from '@/app/projects/[projectId]/datasets/[datasetId]/useDatasetDetails';
import { useTranslation } from 'react-i18next';
/**
* 数据集详情页面
*/
export default function DatasetDetailsPage({ params }) {
const { projectId, datasetId } = params;
const { t } = useTranslation();
// 使用自定义Hook管理状态和逻辑
const {
currentDataset,
loading,
editingAnswer,
editingCot,
editingQuestion,
answerValue,
cotValue,
questionValue,
snackbar,
confirming,
unconfirming,
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
} = useDatasetDetails(projectId, datasetId);
// 加载状态
if (loading) {
return (
<Container maxWidth="lg" sx={{ mt: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '70vh' }}>
<Alert severity="info">{t('datasets.loadingDataset')}</Alert>
</Box>
</Container>
);
}
// 无数据状态
if (!currentDataset) {
return (
<Container maxWidth="lg" sx={{ mt: 4 }}>
<Alert severity="error">{t('datasets.datasetNotFound')}</Alert>
</Container>
);
}
return (
<Container maxWidth="xl" sx={{ mt: 4, mb: 4 }}>
{/* 顶部导航栏 */}
<DatasetHeader
projectId={projectId}
datasetsAllCount={datasetsAllCount}
datasetsConfirmCount={datasetsConfirmCount}
confirming={confirming}
unconfirming={unconfirming}
currentDataset={currentDataset}
shortcutsEnabled={shortcutsEnabled}
setShortcutsEnabled={setShortcutsEnabled}
onNavigate={handleNavigate}
onConfirm={handleConfirm}
onUnconfirm={handleUnconfirm}
onDelete={handleDelete}
/>
{/* 主要布局:左右分栏 */}
<Box sx={{ display: 'flex', gap: 3, alignItems: 'flex-start' }}>
{/* 左侧主要内容区域 */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<Paper sx={{ p: 3 }}>
<EditableField
label={t('datasets.question')}
value={questionValue}
editing={editingQuestion}
onEdit={() => setEditingQuestion(true)}
onChange={e => setQuestionValue(e.target.value)}
onSave={() => handleSave('question', questionValue)}
dataset={currentDataset}
onCancel={() => {
setEditingQuestion(false);
setQuestionValue(currentDataset.question);
}}
/>
<EditableField
label={t('datasets.answer')}
value={answerValue}
editing={editingAnswer}
onEdit={() => setEditingAnswer(true)}
onChange={e => setAnswerValue(e.target.value)}
onSave={() => handleSave('answer', answerValue)}
onCancel={() => {
setEditingAnswer(false);
setAnswerValue(currentDataset.answer);
}}
dataset={currentDataset}
onOptimize={handleOpenOptimizeDialog}
tokenCount={answerTokens}
optimizing={optimizeDialog.loading}
/>
<EditableField
label={t('datasets.cot')}
value={cotValue}
editing={editingCot}
onEdit={() => setEditingCot(true)}
onChange={e => setCotValue(e.target.value)}
onSave={() => handleSave('cot', cotValue)}
dataset={currentDataset}
onCancel={() => {
setEditingCot(false);
setCotValue(currentDataset.cot || '');
}}
tokenCount={cotTokens}
/>
</Paper>
</Box>
{/* 右侧固定侧边栏 */}
<Box
sx={{
width: 360,
position: 'sticky',
top: 24,
maxHeight: 'calc(100vh - 48px)',
overflowY: 'auto'
}}
>
{/* 数据集元数据信息 */}
<DatasetMetadata currentDataset={currentDataset} onViewChunk={handleViewChunk} />
{/* 评分、标签、备注区域 */}
<DatasetRatingSection
dataset={currentDataset}
projectId={projectId}
onUpdate={() => {
// 更新成功后刷新数据,保持页面状态同步
// 这里可以调用 useDatasetDetails 的刷新逻辑
}}
currentDataset={currentDataset}
/>
</Box>
</Box>
{/* 消息提示 */}
<Snackbar
open={snackbar.open}
autoHideDuration={2000}
onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
severity={snackbar.severity}
sx={{ width: '100%' }}
>
{snackbar.message}
</Alert>
</Snackbar>
{/* AI优化对话框 */}
<OptimizeDialog open={optimizeDialog.open} onClose={handleCloseOptimizeDialog} onConfirm={handleOptimize} />
{/* 文本块详情对话框 */}
<ChunkViewDialog open={viewDialogOpen} chunk={viewChunk} onClose={handleCloseViewDialog} />
</Container>
);
}

View File

@@ -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
};
}