Files
OllaHub/components/setup/ollama-install-step.tsx
2026-02-22 14:06:01 -03:00

638 lines
25 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { useSetupWizard } from '@/hooks/use-setup-wizard';
import { useOllamaDownload } from '@/hooks/use-ollama-download';
import { useOllamaCheck } from '@/hooks/use-ollama-check';
import { useOperatingSystem } from '@/hooks/use-operating-system';
import { InstallationTerminal } from './installation-terminal';
import { Loader2, CheckCircle2, AlertCircle, Download, RefreshCw, Shield, Clock, WifiOff, ArrowRight } from 'lucide-react';
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
function formatTime(seconds: number): string {
if (seconds < 60) return `${Math.round(seconds)}s`;
const minutes = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return `${minutes}m ${secs}s`;
}
type InstallErrorType = 'ERR_NO_PRIVILEGE' | 'ERR_DOWNLOAD_FAIL' | 'ERR_TIMEOUT' | 'ERR_INSTALL_FAILED';
function parseInstallError(error: string): InstallErrorType {
const errorLower = error.toLowerCase();
// Verificar privilégios/permissões
if (
errorLower.includes('privilege') ||
errorLower.includes('permission') ||
errorLower.includes('elevated') ||
errorLower.includes('uac') ||
errorLower.includes('administrator') ||
errorLower.includes('access denied') ||
errorLower.includes('acesso negado')
) {
return 'ERR_NO_PRIVILEGE';
}
// Verificar erros de download/rede
if (
errorLower.includes('download') ||
errorLower.includes('network') ||
errorLower.includes('connection') ||
errorLower.includes('failed to download') ||
errorLower.includes('falha ao baixar') ||
errorLower.includes('conexão') ||
errorLower.includes('timeout') && (errorLower.includes('download') || errorLower.includes('conexão'))
) {
return 'ERR_DOWNLOAD_FAIL';
}
// Verificar timeout
if (
errorLower.includes('timeout') ||
errorLower.includes('90 segundos') ||
errorLower.includes('timed out') ||
errorLower.includes('tempo esgotado')
) {
return 'ERR_TIMEOUT';
}
return 'ERR_INSTALL_FAILED';
}
interface ErrorDisplayProps {
errorType: InstallErrorType;
errorMessage: string;
onRetry: () => void;
isRetrying: boolean;
}
function ErrorDisplay({ errorType, errorMessage, onRetry, isRetrying }: ErrorDisplayProps) {
switch (errorType) {
case 'ERR_NO_PRIVILEGE':
return (
<Card className="border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-950/20">
<CardHeader className="pb-3 sm:pb-6">
<div className="flex items-start gap-2 sm:gap-3">
<Shield className="w-4 h-4 sm:w-5 sm:h-5 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<CardTitle className="text-sm sm:text-base text-amber-900 dark:text-amber-100">
Permissões Administrativas Necessárias
</CardTitle>
<CardDescription className="text-xs sm:text-sm text-amber-700 dark:text-amber-300 mt-1 sm:mt-2">
O Windows solicitou permissões de administrador para instalar o Ollama.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
<div className="bg-white dark:bg-zinc-900 p-3 sm:p-4 rounded-lg border border-amber-200 dark:border-amber-800">
<p className="text-xs sm:text-sm font-medium text-foreground mb-2">O que fazer:</p>
<ol className="list-decimal list-inside space-y-1 text-xs sm:text-sm text-muted-foreground">
<li>Verifique se apareceu um prompt do Windows pedindo permissões</li>
<li>Clique em &quot;Sim&quot; ou &quot;Allow&quot; no prompt de UAC (User Account Control)</li>
<li>Aguarde alguns segundos para a instalação continuar</li>
<li>Se o prompt não apareceu, tente executar o OllaHub como administrador</li>
</ol>
</div>
<Button
onClick={onRetry}
disabled={isRetrying}
variant="outline"
className="w-full sm:w-auto"
>
{isRetrying ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verificando...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Verificar Novamente
</>
)}
</Button>
<p className="text-xs text-muted-foreground text-center">
{errorMessage}
</p>
</CardContent>
</Card>
);
case 'ERR_DOWNLOAD_FAIL':
return (
<Card className="border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/20">
<CardHeader className="pb-3 sm:pb-6">
<div className="flex items-start gap-2 sm:gap-3">
<WifiOff className="w-4 h-4 sm:w-5 sm:h-5 text-red-600 dark:text-red-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<CardTitle className="text-sm sm:text-base text-red-900 dark:text-red-100">
Erro ao Baixar Instalador
</CardTitle>
<CardDescription className="text-xs sm:text-sm text-red-700 dark:text-red-300 mt-1 sm:mt-2">
Não foi possível baixar o instalador do Ollama. Verifique sua conexão com a internet.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
<div className="bg-white dark:bg-zinc-900 p-3 sm:p-4 rounded-lg border border-red-200 dark:border-red-800">
<p className="text-xs sm:text-sm text-muted-foreground break-words">
{errorMessage}
</p>
</div>
<Button
onClick={onRetry}
disabled={isRetrying}
variant="outline"
className="w-full sm:w-auto"
>
{isRetrying ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Tentando Novamente...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Tentar Novamente
</>
)}
</Button>
</CardContent>
</Card>
);
case 'ERR_TIMEOUT':
return (
<Card className="border-orange-200 dark:border-orange-900 bg-orange-50 dark:bg-orange-950/20">
<CardHeader className="pb-3 sm:pb-6">
<div className="flex items-start gap-2 sm:gap-3">
<Clock className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600 dark:text-orange-400 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<CardTitle className="text-sm sm:text-base text-orange-900 dark:text-orange-100">
Instalação Demorando Mais do Que o Esperado
</CardTitle>
<CardDescription className="text-xs sm:text-sm text-orange-700 dark:text-orange-300 mt-1 sm:mt-2">
A instalação pode ainda estar em andamento em segundo plano.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
<div className="bg-white dark:bg-zinc-900 p-3 sm:p-4 rounded-lg border border-orange-200 dark:border-orange-800">
<p className="text-xs sm:text-sm text-muted-foreground mb-2 break-words">
O processo de instalação pode levar alguns minutos. Se o Ollama estiver sendo instalado,
você pode verificar o status manualmente.
</p>
<p className="text-[10px] sm:text-xs text-muted-foreground break-words">
{errorMessage}
</p>
</div>
<Button
onClick={onRetry}
disabled={isRetrying}
variant="outline"
className="w-full sm:w-auto"
>
{isRetrying ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verificando...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Verificar Novamente
</>
)}
</Button>
</CardContent>
</Card>
);
default:
return (
<div className="flex items-start gap-2 p-3 sm:p-4 bg-red-50 dark:bg-red-950/20 rounded-lg">
<AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-red-600 dark:text-red-400 mt-0.5 shrink-0" />
<div className="flex-1 space-y-2 sm:space-y-3 min-w-0">
<div>
<p className="text-sm sm:text-base font-medium text-red-900 dark:text-red-100">Erro na instalação</p>
<p className="text-xs sm:text-sm text-red-700 dark:text-red-300 mt-1 break-words">{errorMessage}</p>
</div>
<Button
onClick={onRetry}
disabled={isRetrying}
variant="outline"
size="sm"
className="w-full sm:w-auto"
>
{isRetrying ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verificando...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Verificar Novamente
</>
)}
</Button>
</div>
</div>
);
}
}
type InstallStepState = 'IDLE' | 'INSTALLING' | 'VERIFYING' | 'SUCCESS';
interface OllamaInstallStepProps {
onNext: () => void;
}
export function OllamaInstallStep({ onNext }: OllamaInstallStepProps) {
const { state, installOllama, checkChocolatey, installChocolatey, installOllamaViaChoco, checkSetupState } = useSetupWizard();
const { os: detectedOS } = useOperatingSystem();
const downloadState = useOllamaDownload();
const ollamaCheck = useOllamaCheck();
const [stepState, setStepState] = useState<InstallStepState>('IDLE');
const [useChocolatey, setUseChocolatey] = useState(false);
const [showTerminal, setShowTerminal] = useState(false);
const [isRetrying, setIsRetrying] = useState(false);
const installationInProgressRef = useRef(false);
const hasCheckedInitialRef = useRef(false);
// Função de polling ativo para aguardar serviço ficar disponível
const waitForService = async (maxAttempts = 15): Promise<boolean> => {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const isIntact = await invoke<boolean>('verify_ollama_integrity_command');
if (isIntact) {
return true;
}
} catch (error) {
console.warn(`Tentativa ${attempt}/${maxAttempts} falhou:`, error);
}
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
throw new Error('Ollama não ficou disponível após 30 segundos');
};
// Verificação inicial no mount: se Ollama já estiver instalado, ir direto para SUCCESS
useEffect(() => {
if (hasCheckedInitialRef.current) return;
const checkInitial = async () => {
if (stepState === 'IDLE') {
try {
const isIntact = await invoke<boolean>('verify_ollama_integrity_command');
if (isIntact) {
// Se a integridade está OK, atualizar estado do wizard e ir para SUCCESS
await checkSetupState();
setStepState('SUCCESS');
}
} catch {
// Silenciosamente ignorar - usuário ainda não instalou
}
}
hasCheckedInitialRef.current = true;
};
checkInitial();
}, [stepState, checkSetupState]);
// Sincronizar estado do polling com wizard state e atualizar FSM
useEffect(() => {
if (ollamaCheck.status === 'running' && !state.ollamaRunning) {
// Polling detectou que Ollama está rodando, atualizar wizard state
checkSetupState().catch(console.error);
}
// Atualizar FSM baseado no status
if (stepState === 'VERIFYING' && ollamaCheck.status === 'running' && state.ollamaInstalled && state.ollamaRunning) {
setStepState('SUCCESS');
ollamaCheck.stopPolling();
}
}, [ollamaCheck.status, state.ollamaRunning, state.ollamaInstalled, checkSetupState, stepState, ollamaCheck]);
const handleManualRetry = async () => {
setIsRetrying(true);
try {
await checkSetupState();
// Se já estiver instalado, ir para SUCCESS
if (state.ollamaInstalled && state.ollamaRunning) {
setStepState('SUCCESS');
} else {
// Iniciar verificação manual
setStepState('VERIFYING');
ollamaCheck.startPolling();
}
} catch (error) {
console.error('Erro ao verificar novamente:', error);
} finally {
setIsRetrying(false);
}
};
const handleCheckExisting = async () => {
setStepState('VERIFYING');
ollamaCheck.startPolling();
await checkSetupState();
};
const handleInstall = async () => {
if (installationInProgressRef.current) {
return; // Evitar múltiplas instalações simultâneas
}
installationInProgressRef.current = true;
setStepState('INSTALLING');
setShowTerminal(true);
try {
// No Windows, verificar se Chocolatey está disponível
if (detectedOS === 'windows') {
const hasChoco = await checkChocolatey();
if (hasChoco) {
setUseChocolatey(true);
// Instalar Ollama via Chocolatey
await installOllamaViaChoco();
// Após instalação, fazer polling ativo
setStepState('VERIFYING');
await waitForService(15); // 15 tentativas = 30 segundos
// Se chegou aqui, Ollama está funcional
setStepState('SUCCESS');
await checkSetupState(); // Atualizar estado do wizard
installationInProgressRef.current = false;
return;
} else {
// Tentar instalar Chocolatey primeiro
try {
await installChocolatey();
setUseChocolatey(true);
await installOllamaViaChoco();
// Após instalação, fazer polling ativo
setStepState('VERIFYING');
await waitForService(15); // 15 tentativas = 30 segundos
// Se chegou aqui, Ollama está funcional
setStepState('SUCCESS');
await checkSetupState(); // Atualizar estado do wizard
installationInProgressRef.current = false;
return;
} catch (error) {
console.warn('Falha ao usar Chocolatey, usando instalação direta:', error);
setUseChocolatey(false);
setShowTerminal(false);
}
}
}
// Fallback: instalação direta
if (!downloadState.filePath && detectedOS) {
await downloadState.handleDownload(detectedOS);
}
if (downloadState.filePath) {
await installOllama(downloadState.filePath);
} else {
await installOllama();
}
// Após instalação, fazer polling ativo
setStepState('VERIFYING');
await waitForService(15); // 15 tentativas = 30 segundos
// Se chegou aqui, Ollama está funcional
setStepState('SUCCESS');
await checkSetupState(); // Atualizar estado do wizard
} catch (error) {
console.error('Erro ao aguardar serviço:', error);
setStepState('IDLE');
} finally {
installationInProgressRef.current = false;
}
};
// Estado SUCCESS: mostrar mensagem de sucesso e botão Continuar
if (stepState === 'SUCCESS' || (state.ollamaInstalled && state.ollamaRunning)) {
return (
<Card className="w-full max-w-2xl mx-auto border-zinc-800 bg-zinc-950/50 backdrop-blur-sm shadow-2xl">
<CardHeader className="p-6 sm:p-8">
<CardTitle className="text-2xl font-bold text-white flex items-center gap-2 mb-2">
<CheckCircle2 className="w-6 h-6 text-emerald-500 shrink-0" />
<span>Instalação Concluída</span>
</CardTitle>
<CardDescription className="text-zinc-400 mb-8 text-base">
Ollama está instalado e rodando com sucesso.
</CardDescription>
</CardHeader>
<CardContent className="px-6 sm:px-8 pb-6 sm:pb-8 pt-0 space-y-6">
<div className="flex items-start sm:items-center gap-3 p-4 bg-emerald-950/30 rounded-lg border border-emerald-900/50">
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-100">Ollama Service is Running</p>
<p className="text-xs text-emerald-300 mt-1">Pronto para continuar com o download de modelos.</p>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-end p-6 sm:p-8 pt-6 mt-0 border-t border-zinc-800">
<Button onClick={onNext} className="w-full sm:w-auto bg-white text-black hover:bg-zinc-200 h-12 px-8 text-base font-medium">
Continuar <ArrowRight className="w-4 h-4 ml-2" />
</Button>
</CardFooter>
</Card>
);
}
// Estado IDLE: mostrar card informativo
if (stepState === 'IDLE') {
return (
<Card className="w-full max-w-2xl mx-auto border-zinc-800 bg-zinc-950/50 backdrop-blur-sm shadow-2xl">
<CardHeader className="p-6 sm:p-8">
<CardTitle className="text-2xl font-bold text-white flex items-center gap-2 mb-2">
<Download className="w-6 h-6 shrink-0" />
<span>Ollama Engine</span>
</CardTitle>
<CardDescription className="text-zinc-400 mb-8 text-base">
O OllaHub requer o motor Ollama para executar IAs localmente.
</CardDescription>
</CardHeader>
<CardContent className="px-6 sm:px-8 pb-6 sm:pb-8 pt-0 space-y-6">
<p className="text-sm text-zinc-400 leading-relaxed">
O Ollama é uma ferramenta open-source que permite executar modelos de linguagem grandes (LLMs) diretamente no seu computador, sem necessidade de conexão com a internet.
</p>
</CardContent>
<CardFooter className="flex flex-col sm:flex-row justify-end gap-4 p-6 sm:p-8 pt-6 mt-0 border-t border-zinc-800">
<Button onClick={handleCheckExisting} variant="outline" className="w-full sm:w-auto h-12 border-zinc-700 text-zinc-300 hover:bg-zinc-800">
Já tenho o Ollama instalado
</Button>
<Button onClick={handleInstall} className="w-full sm:w-auto bg-white text-black hover:bg-zinc-200 h-12 px-8 text-base font-medium">
<Download className="w-4 h-4 mr-2" />
Instalar Ollama
</Button>
</CardFooter>
</Card>
);
}
const errorType = state.lastError ? parseInstallError(state.lastError) : null;
const showRetryButton = state.lastError || state.progressStatus === 'error';
const showManualRetry = showRetryButton && state.lastError;
const hasTerminal = showTerminal && (state.progressStatus === 'installing_chocolatey' || state.progressStatus === 'installing_ollama' || useChocolatey);
return (
<Card className={`w-full max-w-2xl mx-auto border-zinc-800 bg-zinc-950/50 backdrop-blur-sm shadow-2xl ${hasTerminal ? 'max-h-[calc(100vh-16rem)] flex flex-col' : ''}`}>
<CardHeader className={`p-6 sm:p-8 ${hasTerminal ? 'flex-shrink-0' : ''}`}>
<CardTitle className="text-2xl font-bold text-white flex items-center gap-2 mb-2">
{stepState === 'INSTALLING' && <Loader2 className="w-6 h-6 animate-spin" />}
{stepState === 'VERIFYING' && <Loader2 className="w-6 h-6 animate-spin" />}
{(state.lastError || state.progressStatus === 'error') && <AlertCircle className="w-6 h-6 text-red-500" />}
<span>
{stepState === 'INSTALLING' ? 'Instalando Ollama...' :
stepState === 'VERIFYING' ? 'Verificando Instalação...' :
'Instalação em Progresso'}
</span>
</CardTitle>
<CardDescription className="text-zinc-400 mb-8 text-base">
Acompanhe o progresso da configuração do ambiente.
</CardDescription>
</CardHeader>
<CardContent className={`px-6 sm:px-8 pb-6 sm:pb-8 pt-0 space-y-6 ${hasTerminal ? 'flex-1 min-h-0 overflow-y-auto' : ''}`}>
{/* Terminal integrado para instalação via Chocolatey */}
{(showTerminal && (state.progressStatus === 'installing_chocolatey' || state.progressStatus === 'installing_ollama' || useChocolatey)) && (
<InstallationTerminal
onComplete={() => {
setShowTerminal(false);
}}
onError={(error) => {
console.error('Erro na instalação:', error);
}}
/>
)}
{/* Progresso para instalação direta (fallback) */}
{state.progressMessage && !showTerminal && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{state.progressMessage}</span>
{ollamaCheck.isChecking && (
<span className="text-xs text-muted-foreground">
Verificando... {formatTime(ollamaCheck.elapsedTime)}
</span>
)}
</div>
{state.progressStatus === 'installing_ollama' && (
<Progress value={undefined} className="h-2" />
)}
</div>
)}
{/* Feedback do polling em background (estado VERIFYING) */}
{stepState === 'VERIFYING' && (
<div className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground p-4 bg-muted/50 rounded-lg">
<Loader2 className="w-4 h-4 animate-spin shrink-0" />
<span className="break-words">
Instalação concluída. Aguardando inicialização do serviço...
</span>
</div>
)}
{/* Display de erro granular */}
{state.lastError && errorType && (
<ErrorDisplay
errorType={errorType}
errorMessage={state.lastError}
onRetry={handleManualRetry}
isRetrying={isRetrying}
/>
)}
{/* Botão de retry manual quando não há erro específico */}
{showRetryButton && !showManualRetry && (
<Button
onClick={handleManualRetry}
disabled={isRetrying || stepState === 'INSTALLING'}
variant="outline"
className="w-full"
>
{isRetrying ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verificando...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Verificar Novamente
</>
)}
</Button>
)}
{downloadState.isDownloading && (
<div className="space-y-2 w-full">
<div className="flex justify-between text-xs text-muted-foreground">
<span className="flex items-center gap-2">
{downloadState.status === 'verifying' ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Download className="w-3 h-3" />
)}
{downloadState.status === 'verifying'
? 'Verificando integridade...'
: 'Baixando...'}
</span>
<span className="font-mono">{downloadState.speed || '0 MB/s'}</span>
</div>
<Progress
value={downloadState.downloadProgress}
className="h-2 w-full transition-all duration-300"
/>
<div className="flex justify-between text-[10px] text-muted-foreground/70 uppercase font-medium tracking-wider">
<span>
{formatBytes(downloadState.downloadedBytes)} / {formatBytes(downloadState.totalBytes)}
</span>
<span>{downloadState.downloadProgress.toFixed(1)}%</span>
</div>
{downloadState.eta > 0 && downloadState.status === 'downloading' && (
<div className="text-xs text-muted-foreground text-center">
Tempo restante: {formatTime(downloadState.eta)}
</div>
)}
</div>
)}
{/* Estado INSTALLING: mostrar progresso */}
{stepState === 'INSTALLING' && (
<div className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground">
<Loader2 className="w-4 h-4 animate-spin shrink-0" />
<span className="break-words">Instalando Ollama... Isso pode levar alguns minutos.</span>
</div>
)}
</CardContent>
{/* Footer opcional se necessário */}
</Card>
);
}