feat: add emoji-regex and remove-accents dependencies; enhance chat and setup pages

- Added `emoji-regex` and `remove-accents` packages to improve text processing capabilities.
- Implemented automatic Ollama startup in the chat page for better user experience.
- Enhanced setup page to include GPU selection based on hardware specifications.
- Updated chat input to support real-time validation and improved user feedback.
- Refactored message handling in chat to incorporate new processing features.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-26 16:12:16 -03:00
1 parent 4275295e4f
commit 84ea2dec19
51 files changed
+7857 -573

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
'use client';
import { useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Check, Cpu, Monitor, Zap, Laptop } from "lucide-react";
import { GpuInfo } from "@/lib/recommendation";
import { useSettingsStore } from "@/store/settings-store";
import { cn } from "@/lib/utils";
interface GpuSelectorProps {
gpus: GpuInfo[];
}
export function GpuSelector({ gpus }: GpuSelectorProps) {
const selectedGpu = useSettingsStore((state) => state.selectedGpu);
const setSelectedGpu = useSettingsStore((state) => state.setSelectedGpu);
// Se não há GPUs ou apenas uma, não mostrar seletor
if (gpus.length === 0) {
return null;
}
// Se há apenas uma GPU, selecionar automaticamente
if (gpus.length === 1 && !selectedGpu) {
setSelectedGpu(gpus[0].id);
}
// Selecionar primeira GPU por padrão se nenhuma estiver selecionada
useEffect(() => {
if (!selectedGpu && gpus.length > 0) {
setSelectedGpu(gpus[0].id);
}
}, [selectedGpu, gpus, setSelectedGpu]);
const getVendorIcon = (vendor: string | null) => {
const iconClass = "h-5 w-5";
switch (vendor?.toUpperCase()) {
case 'NVIDIA':
return <Zap className={cn(iconClass, "text-green-500")} />;
case 'AMD':
return <Zap className={cn(iconClass, "text-red-500")} />;
case 'INTEL':
return <Cpu className={cn(iconClass, "text-blue-500")} />;
case 'APPLE':
return <Monitor className={cn(iconClass, "text-gray-500")} />;
default:
return <Monitor className={cn(iconClass, "text-muted-foreground")} />;
}
};
const formatMemory = (memoryMb: number | null) => {
if (!memoryMb) return null;
if (memoryMb >= 1024) {
return `${(memoryMb / 1024).toFixed(1)} GB`;
}
return `${memoryMb} MB`;
};
return (
<Card className="w-full max-w-2xl mx-auto bg-muted/30 animate-in fade-in slide-in-from-bottom-4">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Monitor className="text-primary" />
Placas de Vídeo Detectadas
</CardTitle>
<CardDescription>
Selecione qual GPU você deseja usar para processamento de IA
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{gpus.map((gpu) => {
const isSelected = selectedGpu === gpu.id;
const memoryStr = formatMemory(gpu.memory_mb);
return (
<button
key={gpu.id}
onClick={() => setSelectedGpu(gpu.id)}
className={cn(
"relative p-4 rounded-lg border-2 transition-all text-left",
"hover:border-primary/50 hover:bg-accent/50",
isSelected
? "border-primary bg-primary/10 shadow-md"
: "border-border bg-background"
)}
>
{isSelected && (
<div className="absolute top-2 right-2">
<div className="h-6 w-6 rounded-full bg-primary flex items-center justify-center">
<Check className="h-4 w-4 text-primary-foreground" />
</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-start gap-2">
<div className="mt-0.5">
{getVendorIcon(gpu.vendor)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-sm truncate">{gpu.name}</h3>
{gpu.vendor && (
<Badge variant="outline" className="mt-1 text-xs">
{gpu.vendor}
</Badge>
)}
</div>
</div>
{memoryStr && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Cpu className="h-3 w-3" />
<span>{memoryStr} VRAM</span>
</div>
)}
{!memoryStr && gpu.vendor && (
<div className="text-xs text-muted-foreground">
{gpu.vendor === 'Intel' ? 'GPU Integrada' : 'GPU Dedicada'}
</div>
)}
</div>
</button>
);
})}
</div>
{gpus.length === 1 && (
<p className="text-sm text-muted-foreground mt-4 text-center">
Apenas uma GPU foi detectada e será usada automaticamente.
</p>
)}
</CardContent>
</Card>
);
}
+72 -13
View File
@@ -14,12 +14,22 @@ interface ModelSelectorProps {
onComplete: () => void;
}
interface DownloadInfo {
status: string;
percent?: number;
downloaded?: string;
total?: string;
speed?: string;
raw: string;
}
export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps) {
const [selectedModelId, setSelectedModelId] = useState(recommendation.modelId);
const [isInstalled, setIsInstalled] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const [downloadStatus, setDownloadStatus] = useState("");
const [downloadInfo, setDownloadInfo] = useState<DownloadInfo | null>(null);
const selectedModel = RECOMMENDED_MODELS.find(m => m.id === selectedModelId) || RECOMMENDED_MODELS[0];
@@ -29,25 +39,60 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
useEffect(() => {
const unlisten = listen<string>('download-progress', (event) => {
const line = event.payload;
setDownloadStatus(line);
const payload = event.payload;
// Simple heuristic parsing for progress
// Ollama output example: "pulling manifest", "downloading 10%", "verifying sha256 digest"
if (line.includes('%')) {
const match = line.match(/(\d+)%/);
if (match) {
setDownloadProgress(parseInt(match[1], 10));
// Tentar parsear como JSON estruturado
let info: DownloadInfo | null = null;
try {
info = JSON.parse(payload) as DownloadInfo;
} catch {
// Fallback: tratar como string raw
info = {
status: "downloading",
raw: payload,
};
}
if (info) {
setDownloadInfo(info);
// Atualizar status text
const statusText = info.status === "pulling" ? "Baixando modelo..." :
info.status === "verifying" ? "Verificando..." :
info.status === "writing" ? "Salvando..." :
info.status === "success" ? "Concluído!" :
"Baixando...";
setDownloadStatus(statusText);
// Atualizar progresso
if (info.percent !== undefined) {
setDownloadProgress(info.percent);
} else {
// Fallback: estimar progresso baseado no status
if (info.status === "pulling") {
setDownloadProgress(prev => prev < 10 ? 10 : prev);
} else if (info.status === "verifying") {
setDownloadProgress(90);
} else if (info.status === "writing") {
setDownloadProgress(95);
}
}
// Se chegou a 100% ou success, marcar como instalado após um pequeno delay
if (info.percent === 100 || info.status === "success") {
setTimeout(() => {
setIsInstalled(true);
setIsDownloading(false);
checkInstallation(selectedModelId);
}, 500);
}
} else if (line.includes('success')) {
setDownloadProgress(100);
}
});
return () => {
unlisten.then(f => f());
};
}, []);
}, [selectedModelId]);
const checkInstallation = async (modelId: string) => {
try {
@@ -61,13 +106,15 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
const handleDownload = async () => {
setIsDownloading(true);
setDownloadProgress(0);
setDownloadStatus("Iniciando download...");
try {
await invoke('pull_model', { name: selectedModelId });
setIsInstalled(true);
setIsDownloading(false);
// O status será atualizado pelo listener quando o download completar
// Não precisamos setar aqui pois o listener já faz isso
} catch (error) {
console.error("Download failed", error);
setDownloadStatus("Falha no download. Tente novamente.");
setDownloadProgress(0);
setIsDownloading(false);
}
};
@@ -128,6 +175,18 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
<span>{downloadProgress}%</span>
</div>
<Progress value={downloadProgress} className="h-2" />
{downloadInfo && (downloadInfo.downloaded || downloadInfo.speed) && (
<div className="flex justify-between text-xs text-muted-foreground">
<span>
{downloadInfo.downloaded && downloadInfo.total
? `${downloadInfo.downloaded} / ${downloadInfo.total}`
: downloadInfo.downloaded || ""}
</span>
{downloadInfo.speed && (
<span>{downloadInfo.speed}</span>
)}
</div>
)}
</div>
)}
</CardContent>