Deployment.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { useState, useEffect } from 'react'
  2. import api, { DeployResponse, TrainingJob } from '../api/client'
  3. const EXPORT_FORMATS = [
  4. { value: 'safetensors', label: 'SafeTensors (推荐)' },
  5. { value: 'pytorch', label: 'PyTorch (.bin)' },
  6. { value: 'gguf', label: 'GGUF (llama.cpp)' },
  7. ]
  8. export function Deployment() {
  9. const [jobs, setJobs] = useState<TrainingJob[]>([])
  10. const [jobId, setJobId] = useState('')
  11. const [mergeWithBase, setMergeWithBase] = useState(false)
  12. const [exportFormat, setExportFormat] = useState('safetensors')
  13. const [running, setRunning] = useState(false)
  14. const [result, setResult] = useState<DeployResponse | null>(null)
  15. const [error, setError] = useState('')
  16. useEffect(() => {
  17. api.training.list()
  18. .then(data => setJobs(data.filter(j => j.status === 'completed')))
  19. .catch(() => setJobs([]))
  20. }, [])
  21. const handleExport = () => {
  22. if (!jobId.trim()) return
  23. setRunning(true)
  24. setError('')
  25. setResult(null)
  26. api.deployment.export({
  27. job_id: jobId,
  28. merge_with_base: mergeWithBase,
  29. export_format: exportFormat,
  30. })
  31. .then(setResult)
  32. .catch(err => setError(err instanceof Error ? err.message : '导出失败'))
  33. .finally(() => setRunning(false))
  34. }
  35. return (
  36. <div>
  37. <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型部署</h1>
  38. <p style={{ color: '#666', fontSize: 13, margin: '4px 0 16px' }}>导出训练好的模型用于生产部署</p>
  39. <div style={{ background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
  40. <h2 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>导出 Adapter</h2>
  41. <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12, alignItems: 'end' }}>
  42. <div>
  43. <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>训练任务</label>
  44. <select
  45. value={jobId}
  46. onChange={e => setJobId(e.target.value)}
  47. style={{ width: '100%', padding: '8px 12px', borderRadius: 4, border: '1px solid #d0d0d0', boxSizing: 'border-box', fontSize: 13 }}
  48. >
  49. <option value="" disabled>选择已完成的训练任务</option>
  50. {jobs.map(j => (
  51. <option key={j.id} value={j.id}>{j.id.slice(0, 8)}... — {j.model_id}</option>
  52. ))}
  53. </select>
  54. </div>
  55. <div>
  56. <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>导出格式</label>
  57. <select
  58. value={exportFormat}
  59. onChange={e => setExportFormat(e.target.value)}
  60. style={{ width: '100%', padding: '8px 12px', borderRadius: 4, border: '1px solid #d0d0d0', boxSizing: 'border-box', fontSize: 13 }}
  61. >
  62. {EXPORT_FORMATS.map(f => (
  63. <option key={f.value} value={f.value}>{f.label}</option>
  64. ))}
  65. </select>
  66. </div>
  67. <div>
  68. <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, cursor: 'pointer' }}>
  69. <input type="checkbox" checked={mergeWithBase} onChange={e => setMergeWithBase(e.target.checked)} />
  70. 合并基础模型
  71. </label>
  72. </div>
  73. </div>
  74. {error && (
  75. <div style={{ marginTop: 12, padding: 10, background: '#fff2f0', borderRadius: 4, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
  76. {error}
  77. </div>
  78. )}
  79. <button
  80. onClick={handleExport}
  81. disabled={running || !jobId}
  82. style={{
  83. marginTop: 16, padding: '10px 32px', borderRadius: 6, border: 'none',
  84. background: '#e94560', color: '#fff', cursor: 'pointer',
  85. opacity: (running || !jobId) ? 0.5 : 1, fontSize: 14, fontWeight: 600,
  86. }}
  87. >
  88. {running ? '导出中...' : '开始导出'}
  89. </button>
  90. </div>
  91. {result && (
  92. <div style={{ marginTop: 24, background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
  93. <h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>导出状态</h3>
  94. <p style={{ fontSize: 13 }}><strong>任务 ID:</strong> <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 3 }}>{result.job_id}</code></p>
  95. <p style={{ fontSize: 13 }}><strong>状态:</strong> <span style={{ color: result.error ? '#e94560' : '#4caf50', fontWeight: 600 }}>{result.status}</span></p>
  96. {result.output_path && <p style={{ fontSize: 13 }}><strong>输出路径:</strong> <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 3 }}>{result.output_path}</code></p>}
  97. {result.error && <p style={{ color: '#e94560', fontSize: 13 }}><strong>错误:</strong> {result.error}</p>}
  98. </div>
  99. )}
  100. </div>
  101. )
  102. }