Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Lee Jackson
dc6991b635
Merge branch 'main' into studio-consolidate-spinners 2026-06-15 11:33:31 +01:00
shimmyshimmer
7aa065b363 Studio: consolidate loading spinners onto the shared Spinner
Replace the duplicate ToolCallSpinner and the remaining inline loading
spinners (hugeicons Loading03, lucide Loader2/Loader/LoaderCircle, and
the hand-rolled border rings) with the shared Spinner component, then
delete tool-call-spinner.tsx. Refresh and regenerate action icons and
the shimmer button are left alone since they are not loading spinners.
2026-06-15 03:32:24 -07:00
15 changed files with 35 additions and 74 deletions

View file

@ -17,10 +17,10 @@ import {
CopyIcon,
ImageIcon,
ImageOffIcon,
Loader2Icon,
RefreshCwIcon,
ShieldAlertIcon,
} from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { Download01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
@ -348,7 +348,7 @@ function ImageGenerating({ className }: { className?: string }) {
className,
)}
>
<Loader2Icon className="size-8 animate-spin text-muted-foreground" />
<Spinner className="size-8 text-muted-foreground" />
<span className="sr-only">Generating image</span>
</div>
);

View file

@ -1,17 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"use client";
import { Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils";
/** Spinner shown while a tool call runs. Inherits text color to match its surroundings. */
export function ToolCallSpinner({ className }: { className?: string }) {
return (
<Loader2Icon
aria-label="Loading"
className={cn("size-4 shrink-0 animate-spin", className)}
/>
);
}

View file

@ -8,7 +8,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
import { Spinner } from "@/components/ui/spinner";
import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock";
import { cn } from "@/lib/utils";
import {
@ -142,7 +142,7 @@ function ToolFallbackTrigger({
{...props}
>
{isRunning ? (
<ToolCallSpinner className="aui-tool-fallback-trigger-icon" />
<Spinner className="aui-tool-fallback-trigger-icon" />
) : ToolIcon ? (
<ToolIcon
data-slot="tool-fallback-trigger-icon"

View file

@ -21,7 +21,7 @@ import {
} from "@/components/ui/collapsible";
import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock";
import { cn } from "@/lib/utils";
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
import { Spinner } from "@/components/ui/spinner";
const ANIMATION_DURATION = 200;
@ -124,7 +124,7 @@ function ToolGroupTrigger({
{...props}
>
{active ? (
<ToolCallSpinner className="aui-tool-group-trigger-loader" />
<Spinner className="aui-tool-group-trigger-loader" />
) : (
<HugeiconsIcon
icon={Wrench01Icon}

View file

@ -14,7 +14,7 @@ import {
FileTextIcon,
TerminalIcon,
} from "lucide-react";
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
import { Spinner } from "@/components/ui/spinner";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ToolFallbackContent,
@ -181,7 +181,7 @@ const CodeExecutionToolUIImpl: ToolCallMessagePartComponent = ({
<ToolFallbackContent>
{isRunning ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<ToolCallSpinner className="size-3.5" />
<Spinner className="size-3.5" />
<span>{runningLabel}</span>
</div>
) : resultText ? (

View file

@ -7,7 +7,8 @@ import {
type ToolCallMessagePartComponent,
useAuiState,
} from "@assistant-ui/react";
import { FileTextIcon, LibraryBigIcon, LoaderIcon } from "lucide-react";
import { FileTextIcon, LibraryBigIcon } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { memo, useEffect, useMemo, useState } from "react";
import { Badge } from "./badge";
import {
@ -106,7 +107,7 @@ const KnowledgeBaseToolUIImpl: ToolCallMessagePartComponent = ({
<ToolFallbackContent>
{isRunning ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<LoaderIcon className="size-3.5 animate-spin" />
<Spinner className="size-3.5" />
<span>
{query ? (
<>Searching documents for &ldquo;{query}&rdquo;&hellip;</>

View file

@ -8,7 +8,7 @@ import { getAuthToken } from "@/features/auth/session";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { code as codePlugin } from "@streamdown/code";
import { CheckIcon, CodeIcon, CopyIcon } from "lucide-react";
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
import { Spinner } from "@/components/ui/spinner";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Streamdown } from "streamdown";
import {
@ -149,7 +149,7 @@ const PythonToolUIImpl: ToolCallMessagePartComponent = ({
{/* Output */}
{isRunning ? (
<div className="mt-2 flex items-center gap-2 text-sm text-muted-foreground">
<ToolCallSpinner className="size-3.5" />
<Spinner className="size-3.5" />
<span>Running&hellip;</span>
</div>
) : output ? (

View file

@ -6,7 +6,7 @@
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { CheckIcon, CopyIcon, TerminalIcon } from "lucide-react";
import { ToolCallSpinner } from "@/components/assistant-ui/tool-call-spinner";
import { Spinner } from "@/components/ui/spinner";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import {
ToolFallbackContent,
@ -87,7 +87,7 @@ const TerminalToolUIImpl: ToolCallMessagePartComponent = ({
<div className="border-l-2 border-muted-foreground/20 pl-2">
{isRunning ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<ToolCallSpinner className="size-3.5" />
<Spinner className="size-3.5" />
<span>Running&hellip;</span>
</div>
) : output ? (

View file

@ -2,6 +2,7 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { ShimmerButton } from "@/components/ui/shimmer-button";
import { Spinner } from "@/components/ui/spinner";
import type { BackendStatus } from "@/hooks/use-tauri-backend";
import type { CopySupportDiagnosticsResult } from "@/lib/tauri-diagnostics";
import { AnimatePresence, motion } from "motion/react";
@ -98,15 +99,6 @@ const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1];
// Sub-components
// ---------------------------------------------------------------------------
function TealSpinner({ size = 24 }: { size?: number }) {
return (
<span
className="inline-block animate-spin rounded-full border-2 border-primary border-t-transparent"
style={{ width: size, height: size, animationDuration: "0.8s" }}
/>
);
}
function Logo() {
return (
<div className="flex flex-col items-center gap-4">
@ -148,7 +140,7 @@ function CheckingContent() {
<Logo />
</div>
<div className="mb-10 flex flex-col items-center gap-2">
<TealSpinner />
<Spinner className="size-6 text-primary" />
<p className="text-sm text-muted-foreground">Checking...</p>
</div>
</div>
@ -194,7 +186,7 @@ function InstallingContent({
<Logo />
</div>
<div className="mb-10 flex flex-col items-center gap-2">
<TealSpinner />
<Spinner className="size-6 text-primary" />
<p className="text-sm font-bold text-foreground">Installing...</p>
<p className="text-sm font-bold text-muted-foreground">
Please wait a few mins, then you can start training.
@ -227,7 +219,7 @@ function RepairingContent({
<Logo />
</div>
<div className="mb-10 flex flex-col items-center gap-2">
<TealSpinner />
<Spinner className="size-6 text-primary" />
<p className="text-sm font-bold text-foreground">Updating existing Unsloth install...</p>
{latest && (
<p className="max-w-xs text-center text-xs text-muted-foreground">{latest}</p>
@ -325,7 +317,7 @@ function StartingContent() {
<Logo />
</div>
<div className="mb-10 flex flex-col items-center gap-2">
<TealSpinner />
<Spinner className="size-6 text-primary" />
<p className="text-sm text-muted-foreground">Starting server...</p>
</div>
</div>

View file

@ -4,6 +4,7 @@
import type { UpdateStatus } from "@/hooks/use-tauri-update";
import type { CopySupportDiagnosticsResult } from "@/lib/tauri-diagnostics";
import { AnimatePresence, motion } from "motion/react";
import { Spinner } from "@/components/ui/spinner";
import { useEffect, useRef, useState } from "react";
interface UpdateScreenProps {
@ -18,15 +19,6 @@ interface UpdateScreenProps {
const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1];
function Spinner({ size = 24 }: { size?: number }) {
return (
<span
className="inline-block animate-spin rounded-full border-2 border-primary border-t-transparent"
style={{ width: size, height: size, animationDuration: "0.8s" }}
/>
);
}
function Logo() {
return (
<div className="flex flex-col items-center gap-4">
@ -135,7 +127,7 @@ export function UpdateScreen({
<Logo />
<div className="mt-8 flex flex-col items-center gap-2">
{!isError && <Spinner />}
{!isError && <Spinner className="size-6 text-primary" />}
<p className="text-sm font-semibold text-foreground">
{statusLabel(status)}
</p>

View file

@ -9,10 +9,10 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
FileTextIcon,
LoaderIcon,
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import {
Sheet,
@ -209,7 +209,7 @@ function PdfPreview({
onLoadError={(e) => setError(e.message)}
loading={
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
<LoaderIcon className="size-3.5 animate-spin" /> Loading PDF
<Spinner className="size-3.5" /> Loading PDF
</div>
}
>
@ -444,7 +444,7 @@ export function DocumentPreviewSheet() {
<div className="min-h-0 flex-1">
{loading ? (
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
<LoaderIcon className="size-3.5 animate-spin" /> Resolving source
<Spinner className="size-3.5" /> Resolving source
</div>
) : error ? (
<div className="p-6 text-sm text-muted-foreground">

View file

@ -1,7 +1,8 @@
// 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 { LoaderCircleIcon, XIcon } from "lucide-react";
import { XIcon } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { HugeiconsIcon } from "@hugeicons/react";
import { File02Icon } from "@hugeicons/core-free-icons";
import { Badge } from "@/components/assistant-ui/badge";
@ -40,11 +41,7 @@ export function DocumentStatusChip({
<span className="truncate">{filename}</span>
{/* spinner while indexing, else close button */}
{processing ? (
<LoaderCircleIcon
className="shrink-0 animate-spin size-3.5 text-muted-foreground"
role="status"
aria-label="Loading"
/>
<Spinner className="shrink-0 size-3.5 text-muted-foreground" />
) : onRemove ? (
<button
type="button"

View file

@ -4,6 +4,7 @@
import { MarkdownPreview } from "@/components/markdown/markdown-preview";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
import {
BalanceScaleIcon,
@ -474,10 +475,7 @@ function RecipeGraphNodeBase({
>
{runtimeState === "running" && config?.kind === "llm" && (
<div className="pointer-events-none absolute -top-7 right-2 z-20">
<span
className="block size-6 animate-spin rounded-full border-[3px] border-primary/90 border-t-transparent bg-background"
aria-label="Running"
/>
<Spinner className="size-6 text-primary/90" />
</div>
)}
<NodeResizer

View file

@ -2,10 +2,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
import {
CloudUploadIcon,
Cancel01Icon,
Loading03Icon,
CheckmarkCircle02Icon,
Alert02Icon,
} from "@hugeicons/core-free-icons";
import { Spinner } from "@/components/ui/spinner";
import { HugeiconsIcon } from "@hugeicons/react";
import { uploadUnstructuredFile, removeUnstructuredFile } from "../../api";
import {
@ -226,10 +226,7 @@ export function UnstructuredDropZone({
className="flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm"
>
{entry.status === "uploading" && (
<HugeiconsIcon
icon={Loading03Icon}
className="text-muted-foreground size-4 animate-spin"
/>
<Spinner className="text-muted-foreground size-4" />
)}
{entry.status === "ok" && (
<HugeiconsIcon

View file

@ -14,7 +14,8 @@ import type { CheckFormatResponse } from "@/features/training/types/datasets";
import { cn } from "@/lib/utils";
import { AlertCircleIcon, CheckmarkCircle02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Loader2, Sparkles } from "lucide-react";
import { Sparkles } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
const CHATML_ROLES = ["system", "user", "assistant"] as const;
const ALPACA_ROLES = ["instruction", "input", "output"] as const;
@ -203,7 +204,7 @@ export function DatasetMappingCard({
>
{isAiLoading ? (
<>
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
<Spinner className="mr-1.5 size-3.5" />
Analyzing dataset...
</>
) : (