Spaces:
No application file
No application file
| import React, { 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 { Progress } from '@/components/ui/progress'; | |
| import { Plus, Play, Pause, Download, Trash2, Eye } from 'lucide-react'; | |
| import '../App.css'; | |
| const Dashboard = ({ onCreateProject, onViewProject }) => { | |
| const [projects, setProjects] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| fetchProjects(); | |
| }, []); | |
| const fetchProjects = async () => { | |
| try { | |
| const response = await fetch('/api/projects'); | |
| const data = await response.json(); | |
| setProjects(data); | |
| } catch (error) { | |
| console.error('Erro ao buscar projetos:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const getStatusColor = (status) => { | |
| switch (status) { | |
| case 'completed': | |
| return 'bg-green-500'; | |
| case 'running': | |
| return 'bg-blue-500'; | |
| case 'failed': | |
| return 'bg-red-500'; | |
| case 'pending': | |
| return 'bg-yellow-500'; | |
| default: | |
| return 'bg-gray-500'; | |
| } | |
| }; | |
| const getStatusText = (status) => { | |
| switch (status) { | |
| case 'completed': | |
| return 'Concluído'; | |
| case 'running': | |
| return 'Treinando'; | |
| case 'failed': | |
| return 'Falhou'; | |
| case 'pending': | |
| return 'Pendente'; | |
| case 'cancelled': | |
| return 'Cancelado'; | |
| default: | |
| return 'Desconhecido'; | |
| } | |
| }; | |
| const handleStartTraining = async (projectId) => { | |
| try { | |
| await fetch(`/api/projects/${projectId}/start-training`, { | |
| method: 'POST', | |
| }); | |
| fetchProjects(); // Refresh projects | |
| } catch (error) { | |
| console.error('Erro ao iniciar treinamento:', error); | |
| } | |
| }; | |
| const handleStopTraining = async (projectId) => { | |
| try { | |
| await fetch(`/api/projects/${projectId}/stop-training`, { | |
| method: 'POST', | |
| }); | |
| fetchProjects(); // Refresh projects | |
| } catch (error) { | |
| console.error('Erro ao parar treinamento:', error); | |
| } | |
| }; | |
| const handleDeleteProject = async (projectId) => { | |
| if (window.confirm('Tem certeza que deseja excluir este projeto?')) { | |
| try { | |
| await fetch(`/api/projects/${projectId}`, { | |
| method: 'DELETE', | |
| }); | |
| fetchProjects(); // Refresh projects | |
| } catch (error) { | |
| console.error('Erro ao excluir projeto:', error); | |
| } | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center h-64"> | |
| <div className="text-lg">Carregando projetos...</div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Header */} | |
| <div className="flex justify-between items-center"> | |
| <div> | |
| <h1 className="text-3xl font-bold">Dashboard LoRA</h1> | |
| <p className="text-gray-600 mt-2"> | |
| Gerencie seus projetos de treinamento LoRA | |
| </p> | |
| </div> | |
| <Button onClick={onCreateProject} className="flex items-center gap-2"> | |
| <Plus className="w-4 h-4" /> | |
| Novo Projeto | |
| </Button> | |
| </div> | |
| {/* Stats Cards */} | |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> | |
| <Card> | |
| <CardHeader className="pb-2"> | |
| <CardTitle className="text-sm font-medium">Total de Projetos</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold">{projects.length}</div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader className="pb-2"> | |
| <CardTitle className="text-sm font-medium">Em Treinamento</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {projects.filter(p => p.status === 'running').length} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader className="pb-2"> | |
| <CardTitle className="text-sm font-medium">Concluídos</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {projects.filter(p => p.status === 'completed').length} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader className="pb-2"> | |
| <CardTitle className="text-sm font-medium">Falharam</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {projects.filter(p => p.status === 'failed').length} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Projects List */} | |
| <div className="space-y-4"> | |
| <h2 className="text-xl font-semibold">Projetos Recentes</h2> | |
| {projects.length === 0 ? ( | |
| <Card> | |
| <CardContent className="flex flex-col items-center justify-center py-12"> | |
| <div className="text-gray-500 text-center"> | |
| <h3 className="text-lg font-medium mb-2">Nenhum projeto encontrado</h3> | |
| <p className="mb-4">Crie seu primeiro projeto LoRA para começar</p> | |
| <Button onClick={onCreateProject} className="flex items-center gap-2"> | |
| <Plus className="w-4 h-4" /> | |
| Criar Primeiro Projeto | |
| </Button> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ) : ( | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> | |
| {projects.map((project) => ( | |
| <Card key={project.id} className="hover:shadow-lg transition-shadow"> | |
| <CardHeader> | |
| <div className="flex justify-between items-start"> | |
| <div> | |
| <CardTitle className="text-lg">{project.name}</CardTitle> | |
| <CardDescription className="mt-1"> | |
| {project.description || 'Sem descrição'} | |
| </CardDescription> | |
| </div> | |
| <Badge className={getStatusColor(project.status)}> | |
| {getStatusText(project.status)} | |
| </Badge> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="text-sm text-gray-600"> | |
| <div>Modelo Base: {project.base_model}</div> | |
| <div>Rank: {project.rank} | Alpha: {project.alpha}</div> | |
| <div>Épocas: {project.current_epoch || 0}/{project.num_epochs}</div> | |
| </div> | |
| {project.status === 'running' && ( | |
| <div className="space-y-2"> | |
| <div className="flex justify-between text-sm"> | |
| <span>Progresso</span> | |
| <span>{Math.round((project.progress || 0) * 100)}%</span> | |
| </div> | |
| <Progress value={(project.progress || 0) * 100} /> | |
| {project.current_loss && ( | |
| <div className="text-sm text-gray-600"> | |
| Loss atual: {project.current_loss.toFixed(4)} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| <div className="flex gap-2 flex-wrap"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => onViewProject(project.id)} | |
| className="flex items-center gap-1" | |
| > | |
| <Eye className="w-3 h-3" /> | |
| Ver | |
| </Button> | |
| {project.status === 'pending' || project.status === 'failed' ? ( | |
| <Button | |
| size="sm" | |
| onClick={() => handleStartTraining(project.id)} | |
| className="flex items-center gap-1" | |
| > | |
| <Play className="w-3 h-3" /> | |
| Iniciar | |
| </Button> | |
| ) : project.status === 'running' ? ( | |
| <Button | |
| variant="destructive" | |
| size="sm" | |
| onClick={() => handleStopTraining(project.id)} | |
| className="flex items-center gap-1" | |
| > | |
| <Pause className="w-3 h-3" /> | |
| Parar | |
| </Button> | |
| ) : project.status === 'completed' ? ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="flex items-center gap-1" | |
| > | |
| <Download className="w-3 h-3" /> | |
| Download | |
| </Button> | |
| ) : null} | |
| {project.status !== 'running' && ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handleDeleteProject(project.id)} | |
| className="flex items-center gap-1 text-red-600 hover:text-red-700" | |
| > | |
| <Trash2 className="w-3 h-3" /> | |
| Excluir | |
| </Button> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Dashboard; | |