Studio: polish model load toast styling (#5648)

* fix: toast cancel and style

* fix: align model load toast Cancel and dismiss on the right

* fix: show short cased model name in loaded toast and removed prefix org

* revert: chat load toast refactor to visual-only changes

* fix: align model load toast close button

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: guard empty toast label and dedupe toast padding CSS

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>
Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
This commit is contained in:
Lee Jackson 2026-06-02 07:40:56 +01:00 committed by GitHub
commit a05944ab75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 112 additions and 78 deletions

View file

@ -65,13 +65,12 @@ const Toaster = ({ ...props }: ToasterProps) => {
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
// Pin close button to the top-right corner inside the toast.
// Overrides sonner's default left placement and outside-corner
// translate; top offset is set via a rule in index.css since sonner
// hardcodes `top: 0` (not a CSS variable).
"--toast-close-button-start": "unset",
"--toast-close-button-end": "8px",
"--toast-close-button-transform": "none",
// Pin the close button inside the toast's top-right corner.
// Sonner defaults to the left/outside edge, so keep the horizontal
// override here and the top offset in index.css.
"--toast-close-button-start": "unset",
"--toast-close-button-end": "8px",
"--toast-close-button-transform": "none",
} as React.CSSProperties
}
// No swipe gestures; keeps toast text selectable.

View file

@ -10,7 +10,6 @@ type ModelLoadDescriptionProps = {
message?: string | null;
progressPercent?: number | null;
progressLabel?: string | null;
onStop?: () => void;
};
function clampProgress(value: number): number {
@ -45,7 +44,6 @@ export function ModelLoadDescription({
message,
progressPercent,
progressLabel,
onStop,
}: ModelLoadDescriptionProps) {
const hasProgress = typeof progressPercent === "number";
// Split once at the top of the render so the JSX below stays flat --
@ -58,7 +56,7 @@ export function ModelLoadDescription({
<div className="flex h-full shrink-0 items-center self-center">
<Spinner className="size-3.5 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1 pr-5">
<div className="min-w-0 flex-1">
{title ? <p className="text-foreground leading-5 font-semibold">{title}</p> : null}
{hasProgress ? (
<div className="w-full pt-1">
@ -82,18 +80,6 @@ export function ModelLoadDescription({
<p className="pt-1 text-xs leading-relaxed text-muted-foreground">{message}</p>
) : null}
</div>
{onStop ? (
<Button
type="button"
size="xs"
variant="ghost"
aria-label="Stop model loading"
className="h-auto self-stretch shrink-0 !rounded-none !border-0 bg-transparent px-1 text-[10px] text-muted-foreground hover:bg-transparent hover:text-destructive focus-visible:text-destructive"
onClick={onStop}
>
Cancel
</Button>
) : null}
</div>
);
}

View file

@ -56,10 +56,16 @@ type SelectedModelInput = {
};
const MODEL_LOAD_TOAST_CLASSNAMES = {
toast: "items-start gap-2.5",
toast: "chat-model-load-toast items-center gap-2.5",
content: "gap-0.5 flex-1 min-w-0",
title: "leading-5",
description: "mt-0 w-full",
cancelButton:
"!h-auto !rounded-none !border-0 !bg-transparent !px-1 !text-[11px] !font-normal !text-muted-foreground hover:!bg-transparent hover:!text-destructive focus-visible:!text-destructive",
} as const;
const MODEL_LOADED_TOAST_CLASSNAMES = {
toast: "chat-model-loaded-toast items-center gap-2.5",
} as const;
const LORA_SUFFIX_RE = /_(\d{9,})$/;
@ -78,6 +84,12 @@ function stripTrailingEpoch(input: string): string {
return cleaned || input;
}
function shortModelLabel(idOrName: string): string {
const slash = idOrName.lastIndexOf("/");
const label = slash >= 0 ? idOrName.slice(slash + 1) : idOrName;
return label || idOrName;
}
function describeModel(model: {
is_lora?: boolean;
is_vision?: boolean;
@ -233,14 +245,12 @@ export function useChatModelRuntime() {
message: string,
progressPercent?: number | null,
progressLabel?: string | null,
onStop?: () => void,
) =>
createElement(ModelLoadDescription, {
title,
message,
progressPercent,
progressLabel,
onStop,
}),
[],
);
@ -471,10 +481,11 @@ export function useChatModelRuntime() {
const isLora =
explicitIsLora ?? model?.isLora ?? loraIsAdapter ?? false;
const displayName = model?.name || lora?.name || modelId;
const toastDisplayName = shortModelLabel(displayName);
const loadAttemptId = ++loadAttemptRef.current;
primeNativeNotificationPermission().catch(() => undefined);
const notificationModelKey = `${modelId}:${ggufVariant ?? ""}:${loadAttemptId}`;
const safeModelName = safeNotificationLabel(displayName, "The model");
const safeModelName = safeNotificationLabel(toastDisplayName, "The model");
const currentCheckpoint =
useChatRuntimeStore.getState().params.checkpoint;
const previousCheckpoint = currentCheckpoint;
@ -794,25 +805,32 @@ export function useChatModelRuntime() {
const isCachedLoad = isDownloaded || isCachedLora;
const toastTitle = isCachedLoad ? "Starting model…" : "Downloading model…";
const modelLoadToastOptions = (description: ReturnType<typeof renderLoadDescription>) => ({
description,
duration: Infinity,
closeButton: true,
cancel: {
label: "Cancel",
onClick: cancelLoading,
},
classNames: MODEL_LOAD_TOAST_CLASSNAMES,
onDismiss: (dismissedToast: { id: string | number }) => {
if (loadToastIdRef.current !== dismissedToast.id) {
return;
}
setLoadToastDismissedState(true);
},
});
const toastId = toast(
null,
{
description: renderLoadDescription(
modelLoadToastOptions(
renderLoadDescription(
toastTitle,
loadingDescription,
isCachedLoad ? null : 0,
isCachedLoad ? null : "Preparing download",
cancelLoading,
),
duration: Infinity,
classNames: MODEL_LOAD_TOAST_CLASSNAMES,
onDismiss: (dismissedToast) => {
if (loadToastIdRef.current !== dismissedToast.id) {
return;
}
setLoadToastDismissedState(true);
},
},
),
);
loadToastIdRef.current = toastId;
@ -919,19 +937,14 @@ export function useChatModelRuntime() {
if (loadToastDismissedRef.current) return;
toast(null, {
id: toastId,
description: renderLoadDescription(
"Downloading model…",
loadingDescription,
pct,
progressLabel,
cancelLoading,
...modelLoadToastOptions(
renderLoadDescription(
"Downloading model…",
loadingDescription,
pct,
progressLabel,
),
),
duration: Infinity,
classNames: MODEL_LOAD_TOAST_CLASSNAMES,
onDismiss: (dismissedToast) => {
if (loadToastIdRef.current !== dismissedToast.id) return;
setLoadToastDismissedState(true);
},
});
} else if (
prog.downloaded_bytes > 0 &&
@ -958,19 +971,14 @@ export function useChatModelRuntime() {
if (!loadToastDismissedRef.current) {
toast(null, {
id: toastId,
description: renderLoadDescription(
"Starting model…",
"Download complete. Loading the model into memory.",
100,
"Download complete",
cancelLoading,
...modelLoadToastOptions(
renderLoadDescription(
"Starting model…",
"Download complete. Loading the model into memory.",
100,
"Download complete",
),
),
duration: Infinity,
classNames: MODEL_LOAD_TOAST_CLASSNAMES,
onDismiss: (dismissedToast) => {
if (loadToastIdRef.current !== dismissedToast.id) return;
setLoadToastDismissedState(true);
},
});
}
notifyNative({
@ -1020,19 +1028,14 @@ export function useChatModelRuntime() {
if (loadToastDismissedRef.current) return;
toast(null, {
id: toastId,
description: renderLoadDescription(
"Starting model…",
"Paging weights into memory.",
pct,
label,
cancelLoading,
...modelLoadToastOptions(
renderLoadDescription(
"Starting model…",
"Paging weights into memory.",
pct,
label,
),
),
duration: Infinity,
classNames: MODEL_LOAD_TOAST_CLASSNAMES,
onDismiss: (dismissedToast) => {
if (loadToastIdRef.current !== dismissedToast.id) return;
setLoadToastDismissedState(true);
},
});
} catch {
// Ignore polling errors.
@ -1054,12 +1057,20 @@ export function useChatModelRuntime() {
try {
await performLoad();
if (loadToastDismissedRef.current) {
toast.success(`${displayName} loaded`);
toast.success(`${toastDisplayName} loaded`, {
classNames: MODEL_LOADED_TOAST_CLASSNAMES,
closeButton: true,
duration: 8000,
});
} else {
toast.success(`${displayName} loaded`, {
toast.success(`${toastDisplayName} loaded`, {
id: toastId,
description: undefined,
cancel: undefined,
classNames: MODEL_LOADED_TOAST_CLASSNAMES,
closeButton: true,
duration: 8000,
onDismiss: undefined,
});
}
notifyNative({
@ -1078,7 +1089,11 @@ export function useChatModelRuntime() {
toast.error(message, {
id: toastId,
description: undefined,
cancel: undefined,
classNames: undefined,
closeButton: true,
duration: 8000,
onDismiss: undefined,
});
}
notifyNative({

View file

@ -1225,10 +1225,42 @@
/* Lighter shadow + tighter vertical padding than Sonner's defaults; !important because Sonner injects its base rules at runtime. */
[data-sonner-toast][data-styled='true'] {
padding: 10px 16px !important;
padding: 12px 18px !important;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08) !important;
}
[data-sonner-toast][data-styled='true']:has([data-close-button]):not(:has([data-cancel])) {
padding-right: 48px !important;
}
[data-sonner-toast][data-styled='true']:has([data-cancel]):has([data-close-button]) {
padding-right: 88px !important;
}
[data-sonner-toast][data-styled='true'].chat-model-load-toast,
[data-sonner-toast][data-styled='true'].chat-model-loaded-toast {
padding-top: 14px !important;
padding-bottom: 14px !important;
}
[data-sonner-toast][data-styled='true'].chat-model-loaded-toast [data-close-button] {
top: calc(50% - 0.25px) !important;
transform: translateY(-50%) !important;
}
[data-sonner-toast][data-styled='true'].chat-model-load-toast:not(:has([data-cancel])) [data-close-button] {
top: calc(50% - 0.25px) !important;
transform: translateY(-50%) !important;
}
[data-sonner-toast][data-styled="true"]:has([data-cancel]) [data-cancel] {
position: absolute !important;
right: 36px !important;
top: 50% !important;
transform: translateY(-50%) !important;
margin: 0 !important;
}
/* Boost shadow on dark surfaces; mirrors .shadow-border / .menu-soft-surface pattern. */
.dark [data-sonner-toast][data-styled='true'] {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important;
@ -1391,13 +1423,15 @@
mix-blend-mode: normal;
}
/* Override sonner top: 0 and pin to theme tokens (--gray2 hover ignores data-sonner-theme). */
/* Keep Sonner close button inside the toast and pin to theme tokens (--gray2 hover ignores data-sonner-theme). */
[data-sonner-toast][data-styled="true"] [data-close-button] {
top: 8px !important;
transform: none !important;
background: var(--popover) !important;
color: var(--popover-foreground) !important;
border-color: var(--border) !important;
}
[data-sonner-toast][data-styled="true"] [data-close-button] svg {
stroke-width: 2.25;
}