import { AlertCircle, Check, Download, Folder, HardDrive, Trash2 } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useAppSettings } from '../../contexts/AppSettingsContext' import { useHfAuth } from '../../hooks/use-hf-auth' import { useHfModelAccess } from '../../hooks/use-hf-model-access' import { ApiClient, type ApiRequestBodyOf, type ApiSuccessOf } from '../../lib/api-client' import { formatBytes } from '../../lib/format' import { logger } from '../../lib/logger' import { HfModelAccessGate } from '../HfModelAccessGate' import { Button } from '../ui/button' type LtxModelVersionItem = ApiSuccessOf<'getLtxVersions'>['versions'][number] type ModelCheckpointID = NonNullable< NonNullable>['cp_ids'] >[number] type HfAuthStatus = ApiSuccessOf<'getHuggingFaceAuthStatus'>['status'] const DOWNLOAD_POLL_INTERVAL_MS = 1000 function VersionRow({ version, onChanged, resumeSessionId, hfAuthStatus, hfAuthPolling, startHuggingFaceLogin, }: { version: LtxModelVersionItem onChanged: () => Promise resumeSessionId: string | null hfAuthStatus: HfAuthStatus hfAuthPolling: boolean startHuggingFaceLogin: () => void }) { const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [downloadSessionId, setDownloadSessionId] = useState(null) const [downloadPercent, setDownloadPercent] = useState(0) const mountedRef = useRef(true) useEffect(() => () => { mountedRef.current = false }, []) const cpsToDownload = useMemo( () => (version.installed ? [] : (version.cps_to_download as ModelCheckpointID[])), [version.installed, version.cps_to_download], ) const { accessMap, allAuthorized, checking: checkingAccess, checkError, recheckAccess } = useHfModelAccess( cpsToDownload, hfAuthStatus, ) const canDownload = version.installed || (allAuthorized && !checkingAccess) const adoptedRef = useRef(null) useEffect(() => { if (resumeSessionId && resumeSessionId !== adoptedRef.current && !downloadSessionId) { adoptedRef.current = resumeSessionId setDownloadSessionId(resumeSessionId) setBusy(true) } }, [resumeSessionId, downloadSessionId]) useEffect(() => { if (!downloadSessionId) return let cancelled = false const poll = async () => { const result = await ApiClient.getModelDownloadProgress({ sessionId: downloadSessionId }) if (cancelled) return if (!result.ok) { logger.error(`Progress poll error: ${result.error.message}`) return } const progress = result.data if (progress.status === 'downloading') { setDownloadPercent(Math.round(progress.total_progress)) return } if (progress.status === 'error') { setDownloadSessionId(null) setBusy(false) setError(progress.error || 'Download failed.') return } if (progress.status === 'complete') { setDownloadSessionId(null) setDownloadPercent(100) await onChanged() if (!cancelled) setBusy(false) } } void poll() const interval = setInterval(() => void poll(), DOWNLOAD_POLL_INTERVAL_MS) return () => { cancelled = true clearInterval(interval) } }, [downloadSessionId, onChanged]) const handleSetActive = useCallback(async () => { setError(null) setBusy(true) const result = await ApiClient.setActiveLtxModel({ model_id: version.model_id }) if (!result.ok) { setBusy(false) setError(result.error.message || 'Failed to set active model.') return } await onChanged() if (mountedRef.current) setBusy(false) }, [version.model_id, onChanged]) const handleDownload = useCallback(async () => { setError(null) setBusy(true) setDownloadPercent(0) const result = await ApiClient.startModelDownload({ type: 'download', cp_ids: version.cps_to_download }) if (!result.ok) { setBusy(false) setError(result.error.message || 'Failed to start download.') return } if (result.data.status !== 'started') { setBusy(false) setError('Unexpected response while starting download.') return } setDownloadSessionId(result.data.sessionId) }, [version.cps_to_download]) const handleDelete = useCallback(async () => { setError(null) setBusy(true) const result = await ApiClient.deleteModels({ cp_ids: [version.model_cp] }) if (!result.ok) { setBusy(false) setError(result.error.message || 'Failed to delete model.') return } await onChanged() if (mountedRef.current) setBusy(false) }, [version.model_cp, onChanged]) const isDownloading = downloadSessionId !== null return (
void handleSetActive()} className="h-4 w-4 accent-blue-500 flex-shrink-0 disabled:cursor-not-allowed" /> {version.label} {version.active && ( Active )} {version.is_newest && !version.installed && ( New )}
{!version.installed && ( )} {version.installed && !version.active && ( )}
{!version.installed && ( )} {error && (
{error}
)}
) } export function BaseModelSection() { const [versions, setVersions] = useState([]) const [modelsDir, setModelsDir] = useState('') const [activeDownload, setActiveDownload] = useState<{ sessionId: string; cpIds: string[] } | null>(null) const { hfAuthStatus, hfAuthPolling, startHuggingFaceLogin } = useHfAuth(true) const { notifyModelsChanged } = useAppSettings() const knownActiveRef = useRef(null) const refreshVersions = useCallback(async () => { const [versionsResult, activeResult] = await Promise.all([ ApiClient.getLtxVersions(), ApiClient.getActiveDownload(), ]) if (!versionsResult.ok) { logger.error(`Failed to fetch LTX versions: ${versionsResult.error.message}`) return } setVersions(versionsResult.data.versions) // Signal only on a real change so mounting the panel doesn't refetch generation specs. const nextActive = versionsResult.data.versions.find((item) => item.active)?.model_id ?? null const nextKey = `${nextActive}|${versionsResult.data.versions.filter((item) => item.installed).map((item) => item.model_id).join(',')}` if (knownActiveRef.current !== null && knownActiveRef.current !== nextKey) { notifyModelsChanged() } knownActiveRef.current = nextKey if (activeResult.ok) { setActiveDownload( activeResult.data.session_id ? { sessionId: activeResult.data.session_id, cpIds: activeResult.data.cp_ids ?? [] } : null, ) } }, [notifyModelsChanged]) useEffect(() => { void refreshVersions() void (async () => { const result = await ApiClient.getSettings() if (!result.ok) { logger.error(`Failed to fetch settings: ${result.error.message}`) return } setModelsDir(result.data.modelsDir ?? '') })() }, [refreshVersions]) return ( <>

Models Folder

Where model checkpoints are stored. Changing the location requires restarting the app.

{modelsDir || Not set}

Base Model

The active version is used for new generations. Download a version to make it available, then set it active. Newer versions may require a Hugging Face sign-in.

{versions.length === 0 ? (
No versions available.
) : ( versions.map((version) => ( { void startHuggingFaceLogin() }} /> )) )}
) }