import { useCallback, useEffect, useMemo, useState } from 'react' import { AlertCircle, Download, Loader2, Sparkles, X } from 'lucide-react' 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 { logger } from '../lib/logger' import { HfModelAccessGate } from './HfModelAccessGate' import { Button } from './ui/button' import './LtxUpgradePrompt.css' type UpgradeRecommendation = Extract, { status: 'upgrade' }> type ModelCheckpointID = NonNullable< NonNullable>['cp_ids'] >[number] interface LtxUpgradePromptProps { recommendation: UpgradeRecommendation onClose: () => void // Dismiss permanently for this model id (persisted), so the prompt doesn't return for it. onDontShowAgain: () => void onComplete: () => Promise | void } type UpgradePhase = 'idle' | 'starting' | 'downloading' | 'finishing' function formatCheckpointId(cpId: string): string { return cpId.replace(/-/g, ' ') } export function LtxUpgradePrompt({ recommendation, onClose, onDontShowAgain, onComplete, }: LtxUpgradePromptProps) { const [wantsUpgrade, setWantsUpgrade] = useState(false) const [phase, setPhase] = useState('idle') const [downloadSessionId, setDownloadSessionId] = useState(null) const [downloadProgress, setDownloadProgress] = useState | null>( null, ) const [errorMessage, setErrorMessage] = useState(null) // Default off when deleting the old bundle would also wipe built-in Union Control IC-LoRA // (2.3 → 2.5). Otherwise default on to reclaim the tens of GB the old transformer uses. const [deleteOld, setDeleteOld] = useState(!recommendation.loses_built_in_control) const cpsToDownload = useMemo( () => recommendation.cps_to_download as ModelCheckpointID[], [recommendation.cps_to_download], ) const { hfAuthStatus, hfAuthPolling, startHuggingFaceLogin } = useHfAuth(true) const { accessMap, allAuthorized, checking: checkingAccess, checkError, recheckAccess } = useHfModelAccess( cpsToDownload, hfAuthStatus, ) const hasOldToDelete = recommendation.cps_to_delete.length > 0 const canClose = phase === 'idle' const canStartUpgrade = wantsUpgrade && phase === 'idle' && allAuthorized && !checkingAccess const runningProgress = downloadProgress?.status === 'downloading' ? downloadProgress : null const totalProgress = runningProgress?.total_progress ?? (phase === 'finishing' ? 100 : 0) const completedCount = runningProgress?.completed_files.length ?? 0 const totalCount = runningProgress?.all_files.length ?? recommendation.cps_to_download.length useEffect(() => { if (!canClose) return const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onClose() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [canClose, onClose]) useEffect(() => { if (phase !== 'downloading' || !downloadSessionId) return let cancelled = false const pollProgress = async () => { const progressResult = await ApiClient.getModelDownloadProgress({ sessionId: downloadSessionId }) if (!progressResult.ok) { logger.warn(`Failed polling LTX upgrade progress: ${progressResult.error.message}`) return } if (cancelled) return const progress = progressResult.data setDownloadProgress(progress) if (progress.status === 'error') { setPhase('idle') setErrorMessage(progress.error || 'Upgrade download failed.') return } if (progress.status === 'complete') { setPhase('finishing') if (deleteOld && recommendation.cps_to_delete.length > 0) { const deleteResult = await ApiClient.deleteModels({ cp_ids: recommendation.cps_to_delete }) if (!deleteResult.ok) { logger.error(`Failed finalizing LTX upgrade: ${deleteResult.error.message}`) if (cancelled) return setPhase('idle') setErrorMessage(deleteResult.error.message) return } } try { await onComplete() if (cancelled) return // Reset to idle rather than waiting for the parent's refresh to flip away from 'upgrade' // — if the active model isn't flipped yet we'd otherwise be stuck on "Finishing up...". setPhase('idle') onClose() } catch (e) { logger.error(`Failed finalizing LTX upgrade: ${e}`) if (cancelled) return setPhase('idle') setErrorMessage(e instanceof Error ? e.message : 'Upgrade downloaded, but cleanup failed.') } } } void pollProgress() const interval = setInterval(() => { void pollProgress() }, 700) return () => { cancelled = true clearInterval(interval) } }, [deleteOld, downloadSessionId, onClose, onComplete, phase, recommendation.cps_to_delete]) const handleStartUpgrade = useCallback(async () => { if (!canStartUpgrade) return setErrorMessage(null) setDownloadProgress(null) setPhase('starting') const result = await ApiClient.startModelDownload({ type: 'upgrade', cp_ids: recommendation.cps_to_download, }) if (!result.ok) { logger.warn(`Failed to start LTX upgrade download: ${result.error.message}`) setPhase('idle') setErrorMessage(result.error.message) return } const response = result.data if (response.status !== 'started') { setPhase('idle') setErrorMessage('Unexpected response while starting the upgrade.') return } setDownloadSessionId(response.sessionId) setPhase('downloading') }, [canStartUpgrade, recommendation.cps_to_download]) return (
{ if (canClose) onClose() }} />
Optional Upgrade

LTX Model Upgrade Detected!

Upgrade target: {recommendation.ltx_model_id}

{canClose && (
)}
{recommendation.upgrade_message ? ( <>

What's new in this version

    {recommendation.upgrade_message.split('\n').map((line) => line.trim()).filter(Boolean).map((line, i) => (
  • • {line}
  • ))}
) : (

A better LTX checkpoint is ready for this install.

)} {hasOldToDelete && (

{deleteOld ? recommendation.loses_built_in_control ? 'Your previous checkpoint and its built-in depth/canny/pose control models will be removed from disk.' : 'Your previous checkpoint will be removed from disk once the download completes.' : 'Your previous checkpoint will be kept — switch between versions anytime in Settings → Models.'}

)} {wantsUpgrade && hasOldToDelete && ( )}
{wantsUpgrade && (
{!allAuthorized && (
{ void startHuggingFaceLogin() }} checkError={checkError} onRetryCheck={recheckAccess} />
)} {canStartUpgrade && (

Ready when you are

This will download {recommendation.cps_to_download.length} checkpoint {recommendation.cps_to_download.length === 1 ? '' : 's'} {hasOldToDelete && deleteOld ? ' and remove the old one.' : '.'}

)}
)} {phase !== 'idle' && (

{phase === 'starting' ? 'Preparing your upgrade...' : phase === 'downloading' ? 'Downloading update...' : 'Finishing up...'}

{phase === 'finishing' ? (deleteOld && hasOldToDelete ? 'Cleaning up the previous checkpoint files.' : 'Finalizing the upgrade.') : 'Keep this window open while the new checkpoint is downloaded.'}

Working
{phase === 'starting' ? 'Waiting for the download session to start' : `Checkpoint progress ${Math.min(completedCount + (runningProgress ? 1 : 0), totalCount)} / ${totalCount}`} {Math.round(totalProgress)}%
{phase === 'starting' ? (
) : (
)}
{runningProgress?.current_downloading_file && (
Current file {Math.round(runningProgress.current_file_progress)}%
{formatCheckpointId(runningProgress.current_downloading_file)}
)}
)} {errorMessage && (
{errorMessage}
)}
) }