mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
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:
parent
c94c46f5c1
commit
46e8ba0312
17
CHANGELOG.md
17
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user