From 4bf98c4d2b46b59f32137f431358edddf692ca13 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sun, 26 Apr 2026 01:50:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20Setup=20tab=20=EF=BF=BD=20health=20?= =?UTF-8?q?check=20+=20bootstrap=20with=20live=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/SetupPanel.jsx | 189 +++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 frontend/src/components/SetupPanel.jsx diff --git a/frontend/src/components/SetupPanel.jsx b/frontend/src/components/SetupPanel.jsx new file mode 100644 index 0000000..01f30a7 --- /dev/null +++ b/frontend/src/components/SetupPanel.jsx @@ -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 ( +
+ +

Connect to the GPU server first to run setup.

+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

Remote setup

+

+ Verify and install everything llm-trainer needs on the GPU host + {user && <> — connected as {user}} +

+
+ +
+ + {/* Health check grid */} +
+
+ Health check + {ready && ( + + Ready to train + + )} +
+
+ {checks + ? CHECK_ORDER.map(key => { + const meta = CHECK_LABELS[key] + const ok = checks[key] + const Icon = meta.icon + return ( +
+ + {meta.label} + {ok + ? ok + : missing + } +
+ ) + }) + :
+ {loading ? 'Probing remote…' : 'No data'} +
+ } +
+
+ + {/* Bootstrap action */} + {checks && !ready && ( +
+
+
+

Bootstrap remote

+

+ 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. +

+
+ +
+
+ )} + + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Live log */} + {logs.length > 0 && ( +
+
+ Bootstrap log + {running && } +
+
+            {logs.join('')}
+          
+
+ )} +
+ ) +} \ No newline at end of file