refactor: enhance chat and UI elements with animations, tooltips, and improved styling; streamline sidebar, navbar, and chat-page interactions in top bar

This commit is contained in:
shine1i 2026-02-02 15:25:15 +01:00
commit db92ab230b
5 changed files with 114 additions and 27 deletions

View file

@ -4,6 +4,7 @@ import {
createRootRoute,
useRouterState,
} from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { Suspense } from "react";
import { AppProvider } from "../provider";
@ -20,9 +21,19 @@ function RootLayout() {
return (
<AppProvider>
{!hideNavbar && <Navbar />}
<Suspense fallback={null}>
<Outlet />
</Suspense>
<AnimatePresence initial={false}>
<motion.div
key={pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
className="flex-1"
>
<Suspense fallback={null}>
<Outlet />
</Suspense>
</motion.div>
</AnimatePresence>
</AppProvider>
);
}

View file

@ -5,8 +5,11 @@ import {
} from "@/components/ui/hover-card";
import { cn } from "@/lib/utils";
import {
AiChat02Icon,
Analytics01Icon,
ArrowRight01Icon,
Book03Icon,
PackageIcon,
ZapIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -16,9 +19,9 @@ import { useState } from "react";
const NAV_ITEMS = [
{ label: "Studio", href: "/studio", icon: ZapIcon, enabled: true },
{ label: "Evaluate", href: "/evaluate", enabled: false },
{ label: "Export", href: "/export", enabled: true },
{ label: "Chat", href: "/chat", enabled: true },
{ label: "Evaluate", href: "/evaluate", icon: Analytics01Icon, enabled: false },
{ label: "Export", href: "/export", icon: PackageIcon, enabled: true },
{ label: "Chat", href: "/chat", icon: AiChat02Icon, enabled: true },
];
export function Navbar() {
@ -37,11 +40,11 @@ export function Navbar() {
<motion.img
src="https://unsloth.ai/cgi/image/unsloth_sticker_no_shadow_ldN4V4iydw00qSIIWDCUv.png?width=96&quality=80&format=auto"
alt="unsloth"
className="size-8"
className="size-10"
animate={{ rotate: logoHovered ? 360 : 0 }}
transition={{ duration: 0.5, ease: [0.165, 0.84, 0.44, 1] }}
/>
<span className="text-xl font-bold tracking-tight font-heading">
<span className="text-2xl font-bold tracking-wide font-heading">
unsloth
</span>
<AnimatePresence>
@ -67,7 +70,7 @@ export function Navbar() {
return (
<span
key={item.href}
className="rounded-full px-4 py-1.5 text-sm font-medium text-muted-foreground/40 cursor-not-allowed"
className="relative rounded-full px-4 py-1.5 text-sm font-medium text-muted-foreground/40 cursor-not-allowed"
>
{item.label}
</span>
@ -78,19 +81,44 @@ export function Navbar() {
key={item.href}
to={item.href}
className={cn(
"rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
"relative rounded-full px-4 py-1.5 text-sm font-medium transition-colors",
active
? "bg-foreground text-background"
? "text-background"
: "text-muted-foreground hover:text-foreground",
)}
>
{"icon" in item && item.icon && (
<HugeiconsIcon
icon={item.icon}
className="size-3.5 inline-block mr-1 -mt-px fill-current"
{active && (
<motion.span
layoutId="nav-pill"
className="absolute inset-0 rounded-full bg-foreground"
transition={{
type: "spring",
stiffness: 500,
damping: 35,
mass: 0.5,
}}
/>
)}
{item.label}
<span className="relative z-10 flex items-center gap-1.5">
<AnimatePresence mode="popLayout">
{active && item.icon && (
<motion.span
key={item.href}
initial={{ width: 0, opacity: 0 }}
animate={{ width: "auto", opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: [0.165, 0.84, 0.44, 1] }}
className="overflow-hidden"
>
<HugeiconsIcon
icon={item.icon}
className="size-3.5 -mt-px fill-current"
/>
</motion.span>
)}
</AnimatePresence>
{item.label}
</span>
</Link>
);
})}

View file

@ -3,13 +3,23 @@ import {
ModelSelector,
} from "@/components/assistant-ui/model-selector";
import { Thread } from "@/components/assistant-ui/thread";
import { Button } from "@/components/ui/button";
import {
SidebarProvider,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Settings04Icon } from "@hugeicons/core-free-icons";
import {
ColumnInsertIcon,
PencilEdit02Icon,
Settings04Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
type CSSProperties,
@ -176,7 +186,7 @@ function InlineSidebar({
<aside
data-sidebar="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground h-full overflow-hidden transition-[width] duration-200 ease-linear",
"bg-sidebar text-sidebar-foreground h-full overflow-hidden rounded-2xl corner-squircle transition-[width] duration-200 ease-linear",
!collapsed && (side === "left" ? "border-r border-0 border-sidebar-border" : "border-l border-0 border-sidebar-border"),
collapsed ? "w-0" : "w-(--sidebar-width)",
)}
@ -189,6 +199,34 @@ function InlineSidebar({
);
}
function TopBarActions({
onNewThread,
onNewCompare,
}: { onNewThread: () => void; onNewCompare: () => void }) {
const { state } = useSidebar();
if (state !== "collapsed") return null;
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon-sm" onClick={onNewThread}>
<HugeiconsIcon icon={PencilEdit02Icon} strokeWidth={2} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">New Chat</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon-sm" onClick={onNewCompare}>
<HugeiconsIcon icon={ColumnInsertIcon} strokeWidth={2} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Compare</TooltipContent>
</Tooltip>
</>
);
}
export function ChatPage(): ReactElement {
const [view, setView] = useState<ChatView>({ mode: "single" });
const [settingsOpen, setSettingsOpen] = useState(false);
@ -204,6 +242,14 @@ export function ChatPage(): ReactElement {
() => setInferenceParams((p) => ({ ...p, checkpoint: "" })),
[],
);
const handleNewThread = useCallback(
() => setView({ mode: "single" }),
[],
);
const handleNewCompare = useCallback(
() => setView({ mode: "compare", pairId: crypto.randomUUID() }),
[],
);
const models =
inferenceParams.inferenceEngine === "llama-cpp"
@ -212,7 +258,7 @@ export function ChatPage(): ReactElement {
return (
<SidebarProvider
defaultOpen={false}
defaultOpen={true}
className="!min-h-0 h-[calc(100vh-4rem)] max-w-7xl mx-auto px-4"
style={{ "--sidebar-width": "14rem", "--sidebar-width-icon": "3rem" } as CSSProperties}
>
@ -220,17 +266,19 @@ export function ChatPage(): ReactElement {
<ThreadSidebar
view={view}
onSelect={setView}
onNewThread={() => setView({ mode: "single" })}
onNewCompare={() =>
setView({ mode: "compare", pairId: crypto.randomUUID() })
}
onNewThread={handleNewThread}
onNewCompare={handleNewCompare}
/>
</InlineSidebar>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex h-11 shrink-0 items-center px-2">
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
<SidebarTrigger />
<TopBarActions
onNewThread={handleNewThread}
onNewCompare={handleNewCompare}
/>
<ModelSelector
models={models}
value={inferenceParams.checkpoint}

View file

@ -211,7 +211,7 @@ export function ChatSettingsPanel({
return (
<aside
className={`shrink-0 h-full overflow-hidden bg-sidebar transition-[width] duration-200 ease-linear ${open ? "w-[17rem] border-sidebar-border" : "w-0"}`}
className={`shrink-0 h-full overflow-hidden bg-sidebar rounded-2xl corner-squircle transition-[width] duration-200 ease-linear ${open ? "w-[17rem] border-sidebar-border" : "w-0"}`}
>
<div className="flex h-full w-[17rem] flex-col">
<div className="flex items-center gap-2 px-3 py-2">

View file

@ -52,7 +52,7 @@
--chart-4: oklch(0.6926 0.1112 346.5775);
--chart-5: oklch(0.7497 0.1003 85.0057);
--radius: 1.2rem;
--sidebar: oklch(0.9845 0.005 165.0106);
--sidebar: oklch(0.975 0 0);
--sidebar-foreground: oklch(0.1281 0.0179 169.2764);
--sidebar-primary: oklch(0.6929 0.1396 166.5513);
--sidebar-primary-foreground: oklch(1 0 0);
@ -118,7 +118,7 @@
--chart-3: oklch(0.7554 0.1285 197.339);
--chart-4: oklch(0.7503 0.1199 346.7805);
--chart-5: oklch(0.799 0.1196 84.6633);
--sidebar: oklch(0.0853 0.0204 156.6743);
--sidebar: oklch(0.15 0 0);
--sidebar-foreground: oklch(0.9808 0.0059 170.4505);
--sidebar-primary: oklch(0.6929 0.1396 166.5513);
--sidebar-primary-foreground: oklch(1 0 0);