Studio: keep automatic model loading toast visible until completion (#7425)

This commit is contained in:
oobabooga 2026-07-28 00:16:28 -03:00 committed by GitHub
commit ba512f69e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 125 additions and 31 deletions

View file

@ -8,8 +8,8 @@ import {
MultiplicationSignCircleIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Spinner } from "@/components/ui/spinner";
import { useTheme } from "@/features/settings/stores/theme-store";
import { createLoadingToastIcon } from "@/lib/toast";
import { Toaster as Sonner, type ToasterProps } from "sonner";
// Make toast text selectable. Sonner's onPointerDown calls setPointerCapture(),
@ -78,7 +78,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
/>
),
// App-wide arc spinner so loading toasts match the "Downloading model" toast.
loading: <Spinner className="size-4 text-muted-foreground" />,
loading: createLoadingToastIcon(),
}}
style={
{

View file

@ -6,7 +6,7 @@ import { resolveInitialConfig } from "@/features/model-picker";
import { projectHasSources } from "@/features/rag/api/rag-api";
import { apiUrl } from "@/lib/api-base";
import { parseParamCountB } from "@/lib/model-size";
import { toast } from "@/lib/toast";
import { createLoadingToastIcon, toast } from "@/lib/toast";
import type { MessageTiming, ToolCallMessagePart } from "@assistant-ui/core";
import type { ChatModelAdapter } from "@assistant-ui/react";
import { parsePartialJsonObject } from "assistant-stream/utils";
@ -1512,13 +1512,38 @@ async function autoLoadSmallestModel(): Promise<{
const trustRemoteCode = store.params.trustRemoteCode ?? false;
const specSettings = resolveSpeculativeSettingsForLoad();
const lastLoaded = readLastLocalModelLoad();
const toastId = toast("Loading a model…", {
let autoLoadToastDismissed = false;
const toastId = toast.message("Loading a model…", {
description: lastLoaded
? "Loading last used model."
: "Auto-selecting the smallest downloaded model.",
duration: 5000,
duration: Number.POSITIVE_INFINITY,
closeButton: true,
icon: createLoadingToastIcon(),
onDismiss: () => {
autoLoadToastDismissed = true;
},
});
const updateAutoLoadToast = (message: string, description: string): void => {
if (autoLoadToastDismissed) return;
toast.message(message, {
id: toastId,
description,
duration: Number.POSITIVE_INFINITY,
});
};
const showAutoLoadSuccess = (message: string): void => {
const options = {
description: undefined,
duration: 5000,
icon: undefined,
};
if (autoLoadToastDismissed) {
toast.success(message, options);
return;
}
toast.success(message, { ...options, id: toastId });
};
let blockedByTrustRemoteCode = false;
let hadNonTrustFailure = false;
let loadAttempts = 0;
@ -1774,7 +1799,7 @@ async function autoLoadSmallestModel(): Promise<{
ggufVariant: candidate.ggufVariant,
});
}
toast.success(candidate.successLabel, { id: toastId });
showAutoLoadSuccess(candidate.successLabel);
return true;
}
try {
@ -1800,11 +1825,10 @@ async function autoLoadSmallestModel(): Promise<{
isAutoLoadableGgufVariant(entry),
);
if (variant) {
toast("Loading last used model…", {
id: toastId,
description: `${repo.repo_id} (${variant.quant})`,
duration: 5000,
});
updateAutoLoadToast(
"Loading last used model…",
`${repo.repo_id} (${variant.quant})`,
);
if (
await loadAutoLoadCandidate({
id: repo.repo_id,
@ -1829,11 +1853,7 @@ async function autoLoadSmallestModel(): Promise<{
const repo = findCachedRepo(modelRepos, lastLoaded.id);
if (repo) {
try {
toast("Loading last used model…", {
id: toastId,
description: repo.repo_id,
duration: 5000,
});
updateAutoLoadToast("Loading last used model…", repo.repo_id);
if (
await loadAutoLoadCandidate({
id: repo.repo_id,
@ -1854,11 +1874,10 @@ async function autoLoadSmallestModel(): Promise<{
}
}
}
toast("Loading a model…", {
id: toastId,
description: "Auto-selecting the smallest downloaded model.",
duration: 5000,
});
updateAutoLoadToast(
"Loading a model…",
"Auto-selecting the smallest downloaded model.",
);
}
// GGUF first: smallest-total-size repo, then its smallest variant.
@ -1949,12 +1968,10 @@ async function autoLoadSmallestModel(): Promise<{
}
// No cached models — try downloading a small default GGUF.
toast("Downloading a small model…", {
id: toastId,
description:
"No downloaded models found. Fetching Qwen3.5-4B-MTP (UD-Q4_K_XL).",
duration: 30000,
});
updateAutoLoadToast(
"Downloading a small model…",
"No downloaded models found. Fetching Qwen3.5-4B-MTP (UD-Q4_K_XL).",
);
try {
const rt = useChatRuntimeStore.getState();
if (
@ -2050,7 +2067,7 @@ async function autoLoadSmallestModel(): Promise<{
kind: "gguf",
ggufVariant: "UD-Q4_K_XL",
});
toast.success("Loaded Qwen3.5-4B-MTP (UD-Q4_K_XL)", { id: toastId });
showAutoLoadSuccess("Loaded Qwen3.5-4B-MTP (UD-Q4_K_XL)");
return { loaded: true, blockedByTrustRemoteCode: false };
} catch {
toast.dismiss(toastId);

View file

@ -2,7 +2,7 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { getInferenceStatus, loadModel } from "@/features/chat";
import { toast } from "@/lib/toast";
import { createLoadingToastIcon, toast } from "@/lib/toast";
import { toastError } from "@/shared/toast";
import { useCallback, useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
@ -238,8 +238,15 @@ async function loadLocalModelSelection(
): Promise<string | null> {
const { target, ggufVariant } = selection;
const modelLabel = ggufVariant ? `${target} (${ggufVariant})` : target;
const toastId = toast.loading(`Loading ${modelLabel}...`, {
let loadToastDismissed = false;
const toastId = toast.message(`Loading ${modelLabel}...`, {
description: "Starting the local inference server for this recipe.",
duration: Number.POSITIVE_INFINITY,
closeButton: true,
icon: createLoadingToastIcon(),
onDismiss: () => {
loadToastDismissed = true;
},
});
try {
const isGguf = GGUF_MODEL_PATTERN.test(target) || Boolean(ggufVariant);
@ -267,7 +274,16 @@ async function loadLocalModelSelection(
// biome-ignore lint/style/useNamingConvention: api schema
tensor_parallel: false,
});
toast.success(`Loaded ${modelLabel}`, { id: toastId, duration: 2000 });
const successOptions = {
description: undefined,
duration: 2000,
icon: undefined,
};
if (loadToastDismissed) {
toast.success(`Loaded ${modelLabel}`, successOptions);
} else {
toast.success(`Loaded ${modelLabel}`, { ...successOptions, id: toastId });
}
return null;
} catch (error) {
toast.dismiss(toastId);

View file

@ -4,5 +4,15 @@
// Re-export of sonner. Swipe blocking lives on the Toaster via
// `swipeDirections={[]}`, so no per-toast dismissible override.
import { Spinner } from "@/components/ui/spinner";
import { createElement } from "react";
function createLoadingToastIcon() {
return createElement(Spinner, {
className: "size-4 text-muted-foreground",
});
}
export { toast } from "sonner";
export type { ExternalToast } from "sonner";
export { createLoadingToastIcon };