Kaynağa Gözat

优化前端界面

lxylxy123321 6 gün önce
ebeveyn
işleme
1d4ca708da

+ 117 - 35
frontend/src/components/layout/Layout.tsx

@@ -2,61 +2,143 @@ import { Link, useLocation } from 'react-router-dom'
 import { useAuth } from '../../contexts/AuthContext'
 
 const NAV_ITEMS = [
-  { path: '/', label: '仪表盘' },
-  { path: '/models', label: '模型' },
-  { path: '/datasets', label: '数据集' },
-  { path: '/training', label: '训练' },
-  { path: '/evaluation', label: '评估' },
-  { path: '/deployment', label: '部署' },
-  { path: '/inference', label: '推理' },
+  { path: '/', label: '仪表盘', icon: '📊' },
+  { path: '/models', label: '模型', icon: '🧠' },
+  { path: '/datasets', label: '数据集', icon: '📁' },
+  { path: '/training', label: '训练', icon: '⚡' },
+  { path: '/evaluation', label: '评估', icon: '📈' },
+  { path: '/deployment', label: '部署', icon: '🚀' },
+  { path: '/inference', label: '推理', icon: '💬' },
 ]
 
 export function Layout({ children }: { children: React.ReactNode }) {
   const location = useLocation()
   const { user, logout } = useAuth()
+  const isActive = (path: string) => location.pathname === path
 
   return (
-    <div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, sans-serif' }}>
+    <div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, -apple-system, sans-serif', background: '#f8f9fa' }}>
       <nav style={{
-        width: 200, background: '#1a1a2e', color: '#fff', padding: '24px 0',
-        display: 'flex', flexDirection: 'column', gap: 4,
+        width: 220,
+        background: 'linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)',
+        color: '#fff',
+        display: 'flex',
+        flexDirection: 'column',
+        boxShadow: '2px 0 8px rgba(0,0,0,0.1)',
       }}>
-        <h2 style={{ padding: '0 16px 20px', fontSize: 16, fontWeight: 700, margin: 0 }}>
-          PEFT Platform
-        </h2>
-        {NAV_ITEMS.map((item) => (
-          <Link
-            key={item.path}
-            to={item.path}
-            style={{
-              padding: '10px 16px',
-              textDecoration: 'none',
-              color: location.pathname === item.path ? '#fff' : '#999',
-              background: location.pathname === item.path ? '#16213e' : 'transparent',
-              borderLeft: location.pathname === item.path ? '3px solid #e94560' : '3px solid transparent',
+        {/* Logo / Brand */}
+        <div style={{ padding: '24px 20px', borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
+          <h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, letterSpacing: '-0.5px' }}>
+            PEFT Platform
+          </h2>
+          <p style={{ fontSize: 11, color: 'rgba(255,255,255,0.4)', margin: '4px 0 0' }}>
+            Fine-Tuning Studio
+          </p>
+        </div>
+
+        {/* Navigation */}
+        <div style={{ flex: 1, padding: '12px 8px' }}>
+          {NAV_ITEMS.map((item) => {
+            const active = isActive(item.path)
+            return (
+              <Link
+                key={item.path}
+                to={item.path}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: 10,
+                  padding: '10px 14px',
+                  textDecoration: 'none',
+                  color: active ? '#fff' : 'rgba(255,255,255,0.55)',
+                  background: active ? 'rgba(233,69,96,0.15)' : 'transparent',
+                  borderRadius: 8,
+                  fontSize: 14,
+                  fontWeight: active ? 600 : 400,
+                  marginBottom: 2,
+                  transition: 'all 0.15s ease',
+                }}
+                onMouseEnter={e => {
+                  if (!active) {
+                    e.currentTarget.style.background = 'rgba(255,255,255,0.06)'
+                    e.currentTarget.style.color = 'rgba(255,255,255,0.85)'
+                  }
+                }}
+                onMouseLeave={e => {
+                  if (!active) {
+                    e.currentTarget.style.background = 'transparent'
+                    e.currentTarget.style.color = 'rgba(255,255,255,0.55)'
+                  }
+                }}
+              >
+                <span style={{ fontSize: 16, width: 22, textAlign: 'center' }}>{item.icon}</span>
+                {item.label}
+              </Link>
+            )
+          })}
+        </div>
+
+        {/* User section */}
+        <div style={{ padding: '16px 12px', borderTop: '1px solid rgba(255,255,255,0.08)' }}>
+          <div style={{
+            display: 'flex',
+            alignItems: 'center',
+            gap: 10,
+            padding: '8px 10px',
+            background: 'rgba(255,255,255,0.04)',
+            borderRadius: 8,
+            marginBottom: 10,
+          }}>
+            <div style={{
+              width: 32,
+              height: 32,
+              borderRadius: '50%',
+              background: 'linear-gradient(135deg, #e94560, #c23656)',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
               fontSize: 14,
-            }}
-          >
-            {item.label}
-          </Link>
-        ))}
-        <div style={{ marginTop: 'auto', padding: '16px', borderTop: '1px solid #333' }}>
-          <div style={{ fontSize: 13, color: '#ccc', marginBottom: 8, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
-            {user?.real_name || user?.username || '用户'}
+              fontWeight: 700,
+              flexShrink: 0,
+            }}>
+              {(user?.real_name || user?.username || 'U').charAt(0).toUpperCase()}
+            </div>
+            <div style={{ overflow: 'hidden', flex: 1 }}>
+              <div style={{ fontSize: 13, fontWeight: 500, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
+                {user?.real_name || user?.username || '用户'}
+              </div>
+              <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.4)' }}>在线</div>
+            </div>
           </div>
           <button
             onClick={logout}
             style={{
-              width: '100%', padding: '6px 12px', background: 'transparent',
-              border: '1px solid #555', borderRadius: 4, color: '#999',
-              cursor: 'pointer', fontSize: 12,
+              width: '100%',
+              padding: '8px 12px',
+              background: 'rgba(255,255,255,0.04)',
+              border: '1px solid rgba(255,255,255,0.1)',
+              borderRadius: 8,
+              color: 'rgba(255,255,255,0.6)',
+              cursor: 'pointer',
+              fontSize: 13,
+              transition: 'all 0.15s ease',
+            }}
+            onMouseEnter={e => {
+              e.currentTarget.style.background = 'rgba(233,69,96,0.15)'
+              e.currentTarget.style.borderColor = 'rgba(233,69,96,0.3)'
+              e.currentTarget.style.color = '#e94560'
+            }}
+            onMouseLeave={e => {
+              e.currentTarget.style.background = 'rgba(255,255,255,0.04)'
+              e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'
+              e.currentTarget.style.color = 'rgba(255,255,255,0.6)'
             }}
           >
             退出登录
           </button>
         </div>
       </nav>
-      <main style={{ flex: 1, padding: 24, background: '#f5f5f5' }}>{children}</main>
+      <main style={{ flex: 1, padding: '28px 32px', overflowY: 'auto' }}>{children}</main>
     </div>
   )
 }

+ 83 - 12
frontend/src/pages/AuthLogin.tsx

@@ -2,6 +2,12 @@ import { useEffect } from 'react'
 import { useNavigate } from 'react-router-dom'
 import { useAuth } from '../contexts/AuthContext'
 
+const FEATURES = [
+  { icon: '⚡', text: 'LoRA / QLoRA / IA3 等多种微调方法' },
+  { icon: '🧠', text: '支持 LLaMA、Qwen 等主流模型' },
+  { icon: '📊', text: '可视化训练监控与评估' },
+]
+
 export function Login() {
   const { isAuthenticated } = useAuth()
   const navigate = useNavigate()
@@ -11,34 +17,99 @@ export function Login() {
   }, [isAuthenticated, navigate])
 
   const handleSSOLogin = () => {
-    // 直接让浏览器处理 307 重定向到 SSO 授权页
     window.location.href = '/auth/sso/authorize?redirect=true'
   }
 
   return (
     <div style={{
       display: 'flex', justifyContent: 'center', alignItems: 'center',
-      minHeight: '100vh', background: '#0f0f23',
+      minHeight: '100vh',
+      background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%)',
+      position: 'relative',
+      overflow: 'hidden',
     }}>
+      {/* 装饰性背景圆 */}
+      <div style={{
+        position: 'absolute', width: 500, height: 500, borderRadius: '50%',
+        background: 'radial-gradient(circle, rgba(233,69,96,0.08) 0%, transparent 70%)',
+        top: -150, right: -100, pointerEvents: 'none',
+      }} />
       <div style={{
-        background: '#1a1a2e', borderRadius: 12, padding: 48, width: 420,
-        boxShadow: '0 4px 24px rgba(0,0,0,0.4)', textAlign: 'center',
-        border: '1px solid #333',
+        position: 'absolute', width: 400, height: 400, borderRadius: '50%',
+        background: 'radial-gradient(circle, rgba(33,150,243,0.06) 0%, transparent 70%)',
+        bottom: -100, left: -80, pointerEvents: 'none',
+      }} />
+
+      <div style={{
+        position: 'relative', zIndex: 1,
+        width: 440,
+        background: 'rgba(26,26,46,0.95)',
+        backdropFilter: 'blur(10px)',
+        borderRadius: 16,
+        padding: '40px 36px',
+        border: '1px solid rgba(255,255,255,0.08)',
+        boxShadow: '0 8px 40px rgba(0,0,0,0.5)',
       }}>
-        <h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8, color: '#fff' }}>
-          PEFT Fine-Tuning Platform
-        </h1>
-        <p style={{ color: '#999', marginBottom: 32, fontSize: 14 }}>统一认证登录</p>
+        {/* Header */}
+        <div style={{ textAlign: 'center', marginBottom: 32 }}>
+          <div style={{
+            width: 56, height: 56, borderRadius: 16, margin: '0 auto 16px',
+            background: 'linear-gradient(135deg, #e94560, #c23656)',
+            display: 'flex', alignItems: 'center', justifyContent: 'center',
+            fontSize: 24, boxShadow: '0 4px 16px rgba(233,69,96,0.3)',
+          }}>
+            🎯
+          </div>
+          <h1 style={{ fontSize: 22, fontWeight: 800, margin: '0 0 6px', color: '#fff', letterSpacing: '-0.5px' }}>
+            PEFT Fine-Tuning Platform
+          </h1>
+          <p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>
+            高效、易用的参数高效微调平台
+          </p>
+        </div>
+
+        {/* Features */}
+        <div style={{ marginBottom: 28 }}>
+          {FEATURES.map((f, i) => (
+            <div key={i} style={{
+              display: 'flex', alignItems: 'center', gap: 10,
+              padding: '8px 12px', marginBottom: i < FEATURES.length - 1 ? 4 : 0,
+              background: 'rgba(255,255,255,0.03)', borderRadius: 8,
+            }}>
+              <span style={{ fontSize: 16 }}>{f.icon}</span>
+              <span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)' }}>{f.text}</span>
+            </div>
+          ))}
+        </div>
+
+        {/* SSO Button */}
         <button
           onClick={handleSSOLogin}
           style={{
-            width: '100%', padding: '12px 24px', borderRadius: 8,
-            border: 'none', background: '#e94560', color: '#fff',
-            fontSize: 16, fontWeight: 600, cursor: 'pointer',
+            width: '100%', padding: '14px 24px', borderRadius: 10,
+            border: 'none',
+            background: 'linear-gradient(135deg, #e94560, #c23656)',
+            color: '#fff', fontSize: 15, fontWeight: 600,
+            cursor: 'pointer',
+            boxShadow: '0 4px 16px rgba(233,69,96,0.3)',
+            transition: 'all 0.2s ease',
+          }}
+          onMouseEnter={e => {
+            e.currentTarget.style.transform = 'translateY(-1px)'
+            e.currentTarget.style.boxShadow = '0 6px 20px rgba(233,69,96,0.4)'
+          }}
+          onMouseLeave={e => {
+            e.currentTarget.style.transform = 'translateY(0)'
+            e.currentTarget.style.boxShadow = '0 4px 16px rgba(233,69,96,0.3)'
           }}
         >
           使用统一认证平台登录
         </button>
+
+        {/* Footer */}
+        <p style={{ textAlign: 'center', fontSize: 12, color: 'rgba(255,255,255,0.25)', margin: '24px 0 0' }}>
+          登录后即可使用全部微调功能
+        </p>
       </div>
     </div>
   )

+ 116 - 13
frontend/src/pages/Dashboard.tsx

@@ -1,20 +1,53 @@
 import { useState, useEffect, memo } from 'react'
 import api from '../api/client'
 
-const StatCard = memo(function StatCard({ title, value, desc }: { title: string; value: number; desc: string }) {
+const STAT_CARDS = [
+  { title: '模型', icon: '🧠', desc: '已缓存', color: '#e94560', bg: '#fff5f7' },
+  { title: '数据集', icon: '📁', desc: '已上传', color: '#2196f3', bg: '#f0f7ff' },
+  { title: '训练任务', icon: '⚡', desc: '总数', color: '#4caf50', bg: '#f0fff4' },
+]
+
+const StatCard = memo(function StatCard({ title, value, desc, icon, color, bg }: {
+  title: string; value: number; desc: string; icon: string; color: string; bg: string
+}) {
   return (
-    <div style={{ background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.1)' }}>
-      <div style={{ fontSize: 13, color: '#666' }}>{title}</div>
-      <div style={{ fontSize: 32, fontWeight: 700, margin: '8px 0' }}>{value}</div>
+    <div style={{
+      background: '#fff', borderRadius: 12, padding: '20px 24px',
+      boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
+      border: '1px solid rgba(0,0,0,0.04)',
+      transition: 'all 0.2s ease',
+      cursor: 'default',
+    }}
+    onMouseEnter={e => {
+      e.currentTarget.style.transform = 'translateY(-2px)'
+      e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'
+    }}
+    onMouseLeave={e => {
+      e.currentTarget.style.transform = 'translateY(0)'
+      e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)'
+    }}
+    >
+      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
+        <div style={{ fontSize: 13, color: '#666', fontWeight: 500 }}>{title}</div>
+        <div style={{
+          width: 36, height: 36, borderRadius: 10, background: bg,
+          display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 18,
+        }}>
+          {icon}
+        </div>
+      </div>
+      <div style={{ fontSize: 36, fontWeight: 800, margin: '4px 0', color, letterSpacing: '-1px' }}>
+        {value}
+      </div>
       <div style={{ fontSize: 12, color: '#999' }}>{desc}</div>
     </div>
   )
 })
 
 export function Dashboard() {
-  const [models, setModels] = useState(0)
-  const [datasets, setDatasets] = useState(0)
-  const [jobs, setJobs] = useState(0)
+  const [models, setModels] = useState<number | null>(null)
+  const [datasets, setDatasets] = useState<number | null>(null)
+  const [jobs, setJobs] = useState<number | null>(null)
 
   useEffect(() => {
     api.models.list()
@@ -28,14 +61,84 @@ export function Dashboard() {
       .catch(() => setJobs(0))
   }, [])
 
+  const loading = models === null || datasets === null || jobs === null
+
   return (
     <div>
-      <h1>仪表盘</h1>
-      <p>PEFT Fine-Tuning Platform v0.1.0</p>
-      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginTop: 24 }}>
-        <StatCard title="模型" value={models} desc="已缓存" />
-        <StatCard title="数据集" value={datasets} desc="已上传" />
-        <StatCard title="训练任务" value={jobs} desc="总数" />
+      {/* Header */}
+      <div style={{ marginBottom: 28 }}>
+        <h1 style={{ margin: 0, fontSize: 24, fontWeight: 800, color: '#1a1a2e', letterSpacing: '-0.5px' }}>
+          仪表盘
+        </h1>
+        <p style={{ color: '#888', fontSize: 14, margin: '6px 0 0' }}>
+          PEFT Fine-Tuning Platform v0.1.0 — 欢迎回来
+        </p>
+      </div>
+
+      {/* Stats */}
+      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
+        {loading ? (
+          STAT_CARDS.map((c, i) => (
+            <div key={i} style={{
+              background: '#fff', borderRadius: 12, padding: '20px 24px',
+              boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+            }}>
+              <div style={{ height: 36, marginBottom: 12, background: '#f0f0f0', borderRadius: 10, width: 36, marginLeft: 'auto' }} />
+              <div style={{ height: 14, width: 60, background: '#f0f0f0', borderRadius: 4, marginBottom: 12 }} />
+              <div style={{ height: 36, width: 80, background: '#f0f0f0', borderRadius: 6, marginBottom: 8 }} />
+              <div style={{ height: 12, width: 40, background: '#f0f0f0', borderRadius: 4 }} />
+            </div>
+          ))
+        ) : (
+          STAT_CARDS.map((c, i) => (
+            <StatCard key={i} title={c.title} value={[models, datasets, jobs][i]!} desc={c.desc} icon={c.icon} color={c.color} bg={c.bg} />
+          ))
+        )}
+      </div>
+
+      {/* Quick actions */}
+      <div style={{ marginTop: 32 }}>
+        <h2 style={{ margin: '0 0 16px', fontSize: 16, fontWeight: 700, color: '#1a1a2e' }}>快速开始</h2>
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
+          {[
+            { label: '下载模型', desc: '从 HuggingFace 或 ModelScope 下载', icon: '📥', link: '/models', color: '#e94560' },
+            { label: '上传数据集', desc: '支持 JSONL / CSV / Parquet', icon: '📤', link: '/datasets', color: '#2196f3' },
+            { label: '创建训练', desc: '配置超参数并启动微调', icon: '🚀', link: '/training', color: '#4caf50' },
+          ].map(item => (
+            <a
+              key={item.label}
+              href={item.link}
+              style={{
+                display: 'flex', gap: 14, alignItems: 'center',
+                padding: '16px 20px', background: '#fff', borderRadius: 10,
+                border: '1px solid rgba(0,0,0,0.06)',
+                boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
+                textDecoration: 'none', transition: 'all 0.2s ease',
+              }}
+              onMouseEnter={e => {
+                e.currentTarget.style.transform = 'translateY(-2px)'
+                e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'
+                e.currentTarget.style.borderColor = item.color + '30'
+              }}
+              onMouseLeave={e => {
+                e.currentTarget.style.transform = 'translateY(0)'
+                e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.04)'
+                e.currentTarget.style.borderColor = 'rgba(0,0,0,0.06)'
+              }}
+            >
+              <div style={{
+                width: 40, height: 40, borderRadius: 10, background: item.color + '10',
+                display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 20,
+              }}>
+                {item.icon}
+              </div>
+              <div>
+                <div style={{ fontSize: 14, fontWeight: 600, color: '#1a1a2e' }}>{item.label}</div>
+                <div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>{item.desc}</div>
+              </div>
+            </a>
+          ))}
+        </div>
       </div>
     </div>
   )

+ 155 - 75
frontend/src/pages/Datasets.tsx

@@ -7,14 +7,41 @@ const DatasetRow = memo(function DatasetRow({ d, onPreview, onDelete }: {
   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>
+    <tr style={{
+      borderBottom: '1px solid #f0f0f0',
+      transition: 'background 0.15s ease',
+    }}
+    onMouseEnter={e => { e.currentTarget.style.background = '#fafbfc' }}
+    onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
+    >
+      <td style={{ padding: '12px 12px', fontWeight: 500, fontSize: 13 }}>{d.name}</td>
+      <td style={{ padding: '12px 12px', fontSize: 13, color: '#666' }}>
+        <span style={{
+          display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
+          background: '#f5f5f5', color: '#666',
+        }}>
+          {d.format}
+        </span>
+      </td>
+      <td style={{ padding: '12px 12px', fontSize: 13 }}>{d.record_count}</td>
+      <td style={{ padding: '12px 12px', fontSize: 13, color: '#888' }}>{d.created_at}</td>
+      <td style={{ padding: '12px 12px' }}>
+        <button onClick={() => onPreview(d.id)} style={{
+          marginRight: 8, padding: '4px 12px', color: '#2196f3',
+          border: '1px solid #2196f3', borderRadius: 6, background: 'transparent',
+          cursor: 'pointer', fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
+        }}
+        onMouseEnter={e => { e.currentTarget.style.background = '#2196f3'; e.currentTarget.style.color = '#fff' }}
+        onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#2196f3' }}
+        >预览</button>
+        <button onClick={() => onDelete(d.id)} style={{
+          padding: '4px 12px', color: '#e94560', border: '1px solid #e94560',
+          borderRadius: 6, background: 'transparent', cursor: 'pointer',
+          fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
+        }}
+        onMouseEnter={e => { e.currentTarget.style.background = '#e94560'; e.currentTarget.style.color = '#fff' }}
+        onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#e94560' }}
+        >删除</button>
       </td>
     </tr>
   )
@@ -87,18 +114,28 @@ export function Datasets() {
 
   return (
     <div>
-      <h1>数据集管理</h1>
+      <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>数据集管理</h1>
+      <p style={{ color: '#888', fontSize: 13, margin: '4px 0 16px' }}>上传和管理训练数据集</p>
 
       {/* 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,
+          marginTop: 16, border: '2px dashed #d0d0d0', borderRadius: 12,
+          padding: 40, textAlign: 'center', color: '#888', cursor: 'pointer',
+          opacity: uploading ? 0.6 : 1, background: '#fff',
+          transition: 'all 0.2s ease',
         }}
+        onMouseEnter={e => { e.currentTarget.style.borderColor = '#e94560'; e.currentTarget.style.background = '#fff5f7' }}
+        onMouseLeave={e => { e.currentTarget.style.borderColor = '#d0d0d0'; e.currentTarget.style.background = '#fff' }}
       >
-        {uploading ? '上传中...' : '拖拽文件到此处或点击上传 (JSONL / CSV / Parquet / JSON)'}
+        <div style={{ fontSize: 32, marginBottom: 8 }}>{uploading ? '⏳' : '📤'}</div>
+        <div style={{ fontSize: 14, fontWeight: 500 }}>
+          {uploading ? '上传中...' : '拖拽文件到此处或点击上传'}
+        </div>
+        <div style={{ fontSize: 12, color: '#aaa', marginTop: 4 }}>
+          支持 JSONL / CSV / Parquet / JSON 格式
+        </div>
         <input
           ref={inputRef}
           type="file"
@@ -109,105 +146,148 @@ export function Datasets() {
       </div>
 
       {/* Download section */}
-      <div style={{ marginTop: 24 }}>
-        <h2 style={{ margin: '0 0 12px', fontSize: 16 }}>从平台下载</h2>
+      <div style={{
+        marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
+        <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>从平台下载</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' }}
+            style={{
+              padding: '10px 14px', flex: 1, maxWidth: 400, borderRadius: 8,
+              border: '1px solid #d0d0d0', fontSize: 14, outline: 'none',
+              transition: 'border-color 0.2s',
+            }}
+            onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+            onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
           />
-          <label style={{ fontSize: 13, color: '#666', whiteSpace: 'nowrap' }}>
+          <label style={{ fontSize: 13, color: '#666', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
             <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 }}
+            style={{
+              padding: '10px 20px', borderRadius: 8, border: 'none',
+              background: '#e94560', color: '#fff', cursor: 'pointer',
+              opacity: downloading ? 0.6 : 1, fontSize: 14, fontWeight: 600,
+            }}
           >
             {downloading ? '下载中...' : '下载数据集'}
           </button>
         </div>
-        {dlStatus && <p style={{ marginTop: 8, fontSize: 13, color: dlStatus.includes('failed') || dlStatus.includes('失败') ? '#e94560' : '#666' }}>{dlStatus}</p>}
+        {dlStatus && <p style={{
+          marginTop: 10, padding: '8px 12px', borderRadius: 6, fontSize: 13,
+          background: dlStatus.includes('失败') ? '#fff2f0' : '#f5f5f5',
+          color: dlStatus.includes('失败') ? '#cf1322' : '#666',
+          border: `1px solid ${dlStatus.includes('失败') ? '#ffccc7' : 'transparent'}`,
+        }}>{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' }}>
+          <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>已上传数据集</h2>
+          <button onClick={fetchDatasets} style={{
+            padding: '6px 14px', borderRadius: 6, border: '1px solid #d0d0d0',
+            background: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500,
+          }}
+          onMouseEnter={e => { e.currentTarget.style.background = '#f5f5f5' }}
+          onMouseLeave={e => { e.currentTarget.style.background = '#fff' }}
+          >
             刷新
           </button>
         </div>
 
-        {loading && <p style={{ color: '#999' }}>加载中...</p>}
+        {loading && <p style={{ color: '#999', fontSize: 13 }}>加载中...</p>}
 
         {!loading && datasets.length === 0 && (
-          <p style={{ color: '#999', fontSize: 14 }}>暂无数据集</p>
+          <div style={{
+            padding: 40, textAlign: 'center', color: '#999', fontSize: 14,
+            background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
+          }}>
+            <div style={{ fontSize: 32, marginBottom: 8 }}>📁</div>
+            暂无数据集,请上传文件或从平台下载
+          </div>
         )}
 
         {!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 style={{
+            background: '#fff', borderRadius: 10, overflow: 'hidden',
+            boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+          }}>
+            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
+              <thead>
+                <tr style={{ background: '#fafbfc', borderBottom: '2px solid #f0f0f0', textAlign: 'left' }}>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>名称</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>格式</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>记录数</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>上传时间</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>操作</th>
+                </tr>
+              </thead>
+              <tbody>
+                {datasets.map(d => (
+                  <DatasetRow key={d.id} d={d} onPreview={handlePreview} onDelete={handleDelete} />
+                ))}
+              </tbody>
+            </table>
+          </div>
         )}
       </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>
-                    )
-                  })}
+        <div style={{
+          marginTop: 24, background: '#fff', borderRadius: 10, padding: 20,
+          boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+        }}>
+          <h3 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>数据预览</h3>
+          <div style={{ overflowX: 'auto' }}>
+            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
+              <thead>
+                <tr style={{ background: '#fafbfc', borderBottom: '2px solid #f0f0f0', textAlign: 'left' }}>
+                  {previewData.columns.map(col => (
+                    <th key={col} style={{ padding: '8px 12px', fontSize: 12, color: '#666', fontWeight: 600, whiteSpace: 'nowrap' }}>{col}</th>
+                  ))}
                 </tr>
-              ))}
-            </tbody>
-          </table>
+              </thead>
+              <tbody>
+                {previewData.rows.slice(0, 10).map((row, i) => (
+                  <tr key={i} style={{ borderBottom: '1px solid #f0f0f0', transition: 'background 0.15s ease' }}
+                    onMouseEnter={e => { e.currentTarget.style.background = '#fafbfc' }}
+                    onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
+                  >
+                    {previewData.columns.map(col => {
+                      const cellVal = String(row.data[col] ?? '')
+                      const isMultiline = cellVal.includes('\n') || cellVal.length > 100
+                      return (
+                        <td
+                          key={col}
+                          style={{
+                            padding: '8px 12px',
+                            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>
       )}
     </div>

+ 58 - 16
frontend/src/pages/Deployment.tsx

@@ -40,17 +40,27 @@ export function Deployment() {
   return (
     <div>
       <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型部署</h1>
-      <p style={{ color: '#666', fontSize: 13, margin: '4px 0 16px' }}>导出训练好的模型用于生产部署</p>
+      <p style={{ color: '#888', 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)' }}>
+      <div style={{
+        background: '#fff', borderRadius: 12, padding: 24,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
         <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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, alignItems: 'end' }}>
           <div>
-            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>训练任务</label>
+            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 6, fontWeight: 500 }}>训练任务</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 }}
+              style={{
+                width: '100%', padding: '10px 12px', borderRadius: 8,
+                border: '1px solid #d0d0d0', boxSizing: 'border-box',
+                fontSize: 14, outline: 'none', background: '#fff',
+                transition: 'border-color 0.2s',
+              }}
+              onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+              onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
             >
               <option value="" disabled>选择已完成的训练任务</option>
               {jobs.map(j => (
@@ -59,11 +69,18 @@ export function Deployment() {
             </select>
           </div>
           <div>
-            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>导出格式</label>
+            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 6, fontWeight: 500 }}>导出格式</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 }}
+              style={{
+                width: '100%', padding: '10px 12px', borderRadius: 8,
+                border: '1px solid #d0d0d0', boxSizing: 'border-box',
+                fontSize: 14, outline: 'none', background: '#fff',
+                transition: 'border-color 0.2s',
+              }}
+              onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+              onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
             >
               {EXPORT_FORMATS.map(f => (
                 <option key={f.value} value={f.value}>{f.label}</option>
@@ -71,7 +88,10 @@ export function Deployment() {
             </select>
           </div>
           <div>
-            <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, cursor: 'pointer' }}>
+            <label style={{
+              display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, cursor: 'pointer',
+              padding: '10px 14px', background: '#fafbfc', borderRadius: 8, border: '1px solid #f0f0f0',
+            }}>
               <input type="checkbox" checked={mergeWithBase} onChange={e => setMergeWithBase(e.target.checked)} />
               合并基础模型
             </label>
@@ -79,7 +99,7 @@ export function Deployment() {
         </div>
 
         {error && (
-          <div style={{ marginTop: 12, padding: 10, background: '#fff2f0', borderRadius: 4, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
+          <div style={{ marginTop: 16, padding: 12, background: '#fff2f0', borderRadius: 8, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
             {error}
           </div>
         )}
@@ -88,9 +108,10 @@ export function Deployment() {
           onClick={handleExport}
           disabled={running || !jobId}
           style={{
-            marginTop: 16, padding: '10px 32px', borderRadius: 6, border: 'none',
+            marginTop: 20, padding: '10px 32px', borderRadius: 8, border: 'none',
             background: '#e94560', color: '#fff', cursor: 'pointer',
             opacity: (running || !jobId) ? 0.5 : 1, fontSize: 14, fontWeight: 600,
+            transition: 'all 0.2s ease',
           }}
         >
           {running ? '导出中...' : '开始导出'}
@@ -98,12 +119,33 @@ export function Deployment() {
       </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 style={{
+          marginTop: 24, background: '#fff', borderRadius: 12, padding: 24,
+          boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+        }}>
+          <h3 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>导出状态</h3>
+          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
+            <div style={{ padding: '14px 16px', background: '#fafbfc', borderRadius: 8, border: '1px solid #f0f0f0' }}>
+              <div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>任务 ID</div>
+              <div style={{ fontSize: 14, fontFamily: 'monospace' }}>{result.job_id}</div>
+            </div>
+            <div style={{ padding: '14px 16px', background: result.error ? '#fff2f0' : '#f0fff4', borderRadius: 8, border: `1px solid ${result.error ? '#ffccc7' : '#d1f0d8'}` }}>
+              <div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>状态</div>
+              <div style={{ color: result.error ? '#cf1322' : '#22863a', fontWeight: 600, fontSize: 14 }}>{result.status}</div>
+            </div>
+            {result.output_path && (
+              <div style={{ padding: '14px 16px', background: '#fafbfc', borderRadius: 8, border: '1px solid #f0f0f0' }}>
+                <div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>输出路径</div>
+                <div style={{ fontSize: 13, fontFamily: 'monospace', wordBreak: 'break-all' }}>{result.output_path}</div>
+              </div>
+            )}
+            {result.error && (
+              <div style={{ padding: '14px 16px', background: '#fff2f0', borderRadius: 8, border: '1px solid #ffccc7' }}>
+                <div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>错误</div>
+                <div style={{ color: '#cf1322', fontSize: 13 }}>{result.error}</div>
+              </div>
+            )}
+          </div>
         </div>
       )}
     </div>

+ 46 - 28
frontend/src/pages/Evaluation.tsx

@@ -39,17 +39,27 @@ export function Evaluation() {
   return (
     <div>
       <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型评估</h1>
-      <p style={{ color: '#666', fontSize: 13, margin: '4px 0 16px' }}>对已完成的训练任务进行性能评估</p>
+      <p style={{ color: '#888', 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)' }}>
+      <div style={{
+        background: '#fff', borderRadius: 12, padding: 24,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
         <h2 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>运行评估</h2>
-        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
+        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
           <div>
-            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>训练任务</label>
+            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 6, fontWeight: 500 }}>训练任务</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 }}
+              style={{
+                width: '100%', padding: '10px 12px', borderRadius: 8,
+                border: '1px solid #d0d0d0', boxSizing: 'border-box',
+                fontSize: 14, outline: 'none', background: '#fff',
+                transition: 'border-color 0.2s',
+              }}
+              onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+              onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
             >
               <option value="" disabled>选择已完成的训练任务</option>
               {jobs.map(j => (
@@ -58,11 +68,18 @@ export function Evaluation() {
             </select>
           </div>
           <div>
-            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>评估指标</label>
+            <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 6, fontWeight: 500 }}>评估指标</label>
             <select
               value={metrics}
               onChange={e => setMetrics(e.target.value)}
-              style={{ width: '100%', padding: '8px 12px', borderRadius: 4, border: '1px solid #d0d0d0', boxSizing: 'border-box', fontSize: 13 }}
+              style={{
+                width: '100%', padding: '10px 12px', borderRadius: 8,
+                border: '1px solid #d0d0d0', boxSizing: 'border-box',
+                fontSize: 14, outline: 'none', background: '#fff',
+                transition: 'border-color 0.2s',
+              }}
+              onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+              onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
             >
               {METRICS_PRESETS.map(m => (
                 <option key={m.value} value={m.value}>{m.label}</option>
@@ -72,7 +89,7 @@ export function Evaluation() {
         </div>
 
         {error && (
-          <div style={{ marginTop: 12, padding: 10, background: '#fff2f0', borderRadius: 4, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
+          <div style={{ marginTop: 16, padding: 12, background: '#fff2f0', borderRadius: 8, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
             {error}
           </div>
         )}
@@ -81,9 +98,10 @@ export function Evaluation() {
           onClick={handleRun}
           disabled={running || !jobId}
           style={{
-            marginTop: 16, padding: '10px 32px', borderRadius: 6, border: 'none',
+            marginTop: 20, padding: '10px 32px', borderRadius: 8, border: 'none',
             background: '#e94560', color: '#fff', cursor: 'pointer',
             opacity: (running || !jobId) ? 0.5 : 1, fontSize: 14, fontWeight: 600,
+            transition: 'all 0.2s ease',
           }}
         >
           {running ? '评估中...' : '启动评估'}
@@ -91,27 +109,27 @@ export function Evaluation() {
       </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, color: '#666' }}>评估 ID: <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 3 }}>{result.id}</code></p>
+        <div style={{
+          marginTop: 24, background: '#fff', borderRadius: 12, padding: 24,
+          boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+        }}>
+          <h3 style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 600 }}>评估结果</h3>
+          <p style={{ fontSize: 13, color: '#666', margin: '0 0 12px' }}>
+            评估 ID: <code style={{ background: '#f5f5f5', padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{result.id}</code>
+          </p>
           {Object.keys(result.metrics).length > 0
             ? (
-              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14, marginTop: 12 }}>
-                <thead>
-                  <tr style={{ borderBottom: '2px solid #eee', textAlign: 'left' }}>
-                    <th style={{ padding: '8px 12px', fontSize: 12, color: '#666' }}>指标</th>
-                    <th style={{ padding: '8px 12px', fontSize: 12, color: '#666' }}>值</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {Object.entries(result.metrics).map(([k, v]) => (
-                    <tr key={k} style={{ borderBottom: '1px solid #eee' }}>
-                      <td style={{ padding: '8px 12px' }}>{k}</td>
-                      <td style={{ padding: '8px 12px', fontFamily: 'monospace' }}>{String(v)}</td>
-                    </tr>
-                  ))}
-                </tbody>
-              </table>
+              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
+                {Object.entries(result.metrics).map(([k, v]) => (
+                  <div key={k} style={{
+                    padding: '14px 16px', background: '#fafbfc', borderRadius: 8,
+                    border: '1px solid #f0f0f0',
+                  }}>
+                    <div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>{k}</div>
+                    <div style={{ fontSize: 20, fontWeight: 700, fontFamily: 'monospace', color: '#1a1a2e' }}>{String(v)}</div>
+                  </div>
+                ))}
+              </div>
             )
             : <p style={{ color: '#999', fontSize: 13 }}>评估结果为空(后端尚未返回数据)</p>
           }

+ 68 - 29
frontend/src/pages/Inference.tsx

@@ -115,20 +115,33 @@ export function Inference() {
   return (
     <div>
       <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型推理</h1>
-      <p style={{ color: '#666', fontSize: 13, margin: '4px 0 16px' }}>使用训练好的 Adapter 进行文本生成</p>
+      <p style={{ color: '#888', fontSize: 13, margin: '4px 0 16px' }}>使用训练好的 Adapter 进行文本生成</p>
 
       {/* Adapter selector */}
-      <div style={{ background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
+      <div style={{
+        background: '#fff', borderRadius: 12, padding: 20,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
         <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>选择 Adapter</h2>
         {adapters.length === 0 ? (
-          <div style={{ padding: 20, textAlign: 'center', color: '#999', fontSize: 13, background: '#fafafa', borderRadius: 6 }}>
+          <div style={{
+            padding: 24, textAlign: 'center', color: '#999', fontSize: 13,
+            background: '#fafbfc', borderRadius: 8, border: '1px solid #f0f0f0',
+          }}>
+            <div style={{ fontSize: 24, marginBottom: 6 }}>💬</div>
             暂无可用的 adapter,请先完成训练任务
           </div>
         ) : (
           <select
             value={adapterId}
             onChange={e => setAdapterId(e.target.value)}
-            style={{ padding: '8px 12px', borderRadius: 4, border: '1px solid #d0d0d0', width: '100%', maxWidth: 500, fontSize: 13 }}
+            style={{
+              padding: '10px 14px', borderRadius: 8, border: '1px solid #d0d0d0',
+              width: '100%', maxWidth: 500, fontSize: 14, outline: 'none',
+              transition: 'border-color 0.2s',
+            }}
+            onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+            onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
           >
             {adapters.map(a => (
               <option key={a.id} value={a.id}>{a.id} — {a.base_model} ({a.peft_type})</option>
@@ -138,41 +151,56 @@ export function Inference() {
       </div>
 
       {/* Prompt input */}
-      <div style={{ marginTop: 16, background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
+      <div style={{
+        marginTop: 16, background: '#fff', borderRadius: 12, padding: 24,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
         <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>输入提示词</h2>
         <textarea
           value={prompt}
           onChange={e => setPrompt(e.target.value)}
           placeholder="输入你的问题或指令..."
           rows={4}
-          style={{ width: '100%', padding: 12, borderRadius: 6, border: '1px solid #d0d0d0', fontSize: 14, boxSizing: 'border-box', resize: 'vertical', lineHeight: 1.6 }}
+          style={{
+            width: '100%', padding: 14, borderRadius: 8, border: '1px solid #d0d0d0',
+            fontSize: 14, boxSizing: 'border-box', resize: 'vertical', lineHeight: 1.6,
+            outline: 'none', transition: 'border-color 0.2s',
+          }}
+          onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+          onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
         />
 
         {/* Generation params */}
-        <div style={{ marginTop: 16 }}>
-          <div style={{ fontSize: 13, fontWeight: 600, color: '#666', marginBottom: 8 }}>生成参数</div>
+        <div style={{ marginTop: 20 }}>
+          <div style={{ fontSize: 13, fontWeight: 600, color: '#666', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
+            <span>生成参数</span>
+            <span style={{ fontSize: 11, color: '#aaa', fontWeight: 400 }}>(选择预设值快速配置)</span>
+          </div>
           <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
             <div>
-              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 4 }}>最大输出长度</label>
+              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 6 }}>最大输出长度</label>
               <Select options={MAX_TOKEN_PRESETS} value={String(maxTokens)} onChange={v => setMaxTokens(Number(v))} />
             </div>
             <div>
-              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 4 }}>Temperature</label>
+              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 6 }}>Temperature</label>
               <Select options={TEMP_PRESETS} value={String(temperature)} onChange={v => setTemperature(Number(v))} />
             </div>
             <div>
-              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 4 }}>Top P</label>
+              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 6 }}>Top P</label>
               <Select options={TOP_P_PRESETS} value={String(topP)} onChange={v => setTopP(Number(v))} />
             </div>
             <div>
-              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 4 }}>重复惩罚</label>
+              <label style={{ display: 'block', fontSize: 12, color: '#888', marginBottom: 6 }}>重复惩罚</label>
               <Select options={REP_PENALTY_PRESETS} value={String(repetitionPenalty)} onChange={v => setRepetitionPenalty(Number(v))} />
             </div>
           </div>
 
-          <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, cursor: 'pointer', marginTop: 12 }}>
+          <label style={{
+            display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, cursor: 'pointer',
+            marginTop: 16, padding: '10px 14px', background: '#fafbfc', borderRadius: 8, border: '1px solid #f0f0f0',
+          }}>
             <input type="checkbox" checked={doSample} onChange={e => setDoSample(e.target.checked)} />
-            启用随机采样 (关闭则为贪婪解码)
+            启用随机采样 <span style={{ fontSize: 12, color: '#aaa' }}>(关闭则为贪婪解码)</span>
           </label>
         </div>
 
@@ -180,10 +208,10 @@ export function Inference() {
           onClick={handleGenerate}
           disabled={generating || !adapterId || !prompt.trim()}
           style={{
-            marginTop: 16, padding: '10px 32px', borderRadius: 6, border: 'none',
+            marginTop: 20, padding: '12px 36px', borderRadius: 8, border: 'none',
             background: '#2196f3', color: '#fff', cursor: 'pointer',
             opacity: (generating || !adapterId || !prompt.trim()) ? 0.5 : 1,
-            fontSize: 14, fontWeight: 600,
+            fontSize: 14, fontWeight: 600, transition: 'all 0.2s ease',
           }}
         >
           {generating ? '生成中...' : '生成'}
@@ -192,41 +220,51 @@ export function Inference() {
 
       {/* Error */}
       {error && (
-        <div style={{ marginTop: 16, padding: 14, background: '#fff2f0', borderRadius: 6, color: '#cf1322', fontSize: 13, border: '1px solid #ffccc7' }}>
+        <div style={{
+          marginTop: 16, padding: 14, background: '#fff2f0', borderRadius: 8,
+          color: '#cf1322', fontSize: 13, border: '1px solid #ffccc7',
+        }}>
           {error}
         </div>
       )}
 
       {/* Result */}
       {result && (
-        <div style={{ marginTop: 16, background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
-          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
+        <div style={{
+          marginTop: 16, background: '#fff', borderRadius: 12, padding: 24,
+          boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+        }}>
+          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
             <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>生成结果</h2>
-            <span style={{ fontSize: 12, color: '#999', background: '#f5f5f5', padding: '2px 10px', borderRadius: 10 }}>
+            <span style={{
+              fontSize: 12, color: '#666', background: '#f0f7ff', padding: '4px 12px',
+              borderRadius: 12, fontWeight: 500,
+            }}>
               {result.tokens_generated} tokens
             </span>
           </div>
 
           {/* Prompt */}
-          <div style={{ marginBottom: 16 }}>
-            <div style={{ fontSize: 12, color: '#999', marginBottom: 6, fontWeight: 600 }}>Prompt</div>
-            <div style={{ padding: 12, background: '#f0f7ff', borderRadius: 6, fontSize: 14, lineHeight: 1.6 }}>
+          <div style={{ marginBottom: 20 }}>
+            <div style={{ fontSize: 12, color: '#888', marginBottom: 8, fontWeight: 600 }}>Prompt</div>
+            <div style={{ padding: 14, background: '#f0f7ff', borderRadius: 8, fontSize: 14, lineHeight: 1.6, border: '1px solid #d6e8fa' }}>
               {prompt}
             </div>
           </div>
 
           {/* Response */}
           <div>
-            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
-              <span style={{ fontSize: 12, color: '#999', fontWeight: 600 }}>Response</span>
+            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
+              <span style={{ fontSize: 12, color: '#888', fontWeight: 600 }}>Response</span>
               <div style={{ display: 'flex', gap: 4 }}>
                 <button
                   onClick={() => setViewMode('full')}
                   style={{
-                    padding: '3px 10px', borderRadius: 4, fontSize: 12, cursor: 'pointer',
+                    padding: '4px 12px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
                     border: `1px solid ${viewMode === 'full' ? '#e94560' : '#d0d0d0'}`,
                     background: viewMode === 'full' ? '#e94560' : '#fff',
                     color: viewMode === 'full' ? '#fff' : '#666',
+                    fontWeight: 500, transition: 'all 0.15s ease',
                   }}
                 >
                   完整输出
@@ -234,10 +272,11 @@ export function Inference() {
                 <button
                   onClick={() => setViewMode('new')}
                   style={{
-                    padding: '3px 10px', borderRadius: 4, fontSize: 12, cursor: 'pointer',
+                    padding: '4px 12px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
                     border: `1px solid ${viewMode === 'new' ? '#e94560' : '#d0d0d0'}`,
                     background: viewMode === 'new' ? '#e94560' : '#fff',
                     color: viewMode === 'new' ? '#fff' : '#666',
+                    fontWeight: 500, transition: 'all 0.15s ease',
                   }}
                 >
                   仅新生成
@@ -245,8 +284,8 @@ export function Inference() {
               </div>
             </div>
             <pre style={{
-              whiteSpace: 'pre-wrap', wordBreak: 'break-word', background: '#fafafa',
-              padding: 16, borderRadius: 6, fontSize: 14, lineHeight: 1.6,
+              whiteSpace: 'pre-wrap', wordBreak: 'break-word', background: '#fafbfc',
+              padding: 16, borderRadius: 8, fontSize: 14, lineHeight: 1.6,
               maxHeight: 400, overflow: 'auto', margin: 0, border: '1px solid #f0f0f0',
             }}>
               {viewMode === 'full' ? result.generated_text : result.generated_text.slice(prompt.length).trim()}

+ 154 - 66
frontend/src/pages/Models.tsx

@@ -7,19 +7,46 @@ const ModelRow = memo(function ModelRow({ m, onTest, onDelete }: {
   onDelete: (id: string, name: string) => void
 }) {
   return (
-    <tr style={{ borderBottom: '1px solid #eee' }}>
-      <td style={{ padding: '8px 0', fontFamily: 'monospace', fontSize: 12 }}>{m.id}</td>
-      <td>{m.name}</td>
-      <td>{m.model_type}</td>
-      <td style={{ color: m.is_downloaded ? '#4caf50' : '#e94560' }}>
-        {m.is_downloaded ? '已缓存' : '未下载'}
+    <tr style={{
+      borderBottom: '1px solid #f0f0f0',
+      transition: 'background 0.15s ease',
+    }}
+    onMouseEnter={e => { e.currentTarget.style.background = '#fafbfc' }}
+    onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
+    >
+      <td style={{ padding: '12px 12px', fontFamily: 'monospace', fontSize: 12, color: '#666' }}>{m.id}</td>
+      <td style={{ padding: '12px 12px', fontWeight: 500, fontSize: 13 }}>{m.name}</td>
+      <td style={{ padding: '12px 12px', fontSize: 13, color: '#666' }}>{m.model_type}</td>
+      <td style={{ padding: '12px 12px' }}>
+        <span style={{
+          display: 'inline-block', padding: '3px 10px', borderRadius: 12, fontSize: 12, fontWeight: 500,
+          background: m.is_downloaded ? '#f0fff4' : '#fff5f7',
+          color: m.is_downloaded ? '#22863a' : '#e94560',
+          border: `1px solid ${m.is_downloaded ? '#d1f0d8' : '#ffdce0'}`,
+        }}>
+          {m.is_downloaded ? '已缓存' : '未下载'}
+        </span>
       </td>
-      <td>{m.supported_peft_methods.join(', ') || '-'}</td>
-      <td>
+      <td style={{ padding: '12px 12px', fontSize: 13, color: '#666' }}>{m.supported_peft_methods.join(', ') || '-'}</td>
+      <td style={{ padding: '12px 12px' }}>
         {m.is_downloaded && (
-          <button onClick={() => onTest(m.id)} style={{ marginRight: 8, padding: '2px 8px', color: '#2196f3', border: '1px solid #2196f3', borderRadius: 4, background: 'transparent', cursor: 'pointer' }}>测试</button>
+          <button onClick={() => onTest(m.id)} style={{
+            marginRight: 8, padding: '4px 12px', color: '#2196f3',
+            border: '1px solid #2196f3', borderRadius: 6, background: 'transparent',
+            cursor: 'pointer', fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
+          }}
+          onMouseEnter={e => { e.currentTarget.style.background = '#2196f3'; e.currentTarget.style.color = '#fff' }}
+          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#2196f3' }}
+          >测试</button>
         )}
-        <button onClick={() => onDelete(m.id, m.name)} style={{ padding: '2px 8px', color: '#e94560', border: '1px solid #e94560', borderRadius: 4, background: 'transparent', cursor: 'pointer' }}>删除</button>
+        <button onClick={() => onDelete(m.id, m.name)} style={{
+          padding: '4px 12px', color: '#e94560', border: '1px solid #e94560',
+          borderRadius: 6, background: 'transparent', cursor: 'pointer',
+          fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
+        }}
+        onMouseEnter={e => { e.currentTarget.style.background = '#e94560'; e.currentTarget.style.color = '#fff' }}
+        onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#e94560' }}
+        >删除</button>
       </td>
     </tr>
   )
@@ -102,74 +129,124 @@ export function Models() {
 
   return (
     <div>
-      <h1>模型注册</h1>
+      <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>模型注册</h1>
+      <p style={{ color: '#888', fontSize: 13, margin: '4px 0 16px' }}>下载和管理预训练模型</p>
 
       {/* Download form */}
-      <div style={{ marginTop: 16, display: 'flex', gap: 8, alignItems: 'center' }}>
-        <input
-          type="text"
-          placeholder="输入模型 ID (如 meta-llama/Llama-3.1-8B)"
-          value={modelId}
-          onChange={e => setModelId(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={useModelscope} onChange={e => setUseModelscope(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 style={{
+        marginTop: 16, background: '#fff', borderRadius: 10, padding: 20,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
+        <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 600 }}>下载模型</h2>
+        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
+          <input
+            type="text"
+            placeholder="输入模型 ID (如 meta-llama/Llama-3.1-8B)"
+            value={modelId}
+            onChange={e => setModelId(e.target.value)}
+            style={{
+              padding: '10px 14px', flex: 1, maxWidth: 400, borderRadius: 8,
+              border: '1px solid #d0d0d0', fontSize: 14, outline: 'none',
+              transition: 'border-color 0.2s',
+            }}
+            onFocus={e => { e.currentTarget.style.borderColor = '#e94560' }}
+            onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
+          />
+          <label style={{ fontSize: 13, color: '#666', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
+            <input type="checkbox" checked={useModelscope} onChange={e => setUseModelscope(e.target.checked)} />
+            {' '}ModelScope
+          </label>
+          <button
+            onClick={handleDownload}
+            disabled={downloading}
+            style={{
+              padding: '10px 20px', borderRadius: 8, border: 'none',
+              background: '#e94560', color: '#fff', cursor: 'pointer',
+              opacity: downloading ? 0.6 : 1, fontSize: 14, fontWeight: 600,
+              transition: 'all 0.2s ease',
+            }}
+          >
+            {downloading ? '下载中...' : '下载模型'}
+          </button>
+        </div>
+        {statusMsg && (
+          <p style={{
+            marginTop: 10, padding: '8px 12px', borderRadius: 6, fontSize: 13, margin: '10px 0 0',
+            background: statusMsg.includes('❌') ? '#fff2f0' : '#f0fff4',
+            color: statusMsg.includes('❌') ? '#cf1322' : '#22863a',
+            border: `1px solid ${statusMsg.includes('❌') ? '#ffccc7' : '#d1f0d8'}`,
+          }}>
+            {statusMsg}
+          </p>
+        )}
       </div>
 
-      {statusMsg && <p style={{ marginTop: 8, fontSize: 13, color: statusMsg.includes('❌') ? '#e94560' : '#4caf50' }}>{statusMsg}</p>}
-
       {/* Model list */}
       <div style={{ marginTop: 24 }}>
         <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
-          <h2 style={{ margin: 0 }}>已缓存模型</h2>
-          <button onClick={fetchModels} style={{ padding: '4px 12px', borderRadius: 4, border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}>
+          <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>已缓存模型</h2>
+          <button onClick={fetchModels} style={{
+            padding: '6px 14px', borderRadius: 6, border: '1px solid #d0d0d0',
+            background: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500,
+            transition: 'all 0.15s ease',
+          }}
+          onMouseEnter={e => { e.currentTarget.style.background = '#f5f5f5' }}
+          onMouseLeave={e => { e.currentTarget.style.background = '#fff' }}
+          >
             刷新
           </button>
         </div>
 
-        {loading && <p style={{ color: '#999' }}>加载中...</p>}
+        {loading && <p style={{ color: '#999', fontSize: 13 }}>加载中...</p>}
 
         {!loading && models.length === 0 && (
-          <p style={{ color: '#999', fontSize: 14 }}>暂无已缓存模型</p>
+          <div style={{
+            padding: 40, textAlign: 'center', color: '#999', fontSize: 14,
+            background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
+          }}>
+            <div style={{ fontSize: 32, marginBottom: 8 }}>🧠</div>
+            暂无已缓存模型,请在上方下载模型
+          </div>
         )}
 
         {!loading && models.length > 0 && (
-          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
-            <thead>
-              <tr style={{ borderBottom: '2px solid #eee', textAlign: 'left' }}>
-                <th style={{ padding: '8px 0' }}>ID</th>
-                <th>名称</th>
-                <th>类型</th>
-                <th>状态</th>
-                <th>PEFT 支持</th>
-                <th>操作</th>
-              </tr>
-            </thead>
-            <tbody>
-              {models.map(m => (
-                <ModelRow key={m.id} m={m} onTest={handleTest} onDelete={handleDelete} />
-              ))}
-            </tbody>
-          </table>
+          <div style={{
+            background: '#fff', borderRadius: 10, overflow: 'hidden',
+            boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+          }}>
+            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
+              <thead>
+                <tr style={{ background: '#fafbfc', borderBottom: '2px solid #f0f0f0', textAlign: 'left' }}>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>ID</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>名称</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>类型</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>状态</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>PEFT 支持</th>
+                  <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>操作</th>
+                </tr>
+              </thead>
+              <tbody>
+                {models.map(m => (
+                  <ModelRow key={m.id} m={m} onTest={handleTest} onDelete={handleDelete} />
+                ))}
+              </tbody>
+            </table>
+          </div>
         )}
       </div>
 
       {/* Test Panel */}
       {testModelId && (
-        <div style={{ marginTop: 24, background: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 3px rgba(0,0,0,0.1)' }}>
-          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
-            <h2 style={{ margin: 0, fontSize: 16 }}>模型测试 — {testModelId}</h2>
-            <button onClick={() => { setTestModelId(''); setTestResult(''); setTestError(''); setTestPrompt('') }} style={{ padding: '4px 12px', borderRadius: 4, border: '1px solid #ccc', background: '#fff', cursor: 'pointer' }}>关闭</button>
+        <div style={{
+          marginTop: 24, background: '#fff', borderRadius: 12, padding: 24,
+          boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid rgba(0,0,0,0.04)',
+        }}>
+          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
+            <h2 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>模型测试 — <code style={{ background: '#f5f5f5', padding: '2px 8px', borderRadius: 4, fontSize: 13 }}>{testModelId}</code></h2>
+            <button onClick={() => { setTestModelId(''); setTestResult(''); setTestError(''); setTestPrompt('') }} style={{
+              padding: '6px 14px', borderRadius: 6, border: '1px solid #d0d0d0',
+              background: '#fff', cursor: 'pointer', fontSize: 13,
+            }}>关闭</button>
           </div>
 
           {/* Chat-like input */}
@@ -179,12 +256,23 @@ export function Models() {
               onChange={e => setTestPrompt(e.target.value)}
               onKeyDown={e => { if (e.key === 'Enter') handleTestSubmit() }}
               placeholder="输入提示词,按 Enter 发送..."
-              style={{ flex: 1, padding: '10px 12px', borderRadius: 4, border: '1px solid #ccc', fontSize: 14 }}
+              style={{
+                flex: 1, padding: '10px 14px', borderRadius: 8,
+                border: '1px solid #d0d0d0', fontSize: 14, outline: 'none',
+                transition: 'border-color 0.2s',
+              }}
+              onFocus={e => { e.currentTarget.style.borderColor = '#2196f3' }}
+              onBlur={e => { e.currentTarget.style.borderColor = '#d0d0d0' }}
             />
             <button
               onClick={handleTestSubmit}
               disabled={testing}
-              style={{ padding: '10px 20px', borderRadius: 4, border: 'none', background: '#2196f3', color: '#fff', cursor: 'pointer', opacity: testing ? 0.6 : 1, whiteSpace: 'nowrap' }}
+              style={{
+                padding: '10px 24px', borderRadius: 8, border: 'none',
+                background: '#2196f3', color: '#fff', cursor: 'pointer',
+                opacity: testing ? 0.6 : 1, whiteSpace: 'nowrap', fontSize: 14, fontWeight: 600,
+                transition: 'all 0.2s ease',
+              }}
             >
               {testing ? '生成中...' : '发送'}
             </button>
@@ -192,24 +280,24 @@ export function Models() {
 
           {/* Error */}
           {testError && (
-            <div style={{ marginTop: 12, padding: 12, background: '#ffebee', borderRadius: 4, color: '#c62828', fontSize: 13 }}>
-              {testError}
+            <div style={{ marginTop: 16, padding: 12, background: '#fff2f0', borderRadius: 8, color: '#cf1322', fontSize: 13, border: '1px solid #ffccc7' }}>
+              {testError}
             </div>
           )}
 
           {/* Result */}
           {testResult && (
-            <div style={{ marginTop: 12 }}>
+            <div style={{ marginTop: 16 }}>
               <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
-                <span style={{ fontSize: 12, color: '#999', background: '#f5f5f5', padding: '2px 8px', borderRadius: 4 }}>Prompt</span>
+                <span style={{ fontSize: 12, color: '#666', fontWeight: 600 }}>Prompt</span>
               </div>
-              <div style={{ padding: 12, background: '#f0f7ff', borderRadius: 4, fontSize: 14, lineHeight: 1.6, marginBottom: 12 }}>
+              <div style={{ padding: 14, background: '#f0f7ff', borderRadius: 8, fontSize: 14, lineHeight: 1.6, marginBottom: 16, border: '1px solid #d6e8fa' }}>
                 {testPrompt}
               </div>
               <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
-                <span style={{ fontSize: 12, color: '#999', background: '#f5f5f5', padding: '2px 8px', borderRadius: 4 }}>Response</span>
+                <span style={{ fontSize: 12, color: '#666', fontWeight: 600 }}>Response</span>
               </div>
-              <div style={{ padding: 12, background: '#f0fff0', borderRadius: 4, fontSize: 14, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
+              <div style={{ padding: 14, background: '#f0fff4', borderRadius: 8, fontSize: 14, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', border: '1px solid #d1f0d8' }}>
                 {testResult}
               </div>
             </div>

+ 79 - 32
frontend/src/pages/Training.tsx

@@ -256,31 +256,50 @@ const statusLabel = (status: string) => {
 // --- 任务行(memo) ---
 const JobRow = memo(function JobRow({ j, onCancel }: { j: TrainingJob; onCancel: (id: string) => void }) {
   return (
-    <tr style={{ borderBottom: '1px solid #eee' }}>
-      <td style={{ padding: '8px 0', fontFamily: 'monospace', fontSize: 12 }}>{j.id.slice(0, 8)}...</td>
-      <td style={{ fontSize: 13 }}>{j.model_id}</td>
-      <td style={{ fontSize: 13, textTransform: 'uppercase' }}>{j.peft_method}</td>
-      <td>
+    <tr style={{
+      borderBottom: '1px solid #f0f0f0',
+      transition: 'background 0.15s ease',
+    }}
+    onMouseEnter={e => { e.currentTarget.style.background = '#fafbfc' }}
+    onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
+    >
+      <td style={{ padding: '12px 12px', fontFamily: 'monospace', fontSize: 12, color: '#666' }}>{j.id.slice(0, 8)}...</td>
+      <td style={{ padding: '12px 12px', fontSize: 13, fontWeight: 500 }}>{j.model_id}</td>
+      <td style={{ padding: '12px 12px', fontSize: 13, textTransform: 'uppercase', color: '#666' }}>{j.peft_method}</td>
+      <td style={{ padding: '12px 12px' }}>
         <span style={{
-          display: 'inline-block', padding: '2px 8px', borderRadius: 4, fontSize: 12,
-          background: statusColor(j.status) + '18', color: statusColor(j.status), fontWeight: 600,
+          display: 'inline-block', padding: '3px 10px', borderRadius: 12, fontSize: 12, fontWeight: 600,
+          background: statusColor(j.status) + '15', color: statusColor(j.status),
         }}>
           {statusLabel(j.status)}
         </span>
       </td>
-      <td>
-        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
-          <div style={{ width: 100, height: 6, background: '#eee', borderRadius: 3, overflow: 'hidden' }}>
-            <div style={{ width: `${j.progress}%`, height: '100%', background: statusColor(j.status), transition: 'width 0.3s' }} />
+      <td style={{ padding: '12px 12px' }}>
+        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
+          <div style={{
+            width: 120, height: 8, background: '#f0f0f0', borderRadius: 4, overflow: 'hidden',
+          }}>
+            <div style={{
+              width: `${j.progress}%`, height: '100%', borderRadius: 4,
+              background: `linear-gradient(90deg, ${statusColor(j.status)}, ${statusColor(j.status)}cc)`,
+              transition: 'width 0.3s ease',
+            }} />
           </div>
-          <span style={{ fontSize: 12, color: '#666', minWidth: 40 }}>{j.progress.toFixed(1)}%</span>
+          <span style={{ fontSize: 12, color: '#666', minWidth: 45, fontWeight: 500 }}>{j.progress.toFixed(1)}%</span>
         </div>
       </td>
-      <td style={{ fontSize: 13, fontFamily: 'monospace' }}>{j.loss?.toFixed(4) ?? '-'}</td>
-      <td style={{ fontSize: 12, color: '#666' }}>Epoch {j.current_epoch}</td>
-      <td>
+      <td style={{ padding: '12px 12px', fontSize: 13, fontFamily: 'monospace', fontWeight: 500 }}>{j.loss?.toFixed(4) ?? '-'}</td>
+      <td style={{ padding: '12px 12px', fontSize: 12, color: '#888' }}>Epoch {j.current_epoch}</td>
+      <td style={{ padding: '12px 12px' }}>
         {(j.status === 'training' || j.status === 'pending' || j.status === 'queued' || j.status === 'preprocessing') && (
-          <button onClick={() => onCancel(j.id)} style={{ padding: '2px 8px', color: '#e94560', border: '1px solid #e94560', borderRadius: 4, background: 'transparent', cursor: 'pointer', fontSize: 12 }}>取消</button>
+          <button onClick={() => onCancel(j.id)} style={{
+            padding: '4px 12px', color: '#e94560', border: '1px solid #e94560',
+            borderRadius: 6, background: 'transparent', cursor: 'pointer',
+            fontSize: 12, fontWeight: 500, transition: 'all 0.15s ease',
+          }}
+          onMouseEnter={e => { e.currentTarget.style.background = '#e94560'; e.currentTarget.style.color = '#fff' }}
+          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = '#e94560' }}
+          >取消</button>
         )}
       </td>
     </tr>
@@ -409,15 +428,21 @@ export function Training() {
   return (
     <div>
       <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }}>训练任务</h1>
-      <p style={{ color: '#666', fontSize: 13, margin: '4px 0 16px' }}>创建和管理模型微调任务</p>
+      <p style={{ color: '#888', fontSize: 13, margin: '4px 0 16px' }}>创建和管理模型微调任务</p>
 
       {/* Create form */}
-      <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 }}>创建训练任务</h2>
+      <div style={{
+        background: '#fff', borderRadius: 12, padding: 24,
+        boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+      }}>
+        <h2 style={{ margin: '0 0 20px', fontSize: 15, fontWeight: 600 }}>创建训练任务</h2>
 
         {/* 核心配置 */}
-        <div style={{ fontSize: 13, fontWeight: 600, color: '#e94560', marginBottom: 8, paddingBottom: 4, borderBottom: '1px solid #f0f0f0' }}>核心配置</div>
-        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 16 }}>
+        <div style={{
+          fontSize: 13, fontWeight: 600, color: '#e94560', marginBottom: 12,
+          paddingBottom: 6, borderBottom: '2px solid #fff5f7',
+        }}>核心配置</div>
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginBottom: 20 }}>
           <div>
             <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>基础模型</label>
             <SearchableSelect options={modelOptions} value={modelId} onChange={setModelId} placeholder="选择已下载的模型" loading={loadingOptions} />
@@ -445,8 +470,11 @@ export function Training() {
         </div>
 
         {/* 训练超参 */}
-        <div style={{ fontSize: 13, fontWeight: 600, color: '#e94560', marginBottom: 8, paddingBottom: 4, borderBottom: '1px solid #f0f0f0' }}>训练超参数</div>
-        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 16 }}>
+        <div style={{
+          fontSize: 13, fontWeight: 600, color: '#e94560', marginBottom: 12,
+          paddingBottom: 6, borderBottom: '2px solid #fff5f7',
+        }}>训练超参数</div>
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginBottom: 20 }}>
           <div>
             <label style={{ display: 'block', fontSize: 12, color: '#666', marginBottom: 4 }}>训练轮数 (Epochs)</label>
             <Select options={EPOCH_PRESETS} value={String(epochs)} onChange={v => setEpochs(Number(v))} />
@@ -474,8 +502,11 @@ export function Training() {
         </div>
 
         {/* 高级选项 */}
-        <div style={{ fontSize: 13, fontWeight: 600, color: '#e94560', marginBottom: 8, paddingBottom: 4, borderBottom: '1px solid #f0f0f0' }}>高级选项</div>
-        <div style={{ display: 'flex', gap: 16, alignItems: 'center', marginBottom: 16 }}>
+        <div style={{
+          fontSize: 13, fontWeight: 600, color: '#e94560', marginBottom: 12,
+          paddingBottom: 6, borderBottom: '2px solid #fff5f7',
+        }}>高级选项</div>
+        <div style={{ display: 'flex', gap: 16, alignItems: 'center', marginBottom: 20 }}>
           <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, cursor: 'pointer' }}>
             <input type="checkbox" checked={deepspeed} onChange={e => setDeepspeed(e.target.checked)} />
             DeepSpeed ZeRO-2 (多 GPU)
@@ -484,7 +515,10 @@ export function Training() {
 
         {/* 错误提示 */}
         {createError && (
-          <div style={{ marginBottom: 12, padding: 10, background: '#fff2f0', borderRadius: 4, fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7' }}>
+          <div style={{
+            marginBottom: 16, padding: 12, background: '#fff2f0', borderRadius: 8,
+            fontSize: 13, color: '#cf1322', border: '1px solid #ffccc7',
+          }}>
             {createError}
           </div>
         )}
@@ -493,10 +527,10 @@ export function Training() {
           onClick={handleCreate}
           disabled={submitting || !modelId || !datasetId}
           style={{
-            marginTop: 8, padding: '10px 32px', borderRadius: 6, border: 'none',
+            padding: '12px 36px', borderRadius: 8, border: 'none',
             background: '#e94560', color: '#fff', cursor: 'pointer',
             opacity: (submitting || !modelId || !datasetId) ? 0.5 : 1,
-            fontSize: 14, fontWeight: 600,
+            fontSize: 14, fontWeight: 600, transition: 'all 0.2s ease',
           }}
         >
           {submitting ? '创建中...' : '启动训练'}
@@ -507,7 +541,13 @@ export function Training() {
       <div style={{ marginTop: 24 }}>
         <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
           <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>任务列表</h2>
-          <button onClick={fetchJobs} style={{ padding: '4px 12px', borderRadius: 4, border: '1px solid #d0d0d0', background: '#fff', cursor: 'pointer', fontSize: 12 }}>
+          <button onClick={fetchJobs} style={{
+            padding: '6px 14px', borderRadius: 6, border: '1px solid #d0d0d0',
+            background: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500,
+          }}
+          onMouseEnter={e => { e.currentTarget.style.background = '#f5f5f5' }}
+          onMouseLeave={e => { e.currentTarget.style.background = '#fff' }}
+          >
             刷新
           </button>
         </div>
@@ -515,16 +555,23 @@ export function Training() {
         {loading && <p style={{ color: '#999', fontSize: 13 }}>加载中...</p>}
 
         {!loading && jobs.length === 0 && (
-          <div style={{ padding: 40, textAlign: 'center', color: '#999', fontSize: 14, background: '#fff', borderRadius: 8 }}>
+          <div style={{
+            padding: 40, textAlign: 'center', color: '#999', fontSize: 14,
+            background: '#fff', borderRadius: 10, boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
+          }}>
+            <div style={{ fontSize: 32, marginBottom: 8 }}>⚡</div>
             暂无训练任务,请先创建训练任务
           </div>
         )}
 
         {!loading && jobs.length > 0 && (
-          <div style={{ background: '#fff', borderRadius: 8, overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}>
+          <div style={{
+            background: '#fff', borderRadius: 10, overflow: 'hidden',
+            boxShadow: '0 1px 3px rgba(0,0,0,0.06)', border: '1px solid rgba(0,0,0,0.04)',
+          }}>
             <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
               <thead>
-                <tr style={{ background: '#fafafa', borderBottom: '2px solid #eee', textAlign: 'left' }}>
+                <tr style={{ background: '#fafbfc', borderBottom: '2px solid #f0f0f0', textAlign: 'left' }}>
                   <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>任务 ID</th>
                   <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>模型</th>
                   <th style={{ padding: '10px 12px', fontSize: 12, color: '#666', fontWeight: 600 }}>PEFT</th>