mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
Compare commits
24 Commits
478ca2441d
...
bb85c83c5b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb85c83c5b | ||
|
|
2175458186 | ||
|
|
190777dcd0 | ||
|
|
8c6e6aedd3 | ||
|
|
8b45fe72e7 | ||
|
|
ccbac66d28 | ||
|
|
46e8ba0312 | ||
|
|
c94c46f5c1 | ||
|
|
28ac3ef8cc | ||
|
|
756335f78f | ||
|
|
9d303b0c7a | ||
|
|
0eafbd5350 | ||
|
|
4609217fb7 | ||
|
|
1c6aa56b32 | ||
|
|
2929946499 | ||
|
|
1bc307d599 | ||
|
|
e9005e429f | ||
|
|
b767d60c50 | ||
|
|
63498e92c6 | ||
|
|
cc45bb974d | ||
|
|
da62f5e722 | ||
|
|
d1e44f84b7 | ||
|
|
f602f4c666 | ||
|
|
206f7bcb30 |
74
CHANGELOG.md
74
CHANGELOG.md
@ -250,3 +250,77 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
||||
- Configuração centralizada de métricas
|
||||
- Suporte a tooltips e ícones personalizados
|
||||
- Responsividade e acessibilidade melhoradas
|
||||
|
||||
### Técnico
|
||||
- Normalização do JSON Schema da análise de redações para corresponder à estrutura do banco de dados
|
||||
- Reordenação dos campos para corresponder à estrutura das tabelas
|
||||
- Ajuste nas descrições dos campos para maior clareza
|
||||
- Alinhamento com as tabelas: essay_analyses, essay_analysis_feedback, essay_analysis_strengths e essay_analysis_improvements
|
||||
- Melhoria na validação dos dados com JSON Schema mais preciso
|
||||
|
||||
### Técnico
|
||||
- Correção das políticas de segurança (RLS) para o sistema de análise de redações:
|
||||
- Simplificada a política de inserção para service_role
|
||||
- Adicionadas políticas para tabelas relacionadas (feedback, pontos fortes, melhorias e notas)
|
||||
- Melhorada a segurança com políticas específicas para cada operação
|
||||
- Corrigido erro de permissão na inserção de análises pela Edge Function
|
||||
|
||||
### Técnico
|
||||
- Removidas restrições de validação do JSON Schema da análise de redações:
|
||||
- Removidos limites `minimum` e `maximum` dos campos numéricos
|
||||
- Removida restrição `minItems` dos arrays de pontos fortes e melhorias
|
||||
- Simplificada a validação para maior flexibilidade na Edge Function
|
||||
|
||||
### Técnico
|
||||
- Corrigida consulta de análise de redações no componente `EssayAnalysis`:
|
||||
- Adicionado join com tabelas relacionadas (feedback, strengths, improvements, scores)
|
||||
- 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
|
||||
|
||||
## [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
|
||||
|
||||
1244
package-lock.json
generated
1244
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -23,14 +23,27 @@
|
||||
"@opentelemetry/sdk-metrics": "^1.30.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.30.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@sentry/react": "^8.48.0",
|
||||
"@supabase/supabase-js": "^2.39.7",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.62.8",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@tiptap/extension-character-count": "^2.11.5",
|
||||
"@tiptap/extension-color": "^2.11.5",
|
||||
"@tiptap/extension-highlight": "^2.11.5",
|
||||
"@tiptap/extension-placeholder": "^2.11.5",
|
||||
"@tiptap/extension-text-align": "^2.11.5",
|
||||
"@tiptap/extension-text-style": "^2.11.5",
|
||||
"@tiptap/extension-underline": "^2.11.5",
|
||||
"@tiptap/pm": "^2.11.5",
|
||||
"@tiptap/react": "^2.11.5",
|
||||
"@tiptap/starter-kit": "^2.11.5",
|
||||
"@tremor/react": "^3.18.7",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^29.5.14",
|
||||
@ -48,11 +61,14 @@
|
||||
"resend": "^3.2.0",
|
||||
"shadcn-ui": "^0.9.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.0.3",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^2.1.8",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@types/react": "^18.3.17",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
|
||||
@ -1,18 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Calendar, HelpCircle } from 'lucide-react';
|
||||
import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts';
|
||||
|
||||
interface WeeklyMetrics {
|
||||
week: string;
|
||||
fluency: number;
|
||||
pronunciation: number;
|
||||
accuracy: number;
|
||||
comprehension: number;
|
||||
wordsPerMinute: number;
|
||||
pauses: number;
|
||||
errors: number;
|
||||
minutesRead: number;
|
||||
}
|
||||
import type { WeeklyReadingMetrics } from '@/types/metrics';
|
||||
|
||||
interface MetricConfig {
|
||||
key: string;
|
||||
@ -44,11 +33,11 @@ const TIME_FILTERS: TimeFilterOption[] = [
|
||||
];
|
||||
|
||||
interface MetricsChartProps {
|
||||
data: WeeklyMetrics[];
|
||||
data: WeeklyReadingMetrics[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
||||
export function MetricsChart({ data = [], className = '' }: MetricsChartProps) {
|
||||
const [visibleMetrics, setVisibleMetrics] = React.useState<Set<string>>(
|
||||
new Set(METRICS_CONFIG.map(metric => metric.key))
|
||||
);
|
||||
@ -66,7 +55,9 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const filterDataByTime = (data: WeeklyMetrics[]): WeeklyMetrics[] => {
|
||||
const filterDataByTime = (data: WeeklyReadingMetrics[]): WeeklyReadingMetrics[] => {
|
||||
if (!data || !Array.isArray(data)) return [];
|
||||
|
||||
if (timeFilter === 'all') return data;
|
||||
|
||||
const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12;
|
||||
@ -74,21 +65,23 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
||||
cutoffDate.setMonth(cutoffDate.getMonth() - months);
|
||||
|
||||
return data.filter(item => {
|
||||
if (!item?.week) return false;
|
||||
const [year, week] = item.week.split('-W').map(Number);
|
||||
if (!year || !week) return false;
|
||||
const itemDate = new Date(year, 0, 1 + (week - 1) * 7);
|
||||
return itemDate >= cutoffDate;
|
||||
});
|
||||
};
|
||||
|
||||
const filteredData = filterDataByTime(data);
|
||||
const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]);
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-8 ${className}`}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Evolução das Métricas por Semana</h2>
|
||||
<p className="text-sm text-gray-500">Acompanhe seu progresso ao longo do tempo</p>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Evolução da Leitura por Semana</h2>
|
||||
<p className="text-sm text-gray-500">Acompanhe seu progresso na leitura ao longo do tempo</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Filtro de Período */}
|
||||
@ -191,9 +184,7 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
||||
accuracy: 'Precisão',
|
||||
comprehension: 'Compreensão',
|
||||
wordsPerMinute: 'Palavras/Min',
|
||||
pauses: 'Pausas',
|
||||
errors: 'Erros',
|
||||
minutesRead: 'Minutos Lidos'
|
||||
minutesRead: 'Minutos Lendo'
|
||||
};
|
||||
return [value, metricNames[name] || name];
|
||||
}}
|
||||
@ -233,7 +224,7 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
||||
<Bar
|
||||
yAxisId="right"
|
||||
dataKey="minutesRead"
|
||||
name="Minutos Lidos"
|
||||
name="Minutos Lendo"
|
||||
fill="url(#barGradient)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
|
||||
241
src/components/dashboard/WritingMetricsChart.tsx
Normal file
241
src/components/dashboard/WritingMetricsChart.tsx
Normal file
@ -0,0 +1,241 @@
|
||||
import React from 'react';
|
||||
import { Calendar, HelpCircle } from 'lucide-react';
|
||||
import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts';
|
||||
import type { WeeklyWritingMetrics } from '@/types/metrics';
|
||||
|
||||
interface MetricConfig {
|
||||
key: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
type TimeFilter = '3m' | '6m' | '12m' | 'all';
|
||||
|
||||
interface TimeFilterOption {
|
||||
value: TimeFilter;
|
||||
label: string;
|
||||
months: number | null;
|
||||
}
|
||||
|
||||
const METRICS_CONFIG: MetricConfig[] = [
|
||||
{ key: 'score', name: 'Nota Geral', color: '#6366f1' },
|
||||
{ key: 'adequacy', name: 'Adequação', color: '#f43f5e' },
|
||||
{ key: 'coherence', name: 'Coerência', color: '#0ea5e9' },
|
||||
{ key: 'cohesion', name: 'Coesão', color: '#10b981' },
|
||||
{ key: 'vocabulary', name: 'Vocabulário', color: '#8b5cf6' },
|
||||
{ key: 'grammar', name: 'Gramática', color: '#f59e0b' }
|
||||
];
|
||||
|
||||
const TIME_FILTERS: TimeFilterOption[] = [
|
||||
{ value: '3m', label: '3 meses', months: 3 },
|
||||
{ value: '6m', label: '6 meses', months: 6 },
|
||||
{ value: '12m', label: '12 meses', months: 12 },
|
||||
{ value: 'all', label: 'Todo período', months: null },
|
||||
];
|
||||
|
||||
interface WritingMetricsChartProps {
|
||||
data: WeeklyWritingMetrics[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WritingMetricsChart({ data = [], className = '' }: WritingMetricsChartProps) {
|
||||
const [visibleMetrics, setVisibleMetrics] = React.useState<Set<string>>(
|
||||
new Set(METRICS_CONFIG.map(metric => metric.key))
|
||||
);
|
||||
const [timeFilter, setTimeFilter] = React.useState<TimeFilter>('12m');
|
||||
|
||||
const toggleMetric = (metricKey: string) => {
|
||||
setVisibleMetrics(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(metricKey)) {
|
||||
newSet.delete(metricKey);
|
||||
} else {
|
||||
newSet.add(metricKey);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const filterDataByTime = (data: WeeklyWritingMetrics[]): WeeklyWritingMetrics[] => {
|
||||
if (!data || !Array.isArray(data)) return [];
|
||||
|
||||
if (timeFilter === 'all') return data;
|
||||
|
||||
const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12;
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setMonth(cutoffDate.getMonth() - months);
|
||||
|
||||
return data.filter(item => {
|
||||
if (!item?.week) return false;
|
||||
const [year, week] = item.week.split('-W').map(Number);
|
||||
if (!year || !week) return false;
|
||||
const itemDate = new Date(year, 0, 1 + (week - 1) * 7);
|
||||
return itemDate >= cutoffDate;
|
||||
});
|
||||
};
|
||||
|
||||
const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]);
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-8 ${className}`}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Evolução da Escrita por Semana</h2>
|
||||
<p className="text-sm text-gray-500">Acompanhe seu progresso na escrita ao longo do tempo</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Filtro de Período */}
|
||||
<div className="flex items-center gap-2 bg-gray-50 p-1 rounded-lg">
|
||||
<Calendar className="h-4 w-4 text-gray-500" />
|
||||
{TIME_FILTERS.map(filter => (
|
||||
<button
|
||||
key={filter.value}
|
||||
onClick={() => setTimeFilter(filter.value)}
|
||||
className={`
|
||||
px-3 py-1 rounded-md text-sm font-medium transition-all duration-200
|
||||
${timeFilter === filter.value
|
||||
? 'bg-white text-purple-600 shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="text-gray-400 hover:text-gray-600 cursor-help transition-colors"
|
||||
title="Gráfico mostrando a evolução das suas métricas de escrita ao longo das semanas"
|
||||
>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pill Buttons */}
|
||||
<div className="flex flex-wrap gap-2 p-1">
|
||||
{METRICS_CONFIG.map(metric => (
|
||||
<button
|
||||
key={metric.key}
|
||||
onClick={() => toggleMetric(metric.key)}
|
||||
className={`
|
||||
px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ease-in-out
|
||||
${visibleMetrics.has(metric.key)
|
||||
? 'shadow-md transform -translate-y-px'
|
||||
: 'bg-gray-50 text-gray-500 hover:bg-gray-100'
|
||||
}
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-opacity-50
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: visibleMetrics.has(metric.key) ? metric.color : undefined,
|
||||
color: visibleMetrics.has(metric.key) ? 'white' : undefined,
|
||||
boxShadow: visibleMetrics.has(metric.key) ? '0 2px 4px rgba(0,0,0,0.1)' : undefined
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${visibleMetrics.has(metric.key) ? 'bg-white' : 'bg-gray-400'}`}></span>
|
||||
{metric.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="h-[400px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={filteredData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
|
||||
<defs>
|
||||
<linearGradient id="barGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.3} />
|
||||
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="#f0f0f0"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="week"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||
dy={10}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||
dx={-10}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#6b7280' }}
|
||||
dx={10}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number, name: string) => {
|
||||
const metricNames: { [key: string]: string } = {
|
||||
score: 'Nota Geral',
|
||||
adequacy: 'Adequação',
|
||||
coherence: 'Coerência',
|
||||
cohesion: 'Coesão',
|
||||
vocabulary: 'Vocabulário',
|
||||
grammar: 'Gramática',
|
||||
minutesWriting: 'Minutos Escrevendo'
|
||||
};
|
||||
return [value, metricNames[name] || name];
|
||||
}}
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
padding: '12px'
|
||||
}}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
align="right"
|
||||
iconType="circle"
|
||||
wrapperStyle={{
|
||||
paddingBottom: '20px'
|
||||
}}
|
||||
/>
|
||||
{METRICS_CONFIG.map(metric => (
|
||||
visibleMetrics.has(metric.key) && (
|
||||
<Line
|
||||
key={metric.key}
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey={metric.key}
|
||||
stroke={metric.color}
|
||||
name={metric.name}
|
||||
strokeWidth={2.5}
|
||||
dot={{ strokeWidth: 2, r: 4, fill: 'white' }}
|
||||
activeDot={{ r: 6, strokeWidth: 2 }}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
<Bar
|
||||
yAxisId="right"
|
||||
dataKey="minutesWriting"
|
||||
name="Minutos Escrevendo"
|
||||
fill="url(#barGradient)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
isAnimationActive={false}
|
||||
maxBarSize={50}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
143
src/components/dashboard/WritingMetricsSection.tsx
Normal file
143
src/components/dashboard/WritingMetricsSection.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import { BookOpen, Clock, TrendingUp, Award, Target, Brain, Gauge, Sparkles, Puzzle, Pencil, HelpCircle } from 'lucide-react';
|
||||
import { MetricCard } from './MetricCard';
|
||||
import type { WritingMetrics } from '@/types/metrics';
|
||||
|
||||
interface WritingMetricsSectionProps {
|
||||
data: WritingMetrics;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MAIN_METRICS = [
|
||||
{
|
||||
key: 'totalEssays',
|
||||
title: 'Total de Redações',
|
||||
getValue: (data: WritingMetrics) => data.totalEssays,
|
||||
icon: BookOpen,
|
||||
iconColor: 'text-purple-600',
|
||||
iconBgColor: 'bg-purple-100'
|
||||
},
|
||||
{
|
||||
key: 'averageScore',
|
||||
title: 'Nota Média',
|
||||
getValue: (data: WritingMetrics) => `${data.averageScore}%`,
|
||||
icon: TrendingUp,
|
||||
iconColor: 'text-green-600',
|
||||
iconBgColor: 'bg-green-100'
|
||||
},
|
||||
{
|
||||
key: 'totalEssaysTime',
|
||||
title: 'Tempo de Escrita',
|
||||
getValue: (data: WritingMetrics) => `${data.totalEssaysTime}min`,
|
||||
icon: Clock,
|
||||
iconColor: 'text-blue-600',
|
||||
iconBgColor: 'bg-blue-100'
|
||||
},
|
||||
{
|
||||
key: 'currentWritingLevel',
|
||||
title: 'Nível de Escrita',
|
||||
getValue: (data: WritingMetrics) => data.currentWritingLevel,
|
||||
icon: Award,
|
||||
iconColor: 'text-yellow-600',
|
||||
iconBgColor: 'bg-yellow-100'
|
||||
}
|
||||
];
|
||||
|
||||
const DETAILED_METRICS = [
|
||||
{
|
||||
key: 'averageAdequacy',
|
||||
title: 'Adequação ao Tema',
|
||||
getValue: (data: WritingMetrics) => `${data.averageAdequacy}%`,
|
||||
icon: Target,
|
||||
iconColor: 'text-indigo-600',
|
||||
iconBgColor: 'bg-indigo-100',
|
||||
tooltip: 'Avalia o quanto sua redação está alinhada com o tema e gênero propostos'
|
||||
},
|
||||
{
|
||||
key: 'averageCoherence',
|
||||
title: 'Coerência',
|
||||
getValue: (data: WritingMetrics) => `${data.averageCoherence}%`,
|
||||
icon: Brain,
|
||||
iconColor: 'text-pink-600',
|
||||
iconBgColor: 'bg-pink-100',
|
||||
tooltip: 'Indica a clareza e lógica no desenvolvimento das ideias do texto'
|
||||
},
|
||||
{
|
||||
key: 'averageCohesion',
|
||||
title: 'Coesão',
|
||||
getValue: (data: WritingMetrics) => `${data.averageCohesion}%`,
|
||||
icon: Puzzle,
|
||||
iconColor: 'text-orange-600',
|
||||
iconBgColor: 'bg-orange-100',
|
||||
tooltip: 'Avalia o uso adequado de conectivos e elementos de ligação entre as partes do texto'
|
||||
},
|
||||
{
|
||||
key: 'averageVocabulary',
|
||||
title: 'Vocabulário',
|
||||
getValue: (data: WritingMetrics) => `${data.averageVocabulary}%`,
|
||||
icon: Sparkles,
|
||||
iconColor: 'text-cyan-600',
|
||||
iconBgColor: 'bg-cyan-100',
|
||||
tooltip: 'Analisa a riqueza e adequação do vocabulário utilizado'
|
||||
},
|
||||
{
|
||||
key: 'averageGrammar',
|
||||
title: 'Gramática',
|
||||
getValue: (data: WritingMetrics) => `${data.averageGrammar}%`,
|
||||
icon: Pencil,
|
||||
iconColor: 'text-amber-600',
|
||||
iconBgColor: 'bg-amber-100',
|
||||
tooltip: 'Avalia o uso correto das regras gramaticais e ortográficas'
|
||||
}
|
||||
];
|
||||
|
||||
export function WritingMetricsSection({ data, className = '' }: WritingMetricsSectionProps) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Escrita</h2>
|
||||
|
||||
{/* Métricas Principais */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
{MAIN_METRICS.map(metric => (
|
||||
<MetricCard
|
||||
key={metric.key}
|
||||
title={metric.title}
|
||||
value={metric.getValue(data)}
|
||||
icon={metric.icon}
|
||||
iconColor={metric.iconColor}
|
||||
iconBgColor={metric.iconBgColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Métricas Detalhadas */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Métricas Detalhadas de Escrita</h3>
|
||||
<div
|
||||
className="text-gray-400 hover:text-gray-600 cursor-help transition-colors"
|
||||
title="Estas métricas são calculadas com base em todas as suas redações, fornecendo uma visão detalhada do seu progresso na escrita"
|
||||
>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{DETAILED_METRICS.map(metric => (
|
||||
<MetricCard
|
||||
key={metric.key}
|
||||
title={metric.title}
|
||||
value={metric.getValue(data)}
|
||||
icon={metric.icon}
|
||||
iconColor={metric.iconColor}
|
||||
iconBgColor={metric.iconBgColor}
|
||||
tooltip={metric.tooltip}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
146
src/components/ui/alert-dialog.tsx
Normal file
146
src/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50",
|
||||
"transition-all duration-200",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4",
|
||||
"border bg-white p-6 shadow-lg rounded-lg",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@ -7,11 +7,41 @@ import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
as?: 'button' | 'span';
|
||||
trackingId: string;
|
||||
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
|
||||
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
trackingProperties?: ButtonTrackingOptions;
|
||||
}
|
||||
|
||||
export function buttonVariants({
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
className = '',
|
||||
}: {
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
className?: string;
|
||||
} = {}) {
|
||||
return cn(
|
||||
'inline-flex items-center justify-center px-4 py-2',
|
||||
'text-sm font-medium',
|
||||
'rounded-md shadow-sm',
|
||||
'transition-colors duration-200',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
'text-white bg-purple-600 hover:bg-purple-700': variant === 'primary' || variant === 'default',
|
||||
'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50': variant === 'secondary',
|
||||
'text-purple-600 bg-transparent hover:bg-purple-50': variant === 'ghost',
|
||||
'text-purple-600 bg-transparent hover:underline': variant === 'link',
|
||||
'text-purple-600 border border-purple-600 hover:bg-purple-50': variant === 'outline',
|
||||
'text-white bg-red-600 hover:bg-red-700': variant === 'destructive',
|
||||
'px-3 py-1.5 text-sm': size === 'sm',
|
||||
'px-4 py-2 text-base': size === 'md',
|
||||
'px-6 py-3 text-lg': size === 'lg',
|
||||
},
|
||||
className
|
||||
);
|
||||
}
|
||||
|
||||
export function Button({
|
||||
as: Component = 'button',
|
||||
children,
|
||||
@ -41,29 +71,10 @@ export function Button({
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
const baseStyles = cn(
|
||||
'inline-flex items-center justify-center px-4 py-2',
|
||||
'text-sm font-medium',
|
||||
'rounded-md shadow-sm',
|
||||
'transition-colors duration-200',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
'text-white bg-purple-600 hover:bg-purple-700': variant === 'primary' || variant === 'default',
|
||||
'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50': variant === 'secondary',
|
||||
'text-purple-600 bg-transparent hover:bg-purple-50': variant === 'ghost',
|
||||
'text-purple-600 bg-transparent hover:underline': variant === 'link',
|
||||
'text-purple-600 border border-purple-600 hover:bg-purple-50': variant === 'outline',
|
||||
'px-3 py-1.5 text-sm': size === 'sm',
|
||||
'px-4 py-2 text-base': size === 'md',
|
||||
'px-6 py-3 text-lg': size === 'lg',
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<Component
|
||||
type={Component === 'button' ? type : undefined}
|
||||
className={baseStyles}
|
||||
className={buttonVariants({ variant, size, className })}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
|
||||
251
src/components/ui/editor.tsx
Normal file
251
src/components/ui/editor.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import { useEditor, EditorContent, Editor as TiptapEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import CharacterCount from '@tiptap/extension-character-count'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
import TextStyle from '@tiptap/extension-text-style'
|
||||
import Color from '@tiptap/extension-color'
|
||||
import { useEffect } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from './button'
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Underline as UnderlineIcon,
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
AlignRight,
|
||||
Highlighter,
|
||||
Strikethrough,
|
||||
Code,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface EditorProps {
|
||||
content: string
|
||||
onChange: (content: string) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
minHeight?: string
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
interface MenuBarProps {
|
||||
editor: TiptapEditor | null
|
||||
}
|
||||
|
||||
function MenuBar({ editor }: MenuBarProps) {
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-input bg-transparent p-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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"
|
||||
size="sm"
|
||||
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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Editor({
|
||||
content,
|
||||
onChange,
|
||||
placeholder = 'Comece a escrever...',
|
||||
className,
|
||||
minHeight = '500px',
|
||||
readOnly = false,
|
||||
}: EditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass: 'is-editor-empty',
|
||||
}),
|
||||
CharacterCount,
|
||||
Highlight.configure({
|
||||
multicolor: true,
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['paragraph'],
|
||||
}),
|
||||
Underline,
|
||||
TextStyle,
|
||||
Color,
|
||||
],
|
||||
content,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML())
|
||||
},
|
||||
editable: !readOnly,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && content !== editor.getHTML()) {
|
||||
editor.commands.setContent(content)
|
||||
}
|
||||
}, [content, editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-transparent',
|
||||
'focus-within:outline-none focus-within:ring-2',
|
||||
'focus-within:ring-ring focus-within:ring-offset-2',
|
||||
'transition-all duration-200',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!readOnly && <MenuBar editor={editor} />}
|
||||
<div
|
||||
className={cn(
|
||||
'prose prose-purple max-w-none px-4 py-2',
|
||||
'[&_.is-editor-empty]:before:text-muted-foreground',
|
||||
'[&_.is-editor-empty]:before:content-[attr(data-placeholder)]',
|
||||
'[&_.is-editor-empty]:before:float-left',
|
||||
'[&_.is-editor-empty]:before:h-0',
|
||||
'[&_.is-editor-empty]:before:pointer-events-none',
|
||||
'transition-all duration-200',
|
||||
readOnly && 'prose-sm'
|
||||
)}
|
||||
style={{ minHeight }}
|
||||
>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
{!readOnly && editor && (
|
||||
<div className="border-t border-input px-4 py-2 text-sm text-muted-foreground">
|
||||
{editor.storage.characterCount.words()} palavras
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -12,13 +12,13 @@ const Progress = React.forwardRef<
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-gray-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
className="h-full w-full flex-1 bg-purple-600 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
24
src/components/ui/textarea.tsx
Normal file
24
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
@ -8,13 +8,7 @@ if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Variáveis de ambiente do Supabase não configuradas')
|
||||
}
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: true
|
||||
}
|
||||
})
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
export const generateStoryFunction = async (prompt: StoryPrompt) => {
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
X,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
PenTool
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
@ -69,6 +70,21 @@ export function StudentDashboardLayout() {
|
||||
{!isCollapsed && <span>Painel</span>}
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/aluno/redacoes"
|
||||
onClick={handleNavigation}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
|
||||
isActive
|
||||
? 'bg-purple-50 text-purple-700'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<PenTool className="h-5 w-5" />
|
||||
{!isCollapsed && <span>Redações</span>}
|
||||
</NavLink>
|
||||
|
||||
{/* <NavLink
|
||||
to="/aluno/conquistas"
|
||||
onClick={handleNavigation}
|
||||
|
||||
@ -5,19 +5,15 @@ import { supabase } from '../../lib/supabase';
|
||||
import type { Story, Student } from '../../types/database';
|
||||
import { MetricsChart } from '@/components/dashboard/MetricsChart';
|
||||
import { DashboardMetrics } from '@/components/dashboard/DashboardMetrics';
|
||||
|
||||
interface DashboardMetrics {
|
||||
totalStories: number;
|
||||
averageReadingFluency: number;
|
||||
totalReadingTime: number;
|
||||
currentLevel: number;
|
||||
averagePronunciation: number;
|
||||
averageAccuracy: number;
|
||||
averageComprehension: number;
|
||||
averageWordsPerMinute: number;
|
||||
averagePauses: number;
|
||||
averageErrors: number;
|
||||
}
|
||||
import { WritingMetricsSection } from '@/components/dashboard/WritingMetricsSection';
|
||||
import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart';
|
||||
import { useMetricsStore } from '@/stores/metricsStore';
|
||||
import type {
|
||||
DashboardMetrics as DashboardMetricsType,
|
||||
DashboardWeeklyMetrics,
|
||||
WeeklyReadingMetrics,
|
||||
WeeklyWritingMetrics
|
||||
} from '@/types/metrics';
|
||||
|
||||
interface WeeklyMetrics {
|
||||
week: string;
|
||||
@ -54,25 +50,64 @@ interface WeeklyData {
|
||||
minutesRead: number;
|
||||
}
|
||||
|
||||
interface EssayAnalysis {
|
||||
id: string;
|
||||
created_at: string;
|
||||
overall_score: number;
|
||||
suggestions: string;
|
||||
essay_analysis_scores: Array<{
|
||||
adequacy: number;
|
||||
coherence: number;
|
||||
cohesion: number;
|
||||
vocabulary: number;
|
||||
grammar: number;
|
||||
}>;
|
||||
essay_analysis_feedback: Array<{
|
||||
structure_feedback: string;
|
||||
content_feedback: string;
|
||||
language_feedback: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ProcessedEssayAnalysis {
|
||||
id: string;
|
||||
created_at: string;
|
||||
overall_score: number;
|
||||
essay_id: string;
|
||||
scores: {
|
||||
adequacy: number;
|
||||
coherence: number;
|
||||
cohesion: number;
|
||||
vocabulary: number;
|
||||
grammar: number;
|
||||
};
|
||||
feedback: {
|
||||
structure_feedback: string;
|
||||
content_feedback: string;
|
||||
language_feedback: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function StudentDashboardPage() {
|
||||
const navigate = useNavigate();
|
||||
const [student, setStudent] = React.useState<Student | null>(null);
|
||||
const [metrics, setMetrics] = React.useState<DashboardMetrics>({
|
||||
totalStories: 0,
|
||||
averageReadingFluency: 0,
|
||||
totalReadingTime: 0,
|
||||
currentLevel: 1,
|
||||
averagePronunciation: 0,
|
||||
averageAccuracy: 0,
|
||||
averageComprehension: 0,
|
||||
averageWordsPerMinute: 0,
|
||||
averagePauses: 0,
|
||||
averageErrors: 0
|
||||
});
|
||||
const [weeklyMetrics, setWeeklyMetrics] = React.useState<WeeklyMetrics[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [recentStories, setRecentStories] = React.useState<Story[]>([]);
|
||||
|
||||
const {
|
||||
metrics,
|
||||
weeklyMetrics,
|
||||
loading,
|
||||
error,
|
||||
setMetrics,
|
||||
setWeeklyMetrics,
|
||||
updateReadingMetrics,
|
||||
updateWritingMetrics,
|
||||
updateWeeklyReadingMetrics,
|
||||
updateWeeklyWritingMetrics,
|
||||
setLoading,
|
||||
setError,
|
||||
resetMetrics
|
||||
} = useMetricsStore();
|
||||
|
||||
const processWeeklyMetrics = (recordings: Recording[]) => {
|
||||
const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => {
|
||||
@ -121,9 +156,54 @@ export function StudentDashboardPage() {
|
||||
.sort((a, b) => a.week.localeCompare(b.week));
|
||||
};
|
||||
|
||||
const processWeeklyWritingMetrics = (analyses: ProcessedEssayAnalysis[]) => {
|
||||
const weeklyData = analyses.reduce((acc: { [key: string]: any }, analysis) => {
|
||||
const date = new Date(analysis.created_at);
|
||||
const week = `${date.getFullYear()}-W${Math.ceil((date.getDate() + date.getDay()) / 7)}`;
|
||||
|
||||
if (!acc[week]) {
|
||||
acc[week] = {
|
||||
count: 0,
|
||||
score: 0,
|
||||
adequacy: 0,
|
||||
coherence: 0,
|
||||
cohesion: 0,
|
||||
vocabulary: 0,
|
||||
grammar: 0,
|
||||
minutesWriting: 0
|
||||
};
|
||||
}
|
||||
|
||||
acc[week].count += 1;
|
||||
acc[week].score += analysis.overall_score;
|
||||
acc[week].adequacy += analysis.scores.adequacy;
|
||||
acc[week].coherence += analysis.scores.coherence;
|
||||
acc[week].cohesion += analysis.scores.cohesion;
|
||||
acc[week].vocabulary += analysis.scores.vocabulary;
|
||||
acc[week].grammar += analysis.scores.grammar;
|
||||
acc[week].minutesWriting += 30; // Tempo médio estimado por redação
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.entries(weeklyData)
|
||||
.map(([week, data]: [string, any]) => ({
|
||||
week,
|
||||
score: Math.round(data.score / data.count),
|
||||
adequacy: Math.round(data.adequacy / data.count),
|
||||
coherence: Math.round(data.coherence / data.count),
|
||||
cohesion: Math.round(data.cohesion / data.count),
|
||||
vocabulary: Math.round(data.vocabulary / data.count),
|
||||
grammar: Math.round(data.grammar / data.count),
|
||||
minutesWriting: data.minutesWriting
|
||||
}))
|
||||
.sort((a, b) => a.week.localeCompare(b.week));
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session?.user?.id) return;
|
||||
|
||||
@ -164,7 +244,8 @@ export function StudentDashboardPage() {
|
||||
|
||||
// Processar métricas semanais
|
||||
const weeklyData = processWeeklyMetrics(recordings);
|
||||
setWeeklyMetrics(weeklyData);
|
||||
// Atualizar métricas semanais de leitura
|
||||
updateWeeklyReadingMetrics(weeklyData);
|
||||
|
||||
// Buscar histórias recentes com a capa definida
|
||||
const { data: stories, error: storiesError } = await supabase
|
||||
@ -223,8 +304,8 @@ export function StudentDashboardPage() {
|
||||
errors: 0
|
||||
});
|
||||
|
||||
// Calcular médias
|
||||
setMetrics({
|
||||
// Atualizar métricas de leitura
|
||||
updateReadingMetrics({
|
||||
totalStories: allStoriesData.length,
|
||||
averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings),
|
||||
totalReadingTime: recordings.length * 2,
|
||||
@ -238,6 +319,125 @@ export function StudentDashboardPage() {
|
||||
});
|
||||
}
|
||||
|
||||
// Buscar todas as redações do aluno
|
||||
const { data: essays, error: essaysError } = await supabase
|
||||
.from('student_essays')
|
||||
.select(`
|
||||
id,
|
||||
created_at,
|
||||
status,
|
||||
essay_analyses(
|
||||
id,
|
||||
overall_score,
|
||||
suggestions,
|
||||
created_at,
|
||||
essay_analysis_scores(
|
||||
adequacy,
|
||||
coherence,
|
||||
cohesion,
|
||||
vocabulary,
|
||||
grammar
|
||||
),
|
||||
essay_analysis_feedback(
|
||||
structure_feedback,
|
||||
content_feedback,
|
||||
language_feedback
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('student_id', session.user.id)
|
||||
.eq('status', 'analyzed')
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (essaysError) throw essaysError;
|
||||
|
||||
console.log('Redações carregadas:', essays);
|
||||
|
||||
// Processar métricas semanais de escrita
|
||||
const analyses = essays?.flatMap(essay =>
|
||||
essay.essay_analyses?.map(analysis => {
|
||||
console.log('Análise individual:', analysis);
|
||||
return {
|
||||
id: analysis.id,
|
||||
created_at: analysis.created_at,
|
||||
overall_score: analysis.overall_score || 0,
|
||||
essay_id: essay.id,
|
||||
scores: {
|
||||
adequacy: analysis.essay_analysis_scores?.[0]?.adequacy || 0,
|
||||
coherence: analysis.essay_analysis_scores?.[0]?.coherence || 0,
|
||||
cohesion: analysis.essay_analysis_scores?.[0]?.cohesion || 0,
|
||||
vocabulary: analysis.essay_analysis_scores?.[0]?.vocabulary || 0,
|
||||
grammar: analysis.essay_analysis_scores?.[0]?.grammar || 0
|
||||
},
|
||||
feedback: {
|
||||
structure_feedback: analysis.essay_analysis_feedback?.[0]?.structure_feedback || '',
|
||||
content_feedback: analysis.essay_analysis_feedback?.[0]?.content_feedback || '',
|
||||
language_feedback: analysis.essay_analysis_feedback?.[0]?.language_feedback || ''
|
||||
}
|
||||
};
|
||||
})
|
||||
).filter(Boolean) || [];
|
||||
|
||||
console.log('Análises processadas:', analyses);
|
||||
|
||||
// Calcular métricas gerais de escrita
|
||||
if (analyses && analyses.length > 0) {
|
||||
const totalAnalyses = analyses.length;
|
||||
const metricsSum = analyses.reduce((acc, analysis) => ({
|
||||
score: acc.score + (analysis.overall_score || 0),
|
||||
adequacy: acc.adequacy + (analysis.scores?.adequacy || 0),
|
||||
coherence: acc.coherence + (analysis.scores?.coherence || 0),
|
||||
cohesion: acc.cohesion + (analysis.scores?.cohesion || 0),
|
||||
vocabulary: acc.vocabulary + (analysis.scores?.vocabulary || 0),
|
||||
grammar: acc.grammar + (analysis.scores?.grammar || 0)
|
||||
}), {
|
||||
score: 0,
|
||||
adequacy: 0,
|
||||
coherence: 0,
|
||||
cohesion: 0,
|
||||
vocabulary: 0,
|
||||
grammar: 0
|
||||
});
|
||||
|
||||
console.log('Soma das métricas:', metricsSum);
|
||||
console.log('Total de análises:', totalAnalyses);
|
||||
|
||||
// Atualizar métricas de escrita
|
||||
const writingMetrics = {
|
||||
totalEssays: essays?.length || 0,
|
||||
averageScore: Math.round(metricsSum.score / totalAnalyses),
|
||||
totalEssaysTime: totalAnalyses * 30,
|
||||
currentWritingLevel: Math.ceil(metricsSum.score / (totalAnalyses * 20)),
|
||||
averageAdequacy: Math.round(metricsSum.adequacy / totalAnalyses),
|
||||
averageCoherence: Math.round(metricsSum.coherence / totalAnalyses),
|
||||
averageCohesion: Math.round(metricsSum.cohesion / totalAnalyses),
|
||||
averageVocabulary: Math.round(metricsSum.vocabulary / totalAnalyses),
|
||||
averageGrammar: Math.round(metricsSum.grammar / totalAnalyses)
|
||||
};
|
||||
|
||||
console.log('Métricas de escrita calculadas:', writingMetrics);
|
||||
updateWritingMetrics(writingMetrics);
|
||||
|
||||
const weeklyWritingData = processWeeklyWritingMetrics(analyses);
|
||||
console.log('Dados semanais de escrita:', weeklyWritingData);
|
||||
updateWeeklyWritingMetrics(weeklyWritingData);
|
||||
} else {
|
||||
console.log('Nenhuma análise encontrada');
|
||||
// Definir valores padrão quando não há análises
|
||||
updateWritingMetrics({
|
||||
totalEssays: 0,
|
||||
averageScore: 0,
|
||||
totalEssaysTime: 0,
|
||||
currentWritingLevel: 1,
|
||||
averageAdequacy: 0,
|
||||
averageCoherence: 0,
|
||||
averageCohesion: 0,
|
||||
averageVocabulary: 0,
|
||||
averageGrammar: 0
|
||||
});
|
||||
updateWeeklyWritingMetrics([]);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar dashboard:', err);
|
||||
setError('Não foi possível carregar seus dados');
|
||||
@ -247,6 +447,11 @@ export function StudentDashboardPage() {
|
||||
};
|
||||
|
||||
fetchDashboardData();
|
||||
|
||||
// Limpar métricas ao desmontar
|
||||
return () => {
|
||||
resetMetrics();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
@ -317,11 +522,27 @@ export function StudentDashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Métricas */}
|
||||
<DashboardMetrics data={metrics} />
|
||||
{/* Seção de Métricas de Leitura */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Leitura</h2>
|
||||
|
||||
{/* Métricas de Leitura */}
|
||||
<DashboardMetrics data={metrics.reading} className="mb-8" />
|
||||
|
||||
{/* Gráfico de Evolução */}
|
||||
<MetricsChart data={weeklyMetrics} className="mb-8" />
|
||||
{/* Gráfico de Evolução da Leitura */}
|
||||
<MetricsChart data={weeklyMetrics.reading} className="mb-8" />
|
||||
</div>
|
||||
|
||||
{/* Seção de Métricas de Escrita */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Escrita</h2>
|
||||
|
||||
{/* Métricas de Escrita */}
|
||||
<WritingMetricsSection data={metrics.writing} className="mb-8" />
|
||||
|
||||
{/* Gráfico de Evolução da Escrita */}
|
||||
<WritingMetricsChart data={weeklyMetrics.writing} className="mb-8" />
|
||||
</div>
|
||||
|
||||
{/* Histórias Recentes */}
|
||||
<div>
|
||||
|
||||
310
src/pages/student-dashboard/essays/EssayAnalysis.tsx
Normal file
310
src/pages/student-dashboard/essays/EssayAnalysis.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
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 { Progress } from '@/components/ui/progress';
|
||||
|
||||
interface EssayAnalysis {
|
||||
id: string;
|
||||
essay_id: string;
|
||||
overall_score: number;
|
||||
feedback: {
|
||||
structure: string;
|
||||
content: string;
|
||||
language: string;
|
||||
};
|
||||
strengths: string[];
|
||||
improvements: string[];
|
||||
suggestions: string;
|
||||
criteria_scores: {
|
||||
adequacy: number;
|
||||
coherence: number;
|
||||
cohesion: number;
|
||||
vocabulary: number;
|
||||
grammar: number;
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Essay {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
essay_type: {
|
||||
title: string;
|
||||
};
|
||||
essay_genre: {
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EssayAnalysisData {
|
||||
id: string;
|
||||
essay_id: string;
|
||||
overall_score: number;
|
||||
suggestions: string;
|
||||
created_at: string;
|
||||
feedback: Array<{
|
||||
structure_feedback: string;
|
||||
content_feedback: string;
|
||||
language_feedback: string;
|
||||
}>;
|
||||
strengths: Array<{
|
||||
strength: string;
|
||||
}>;
|
||||
improvements: Array<{
|
||||
improvement: string;
|
||||
}>;
|
||||
scores: Array<{
|
||||
adequacy: number;
|
||||
coherence: number;
|
||||
cohesion: number;
|
||||
vocabulary: number;
|
||||
grammar: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function EssayAnalysis() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [analysis, setAnalysis] = useState<EssayAnalysis | null>(null);
|
||||
const [essay, setEssay] = useState<Essay | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadEssayAndAnalysis();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
async function loadEssayAndAnalysis() {
|
||||
try {
|
||||
// Carregar redação
|
||||
const { data: essayData, error: essayError } = await supabase
|
||||
.from('student_essays')
|
||||
.select(`
|
||||
*,
|
||||
essay_type:essay_types(title),
|
||||
essay_genre:essay_genres(title)
|
||||
`)
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (essayError) throw essayError;
|
||||
setEssay(essayData);
|
||||
|
||||
// Carregar análise
|
||||
const { data, 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
|
||||
)
|
||||
`)
|
||||
.eq('essay_id', id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (analysisError) throw analysisError;
|
||||
|
||||
// Transformar os dados para o formato esperado
|
||||
const analysisData = data as EssayAnalysisData;
|
||||
const analysis: EssayAnalysis = {
|
||||
...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: { strength: string }) => s.strength) || [],
|
||||
improvements: analysisData.improvements?.map((i: { improvement: string }) => 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(analysis);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar dados:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/aluno/redacoes')}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
|
||||
trackingId="essay-analysis-back-to-list-button"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
Voltar para redações
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
{/* Título e Tipo */}
|
||||
<div className="border-b border-gray-200 pb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">{essay.title}</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
{essay.essay_type.title} • {essay.essay_genre.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grid de Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Pontuação Geral */}
|
||||
<Card className="bg-purple-50 border-purple-100">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="text-6xl font-bold text-purple-600">{analysis.overall_score}</div>
|
||||
<div className="text-2xl text-purple-600 ml-2">/100</div>
|
||||
</div>
|
||||
<p className="text-center text-gray-600 mt-2">Pontuação Geral</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pontos Fortes */}
|
||||
<Card className="bg-green-50 border-green-100">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
Pontos Fortes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc list-inside space-y-2">
|
||||
{analysis.strengths.map((strength, index) => (
|
||||
<li key={index} className="text-green-600">{strength}</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pontos a Melhorar */}
|
||||
<Card className="bg-orange-50 border-orange-100">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-orange-600">
|
||||
<XCircle className="h-5 w-5" />
|
||||
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-orange-600">{improvement}</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Feedback Detalhado */}
|
||||
<Card className="md:col-span-2 border-gray-200">
|
||||
<CardHeader className="border-b border-gray-200">
|
||||
<CardTitle>Feedback Detalhado</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-4">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2 text-gray-900">Estrutura</h4>
|
||||
<p className="text-gray-600">{analysis.feedback.structure}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2 text-gray-900">Conteúdo</h4>
|
||||
<p className="text-gray-600">{analysis.feedback.content}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2 text-gray-900">Linguagem</h4>
|
||||
<p className="text-gray-600">{analysis.feedback.language}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Critérios de Avaliação */}
|
||||
<Card className="border-gray-200">
|
||||
<CardHeader className="border-b border-gray-200">
|
||||
<CardTitle>Critérios de Avaliação</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-4">
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-gray-600">Adequação ao Gênero</span>
|
||||
<span className="font-medium">{analysis.criteria_scores.adequacy}%</span>
|
||||
</div>
|
||||
<Progress value={analysis.criteria_scores.adequacy} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-gray-600">Coerência</span>
|
||||
<span className="font-medium">{analysis.criteria_scores.coherence}%</span>
|
||||
</div>
|
||||
<Progress value={analysis.criteria_scores.coherence} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-gray-600">Coesão</span>
|
||||
<span className="font-medium">{analysis.criteria_scores.cohesion}%</span>
|
||||
</div>
|
||||
<Progress value={analysis.criteria_scores.cohesion} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-gray-600">Vocabulário</span>
|
||||
<span className="font-medium">{analysis.criteria_scores.vocabulary}%</span>
|
||||
</div>
|
||||
<Progress value={analysis.criteria_scores.vocabulary} className="h-2" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-gray-600">Gramática</span>
|
||||
<span className="font-medium">{analysis.criteria_scores.grammar}%</span>
|
||||
</div>
|
||||
<Progress value={analysis.criteria_scores.grammar} className="h-2" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sugestões */}
|
||||
<Card className="md:col-span-3 bg-blue-50 border-blue-100">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-blue-600">Sugestões para Melhoria</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 whitespace-pre-line">
|
||||
{analysis.suggestions}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
408
src/pages/student-dashboard/essays/EssayPage.tsx
Normal file
408
src/pages/student-dashboard/essays/EssayPage.tsx
Normal file
@ -0,0 +1,408 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
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, BarChart3 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { useUppercasePreference } from '@/hooks/useUppercasePreference';
|
||||
import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '@/components/ui/adaptive-text';
|
||||
import { TextCaseToggle } from '@/components/ui/text-case-toggle';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Editor } from '@/components/ui/editor';
|
||||
|
||||
interface Essay {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type_id: string;
|
||||
genre_id: string;
|
||||
status: 'draft' | 'submitted' | 'analyzed';
|
||||
essay_type: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
essay_genre: {
|
||||
title: string;
|
||||
description: string;
|
||||
requirements: {
|
||||
min_words: number;
|
||||
max_words: number;
|
||||
required_elements: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function EssayPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [essay, setEssay] = useState<Essay | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [wordCount, setWordCount] = useState(0);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase, toggleUppercase, isLoading: isUppercaseLoading } = useUppercasePreference(session?.user?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadEssay();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (essay?.content) {
|
||||
const words = essay.content.trim().split(/\s+/).length;
|
||||
setWordCount(words);
|
||||
}
|
||||
}, [essay?.content]);
|
||||
|
||||
async function loadEssay() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('student_essays')
|
||||
.select(`
|
||||
*,
|
||||
essay_type:essay_types(*),
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEssay() {
|
||||
if (!essay) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('student_essays')
|
||||
.update({
|
||||
title: essay.title,
|
||||
content: essay.content
|
||||
})
|
||||
.eq('id', essay.id);
|
||||
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar redação:', error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForAnalysis() {
|
||||
if (!essay) return;
|
||||
try {
|
||||
setSaving(true);
|
||||
|
||||
// Primeiro salvar o conteúdo atual
|
||||
const { error: saveError } = await supabase
|
||||
.from('student_essays')
|
||||
.update({
|
||||
title: essay.title,
|
||||
content: essay.content,
|
||||
status: 'submitted'
|
||||
})
|
||||
.eq('id', essay.id);
|
||||
|
||||
if (saveError) throw saveError;
|
||||
|
||||
// Chama a Edge Function para análise
|
||||
const { error: analysisError } = await supabase.functions.invoke('analyze-essay', {
|
||||
body: {
|
||||
essay_id: essay.id,
|
||||
content: essay.content,
|
||||
type_id: essay.type_id,
|
||||
genre_id: essay.genre_id
|
||||
}
|
||||
});
|
||||
|
||||
if (analysisError) throw analysisError;
|
||||
|
||||
// Redireciona para a página de análise
|
||||
navigate(`/aluno/redacoes/${essay.id}/analise`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar para análise:', error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEssay() {
|
||||
if (!essay) return;
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('student_essays')
|
||||
.delete()
|
||||
.eq('id', essay.id);
|
||||
|
||||
if (error) throw error;
|
||||
navigate('/aluno/redacoes');
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar redação:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div>Carregando...</div>;
|
||||
if (!essay) return <div>Redação não encontrada</div>;
|
||||
|
||||
const isWithinWordLimit =
|
||||
wordCount >= (essay.essay_genre.requirements.min_words || 0) &&
|
||||
wordCount <= (essay.essay_genre.requirements.max_words || Infinity);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/aluno/redacoes')}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
trackingId="essay-back-to-list-button"
|
||||
trackingProperties={{
|
||||
action: 'back_to_essays_list',
|
||||
page: 'essay_editor'
|
||||
}}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
<AdaptiveText text="Voltar para redações" isUpperCase={isUpperCase} />
|
||||
</Button>
|
||||
<TextCaseToggle
|
||||
isUpperCase={isUpperCase}
|
||||
onToggle={toggleUppercase}
|
||||
isLoading={isUppercaseLoading}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-100 rounded w-1/3" />
|
||||
<div className="h-4 bg-gray-100 rounded w-1/4" />
|
||||
<div className="h-64 bg-gray-100 rounded mt-8" />
|
||||
</div>
|
||||
) : !essay ? (
|
||||
<div className="text-center py-12">
|
||||
<AdaptiveText
|
||||
text="Redação não encontrada"
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-gray-600"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={essay.title}
|
||||
onChange={(e) => setEssay({ ...essay, title: e.target.value })}
|
||||
className="text-2xl font-bold border-none focus:border-none bg-transparent px-0"
|
||||
placeholder="Título da redação"
|
||||
/>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mt-1">
|
||||
<AdaptiveText text={essay.essay_type?.title} isUpperCase={isUpperCase} />
|
||||
<span>•</span>
|
||||
<AdaptiveText text={essay.essay_genre?.title} isUpperCase={isUpperCase} />
|
||||
<span>•</span>
|
||||
<Badge variant={essay.status === 'draft' ? 'secondary' : essay.status === 'analyzed' ? 'success' : 'default'}>
|
||||
<AdaptiveText
|
||||
text={essay.status === 'draft' ? 'Rascunho' : essay.status === 'analyzed' ? 'Analisada' : 'Enviada'}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={saveEssay}
|
||||
disabled={saving}
|
||||
trackingId="essay-save-button"
|
||||
trackingProperties={{
|
||||
action: 'save_essay',
|
||||
page: 'essay_editor',
|
||||
status: essay.status
|
||||
}}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<AdaptiveText
|
||||
text={saving ? 'Salvando...' : 'Salvar'}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{essay.status === 'draft' && (
|
||||
<>
|
||||
<Button
|
||||
onClick={submitForAnalysis}
|
||||
disabled={!isWithinWordLimit}
|
||||
title={!isWithinWordLimit ? 'Número de palavras fora do limite' : ''}
|
||||
trackingId="essay-submit-analysis-button"
|
||||
trackingProperties={{
|
||||
action: 'submit_for_analysis',
|
||||
page: 'essay_editor',
|
||||
word_count: wordCount,
|
||||
within_limit: isWithinWordLimit
|
||||
}}
|
||||
>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
<AdaptiveText text="Enviar para análise" isUpperCase={isUpperCase} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
trackingId="essay-delete-button"
|
||||
trackingProperties={{
|
||||
action: 'delete_essay',
|
||||
page: 'essay_editor',
|
||||
status: essay.status
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<AdaptiveText text="Deletar" isUpperCase={isUpperCase} />
|
||||
</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) => 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(
|
||||
"font-medium",
|
||||
isWithinWordLimit ? "text-gray-600" : "text-red-600"
|
||||
)}>
|
||||
{wordCount} palavras
|
||||
</span>
|
||||
{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})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="h-fit">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="font-semibold mb-4">
|
||||
<AdaptiveText text="Requisitos do Gênero" isUpperCase={isUpperCase} />
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
|
||||
<div className="space-y-2 text-sm text-purple-800">
|
||||
<p>
|
||||
<AdaptiveText
|
||||
text={`Mínimo: ${essay.essay_genre.requirements.min_words} palavras`}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<AdaptiveText
|
||||
text={`Máximo: ${essay.essay_genre.requirements.max_words} palavras`}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</p>
|
||||
<p className="mt-2 font-medium">
|
||||
<AdaptiveText text="Elementos necessários:" isUpperCase={isUpperCase} />
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{essay.essay_genre.requirements.required_elements.map((element, index) => (
|
||||
<li key={index}>
|
||||
<AdaptiveText text={element} isUpperCase={isUpperCase} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<AdaptiveText text="Você tem certeza?" isUpperCase={isUpperCase} />
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<AdaptiveText
|
||||
text="Esta ação não pode ser desfeita. Isso excluirá permanentemente sua redação."
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<AdaptiveText text="Cancelar" isUpperCase={isUpperCase} />
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={deleteEssay} className="bg-red-600 hover:bg-red-700">
|
||||
<AdaptiveText text="Deletar" isUpperCase={isUpperCase} />
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
269
src/pages/student-dashboard/essays/NewEssay.tsx
Normal file
269
src/pages/student-dashboard/essays/NewEssay.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowLeft, Sparkles } from 'lucide-react';
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { useUppercasePreference } from '@/hooks/useUppercasePreference';
|
||||
import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '@/components/ui/adaptive-text';
|
||||
import { TextCaseToggle } from '@/components/ui/text-case-toggle';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface EssayType {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface EssayGenre {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
requirements: {
|
||||
min_words: number;
|
||||
max_words: number;
|
||||
required_elements: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function NewEssay() {
|
||||
const navigate = useNavigate();
|
||||
const [step, setStep] = useState<'type' | 'genre'>('type');
|
||||
const [selectedType, setSelectedType] = useState<EssayType | null>(null);
|
||||
const [types, setTypes] = useState<EssayType[]>([]);
|
||||
const [genres, setGenres] = useState<EssayGenre[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||
|
||||
useEffect(() => {
|
||||
loadTypes();
|
||||
}, []);
|
||||
|
||||
// Carregar tipos textuais
|
||||
async function loadTypes() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('essay_types')
|
||||
.select('*')
|
||||
.eq('active', true);
|
||||
|
||||
if (error) throw error;
|
||||
setTypes(data || []);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar tipos:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Carregar gêneros do tipo selecionado
|
||||
async function loadGenres(typeId: string) {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('essay_genres')
|
||||
.select('*')
|
||||
.eq('type_id', typeId)
|
||||
.eq('active', true);
|
||||
|
||||
if (error) throw error;
|
||||
setGenres(data || []);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar gêneros:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Criar nova redação
|
||||
async function createEssay(genreId: string) {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) throw new Error('Usuário não autenticado');
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('student_essays')
|
||||
.insert({
|
||||
student_id: user.id,
|
||||
type_id: selectedType!.id,
|
||||
genre_id: genreId,
|
||||
status: 'draft',
|
||||
title: 'Nova Redação',
|
||||
content: ''
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
navigate(`/aluno/redacoes/${data.id}`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar redação:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderizar seleção de tipo textual
|
||||
function renderTypeSelection() {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{types.map((type) => (
|
||||
<Card
|
||||
key={type.id}
|
||||
className={cn(
|
||||
"cursor-pointer hover:shadow-lg transition-all duration-200",
|
||||
"bg-white border border-gray-200",
|
||||
selectedType?.id === type.id && "ring-2 ring-purple-600 bg-purple-50"
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedType(type);
|
||||
loadGenres(type.id);
|
||||
setStep('genre');
|
||||
}}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<span className="text-2xl">{type.icon}</span>
|
||||
</div>
|
||||
<AdaptiveText
|
||||
text={type.title}
|
||||
isUpperCase={isUpperCase}
|
||||
className="font-bold"
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<AdaptiveText
|
||||
text={type.description}
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-gray-600"
|
||||
/>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Renderizar seleção de gênero textual
|
||||
function renderGenreSelection() {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{genres.map((genre) => (
|
||||
<Card
|
||||
key={genre.id}
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 bg-white border border-gray-200"
|
||||
onClick={() => createEssay(genre.id)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<span className="text-2xl">{genre.icon}</span>
|
||||
</div>
|
||||
<AdaptiveText
|
||||
text={genre.title}
|
||||
isUpperCase={isUpperCase}
|
||||
className="font-bold"
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<AdaptiveText
|
||||
text={genre.description}
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-gray-600"
|
||||
/>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
|
||||
<h4 className="text-sm font-medium text-purple-900 mb-2">Requisitos</h4>
|
||||
<div className="space-y-2 text-sm text-purple-800">
|
||||
<p>Mínimo: {genre.requirements?.min_words || 0} palavras</p>
|
||||
<p>Máximo: {genre.requirements?.max_words || 'Sem limite'} palavras</p>
|
||||
{genre.requirements?.required_elements && genre.requirements.required_elements.length > 0 && (
|
||||
<>
|
||||
<p className="mt-2 font-medium">Elementos necessários:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{genre.requirements.required_elements.map((element, index) => (
|
||||
<li key={index}>
|
||||
<AdaptiveText
|
||||
text={element}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<Sparkles className="h-6 w-6 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<AdaptiveTitle
|
||||
text="Nova Redação"
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-2xl font-bold text-gray-900"
|
||||
/>
|
||||
<AdaptiveParagraph
|
||||
text={step === 'type'
|
||||
? "Selecione o tipo textual para começar"
|
||||
: "Escolha o gênero textual da sua redação"}
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<TextCaseToggle
|
||||
isUpperCase={isUpperCase}
|
||||
onToggle={toggleUppercase}
|
||||
isLoading={isLoading}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{step === 'genre' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setStep('type')}
|
||||
className="mb-6"
|
||||
trackingId="essay-new-back-button"
|
||||
trackingProperties={{
|
||||
action: 'back_to_type_selection',
|
||||
current_step: 'genre',
|
||||
page: 'new_essay'
|
||||
}}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Voltar para tipos textuais
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-48 bg-gray-100 rounded-lg" />
|
||||
<div className="h-48 bg-gray-100 rounded-lg" />
|
||||
<div className="h-48 bg-gray-100 rounded-lg" />
|
||||
</div>
|
||||
) : step === 'type' ? (
|
||||
renderTypeSelection()
|
||||
) : (
|
||||
renderGenreSelection()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
286
src/pages/student-dashboard/essays/index.tsx
Normal file
286
src/pages/student-dashboard/essays/index.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus, Search, Filter, PenTool } from 'lucide-react';
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { useUppercasePreference } from '@/hooks/useUppercasePreference';
|
||||
import { AdaptiveText } from '@/components/ui/adaptive-text';
|
||||
|
||||
interface Essay {
|
||||
id: string;
|
||||
title: string;
|
||||
type_id: string;
|
||||
genre_id: string;
|
||||
status: 'draft' | 'submitted' | 'analyzed';
|
||||
created_at: string;
|
||||
essay_type: {
|
||||
title: string;
|
||||
};
|
||||
essay_genre: {
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
||||
type EssayStatus = 'all' | 'draft' | 'submitted' | 'analyzed';
|
||||
type SortOption = 'recent' | 'oldest' | 'title';
|
||||
|
||||
export function EssaysPage() {
|
||||
const navigate = useNavigate();
|
||||
const [essays, setEssays] = useState<Essay[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<EssayStatus>('all');
|
||||
const [sortBy, setSortBy] = useState<SortOption>('recent');
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase } = useUppercasePreference(session?.user?.id);
|
||||
|
||||
useEffect(() => {
|
||||
loadEssays();
|
||||
}, [statusFilter, sortBy]);
|
||||
|
||||
async function loadEssays() {
|
||||
try {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session?.user?.id) return;
|
||||
|
||||
const query = supabase
|
||||
.from('student_essays')
|
||||
.select(`
|
||||
*,
|
||||
essay_type:essay_types(title),
|
||||
essay_genre:essay_genres(title)
|
||||
`)
|
||||
.eq('student_id', session.user.id);
|
||||
|
||||
if (statusFilter !== 'all') {
|
||||
query.eq('status', statusFilter);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
setError('Não foi possível carregar suas redações');
|
||||
return;
|
||||
}
|
||||
|
||||
// Aplicar ordenação
|
||||
const sortedData = (data || []).sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'oldest':
|
||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
case 'title':
|
||||
return a.title.localeCompare(b.title);
|
||||
default: // recent
|
||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
||||
}
|
||||
});
|
||||
|
||||
setEssays(sortedData);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar redações:', error);
|
||||
setError('Não foi possível carregar suas redações');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredEssays = essays.filter(essay =>
|
||||
essay.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
function getStatusBadge(status: Essay['status']) {
|
||||
const statusMap = {
|
||||
draft: { label: 'Rascunho', variant: 'secondary' as const, classes: 'bg-yellow-100 text-yellow-800' },
|
||||
submitted: { label: 'Enviada', variant: 'default' as const, classes: 'bg-blue-100 text-blue-800' },
|
||||
analyzed: { label: 'Analisada', variant: 'success' as const, classes: 'bg-green-100 text-green-800' }
|
||||
};
|
||||
|
||||
const { label, classes } = statusMap[status];
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${classes}`}>
|
||||
<AdaptiveText text={label} isUpperCase={isUpperCase} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-20 bg-gray-200 rounded-xl mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div key={i} className="h-64 bg-gray-200 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
<AdaptiveText text="Minhas Redações" isUpperCase={isUpperCase} />
|
||||
</h1>
|
||||
<Button
|
||||
onClick={() => navigate('/aluno/redacoes/nova')}
|
||||
className="flex items-center gap-2"
|
||||
trackingId="essay-new-create-button"
|
||||
trackingProperties={{
|
||||
action: 'create_new_essay',
|
||||
page: 'essays_list'
|
||||
}}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
<AdaptiveText text="Nova Redação" isUpperCase={isUpperCase} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Busca */}
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar redações..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filtros e Ordenação */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<Filter className="h-5 w-5" />
|
||||
<AdaptiveText text="Filtros" isUpperCase={isUpperCase} />
|
||||
</button>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
<option value="recent">Mais recentes</option>
|
||||
<option value="oldest">Mais antigas</option>
|
||||
<option value="title">Por título</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Painel de Filtros */}
|
||||
{showFilters && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`px-4 py-2 rounded-lg ${
|
||||
statusFilter === 'all'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<AdaptiveText text="Todas" isUpperCase={isUpperCase} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('analyzed')}
|
||||
className={`px-4 py-2 rounded-lg ${
|
||||
statusFilter === 'analyzed'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<AdaptiveText text="Analisadas" isUpperCase={isUpperCase} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('submitted')}
|
||||
className={`px-4 py-2 rounded-lg ${
|
||||
statusFilter === 'submitted'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<AdaptiveText text="Enviadas" isUpperCase={isUpperCase} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStatusFilter('draft')}
|
||||
className={`px-4 py-2 rounded-lg ${
|
||||
statusFilter === 'draft'
|
||||
? 'bg-purple-100 text-purple-700'
|
||||
: 'hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<AdaptiveText text="Rascunhos" isUpperCase={isUpperCase} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lista de Redações */}
|
||||
{filteredEssays.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<PenTool className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
<AdaptiveText text="Nenhuma redação encontrada" isUpperCase={isUpperCase} />
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-6">
|
||||
<AdaptiveText
|
||||
text={searchTerm
|
||||
? 'Tente usar outros termos na busca'
|
||||
: 'Comece criando sua primeira redação!'}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<Button
|
||||
onClick={() => navigate('/aluno/redacoes/nova')}
|
||||
className="inline-flex items-center gap-2"
|
||||
trackingId="essay-empty-create-button"
|
||||
trackingProperties={{
|
||||
action: 'create_first_essay',
|
||||
page: 'essays_list',
|
||||
context: 'empty_state'
|
||||
}}
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
<AdaptiveText text="Criar Redação" isUpperCase={isUpperCase} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6">
|
||||
{filteredEssays.map((essay) => (
|
||||
<div
|
||||
key={essay.id}
|
||||
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden cursor-pointer hover:shadow-md transition"
|
||||
onClick={() => navigate(`/aluno/redacoes/${essay.id}`)}
|
||||
>
|
||||
<div className="p-6">
|
||||
<h3 className="font-medium text-gray-900 mb-2">
|
||||
<AdaptiveText text={essay.title} isUpperCase={isUpperCase} />
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-4">
|
||||
<AdaptiveText text={essay.essay_type?.title} isUpperCase={isUpperCase} />
|
||||
<span>•</span>
|
||||
<AdaptiveText text={essay.essay_genre?.title} isUpperCase={isUpperCase} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<span>{new Date(essay.created_at).toLocaleDateString('pt-BR')}</span>
|
||||
{getStatusBadge(essay.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -33,6 +33,10 @@ import { EvidenceBased } from './pages/landing/EvidenceBased';
|
||||
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
|
||||
import { PhonicsPage } from "./pages/student-dashboard/PhonicsPage";
|
||||
import { PhonicsProgressPage } from "./pages/student-dashboard/PhonicsProgressPage";
|
||||
import { EssaysPage } from './pages/student-dashboard/essays';
|
||||
import { NewEssay } from './pages/student-dashboard/essays/NewEssay';
|
||||
import { EssayPage } from './pages/student-dashboard/essays/EssayPage';
|
||||
import { EssayAnalysis } from './pages/student-dashboard/essays/EssayAnalysis';
|
||||
|
||||
function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@ -219,6 +223,27 @@ export const router = createBrowserRouter([
|
||||
{
|
||||
path: 'fonicos/progresso',
|
||||
element: <PhonicsProgressPage />,
|
||||
},
|
||||
{
|
||||
path: 'redacoes',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <EssaysPage />,
|
||||
},
|
||||
{
|
||||
path: 'nova',
|
||||
element: <NewEssay />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <EssayPage />,
|
||||
},
|
||||
{
|
||||
path: ':id/analise',
|
||||
element: <EssayAnalysis />,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
106
src/stores/metricsStore.ts
Normal file
106
src/stores/metricsStore.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { create } from 'zustand';
|
||||
import type {
|
||||
DashboardMetrics,
|
||||
DashboardWeeklyMetrics,
|
||||
ReadingMetrics,
|
||||
WritingMetrics,
|
||||
WeeklyReadingMetrics,
|
||||
WeeklyWritingMetrics
|
||||
} from '@/types/metrics';
|
||||
|
||||
interface MetricsState {
|
||||
metrics: DashboardMetrics;
|
||||
weeklyMetrics: DashboardWeeklyMetrics;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setMetrics: (metrics: DashboardMetrics) => void;
|
||||
setWeeklyMetrics: (weeklyMetrics: DashboardWeeklyMetrics) => void;
|
||||
updateReadingMetrics: (readingMetrics: ReadingMetrics) => void;
|
||||
updateWritingMetrics: (writingMetrics: WritingMetrics) => void;
|
||||
updateWeeklyReadingMetrics: (weeklyReadingMetrics: WeeklyReadingMetrics[]) => void;
|
||||
updateWeeklyWritingMetrics: (weeklyWritingMetrics: WeeklyWritingMetrics[]) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
resetMetrics: () => void;
|
||||
}
|
||||
|
||||
const initialMetrics: DashboardMetrics = {
|
||||
reading: {
|
||||
totalStories: 0,
|
||||
averageReadingFluency: 0,
|
||||
totalReadingTime: 0,
|
||||
currentLevel: 1,
|
||||
averagePronunciation: 0,
|
||||
averageAccuracy: 0,
|
||||
averageComprehension: 0,
|
||||
averageWordsPerMinute: 0,
|
||||
averagePauses: 0,
|
||||
averageErrors: 0
|
||||
},
|
||||
writing: {
|
||||
totalEssays: 0,
|
||||
averageScore: 0,
|
||||
totalEssaysTime: 0,
|
||||
currentWritingLevel: 1,
|
||||
averageAdequacy: 0,
|
||||
averageCoherence: 0,
|
||||
averageCohesion: 0,
|
||||
averageVocabulary: 0,
|
||||
averageGrammar: 0
|
||||
}
|
||||
};
|
||||
|
||||
const initialWeeklyMetrics: DashboardWeeklyMetrics = {
|
||||
reading: [],
|
||||
writing: []
|
||||
};
|
||||
|
||||
export const useMetricsStore = create<MetricsState>((set) => ({
|
||||
metrics: initialMetrics,
|
||||
weeklyMetrics: initialWeeklyMetrics,
|
||||
loading: true,
|
||||
error: null,
|
||||
|
||||
setMetrics: (metrics) => set({ metrics }),
|
||||
|
||||
setWeeklyMetrics: (weeklyMetrics) => set({ weeklyMetrics }),
|
||||
|
||||
updateReadingMetrics: (readingMetrics) => set((state) => ({
|
||||
metrics: {
|
||||
...state.metrics,
|
||||
reading: readingMetrics
|
||||
}
|
||||
})),
|
||||
|
||||
updateWritingMetrics: (writingMetrics) => set((state) => ({
|
||||
metrics: {
|
||||
...state.metrics,
|
||||
writing: writingMetrics
|
||||
}
|
||||
})),
|
||||
|
||||
updateWeeklyReadingMetrics: (weeklyReadingMetrics) => set((state) => ({
|
||||
weeklyMetrics: {
|
||||
...state.weeklyMetrics,
|
||||
reading: weeklyReadingMetrics
|
||||
}
|
||||
})),
|
||||
|
||||
updateWeeklyWritingMetrics: (weeklyWritingMetrics) => set((state) => ({
|
||||
weeklyMetrics: {
|
||||
...state.weeklyMetrics,
|
||||
writing: weeklyWritingMetrics
|
||||
}
|
||||
})),
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
resetMetrics: () => set({
|
||||
metrics: initialMetrics,
|
||||
weeklyMetrics: initialWeeklyMetrics,
|
||||
loading: false,
|
||||
error: null
|
||||
})
|
||||
}));
|
||||
57
src/types/metrics.ts
Normal file
57
src/types/metrics.ts
Normal file
@ -0,0 +1,57 @@
|
||||
export interface ReadingMetrics {
|
||||
totalStories: number;
|
||||
averageReadingFluency: number;
|
||||
totalReadingTime: number;
|
||||
currentLevel: number;
|
||||
averagePronunciation: number;
|
||||
averageAccuracy: number;
|
||||
averageComprehension: number;
|
||||
averageWordsPerMinute: number;
|
||||
averagePauses: number;
|
||||
averageErrors: number;
|
||||
}
|
||||
|
||||
export interface WritingMetrics {
|
||||
totalEssays: number;
|
||||
averageScore: number;
|
||||
totalEssaysTime: number;
|
||||
currentWritingLevel: number;
|
||||
averageAdequacy: number;
|
||||
averageCoherence: number;
|
||||
averageCohesion: number;
|
||||
averageVocabulary: number;
|
||||
averageGrammar: number;
|
||||
}
|
||||
|
||||
export interface WeeklyReadingMetrics {
|
||||
week: string;
|
||||
fluency: number;
|
||||
pronunciation: number;
|
||||
accuracy: number;
|
||||
comprehension: number;
|
||||
wordsPerMinute: number;
|
||||
pauses: number;
|
||||
errors: number;
|
||||
minutesRead: number;
|
||||
}
|
||||
|
||||
export interface WeeklyWritingMetrics {
|
||||
week: string;
|
||||
score: number;
|
||||
adequacy: number;
|
||||
coherence: number;
|
||||
cohesion: number;
|
||||
vocabulary: number;
|
||||
grammar: number;
|
||||
minutesWriting: number;
|
||||
}
|
||||
|
||||
export interface DashboardMetrics {
|
||||
reading: ReadingMetrics;
|
||||
writing: WritingMetrics;
|
||||
}
|
||||
|
||||
export interface DashboardWeeklyMetrics {
|
||||
reading: WeeklyReadingMetrics[];
|
||||
writing: WeeklyWritingMetrics[];
|
||||
}
|
||||
@ -88,6 +88,8 @@
|
||||
| public | interests | item | UNIQUE |
|
||||
| public | interests | student_id | FOREIGN KEY |
|
||||
| public | languages | code | UNIQUE |
|
||||
| public | essay_analysis_scores | id | PRIMARY KEY |
|
||||
| public | essay_analysis_scores | analysis_id | FOREIGN KEY |
|
||||
| supabase_migrations | schema_migrations | version | PRIMARY KEY |
|
||||
| supabase_migrations | seed_files | path | PRIMARY KEY |
|
||||
| public | phonics_categories | id | PRIMARY KEY |
|
||||
@ -108,10 +110,19 @@
|
||||
| public | story_characters | slug | UNIQUE |
|
||||
| public | story_settings | id | PRIMARY KEY |
|
||||
| public | story_settings | slug | UNIQUE |
|
||||
| public | essay_types | id | PRIMARY KEY |
|
||||
| public | essay_types | slug | UNIQUE |
|
||||
| public | stories | theme_id | FOREIGN KEY |
|
||||
| public | stories | subject_id | FOREIGN KEY |
|
||||
| public | stories | character_id | FOREIGN KEY |
|
||||
| public | stories | setting_id | FOREIGN KEY |
|
||||
| public | essay_genres | id | PRIMARY KEY |
|
||||
| public | essay_genres | slug | UNIQUE |
|
||||
| public | essay_genres | type_id | FOREIGN KEY |
|
||||
| public | student_essays | id | PRIMARY KEY |
|
||||
| public | student_essays | student_id | FOREIGN KEY |
|
||||
| public | student_essays | type_id | FOREIGN KEY |
|
||||
| public | student_essays | genre_id | FOREIGN KEY |
|
||||
| auth | identities | user_id | FOREIGN KEY |
|
||||
| auth | refresh_tokens | token | UNIQUE |
|
||||
| auth | sessions | id | PRIMARY KEY |
|
||||
@ -150,12 +161,20 @@
|
||||
| storage | s3_multipart_uploads_parts | bucket_id | FOREIGN KEY |
|
||||
| realtime | schema_migrations | version | PRIMARY KEY |
|
||||
| realtime | subscription | id | PRIMARY KEY |
|
||||
| public | essay_analyses | id | PRIMARY KEY |
|
||||
| public | essay_analyses | essay_id | FOREIGN KEY |
|
||||
| public | stories | theme_id | FOREIGN KEY |
|
||||
| public | stories | subject_id | FOREIGN KEY |
|
||||
| public | stories | character_id | FOREIGN KEY |
|
||||
| public | stories | setting_id | FOREIGN KEY |
|
||||
| public | essay_analysis_feedback | id | PRIMARY KEY |
|
||||
| realtime | messages | id | PRIMARY KEY |
|
||||
| realtime | messages | inserted_at | PRIMARY KEY |
|
||||
| public | essay_analysis_feedback | analysis_id | FOREIGN KEY |
|
||||
| public | essay_analysis_strengths | id | PRIMARY KEY |
|
||||
| public | essay_analysis_strengths | analysis_id | FOREIGN KEY |
|
||||
| public | essay_analysis_improvements | id | PRIMARY KEY |
|
||||
| public | essay_analysis_improvements | analysis_id | FOREIGN KEY |
|
||||
| public | story_subjects | null | CHECK |
|
||||
| public | story_recordings | null | CHECK |
|
||||
| public | story_characters | null | CHECK |
|
||||
@ -174,9 +193,11 @@
|
||||
| public | students | null | CHECK |
|
||||
| public | phonics_word_audio | null | CHECK |
|
||||
| public | languages | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| pgsodium | key | null | CHECK |
|
||||
| public | phonics_word_audio | null | CHECK |
|
||||
| realtime | messages | null | CHECK |
|
||||
| public | essay_analysis_strengths | null | CHECK |
|
||||
| public | achievements | null | CHECK |
|
||||
| public | phonics_exercise_types | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
@ -186,6 +207,7 @@
|
||||
| auth | refresh_tokens | null | CHECK |
|
||||
| pgsodium | key | null | CHECK |
|
||||
| public | student_phonics_attempt_answers | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| auth | identities | null | CHECK |
|
||||
| storage | s3_multipart_uploads | null | CHECK |
|
||||
| auth | flow_state | null | CHECK |
|
||||
@ -194,6 +216,7 @@
|
||||
| public | stories | null | CHECK |
|
||||
| public | story_subjects | null | CHECK |
|
||||
| auth | flow_state | null | CHECK |
|
||||
| public | essay_analyses | null | CHECK |
|
||||
| public | phonics_words | null | CHECK |
|
||||
| realtime | subscription | null | CHECK |
|
||||
| auth | sso_domains | null | CHECK |
|
||||
@ -201,25 +224,36 @@
|
||||
| auth | users | null | CHECK |
|
||||
| public | phonics_categories | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| public | stories | null | CHECK |
|
||||
| public | story_themes | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| public | teachers | null | CHECK |
|
||||
| public | teachers | null | CHECK |
|
||||
| public | story_themes | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| public | phonics_exercise_words | null | CHECK |
|
||||
| public | students | null | CHECK |
|
||||
| realtime | subscription | null | CHECK |
|
||||
| auth | mfa_amr_claims | null | CHECK |
|
||||
| public | teacher_classes | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| auth | flow_state | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| public | students | null | CHECK |
|
||||
| public | story_characters | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| auth | mfa_factors | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| realtime | schema_migrations | null | CHECK |
|
||||
| public | languages | null | CHECK |
|
||||
| public | stories | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| public | phonics_word_audio | null | CHECK |
|
||||
| public | essay_analysis_feedback | null | CHECK |
|
||||
| pgsodium | key | null | CHECK |
|
||||
| storage | s3_multipart_uploads | null | CHECK |
|
||||
| auth | saml_providers | null | CHECK |
|
||||
@ -232,7 +266,9 @@
|
||||
| vault | secrets | null | CHECK |
|
||||
| public | students | null | CHECK |
|
||||
| vault | secrets | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| auth | users | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| public | story_themes | null | CHECK |
|
||||
| public | classes | null | CHECK |
|
||||
| auth | mfa_amr_claims | null | CHECK |
|
||||
@ -246,13 +282,19 @@
|
||||
| public | teachers | null | CHECK |
|
||||
| auth | mfa_amr_claims | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| public | teachers | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| public | essay_analysis_feedback | null | CHECK |
|
||||
| public | phonics_categories | null | CHECK |
|
||||
| public | story_settings | null | CHECK |
|
||||
| public | schools | null | CHECK |
|
||||
| realtime | subscription | null | CHECK |
|
||||
| public | story_recordings | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| public | essay_analysis_feedback | null | CHECK |
|
||||
| public | stories | null | CHECK |
|
||||
| public | story_pages | null | CHECK |
|
||||
| auth | identities | null | CHECK |
|
||||
@ -260,34 +302,44 @@
|
||||
| auth | flow_state | null | CHECK |
|
||||
| auth | one_time_tokens | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| auth | schema_migrations | null | CHECK |
|
||||
| auth | mfa_challenges | null | CHECK |
|
||||
| public | student_phonics_attempts | null | CHECK |
|
||||
| public | story_settings | null | CHECK |
|
||||
| public | teachers | null | CHECK |
|
||||
| pgsodium | key | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| public | interests | null | CHECK |
|
||||
| auth | instances | null | CHECK |
|
||||
| public | schools | null | CHECK |
|
||||
| public | student_phonics_achievements | null | CHECK |
|
||||
| storage | migrations | null | CHECK |
|
||||
| public | classes | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| public | story_exercise_words | null | CHECK |
|
||||
| public | phonics_word_audio | null | CHECK |
|
||||
| public | media_types | null | CHECK |
|
||||
| public | languages | null | CHECK |
|
||||
| public | story_themes | null | CHECK |
|
||||
| public | essay_analysis_strengths | null | CHECK |
|
||||
| auth | one_time_tokens | null | CHECK |
|
||||
| public | essay_analyses | null | CHECK |
|
||||
| auth | one_time_tokens | null | CHECK |
|
||||
| public | essay_analysis_feedback | null | CHECK |
|
||||
| realtime | messages | null | CHECK |
|
||||
| realtime | messages | null | CHECK |
|
||||
| public | story_characters | null | CHECK |
|
||||
| public | story_subjects | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| auth | sso_domains | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
| public | students | null | CHECK |
|
||||
| public | essay_analysis_improvements | null | CHECK |
|
||||
| auth | saml_relay_states | null | CHECK |
|
||||
| public | stories | null | CHECK |
|
||||
| public | story_settings | null | CHECK |
|
||||
@ -301,17 +353,22 @@
|
||||
| public | phonics_achievements | null | CHECK |
|
||||
| public | phonics_exercises | null | CHECK |
|
||||
| public | story_pages | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| public | student_phonics_progress | null | CHECK |
|
||||
| auth | saml_providers | null | CHECK |
|
||||
| public | essay_analysis_feedback | null | CHECK |
|
||||
| realtime | subscription | null | CHECK |
|
||||
| public | story_subjects | null | CHECK |
|
||||
| public | essay_analyses | null | CHECK |
|
||||
| auth | sso_providers | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| public | media_types | null | CHECK |
|
||||
| storage | s3_multipart_uploads | null | CHECK |
|
||||
| auth | one_time_tokens | null | CHECK |
|
||||
| public | story_generations | null | CHECK |
|
||||
| auth | saml_relay_states | null | CHECK |
|
||||
| public | story_themes | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| storage | buckets | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| vault | secrets | null | CHECK |
|
||||
@ -330,35 +387,44 @@
|
||||
| public | student_phonics_attempts | null | CHECK |
|
||||
| public | story_generations | null | CHECK |
|
||||
| public | phonics_exercises | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| auth | sso_domains | null | CHECK |
|
||||
| storage | s3_multipart_uploads | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| public | schools | null | CHECK |
|
||||
| auth | users | null | CHECK |
|
||||
| public | interests | null | CHECK |
|
||||
| realtime | messages | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| public | phonics_exercise_types | null | CHECK |
|
||||
| public | story_exercise_words | null | CHECK |
|
||||
| auth | audit_log_entries | null | CHECK |
|
||||
| public | story_characters | null | CHECK |
|
||||
| public | student_achievements_old | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| net | http_request_queue | null | CHECK |
|
||||
| auth | saml_providers | null | CHECK |
|
||||
| storage | s3_multipart_uploads | null | CHECK |
|
||||
| public | essay_analysis_strengths | null | CHECK |
|
||||
| supabase_migrations | seed_files | null | CHECK |
|
||||
| public | phonics_achievements | null | CHECK |
|
||||
| public | story_pages | null | CHECK |
|
||||
| public | phonics_exercises | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| storage | objects | null | CHECK |
|
||||
| public | essay_analysis_improvements | null | CHECK |
|
||||
| public | student_phonics_attempt_answers | null | CHECK |
|
||||
| storage | migrations | null | CHECK |
|
||||
| auth | sso_domains | null | CHECK |
|
||||
| public | story_recordings | null | CHECK |
|
||||
| public | essay_types | null | CHECK |
|
||||
| public | classes | null | CHECK |
|
||||
| net | _http_response | null | CHECK |
|
||||
| realtime | subscription | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
| auth | sessions | null | CHECK |
|
||||
| public | phonics_exercise_media | null | CHECK |
|
||||
| public | essay_analysis_improvements | null | CHECK |
|
||||
| public | schools | null | CHECK |
|
||||
| net | http_request_queue | null | CHECK |
|
||||
| public | classes | null | CHECK |
|
||||
@ -373,12 +439,16 @@
|
||||
| auth | identities | null | CHECK |
|
||||
| storage | s3_multipart_uploads | null | CHECK |
|
||||
| public | story_themes | null | CHECK |
|
||||
| public | essay_analyses | null | CHECK |
|
||||
| auth | saml_relay_states | null | CHECK |
|
||||
| public | essay_analyses | null | CHECK |
|
||||
| public | teacher_invites | null | CHECK |
|
||||
| public | essay_analysis_feedback | null | CHECK |
|
||||
| auth | mfa_factors | null | CHECK |
|
||||
| auth | identities | null | CHECK |
|
||||
| public | stories | null | CHECK |
|
||||
| public | story_pages | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| vault | secrets | null | CHECK |
|
||||
| vault | secrets | null | CHECK |
|
||||
| public | achievement_types | null | CHECK |
|
||||
@ -388,6 +458,7 @@
|
||||
| supabase_migrations | schema_migrations | null | CHECK |
|
||||
| public | stories | null | CHECK |
|
||||
| public | phonics_exercise_media | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| auth | mfa_challenges | null | CHECK |
|
||||
| public | story_characters | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
@ -401,17 +472,23 @@
|
||||
| auth | sessions | null | CHECK |
|
||||
| public | story_pages | null | CHECK |
|
||||
| net | http_request_queue | null | CHECK |
|
||||
| public | essay_analysis_strengths | null | CHECK |
|
||||
| auth | saml_providers | null | CHECK |
|
||||
| public | teacher_classes | null | CHECK |
|
||||
| public | story_generations | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| auth | flow_state | null | CHECK |
|
||||
| public | story_characters | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| public | student_achievements | null | CHECK |
|
||||
| public | student_essays | null | CHECK |
|
||||
| public | essay_analysis_scores | null | CHECK |
|
||||
| public | students | null | CHECK |
|
||||
| public | story_generations | null | CHECK |
|
||||
| auth | one_time_tokens | null | CHECK |
|
||||
| net | http_request_queue | null | CHECK |
|
||||
| auth | mfa_factors | null | CHECK |
|
||||
| public | essay_analysis_improvements | null | CHECK |
|
||||
| public | essay_genres | null | CHECK |
|
||||
| storage | s3_multipart_uploads_parts | null | CHECK |
|
||||
| public | teacher_classes | null | CHECK |
|
||||
@ -617,14 +617,52 @@
|
||||
| 29638 | schools |
|
||||
| 29639 | students |
|
||||
| 29639 | schools |
|
||||
| 113605 | essay_types_pkey |
|
||||
| 113605 | essay_types |
|
||||
| 113617 | student_essays |
|
||||
| 113622 | students_pkey |
|
||||
| 113622 | students |
|
||||
| 113627 | essay_types_pkey |
|
||||
| 113627 | essay_types |
|
||||
| 113632 | essay_genres_pkey |
|
||||
| 113632 | essay_genres |
|
||||
| 113642 | essay_analyses |
|
||||
| 113647 | student_essays_pkey |
|
||||
| 113647 | student_essays |
|
||||
| 113661 | essay_analyses_pkey |
|
||||
| 113661 | essay_analyses |
|
||||
| 113675 | essay_analyses_pkey |
|
||||
| 113675 | essay_analyses |
|
||||
| 113689 | essay_analyses_pkey |
|
||||
| 113689 | essay_analyses |
|
||||
| 29709 | students_pkey |
|
||||
| 29709 | students |
|
||||
| 29716 | storage.objects |
|
||||
| 29717 | storage.objects |
|
||||
| 113699 | essay_analysis_scores |
|
||||
| 113700 | essay_analysis_scores |
|
||||
| 113701 | essay_analysis_scores |
|
||||
| 113702 | essay_analysis_scores |
|
||||
| 113703 | essay_analysis_scores |
|
||||
| 113706 | essay_analyses_pkey |
|
||||
| 113706 | essay_analyses |
|
||||
| 29748 | story_recordings |
|
||||
| 29749 | story_recordings |
|
||||
| 74045 | storage.objects |
|
||||
| 34119 | net.http_request_queue_id_seq |
|
||||
| 113764 | essay_types |
|
||||
| 113765 | essay_genres |
|
||||
| 113766 | student_essays |
|
||||
| 113767 | student_essays |
|
||||
| 113768 | student_essays |
|
||||
| 113768 | student_essays |
|
||||
| 113769 | student_essays |
|
||||
| 113770 | essay_analyses |
|
||||
| 113770 | student_essays |
|
||||
| 113770 | student_essays |
|
||||
| 113771 | essay_analyses |
|
||||
| 113771 | student_essays |
|
||||
| 113771 | student_essays |
|
||||
| 29823 | auth.users |
|
||||
| 53921 | story_exercise_words |
|
||||
| 53928 | stories_pkey |
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
| table_schema | table_name | column_name | data_type | is_nullable | column_default |
|
||||
| ------------------- | ------------------------------- | --------------------------- | --------------------------- | ----------- | -------------------------------------------------- |
|
||||
| storage | s3_multipart_uploads | user_metadata | jsonb | YES | null |
|
||||
| realtime | schema_migrations | version | bigint | NO | null |
|
||||
| realtime | schema_migrations | inserted_at | timestamp without time zone | YES | null |
|
||||
| extensions | pg_stat_statements_info | dealloc | bigint | YES | null |
|
||||
| extensions | pg_stat_statements_info | stats_reset | timestamp with time zone | YES | null |
|
||||
@ -8,6 +8,23 @@
|
||||
| extensions | pg_stat_statements | dbid | oid | YES | null |
|
||||
| extensions | pg_stat_statements | toplevel | boolean | YES | null |
|
||||
| extensions | pg_stat_statements | queryid | bigint | YES | null |
|
||||
| public | teachers | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | teachers | class_ids | ARRAY | YES | null |
|
||||
| public | classes | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | classes | school_id | uuid | NO | null |
|
||||
| public | classes | year | integer | NO | null |
|
||||
| public | classes | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | classes | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | classes | teacher_id | uuid | YES | null |
|
||||
| public | essay_analysis_scores | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_analysis_scores | analysis_id | uuid | NO | null |
|
||||
| public | essay_analysis_scores | adequacy | integer | NO | null |
|
||||
| public | essay_analysis_scores | coherence | integer | NO | null |
|
||||
| public | essay_analysis_scores | cohesion | integer | NO | null |
|
||||
| public | essay_analysis_scores | vocabulary | integer | NO | null |
|
||||
| public | essay_analysis_scores | grammar | integer | NO | null |
|
||||
| public | essay_analysis_scores | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | teacher_classes | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | teacher_classes | teacher_id | uuid | NO | null |
|
||||
| public | teacher_classes | class_id | uuid | NO | null |
|
||||
| public | teacher_classes | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
@ -233,7 +250,7 @@
|
||||
| storage | s3_multipart_uploads_parts | created_at | timestamp with time zone | NO | now() |
|
||||
| storage | s3_multipart_uploads | in_progress_size | bigint | NO | 0 |
|
||||
| storage | s3_multipart_uploads | created_at | timestamp with time zone | NO | now() |
|
||||
| realtime | schema_migrations | version | bigint | NO | null |
|
||||
| storage | s3_multipart_uploads | user_metadata | jsonb | YES | null |
|
||||
| extensions | pg_stat_statements | plans | bigint | YES | null |
|
||||
| extensions | pg_stat_statements | total_plan_time | double precision | YES | null |
|
||||
| extensions | pg_stat_statements | min_plan_time | double precision | YES | null |
|
||||
@ -338,7 +355,17 @@
|
||||
| storage | objects | user_metadata | jsonb | YES | null |
|
||||
| storage | migrations | id | integer | NO | null |
|
||||
| storage | migrations | executed_at | timestamp without time zone | YES | CURRENT_TIMESTAMP |
|
||||
| public | essay_types | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_types | active | boolean | YES | true |
|
||||
| public | essay_types | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | essay_types | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| pgsodium | mask_columns | attrelid | oid | YES | null |
|
||||
| public | essay_genres | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_genres | type_id | uuid | NO | null |
|
||||
| public | essay_genres | requirements | jsonb | NO | '{}'::jsonb |
|
||||
| public | essay_genres | active | boolean | YES | true |
|
||||
| public | essay_genres | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | essay_genres | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| pgsodium | valid_key | id | uuid | YES | null |
|
||||
| pgsodium | valid_key | status | USER-DEFINED | YES | null |
|
||||
| pgsodium | valid_key | key_type | USER-DEFINED | YES | null |
|
||||
@ -346,6 +373,12 @@
|
||||
| pgsodium | valid_key | key_context | bytea | YES | null |
|
||||
| pgsodium | valid_key | created | timestamp with time zone | YES | null |
|
||||
| pgsodium | valid_key | expires | timestamp with time zone | YES | null |
|
||||
| public | student_essays | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | student_essays | student_id | uuid | NO | null |
|
||||
| public | student_essays | type_id | uuid | NO | null |
|
||||
| public | student_essays | genre_id | uuid | NO | null |
|
||||
| public | student_essays | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | student_essays | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| pgsodium | decrypted_key | id | uuid | YES | null |
|
||||
| pgsodium | decrypted_key | status | USER-DEFINED | YES | null |
|
||||
| pgsodium | decrypted_key | created | timestamp with time zone | YES | null |
|
||||
@ -404,71 +437,89 @@
|
||||
| public | story_pages | story_id | uuid | YES | null |
|
||||
| public | story_pages | page_number | integer | NO | null |
|
||||
| public | story_pages | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | essay_analyses | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_analyses | essay_id | uuid | NO | null |
|
||||
| public | essay_analyses | overall_score | integer | NO | null |
|
||||
| public | essay_analyses | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | essay_analysis_feedback | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_analysis_feedback | analysis_id | uuid | NO | null |
|
||||
| public | essay_analysis_feedback | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | schools | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | schools | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | schools | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | essay_analysis_strengths | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_analysis_strengths | analysis_id | uuid | NO | null |
|
||||
| public | essay_analysis_strengths | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | essay_analysis_improvements | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | essay_analysis_improvements | analysis_id | uuid | NO | null |
|
||||
| public | essay_analysis_improvements | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | teachers | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | teachers | school_id | uuid | NO | null |
|
||||
| public | teachers | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | teachers | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | teachers | class_ids | ARRAY | YES | null |
|
||||
| public | classes | id | uuid | NO | uuid_generate_v4() |
|
||||
| public | classes | school_id | uuid | NO | null |
|
||||
| public | classes | year | integer | NO | null |
|
||||
| public | classes | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | classes | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
|
||||
| public | classes | teacher_id | uuid | YES | null |
|
||||
| public | teacher_classes | id | uuid | NO | uuid_generate_v4() |
|
||||
| vault | secrets | secret | text | NO | null |
|
||||
| public | story_themes | icon | text | NO | null |
|
||||
| public | story_recordings | improvements | ARRAY | YES | null |
|
||||
| public | achievement_types | name | character varying | NO | null |
|
||||
| public | achievement_types | description | text | YES | null |
|
||||
| public | story_recordings | suggestions | text | YES | null |
|
||||
| vault | decrypted_secrets | name | text | YES | null |
|
||||
| vault | decrypted_secrets | description | text | YES | null |
|
||||
| vault | decrypted_secrets | secret | text | YES | null |
|
||||
| pgsodium | key | associated_data | text | YES | 'associated'::text |
|
||||
| storage | s3_multipart_uploads_parts | bucket_id | text | NO | null |
|
||||
| auth | sso_domains | domain | text | NO | null |
|
||||
| auth | flow_state | authentication_method | text | NO | null |
|
||||
| pgsodium | key | comment | text | YES | null |
|
||||
| pgsodium | key | user_data | text | YES | null |
|
||||
| public | stories | context | text | YES | null |
|
||||
| public | student_phonics_attempt_answers | answer_text | text | YES | null |
|
||||
| public | story_recordings | strengths | ARRAY | YES | null |
|
||||
| pgsodium | masking_rule | format_type | text | YES | null |
|
||||
| public | story_themes | slug | text | NO | null |
|
||||
| public | story_themes | title | text | NO | null |
|
||||
| public | story_themes | description | text | NO | null |
|
||||
| vault | secrets | name | text | YES | null |
|
||||
| vault | secrets | description | text | NO | ''::text |
|
||||
| extensions | pg_stat_statements | query | text | YES | null |
|
||||
| public | story_subjects | slug | text | NO | null |
|
||||
| public | story_subjects | title | text | NO | null |
|
||||
| public | story_subjects | description | text | NO | null |
|
||||
| public | story_subjects | icon | text | NO | null |
|
||||
| realtime | messages | topic | text | NO | null |
|
||||
| realtime | messages | extension | text | NO | null |
|
||||
| realtime | messages | event | text | YES | null |
|
||||
| auth | mfa_factors | secret | text | YES | null |
|
||||
| public | interests | category | text | NO | null |
|
||||
| public | interests | item | text | NO | null |
|
||||
| public | story_characters | slug | text | NO | null |
|
||||
| public | story_characters | title | text | NO | null |
|
||||
| public | story_characters | description | text | NO | null |
|
||||
| public | story_generations | original_prompt | text | NO | null |
|
||||
| public | story_generations | ai_response | text | NO | null |
|
||||
| public | story_generations | model_used | text | NO | null |
|
||||
| public | story_characters | icon | text | NO | null |
|
||||
| auth | mfa_factors | phone | text | YES | null |
|
||||
| auth | identities | provider_id | text | NO | null |
|
||||
| net | http_request_queue | method | text | NO | null |
|
||||
| net | http_request_queue | url | text | NO | null |
|
||||
| storage | s3_multipart_uploads_parts | etag | text | NO | null |
|
||||
| public | achievements | name | text | YES | null |
|
||||
| public | story_settings | slug | text | NO | null |
|
||||
| public | story_settings | title | text | NO | null |
|
||||
| public | story_settings | description | text | NO | null |
|
||||
| net | _http_response | content_type | text | YES | null |
|
||||
| public | story_settings | icon | text | NO | null |
|
||||
| net | _http_response | content | text | YES | null |
|
||||
| public | achievements | description | text | YES | null |
|
||||
| public | story_details | title | text | YES | null |
|
||||
| net | _http_response | error_msg | text | YES | null |
|
||||
| public | story_details | status | text | YES | null |
|
||||
| storage | s3_multipart_uploads_parts | owner_id | text | YES | null |
|
||||
| auth | mfa_amr_claims | authentication_method | text | NO | null |
|
||||
| auth | identities | provider | text | NO | null |
|
||||
| storage | s3_multipart_uploads_parts | version | text | NO | null |
|
||||
| realtime | messages | topic | text | NO | null |
|
||||
| realtime | messages | extension | text | NO | null |
|
||||
| public | teacher_invites | email | text | NO | null |
|
||||
| realtime | messages | event | text | YES | null |
|
||||
| public | teacher_invites | name | text | NO | null |
|
||||
| public | teacher_invites | subject | text | YES | null |
|
||||
| public | teacher_invites | message | text | YES | null |
|
||||
| public | teacher_invites | status | text | YES | 'pending'::text |
|
||||
| public | teacher_invites | token | text | NO | null |
|
||||
| storage | s3_multipart_uploads | version | text | NO | null |
|
||||
| public | story_generations | original_prompt | text | NO | null |
|
||||
| public | story_generations | ai_response | text | NO | null |
|
||||
| public | story_generations | model_used | text | NO | null |
|
||||
| auth | saml_providers | entity_id | text | NO | null |
|
||||
| auth | mfa_challenges | otp_code | text | YES | null |
|
||||
| auth | saml_providers | metadata_xml | text | NO | null |
|
||||
| net | http_request_queue | method | text | NO | null |
|
||||
| net | http_request_queue | url | text | NO | null |
|
||||
| auth | saml_providers | metadata_url | text | YES | null |
|
||||
| public | phonics_exercises | title | character varying | NO | null |
|
||||
| public | phonics_exercises | description | text | YES | null |
|
||||
| public | phonics_achievements | name | character varying | NO | null |
|
||||
| public | phonics_achievements | description | text | YES | null |
|
||||
| net | _http_response | content_type | text | YES | null |
|
||||
| public | phonics_exercises | instructions | text | NO | null |
|
||||
| net | _http_response | content | text | YES | null |
|
||||
| storage | s3_multipart_uploads | id | text | NO | null |
|
||||
| net | _http_response | error_msg | text | YES | null |
|
||||
| public | phonics_achievements | icon_url | text | YES | null |
|
||||
| auth | identities | email | text | YES | null |
|
||||
| storage | s3_multipart_uploads | owner_id | text | YES | null |
|
||||
| auth | saml_relay_states | request_id | text | NO | null |
|
||||
| public | story_pages | text | text | NO | null |
|
||||
| public | story_pages | image_url | text | NO | null |
|
||||
| auth | saml_relay_states | for_email | text | YES | null |
|
||||
| public | story_pages | image_path | text | YES | null |
|
||||
| public | story_pages | image_url_thumb | text | YES | null |
|
||||
| public | story_details | title | text | YES | null |
|
||||
| public | story_pages | image_url_medium | text | YES | null |
|
||||
| public | story_details | status | text | YES | null |
|
||||
| public | story_pages | image_url_large | text | YES | null |
|
||||
| public | story_pages | image_path_thumb | text | YES | null |
|
||||
| public | story_pages | image_path_medium | text | YES | null |
|
||||
| public | story_pages | image_path_large | text | YES | null |
|
||||
| public | story_pages | text_syllables | text | YES | null |
|
||||
| public | phonics_exercise_types | name | character varying | NO | null |
|
||||
| public | story_details | context | text | YES | null |
|
||||
| public | story_details | theme_title | text | YES | null |
|
||||
| public | story_details | theme_icon | text | YES | null |
|
||||
@ -478,70 +529,70 @@
|
||||
| public | story_details | character_icon | text | YES | null |
|
||||
| public | story_details | setting_title | text | YES | null |
|
||||
| public | story_details | setting_icon | text | YES | null |
|
||||
| public | schools | name | text | NO | null |
|
||||
| public | schools | address | text | YES | null |
|
||||
| public | teacher_invites | email | text | NO | null |
|
||||
| public | story_pages | image_path | text | YES | null |
|
||||
| public | story_exercise_words | word | text | NO | null |
|
||||
| public | story_exercise_words | exercise_type | text | NO | null |
|
||||
| public | story_exercise_words | phonemes | ARRAY | YES | null |
|
||||
| public | story_exercise_words | syllable_pattern | text | YES | null |
|
||||
| public | schools | phone | text | YES | null |
|
||||
| public | schools | email | text | YES | null |
|
||||
| public | phonics_exercise_types | description | text | YES | null |
|
||||
| auth | saml_relay_states | redirect_to | text | YES | null |
|
||||
| public | schools | director_name | text | NO | 'Não informado'::text |
|
||||
| auth | audit_log_entries | ip_address | character varying | NO | ''::character varying |
|
||||
| public | phonics_words | word | character varying | NO | null |
|
||||
| public | teachers | name | text | NO | null |
|
||||
| public | story_pages | image_url_thumb | text | YES | null |
|
||||
| public | story_pages | image_url_medium | text | YES | null |
|
||||
| public | story_pages | image_url_large | text | YES | null |
|
||||
| public | story_pages | image_path_thumb | text | YES | null |
|
||||
| public | story_pages | image_path_medium | text | YES | null |
|
||||
| public | story_pages | image_path_large | text | YES | null |
|
||||
| public | story_pages | text_syllables | text | YES | null |
|
||||
| public | teacher_invites | name | text | NO | null |
|
||||
| auth | schema_migrations | version | character varying | NO | null |
|
||||
| public | teachers | email | text | NO | null |
|
||||
| public | teachers | phone | text | YES | null |
|
||||
| public | teacher_invites | subject | text | YES | null |
|
||||
| public | teacher_invites | message | text | YES | null |
|
||||
| auth | instances | raw_base_config | text | YES | null |
|
||||
| public | teachers | subject | text | YES | null |
|
||||
| public | phonics_words | phonetic_transcription | character varying | YES | null |
|
||||
| auth | saml_providers | name_id_format | text | YES | null |
|
||||
| public | teachers | status | text | YES | 'pending'::text |
|
||||
| public | essay_analyses | suggestions | text | YES | null |
|
||||
| public | teacher_invites | status | text | YES | 'pending'::text |
|
||||
| public | teacher_invites | token | text | NO | null |
|
||||
| storage | s3_multipart_uploads | version | text | NO | null |
|
||||
| auth | users | aud | character varying | YES | null |
|
||||
| auth | users | role | character varying | YES | null |
|
||||
| auth | users | email | character varying | YES | null |
|
||||
| auth | users | encrypted_password | character varying | YES | null |
|
||||
| public | phonics_word_audio | word | text | NO | null |
|
||||
| public | phonics_word_audio | audio_url | text | NO | null |
|
||||
| public | essay_analysis_feedback | structure_feedback | text | NO | null |
|
||||
| public | essay_analysis_feedback | content_feedback | text | NO | null |
|
||||
| auth | users | confirmation_token | character varying | YES | null |
|
||||
| public | phonics_word_audio | audio_path | text | NO | null |
|
||||
| public | essay_analysis_feedback | language_feedback | text | NO | null |
|
||||
| auth | users | recovery_token | character varying | YES | null |
|
||||
| public | classes | name | text | NO | null |
|
||||
| auth | saml_providers | entity_id | text | NO | null |
|
||||
| auth | users | email_change_token_new | character varying | YES | null |
|
||||
| auth | users | email_change | character varying | YES | null |
|
||||
| public | classes | grade | text | NO | null |
|
||||
| storage | s3_multipart_uploads | upload_signature | text | NO | null |
|
||||
| public | classes | period | text | YES | null |
|
||||
| storage | s3_multipart_uploads_parts | upload_id | text | NO | null |
|
||||
| public | languages | name | character varying | NO | null |
|
||||
| public | languages | code | character varying | NO | null |
|
||||
| public | languages | instructions | text | YES | null |
|
||||
| auth | mfa_challenges | otp_code | text | YES | null |
|
||||
| public | schools | name | text | NO | null |
|
||||
| public | schools | address | text | YES | null |
|
||||
| public | schools | phone | text | YES | null |
|
||||
| public | schools | email | text | YES | null |
|
||||
| auth | saml_providers | metadata_xml | text | NO | null |
|
||||
| auth | saml_providers | metadata_url | text | YES | null |
|
||||
| auth | users | phone | text | YES | NULL::character varying |
|
||||
| public | phonics_categories | name | character varying | NO | null |
|
||||
| public | schools | director_name | text | NO | 'Não informado'::text |
|
||||
| auth | users | phone_change | text | YES | ''::character varying |
|
||||
| auth | users | phone_change_token | character varying | YES | ''::character varying |
|
||||
| public | phonics_categories | description | text | YES | null |
|
||||
| public | languages | flag_icon | character varying | YES | null |
|
||||
| public | phonics_exercises | title | character varying | NO | null |
|
||||
| public | phonics_exercises | description | text | YES | null |
|
||||
| auth | users | email_change_token_current | character varying | YES | ''::character varying |
|
||||
| auth | sso_providers | resource_id | text | YES | null |
|
||||
| auth | flow_state | auth_code | text | NO | null |
|
||||
| public | essay_analysis_strengths | strength | text | NO | null |
|
||||
| public | phonics_achievements | name | character varying | NO | null |
|
||||
| auth | users | reauthentication_token | character varying | YES | ''::character varying |
|
||||
| public | students | name | text | NO | null |
|
||||
| public | students | email | text | NO | null |
|
||||
| storage | s3_multipart_uploads | bucket_id | text | NO | null |
|
||||
| public | students | guardian_name | text | YES | null |
|
||||
| public | students | guardian_phone | text | YES | null |
|
||||
| public | students | guardian_email | text | YES | null |
|
||||
| public | phonics_achievements | description | text | YES | null |
|
||||
| public | phonics_exercises | instructions | text | NO | null |
|
||||
| public | essay_analysis_improvements | improvement | text | NO | null |
|
||||
| storage | s3_multipart_uploads | id | text | NO | null |
|
||||
| public | phonics_achievements | icon_url | text | YES | null |
|
||||
| auth | identities | email | text | YES | null |
|
||||
| auth | refresh_tokens | token | character varying | YES | null |
|
||||
| auth | refresh_tokens | user_id | character varying | YES | null |
|
||||
| auth | flow_state | code_challenge | text | NO | null |
|
||||
| auth | flow_state | provider_type | text | NO | null |
|
||||
| public | students | status | text | NO | 'active'::text |
|
||||
| public | teachers | name | text | NO | null |
|
||||
| public | teachers | email | text | NO | null |
|
||||
| public | teachers | phone | text | YES | null |
|
||||
| auth | refresh_tokens | parent | character varying | YES | null |
|
||||
| public | story_recordings | audio_url | text | YES | null |
|
||||
| public | teachers | subject | text | YES | null |
|
||||
| supabase_migrations | seed_files | path | text | NO | null |
|
||||
| supabase_migrations | seed_files | hash | text | NO | null |
|
||||
| supabase_migrations | schema_migrations | version | text | NO | null |
|
||||
@ -549,99 +600,102 @@
|
||||
| supabase_migrations | schema_migrations | name | text | YES | null |
|
||||
| storage | buckets | id | text | NO | null |
|
||||
| storage | buckets | name | text | NO | null |
|
||||
| storage | s3_multipart_uploads | owner_id | text | YES | null |
|
||||
| auth | saml_relay_states | request_id | text | NO | null |
|
||||
| public | teachers | status | text | YES | 'pending'::text |
|
||||
| auth | saml_relay_states | for_email | text | YES | null |
|
||||
| public | phonics_exercise_types | name | character varying | NO | null |
|
||||
| public | phonics_exercise_types | description | text | YES | null |
|
||||
| storage | buckets | allowed_mime_types | ARRAY | YES | null |
|
||||
| storage | buckets | owner_id | text | YES | null |
|
||||
| public | classes | name | text | NO | null |
|
||||
| storage | objects | bucket_id | text | YES | null |
|
||||
| storage | objects | name | text | YES | null |
|
||||
| public | classes | grade | text | NO | null |
|
||||
| auth | saml_relay_states | redirect_to | text | YES | null |
|
||||
| public | classes | period | text | YES | null |
|
||||
| auth | audit_log_entries | ip_address | character varying | NO | ''::character varying |
|
||||
| public | phonics_words | word | character varying | NO | null |
|
||||
| storage | objects | path_tokens | ARRAY | YES | null |
|
||||
| storage | objects | version | text | YES | null |
|
||||
| storage | objects | owner_id | text | YES | null |
|
||||
| public | phonics_words | phonetic_transcription | character varying | YES | null |
|
||||
| auth | saml_providers | name_id_format | text | YES | null |
|
||||
| storage | migrations | name | character varying | NO | null |
|
||||
| storage | migrations | hash | character varying | NO | null |
|
||||
| public | phonics_word_audio | word | text | NO | null |
|
||||
| public | phonics_word_audio | audio_url | text | NO | null |
|
||||
| public | essay_types | slug | text | NO | null |
|
||||
| public | essay_types | title | text | NO | null |
|
||||
| public | essay_types | description | text | NO | null |
|
||||
| public | essay_types | icon | text | NO | null |
|
||||
| public | phonics_word_audio | audio_path | text | NO | null |
|
||||
| storage | s3_multipart_uploads | upload_signature | text | NO | null |
|
||||
| storage | s3_multipart_uploads_parts | upload_id | text | NO | null |
|
||||
| public | languages | name | character varying | NO | null |
|
||||
| pgsodium | mask_columns | format_type | text | YES | null |
|
||||
| public | languages | code | character varying | NO | null |
|
||||
| public | languages | instructions | text | YES | null |
|
||||
| public | essay_genres | slug | text | NO | null |
|
||||
| public | essay_genres | title | text | NO | null |
|
||||
| public | essay_genres | description | text | NO | null |
|
||||
| public | essay_genres | icon | text | NO | null |
|
||||
| public | phonics_categories | name | character varying | NO | null |
|
||||
| public | phonics_categories | description | text | YES | null |
|
||||
| public | languages | flag_icon | character varying | YES | null |
|
||||
| auth | sso_providers | resource_id | text | YES | null |
|
||||
| auth | flow_state | auth_code | text | NO | null |
|
||||
| pgsodium | valid_key | name | text | YES | null |
|
||||
| public | students | name | text | NO | null |
|
||||
| public | students | email | text | NO | null |
|
||||
| storage | s3_multipart_uploads | bucket_id | text | NO | null |
|
||||
| public | students | guardian_name | text | YES | null |
|
||||
| public | students | guardian_phone | text | YES | null |
|
||||
| public | students | guardian_email | text | YES | null |
|
||||
| pgsodium | valid_key | associated_data | text | YES | null |
|
||||
| auth | flow_state | code_challenge | text | NO | null |
|
||||
| auth | flow_state | provider_type | text | NO | null |
|
||||
| public | students | status | text | NO | 'active'::text |
|
||||
| public | story_recordings | audio_url | text | YES | null |
|
||||
| public | student_essays | title | text | NO | null |
|
||||
| public | student_essays | content | text | NO | null |
|
||||
| public | student_essays | status | text | NO | 'draft'::text |
|
||||
| public | students | avatar_url | text | YES | null |
|
||||
| public | students | nickname | character varying | YES | null |
|
||||
| public | phonics_exercise_media | url | text | NO | null |
|
||||
| public | phonics_exercise_media | alt_text | text | YES | null |
|
||||
| public | students | preferred_themes | ARRAY | YES | null |
|
||||
| public | story_recordings | status | text | NO | 'pending_analysis'::text |
|
||||
| storage | buckets | allowed_mime_types | ARRAY | YES | null |
|
||||
| storage | buckets | owner_id | text | YES | null |
|
||||
| auth | flow_state | provider_access_token | text | YES | null |
|
||||
| storage | objects | bucket_id | text | YES | null |
|
||||
| storage | objects | name | text | YES | null |
|
||||
| auth | one_time_tokens | token_hash | text | NO | null |
|
||||
| public | media_types | name | character varying | NO | null |
|
||||
| pgsodium | decrypted_key | name | text | YES | null |
|
||||
| pgsodium | decrypted_key | associated_data | text | YES | null |
|
||||
| public | media_types | description | text | YES | null |
|
||||
| public | story_recordings | transcription | text | YES | null |
|
||||
| auth | one_time_tokens | relates_to | text | NO | null |
|
||||
| storage | objects | path_tokens | ARRAY | YES | null |
|
||||
| storage | objects | version | text | YES | null |
|
||||
| storage | objects | owner_id | text | YES | null |
|
||||
| public | story_recordings | error_message | text | YES | null |
|
||||
| pgsodium | decrypted_key | comment | text | YES | null |
|
||||
| auth | sessions | user_agent | text | YES | null |
|
||||
| storage | migrations | name | character varying | NO | null |
|
||||
| storage | migrations | hash | character varying | NO | null |
|
||||
| public | stories | title | text | NO | null |
|
||||
| pgsodium | masking_rule | format_type | text | YES | null |
|
||||
| auth | identities | provider_id | text | NO | null |
|
||||
| storage | s3_multipart_uploads_parts | etag | text | NO | null |
|
||||
| public | achievements | name | text | YES | null |
|
||||
| vault | secrets | name | text | YES | null |
|
||||
| vault | secrets | description | text | NO | ''::text |
|
||||
| vault | secrets | secret | text | NO | null |
|
||||
| public | story_settings | slug | text | NO | null |
|
||||
| public | story_settings | title | text | NO | null |
|
||||
| public | story_settings | description | text | NO | null |
|
||||
| public | story_settings | icon | text | NO | null |
|
||||
| public | achievements | description | text | YES | null |
|
||||
| vault | decrypted_secrets | name | text | YES | null |
|
||||
| vault | decrypted_secrets | description | text | YES | null |
|
||||
| vault | decrypted_secrets | secret | text | YES | null |
|
||||
| extensions | pg_stat_statements | query | text | YES | null |
|
||||
| auth | flow_state | provider_refresh_token | text | YES | null |
|
||||
| pgsodium | mask_columns | format_type | text | YES | null |
|
||||
| public | stories | status | text | YES | 'draft'::text |
|
||||
| pgsodium | valid_key | name | text | YES | null |
|
||||
| auth | sessions | tag | text | YES | null |
|
||||
| auth | mfa_factors | friendly_name | text | YES | null |
|
||||
| storage | s3_multipart_uploads_parts | bucket_id | text | NO | null |
|
||||
| auth | sso_domains | domain | text | NO | null |
|
||||
| auth | flow_state | authentication_method | text | NO | null |
|
||||
| pgsodium | valid_key | associated_data | text | YES | null |
|
||||
| public | stories | context | text | YES | null |
|
||||
| public | student_phonics_attempt_answers | answer_text | text | YES | null |
|
||||
| public | story_recordings | strengths | ARRAY | YES | null |
|
||||
| public | story_themes | slug | text | NO | null |
|
||||
| public | story_themes | title | text | NO | null |
|
||||
| public | story_themes | description | text | NO | null |
|
||||
| public | story_themes | icon | text | NO | null |
|
||||
| pgsodium | decrypted_key | name | text | YES | null |
|
||||
| pgsodium | decrypted_key | associated_data | text | YES | null |
|
||||
| public | story_recordings | improvements | ARRAY | YES | null |
|
||||
| public | achievement_types | name | character varying | NO | null |
|
||||
| public | achievement_types | description | text | YES | null |
|
||||
| public | story_recordings | suggestions | text | YES | null |
|
||||
| pgsodium | decrypted_key | comment | text | YES | null |
|
||||
| public | story_subjects | slug | text | NO | null |
|
||||
| public | story_subjects | title | text | NO | null |
|
||||
| public | story_subjects | description | text | NO | null |
|
||||
| public | story_subjects | icon | text | NO | null |
|
||||
| auth | mfa_factors | secret | text | YES | null |
|
||||
| public | interests | category | text | NO | null |
|
||||
| pgsodium | key | name | text | YES | null |
|
||||
| pgsodium | key | associated_data | text | YES | 'associated'::text |
|
||||
| public | interests | item | text | NO | null |
|
||||
| public | story_characters | slug | text | NO | null |
|
||||
| public | story_characters | title | text | NO | null |
|
||||
| pgsodium | key | comment | text | YES | null |
|
||||
| pgsodium | key | user_data | text | YES | null |
|
||||
| public | story_characters | description | text | NO | null |
|
||||
| public | story_characters | icon | text | NO | null |
|
||||
| auth | mfa_factors | phone | text | YES | null |
|
||||
| pgsodium | mask_columns | attname | name | YES | null |
|
||||
| pgsodium | masking_rule | nonce_column | text | YES | null |
|
||||
| pgsodium | mask_columns | key_id | text | YES | null |
|
||||
| vault | decrypted_secrets | decrypted_secret | text | YES | null |
|
||||
| pgsodium | mask_columns | key_id_column | text | YES | null |
|
||||
| pgsodium | mask_columns | associated_columns | text | YES | null |
|
||||
| pgsodium | mask_columns | nonce_column | text | YES | null |
|
||||
| pgsodium | masking_rule | view_name | text | YES | null |
|
||||
| pgsodium | masking_rule | relname | name | YES | null |
|
||||
| pgsodium | masking_rule | attname | name | YES | null |
|
||||
| vault | decrypted_secrets | decrypted_secret | text | YES | null |
|
||||
| storage | s3_multipart_uploads | key | text | NO | null |
|
||||
| pgsodium | masking_rule | col_description | text | YES | null |
|
||||
| pgsodium | masking_rule | key_id_column | text | YES | null |
|
||||
| pgsodium | masking_rule | key_id | text | YES | null |
|
||||
| storage | s3_multipart_uploads_parts | key | text | NO | null |
|
||||
| pgsodium | masking_rule | associated_columns | text | YES | null |
|
||||
| storage | s3_multipart_uploads | key | text | NO | null |
|
||||
| pgsodium | masking_rule | key_id | text | YES | null |
|
||||
| pgsodium | masking_rule | associated_columns | text | YES | null |
|
||||
| pgsodium | masking_rule | nonce_column | text | YES | null |
|
||||
| pgsodium | masking_rule | relname | name | YES | null |
|
||||
| pgsodium | mask_columns | attname | name | YES | null |
|
||||
| pgsodium | masking_rule | attname | name | YES | null |
|
||||
| pgsodium | mask_columns | key_id | text | YES | null |
|
||||
@ -63,18 +63,18 @@ WITH
|
||||
SELECT extensions.url_encode(extensions.hmac(signables, secret, alg.id)) FROM alg;
|
||||
$function$
|
||||
|
|
||||
| extensions | armor | CREATE OR REPLACE FUNCTION extensions.armor(bytea, text[], text[])
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pg_armor$function$
|
||||
|
|
||||
| extensions | armor | CREATE OR REPLACE FUNCTION extensions.armor(bytea)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pg_armor$function$
|
||||
|
|
||||
| extensions | armor | CREATE OR REPLACE FUNCTION extensions.armor(bytea, text[], text[])
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pg_armor$function$
|
||||
|
|
||||
| extensions | bytea_to_text | CREATE OR REPLACE FUNCTION extensions.bytea_to_text(data bytea)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
@ -296,16 +296,21 @@ AS '$libdir/pgcrypto', $function$pg_hmac$function$
|
||||
LANGUAGE c
|
||||
AS '$libdir/http', $function$http_request$function$
|
||||
|
|
||||
| extensions | http_delete | CREATE OR REPLACE FUNCTION extensions.http_delete(uri character varying, content character varying, content_type character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('DELETE', $1, NULL, $3, $2)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_delete | CREATE OR REPLACE FUNCTION extensions.http_delete(uri character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('DELETE', $1, NULL, NULL, NULL)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_delete | CREATE OR REPLACE FUNCTION extensions.http_delete(uri character varying, content character varying, content_type character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('DELETE', $1, NULL, $3, $2)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_get | CREATE OR REPLACE FUNCTION extensions.http_get(uri character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('GET', $1, NULL, NULL, NULL)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_get | CREATE OR REPLACE FUNCTION extensions.http_get(uri character varying, data jsonb)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
@ -313,11 +318,6 @@ AS $function$
|
||||
SELECT extensions.http(('GET', $1 || '?' || extensions.urlencode($2), NULL, NULL, NULL)::extensions.http_request)
|
||||
$function$
|
||||
|
|
||||
| extensions | http_get | CREATE OR REPLACE FUNCTION extensions.http_get(uri character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('GET', $1, NULL, NULL, NULL)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_head | CREATE OR REPLACE FUNCTION extensions.http_head(uri character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
@ -338,11 +338,6 @@ AS '$libdir/http', $function$http_list_curlopt$function$
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('PATCH', $1, NULL, $3, $2)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_post | CREATE OR REPLACE FUNCTION extensions.http_post(uri character varying, content character varying, content_type character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('POST', $1, NULL, $3, $2)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_post | CREATE OR REPLACE FUNCTION extensions.http_post(uri character varying, data jsonb)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
@ -350,6 +345,11 @@ AS $function$
|
||||
SELECT extensions.http(('POST', $1, NULL, 'application/x-www-form-urlencoded', extensions.urlencode($2))::extensions.http_request)
|
||||
$function$
|
||||
|
|
||||
| extensions | http_post | CREATE OR REPLACE FUNCTION extensions.http_post(uri character varying, content character varying, content_type character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
AS $function$ SELECT extensions.http(('POST', $1, NULL, $3, $2)::extensions.http_request) $function$
|
||||
|
|
||||
| extensions | http_put | CREATE OR REPLACE FUNCTION extensions.http_put(uri character varying, content character varying, content_type character varying)
|
||||
RETURNS http_response
|
||||
LANGUAGE sql
|
||||
@ -395,12 +395,6 @@ AS '$libdir/pgcrypto', $function$pgp_armor_headers$function$
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_key_id_w$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt(bytea, bytea, text, text)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt(bytea, bytea)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
@ -413,24 +407,30 @@ AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt(bytea, bytea, text, text)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_pub_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt(text, bytea)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
@ -443,18 +443,18 @@ AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_pub_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_pub_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_pub_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_sym_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt(bytea, text)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
@ -467,42 +467,42 @@ AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_sym_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_sym_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_sym_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt(text, text, text)
|
||||
| extensions | pgp_sym_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
||||
|
|
||||
IMMUTABLE PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_sym_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt(text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_sym_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text, text)
|
||||
| extensions | pgp_sym_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt(text, text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
|
||||
|
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
||||
|
|
||||
| extensions | pgp_sym_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgp_sym_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text, text)
|
||||
RETURNS bytea
|
||||
LANGUAGE c
|
||||
PARALLEL SAFE STRICT
|
||||
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
|
||||
|
|
||||
| extensions | pgrst_ddl_watch | CREATE OR REPLACE FUNCTION extensions.pgrst_ddl_watch()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
@ -675,6 +675,12 @@ AS $function$
|
||||
SELECT translate(encode(data, 'base64'), E'+/=\n', '-_');
|
||||
$function$
|
||||
|
|
||||
| extensions | urlencode | CREATE OR REPLACE FUNCTION extensions.urlencode(string bytea)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE STRICT
|
||||
AS '$libdir/http', $function$urlencode$function$
|
||||
|
|
||||
| extensions | urlencode | CREATE OR REPLACE FUNCTION extensions.urlencode(string character varying)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
@ -687,12 +693,6 @@ AS '$libdir/http', $function$urlencode$function$
|
||||
IMMUTABLE STRICT
|
||||
AS '$libdir/http', $function$urlencode_jsonb$function$
|
||||
|
|
||||
| extensions | urlencode | CREATE OR REPLACE FUNCTION extensions.urlencode(string bytea)
|
||||
RETURNS text
|
||||
LANGUAGE c
|
||||
IMMUTABLE STRICT
|
||||
AS '$libdir/http', $function$urlencode$function$
|
||||
|
|
||||
| extensions | uuid_generate_v1 | CREATE OR REPLACE FUNCTION extensions.uuid_generate_v1()
|
||||
RETURNS uuid
|
||||
LANGUAGE c
|
||||
|
||||
@ -1,31 +1,39 @@
|
||||
| policy_id | schema_name | table_name | policy_name | command | policy_using | policy_check |
|
||||
| --------- | ----------- | ---------------------- | ------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| 29823 | auth | users | Validate user metadata | * | (((raw_user_meta_data ->> 'role'::text))::user_role IS NOT NULL) | null |
|
||||
| 29615 | public | classes | Schools can create classes | a | null | (school_id = auth.uid()) |
|
||||
| 29616 | public | classes | Schools can update their classes | w | (school_id = auth.uid()) | (school_id = auth.uid()) |
|
||||
| 29614 | public | classes | Schools can view their classes | r | (school_id = auth.uid()) | null |
|
||||
| 29301 | public | classes | Turmas visíveis para usuários autenticados | r | true | null |
|
||||
| 65878 | public | interests | Students can delete their own interests | d | (auth.uid() = student_id) | null |
|
||||
| 65876 | public | interests | Students can insert their own interests | a | null | (auth.uid() = student_id) |
|
||||
| 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) |
|
||||
| 65875 | public | interests | Students can view their own interests | r | (auth.uid() = student_id) | null |
|
||||
| 104599 | public | languages | Allow insert/update for admins only | * | ((auth.jwt() ->> 'role'::text) = 'admin'::text) | ((auth.jwt() ->> 'role'::text) = 'admin'::text) |
|
||||
| 104598 | public | languages | Allow read access for all authenticated users | r | true | null |
|
||||
| 79931 | public | phonics_categories | Permitir leitura de categorias fonéticas para usuários autent | r | true | null |
|
||||
| 79932 | public | phonics_exercise_types | Permitir leitura de tipos de exercícios fonéticos para usuár | r | true | null |
|
||||
| 79934 | public | phonics_exercise_words | Permitir leitura de relações exercício-palavra para usuário | r | true | null |
|
||||
| 79930 | public | phonics_exercises | Permitir leitura de exercícios fonéticos para usuários auten | r | true | null |
|
||||
| 79933 | public | phonics_words | Permitir leitura de palavras fonéticas para usuários autentic | r | true | null |
|
||||
| 29440 | public | schools | Enable insert for registration | a | null | true |
|
||||
| 29299 | public | schools | Escolas visíveis para usuários autenticados | r | true | null |
|
||||
| 29442 | public | schools | Schools can update own data | w | (auth.uid() = id) | (auth.uid() = id) |
|
||||
| 29441 | public | schools | Schools can view own data | r | (auth.uid() = id) | null |
|
||||
| policy_id | schema_name | table_name | policy_name | command | policy_using | policy_check |
|
||||
| --------- | ----------- | ---------------------- | ------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 29823 | auth | users | Validate user metadata | * | (((raw_user_meta_data ->> 'role'::text))::user_role IS NOT NULL) | null |
|
||||
| 29615 | public | classes | Schools can create classes | a | null | (school_id = auth.uid()) |
|
||||
| 29616 | public | classes | Schools can update their classes | w | (school_id = auth.uid()) | (school_id = auth.uid()) |
|
||||
| 29614 | public | classes | Schools can view their classes | r | (school_id = auth.uid()) | null |
|
||||
| 29301 | public | classes | Turmas visíveis para usuários autenticados | r | true | null |
|
||||
| 113771 | public | essay_analyses | Alunos podem ver análises de suas próprias redações | r | (EXISTS ( SELECT 1
|
||||
FROM student_essays
|
||||
WHERE ((student_essays.id = essay_analyses.essay_id) AND (student_essays.student_id = auth.uid())))) | null |
|
||||
| 113770 | public | essay_analyses | Edge Function pode inserir análises | a | null | (((auth.jwt() ->> 'role'::text) = 'service_role'::text) OR (EXISTS ( SELECT 1
|
||||
FROM student_essays
|
||||
WHERE ((student_essays.id = essay_analyses.essay_id) AND (student_essays.student_id = auth.uid()))))) |
|
||||
| 113765 | public | essay_genres | Gêneros textuais visíveis para todos | r | (active = true) | null |
|
||||
| 113764 | public | essay_types | Tipos de redação visíveis para todos | r | (active = true) | null |
|
||||
| 65878 | public | interests | Students can delete their own interests | d | (auth.uid() = student_id) | null |
|
||||
| 65876 | public | interests | Students can insert their own interests | a | null | (auth.uid() = student_id) |
|
||||
| 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) |
|
||||
| 65875 | public | interests | Students can view their own interests | r | (auth.uid() = student_id) | null |
|
||||
| 104599 | public | languages | Allow insert/update for admins only | * | ((auth.jwt() ->> 'role'::text) = 'admin'::text) | ((auth.jwt() ->> 'role'::text) = 'admin'::text) |
|
||||
| 104598 | public | languages | Allow read access for all authenticated users | r | true | null |
|
||||
| 79931 | public | phonics_categories | Permitir leitura de categorias fonéticas para usuários autent | r | true | null |
|
||||
| 79932 | public | phonics_exercise_types | Permitir leitura de tipos de exercícios fonéticos para usuár | r | true | null |
|
||||
| 79934 | public | phonics_exercise_words | Permitir leitura de relações exercício-palavra para usuário | r | true | null |
|
||||
| 79930 | public | phonics_exercises | Permitir leitura de exercícios fonéticos para usuários auten | r | true | null |
|
||||
| 79933 | public | phonics_words | Permitir leitura de palavras fonéticas para usuários autentic | r | true | null |
|
||||
| 29440 | public | schools | Enable insert for registration | a | null | true |
|
||||
| 29299 | public | schools | Escolas visíveis para usuários autenticados | r | true | null |
|
||||
| 29442 | public | schools | Schools can update own data | w | (auth.uid() = id) | (auth.uid() = id) |
|
||||
| 29441 | public | schools | Schools can view own data | r | (auth.uid() = id) | null |
|
||||
| 29347 | public | stories | Alunos podem atualizar suas próprias histórias | w | (student_id IN ( SELECT students.id
|
||||
FROM students
|
||||
WHERE (students.email = auth.email()))) | null |
|
||||
WHERE (students.email = auth.email()))) | null |
|
||||
| 29346 | public | stories | Alunos podem criar suas próprias histórias | a | null | (student_id IN ( SELECT students.id
|
||||
FROM students
|
||||
WHERE (students.email = auth.email()))) |
|
||||
WHERE (students.email = auth.email()))) |
|
||||
| 36241 | public | stories | Estudantes podem ver suas próprias histórias | r | ((auth.uid() = student_id) AND (EXISTS ( SELECT 1
|
||||
FROM story_themes
|
||||
WHERE ((story_themes.id = stories.theme_id) AND (story_themes.active = true)))) AND (EXISTS ( SELECT 1
|
||||
@ -34,60 +42,64 @@
|
||||
FROM story_characters
|
||||
WHERE ((story_characters.id = stories.character_id) AND (story_characters.active = true)))) AND (EXISTS ( SELECT 1
|
||||
FROM story_settings
|
||||
WHERE ((story_settings.id = stories.setting_id) AND (story_settings.active = true))))) | null |
|
||||
| 29345 | public | stories | Histórias visíveis para usuários autenticados | r | true | null |
|
||||
| 53384 | public | stories | Permitir deleção pelo dono | d | (auth.uid() = student_id) | null |
|
||||
| 34952 | public | story_characters | Permitir leitura pública dos personagens | r | (active = true) | null |
|
||||
| 53955 | public | story_exercise_words | Apenas sistema pode inserir | a | null | (auth.role() = 'service_role'::text) |
|
||||
| 53954 | public | story_exercise_words | Leitura pública das palavras | r | true | null |
|
||||
| 37664 | public | story_generations | Apenas service_role pode inserir metadados | a | null | true |
|
||||
| 37663 | public | story_generations | Metadados são visíveis para todos | r | true | null |
|
||||
| 37662 | public | story_pages | Apenas service_role pode inserir páginas | a | null | true |
|
||||
| 37661 | public | story_pages | Páginas são visíveis para todos | r | true | null |
|
||||
WHERE ((story_settings.id = stories.setting_id) AND (story_settings.active = true))))) | null |
|
||||
| 29345 | public | stories | Histórias visíveis para usuários autenticados | r | true | null |
|
||||
| 53384 | public | stories | Permitir deleção pelo dono | d | (auth.uid() = student_id) | null |
|
||||
| 34952 | public | story_characters | Permitir leitura pública dos personagens | r | (active = true) | null |
|
||||
| 53955 | public | story_exercise_words | Apenas sistema pode inserir | a | null | (auth.role() = 'service_role'::text) |
|
||||
| 53954 | public | story_exercise_words | Leitura pública das palavras | r | true | null |
|
||||
| 37664 | public | story_generations | Apenas service_role pode inserir metadados | a | null | true |
|
||||
| 37663 | public | story_generations | Metadados são visíveis para todos | r | true | null |
|
||||
| 37662 | public | story_pages | Apenas service_role pode inserir páginas | a | null | true |
|
||||
| 37661 | public | story_pages | Páginas são visíveis para todos | r | true | null |
|
||||
| 31560 | public | story_recordings | Escolas podem ver todas as gravações | r | (EXISTS ( SELECT 1
|
||||
FROM students s
|
||||
WHERE ((s.id = story_recordings.student_id) AND (s.school_id = auth.uid())))) | null |
|
||||
| 30092 | public | story_recordings | Estudantes podem gravar áudios | a | null | (auth.uid() = student_id) |
|
||||
| 31511 | public | story_recordings | Estudantes podem ver suas próprias gravações | r | (auth.uid() = student_id) | null |
|
||||
WHERE ((s.id = story_recordings.student_id) AND (s.school_id = auth.uid())))) | null |
|
||||
| 30092 | public | story_recordings | Estudantes podem gravar áudios | a | null | (auth.uid() = student_id) |
|
||||
| 31511 | public | story_recordings | Estudantes podem ver suas próprias gravações | r | (auth.uid() = student_id) | null |
|
||||
| 31558 | public | story_recordings | Professores podem ver gravações de seus alunos | r | (EXISTS ( SELECT 1
|
||||
FROM (classes c
|
||||
JOIN students s ON ((s.class_id = c.id)))
|
||||
WHERE ((s.id = story_recordings.student_id) AND (c.teacher_id = auth.uid())))) | null |
|
||||
| 29748 | public | story_recordings | Students can insert their own recordings | a | null | (auth.uid() = student_id) |
|
||||
| 29749 | public | story_recordings | Students can view their own recordings | r | (auth.uid() = student_id) | null |
|
||||
| 34953 | public | story_settings | Permitir leitura pública dos cenários | r | (active = true) | null |
|
||||
| 34951 | public | story_subjects | Permitir leitura pública das disciplinas | r | (active = true) | null |
|
||||
| 34950 | public | story_themes | Permitir leitura pública das categorias | r | (active = true) | null |
|
||||
| 29302 | public | students | Alunos visíveis para usuários autenticados | r | true | null |
|
||||
WHERE ((s.id = story_recordings.student_id) AND (c.teacher_id = auth.uid())))) | null |
|
||||
| 29748 | public | story_recordings | Students can insert their own recordings | a | null | (auth.uid() = student_id) |
|
||||
| 29749 | public | story_recordings | Students can view their own recordings | r | (auth.uid() = student_id) | null |
|
||||
| 34953 | public | story_settings | Permitir leitura pública dos cenários | r | (active = true) | null |
|
||||
| 34951 | public | story_subjects | Permitir leitura pública das disciplinas | r | (active = true) | null |
|
||||
| 34950 | public | story_themes | Permitir leitura pública das categorias | r | (active = true) | null |
|
||||
| 113768 | public | student_essays | Alunos podem atualizar suas próprias redações | w | (student_id = auth.uid()) | (student_id = auth.uid()) |
|
||||
| 113767 | public | student_essays | Alunos podem criar suas próprias redações | a | null | (student_id = auth.uid()) |
|
||||
| 113769 | public | student_essays | Alunos podem deletar suas próprias redações | d | (student_id = auth.uid()) | null |
|
||||
| 113766 | public | student_essays | Alunos podem ver suas próprias redações | r | (student_id = auth.uid()) | null |
|
||||
| 29302 | public | students | Alunos visíveis para usuários autenticados | r | true | null |
|
||||
| 29638 | public | students | Escolas podem inserir seus próprios alunos | a | null | (auth.uid() IN ( SELECT schools.id
|
||||
FROM schools
|
||||
WHERE (schools.id = students.school_id))) |
|
||||
WHERE (schools.id = students.school_id))) |
|
||||
| 29639 | public | students | Escolas podem ver seus próprios alunos | r | (auth.uid() IN ( SELECT schools.id
|
||||
FROM schools
|
||||
WHERE (schools.id = students.school_id))) | null |
|
||||
| 29584 | public | students | Schools can view their students | r | (school_id = auth.uid()) | null |
|
||||
WHERE (schools.id = students.school_id))) | null |
|
||||
| 29584 | public | students | Schools can view their students | r | (school_id = auth.uid()) | null |
|
||||
| 29511 | public | teacher_invites | Schools can invite teachers | a | null | (school_id IN ( SELECT schools.id
|
||||
FROM schools
|
||||
WHERE (schools.id = auth.uid()))) |
|
||||
| 29300 | public | teachers | Professores visíveis para usuários autenticados | r | true | null |
|
||||
WHERE (schools.id = auth.uid()))) |
|
||||
| 29300 | public | teachers | Professores visíveis para usuários autenticados | r | true | null |
|
||||
| 29510 | public | teachers | Schools can view their teachers | r | (school_id IN ( SELECT schools.id
|
||||
FROM schools
|
||||
WHERE (schools.id = auth.uid()))) | null |
|
||||
| 29509 | public | teachers | Teachers can view own data | r | (auth.uid() = id) | null |
|
||||
| 29717 | storage | objects | Anyone can read recordings | r | (bucket_id = 'recordings'::text) | null |
|
||||
| 30136 | storage | objects | Estudantes podem fazer upload de áudios | a | null | ((bucket_id = 'recordings'::text) AND ((auth.uid())::text = (storage.foldername(name))[1])) |
|
||||
| 75352 | storage | objects | Imagens são publicamente acessíveis | r | (bucket_id = 'story-images'::text) | null |
|
||||
| 43940 | storage | objects | Permitir acesso da Edge Function | r | ((bucket_id = 'recordings'::text) AND ((auth.jwt() ->> 'role'::text) = 'service_role'::text)) | null |
|
||||
| 52098 | storage | objects | Permitir acesso público para leitura | r | (bucket_id = 'recordings'::text) | null |
|
||||
| 37570 | storage | objects | Permitir acesso público para leitura de imagens de histórias | r | (bucket_id = 'story-images'::text) | null |
|
||||
| 37573 | storage | objects | Permitir delete pela edge function | d | (bucket_id = 'story-images'::text) | null |
|
||||
WHERE (schools.id = auth.uid()))) | null |
|
||||
| 29509 | public | teachers | Teachers can view own data | r | (auth.uid() = id) | null |
|
||||
| 29717 | storage | objects | Anyone can read recordings | r | (bucket_id = 'recordings'::text) | null |
|
||||
| 30136 | storage | objects | Estudantes podem fazer upload de áudios | a | null | ((bucket_id = 'recordings'::text) AND ((auth.uid())::text = (storage.foldername(name))[1])) |
|
||||
| 75352 | storage | objects | Imagens são publicamente acessíveis | r | (bucket_id = 'story-images'::text) | null |
|
||||
| 43940 | storage | objects | Permitir acesso da Edge Function | r | ((bucket_id = 'recordings'::text) AND ((auth.jwt() ->> 'role'::text) = 'service_role'::text)) | null |
|
||||
| 52098 | storage | objects | Permitir acesso público para leitura | r | (bucket_id = 'recordings'::text) | null |
|
||||
| 37570 | storage | objects | Permitir acesso público para leitura de imagens de histórias | r | (bucket_id = 'story-images'::text) | null |
|
||||
| 37573 | storage | objects | Permitir delete pela edge function | d | (bucket_id = 'story-images'::text) | null |
|
||||
| 53468 | storage | objects | Permitir deleção de imagens pelo dono da história | d | ((bucket_id = 'story-images'::text) AND (EXISTS ( SELECT 1
|
||||
FROM stories s
|
||||
WHERE ((s.id = ((storage.foldername(objects.name))[1])::uuid) AND (s.student_id = auth.uid()))))) | null |
|
||||
| 53426 | storage | objects | Permitir deleção pelo dono do arquivo | d | ((bucket_id = 'recordings'::text) AND ((storage.foldername(name))[1] = (auth.uid())::text)) | null |
|
||||
| 52099 | storage | objects | Permitir download público | r | (bucket_id = 'recordings'::text) | null |
|
||||
| 37572 | storage | objects | Permitir update pela edge function | w | (bucket_id = 'story-images'::text) | null |
|
||||
| 43939 | storage | objects | Permitir upload de áudios autenticado | a | null | ((bucket_id = 'recordings'::text) AND (auth.role() = 'authenticated'::text)) |
|
||||
| 37571 | storage | objects | Permitir upload pela edge function | a | null | (bucket_id = 'story-images'::text) |
|
||||
| 29716 | storage | objects | Students can upload their recordings | a | null | ((bucket_id = 'recordings'::text) AND (auth.role() = 'student'::text)) |
|
||||
| 74045 | storage | objects | Áudios públicos | r | (bucket_id = 'phonics-audio'::text) | null |
|
||||
WHERE ((s.id = ((storage.foldername(objects.name))[1])::uuid) AND (s.student_id = auth.uid()))))) | null |
|
||||
| 53426 | storage | objects | Permitir deleção pelo dono do arquivo | d | ((bucket_id = 'recordings'::text) AND ((storage.foldername(name))[1] = (auth.uid())::text)) | null |
|
||||
| 52099 | storage | objects | Permitir download público | r | (bucket_id = 'recordings'::text) | null |
|
||||
| 37572 | storage | objects | Permitir update pela edge function | w | (bucket_id = 'story-images'::text) | null |
|
||||
| 43939 | storage | objects | Permitir upload de áudios autenticado | a | null | ((bucket_id = 'recordings'::text) AND (auth.role() = 'authenticated'::text)) |
|
||||
| 37571 | storage | objects | Permitir upload pela edge function | a | null | (bucket_id = 'story-images'::text) |
|
||||
| 29716 | storage | objects | Students can upload their recordings | a | null | ((bucket_id = 'recordings'::text) AND (auth.role() = 'student'::text)) |
|
||||
| 74045 | storage | objects | Áudios públicos | r | (bucket_id = 'phonics-audio'::text) | null |
|
||||
21
supabase/functions/_shared/cors.ts
Normal file
21
supabase/functions/_shared/cors.ts
Normal file
@ -0,0 +1,21 @@
|
||||
const ALLOWED_ORIGINS = [
|
||||
'http://localhost:5173', // Vite dev server
|
||||
'http://localhost:9999', // Supabase Edge Functions local
|
||||
'http://localhost', // Vite dev server
|
||||
'http://localhost:3000', // Caso use outro port
|
||||
'https://leiturama.ai', // Produção
|
||||
'https://leiturama.netlify.app' // Staging
|
||||
];
|
||||
|
||||
export function getCorsHeaders(req: Request) {
|
||||
const origin = req.headers.get('origin') || '';
|
||||
|
||||
return {
|
||||
'Cross-Origin-Resource-Policy': 'cross-origin',
|
||||
'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0],
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
'Access-Control-Max-Age': '86400', // 24 horas
|
||||
'Cross-Origin-Embedder-Policy': 'credentialless'
|
||||
};
|
||||
}
|
||||
461
supabase/functions/analyze-essay/index.ts
Normal file
461
supabase/functions/analyze-essay/index.ts
Normal file
@ -0,0 +1,461 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
import { getCorsHeaders } from '../_shared/cors.ts'
|
||||
import { OpenAI } from "https://deno.land/x/openai@v4.24.0/mod.ts";
|
||||
|
||||
interface EssayAnalysisRequest {
|
||||
essay_id: string;
|
||||
content: string;
|
||||
type_id: string;
|
||||
genre_id: string;
|
||||
}
|
||||
|
||||
interface EssayAnalysisResponse {
|
||||
overall_score: number;
|
||||
suggestions: string;
|
||||
feedback: {
|
||||
structure: string;
|
||||
content: string;
|
||||
language: string;
|
||||
};
|
||||
strengths: string[];
|
||||
improvements: string[];
|
||||
criteria_scores: {
|
||||
adequacy: number;
|
||||
coherence: number;
|
||||
cohesion: number;
|
||||
vocabulary: number;
|
||||
grammar: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface EssayType {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface EssayGenre {
|
||||
id: string;
|
||||
type_id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
requirements: {
|
||||
min_words: number;
|
||||
max_words: number;
|
||||
required_elements: string[];
|
||||
};
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
console.log(`[${new Date().toISOString()}] Nova requisição recebida`)
|
||||
|
||||
// Handle CORS
|
||||
if (req.method === 'OPTIONS') {
|
||||
console.log('Requisição OPTIONS - CORS preflight')
|
||||
return new Response('ok', { headers: getCorsHeaders(req) })
|
||||
}
|
||||
|
||||
try {
|
||||
// Criar cliente Supabase
|
||||
console.log('Inicializando cliente Supabase...')
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
throw new Error('Variáveis de ambiente do Supabase não configuradas')
|
||||
}
|
||||
|
||||
const supabaseClient = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
}
|
||||
})
|
||||
|
||||
// Criar cliente OpenAI
|
||||
console.log('Inicializando cliente OpenAI...')
|
||||
const openaiKey = Deno.env.get('OPENAI_API_KEY')
|
||||
if (!openaiKey) {
|
||||
throw new Error('OPENAI_API_KEY não configurada')
|
||||
}
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: openaiKey,
|
||||
});
|
||||
|
||||
// Obter dados da requisição
|
||||
console.log('Obtendo dados da requisição...')
|
||||
const requestData = await req.json()
|
||||
console.log('Dados recebidos:', JSON.stringify(requestData, null, 2))
|
||||
|
||||
const { essay_id, content, type_id, genre_id }: EssayAnalysisRequest = requestData
|
||||
|
||||
// Validar dados obrigatórios
|
||||
if (!essay_id || !content || !type_id || !genre_id) {
|
||||
console.error('Dados obrigatórios faltando:', { essay_id, content: !!content, type_id, genre_id })
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Dados obrigatórios não fornecidos',
|
||||
details: {
|
||||
essay_id: !essay_id,
|
||||
content: !content,
|
||||
type_id: !type_id,
|
||||
genre_id: !genre_id
|
||||
}
|
||||
}),
|
||||
{ status: 400, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Buscar informações do tipo e gênero
|
||||
console.log('Buscando informações do tipo e gênero...')
|
||||
const { data: typeData, error: typeError } = await supabaseClient
|
||||
.from('essay_types')
|
||||
.select('*')
|
||||
.eq('id', type_id)
|
||||
.single()
|
||||
|
||||
if (typeError) {
|
||||
console.error('Erro ao buscar tipo:', typeError)
|
||||
}
|
||||
|
||||
const { data: genreData, error: genreError } = await supabaseClient
|
||||
.from('essay_genres')
|
||||
.select('*')
|
||||
.eq('id', genre_id)
|
||||
.single()
|
||||
|
||||
if (genreError) {
|
||||
console.error('Erro ao buscar gênero:', genreError)
|
||||
}
|
||||
|
||||
if (typeError || genreError || !typeData || !genreData) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Tipo ou gênero não encontrado',
|
||||
details: {
|
||||
typeError: typeError?.message,
|
||||
genreError: genreError?.message
|
||||
}
|
||||
}),
|
||||
{ status: 404, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
const essayType: EssayType = typeData
|
||||
const essayGenre: EssayGenre = genreData
|
||||
|
||||
console.log('Tipo e gênero encontrados:', {
|
||||
type: essayType.title,
|
||||
genre: essayGenre.title
|
||||
})
|
||||
|
||||
// Construir prompt para a análise
|
||||
console.log('Construindo prompt para análise...')
|
||||
const prompt = `Você é um professor especialista em análise de textos. Analise a redação a seguir considerando que é do tipo "${essayType.title}" e gênero "${essayGenre.title}".
|
||||
|
||||
Requisitos específicos do gênero:
|
||||
- Mínimo de palavras: ${essayGenre.requirements.min_words}
|
||||
- Máximo de palavras: ${essayGenre.requirements.max_words}
|
||||
- Elementos obrigatórios: ${essayGenre.requirements.required_elements.join(', ')}
|
||||
|
||||
Texto para análise:
|
||||
${content}
|
||||
|
||||
Forneça uma análise detalhada considerando:
|
||||
1. Adequação ao tipo e gênero textual
|
||||
2. Coerência e coesão textual
|
||||
3. Vocabulário e linguagem
|
||||
4. Gramática e ortografia
|
||||
5. Elementos obrigatórios do gênero
|
||||
6. Pontos fortes
|
||||
7. Pontos a melhorar
|
||||
8. Sugestões específicas para aprimoramento
|
||||
|
||||
Responda em formato JSON seguindo exatamente esta estrutura:
|
||||
{
|
||||
"overall_score": number, // 0 a 100
|
||||
"suggestions": string, // Sugestões específicas
|
||||
"feedback": {
|
||||
"structure": string, // Feedback sobre estrutura e organização
|
||||
"content": string, // Feedback sobre conteúdo e ideias
|
||||
"language": string // Feedback sobre linguagem e gramática
|
||||
},
|
||||
"strengths": string[], // Lista de pontos fortes
|
||||
"improvements": string[], // Lista de pontos a melhorar
|
||||
"criteria_scores": {
|
||||
"adequacy": number, // 0 a 100 - Adequação ao tema/gênero
|
||||
"coherence": number, // 0 a 100 - Coerência textual
|
||||
"cohesion": number, // 0 a 100 - Coesão
|
||||
"vocabulary": number, // 0 a 100 - Vocabulário
|
||||
"grammar": number // 0 a 100 - Gramática e ortografia
|
||||
}
|
||||
}`
|
||||
|
||||
// Realizar análise com OpenAI
|
||||
console.log('Enviando requisição para OpenAI...')
|
||||
try {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "Você é um professor especialista em análise de textos, com vasta experiência em avaliação de redações."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "essay_analysis",
|
||||
strict: true,
|
||||
description: "Análise detalhada da redação",
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["overall_score", "suggestions", "feedback", "strengths", "improvements", "criteria_scores"],
|
||||
properties: {
|
||||
overall_score: {
|
||||
type: "number",
|
||||
description: "Pontuação geral da redação (0-100)"
|
||||
},
|
||||
suggestions: {
|
||||
type: "string",
|
||||
description: "Sugestões específicas para aprimoramento"
|
||||
},
|
||||
feedback: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["structure", "content", "language"],
|
||||
properties: {
|
||||
structure: {
|
||||
type: "string",
|
||||
description: "Feedback sobre estrutura e organização"
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "Feedback sobre conteúdo e ideias"
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "Feedback sobre linguagem e gramática"
|
||||
}
|
||||
}
|
||||
},
|
||||
strengths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
description: "Ponto forte da redação"
|
||||
},
|
||||
description: "Lista de pontos fortes da redação"
|
||||
},
|
||||
improvements: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
description: "Ponto a melhorar na redação"
|
||||
},
|
||||
description: "Lista de pontos a melhorar na redação"
|
||||
},
|
||||
criteria_scores: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["adequacy", "coherence", "cohesion", "vocabulary", "grammar"],
|
||||
properties: {
|
||||
adequacy: {
|
||||
type: "number",
|
||||
description: "Adequação ao tema/gênero (0-100)"
|
||||
},
|
||||
coherence: {
|
||||
type: "number",
|
||||
description: "Coerência textual (0-100)"
|
||||
},
|
||||
cohesion: {
|
||||
type: "number",
|
||||
description: "Coesão textual (0-100)"
|
||||
},
|
||||
vocabulary: {
|
||||
type: "number",
|
||||
description: "Vocabulário (0-100)"
|
||||
},
|
||||
grammar: {
|
||||
type: "number",
|
||||
description: "Gramática e ortografia (0-100)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Resposta recebida da OpenAI')
|
||||
const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content)
|
||||
console.log('Análise gerada:', JSON.stringify(analysis, null, 2))
|
||||
|
||||
// Salvar análise no banco
|
||||
console.log('Salvando análise no banco...')
|
||||
|
||||
// Primeiro, criar a análise principal
|
||||
const { data: analysisData, error: analysisError } = await supabaseClient
|
||||
.from('essay_analyses')
|
||||
.insert({
|
||||
essay_id,
|
||||
overall_score: analysis.overall_score,
|
||||
suggestions: analysis.suggestions
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (analysisError) {
|
||||
console.error('Erro ao salvar análise principal:', analysisError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao salvar análise',
|
||||
details: analysisError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Salvar feedback
|
||||
const { error: feedbackError } = await supabaseClient
|
||||
.from('essay_analysis_feedback')
|
||||
.insert({
|
||||
analysis_id: analysisData.id,
|
||||
structure_feedback: analysis.feedback.structure,
|
||||
content_feedback: analysis.feedback.content,
|
||||
language_feedback: analysis.feedback.language
|
||||
})
|
||||
|
||||
if (feedbackError) {
|
||||
console.error('Erro ao salvar feedback:', feedbackError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao salvar feedback',
|
||||
details: feedbackError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Salvar pontos fortes
|
||||
const strengths = analysis.strengths.map(strength => ({
|
||||
analysis_id: analysisData.id,
|
||||
strength
|
||||
}))
|
||||
|
||||
const { error: strengthsError } = await supabaseClient
|
||||
.from('essay_analysis_strengths')
|
||||
.insert(strengths)
|
||||
|
||||
if (strengthsError) {
|
||||
console.error('Erro ao salvar pontos fortes:', strengthsError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao salvar pontos fortes',
|
||||
details: strengthsError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Salvar pontos a melhorar
|
||||
const improvements = analysis.improvements.map(improvement => ({
|
||||
analysis_id: analysisData.id,
|
||||
improvement
|
||||
}))
|
||||
|
||||
const { error: improvementsError } = await supabaseClient
|
||||
.from('essay_analysis_improvements')
|
||||
.insert(improvements)
|
||||
|
||||
if (improvementsError) {
|
||||
console.error('Erro ao salvar pontos a melhorar:', improvementsError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao salvar pontos a melhorar',
|
||||
details: improvementsError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Salvar notas por critério
|
||||
const { error: scoresError } = await supabaseClient
|
||||
.from('essay_analysis_scores')
|
||||
.insert({
|
||||
analysis_id: analysisData.id,
|
||||
adequacy: analysis.criteria_scores.adequacy,
|
||||
coherence: analysis.criteria_scores.coherence,
|
||||
cohesion: analysis.criteria_scores.cohesion,
|
||||
vocabulary: analysis.criteria_scores.vocabulary,
|
||||
grammar: analysis.criteria_scores.grammar
|
||||
})
|
||||
|
||||
if (scoresError) {
|
||||
console.error('Erro ao salvar notas:', scoresError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao salvar notas',
|
||||
details: scoresError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// Atualizar status da redação
|
||||
console.log('Atualizando status da redação...')
|
||||
const { error: updateError } = await supabaseClient
|
||||
.from('student_essays')
|
||||
.update({ status: 'analyzed' })
|
||||
.eq('id', essay_id)
|
||||
|
||||
if (updateError) {
|
||||
console.error('Erro ao atualizar status:', updateError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao atualizar status da redação',
|
||||
details: updateError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Análise concluída com sucesso')
|
||||
// Retornar análise
|
||||
return new Response(
|
||||
JSON.stringify(analysis),
|
||||
{ headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
} catch (openaiError) {
|
||||
console.error('Erro na chamada da OpenAI:', openaiError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro ao gerar análise',
|
||||
details: openaiError.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro geral na função:', error)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Erro interno do servidor',
|
||||
details: error.message
|
||||
}),
|
||||
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
})
|
||||
229
supabase/migrations/20240326000001_create_essay_system.sql
Normal file
229
supabase/migrations/20240326000001_create_essay_system.sql
Normal file
@ -0,0 +1,229 @@
|
||||
-- Criação do sistema de redações (Essays)
|
||||
-- Tipos de texto (Narrativo, Dissertativo, etc)
|
||||
CREATE TABLE public.essay_types (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Gêneros textuais (Artigo de opinião, Carta argumentativa, etc)
|
||||
CREATE TABLE public.essay_genres (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
type_id UUID NOT NULL REFERENCES public.essay_types(id),
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
requirements JSONB NOT NULL DEFAULT '{}',
|
||||
active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Redações dos alunos
|
||||
CREATE TABLE public.student_essays (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
student_id UUID NOT NULL REFERENCES public.students(id),
|
||||
type_id UUID NOT NULL REFERENCES public.essay_types(id),
|
||||
genre_id UUID NOT NULL REFERENCES public.essay_genres(id),
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||
CONSTRAINT status_check CHECK (status IN ('draft', 'submitted', 'analyzed'))
|
||||
);
|
||||
|
||||
-- Análises das redações
|
||||
CREATE TABLE public.essay_analyses (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
essay_id UUID NOT NULL REFERENCES public.student_essays(id),
|
||||
overall_score INTEGER NOT NULL CHECK (overall_score >= 0 AND overall_score <= 100),
|
||||
suggestions TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Feedback das análises
|
||||
CREATE TABLE public.essay_analysis_feedback (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL REFERENCES public.essay_analyses(id) ON DELETE CASCADE,
|
||||
structure_feedback TEXT NOT NULL,
|
||||
content_feedback TEXT NOT NULL,
|
||||
language_feedback TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Pontos fortes das análises
|
||||
CREATE TABLE public.essay_analysis_strengths (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL REFERENCES public.essay_analyses(id) ON DELETE CASCADE,
|
||||
strength TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Pontos a melhorar das análises
|
||||
CREATE TABLE public.essay_analysis_improvements (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL REFERENCES public.essay_analyses(id) ON DELETE CASCADE,
|
||||
improvement TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Notas por critério das análises
|
||||
CREATE TABLE public.essay_analysis_scores (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL REFERENCES public.essay_analyses(id) ON DELETE CASCADE,
|
||||
adequacy INTEGER NOT NULL CHECK (adequacy >= 0 AND adequacy <= 100),
|
||||
coherence INTEGER NOT NULL CHECK (coherence >= 0 AND coherence <= 100),
|
||||
cohesion INTEGER NOT NULL CHECK (cohesion >= 0 AND cohesion <= 100),
|
||||
vocabulary INTEGER NOT NULL CHECK (vocabulary >= 0 AND vocabulary <= 100),
|
||||
grammar INTEGER NOT NULL CHECK (grammar >= 0 AND grammar <= 100),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Índices para melhor performance
|
||||
CREATE INDEX idx_student_essays_student_id ON public.student_essays(student_id);
|
||||
CREATE INDEX idx_student_essays_status ON public.student_essays(status);
|
||||
CREATE INDEX idx_essay_genres_type_id ON public.essay_genres(type_id);
|
||||
CREATE INDEX idx_essay_analyses_essay_id ON public.essay_analyses(essay_id);
|
||||
CREATE INDEX idx_essay_analysis_feedback_analysis_id ON public.essay_analysis_feedback(analysis_id);
|
||||
CREATE INDEX idx_essay_analysis_strengths_analysis_id ON public.essay_analysis_strengths(analysis_id);
|
||||
CREATE INDEX idx_essay_analysis_improvements_analysis_id ON public.essay_analysis_improvements(analysis_id);
|
||||
CREATE INDEX idx_essay_analysis_scores_analysis_id ON public.essay_analysis_scores(analysis_id);
|
||||
|
||||
-- Triggers para updated_at
|
||||
CREATE OR REPLACE FUNCTION public.handle_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER essay_types_updated_at
|
||||
BEFORE UPDATE ON public.essay_types
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE public.handle_updated_at();
|
||||
|
||||
CREATE TRIGGER essay_genres_updated_at
|
||||
BEFORE UPDATE ON public.essay_genres
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE public.handle_updated_at();
|
||||
|
||||
CREATE TRIGGER student_essays_updated_at
|
||||
BEFORE UPDATE ON public.student_essays
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE public.handle_updated_at();
|
||||
|
||||
-- Políticas RLS
|
||||
ALTER TABLE public.essay_types ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_genres ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.student_essays ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analyses ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Políticas para essay_types (visível para todos)
|
||||
CREATE POLICY "Tipos de redação visíveis para todos"
|
||||
ON public.essay_types FOR SELECT
|
||||
USING (active = true);
|
||||
|
||||
-- Políticas para essay_genres (visível para todos)
|
||||
CREATE POLICY "Gêneros textuais visíveis para todos"
|
||||
ON public.essay_genres FOR SELECT
|
||||
USING (active = true);
|
||||
|
||||
-- Políticas para student_essays
|
||||
CREATE POLICY "Alunos podem ver suas próprias redações"
|
||||
ON public.student_essays FOR SELECT
|
||||
USING (student_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Alunos podem criar suas próprias redações"
|
||||
ON public.student_essays FOR INSERT
|
||||
WITH CHECK (student_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Alunos podem atualizar suas próprias redações"
|
||||
ON public.student_essays FOR UPDATE
|
||||
USING (student_id = auth.uid())
|
||||
WITH CHECK (student_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Alunos podem deletar suas próprias redações"
|
||||
ON public.student_essays FOR DELETE
|
||||
USING (student_id = auth.uid());
|
||||
|
||||
-- Políticas para essay_analyses
|
||||
CREATE POLICY "Edge Function pode inserir análises"
|
||||
ON public.essay_analyses FOR INSERT
|
||||
WITH CHECK (
|
||||
-- A função de análise roda com a service_role, então permitimos
|
||||
(auth.jwt() ->> 'role' = 'service_role') OR
|
||||
-- Permitir inserção se o essay_id pertence ao usuário atual
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Alunos podem ver análises de suas próprias redações"
|
||||
ON public.essay_analyses FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Função para verificar se uma redação pertence ao aluno
|
||||
CREATE OR REPLACE FUNCTION public.check_essay_ownership(essay_id UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
);
|
||||
END;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
-- Comentários nas tabelas
|
||||
COMMENT ON TABLE public.essay_types IS 'Tipos de texto (Narrativo, Dissertativo, etc)';
|
||||
COMMENT ON TABLE public.essay_genres IS 'Gêneros textuais relacionados a cada tipo de texto';
|
||||
COMMENT ON TABLE public.student_essays IS 'Redações escritas pelos alunos';
|
||||
COMMENT ON TABLE public.essay_analyses IS 'Análises e feedbacks das redações dos alunos';
|
||||
|
||||
-- Dados iniciais
|
||||
INSERT INTO public.essay_types (slug, title, description, icon) VALUES
|
||||
('narrative', 'Narrativo', 'Textos que contam uma história com personagens, tempo e espaço definidos', '📖'),
|
||||
('dissertation', 'Dissertativo', 'Textos que apresentam uma análise e discussão de um tema', '📝'),
|
||||
('descriptive', 'Descritivo', 'Textos que descrevem detalhadamente um objeto, pessoa, lugar ou situação', '🎨');
|
||||
|
||||
INSERT INTO public.essay_genres (type_id, slug, title, description, icon, requirements) VALUES
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'dissertation'),
|
||||
'opinion-article',
|
||||
'Artigo de Opinião',
|
||||
'Texto que apresenta um ponto de vista sobre um tema atual',
|
||||
'📰',
|
||||
'{"min_words": 300, "max_words": 600, "required_sections": ["introduction", "development", "conclusion"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'narrative'),
|
||||
'short-story',
|
||||
'Conto',
|
||||
'História curta com poucos personagens e um único conflito',
|
||||
'📚',
|
||||
'{"min_words": 200, "max_words": 1000, "required_elements": ["characters", "setting", "conflict", "resolution"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'descriptive'),
|
||||
'character-description',
|
||||
'Descrição de Personagem',
|
||||
'Texto que descreve características físicas e psicológicas de um personagem',
|
||||
'👤',
|
||||
'{"min_words": 150, "max_words": 400, "required_aspects": ["physical", "psychological", "habits"]}'
|
||||
);
|
||||
@ -0,0 +1,37 @@
|
||||
-- Remover políticas
|
||||
DROP POLICY IF EXISTS "Edge Function pode inserir análises" ON public.essay_analyses;
|
||||
DROP POLICY IF EXISTS "Alunos podem ver análises de suas próprias redações" ON public.essay_analyses;
|
||||
DROP POLICY IF EXISTS "Alunos podem deletar suas próprias redações" ON public.student_essays;
|
||||
DROP POLICY IF EXISTS "Alunos podem atualizar suas próprias redações" ON public.student_essays;
|
||||
DROP POLICY IF EXISTS "Alunos podem criar suas próprias redações" ON public.student_essays;
|
||||
DROP POLICY IF EXISTS "Alunos podem ver suas próprias redações" ON public.student_essays;
|
||||
DROP POLICY IF EXISTS "Gêneros textuais visíveis para todos" ON public.essay_genres;
|
||||
DROP POLICY IF EXISTS "Tipos de redação visíveis para todos" ON public.essay_types;
|
||||
|
||||
-- Remover função de verificação de propriedade
|
||||
DROP FUNCTION IF EXISTS public.check_essay_ownership(UUID);
|
||||
|
||||
-- Remover triggers
|
||||
DROP TRIGGER IF EXISTS student_essays_updated_at ON public.student_essays;
|
||||
DROP TRIGGER IF EXISTS essay_genres_updated_at ON public.essay_genres;
|
||||
DROP TRIGGER IF EXISTS essay_types_updated_at ON public.essay_types;
|
||||
|
||||
-- Remover índices
|
||||
DROP INDEX IF EXISTS public.idx_essay_analysis_scores_analysis_id;
|
||||
DROP INDEX IF EXISTS public.idx_essay_analysis_improvements_analysis_id;
|
||||
DROP INDEX IF EXISTS public.idx_essay_analysis_strengths_analysis_id;
|
||||
DROP INDEX IF EXISTS public.idx_essay_analysis_feedback_analysis_id;
|
||||
DROP INDEX IF EXISTS public.idx_essay_analyses_essay_id;
|
||||
DROP INDEX IF EXISTS public.idx_essay_genres_type_id;
|
||||
DROP INDEX IF EXISTS public.idx_student_essays_status;
|
||||
DROP INDEX IF EXISTS public.idx_student_essays_student_id;
|
||||
|
||||
-- Remover tabelas
|
||||
DROP TABLE IF EXISTS public.essay_analysis_scores;
|
||||
DROP TABLE IF EXISTS public.essay_analysis_improvements;
|
||||
DROP TABLE IF EXISTS public.essay_analysis_strengths;
|
||||
DROP TABLE IF EXISTS public.essay_analysis_feedback;
|
||||
DROP TABLE IF EXISTS public.essay_analyses;
|
||||
DROP TABLE IF EXISTS public.student_essays;
|
||||
DROP TABLE IF EXISTS public.essay_genres;
|
||||
DROP TABLE IF EXISTS public.essay_types;
|
||||
141
supabase/migrations/20240326000002_insert_essay_data.sql
Normal file
141
supabase/migrations/20240326000002_insert_essay_data.sql
Normal file
@ -0,0 +1,141 @@
|
||||
-- Inserir tipos textuais
|
||||
INSERT INTO public.essay_types (slug, title, description, icon) VALUES
|
||||
('narrative', 'Narrativo', 'Textos que narram acontecimentos reais ou fictícios, com personagens, tempo e espaço definidos', '📖'),
|
||||
('descriptive', 'Descrição', 'Textos que descrevem detalhadamente características de algo ou alguém', '🎨'),
|
||||
('expository', 'Expositivo', 'Textos que explicam e informam sobre um determinado assunto', '📚'),
|
||||
('argumentative', 'Argumentativo', 'Textos que defendem uma ideia ou ponto de vista com argumentos', '⚖️'),
|
||||
('injunctive', 'Injuntivo', 'Textos que orientam ou instruem sobre como realizar algo', '📝');
|
||||
|
||||
-- Inserir gêneros textuais para tipo Narrativo
|
||||
INSERT INTO public.essay_genres (type_id, slug, title, description, icon, requirements) VALUES
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'narrative'),
|
||||
'short-story',
|
||||
'Conto',
|
||||
'História curta com poucos personagens e um único conflito',
|
||||
'📚',
|
||||
'{"min_words": 200, "max_words": 1000, "required_elements": ["personagens", "tempo", "espaço", "conflito", "resolução"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'narrative'),
|
||||
'chronicle',
|
||||
'Crônica',
|
||||
'Narrativa curta que retrata situações do cotidiano',
|
||||
'📰',
|
||||
'{"min_words": 150, "max_words": 800, "required_elements": ["situação_cotidiana", "reflexão", "linguagem_informal"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'narrative'),
|
||||
'novel',
|
||||
'Romance',
|
||||
'História longa com desenvolvimento aprofundado de personagens e tramas',
|
||||
'📖',
|
||||
'{"min_words": 1000, "max_words": 3000, "required_elements": ["personagens_principais", "personagens_secundários", "múltiplos_conflitos", "desenvolvimento_completo"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'narrative'),
|
||||
'news',
|
||||
'Notícia',
|
||||
'Relato de fatos reais de forma objetiva',
|
||||
'📰',
|
||||
'{"min_words": 150, "max_words": 500, "required_elements": ["lead", "corpo_da_notícia", "objetividade"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'narrative'),
|
||||
'biography',
|
||||
'Biografia/Autobiografia',
|
||||
'Relato da vida de uma pessoa',
|
||||
'👤',
|
||||
'{"min_words": 300, "max_words": 1000, "required_elements": ["dados_pessoais", "acontecimentos_importantes", "ordem_cronológica"]}'
|
||||
);
|
||||
|
||||
-- Inserir gêneros textuais para tipo Descritivo
|
||||
INSERT INTO public.essay_genres (type_id, slug, title, description, icon, requirements) VALUES
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'descriptive'),
|
||||
'menu',
|
||||
'Cardápio',
|
||||
'Descrição detalhada de pratos e bebidas',
|
||||
'🍽️',
|
||||
'{"min_words": 50, "max_words": 200, "required_elements": ["nome_do_prato", "ingredientes", "preço"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'descriptive'),
|
||||
'descriptive-report',
|
||||
'Relato descritivo',
|
||||
'Descrição detalhada de um objeto, pessoa ou ambiente',
|
||||
'🔍',
|
||||
'{"min_words": 200, "max_words": 600, "required_elements": ["características_físicas", "sensações", "detalhes_específicos"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'descriptive'),
|
||||
'reportage',
|
||||
'Reportagem',
|
||||
'Descrição aprofundada de um fato ou tema',
|
||||
'📰',
|
||||
'{"min_words": 400, "max_words": 1000, "required_elements": ["contextualização", "detalhamento", "fontes"]}'
|
||||
);
|
||||
|
||||
-- Inserir gêneros textuais para tipo Expositivo
|
||||
INSERT INTO public.essay_genres (type_id, slug, title, description, icon, requirements) VALUES
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'expository'),
|
||||
'didactic-text',
|
||||
'Texto didático',
|
||||
'Explicação clara de um conteúdo para fins educacionais',
|
||||
'📚',
|
||||
'{"min_words": 200, "max_words": 800, "required_elements": ["definição", "exemplos", "explicação"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'expository'),
|
||||
'lecture',
|
||||
'Palestra',
|
||||
'Apresentação expositiva sobre um tema específico',
|
||||
'🎤',
|
||||
'{"min_words": 500, "max_words": 1500, "required_elements": ["introdução", "desenvolvimento", "conclusão", "exemplos_práticos"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'expository'),
|
||||
'reportage-exp',
|
||||
'Reportagem',
|
||||
'Texto informativo sobre um tema ou acontecimento',
|
||||
'📰',
|
||||
'{"min_words": 400, "max_words": 1000, "required_elements": ["contextualização", "dados", "fontes"]}'
|
||||
);
|
||||
|
||||
-- Inserir gêneros textuais para tipo Argumentativo
|
||||
INSERT INTO public.essay_genres (type_id, slug, title, description, icon, requirements) VALUES
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'argumentative'),
|
||||
'open-letter',
|
||||
'Carta aberta',
|
||||
'Texto que expõe publicamente argumentos sobre uma questão',
|
||||
'✉️',
|
||||
'{"min_words": 300, "max_words": 800, "required_elements": ["destinatário", "argumentos", "pedido_ou_reivindicação"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'argumentative'),
|
||||
'thesis',
|
||||
'Tese',
|
||||
'Texto que defende uma ideia central com argumentos',
|
||||
'📑',
|
||||
'{"min_words": 500, "max_words": 1500, "required_elements": ["hipótese", "argumentos", "comprovação"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'argumentative'),
|
||||
'scientific-article',
|
||||
'Artigo científico',
|
||||
'Texto que apresenta resultados de uma pesquisa',
|
||||
'🔬',
|
||||
'{"min_words": 1000, "max_words": 3000, "required_elements": ["introdução", "metodologia", "resultados", "conclusão"]}'
|
||||
);
|
||||
|
||||
-- Inserir gêneros textuais para tipo Injuntivo
|
||||
INSERT INTO public.essay_genres (type_id, slug, title, description, icon, requirements) VALUES
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'injunctive'),
|
||||
'instruction-manual',
|
||||
'Manual de instrução',
|
||||
'Texto que orienta sobre o uso de um produto',
|
||||
'📖',
|
||||
'{"min_words": 100, "max_words": 500, "required_elements": ["passo_a_passo", "advertências", "ilustrações"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'injunctive'),
|
||||
'advertisement',
|
||||
'Propaganda',
|
||||
'Texto que persuade o leitor a uma ação',
|
||||
'📢',
|
||||
'{"min_words": 50, "max_words": 200, "required_elements": ["slogan", "argumentos_persuasivos", "chamada_para_ação"]}'
|
||||
),
|
||||
((SELECT id FROM public.essay_types WHERE slug = 'injunctive'),
|
||||
'recipe',
|
||||
'Receita',
|
||||
'Texto que instrui o preparo de um prato',
|
||||
'👩🍳',
|
||||
'{"min_words": 100, "max_words": 400, "required_elements": ["ingredientes", "modo_de_preparo", "tempo_de_preparo", "rendimento"]}'
|
||||
);
|
||||
@ -0,0 +1,94 @@
|
||||
-- Corrige as políticas de segurança para análises de redação
|
||||
DROP POLICY IF EXISTS "Edge Function pode inserir análises" ON public.essay_analyses;
|
||||
DROP POLICY IF EXISTS "Alunos podem ver análises de suas próprias redações" ON public.essay_analyses;
|
||||
|
||||
-- Política para permitir que a service_role insira análises
|
||||
CREATE POLICY "Service role pode inserir análises"
|
||||
ON public.essay_analyses FOR INSERT
|
||||
WITH CHECK (auth.jwt() ->> 'role' = 'service_role');
|
||||
|
||||
-- Política para permitir que alunos vejam suas análises
|
||||
CREATE POLICY "Alunos podem ver suas análises"
|
||||
ON public.essay_analyses FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Habilita RLS para tabelas relacionadas
|
||||
ALTER TABLE public.essay_analysis_feedback ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_strengths ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_improvements ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_scores ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Políticas para feedback
|
||||
CREATE POLICY "Service role pode inserir feedback"
|
||||
ON public.essay_analysis_feedback FOR INSERT
|
||||
WITH CHECK (auth.jwt() ->> 'role' = 'service_role');
|
||||
|
||||
CREATE POLICY "Alunos podem ver feedback de suas análises"
|
||||
ON public.essay_analysis_feedback FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses a
|
||||
JOIN public.student_essays e ON a.essay_id = e.id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Políticas para pontos fortes
|
||||
CREATE POLICY "Service role pode inserir pontos fortes"
|
||||
ON public.essay_analysis_strengths FOR INSERT
|
||||
WITH CHECK (auth.jwt() ->> 'role' = 'service_role');
|
||||
|
||||
CREATE POLICY "Alunos podem ver pontos fortes de suas análises"
|
||||
ON public.essay_analysis_strengths FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses a
|
||||
JOIN public.student_essays e ON a.essay_id = e.id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Políticas para pontos a melhorar
|
||||
CREATE POLICY "Service role pode inserir pontos a melhorar"
|
||||
ON public.essay_analysis_improvements FOR INSERT
|
||||
WITH CHECK (auth.jwt() ->> 'role' = 'service_role');
|
||||
|
||||
CREATE POLICY "Alunos podem ver pontos a melhorar de suas análises"
|
||||
ON public.essay_analysis_improvements FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses a
|
||||
JOIN public.student_essays e ON a.essay_id = e.id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Políticas para notas por critério
|
||||
CREATE POLICY "Service role pode inserir notas"
|
||||
ON public.essay_analysis_scores FOR INSERT
|
||||
WITH CHECK (auth.jwt() ->> 'role' = 'service_role');
|
||||
|
||||
CREATE POLICY "Alunos podem ver notas de suas análises"
|
||||
ON public.essay_analysis_scores FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses a
|
||||
JOIN public.student_essays e ON a.essay_id = e.id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
165
supabase/migrations/20240327000001_normalize_essay_analyses.sql
Normal file
165
supabase/migrations/20240327000001_normalize_essay_analyses.sql
Normal file
@ -0,0 +1,165 @@
|
||||
-- Primeiro, vamos criar as novas tabelas normalizadas
|
||||
CREATE TABLE public.essay_analysis_feedback (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL,
|
||||
structure_feedback TEXT NOT NULL,
|
||||
content_feedback TEXT NOT NULL,
|
||||
language_feedback TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.essay_analysis_strengths (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL,
|
||||
strength TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.essay_analysis_improvements (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL,
|
||||
improvement TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.essay_analysis_scores (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL,
|
||||
adequacy INTEGER NOT NULL CHECK (adequacy >= 0 AND adequacy <= 100),
|
||||
coherence INTEGER NOT NULL CHECK (coherence >= 0 AND coherence <= 100),
|
||||
cohesion INTEGER NOT NULL CHECK (cohesion >= 0 AND cohesion <= 100),
|
||||
vocabulary INTEGER NOT NULL CHECK (vocabulary >= 0 AND vocabulary <= 100),
|
||||
grammar INTEGER NOT NULL CHECK (grammar >= 0 AND grammar <= 100),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Criar nova versão da tabela essay_analyses sem os campos JSONB e arrays
|
||||
CREATE TABLE public.essay_analyses_new (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
essay_id UUID NOT NULL REFERENCES public.student_essays(id),
|
||||
overall_score INTEGER NOT NULL CHECK (overall_score >= 0 AND overall_score <= 100),
|
||||
suggestions TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Migrar dados da tabela antiga para as novas tabelas
|
||||
INSERT INTO public.essay_analyses_new (id, essay_id, overall_score, suggestions, created_at)
|
||||
SELECT id, essay_id, overall_score, suggestions, created_at
|
||||
FROM public.essay_analyses;
|
||||
|
||||
-- Adicionar chaves estrangeiras nas tabelas de normalização
|
||||
ALTER TABLE public.essay_analysis_feedback
|
||||
ADD CONSTRAINT fk_analysis_feedback
|
||||
FOREIGN KEY (analysis_id)
|
||||
REFERENCES public.essay_analyses_new(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.essay_analysis_strengths
|
||||
ADD CONSTRAINT fk_analysis_strengths
|
||||
FOREIGN KEY (analysis_id)
|
||||
REFERENCES public.essay_analyses_new(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.essay_analysis_improvements
|
||||
ADD CONSTRAINT fk_analysis_improvements
|
||||
FOREIGN KEY (analysis_id)
|
||||
REFERENCES public.essay_analyses_new(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.essay_analysis_scores
|
||||
ADD CONSTRAINT fk_analysis_scores
|
||||
FOREIGN KEY (analysis_id)
|
||||
REFERENCES public.essay_analyses_new(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Criar índices para melhor performance
|
||||
CREATE INDEX idx_essay_analysis_feedback_analysis_id ON public.essay_analysis_feedback(analysis_id);
|
||||
CREATE INDEX idx_essay_analysis_strengths_analysis_id ON public.essay_analysis_strengths(analysis_id);
|
||||
CREATE INDEX idx_essay_analysis_improvements_analysis_id ON public.essay_analysis_improvements(analysis_id);
|
||||
CREATE INDEX idx_essay_analysis_scores_analysis_id ON public.essay_analysis_scores(analysis_id);
|
||||
|
||||
-- Aplicar políticas RLS nas novas tabelas
|
||||
ALTER TABLE public.essay_analyses_new ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_feedback ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_strengths ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_improvements ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.essay_analysis_scores ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Políticas para essay_analyses_new
|
||||
CREATE POLICY "Edge Function pode inserir análises"
|
||||
ON public.essay_analyses_new FOR INSERT
|
||||
WITH CHECK (
|
||||
(auth.jwt() ->> 'role' = 'service_role') OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Alunos podem ver análises de suas próprias redações"
|
||||
ON public.essay_analyses_new FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Políticas para tabelas relacionadas
|
||||
CREATE POLICY "Acesso vinculado à análise principal - feedback"
|
||||
ON public.essay_analysis_feedback FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses_new a
|
||||
JOIN public.student_essays e ON e.id = a.essay_id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Acesso vinculado à análise principal - strengths"
|
||||
ON public.essay_analysis_strengths FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses_new a
|
||||
JOIN public.student_essays e ON e.id = a.essay_id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Acesso vinculado à análise principal - improvements"
|
||||
ON public.essay_analysis_improvements FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses_new a
|
||||
JOIN public.student_essays e ON e.id = a.essay_id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Acesso vinculado à análise principal - scores"
|
||||
ON public.essay_analysis_scores FOR ALL
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.essay_analyses_new a
|
||||
JOIN public.student_essays e ON e.id = a.essay_id
|
||||
WHERE a.id = analysis_id
|
||||
AND e.student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Dropar a tabela antiga
|
||||
DROP TABLE public.essay_analyses;
|
||||
|
||||
-- Renomear a nova tabela
|
||||
ALTER TABLE public.essay_analyses_new RENAME TO essay_analyses;
|
||||
@ -0,0 +1,89 @@
|
||||
-- Recriar a tabela original com os campos JSONB e arrays
|
||||
CREATE TABLE public.essay_analyses_old (
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
essay_id UUID NOT NULL REFERENCES public.student_essays(id),
|
||||
overall_score INTEGER NOT NULL CHECK (overall_score >= 0 AND overall_score <= 100),
|
||||
feedback JSONB NOT NULL DEFAULT '{}',
|
||||
strengths TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
improvements TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
suggestions TEXT,
|
||||
criteria_scores JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||
);
|
||||
|
||||
-- Migrar dados das tabelas normalizadas para a tabela original
|
||||
INSERT INTO public.essay_analyses_old (
|
||||
id,
|
||||
essay_id,
|
||||
overall_score,
|
||||
feedback,
|
||||
strengths,
|
||||
improvements,
|
||||
suggestions,
|
||||
criteria_scores,
|
||||
created_at
|
||||
)
|
||||
SELECT
|
||||
a.id,
|
||||
a.essay_id,
|
||||
a.overall_score,
|
||||
jsonb_build_object(
|
||||
'structure', f.structure_feedback,
|
||||
'content', f.content_feedback,
|
||||
'language', f.language_feedback
|
||||
) as feedback,
|
||||
array_agg(DISTINCT s.strength) as strengths,
|
||||
array_agg(DISTINCT i.improvement) as improvements,
|
||||
a.suggestions,
|
||||
jsonb_build_object(
|
||||
'adequacy', sc.adequacy,
|
||||
'coherence', sc.coherence,
|
||||
'cohesion', sc.cohesion,
|
||||
'vocabulary', sc.vocabulary,
|
||||
'grammar', sc.grammar
|
||||
) as criteria_scores,
|
||||
a.created_at
|
||||
FROM public.essay_analyses a
|
||||
LEFT JOIN public.essay_analysis_feedback f ON f.analysis_id = a.id
|
||||
LEFT JOIN public.essay_analysis_strengths s ON s.analysis_id = a.id
|
||||
LEFT JOIN public.essay_analysis_improvements i ON i.analysis_id = a.id
|
||||
LEFT JOIN public.essay_analysis_scores sc ON sc.analysis_id = a.id
|
||||
GROUP BY a.id, a.essay_id, a.overall_score, a.suggestions, a.created_at,
|
||||
f.structure_feedback, f.content_feedback, f.language_feedback,
|
||||
sc.adequacy, sc.coherence, sc.cohesion, sc.vocabulary, sc.grammar;
|
||||
|
||||
-- Dropar as tabelas normalizadas
|
||||
DROP TABLE IF EXISTS public.essay_analysis_scores;
|
||||
DROP TABLE IF EXISTS public.essay_analysis_improvements;
|
||||
DROP TABLE IF EXISTS public.essay_analysis_strengths;
|
||||
DROP TABLE IF EXISTS public.essay_analysis_feedback;
|
||||
DROP TABLE IF EXISTS public.essay_analyses;
|
||||
|
||||
-- Renomear a tabela antiga
|
||||
ALTER TABLE public.essay_analyses_old RENAME TO essay_analyses;
|
||||
|
||||
-- Recriar as políticas originais
|
||||
ALTER TABLE public.essay_analyses ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Edge Function pode inserir análises"
|
||||
ON public.essay_analyses FOR INSERT
|
||||
WITH CHECK (
|
||||
(auth.jwt() ->> 'role' = 'service_role') OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Alunos podem ver análises de suas próprias redações"
|
||||
ON public.essay_analyses FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM public.student_essays
|
||||
WHERE id = essay_id
|
||||
AND student_id = auth.uid()
|
||||
)
|
||||
);
|
||||
@ -1,12 +1,55 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
purple: {
|
||||
50: '#f5f3ff',
|
||||
100: '#ede9fe',
|
||||
@ -32,56 +75,243 @@ export default {
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'in': 'in 200ms ease-in',
|
||||
'out': 'out 200ms ease-out',
|
||||
'slide-in-from-top': 'slide-in-from-top 200ms ease-out',
|
||||
'slide-in-from-bottom': 'slide-in-from-bottom 200ms ease-out',
|
||||
'slide-out-to-right': 'slide-out-to-right 200ms ease-out',
|
||||
'fade-in': 'fade-in 200ms ease-in',
|
||||
'fade-out': 'fade-out 200ms ease-out',
|
||||
'scale-in': 'scale-in 200ms ease-out',
|
||||
'scale-out': 'scale-out 200ms ease-in',
|
||||
borderRadius: {
|
||||
none: '0',
|
||||
sm: '0.125rem',
|
||||
DEFAULT: '0.25rem',
|
||||
md: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
'2xl': '1rem',
|
||||
'3xl': '1.5rem',
|
||||
full: '9999px',
|
||||
},
|
||||
keyframes: {
|
||||
in: {
|
||||
'0%': { transform: 'translateX(100%)' },
|
||||
'100%': { transform: 'translateX(0)' },
|
||||
"fade-in": {
|
||||
"0%": { opacity: 0 },
|
||||
"100%": { opacity: 1 }
|
||||
},
|
||||
out: {
|
||||
'0%': { transform: 'translateX(0)' },
|
||||
'100%': { transform: 'translateX(100%)' },
|
||||
},
|
||||
'slide-in-from-top': {
|
||||
'0%': { transform: 'translateY(-100%)' },
|
||||
'100%': { transform: 'translateY(0)' },
|
||||
},
|
||||
'slide-in-from-bottom': {
|
||||
'0%': { transform: 'translateY(100%)' },
|
||||
'100%': { transform: 'translateY(0)' },
|
||||
},
|
||||
'slide-out-to-right': {
|
||||
'0%': { transform: 'translateX(0)' },
|
||||
'100%': { transform: 'translateX(100%)' },
|
||||
},
|
||||
'fade-in': {
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
},
|
||||
'fade-out': {
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
},
|
||||
'scale-in': {
|
||||
'0%': { transform: 'scale(0.95)', opacity: 0 },
|
||||
'100%': { transform: 'scale(1)', opacity: 1 },
|
||||
},
|
||||
'scale-out': {
|
||||
'0%': { transform: 'scale(1)', opacity: 1 },
|
||||
'100%': { transform: 'scale(0.95)', opacity: 0 },
|
||||
"fade-out": {
|
||||
"0%": { opacity: 1 },
|
||||
"100%": { opacity: 0 }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
"fade-in": "fade-in 200ms ease-in",
|
||||
"fade-out": "fade-out 200ms ease-out"
|
||||
},
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
maxWidth: '100%',
|
||||
color: 'var(--tw-prose-body)',
|
||||
'[class~="lead"]': {
|
||||
color: 'var(--tw-prose-lead)',
|
||||
},
|
||||
a: {
|
||||
color: 'var(--tw-prose-links)',
|
||||
textDecoration: 'underline',
|
||||
fontWeight: '500',
|
||||
},
|
||||
strong: {
|
||||
color: 'var(--tw-prose-bold)',
|
||||
fontWeight: '600',
|
||||
},
|
||||
'ol[type="A"]': {
|
||||
'--list-counter-style': 'upper-alpha',
|
||||
},
|
||||
'ol[type="a"]': {
|
||||
'--list-counter-style': 'lower-alpha',
|
||||
},
|
||||
'ol[type="A" s]': {
|
||||
'--list-counter-style': 'upper-alpha',
|
||||
},
|
||||
'ol[type="a" s]': {
|
||||
'--list-counter-style': 'lower-alpha',
|
||||
},
|
||||
'ol[type="I"]': {
|
||||
'--list-counter-style': 'upper-roman',
|
||||
},
|
||||
'ol[type="i"]': {
|
||||
'--list-counter-style': 'lower-roman',
|
||||
},
|
||||
'ol[type="I" s]': {
|
||||
'--list-counter-style': 'upper-roman',
|
||||
},
|
||||
'ol[type="i" s]': {
|
||||
'--list-counter-style': 'lower-roman',
|
||||
},
|
||||
'ol[type="1"]': {
|
||||
'--list-counter-style': 'decimal',
|
||||
},
|
||||
'ol > li': {
|
||||
position: 'relative',
|
||||
},
|
||||
'ol > li::before': {
|
||||
content: 'counter(list-item, var(--list-counter-style, decimal)) "."',
|
||||
position: 'absolute',
|
||||
fontWeight: '400',
|
||||
color: 'var(--tw-prose-counters)',
|
||||
},
|
||||
'ul > li': {
|
||||
position: 'relative',
|
||||
},
|
||||
'ul > li::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
backgroundColor: 'var(--tw-prose-bullets)',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
hr: {
|
||||
borderColor: 'var(--tw-prose-hr)',
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
blockquote: {
|
||||
fontWeight: '500',
|
||||
fontStyle: 'italic',
|
||||
color: 'var(--tw-prose-quotes)',
|
||||
borderLeftWidth: '0.25rem',
|
||||
borderLeftColor: 'var(--tw-prose-quote-borders)',
|
||||
quotes: '"\\201C""\\201D""\\2018""\\2019"',
|
||||
},
|
||||
'blockquote p:first-of-type::before': {
|
||||
content: 'open-quote',
|
||||
},
|
||||
'blockquote p:last-of-type::after': {
|
||||
content: 'close-quote',
|
||||
},
|
||||
h1: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '800',
|
||||
},
|
||||
'h1 strong': {
|
||||
fontWeight: '900',
|
||||
color: 'inherit',
|
||||
},
|
||||
h2: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '700',
|
||||
},
|
||||
'h2 strong': {
|
||||
fontWeight: '800',
|
||||
color: 'inherit',
|
||||
},
|
||||
h3: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '600',
|
||||
},
|
||||
'h3 strong': {
|
||||
fontWeight: '700',
|
||||
color: 'inherit',
|
||||
},
|
||||
h4: {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '600',
|
||||
},
|
||||
'h4 strong': {
|
||||
fontWeight: '700',
|
||||
color: 'inherit',
|
||||
},
|
||||
img: {
|
||||
marginTop: '2em',
|
||||
marginBottom: '2em',
|
||||
},
|
||||
'figure > *': {
|
||||
marginTop: '0',
|
||||
marginBottom: '0',
|
||||
},
|
||||
figcaption: {
|
||||
color: 'var(--tw-prose-captions)',
|
||||
fontSize: '0.875em',
|
||||
lineHeight: '1.4285714',
|
||||
marginTop: '0.8571429em',
|
||||
},
|
||||
code: {
|
||||
color: 'var(--tw-prose-code)',
|
||||
fontWeight: '600',
|
||||
},
|
||||
'code::before': {
|
||||
content: '"`"',
|
||||
},
|
||||
'code::after': {
|
||||
content: '"`"',
|
||||
},
|
||||
'a code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'h1 code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'h2 code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'h3 code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'h4 code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'blockquote code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'thead th code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
pre: {
|
||||
color: 'var(--tw-prose-pre-code)',
|
||||
backgroundColor: 'var(--tw-prose-pre-bg)',
|
||||
overflowX: 'auto',
|
||||
fontWeight: '400',
|
||||
},
|
||||
'pre code': {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: '0',
|
||||
borderRadius: '0',
|
||||
padding: '0',
|
||||
fontWeight: 'inherit',
|
||||
color: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
},
|
||||
'pre code::before': {
|
||||
content: 'none',
|
||||
},
|
||||
'pre code::after': {
|
||||
content: 'none',
|
||||
},
|
||||
table: {
|
||||
width: '100%',
|
||||
tableLayout: 'auto',
|
||||
textAlign: 'left',
|
||||
marginTop: '2em',
|
||||
marginBottom: '2em',
|
||||
},
|
||||
thead: {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'thead th': {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: '600',
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
'tbody tr': {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-td-borders)',
|
||||
},
|
||||
'tbody tr:last-child': {
|
||||
borderBottomWidth: '0',
|
||||
},
|
||||
'tbody td': {
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user