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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | |
| import { ArrowLeft, Play, Pause, Download, RefreshCw } from 'lucide-react'; | |
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; | |
| import '../App.css'; | |
| const ProjectView = ({ projectId, onBack }) => { | |
| const [project, setProject] = useState(null); | |
| const [logs, setLogs] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| const [refreshing, setRefreshing] = useState(false); | |
| useEffect(() => { | |
| fetchProject(); | |
| fetchLogs(); | |
| // Auto-refresh if project is running | |
| const interval = setInterval(() => { | |
| if (project?.status === 'running') { | |
| fetchProject(); | |
| fetchLogs(); | |
| } | |
| }, 5000); | |
| return () => clearInterval(interval); | |
| }, [projectId, project?.status]); | |
| const fetchProject = async () => { | |
| try { | |
| const response = await fetch(`/api/projects/${projectId}`); | |
| const data = await response.json(); | |
| setProject(data); | |
| } catch (error) { | |
| console.error('Erro ao buscar projeto:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const fetchLogs = async () => { | |
| try { | |
| const response = await fetch(`/api/projects/${projectId}/logs`); | |
| const data = await response.json(); | |
| setLogs(data.logs || []); | |
| } catch (error) { | |
| console.error('Erro ao buscar logs:', error); | |
| } | |
| }; | |
| const handleRefresh = async () => { | |
| setRefreshing(true); | |
| await fetchProject(); | |
| await fetchLogs(); | |
| setRefreshing(false); | |
| }; | |
| const handleStartTraining = async () => { | |
| try { | |
| await fetch(`/api/projects/${projectId}/start-training`, { | |
| method: 'POST', | |
| }); | |
| fetchProject(); | |
| } catch (error) { | |
| console.error('Erro ao iniciar treinamento:', error); | |
| } | |
| }; | |
| const handleStopTraining = async () => { | |
| try { | |
| await fetch(`/api/projects/${projectId}/stop-training`, { | |
| method: 'POST', | |
| }); | |
| fetchProject(); | |
| } catch (error) { | |
| console.error('Erro ao parar treinamento:', error); | |
| } | |
| }; | |
| 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'; | |
| } | |
| }; | |
| // Generate mock training data for chart | |
| const generateTrainingData = () => { | |
| if (!project || !project.current_epoch) return []; | |
| const data = []; | |
| for (let i = 1; i <= project.current_epoch; i++) { | |
| data.push({ | |
| epoch: i, | |
| loss: Math.max(0.1, 1.0 - (i / project.num_epochs) * 0.8 + (Math.random() - 0.5) * 0.1) | |
| }); | |
| } | |
| return data; | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center h-64"> | |
| <div className="text-lg">Carregando projeto...</div> | |
| </div> | |
| ); | |
| } | |
| if (!project) { | |
| return ( | |
| <div className="flex items-center justify-center h-64"> | |
| <div className="text-lg text-red-600">Projeto não encontrado</div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-4"> | |
| <Button variant="outline" onClick={onBack} className="flex items-center gap-2"> | |
| <ArrowLeft className="w-4 h-4" /> | |
| Voltar | |
| </Button> | |
| <div> | |
| <h1 className="text-3xl font-bold">{project.name}</h1> | |
| <p className="text-gray-600 mt-2"> | |
| {project.description || 'Sem descrição'} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Button | |
| variant="outline" | |
| onClick={handleRefresh} | |
| disabled={refreshing} | |
| className="flex items-center gap-2" | |
| > | |
| <RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} /> | |
| Atualizar | |
| </Button> | |
| {project.status === 'pending' || project.status === 'failed' ? ( | |
| <Button onClick={handleStartTraining} className="flex items-center gap-2"> | |
| <Play className="w-4 h-4" /> | |
| Iniciar Treinamento | |
| </Button> | |
| ) : project.status === 'running' ? ( | |
| <Button | |
| variant="destructive" | |
| onClick={handleStopTraining} | |
| className="flex items-center gap-2" | |
| > | |
| <Pause className="w-4 h-4" /> | |
| Parar Treinamento | |
| </Button> | |
| ) : project.status === 'completed' ? ( | |
| <Button variant="outline" className="flex items-center gap-2"> | |
| <Download className="w-4 h-4" /> | |
| Download Modelo | |
| </Button> | |
| ) : null} | |
| </div> | |
| </div> | |
| {/* Status Card */} | |
| <Card> | |
| <CardHeader> | |
| <div className="flex justify-between items-center"> | |
| <CardTitle>Status do Projeto</CardTitle> | |
| <Badge className={getStatusColor(project.status)}> | |
| {getStatusText(project.status)} | |
| </Badge> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div> | |
| <div className="text-sm text-gray-600">Modelo Base</div> | |
| <div className="font-medium">{project.base_model}</div> | |
| </div> | |
| <div> | |
| <div className="text-sm text-gray-600">Progresso</div> | |
| <div className="font-medium"> | |
| Época {project.current_epoch || 0} de {project.num_epochs} | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-sm text-gray-600">Criado em</div> | |
| <div className="font-medium"> | |
| {new Date(project.created_at).toLocaleDateString('pt-BR')} | |
| </div> | |
| </div> | |
| </div> | |
| {project.status === 'running' && ( | |
| <div className="mt-4 space-y-2"> | |
| <div className="flex justify-between text-sm"> | |
| <span>Progresso do Treinamento</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> | |
| )} | |
| {project.error_message && ( | |
| <div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md"> | |
| <div className="text-sm text-red-800"> | |
| <strong>Erro:</strong> {project.error_message} | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| {/* Tabs */} | |
| <Tabs defaultValue="config" className="space-y-4"> | |
| <TabsList> | |
| <TabsTrigger value="config">Configuração</TabsTrigger> | |
| <TabsTrigger value="training">Treinamento</TabsTrigger> | |
| <TabsTrigger value="logs">Logs</TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="config" className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Parâmetros LoRA</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-2"> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Rank:</span> | |
| <span>{project.rank}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Alpha:</span> | |
| <span>{project.alpha}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Dropout:</span> | |
| <span>{project.dropout}</span> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Parâmetros de Treinamento</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-2"> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Taxa de Aprendizado:</span> | |
| <span>{project.learning_rate}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Batch Size:</span> | |
| <span>{project.batch_size}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Épocas:</span> | |
| <span>{project.num_epochs}</span> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Otimizações</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-2"> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Otimizador 8-bit:</span> | |
| <span>{project.use_8bit_optimizer ? 'Sim' : 'Não'}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Gradient Checkpointing:</span> | |
| <span>{project.use_gradient_checkpointing ? 'Sim' : 'Não'}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Precisão Mista:</span> | |
| <span>{project.mixed_precision}</span> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Dataset</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-2"> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Caminho:</span> | |
| <span className="text-sm truncate"> | |
| {project.dataset_path || 'Não carregado'} | |
| </span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-600">Número de Imagens:</span> | |
| <span>{project.num_images || 'N/A'}</span> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="training" className="space-y-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Gráfico de Loss</CardTitle> | |
| <CardDescription> | |
| Acompanhe a evolução da loss durante o treinamento | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <LineChart data={generateTrainingData()}> | |
| <CartesianGrid strokeDasharray="3 3" /> | |
| <XAxis dataKey="epoch" /> | |
| <YAxis /> | |
| <Tooltip /> | |
| <Line | |
| type="monotone" | |
| dataKey="loss" | |
| stroke="#3b82f6" | |
| strokeWidth={2} | |
| dot={{ fill: '#3b82f6' }} | |
| /> | |
| </LineChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="logs" className="space-y-4"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Logs de Treinamento</CardTitle> | |
| <CardDescription> | |
| Logs em tempo real do processo de treinamento | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="bg-gray-900 text-green-400 p-4 rounded-md font-mono text-sm h-64 overflow-y-auto"> | |
| {logs.length === 0 ? ( | |
| <div className="text-gray-500">Nenhum log disponível</div> | |
| ) : ( | |
| logs.map((log, index) => ( | |
| <div key={index} className="mb-1"> | |
| {log} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| </Tabs> | |
| </div> | |
| ); | |
| }; | |
| export default ProjectView; | |