Compare commits

..

No commits in common. "bb85c83c5b630305e57bbd1ebfe51188a9b71f70" and "478ca2441d8c9f4dadbc3c88f066d56c3277de02" have entirely different histories.

35 changed files with 512 additions and 5823 deletions

View File

@ -250,77 +250,3 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
- Configuração centralizada de métricas - Configuração centralizada de métricas
- Suporte a tooltips e ícones personalizados - Suporte a tooltips e ícones personalizados
- Responsividade e acessibilidade melhoradas - 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

File diff suppressed because it is too large Load Diff

View File

@ -23,27 +23,14 @@
"@opentelemetry/sdk-metrics": "^1.30.1", "@opentelemetry/sdk-metrics": "^1.30.1",
"@opentelemetry/sdk-trace-web": "^1.30.1", "@opentelemetry/sdk-trace-web": "^1.30.1",
"@radix-ui/react-accordion": "^1.2.2", "@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-dialog": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-toast": "^1.2.4",
"@sentry/react": "^8.48.0", "@sentry/react": "^8.48.0",
"@supabase/supabase-js": "^2.39.7", "@supabase/supabase-js": "^2.39.7",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.62.8", "@tanstack/react-query": "^5.62.8",
"@testing-library/react": "^16.1.0", "@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", "@tremor/react": "^3.18.7",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
@ -61,14 +48,11 @@
"resend": "^3.2.0", "resend": "^3.2.0",
"shadcn-ui": "^0.9.4", "shadcn-ui": "^0.9.4",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"vitest": "^2.1.8", "vitest": "^2.1.8"
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.9.1", "@eslint/js": "^9.9.1",
"@shadcn/ui": "^0.0.4",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@types/react": "^18.3.17", "@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",

View File

@ -1,7 +1,18 @@
import React from 'react'; import React from 'react';
import { Calendar, HelpCircle } from 'lucide-react'; import { Calendar, HelpCircle } from 'lucide-react';
import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts'; import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts';
import type { WeeklyReadingMetrics } from '@/types/metrics';
interface WeeklyMetrics {
week: string;
fluency: number;
pronunciation: number;
accuracy: number;
comprehension: number;
wordsPerMinute: number;
pauses: number;
errors: number;
minutesRead: number;
}
interface MetricConfig { interface MetricConfig {
key: string; key: string;
@ -33,11 +44,11 @@ const TIME_FILTERS: TimeFilterOption[] = [
]; ];
interface MetricsChartProps { interface MetricsChartProps {
data: WeeklyReadingMetrics[]; data: WeeklyMetrics[];
className?: string; className?: string;
} }
export function MetricsChart({ data = [], className = '' }: MetricsChartProps) { export function MetricsChart({ data, className = '' }: MetricsChartProps) {
const [visibleMetrics, setVisibleMetrics] = React.useState<Set<string>>( const [visibleMetrics, setVisibleMetrics] = React.useState<Set<string>>(
new Set(METRICS_CONFIG.map(metric => metric.key)) new Set(METRICS_CONFIG.map(metric => metric.key))
); );
@ -55,9 +66,7 @@ export function MetricsChart({ data = [], className = '' }: MetricsChartProps) {
}); });
}; };
const filterDataByTime = (data: WeeklyReadingMetrics[]): WeeklyReadingMetrics[] => { const filterDataByTime = (data: WeeklyMetrics[]): WeeklyMetrics[] => {
if (!data || !Array.isArray(data)) return [];
if (timeFilter === 'all') return data; if (timeFilter === 'all') return data;
const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12; const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12;
@ -65,23 +74,21 @@ export function MetricsChart({ data = [], className = '' }: MetricsChartProps) {
cutoffDate.setMonth(cutoffDate.getMonth() - months); cutoffDate.setMonth(cutoffDate.getMonth() - months);
return data.filter(item => { return data.filter(item => {
if (!item?.week) return false;
const [year, week] = item.week.split('-W').map(Number); const [year, week] = item.week.split('-W').map(Number);
if (!year || !week) return false;
const itemDate = new Date(year, 0, 1 + (week - 1) * 7); const itemDate = new Date(year, 0, 1 + (week - 1) * 7);
return itemDate >= cutoffDate; return itemDate >= cutoffDate;
}); });
}; };
const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]); const filteredData = filterDataByTime(data);
return ( return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-8 ${className}`}> <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 flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-xl font-semibold text-gray-900">Evolução da Leitura por Semana</h2> <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 na leitura ao longo do tempo</p> <p className="text-sm text-gray-500">Acompanhe seu progresso ao longo do tempo</p>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Filtro de Período */} {/* Filtro de Período */}
@ -184,7 +191,9 @@ export function MetricsChart({ data = [], className = '' }: MetricsChartProps) {
accuracy: 'Precisão', accuracy: 'Precisão',
comprehension: 'Compreensão', comprehension: 'Compreensão',
wordsPerMinute: 'Palavras/Min', wordsPerMinute: 'Palavras/Min',
minutesRead: 'Minutos Lendo' pauses: 'Pausas',
errors: 'Erros',
minutesRead: 'Minutos Lidos'
}; };
return [value, metricNames[name] || name]; return [value, metricNames[name] || name];
}} }}
@ -224,7 +233,7 @@ export function MetricsChart({ data = [], className = '' }: MetricsChartProps) {
<Bar <Bar
yAxisId="right" yAxisId="right"
dataKey="minutesRead" dataKey="minutesRead"
name="Minutos Lendo" name="Minutos Lidos"
fill="url(#barGradient)" fill="url(#barGradient)"
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
isAnimationActive={false} isAnimationActive={false}

View File

@ -1,241 +0,0 @@
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>
);
}

View File

@ -1,143 +0,0 @@
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>
);
}

View File

@ -1,146 +0,0 @@
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,
}

View File

@ -7,41 +7,11 @@ import { EVENT_CATEGORIES } from '../../constants/analytics';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
as?: 'button' | 'span'; as?: 'button' | 'span';
trackingId: string; trackingId: string;
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive'; variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
trackingProperties?: ButtonTrackingOptions; 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({ export function Button({
as: Component = 'button', as: Component = 'button',
children, children,
@ -71,10 +41,29 @@ export function Button({
onClick?.(event); 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 ( return (
<Component <Component
type={Component === 'button' ? type : undefined} type={Component === 'button' ? type : undefined}
className={buttonVariants({ variant, size, className })} className={baseStyles}
onClick={handleClick} onClick={handleClick}
disabled={disabled} disabled={disabled}
{...props} {...props}

View File

@ -1,251 +0,0 @@
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>
)
}

View File

@ -12,13 +12,13 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-gray-200", "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className className
)} )}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-purple-600 transition-all" className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>

View File

@ -1,24 +0,0 @@
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 }

View File

@ -8,7 +8,13 @@ if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Variáveis de ambiente do Supabase não configuradas') throw new Error('Variáveis de ambiente do Supabase não configuradas')
} }
export const supabase = createClient(supabaseUrl, supabaseAnonKey) export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
})
export const generateStoryFunction = async (prompt: StoryPrompt) => { export const generateStoryFunction = async (prompt: StoryPrompt) => {
const { data: { session } } = await supabase.auth.getSession() const { data: { session } } = await supabase.auth.getSession()

View File

@ -12,7 +12,6 @@ import {
X, X,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
PenTool
} from 'lucide-react'; } from 'lucide-react';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import * as Dialog from '@radix-ui/react-dialog'; import * as Dialog from '@radix-ui/react-dialog';
@ -70,21 +69,6 @@ export function StudentDashboardLayout() {
{!isCollapsed && <span>Painel</span>} {!isCollapsed && <span>Painel</span>}
</NavLink> </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 {/* <NavLink
to="/aluno/conquistas" to="/aluno/conquistas"
onClick={handleNavigation} onClick={handleNavigation}

View File

@ -5,15 +5,19 @@ import { supabase } from '../../lib/supabase';
import type { Story, Student } from '../../types/database'; import type { Story, Student } from '../../types/database';
import { MetricsChart } from '@/components/dashboard/MetricsChart'; import { MetricsChart } from '@/components/dashboard/MetricsChart';
import { DashboardMetrics } from '@/components/dashboard/DashboardMetrics'; import { DashboardMetrics } from '@/components/dashboard/DashboardMetrics';
import { WritingMetricsSection } from '@/components/dashboard/WritingMetricsSection';
import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart'; interface DashboardMetrics {
import { useMetricsStore } from '@/stores/metricsStore'; totalStories: number;
import type { averageReadingFluency: number;
DashboardMetrics as DashboardMetricsType, totalReadingTime: number;
DashboardWeeklyMetrics, currentLevel: number;
WeeklyReadingMetrics, averagePronunciation: number;
WeeklyWritingMetrics averageAccuracy: number;
} from '@/types/metrics'; averageComprehension: number;
averageWordsPerMinute: number;
averagePauses: number;
averageErrors: number;
}
interface WeeklyMetrics { interface WeeklyMetrics {
week: string; week: string;
@ -50,65 +54,26 @@ interface WeeklyData {
minutesRead: number; 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() { export function StudentDashboardPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [student, setStudent] = React.useState<Student | null>(null); 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 [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 processWeeklyMetrics = (recordings: Recording[]) => {
const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => { const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => {
const date = new Date(recording.created_at); const date = new Date(recording.created_at);
@ -156,54 +121,9 @@ export function StudentDashboardPage() {
.sort((a, b) => a.week.localeCompare(b.week)); .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(() => { React.useEffect(() => {
const fetchDashboardData = async () => { const fetchDashboardData = async () => {
try { try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession(); const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) return; if (!session?.user?.id) return;
@ -244,8 +164,7 @@ export function StudentDashboardPage() {
// Processar métricas semanais // Processar métricas semanais
const weeklyData = processWeeklyMetrics(recordings); const weeklyData = processWeeklyMetrics(recordings);
// Atualizar métricas semanais de leitura setWeeklyMetrics(weeklyData);
updateWeeklyReadingMetrics(weeklyData);
// Buscar histórias recentes com a capa definida // Buscar histórias recentes com a capa definida
const { data: stories, error: storiesError } = await supabase const { data: stories, error: storiesError } = await supabase
@ -304,8 +223,8 @@ export function StudentDashboardPage() {
errors: 0 errors: 0
}); });
// Atualizar métricas de leitura // Calcular médias
updateReadingMetrics({ setMetrics({
totalStories: allStoriesData.length, totalStories: allStoriesData.length,
averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings), averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings),
totalReadingTime: recordings.length * 2, totalReadingTime: recordings.length * 2,
@ -319,125 +238,6 @@ 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) { } catch (err) {
console.error('Erro ao carregar dashboard:', err); console.error('Erro ao carregar dashboard:', err);
setError('Não foi possível carregar seus dados'); setError('Não foi possível carregar seus dados');
@ -447,11 +247,6 @@ export function StudentDashboardPage() {
}; };
fetchDashboardData(); fetchDashboardData();
// Limpar métricas ao desmontar
return () => {
resetMetrics();
};
}, []); }, []);
if (loading) { if (loading) {
@ -522,27 +317,11 @@ export function StudentDashboardPage() {
</div> </div>
</div> </div>
{/* Seção de Métricas de Leitura */} {/* Métricas */}
<div className="mb-12"> <DashboardMetrics data={metrics} />
<h2 className="text-2xl font-bold text-gray-900 mb-6">Métricas de Leitura</h2>
{/* Métricas de Leitura */} {/* Gráfico de Evolução */}
<DashboardMetrics data={metrics.reading} className="mb-8" /> <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 */} {/* Histórias Recentes */}
<div> <div>

View File

@ -1,310 +0,0 @@
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>
);
}

View File

@ -1,408 +0,0 @@
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>
);
}

View File

@ -1,269 +0,0 @@
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>
);
}

View File

@ -1,286 +0,0 @@
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>
);
}

View File

@ -33,10 +33,6 @@ import { EvidenceBased } from './pages/landing/EvidenceBased';
import { TextSalesLetter } from './pages/landing/TextSalesLetter'; import { TextSalesLetter } from './pages/landing/TextSalesLetter';
import { PhonicsPage } from "./pages/student-dashboard/PhonicsPage"; import { PhonicsPage } from "./pages/student-dashboard/PhonicsPage";
import { PhonicsProgressPage } from "./pages/student-dashboard/PhonicsProgressPage"; 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 }) { function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
@ -223,27 +219,6 @@ export const router = createBrowserRouter([
{ {
path: 'fonicos/progresso', path: 'fonicos/progresso',
element: <PhonicsProgressPage />, element: <PhonicsProgressPage />,
},
{
path: 'redacoes',
children: [
{
index: true,
element: <EssaysPage />,
},
{
path: 'nova',
element: <NewEssay />,
},
{
path: ':id',
element: <EssayPage />,
},
{
path: ':id/analise',
element: <EssayAnalysis />,
}
]
} }
] ]
}, },

View File

@ -1,106 +0,0 @@
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
})
}));

View File

@ -1,57 +0,0 @@
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[];
}

View File

@ -88,8 +88,6 @@
| public | interests | item | UNIQUE | | public | interests | item | UNIQUE |
| public | interests | student_id | FOREIGN KEY | | public | interests | student_id | FOREIGN KEY |
| public | languages | code | UNIQUE | | 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 | schema_migrations | version | PRIMARY KEY |
| supabase_migrations | seed_files | path | PRIMARY KEY | | supabase_migrations | seed_files | path | PRIMARY KEY |
| public | phonics_categories | id | PRIMARY KEY | | public | phonics_categories | id | PRIMARY KEY |
@ -110,19 +108,10 @@
| public | story_characters | slug | UNIQUE | | public | story_characters | slug | UNIQUE |
| public | story_settings | id | PRIMARY KEY | | public | story_settings | id | PRIMARY KEY |
| public | story_settings | slug | UNIQUE | | public | story_settings | slug | UNIQUE |
| public | essay_types | id | PRIMARY KEY |
| public | essay_types | slug | UNIQUE |
| public | stories | theme_id | FOREIGN KEY | | public | stories | theme_id | FOREIGN KEY |
| public | stories | subject_id | FOREIGN KEY | | public | stories | subject_id | FOREIGN KEY |
| public | stories | character_id | FOREIGN KEY | | public | stories | character_id | FOREIGN KEY |
| public | stories | setting_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 | identities | user_id | FOREIGN KEY |
| auth | refresh_tokens | token | UNIQUE | | auth | refresh_tokens | token | UNIQUE |
| auth | sessions | id | PRIMARY KEY | | auth | sessions | id | PRIMARY KEY |
@ -161,20 +150,12 @@
| storage | s3_multipart_uploads_parts | bucket_id | FOREIGN KEY | | storage | s3_multipart_uploads_parts | bucket_id | FOREIGN KEY |
| realtime | schema_migrations | version | PRIMARY KEY | | realtime | schema_migrations | version | PRIMARY KEY |
| realtime | subscription | id | 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 | theme_id | FOREIGN KEY |
| public | stories | subject_id | FOREIGN KEY | | public | stories | subject_id | FOREIGN KEY |
| public | stories | character_id | FOREIGN KEY | | public | stories | character_id | FOREIGN KEY |
| public | stories | setting_id | FOREIGN KEY | | public | stories | setting_id | FOREIGN KEY |
| public | essay_analysis_feedback | id | PRIMARY KEY |
| realtime | messages | id | PRIMARY KEY | | realtime | messages | id | PRIMARY KEY |
| realtime | messages | inserted_at | 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_subjects | null | CHECK |
| public | story_recordings | null | CHECK | | public | story_recordings | null | CHECK |
| public | story_characters | null | CHECK | | public | story_characters | null | CHECK |
@ -193,11 +174,9 @@
| public | students | null | CHECK | | public | students | null | CHECK |
| public | phonics_word_audio | null | CHECK | | public | phonics_word_audio | null | CHECK |
| public | languages | null | CHECK | | public | languages | null | CHECK |
| public | student_essays | null | CHECK |
| pgsodium | key | null | CHECK | | pgsodium | key | null | CHECK |
| public | phonics_word_audio | null | CHECK | | public | phonics_word_audio | null | CHECK |
| realtime | messages | null | CHECK | | realtime | messages | null | CHECK |
| public | essay_analysis_strengths | null | CHECK |
| public | achievements | null | CHECK | | public | achievements | null | CHECK |
| public | phonics_exercise_types | null | CHECK | | public | phonics_exercise_types | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
@ -207,7 +186,6 @@
| auth | refresh_tokens | null | CHECK | | auth | refresh_tokens | null | CHECK |
| pgsodium | key | null | CHECK | | pgsodium | key | null | CHECK |
| public | student_phonics_attempt_answers | null | CHECK | | public | student_phonics_attempt_answers | null | CHECK |
| public | essay_types | null | CHECK |
| auth | identities | null | CHECK | | auth | identities | null | CHECK |
| storage | s3_multipart_uploads | null | CHECK | | storage | s3_multipart_uploads | null | CHECK |
| auth | flow_state | null | CHECK | | auth | flow_state | null | CHECK |
@ -216,7 +194,6 @@
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | story_subjects | null | CHECK | | public | story_subjects | null | CHECK |
| auth | flow_state | null | CHECK | | auth | flow_state | null | CHECK |
| public | essay_analyses | null | CHECK |
| public | phonics_words | null | CHECK | | public | phonics_words | null | CHECK |
| realtime | subscription | null | CHECK | | realtime | subscription | null | CHECK |
| auth | sso_domains | null | CHECK | | auth | sso_domains | null | CHECK |
@ -224,36 +201,25 @@
| auth | users | null | CHECK | | auth | users | null | CHECK |
| public | phonics_categories | null | CHECK | | public | phonics_categories | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
| public | essay_genres | null | CHECK |
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | story_themes | null | CHECK | | public | story_themes | null | CHECK |
| storage | s3_multipart_uploads_parts | null | CHECK | | storage | s3_multipart_uploads_parts | null | CHECK |
| public | student_essays | null | CHECK |
| public | teachers | null | CHECK | | public | teachers | null | CHECK |
| public | teachers | null | CHECK | | public | teachers | null | CHECK |
| public | story_themes | null | CHECK | | public | story_themes | null | CHECK |
| public | essay_genres | null | CHECK |
| public | phonics_exercise_words | null | CHECK | | public | phonics_exercise_words | null | CHECK |
| public | students | null | CHECK | | public | students | null | CHECK |
| realtime | subscription | null | CHECK | | realtime | subscription | null | CHECK |
| auth | mfa_amr_claims | null | CHECK | | auth | mfa_amr_claims | null | CHECK |
| public | teacher_classes | null | CHECK | | public | teacher_classes | null | CHECK |
| public | essay_genres | null | CHECK |
| auth | flow_state | 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 | students | null | CHECK |
| public | story_characters | null | CHECK | | public | story_characters | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| auth | mfa_factors | null | CHECK | | auth | mfa_factors | null | CHECK |
| public | student_essays | null | CHECK |
| realtime | schema_migrations | null | CHECK | | realtime | schema_migrations | null | CHECK |
| public | languages | null | CHECK | | public | languages | null | CHECK |
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| public | phonics_word_audio | null | CHECK | | public | phonics_word_audio | null | CHECK |
| public | essay_analysis_feedback | null | CHECK |
| pgsodium | key | null | CHECK | | pgsodium | key | null | CHECK |
| storage | s3_multipart_uploads | null | CHECK | | storage | s3_multipart_uploads | null | CHECK |
| auth | saml_providers | null | CHECK | | auth | saml_providers | null | CHECK |
@ -266,9 +232,7 @@
| vault | secrets | null | CHECK | | vault | secrets | null | CHECK |
| public | students | null | CHECK | | public | students | null | CHECK |
| vault | secrets | null | CHECK | | vault | secrets | null | CHECK |
| public | essay_genres | null | CHECK |
| auth | users | null | CHECK | | auth | users | null | CHECK |
| public | student_essays | null | CHECK |
| public | story_themes | null | CHECK | | public | story_themes | null | CHECK |
| public | classes | null | CHECK | | public | classes | null | CHECK |
| auth | mfa_amr_claims | null | CHECK | | auth | mfa_amr_claims | null | CHECK |
@ -282,19 +246,13 @@
| public | teachers | null | CHECK | | public | teachers | null | CHECK |
| auth | mfa_amr_claims | null | CHECK | | auth | mfa_amr_claims | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
| public | student_essays | null | CHECK |
| storage | s3_multipart_uploads_parts | null | CHECK | | storage | s3_multipart_uploads_parts | null | CHECK |
| public | teachers | 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 | phonics_categories | null | CHECK |
| public | story_settings | null | CHECK | | public | story_settings | null | CHECK |
| public | schools | null | CHECK | | public | schools | null | CHECK |
| realtime | subscription | null | CHECK | | realtime | subscription | null | CHECK |
| public | story_recordings | null | CHECK | | public | story_recordings | null | CHECK |
| public | essay_genres | null | CHECK |
| public | essay_analysis_feedback | null | CHECK |
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | story_pages | null | CHECK | | public | story_pages | null | CHECK |
| auth | identities | null | CHECK | | auth | identities | null | CHECK |
@ -302,44 +260,34 @@
| auth | flow_state | null | CHECK | | auth | flow_state | null | CHECK |
| auth | one_time_tokens | null | CHECK | | auth | one_time_tokens | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
| public | essay_genres | null | CHECK |
| auth | schema_migrations | null | CHECK | | auth | schema_migrations | null | CHECK |
| auth | mfa_challenges | null | CHECK | | auth | mfa_challenges | null | CHECK |
| public | student_phonics_attempts | null | CHECK | | public | student_phonics_attempts | null | CHECK |
| public | story_settings | null | CHECK | | public | story_settings | null | CHECK |
| public | teachers | null | CHECK | | public | teachers | null | CHECK |
| pgsodium | key | null | CHECK | | pgsodium | key | null | CHECK |
| public | essay_types | null | CHECK |
| public | interests | null | CHECK | | public | interests | null | CHECK |
| auth | instances | null | CHECK | | auth | instances | null | CHECK |
| public | schools | null | CHECK | | public | schools | null | CHECK |
| public | student_phonics_achievements | null | CHECK | | public | student_phonics_achievements | null | CHECK |
| storage | migrations | null | CHECK | | storage | migrations | null | CHECK |
| public | classes | null | CHECK | | public | classes | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| public | story_exercise_words | null | CHECK | | public | story_exercise_words | null | CHECK |
| public | phonics_word_audio | null | CHECK | | public | phonics_word_audio | null | CHECK |
| public | media_types | null | CHECK | | public | media_types | null | CHECK |
| public | languages | null | CHECK | | public | languages | null | CHECK |
| public | story_themes | null | CHECK | | public | story_themes | null | CHECK |
| public | essay_analysis_strengths | null | CHECK |
| auth | one_time_tokens | null | CHECK | | auth | one_time_tokens | null | CHECK |
| public | essay_analyses | null | CHECK |
| auth | one_time_tokens | null | CHECK | | auth | one_time_tokens | null | CHECK |
| public | essay_analysis_feedback | null | CHECK |
| realtime | messages | null | CHECK | | realtime | messages | null | CHECK |
| realtime | messages | null | CHECK | | realtime | messages | null | CHECK |
| public | story_characters | null | CHECK | | public | story_characters | null | CHECK |
| public | story_subjects | 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 | | storage | s3_multipart_uploads_parts | null | CHECK |
| auth | sso_domains | null | CHECK | | auth | sso_domains | null | CHECK |
| storage | s3_multipart_uploads_parts | null | CHECK | | storage | s3_multipart_uploads_parts | null | CHECK |
| public | essay_genres | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
| public | students | null | CHECK | | public | students | null | CHECK |
| public | essay_analysis_improvements | null | CHECK |
| auth | saml_relay_states | null | CHECK | | auth | saml_relay_states | null | CHECK |
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | story_settings | null | CHECK | | public | story_settings | null | CHECK |
@ -353,22 +301,17 @@
| public | phonics_achievements | null | CHECK | | public | phonics_achievements | null | CHECK |
| public | phonics_exercises | null | CHECK | | public | phonics_exercises | null | CHECK |
| public | story_pages | null | CHECK | | public | story_pages | null | CHECK |
| public | essay_types | null | CHECK |
| public | student_phonics_progress | null | CHECK | | public | student_phonics_progress | null | CHECK |
| auth | saml_providers | null | CHECK | | auth | saml_providers | null | CHECK |
| public | essay_analysis_feedback | null | CHECK |
| realtime | subscription | null | CHECK | | realtime | subscription | null | CHECK |
| public | story_subjects | null | CHECK | | public | story_subjects | null | CHECK |
| public | essay_analyses | null | CHECK |
| auth | sso_providers | null | CHECK | | auth | sso_providers | null | CHECK |
| public | student_essays | null | CHECK |
| public | media_types | null | CHECK | | public | media_types | null | CHECK |
| storage | s3_multipart_uploads | null | CHECK | | storage | s3_multipart_uploads | null | CHECK |
| auth | one_time_tokens | null | CHECK | | auth | one_time_tokens | null | CHECK |
| public | story_generations | null | CHECK | | public | story_generations | null | CHECK |
| auth | saml_relay_states | null | CHECK | | auth | saml_relay_states | null | CHECK |
| public | story_themes | null | CHECK | | public | story_themes | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| storage | buckets | null | CHECK | | storage | buckets | null | CHECK |
| storage | s3_multipart_uploads_parts | null | CHECK | | storage | s3_multipart_uploads_parts | null | CHECK |
| vault | secrets | null | CHECK | | vault | secrets | null | CHECK |
@ -387,44 +330,35 @@
| public | student_phonics_attempts | null | CHECK | | public | student_phonics_attempts | null | CHECK |
| public | story_generations | null | CHECK | | public | story_generations | null | CHECK |
| public | phonics_exercises | null | CHECK | | public | phonics_exercises | null | CHECK |
| public | student_essays | null | CHECK |
| auth | sso_domains | null | CHECK | | auth | sso_domains | null | CHECK |
| storage | s3_multipart_uploads | null | CHECK | | storage | s3_multipart_uploads | null | CHECK |
| public | essay_genres | null | CHECK |
| public | schools | null | CHECK | | public | schools | null | CHECK |
| auth | users | null | CHECK | | auth | users | null | CHECK |
| public | interests | null | CHECK | | public | interests | null | CHECK |
| realtime | messages | null | CHECK | | realtime | messages | null | CHECK |
| public | essay_types | null | CHECK |
| public | phonics_exercise_types | null | CHECK | | public | phonics_exercise_types | null | CHECK |
| public | story_exercise_words | null | CHECK | | public | story_exercise_words | null | CHECK |
| auth | audit_log_entries | null | CHECK | | auth | audit_log_entries | null | CHECK |
| public | story_characters | null | CHECK | | public | story_characters | null | CHECK |
| public | student_achievements_old | null | CHECK | | public | student_achievements_old | null | CHECK |
| public | essay_types | null | CHECK |
| net | http_request_queue | null | CHECK | | net | http_request_queue | null | CHECK |
| auth | saml_providers | null | CHECK | | auth | saml_providers | null | CHECK |
| storage | s3_multipart_uploads | null | CHECK | | storage | s3_multipart_uploads | null | CHECK |
| public | essay_analysis_strengths | null | CHECK |
| supabase_migrations | seed_files | null | CHECK | | supabase_migrations | seed_files | null | CHECK |
| public | phonics_achievements | null | CHECK | | public | phonics_achievements | null | CHECK |
| public | story_pages | null | CHECK | | public | story_pages | null | CHECK |
| public | phonics_exercises | null | CHECK | | public | phonics_exercises | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| storage | objects | null | CHECK | | storage | objects | null | CHECK |
| public | essay_analysis_improvements | null | CHECK |
| public | student_phonics_attempt_answers | null | CHECK | | public | student_phonics_attempt_answers | null | CHECK |
| storage | migrations | null | CHECK | | storage | migrations | null | CHECK |
| auth | sso_domains | null | CHECK | | auth | sso_domains | null | CHECK |
| public | story_recordings | null | CHECK | | public | story_recordings | null | CHECK |
| public | essay_types | null | CHECK |
| public | classes | null | CHECK | | public | classes | null | CHECK |
| net | _http_response | null | CHECK | | net | _http_response | null | CHECK |
| realtime | subscription | null | CHECK | | realtime | subscription | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
| auth | sessions | null | CHECK | | auth | sessions | null | CHECK |
| public | phonics_exercise_media | null | CHECK | | public | phonics_exercise_media | null | CHECK |
| public | essay_analysis_improvements | null | CHECK |
| public | schools | null | CHECK | | public | schools | null | CHECK |
| net | http_request_queue | null | CHECK | | net | http_request_queue | null | CHECK |
| public | classes | null | CHECK | | public | classes | null | CHECK |
@ -439,16 +373,12 @@
| auth | identities | null | CHECK | | auth | identities | null | CHECK |
| storage | s3_multipart_uploads | null | CHECK | | storage | s3_multipart_uploads | null | CHECK |
| public | story_themes | null | CHECK | | public | story_themes | null | CHECK |
| public | essay_analyses | null | CHECK |
| auth | saml_relay_states | null | CHECK | | auth | saml_relay_states | null | CHECK |
| public | essay_analyses | null | CHECK |
| public | teacher_invites | null | CHECK | | public | teacher_invites | null | CHECK |
| public | essay_analysis_feedback | null | CHECK |
| auth | mfa_factors | null | CHECK | | auth | mfa_factors | null | CHECK |
| auth | identities | null | CHECK | | auth | identities | null | CHECK |
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | story_pages | null | CHECK | | public | story_pages | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| vault | secrets | null | CHECK | | vault | secrets | null | CHECK |
| vault | secrets | null | CHECK | | vault | secrets | null | CHECK |
| public | achievement_types | null | CHECK | | public | achievement_types | null | CHECK |
@ -458,7 +388,6 @@
| supabase_migrations | schema_migrations | null | CHECK | | supabase_migrations | schema_migrations | null | CHECK |
| public | stories | null | CHECK | | public | stories | null | CHECK |
| public | phonics_exercise_media | null | CHECK | | public | phonics_exercise_media | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| auth | mfa_challenges | null | CHECK | | auth | mfa_challenges | null | CHECK |
| public | story_characters | null | CHECK | | public | story_characters | null | CHECK |
| storage | s3_multipart_uploads_parts | null | CHECK | | storage | s3_multipart_uploads_parts | null | CHECK |
@ -472,23 +401,17 @@
| auth | sessions | null | CHECK | | auth | sessions | null | CHECK |
| public | story_pages | null | CHECK | | public | story_pages | null | CHECK |
| net | http_request_queue | null | CHECK | | net | http_request_queue | null | CHECK |
| public | essay_analysis_strengths | null | CHECK |
| auth | saml_providers | null | CHECK | | auth | saml_providers | null | CHECK |
| public | teacher_classes | null | CHECK | | public | teacher_classes | null | CHECK |
| public | story_generations | null | CHECK | | public | story_generations | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| auth | flow_state | null | CHECK | | auth | flow_state | null | CHECK |
| public | story_characters | null | CHECK | | public | story_characters | null | CHECK |
| storage | s3_multipart_uploads_parts | null | CHECK | | storage | s3_multipart_uploads_parts | null | CHECK |
| public | student_achievements | null | CHECK | | public | student_achievements | null | CHECK |
| public | student_essays | null | CHECK |
| public | essay_analysis_scores | null | CHECK |
| public | students | null | CHECK | | public | students | null | CHECK |
| public | story_generations | null | CHECK | | public | story_generations | null | CHECK |
| auth | one_time_tokens | null | CHECK | | auth | one_time_tokens | null | CHECK |
| net | http_request_queue | null | CHECK | | net | http_request_queue | null | CHECK |
| auth | mfa_factors | 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 | | storage | s3_multipart_uploads_parts | null | CHECK |
| public | teacher_classes | null | CHECK | | public | teacher_classes | null | CHECK |

View File

@ -617,52 +617,14 @@
| 29638 | schools | | 29638 | schools |
| 29639 | students | | 29639 | students |
| 29639 | schools | | 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_pkey |
| 29709 | students | | 29709 | students |
| 29716 | storage.objects | | 29716 | storage.objects |
| 29717 | 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 | | 29748 | story_recordings |
| 29749 | story_recordings | | 29749 | story_recordings |
| 74045 | storage.objects | | 74045 | storage.objects |
| 34119 | net.http_request_queue_id_seq | | 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 | | 29823 | auth.users |
| 53921 | story_exercise_words | | 53921 | story_exercise_words |
| 53928 | stories_pkey | | 53928 | stories_pkey |

View File

@ -1,6 +1,6 @@
| table_schema | table_name | column_name | data_type | is_nullable | column_default | | table_schema | table_name | column_name | data_type | is_nullable | column_default |
| ------------------- | ------------------------------- | --------------------------- | --------------------------- | ----------- | -------------------------------------------------- | | ------------------- | ------------------------------- | --------------------------- | --------------------------- | ----------- | -------------------------------------------------- |
| realtime | schema_migrations | version | bigint | NO | null | | storage | s3_multipart_uploads | user_metadata | jsonb | YES | null |
| realtime | schema_migrations | inserted_at | timestamp without time zone | YES | 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 | dealloc | bigint | YES | null |
| extensions | pg_stat_statements_info | stats_reset | timestamp with time zone | YES | null | | extensions | pg_stat_statements_info | stats_reset | timestamp with time zone | YES | null |
@ -8,23 +8,6 @@
| extensions | pg_stat_statements | dbid | oid | YES | null | | extensions | pg_stat_statements | dbid | oid | YES | null |
| extensions | pg_stat_statements | toplevel | boolean | YES | null | | extensions | pg_stat_statements | toplevel | boolean | YES | null |
| extensions | pg_stat_statements | queryid | bigint | 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 | teacher_id | uuid | NO | null |
| public | teacher_classes | class_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()) | | public | teacher_classes | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
@ -250,7 +233,7 @@
| storage | s3_multipart_uploads_parts | created_at | timestamp with time zone | NO | now() | | 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 | in_progress_size | bigint | NO | 0 |
| storage | s3_multipart_uploads | created_at | timestamp with time zone | NO | now() | | storage | s3_multipart_uploads | created_at | timestamp with time zone | NO | now() |
| storage | s3_multipart_uploads | user_metadata | jsonb | YES | null | | realtime | schema_migrations | version | bigint | NO | null |
| extensions | pg_stat_statements | plans | bigint | 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 | total_plan_time | double precision | YES | null |
| extensions | pg_stat_statements | min_plan_time | double precision | YES | null | | extensions | pg_stat_statements | min_plan_time | double precision | YES | null |
@ -355,17 +338,7 @@
| storage | objects | user_metadata | jsonb | YES | null | | storage | objects | user_metadata | jsonb | YES | null |
| storage | migrations | id | integer | NO | null | | storage | migrations | id | integer | NO | null |
| storage | migrations | executed_at | timestamp without time zone | YES | CURRENT_TIMESTAMP | | 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 | | 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 | id | uuid | YES | null |
| pgsodium | valid_key | status | USER-DEFINED | YES | null | | pgsodium | valid_key | status | USER-DEFINED | YES | null |
| pgsodium | valid_key | key_type | USER-DEFINED | YES | null | | pgsodium | valid_key | key_type | USER-DEFINED | YES | null |
@ -373,12 +346,6 @@
| pgsodium | valid_key | key_context | bytea | YES | null | | pgsodium | valid_key | key_context | bytea | YES | null |
| pgsodium | valid_key | created | timestamp with time zone | YES | null | | pgsodium | valid_key | created | timestamp with time zone | YES | null |
| pgsodium | valid_key | expires | 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 | id | uuid | YES | null |
| pgsodium | decrypted_key | status | USER-DEFINED | YES | null | | pgsodium | decrypted_key | status | USER-DEFINED | YES | null |
| pgsodium | decrypted_key | created | timestamp with time zone | YES | null | | pgsodium | decrypted_key | created | timestamp with time zone | YES | null |
@ -437,89 +404,71 @@
| public | story_pages | story_id | uuid | YES | null | | public | story_pages | story_id | uuid | YES | null |
| public | story_pages | page_number | integer | NO | null | | public | story_pages | page_number | integer | NO | null |
| public | story_pages | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) | | 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 | id | uuid | NO | uuid_generate_v4() |
| public | schools | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) | | 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 | 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 | id | uuid | NO | uuid_generate_v4() |
| public | teachers | school_id | uuid | NO | null | | public | teachers | school_id | uuid | NO | null |
| public | teachers | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) | | public | teachers | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| vault | secrets | secret | text | NO | null | | public | teachers | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| public | story_themes | icon | text | NO | null | | public | teachers | class_ids | ARRAY | YES | null |
| public | story_recordings | improvements | ARRAY | YES | null | | public | classes | id | uuid | NO | uuid_generate_v4() |
| public | achievement_types | name | character varying | NO | null | | public | classes | school_id | uuid | NO | null |
| public | achievement_types | description | text | YES | null | | public | classes | year | integer | NO | null |
| public | story_recordings | suggestions | text | YES | null | | public | classes | created_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| vault | decrypted_secrets | name | text | YES | null | | public | classes | updated_at | timestamp with time zone | NO | timezone('utc'::text, now()) |
| vault | decrypted_secrets | description | text | YES | null | | public | classes | teacher_id | uuid | YES | null |
| vault | decrypted_secrets | secret | text | YES | null | | public | teacher_classes | id | uuid | NO | uuid_generate_v4() |
| 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 | | storage | s3_multipart_uploads_parts | owner_id | text | YES | null |
| auth | mfa_amr_claims | authentication_method | text | NO | null | | auth | mfa_amr_claims | authentication_method | text | NO | null |
| auth | identities | provider | text | NO | null | | auth | identities | provider | text | NO | null |
| storage | s3_multipart_uploads_parts | version | 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 | text | text | NO | null |
| public | story_pages | image_url | 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 | context | text | YES | null |
| public | story_details | theme_title | text | YES | null | | public | story_details | theme_title | text | YES | null |
| public | story_details | theme_icon | text | YES | null | | public | story_details | theme_icon | text | YES | null |
@ -529,70 +478,70 @@
| public | story_details | character_icon | text | YES | null | | public | story_details | character_icon | text | YES | null |
| public | story_details | setting_title | text | YES | null | | public | story_details | setting_title | text | YES | null |
| public | story_details | setting_icon | text | YES | null | | public | story_details | setting_icon | text | YES | null |
| public | teacher_invites | email | text | NO | null | | public | schools | name | text | NO | null |
| public | story_pages | image_path | text | YES | null | | public | schools | address | text | YES | null |
| public | story_exercise_words | word | text | NO | null | | public | story_exercise_words | word | text | NO | null |
| public | story_exercise_words | exercise_type | text | NO | null | | public | story_exercise_words | exercise_type | text | NO | null |
| public | story_exercise_words | phonemes | ARRAY | YES | null | | public | story_exercise_words | phonemes | ARRAY | YES | null |
| public | story_exercise_words | syllable_pattern | text | YES | null | | public | story_exercise_words | syllable_pattern | text | YES | null |
| public | story_pages | image_url_thumb | text | YES | null | | public | schools | phone | text | YES | null |
| public | story_pages | image_url_medium | text | YES | null | | public | schools | email | text | YES | null |
| public | story_pages | image_url_large | text | YES | null | | public | phonics_exercise_types | description | text | YES | null |
| public | story_pages | image_path_thumb | text | YES | null | | auth | saml_relay_states | redirect_to | text | YES | null |
| public | story_pages | image_path_medium | text | YES | null | | public | schools | director_name | text | NO | 'Não informado'::text |
| public | story_pages | image_path_large | text | YES | null | | auth | audit_log_entries | ip_address | character varying | NO | ''::character varying |
| public | story_pages | text_syllables | text | YES | null | | public | phonics_words | word | character varying | NO | null |
| public | teacher_invites | name | text | NO | null | | public | teachers | name | text | NO | null |
| auth | schema_migrations | version | character varying | NO | null | | auth | schema_migrations | version | character varying | NO | null |
| public | teacher_invites | subject | text | YES | null | | public | teachers | email | text | NO | null |
| public | teacher_invites | message | text | YES | null | | public | teachers | phone | text | YES | null |
| auth | instances | raw_base_config | text | YES | null | | auth | instances | raw_base_config | text | YES | null |
| public | essay_analyses | suggestions | text | YES | null | | public | teachers | subject | text | YES | null |
| public | teacher_invites | status | text | YES | 'pending'::text | | public | phonics_words | phonetic_transcription | character varying | YES | null |
| public | teacher_invites | token | text | NO | null | | auth | saml_providers | name_id_format | text | YES | null |
| storage | s3_multipart_uploads | version | text | NO | null | | public | teachers | status | text | YES | 'pending'::text |
| auth | users | aud | character varying | YES | null | | auth | users | aud | character varying | YES | null |
| auth | users | role | character varying | YES | null | | auth | users | role | character varying | YES | null |
| auth | users | email | character varying | YES | null | | auth | users | email | character varying | YES | null |
| auth | users | encrypted_password | character varying | YES | null | | auth | users | encrypted_password | character varying | YES | null |
| public | essay_analysis_feedback | structure_feedback | text | NO | null | | public | phonics_word_audio | word | text | NO | null |
| public | essay_analysis_feedback | content_feedback | text | NO | null | | public | phonics_word_audio | audio_url | text | NO | null |
| auth | users | confirmation_token | character varying | YES | null | | auth | users | confirmation_token | character varying | YES | null |
| public | essay_analysis_feedback | language_feedback | text | NO | null | | public | phonics_word_audio | audio_path | text | NO | null |
| auth | users | recovery_token | character varying | YES | null | | auth | users | recovery_token | character varying | YES | null |
| auth | saml_providers | entity_id | text | NO | null | | public | classes | name | text | NO | null |
| auth | users | email_change_token_new | character varying | YES | null | | auth | users | email_change_token_new | character varying | YES | null |
| auth | users | email_change | character varying | YES | null | | auth | users | email_change | character varying | YES | null |
| auth | mfa_challenges | otp_code | text | YES | null | | public | classes | grade | text | NO | null |
| public | schools | name | text | NO | null | | storage | s3_multipart_uploads | upload_signature | text | NO | null |
| public | schools | address | text | YES | null | | public | classes | period | text | YES | null |
| public | schools | phone | text | YES | null | | storage | s3_multipart_uploads_parts | upload_id | text | NO | null |
| public | schools | email | text | YES | null | | public | languages | name | character varying | NO | null |
| auth | saml_providers | metadata_xml | text | NO | null | | public | languages | code | character varying | NO | null |
| auth | saml_providers | metadata_url | text | YES | null | | public | languages | instructions | text | YES | null |
| auth | users | phone | text | YES | NULL::character varying | | auth | users | phone | text | YES | NULL::character varying |
| public | schools | director_name | text | NO | 'Não informado'::text | | public | phonics_categories | name | character varying | NO | null |
| auth | users | phone_change | text | YES | ''::character varying | | auth | users | phone_change | text | YES | ''::character varying |
| auth | users | phone_change_token | character varying | YES | ''::character varying | | auth | users | phone_change_token | character varying | YES | ''::character varying |
| public | phonics_exercises | title | character varying | NO | null | | public | phonics_categories | description | text | YES | null |
| public | phonics_exercises | description | text | YES | null | | public | languages | flag_icon | character varying | YES | null |
| auth | users | email_change_token_current | character varying | YES | ''::character varying | | auth | users | email_change_token_current | character varying | YES | ''::character varying |
| public | essay_analysis_strengths | strength | text | NO | null | | auth | sso_providers | resource_id | text | YES | null |
| public | phonics_achievements | name | character varying | NO | null | | auth | flow_state | auth_code | text | NO | null |
| auth | users | reauthentication_token | character varying | YES | ''::character varying | | auth | users | reauthentication_token | character varying | YES | ''::character varying |
| public | phonics_achievements | description | text | YES | null | | public | students | name | text | NO | null |
| public | phonics_exercises | instructions | text | NO | null | | public | students | email | text | NO | null |
| public | essay_analysis_improvements | improvement | text | NO | null | | storage | s3_multipart_uploads | bucket_id | text | NO | null |
| storage | s3_multipart_uploads | id | text | NO | null | | public | students | guardian_name | text | YES | null |
| public | phonics_achievements | icon_url | text | YES | null | | public | students | guardian_phone | text | YES | null |
| auth | identities | email | text | YES | null | | public | students | guardian_email | text | YES | null |
| auth | refresh_tokens | token | character varying | YES | null | | auth | refresh_tokens | token | character varying | YES | null |
| auth | refresh_tokens | user_id | character varying | YES | null | | auth | refresh_tokens | user_id | character varying | YES | null |
| public | teachers | name | text | NO | null | | auth | flow_state | code_challenge | text | NO | null |
| public | teachers | email | text | NO | null | | auth | flow_state | provider_type | text | NO | null |
| public | teachers | phone | text | YES | null | | public | students | status | text | NO | 'active'::text |
| auth | refresh_tokens | parent | character varying | YES | null | | auth | refresh_tokens | parent | character varying | YES | null |
| public | teachers | subject | text | YES | null | | public | story_recordings | audio_url | text | YES | null |
| supabase_migrations | seed_files | path | text | NO | null | | supabase_migrations | seed_files | path | text | NO | null |
| supabase_migrations | seed_files | hash | text | NO | null | | supabase_migrations | seed_files | hash | text | NO | null |
| supabase_migrations | schema_migrations | version | text | NO | null | | supabase_migrations | schema_migrations | version | text | NO | null |
@ -600,102 +549,99 @@
| supabase_migrations | schema_migrations | name | text | YES | null | | supabase_migrations | schema_migrations | name | text | YES | null |
| storage | buckets | id | text | NO | null | | storage | buckets | id | text | NO | null |
| storage | buckets | name | 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 | avatar_url | text | YES | null |
| public | students | nickname | character varying | YES | null | | public | students | nickname | character varying | YES | null |
| public | phonics_exercise_media | url | text | NO | null | | public | phonics_exercise_media | url | text | NO | null |
| public | phonics_exercise_media | alt_text | text | YES | null | | public | phonics_exercise_media | alt_text | text | YES | null |
| public | students | preferred_themes | ARRAY | YES | null | | public | students | preferred_themes | ARRAY | YES | null |
| public | story_recordings | status | text | NO | 'pending_analysis'::text | | 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 | | 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 | | auth | one_time_tokens | token_hash | text | NO | null |
| public | media_types | name | character varying | 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 | media_types | description | text | YES | null |
| public | story_recordings | transcription | text | YES | null | | public | story_recordings | transcription | text | YES | null |
| auth | one_time_tokens | relates_to | text | NO | 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 | | public | story_recordings | error_message | text | YES | null |
| pgsodium | decrypted_key | comment | text | YES | null |
| auth | sessions | user_agent | 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 | | 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 | | auth | flow_state | provider_refresh_token | text | YES | null |
| pgsodium | mask_columns | format_type | text | YES | null |
| public | stories | status | text | YES | 'draft'::text | | public | stories | status | text | YES | 'draft'::text |
| pgsodium | valid_key | name | text | YES | null |
| auth | sessions | tag | text | YES | null | | auth | sessions | tag | text | YES | null |
| auth | mfa_factors | friendly_name | 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 | name | text | YES | null |
| vault | decrypted_secrets | decrypted_secret | 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 |
| pgsodium | mask_columns | key_id_column | text | YES | null | | pgsodium | mask_columns | key_id_column | text | YES | null |
| pgsodium | mask_columns | associated_columns | text | YES | null | | pgsodium | mask_columns | associated_columns | text | YES | null |
| pgsodium | mask_columns | nonce_column | text | YES | null | | pgsodium | mask_columns | nonce_column | text | YES | null |
| pgsodium | masking_rule | view_name | 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 | col_description | text | YES | null |
| pgsodium | masking_rule | key_id_column | text | YES | null | | pgsodium | masking_rule | key_id_column | text | YES | null |
| storage | s3_multipart_uploads_parts | key | text | NO | null |
| storage | s3_multipart_uploads | key | text | NO | null |
| pgsodium | masking_rule | key_id | 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 | | 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 |

View File

@ -63,18 +63,18 @@ WITH
SELECT extensions.url_encode(extensions.hmac(signables, secret, alg.id)) FROM alg; SELECT extensions.url_encode(extensions.hmac(signables, secret, alg.id)) FROM alg;
$function$ $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[]) | extensions | armor | CREATE OR REPLACE FUNCTION extensions.armor(bytea, text[], text[])
RETURNS text RETURNS text
LANGUAGE c LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_armor$function$ 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 | bytea_to_text | CREATE OR REPLACE FUNCTION extensions.bytea_to_text(data bytea) | extensions | bytea_to_text | CREATE OR REPLACE FUNCTION extensions.bytea_to_text(data bytea)
RETURNS text RETURNS text
LANGUAGE c LANGUAGE c
@ -296,21 +296,16 @@ AS '$libdir/pgcrypto', $function$pg_hmac$function$
LANGUAGE c LANGUAGE c
AS '$libdir/http', $function$http_request$function$ AS '$libdir/http', $function$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) | extensions | http_delete | CREATE OR REPLACE FUNCTION extensions.http_delete(uri character varying, content character varying, content_type character varying)
RETURNS http_response RETURNS http_response
LANGUAGE sql LANGUAGE sql
AS $function$ SELECT extensions.http(('DELETE', $1, NULL, $3, $2)::extensions.http_request) $function$ 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) | extensions | http_delete | CREATE OR REPLACE FUNCTION extensions.http_delete(uri character varying)
RETURNS http_response RETURNS http_response
LANGUAGE sql LANGUAGE sql
AS $function$ SELECT extensions.http(('GET', $1, NULL, NULL, NULL)::extensions.http_request) $function$ AS $function$ SELECT extensions.http(('DELETE', $1, NULL, NULL, NULL)::extensions.http_request) $function$
| |
| extensions | http_get | CREATE OR REPLACE FUNCTION extensions.http_get(uri character varying, data jsonb) | extensions | http_get | CREATE OR REPLACE FUNCTION extensions.http_get(uri character varying, data jsonb)
RETURNS http_response RETURNS http_response
LANGUAGE sql LANGUAGE sql
@ -318,6 +313,11 @@ AS $function$
SELECT extensions.http(('GET', $1 || '?' || extensions.urlencode($2), NULL, NULL, NULL)::extensions.http_request) SELECT extensions.http(('GET', $1 || '?' || extensions.urlencode($2), NULL, NULL, NULL)::extensions.http_request)
$function$ $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) | extensions | http_head | CREATE OR REPLACE FUNCTION extensions.http_head(uri character varying)
RETURNS http_response RETURNS http_response
LANGUAGE sql LANGUAGE sql
@ -338,6 +338,11 @@ AS '$libdir/http', $function$http_list_curlopt$function$
LANGUAGE sql LANGUAGE sql
AS $function$ SELECT extensions.http(('PATCH', $1, NULL, $3, $2)::extensions.http_request) $function$ 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) | extensions | http_post | CREATE OR REPLACE FUNCTION extensions.http_post(uri character varying, data jsonb)
RETURNS http_response RETURNS http_response
LANGUAGE sql LANGUAGE sql
@ -345,11 +350,6 @@ AS $function$
SELECT extensions.http(('POST', $1, NULL, 'application/x-www-form-urlencoded', extensions.urlencode($2))::extensions.http_request) SELECT extensions.http(('POST', $1, NULL, 'application/x-www-form-urlencoded', extensions.urlencode($2))::extensions.http_request)
$function$ $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) | extensions | http_put | CREATE OR REPLACE FUNCTION extensions.http_put(uri character varying, content character varying, content_type character varying)
RETURNS http_response RETURNS http_response
LANGUAGE sql LANGUAGE sql
@ -395,6 +395,12 @@ AS '$libdir/pgcrypto', $function$pgp_armor_headers$function$
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_key_id_w$function$ 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) | extensions | pgp_pub_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt(bytea, bytea)
RETURNS text RETURNS text
LANGUAGE c LANGUAGE c
@ -407,30 +413,24 @@ AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$ 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) | extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$ 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) | extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$ 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_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt(text, bytea) | extensions | pgp_pub_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt(text, bytea)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
@ -443,18 +443,18 @@ AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
PARALLEL SAFE STRICT PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$ 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)
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) | extensions | pgp_pub_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
PARALLEL SAFE STRICT PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$ 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_sym_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt(bytea, text) | extensions | pgp_sym_decrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt(bytea, text)
RETURNS text RETURNS text
LANGUAGE c LANGUAGE c
@ -467,42 +467,42 @@ AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$ 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)
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, text) | extensions | pgp_sym_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$ AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
| |
| extensions | pgp_sym_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt(text, text) | extensions | pgp_sym_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_decrypt_bytea(bytea, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
PARALLEL SAFE STRICT IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$ 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_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt(text, text, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
PARALLEL SAFE STRICT PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$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) | extensions | pgp_sym_encrypt | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt(text, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
PARALLEL SAFE STRICT 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, text) | extensions | pgp_sym_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text, text)
RETURNS bytea RETURNS bytea
LANGUAGE c LANGUAGE c
PARALLEL SAFE STRICT PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$ 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)
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() | extensions | pgrst_ddl_watch | CREATE OR REPLACE FUNCTION extensions.pgrst_ddl_watch()
RETURNS event_trigger RETURNS event_trigger
LANGUAGE plpgsql LANGUAGE plpgsql
@ -675,12 +675,6 @@ AS $function$
SELECT translate(encode(data, 'base64'), E'+/=\n', '-_'); SELECT translate(encode(data, 'base64'), E'+/=\n', '-_');
$function$ $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) | extensions | urlencode | CREATE OR REPLACE FUNCTION extensions.urlencode(string character varying)
RETURNS text RETURNS text
LANGUAGE c LANGUAGE c
@ -693,6 +687,12 @@ AS '$libdir/http', $function$urlencode$function$
IMMUTABLE STRICT IMMUTABLE STRICT
AS '$libdir/http', $function$urlencode_jsonb$function$ 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() | extensions | uuid_generate_v1 | CREATE OR REPLACE FUNCTION extensions.uuid_generate_v1()
RETURNS uuid RETURNS uuid
LANGUAGE c LANGUAGE c

View File

@ -1,39 +1,31 @@
| policy_id | schema_name | table_name | policy_name | command | policy_using | policy_check | | 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 | | 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()) | | 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()) | | 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 | | 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 | | 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 | 65878 | public | interests | Students can delete their own interests | d | (auth.uid() = student_id) | null |
FROM student_essays | 65876 | public | interests | Students can insert their own interests | a | null | (auth.uid() = student_id) |
WHERE ((student_essays.id = essay_analyses.essay_id) AND (student_essays.student_id = auth.uid())))) | null | | 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) |
| 113770 | public | essay_analyses | Edge Function pode inserir análises | a | null | (((auth.jwt() ->> 'role'::text) = 'service_role'::text) OR (EXISTS ( SELECT 1 | 65875 | public | interests | Students can view their own interests | r | (auth.uid() = student_id) | null |
FROM student_essays | 104599 | public | languages | Allow insert/update for admins only | * | ((auth.jwt() ->> 'role'::text) = 'admin'::text) | ((auth.jwt() ->> 'role'::text) = 'admin'::text) |
WHERE ((student_essays.id = essay_analyses.essay_id) AND (student_essays.student_id = auth.uid()))))) | | 104598 | public | languages | Allow read access for all authenticated users | r | true | null |
| 113765 | public | essay_genres | Gêneros textuais visíveis para todos | r | (active = true) | null | | 79931 | public | phonics_categories | Permitir leitura de categorias fonéticas para usuários autent | r | true | null |
| 113764 | public | essay_types | Tipos de redação visíveis para todos | r | (active = true) | null | | 79932 | public | phonics_exercise_types | Permitir leitura de tipos de exercícios fonéticos para usuár | r | true | null |
| 65878 | public | interests | Students can delete their own interests | d | (auth.uid() = student_id) | null | | 79934 | public | phonics_exercise_words | Permitir leitura de relações exercício-palavra para usuário | r | true | null |
| 65876 | public | interests | Students can insert their own interests | a | null | (auth.uid() = student_id) | | 79930 | public | phonics_exercises | Permitir leitura de exercícios fonéticos para usuários auten | r | true | null |
| 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) | | 79933 | public | phonics_words | Permitir leitura de palavras fonéticas para usuários autentic | r | true | null |
| 65875 | public | interests | Students can view their own interests | r | (auth.uid() = student_id) | null | | 29440 | public | schools | Enable insert for registration | a | null | true |
| 104599 | public | languages | Allow insert/update for admins only | * | ((auth.jwt() ->> 'role'::text) = 'admin'::text) | ((auth.jwt() ->> 'role'::text) = 'admin'::text) | | 29299 | public | schools | Escolas visíveis para usuários autenticados | r | true | null |
| 104598 | public | languages | Allow read access for all authenticated users | r | true | null | | 29442 | public | schools | Schools can update own data | w | (auth.uid() = id) | (auth.uid() = id) |
| 79931 | public | phonics_categories | Permitir leitura de categorias fonéticas para usuários autent | r | true | null | | 29441 | public | schools | Schools can view own data | r | (auth.uid() = id) | 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 | 29347 | public | stories | Alunos podem atualizar suas próprias histórias | w | (student_id IN ( SELECT students.id
FROM students 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 | 29346 | public | stories | Alunos podem criar suas próprias histórias | a | null | (student_id IN ( SELECT students.id
FROM students 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 | 36241 | public | stories | Estudantes podem ver suas próprias histórias | r | ((auth.uid() = student_id) AND (EXISTS ( SELECT 1
FROM story_themes FROM story_themes
WHERE ((story_themes.id = stories.theme_id) AND (story_themes.active = true)))) AND (EXISTS ( SELECT 1 WHERE ((story_themes.id = stories.theme_id) AND (story_themes.active = true)))) AND (EXISTS ( SELECT 1
@ -42,64 +34,60 @@
FROM story_characters FROM story_characters
WHERE ((story_characters.id = stories.character_id) AND (story_characters.active = true)))) AND (EXISTS ( SELECT 1 WHERE ((story_characters.id = stories.character_id) AND (story_characters.active = true)))) AND (EXISTS ( SELECT 1
FROM story_settings FROM story_settings
WHERE ((story_settings.id = stories.setting_id) AND (story_settings.active = 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 | | 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 | | 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 | | 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) | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | 31560 | public | story_recordings | Escolas podem ver todas as gravações | r | (EXISTS ( SELECT 1
FROM students s FROM students s
WHERE ((s.id = story_recordings.student_id) AND (s.school_id = auth.uid())))) | 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) | | 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 | | 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 | 31558 | public | story_recordings | Professores podem ver gravações de seus alunos | r | (EXISTS ( SELECT 1
FROM (classes c FROM (classes c
JOIN students s ON ((s.class_id = c.id))) JOIN students s ON ((s.class_id = c.id)))
WHERE ((s.id = story_recordings.student_id) AND (c.teacher_id = auth.uid())))) | 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) | | 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 | | 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 | | 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 | | 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 | | 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()) | | 29302 | public | students | Alunos visíveis para usuários autenticados | r | true | null |
| 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 | 29638 | public | students | Escolas podem inserir seus próprios alunos | a | null | (auth.uid() IN ( SELECT schools.id
FROM schools 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 | 29639 | public | students | Escolas podem ver seus próprios alunos | r | (auth.uid() IN ( SELECT schools.id
FROM schools FROM schools
WHERE (schools.id = students.school_id))) | null | WHERE (schools.id = students.school_id))) | null |
| 29584 | public | students | Schools can view their students | r | (school_id = auth.uid()) | 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 | 29511 | public | teacher_invites | Schools can invite teachers | a | null | (school_id IN ( SELECT schools.id
FROM schools FROM schools
WHERE (schools.id = auth.uid()))) | WHERE (schools.id = auth.uid()))) |
| 29300 | public | teachers | Professores visíveis para usuários autenticados | r | true | null | | 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 | 29510 | public | teachers | Schools can view their teachers | r | (school_id IN ( SELECT schools.id
FROM schools FROM schools
WHERE (schools.id = auth.uid()))) | null | WHERE (schools.id = auth.uid()))) | null |
| 29509 | public | teachers | Teachers can view own data | r | (auth.uid() = id) | 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 | | 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])) | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | 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 FROM stories s
WHERE ((s.id = ((storage.foldername(objects.name))[1])::uuid) AND (s.student_id = auth.uid()))))) | 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 | | 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 | | 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 | | 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)) | | 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) | | 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)) | | 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 | | 74045 | storage | objects | Áudios públicos | r | (bucket_id = 'phonics-audio'::text) | null |

View File

@ -1,21 +0,0 @@
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'
};
}

View File

@ -1,461 +0,0 @@
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' } }
)
}
})

View File

@ -1,229 +0,0 @@
-- 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"]}'
);

View File

@ -1,37 +0,0 @@
-- 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;

View File

@ -1,141 +0,0 @@
-- 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"]}'
);

View File

@ -1,94 +0,0 @@
-- 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()
)
);

View File

@ -1,165 +0,0 @@
-- 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;

View File

@ -1,89 +0,0 @@
-- 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()
)
);

View File

@ -1,55 +1,12 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { export default {
darkMode: ["class"], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: { theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: { extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: { 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: { purple: {
50: '#f5f3ff', 50: '#f5f3ff',
100: '#ede9fe', 100: '#ede9fe',
@ -75,243 +32,56 @@ module.exports = {
900: '#1e3a8a', 900: '#1e3a8a',
}, },
}, },
borderRadius: { animation: {
none: '0', 'in': 'in 200ms ease-in',
sm: '0.125rem', 'out': 'out 200ms ease-out',
DEFAULT: '0.25rem', 'slide-in-from-top': 'slide-in-from-top 200ms ease-out',
md: '0.375rem', 'slide-in-from-bottom': 'slide-in-from-bottom 200ms ease-out',
lg: '0.5rem', 'slide-out-to-right': 'slide-out-to-right 200ms ease-out',
xl: '0.75rem', 'fade-in': 'fade-in 200ms ease-in',
'2xl': '1rem', 'fade-out': 'fade-out 200ms ease-out',
'3xl': '1.5rem', 'scale-in': 'scale-in 200ms ease-out',
full: '9999px', 'scale-out': 'scale-out 200ms ease-in',
}, },
keyframes: { keyframes: {
"fade-in": { in: {
"0%": { opacity: 0 }, '0%': { transform: 'translateX(100%)' },
"100%": { opacity: 1 } '100%': { transform: 'translateX(0)' },
}, },
"fade-out": { out: {
"0%": { opacity: 1 }, '0%': { transform: 'translateX(0)' },
"100%": { opacity: 0 } '100%': { transform: 'translateX(100%)' },
} },
}, 'slide-in-from-top': {
animation: { '0%': { transform: 'translateY(-100%)' },
"fade-in": "fade-in 200ms ease-in", '100%': { transform: 'translateY(0)' },
"fade-out": "fade-out 200ms ease-out" },
}, 'slide-in-from-bottom': {
typography: { '0%': { transform: 'translateY(100%)' },
DEFAULT: { '100%': { transform: 'translateY(0)' },
css: { },
maxWidth: '100%', 'slide-out-to-right': {
color: 'var(--tw-prose-body)', '0%': { transform: 'translateX(0)' },
'[class~="lead"]': { '100%': { transform: 'translateX(100%)' },
color: 'var(--tw-prose-lead)', },
}, 'fade-in': {
a: { '0%': { opacity: 0 },
color: 'var(--tw-prose-links)', '100%': { opacity: 1 },
textDecoration: 'underline', },
fontWeight: '500', 'fade-out': {
}, '0%': { opacity: 1 },
strong: { '100%': { opacity: 0 },
color: 'var(--tw-prose-bold)', },
fontWeight: '600', 'scale-in': {
}, '0%': { transform: 'scale(0.95)', opacity: 0 },
'ol[type="A"]': { '100%': { transform: 'scale(1)', opacity: 1 },
'--list-counter-style': 'upper-alpha', },
}, 'scale-out': {
'ol[type="a"]': { '0%': { transform: 'scale(1)', opacity: 1 },
'--list-counter-style': 'lower-alpha', '100%': { transform: 'scale(0.95)', opacity: 0 },
},
'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'), };
],
}