Datasets.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import { useState, useEffect, useRef, memo } from 'react'
  2. import api, { DatasetInfo } from '../api/client'
  3. import { Database, Upload, Loader2 } from 'lucide-react'
  4. const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
  5. d: DatasetInfo
  6. onPreview: (id: string) => void
  7. onDelete: (id: string) => void
  8. }) {
  9. return (
  10. <tr style={{
  11. borderBottom: '1px solid #f1f5f9',
  12. transition: 'background 0.15s ease',
  13. }}
  14. onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
  15. onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
  16. >
  17. <td style={{ padding: '12px 12px', fontWeight: 500, fontSize: 13 }}>{d.name}</td>
  18. <td style={{ padding: '12px 12px', fontSize: 13, color: '#64748b' }}>
  19. <span style={{
  20. display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
  21. background: '#f1f5f9', color: '#64748b',
  22. }}>
  23. {d.format}
  24. </span>
  25. </td>
  26. <td style={{ padding: '12px 12px', fontSize: 13 }}>{d.record_count}</td>
  27. <td style={{ padding: '12px 12px', fontSize: 13, color: '#94a3b8' }}>{d.created_at}</td>
  28. <td style={{ padding: '12px 12px' }}>
  29. <button onClick={() => onPreview(d.id)} style={{
  30. marginRight: 8, padding: '4px 12px', color: '#0ea5e9',
  31. border: '1px solid #0ea5e9', borderRadius: 6, background: 'transparent',
  32. cursor: 'pointer', fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
  33. }}
  34. onMouseEnter={e => { e.currentTarget.style.background = '#0ea5e9'; e.currentTarget.style.color = '#fff' }}
  35. onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#0ea5e9' }}
  36. >预览</button>
  37. <button onClick={() => onDelete(d.id)} style={{
  38. padding: '4px 12px', color: '#f43f5e', border: '1px solid #f43f5e',
  39. borderRadius: 6, background: 'transparent', cursor: 'pointer',
  40. fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
  41. }}
  42. onMouseEnter={e => { e.currentTarget.style.background = '#f43f5e'; e.currentTarget.style.color = '#fff' }}
  43. onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#f43f5e' }}
  44. >删除</button>
  45. </td>
  46. </tr>
  47. )
  48. })
  49. export function Datasets() {
  50. const [datasets, setDatasets] = useState<DatasetInfo[]>([])
  51. const [uploading, setUploading] = useState(false)
  52. const [downloading, setDownloading] = useState(false)
  53. const [loading, setLoading] = useState(false)
  54. const [previewData, setPreviewData] = useState<{ columns: string[]; rows: { row_index: number; data: Record<string, unknown> }[] } | null>(null)
  55. const inputRef = useRef<HTMLInputElement>(null)
  56. useEffect(() => {
  57. fetchDatasets()
  58. }, [])
  59. // Download form
  60. const [dlDatasetId, setDlDatasetId] = useState('')
  61. const [dlUseModelscope, setDlUseModelscope] = useState(false)
  62. const [dlStatus, setDlStatus] = useState('')
  63. const fetchDatasets = () => {
  64. setLoading(true)
  65. api.datasets.list()
  66. .then(setDatasets)
  67. .catch(() => setDatasets([]))
  68. .finally(() => setLoading(false))
  69. }
  70. const handleFileUpload = async (file: File) => {
  71. setUploading(true)
  72. try {
  73. await api.datasets.upload(file)
  74. fetchDatasets()
  75. } catch (err) {
  76. console.error('Upload failed:', err)
  77. } finally {
  78. setUploading(false)
  79. }
  80. }
  81. const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  82. const file = e.target.files?.[0]
  83. if (file) handleFileUpload(file)
  84. }
  85. const handleDownload = () => {
  86. if (!dlDatasetId.trim()) return
  87. setDownloading(true)
  88. setDlStatus('正在下载...')
  89. api.datasets.download(dlDatasetId, dlUseModelscope)
  90. .then(res => setDlStatus(`${res.dataset_id}: ${res.status}${res.error ? ` - ${res.error}` : ''}`))
  91. .catch(err => setDlStatus(`下载失败: ${err.message}`))
  92. .finally(() => setDownloading(false))
  93. }
  94. const handlePreview = (id: string) => {
  95. api.datasets.preview(id, 10)
  96. .then(res => setPreviewData({ columns: res.columns, rows: res.preview_rows }))
  97. .catch(() => setPreviewData(null))
  98. }
  99. const handleDelete = async (id: string) => {
  100. if (!confirm('确定删除此数据集?')) return
  101. try {
  102. await api.datasets.delete(id)
  103. fetchDatasets()
  104. setPreviewData(null)
  105. } catch (err) {
  106. console.error('Delete failed:', err)
  107. }
  108. }
  109. return (
  110. <div>
  111. <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
  112. <p style={{ color: '#64748b', fontSize: 13, margin: '4px 0 16px' }}>上传和管理训练数据集</p>
  113. {/* Upload area */}
  114. <div
  115. onClick={() => inputRef.current?.click()}
  116. style={{
  117. marginTop: 16, border: '2px dashed #cbd5e1', borderRadius: 12,
  118. padding: 40, textAlign: 'center', color: '#94a3b8', cursor: 'pointer',
  119. opacity: uploading ? 0.6 : 1, background: '#fff',
  120. transition: 'all 0.2s ease',
  121. }}
  122. onMouseEnter={e => { e.currentTarget.style.borderColor = '#14b8a6'; e.currentTarget.style.background = '#f0fdfa' }}
  123. onMouseLeave={e => { e.currentTarget.style.borderColor = '#cbd5e1'; e.currentTarget.style.background = '#fff' }}
  124. >
  125. <div style={{ marginBottom: 8 }}>
  126. {uploading ? (
  127. <Loader2 size={32} color="#14b8a6" strokeWidth={1.5} style={{ animation: 'lucide-spin 1s linear infinite' }} />
  128. ) : (
  129. <Upload size={32} color="#14b8a6" strokeWidth={1.5} />
  130. )}
  131. </div>
  132. <div style={{ fontSize: 14, fontWeight: 500 }}>
  133. {uploading ? '上传中...' : '拖拽文件到此处或点击上传'}
  134. </div>
  135. <div style={{ fontSize: 12, color: '#cbd5e1', marginTop: 4 }}>
  136. 支持 JSONL / CSV / Parquet / JSON 格式
  137. </div>
  138. <input
  139. ref={inputRef}
  140. type="file"
  141. accept=".jsonl,.csv,.parquet,.json"
  142. style={{ display: 'none' }}
  143. onChange={handleInputChange}
  144. />
  145. </div>
  146. {/* Download section */}
  147. <div style={{
  148. marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
  149. boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
  150. }}>
  151. <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>从平台下载</h2>
  152. <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
  153. <input
  154. type="text"
  155. placeholder="数据集 ID (如 glue, MRPC, stanfordnlp/imdb)"
  156. value={dlDatasetId}
  157. onChange={e => setDlDatasetId(e.target.value)}
  158. style={{
  159. padding: '10px 14px', flex: 1, maxWidth: 400, borderRadius: 8,
  160. border: '1px solid #cbd5e1', fontSize: 14, outline: 'none',
  161. transition: 'border-color 0.2s',
  162. }}
  163. onFocus={e => { e.currentTarget.style.borderColor = '#14b8a6' }}
  164. onBlur={e => { e.currentTarget.style.borderColor = '#cbd5e1' }}
  165. />
  166. <label style={{ fontSize: 13, color: '#64748b', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
  167. <input type="checkbox" checked={dlUseModelscope} onChange={e => setDlUseModelscope(e.target.checked)} />
  168. {' '}ModelScope
  169. </label>
  170. <button
  171. onClick={handleDownload}
  172. disabled={downloading}
  173. style={{
  174. padding: '10px 20px', borderRadius: 8, border: 'none',
  175. background: '#14b8a6', color: '#fff', cursor: 'pointer',
  176. opacity: downloading ? 0.6 : 1, fontSize: 14, fontWeight: 600,
  177. }}
  178. >
  179. {downloading ? '下载中...' : '下载数据集'}
  180. </button>
  181. </div>
  182. {dlStatus && <p style={{
  183. marginTop: 10, padding: '8px 12px', borderRadius: 6, fontSize: 13,
  184. background: dlStatus.includes('失败') ? '#fff1f2' : '#f1f5f9',
  185. color: dlStatus.includes('失败') ? '#e11d48' : '#64748b',
  186. border: `1px solid ${dlStatus.includes('失败') ? '#fecdd3' : 'transparent'}`,
  187. }}>{dlStatus}</p>}
  188. </div>
  189. {/* Dataset list */}
  190. <div style={{ marginTop: 24 }}>
  191. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
  192. <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>已上传数据集</h2>
  193. <button onClick={fetchDatasets} style={{
  194. padding: '6px 14px', borderRadius: 6, border: '1px solid #cbd5e1',
  195. background: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500,
  196. }}
  197. onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
  198. onMouseLeave={e => { e.currentTarget.style.background = '#fff' }}
  199. >
  200. 刷新
  201. </button>
  202. </div>
  203. {loading && <p style={{ color: '#94a3b8', fontSize: 13 }}>加载中...</p>}
  204. {!loading && datasets.length === 0 && (
  205. <div style={{
  206. padding: 40, textAlign: 'center', color: '#94a3b8', fontSize: 14,
  207. background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
  208. }}>
  209. <div style={{ marginBottom: 8 }}><Database size={32} color="#94a3b8" strokeWidth={1.5} /></div>
  210. 暂无数据集,请上传文件或从平台下载
  211. </div>
  212. )}
  213. {!loading && datasets.length > 0 && (
  214. <div style={{
  215. background: '#fff', borderRadius: 10, overflow: 'hidden',
  216. boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
  217. }}>
  218. <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
  219. <thead>
  220. <tr style={{ background: '#f0fdfa', borderBottom: '2px solid #f1f5f9', textAlign: 'left' }}>
  221. <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>名称</th>
  222. <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>格式</th>
  223. <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>记录数</th>
  224. <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>上传时间</th>
  225. <th style={{ padding: '10px 12px', fontSize: 12, color: '#64748b', fontWeight: 600 }}>操作</th>
  226. </tr>
  227. </thead>
  228. <tbody>
  229. {datasets.map(d => (
  230. <DatasetRow key={d.id} d={d} onPreview={handlePreview} onDelete={handleDelete} />
  231. ))}
  232. </tbody>
  233. </table>
  234. </div>
  235. )}
  236. </div>
  237. {/* Preview */}
  238. {previewData && previewData.rows.length > 0 && (
  239. <div style={{
  240. marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
  241. boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
  242. }}>
  243. <h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>数据预览</h3>
  244. <div style={{ overflowX: 'auto' }}>
  245. <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
  246. <thead>
  247. <tr style={{ background: '#f0fdfa', borderBottom: '2px solid #f1f5f9', textAlign: 'left' }}>
  248. {previewData.columns.map(col => (
  249. <th key={col} style={{ padding: '8px 12px', fontSize: 12, color: '#64748b', fontWeight: 600, whiteSpace: 'nowrap' }}>{col}</th>
  250. ))}
  251. </tr>
  252. </thead>
  253. <tbody>
  254. {previewData.rows.slice(0, 10).map((row, i) => (
  255. <tr key={i} style={{ borderBottom: '1px solid #f1f5f9', transition: 'background 0.15s ease' }}
  256. onMouseEnter={e => { e.currentTarget.style.background = '#f0fdfa' }}
  257. onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
  258. >
  259. {previewData.columns.map(col => {
  260. const cellVal = String(row.data[col] ?? '')
  261. const isMultiline = cellVal.includes('\n') || cellVal.length > 100
  262. return (
  263. <td
  264. key={col}
  265. style={{
  266. padding: '8px 12px',
  267. maxWidth: isMultiline ? 500 : 200,
  268. overflow: isMultiline ? 'auto' : 'hidden',
  269. textOverflow: isMultiline ? undefined : 'ellipsis',
  270. whiteSpace: isMultiline ? 'pre-wrap' : 'nowrap',
  271. fontFamily: isMultiline ? 'monospace' : undefined,
  272. fontSize: isMultiline ? 12 : 13,
  273. }}
  274. >
  275. {cellVal}
  276. </td>
  277. )
  278. })}
  279. </tr>
  280. ))}
  281. </tbody>
  282. </table>
  283. </div>
  284. </div>
  285. )}
  286. </div>
  287. )
  288. }