'use client'; import { useEffect, useMemo, useState, useCallback, startTransition } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { useSetupWizard } from '@/hooks/use-setup-wizard'; import { useDownloadContext } from '@/contexts/download-context'; import { Search, Download, CheckCircle2, AlertCircle, Loader2, Sparkles, ArrowRight } from 'lucide-react'; import { cn } from '@/lib/utils'; interface ModelInfo { id: string; name: string; size: string; minRam: number; description?: string; } // Lista expandida de modelos populares const POPULAR_MODELS: ModelInfo[] = [ { id: 'llama3.2:1b', name: 'Llama 3.2 1B', size: '1.3GB', minRam: 4, description: 'Leve e rápido, ideal para iniciantes' }, { id: 'llama3.2:3b', name: 'Llama 3.2 3B', size: '2.0GB', minRam: 8, description: 'Equilíbrio perfeito entre velocidade e qualidade' }, { id: 'llama3.1:8b', name: 'Llama 3.1 8B', size: '4.7GB', minRam: 16, description: 'Padrão da indústria, alta capacidade' }, { id: 'mistral:latest', name: 'Mistral 7B', size: '4.1GB', minRam: 16, description: 'Excelente para raciocínio e código' }, { id: 'gemma2:9b', name: 'Gemma 2 9B', size: '5.4GB', minRam: 12, description: 'Modelo avançado do Google' }, { id: 'qwen2.5:7b', name: 'Qwen 2.5 7B', size: '4.5GB', minRam: 16, description: 'Multilíngue e versátil' }, { id: 'phi3:mini', name: 'Phi-3 Mini', size: '2.3GB', minRam: 8, description: 'Compacto e eficiente da Microsoft' }, { id: 'deepseek-coder:6.7b', name: 'DeepSeek Coder 6.7B', size: '4.2GB', minRam: 16, description: 'Especializado em programação' }, ]; interface ModelDownloadStepProps { onNext: () => void; } export function ModelDownloadStep({ onNext }: ModelDownloadStepProps) { const { state, checkSetupState } = useSetupWizard(); const { state: downloadState, startDownload } = useDownloadContext(); const [searchQuery, setSearchQuery] = useState(''); const [customModelInput, setCustomModelInput] = useState(''); const [activeModelTag, setActiveModelTag] = useState(null); const [installedModels, setInstalledModels] = useState>(new Set()); const [customModelInstalled, setCustomModelInstalled] = useState(false); // Verificar modelos instalados ao montar useEffect(() => { const checkAllModels = async () => { const installed = new Set(); // Verificar todos os modelos em paralelo const checks = POPULAR_MODELS.map(async (model) => { try { const isInstalled = await invoke('check_if_model_installed_command', { name: model.id }); if (isInstalled) { installed.add(model.id); } } catch (error) { console.error(`Erro ao verificar ${model.id}:`, error); } }); await Promise.all(checks); startTransition(() => { setInstalledModels(installed); }); }; checkAllModels(); }, []); // Verificar modelo customizado quando o usuário digita useEffect(() => { const checkCustomModel = async () => { const modelTag = customModelInput.trim(); if (!modelTag) { setCustomModelInstalled(false); return; } try { const isInstalled = await invoke('check_if_model_installed_command', { name: modelTag }); setCustomModelInstalled(isInstalled); } catch (error) { console.error(`Erro ao verificar modelo customizado ${modelTag}:`, error); setCustomModelInstalled(false); } }; // Debounce: aguardar 500ms após o usuário parar de digitar const timer = setTimeout(checkCustomModel, 500); return () => clearTimeout(timer); }, [customModelInput]); // Filtrar modelos baseado na pesquisa const filteredModels = useMemo(() => { if (!searchQuery.trim()) return POPULAR_MODELS; const query = searchQuery.toLowerCase(); return POPULAR_MODELS.filter( model => model.name.toLowerCase().includes(query) || model.id.toLowerCase().includes(query) || model.description?.toLowerCase().includes(query) ); }, [searchQuery]); // Progresso do download ativo const activeProgress = useMemo(() => { if (!downloadState.isDownloading || downloadState.currentModel !== activeModelTag) { return null; } return { percent: downloadState.progress.percent, downloaded: downloadState.progress.downloadedBytes, total: downloadState.progress.totalBytes, downloadedFormatted: downloadState.progress.downloadedFormatted, totalFormatted: downloadState.progress.totalFormatted, status: downloadState.progress.status, speed: downloadState.progress.speed, }; }, [downloadState, activeModelTag]); // Atualizar lista de instalados e limpar activeModelTag quando download completar useEffect(() => { if (downloadState.isSuccess && activeModelTag) { const modelTag = activeModelTag; // Adicionar modelo à lista de instalados usando startTransition para evitar cascading renders startTransition(() => { setInstalledModels(prev => new Set([...prev, modelTag])); // Verificar também o modelo customizado se for o caso if (modelTag === customModelInput.trim()) { setCustomModelInstalled(true); } }); // Limpar activeModelTag após um pequeno delay para garantir que o estado foi atualizado const timer = setTimeout(() => { setActiveModelTag(null); }, 100); return () => clearTimeout(timer); } }, [downloadState.isSuccess, activeModelTag, customModelInput]); // Verificar estado do setup quando download completar useEffect(() => { if (downloadState.isSuccess || (!downloadState.isDownloading && downloadState.progress.percent === 100 && downloadState.progress.status === 'success')) { checkSetupState().catch(console.error); } }, [downloadState.isSuccess, downloadState.isDownloading, downloadState.progress.percent, downloadState.progress.status, checkSetupState]); const handleModelDownload = useCallback(async (modelTag: string) => { if (downloadState.isDownloading) { console.warn('Download já em andamento'); return; } setActiveModelTag(modelTag); try { await startDownload(modelTag); } catch (error) { console.error('Erro ao iniciar download:', error); setActiveModelTag(null); } }, [downloadState.isDownloading, startDownload]); const handleCustomDownload = useCallback(async () => { const modelTag = customModelInput.trim(); if (!modelTag) return; await handleModelDownload(modelTag); setCustomModelInput(''); }, [customModelInput, handleModelDownload]); if (state.modelDownloaded) { return ( Modelo Instalado Modelo {state.modelName || 'padrão'} pronto para uso.

Download concluído com sucesso!

Avançando para o próximo passo...

); } return ( Download do Modelo Escolha um modelo e/ou informe uma tag custom para baixar.
setSearchQuery(e.target.value)} className="pl-9 h-10 text-sm" />
setCustomModelInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { handleCustomDownload(); } }} disabled={downloadState.isDownloading} className="flex-1 h-10 text-sm" />
{/* Lista de Modelos Recomendados */}

Modelos Recomendados

{filteredModels.length === 0 ? (
Nenhum modelo encontrado para “{searchQuery}”
) : ( filteredModels.map((model) => { const isActive = activeModelTag === model.id; const isDownloading = downloadState.isDownloading && isActive; const progress = isActive ? activeProgress : null; const isInstalled = installedModels.has(model.id); return (

{model.name}

{isInstalled && ( Instalado )}

{model.description || ''}

{model.size}
Mín. {model.minRam}GB RAM {model.id}
{isDownloading && progress && (
{progress.status || 'Baixando...'} {progress.speed && ( {progress.speed} )}
0 && progress.percent !== null ? progress.percent : undefined} className="h-1" /> {progress.total > 0 && (
{progress.downloadedFormatted} / {progress.totalFormatted} {progress.percent ?? 0}%
)}
)}
); }) )}
{/* Mensagens de Erro */} {state.lastError && (

Erro no download

{state.lastError}

)} {/* SLIM FOOTER NAVIGATION */}
Selecione um modelo ou pule para configurar depois.
{/* Footer vazio - mantido para compatibilidade, mas conteúdo movido para CardContent */}
); }