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>
142 lines
5.4 KiB
JavaScript
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>
|
|
)
|
|
}
|