import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Download, Loader2, X, Upload, Globe, AlertCircle, File } from "lucide-react"; import { useState, useEffect, useRef } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; // Usar file dialog do Tauri v2 interface ModelDownloadDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: (modelName: string) => void; } interface DownloadInfo { status: string; percent?: number; downloaded?: string; total?: string; speed?: string; raw: string; } export function ModelDownloadDialog({ open, onOpenChange, onSuccess }: ModelDownloadDialogProps) { const [activeTab, setActiveTab] = useState<"download" | "local">("download"); const [modelName, setModelName] = useState(""); const [isDownloading, setIsDownloading] = useState(false); const [downloadProgress, setDownloadProgress] = useState(0); const [downloadStatus, setDownloadStatus] = useState(""); const [downloadInfo, setDownloadInfo] = useState(null); const [error, setError] = useState(null); const [isInstalled, setIsInstalled] = useState(false); const isDownloadingRef = useRef(false); const modelNameRef = useRef(""); // Estados para instalação local const [isInstallingLocal, setIsInstallingLocal] = useState(false); const [localInstallProgress, setLocalInstallProgress] = useState(0); const [localInstallError, setLocalInstallError] = useState(null); const [localModelName, setLocalModelName] = useState(""); // Atualizar refs quando estado muda useEffect(() => { isDownloadingRef.current = isDownloading; modelNameRef.current = modelName; }, [isDownloading, modelName]); // Resetar estado quando dialog abre/fecha useEffect(() => { if (open) { setActiveTab("download"); setModelName(""); setIsDownloading(false); setDownloadProgress(0); setDownloadStatus(""); setDownloadInfo(null); setError(null); setIsInstalled(false); isDownloadingRef.current = false; modelNameRef.current = ""; // Reset estados locais setIsInstallingLocal(false); setLocalInstallProgress(0); setLocalInstallError(null); setLocalModelName(""); } }, [open]); // Verificar se modelo já está instalado quando nome muda useEffect(() => { if (modelName.trim() && !isDownloading) { checkInstallation(modelName.trim()); } }, [modelName, isDownloading]); // Listener para progresso do download - sempre ativo quando dialog está aberto useEffect(() => { if (!open) return; let unlistenFn: (() => void) | null = null; const setupListener = async () => { const unlisten = await listen('download-progress', (event) => { // Só processar se estiver fazendo download (usar ref para evitar stale closure) if (!isDownloadingRef.current) return; const payload = event.payload; // Tentar parsear como JSON estruturado let info: DownloadInfo | null = null; try { info = JSON.parse(payload) as DownloadInfo; } catch { // Fallback: tratar como string raw info = { status: "downloading", raw: payload, }; } if (info) { setDownloadInfo(info); // Atualizar status text const statusText = info.status === "pulling" ? "Baixando modelo..." : info.status === "verifying" ? "Verificando..." : info.status === "writing" ? "Salvando..." : info.status === "success" ? "Concluído!" : "Baixando..."; setDownloadStatus(statusText); // Atualizar progresso if (info.percent !== undefined) { setDownloadProgress(info.percent); } else { // Fallback: estimar progresso baseado no status if (info.status === "pulling") { setDownloadProgress(prev => prev < 10 ? 10 : prev); } else if (info.status === "verifying") { setDownloadProgress(90); } else if (info.status === "writing") { setDownloadProgress(95); } } // Se chegou a 100% ou success, marcar como instalado após um pequeno delay if (info.percent === 100 || info.status === "success") { setTimeout(async () => { setIsInstalled(true); setIsDownloading(false); isDownloadingRef.current = false; const currentModelName = modelNameRef.current.trim(); await checkInstallation(currentModelName); // Após verificar instalação, chamar onSuccess e fechar if (onSuccess) { onSuccess(currentModelName); } onOpenChange(false); }, 500); } } }); unlistenFn = unlisten; }; setupListener(); return () => { if (unlistenFn) { unlistenFn(); } }; }, [open, onSuccess, onOpenChange]); const checkInstallation = async (name: string) => { try { const installed = await invoke('check_if_model_installed', { name }); setIsInstalled(installed); if (installed) { setError(null); } } catch (e) { console.error("Check failed", e); } }; const validateModelName = (name: string): string | null => { const trimmed = name.trim(); if (!trimmed) { return "Nome do modelo não pode estar vazio"; } // Validar formato: aceita letras, números, pontos, underscores, hífens, dois pontos e barras // Exemplos válidos: "llama3.2:1b", "huihui_ai/qwen3-abliterated:1.7b", "mistral:latest" if (!/^[a-zA-Z0-9._:\/-]+$/.test(trimmed)) { return "Nome do modelo contém caracteres inválidos"; } // Não pode começar ou terminar com barra, dois pontos ou hífen if (/^[\/:\-]|[\/:\-]$/.test(trimmed)) { return "Nome do modelo não pode começar ou terminar com /, : ou -"; } return null; }; const handleDownload = async () => { const trimmedName = modelName.trim(); const validationError = validateModelName(trimmedName); if (validationError) { setError(validationError); return; } if (isInstalled) { // Se já está instalado, apenas fechar e notificar sucesso handleSuccess(trimmedName); return; } setIsDownloading(true); isDownloadingRef.current = true; setDownloadProgress(0); setDownloadStatus("Iniciando download..."); setError(null); modelNameRef.current = trimmedName; try { await invoke('pull_model', { name: trimmedName }); // O status será atualizado pelo listener quando o download completar } catch (error: any) { console.error("Download failed", error); setError(error?.message || "Falha no download. Tente novamente."); setDownloadStatus(""); setDownloadProgress(0); setIsDownloading(false); isDownloadingRef.current = false; } }; const handleSuccess = (name: string) => { // Só fechar e chamar onSuccess se realmente completou o download // Se já estava instalado, também chamar onSuccess if (onSuccess) { onSuccess(name); } // Não fechar automaticamente se foi apenas verificação de instalação // Deixar o usuário decidir quando fechar }; const handleClose = () => { // Permitir fechar sempre, mas avisar se estiver baixando if (isDownloading) { const confirmed = window.confirm("O download está em andamento. Deseja realmente fechar? O download continuará em segundo plano."); if (!confirmed) { return; } } onOpenChange(false); }; const handleCancel = () => { // Cancelar download (por enquanto apenas fecha o dialog e para o estado) // TODO: Implementar cancelamento real do processo ollama pull if (isDownloading) { const confirmed = window.confirm("Deseja cancelar o download? O processo continuará em segundo plano."); if (confirmed) { setIsDownloading(false); isDownloadingRef.current = false; setDownloadProgress(0); setDownloadStatus(""); onOpenChange(false); } } else { onOpenChange(false); } }; const handleSelectFileButton = async () => { try { // Usar comando Rust para abrir dialog nativo do sistema // Isso evita limitações do input file HTML5 com arquivos grandes const selectedPath = await invoke('open_gguf_file_dialog'); if (!selectedPath) { // Usuário cancelou return; } setIsInstallingLocal(true); setLocalInstallError(null); setLocalInstallProgress(0); try { setLocalInstallProgress(10); // Extrair nome do arquivo do caminho const pathParts = selectedPath.split(/[/\\]/); const fileName = pathParts[pathParts.length - 1] || 'model'; const fileNameLower = fileName.toLowerCase(); const hasGgufExtension = fileNameLower.endsWith('.gguf'); // Extrair nome do modelo (remover extensão se for .gguf, senão manter) const fileNameWithoutExt = hasGgufExtension ? fileName.replace(/\.gguf$/i, '') : fileName; const modelNameToUse = localModelName.trim() || fileNameWithoutExt; setLocalInstallProgress(30); // Instalar diretamente do caminho (sem precisar copiar para temp primeiro) const installedModelName = await invoke('install_gguf_model', { filePath: selectedPath, modelName: modelNameToUse || undefined, }); setLocalInstallProgress(100); await new Promise(resolve => setTimeout(resolve, 500)); if (onSuccess) { onSuccess(installedModelName); } setTimeout(() => { onOpenChange(false); }, 1000); } catch (err: any) { const errorMsg = err?.message || 'Erro ao instalar modelo GGUF'; setLocalInstallError(errorMsg); console.error('Erro ao instalar modelo GGUF:', err); } finally { setIsInstallingLocal(false); } } catch (err: any) { console.error('Erro ao abrir dialog de arquivo:', err); setLocalInstallError('Erro ao abrir seletor de arquivo'); } }; const isValid = modelName.trim().length > 0 && !validateModelName(modelName.trim()); return ( { if (!open) { handleClose(); } }}> Adicionar Modelo Baixe modelos do Ollama ou instale arquivos GGUF locais setActiveTab(v as "download" | "local")} className="w-full"> Baixar do Ollama Instalar Local
setModelName(e.target.value)} placeholder="ex: llama3.2:1b" disabled={isDownloading} onKeyDown={(e) => { if (e.key === 'Enter' && isValid && !isDownloading) { handleDownload(); } }} /> {error && (

{error}

)} {isInstalled && !isDownloading && (

✓ Este modelo já está instalado

)}
{isDownloading && (
{downloadStatus || "Iniciando..."} {downloadProgress}%
{downloadInfo && (downloadInfo.downloaded || downloadInfo.speed) && (
{downloadInfo.downloaded && downloadInfo.total ? `${downloadInfo.downloaded} / ${downloadInfo.total}` : downloadInfo.downloaded || ""} {downloadInfo.speed && ( {downloadInfo.speed} )}
)}
)} {isInstalled && !isDownloading && (

O modelo está instalado e pronto para uso.

)}
setLocalModelName(e.target.value)} placeholder="Deixe vazio para usar o nome do arquivo" disabled={isInstallingLocal} />

Se não especificar, o nome será extraído do arquivo

{isInstallingLocal ? (

Instalando modelo...

Processando arquivo...

{localInstallProgress > 0 && (

{localInstallProgress}%

)}
) : ( <>

Selecione um arquivo GGUF para instalar localmente

{localInstallError && (

Erro ao instalar modelo

{localInstallError}

)} )}

ℹ️ Modelos instalados localmente

Funcionam sem conexão à internet. O arquivo será copiado para o diretório do Ollama.

{activeTab === "download" && ( )}
); }