import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate, useOutletContext } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Input } from '@/components/ui/Input'; import { SecondaryScreenShell } from '@/components/common/SecondaryScreenShell'; import { useEdgeSwipeBack } from '@/hooks/useEdgeSwipeBack'; import { modelsApi } from '@/services/api'; import type { ModelInfo } from '@/utils/models'; import { buildHeaderObject } from '@/utils/headers'; import { buildOpenAIModelsEndpoint } from '@/components/providers/utils'; import type { OpenAIEditOutletContext } from './AiProvidersOpenAIEditLayout'; import styles from './AiProvidersPage.module.scss'; import layoutStyles from './AiProvidersEditLayout.module.scss'; const getErrorMessage = (err: unknown) => { if (err instanceof Error) return err.message; if (typeof err === 'string') return err; return ''; }; export function AiProvidersOpenAIModelsPage() { const { t } = useTranslation(); const navigate = useNavigate(); const { disableControls, loading: initialLoading, saving, form, mergeDiscoveredModels, } = useOutletContext(); const [endpoint, setEndpoint] = useState(''); const [models, setModels] = useState([]); const [fetching, setFetching] = useState(false); const [error, setError] = useState(''); const [search, setSearch] = useState(''); const [selected, setSelected] = useState>(new Set()); const filteredModels = useMemo(() => { const filter = search.trim().toLowerCase(); if (!filter) return models; return models.filter((model) => { const name = (model.name || '').toLowerCase(); const alias = (model.alias || '').toLowerCase(); const desc = (model.description || '').toLowerCase(); return name.includes(filter) || alias.includes(filter) || desc.includes(filter); }); }, [models, search]); const fetchOpenaiModelDiscovery = useCallback( async ({ allowFallback = true }: { allowFallback?: boolean } = {}) => { const trimmedBaseUrl = form.baseUrl.trim(); if (!trimmedBaseUrl) return; setFetching(true); setError(''); try { const headerObject = buildHeaderObject(form.headers); const firstKey = form.apiKeyEntries.find((entry) => entry.apiKey?.trim())?.apiKey?.trim(); const hasAuthHeader = Boolean(headerObject.Authorization || headerObject['authorization']); const list = await modelsApi.fetchModelsViaApiCall( trimmedBaseUrl, hasAuthHeader ? undefined : firstKey, headerObject ); setModels(list); } catch (err: unknown) { if (allowFallback) { try { const list = await modelsApi.fetchModelsViaApiCall(trimmedBaseUrl); setModels(list); return; } catch (fallbackErr: unknown) { const message = getErrorMessage(fallbackErr) || getErrorMessage(err); setModels([]); setError(`${t('ai_providers.openai_models_fetch_error')}: ${message}`); } } else { setModels([]); setError(`${t('ai_providers.openai_models_fetch_error')}: ${getErrorMessage(err)}`); } } finally { setFetching(false); } }, [form.apiKeyEntries, form.baseUrl, form.headers, t] ); useEffect(() => { if (initialLoading) return; setEndpoint(buildOpenAIModelsEndpoint(form.baseUrl)); setModels([]); setSearch(''); setSelected(new Set()); setError(''); void fetchOpenaiModelDiscovery(); }, [fetchOpenaiModelDiscovery, form.baseUrl, initialLoading]); const handleBack = useCallback(() => { navigate(-1); }, [navigate]); const swipeRef = useEdgeSwipeBack({ onBack: handleBack }); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { handleBack(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleBack]); const toggleSelection = (name: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(name)) { next.delete(name); } else { next.add(name); } return next; }); }; const handleApply = () => { const selectedModels = models.filter((model) => selected.has(model.name)); if (selectedModels.length) { mergeDiscoveredModels(selectedModels); } handleBack(); }; const canApply = !disableControls && !saving && !fetching; return ( {t('ai_providers.openai_models_fetch_apply')} } isLoading={initialLoading} loadingLabel={t('common.loading')} >
{t('ai_providers.openai_models_fetch_hint')}
setSearch(e.target.value)} disabled={fetching} /> {error &&
{error}
} {fetching ? (
{t('ai_providers.openai_models_fetch_loading')}
) : models.length === 0 ? (
{t('ai_providers.openai_models_fetch_empty')}
) : filteredModels.length === 0 ? (
{t('ai_providers.openai_models_search_empty')}
) : (
{filteredModels.map((model) => { const checked = selected.has(model.name); return ( ); })}
)}
); }