Studio: make the model download folder reachable from the Hub, and findable in search (#7466)
* Studio: make the model download folder reachable from the Hub, and findable in search The only control for where models download lived in Settings > System > Storage, labelled "Model downloads". Settings search matched a row's visible label only, so "models folder", "directory", "path" and "drive" all returned nothing, and users concluded the location could not be changed at all. Hub > On-device locations now leads with a Download location row: current path, Change (folder browser on web, native picker on desktop), Use default, free space, and a note when HF_HOME pins it. That dialog is where people already look for where models live, but it only managed read-only scan folders. Changing the location refreshes the inventory. Settings search now also matches per-row keyword aliases, so "folder", "directory", "path", "location", "drive", "disk" and "cache" find the row. Relabels it "Models folder" and says it can be moved off the system drive. Adds the German strings for the block, which fell back to English. * Re-read the download location on every open, and drop it when the read fails The dialog stays mounted between opens, so a reopen that hit a failing or slow GET /api/settings/hugging-face-cache kept showing the previous path with Change and Use default still enabled, as though it had just been confirmed. The loaded flag is re-armed on each open and a failed read now clears the settings, so the field falls back to Unknown and both buttons disable until a read succeeds. * Let the inventory version bump be the only refresh after a cache move updateHuggingFaceCacheSettings already bumps the inventory version, which re-fetches every source. Calling onInventoryChange as well started a second round under the previous version, and the differing keys meant the two could not be deduplicated, so moving the folder scanned everything twice. The settings Resources tab already relies on the bump alone for the same call. --------- Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
This commit is contained in:
parent
f4f36a0d2d
commit
5b73c9c5b5
15 changed files with 227 additions and 10 deletions
|
|
@ -23,12 +23,21 @@ import {
|
|||
removeScanFolder,
|
||||
} from "@/features/hub";
|
||||
import { FolderBrowser } from "@/features/model-picker";
|
||||
import { openModelsDir } from "@/features/native-intents";
|
||||
import {
|
||||
openModelsDir,
|
||||
pickHuggingFaceCacheDir,
|
||||
} from "@/features/native-intents";
|
||||
import {
|
||||
type HuggingFaceCacheSettings,
|
||||
loadHuggingFaceCacheSettings,
|
||||
updateHuggingFaceCacheSettings,
|
||||
} from "@/features/settings";
|
||||
import { isTauri } from "@/lib/api-base";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Delete02Icon,
|
||||
DownloadCircle01Icon,
|
||||
FileSearchIcon,
|
||||
FolderAddIcon,
|
||||
FolderExportIcon,
|
||||
|
|
@ -49,6 +58,12 @@ function formatError(error: unknown): string {
|
|||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function formatFreeSpace(bytes: number | null): string | null {
|
||||
if (bytes === null || !Number.isFinite(bytes)) return null;
|
||||
const gb = bytes / 1024 ** 3;
|
||||
return gb >= 10 ? `${Math.round(gb)} GB free` : `${gb.toFixed(1)} GB free`;
|
||||
}
|
||||
|
||||
export function OnDeviceFoldersDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
|
|
@ -68,6 +83,11 @@ export function OnDeviceFoldersDialog({
|
|||
);
|
||||
const refreshIdRef = useRef(0);
|
||||
const mutationVersionRef = useRef(0);
|
||||
const [downloadCache, setDownloadCache] =
|
||||
useState<HuggingFaceCacheSettings | null>(null);
|
||||
const [downloadCacheLoaded, setDownloadCacheLoaded] = useState(false);
|
||||
const [downloadBrowserOpen, setDownloadBrowserOpen] = useState(false);
|
||||
const [downloadSaving, setDownloadSaving] = useState(false);
|
||||
|
||||
const sortedFolders = useMemo(
|
||||
() => [...folders].sort((a, b) => a.path.localeCompare(b.path)),
|
||||
|
|
@ -108,10 +128,66 @@ export function OnDeviceFoldersDialog({
|
|||
return () => window.clearTimeout(timer);
|
||||
}, [open, refreshFolders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
// The dialog stays mounted between opens, so re-arm the flag or a reopen
|
||||
// shows the previous answer as if it were fresh.
|
||||
setDownloadCacheLoaded(false);
|
||||
loadHuggingFaceCacheSettings()
|
||||
// Indexed locations do not depend on this. Null drops the stale path
|
||||
// rather than offer Change against a location we could not confirm.
|
||||
.catch(() => null)
|
||||
.then((settings) => {
|
||||
if (cancelled) return;
|
||||
setDownloadCache(settings);
|
||||
setDownloadCacheLoaded(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const handleInventoryChanged = useCallback(() => {
|
||||
onInventoryChange?.();
|
||||
}, [onInventoryChange]);
|
||||
|
||||
// Relocating the cache changes which repos are on disk, but
|
||||
// updateHuggingFaceCacheSettings already bumps the inventory version, which
|
||||
// re-fetches every source. Refreshing here too would scan twice, since the
|
||||
// two rounds carry different version keys and cannot be deduplicated.
|
||||
const saveDownloadLocation = useCallback(async (nextPath: string | null) => {
|
||||
setDownloadSaving(true);
|
||||
try {
|
||||
const settings = await updateHuggingFaceCacheSettings(nextPath);
|
||||
setDownloadCache(settings);
|
||||
toast.success("Download location updated", {
|
||||
description: settings.cacheHome,
|
||||
});
|
||||
} catch (err) {
|
||||
toast.error("Couldn't update the download location", {
|
||||
description: formatError(err),
|
||||
});
|
||||
} finally {
|
||||
setDownloadSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const changeDownloadLocation = useCallback(async () => {
|
||||
if (!isTauri) {
|
||||
setDownloadBrowserOpen(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const picked = await pickHuggingFaceCacheDir();
|
||||
if (picked) await saveDownloadLocation(picked);
|
||||
} catch (err) {
|
||||
toast.error("Couldn't open the folder picker", {
|
||||
description: formatError(err),
|
||||
});
|
||||
}
|
||||
}, [saveDownloadLocation]);
|
||||
|
||||
const handleAdd = useCallback(
|
||||
async (rawPath: string) => {
|
||||
const nextPath = rawPath.trim();
|
||||
|
|
@ -182,10 +258,10 @@ export function OnDeviceFoldersDialog({
|
|||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="gap-0 overflow-hidden p-0 sm:max-w-[620px] lg:max-w-[660px] xl:max-w-[680px] [&_[data-slot=dialog-close]]:right-3 [&_[data-slot=dialog-close]]:top-3"
|
||||
className="flex max-h-[90dvh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[620px] lg:max-w-[660px] xl:max-w-[680px] [&_[data-slot=dialog-close]]:right-3 [&_[data-slot=dialog-close]]:top-3"
|
||||
overlayClassName="bg-black/20 backdrop-blur-none"
|
||||
>
|
||||
<DialogHeader className="border-b border-border/60 px-5 py-4">
|
||||
<DialogHeader className="shrink-0 border-b border-border/60 px-5 py-4">
|
||||
<DialogTitle className="text-ui-15">
|
||||
On-device locations
|
||||
</DialogTitle>
|
||||
|
|
@ -195,7 +271,78 @@ export function OnDeviceFoldersDialog({
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 px-5 py-4">
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-5 py-4">
|
||||
<div className="rounded-[14px] border border-border/70 bg-muted/20 p-3">
|
||||
<div className="mb-2 flex items-center gap-2 text-ui-12 font-medium text-foreground">
|
||||
<HugeiconsIcon
|
||||
icon={DownloadCircle01Icon}
|
||||
strokeWidth={1.75}
|
||||
className="size-3.5 text-muted-foreground"
|
||||
/>
|
||||
Download location
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Input
|
||||
readOnly={true}
|
||||
aria-label="Model download location"
|
||||
value={
|
||||
downloadCache?.cacheHome ??
|
||||
(downloadCacheLoaded ? "Unknown" : "Loading...")
|
||||
}
|
||||
title={downloadCache?.cacheHome}
|
||||
className="field-soft h-9 min-w-0 flex-1 rounded-full px-3 font-mono text-ui-12"
|
||||
/>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void changeDownloadLocation()}
|
||||
disabled={!downloadCache?.editable || downloadSaving}
|
||||
className="h-9 rounded-full px-3 text-ui-12p5"
|
||||
>
|
||||
{downloadSaving ? (
|
||||
<Spinner className="size-3.5" />
|
||||
) : (
|
||||
<HugeiconsIcon
|
||||
icon={FolderSearchIcon}
|
||||
strokeWidth={1.75}
|
||||
data-icon="inline-start"
|
||||
className="size-3.5"
|
||||
/>
|
||||
)}
|
||||
Change
|
||||
</Button>
|
||||
{downloadCache?.isCustom ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => void saveDownloadLocation(null)}
|
||||
disabled={downloadSaving}
|
||||
className="h-9 rounded-full px-3 text-ui-12p5 text-muted-foreground"
|
||||
>
|
||||
Use default
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-ui-10p5 text-muted-foreground">
|
||||
{downloadCache?.source === "environment"
|
||||
? `Managed by the ${
|
||||
downloadCache.environmentVariable ?? "HF_HOME"
|
||||
} environment variable.`
|
||||
: [
|
||||
"New downloads only. Models already on disk stay where they are.",
|
||||
formatFreeSpace(downloadCache?.freeBytes ?? null),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[14px] border border-border/70 bg-muted/20 p-3">
|
||||
<div className="mb-2 flex items-center gap-2 text-ui-12 font-medium text-foreground">
|
||||
<HugeiconsIcon
|
||||
|
|
@ -425,6 +572,16 @@ export function OnDeviceFoldersDialog({
|
|||
onOpenChange={setBrowserOpen}
|
||||
onSelect={(selectedPath) => void handleAdd(selectedPath)}
|
||||
/>
|
||||
|
||||
<FolderBrowser
|
||||
open={!isTauri && downloadBrowserOpen}
|
||||
onOpenChange={setDownloadBrowserOpen}
|
||||
onSelect={(selectedPath) => void saveDownloadLocation(selectedPath)}
|
||||
initialPath={downloadCache?.cacheHome}
|
||||
title="Choose model download location"
|
||||
confirmLabel="Use for future downloads"
|
||||
showModelHints={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
|
||||
export { SettingsDialog } from "./settings-dialog";
|
||||
export { loadEmbeddingModelSettings } from "./api/embedding-model";
|
||||
export {
|
||||
loadHuggingFaceCacheSettings,
|
||||
updateHuggingFaceCacheSettings,
|
||||
} from "./api/hugging-face-cache";
|
||||
export type { HuggingFaceCacheSettings } from "./api/hugging-face-cache";
|
||||
export {
|
||||
loadPersonalization,
|
||||
savePersonalization,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { SETTINGS_SEARCH_INDEX } from "./settings-search";
|
||||
import {
|
||||
SETTINGS_SEARCH_INDEX,
|
||||
SETTINGS_SEARCH_KEYWORDS,
|
||||
} from "./settings-search";
|
||||
import {
|
||||
type SettingsTab,
|
||||
useSettingsDialogStore,
|
||||
|
|
@ -157,8 +160,12 @@ export function SettingsDialog() {
|
|||
return TABS.map((tab) => {
|
||||
const tabLabel = t(tab.labelKey);
|
||||
const entries = SETTINGS_SEARCH_INDEX[tab.id]
|
||||
.map((key) => t(key))
|
||||
.filter((label) => label.toLowerCase().includes(q));
|
||||
.filter((key) => {
|
||||
if (t(key).toLowerCase().includes(q)) return true;
|
||||
const keywordsKey = SETTINGS_SEARCH_KEYWORDS[key];
|
||||
return keywordsKey ? t(keywordsKey).toLowerCase().includes(q) : false;
|
||||
})
|
||||
.map((key) => t(key));
|
||||
const deduped = [...new Set(entries)];
|
||||
return {
|
||||
tab,
|
||||
|
|
|
|||
|
|
@ -146,3 +146,15 @@ export const SETTINGS_SEARCH_INDEX: Record<SettingsTab, TranslationKey[]> = {
|
|||
"settings.about.shutDownStudio",
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Extra terms a row matches on, beyond its own label. The value is a
|
||||
* translation key holding space-separated synonyms; it is never rendered.
|
||||
* Search matched labels only, so "models folder" or "directory" found nothing.
|
||||
*/
|
||||
export const SETTINGS_SEARCH_KEYWORDS: Partial<
|
||||
Record<TranslationKey, TranslationKey>
|
||||
> = {
|
||||
"settings.resources.storage.modelsFolder":
|
||||
"settings.resources.storage.modelsFolderKeywords",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -317,6 +317,8 @@ export const ar = {
|
|||
diskUsage: "{used} مستخدم / {total}",
|
||||
diskFree: "{free} متاح",
|
||||
modelsFolder: "مجلد النماذج",
|
||||
modelsFolderKeywords:
|
||||
"النماذج مجلد دليل مسار موقع تنزيلات التنزيل ذاكرة التخزين المؤقت تخزين قرص محرك نقل تغيير models folder path hugging face",
|
||||
modelsFolderDescription: "المكان الذي تُخزَّن فيه النماذج المُنزَّلة.",
|
||||
openAction: "فتح",
|
||||
copyAction: "نسخ المسار",
|
||||
|
|
|
|||
|
|
@ -330,9 +330,23 @@ export const de = {
|
|||
diskFree: "{free} frei",
|
||||
modelsFolder: "Modell-Ordner",
|
||||
modelsFolderDescription:
|
||||
"Wo heruntergeladene Modelle gespeichert werden.",
|
||||
"Wo heruntergeladene Modelle gespeichert werden. Ändern Sie ihn, um Modelle nicht auf dem Systemlaufwerk abzulegen.",
|
||||
modelsFolderKeywords:
|
||||
"Modelle Ordner Verzeichnis Pfad Speicherort Download Downloads Cache Speicher Festplatte Laufwerk verschieben ändern hugging face",
|
||||
futureDownloads: "Nur neue Downloads",
|
||||
environmentManaged:
|
||||
"Wird über die Umgebungsvariable {variable} verwaltet.",
|
||||
locationFree: "{free} frei",
|
||||
openAction: "Öffnen",
|
||||
copyAction: "Pfad kopieren",
|
||||
changeAction: "Ändern",
|
||||
resetAction: "Standard verwenden",
|
||||
chooseTitle: "Speicherort für Modell-Downloads wählen",
|
||||
chooseAction: "Für künftige Downloads verwenden",
|
||||
cacheSaved: "Speicherort für Modell-Downloads aktualisiert",
|
||||
cacheSaveError:
|
||||
"Der Speicherort für Modell-Downloads konnte nicht geändert werden",
|
||||
cachePickerError: "Die Ordnerauswahl konnte nicht geöffnet werden",
|
||||
copied: "Pfad kopiert",
|
||||
openError: "Der Ordner konnte nicht geöffnet werden",
|
||||
copyError: "Der Pfad konnte nicht kopiert werden",
|
||||
|
|
|
|||
|
|
@ -556,8 +556,12 @@ export const en = {
|
|||
systemDisk: "System disk",
|
||||
diskUsage: "{used} used / {total}",
|
||||
diskFree: "{free} free",
|
||||
modelsFolder: "Model downloads",
|
||||
modelsFolderDescription: "Hugging Face cache used for model downloads.",
|
||||
modelsFolder: "Models folder",
|
||||
modelsFolderDescription:
|
||||
"Where downloaded models are stored. Change it to keep models off your system drive.",
|
||||
// Not rendered: extra terms the settings search matches this row on.
|
||||
modelsFolderKeywords:
|
||||
"models folder directory path location download downloads cache storage disk drive move relocate hugging face",
|
||||
futureDownloads: "New downloads only",
|
||||
environmentManaged: "Managed by the {variable} environment variable.",
|
||||
locationFree: "{free} free",
|
||||
|
|
|
|||
|
|
@ -328,6 +328,8 @@ export const es = {
|
|||
diskUsage: "{used} en uso / {total}",
|
||||
diskFree: "{free} libre",
|
||||
modelsFolder: "Carpeta de modelos",
|
||||
modelsFolderKeywords:
|
||||
"modelos carpeta directorio ruta ubicacion ubicación descargas descarga cache caché almacenamiento disco unidad mover cambiar models folder path hugging face",
|
||||
modelsFolderDescription:
|
||||
"Dónde se almacenan los modelos descargados.",
|
||||
openAction: "Abrir",
|
||||
|
|
|
|||
|
|
@ -325,6 +325,8 @@ export const fr = {
|
|||
diskUsage: "{used} utilisé / {total}",
|
||||
diskFree: "{free} libre",
|
||||
modelsFolder: "Dossier des modèles",
|
||||
modelsFolderKeywords:
|
||||
"modeles modèles dossier repertoire répertoire chemin emplacement telechargements téléchargements cache stockage disque lecteur deplacer déplacer changer models folder path hugging face",
|
||||
modelsFolderDescription: "Emplacement de stockage des modèles téléchargés.",
|
||||
openAction: "Ouvrir",
|
||||
copyAction: "Copier le chemin",
|
||||
|
|
|
|||
|
|
@ -316,6 +316,8 @@ export const hi = {
|
|||
diskUsage: "{used} उपयोग में / {total}",
|
||||
diskFree: "{free} खाली",
|
||||
modelsFolder: "मॉडल फ़ोल्डर",
|
||||
modelsFolderKeywords:
|
||||
"मॉडल फ़ोल्डर फोल्डर निर्देशिका पथ स्थान डाउनलोड कैश संग्रहण डिस्क ड्राइव स्थानांतरित बदलें models folder path hugging face",
|
||||
modelsFolderDescription: "जहां डाउनलोड किए गए मॉडल संग्रहीत होते हैं।",
|
||||
openAction: "खोलें",
|
||||
copyAction: "पथ कॉपी करें",
|
||||
|
|
|
|||
|
|
@ -393,6 +393,8 @@ export const ja = {
|
|||
diskUsage: "{used} 使用中 / {total}",
|
||||
diskFree: "{free} 空き",
|
||||
modelsFolder: "モデルフォルダ",
|
||||
modelsFolderKeywords:
|
||||
"モデル フォルダ ディレクトリ パス 保存先 場所 ダウンロード キャッシュ ストレージ ディスク ドライブ 移動 変更 models folder path hugging face",
|
||||
modelsFolderDescription: "ダウンロードしたモデルの保存先。",
|
||||
openAction: "開く",
|
||||
copyAction: "パスをコピー",
|
||||
|
|
|
|||
|
|
@ -315,6 +315,8 @@ export const ko = {
|
|||
diskUsage: "{used} 사용 중 / {total}",
|
||||
diskFree: "{free} 여유",
|
||||
modelsFolder: "모델 폴더",
|
||||
modelsFolderKeywords:
|
||||
"모델 폴더 디렉터리 디렉토리 경로 위치 저장 다운로드 캐시 저장소 디스크 드라이브 이동 변경 models folder path hugging face",
|
||||
modelsFolderDescription: "다운로드한 모델이 저장되는 위치입니다.",
|
||||
openAction: "열기",
|
||||
copyAction: "경로 복사",
|
||||
|
|
|
|||
|
|
@ -417,6 +417,8 @@ export const ptBR = {
|
|||
diskUsage: "{used} usados / {total}",
|
||||
diskFree: "{free} livres",
|
||||
modelsFolder: "Pasta de modelos",
|
||||
modelsFolderKeywords:
|
||||
"modelos pasta diretorio diretório caminho local localizacao localização downloads baixar cache armazenamento disco unidade mover alterar models folder path hugging face",
|
||||
modelsFolderDescription: "Onde os modelos baixados são armazenados.",
|
||||
openAction: "Abrir",
|
||||
copyAction: "Copiar caminho",
|
||||
|
|
|
|||
|
|
@ -316,6 +316,8 @@ export const ru = {
|
|||
diskUsage: "{used} использовано / {total}",
|
||||
diskFree: "{free} свободно",
|
||||
modelsFolder: "Папка моделей",
|
||||
modelsFolderKeywords:
|
||||
"модели папка каталог путь расположение загрузки кэш хранилище диск перенести изменить models folder path hugging face",
|
||||
modelsFolderDescription: "Где хранятся загруженные модели.",
|
||||
openAction: "Открыть",
|
||||
copyAction: "Копировать путь",
|
||||
|
|
|
|||
|
|
@ -408,6 +408,8 @@ export const zhCN = {
|
|||
diskUsage: "已用 {used} / {total}",
|
||||
diskFree: "{free} 可用",
|
||||
modelsFolder: "模型文件夹",
|
||||
modelsFolderKeywords:
|
||||
"模型 文件夹 目录 路径 位置 下载 缓存 存储 磁盘 驱动器 移动 更改 models folder path hugging face",
|
||||
modelsFolderDescription: "已下载模型的存储位置。",
|
||||
openAction: "打开",
|
||||
copyAction: "复制路径",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue