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:
parent
07f0eddb0b
commit
a05944ab75
4 changed files with 112 additions and 78 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue