| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- import { useState, useEffect, useRef, memo } from 'react'
- import api, { DatasetInfo } from '../api/client'
- import { Database, Upload, Loader2 } from 'lucide-react'
- const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
- d: DatasetInfo
- onPreview: (id: string) => void
- onDelete: (id: string) => void
- }) {
- return (
- <tr style={{
- borderBottom: '1px solid #f1f5f9',
- transition: 'background 0.15s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
- >
- <td style={{ padding: '12px 12px', fontWeight: 500, fontSize: 13 }}>{d.name}</td>
- <td style={{ padding: '12px 12px', fontSize: 13, color: '#64748b' }}>
- <span style={{
- display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
- background: '#f1f5f9', color: '#64748b',
- }}>
- {d.format}
- </span>
- </td>
- <td style={{ padding: '12px 12px', fontSize: 13 }}>{d.record_count}</td>
- <td style={{ padding: '12px 12px', fontSize: 13, color: '#94a3b8' }}>{d.created_at}</td>
- <td style={{ padding: '12px 12px' }}>
- <button onClick={() => onPreview(d.id)} style={{
- marginRight: 8, padding: '4px 12px', color: '#0ea5e9',
- border: '1px solid #0ea5e9', borderRadius: 6, background: 'transparent',
- cursor: 'pointer', fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#0ea5e9'; e.currentTarget.style.color = '#fff' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#0ea5e9' }}
- >预览</button>
- <button onClick={() => onDelete(d.id)} style={{
- padding: '4px 12px', color: '#f43f5e', border: '1px solid #f43f5e',
- borderRadius: 6, background: 'transparent', cursor: 'pointer',
- fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f43f5e'; e.currentTarget.style.color = '#fff' }}
- onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#f43f5e' }}
- >删除</button>
- </td>
- </tr>
- )
- })
- export function Datasets() {
- const [datasets, setDatasets] = useState<DatasetInfo[]>([])
- 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<string, unknown> }[] } | null>(null)
- const inputRef = useRef<HTMLInputElement>(null)
- 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<HTMLInputElement>) => {
- const file = e.target.files?.[0]
- if (file) handleFileUpload(file)
- }
- const handleDownload = () => {
- if (!dlDatasetId.trim()) return
- setDownloading(true)
- setDlStatus('正在下载...')
- api.datasets.download(dlDatasetId, dlUseModelscope)
- .then(res => setDlStatus(`${res.dataset_id}: ${res.status}${res.error ? ` - ${res.error}` : ''}`))
- .catch(err => setDlStatus(`下载失败: ${err.message}`))
- .finally(() => setDownloading(false))
- }
- 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)
- }
- }
- return (
- <div>
- <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
- <p style={{ color: '#64748b', fontSize: 13, margin: '4px 0 16px' }}>上传和管理训练数据集</p>
- {/* Upload area */}
- <div
- onClick={() => 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' }}
- >
- <div style={{ marginBottom: 8 }}>
- {uploading ? (
- <Loader2 size={32} color="#14b8a6" strokeWidth={1.5} style={{ animation: 'lucide-spin 1s linear infinite' }} />
- ) : (
- <Upload size={32} color="#14b8a6" strokeWidth={1.5} />
- )}
- </div>
- <div style={{ fontSize: 14, fontWeight: 500 }}>
- {uploading ? '上传中...' : '拖拽文件到此处或点击上传'}
- </div>
- <div style={{ fontSize: 12, color: '#cbd5e1', marginTop: 4 }}>
- 支持 JSONL / CSV / Parquet / JSON 格式
- </div>
- <input
- ref={inputRef}
- type="file"
- accept=".jsonl,.csv,.parquet,.json"
- style={{ display: 'none' }}
- onChange={handleInputChange}
- />
- </div>
- {/* Download section */}
- <div style={{
- marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>从平台下载</h2>
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
- <input
- type="text"
- placeholder="数据集 ID (如 glue, MRPC, stanfordnlp/imdb)"
- value={dlDatasetId}
- onChange={e => 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' }}
- />
- <label style={{ fontSize: 13, color: '#64748b', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
- <input type="checkbox" checked={dlUseModelscope} onChange={e => setDlUseModelscope(e.target.checked)} />
- {' '}ModelScope
- </label>
- <button
- onClick={handleDownload}
- disabled={downloading}
- style={{
- padding: '10px 20px', borderRadius: 8, border: 'none',
- background: '#14b8a6', color: '#fff', cursor: 'pointer',
- opacity: downloading ? 0.6 : 1, fontSize: 14, fontWeight: 600,
- }}
- >
- {downloading ? '下载中...' : '下载数据集'}
- </button>
- </div>
- {dlStatus && <p style={{
- marginTop: 10, padding: '8px 12px', borderRadius: 6, fontSize: 13,
- background: dlStatus.includes('失败') ? '#fff1f2' : '#f1f5f9',
- color: dlStatus.includes('失败') ? '#e11d48' : '#64748b',
- border: `1px solid ${dlStatus.includes('失败') ? '#fecdd3' : 'transparent'}`,
- }}>{dlStatus}</p>}
- </div>
- {/* Dataset list */}
- <div style={{ marginTop: 24 }}>
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
- <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>已上传数据集</h2>
- <button onClick={fetchDatasets} style={{
- padding: '6px 14px', borderRadius: 6, border: '1px solid #cbd5e1',
- background: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500,
- }}
- onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
- onMouseLeave={e => { e.currentTarget.style.background = '#fff' }}
- >
- 刷新
- </button>
- </div>
- {loading && <p style={{ color: '#94a3b8', fontSize: 13 }}>加载中...</p>}
- {!loading && datasets.length === 0 && (
- <div style={{
- padding: 40, textAlign: 'center', color: '#94a3b8', fontSize: 14,
- background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
- }}>
- <div style={{ marginBottom: 8 }}><Database size={32} color="#94a3b8" strokeWidth={1.5} /></div>
- 暂无数据集,请上传文件或从平台下载
- </div>
- )}
- {!loading && datasets.length > 0 && (
- <div style={{
- background: '#fff', borderRadius: 10, overflow: 'hidden',
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
- <thead>
- <tr style={{ background: '#f0fdfa', borderBottom: '2px solid #f1f5f9', textAlign: 'left' }}>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>名称</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>格式</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>记录数</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>上传时间</th>
- <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>操作</th>
- </tr>
- </thead>
- <tbody>
- {datasets.map(d => (
- <DatasetRow key={d.id} d={d} onPreview={handlePreview} onDelete={handleDelete} />
- ))}
- </tbody>
- </table>
- </div>
- )}
- </div>
- {/* Preview */}
- {previewData && previewData.rows.length > 0 && (
- <div style={{
- marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
- }}>
- <h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>数据预览</h3>
- <div style={{ overflowX: 'auto' }}>
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
- <thead>
- <tr style={{ background: '#f0fdfa', borderBottom: '2px solid #f1f5f9', textAlign: 'left' }}>
- {previewData.columns.map(col => (
- <th key={col} style={{ padding: '8px 12px', fontSize: 12, color: '#64748b', fontWeight: 600, whiteSpace: 'nowrap' }}>{col}</th>
- ))}
- </tr>
- </thead>
- <tbody>
- {previewData.rows.slice(0, 10).map((row, i) => (
- <tr key={i} style={{ borderBottom: '1px solid #f1f5f9', transition: 'background 0.15s ease' }}
- onMouseEnter={e => { 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 (
- <td
- key={col}
- style={{
- padding: '8px 12px',
- maxWidth: isMultiline ? 500 : 200,
- overflow: isMultiline ? 'auto' : 'hidden',
- textOverflow: isMultiline ? undefined : 'ellipsis',
- whiteSpace: isMultiline ? 'pre-wrap' : 'nowrap',
- fontFamily: isMultiline ? 'monospace' : undefined,
- fontSize: isMultiline ? 12 : 13,
- }}
- >
- {cellVal}
- </td>
- )
- })}
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- )}
- </div>
- )
- }
|