import { useState, useEffect, useCallback, useRef } from 'react'; import { useConfig } from './ConfigContext'; import { listLocalModels, downloadHfModel, getLocalModelDownloadProgress, cancelLocalModelDownload, type DownloadProgress, type LocalModelResponse, } from '../api'; import { toastService } from '../toasts'; import { trackOnboardingSetupFailed } from '../utils/analytics'; import { Goose } from './icons'; interface LocalModelSetupProps { onSuccess: () => void; onCancel: () => void; } const formatBytes = (bytes: number): string => { if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)}MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`; }; const formatSize = (bytes: number): string => { const mb = bytes / (1024 * 1024); return mb >= 1024 ? `${(mb / 1024).toFixed(1)}GB` : `${mb.toFixed(0)}MB`; }; type SetupPhase = 'loading' | 'select' | 'downloading' | 'error'; export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) { const { upsert } = useConfig(); const [phase, setPhase] = useState('loading'); const [models, setModels] = useState([]); const [selectedModelId, setSelectedModelId] = useState(null); const [downloadProgress, setDownloadProgress] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [showAllModels, setShowAllModels] = useState(false); const pollRef = useRef | null>(null); const cleanup = useCallback(() => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }, []); useEffect(() => cleanup, [cleanup]); useEffect(() => { const load = async () => { try { const response = await listLocalModels(); if (response.data) { setModels(response.data); const alreadyDownloaded = response.data.find((m) => m.status.state === 'Downloaded'); if (alreadyDownloaded) { setSelectedModelId(alreadyDownloaded.id); } else { const recommended = response.data.find((m: LocalModelResponse) => m.recommended); if (recommended) setSelectedModelId(recommended.id); } } } catch (error) { console.error('Failed to load local models:', error); setErrorMessage('Failed to load available models. Please try again.'); setPhase('error'); return; } setPhase('select'); }; load(); }, []); const finishSetup = async (modelId: string) => { await upsert('GOOSE_PROVIDER', 'local', false); await upsert('GOOSE_MODEL', modelId, false); toastService.success({ title: 'Local Model Ready', msg: `Running entirely on your machine with ${modelId}.`, }); onSuccess(); }; const startDownload = async (modelId: string) => { setPhase('downloading'); setDownloadProgress(null); setErrorMessage(null); const model = models.find((m) => m.id === modelId); if (!model) { setErrorMessage('Model not found'); setPhase('error'); return; } try { await downloadHfModel({ body: { spec: model.id } }); } catch (error) { console.error('Failed to start download:', error); setErrorMessage('Failed to start download. Please try again.'); trackOnboardingSetupFailed('local', 'download_start_failed'); setPhase('error'); return; } pollRef.current = setInterval(async () => { try { const response = await getLocalModelDownloadProgress({ path: { model_id: modelId } }); if (response.data) { setDownloadProgress(response.data); if (response.data.status === 'completed') { cleanup(); await finishSetup(modelId); } else if (response.data.status === 'failed') { cleanup(); setErrorMessage(response.data.error || 'Download failed.'); trackOnboardingSetupFailed('local', response.data.error || 'download_failed'); setPhase('error'); } else if (response.data.status === 'cancelled') { cleanup(); setPhase('select'); } } } catch { cleanup(); setErrorMessage('Lost connection to download. Please try again.'); trackOnboardingSetupFailed('local', 'progress_poll_failed'); setPhase('error'); } }, 500); }; const handleCancel = async () => { if (phase === 'downloading' && selectedModelId) { cleanup(); try { await cancelLocalModelDownload({ path: { model_id: selectedModelId } }); } catch { // best-effort } setDownloadProgress(null); setPhase('select'); } else { onCancel(); } }; const handlePrimaryAction = async () => { if (!selectedModelId) return; const model = models.find((m) => m.id === selectedModelId); if (!model) return; if (model.status.state === 'Downloaded') { await finishSetup(model.id); } else { await startDownload(model.id); } }; const recommended = models.find((m) => m.recommended); const otherModels = models.filter((m) => m.id !== recommended?.id); const selectedModel = models.find((m) => m.id === selectedModelId); if (phase === 'loading') { return (

Checking available models...

); } return (
{/* Header */}

Run Locally

Download a model to run Goose entirely on your machine — no API keys, no accounts, completely free and private.

{/* Error state */} {phase === 'error' && (

{errorMessage}

)} {/* Model selection */} {phase === 'select' && (
{/* Recommended model card */} {recommended && (
setSelectedModelId(recommended.id)} className={`relative w-full p-4 sm:p-6 border rounded-xl cursor-pointer transition-all duration-200 group ${ selectedModelId === recommended.id ? 'border-blue-500 bg-blue-500/5' : 'border-border-subtle hover:border-border-default' }`} >
Best for your machine
setSelectedModelId(recommended.id)} className="cursor-pointer flex-shrink-0 mt-1" />
{recommended.display_name} {recommended.status.state === 'Downloaded' && ( Ready )}

{formatSize(recommended.size_bytes)}

)} {/* Expandable other models */} {otherModels.length > 0 && (
{showAllModels && (
{otherModels.map((model) => (
setSelectedModelId(model.id)} className={`w-full p-4 border rounded-xl cursor-pointer transition-all duration-200 ${ selectedModelId === model.id ? 'border-blue-500 bg-blue-500/5' : 'border-border-subtle hover:border-border-default' }`} >
setSelectedModelId(model.id)} className="cursor-pointer flex-shrink-0 mt-0.5" />
{model.display_name} {formatSize(model.size_bytes)} {model.status.state === 'Downloaded' && ( Ready )}
))}
)}
)} {/* Primary action */}
)} {/* Downloading state */} {phase === 'downloading' && selectedModel && (

Downloading {selectedModel.display_name}

{downloadProgress ? (
{/* Progress bar */}
{/* Stats row */}
{formatBytes(downloadProgress.bytes_downloaded)} of{' '} {formatBytes(downloadProgress.total_bytes)} {downloadProgress.progress_percent.toFixed(0)}%
{downloadProgress.speed_bps ? ( {formatBytes(downloadProgress.speed_bps)}/s ) : ( )} {downloadProgress.eta_seconds != null && downloadProgress.eta_seconds > 0 && ( ~{downloadProgress.eta_seconds < 60 ? `${Math.round(downloadProgress.eta_seconds)}s` : `${Math.round(downloadProgress.eta_seconds / 60)}m`}{' '} remaining )}
) : (
Starting download...
)}
)}
); }