From 4ed40cb0747d21b3b91d21558168d259cb07f7cf Mon Sep 17 00:00:00 2001 From: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> Date: Mon, 15 Jun 2026 05:15:51 -0700 Subject: [PATCH] Studio: loading spinner consolidation and sidebar hover pill polish (#6334) * Studio: use the shared spinner for loading toasts The loading toast used a different spinner from the model download toast, so 'Unloading model' and 'Downloading model' looked inconsistent. Switch the sonner loading icon to the app-wide Spinner so they match. * 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. * Studio: align and even out sidebar hover pills The Recents chat rows started further left than the nav items above them and the hover pill widths varied. Give every nav and chat group the same pl-1.5 pr-2 so the pills line up and share one width, and on hover the chat rows only reserve room for the kebab so the title keeps one more character. * Studio: address review feedback on spinner and sidebar polish - recipe-graph-node: restore rounded-full bg-background on the spinner wrapper so it keeps masking the grid lines behind the floating spinner (the original hand-rolled ring had this mask). - app-sidebar: revert the pinned-row open-state padding to pr-8 so the unpin button stays clear of the title when the kebab menu is open. - app-sidebar: correct the now-stale inset comment (pl-1.5 = 6px, 18px). --- .../frontend/src/components/app-sidebar.tsx | 23 +++++++++++-------- .../src/components/assistant-ui/image.tsx | 4 ++-- .../assistant-ui/tool-call-spinner.tsx | 17 -------------- .../components/assistant-ui/tool-fallback.tsx | 4 ++-- .../components/assistant-ui/tool-group.tsx | 4 ++-- .../assistant-ui/tool-ui-code-execution.tsx | 4 ++-- .../assistant-ui/tool-ui-knowledge-base.tsx | 5 ++-- .../assistant-ui/tool-ui-python.tsx | 4 ++-- .../assistant-ui/tool-ui-terminal.tsx | 4 ++-- .../src/components/tauri/startup-screen.tsx | 18 ++++----------- .../src/components/tauri/update-screen.tsx | 12 ++-------- studio/frontend/src/components/ui/sonner.tsx | 11 +++------ .../rag/components/document-preview-sheet.tsx | 6 ++--- .../rag/components/document-status-chip.tsx | 9 +++----- .../components/recipe-graph-node.tsx | 9 ++++---- .../dialogs/seed/unstructured-drop-zone.tsx | 7 ++---- .../dataset-preview-dialog-mapping.tsx | 5 ++-- 17 files changed, 53 insertions(+), 93 deletions(-) delete mode 100644 studio/frontend/src/components/assistant-ui/tool-call-spinner.tsx diff --git a/studio/frontend/src/components/app-sidebar.tsx b/studio/frontend/src/components/app-sidebar.tsx index 8087b30b91..66accdce3a 100644 --- a/studio/frontend/src/components/app-sidebar.tsx +++ b/studio/frontend/src/components/app-sidebar.tsx @@ -668,15 +668,17 @@ export function AppSidebar() { : "sidebar-row-action group-hover/recent-item:opacity-100 group-hover/recent-item:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto"; const buttonClass = cn( "sidebar-nav-btn h-[33px] cursor-pointer rounded-full pr-4 text-[14.5px] leading-[19px] tracking-nav font-medium", - // pl-3 (12px) plus the content's pl-1 (4px) lines the title up with the - // Recents label text at 16px. + // pl-3 (12px) over the content's pl-1.5 (6px) = 18px, aligning the + // title with the nav items above. variant === "project" ? "pl-[39px]" : "pl-3", variant === "project" - ? "group-hover/project-chat-item:pr-8 group-has-[.sidebar-row-action[data-state=open]]/project-chat-item:pr-8" + ? "group-hover/project-chat-item:pr-6 group-has-[.sidebar-row-action[data-state=open]]/project-chat-item:pr-6" : isPinned - ? // Pinned rows show an extra unpin button on hover, so reserve more room. + ? // Pinned rows show an extra unpin button on hover, so reserve more room + // (pr-8 when the menu is open keeps the unpin button clear of the title). "group-hover/recent-item:pr-16 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-8" - : "group-hover/recent-item:pr-8 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-8", + : // Hover room for the kebab only; title keeps one more character. + "group-hover/recent-item:pr-6 group-has-[.sidebar-row-action[data-state=open]]/recent-item:pr-6", ); const isRenamingThis = @@ -962,7 +964,8 @@ export function AppSidebar() { )} - + {/* Uniform pl-1.5 pr-2 keeps every hover pill the same width, inset from the edge. */} + - + - + - + {pinnedChatItems.map((item) => renderChatSidebarItem(item, "recent"), @@ -1146,7 +1149,7 @@ export function AppSidebar() { - + {recentChatItems.map((item) => renderChatSidebarItem(item, "recent"), diff --git a/studio/frontend/src/components/assistant-ui/image.tsx b/studio/frontend/src/components/assistant-ui/image.tsx index 0850a8230a..f10c1bf024 100644 --- a/studio/frontend/src/components/assistant-ui/image.tsx +++ b/studio/frontend/src/components/assistant-ui/image.tsx @@ -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, )} > - + Generating image… ); diff --git a/studio/frontend/src/components/assistant-ui/tool-call-spinner.tsx b/studio/frontend/src/components/assistant-ui/tool-call-spinner.tsx deleted file mode 100644 index 79789692cb..0000000000 --- a/studio/frontend/src/components/assistant-ui/tool-call-spinner.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/studio/frontend/src/components/assistant-ui/tool-fallback.tsx b/studio/frontend/src/components/assistant-ui/tool-fallback.tsx index 8930b6386f..965ad56bfa 100644 --- a/studio/frontend/src/components/assistant-ui/tool-fallback.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-fallback.tsx @@ -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 ? ( - + ) : ToolIcon ? ( {active ? ( - + ) : ( {isRunning ? (
- + {runningLabel}
) : resultText ? ( diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-knowledge-base.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-knowledge-base.tsx index 09d6af2d64..ec24060072 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-knowledge-base.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-knowledge-base.tsx @@ -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 = ({ {isRunning ? (
- + {query ? ( <>Searching documents for “{query}”… diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx index 1231ca4a7f..d7b0e58f18 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx @@ -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 ? (
- + Running…
) : output ? ( diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx index 40cb28d4e4..3c12b16e6b 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx @@ -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 = ({
{isRunning ? (
- + Running…
) : output ? ( diff --git a/studio/frontend/src/components/tauri/startup-screen.tsx b/studio/frontend/src/components/tauri/startup-screen.tsx index 8e373c82a0..fd67a8a841 100644 --- a/studio/frontend/src/components/tauri/startup-screen.tsx +++ b/studio/frontend/src/components/tauri/startup-screen.tsx @@ -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 ( - - ); -} - function Logo() { return (
@@ -148,7 +140,7 @@ function CheckingContent() {
- +

Checking...

@@ -194,7 +186,7 @@ function InstallingContent({
- +

Installing...

Please wait a few mins, then you can start training. @@ -227,7 +219,7 @@ function RepairingContent({

- +

Updating existing Unsloth install...

{latest && (

{latest}

@@ -325,7 +317,7 @@ function StartingContent() {
- +

Starting server...

diff --git a/studio/frontend/src/components/tauri/update-screen.tsx b/studio/frontend/src/components/tauri/update-screen.tsx index 53e39612c0..aa540b7813 100644 --- a/studio/frontend/src/components/tauri/update-screen.tsx +++ b/studio/frontend/src/components/tauri/update-screen.tsx @@ -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 ( - - ); -} - function Logo() { return (
@@ -135,7 +127,7 @@ export function UpdateScreen({
- {!isError && } + {!isError && }

{statusLabel(status)}

diff --git a/studio/frontend/src/components/ui/sonner.tsx b/studio/frontend/src/components/ui/sonner.tsx index 9859190d2c..faaa075fa2 100644 --- a/studio/frontend/src/components/ui/sonner.tsx +++ b/studio/frontend/src/components/ui/sonner.tsx @@ -5,10 +5,10 @@ import { Alert02Icon, CheckmarkCircle02Icon, InformationCircleIcon, - Loading03Icon, MultiplicationSignCircleIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; +import { Spinner } from "@/components/ui/spinner"; import { useTheme } from "next-themes"; import { Toaster as Sonner, type ToasterProps } from "sonner"; @@ -51,13 +51,8 @@ const Toaster = ({ ...props }: ToasterProps) => { className="size-4" /> ), - loading: ( - - ), + // App-wide arc spinner so loading toasts match the "Downloading model" toast. + loading: , }} style={ { diff --git a/studio/frontend/src/features/rag/components/document-preview-sheet.tsx b/studio/frontend/src/features/rag/components/document-preview-sheet.tsx index fb65914ac6..d7f6842d83 100644 --- a/studio/frontend/src/features/rag/components/document-preview-sheet.tsx +++ b/studio/frontend/src/features/rag/components/document-preview-sheet.tsx @@ -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={
- Loading PDF… + Loading PDF…
} > @@ -444,7 +444,7 @@ export function DocumentPreviewSheet() {
{loading ? (
- Resolving source… + Resolving source…
) : error ? (
diff --git a/studio/frontend/src/features/rag/components/document-status-chip.tsx b/studio/frontend/src/features/rag/components/document-status-chip.tsx index 0f2d5341c9..839541871a 100644 --- a/studio/frontend/src/features/rag/components/document-status-chip.tsx +++ b/studio/frontend/src/features/rag/components/document-status-chip.tsx @@ -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({ {filename} {/* spinner while indexing, else close button */} {processing ? ( - + ) : onRemove ? (