diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 3b2f043255..8e79b32f27 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -1108,6 +1108,11 @@ export function validateChatSearch(search: Record): ChatSearch }; } +type PendingHubAutoLoad = { + selection: SelectedModelInput; + contextKey: string; +}; + // `search` comes from RootLayout (not useSearch) so ChatPage stays mounted off-route // (keeping an in-flight generation alive), frozen to the last /chat search. `active` // is false off-route: close body-portaled surfaces and stop route-specific listeners @@ -1680,8 +1685,9 @@ export function ChatPage({ }, [activeThreadId, closeArtifactSurface, selectedArtifact, view]); const hasActiveModel = Boolean(inferenceParams.checkpoint); + const chatContextKey = `${view.mode}|${activeThreadId ?? ""}|${search.new ?? ""}|${search.project ?? ""}`; const [pendingHubAutoLoad, setPendingHubAutoLoad] = - useState(null); + useState(null); const stageOrLoad = useCallback( async (selection: SelectedModelInput) => { const store = useChatRuntimeStore.getState(); @@ -1733,7 +1739,7 @@ export function ChatPage({ hasGgufSource(selection) && !selection.isDownloaded); if (wantManagerStage) { - setPendingHubAutoLoad(selection); + setPendingHubAutoLoad({ selection, contextKey: chatContextKey }); return; } const previousConfig = currentRuntimePerModelConfig({ @@ -1748,24 +1754,30 @@ export function ChatPage({ previousConfig, }); }, - [selectModel, loadingModel, rememberedConfigFor], + [selectModel, loadingModel, rememberedConfigFor, chatContextKey], ); useRepoDownload({ kind: DOWNLOAD_KIND.MODEL, - repoId: pendingHubAutoLoad?.id ?? "__hub_autoload_idle__", - activeVariant: pendingHubAutoLoad?.ggufVariant ?? null, + repoId: pendingHubAutoLoad?.selection.id ?? "__hub_autoload_idle__", + activeVariant: pendingHubAutoLoad?.selection.ggufVariant ?? null, onComplete: (variant) => { const pending = pendingHubAutoLoad; - if (!pending || (pending.ggufVariant ?? null) !== (variant ?? null)) { + if ( + !pending || + (pending.selection.ggufVariant ?? null) !== (variant ?? null) + ) { return; } setPendingHubAutoLoad(null); - void stageOrLoad({ ...pending, isDownloaded: true }); + if (!active || pending.contextKey !== chatContextKey) { + return; + } + void stageOrLoad({ ...pending.selection, isDownloaded: true }); }, onError: (variant) => { if ( pendingHubAutoLoad && - (pendingHubAutoLoad.ggufVariant ?? null) === (variant ?? null) + (pendingHubAutoLoad.selection.ggufVariant ?? null) === (variant ?? null) ) { setPendingHubAutoLoad(null); } @@ -1773,7 +1785,7 @@ export function ChatPage({ onCancelled: (variant) => { if ( pendingHubAutoLoad && - (pendingHubAutoLoad.ggufVariant ?? null) === (variant ?? null) + (pendingHubAutoLoad.selection.ggufVariant ?? null) === (variant ?? null) ) { setPendingHubAutoLoad(null); } @@ -1786,9 +1798,9 @@ export function ChatPage({ void (async () => { const outcome = await downloadManager.requestStart({ kind: DOWNLOAD_KIND.MODEL, - repoId: pending.id, - variant: pending.ggufVariant ?? null, - expectedBytes: pending.expectedBytes ?? 0, + repoId: pending.selection.id, + variant: pending.selection.ggufVariant ?? null, + expectedBytes: pending.selection.expectedBytes ?? 0, }); if (!active) return; if (outcome === "started") { diff --git a/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx b/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx index bfe4181e92..16ab776e61 100644 --- a/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx +++ b/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx @@ -12,7 +12,7 @@ import { } from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; import { Textarea } from "@/components/ui/textarea"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { validateChatTemplate } from "../api/templates"; import { MAX_CHAT_TEMPLATE_BYTES, @@ -39,27 +39,26 @@ export function ChatTemplateEditorDialog({ onSave, readOnly = false, }: ChatTemplateEditorDialogProps) { - const [draft, setDraft] = useState(""); + const [draft, setDraft] = useState(null); const [error, setError] = useState(null); const [validating, setValidating] = useState(false); - const initialDraft = value ?? defaultTemplate ?? ""; - const renderedDraft = useMemo( - () => - open && value == null && defaultTemplate != null && draft.length === 0 - ? defaultTemplate - : draft, - [open, value, defaultTemplate, draft], - ); + const renderedDraft = draft ?? value ?? defaultTemplate ?? ""; const byteLength = chatTemplateByteLength(renderedDraft); const overLimit = !isChatTemplateWithinLimit(renderedDraft); const matchesDefault = defaultTemplate != null && renderedDraft === defaultTemplate; + const handleClose = () => { + setDraft(null); + setError(null); + onOpenChange(false); + }; + const handleSave = async () => { if (renderedDraft.trim().length === 0 || matchesDefault) { onSave(null); - onOpenChange(false); + handleClose(); return; } if (overLimit) { @@ -74,7 +73,7 @@ export function ChatTemplateEditorDialog({ return; } onSave(renderedDraft); - onOpenChange(false); + handleClose(); } catch { setError("Could not validate the template."); } finally { @@ -87,10 +86,10 @@ export function ChatTemplateEditorDialog({ open={open} onOpenChange={(nextOpen) => { if (nextOpen) { - setDraft(initialDraft); - setError(null); + onOpenChange(true); + return; } - onOpenChange(nextOpen); + handleClose(); }} > @@ -135,7 +134,7 @@ export function ChatTemplateEditorDialog({ {readOnly ? (
-
@@ -157,11 +156,7 @@ export function ChatTemplateEditorDialog({ )}
-