Files
llm-trainer/frontend/src/App.jsx
tocmo0nlord 90a6ee6fbf 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>
2026-03-21 17:13:32 -04:00

142 lines
5.4 KiB
JavaScript

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>
)
}