diff --git a/studio/frontend/src/features/chat/index.ts b/studio/frontend/src/features/chat/index.ts index bfaf29f519..dde3a75475 100644 --- a/studio/frontend/src/features/chat/index.ts +++ b/studio/frontend/src/features/chat/index.ts @@ -19,6 +19,7 @@ export { export { useChatRuntimeStore } from "./stores/chat-runtime-store"; export { useChatSearchStore } from "./stores/chat-search-store"; export { useChatModelRuntime } from "./hooks/use-chat-model-runtime"; +export { isExternalModelId } from "./external-providers"; export { ChatSearchDialog } from "./components/chat-search-dialog"; export { setTrainingCompareHandoff } from "./lib/training-compare-handoff"; export type { ProjectRecord } from "./types"; diff --git a/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx b/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx index 72743af722..e0178c3472 100644 --- a/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx +++ b/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx @@ -20,7 +20,7 @@ import { import { type GgufVariantDetail, deleteCachedModel } from "../inventory"; import { formatBytes } from "../lib/format"; import { type GgufFitClass, classifyGgufFit } from "../lib/gguf-fit"; -import { HUB_POST_DOWNLOAD_ACTIONS_VISIBLE } from "../lib/hub-feature-flags"; +import { HUB_GGUF_RUN_ACTIONS_VISIBLE } from "../lib/hub-feature-flags"; import { ggufVariantsMatch, normalizeGgufVariantIdentity, @@ -794,8 +794,7 @@ export function GgufDownloadCard({ downloadingThisVariant && !cancelling && "hover:bg-rose-500/10 hover:text-rose-600 dark:hover:text-rose-400", - // Hide post-download CTAs (Run / New Chat) for this PR. - !HUB_POST_DOWNLOAD_ACTIONS_VISIBLE && + !HUB_GGUF_RUN_ACTIONS_VISIBLE && !downloadingThisVariant && !cancelling && !isLoadingThisModel && diff --git a/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx b/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx index 9b3d401b99..0dd50f0a41 100644 --- a/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx +++ b/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx @@ -2,7 +2,10 @@ // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 import { TrainIcon } from "../components/train-icon"; -import { HUB_POST_DOWNLOAD_ACTIONS_VISIBLE } from "../lib/hub-feature-flags"; +import { + HUB_GGUF_RUN_ACTIONS_VISIBLE, + HUB_POST_DOWNLOAD_ACTIONS_VISIBLE, +} from "../lib/hub-feature-flags"; import { Popover, PopoverContent, @@ -295,6 +298,9 @@ export function LocalOnDeviceCard({ const formatTone = modelFormat === "adapter" ? "adapter" : isGguf ? "gguf" : "checkpoint"; const showOldCacheHint = source === "hf_cache" && !!unsupportedReason; + const runActionsVisible = isGguf + ? HUB_GGUF_RUN_ACTIONS_VISIBLE + : HUB_POST_DOWNLOAD_ACTIONS_VISIBLE; return (
@@ -440,11 +446,10 @@ export function LocalOnDeviceCard({ className="ml-1 mr-0 h-5 w-px shrink-0 bg-foreground/[0.06] opacity-100 transition-opacity duration-150 group-hover/dl:opacity-0 dark:bg-white/[0.04]" /> )} - {/* Run + Train pair hidden until Hub->chat/train pickers ship. */}
{onTrain && ( diff --git a/studio/frontend/src/features/hub/catalog/model-inspector.tsx b/studio/frontend/src/features/hub/catalog/model-inspector.tsx index 54165aa5e6..a3a73f2714 100644 --- a/studio/frontend/src/features/hub/catalog/model-inspector.tsx +++ b/studio/frontend/src/features/hub/catalog/model-inspector.tsx @@ -725,8 +725,6 @@ export const ModelInspector = memo(function ModelInspector({ /> )}
- - ); }); diff --git a/studio/frontend/src/features/hub/hub-page.tsx b/studio/frontend/src/features/hub/hub-page.tsx index 5b7fc4ad77..7324bed262 100644 --- a/studio/frontend/src/features/hub/hub-page.tsx +++ b/studio/frontend/src/features/hub/hub-page.tsx @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -// Hub page: browse + download surface only. Train CTA hidden until Hub->train picker ships. import { useHubInventory } from "@/features/hub/inventory"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useGpuInfo } from "@/hooks/use-gpu-info"; @@ -12,9 +11,15 @@ import { import { useOnlineStatus } from "@/features/hub/hooks/use-online-status"; import { useIsHubDesktop } from "@/features/hub/hooks/use-is-hub-desktop"; import { useHubInfiniteScroll } from "@/features/hub/hooks/use-hub-infinite-scroll"; -import { modelIdsMatch } from "@/features/hub/lib/model-identity"; +import { ggufVariantsMatch, modelIdsMatch } from "@/features/hub/lib/model-identity"; import { cn } from "@/lib/utils"; import { useHfTokenStore } from "@/features/hub/stores/hf-token-store"; +import { + getInferenceStatus, + isExternalModelId, + useChatModelRuntime, + useChatRuntimeStore, +} from "@/features/chat"; import { ArrowLeft01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { useNavigate, useSearch } from "@tanstack/react-router"; @@ -179,14 +184,35 @@ export function ModelsPage() { const gpu = useGpuInfo(); const online = useOnlineStatus(); - // "Use in chat" just navigates to /chat; checkpoint/variant handoff lands with the Hub-aware picker. - const activeCheckpoint: string | null = null; - const activeGgufVariant: string | null = null; - // Stubs so the inspector + load-status memo still typecheck; cast keeps the shape from narrowing to `never`. - const loadingModel = null as { id: string } | null; - const loadProgress = null as { - phase?: "downloading" | "starting"; - } | null; + const { selectModel, loadingModel, loadProgress, ejectModel } = + useChatModelRuntime(); + const checkpoint = useChatRuntimeStore((s) => s.params.checkpoint); + const activeCheckpoint = + checkpoint && !isExternalModelId(checkpoint) ? checkpoint : null; + const activeGgufVariant = useChatRuntimeStore((s) => s.activeGgufVariant); + + useEffect(() => { + let cancelled = false; + void getInferenceStatus() + .then((status) => { + if (cancelled || !status.active_model) return; + const store = useChatRuntimeStore.getState(); + if ( + !isExternalModelId(store.params.checkpoint) && + (!modelIdsMatch(store.params.checkpoint, status.active_model) || + !ggufVariantsMatch( + store.activeGgufVariant, + status.gguf_variant ?? null, + )) + ) { + store.setCheckpoint(status.active_model, status.gguf_variant ?? null); + } + }) + .catch(() => undefined); + return () => { + cancelled = true; + }; + }, []); const { tab, setTab: setModelsTab } = useModelsTabState(); const [query, setQuery] = useState(""); @@ -522,8 +548,8 @@ export function ModelsPage() { ); const isLoadingThisModel = useMemo(() => { - if (!loadingModel) return false; - return selectedRepoMatchesRuntime(selectedModel, loadingModel.id, null); + if (!loadingModel || !selectedModel) return false; + return modelIdsMatch(loadingModel.id, selectedModel.resource.runId); }, [loadingModel, selectedModel]); const { vramInfo, minMemory } = useHubModelVram(selectedModel, gpu); @@ -536,22 +562,41 @@ export function ModelsPage() { ? `${Math.floor(gpu.systemRamAvailableGb)} GB` : "Unavailable"; - // Load / use-in-chat / train handlers are stubs; they wire in once the - // Hub-aware chat and train pickers ship. + const openNewChat = useCallback(() => { + void navigate({ to: "/chat", search: { new: crypto.randomUUID() } }); + }, [navigate]); + const runSelectedModel = useCallback( + (opts: ModelLoadOptions, isDownloaded: boolean) => { + if (!selectedModel) return; + const runId = selectedModel.resource.runId; + void selectModel({ + id: runId, + ggufVariant: opts.ggufVariant, + isDownloaded, + expectedBytes: opts.expectedBytes, + throwOnError: true, + }) + .then(() => { + const store = useChatRuntimeStore.getState(); + if (!modelIdsMatch(store.params.checkpoint, runId)) { + store.setCheckpoint(runId, opts.ggufVariant ?? null); + } + }) + .catch(() => undefined); + openNewChat(); + }, + [openNewChat, selectModel, selectedModel], + ); const handleLoad = useCallback( - (_opts: ModelLoadOptions) => undefined, - [], + (opts: ModelLoadOptions) => + runSelectedModel(opts, selectedModel?.isDownloaded ?? true), + [runSelectedModel, selectedModel], ); const handleLoadLocal = useCallback( - (_opts: ModelLoadOptions = {}) => undefined, - [], + (opts: ModelLoadOptions = {}) => runSelectedModel(opts, true), + [runSelectedModel], ); - const handleUseInChat = useCallback(() => { - void navigate({ to: "/chat" }); - }, [navigate]); - const handleTrain = useCallback(() => { - // Train CTA hidden until Hub->train picker ships. - }, []); + const handleTrain = useCallback(() => undefined, []); const inspectorRuntime = useMemo( () => ({ @@ -582,14 +627,14 @@ export function ModelsPage() { () => ({ onLoad: handleLoad, onLoadLocal: handleLoadLocal, - onUseInChat: handleUseInChat, + onUseInChat: openNewChat, onTrain: handleTrain, onInventoryChange: refreshInventory, }), [ handleLoad, handleLoadLocal, - handleUseInChat, + openNewChat, handleTrain, refreshInventory, ], @@ -685,7 +730,7 @@ export function ModelsPage() { ramLabel={ramLabel} activeCheckpoint={activeCheckpoint} activeGgufVariant={activeGgufVariant} - onEject={() => undefined} + onEject={() => void ejectModel()} />
diff --git a/studio/frontend/src/features/hub/lib/hub-feature-flags.ts b/studio/frontend/src/features/hub/lib/hub-feature-flags.ts index cf28403725..d0d527b720 100644 --- a/studio/frontend/src/features/hub/lib/hub-feature-flags.ts +++ b/studio/frontend/src/features/hub/lib/hub-feature-flags.ts @@ -1,8 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -// Hub feature flags for staged rollout. Each gates a CTA whose integration ships -// in a later PR; JSX/wiring stay in place so flipping here is the only re-enable edit. - -// Post-download Run / chat / Train CTAs, hidden until the Hub-aware chat and train pickers ship. export const HUB_POST_DOWNLOAD_ACTIONS_VISIBLE = false; +export const HUB_GGUF_RUN_ACTIONS_VISIBLE = true;