chore: expurgo de blobs excedentes e realinhamento de estado
release.yml (push) Invalid workflow file

This commit is contained in:
Evandro Fonseca Junior committed 2026-02-22 14:06:01 -03:00
1 parent 593efd42e0
commit a7cbd1a3b4
114 files changed
+20716 -4258

No files matched your search

+8
View File
@@ -269,3 +269,11 @@ export function GpuDashboard({ gpuId, pollInterval = 2000 }: GpuDashboardProps)
}
@@ -0,0 +1,169 @@
'use client';
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ExternalLink, Copy, Check, Terminal } from 'lucide-react';
import { ProblemSolution } from '@/lib/docker-problems';
import { useToast } from '@/hooks/use-toast';
import { invoke } from '@tauri-apps/api/core';
import { cn } from '@/lib/utils';
interface DockerCommandsModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
problem: ProblemSolution | null;
}
export function DockerCommandsModal({ open, onOpenChange, problem }: DockerCommandsModalProps) {
const { toast } = useToast();
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
if (!problem) return null;
const handleCopyCommand = async (command: string, index: number) => {
try {
await navigator.clipboard.writeText(command);
setCopiedIndex(index);
toast({
title: 'Comando copiado',
description: 'Comando copiado para a área de transferência',
});
setTimeout(() => setCopiedIndex(null), 2000);
} catch (error) {
toast({
title: 'Erro ao copiar',
description: 'Não foi possível copiar o comando',
variant: 'destructive',
});
}
};
const handleOpenLink = async (url: string) => {
try {
await invoke('open_url', { url });
} catch (error) {
console.error('Error opening URL:', error);
// Fallback para window.open se Tauri falhar
window.open(url, '_blank', 'noopener,noreferrer');
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-3xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6">
<DialogHeader className="flex-shrink-0 pb-2 sm:pb-4">
<DialogTitle className="flex items-center gap-2 text-sm sm:text-base md:text-lg">
<Terminal className="w-4 h-4 sm:w-5 sm:h-5 shrink-0" />
<span className="break-words">{problem.problem}</span>
</DialogTitle>
{problem.description && (
<DialogDescription className="text-xs sm:text-sm mt-1 sm:mt-2">
{problem.description}
</DialogDescription>
)}
</DialogHeader>
<div className="space-y-3 sm:space-y-4 overflow-y-auto flex-1 min-h-0 pr-1 -mr-1">
{/* Links de Documentação */}
{problem.links.length > 0 && (
<Card>
<CardHeader className="pb-2 sm:pb-3">
<CardTitle className="text-sm sm:text-base flex items-center gap-2">
<ExternalLink className="w-3 h-3 sm:w-4 sm:h-4" />
Documentação Oficial
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
Links para documentação oficial sobre este problema
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{problem.links.map((link, idx) => (
<Button
key={idx}
variant="outline"
className="w-full justify-start text-xs sm:text-sm h-9 sm:h-10"
onClick={() => handleOpenLink(link.url)}
>
<ExternalLink className="w-3 h-3 sm:w-4 sm:h-4 mr-2 shrink-0" />
<span className="truncate">{link.title}</span>
</Button>
))}
</CardContent>
</Card>
)}
{/* Comandos */}
{problem.commands && problem.commands.length > 0 && (
<Card>
<CardHeader className="pb-2 sm:pb-3">
<CardTitle className="text-sm sm:text-base flex items-center gap-2">
<Terminal className="w-3 h-3 sm:w-4 sm:h-4" />
Comandos
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
Execute estes comandos no terminal para resolver o problema
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4">
{problem.commands.map((command, idx) => (
<div key={idx} className="space-y-2">
<div className="flex items-start justify-between gap-2">
<h4 className="text-xs sm:text-sm font-medium flex-1 break-words">{command.title}</h4>
<Button
size="sm"
variant="ghost"
onClick={() => handleCopyCommand(command.code, idx)}
className="h-7 sm:h-8 shrink-0 text-xs"
>
{copiedIndex === idx ? (
<>
<Check className="w-3 h-3 mr-1 text-green-500" />
<span className="hidden sm:inline">Copiado</span>
</>
) : (
<>
<Copy className="w-3 h-3 mr-1" />
<span className="hidden sm:inline">Copiar</span>
</>
)}
</Button>
</div>
{command.description && (
<p className="text-xs text-muted-foreground">{command.description}</p>
)}
<div className="relative">
<pre className="bg-muted p-2 sm:p-3 md:p-4 rounded-lg overflow-x-auto text-xs sm:text-sm">
<code className="font-mono whitespace-pre-wrap break-all">{command.code}</code>
</pre>
</div>
</div>
))}
</CardContent>
</Card>
)}
{/* Mensagem quando não há comandos nem links */}
{problem.links.length === 0 && (!problem.commands || problem.commands.length === 0) && (
<Card>
<CardContent className="py-4 sm:py-6 text-center text-muted-foreground">
<p className="text-xs sm:text-sm">Nenhuma solução disponível para este problema.</p>
<p className="text-xs mt-2">
Consulte a documentação oficial do Docker para mais informações.
</p>
</CardContent>
</Card>
)}
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,899 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Progress } from '@/components/ui/progress';
import {
CheckCircle2,
XCircle,
Loader2,
Download,
RefreshCw,
AlertCircle,
ExternalLink,
Settings,
Wrench,
Server,
Cpu,
HardDrive,
MemoryStick,
Shield,
Zap,
Terminal
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { useToast } from '@/hooks/use-toast';
import { SearxngInstallWizard } from './searxng-install-wizard';
import { DockerCommandsModal } from './docker-commands-modal';
import { findProblemSolution, ProblemSolution } from '@/lib/docker-problems';
import { cn } from '@/lib/utils';
interface SystemRequirements {
status: 'pass' | 'fail' | 'warn';
os: string;
arch: string;
ram_mb: number;
disk_gb: number;
cpu_cores: number;
kernel_version?: string;
issues: string[];
warnings: string[];
}
interface InstallProgressEvent {
event_type: 'step_start' | 'log' | 'step_complete' | 'error' | 'complete';
step?: number;
step_name?: string;
total_steps?: number;
progress: number;
message?: string;
log_line?: string;
}
interface FixProgressEvent {
event_type: 'start' | 'progress' | 'complete' | 'error';
message: string;
progress?: number;
}
interface ValidationResult {
status: 'pass' | 'fail' | 'partial';
tests: Array<{
name: string;
status: 'pass' | 'fail' | 'warn';
message?: string;
}>;
docker_version?: string;
}
export function DockerSettingsPanel() {
const { toast } = useToast();
const [dockerAvailable, setDockerAvailable] = useState<boolean | null>(null);
const [dockerRunning, setDockerRunning] = useState<boolean | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [requirements, setRequirements] = useState<SystemRequirements | null>(null);
const [isCheckingRequirements, setIsCheckingRequirements] = useState(false);
const [isInstallingDocker, setIsInstallingDocker] = useState(false);
const [isFixing, setIsFixing] = useState(false);
const [fixProgress, setFixProgress] = useState<{ message: string; progress: number } | null>(null);
const [dockerInstallProgress, setDockerInstallProgress] = useState<{
currentStep: number;
totalSteps: number;
progress: number;
currentStepName: string;
logs: string[];
}>({
currentStep: 0,
totalSteps: 0,
progress: 0,
currentStepName: '',
logs: [],
});
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [showInstallWizard, setShowInstallWizard] = useState(false);
const [showCommandsModal, setShowCommandsModal] = useState(false);
const [selectedProblem, setSelectedProblem] = useState<ProblemSolution | null>(null);
// Verificar Docker ao montar
useEffect(() => {
checkDocker();
checkRequirements();
}, []);
// Listener de eventos de progresso Docker
useEffect(() => {
let unlistenPromise: Promise<() => void> | null = null;
if (isInstallingDocker) {
unlistenPromise = listen<InstallProgressEvent>('docker-install-progress', (event) => {
const data = event.payload;
switch (data.event_type) {
case 'step_start':
setDockerInstallProgress(prev => ({
...prev,
currentStep: data.step || 0,
totalSteps: data.total_steps || prev.totalSteps,
currentStepName: data.step_name || '',
progress: data.progress,
}));
if (data.message) {
setDockerInstallProgress(prev => ({
...prev,
logs: [...prev.logs.slice(-49), data.message!],
}));
}
break;
case 'log':
if (data.log_line) {
setDockerInstallProgress(prev => ({
...prev,
logs: [...prev.logs.slice(-49), data.log_line!],
}));
}
break;
case 'step_complete':
setDockerInstallProgress(prev => ({
...prev,
progress: data.progress,
}));
if (data.message) {
setDockerInstallProgress(prev => ({
...prev,
logs: [...prev.logs.slice(-49), data.message!],
}));
}
break;
case 'error':
toast({
title: 'Erro na instalação',
description: data.message || 'Erro desconhecido',
variant: 'destructive',
});
setIsInstallingDocker(false);
break;
case 'complete':
setDockerInstallProgress(prev => ({ ...prev, progress: 100 }));
setIsInstallingDocker(false);
toast({
title: 'Docker instalado',
description: 'Docker foi instalado com sucesso!',
});
setTimeout(() => {
validateDocker();
checkDocker();
}, 1000);
break;
}
});
}
return () => {
if (unlistenPromise) {
unlistenPromise.then(fn => fn());
}
};
}, [isInstallingDocker, toast]);
// Listener de eventos de correção
useEffect(() => {
let unlistenPromise: Promise<() => void> | null = null;
if (isFixing) {
unlistenPromise = listen<FixProgressEvent>('docker-fix-progress', (event) => {
const data = event.payload;
switch (data.event_type) {
case 'start':
setFixProgress({ message: data.message, progress: 0 });
break;
case 'progress':
setFixProgress({ message: data.message, progress: data.progress || 0 });
break;
case 'complete':
setFixProgress({ message: data.message, progress: 100 });
setTimeout(() => {
setIsFixing(false);
setFixProgress(null);
checkRequirements();
toast({
title: 'Correção concluída',
description: data.message,
});
}, 1500);
break;
case 'error':
setIsFixing(false);
setFixProgress(null);
toast({
title: 'Erro na correção',
description: data.message,
variant: 'destructive',
});
break;
}
});
}
return () => {
if (unlistenPromise) {
unlistenPromise.then(fn => fn());
}
};
}, [isFixing, toast]);
const checkDocker = async () => {
setIsChecking(true);
try {
const available = await invoke<boolean>('check_docker_available');
setDockerAvailable(available);
if (available) {
const running = await invoke<boolean>('check_docker_running');
setDockerRunning(running);
} else {
setDockerRunning(false);
}
} catch (error) {
console.error('Error checking Docker:', error);
setDockerAvailable(false);
setDockerRunning(false);
} finally {
setIsChecking(false);
}
};
const checkRequirements = async () => {
setIsCheckingRequirements(true);
try {
const reqs = await invoke<SystemRequirements>('check_docker_requirements');
setRequirements(reqs);
} catch (error) {
console.error('Error checking requirements:', error);
setRequirements({
status: 'fail',
os: 'unknown',
arch: 'unknown',
ram_mb: 0,
disk_gb: 0,
cpu_cores: 0,
issues: ['Erro ao verificar requisitos do sistema'],
warnings: [],
});
} finally {
setIsCheckingRequirements(false);
}
};
const handleFixRequirements = async () => {
if (!requirements || requirements.issues.length === 0) {
toast({
title: 'Nada para corrigir',
description: 'Não há problemas encontrados que possam ser corrigidos automaticamente',
});
return;
}
setIsFixing(true);
setFixProgress({ message: 'Preparando correção...', progress: 0 });
try {
await invoke('fix_docker_requirements', {
os: requirements.os,
issues: requirements.issues,
});
} catch (error) {
console.error('Error fixing requirements:', error);
setIsFixing(false);
setFixProgress(null);
toast({
title: 'Erro na correção',
description: error instanceof Error ? error.message : 'Erro desconhecido',
variant: 'destructive',
});
}
};
const handleInstallDocker = async () => {
setIsInstallingDocker(true);
setDockerInstallProgress({
currentStep: 0,
totalSteps: 0,
progress: 0,
currentStepName: '',
logs: [],
});
try {
await invoke('install_docker');
} catch (error) {
console.error('Docker installation error:', error);
toast({
title: 'Erro na instalação',
description: error instanceof Error ? error.message : 'Erro desconhecido durante instalação do Docker',
variant: 'destructive',
});
setIsInstallingDocker(false);
}
};
const validateDocker = async () => {
setIsValidating(true);
setValidationResult(null);
try {
const result = await invoke<ValidationResult>('validate_docker_installation');
setValidationResult(result);
if (result.status === 'pass' || result.status === 'partial') {
setDockerAvailable(true);
setDockerRunning(true);
toast({
title: 'Docker validado',
description: 'Docker está funcionando corretamente',
});
} else {
toast({
title: 'Validação falhou',
description: 'Alguns testes de validação falharam',
variant: 'destructive',
});
}
} catch (error) {
console.error('Validation error:', error);
toast({
title: 'Erro na validação',
description: error instanceof Error ? error.message : 'Erro ao validar Docker',
variant: 'destructive',
});
} finally {
setIsValidating(false);
}
};
const getStatusIcon = (status: 'pass' | 'fail' | 'warn') => {
switch (status) {
case 'pass':
return <CheckCircle2 className="w-5 h-5 text-green-500" />;
case 'fail':
return <XCircle className="w-5 h-5 text-red-500" />;
case 'warn':
return <AlertCircle className="w-5 h-5 text-yellow-500" />;
}
};
return (
<div className="space-y-4">
{/* Status do Docker */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Server className="w-5 h-5 text-primary" />
<CardTitle className="text-lg">Status do Docker</CardTitle>
</div>
<Button
variant="outline"
size="sm"
onClick={checkDocker}
disabled={isChecking}
>
<RefreshCw className={cn("w-4 h-4 mr-1", isChecking && "animate-spin")} />
Atualizar
</Button>
</div>
<CardDescription>
Verifique se Docker está instalado e rodando
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isChecking ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="w-5 h-5 animate-spin text-primary mr-2" />
<span className="text-sm text-muted-foreground">Verificando...</span>
</div>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<div className={cn(
"p-4 rounded-lg border-2 transition-colors",
dockerAvailable
? "border-green-500/50 bg-green-500/10 dark:bg-green-500/20"
: "border-red-500/50 bg-red-500/10 dark:bg-red-500/20"
)}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-foreground">Instalação</span>
{dockerAvailable ? (
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400" />
) : (
<XCircle className="w-5 h-5 text-red-600 dark:text-red-400" />
)}
</div>
<Badge
variant="outline"
className={cn(
dockerAvailable
? "text-green-700 dark:text-green-300 border-green-600 dark:border-green-400 bg-green-100 dark:bg-green-500/20"
: "text-red-700 dark:text-red-300 border-red-600 dark:border-red-400 bg-red-100 dark:bg-red-500/20"
)}
>
{dockerAvailable ? 'Instalado' : 'Não Instalado'}
</Badge>
</div>
<div className={cn(
"p-4 rounded-lg border-2 transition-colors",
dockerRunning
? "border-green-500/50 bg-green-500/10 dark:bg-green-500/20"
: "border-red-500/50 bg-red-500/10 dark:bg-red-500/20"
)}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-foreground">Serviço</span>
{dockerRunning ? (
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400" />
) : (
<XCircle className="w-5 h-5 text-red-600 dark:text-red-400" />
)}
</div>
<Badge
variant="outline"
className={cn(
dockerRunning
? "text-green-700 dark:text-green-300 border-green-600 dark:border-green-400 bg-green-100 dark:bg-green-500/20"
: "text-red-700 dark:text-red-300 border-red-600 dark:border-red-400 bg-red-100 dark:bg-red-500/20"
)}
>
{dockerRunning ? 'Rodando' : 'Parado'}
</Badge>
</div>
</div>
{dockerAvailable === false && (
<Alert variant="destructive">
<AlertCircle className="w-4 h-4" />
<AlertDescription>
Docker não está instalado. Instale-o para usar SearXNG.
</AlertDescription>
</Alert>
)}
{dockerAvailable && !dockerRunning && (
<Alert>
<AlertCircle className="w-4 h-4" />
<AlertDescription>
Docker está instalado mas não está rodando. Inicie o Docker Desktop.
</AlertDescription>
</Alert>
)}
</>
)}
</CardContent>
</Card>
{/* Requisitos do Sistema */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-primary" />
<CardTitle className="text-lg">Requisitos do Sistema</CardTitle>
</div>
<div className="flex gap-2">
{requirements && requirements.issues.length > 0 && (
<Button
variant="default"
size="sm"
onClick={handleFixRequirements}
disabled={isFixing || isCheckingRequirements}
>
<Wrench className="w-4 h-4 mr-1" />
Corrigir
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={checkRequirements}
disabled={isCheckingRequirements}
>
<RefreshCw className={cn("w-4 h-4 mr-1", isCheckingRequirements && "animate-spin")} />
Verificar
</Button>
</div>
</div>
<CardDescription>
Verifique se seu sistema atende aos requisitos para Docker
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isCheckingRequirements ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="w-5 h-5 animate-spin text-primary mr-2" />
<span className="text-sm text-muted-foreground">Verificando requisitos...</span>
</div>
) : requirements ? (
<>
{/* Status Geral */}
<div className={cn(
"p-4 rounded-lg border-2 flex items-center justify-between",
requirements.status === 'pass' && "border-green-500/50 bg-green-500/10 dark:bg-green-500/20",
requirements.status === 'warn' && "border-yellow-500/50 bg-yellow-500/10 dark:bg-yellow-500/20",
requirements.status === 'fail' && "border-red-500/50 bg-red-500/10 dark:bg-red-500/20"
)}>
<div className="flex items-center gap-3">
{getStatusIcon(requirements.status)}
<div>
<p className="font-semibold text-foreground">Status Geral</p>
<p className="text-xs text-muted-foreground">
{requirements.status === 'pass' && 'Sistema compatível'}
{requirements.status === 'warn' && 'Avisos encontrados'}
{requirements.status === 'fail' && 'Problemas encontrados'}
</p>
</div>
</div>
<Badge className={cn(
requirements.status === 'pass' && "bg-green-500",
requirements.status === 'warn' && "bg-yellow-500",
requirements.status === 'fail' && "bg-red-500"
)}>
{requirements.status === 'pass' && 'Compatível'}
{requirements.status === 'warn' && 'Avisos'}
{requirements.status === 'fail' && 'Incompatível'}
</Badge>
</div>
{/* Especificações do Sistema */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-1">
<MemoryStick className="w-4 h-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">RAM</span>
</div>
<p className="text-sm font-medium">{(requirements.ram_mb / 1024).toFixed(1)} GB</p>
</div>
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-1">
<HardDrive className="w-4 h-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Disco Livre</span>
</div>
<p className="text-sm font-medium">{requirements.disk_gb} GB</p>
</div>
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-1">
<Cpu className="w-4 h-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">CPU Cores</span>
</div>
<p className="text-sm font-medium">{requirements.cpu_cores}</p>
</div>
<div className="p-3 rounded-lg border bg-muted/30">
<div className="flex items-center gap-2 mb-1">
<Server className="w-4 h-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Sistema</span>
</div>
<p className="text-sm font-medium">{requirements.os} ({requirements.arch})</p>
</div>
</div>
{/* Progresso de Correção */}
{isFixing && fixProgress && (
<div className="space-y-2 p-4 rounded-lg border bg-blue-50 border-blue-200">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{fixProgress.message}</span>
<span>{fixProgress.progress}%</span>
</div>
<Progress value={fixProgress.progress} className="h-2" />
</div>
)}
{/* Problemas */}
{requirements.issues.length > 0 && (
<Alert variant="destructive">
<AlertCircle className="w-4 h-4" />
<AlertDescription>
<p className="font-medium mb-2">Problemas Encontrados:</p>
<ul className="space-y-2 text-sm">
{requirements.issues.map((issue, idx) => {
const solution = findProblemSolution(issue, requirements.os);
return (
<li key={idx} className="flex items-start justify-between gap-2">
<span className="flex-1">{issue}</span>
{solution && (
<div className="flex gap-1 shrink-0">
{solution.commands && solution.commands.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedProblem(solution);
setShowCommandsModal(true);
}}
className="h-7 text-xs"
>
<Terminal className="w-3 h-3 mr-1" />
Comandos
</Button>
)}
{solution.links.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={async () => {
if (solution.links[0]) {
try {
await invoke('open_url', { url: solution.links[0].url });
} catch (error) {
console.error('Error opening URL:', error);
window.open(solution.links[0].url, '_blank', 'noopener,noreferrer');
}
}
}}
className="h-7 text-xs"
>
<ExternalLink className="w-3 h-3 mr-1" />
Docs
</Button>
)}
</div>
)}
</li>
);
})}
</ul>
</AlertDescription>
</Alert>
)}
{/* Avisos */}
{requirements.warnings.length > 0 && (
<Alert>
<AlertCircle className="w-4 h-4" />
<AlertDescription>
<p className="font-medium mb-2">Avisos:</p>
<ul className="space-y-2 text-sm">
{requirements.warnings.map((warning, idx) => {
const solution = findProblemSolution(warning, requirements.os);
return (
<li key={idx} className="flex items-start justify-between gap-2">
<span className="flex-1">{warning}</span>
{solution && (
<div className="flex gap-1 shrink-0">
{solution.commands && solution.commands.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedProblem(solution);
setShowCommandsModal(true);
}}
className="h-7 text-xs"
>
<Terminal className="w-3 h-3 mr-1" />
Comandos
</Button>
)}
{solution.links.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={async () => {
if (solution.links[0]) {
try {
await invoke('open_url', { url: solution.links[0].url });
} catch (error) {
console.error('Error opening URL:', error);
window.open(solution.links[0].url, '_blank', 'noopener,noreferrer');
}
}
}}
className="h-7 text-xs"
>
<ExternalLink className="w-3 h-3 mr-1" />
Docs
</Button>
)}
</div>
)}
</li>
);
})}
</ul>
</AlertDescription>
</Alert>
)}
</>
) : null}
</CardContent>
</Card>
{/* Instalação Docker */}
{dockerAvailable === false && (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Download className="w-5 h-5 text-primary" />
<CardTitle className="text-lg">Instalar Docker</CardTitle>
</div>
<CardDescription>
Instale Docker automaticamente ou manualmente
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => {
window.open('https://www.docker.com/products/docker-desktop', '_blank');
}}
className="flex-1"
>
<ExternalLink className="w-4 h-4 mr-2" />
Instalação Manual
</Button>
<Button
variant="default"
onClick={handleInstallDocker}
disabled={isInstallingDocker || requirements?.status === 'fail'}
className="flex-1"
>
{isInstallingDocker ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Instalando...
</>
) : (
<>
<Zap className="w-4 h-4 mr-2" />
Instalar Agora
</>
)}
</Button>
</div>
{isInstallingDocker && (
<div className="space-y-3">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{dockerInstallProgress.currentStepName || 'Instalando Docker...'}</span>
<span className="text-muted-foreground">{dockerInstallProgress.progress}%</span>
</div>
<Progress value={dockerInstallProgress.progress} className="h-2" />
</div>
{dockerInstallProgress.logs.length > 0 && (
<div className="bg-black text-green-400 font-mono text-xs p-3 rounded-lg h-40 overflow-y-auto">
{dockerInstallProgress.logs.map((log, idx) => (
<div key={idx}>{log}</div>
))}
</div>
)}
</div>
)}
</CardContent>
</Card>
)}
{/* Validação */}
{dockerAvailable && (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-primary" />
<CardTitle className="text-lg">Validação</CardTitle>
</div>
<Button
variant="outline"
size="sm"
onClick={validateDocker}
disabled={isValidating}
>
{isValidating ? (
<>
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
Validando...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-1" />
Validar
</>
)}
</Button>
</div>
<CardDescription>
Execute testes para verificar se Docker está funcionando corretamente
</CardDescription>
</CardHeader>
<CardContent>
{validationResult ? (
<div className="space-y-2">
{validationResult.tests.map((test, idx) => (
<div
key={idx}
className={cn(
"flex items-center justify-between p-3 rounded-lg border",
test.status === 'pass' && "bg-green-50 border-green-200",
test.status === 'warn' && "bg-yellow-50 border-yellow-200",
test.status === 'fail' && "bg-red-50 border-red-200"
)}
>
<div className="flex-1">
<p className="text-sm font-medium">{test.name}</p>
{test.message && (
<p className="text-xs text-muted-foreground mt-1">{test.message}</p>
)}
</div>
{test.status === 'pass' ? (
<CheckCircle2 className="w-5 h-5 text-green-500 flex-shrink-0" />
) : test.status === 'warn' ? (
<AlertCircle className="w-5 h-5 text-yellow-500 flex-shrink-0" />
) : (
<XCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
)}
</div>
))}
{validationResult.docker_version && (
<p className="text-xs text-muted-foreground mt-2">
Docker {validationResult.docker_version} instalado
</p>
)}
</div>
) : (
<p className="text-sm text-muted-foreground text-center py-4">
Clique em "Validar" para executar os testes
</p>
)}
</CardContent>
</Card>
)}
{/* Botão para configurar SearXNG */}
{dockerAvailable && dockerRunning && (
<Card>
<CardContent className="pt-6">
<Button
variant="default"
onClick={() => setShowInstallWizard(true)}
className="w-full"
size="lg"
>
<Settings className="w-5 h-5 mr-2" />
Configurar SearXNG
</Button>
</CardContent>
</Card>
)}
{showInstallWizard && (
<SearxngInstallWizard
onComplete={() => {
setShowInstallWizard(false);
checkDocker();
toast({
title: 'SearXNG configurado',
description: 'SearXNG foi instalado e configurado com sucesso!',
});
}}
onCancel={() => setShowInstallWizard(false)}
initialUrl="http://localhost:8080"
/>
)}
<DockerCommandsModal
open={showCommandsModal}
onOpenChange={setShowCommandsModal}
problem={selectedProblem}
/>
</div>
);
}
@@ -0,0 +1,244 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import { useSettingsStore } from '@/store/settings-store';
import { invoke } from '@tauri-apps/api/core';
import { Zap, Cpu } from 'lucide-react';
interface LocalGgufModel {
path: string;
name: string;
size: number;
quantization?: string;
architecture?: string;
}
export function RuntimeSettingsPanel() {
const { runtime, llamaCppParams, setRuntime, setLlamaCppParams } = useSettingsStore();
const [localModels, setLocalModels] = useState<LocalGgufModel[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadLocalModels();
}, []);
const loadLocalModels = async () => {
try {
setLoading(true);
const models = await invoke<LocalGgufModel[]>('list_local_gguf_models');
setLocalModels(models);
} catch (error) {
console.error('Failed to load local GGUF models:', error);
} finally {
setLoading(false);
}
};
const hasLocalModels = localModels.length > 0;
const isLlamaCppAvailable = hasLocalModels;
return (
<Card>
<CardHeader>
<CardTitle>Runtime de Modelos</CardTitle>
<CardDescription>
Escolha entre Ollama (servidor externo) ou llama.cpp nativo (mais rápido, sem servidor)
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Toggle de Runtime */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="runtime-toggle">Usar llama.cpp nativo</Label>
<p className="text-sm text-muted-foreground">
{runtime === 'ollama'
? 'Usando Ollama como runtime (requer servidor externo)'
: 'Usando llama.cpp nativo (mais rápido, sem servidor externo)'}
</p>
</div>
<div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant={runtime === 'ollama' ? 'default' : 'secondary'} className="gap-1">
{runtime === 'ollama' ? (
<>
<span>🦙</span> Ollama
</>
) : (
<>
<Zap className="h-3 w-3" />
llama.cpp
</>
)}
</Badge>
</TooltipTrigger>
<TooltipContent>
{runtime === 'ollama'
? 'Usando Ollama como runtime (requer servidor externo)'
: 'Usando llama.cpp nativo (mais rápido, sem servidor externo)'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Switch
id="runtime-toggle"
checked={runtime === 'llama-cpp'}
onCheckedChange={(checked) => setRuntime(checked ? 'llama-cpp' : 'ollama')}
disabled={!isLlamaCppAvailable}
/>
</div>
</div>
{!isLlamaCppAvailable && (
<div className="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-3 text-sm text-yellow-800 dark:text-yellow-200">
<p>
Nenhum modelo GGUF local encontrado. Baixe modelos do Hugging Face para usar llama.cpp nativo.
</p>
</div>
)}
{loading && (
<div className="text-sm text-muted-foreground">Carregando modelos locais...</div>
)}
{hasLocalModels && (
<div className="rounded-md bg-muted p-3">
<p className="text-sm font-medium mb-2">
Modelos GGUF locais encontrados: {localModels.length}
</p>
<div className="space-y-1">
{localModels.slice(0, 5).map((model, idx) => (
<div key={idx} className="text-xs text-muted-foreground">
• {model.name} {model.quantization && `(${model.quantization})`}
</div>
))}
{localModels.length > 5 && (
<div className="text-xs text-muted-foreground">
... e mais {localModels.length - 5} modelo(s)
</div>
)}
</div>
</div>
)}
{/* Configurações do llama.cpp */}
{runtime === 'llama-cpp' && (
<div className="space-y-4 pt-4 border-t">
<div>
<Label>Camadas GPU (n_gpu_layers)</Label>
<p className="text-sm text-muted-foreground mb-2">
Número de camadas para carregar na GPU. 0 = CPU apenas
</p>
<div className="flex items-center gap-4">
<Slider
value={[llamaCppParams.nGpuLayers]}
onValueChange={([value]) =>
setLlamaCppParams({ nGpuLayers: value })
}
min={0}
max={100}
step={1}
className="flex-1"
/>
<Input
type="number"
value={llamaCppParams.nGpuLayers}
onChange={(e) =>
setLlamaCppParams({ nGpuLayers: parseInt(e.target.value) || 0 })
}
className="w-20"
min={0}
max={100}
/>
</div>
</div>
<div>
<Label>Tamanho do Contexto (n_ctx)</Label>
<p className="text-sm text-muted-foreground mb-2">
Tamanho máximo do contexto em tokens
</p>
<div className="flex items-center gap-4">
<Slider
value={[llamaCppParams.nCtx]}
onValueChange={([value]) =>
setLlamaCppParams({ nCtx: value })
}
min={512}
max={32768}
step={512}
className="flex-1"
/>
<Input
type="number"
value={llamaCppParams.nCtx}
onChange={(e) =>
setLlamaCppParams({ nCtx: parseInt(e.target.value) || 4096 })
}
className="w-24"
min={512}
max={32768}
step={512}
/>
</div>
</div>
<div>
<Label>Threads</Label>
<p className="text-sm text-muted-foreground mb-2">
Número de threads (0 = automático)
</p>
<Input
type="number"
value={llamaCppParams.threads || 0}
onChange={(e) =>
setLlamaCppParams({ threads: parseInt(e.target.value) || 0 })
}
className="w-32"
min={0}
max={32}
/>
</div>
<div>
<Label>Temperatura</Label>
<p className="text-sm text-muted-foreground mb-2">
Controla a aleatoriedade das respostas (0.0 - 2.0)
</p>
<div className="flex items-center gap-4">
<Slider
value={[llamaCppParams.temperature]}
onValueChange={([value]) =>
setLlamaCppParams({ temperature: value })
}
min={0}
max={2}
step={0.1}
className="flex-1"
/>
<Input
type="number"
value={llamaCppParams.temperature}
onChange={(e) =>
setLlamaCppParams({ temperature: parseFloat(e.target.value) || 0.7 })
}
className="w-20"
min={0}
max={2}
step={0.1}
/>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,209 @@
'use client';
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useToast } from '@/hooks/use-toast';
import { Loader2, Save, Search, Shield } from 'lucide-react';
interface SearchConfig {
region: string;
safe_search: string;
max_results: number;
enabled: boolean;
log_search_content: boolean;
retention_days: number;
}
export function SearchSettingsPanel() {
const { toast } = useToast();
const [config, setConfig] = useState<SearchConfig | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
loadConfig();
}, []);
const loadConfig = async () => {
setIsLoading(true);
try {
const loadedConfig = await invoke<SearchConfig>('load_search_config_command');
setConfig(loadedConfig);
} catch (error) {
console.error('Failed to load Search config:', error);
toast({
title: 'Error loading configuration',
description: String(error),
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
if (!config) return;
setIsSaving(true);
try {
await invoke('save_search_config_command', { config });
toast({
title: 'Configuration saved',
description: 'Search settings have been updated successfully.',
});
} catch (error) {
console.error('Failed to save Search config:', error);
toast({
title: 'Error saving configuration',
description: String(error),
variant: 'destructive',
});
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!config) {
return (
<div className="text-center p-8 text-muted-foreground">
Failed to load configuration.
<Button variant="outline" className="ml-4" onClick={loadConfig}>Retry</Button>
</div>
);
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Search className="h-5 w-5" />
DuckDuckGo Search Integration
</CardTitle>
<CardDescription>
Configure anonymous web search settings provided by DuckDuckGo.
</CardDescription>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={config.enabled}
onCheckedChange={(checked) => setConfig({ ...config, enabled: checked })}
id="search-enabled"
/>
<Label htmlFor="search-enabled">Enabled</Label>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="region">Region</Label>
<Select
value={config.region}
onValueChange={(val) => setConfig({ ...config, region: val })}
>
<SelectTrigger id="region">
<SelectValue placeholder="Select region" />
</SelectTrigger>
<SelectContent>
<SelectItem value="wt-wt">Global (wt-wt)</SelectItem>
<SelectItem value="us-en">United States (us-en)</SelectItem>
<SelectItem value="uk-en">United Kingdom (uk-en)</SelectItem>
<SelectItem value="br-pt">Brazil (br-pt)</SelectItem>
<SelectItem value="de-de">Germany (de-de)</SelectItem>
<SelectItem value="fr-fr">France (fr-fr)</SelectItem>
<SelectItem value="es-es">Spain (es-es)</SelectItem>
<SelectItem value="it-it">Italy (it-it)</SelectItem>
<SelectItem value="jp-jp">Japan (jp-jp)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="safe-search" className="flex items-center gap-2">
<Shield className="h-3 w-3" /> Safe Search
</Label>
<Select
value={config.safe_search}
onValueChange={(val) => setConfig({ ...config, safe_search: val })}
>
<SelectTrigger id="safe-search">
<SelectValue placeholder="Select safe search level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="on">Strict (On)</SelectItem>
<SelectItem value="moderate">Moderate</SelectItem>
<SelectItem value="off">Off</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="max-results">Max Results</Label>
<Input
id="max-results"
type="number"
value={config.max_results}
onChange={(e) => setConfig({ ...config, max_results: parseInt(e.target.value) || 10 })}
min={1}
max={50}
/>
</div>
<div className="space-y-2">
<Label htmlFor="retention">Log Retention (days)</Label>
<Input
id="retention"
type="number"
value={config.retention_days}
onChange={(e) => setConfig({ ...config, retention_days: parseInt(e.target.value) || 30 })}
min={1}
max={365}
/>
</div>
</div>
<div className="space-y-4 pt-4 border-t">
<h3 className="text-sm font-medium">Privacy</h3>
<div className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<Label className="text-sm">Log Search Content</Label>
<p className="text-xs text-muted-foreground">
Store query text in local history
</p>
</div>
<Switch
checked={config.log_search_content}
onCheckedChange={(checked) => setConfig({ ...config, log_search_content: checked })}
/>
</div>
</div>
<div className="flex justify-end pt-4">
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
Save Settings
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,881 @@
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import {
CheckCircle2,
XCircle,
Loader2,
ArrowLeft,
ArrowRight,
AlertCircle,
ExternalLink,
Download,
Settings,
Play,
Square,
X
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { cn } from '@/lib/utils';
interface SearxngInstallWizardProps {
onComplete: () => void;
onCancel: () => void;
initialUrl?: string;
}
type Step = 0 | 1 | 2 | 3 | 4 | 5;
interface SystemRequirements {
status: 'pass' | 'fail' | 'warn';
os: string;
arch: string;
ram_mb: number;
disk_gb: number;
cpu_cores: number;
kernel_version?: string;
issues: string[];
warnings: string[];
}
interface InstallProgressEvent {
event_type: 'step_start' | 'log' | 'step_complete' | 'error' | 'complete';
step?: number;
step_name?: string;
total_steps?: number;
progress: number;
message?: string;
log_line?: string;
}
interface ValidationResult {
status: 'pass' | 'fail' | 'partial';
tests: Array<{
name: string;
status: 'pass' | 'fail' | 'warn';
message?: string;
}>;
docker_version?: string;
}
export function SearxngInstallWizard({ onComplete, onCancel, initialUrl = 'http://localhost:8080' }: SearxngInstallWizardProps) {
const [currentStep, setCurrentStep] = useState<Step>(0);
const [requirements, setRequirements] = useState<SystemRequirements | null>(null);
const [isCheckingRequirements, setIsCheckingRequirements] = useState(false);
const [dockerAvailable, setDockerAvailable] = useState<boolean | null>(null);
const [dockerRunning, setDockerRunning] = useState<boolean | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [url, setUrl] = useState(initialUrl);
const [port, setPort] = useState('8080');
const [autoStart, setAutoStart] = useState(true);
const [installProgress, setInstallProgress] = useState<string>('');
const [isInstalling, setIsInstalling] = useState(false);
const [installError, setInstallError] = useState<string | null>(null);
const [dockerInstallProgress, setDockerInstallProgress] = useState<{
currentStep: number;
totalSteps: number;
progress: number;
currentStepName: string;
logs: string[];
}>({
currentStep: 0,
totalSteps: 0,
progress: 0,
currentStepName: '',
logs: [],
});
const [isInstallingDocker, setIsInstallingDocker] = useState(false);
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
const [isValidating, setIsValidating] = useState(false);
const [verificationStatus, setVerificationStatus] = useState<'checking' | 'success' | 'error' | null>(null);
const [verificationError, setVerificationError] = useState<string | null>(null);
// Step 0: Verificar Requisitos
useEffect(() => {
if (currentStep === 0) {
checkRequirements();
}
}, [currentStep]);
// Step 1: Verificar Docker
useEffect(() => {
if (currentStep === 1) {
checkDocker();
}
}, [currentStep]);
// Listener de eventos de progresso Docker
useEffect(() => {
let unlistenPromise: Promise<() => void> | null = null;
if (isInstallingDocker) {
unlistenPromise = listen<InstallProgressEvent>('docker-install-progress', (event) => {
const data = event.payload;
switch (data.event_type) {
case 'step_start':
setDockerInstallProgress(prev => ({
...prev,
currentStep: data.step || 0,
totalSteps: data.total_steps || prev.totalSteps,
currentStepName: data.step_name || '',
progress: data.progress,
}));
if (data.message) {
setDockerInstallProgress(prev => ({
...prev,
logs: [...prev.logs.slice(-49), data.message!],
}));
}
break;
case 'log':
if (data.log_line) {
setDockerInstallProgress(prev => ({
...prev,
logs: [...prev.logs.slice(-49), data.log_line!],
}));
}
break;
case 'step_complete':
setDockerInstallProgress(prev => ({
...prev,
progress: data.progress,
}));
if (data.message) {
setDockerInstallProgress(prev => ({
...prev,
logs: [...prev.logs.slice(-49), data.message!],
}));
}
break;
case 'error':
setInstallError(data.message || 'Erro desconhecido');
setIsInstallingDocker(false);
break;
case 'complete':
setDockerInstallProgress(prev => ({ ...prev, progress: 100 }));
setIsInstallingDocker(false);
// Validar instalação após conclusão
setTimeout(() => {
validateDocker();
}, 1000);
break;
}
});
}
return () => {
if (unlistenPromise) {
unlistenPromise.then(fn => fn());
}
};
}, [isInstallingDocker]);
const checkRequirements = async () => {
setIsCheckingRequirements(true);
try {
const reqs = await invoke<SystemRequirements>('check_docker_requirements');
setRequirements(reqs);
} catch (error) {
console.error('Error checking requirements:', error);
setRequirements({
status: 'fail',
os: 'unknown',
arch: 'unknown',
ram_mb: 0,
disk_gb: 0,
cpu_cores: 0,
issues: ['Erro ao verificar requisitos do sistema'],
warnings: [],
});
} finally {
setIsCheckingRequirements(false);
}
};
const checkDocker = async () => {
setIsChecking(true);
try {
const available = await invoke<boolean>('check_docker_available');
setDockerAvailable(available);
if (available) {
const running = await invoke<boolean>('check_docker_running');
setDockerRunning(running);
}
} catch (error) {
console.error('Error checking Docker:', error);
setDockerAvailable(false);
setDockerRunning(false);
} finally {
setIsChecking(false);
}
};
const handleNext = () => {
if (currentStep < 5) {
setCurrentStep((prev) => (prev + 1) as Step);
}
};
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep((prev) => (prev - 1) as Step);
}
};
const handleInstallDocker = async () => {
setIsInstallingDocker(true);
setInstallError(null);
setDockerInstallProgress({
currentStep: 0,
totalSteps: 0,
progress: 0,
currentStepName: '',
logs: [],
});
try {
await invoke('install_docker');
} catch (error) {
console.error('Docker installation error:', error);
setInstallError(error instanceof Error ? error.message : 'Erro desconhecido durante instalação do Docker');
setIsInstallingDocker(false);
}
};
const validateDocker = async () => {
setIsValidating(true);
setValidationResult(null);
try {
const result = await invoke<ValidationResult>('validate_docker_installation');
setValidationResult(result);
if (result.status === 'pass' || result.status === 'partial') {
// Docker validado, atualizar estado
setDockerAvailable(true);
setDockerRunning(true);
}
} catch (error) {
console.error('Validation error:', error);
setValidationResult({
status: 'fail',
tests: [{
name: 'Validação',
status: 'fail',
message: error instanceof Error ? error.message : 'Erro ao validar Docker',
}],
});
} finally {
setIsValidating(false);
}
};
const handleInstall = async () => {
setIsInstalling(true);
setInstallError(null);
setInstallProgress('Iniciando instalação...');
try {
setInstallProgress('Baixando imagem SearXNG...');
await invoke('install_searxng', { url, port: parseInt(port, 10) });
setInstallProgress('Criando container...');
// Installation is handled by install_searxng command
setInstallProgress('Iniciando serviços...');
await invoke('start_searxng');
setInstallProgress('Instalação concluída!');
setTimeout(() => {
handleNext();
}, 1000);
} catch (error) {
console.error('Installation error:', error);
setInstallError(error instanceof Error ? error.message : 'Erro desconhecido durante instalação');
} finally {
setIsInstalling(false);
}
};
const handleVerify = async () => {
setVerificationStatus('checking');
setVerificationError(null);
try {
const isAvailable = await invoke<boolean>('check_searxng_status', { url });
if (isAvailable) {
setVerificationStatus('success');
setTimeout(() => {
onComplete();
}, 1500);
} else {
setVerificationStatus('error');
setVerificationError('SearXNG não está respondendo. Verifique se o container está rodando.');
}
} catch (error) {
setVerificationStatus('error');
setVerificationError(error instanceof Error ? error.message : 'Erro ao verificar SearXNG');
}
};
const renderStep = () => {
switch (currentStep) {
case 0:
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Verificação de Requisitos do Sistema</h3>
<p className="text-sm text-muted-foreground">
Verificando se seu sistema atende aos requisitos para instalação do Docker.
</p>
</div>
{isCheckingRequirements ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<span className="ml-3 text-sm text-muted-foreground">Verificando requisitos...</span>
</div>
) : requirements ? (
<div className="space-y-4">
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center justify-between mb-4">
<span className="font-medium text-lg">Status Geral</span>
{requirements.status === 'pass' ? (
<Badge className="bg-green-500">Compatível</Badge>
) : requirements.status === 'warn' ? (
<Badge className="bg-yellow-500">Avisos</Badge>
) : (
<Badge className="bg-red-500">Incompatível</Badge>
)}
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<p className="text-xs text-muted-foreground">Sistema Operacional</p>
<p className="font-medium">{requirements.os} ({requirements.arch})</p>
</div>
<div>
<p className="text-xs text-muted-foreground">RAM</p>
<p className="font-medium">{(requirements.ram_mb / 1024).toFixed(1)} GB</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Disco Livre</p>
<p className="font-medium">{requirements.disk_gb} GB</p>
</div>
<div>
<p className="text-xs text-muted-foreground">CPU Cores</p>
<p className="font-medium">{requirements.cpu_cores}</p>
</div>
</div>
{requirements.kernel_version && (
<div className="mt-4">
<p className="text-xs text-muted-foreground">Versão do Kernel</p>
<p className="font-medium">{requirements.kernel_version}</p>
</div>
)}
</div>
{requirements.issues.length > 0 && (
<Alert variant="destructive">
<AlertCircle className="w-4 h-4" />
<AlertDescription>
<div className="space-y-2">
<p className="font-medium">Problemas Encontrados:</p>
<ul className="list-disc list-inside space-y-1 text-sm">
{requirements.issues.map((issue, idx) => (
<li key={idx}>{issue}</li>
))}
</ul>
</div>
</AlertDescription>
</Alert>
)}
{requirements.warnings.length > 0 && (
<Alert>
<AlertCircle className="w-4 h-4" />
<AlertDescription>
<div className="space-y-2">
<p className="font-medium">Avisos:</p>
<ul className="list-disc list-inside space-y-1 text-sm">
{requirements.warnings.map((warning, idx) => (
<li key={idx}>{warning}</li>
))}
</ul>
</div>
</AlertDescription>
</Alert>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={checkRequirements} disabled={isCheckingRequirements}>
Verificar Novamente
</Button>
<Button
onClick={handleNext}
disabled={requirements.status === 'fail'}
className="ml-auto"
>
Continuar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
) : null}
</div>
);
case 1:
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Verificação de Docker</h3>
<p className="text-sm text-muted-foreground">
SearXNG requer Docker para funcionar. Vamos verificar se está instalado e rodando.
</p>
</div>
{isChecking ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<span className="ml-3 text-sm text-muted-foreground">Verificando Docker...</span>
</div>
) : (
<div className="space-y-4">
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center justify-between mb-2">
<span className="font-medium">Docker Instalado</span>
{dockerAvailable === null ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : dockerAvailable ? (
<CheckCircle2 className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-red-500" />
)}
</div>
{dockerAvailable === false && (
<p className="text-xs text-muted-foreground mt-2">
Docker não está instalado. Você precisa instalá-lo primeiro.
</p>
)}
</div>
{dockerAvailable && (
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center justify-between mb-2">
<span className="font-medium">Docker Rodando</span>
{dockerRunning === null ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : dockerRunning ? (
<CheckCircle2 className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-red-500" />
)}
</div>
{dockerRunning === false && (
<p className="text-xs text-muted-foreground mt-2">
Docker está instalado mas não está rodando. Inicie o Docker Desktop.
</p>
)}
</div>
)}
{dockerAvailable === false && (
<Alert>
<AlertCircle className="w-4 h-4" />
<AlertDescription>
<div className="space-y-2">
<p>Docker não está instalado. Você pode instalá-lo agora ou manualmente.</p>
<div className="flex gap-2">
<Button
variant="default"
size="sm"
onClick={handleInstallDocker}
disabled={isInstallingDocker}
>
{isInstallingDocker ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Instalando...
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
Instalar Docker Agora
</>
)}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
window.open('https://www.docker.com/products/docker-desktop', '_blank');
}}
>
<ExternalLink className="w-4 h-4 mr-2" />
Instalar Manualmente
</Button>
</div>
</div>
</AlertDescription>
</Alert>
)}
{isInstallingDocker && (
<div className="space-y-4 mt-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>{dockerInstallProgress.currentStepName || 'Instalando Docker...'}</span>
<span>{dockerInstallProgress.progress}%</span>
</div>
<Progress value={dockerInstallProgress.progress} />
</div>
{dockerInstallProgress.logs.length > 0 && (
<div className="bg-black text-green-400 font-mono text-xs p-4 rounded-lg h-48 overflow-y-auto">
{dockerInstallProgress.logs.map((log, idx) => (
<div key={idx}>{log}</div>
))}
</div>
)}
</div>
)}
{validationResult && (
<div className="space-y-2 mt-4">
<h4 className="font-medium">Resultados da Validação:</h4>
{validationResult.tests.map((test, idx) => (
<div key={idx} className="flex items-center justify-between p-2 rounded border bg-muted/30">
<span className="text-sm">{test.name}</span>
{test.status === 'pass' ? (
<CheckCircle2 className="w-4 h-4 text-green-500" />
) : test.status === 'warn' ? (
<AlertCircle className="w-4 h-4 text-yellow-500" />
) : (
<XCircle className="w-4 h-4 text-red-500" />
)}
</div>
))}
{validationResult.docker_version && (
<p className="text-xs text-muted-foreground">
Docker {validationResult.docker_version} instalado
</p>
)}
</div>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={checkDocker} disabled={isChecking}>
Verificar Novamente
</Button>
{validationResult && (validationResult.status === 'pass' || validationResult.status === 'partial') && (
<Button
onClick={handleNext}
className="ml-auto"
>
Continuar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
)}
{!validationResult && dockerAvailable && dockerRunning && (
<Button
onClick={handleNext}
className="ml-auto"
>
Continuar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
)}
{!dockerAvailable && !isInstallingDocker && (
<Button
onClick={handleNext}
variant="outline"
className="ml-auto"
>
Pular Instalação Docker
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
)}
</div>
);
case 2:
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Configuração</h3>
<p className="text-sm text-muted-foreground">
Configure a URL e porta onde SearXNG será acessível.
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="searxng-url">URL do SearXNG</Label>
<Input
id="searxng-url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="http://localhost:8080"
/>
<p className="text-xs text-muted-foreground">
URL completa onde o SearXNG estará acessível
</p>
</div>
<div className="space-y-2">
<Label htmlFor="searxng-port">Porta</Label>
<Input
id="searxng-port"
type="number"
value={port}
onChange={(e) => setPort(e.target.value)}
placeholder="8080"
min="1024"
max="65535"
/>
<p className="text-xs text-muted-foreground">
Porta que será usada pelo container (padrão: 8080)
</p>
</div>
<div className="flex items-center space-x-2 p-4 rounded-lg border bg-muted/30">
<Checkbox
id="auto-start"
checked={autoStart}
onCheckedChange={(checked) => setAutoStart(checked === true)}
/>
<Label htmlFor="auto-start" className="cursor-pointer">
Iniciar automaticamente com o OllaHub
</Label>
</div>
</div>
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
<Button onClick={handleNext} className="ml-auto">
Continuar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Instalação do SearXNG</h3>
<p className="text-sm text-muted-foreground">
Instalando e configurando SearXNG. Isso pode levar alguns minutos.
</p>
</div>
{isInstalling ? (
<div className="space-y-4">
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center gap-3">
<Loader2 className="w-5 h-5 animate-spin text-primary" />
<div className="flex-1">
<p className="font-medium">{installProgress}</p>
<div className="mt-2 h-2 bg-muted rounded-full overflow-hidden">
<div className="h-full bg-primary animate-pulse" style={{ width: '60%' }} />
</div>
</div>
</div>
</div>
</div>
) : installError ? (
<Alert variant="destructive">
<XCircle className="w-4 h-4" />
<AlertDescription>
<p className="font-medium mb-2">Erro durante instalação</p>
<p className="text-sm">{installError}</p>
<Button
variant="outline"
size="sm"
onClick={handleInstall}
className="mt-3"
>
Tentar Novamente
</Button>
</AlertDescription>
</Alert>
) : (
<div className="p-4 rounded-lg border bg-green-500/10 border-green-500/20">
<div className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium">Instalação concluída!</p>
<p className="text-sm text-muted-foreground">
SearXNG foi instalado com sucesso.
</p>
</div>
</div>
</div>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack} disabled={isInstalling}>
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
{!isInstalling && !installError && (
<Button onClick={handleNext} className="ml-auto">
Continuar
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
)}
{isInstalling && (
<Button variant="outline" onClick={onCancel} className="ml-auto">
Cancelar
</Button>
)}
</div>
</div>
);
case 5:
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Verificação</h3>
<p className="text-sm text-muted-foreground">
Verificando se SearXNG está funcionando corretamente.
</p>
</div>
{verificationStatus === null && (
<div className="p-4 rounded-lg border bg-muted/30 text-center">
<p className="text-sm text-muted-foreground mb-4">
Clique em "Verificar" para testar a conexão com SearXNG.
</p>
<Button onClick={handleVerify} size="lg">
<Play className="w-4 h-4 mr-2" />
Verificar
</Button>
</div>
)}
{verificationStatus === 'checking' && (
<div className="p-4 rounded-lg border bg-muted/30">
<div className="flex items-center justify-center gap-3">
<Loader2 className="w-5 h-5 animate-spin text-primary" />
<span>Verificando conexão...</span>
</div>
</div>
)}
{verificationStatus === 'success' && (
<div className="p-4 rounded-lg border bg-green-500/10 border-green-500/20">
<div className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium">SearXNG está disponível!</p>
<p className="text-sm text-muted-foreground">
A conexão foi estabelecida com sucesso.
</p>
</div>
</div>
</div>
)}
{verificationStatus === 'error' && (
<Alert variant="destructive">
<XCircle className="w-4 h-4" />
<AlertDescription>
<p className="font-medium mb-2">Falha na verificação</p>
<p className="text-sm mb-3">{verificationError || 'SearXNG não está respondendo'}</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleVerify}>
Tentar Novamente
</Button>
<Button
variant="outline"
size="sm"
onClick={async () => {
try {
await invoke('start_searxng');
setTimeout(handleVerify, 2000);
} catch (error) {
console.error('Error starting SearXNG:', error);
}
}}
>
<Play className="w-4 h-4 mr-2" />
Iniciar Container
</Button>
</div>
</AlertDescription>
</Alert>
)}
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack} disabled={verificationStatus === 'checking'}>
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
{verificationStatus === 'success' && (
<Button onClick={onComplete} className="ml-auto">
Concluir
<CheckCircle2 className="w-4 h-4 ml-2" />
</Button>
)}
</div>
</div>
);
default:
return null;
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
<Card className="w-full max-w-2xl relative">
<Button
variant="ghost"
size="icon"
onClick={onCancel}
className="absolute top-2 right-2 sm:top-4 sm:right-4 z-10"
aria-label="Fechar"
disabled={isInstalling || isInstallingDocker}
>
<X className="w-4 h-4" />
</Button>
<CardHeader>
<CardTitle>SearXNG - Instalação</CardTitle>
<CardDescription>
Passo {currentStep + 1} de 6
</CardDescription>
</CardHeader>
<CardContent>
{renderStep()}
</CardContent>
</Card>
</div>
);
}
+186
View File
@@ -0,0 +1,186 @@
'use client';
import { CheckCircle2, Shield, Infinity, Globe, Settings, ArrowRight, X, ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
interface SearxngOnboardingProps {
onInstall: () => void;
onSkip: () => void;
onLearnMore?: () => void;
}
export function SearxngOnboarding({ onInstall, onSkip, onLearnMore }: SearxngOnboardingProps) {
const benefits = [
{
icon: Infinity,
title: 'Sem limites de queries',
description: 'Enquanto Brave API oferece 2000 queries/mês, SearXNG é ilimitado',
color: 'text-green-500',
},
{
icon: Shield,
title: 'Privacidade total',
description: 'Sem rastreamento, sem cookies, sem logs. Seus dados ficam privados',
color: 'text-blue-500',
},
{
icon: Globe,
title: 'Agrega múltiplos motores',
description: 'Combina resultados de Google, Bing, DuckDuckGo e outros em uma única busca',
color: 'text-purple-500',
},
{
icon: Settings,
title: 'Controle total',
description: 'Configure motores, filtros e personalizações conforme sua necessidade',
color: 'text-orange-500',
},
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
<Card className="w-full max-w-3xl max-h-[90vh] overflow-y-auto relative">
<Button
variant="ghost"
size="icon"
onClick={onSkip}
className="absolute top-2 right-2 sm:top-4 sm:right-4 z-10"
aria-label="Fechar"
>
<X className="w-4 h-4" />
</Button>
<CardHeader className="text-center space-y-4 pb-6">
<div className="mx-auto w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
<Globe className="w-8 h-8 text-primary" />
</div>
<CardTitle className="text-2xl sm:text-3xl">SearXNG Self-Hosted</CardTitle>
<CardDescription className="text-base">
Busca Ilimitada e Privada
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Por que SearXNG? */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-green-500" />
Por que SearXNG?
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{benefits.map((benefit, idx) => {
const Icon = benefit.icon;
return (
<div
key={idx}
className="p-4 rounded-lg border border-muted bg-muted/30 hover:bg-muted/50 transition-colors"
>
<div className="flex items-start gap-3">
<Icon className={cn('w-5 h-5 mt-0.5 flex-shrink-0', benefit.color)} />
<div className="space-y-1 flex-1">
<h4 className="font-medium text-sm">{benefit.title}</h4>
<p className="text-xs text-muted-foreground">{benefit.description}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Como Funciona */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Globe className="w-5 h-5 text-blue-500" />
Como Funciona
</h3>
<div className="p-4 rounded-lg border border-muted bg-muted/30">
<div className="flex items-center justify-center gap-2 sm:gap-4 flex-wrap">
<div className="flex flex-col items-center gap-2">
<div className="px-4 py-2 rounded-md bg-blue-500/10 border border-blue-500/20 text-sm font-medium">
Brave API
</div>
<span className="text-xs text-muted-foreground">Tier 1</span>
</div>
<ArrowRight className="w-5 h-5 text-muted-foreground" />
<div className="flex flex-col items-center gap-2">
<div className="px-4 py-2 rounded-md bg-green-500/10 border border-green-500/20 text-sm font-medium">
SearXNG
</div>
<span className="text-xs text-muted-foreground">Tier 2</span>
</div>
<ArrowRight className="w-5 h-5 text-muted-foreground" />
<div className="flex flex-col items-center gap-2">
<div className="px-4 py-2 rounded-md bg-yellow-500/10 border border-yellow-500/20 text-sm font-medium">
DuckDuckGo
</div>
<span className="text-xs text-muted-foreground">Tier 3</span>
</div>
</div>
<p className="text-sm text-muted-foreground mt-4 text-center">
SearXNG é usado como fallback quando Brave API esgota sua quota mensal (2000 queries).
É completamente opcional e não afeta o uso normal do sistema.
</p>
</div>
</div>
{/* Requisitos */}
<div className="space-y-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Settings className="w-5 h-5 text-orange-500" />
Requisitos
</h3>
<div className="space-y-2">
<div className="flex items-start gap-3 p-3 rounded-lg border border-muted bg-muted/30">
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm">Docker instalado e rodando</p>
<p className="text-xs text-muted-foreground">
O SearXNG roda em um container Docker. Se não tiver Docker, você pode instalá-lo durante o processo.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg border border-muted bg-muted/30">
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm">~200MB de RAM</p>
<p className="text-xs text-muted-foreground">
O container SearXNG consome aproximadamente 200MB de memória RAM.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 rounded-lg border border-muted bg-muted/30">
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-sm">Porta 8080 disponível</p>
<p className="text-xs text-muted-foreground">
Por padrão, SearXNG usa a porta 8080. Você pode configurar uma porta diferente se necessário.
</p>
</div>
</div>
</div>
</div>
{/* Ações */}
<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t">
<Button onClick={onInstall} className="flex-1" size="lg">
<ArrowRight className="w-4 h-4 mr-2" />
Instalar Agora
</Button>
{onLearnMore && (
<Button onClick={onLearnMore} variant="outline" className="flex-1" size="lg">
<ExternalLink className="w-4 h-4 mr-2" />
Saiba Mais
</Button>
)}
<Button onClick={onSkip} variant="ghost" className="flex-1" size="lg">
<X className="w-4 h-4 mr-2" />
Pular por Enquanto
</Button>
</div>
</CardContent>
</Card>
</div>
);
}