'use client'; import { useState, useEffect, useRef } from 'react'; import { Box, Typography, LinearProgress, Alert, Paper, List, ListItem, ListItemIcon, ListItemText, Chip } from '@mui/material'; import { CheckCircle as CheckIcon, Error as ErrorIcon, Info as InfoIcon } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; /** * 导入进度步骤组件 */ export default function ImportProgressStep({ projectId, rawData, fieldMapping, sourceInfo, onComplete, onError }) { const { t } = useTranslation(); const [progress, setProgress] = useState(0); const [currentStep, setCurrentStep] = useState(''); const [importStats, setImportStats] = useState({ total: 0, processed: 0, success: 0, failed: 0, skipped: 0, errors: [] }); const [completed, setCompleted] = useState(false); const startedRef = useRef(false); // 防止在开发模式下因严格模式导致重复执行 useEffect(() => { if (!startedRef.current && rawData && fieldMapping && projectId) { startedRef.current = true; startImport(); } }, [rawData, fieldMapping, projectId]); const startImport = async () => { try { setCurrentStep(t('import.preparingData', '准备数据...')); setImportStats(prev => ({ ...prev, total: rawData.length })); // 转换数据格式 const convertedData = rawData.map(item => { // 支持 question 映射多个字段,拼接为一个字符串 const qFields = fieldMapping.question; const question = Array.isArray(qFields) ? qFields .map(f => item[f] || '') .filter(v => v && String(v).trim()) .join('\n') : item[qFields] || ''; const converted = { question, answer: item[fieldMapping.answer] || '', cot: fieldMapping.cot ? item[fieldMapping.cot] || '' : '', questionLabel: '', // 默认标签,后续可以通过AI生成 chunkName: sourceInfo?.datasetName || sourceInfo?.fileName || 'Imported Data', chunkContent: `Imported from ${sourceInfo?.type || 'file'}`, model: 'imported', confirmed: false, score: 0, tags: fieldMapping.tags ? JSON.stringify(parseTagsField(item[fieldMapping.tags])) : '[]', note: '', other: JSON.stringify(getOtherFields(item, fieldMapping)) }; // 不在前端抛错,由后端负责校验并统计 skipped return converted; }); setProgress(25); setCurrentStep(t('import.uploadingData', '上传数据...')); // 分批上传数据 const batchSize = 500; let processed = 0; let success = 0; let failed = 0; let skipped = 0; const errors = []; for (let i = 0; i < convertedData.length; i += batchSize) { const batch = convertedData.slice(i, i + batchSize); try { const response = await fetch(`/api/projects/${projectId}/datasets/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ datasets: batch, sourceInfo }) }); if (!response.ok) { throw new Error(`批次上传失败: ${response.statusText}`); } const result = await response.json(); success += result.success || 0; failed += typeof result.failed === 'number' ? result.failed : result.errors?.length || 0; skipped += result.skipped || 0; processed += batch.length; if (result.errors && result.errors.length > 0) { errors.push(...result.errors); } } catch (error) { failed += batch.length; processed += batch.length; errors.push(`批次 ${Math.floor(i / batchSize) + 1}: ${error.message}`); } // 更新进度 const progressPercent = 25 + (processed / convertedData.length) * 70; setProgress(progressPercent); setImportStats({ total: convertedData.length, processed, success, failed, skipped, errors }); setCurrentStep( t('import.processing', '处理中... {{processed}}/{{total}}', { processed, total: convertedData.length }) ); } setProgress(100); setCurrentStep(t('import.completed', '导入完成')); setCompleted(true); // 延迟一下再调用完成回调,让用户看到完成状态 setTimeout(() => { onComplete(); }, 2000); } catch (error) { onError(error.message); setImportStats(prev => ({ ...prev, errors: [...prev.errors, error.message] })); } }; // 解析标签字段 const parseTagsField = tagsValue => { if (!tagsValue) return []; if (Array.isArray(tagsValue)) { return tagsValue; } if (typeof tagsValue === 'string') { return tagsValue .split(',') .map(tag => tag.trim()) .filter(tag => tag); } return []; }; // 获取其他字段(兼容数组映射) const getOtherFields = (item, mapping) => { const used = []; Object.values(mapping).forEach(field => { if (!field) return; if (Array.isArray(field)) used.push(...field); else used.push(field); }); const mappedFields = new Set(used); const otherFields = {}; Object.keys(item).forEach(key => { if (!mappedFields.has(key)) { otherFields[key] = item[key]; } }); return otherFields; }; return ( {t('import.importing', '正在导入数据集')} {/* 进度条 */} {currentStep} {Math.round(progress)}% {t('import.complete', '完成')} {/* 导入统计 */} {t('import.importStats', '导入统计')} } label={t('import.total', '总计: {{count}}', { count: importStats.total })} variant="outlined" /> } label={t('import.success', '成功: {{count}}', { count: importStats.success })} color="success" variant="outlined" /> {importStats.skipped > 0 && ( } label={t('import.skipped', '跳过: {{count}}', { count: importStats.skipped })} color="warning" variant="outlined" /> )} {importStats.failed > 0 && ( } label={t('import.failed', '失败: {{count}}', { count: importStats.failed })} color="error" variant="outlined" /> )} {sourceInfo && ( {t('import.source', '数据源')}:{' '} {sourceInfo.type === 'file' ? sourceInfo.fileName : sourceInfo.datasetName} {sourceInfo.description && ( {t('import.description', '描述')}: {sourceInfo.description} )} )} {/* 错误列表 */} {importStats.errors.length > 0 && ( {t('import.errors', '错误信息')} {importStats.errors.slice(0, 10).map((error, index) => ( ))} {importStats.errors.length > 10 && ( {t('import.moreErrors', '还有 {{count}} 个错误未显示...', { count: importStats.errors.length - 10 })} )} )} {/* 完成提示 */} {completed && ( {t('import.importSuccess', '数据集导入完成!成功导入 {{success}} 条记录。', { success: importStats.success })} )} ); }