first-update
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function useImageDatasetDetail(projectId, datasetId) {
|
||||
const { t } = useTranslation();
|
||||
const [dataset, setDataset] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// 获取详情
|
||||
const fetchDetail = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get(`/api/projects/${projectId}/image-datasets/${datasetId}`);
|
||||
setDataset(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dataset detail:', error);
|
||||
toast.error(t('imageDatasets.fetchDetailFailed', { defaultValue: '获取详情失败' }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, datasetId, t]);
|
||||
|
||||
// 更新数据集
|
||||
const updateDataset = useCallback(
|
||||
async updates => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const response = await axios.put(`/api/projects/${projectId}/image-datasets/${datasetId}`, updates);
|
||||
setDataset(response.data);
|
||||
toast.success(t('imageDatasets.updateSuccess', { defaultValue: '更新成功' }));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to update dataset:', error);
|
||||
toast.error(t('imageDatasets.updateFailed', { defaultValue: '更新失败' }));
|
||||
throw error;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[projectId, datasetId, t]
|
||||
);
|
||||
|
||||
// AI 重新识别
|
||||
const regenerateAnswer = useCallback(async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const response = await axios.post(`/api/projects/${projectId}/image-datasets/${datasetId}/regenerate`);
|
||||
setDataset(response.data);
|
||||
toast.success(t('imageDatasets.regenerateSuccess', { defaultValue: 'AI 识别成功' }));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to regenerate answer:', error);
|
||||
toast.error(t('imageDatasets.regenerateFailed', { defaultValue: 'AI 识别失败' }));
|
||||
throw error;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [projectId, datasetId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && datasetId) {
|
||||
fetchDetail();
|
||||
}
|
||||
}, [projectId, datasetId, fetchDetail]);
|
||||
|
||||
return {
|
||||
dataset,
|
||||
loading,
|
||||
saving,
|
||||
updateDataset,
|
||||
regenerateAnswer,
|
||||
fetchDetail
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function useImageDatasetDetails(projectId, datasetId) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [currentDataset, setCurrentDataset] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [unconfirming, setUnconfirming] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [datasetsAllCount, setDatasetsAllCount] = useState(0);
|
||||
const [datasetsConfirmCount, setDatasetsConfirmCount] = useState(0);
|
||||
|
||||
// 获取数据集列表信息
|
||||
const fetchDatasetsList = useCallback(async () => {
|
||||
try {
|
||||
// 获取所有数据集以正确统计已确认数量
|
||||
const response = await axios.get(`/api/projects/${projectId}/image-datasets?page=1&pageSize=10000`);
|
||||
const data = response.data;
|
||||
setDatasetsAllCount(data.total || 0);
|
||||
setDatasetsConfirmCount(data.data?.filter(d => d.confirmed).length || 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch datasets list:', error);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// 获取当前数据集详情
|
||||
const fetchDatasetDetail = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get(`/api/projects/${projectId}/image-datasets/${datasetId}`);
|
||||
setCurrentDataset(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dataset detail:', error);
|
||||
toast.error(t('imageDatasets.fetchDetailFailed', '获取详情失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, datasetId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && datasetId) {
|
||||
fetchDatasetDetail();
|
||||
fetchDatasetsList();
|
||||
}
|
||||
}, [projectId, datasetId, fetchDatasetDetail, fetchDatasetsList]);
|
||||
|
||||
// 更新数据集
|
||||
const updateDataset = useCallback(
|
||||
async updates => {
|
||||
try {
|
||||
setSaving(true);
|
||||
await axios.put(`/api/projects/${projectId}/image-datasets/${datasetId}`, updates);
|
||||
toast.success(t('imageDatasets.updateSuccess', '更新成功'));
|
||||
// 刷新数据
|
||||
await fetchDatasetDetail();
|
||||
await fetchDatasetsList();
|
||||
} catch (error) {
|
||||
console.error('Failed to update dataset:', error);
|
||||
toast.error(t('imageDatasets.updateFailed', '更新失败'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[projectId, datasetId, t, fetchDatasetDetail, fetchDatasetsList]
|
||||
);
|
||||
|
||||
// 翻页导航
|
||||
const handleNavigate = useCallback(
|
||||
async (direction, skipCurrentId = null) => {
|
||||
try {
|
||||
// 获取所有数据集(不分页),使用一个足够大的 pageSize
|
||||
const response = await axios.get(`/api/projects/${projectId}/image-datasets?page=1&pageSize=10000`);
|
||||
const datasets = response.data.data || [];
|
||||
|
||||
if (datasets.length === 0) {
|
||||
router.push(`/projects/${projectId}/image-datasets`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确定当前索引
|
||||
let currentIndex = -1;
|
||||
const searchId = skipCurrentId || datasetId;
|
||||
const currentDatasetId = String(searchId);
|
||||
|
||||
// 查找当前数据集的索引
|
||||
currentIndex = datasets.findIndex(d => String(d.id) === currentDatasetId);
|
||||
|
||||
// 如果找不到(删除场景或其他原因),从第一个开始
|
||||
if (currentIndex === -1) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
// 计算下一个索引
|
||||
let nextIndex;
|
||||
if (direction === 'prev') {
|
||||
nextIndex = currentIndex > 0 ? currentIndex - 1 : datasets.length - 1;
|
||||
} else {
|
||||
nextIndex = currentIndex < datasets.length - 1 ? currentIndex + 1 : 0;
|
||||
}
|
||||
|
||||
const nextDataset = datasets[nextIndex];
|
||||
if (nextDataset) {
|
||||
router.push(`/projects/${projectId}/image-datasets/${nextDataset.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate:', error);
|
||||
toast.error(t('common.navigationFailed', '导航失败'));
|
||||
}
|
||||
},
|
||||
[projectId, datasetId, router, t]
|
||||
);
|
||||
|
||||
// 确认保留
|
||||
const handleConfirm = useCallback(async () => {
|
||||
setConfirming(true);
|
||||
try {
|
||||
await updateDataset({ confirmed: true });
|
||||
// 确认后导航到下一条
|
||||
await handleNavigate('next');
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
}, [updateDataset, handleNavigate]);
|
||||
|
||||
// 取消确认
|
||||
const handleUnconfirm = useCallback(async () => {
|
||||
setUnconfirming(true);
|
||||
try {
|
||||
await updateDataset({ confirmed: false });
|
||||
} finally {
|
||||
setUnconfirming(false);
|
||||
}
|
||||
}, [updateDataset]);
|
||||
|
||||
// 删除数据集
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (confirm(t('imageDatasets.deleteConfirm', '确定要删除这个数据集吗?'))) {
|
||||
try {
|
||||
await axios.delete(`/api/projects/${projectId}/image-datasets/${datasetId}`);
|
||||
toast.success(t('imageDatasets.deleteSuccess', '删除成功'));
|
||||
// 导航到下一条,传递 datasetId 以便 handleNavigate 知道是删除场景
|
||||
await handleNavigate('next', datasetId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete dataset:', error);
|
||||
toast.error(t('imageDatasets.deleteFailed', '删除失败'));
|
||||
}
|
||||
}
|
||||
}, [projectId, datasetId, handleNavigate, t]);
|
||||
|
||||
return {
|
||||
currentDataset,
|
||||
loading,
|
||||
saving,
|
||||
confirming,
|
||||
unconfirming,
|
||||
datasetsAllCount,
|
||||
datasetsConfirmCount,
|
||||
updateDataset,
|
||||
handleNavigate,
|
||||
handleConfirm,
|
||||
handleUnconfirm,
|
||||
handleDelete
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
|
||||
const useImageDatasetExport = projectId => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* 解析标签格式的答案
|
||||
* 如果答案是 JSON 数组格式,解析并用逗号连接
|
||||
*/
|
||||
const parseAnswerLabels = item => {
|
||||
const { answer, answerType } = item;
|
||||
if (answerType !== 'label' || !answer) {
|
||||
return answer;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试解析 JSON
|
||||
const parsed = JSON.parse(answer);
|
||||
if (Array.isArray(parsed)) {
|
||||
// 如果是数组,用逗号连接
|
||||
return parsed.join(', ');
|
||||
}
|
||||
return answer;
|
||||
} catch (e) {
|
||||
// 不是 JSON 格式,直接返回原答案
|
||||
return answer;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出图片数据集
|
||||
*/
|
||||
const exportImageDatasets = async exportOptions => {
|
||||
try {
|
||||
// 1. 获取数据集数据
|
||||
const apiUrl = `/api/projects/${projectId}/image-datasets/export`;
|
||||
const response = await axios.post(apiUrl, {
|
||||
confirmedOnly: exportOptions.confirmedOnly
|
||||
});
|
||||
|
||||
let datasets = response.data;
|
||||
|
||||
if (!datasets || datasets.length === 0) {
|
||||
toast.warning(t('imageDatasets.noDataToExport', '没有可导出的数据'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 处理答案中的标签格式
|
||||
datasets = datasets.map(item => ({
|
||||
...item,
|
||||
answer: parseAnswerLabels(item)
|
||||
}));
|
||||
|
||||
// 3. 根据格式类型转换数据
|
||||
let formattedData;
|
||||
|
||||
if (exportOptions.formatType === 'raw') {
|
||||
// 原始格式:直接导出数据集
|
||||
formattedData = datasets.map(item => {
|
||||
const result = { ...item };
|
||||
|
||||
// 如果需要包含图片路径
|
||||
if (exportOptions.includeImagePath && item.imageName) {
|
||||
result.image_path = `/images/${item.imageName}`;
|
||||
}
|
||||
|
||||
if (item.answerType === 'custom_format') {
|
||||
try {
|
||||
result.answerObj = JSON.parse(item.answer);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
} else if (exportOptions.formatType === 'alpaca') {
|
||||
formattedData = datasets.map(({ question, answer, imageName }) => {
|
||||
const item = {
|
||||
instruction: question,
|
||||
input: '',
|
||||
output: answer
|
||||
};
|
||||
|
||||
// 如果需要包含图片路径
|
||||
if (exportOptions.includeImagePath && imageName) {
|
||||
item.images = [`/images/${imageName}`];
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
} else if (exportOptions.formatType === 'sharegpt') {
|
||||
formattedData = datasets.map(({ question, answer, imageName }) => {
|
||||
const messages = [];
|
||||
|
||||
// 添加系统提示词(如果有)
|
||||
if (exportOptions.systemPrompt) {
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: exportOptions.systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户问题
|
||||
const userContent = [];
|
||||
|
||||
// 如果需要包含图片路径
|
||||
if (exportOptions.includeImagePath && imageName) {
|
||||
userContent.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: `/images/${imageName}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text: question
|
||||
});
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: userContent
|
||||
});
|
||||
|
||||
// 添加助手回答
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: answer
|
||||
});
|
||||
|
||||
return { messages };
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 生成 JSON 文件
|
||||
const jsonContent = JSON.stringify(formattedData, null, 2);
|
||||
const blob = new Blob([jsonContent], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
const formatSuffix = exportOptions.formatType;
|
||||
const dateStr = new Date().toISOString().slice(0, 10);
|
||||
a.download = `image-datasets-${projectId}-${formatSuffix}-${dateStr}.json`;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success(t('imageDatasets.exportSuccess', '数据集导出成功'));
|
||||
|
||||
// 5. 如果需要导出图片,调用压缩包接口
|
||||
if (exportOptions.exportImages) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
confirmedOnly: exportOptions.confirmedOnly.toString()
|
||||
});
|
||||
|
||||
const zipUrl = `/api/projects/${projectId}/image-datasets/export-zip?${params.toString()}`;
|
||||
|
||||
// 创建一个隐藏的 a 标签来触发下载
|
||||
const a = document.createElement('a');
|
||||
a.href = zipUrl;
|
||||
a.style.display = 'none';
|
||||
a.target = '_blank';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
toast.success(t('imageDatasets.exportImagesSuccess', '图片压缩包导出成功'));
|
||||
} catch (error) {
|
||||
console.error('Failed to export images:', error);
|
||||
toast.error(t('imageDatasets.exportImagesFailed', '图片导出失败'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
toast.error(error.message || t('imageDatasets.exportFailed', '导出失败'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
exportImageDatasets
|
||||
};
|
||||
};
|
||||
|
||||
export default useImageDatasetExport;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const STORAGE_KEY = 'imageDatasetFilters';
|
||||
|
||||
export function useImageDatasetFilters(projectId) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [scoreFilter, setScoreFilter] = useState([0, 5]);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// 从 localStorage 恢复筛选条件
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(`${STORAGE_KEY}_${projectId}`);
|
||||
if (stored) {
|
||||
const filters = JSON.parse(stored);
|
||||
setSearchQuery(filters.searchQuery || '');
|
||||
setStatusFilter(filters.statusFilter || 'all');
|
||||
setScoreFilter(filters.scoreFilter || [0, 5]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore filters:', error);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}, [projectId]);
|
||||
|
||||
// 保存筛选条件到 localStorage
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
`${STORAGE_KEY}_${projectId}`,
|
||||
JSON.stringify({
|
||||
searchQuery,
|
||||
statusFilter,
|
||||
scoreFilter
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save filters:', error);
|
||||
}
|
||||
}
|
||||
}, [projectId, searchQuery, statusFilter, scoreFilter, isInitialized]);
|
||||
|
||||
// 计算活跃筛选条件数
|
||||
const getActiveFilterCount = useCallback(() => {
|
||||
let count = 0;
|
||||
if (statusFilter !== 'all') count++;
|
||||
if (scoreFilter[0] > 0 || scoreFilter[1] < 5) count++;
|
||||
return count;
|
||||
}, [statusFilter, scoreFilter]);
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = useCallback(() => {
|
||||
setSearchQuery('');
|
||||
setStatusFilter('all');
|
||||
setScoreFilter([0, 5]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
scoreFilter,
|
||||
setScoreFilter,
|
||||
isInitialized,
|
||||
getActiveFilterCount,
|
||||
resetFilters
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function useImageDatasets(projectId, filters = {}) {
|
||||
const { t } = useTranslation();
|
||||
const [datasets, setDatasets] = useState({ data: [], total: 0 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 20;
|
||||
|
||||
// 使用 useMemo 稳定 filters 对象引用
|
||||
const stableFilters = useMemo(
|
||||
() => ({
|
||||
search: filters.search || '',
|
||||
confirmed: filters.confirmed,
|
||||
minScore: filters.minScore,
|
||||
maxScore: filters.maxScore
|
||||
}),
|
||||
[filters.search, filters.confirmed, filters.minScore, filters.maxScore]
|
||||
);
|
||||
|
||||
// 获取数据集列表
|
||||
const fetchDatasets = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
let url = `/api/projects/${projectId}/image-datasets?page=${page}&pageSize=${pageSize}`;
|
||||
|
||||
// 搜索条件
|
||||
if (stableFilters.search) {
|
||||
url += `&search=${encodeURIComponent(stableFilters.search)}`;
|
||||
}
|
||||
|
||||
// 确认状态筛选
|
||||
if (stableFilters.confirmed !== undefined) {
|
||||
url += `&confirmed=${stableFilters.confirmed}`;
|
||||
}
|
||||
|
||||
// 评分筛选
|
||||
if (stableFilters.minScore !== undefined || stableFilters.maxScore !== undefined) {
|
||||
if (stableFilters.minScore !== undefined) {
|
||||
url += `&minScore=${stableFilters.minScore}`;
|
||||
}
|
||||
if (stableFilters.maxScore !== undefined) {
|
||||
url += `&maxScore=${stableFilters.maxScore}`;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios.get(url);
|
||||
setDatasets(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch datasets:', error);
|
||||
toast.error(t('imageDatasets.fetchFailed', { defaultValue: '获取数据集失败' }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId, page, pageSize, stableFilters, t]);
|
||||
|
||||
// 删除数据集
|
||||
const deleteDataset = useCallback(
|
||||
async datasetId => {
|
||||
try {
|
||||
await axios.delete(`/api/projects/${projectId}/image-datasets/${datasetId}`);
|
||||
toast.success(t('imageDatasets.deleteSuccess', { defaultValue: '删除成功' }));
|
||||
fetchDatasets();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete dataset:', error);
|
||||
toast.error(t('imageDatasets.deleteFailed', { defaultValue: '删除失败' }));
|
||||
}
|
||||
},
|
||||
[projectId, fetchDatasets, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
fetchDatasets();
|
||||
}
|
||||
}, [projectId, page, stableFilters, fetchDatasets]);
|
||||
|
||||
return {
|
||||
datasets,
|
||||
loading,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
fetchDatasets,
|
||||
deleteDataset
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user