| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- import { useState, useRef, memo } from 'react'
- import api, { DatasetInfo } from '../api/client'
- const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
- d: DatasetInfo
- onPreview: (id: string) => void
- onDelete: (id: string) => void
- }) {
- return (
- <tr style={{ borderBottom: '1px solid #eee' }}>
- <td style={{ padding: '8px 0' }}>{d.name}</td>
- <td>{d.format}</td>
- <td>{d.record_count}</td>
- <td>{d.created_at}</td>
- <td>
- <button onClick={() => onPreview(d.id)} style={{ marginRight: 8, padding: '2px 8px', cursor: 'pointer' }}>预览</button>
- <button onClick={() => onDelete(d.id)} style={{ padding: '2px 8px', color: '#e94560', border: '1px solid #e94560', borderRadius: 4, background: 'transparent', cursor: 'pointer' }}>删除</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)
- // 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>数据集管理</h1>
- {/* Upload area */}
- <div
- onClick={() => inputRef.current?.click()}
- style={{
- marginTop: 16, border: '2px dashed #ccc', borderRadius: 8,
- padding: 40, textAlign: 'center', color: '#999', cursor: 'pointer',
- opacity: uploading ? 0.6 : 1,
- }}
- >
- {uploading ? '上传中...' : '拖拽文件到此处或点击上传 (JSONL / CSV / Parquet / JSON)'}
- <input
- ref={inputRef}
- type="file"
- accept=".jsonl,.csv,.parquet,.json"
- style={{ display: 'none' }}
- onChange={handleInputChange}
- />
- </div>
- {/* Download section */}
- <div style={{ marginTop: 24 }}>
- <h2 style={{ margin: '0 0 12px', fontSize: 16 }}>从平台下载</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: '8px 12px', width: 400, borderRadius: 4, border: '1px solid #ccc' }}
- />
- <label style={{ fontSize: 13, color: '#666', whiteSpace: 'nowrap' }}>
- <input type="checkbox" checked={dlUseModelscope} onChange={e => setDlUseModelscope(e.target.checked)} />
- {' '}ModelScope
- </label>
- <button
- onClick={handleDownload}
- disabled={downloading}
- style={{ padding: '8px 16px', borderRadius: 4, border: 'none', background: '#e94560', color: '#fff', cursor: 'pointer', opacity: downloading ? 0.6 : 1 }}
- >
- {downloading ? '下载中...' : '下载数据集'}
- </button>
- </div>
- {dlStatus && <p style={{ marginTop: 8, fontSize: 13, color: dlStatus.includes('failed') || dlStatus.includes('失败') ? '#e94560' : '#666' }}>{dlStatus}</p>}
- </div>
- {/* Dataset list */}
- <div style={{ marginTop: 24 }}>
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
- <h2 style={{ margin: 0 }}>已上传数据集</h2>
- <button onClick={fetchDatasets} style={{ padding: '4px 12px', borderRadius: 4, border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}>
- 刷新
- </button>
- </div>
- {loading && <p style={{ color: '#999' }}>加载中...</p>}
- {!loading && datasets.length === 0 && (
- <p style={{ color: '#999', fontSize: 14 }}>暂无数据集</p>
- )}
- {!loading && datasets.length > 0 && (
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
- <thead>
- <tr style={{ borderBottom: '2px solid #eee', textAlign: 'left' }}>
- <th style={{ padding: '8px 0' }}>名称</th>
- <th>格式</th>
- <th>记录数</th>
- <th>上传时间</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- {datasets.map(d => (
- <DatasetRow key={d.id} d={d} onPreview={handlePreview} onDelete={handleDelete} />
- ))}
- </tbody>
- </table>
- )}
- </div>
- {/* Preview */}
- {previewData && previewData.rows.length > 0 && (
- <div style={{ marginTop: 24 }}>
- <h3>数据预览</h3>
- <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
- <thead>
- <tr style={{ borderBottom: '2px solid #eee', textAlign: 'left' }}>
- {previewData.columns.map(col => (
- <th key={col} style={{ padding: '6px 8px' }}>{col}</th>
- ))}
- </tr>
- </thead>
- <tbody>
- {previewData.rows.slice(0, 10).map((row, i) => (
- <tr key={i} style={{ borderBottom: '1px solid #eee' }}>
- {previewData.columns.map(col => {
- const cellVal = String(row.data[col] ?? '')
- const isMultiline = cellVal.includes('\n') || cellVal.length > 100
- return (
- <td
- key={col}
- style={{
- padding: '6px 8px',
- 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>
- )
- }
|