fix: corrige fluxo de redações e visualização pós-análise - Corrige carregamento do conteúdo após envio - Adiciona salvamento automático antes da análise - Melhora UX com feedback visual e badges

This commit is contained in:
Lucas Santana 2025-02-07 10:20:48 -03:00
parent c94c46f5c1
commit 46e8ba0312
3 changed files with 210 additions and 135 deletions

View File

@ -277,3 +277,20 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
- Implementada transformação dos dados para o formato esperado
- Adicionado tratamento para valores nulos
- Melhorada tipagem dos dados retornados
### Modificado
- Melhorado o fluxo de redações:
- Corrigido carregamento do conteúdo da redação após envio para análise
- Adicionado salvamento automático do conteúdo antes de enviar para análise
- Melhorada visualização do status 'analisada' com badge verde
- Adicionado botão "Ver Análise" para redações analisadas
- Ajustado Editor para modo somente leitura após envio
- Melhorada contagem de palavras em todos os estados da redação
### Técnico
- Refatorado componente `EssayPage`:
- Adicionada lógica de salvamento antes do envio para análise
- Melhorada query do Supabase para incluir conteúdo explicitamente
- Implementado feedback visual durante operações de salvamento
- Otimizado carregamento inicial da redação
- Adicionado tratamento de estados para diferentes status da redação

View File

@ -3,8 +3,9 @@ import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, CheckCircle2, XCircle } from 'lucide-react';
import { ArrowLeft, CheckCircle2, XCircle, Loader2 } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
interface EssayAnalysis {
id: string;
@ -123,155 +124,177 @@ export function EssayAnalysis() {
}
}
if (loading) return <div>Carregando...</div>;
if (!essay || !analysis) return <div>Análise não encontrada</div>;
if (loading) {
return (
<div className="container mx-auto p-6">
<div className="animate-pulse space-y-6">
<div className="h-8 w-48 bg-gray-200 rounded" />
<div className="h-96 bg-gray-200 rounded-xl" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<div key={i} className="h-48 bg-gray-200 rounded-xl" />
))}
</div>
</div>
</div>
);
}
if (!essay || !analysis) {
return (
<div className="container mx-auto p-6">
<div className="text-center py-12">
<div className="text-red-500 mb-4">Análise não encontrada</div>
<Button
variant="ghost"
onClick={() => navigate('/aluno/redacoes')}
className="text-purple-600 hover:text-purple-700"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar para redações
</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto p-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
onClick={() => navigate(`/aluno/redacoes/${id}`)}
trackingId="essay-analysis-back-button"
trackingProperties={{
action: 'back_to_essay',
page: 'essay_analysis',
essay_id: id
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar para redação
</Button>
<div>
<h1 className="text-3xl font-bold">{essay.title}</h1>
<p className="text-muted-foreground">
{essay.essay_type.title} {essay.essay_genre.title}
</p>
{/* Cabeçalho */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Button
variant="ghost"
onClick={() => navigate(`/aluno/redacoes/${id}`)}
className="text-gray-600 hover:text-gray-900"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar para redação
</Button>
<div>
<h1 className="text-3xl font-bold text-gray-900">{essay.title}</h1>
<p className="text-gray-500">
{essay.essay_type.title} {essay.essay_genre.title}
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Pontuação Geral */}
<Card>
<CardHeader>
<CardTitle>Pontuação Geral</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center">
<div className="text-6xl font-bold text-primary">
{analysis.overall_score}
{/* Conteúdo Principal */}
<div className="space-y-8">
{/* Cartão de Pontuação */}
<Card className="bg-gradient-to-br from-purple-50 to-white border-purple-100">
<CardContent className="pt-6">
<div className="flex flex-col items-center justify-center">
<div className="flex items-center mb-2">
<span className="text-6xl font-bold text-purple-600">{analysis.overall_score}</span>
<span className="text-2xl text-purple-600 ml-2">/100</span>
</div>
<div className="text-2xl ml-1">/100</div>
<div className="text-gray-500">Pontuação Geral</div>
</div>
</CardContent>
</Card>
{/* Pontos Fortes */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-success" />
Pontos Fortes
</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc list-inside space-y-2">
{analysis.strengths.map((strength, index) => (
<li key={index} className="text-success">{strength}</li>
))}
</ul>
</CardContent>
</Card>
{/* Grid de Métricas */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Pontos Fortes */}
<Card className="bg-gradient-to-br from-green-50 to-white border-green-100">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-green-700">
<CheckCircle2 className="h-5 w-5" />
Pontos Fortes
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{analysis.strengths.map((strength, index) => (
<li key={index} className="flex items-start gap-2 text-green-700">
<div className="w-1.5 h-1.5 rounded-full bg-green-500 mt-2" />
<span>{strength}</span>
</li>
))}
</ul>
</CardContent>
</Card>
{/* Pontos a Melhorar */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<XCircle className="h-5 w-5 text-destructive" />
Pontos a Melhorar
</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc list-inside space-y-2">
{analysis.improvements.map((improvement, index) => (
<li key={index} className="text-destructive">{improvement}</li>
{/* Pontos a Melhorar */}
<Card className="bg-gradient-to-br from-amber-50 to-white border-amber-100">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-amber-700">
<XCircle className="h-5 w-5" />
Pontos a Melhorar
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{analysis.improvements.map((improvement, index) => (
<li key={index} className="flex items-start gap-2 text-amber-700">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-2" />
<span>{improvement}</span>
</li>
))}
</ul>
</CardContent>
</Card>
{/* Critérios de Avaliação */}
<Card>
<CardHeader>
<CardTitle>Critérios de Avaliação</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{[
{ label: 'Adequação ao Gênero', value: analysis.criteria_scores.adequacy, color: 'bg-blue-500' },
{ label: 'Coerência', value: analysis.criteria_scores.coherence, color: 'bg-green-500' },
{ label: 'Coesão', value: analysis.criteria_scores.cohesion, color: 'bg-purple-500' },
{ label: 'Vocabulário', value: analysis.criteria_scores.vocabulary, color: 'bg-orange-500' },
{ label: 'Gramática', value: analysis.criteria_scores.grammar, color: 'bg-pink-500' }
].map((criterion) => (
<div key={criterion.label}>
<div className="flex justify-between mb-1 text-sm">
<span className="text-gray-600">{criterion.label}</span>
<span className="font-medium">{criterion.value}%</span>
</div>
<div className="h-2 rounded-full bg-gray-100">
<div
className={cn("h-full rounded-full transition-all", criterion.color)}
style={{ width: `${criterion.value}%` }}
/>
</div>
</div>
))}
</ul>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
{/* Feedback Detalhado */}
<Card className="md:col-span-2">
<Card>
<CardHeader>
<CardTitle>Feedback Detalhado</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-semibold mb-2">Estrutura</h4>
<p className="text-muted-foreground">{analysis.feedback.structure}</p>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Estrutura</h4>
<p className="text-gray-600">{analysis.feedback.structure}</p>
</div>
<div>
<h4 className="font-semibold mb-2">Conteúdo</h4>
<p className="text-muted-foreground">{analysis.feedback.content}</p>
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Conteúdo</h4>
<p className="text-gray-600">{analysis.feedback.content}</p>
</div>
<div>
<h4 className="font-semibold mb-2">Linguagem</h4>
<p className="text-muted-foreground">{analysis.feedback.language}</p>
</div>
</CardContent>
</Card>
{/* Critérios de Avaliação */}
<Card>
<CardHeader>
<CardTitle>Critérios de Avaliação</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="flex justify-between mb-1">
<span>Adequação ao Gênero</span>
<span>{analysis.criteria_scores.adequacy}%</span>
</div>
<Progress value={analysis.criteria_scores.adequacy} />
</div>
<div>
<div className="flex justify-between mb-1">
<span>Coerência</span>
<span>{analysis.criteria_scores.coherence}%</span>
</div>
<Progress value={analysis.criteria_scores.coherence} />
</div>
<div>
<div className="flex justify-between mb-1">
<span>Coesão</span>
<span>{analysis.criteria_scores.cohesion}%</span>
</div>
<Progress value={analysis.criteria_scores.cohesion} />
</div>
<div>
<div className="flex justify-between mb-1">
<span>Vocabulário</span>
<span>{analysis.criteria_scores.vocabulary}%</span>
</div>
<Progress value={analysis.criteria_scores.vocabulary} />
</div>
<div>
<div className="flex justify-between mb-1">
<span>Gramática</span>
<span>{analysis.criteria_scores.grammar}%</span>
</div>
<Progress value={analysis.criteria_scores.grammar} />
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Linguagem</h4>
<p className="text-gray-600">{analysis.feedback.language}</p>
</div>
</CardContent>
</Card>
{/* Sugestões */}
<Card className="md:col-span-3">
<Card className="bg-gradient-to-br from-blue-50 to-white border-blue-100">
<CardHeader>
<CardTitle>Sugestões para Melhoria</CardTitle>
<CardTitle className="text-blue-700">Sugestões para Melhoria</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground whitespace-pre-line">
<p className="text-blue-700 whitespace-pre-line">
{analysis.suggestions}
</p>
</CardContent>
@ -279,4 +302,4 @@ export function EssayAnalysis() {
</div>
</div>
);
}
}

View File

@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { ArrowLeft, Save, Send, Trash2 } from 'lucide-react';
import { ArrowLeft, Save, Send, Trash2, BarChart3 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
AlertDialog,
@ -77,13 +77,20 @@ export function EssayPage() {
.select(`
*,
essay_type:essay_types(*),
essay_genre:essay_genres(*)
essay_genre:essay_genres(*),
content
`)
.eq('id', id)
.single();
if (error) throw error;
setEssay(data);
// Atualizar contagem de palavras
if (data?.content) {
const words = data.content.trim().split(/\s+/).length;
setWordCount(words);
}
} catch (error) {
console.error('Erro ao carregar redação:', error);
} finally {
@ -114,13 +121,19 @@ export function EssayPage() {
async function submitForAnalysis() {
if (!essay) return;
try {
// Primeiro atualiza o status
const { error: updateError } = await supabase
setSaving(true);
// Primeiro salvar o conteúdo atual
const { error: saveError } = await supabase
.from('student_essays')
.update({ status: 'submitted' })
.update({
title: essay.title,
content: essay.content,
status: 'submitted'
})
.eq('id', essay.id);
if (updateError) throw updateError;
if (saveError) throw saveError;
// Chama a Edge Function para análise
const { error: analysisError } = await supabase.functions.invoke('analyze-essay', {
@ -138,6 +151,8 @@ export function EssayPage() {
navigate(`/aluno/redacoes/${essay.id}/analise`);
} catch (error) {
console.error('Erro ao enviar para análise:', error);
} finally {
setSaving(false);
}
}
@ -217,9 +232,9 @@ export function EssayPage() {
<span></span>
<AdaptiveText text={essay.essay_genre?.title} isUpperCase={isUpperCase} />
<span></span>
<Badge variant={essay.status === 'draft' ? 'secondary' : 'default'}>
<Badge variant={essay.status === 'draft' ? 'secondary' : essay.status === 'analyzed' ? 'success' : 'default'}>
<AdaptiveText
text={essay.status === 'draft' ? 'Rascunho' : 'Enviada'}
text={essay.status === 'draft' ? 'Rascunho' : essay.status === 'analyzed' ? 'Analisada' : 'Enviada'}
isUpperCase={isUpperCase}
/>
</Badge>
@ -278,16 +293,36 @@ export function EssayPage() {
</Button>
</>
)}
{essay.status === 'analyzed' && (
<Button
variant="default"
onClick={() => navigate(`/aluno/redacoes/${essay.id}/analise`)}
className="bg-purple-600 hover:bg-purple-700 text-white"
trackingId="essay-view-analysis-button"
trackingProperties={{
action: 'view_analysis',
page: 'essay_editor'
}}
>
<BarChart3 className="mr-2 h-4 w-4" />
<AdaptiveText text="Ver Análise" isUpperCase={isUpperCase} />
</Button>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3">
<Editor
content={essay.content}
onChange={(newContent) => setEssay({ ...essay, content: newContent })}
placeholder="Escreva sua redação aqui..."
content={essay.content || ''}
onChange={(newContent) => essay.status === 'draft' ? setEssay({ ...essay, content: newContent }) : null}
placeholder={essay.status === 'draft' ? "Escreva sua redação aqui..." : ""}
readOnly={essay.status !== 'draft'}
className={cn(
"min-h-[400px]",
essay.status !== 'draft' && "bg-gray-50"
)}
/>
<div className="mt-2 text-sm">
<span className={cn(
@ -296,7 +331,7 @@ export function EssayPage() {
)}>
{wordCount} palavras
</span>
{!isWithinWordLimit && (
{essay.status === 'draft' && !isWithinWordLimit && (
<span className="text-red-600">
{' '}(mínimo: {essay.essay_genre.requirements.min_words},
máximo: {essay.essay_genre.requirements.max_words})