"use client"; import { useState, useEffect } from "react"; import { X, Download, Loader2 } from "lucide-react"; interface ModelPullDialogProps { isOpen: boolean; onClose: () => void; modelName: string; onConfirmPull: (modelName: string) => Promise; } interface PullProgress { status: string; digest?: string; total?: number; completed?: number; error?: string; } export function ModelPullDialog({ isOpen, onClose, modelName, onConfirmPull, }: ModelPullDialogProps) { const [isPulling, setIsPulling] = useState(false); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const [isSuccess, setIsSuccess] = useState(false); const [editableName, setEditableName] = useState(modelName); const [tags, setTags] = useState([]); const [isLoadingTags, setIsLoadingTags] = useState(false); useEffect(() => { if (!isOpen) { setIsPulling(false); setProgress(null); setError(null); setIsSuccess(false); setEditableName(modelName); setTags([]); setIsLoadingTags(false); return; } // Ao abrir, tentar buscar tags conhecidas const base = String(modelName).split(":")[0]; setEditableName(modelName); if (!base) { setTags([]); setIsLoadingTags(false); return; } setIsLoadingTags(true); fetch(`/api/ollama/tags?name=${encodeURIComponent(base)}`) .then((res) => res.json()) .then((data) => { if (data?.success && Array.isArray(data.tags)) { setTags(data.tags); } else { setTags([]); } }) .catch(() => setTags([])) .finally(() => setIsLoadingTags(false)); }, [isOpen, modelName]); const handleConfirm = async () => { setIsPulling(true); setError(null); setProgress(null); try { const response = await fetch("/api/ollama/pull", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: editableName.trim() }), }); if (!response.ok) { throw new Error("Falha ao iniciar download do modelo"); } const reader = response.body?.getReader(); if (!reader) throw new Error("Resposta inválida do servidor"); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.trim()) continue; try { const data = JSON.parse(line); // Processar diferentes estados de progresso if (data.status === "downloading" || data.status === "pulling") { setProgress({ status: data.status, total: data.total, completed: data.completed, digest: data.digest, }); } else if (data.status === "success") { setProgress((prev) => ({ status: "success", total: prev?.total, completed: prev?.total })); setIsSuccess(true); } else if (data.error) { setError(data.detail || data.error); } else if (data.total || data.completed) { // Caso o status não seja "downloading" mas tenha dados de progresso setProgress({ status: data.status || "downloading", total: data.total, completed: data.completed, digest: data.digest, }); } } catch (e) { // ignorar linhas inválidas } } } // Não fechar automaticamente; permitir usuário fechar ao concluir } catch (err) { setError(err instanceof Error ? err.message : "Failed to pull model"); } finally { setIsPulling(false); } }; const formatBytes = (n?: number) => { if (!n || n <= 0) return "0 MB"; const mb = n / 1024 / 1024; if (mb < 1024) return `${mb.toFixed(1)} MB`; const gb = mb / 1024; return `${gb.toFixed(2)} GB`; }; const percent = progress?.total && progress?.completed ? Math.max(0, Math.min(100, Math.round((progress.completed / progress.total) * 100))) : 0; if (!isOpen) return null; return (

Install Model

Confirme o identificador do modelo (nome[:tag]):

setEditableName(e.target.value)} disabled={isPulling} className="w-full rounded-md border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)]" placeholder="ex.: llama3:8b-instruct" />

Tags disponíveis

{isLoadingTags && ( Carregando… )}
{tags.length > 0 ? ( ) : (

Nenhuma tag detectada automaticamente. Você pode digitar manualmente.

)}

Isso fará o download do modelo na sua instalação local do Ollama.

{progress && (
{!isSuccess && } {isSuccess ? "Download concluído" : "Baixando modelo..."} {progress.total && progress.completed && {percent}%}
{progress.total && progress.completed ? (
{formatBytes(progress.completed)} / {formatBytes(progress.total)} {progress?.digest && ( sha256: {progress.digest.slice(0, 12)}... )}
) : (
Aguardando informações de progresso...
)}
)} {error && (

{error}

)}
{!isSuccess && ( )}
); }