import React, { useEffect, useRef, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { useApi } from "@/contexts/ApiContext"; import { AlertTriangle, CheckCircle2, Copy, Loader2, XCircle, } from "lucide-react"; type InstallState = "idle" | "installing" | "done" | "error"; interface LogEntry { timestamp: number; message: string; } interface InstallStatus { state: InstallState; error: string | null; logs: LogEntry[]; } interface Props { installHint: string; } const POLL_INTERVAL_MS = 1500; const TrainingExtraGate: React.FC = ({ installHint }) => { const { baseUrl, fetchWithHeaders } = useApi(); const { toast } = useToast(); const [state, setState] = useState("idle"); const [error, setError] = useState(null); const [logs, setLogs] = useState([]); const logBoxRef = useRef(null); // Seed local state from the backend on mount so refresh-mid-install picks // up where we left off (or shows Done/Error if the install already finished). useEffect(() => { let cancelled = false; fetchWithHeaders(`${baseUrl}/system/training-extra/install-status`) .then((r) => r.json()) .then((status: InstallStatus) => { if (cancelled) return; setState(status.state); setError(status.error); if (status.logs.length > 0) { setLogs(status.logs); } }) .catch(() => { // Backend unreachable — stay in idle; the user can still try. }); return () => { cancelled = true; }; }, [baseUrl, fetchWithHeaders]); // Poll while installing. useEffect(() => { if (state !== "installing") return; const id = setInterval(async () => { try { const r = await fetchWithHeaders(`${baseUrl}/system/training-extra/install-status`); if (!r.ok) return; const status: InstallStatus = await r.json(); if (status.logs && status.logs.length > 0) { setLogs((prev) => [...prev, ...status.logs]); } if (status.state !== "installing") { setState(status.state); setError(status.error); } } catch { // Transient errors are fine; we'll retry on next tick. } }, POLL_INTERVAL_MS); return () => clearInterval(id); }, [state, baseUrl, fetchWithHeaders]); // Auto-scroll the log panel as new lines arrive. useEffect(() => { if (logBoxRef.current) { logBoxRef.current.scrollTop = logBoxRef.current.scrollHeight; } }, [logs]); const handleInstall = async () => { // Optimistically transition so the polling effect kicks in even if the // backend response is slow. setState("installing"); setError(null); setLogs([]); try { const r = await fetchWithHeaders(`${baseUrl}/system/training-extra/install`, { method: "POST", }); const body: { started: boolean; message: string } = await r.json(); if (!body.started && r.ok) { // Backend says "already installing" — that's fine; polling already running. return; } if (!r.ok) { setState("error"); setError(body.message || `Install request failed (${r.status})`); } } catch (e) { setState("error"); setError(`Install request failed: ${e instanceof Error ? e.message : String(e)}`); } }; const handleRetry = () => { setState("idle"); setError(null); setLogs([]); }; const handleCopy = async () => { try { await navigator.clipboard.writeText(installHint); toast({ title: "Copied", description: installHint }); } catch { toast({ title: "Copy failed", description: "Select the command and copy manually.", variant: "destructive", }); } }; return (
{state === "done" ? ( ) : state === "error" ? ( ) : state === "installing" ? ( ) : ( )} {state === "done" ? "Install Complete" : state === "error" ? "Install Failed" : state === "installing" ? "Installing…" : "Training Extra Not Installed"} {state === "idle" && ( <>

Training requires the accelerate package, which isn't installed in this environment. Install it to enable the Training page.

{installHint}
)} {state === "installing" && (

Installing accelerate. This usually takes about 10 seconds.

)} {state === "done" && (

Install complete. Restart lelab to enable training:

  1. Press Ctrl+C in the terminal running lelab.
  2. Run lelab again.
)} {state === "error" && ( <>

{error || "Install failed."}

)} {state === "error" && logs.length > 0 && (
{logs.map((log, idx) => (
{log.message}
))}
)}
); }; export default TrainingExtraGate;