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

View File

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

View File

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

View File

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

View File

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