Studio: quick eject from the model selector (#6654)
* Studio: quick eject from the model selector Add a one-click eject shortcut to the loaded-model pill so users do not have to open the picker to unload a model. - The loaded-status indicator shows a green checkmark at rest and swaps to a red eject icon on pill hover, with an "Eject model" tooltip. Clicking it ejects without opening the picker. - On Device tab now uses the placeholder "Search local models" instead of "Search Unsloth models". - The picker's "Eject model" button uses medium font weight. * Studio: drop unused group/eject marker class on the eject control * Studio: make the inline eject control valid HTML The eject shortcut was a focusable span (role/tabIndex) nested inside the trigger button. A button's content model forbids focusable descendants, so make it a plain decorative span (aria-hidden, no role/tabIndex) that keeps the mouse shortcut. Keyboard and screen-reader users eject via the picker's "Eject model" button. * Studio: disable the inline eject shortcut on touch devices On touch (no hover) the red eject icon and title tooltip never reveal, so tapping the loaded pill could unload the model with no visible affordance. Add [@media(hover:none)]:pointer-events-none so taps fall through to the trigger and open the picker; touch users eject from the picker instead. --------- Co-authored-by: shimmyshimmer <shimmyshimmer@users.noreply.github.com> Co-authored-by: Wasim Yousef Said <wasimysdev@gmail.com>
This commit is contained in:
parent
e8945cab46
commit
0a3e5a3172
2 changed files with 55 additions and 9 deletions
|
|
@ -14,6 +14,7 @@ import { isCustomProviderType } from "@/features/chat/external-providers";
|
|||
import { ChevronDownStandardIcon } from "@/lib/chevron-icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
CheckmarkCircle02Icon,
|
||||
CloudIcon,
|
||||
DashboardSquare01Icon,
|
||||
Download01Icon,
|
||||
|
|
@ -146,6 +147,7 @@ function ModelSelectorTrigger({
|
|||
size = "default",
|
||||
className,
|
||||
dataTour,
|
||||
onEject,
|
||||
}: {
|
||||
currentModel?: ModelOption;
|
||||
isLoaded: boolean;
|
||||
|
|
@ -154,6 +156,7 @@ function ModelSelectorTrigger({
|
|||
size?: "sm" | "default" | "lg";
|
||||
className?: string;
|
||||
dataTour?: string;
|
||||
onEject?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<PopoverTrigger asChild={true}>
|
||||
|
|
@ -161,12 +164,15 @@ function ModelSelectorTrigger({
|
|||
type="button"
|
||||
data-tour={dataTour}
|
||||
className={cn(
|
||||
"unsloth-model-selector-trigger flex min-w-0 items-center gap-2 transition-colors",
|
||||
"unsloth-model-selector-trigger group/trigger flex min-w-0 items-center gap-2 transition-colors",
|
||||
// Suppress the pill's hover background while the eject hit area is
|
||||
// hovered, so only the dot's own circle reacts.
|
||||
variant === "outline" &&
|
||||
"rounded-full border border-border/60 hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||
"rounded-full border border-border/60 hover:bg-[#ececec] has-[[data-eject-hit]:hover]:!bg-transparent dark:hover:bg-[#2d2e32]",
|
||||
variant === "ghost" &&
|
||||
"rounded-full hover:bg-[#ececec] dark:hover:bg-[#2d2e32]",
|
||||
variant === "muted" && "rounded-full bg-muted hover:bg-muted/80",
|
||||
"rounded-full hover:bg-[#ececec] has-[[data-eject-hit]:hover]:!bg-transparent dark:hover:bg-[#2d2e32]",
|
||||
variant === "muted" &&
|
||||
"rounded-full bg-muted hover:bg-muted/80 has-[[data-eject-hit]:hover]:!bg-muted",
|
||||
// More left padding than right; the chevron is pulled close to the
|
||||
// label (below) so the trigger reads balanced around the text.
|
||||
size === "sm" && "h-8 pl-3 pr-1.5 text-xs",
|
||||
|
|
@ -175,9 +181,44 @@ function ModelSelectorTrigger({
|
|||
className,
|
||||
)}
|
||||
>
|
||||
{isLoaded && (
|
||||
<span className="size-2 shrink-0 rounded-full bg-emerald-500" />
|
||||
)}
|
||||
{isLoaded &&
|
||||
(onEject ? (
|
||||
// Loaded status doubles as a mouse eject shortcut: green checkmark
|
||||
// at rest, red eject icon on pill hover, click to eject. A plain
|
||||
// span (no role/tabIndex) keeps it out of the trigger button's
|
||||
// content model, which forbids focusable descendants. Keyboard and
|
||||
// screen-reader users eject via the picker's "Eject model" button.
|
||||
// aria-hidden marks it decorative; stopPropagation stops the
|
||||
// popover from toggling. On touch (no hover) the eject icon and
|
||||
// tooltip never reveal, so pointer-events-none disables the
|
||||
// shortcut there and taps open the picker instead of ejecting.
|
||||
<span
|
||||
aria-hidden={true}
|
||||
title="Eject model"
|
||||
data-eject-hit={true}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onEject();
|
||||
}}
|
||||
// Hit area larger than the icon, with a hover circle. Negative
|
||||
// margin keeps the icon in the dot's original spot.
|
||||
className="-m-1 flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-black/10 dark:hover:bg-white/10 [@media(hover:none)]:pointer-events-none"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={CheckmarkCircle02Icon}
|
||||
strokeWidth={1.75}
|
||||
className="size-3.5 text-emerald-500 group-hover/trigger:hidden"
|
||||
/>
|
||||
<HugeiconsIcon
|
||||
icon={RemoveCircleIcon}
|
||||
strokeWidth={1.75}
|
||||
className="hidden size-3.5 text-red-500 group-hover/trigger:block"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span className="size-2 shrink-0 rounded-full bg-emerald-500" />
|
||||
))}
|
||||
{currentModel?.icon ? (
|
||||
<span className="flex shrink-0 items-center">
|
||||
{currentModel.icon}
|
||||
|
|
@ -644,6 +685,7 @@ export function ModelSelector({
|
|||
size={size}
|
||||
className={className}
|
||||
dataTour={triggerDataTour}
|
||||
onEject={onEject ? handleEject : undefined}
|
||||
/>
|
||||
<ModelSelectorContent
|
||||
open={open}
|
||||
|
|
|
|||
|
|
@ -2400,7 +2400,11 @@ export function HubModelPicker({
|
|||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search Unsloth models"
|
||||
placeholder={
|
||||
section === "downloaded"
|
||||
? "Search local models"
|
||||
: "Search Unsloth models"
|
||||
}
|
||||
data-model-picker-search-input={true}
|
||||
className="field-soft h-9 border-0 pl-8 pr-8"
|
||||
/>
|
||||
|
|
@ -3447,7 +3451,7 @@ export function HubModelPicker({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onEject}
|
||||
className="pointer-events-auto inline-flex items-center justify-center gap-2 rounded-md bg-popover px-3 py-2 text-[13px] text-destructive shadow-[0_2px_8px_-2px_rgba(0,0,0,0.16)] transition-colors hover:bg-[color-mix(in_srgb,var(--destructive)_12%,var(--popover))] dark:bg-[color-mix(in_srgb,var(--foreground)_10%,var(--sidebar))] dark:shadow-none dark:hover:bg-[color-mix(in_srgb,var(--destructive)_22%,var(--sidebar))]"
|
||||
className="pointer-events-auto inline-flex items-center justify-center gap-2 rounded-md bg-popover px-3 py-2 text-[13px] font-medium text-destructive shadow-[0_2px_8px_-2px_rgba(0,0,0,0.16)] transition-colors hover:bg-[color-mix(in_srgb,var(--destructive)_12%,var(--popover))] dark:bg-[color-mix(in_srgb,var(--foreground)_10%,var(--sidebar))] dark:shadow-none dark:hover:bg-[color-mix(in_srgb,var(--destructive)_22%,var(--sidebar))]"
|
||||
title="Eject model"
|
||||
>
|
||||
<HugeiconsIcon icon={RemoveCircleIcon} className="size-3.5" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue