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:
1 parent
4275295e4f
commit
84ea2dec19
51 files changed
+7857
-573
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user