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 - Implementada transformação dos dados para o formato esperado
- Adicionado tratamento para valores nulos - Adicionado tratamento para valores nulos
- Melhorada tipagem dos dados retornados - 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 { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 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 { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
interface EssayAnalysis { interface EssayAnalysis {
id: string; id: string;
@ -123,155 +124,177 @@ export function EssayAnalysis() {
} }
} }
if (loading) return <div>Carregando...</div>; if (loading) {
if (!essay || !analysis) return <div>Análise não encontrada</div>; 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 ( return (
<div className="container mx-auto p-6"> <div className="container mx-auto p-6">
<div className="flex items-center gap-4 mb-6"> {/* Cabeçalho */}
<Button <div className="flex items-center justify-between mb-8">
variant="ghost" <div className="flex items-center gap-4">
onClick={() => navigate(`/aluno/redacoes/${id}`)} <Button
trackingId="essay-analysis-back-button" variant="ghost"
trackingProperties={{ onClick={() => navigate(`/aluno/redacoes/${id}`)}
action: 'back_to_essay', className="text-gray-600 hover:text-gray-900"
page: 'essay_analysis', >
essay_id: id <ArrowLeft className="mr-2 h-4 w-4" />
}} Voltar para redação
> </Button>
<ArrowLeft className="mr-2 h-4 w-4" /> <div>
Voltar para redação <h1 className="text-3xl font-bold text-gray-900">{essay.title}</h1>
</Button> <p className="text-gray-500">
<div> {essay.essay_type.title} {essay.essay_genre.title}
<h1 className="text-3xl font-bold">{essay.title}</h1> </p>
<p className="text-muted-foreground"> </div>
{essay.essay_type.title} {essay.essay_genre.title}
</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> {/* Conteúdo Principal */}
{/* Pontuação Geral */} <div className="space-y-8">
<Card> {/* Cartão de Pontuação */}
<CardHeader> <Card className="bg-gradient-to-br from-purple-50 to-white border-purple-100">
<CardTitle>Pontuação Geral</CardTitle> <CardContent className="pt-6">
</CardHeader> <div className="flex flex-col items-center justify-center">
<CardContent> <div className="flex items-center mb-2">
<div className="flex items-center justify-center"> <span className="text-6xl font-bold text-purple-600">{analysis.overall_score}</span>
<div className="text-6xl font-bold text-primary"> <span className="text-2xl text-purple-600 ml-2">/100</span>
{analysis.overall_score}
</div> </div>
<div className="text-2xl ml-1">/100</div> <div className="text-gray-500">Pontuação Geral</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Pontos Fortes */} {/* Grid de Métricas */}
<Card> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<CardHeader> {/* Pontos Fortes */}
<CardTitle className="flex items-center gap-2"> <Card className="bg-gradient-to-br from-green-50 to-white border-green-100">
<CheckCircle2 className="h-5 w-5 text-success" /> <CardHeader>
Pontos Fortes <CardTitle className="flex items-center gap-2 text-green-700">
</CardTitle> <CheckCircle2 className="h-5 w-5" />
</CardHeader> Pontos Fortes
<CardContent> </CardTitle>
<ul className="list-disc list-inside space-y-2"> </CardHeader>
{analysis.strengths.map((strength, index) => ( <CardContent>
<li key={index} className="text-success">{strength}</li> <ul className="space-y-2">
))} {analysis.strengths.map((strength, index) => (
</ul> <li key={index} className="flex items-start gap-2 text-green-700">
</CardContent> <div className="w-1.5 h-1.5 rounded-full bg-green-500 mt-2" />
</Card> <span>{strength}</span>
</li>
))}
</ul>
</CardContent>
</Card>
{/* Pontos a Melhorar */} {/* Pontos a Melhorar */}
<Card> <Card className="bg-gradient-to-br from-amber-50 to-white border-amber-100">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2 text-amber-700">
<XCircle className="h-5 w-5 text-destructive" /> <XCircle className="h-5 w-5" />
Pontos a Melhorar Pontos a Melhorar
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="list-disc list-inside space-y-2"> <ul className="space-y-2">
{analysis.improvements.map((improvement, index) => ( {analysis.improvements.map((improvement, index) => (
<li key={index} className="text-destructive">{improvement}</li> <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>
</CardContent> </Card>
</Card> </div>
{/* Feedback Detalhado */} {/* Feedback Detalhado */}
<Card className="md:col-span-2"> <Card>
<CardHeader> <CardHeader>
<CardTitle>Feedback Detalhado</CardTitle> <CardTitle>Feedback Detalhado</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div className="space-y-2">
<h4 className="font-semibold mb-2">Estrutura</h4> <h4 className="font-semibold text-gray-900">Estrutura</h4>
<p className="text-muted-foreground">{analysis.feedback.structure}</p> <p className="text-gray-600">{analysis.feedback.structure}</p>
</div> </div>
<div> <div className="space-y-2">
<h4 className="font-semibold mb-2">Conteúdo</h4> <h4 className="font-semibold text-gray-900">Conteúdo</h4>
<p className="text-muted-foreground">{analysis.feedback.content}</p> <p className="text-gray-600">{analysis.feedback.content}</p>
</div> </div>
<div> <div className="space-y-2">
<h4 className="font-semibold mb-2">Linguagem</h4> <h4 className="font-semibold text-gray-900">Linguagem</h4>
<p className="text-muted-foreground">{analysis.feedback.language}</p> <p className="text-gray-600">{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> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Sugestões */} {/* Sugestões */}
<Card className="md:col-span-3"> <Card className="bg-gradient-to-br from-blue-50 to-white border-blue-100">
<CardHeader> <CardHeader>
<CardTitle>Sugestões para Melhoria</CardTitle> <CardTitle className="text-blue-700">Sugestões para Melhoria</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-muted-foreground whitespace-pre-line"> <p className="text-blue-700 whitespace-pre-line">
{analysis.suggestions} {analysis.suggestions}
</p> </p>
</CardContent> </CardContent>
@ -279,4 +302,4 @@ export function EssayAnalysis() {
</div> </div>
</div> </div>
); );
} }

View File

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