import { useState, useEffect, useRef, memo, useCallback } from 'react' import api, { DatasetInfo, KnowledgeBaseItem, DatasetDownloadTaskResponse } from '../api/client' import { Database, Upload, Loader2, FolderOpen, CheckCircle, XCircle } from 'lucide-react' const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: { d: DatasetInfo onPreview: (id: string) => void onDelete: (id: string) => void }) { return ( { e.currentTarget.style.background = '#f0fdfa' }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }} > {d.name} {d.format} {d.record_count} {d.created_at} ) }) export function Datasets() { const [datasets, setDatasets] = useState([]) const [uploading, setUploading] = useState(false) const [downloading, setDownloading] = useState(false) const [loading, setLoading] = useState(false) const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record }[] } | null>(null) const inputRef = useRef(null) // Sample center modal state const [showSampleCenter, setShowSampleCenter] = useState(false) const [kbList, setKbList] = useState([]) const [kbLoading, setKbLoading] = useState(false) const [kbImporting, setKbImporting] = useState(null) const [kbStatus, setKbStatus] = useState('') const [kbPage, setKbPage] = useState(1) const [kbTotal, setKbTotal] = useState(0) // Active downloads tracking const [activeDownloads, setActiveDownloads] = useState>(new Map()) const downloadPollIntervals = useRef>>(new Map()) useEffect(() => { fetchDatasets() }, []) // Download form const [dlDatasetId, setDlDatasetId] = useState('') const [dlUseModelscope, setDlUseModelscope] = useState(false) const [dlStatus, setDlStatus] = useState('') const fetchDatasets = () => { setLoading(true) api.datasets.list() .then(setDatasets) .catch(() => setDatasets([])) .finally(() => setLoading(false)) } const handleFileUpload = async (file: File) => { setUploading(true) try { await api.datasets.upload(file) fetchDatasets() } catch (err) { console.error('Upload failed:', err) } finally { setUploading(false) } } const handleInputChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) handleFileUpload(file) } const handleDownload = async () => { if (!dlDatasetId.trim()) return setDownloading(true) setDlStatus('正在提交下载任务...') try { const res = await api.datasets.download(dlDatasetId, dlUseModelscope) setDlStatus(`下载任务已提交: ${res.dataset_id}`) setActiveDownloads(prev => new Map(prev).set(res.task_id, res)) startDatasetDownloadPolling(res.task_id) } catch (err) { setDlStatus(`下载失败: ${err instanceof Error ? err.message : '未知错误'}`) } finally { setDownloading(false) } } const startDatasetDownloadPolling = (taskId: string) => { const interval = setInterval(() => { api.datasets.downloadStatus(taskId) .then(res => { setActiveDownloads(prev => { const next = new Map(prev) next.set(taskId, res) return next }) if (res.status === 'completed') { clearInterval(interval) downloadPollIntervals.current.delete(taskId) fetchDatasets() setDlStatus(`${res.dataset_id} 下载完成 (${res.record_count} 条记录)`) } else if (res.status === 'failed') { clearInterval(interval) downloadPollIntervals.current.delete(taskId) setDlStatus(`${res.dataset_id} 下载失败: ${res.error}`) } }) .catch(() => {}) }, 3000) downloadPollIntervals.current.set(taskId, interval) } const handleCancelDatasetDownload = (taskId: string) => { api.datasets.cancelDownload(taskId) setActiveDownloads(prev => { const next = new Map(prev) next.delete(taskId) return next }) const interval = downloadPollIntervals.current.get(taskId) if (interval) { clearInterval(interval) downloadPollIntervals.current.delete(taskId) } } useEffect(() => { return () => { downloadPollIntervals.current.forEach(interval => clearInterval(interval)) } }, []) const handlePreview = (id: string) => { api.datasets.preview(id, 10) .then(res => setPreviewData({ columns: res.columns, rows: res.preview_rows })) .catch(() => setPreviewData(null)) } const handleDelete = async (id: string) => { if (!confirm('确定删除此数据集?')) return try { await api.datasets.delete(id) fetchDatasets() setPreviewData(null) } catch (err) { console.error('Delete failed:', err) } } const fetchKnowledgeBases = useCallback((page = 1) => { setKbLoading(true) api.sampleCenter.listKnowledgeBases(page, 20) .then(res => { setKbList(res.items) setKbTotal(res.total) setKbPage(res.page) }) .catch(err => setKbStatus(`获取知识库列表失败: ${err.message}`)) .finally(() => setKbLoading(false)) }, []) const handleImportFromKB = async (kb: KnowledgeBaseItem) => { setKbImporting(kb.id) setKbStatus(`正在导入 "${kb.name}" ...`) try { await api.sampleCenter.importFromKnowledgeBase(kb.id, kb.name) setKbStatus(`"${kb.name}" 导入请求已提交,可在样本中心查看入库进度`) // 刷新本地数据集列表 fetchDatasets() } catch (err: unknown) { const msg = err instanceof Error ? err.message : '导入失败' setKbStatus(`导入失败: ${msg}`) } finally { setKbImporting(null) } } return (

数据集管理

上传和管理训练数据集

{/* Upload area */}
inputRef.current?.click()} style={{ marginTop: 16, border: '2px dashed #cbd5e1', borderRadius: 12, padding: 40, textAlign: 'center', color: '#94a3b8', cursor: 'pointer', opacity: uploading ? 0.6 : 1, background: '#fff', transition: 'all 0.2s ease', }} onMouseEnter={e => { e.currentTarget.style.borderColor = '#14b8a6'; e.currentTarget.style.background = '#f0fdfa' }} onMouseLeave={e => { e.currentTarget.style.borderColor = '#cbd5e1'; e.currentTarget.style.background = '#fff' }} >
{uploading ? ( ) : ( )}
{uploading ? '上传中...' : '拖拽文件到此处或点击上传'}
支持 JSONL / CSV / Parquet / JSON 格式
{/* Download section */}

从平台下载

setDlDatasetId(e.target.value)} style={{ padding: '10px 14px', flex: 1, maxWidth: 400, borderRadius: 8, border: '1px solid #cbd5e1', fontSize: 14, outline: 'none', transition: 'border-color 0.2s', }} onFocus={e => { e.currentTarget.style.borderColor = '#14b8a6' }} onBlur={e => { e.currentTarget.style.borderColor = '#cbd5e1' }} />
{dlStatus &&

{dlStatus}

}
{/* Active Downloads */} {activeDownloads.size > 0 && (

下载任务

{Array.from(activeDownloads.values()).map(dl => (
{dl.status === 'completed' ? ( ) : dl.status === 'failed' ? ( ) : ( )}
{dl.dataset_id}
{dl.status === 'completed' ? `已完成 (${dl.record_count} 条记录)` : dl.status === 'failed' ? `失败: ${dl.error}` : dl.status === 'cancelled' ? '已取消' : '下载中...'}
{(dl.status === 'pending' || dl.status === 'running' || dl.status === 'downloading') && ( )}
))}
)} {/* Sample Center section */}

样本中心

从样本中心导入知识库数据作为训练数据集

{/* Sample Center Modal */} {showSampleCenter && (
setShowSampleCenter(false)} >
e.stopPropagation()} style={{ background: '#fff', borderRadius: 12, padding: 24, width: '90%', maxWidth: 700, maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.15)', }} >

样本中心 - 知识库列表

选择要导入的知识库,数据将转为训练格式

{/* KB list */} {kbLoading && (
加载中...
)} {!kbLoading && kbList.length === 0 && (
暂无可用的知识库
)} {!kbLoading && kbList.length > 0 && (
{kbList.map(kb => ( ))}
名称 文档数 状态 字段 操作
{kb.name} {kb.document_count} {kb.status === 1 ? '启用' : '禁用'} {kb.metadata_schema.slice(0, 3).map(f => f.field_name_cn).join('、')} {kb.metadata_schema.length > 3 ? '...' : ''}
)} {/* Pagination */} {!kbLoading && kbTotal > 20 && (
第 {kbPage} 页 / 共 {Math.ceil(kbTotal / 20)} 页
)} {/* Status */} {kbStatus && (

{kbStatus}

)}
)} {/* Dataset list */}

已上传数据集

{loading &&

加载中...

} {!loading && datasets.length === 0 && (
暂无数据集,请上传文件或从平台下载
)} {!loading && datasets.length > 0 && (
{datasets.map(d => ( ))}
名称 格式 记录数 上传时间 操作
)}
{/* Preview */} {previewData && previewData.rows.length > 0 && (

数据预览

{previewData.columns.map(col => ( ))} {previewData.rows.slice(0, 10).map((row, i) => ( { e.currentTarget.style.background = '#f0fdfa' }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }} > {previewData.columns.map(col => { const cellVal = String(row.data[col] ?? '') const isMultiline = cellVal.includes('\n') || cellVal.length > 100 return ( ) })} ))}
{col}
{cellVal}
)}
) }