mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
Compare commits
No commits in common. "bb85c83c5b630305e57bbd1ebfe51188a9b71f70" and "478ca2441d8c9f4dadbc3c88f066d56c3277de02" have entirely different histories.
bb85c83c5b
...
478ca2441d
74
CHANGELOG.md
74
CHANGELOG.md
@ -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
1244
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -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",
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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 }
|
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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 />,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
@ -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[];
|
|
||||||
}
|
|
||||||
@ -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 |
|
||||||
@ -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 |
|
||||||
|
|||||||
@ -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 |
|
|
||||||
@ -63,13 +63,13 @@ 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)
|
| 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, text[], text[])
|
| extensions | armor | CREATE OR REPLACE FUNCTION extensions.armor(bytea)
|
||||||
RETURNS text
|
RETURNS text
|
||||||
LANGUAGE c
|
LANGUAGE c
|
||||||
IMMUTABLE PARALLEL SAFE STRICT
|
IMMUTABLE PARALLEL SAFE STRICT
|
||||||
@ -296,20 +296,15 @@ 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
|
||||||
@ -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,25 +413,19 @@ 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)
|
| 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, text)
|
| extensions | pgp_pub_decrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_decrypt_bytea(bytea, bytea, text)
|
||||||
RETURNS bytea
|
RETURNS bytea
|
||||||
LANGUAGE c
|
LANGUAGE c
|
||||||
IMMUTABLE PARALLEL SAFE STRICT
|
IMMUTABLE PARALLEL SAFE STRICT
|
||||||
@ -443,13 +443,13 @@ 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)
|
| 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, text)
|
| extensions | pgp_pub_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_pub_encrypt_bytea(bytea, bytea)
|
||||||
RETURNS bytea
|
RETURNS bytea
|
||||||
LANGUAGE c
|
LANGUAGE c
|
||||||
PARALLEL SAFE STRICT
|
PARALLEL SAFE STRICT
|
||||||
@ -467,23 +467,17 @@ 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
|
||||||
@ -491,13 +485,19 @@ AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
|||||||
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
|
||||||
|
LANGUAGE c
|
||||||
|
PARALLEL SAFE STRICT
|
||||||
|
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
||||||
|
|
|
||||||
|
| extensions | pgp_sym_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text, text)
|
||||||
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, text)
|
| extensions | pgp_sym_encrypt_bytea | CREATE OR REPLACE FUNCTION extensions.pgp_sym_encrypt_bytea(bytea, text)
|
||||||
RETURNS bytea
|
RETURNS bytea
|
||||||
LANGUAGE c
|
LANGUAGE c
|
||||||
PARALLEL SAFE STRICT
|
PARALLEL SAFE STRICT
|
||||||
@ -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
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
| 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
|
|
||||||
FROM student_essays
|
|
||||||
WHERE ((student_essays.id = essay_analyses.essay_id) AND (student_essays.student_id = auth.uid())))) | null |
|
|
||||||
| 113770 | public | essay_analyses | Edge Function pode inserir análises | a | null | (((auth.jwt() ->> 'role'::text) = 'service_role'::text) OR (EXISTS ( SELECT 1
|
|
||||||
FROM student_essays
|
|
||||||
WHERE ((student_essays.id = essay_analyses.essay_id) AND (student_essays.student_id = auth.uid()))))) |
|
|
||||||
| 113765 | public | essay_genres | Gêneros textuais visíveis para todos | r | (active = true) | null |
|
|
||||||
| 113764 | public | essay_types | Tipos de redação visíveis para todos | r | (active = true) | null |
|
|
||||||
| 65878 | public | interests | Students can delete their own interests | d | (auth.uid() = student_id) | null |
|
| 65878 | public | interests | Students can delete their own interests | d | (auth.uid() = student_id) | null |
|
||||||
| 65876 | public | interests | Students can insert their own interests | a | null | (auth.uid() = student_id) |
|
| 65876 | public | interests | Students can insert their own interests | a | null | (auth.uid() = student_id) |
|
||||||
| 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) |
|
| 65877 | public | interests | Students can update their own interests | w | (auth.uid() = student_id) | (auth.uid() = student_id) |
|
||||||
@ -66,10 +58,6 @@
|
|||||||
| 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()) |
|
|
||||||
| 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 |
|
| 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
|
||||||
|
|||||||
@ -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'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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' } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -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"]}'
|
|
||||||
);
|
|
||||||
@ -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;
|
|
||||||
@ -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"]}'
|
|
||||||
);
|
|
||||||
@ -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()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@ -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;
|
|
||||||
@ -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()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@ -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%)' },
|
||||||
}
|
|
||||||
},
|
},
|
||||||
animation: {
|
'slide-in-from-top': {
|
||||||
"fade-in": "fade-in 200ms ease-in",
|
'0%': { transform: 'translateY(-100%)' },
|
||||||
"fade-out": "fade-out 200ms ease-out"
|
'100%': { transform: 'translateY(0)' },
|
||||||
},
|
},
|
||||||
typography: {
|
'slide-in-from-bottom': {
|
||||||
DEFAULT: {
|
'0%': { transform: 'translateY(100%)' },
|
||||||
css: {
|
'100%': { transform: 'translateY(0)' },
|
||||||
maxWidth: '100%',
|
|
||||||
color: 'var(--tw-prose-body)',
|
|
||||||
'[class~="lead"]': {
|
|
||||||
color: 'var(--tw-prose-lead)',
|
|
||||||
},
|
},
|
||||||
a: {
|
'slide-out-to-right': {
|
||||||
color: 'var(--tw-prose-links)',
|
'0%': { transform: 'translateX(0)' },
|
||||||
textDecoration: 'underline',
|
'100%': { transform: 'translateX(100%)' },
|
||||||
fontWeight: '500',
|
|
||||||
},
|
},
|
||||||
strong: {
|
'fade-in': {
|
||||||
color: 'var(--tw-prose-bold)',
|
'0%': { opacity: 0 },
|
||||||
fontWeight: '600',
|
'100%': { opacity: 1 },
|
||||||
},
|
},
|
||||||
'ol[type="A"]': {
|
'fade-out': {
|
||||||
'--list-counter-style': 'upper-alpha',
|
'0%': { opacity: 1 },
|
||||||
|
'100%': { opacity: 0 },
|
||||||
},
|
},
|
||||||
'ol[type="a"]': {
|
'scale-in': {
|
||||||
'--list-counter-style': 'lower-alpha',
|
'0%': { transform: 'scale(0.95)', opacity: 0 },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: 1 },
|
||||||
},
|
},
|
||||||
'ol[type="A" s]': {
|
'scale-out': {
|
||||||
'--list-counter-style': 'upper-alpha',
|
'0%': { transform: 'scale(1)', opacity: 1 },
|
||||||
},
|
'100%': { transform: 'scale(0.95)', opacity: 0 },
|
||||||
'ol[type="a" s]': {
|
|
||||||
'--list-counter-style': 'lower-alpha',
|
|
||||||
},
|
|
||||||
'ol[type="I"]': {
|
|
||||||
'--list-counter-style': 'upper-roman',
|
|
||||||
},
|
|
||||||
'ol[type="i"]': {
|
|
||||||
'--list-counter-style': 'lower-roman',
|
|
||||||
},
|
|
||||||
'ol[type="I" s]': {
|
|
||||||
'--list-counter-style': 'upper-roman',
|
|
||||||
},
|
|
||||||
'ol[type="i" s]': {
|
|
||||||
'--list-counter-style': 'lower-roman',
|
|
||||||
},
|
|
||||||
'ol[type="1"]': {
|
|
||||||
'--list-counter-style': 'decimal',
|
|
||||||
},
|
|
||||||
'ol > li': {
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
'ol > li::before': {
|
|
||||||
content: 'counter(list-item, var(--list-counter-style, decimal)) "."',
|
|
||||||
position: 'absolute',
|
|
||||||
fontWeight: '400',
|
|
||||||
color: 'var(--tw-prose-counters)',
|
|
||||||
},
|
|
||||||
'ul > li': {
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
'ul > li::before': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
backgroundColor: 'var(--tw-prose-bullets)',
|
|
||||||
borderRadius: '50%',
|
|
||||||
},
|
|
||||||
hr: {
|
|
||||||
borderColor: 'var(--tw-prose-hr)',
|
|
||||||
borderTopWidth: 1,
|
|
||||||
},
|
|
||||||
blockquote: {
|
|
||||||
fontWeight: '500',
|
|
||||||
fontStyle: 'italic',
|
|
||||||
color: 'var(--tw-prose-quotes)',
|
|
||||||
borderLeftWidth: '0.25rem',
|
|
||||||
borderLeftColor: 'var(--tw-prose-quote-borders)',
|
|
||||||
quotes: '"\\201C""\\201D""\\2018""\\2019"',
|
|
||||||
},
|
|
||||||
'blockquote p:first-of-type::before': {
|
|
||||||
content: 'open-quote',
|
|
||||||
},
|
|
||||||
'blockquote p:last-of-type::after': {
|
|
||||||
content: 'close-quote',
|
|
||||||
},
|
|
||||||
h1: {
|
|
||||||
color: 'var(--tw-prose-headings)',
|
|
||||||
fontWeight: '800',
|
|
||||||
},
|
|
||||||
'h1 strong': {
|
|
||||||
fontWeight: '900',
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
h2: {
|
|
||||||
color: 'var(--tw-prose-headings)',
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
'h2 strong': {
|
|
||||||
fontWeight: '800',
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
h3: {
|
|
||||||
color: 'var(--tw-prose-headings)',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
'h3 strong': {
|
|
||||||
fontWeight: '700',
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
h4: {
|
|
||||||
color: 'var(--tw-prose-headings)',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
'h4 strong': {
|
|
||||||
fontWeight: '700',
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
img: {
|
|
||||||
marginTop: '2em',
|
|
||||||
marginBottom: '2em',
|
|
||||||
},
|
|
||||||
'figure > *': {
|
|
||||||
marginTop: '0',
|
|
||||||
marginBottom: '0',
|
|
||||||
},
|
|
||||||
figcaption: {
|
|
||||||
color: 'var(--tw-prose-captions)',
|
|
||||||
fontSize: '0.875em',
|
|
||||||
lineHeight: '1.4285714',
|
|
||||||
marginTop: '0.8571429em',
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
color: 'var(--tw-prose-code)',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
'code::before': {
|
|
||||||
content: '"`"',
|
|
||||||
},
|
|
||||||
'code::after': {
|
|
||||||
content: '"`"',
|
|
||||||
},
|
|
||||||
'a code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'h1 code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'h2 code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'h3 code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'h4 code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'blockquote code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'thead th code': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
pre: {
|
|
||||||
color: 'var(--tw-prose-pre-code)',
|
|
||||||
backgroundColor: 'var(--tw-prose-pre-bg)',
|
|
||||||
overflowX: 'auto',
|
|
||||||
fontWeight: '400',
|
|
||||||
},
|
|
||||||
'pre code': {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
borderWidth: '0',
|
|
||||||
borderRadius: '0',
|
|
||||||
padding: '0',
|
|
||||||
fontWeight: 'inherit',
|
|
||||||
color: 'inherit',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
lineHeight: 'inherit',
|
|
||||||
},
|
|
||||||
'pre code::before': {
|
|
||||||
content: 'none',
|
|
||||||
},
|
|
||||||
'pre code::after': {
|
|
||||||
content: 'none',
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
width: '100%',
|
|
||||||
tableLayout: 'auto',
|
|
||||||
textAlign: 'left',
|
|
||||||
marginTop: '2em',
|
|
||||||
marginBottom: '2em',
|
|
||||||
},
|
|
||||||
thead: {
|
|
||||||
borderBottomWidth: '1px',
|
|
||||||
borderBottomColor: 'var(--tw-prose-th-borders)',
|
|
||||||
},
|
|
||||||
'thead th': {
|
|
||||||
color: 'var(--tw-prose-headings)',
|
|
||||||
fontWeight: '600',
|
|
||||||
verticalAlign: 'bottom',
|
|
||||||
},
|
|
||||||
'tbody tr': {
|
|
||||||
borderBottomWidth: '1px',
|
|
||||||
borderBottomColor: 'var(--tw-prose-td-borders)',
|
|
||||||
},
|
|
||||||
'tbody tr:last-child': {
|
|
||||||
borderBottomWidth: '0',
|
|
||||||
},
|
|
||||||
'tbody td': {
|
|
||||||
verticalAlign: 'baseline',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
plugins: [],
|
||||||
},
|
};
|
||||||
plugins: [
|
|
||||||
require('@tailwindcss/typography'),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user