Update model catalog fetching and loading state handling

This commit is contained in:
2026-04-17 08:48:14 +02:00
parent e9b96812f2
commit e332515585
2 changed files with 49 additions and 26 deletions

View File

@@ -1,4 +1,3 @@
import asyncio
import httpx import httpx
import json import json
import re import re
@@ -147,7 +146,7 @@ async def list_models() -> Dict[str, Any]:
return {"models": models} return {"models": models}
async def list_model_catalog(*, refresh: bool = False) -> Dict[str, Any]: async def list_model_catalog() -> Dict[str, Any]:
ollama_url = get_ollama_api_url() ollama_url = get_ollama_api_url()
async with httpx.AsyncClient(timeout=30.0) as client: async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.get(f"{ollama_url}/api/tags") r = await client.get(f"{ollama_url}/api/tags")
@@ -158,7 +157,7 @@ async def list_model_catalog(*, refresh: bool = False) -> Dict[str, Any]:
models = [ models = [
_build_model_catalog_entry( _build_model_catalog_entry(
item or {}, item or {},
show_model(str((item or {}).get("name") or "").strip(), refresh=refresh) if False else _get_cached_model_details(str((item or {}).get("name") or "").strip()), _get_cached_model_details(str((item or {}).get("name") or "").strip()),
) )
for item in raw_models for item in raw_models
if str((item or {}).get("name") or "").strip() if str((item or {}).get("name") or "").strip()

View File

@@ -35,13 +35,13 @@ function resolveBackendApiUrl(settings) {
return settings.backendApiUrl || settings.ollamaApiUrl || DEFAULT_BACKEND_API_URL; return settings.backendApiUrl || settings.ollamaApiUrl || DEFAULT_BACKEND_API_URL;
} }
function buildSelectOptions(values, currentValue, missingLabel) { function buildSelectOptions(values, currentValue, missingLabel, showMissingLabel = true) {
const uniqueValues = [...new Set((Array.isArray(values) ? values : []).filter(Boolean))]; const uniqueValues = [...new Set((Array.isArray(values) ? values : []).filter(Boolean))];
const options = uniqueValues.map(value => ({ value, label: value })); const options = uniqueValues.map(value => ({ value, label: value }));
if (currentValue && !uniqueValues.includes(currentValue)) { if (currentValue && !uniqueValues.includes(currentValue)) {
options.unshift({ options.unshift({
value: currentValue, value: currentValue,
label: `${currentValue} (${missingLabel})`, label: showMissingLabel ? `${currentValue} (${missingLabel})` : currentValue,
}); });
} }
return options; return options;
@@ -93,6 +93,7 @@ export default function GeneralSettings({
const [isPurgingLibraries, setIsPurgingLibraries] = useState(false); const [isPurgingLibraries, setIsPurgingLibraries] = useState(false);
const [libraryPurgeStatus, setLibraryPurgeStatus] = useState({ tone: 'neutral', message: '' }); const [libraryPurgeStatus, setLibraryPurgeStatus] = useState({ tone: 'neutral', message: '' });
const [settingsHydrated, setSettingsHydrated] = useState(false); const [settingsHydrated, setSettingsHydrated] = useState(false);
const [isLoadingModelCatalog, setIsLoadingModelCatalog] = useState(false);
const audioInputSupported = supportsAudioInputCapture(); const audioInputSupported = supportsAudioInputCapture();
useEffect(() => { useEffect(() => {
@@ -138,18 +139,40 @@ export default function GeneralSettings({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (backendApiUrl) { if (!backendApiUrl) {
fetch(backendApiUrl + '/models') setIsLoadingModelCatalog(false);
.then(r => r.json()) return () => {};
.then(data => {
setChatModels(Array.isArray(data.chat_models) ? data.chat_models : []);
setEmbeddingModels(Array.isArray(data.embedding_models) ? data.embedding_models : []);
setVisionModels(Array.isArray(data.vision_models) ? data.vision_models : []);
setRerankingModels(Array.isArray(data.reranking_models) ? data.reranking_models : []);
setWhisperModels(Array.isArray(data.whisper_models) ? data.whisper_models.map(model => model.name).filter(Boolean) : []);
})
.catch(err => console.error('Failed to load models', err));
} }
let cancelled = false;
setIsLoadingModelCatalog(true);
fetch(backendApiUrl + '/models')
.then(r => r.json())
.then(data => {
if (cancelled) {
return;
}
setChatModels(Array.isArray(data.chat_models) ? data.chat_models : []);
setEmbeddingModels(Array.isArray(data.embedding_models) ? data.embedding_models : []);
setVisionModels(Array.isArray(data.vision_models) ? data.vision_models : []);
setRerankingModels(Array.isArray(data.reranking_models) ? data.reranking_models : []);
setWhisperModels(Array.isArray(data.whisper_models) ? data.whisper_models.map(model => model.name).filter(Boolean) : []);
})
.catch(err => {
if (!cancelled) {
console.error('Failed to load models', err);
}
})
.finally(() => {
if (!cancelled) {
setIsLoadingModelCatalog(false);
}
});
return () => {
cancelled = true;
};
}, [backendApiUrl, ollamaApiUrl]); }, [backendApiUrl, ollamaApiUrl]);
useEffect(() => { useEffect(() => {
@@ -500,11 +523,12 @@ export default function GeneralSettings({
const audioDeviceRefreshLabel = audioInputDevices.some(device => device.hasLabel) const audioDeviceRefreshLabel = audioInputDevices.some(device => device.hasLabel)
? 'Refresh devices' ? 'Refresh devices'
: 'Allow microphone access'; : 'Allow microphone access';
const chatModelOptions = buildSelectOptions(chatModels, selectedModel, 'saved model unavailable'); const showMissingModelLabel = !isLoadingModelCatalog;
const visionModelOptions = buildSelectOptions(visionModels, visionModel, 'saved model unavailable'); const chatModelOptions = buildSelectOptions(chatModels, selectedModel, 'saved model unavailable', showMissingModelLabel);
const embeddingModelOptions = buildSelectOptions(embeddingModels, embedModel, 'saved model unavailable'); const visionModelOptions = buildSelectOptions(visionModels, visionModel, 'saved model unavailable', showMissingModelLabel);
const rerankingModelOptions = buildSelectOptions(rerankingModels, rerankModel, 'saved model unavailable'); const embeddingModelOptions = buildSelectOptions(embeddingModels, embedModel, 'saved model unavailable', showMissingModelLabel);
const transcriptionModelOptions = buildSelectOptions(whisperModels, transcriptionModel, 'saved model unavailable'); const rerankingModelOptions = buildSelectOptions(rerankingModels, rerankModel, 'saved model unavailable', showMissingModelLabel);
const transcriptionModelOptions = buildSelectOptions(whisperModels, transcriptionModel, 'saved model unavailable', showMissingModelLabel);
return ( return (
<div className="settings-content-panel"> <div className="settings-content-panel">
@@ -537,7 +561,7 @@ export default function GeneralSettings({
value={embedModel} value={embedModel}
onChange={handleEmbedModelChange} onChange={handleEmbedModelChange}
> >
{embeddingModelOptions.length === 0 && <option value="">— No embedding models available —</option>} {embeddingModelOptions.length === 0 && <option value="">{isLoadingModelCatalog ? 'Loading models…' : '— No embedding models available —'}</option>}
{embeddingModelOptions.map(model => ( {embeddingModelOptions.map(model => (
<option key={model.value} value={model.value}>{model.label}</option> <option key={model.value} value={model.value}>{model.label}</option>
))} ))}
@@ -553,7 +577,7 @@ export default function GeneralSettings({
value={rerankModel} value={rerankModel}
onChange={handleRerankModelChange} onChange={handleRerankModelChange}
> >
{rerankingModelOptions.length === 0 && <option value="">— No reranking models available —</option>} {rerankingModelOptions.length === 0 && <option value="">{isLoadingModelCatalog ? 'Loading models…' : '— No reranking models available —'}</option>}
{rerankingModelOptions.map(model => ( {rerankingModelOptions.map(model => (
<option key={model.value} value={model.value}>{model.label}</option> <option key={model.value} value={model.value}>{model.label}</option>
))} ))}
@@ -585,7 +609,7 @@ export default function GeneralSettings({
onChange={handleTranscriptionModelChange} onChange={handleTranscriptionModelChange}
disabled={!audioInputSupported} disabled={!audioInputSupported}
> >
{transcriptionModelOptions.length === 0 && <option value="">— No Whisper models available —</option>} {transcriptionModelOptions.length === 0 && <option value="">{isLoadingModelCatalog ? 'Loading models…' : '— No Whisper models available —'}</option>}
{transcriptionModelOptions.map(model => ( {transcriptionModelOptions.map(model => (
<option key={model.value} value={model.value}> <option key={model.value} value={model.value}>
{model.label} {model.label}
@@ -645,7 +669,7 @@ export default function GeneralSettings({
value={selectedModel} value={selectedModel}
onChange={handleModelChange} onChange={handleModelChange}
> >
{chatModelOptions.length === 0 && <option value="">— No chat models available —</option>} {chatModelOptions.length === 0 && <option value="">{isLoadingModelCatalog ? 'Loading models…' : '— No chat models available —'}</option>}
{chatModelOptions.map(model => <option key={model.value} value={model.value}>{model.label}</option>)} {chatModelOptions.map(model => <option key={model.value} value={model.value}>{model.label}</option>)}
</select> </select>
<p className="setting-description"> <p className="setting-description">
@@ -659,7 +683,7 @@ export default function GeneralSettings({
value={visionModel} value={visionModel}
onChange={handleVisionModelChange} onChange={handleVisionModelChange}
> >
{visionModelOptions.length === 0 && <option value="">— No vision models available —</option>} {visionModelOptions.length === 0 && <option value="">{isLoadingModelCatalog ? 'Loading models…' : '— No vision models available —'}</option>}
{visionModelOptions.map(model => <option key={model.value} value={model.value}>{model.label}</option>)} {visionModelOptions.map(model => <option key={model.value} value={model.value}>{model.label}</option>)}
</select> </select>
<p className="setting-description"> <p className="setting-description">