Initial scaffold: LLM Trainer Dashboard
Full-stack app with FastAPI backend (SSH/paramiko, pipeline streaming, GPU stats, xterm.js terminal, Ollama model manager) and React + Tailwind frontend (8 panels: Connection, Documents, Pipeline, QA Pairs, Training, Terminal, Models, Config). Docker Compose included. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
141
frontend/src/App.jsx
Normal file
141
frontend/src/App.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Wifi, WifiOff, Server, FileText, GitBranch,
|
||||
Table2, Activity, TerminalSquare, Box, Settings, RefreshCw,
|
||||
} from 'lucide-react'
|
||||
|
||||
import ConnectionPanel from './components/ConnectionPanel'
|
||||
import DocumentManager from './components/DocumentManager'
|
||||
import PipelineRunner from './components/PipelineRunner'
|
||||
import QAPairViewer from './components/QAPairViewer'
|
||||
import TrainingMonitor from './components/TrainingMonitor'
|
||||
import Terminal from './components/Terminal'
|
||||
import ModelManager from './components/ModelManager'
|
||||
import ConfigEditor from './components/ConfigEditor'
|
||||
|
||||
const API = '' // vite proxy forwards /api → :8080
|
||||
|
||||
const NAV = [
|
||||
{ id: 'connection', label: 'Connection', icon: Server },
|
||||
{ id: 'documents', label: 'Documents', icon: FileText },
|
||||
{ id: 'pipeline', label: 'Pipeline', icon: GitBranch },
|
||||
{ id: 'pairs', label: 'QA Pairs', icon: Table2 },
|
||||
{ id: 'training', label: 'Training', icon: Activity },
|
||||
{ id: 'terminal', label: 'Terminal', icon: TerminalSquare },
|
||||
{ id: 'models', label: 'Models', icon: Box },
|
||||
{ id: 'config', label: 'Config', icon: Settings },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
const [active, setActive] = useState('connection')
|
||||
const [connected, setConnected] = useState(false)
|
||||
const [gpuInfo, setGpuInfo] = useState(null)
|
||||
const [statusMsg, setStatusMsg] = useState('')
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
const { data } = await axios.get(`${API}/api/status`)
|
||||
setConnected(data.connected)
|
||||
if (data.gpu?.gpus?.length) setGpuInfo(data.gpu.gpus[0])
|
||||
} catch {
|
||||
setConnected(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus()
|
||||
const id = setInterval(fetchStatus, 10000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchStatus])
|
||||
|
||||
const panels = {
|
||||
connection: <ConnectionPanel onConnect={fetchStatus} setStatus={setStatusMsg} />,
|
||||
documents: <DocumentManager connected={connected} />,
|
||||
pipeline: <PipelineRunner connected={connected} />,
|
||||
pairs: <QAPairViewer connected={connected} />,
|
||||
training: <TrainingMonitor connected={connected} gpuInfo={gpuInfo} />,
|
||||
terminal: <Terminal connected={connected} />,
|
||||
models: <ModelManager connected={connected} />,
|
||||
config: <ConfigEditor connected={connected} />,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-[#0f1117] text-slate-200">
|
||||
|
||||
{/* ── Sidebar ── */}
|
||||
<aside className="w-52 flex-shrink-0 flex flex-col bg-[#161b27] border-r border-slate-700/50">
|
||||
{/* Logo */}
|
||||
<div className="px-4 py-5 border-b border-slate-700/50">
|
||||
<h1 className="text-sm font-semibold tracking-widest text-blue-400 uppercase">
|
||||
LLM Trainer
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Dashboard v1.0</p>
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
<nav className="flex-1 py-3 overflow-y-auto">
|
||||
{NAV.map(({ id, label, icon: Icon }) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setActive(id)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors
|
||||
${active === id
|
||||
? 'bg-blue-600/20 text-blue-400 border-r-2 border-blue-500'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-700/30'}`}
|
||||
>
|
||||
<Icon size={16} />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Status bar */}
|
||||
<div className="px-4 py-3 border-t border-slate-700/50 space-y-2">
|
||||
<div className={`flex items-center gap-2 text-xs font-medium
|
||||
${connected ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{connected
|
||||
? <><Wifi size={12}/> Connected</>
|
||||
: <><WifiOff size={12}/> Disconnected</>
|
||||
}
|
||||
</div>
|
||||
{gpuInfo && connected && (
|
||||
<div className="text-xs text-slate-500 space-y-0.5">
|
||||
<div className="truncate">{gpuInfo.name}</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="text-purple-400">{gpuInfo.utilization}%</span>
|
||||
<span className="text-blue-400">
|
||||
{gpuInfo.memory_used}/{gpuInfo.memory_total}MB
|
||||
</span>
|
||||
<span className="text-orange-400">{gpuInfo.temperature}°C</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={fetchStatus}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300 transition-colors"
|
||||
>
|
||||
<RefreshCw size={11}/> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ── Main content ── */}
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Top bar */}
|
||||
<header className="flex items-center justify-between px-6 py-3 border-b border-slate-700/50 bg-[#161b27]">
|
||||
<h2 className="text-sm font-semibold text-slate-300 capitalize">
|
||||
{NAV.find(n => n.id === active)?.label}
|
||||
</h2>
|
||||
{statusMsg && (
|
||||
<span className="text-xs text-slate-500 italic">{statusMsg}</span>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{panels[active]}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user