From 815619d972dd3472b94363dd8a17f6e95df4c54d Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:05:05 +0100 Subject: [PATCH] feat: add update instructions card with OS toggle and mobile expand flow (#4721) Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com> --- studio/frontend/src/components/navbar.tsx | 258 +++++++++++++++++++++- 1 file changed, 253 insertions(+), 5 deletions(-) diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 99909b80fa..63189a0d5d 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -6,6 +6,11 @@ import { HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler"; import { Sheet, @@ -16,20 +21,25 @@ import { } from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import { + ArrowReloadHorizontalIcon, ArrowRight01Icon, Cancel01Icon, Book03Icon, BubbleChatIcon, ChefHatIcon, + Copy01Icon, CursorInfo02Icon, PackageIcon, + Tick02Icon, ZapIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; +import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { useTrainingRuntimeStore } from "@/features/training"; import { usePlatformStore } from "@/config/env"; import { Link, useRouterState } from "@tanstack/react-router"; -import { motion } from "motion/react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import type { ReactElement } from "react"; import { useEffect, useRef, useState } from "react"; import { TOUR_OPEN_EVENT } from "@/features/tour"; import { ShutdownDialog } from "@/components/shutdown-dialog"; @@ -41,6 +51,185 @@ const NAV_ITEMS = [ { label: "Chat", href: "/chat", icon: BubbleChatIcon, enabled: true }, ]; +const STUDIO_UPDATE_CMD = "unsloth studio update"; +const STUDIO_UPDATE_FALLBACK_UNIX_CMD = + "curl -fsSL https://unsloth.ai/install.sh | sh"; +const STUDIO_UPDATE_FALLBACK_WINDOWS_CMD = + "irm https://unsloth.ai/install.ps1 | iex"; + +type UpdateShell = "windows" | "unix"; + +function getDefaultUpdateShell(deviceType: string): UpdateShell { + return deviceType === "windows" ? "windows" : "unix"; +} + +function getStudioUpdateInstructionLine(shell: UpdateShell): string { + return shell === "windows" ? "Open PowerShell and run:" : "Open Terminal and run:"; +} + +function CopyableCommand({ + command, + copyLabel, +}: { + command: string; + copyLabel: string; +}): ReactElement { + const [copied, setCopied] = useState(false); + const timerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, []); + + const handleCopy = () => { + if (!copyToClipboard(command)) { + return; + } + setCopied(true); + if (timerRef.current) { + clearTimeout(timerRef.current); + } + timerRef.current = setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ + +
+ ); +} + +function UpdateStudioInstructions({ + className, + defaultShell, + showTitle = true, +}: { + className?: string; + defaultShell: UpdateShell; + showTitle?: boolean; +}): ReactElement { + const [shell, setShell] = useState(defaultShell); + const prefersReducedMotion = useReducedMotion(); + const windows = shell === "windows"; + const fadeTransition = prefersReducedMotion + ? { duration: 0 } + : { duration: 0.16, ease: [0.165, 0.84, 0.44, 1] as const }; + const fadeInitial = prefersReducedMotion ? { opacity: 1 } : { opacity: 0, y: 2 }; + const fadeAnimate = { opacity: 1, y: 0 }; + const fadeExit = prefersReducedMotion ? { opacity: 1 } : { opacity: 0, y: -2 }; + + useEffect(() => { + setShell(defaultShell); + }, [defaultShell]); + + return ( +
+
+ {showTitle ? ( +

+ Update Unsloth Studio +

+ ) : null} +
+ + / + +
+
+ + + {getStudioUpdateInstructionLine(shell)} + + + +

+ If that fails or unsloth studio update is unavailable, run: +

+ + + + + +

+ Restart Studio after updating for changes to take effect. +

+
+ ); +} + function getTourId(pathname: string): "studio" | "chat" | "export" | null { if (pathname === "/studio") return "studio"; if (pathname === "/chat") return "chat"; @@ -52,9 +241,12 @@ export function Navbar() { const pathname = useRouterState({ select: (s) => s.location.pathname }); const isTrainingRunning = useTrainingRuntimeStore((s) => s.isTrainingRunning); const [mobileOpen, setMobileOpen] = useState(false); + const [mobileUpdateOpen, setMobileUpdateOpen] = useState(false); const [shutdownOpen, setShutdownOpen] = useState(false); + const deviceType = usePlatformStore((s) => s.deviceType); const chatOnly = usePlatformStore((s) => s.isChatOnly()); + const defaultUpdateShell = getDefaultUpdateShell(deviceType); // Warn before closing the tab only when training is running (data loss risk). // We store the handler in a ref so removeUnloadHandler() can clean it up @@ -236,6 +428,25 @@ export function Navbar() { Tour + + + + + + + + + ) : null} - + { + setMobileOpen(open); + if (!open) setMobileUpdateOpen(false); + }} + > ) : null} + + + + + + + +