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