* Studio: UI polish for sidebar, menus, hub and toasts - Pinned chats: add a Pinned section above Recents with a Pin/Unpin menu action and a hover unpin button on each pinned row - Sidebar: align the Recents and Pinned section labels with Train, and switch the chat Archive icon to archive-03 - Dropdown menus: pull separators in to match item width, and even out the projects menu padding and radius to match the composer menu - Account menu: match the divider width to the row hover width - Hub toolbar: equalize the format, capability and sort filter widths so the search field has more room - Model run bar: make the quant selector hover a full pill - Tooltips: slightly larger corner radius - Auth: reduce horizontal padding on the submit button - Toasts: center the model load content so top and bottom padding match * Studio: shrink pinned unpin icon, match projects menu to composer menu * Studio: inset dropdown separators slightly from item width * Studio: move archived chats from the sidebar into chat settings Archive now shows a toast pointing to Settings. Archived chats are listed and managed (open, unarchive, delete) under Settings, Chat, Data. * Studio: make the archive toast open the archived chats list * Studio: keep chats archived when opened from the archived list * Chat settings: optional delete confirmation and menu cleanup Add a "Confirm before deleting" toggle in Settings > Chat > Data. When turned off, deleting a chat from the sidebar skips the confirm dialog and removes it instantly. The preference is stored client-side. Add a separator above Archive in the chat row menu so Archive and Delete read as a group separate from Export. Drop the duplicate divider above "Clear all chats". The section already draws a divide-y separator between rows, so the destructive row no longer adds its own top border. * Hub, dialog and page polish Hub catalog: - Drop "inference" from the Hub subtitle. - Make the HF token shield a circle in the header. - Equal, narrower filter triggers that stay a fixed width; long labels truncate with an ellipsis instead of clipping. - Tighter gap between the label and the dropdown chevron so one more character fits. - Keep the toolbar on one row at desktop widths. - Align the model list and row hover with the Discover tab, add right side breathing room. - Make the dark-mode scrollbar lighter so it is easier to see. - Filled size and quant chips no longer draw a border. - Column divider uses the sidebar border color. - Hub card extends into the bottom space. Filters and channels: - Add an MLX format option; detect MLX repos by library or tag. - "Fine-tune ready" now includes 16bit safetensors, not only bnb-4bit. - Shorten the curated channel descriptions. Shared UI: - New standard chevron icons (down and submenu right) shared across the Hub, dropdown menu, menubar, project switcher and model selector. - Dialogs get more top and bottom padding and a larger heading. - Guided Tour hides itself on pages with no tour. Other pages: - Export and Data Recipes use the same page padding as the Hub. * Soften popup overlays and refine hub filters Dialogs, alert dialogs and sheets now use a light blur with a slight darken instead of a heavy black scrim, so popups stay legible without dimming the whole screen. Edit System Prompt and Edit Chat Template drop their custom overlays and share the same look. Also limit the Fine-tune ready channel to checkpoints Unsloth can actually fine-tune (drops fp8, nvfp4, w4a16 and similar), revert chip borders so filled chips stay visible, nudge the Run icon to center it, and shrink the submenu chevron a touch. * Inline chat rename and review-feedback fixes Rename a chat inline as a rounded pill on the row instead of opening a dialog. Enter saves, Escape cancels, clicking away saves. Projects and training runs keep the existing dialog. Address review feedback: - Route the no-confirm chat delete through the shared cleanup helper so it gets the same error toast and pin removal as the confirmed path. - Archived chats now confirm before deleting (respecting the setting) and pass the active thread, so deleting the open chat resets to a new chat instead of leaving a stale thread URL. - Only offer the MLX format filter on Discover, since downloaded inventory rows are never tagged mlx and would filter to empty. - Tighten the non-finetunable name match so a dot-delimited token such as .tflite is also excluded from the Fine-tune ready channel. * Sidebar: borderless inline rename, no name flash, align titles Rename now edits the title as plain highlighted text with no pill or box. While the debounced sidebar refresh catches up after a rename the row shows the new name optimistically, so the old name no longer flashes back in. Recent chat titles now line up flush under the Recents label. * Profile: preferred name and avatar shape options Add a "What should Unsloth call you?" preferred name that drives the new-chat greeting, falling back to the first name then the login id. Add a toggle to show the avatar as a full circle or a rounded rectangle, applied everywhere the avatar appears. * Settings: show version block at the top of General Surface the Unsloth and package versions at the top of General, not just in About. Both tabs share one StudioVersionSection so the version fetch and markup live in a single place. * Profile: pick a sloth sticker as your avatar Add a curated set of sloth stickers users can choose as a profile picture, shown below the picture shape toggle in Settings > Profile. - Only squarish, low-whitespace stickers are listed so each one fills the avatar frame cleanly. - Selecting one reuses the existing avatar persistence (localStorage), so it sits alongside the photo upload with no backend changes. - Image avatars now get a neutral background so transparent stickers read cleanly in both shapes. * Profile: keep transparent avatars transparent A transparent upload is now kept as WebP and shrunk to fit instead of falling back to JPEG, which has no alpha and was painting a background behind the image. The image avatar is also explicitly transparent. * Profile: fall back to PNG for transparent avatars on Safari Safari cannot encode WebP via canvas, so a transparent upload was throwing instead of saving. Fall back to PNG, which keeps alpha and works in every browser, before giving up. Still never uses JPEG. * Archived chats: reset nav when deleting the open compare too Compare panes do not write the active thread to the store, so the pair id only lives in the route search. Derive the open chat id from the route (thread or compare) like the sidebar does, so deleting the open archived compare resets to a new chat instead of a dead URL. * Profile: trim three sloth stickers from the picker Drop the duplicate computer sloth without text on the screen (keeping the Local one), the nature-screen sloth, and the gift sloth. * Chat: tighten model selector right padding Use less right padding than left so the trailing chevron does not leave a heavy gap on the right of the trigger. * Address review feedback on profile, reset and rename - Give each sloth avatar button an accessible name so screen readers can tell the options apart. - Reset all local preferences now clears the delete-confirmation pref, so it returns to the safe confirm default. - A no-op Enter in inline rename now closes the editor instead of leaving the row stuck as an input with its blur suppressed. * Chat: rebalance model selector padding and chevron More left padding than right and pull the chevron close to the label so the trigger reads balanced around the text instead of heavy on the right. * Hub: shrink the external link dialog icon Set the icon to size-6 so it sits proportionally in the media circle instead of filling it. * Hub: shrink the external link dialog media circle Override the media circle to size-12 with a size-5 icon on this dialog only, so it is more compact without changing the shared component.
195 lines
5.3 KiB
TypeScript
195 lines
5.3 KiB
TypeScript
// 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 { Dialog as DialogPrimitive } from "radix-ui";
|
|
import type * as React from "react";
|
|
import { createContext, useContext } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
|
|
const DialogPortalContainerContext = createContext<HTMLElement | null>(null);
|
|
|
|
export function useDialogPortalContainer(): HTMLElement | null {
|
|
return useContext(DialogPortalContainerContext);
|
|
}
|
|
|
|
function Dialog({
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
}
|
|
|
|
function DialogTrigger({
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
}
|
|
|
|
function DialogPortal({
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
}
|
|
|
|
function DialogClose({
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
}
|
|
|
|
function DialogOverlay({
|
|
className,
|
|
position = "fixed",
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay> & {
|
|
position?: "fixed" | "absolute";
|
|
}) {
|
|
return (
|
|
<DialogPrimitive.Overlay
|
|
data-slot="dialog-overlay"
|
|
className={cn(
|
|
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/30 supports-backdrop-filter:backdrop-blur-[2px] duration-100 inset-0 isolate z-50",
|
|
position === "fixed" ? "fixed" : "absolute",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DialogContent({
|
|
className,
|
|
children,
|
|
showCloseButton = true,
|
|
container,
|
|
position = "fixed",
|
|
overlayClassName,
|
|
overlayPosition,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
showCloseButton?: boolean;
|
|
container?: HTMLElement | null;
|
|
position?: "fixed" | "absolute";
|
|
overlayClassName?: string;
|
|
overlayPosition?: "fixed" | "absolute";
|
|
}) {
|
|
const resolvedContainer = container ?? null;
|
|
return (
|
|
<DialogPortalContainerContext.Provider value={resolvedContainer}>
|
|
<DialogPortal container={resolvedContainer ?? undefined}>
|
|
<DialogOverlay
|
|
className={overlayClassName}
|
|
position={overlayPosition ?? position}
|
|
/>
|
|
<DialogPrimitive.Content
|
|
data-slot="dialog-content"
|
|
className={cn(
|
|
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/5 grid max-w-[calc(100%-2rem)] gap-6 rounded-4xl px-7 pt-8 pb-7 text-sm ring-1 duration-100 sm:max-w-md top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2",
|
|
position === "fixed" ? "fixed" : "absolute",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
{showCloseButton && (
|
|
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="absolute top-5 right-5"
|
|
size="icon-sm"
|
|
>
|
|
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
|
|
<span className="sr-only">Close</span>
|
|
</Button>
|
|
</DialogPrimitive.Close>
|
|
)}
|
|
</DialogPrimitive.Content>
|
|
</DialogPortal>
|
|
</DialogPortalContainerContext.Provider>
|
|
);
|
|
}
|
|
|
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-header"
|
|
className={cn("gap-2 flex flex-col", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DialogFooter({
|
|
className,
|
|
showCloseButton = false,
|
|
children,
|
|
...props
|
|
}: React.ComponentProps<"div"> & {
|
|
showCloseButton?: boolean;
|
|
}) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-footer"
|
|
className={cn(
|
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
{showCloseButton && (
|
|
<DialogPrimitive.Close asChild>
|
|
<Button variant="outline">Close</Button>
|
|
</DialogPrimitive.Close>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DialogTitle({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
return (
|
|
<DialogPrimitive.Title
|
|
data-slot="dialog-title"
|
|
className={cn("font-heading text-lg leading-none font-semibold", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DialogDescription({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
return (
|
|
<DialogPrimitive.Description
|
|
data-slot="dialog-description"
|
|
className={cn(
|
|
"text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogOverlay,
|
|
DialogPortal,
|
|
DialogPortalContainerContext,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
};
|