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

View File

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