feat(ui): Setup tab � health check + bootstrap with live log
This commit is contained in:
189
frontend/src/components/SetupPanel.jsx
Normal file
189
frontend/src/components/SetupPanel.jsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import axios from 'axios'
|
||||
import { CheckCircle2, XCircle, Loader2, Play, RefreshCw, Server, Cpu, Box, Folder, Wrench, Sparkles } from 'lucide-react'
|
||||
|
||||
const CHECK_LABELS = {
|
||||
conda: { label: 'Miniconda installed', icon: Box },
|
||||
env: { label: 'Conda env "synthetic-data"', icon: Box },
|
||||
sdk: { label: 'synthetic-data-kit CLI', icon: Wrench },
|
||||
training_deps: { label: 'Training libs (torch/peft/trl)', icon: Sparkles },
|
||||
train_py: { label: 'train.py installed', icon: Wrench },
|
||||
data_dirs: { label: 'Data directories created', icon: Folder },
|
||||
gpu: { label: 'NVIDIA GPU detected', icon: Cpu },
|
||||
}
|
||||
|
||||
const CHECK_ORDER = ['gpu', 'conda', 'env', 'sdk', 'training_deps', 'train_py', 'data_dirs']
|
||||
|
||||
export default function SetupPanel({ connected }) {
|
||||
const [checks, setChecks] = useState(null)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [user, setUser] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [running, setRunning] = useState(false)
|
||||
const [logs, setLogs] = useState([])
|
||||
const [stage, setStage] = useState('')
|
||||
const logRef = useRef(null)
|
||||
|
||||
const runCheck = useCallback(async () => {
|
||||
if (!connected) return
|
||||
setLoading(true); setError('')
|
||||
try {
|
||||
const { data } = await axios.get('/api/setup/check')
|
||||
setChecks(data.checks); setReady(data.ready); setUser(data.user)
|
||||
} catch (e) {
|
||||
setError(e.response?.data?.detail || e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [connected])
|
||||
|
||||
useEffect(() => { runCheck() }, [runCheck])
|
||||
|
||||
useEffect(() => {
|
||||
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight
|
||||
}, [logs])
|
||||
|
||||
const runBootstrap = () => {
|
||||
if (!connected || running) return
|
||||
setRunning(true); setLogs([]); setStage('starting'); setError('')
|
||||
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const ws = new WebSocket(`${proto}//${location.host}/api/setup/bootstrap`)
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data)
|
||||
if (msg.type === 'log') {
|
||||
const line = msg.data
|
||||
const stageMatch = line.match(/::stage:: (\w+)/)
|
||||
if (stageMatch) setStage(stageMatch[1])
|
||||
setLogs(l => [...l, line])
|
||||
} else if (msg.type === 'error') {
|
||||
setError(msg.data)
|
||||
setRunning(false)
|
||||
ws.close()
|
||||
} else if (msg.type === 'done') {
|
||||
setStage('done')
|
||||
setRunning(false)
|
||||
ws.close()
|
||||
runCheck()
|
||||
}
|
||||
}
|
||||
ws.onerror = () => { setError('WebSocket error'); setRunning(false) }
|
||||
ws.onclose = () => setRunning(false)
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700/50 bg-[#161b27] p-6 text-center">
|
||||
<Server size={32} className="mx-auto mb-3 text-slate-600" />
|
||||
<p className="text-sm text-slate-400">Connect to the GPU server first to run setup.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-slate-200">Remote setup</h2>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
Verify and install everything llm-trainer needs on the GPU host
|
||||
{user && <> — connected as <span className="text-slate-400">{user}</span></>}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={runCheck}
|
||||
disabled={loading || running}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-slate-700 hover:bg-slate-700/30 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} /> Re-check
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Health check grid */}
|
||||
<div className="rounded-xl border border-slate-700/50 bg-[#161b27] overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-slate-700/50 flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400">Health check</span>
|
||||
{ready && (
|
||||
<span className="text-xs text-green-400 flex items-center gap-1">
|
||||
<CheckCircle2 size={12}/> Ready to train
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="divide-y divide-slate-700/20">
|
||||
{checks
|
||||
? CHECK_ORDER.map(key => {
|
||||
const meta = CHECK_LABELS[key]
|
||||
const ok = checks[key]
|
||||
const Icon = meta.icon
|
||||
return (
|
||||
<div key={key} className="flex items-center gap-3 px-4 py-2.5 text-xs">
|
||||
<Icon size={14} className="text-slate-500" />
|
||||
<span className="flex-1 text-slate-300">{meta.label}</span>
|
||||
{ok
|
||||
? <span className="flex items-center gap-1 text-green-400"><CheckCircle2 size={13}/> ok</span>
|
||||
: <span className="flex items-center gap-1 text-red-400"><XCircle size={13}/> missing</span>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
: <div className="px-4 py-6 text-xs text-slate-500 text-center">
|
||||
{loading ? 'Probing remote…' : 'No data'}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bootstrap action */}
|
||||
{checks && !ready && (
|
||||
<div className="rounded-xl border border-blue-700/30 bg-blue-900/10 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-medium text-slate-200 mb-1">Bootstrap remote</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
Installs miniconda, creates the conda env, installs synthetic-data-kit + torch + transformers + peft + trl,
|
||||
creates the data directories, and drops the training script into place. This can take 5–10 minutes
|
||||
on first run depending on network speed.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={runBootstrap}
|
||||
disabled={running}
|
||||
className="flex items-center gap-2 px-4 py-2 text-xs font-medium rounded-lg bg-blue-600 hover:bg-blue-500 text-white disabled:opacity-50"
|
||||
>
|
||||
{running
|
||||
? <><Loader2 size={13} className="animate-spin"/> Running ({stage})</>
|
||||
: <><Play size={13}/> Bootstrap</>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="rounded-xl border border-red-700/40 bg-red-900/10 p-3 text-xs text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Live log */}
|
||||
{logs.length > 0 && (
|
||||
<div className="rounded-xl border border-slate-700/50 bg-[#0b0f17] overflow-hidden">
|
||||
<div className="px-4 py-2 border-b border-slate-700/50 flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400">Bootstrap log</span>
|
||||
{running && <Loader2 size={12} className="animate-spin text-blue-400" />}
|
||||
</div>
|
||||
<pre
|
||||
ref={logRef}
|
||||
className="max-h-[420px] overflow-auto p-3 text-xs leading-relaxed text-slate-300 font-mono whitespace-pre-wrap"
|
||||
>
|
||||
{logs.join('')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user