Studio: keep automatic model loading toast visible until completion (#7425)
This commit is contained in:
parent
36e83de336
commit
ba512f69e4
5 changed files with 125 additions and 31 deletions
|
|
@ -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={
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue