first-update
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Eval datasets list hook
|
||||
* @param {string} projectId
|
||||
*/
|
||||
export default function useEvalDatasets(projectId) {
|
||||
const [data, setData] = useState({ items: [], total: 0, stats: null, totalPages: 1 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const isInitialMount = useRef(true);
|
||||
const abortRef = useRef(null);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
|
||||
const [questionType, setQuestionType] = useState('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [debouncedKeyword, setDebouncedKeyword] = useState('');
|
||||
const [chunkId, setChunkId] = useState('');
|
||||
const [tags, setTags] = useState([]);
|
||||
|
||||
const setQuestionTypeWithReset = useCallback(value => {
|
||||
setQuestionType(value);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const setKeywordWithReset = useCallback(value => {
|
||||
setKeyword(value);
|
||||
}, []);
|
||||
|
||||
const setChunkIdWithReset = useCallback(value => {
|
||||
setChunkId(value);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const setTagsWithReset = useCallback(value => {
|
||||
setTags(value);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const [viewMode, setViewMode] = useState('card');
|
||||
const [selectedIds, setSelectedIds] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedKeyword(keyword);
|
||||
if (keyword !== debouncedKeyword) {
|
||||
setPage(1);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [keyword]);
|
||||
|
||||
const fetchDataRef = useRef(null);
|
||||
fetchDataRef.current = async (showLoading = true, options = {}) => {
|
||||
if (!projectId) return;
|
||||
|
||||
const includeStats = options.forceStats || showLoading;
|
||||
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
}
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
if (showLoading) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setSearching(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
pageSize: String(pageSize),
|
||||
includeStats: includeStats ? 'true' : 'false'
|
||||
});
|
||||
|
||||
if (questionType) params.append('questionType', questionType);
|
||||
if (debouncedKeyword) params.append('keyword', debouncedKeyword);
|
||||
if (chunkId) params.append('chunkId', chunkId);
|
||||
if (tags.length > 0) {
|
||||
tags.forEach(tag => params.append('tags', tag));
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/eval-datasets?${params}`, {
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch eval datasets');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setData(prev => ({
|
||||
...result,
|
||||
stats: result.stats ?? prev.stats
|
||||
}));
|
||||
} catch (err) {
|
||||
if (err?.name === 'AbortError') return;
|
||||
setError(err.message);
|
||||
} finally {
|
||||
if (abortRef.current === controller) {
|
||||
abortRef.current = null;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
setLoading(false);
|
||||
} else {
|
||||
setSearching(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = useCallback((showLoading = true, options = {}) => {
|
||||
return fetchDataRef.current?.(showLoading, options);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
fetchDataRef.current?.(true, { forceStats: true });
|
||||
} else {
|
||||
fetchDataRef.current?.(false, { forceStats: false });
|
||||
}
|
||||
}, [projectId, page, pageSize, questionType, debouncedKeyword, chunkId, tags]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const deleteItems = useCallback(
|
||||
async ids => {
|
||||
if (!ids || ids.length === 0) return;
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/eval-datasets`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete items');
|
||||
}
|
||||
|
||||
await fetchData(true, { forceStats: true });
|
||||
setSelectedIds([]);
|
||||
|
||||
return await response.json();
|
||||
},
|
||||
[projectId, fetchData]
|
||||
);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
setQuestionType('');
|
||||
setKeyword('');
|
||||
setChunkId('');
|
||||
setTags([]);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const toggleSelect = useCallback(id => {
|
||||
setSelectedIds(prev => (prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]));
|
||||
}, []);
|
||||
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
if (selectedIds.length === data.items.length) {
|
||||
setSelectedIds([]);
|
||||
} else {
|
||||
setSelectedIds(data.items.map(item => item.id));
|
||||
}
|
||||
}, [selectedIds, data.items]);
|
||||
|
||||
return {
|
||||
items: data.items,
|
||||
total: data.total,
|
||||
stats: data.stats,
|
||||
totalPages: data.totalPages || 1,
|
||||
|
||||
loading,
|
||||
searching,
|
||||
error,
|
||||
|
||||
page,
|
||||
pageSize,
|
||||
setPage,
|
||||
setPageSize,
|
||||
|
||||
questionType,
|
||||
keyword,
|
||||
chunkId,
|
||||
tags,
|
||||
setQuestionType: setQuestionTypeWithReset,
|
||||
setKeyword: setKeywordWithReset,
|
||||
setChunkId: setChunkIdWithReset,
|
||||
setTags: setTagsWithReset,
|
||||
resetFilters,
|
||||
|
||||
viewMode,
|
||||
setViewMode,
|
||||
|
||||
selectedIds,
|
||||
toggleSelect,
|
||||
toggleSelectAll,
|
||||
setSelectedIds,
|
||||
|
||||
fetchData,
|
||||
deleteItems
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* 评估数据集导出 Hook
|
||||
* 管理导出对话框状态、筛选条件和导出逻辑
|
||||
*/
|
||||
export default function useExportEvalDatasets(projectId, stats = {}) {
|
||||
// 对话框状态
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 导出配置
|
||||
const [format, setFormat] = useState('json');
|
||||
const [questionTypes, setQuestionTypes] = useState([]);
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
// 预览数据
|
||||
const [previewTotal, setPreviewTotal] = useState(0);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
|
||||
// 从 stats 中获取可用的标签列表
|
||||
const availableTags = stats?.byTag ? Object.keys(stats.byTag).sort() : [];
|
||||
|
||||
// 当筛选条件变化时,获取预览数量
|
||||
useEffect(() => {
|
||||
if (!dialogOpen || !projectId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchPreview = async () => {
|
||||
try {
|
||||
setPreviewLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (questionTypes.length > 0) {
|
||||
questionTypes.forEach(t => params.append('questionTypes', t));
|
||||
}
|
||||
if (selectedTags.length > 0) {
|
||||
selectedTags.forEach(t => params.append('tags', t));
|
||||
}
|
||||
if (keyword.trim()) {
|
||||
params.append('keyword', keyword.trim());
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/eval-datasets/export?${params.toString()}`, {
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setPreviewTotal(result?.data?.total ?? 0);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') {
|
||||
console.error('获取导出预览失败:', err);
|
||||
}
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPreview();
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [dialogOpen, projectId, questionTypes, selectedTags, keyword]);
|
||||
|
||||
// 打开对话框
|
||||
const openDialog = useCallback(() => {
|
||||
setDialogOpen(true);
|
||||
setError('');
|
||||
}, []);
|
||||
|
||||
// 关闭对话框
|
||||
const closeDialog = useCallback(() => {
|
||||
if (exporting) return;
|
||||
setDialogOpen(false);
|
||||
// 重置状态
|
||||
setFormat('json');
|
||||
setQuestionTypes([]);
|
||||
setSelectedTags([]);
|
||||
setKeyword('');
|
||||
setError('');
|
||||
}, [exporting]);
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = useCallback(() => {
|
||||
setQuestionTypes([]);
|
||||
setSelectedTags([]);
|
||||
setKeyword('');
|
||||
}, []);
|
||||
|
||||
// 执行导出
|
||||
const handleExport = useCallback(async () => {
|
||||
if (previewTotal === 0) {
|
||||
setError('没有符合条件的数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setExporting(true);
|
||||
setError('');
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/eval-datasets/export`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
format,
|
||||
questionTypes,
|
||||
tags: selectedTags,
|
||||
keyword: keyword.trim()
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json();
|
||||
throw new Error(result.error || '导出失败');
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = `eval-datasets-${Date.now()}.${format}`;
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
||||
if (match) {
|
||||
filename = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
// 导出成功,关闭对话框
|
||||
closeDialog();
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('导出失败:', err);
|
||||
setError(err.message || '导出失败');
|
||||
return false;
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}, [projectId, format, questionTypes, selectedTags, keyword, previewTotal, closeDialog]);
|
||||
|
||||
return {
|
||||
// 对话框状态
|
||||
dialogOpen,
|
||||
openDialog,
|
||||
closeDialog,
|
||||
|
||||
// 导出状态
|
||||
exporting,
|
||||
error,
|
||||
setError,
|
||||
|
||||
// 导出配置
|
||||
format,
|
||||
setFormat,
|
||||
questionTypes,
|
||||
setQuestionTypes,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
keyword,
|
||||
setKeyword,
|
||||
|
||||
// 预览数据
|
||||
previewTotal,
|
||||
previewLoading,
|
||||
availableTags,
|
||||
|
||||
// 操作
|
||||
resetFilters,
|
||||
handleExport
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user