Evaluation.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { useState, useEffect } from 'react'
  2. import api, { EvalResult, TrainingJob } from '../api/client'
  3. const METRICS_PRESETS = [
  4. { value: 'perplexity,loss', label: '困惑度 + Loss' },
  5. { value: 'perplexity', label: '困惑度' },
  6. { value: 'loss', label: 'Loss' },
  7. { value: 'accuracy', label: '准确率' },
  8. ]
  9. export function Evaluation() {
  10. const [jobs, setJobs] = useState<TrainingJob[]>([])
  11. const [jobId, setJobId] = useState('')
  12. const [metrics, setMetrics] = useState('perplexity,loss')
  13. const [running, setRunning] = useState(false)
  14. const [result, setResult] = useState<EvalResult | 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 handleRun = () => {
  22. if (!jobId.trim()) return
  23. setRunning(true)
  24. setError('')
  25. setResult(null)
  26. api.evaluation.run({
  27. job_id: jobId,
  28. metrics: metrics.split(',').map(s => s.trim()).filter(Boolean),
  29. })
  30. .then(res => setResult(res))
  31. .catch(err => setError(err instanceof Error ? err.message : '评估失败'))
  32. .finally(() => setRunning(false))
  33. }
  34. return (
  35. <div>
  36. <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型评估</h1>
  37. <p style={{ color: '#888', fontSize: 13, margin: '4px 0 16px' }}>对已完成的训练任务进行性能评估</p>
  38. <div style={{
  39. background: '#fff', borderRadius: 12, padding: 24,
  40. boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
  41. }}>
  42. <h2 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>运行评估</h2>
  43. <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
  44. <div>
  45. <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 6, fontWeight: 500 }}>训练任务</label>
  46. <select
  47. value={jobId}
  48. onChange={e => setJobId(e.target.value)}
  49. style={{
  50. width: '100%', padding: '10px 12px', borderRadius: 8,
  51. border: '1px solid #d0d0d0', boxSizing: 'border-box',
  52. fontSize: 14, outline: 'none', background: '#fff',
  53. transition: 'border-color 0.2s',
  54. }}
  55. onFocus={e => { e.currentTarget.style.borderColor = '#14b8a6' }}
  56. onBlur={e => { e.currentTarget.style.borderColor = '#cbd5e1' }}
  57. >
  58. <option value="" disabled>选择已完成的训练任务</option>
  59. {jobs.map(j => (
  60. <option key={j.id} value={j.id}>{j.id.slice(0, 8)}... — {j.model_id}</option>
  61. ))}
  62. </select>
  63. </div>
  64. <div>
  65. <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 6, fontWeight: 500 }}>评估指标</label>
  66. <select
  67. value={metrics}
  68. onChange={e => setMetrics(e.target.value)}
  69. style={{
  70. width: '100%', padding: '10px 12px', borderRadius: 8,
  71. border: '1px solid #d0d0d0', boxSizing: 'border-box',
  72. fontSize: 14, outline: 'none', background: '#fff',
  73. transition: 'border-color 0.2s',
  74. }}
  75. onFocus={e => { e.currentTarget.style.borderColor = '#14b8a6' }}
  76. onBlur={e => { e.currentTarget.style.borderColor = '#cbd5e1' }}
  77. >
  78. {METRICS_PRESETS.map(m => (
  79. <option key={m.value} value={m.value}>{m.label}</option>
  80. ))}
  81. </select>
  82. </div>
  83. </div>
  84. {error && (
  85. <div style={{ marginTop: 16, padding: 12, background: '#fff1f2', borderRadius: 8, fontSize: 13, color: '#e11d48', border: '1px solid #fecdd3' }}>
  86. {error}
  87. </div>
  88. )}
  89. <button
  90. onClick={handleRun}
  91. disabled={running || !jobId}
  92. style={{
  93. marginTop: 20, padding: '10px 32px', borderRadius: 8, border: 'none',
  94. background: '#14b8a6', color: '#fff', cursor: 'pointer',
  95. opacity: (running || !jobId) ? 0.5 : 1, fontSize: 14, fontWeight: 600,
  96. transition: 'all 0.2s ease',
  97. }}
  98. >
  99. {running ? '评估中...' : '启动评估'}
  100. </button>
  101. </div>
  102. {result && (
  103. <div style={{
  104. marginTop: 24, background: '#fff', borderRadius: 12, padding: 24,
  105. boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
  106. }}>
  107. <h3 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>评估结果</h3>
  108. <p style={{ fontSize: 13, color: '#666', margin: '0 0 12px' }}>
  109. 评估 ID: <code style={{ background: '#f5f5f5', padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{result.id}</code>
  110. </p>
  111. {Object.keys(result.metrics).length > 0
  112. ? (
  113. <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
  114. {Object.entries(result.metrics).map(([k, v]) => (
  115. <div key={k} style={{
  116. padding: '14px 16px', background: '#f0fdfa', borderRadius: 8,
  117. border: '1px solid #ccfbf1',
  118. }}>
  119. <div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 4 }}>{k}</div>
  120. <div style={{ fontSize: 20, fontWeight: 700, fontFamily: 'monospace', color: '#134e4a' }}>{String(v)}</div>
  121. </div>
  122. ))}
  123. </div>
  124. )
  125. : <p style={{ color: '#999', fontSize: 13 }}>评估结果为空(后端尚未返回数据)</p>
  126. }
  127. </div>
  128. )}
  129. </div>
  130. )
  131. }