Files
OllaHub/hooks/use-setup-wizard.ts
2026-02-22 14:06:01 -03:00

367 lines
10 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
export interface SetupWizardState {
currentStep: number;
completedSteps: number[];
ollamaInstalled: boolean;
ollamaRunning: boolean;
modelDownloaded: boolean;
modelName: string | null;
lastError: string | null;
progressMessage: string | null;
progressStatus: string | null;
}
const CHECKPOINT_KEY = 'ollahub_setup_wizard_checkpoint';
const DEFAULT_MODEL = 'llama3.2:1b';
const MAX_STEPS = 4;
const MIN_STEP = 1;
function getDefaultState(): SetupWizardState {
return {
currentStep: 1,
completedSteps: [],
ollamaInstalled: false,
ollamaRunning: false,
modelDownloaded: false,
modelName: null,
lastError: null,
progressMessage: null,
progressStatus: null,
};
}
function validateWizardState(state: unknown): SetupWizardState | null {
// Verificar se é um objeto
if (!state || typeof state !== 'object' || Array.isArray(state)) {
return null;
}
const stateObj = state as Record<string, unknown>;
// Validar currentStep
const currentStep = typeof stateObj.currentStep === 'number'
? Math.max(MIN_STEP, Math.min(MAX_STEPS, Math.round(stateObj.currentStep)))
: MIN_STEP;
// Validar completedSteps
const completedSteps = Array.isArray(stateObj.completedSteps)
? stateObj.completedSteps
.filter((step: unknown) => typeof step === 'number' && step >= MIN_STEP && step <= MAX_STEPS)
.map((step: unknown) => Math.round(step as number))
.filter((step: number, index: number, arr: number[]) => arr.indexOf(step) === index) // Remover duplicatas
: [];
// Validar tipos dos campos booleanos
const ollamaInstalled = typeof stateObj.ollamaInstalled === 'boolean' ? stateObj.ollamaInstalled : false;
const ollamaRunning = typeof stateObj.ollamaRunning === 'boolean' ? stateObj.ollamaRunning : false;
const modelDownloaded = typeof stateObj.modelDownloaded === 'boolean' ? stateObj.modelDownloaded : false;
// Validar modelName (pode ser null ou string)
const modelName = stateObj.modelName === null || typeof stateObj.modelName === 'string'
? stateObj.modelName as string | null
: null;
// Validar lastError (pode ser null ou string)
const lastError = stateObj.lastError === null || typeof stateObj.lastError === 'string'
? stateObj.lastError as string | null
: null;
// Validar progressMessage (pode ser null ou string)
const progressMessage = stateObj.progressMessage === null || typeof stateObj.progressMessage === 'string'
? stateObj.progressMessage as string | null
: null;
// Validar progressStatus (pode ser null ou string)
const progressStatus = stateObj.progressStatus === null || typeof stateObj.progressStatus === 'string'
? stateObj.progressStatus as string | null
: null;
return {
currentStep,
completedSteps,
ollamaInstalled,
ollamaRunning,
modelDownloaded,
modelName,
lastError,
progressMessage,
progressStatus,
};
}
export function useSetupWizard() {
const [state, setState] = useState<SetupWizardState>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(CHECKPOINT_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
const validated = validateWizardState(parsed);
if (validated) {
return validated;
} else {
// Estado inválido, remover do localStorage e usar estado inicial
console.warn('Estado do wizard inválido detectado, resetando para estado inicial');
localStorage.removeItem(CHECKPOINT_KEY);
}
} catch (error) {
// Se falhar ao fazer parse, remover item corrompido
console.error('Erro ao fazer parse do estado do wizard:', error);
localStorage.removeItem(CHECKPOINT_KEY);
}
}
}
return getDefaultState();
});
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem(CHECKPOINT_KEY, JSON.stringify(state));
}
}, [state]);
useEffect(() => {
const unlistenPromise = listen<{ status: string; message: string }>('setup-progress', (event) => {
const { status, message } = event.payload;
setState(prev => ({
...prev,
progressStatus: status,
progressMessage: message,
}));
});
return () => {
unlistenPromise.then(unlisten => unlisten());
};
}, []);
const checkSetupState = useCallback(async () => {
try {
const setupState = await invoke<{
ollama_installed: boolean;
ollama_running: boolean;
default_model_downloaded: boolean;
default_model_name: string | null;
}>('get_setup_state_command');
setState(prev => ({
...prev,
ollamaInstalled: setupState.ollama_installed,
ollamaRunning: setupState.ollama_running,
modelDownloaded: setupState.default_model_downloaded,
modelName: setupState.default_model_name || DEFAULT_MODEL,
}));
let initialStep = 1;
if (setupState.ollama_installed && setupState.ollama_running) {
initialStep = 2;
if (setupState.default_model_downloaded) {
initialStep = 4;
} else {
initialStep = 3;
}
}
setState(prev => ({
...prev,
currentStep: Math.max(prev.currentStep, initialStep),
}));
return setupState;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setState(prev => ({
...prev,
lastError: errorMsg,
}));
throw error;
}
}, []);
const nextStep = useCallback(() => {
setState(prev => {
const next = prev.currentStep + 1;
return {
...prev,
currentStep: next,
completedSteps: [...prev.completedSteps, prev.currentStep],
lastError: null,
};
});
}, []);
const previousStep = useCallback(() => {
setState(prev => ({
...prev,
currentStep: Math.max(1, prev.currentStep - 1),
lastError: null,
}));
}, []);
const goToStep = useCallback((step: number) => {
setState(prev => ({
...prev,
currentStep: Math.max(1, Math.min(4, step)),
lastError: null,
}));
}, []);
const checkChocolatey = useCallback(async (): Promise<boolean> => {
try {
return await invoke<boolean>('check_chocolatey_installed_command');
} catch (error) {
console.error('Erro ao verificar Chocolatey:', error);
return false;
}
}, []);
const installChocolatey = useCallback(async (): Promise<void> => {
try {
setState(prev => ({
...prev,
progressMessage: 'Instalando Chocolatey...',
progressStatus: 'installing_chocolatey',
lastError: null,
}));
await invoke('install_chocolatey_command');
setState(prev => ({
...prev,
progressMessage: 'Chocolatey instalado com sucesso',
progressStatus: null,
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Erro ao instalar Chocolatey';
setState(prev => ({
...prev,
lastError: errorMsg,
progressStatus: null,
}));
throw error;
}
}, []);
const installOllamaViaChoco = useCallback(async (): Promise<void> => {
try {
setState(prev => ({
...prev,
progressMessage: 'Instalando Ollama via Chocolatey...',
progressStatus: 'installing_ollama',
lastError: null,
}));
await invoke('install_ollama_choco_command');
setState(prev => ({
...prev,
progressMessage: 'Ollama instalado com sucesso',
progressStatus: null,
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Erro ao instalar Ollama via Chocolatey';
setState(prev => ({
...prev,
lastError: errorMsg,
progressStatus: null,
}));
throw error;
}
}, []);
const installOllama = useCallback(async (installerPath?: string) => {
try {
setState(prev => ({
...prev,
lastError: null,
progressMessage: 'Iniciando instalação do Ollama...',
progressStatus: 'installing_ollama',
}));
await invoke('install_ollama_silently_command', {
installerPath: installerPath || null,
});
await checkSetupState();
setState(prev => ({
...prev,
ollamaInstalled: true,
ollamaRunning: true,
progressMessage: 'Ollama instalado e rodando!',
progressStatus: 'ollama_ready',
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setState(prev => ({
...prev,
lastError: errorMsg,
progressMessage: `Erro: ${errorMsg}`,
progressStatus: 'error',
}));
throw error;
}
}, [checkSetupState]);
const downloadModel = useCallback(async (modelName: string = DEFAULT_MODEL) => {
try {
setState(prev => ({
...prev,
lastError: null,
progressMessage: `Baixando modelo ${modelName}...`,
progressStatus: 'downloading_model',
}));
await invoke('download_default_model_command', { modelName });
setState(prev => ({
...prev,
modelDownloaded: true,
modelName,
progressMessage: `Modelo ${modelName} baixado com sucesso!`,
progressStatus: 'model_ready',
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setState(prev => ({
...prev,
lastError: errorMsg,
progressMessage: `Erro ao baixar modelo: ${errorMsg}`,
progressStatus: 'error',
}));
throw error;
}
}, []);
const resetWizard = useCallback(() => {
if (typeof window !== 'undefined') {
localStorage.removeItem(CHECKPOINT_KEY);
}
setState(getDefaultState());
}, []);
const isStepComplete = useCallback((step: number) => {
return state.completedSteps.includes(step);
}, [state.completedSteps]);
return {
state,
checkSetupState,
nextStep,
previousStep,
goToStep,
checkChocolatey,
installChocolatey,
installOllamaViaChoco,
installOllama,
downloadModel,
resetWizard,
isStepComplete,
};
}