| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- import { useState, useEffect } from 'react'
- import api, { DeployResponse, TrainingJob } from '../api/client'
- const EXPORT_FORMATS = [
- { value: 'safetensors', label: 'SafeTensors (推荐)' },
- { value: 'pytorch', label: 'PyTorch (.bin)' },
- { value: 'gguf', label: 'GGUF (llama.cpp)' },
- ]
- export function Deployment() {
- const [jobs, setJobs] = useState<TrainingJob[]>([])
- const [jobId, setJobId] = useState('')
- const [mergeWithBase, setMergeWithBase] = useState(false)
- const [exportFormat, setExportFormat] = useState('safetensors')
- const [running, setRunning] = useState(false)
- const [result, setResult] = useState<DeployResponse | null>(null)
- const [error, setError] = useState('')
- useEffect(() => {
- api.training.list()
- .then(data => setJobs(data.filter(j => j.status === 'completed')))
- .catch(() => setJobs([]))
- }, [])
- const handleExport = () => {
- if (!jobId.trim()) return
- setRunning(true)
- setError('')
- setResult(null)
- api.deployment.export({
- job_id: jobId,
- merge_with_base: mergeWithBase,
- export_format: exportFormat,
- })
- .then(setResult)
- .catch(err => setError(err instanceof Error ? err.message : '导出失败'))
- .finally(() => setRunning(false))
- }
- return (
- <div>
- <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型部署</h1>
- <p style={{ color: '#666', fontSize: 13, margin: '4px 0 16px' }}>导出训练好的模型用于生产部署</p>
- <div style={{ background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
- <h2 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>导出 Adapter</h2>
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12, alignItems: 'end' }}>
- <div>
- <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>训练任务</label>
- <select
- value={jobId}
- onChange={e => setJobId(e.target.value)}
- style={{ width: '100%', padding: '8px 12px', borderRadius: 4, border: '1px solid #d0d0d0', boxSizing: 'border-box', fontSize: 13 }}
- >
- <option value="" disabled>选择已完成的训练任务</option>
- {jobs.map(j => (
- <option key={j.id} value={j.id}>{j.id.slice(0, 8)}... — {j.model_id}</option>
- ))}
- </select>
- </div>
- <div>
- <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>导出格式</label>
- <select
- value={exportFormat}
- onChange={e => setExportFormat(e.target.value)}
- style={{ width: '100%', padding: '8px 12px', borderRadius: 4, border: '1px solid #d0d0d0', boxSizing: 'border-box', fontSize: 13 }}
- >
- {EXPORT_FORMATS.map(f => (
- <option key={f.value} value={f.value}>{f.label}</option>
- ))}
- </select>
- </div>
- <div>
- <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, cursor: 'pointer' }}>
- <input type="checkbox" checked={mergeWithBase} onChange={e => setMergeWithBase(e.target.checked)} />
- 合并基础模型
- </label>
- </div>
- </div>
- {error && (
- <div style={{ marginTop: 12, padding: 10, background: '#fff2f0', borderRadius: 4, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
- {error}
- </div>
- )}
- <button
- onClick={handleExport}
- disabled={running || !jobId}
- style={{
- marginTop: 16, padding: '10px 32px', borderRadius: 6, border: 'none',
- background: '#e94560', color: '#fff', cursor: 'pointer',
- opacity: (running || !jobId) ? 0.5 : 1, fontSize: 14, fontWeight: 600,
- }}
- >
- {running ? '导出中...' : '开始导出'}
- </button>
- </div>
- {result && (
- <div style={{ marginTop: 24, background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
- <h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>导出状态</h3>
- <p style={{ fontSize: 13 }}><strong>任务 ID:</strong> <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 3 }}>{result.job_id}</code></p>
- <p style={{ fontSize: 13 }}><strong>状态:</strong> <span style={{ color: result.error ? '#e94560' : '#4caf50', fontWeight: 600 }}>{result.status}</span></p>
- {result.output_path && <p style={{ fontSize: 13 }}><strong>输出路径:</strong> <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 3 }}>{result.output_path}</code></p>}
- {result.error && <p style={{ color: '#e94560', fontSize: 13 }}><strong>错误:</strong> {result.error}</p>}
- </div>
- )}
- </div>
- )
- }
|