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 - Implementado feedback visual durante operações de salvamento
- Otimizado carregamento inicial da redação - Otimizado carregamento inicial da redação
- Adicionado tratamento de estados para diferentes status 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, AlignCenter,
AlignRight, AlignRight,
Highlighter, Highlighter,
Strikethrough,
Code,
List,
ListOrdered,
Quote,
} from 'lucide-react' } from 'lucide-react'
interface EditorProps { interface EditorProps {
@ -47,6 +52,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
className={cn(editor.isActive('bold') && 'bg-muted')} className={cn(editor.isActive('bold') && 'bg-muted')}
aria-label="Negrito" aria-label="Negrito"
trackingId="editor-bold-button"
> >
<Bold className="h-4 w-4" /> <Bold className="h-4 w-4" />
</Button> </Button>
@ -56,6 +62,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
className={cn(editor.isActive('italic') && 'bg-muted')} className={cn(editor.isActive('italic') && 'bg-muted')}
aria-label="Itálico" aria-label="Itálico"
trackingId="editor-italic-button"
> >
<Italic className="h-4 w-4" /> <Italic className="h-4 w-4" />
</Button> </Button>
@ -65,6 +72,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleUnderline().run()} onClick={() => editor.chain().focus().toggleUnderline().run()}
className={cn(editor.isActive('underline') && 'bg-muted')} className={cn(editor.isActive('underline') && 'bg-muted')}
aria-label="Sublinhado" aria-label="Sublinhado"
trackingId="editor-underline-button"
> >
<UnderlineIcon className="h-4 w-4" /> <UnderlineIcon className="h-4 w-4" />
</Button> </Button>
@ -74,9 +82,60 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().toggleHighlight().run()} onClick={() => editor.chain().focus().toggleHighlight().run()}
className={cn(editor.isActive('highlight') && 'bg-muted')} className={cn(editor.isActive('highlight') && 'bg-muted')}
aria-label="Destacar" aria-label="Destacar"
trackingId="editor-highlight-button"
> >
<Highlighter className="h-4 w-4" /> <Highlighter className="h-4 w-4" />
</Button> </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" /> <div className="mx-2 w-[1px] bg-border" />
<Button <Button
variant="ghost" variant="ghost"
@ -84,6 +143,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().setTextAlign('left').run()} onClick={() => editor.chain().focus().setTextAlign('left').run()}
className={cn(editor.isActive({ textAlign: 'left' }) && 'bg-muted')} className={cn(editor.isActive({ textAlign: 'left' }) && 'bg-muted')}
aria-label="Alinhar à esquerda" aria-label="Alinhar à esquerda"
trackingId="editor-align-left-button"
> >
<AlignLeft className="h-4 w-4" /> <AlignLeft className="h-4 w-4" />
</Button> </Button>
@ -93,6 +153,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().setTextAlign('center').run()} onClick={() => editor.chain().focus().setTextAlign('center').run()}
className={cn(editor.isActive({ textAlign: 'center' }) && 'bg-muted')} className={cn(editor.isActive({ textAlign: 'center' }) && 'bg-muted')}
aria-label="Centralizar" aria-label="Centralizar"
trackingId="editor-align-center-button"
> >
<AlignCenter className="h-4 w-4" /> <AlignCenter className="h-4 w-4" />
</Button> </Button>
@ -102,6 +163,7 @@ function MenuBar({ editor }: MenuBarProps) {
onClick={() => editor.chain().focus().setTextAlign('right').run()} onClick={() => editor.chain().focus().setTextAlign('right').run()}
className={cn(editor.isActive({ textAlign: 'right' }) && 'bg-muted')} className={cn(editor.isActive({ textAlign: 'right' }) && 'bg-muted')}
aria-label="Alinhar à direita" aria-label="Alinhar à direita"
trackingId="editor-align-right-button"
> >
<AlignRight className="h-4 w-4" /> <AlignRight className="h-4 w-4" />
</Button> </Button>
@ -124,7 +186,6 @@ export function Editor({
heading: false, heading: false,
codeBlock: false, codeBlock: false,
horizontalRule: false, horizontalRule: false,
table: false,
}), }),
Placeholder.configure({ Placeholder.configure({
placeholder, placeholder,

View File

@ -3,9 +3,8 @@ 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, Loader2 } from 'lucide-react'; import { ArrowLeft, CheckCircle2, XCircle } 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;
@ -73,50 +72,14 @@ export function EssayAnalysis() {
// Carregar análise // Carregar análise
const { data: analysisData, error: analysisError } = await supabase const { data: analysisData, error: analysisError } = await supabase
.from('essay_analyses') .from('essay_analyses')
.select(` .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
)
`)
.eq('essay_id', id) .eq('essay_id', id)
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
.limit(1) .limit(1)
.single(); .single();
if (analysisError) throw analysisError; if (analysisError) throw analysisError;
setAnalysis(analysisData);
// 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);
} catch (error) { } catch (error) {
console.error('Erro ao carregar dados:', error); console.error('Erro ao carregar dados:', error);
} finally { } finally {
@ -124,177 +87,160 @@ export function EssayAnalysis() {
} }
} }
if (loading) { if (loading) return <div>Carregando...</div>;
return ( if (!essay || !analysis) return <div>Análise não encontrada</div>;
<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">
{/* Cabeçalho */} <div className="flex items-center gap-4 mb-6">
<div className="flex items-center justify-between mb-8"> <Button
<div className="flex items-center gap-4"> variant="ghost"
<Button onClick={() => navigate('/aluno/redacoes')}
variant="ghost" className="text-purple-600 hover:text-purple-700"
onClick={() => navigate(`/aluno/redacoes/${id}`)} trackingId="essay-analysis-back-to-list-button"
className="text-gray-600 hover:text-gray-900" >
> <ArrowLeft className="mr-2 h-4 w-4" />
<ArrowLeft className="mr-2 h-4 w-4" /> Voltar para lista de redações
Voltar para redação </Button>
</Button> <Button
<div> variant="ghost"
<h1 className="text-3xl font-bold text-gray-900">{essay.title}</h1> onClick={() => navigate(`/aluno/redacoes/${id}`)}
<p className="text-gray-500"> className="text-gray-600 hover:text-gray-900"
{essay.essay_type.title} {essay.essay_genre.title} trackingId="essay-analysis-back-to-essay-button"
</p> >
</div> <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>
</div> </div>
{/* Conteúdo Principal */} <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-8"> {/* Pontuação Geral */}
{/* Cartão de Pontuação */} <Card>
<Card className="bg-gradient-to-br from-purple-50 to-white border-purple-100"> <CardHeader>
<CardContent className="pt-6"> <CardTitle>Pontuação Geral</CardTitle>
<div className="flex flex-col items-center justify-center"> </CardHeader>
<div className="flex items-center mb-2"> <CardContent>
<span className="text-6xl font-bold text-purple-600">{analysis.overall_score}</span> <div className="flex items-center justify-center">
<span className="text-2xl text-purple-600 ml-2">/100</span> <div className="text-6xl font-bold text-primary">
{analysis.overall_score}
</div> </div>
<div className="text-gray-500">Pontuação Geral</div> <div className="text-2xl ml-1">/100</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Grid de Métricas */} {/* Pontos Fortes */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <Card>
{/* Pontos Fortes */} <CardHeader>
<Card className="bg-gradient-to-br from-green-50 to-white border-green-100"> <CardTitle className="flex items-center gap-2">
<CardHeader> <CheckCircle2 className="h-5 w-5 text-success" />
<CardTitle className="flex items-center gap-2 text-green-700"> Pontos Fortes
<CheckCircle2 className="h-5 w-5" /> </CardTitle>
Pontos Fortes </CardHeader>
</CardTitle> <CardContent>
</CardHeader> <ul className="list-disc list-inside space-y-2">
<CardContent> {analysis.strengths.map((strength, index) => (
<ul className="space-y-2"> <li key={index} className="text-success">{strength}</li>
{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>
))} ))}
</CardContent> </ul>
</Card> </CardContent>
</div> </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 */} {/* Feedback Detalhado */}
<Card> <Card className="md:col-span-2">
<CardHeader> <CardHeader>
<CardTitle>Feedback Detalhado</CardTitle> <CardTitle>Feedback Detalhado</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-6"> <CardContent className="space-y-4">
<div className="space-y-2"> <div>
<h4 className="font-semibold text-gray-900">Estrutura</h4> <h4 className="font-semibold mb-2">Estrutura</h4>
<p className="text-gray-600">{analysis.feedback.structure}</p> <p className="text-muted-foreground">{analysis.feedback.structure}</p>
</div> </div>
<div className="space-y-2"> <div>
<h4 className="font-semibold text-gray-900">Conteúdo</h4> <h4 className="font-semibold mb-2">Conteúdo</h4>
<p className="text-gray-600">{analysis.feedback.content}</p> <p className="text-muted-foreground">{analysis.feedback.content}</p>
</div> </div>
<div className="space-y-2"> <div>
<h4 className="font-semibold text-gray-900">Linguagem</h4> <h4 className="font-semibold mb-2">Linguagem</h4>
<p className="text-gray-600">{analysis.feedback.language}</p> <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> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Sugestões */} {/* Sugestões */}
<Card className="bg-gradient-to-br from-blue-50 to-white border-blue-100"> <Card className="md:col-span-3">
<CardHeader> <CardHeader>
<CardTitle className="text-blue-700">Sugestões para Melhoria</CardTitle> <CardTitle>Sugestões para Melhoria</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-blue-700 whitespace-pre-line"> <p className="text-muted-foreground whitespace-pre-line">
{analysis.suggestions} {analysis.suggestions}
</p> </p>
</CardContent> </CardContent>
@ -302,4 +248,4 @@ export function EssayAnalysis() {
</div> </div>
</div> </div>
); );
} }