diff --git a/studio/frontend/src/app/router.tsx b/studio/frontend/src/app/router.tsx index dbb74ee1a1..5c18e637e2 100644 --- a/studio/frontend/src/app/router.tsx +++ b/studio/frontend/src/app/router.tsx @@ -3,6 +3,7 @@ import { Link, createRouter, useRouterState } from "@tanstack/react-router"; import { Button } from "@/components/ui/button"; +import { MascotImg } from "@/components/mascot-img"; import { useT } from "@/i18n"; import { Route as rootRoute } from "./routes/__root"; import { Route as dataRecipesRoute } from "./routes/data-recipes"; @@ -41,11 +42,7 @@ function DefaultNotFound() { return (
- Sloth mascot +

{t("shell.notFound.title")} diff --git a/studio/frontend/src/asset-queries.d.ts b/studio/frontend/src/asset-queries.d.ts new file mode 100644 index 0000000000..2c7a1cd936 --- /dev/null +++ b/studio/frontend/src/asset-queries.d.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +// Vite `?inline` imports (data URIs); vite/client only types bare extensions. +declare module "*?inline" { + const src: string; + // biome-ignore lint/style/noDefaultExport: Vite asset modules export default. + export default src; +} diff --git a/studio/frontend/src/assets/mascot-fallback.webp b/studio/frontend/src/assets/mascot-fallback.webp new file mode 100644 index 0000000000..ad50361dbb Binary files /dev/null and b/studio/frontend/src/assets/mascot-fallback.webp differ diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index ea660f7af8..3763370dcd 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -36,6 +36,7 @@ import { useScrollThreadToBottom, } from "@/components/assistant-ui/use-intent-aware-autoscroll"; import { Button } from "@/components/ui/button"; +import { MascotImg } from "@/components/mascot-img"; import { Spinner } from "@/components/ui/spinner"; import { DropdownMenu, @@ -781,7 +782,7 @@ const ThreadWelcome: FC<{ setWelcome(buildWelcome(new Date().getHours(), name)); }, [displayName]); - const currentEmojiSrc = `/Sloth emojis/${welcome.sloth}`; + const currentEmojiSrc = `Sloth emojis/${welcome.sloth}`; return (
@@ -789,9 +790,8 @@ const ThreadWelcome: FC<{
{/* Center the greeting (sloth + title) over the composer. */}
- Sloth mascot

diff --git a/studio/frontend/src/components/mascot-img.tsx b/studio/frontend/src/components/mascot-img.tsx new file mode 100644 index 0000000000..cff4d54c4d --- /dev/null +++ b/studio/frontend/src/components/mascot-img.tsx @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { type ComponentProps, useState } from "react"; + +// Bundled as a data URI so the fallback ships inside the JS bundle and can +// never 404, unlike the public-folder originals. +import fallbackMascot from "@/assets/mascot-fallback.webp?inline"; + +const LEADING_SLASH = /^\//; + +// Resolve a public-folder asset against the deploy base (subpath mounts) and +// encode the spaced sloth filenames. +export function publicAssetUrl(path: string): string { + return encodeURI(import.meta.env.BASE_URL + path.replace(LEADING_SLASH, "")); +} + +type MascotImgProps = { src: string } & Omit< + ComponentProps<"img">, + "src" | "onError" +>; + +// Keying on src remounts the inner component, so the retry state resets +// whenever the source changes (greeting sloths rotate). +export function MascotImg(props: MascotImgProps) { + return ; +} + +type Stage = "primary" | "retry" | "fallback"; + +// Decorative mascot that degrades gracefully: a failed load retries once with +// a cache-buster (transient blips, stale caches), then swaps to the bundled +// fallback sloth. Empty alt by default so a broken image never paints text. +function MascotImgInner({ src, alt = "", ...rest }: MascotImgProps) { + const [stage, setStage] = useState("primary"); + + const url = publicAssetUrl(src); + const effectiveSrc = + stage === "primary" + ? url + : stage === "retry" + ? `${url}${url.includes("?") ? "&" : "?"}retry=1` + : fallbackMascot; + + return ( + {alt} setStage((s) => (s === "primary" ? "retry" : "fallback"))} + /> + ); +} diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index f2017ff0b6..d109b5f196 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -3,6 +3,7 @@ import { apiUrl } from "@/lib/api-base"; import { Button } from "@/components/ui/button"; +import { MascotImg } from "@/components/mascot-img"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Link, useNavigate } from "@tanstack/react-router"; @@ -311,9 +312,8 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { return (
- Unsloth waving mascot

{title}

diff --git a/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx b/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx index a685515253..22066b6fc3 100644 --- a/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx +++ b/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx @@ -9,6 +9,7 @@ import { unslothDarkTheme, unslothLightTheme, } from "@/components/assistant-ui/code-themes"; +import { MascotImg } from "@/components/mascot-img"; import { Button } from "@/components/ui/button"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { cn } from "@/lib/utils"; @@ -78,9 +79,8 @@ function ArtifactGeneratingPanel() { return (
- diff --git a/studio/frontend/src/features/onboarding/components/splash-screen.tsx b/studio/frontend/src/features/onboarding/components/splash-screen.tsx index 70438a04d8..47d9c25efe 100644 --- a/studio/frontend/src/features/onboarding/components/splash-screen.tsx +++ b/studio/frontend/src/features/onboarding/components/splash-screen.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { MascotImg } from "@/components/mascot-img"; import { motion } from "motion/react"; interface SplashScreenProps { @@ -19,10 +20,7 @@ export function SplashScreen({ {/* Mascot */}
- + > + +
{/* Brand text */} diff --git a/studio/frontend/src/features/onboarding/components/wizard-content.tsx b/studio/frontend/src/features/onboarding/components/wizard-content.tsx index 6906b87f8f..40d290a73e 100644 --- a/studio/frontend/src/features/onboarding/components/wizard-content.tsx +++ b/studio/frontend/src/features/onboarding/components/wizard-content.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { MascotImg } from "@/components/mascot-img"; import { STEPS } from "@/config/training"; import { useTrainingConfigStore } from "@/features/training"; import type { StepNumber } from "@/types/training"; @@ -19,11 +20,11 @@ const STEP_COMPONENTS = { } as const; const STEP_MASCOTS: Record = { - 1: `${import.meta.env.BASE_URL}Sloth emojis/large sloth wave.png`, - 2: `${import.meta.env.BASE_URL}Sloth emojis/sloth magnify final.png`, - 3: `${import.meta.env.BASE_URL}Sloth emojis/sloth huglove large.png`, - 4: `${import.meta.env.BASE_URL}Sloth emojis/large sloth glasses.png`, - 5: `${import.meta.env.BASE_URL}Sloth emojis/large sloth yay.png`, + 1: "Sloth emojis/large sloth wave.png", + 2: "Sloth emojis/sloth magnify final.png", + 3: "Sloth emojis/sloth huglove large.png", + 4: "Sloth emojis/large sloth glasses.png", + 5: "Sloth emojis/large sloth yay.png", }; export function WizardContent() { @@ -35,7 +36,7 @@ export function WizardContent() { return (
- Unsloth mascot +

{stepConfig.title}

diff --git a/studio/frontend/src/features/studio/training-start-overlay.tsx b/studio/frontend/src/features/studio/training-start-overlay.tsx index b9fb34fedb..3b0cce658b 100644 --- a/studio/frontend/src/features/studio/training-start-overlay.tsx +++ b/studio/frontend/src/features/studio/training-start-overlay.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +import { MascotImg } from "@/components/mascot-img"; import { AlertDialog, AlertDialogAction, @@ -312,11 +313,7 @@ export function TrainingStartOverlay({ return (

- Unsloth Studio +