Update model catalog fetching and loading state handling
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
setIsLoadingModelCatalog(false);
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setIsLoadingModelCatalog(true);
|
||||||
|
|
||||||
fetch(backendApiUrl + '/models')
|
fetch(backendApiUrl + '/models')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setChatModels(Array.isArray(data.chat_models) ? data.chat_models : []);
|
setChatModels(Array.isArray(data.chat_models) ? data.chat_models : []);
|
||||||
setEmbeddingModels(Array.isArray(data.embedding_models) ? data.embedding_models : []);
|
setEmbeddingModels(Array.isArray(data.embedding_models) ? data.embedding_models : []);
|
||||||
setVisionModels(Array.isArray(data.vision_models) ? data.vision_models : []);
|
setVisionModels(Array.isArray(data.vision_models) ? data.vision_models : []);
|
||||||
setRerankingModels(Array.isArray(data.reranking_models) ? data.reranking_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) : []);
|
setWhisperModels(Array.isArray(data.whisper_models) ? data.whisper_models.map(model => model.name).filter(Boolean) : []);
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Failed to load models', err));
|
.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">
|
||||||
|
|||||||
Reference in New Issue
Block a user