feat: adiciona novos recursos de formatação e tracking no editor

This commit is contained in:
Lucas Santana 2025-02-07 10:32:28 -03:00
parent 46e8ba0312
commit ccbac66d28
3 changed files with 223 additions and 186 deletions

View File

@ -294,3 +294,33 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
- Implementado feedback visual durante operações de salvamento
- Otimizado carregamento inicial da redação
- Adicionado tratamento de estados para diferentes status da redação
## [0.2.0] - 2024-03-21
### Adicionado
- Novos recursos de formatação no editor:
- Tachado (strike-through)
- Código inline
- Lista com marcadores
- Lista numerada
- Citação (blockquote)
- Rastreamento de eventos (tracking) em todos os botões do editor
### Modificado
- Melhorias no fluxo de redações:
- Carregamento correto do conteúdo após submissão para análise
- Salvamento automático do conteúdo antes da submissão
- Badge verde para status "analisada"
- Botão "Ver Análise" para redações analisadas
- Editor em modo somente leitura após submissão
- Contagem de palavras em todos os estados da redação
### Técnico
- Refatoração do componente Editor para incluir novos recursos de formatação
- Adição de trackingId em todos os botões para análise de uso
- Melhorias de acessibilidade com aria-labels em português
- Refatoração do EssayPage para incluir lógica de salvamento antes da submissão para análise
- Melhoria na query do Supabase para incluir conteúdo explicitamente
- Implementação de feedback visual durante operações de salvamento
- Otimização do carregamento inicial da redação
- Adição de tratamento de estado para diferentes status da redação

View File

@ -18,6 +18,11 @@ import {
AlignCenter,
AlignRight,
Highlighter,
Strikethrough,
Code,
List,
ListOrdered,
Quote,
} from 'lucide-react'
interface EditorProps {
@ -47,6 +52,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleBold().run()}
className={cn(editor.isActive('bold') && 'bg-muted')}
aria-label="Negrito"
trackingId="editor-bold-button"
>
<Bold className="h-4 w-4" />
</Button>
@ -56,6 +62,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleItalic().run()}
className={cn(editor.isActive('italic') && 'bg-muted')}
aria-label="Itálico"
trackingId="editor-italic-button"
>
<Italic className="h-4 w-4" />
</Button>
@ -65,6 +72,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleUnderline().run()}
className={cn(editor.isActive('underline') && 'bg-muted')}
aria-label="Sublinhado"
trackingId="editor-underline-button"
>
<UnderlineIcon className="h-4 w-4" />
</Button>
@ -74,9 +82,60 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleHighlight().run()}
className={cn(editor.isActive('highlight') && 'bg-muted')}
aria-label="Destacar"
trackingId="editor-highlight-button"
>
<Highlighter className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleStrike().run()}
className={cn(editor.isActive('strike') && 'bg-muted')}
aria-label="Tachado"
trackingId="editor-strike-button"
>
<Strikethrough className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleCode().run()}
className={cn(editor.isActive('code') && 'bg-muted')}
aria-label="Código"
trackingId="editor-code-button"
>
<Code className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={cn(editor.isActive('bulletList') && 'bg-muted')}
aria-label="Lista com marcadores"
trackingId="editor-bullet-list-button"
>
<List className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={cn(editor.isActive('orderedList') && 'bg-muted')}
aria-label="Lista numerada"
trackingId="editor-ordered-list-button"
>
<ListOrdered className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={cn(editor.isActive('blockquote') && 'bg-muted')}
aria-label="Citação"
trackingId="editor-blockquote-button"
>
<Quote className="h-4 w-4" />
</Button>
<div className="mx-2 w-[1px] bg-border" />
<Button
variant="ghost"
@ -84,6 +143,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().setTextAlign('left').run()}
className={cn(editor.isActive({ textAlign: 'left' }) && 'bg-muted')}
aria-label="Alinhar à esquerda"
trackingId="editor-align-left-button"
>
<AlignLeft className="h-4 w-4" />
</Button>
@ -93,6 +153,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().setTextAlign('center').run()}
className={cn(editor.isActive({ textAlign: 'center' }) && 'bg-muted')}
aria-label="Centralizar"
trackingId="editor-align-center-button"
>
<AlignCenter className="h-4 w-4" />
</Button>
@ -102,6 +163,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().setTextAlign('right').run()}
className={cn(editor.isActive({ textAlign: 'right' }) && 'bg-muted')}
aria-label="Alinhar à direita"
trackingId="editor-align-right-button"
>
<AlignRight className="h-4 w-4" />
</Button>
@ -124,7 +186,6 @@ export function Editor({
heading: false,
codeBlock: false,
horizontalRule: false,
table: false,
}),
Placeholder.configure({
placeholder,

View File

@ -3,9 +3,8 @@ 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, Loader2 } from 'lucide-react';
import { ArrowLeft, CheckCircle2, XCircle } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
interface EssayAnalysis {
id: string;
@ -73,50 +72,14 @@ export function EssayAnalysis() {
// Carregar análise
const { data: analysisData, error: analysisError } = await supabase
.from('essay_analyses')
.select(`
*,
feedback:essay_analysis_feedback(
structure_feedback,
content_feedback,
language_feedback
),
strengths:essay_analysis_strengths(strength),
improvements:essay_analysis_improvements(improvement),
scores:essay_analysis_scores(
adequacy,
coherence,
cohesion,
vocabulary,
grammar
)
`)
.select('*')
.eq('essay_id', id)
.order('created_at', { ascending: false })
.limit(1)
.single();
if (analysisError) throw analysisError;
// Transformar os dados para o formato esperado
const formattedAnalysis = {
...analysisData,
feedback: {
structure: analysisData.feedback[0]?.structure_feedback || '',
content: analysisData.feedback[0]?.content_feedback || '',
language: analysisData.feedback[0]?.language_feedback || ''
},
strengths: analysisData.strengths?.map(s => s.strength) || [],
improvements: analysisData.improvements?.map(i => i.improvement) || [],
criteria_scores: {
adequacy: analysisData.scores[0]?.adequacy || 0,
coherence: analysisData.scores[0]?.coherence || 0,
cohesion: analysisData.scores[0]?.cohesion || 0,
vocabulary: analysisData.scores[0]?.vocabulary || 0,
grammar: analysisData.scores[0]?.grammar || 0
}
};
setAnalysis(formattedAnalysis);
setAnalysis(analysisData);
} catch (error) {
console.error('Erro ao carregar dados:', error);
} finally {
@ -124,177 +87,160 @@ export function EssayAnalysis() {
}
}
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>
);
}
if (loading) return <div>Carregando...</div>;
if (!essay || !analysis) return <div>Análise não encontrada</div>;
return (
<div className="container mx-auto p-6">
{/* 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 className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
onClick={() => navigate('/aluno/redacoes')}
className="text-purple-600 hover:text-purple-700"
trackingId="essay-analysis-back-to-list-button"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar para lista de redações
</Button>
<Button
variant="ghost"
onClick={() => navigate(`/aluno/redacoes/${id}`)}
className="text-gray-600 hover:text-gray-900"
trackingId="essay-analysis-back-to-essay-button"
>
<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>
</div>
</div>
{/* 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 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}
</div>
<div className="text-gray-500">Pontuação Geral</div>
<div className="text-2xl ml-1">/100</div>
</div>
</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 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>
{/* 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>
))}
</CardContent>
</Card>
</div>
</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>
))}
</ul>
</CardContent>
</Card>
{/* Feedback Detalhado */}
<Card>
<Card className="md:col-span-2">
<CardHeader>
<CardTitle>Feedback Detalhado</CardTitle>
</CardHeader>
<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>
<CardContent className="space-y-4">
<div>
<h4 className="font-semibold mb-2">Estrutura</h4>
<p className="text-muted-foreground">{analysis.feedback.structure}</p>
</div>
<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>
<h4 className="font-semibold mb-2">Conteúdo</h4>
<p className="text-muted-foreground">{analysis.feedback.content}</p>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-gray-900">Linguagem</h4>
<p className="text-gray-600">{analysis.feedback.language}</p>
<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>
</CardContent>
</Card>
{/* Sugestões */}
<Card className="bg-gradient-to-br from-blue-50 to-white border-blue-100">
<Card className="md:col-span-3">
<CardHeader>
<CardTitle className="text-blue-700">Sugestões para Melhoria</CardTitle>
<CardTitle>Sugestões para Melhoria</CardTitle>
</CardHeader>
<CardContent>
<p className="text-blue-700 whitespace-pre-line">
<p className="text-muted-foreground whitespace-pre-line">
{analysis.suggestions}
</p>
</CardContent>
@ -302,4 +248,4 @@ export function EssayAnalysis() {
</div>
</div>
);
}
}