diff --git a/studio/frontend/public/circle-logo-small.png b/studio/frontend/public/circle-logo-small.png new file mode 100644 index 000000000..8fc411695 Binary files /dev/null and b/studio/frontend/public/circle-logo-small.png differ diff --git a/studio/frontend/public/fonts/FiraCode-VariableFont_wght.ttf b/studio/frontend/public/fonts/FiraCode-VariableFont_wght.ttf new file mode 100644 index 000000000..d7077f1d6 Binary files /dev/null and b/studio/frontend/public/fonts/FiraCode-VariableFont_wght.ttf differ diff --git a/studio/frontend/public/fonts/Hellix-Medium.woff b/studio/frontend/public/fonts/Hellix-Medium.woff new file mode 100644 index 000000000..86e46d94d Binary files /dev/null and b/studio/frontend/public/fonts/Hellix-Medium.woff differ diff --git a/studio/frontend/public/fonts/Hellix-Regular.woff b/studio/frontend/public/fonts/Hellix-Regular.woff new file mode 100644 index 000000000..683aa71fa Binary files /dev/null and b/studio/frontend/public/fonts/Hellix-Regular.woff differ diff --git a/studio/frontend/public/sidebar-logo-black.png b/studio/frontend/public/sidebar-logo-black.png new file mode 100644 index 000000000..3db8fea46 Binary files /dev/null and b/studio/frontend/public/sidebar-logo-black.png differ diff --git a/studio/frontend/public/sidebar-logo-white.png b/studio/frontend/public/sidebar-logo-white.png new file mode 100644 index 000000000..f76b2ea39 Binary files /dev/null and b/studio/frontend/public/sidebar-logo-white.png differ diff --git a/studio/frontend/public/unsloth-beta-black.png b/studio/frontend/public/unsloth-beta-black.png new file mode 100644 index 000000000..beb3f6e82 Binary files /dev/null and b/studio/frontend/public/unsloth-beta-black.png differ diff --git a/studio/frontend/public/unsloth-beta-white.png b/studio/frontend/public/unsloth-beta-white.png new file mode 100644 index 000000000..be689ff87 Binary files /dev/null and b/studio/frontend/public/unsloth-beta-white.png differ diff --git a/studio/frontend/src/components/app-sidebar.tsx b/studio/frontend/src/components/app-sidebar.tsx index 032244ac3..959c2a6b3 100644 --- a/studio/frontend/src/components/app-sidebar.tsx +++ b/studio/frontend/src/components/app-sidebar.tsx @@ -36,11 +36,14 @@ import { ColumnInsertIcon, CursorInfo02Icon, Delete02Icon, + Download03Icon, + GemIcon, MessageSearch01Icon, Search01Icon, NewReleasesIcon, - PackageIcon, + PowerIcon, PencilEdit02Icon, + LayoutAlignLeftIcon, Settings02Icon, ZapIcon, } from "@hugeicons/core-free-icons"; @@ -50,9 +53,8 @@ import { } from "@/components/ui/tooltip"; import { Tooltip as TooltipPrimitive } from "radix-ui"; import { HugeiconsIcon } from "@hugeicons/react"; -import { ChevronDown, ChevronsUpDown, Moon, PanelLeft, Sun } from "lucide-react"; +import { ChevronDown, ChevronsUpDown, Moon, Sun } from "lucide-react"; import { Link, useNavigate, useRouterState } from "@tanstack/react-router"; -import { motion } from "motion/react"; import { useTrainingRuntimeStore } from "@/features/training"; import { useSettingsDialogStore } from "@/features/settings"; import { useEffectiveProfile, UserAvatar } from "@/features/profile"; @@ -67,7 +69,9 @@ import { useChatSearchStore } from "@/features/chat/stores/chat-search-store"; import { ChatSearchDialog } from "@/features/chat/components/chat-search-dialog"; import { useTrainingHistorySidebarItems, deleteTrainingRun } from "@/features/training"; import type { TrainingRunSummary } from "@/features/training"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { ShutdownDialog } from "@/components/shutdown-dialog"; +import { removeTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; function getTourId(pathname: string): string | null { if (pathname.startsWith("/studio")) return "studio"; @@ -76,8 +80,6 @@ function getTourId(pathname: string): string | null { return null; } -const NAV_SPRING = { type: "spring", stiffness: 500, damping: 35, mass: 0.5 } as const; - function runStatusDotClass(status: TrainingRunSummary["status"]): string { switch (status) { case "running": @@ -107,6 +109,13 @@ function formatRelativeShort(iso: string): string { return `${d}d`; } +function createNavigationNonce(): string { + if (typeof globalThis.crypto?.randomUUID === "function") { + return globalThis.crypto.randomUUID(); + } + return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +} + function NavItem({ icon, label, @@ -130,13 +139,6 @@ function NavItem({ return (
- {isNav && active && ( - - )} - - {label} + + {label}
{children} @@ -176,14 +178,16 @@ export function AppSidebar() { const isTrainingRunning = useTrainingRuntimeStore((s) => s.isTrainingRunning); const chatOnly = usePlatformStore((s) => s.isChatOnly()); + const [shutdownOpen, setShutdownOpen] = useState(false); - // Chat collapsible state — open by default, syncs with route + // Chat collapsible state — open by default, auto-expand on route entry const isChatRoute = pathname.startsWith("/chat"); const isStudioRoute = pathname === "/studio" || pathname.startsWith("/studio/"); const [chatOpen, setChatOpen] = useState(true); const [runsOpen, setRunsOpen] = useState(true); - const effectiveChatOpen = isChatRoute || chatOpen; - const effectiveRunsOpen = isStudioRoute || runsOpen; + + useEffect(() => { if (isChatRoute) setChatOpen(true); }, [isChatRoute]); + useEffect(() => { if (isStudioRoute) setRunsOpen(true); }, [isStudioRoute]); const isRecipesRoute = pathname.startsWith("/data-recipes"); const { displayTitle, avatarDataUrl } = useEffectiveProfile(); @@ -219,26 +223,43 @@ export function AppSidebar() { return ( <> - - + + {/* Expanded: compact logo + close toggle */} -
+
{ + event.preventDefault(); + if (chatDisabled) return; + setActiveThreadId(null); + closeMobileIfOpen(); + void navigate({ + to: "/chat", + search: { new: createNavigationNonce() }, + }); + }} + className="flex items-center gap-[6px] select-none" aria-label="Unsloth home" > Unsloth - Unsloth + + unsloth + + + BETA + {!isMobile && ( @@ -246,10 +267,10 @@ export function AppSidebar() { @@ -259,18 +280,18 @@ export function AppSidebar() { )}
- {/* Collapsed: sticker with hover-swap to open toggle */} + {/* Collapsed: panel icon doubles as expand trigger */} {!isMobile && ( -
+
@@ -281,7 +302,7 @@ export function AppSidebar() { )} - + { if (chatDisabled) return; setActiveThreadId(null); - navigate({ to: "/chat", search: { new: crypto.randomUUID() } }); + navigate({ to: "/chat", search: { new: createNavigationNonce() } }); closeMobileIfOpen(); }} /> i.id === search.compare)} disabled={chatDisabled} dataTour="chat-compare" onClick={() => { if (chatDisabled) return; setActiveThreadId(null); - navigate({ to: "/chat", search: { compare: crypto.randomUUID() } }); + navigate({ to: "/chat", search: { compare: createNavigationNonce() } }); closeMobileIfOpen(); }} /> @@ -322,16 +343,15 @@ export function AppSidebar() { /> -
{/* Navigate (no header) */} - + -
- {/* Recent Chats */} - {chatItems.length > 0 && ( - - - + {/* Recent Chats — hide on Studio only (Eyera fac13); chatOpen = ec695 clickability */} + {!isStudioRoute && chatItems.length > 0 && ( + + + - Recent Chats + Recents @@ -385,7 +404,7 @@ export function AppSidebar() { { navigate({ to: "/chat", @@ -406,7 +425,7 @@ export function AppSidebar() { handleDeleteThread(item); }} title="Delete" - className="absolute right-1 top-1/2 -translate-y-1/2 flex size-5 scale-90 items-center justify-center rounded-md text-sidebar-foreground/55 opacity-0 transition-all duration-150 hover:bg-destructive/12 hover:text-destructive group-hover/recent-item:scale-100 group-hover/recent-item:opacity-100" + className="absolute right-1 top-1/2 -translate-y-1/2 flex size-5 scale-90 items-center justify-center rounded-[8px] text-sidebar-foreground/55 opacity-0 transition-all duration-150 hover:bg-destructive/12 hover:text-destructive group-hover/recent-item:scale-100 group-hover/recent-item:opacity-100" > @@ -421,11 +440,11 @@ export function AppSidebar() { {/* Recent Runs */} {isStudioRoute && runItems.length > 0 && !chatOnly && ( - - - + + + - Recent Runs + Recents @@ -442,7 +461,7 @@ export function AppSidebar() { > { setSelectedHistoryRunId(run.id); closeMobileIfOpen(); @@ -456,7 +475,7 @@ export function AppSidebar() { )} aria-hidden /> - + {run.model_name} @@ -482,7 +501,7 @@ export function AppSidebar() { } }} title="Delete" - className="absolute right-1 top-1/2 -translate-y-1/2 flex size-5 scale-90 items-center justify-center rounded-md text-sidebar-foreground/55 opacity-0 transition-all duration-150 hover:bg-destructive/12 hover:text-destructive group-hover/run-item:scale-100 group-hover/run-item:opacity-100" + className="absolute right-1 top-1/2 -translate-y-1/2 flex size-5 scale-90 items-center justify-center rounded-[8px] text-sidebar-foreground/55 opacity-0 transition-all duration-150 hover:bg-destructive/12 hover:text-destructive group-hover/run-item:scale-100 group-hover/run-item:opacity-100" > @@ -497,7 +516,7 @@ export function AppSidebar() { )} - + @@ -505,7 +524,7 @@ export function AppSidebar() {
- {displayTitle} - Train + {displayTitle} + Studio
@@ -525,7 +544,7 @@ export function AppSidebar() { What's New
+ + + + Feedback + + - - - - Feedback - + setShutdownOpen(true)}> + + Shutdown @@ -608,6 +631,11 @@ export function AppSidebar() { + ); } diff --git a/studio/frontend/src/components/assistant-ui/code-plugin.ts b/studio/frontend/src/components/assistant-ui/code-plugin.ts new file mode 100644 index 000000000..5df7ac4f9 --- /dev/null +++ b/studio/frontend/src/components/assistant-ui/code-plugin.ts @@ -0,0 +1,66 @@ +// 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 { + createCodePlugin as createShikiCodePlugin, + type CodeHighlighterPlugin, + type CodePluginOptions, + type HighlightOptions, + type HighlightResult, +} from "@streamdown/code"; +import type { BundledLanguage } from "shiki"; + +// Fence tags LLMs/users commonly write that shiki doesn't expose as aliases. +// Keys are lower-cased input; values are canonical shiki language ids. +const LANGUAGE_ALIAS_OVERRIDES: Record = { + objectivec: "objective-c", + "obj-c": "objective-c", + objectivecpp: "objective-cpp", + "objective-cplusplus": "objective-cpp", + objcpp: "objective-cpp", + "c++": "cpp", + cplusplus: "cpp", + "c#": "csharp", + cs: "csharp", + "f#": "fsharp", + "c-sharp": "csharp", + "f-sharp": "fsharp", + golang: "go", + rs: "rust", + rb: "ruby", + py: "python", + sh: "shellscript", + bash: "shellscript", + zsh: "shellscript", + shell: "shellscript", + yml: "yaml", + ts: "typescript", + js: "javascript", + kt: "kotlin", + rsx: "rust", + "vue-html": "vue", +}; + +const normalizeLanguage = (language: string): BundledLanguage => { + const key = language.trim().toLowerCase(); + const override = LANGUAGE_ALIAS_OVERRIDES[key]; + return (override ?? (key as BundledLanguage)); +}; + +export function createCodePlugin( + options: CodePluginOptions = {}, +): CodeHighlighterPlugin { + const inner = createShikiCodePlugin(options); + return { + ...inner, + supportsLanguage: (language) => inner.supportsLanguage(normalizeLanguage(language)), + highlight: ( + opts: HighlightOptions, + callback?: (result: HighlightResult) => void, + ) => + inner.highlight( + { ...opts, language: normalizeLanguage(opts.language) }, + callback, + ), + }; +} diff --git a/studio/frontend/src/components/assistant-ui/code-themes.ts b/studio/frontend/src/components/assistant-ui/code-themes.ts new file mode 100644 index 000000000..2557b45ef --- /dev/null +++ b/studio/frontend/src/components/assistant-ui/code-themes.ts @@ -0,0 +1,30 @@ +// 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 oneDarkPro from "@shikijs/themes/one-dark-pro"; +import oneLight from "@shikijs/themes/one-light"; +import type { ThemeRegistrationAny } from "shiki"; + +// Canonical Atom One Dark / One Light themes, shipped by `@shikijs/themes`. +// We only override the background so the code block blends into the app's +// `--code-block` surface instead of painting its own. Every token color and +// scope mapping is left intact — that's what gives consistent multi-language +// highlighting (including Objective-C, Go, Rust, etc.) out of the box. +const withTransparentBg = (theme: ThemeRegistrationAny): ThemeRegistrationAny => ({ + ...theme, + bg: "transparent", + colors: { + ...theme.colors, + "editor.background": "transparent", + }, +}); + +export const unslothLightTheme: ThemeRegistrationAny = { + ...withTransparentBg(oneLight), + name: "unsloth-light", +}; + +export const unslothDarkTheme: ThemeRegistrationAny = { + ...withTransparentBg(oneDarkPro), + name: "unsloth-dark", +}; diff --git a/studio/frontend/src/components/assistant-ui/code-toggle-icon.tsx b/studio/frontend/src/components/assistant-ui/code-toggle-icon.tsx new file mode 100644 index 000000000..6d7abefad --- /dev/null +++ b/studio/frontend/src/components/assistant-ui/code-toggle-icon.tsx @@ -0,0 +1,22 @@ +// 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 { FC } from "react"; + +export const CodeToggleIcon: FC<{ className?: string }> = ({ className }) => { + return ( + + ); +}; diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index ea409591f..2a0517a44 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -8,7 +8,7 @@ import { preprocessLaTeX } from "@/lib/latex"; import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { code } from "@streamdown/code"; +import { createCodePlugin } from "./code-plugin"; import { createMathPlugin } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { DownloadIcon, Maximize2Icon, Minimize2Icon } from "lucide-react"; @@ -16,8 +16,12 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Block, type BlockProps, Streamdown } from "streamdown"; import "katex/dist/katex.min.css"; import { AudioPlayer } from "./audio-player"; +import { unslothDarkTheme, unslothLightTheme } from "./code-themes"; const math = createMathPlugin({ singleDollarTextMath: true }); +const code = createCodePlugin({ + themes: [unslothLightTheme, unslothDarkTheme], +}); const { withSmoothContextProvider } = INTERNAL; const STREAMDOWN_COMPONENTS = { @@ -425,7 +429,7 @@ const MarkdownTextImpl = () => { panZoom: true, }, }} - shikiTheme={["github-light", "github-dark"]} + shikiTheme={[unslothLightTheme, unslothDarkTheme]} BlockComponent={StreamdownBlock} > {processedText} diff --git a/studio/frontend/src/components/assistant-ui/model-selector.tsx b/studio/frontend/src/components/assistant-ui/model-selector.tsx index 08e69bbf9..362825217 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector.tsx @@ -68,9 +68,9 @@ function ModelSelectorTrigger({ className={cn( "flex items-center gap-2 transition-colors", variant === "outline" && - "rounded-full border border-border/60 hover:bg-accent", - variant === "ghost" && "rounded-md hover:bg-accent", - variant === "muted" && "rounded-md bg-muted hover:bg-muted/80", + "rounded-[8px] border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2e3035]", + variant === "ghost" && "rounded-[8px] hover:bg-[#ececec] dark:hover:bg-[#2e3035]", + variant === "muted" && "rounded-[8px] bg-muted hover:bg-muted/80", size === "sm" && "h-8 px-3 text-xs", size === "default" && "h-9 px-3.5 text-sm", size === "lg" && "h-10 px-4 text-sm", @@ -80,15 +80,16 @@ function ModelSelectorTrigger({ {isLoaded && ( )} - - {currentModel?.name ?? "Select model..."} + + {currentModel?.name ?? "Select model"} {currentModel?.description && ( {currentModel.description} )} diff --git a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx index 8f318a829..30b8fdecb 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -152,8 +152,8 @@ function ModelRow({ type="button" onClick={onClick} className={cn( - "flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-accent", - selected && "bg-accent/60", + "flex w-full items-center gap-2 rounded-[6px] px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-[#ececec] dark:hover:bg-[#2e3035]", + selected && "bg-[#ececec] dark:bg-[#2e3035]", )} > diff --git a/studio/frontend/src/components/assistant-ui/reasoning.tsx b/studio/frontend/src/components/assistant-ui/reasoning.tsx index 4d46fc8d5..387f8cd45 100644 --- a/studio/frontend/src/components/assistant-ui/reasoning.tsx +++ b/studio/frontend/src/components/assistant-ui/reasoning.tsx @@ -11,6 +11,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock"; import { cn } from "@/lib/utils"; import { type ReasoningGroupComponent, @@ -67,49 +68,8 @@ function ReasoningRoot({ ...props }: ReasoningRootProps) { const collapsibleRef = useRef(null); - const lockCleanupRef = useRef<(() => void) | null>(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); - - useEffect(() => { - return () => { - lockCleanupRef.current?.(); - }; - }, []); - - const lockScroll = useCallback(() => { - lockCleanupRef.current?.(); - - const animatedElement = collapsibleRef.current; - if (!animatedElement) return; - - let scrollContainer: HTMLElement | null = animatedElement; - while (scrollContainer) { - const { overflowY } = getComputedStyle(scrollContainer); - if (overflowY === "scroll" || overflowY === "auto") { - break; - } - scrollContainer = scrollContainer.parentElement; - } - if (!scrollContainer) return; - - const scrollPosition = scrollContainer.scrollTop; - const resetPosition = () => { - scrollContainer.scrollTop = scrollPosition; - }; - - scrollContainer.addEventListener("scroll", resetPosition); - let timeoutId: ReturnType | null = null; - const cleanup = () => { - if (timeoutId !== null) { - clearTimeout(timeoutId); - timeoutId = null; - } - scrollContainer.removeEventListener("scroll", resetPosition); - lockCleanupRef.current = null; - }; - timeoutId = setTimeout(cleanup, ANIMATION_DURATION); - lockCleanupRef.current = cleanup; - }, []); + const lockScroll = useCollapseScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 36713d665..54f64220e 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -16,6 +16,7 @@ import { WebSearchToolUI } from "@/components/assistant-ui/tool-ui-web-search"; import { PythonToolUI } from "@/components/assistant-ui/tool-ui-python"; import { TerminalToolUI } from "@/components/assistant-ui/tool-ui-terminal"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +import { CodeToggleIcon } from "@/components/assistant-ui/code-toggle-icon"; import { Button } from "@/components/ui/button"; import { sentAudioNames } from "@/features/chat/api/chat-adapter"; import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils"; @@ -34,6 +35,7 @@ import { useAui, useAuiEvent, useAuiState, + useThreadViewport, } from "@assistant-ui/react"; import { motion } from "motion/react"; import { @@ -69,12 +71,7 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({ }) => { return ( = ({ > {!hideWelcome && ( @@ -103,31 +98,29 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({ }} /> - {/* Small overlap and extra slack so the last lines can scroll under the composer cleanly */} - {!hideComposer &&
} - - + {/* Bottom slack so the last message has breathing room above the + sticky scroll-to-bottom button (and the floating composer in + single mode). Without this, content would butt against the + sticky footer and feel cramped. */} + !thread.isEmpty}>
+ + + !thread.isEmpty}> + -
-
+ + + {!hideComposer && ( !thread.isEmpty}>
@@ -140,7 +133,7 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({

- LLM's can make mistakes. Double-check all responses. + LLMs can make mistakes. Double-check all responses.

@@ -151,12 +144,24 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({ }; const ThreadScrollToBottom: FC = () => { + // Scoped to the nearest ThreadPrimitive.Root via context, so in compare + // mode each pane reads its own viewport state. + // + // The button stays mounted and toggles visibility via CSS. Conditionally + // rendering (return null) unmounts a DOM node inside the viewport, which + // the assistant-ui autoscroll hook's MutationObserver sees as a content + // change — during streaming that triggered spurious scroll-to-bottom + // calls, especially in the narrower mobile stacked layout. + const isAtBottom = useThreadViewport((vp) => vp.isAtBottom); return ( @@ -227,7 +232,7 @@ const SuggestionItem: FC = () => { const ThreadWelcome: FC<{ hideComposer?: boolean }> = ({ hideComposer }) => { return (
-
+
= ({ hideComposer }) => { alt="Sloth mascot" className="size-20" /> -

+

Chat with your model

-

+

Run GGUFs, safetensors, vision and audio models

@@ -303,13 +308,13 @@ const PendingAudioChip: FC = () => { const Composer: FC = () => { return ( - + { ); }; -const CodeToggleIcon: FC<{ className?: string }> = ({ className }) => { - return ( - - ); -}; - const ToolStatusDisplay: FC = () => { const toolStatus = useChatRuntimeStore((s) => s.toolStatus); const isThreadRunning = useAuiState(({ thread }) => thread.isRunning); @@ -638,7 +625,7 @@ const GeneratingIndicator: FC = () => { const AssistantMessage: FC = () => { return (
@@ -791,14 +778,14 @@ const UserMessageAudio: FC = () => { const UserMessage: FC = () => { return (
-
+
@@ -844,7 +831,7 @@ const EditComposer: FC = () => {
diff --git a/studio/frontend/src/components/assistant-ui/tool-fallback.tsx b/studio/frontend/src/components/assistant-ui/tool-fallback.tsx index 82a5b17e0..e40716304 100644 --- a/studio/frontend/src/components/assistant-ui/tool-fallback.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-fallback.tsx @@ -8,11 +8,11 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock"; import { cn } from "@/lib/utils"; import { type ToolCallMessagePartComponent, type ToolCallMessagePartStatus, - useScrollLock, } from "@assistant-ui/react"; import { AlertCircleIcon, @@ -52,7 +52,7 @@ function ToolFallbackRoot({ }: ToolFallbackRootProps) { const collapsibleRef = useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); - const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); + const lockScroll = useCollapseScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; diff --git a/studio/frontend/src/components/assistant-ui/tool-group.tsx b/studio/frontend/src/components/assistant-ui/tool-group.tsx index bf7a6a9a2..e91da4cee 100644 --- a/studio/frontend/src/components/assistant-ui/tool-group.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-group.tsx @@ -12,12 +12,12 @@ import { ChevronDownIcon, LoaderIcon } from "lucide-react"; import { Wrench01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { cva, type VariantProps } from "class-variance-authority"; -import { useScrollLock } from "@assistant-ui/react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { useCollapseScrollLock } from "@/hooks/use-collapse-scroll-lock"; import { cn } from "@/lib/utils"; const ANIMATION_DURATION = 200; @@ -54,7 +54,7 @@ function ToolGroupRoot({ }: ToolGroupRootProps) { const collapsibleRef = useRef(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); - const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); + const lockScroll = useCollapseScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; diff --git a/studio/frontend/src/components/navbar.tsx b/studio/frontend/src/components/navbar.tsx index 56b1e0346..716c9d791 100644 --- a/studio/frontend/src/components/navbar.tsx +++ b/studio/frontend/src/components/navbar.tsx @@ -7,13 +7,13 @@ export function Navbar() { const { isMobile } = useSidebar(); if (!isMobile) { return ( -
+
); } return ( -
-
- +
+
+
); diff --git a/studio/frontend/src/components/ui/sidebar.tsx b/studio/frontend/src/components/ui/sidebar.tsx index 6d9016f8e..967b0dcaa 100644 --- a/studio/frontend/src/components/ui/sidebar.tsx +++ b/studio/frontend/src/components/ui/sidebar.tsx @@ -26,7 +26,7 @@ import { } from "@/components/ui/tooltip" import { useIsMobile } from "@/hooks/use-mobile" import { HugeiconsIcon } from "@hugeicons/react" -import { SidebarLeftIcon } from "@hugeicons/core-free-icons" +import { LayoutAlignLeftIcon } from "@hugeicons/core-free-icons" const noop = () => {} @@ -337,7 +337,7 @@ function SidebarTrigger({ }} {...props} > - + Toggle Sidebar ) @@ -471,7 +471,7 @@ function SidebarGroupLabel({ data-slot="sidebar-group-label" data-sidebar="group-label" className={cn( - "text-[#94a3b8] dark:text-[#64748b] ring-sidebar-ring h-auto pt-3 pb-2 px-4 rounded-md text-[10px] font-semibold uppercase tracking-[0.08em] group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-3 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0", + "text-[#94a3b8] dark:text-[#666] ring-sidebar-ring h-auto pt-3 pb-2 px-4 rounded-md text-[10px] font-semibold uppercase tracking-[0.08em] group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-3 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0", className )} {...props} @@ -536,7 +536,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { } const sidebarMenuButtonVariants = cva( - "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm cursor-pointer transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:w-full! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate group-data-[collapsible=icon]:[&>span]:hidden [&_svg]:size-4 [&_svg]:shrink-0 group-data-[collapsible=icon]:[&_svg]:size-5", + "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm cursor-pointer transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:w-full! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-2! data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate group-data-[collapsible=icon]:[&>span]:hidden [&_svg]:size-4 [&_svg]:shrink-0 group-data-[collapsible=icon]:[&_svg]:size-[18px]", { variants: { variant: { diff --git a/studio/frontend/src/components/ui/slider.tsx b/studio/frontend/src/components/ui/slider.tsx index 573dea835..705182e5a 100644 --- a/studio/frontend/src/components/ui/slider.tsx +++ b/studio/frontend/src/components/ui/slider.tsx @@ -79,7 +79,7 @@ function Slider({ > ))} diff --git a/studio/frontend/src/components/ui/textarea.tsx b/studio/frontend/src/components/ui/textarea.tsx index b71e59395..36c372bdf 100644 --- a/studio/frontend/src/components/ui/textarea.tsx +++ b/studio/frontend/src/components/ui/textarea.tsx @@ -1,19 +1,28 @@ // 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 * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( -