feat: implementa store global de métricas e corrige processamento de dados

- Adiciona store global usando Zustand para gerenciamento de métricas
- Implementa funções específicas para atualização de métricas
- Corrige processamento de métricas semanais
- Melhora manipulação de estados e performance
- Resolve problema de dados vazios nos gráficos
This commit is contained in:
Lucas Santana 2025-02-07 11:04:49 -03:00
parent 190777dcd0
commit 2175458186
6 changed files with 365 additions and 84 deletions

32
package-lock.json generated
View File

@ -56,7 +56,8 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",
"vitest": "^2.1.8"
"vitest": "^2.1.8",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
@ -11042,6 +11043,35 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zustand": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
}
}

View File

@ -63,7 +63,8 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",
"vitest": "^2.1.8"
"vitest": "^2.1.8",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.9.1",

View File

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

View File

@ -38,7 +38,7 @@ interface WritingMetricsChartProps {
className?: string;
}
export function WritingMetricsChart({ data, className = '' }: WritingMetricsChartProps) {
export function WritingMetricsChart({ data = [], className = '' }: WritingMetricsChartProps) {
const [visibleMetrics, setVisibleMetrics] = React.useState<Set<string>>(
new Set(METRICS_CONFIG.map(metric => metric.key))
);
@ -57,6 +57,8 @@ export function WritingMetricsChart({ data, className = '' }: WritingMetricsChar
};
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;
@ -64,13 +66,15 @@ export function WritingMetricsChart({ data, className = '' }: WritingMetricsChar
cutoffDate.setMonth(cutoffDate.getMonth() - months);
return data.filter(item => {
if (!item?.week) return false;
const [year, week] = item.week.split('-W').map(Number);
if (!year || !week) return false;
const itemDate = new Date(year, 0, 1 + (week - 1) * 7);
return itemDate >= cutoffDate;
});
};
const filteredData = filterDataByTime(data);
const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]);
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-8 ${className}`}>

View File

@ -7,6 +7,7 @@ import { MetricsChart } from '@/components/dashboard/MetricsChart';
import { DashboardMetrics } from '@/components/dashboard/DashboardMetrics';
import { WritingMetricsSection } from '@/components/dashboard/WritingMetricsSection';
import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart';
import { useMetricsStore } from '@/stores/metricsStore';
import type {
DashboardMetrics as DashboardMetricsType,
DashboardWeeklyMetrics,
@ -49,41 +50,64 @@ interface WeeklyData {
minutesRead: number;
}
interface EssayAnalysis {
id: string;
created_at: string;
overall_score: number;
suggestions: string;
essay_analysis_scores: Array<{
adequacy: number;
coherence: number;
cohesion: number;
vocabulary: number;
grammar: number;
}>;
essay_analysis_feedback: Array<{
structure_feedback: string;
content_feedback: string;
language_feedback: string;
}>;
}
interface ProcessedEssayAnalysis {
id: string;
created_at: string;
overall_score: number;
essay_id: string;
scores: {
adequacy: number;
coherence: number;
cohesion: number;
vocabulary: number;
grammar: number;
};
feedback: {
structure_feedback: string;
content_feedback: string;
language_feedback: string;
};
}
export function StudentDashboardPage() {
const navigate = useNavigate();
const [student, setStudent] = React.useState<Student | null>(null);
const [metrics, setMetrics] = React.useState<DashboardMetricsType>({
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 [weeklyMetrics, setWeeklyMetrics] = React.useState<DashboardWeeklyMetrics>({
reading: [],
writing: []
});
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [recentStories, setRecentStories] = React.useState<Story[]>([]);
const {
metrics,
weeklyMetrics,
loading,
error,
setMetrics,
setWeeklyMetrics,
updateReadingMetrics,
updateWritingMetrics,
updateWeeklyReadingMetrics,
updateWeeklyWritingMetrics,
setLoading,
setError,
resetMetrics
} = useMetricsStore();
const processWeeklyMetrics = (recordings: Recording[]) => {
const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => {
@ -132,9 +156,54 @@ export function StudentDashboardPage() {
.sort((a, b) => a.week.localeCompare(b.week));
};
const processWeeklyWritingMetrics = (analyses: ProcessedEssayAnalysis[]) => {
const weeklyData = analyses.reduce((acc: { [key: string]: any }, analysis) => {
const date = new Date(analysis.created_at);
const week = `${date.getFullYear()}-W${Math.ceil((date.getDate() + date.getDay()) / 7)}`;
if (!acc[week]) {
acc[week] = {
count: 0,
score: 0,
adequacy: 0,
coherence: 0,
cohesion: 0,
vocabulary: 0,
grammar: 0,
minutesWriting: 0
};
}
acc[week].count += 1;
acc[week].score += analysis.overall_score;
acc[week].adequacy += analysis.scores.adequacy;
acc[week].coherence += analysis.scores.coherence;
acc[week].cohesion += analysis.scores.cohesion;
acc[week].vocabulary += analysis.scores.vocabulary;
acc[week].grammar += analysis.scores.grammar;
acc[week].minutesWriting += 30; // Tempo médio estimado por redação
return acc;
}, {});
return Object.entries(weeklyData)
.map(([week, data]: [string, any]) => ({
week,
score: Math.round(data.score / data.count),
adequacy: Math.round(data.adequacy / data.count),
coherence: Math.round(data.coherence / data.count),
cohesion: Math.round(data.cohesion / data.count),
vocabulary: Math.round(data.vocabulary / data.count),
grammar: Math.round(data.grammar / data.count),
minutesWriting: data.minutesWriting
}))
.sort((a, b) => a.week.localeCompare(b.week));
};
React.useEffect(() => {
const fetchDashboardData = async () => {
try {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) return;
@ -175,10 +244,8 @@ export function StudentDashboardPage() {
// Processar métricas semanais
const weeklyData = processWeeklyMetrics(recordings);
setWeeklyMetrics({
reading: weeklyData,
writing: []
});
// Atualizar métricas semanais de leitura
updateWeeklyReadingMetrics(weeklyData);
// Buscar histórias recentes com a capa definida
const { data: stories, error: storiesError } = await supabase
@ -237,31 +304,108 @@ export function StudentDashboardPage() {
errors: 0
});
// Calcular médias
setMetrics({
reading: {
totalStories: allStoriesData.length,
averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings),
totalReadingTime: recordings.length * 2,
currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)),
averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings),
averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings),
averageComprehension: Math.round(metricsSum.comprehension / totalRecordings),
averageWordsPerMinute: Math.round(metricsSum.wordsPerMinute / totalRecordings),
averagePauses: Math.round(metricsSum.pauses / totalRecordings),
averageErrors: Math.round(metricsSum.errors / totalRecordings)
// Atualizar métricas de leitura
updateReadingMetrics({
totalStories: allStoriesData.length,
averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings),
totalReadingTime: recordings.length * 2,
currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)),
averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings),
averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings),
averageComprehension: Math.round(metricsSum.comprehension / totalRecordings),
averageWordsPerMinute: Math.round(metricsSum.wordsPerMinute / totalRecordings),
averagePauses: Math.round(metricsSum.pauses / totalRecordings),
averageErrors: Math.round(metricsSum.errors / totalRecordings)
});
}
// 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', 'completed');
if (essaysError) throw essaysError;
// Processar métricas semanais de escrita
const analyses = essays?.flatMap(essay =>
essay.essay_analyses?.map(analysis => ({
id: analysis.id,
created_at: analysis.created_at,
overall_score: analysis.overall_score,
essay_id: essay.id,
scores: analysis.essay_analysis_scores?.[0] || {
adequacy: 0,
coherence: 0,
cohesion: 0,
vocabulary: 0,
grammar: 0
},
writing: {
totalEssays: 0,
averageScore: 0,
totalEssaysTime: 0,
currentWritingLevel: 1,
averageAdequacy: 0,
averageCoherence: 0,
averageCohesion: 0,
averageVocabulary: 0,
averageGrammar: 0
feedback: analysis.essay_analysis_feedback?.[0] || {
structure_feedback: '',
content_feedback: '',
language_feedback: ''
}
}))
).filter(Boolean) || [];
const weeklyWritingData = processWeeklyWritingMetrics(analyses);
// Atualizar métricas semanais de escrita
updateWeeklyWritingMetrics(weeklyWritingData);
// 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,
adequacy: acc.adequacy + analysis.scores.adequacy,
coherence: acc.coherence + analysis.scores.coherence,
cohesion: acc.cohesion + analysis.scores.cohesion,
vocabulary: acc.vocabulary + analysis.scores.vocabulary,
grammar: acc.grammar + analysis.scores.grammar
}), {
score: 0,
adequacy: 0,
coherence: 0,
cohesion: 0,
vocabulary: 0,
grammar: 0
});
// Atualizar métricas de escrita
updateWritingMetrics({
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)
});
}
@ -274,6 +418,11 @@ export function StudentDashboardPage() {
};
fetchDashboardData();
// Limpar métricas ao desmontar
return () => {
resetMetrics();
};
}, []);
if (loading) {

106
src/stores/metricsStore.ts Normal file
View File

@ -0,0 +1,106 @@
import { create } from 'zustand';
import type {
DashboardMetrics,
DashboardWeeklyMetrics,
ReadingMetrics,
WritingMetrics,
WeeklyReadingMetrics,
WeeklyWritingMetrics
} from '@/types/metrics';
interface MetricsState {
metrics: DashboardMetrics;
weeklyMetrics: DashboardWeeklyMetrics;
loading: boolean;
error: string | null;
setMetrics: (metrics: DashboardMetrics) => void;
setWeeklyMetrics: (weeklyMetrics: DashboardWeeklyMetrics) => void;
updateReadingMetrics: (readingMetrics: ReadingMetrics) => void;
updateWritingMetrics: (writingMetrics: WritingMetrics) => void;
updateWeeklyReadingMetrics: (weeklyReadingMetrics: WeeklyReadingMetrics[]) => void;
updateWeeklyWritingMetrics: (weeklyWritingMetrics: WeeklyWritingMetrics[]) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
resetMetrics: () => void;
}
const initialMetrics: DashboardMetrics = {
reading: {
totalStories: 0,
averageReadingFluency: 0,
totalReadingTime: 0,
currentLevel: 1,
averagePronunciation: 0,
averageAccuracy: 0,
averageComprehension: 0,
averageWordsPerMinute: 0,
averagePauses: 0,
averageErrors: 0
},
writing: {
totalEssays: 0,
averageScore: 0,
totalEssaysTime: 0,
currentWritingLevel: 1,
averageAdequacy: 0,
averageCoherence: 0,
averageCohesion: 0,
averageVocabulary: 0,
averageGrammar: 0
}
};
const initialWeeklyMetrics: DashboardWeeklyMetrics = {
reading: [],
writing: []
};
export const useMetricsStore = create<MetricsState>((set) => ({
metrics: initialMetrics,
weeklyMetrics: initialWeeklyMetrics,
loading: true,
error: null,
setMetrics: (metrics) => set({ metrics }),
setWeeklyMetrics: (weeklyMetrics) => set({ weeklyMetrics }),
updateReadingMetrics: (readingMetrics) => set((state) => ({
metrics: {
...state.metrics,
reading: readingMetrics
}
})),
updateWritingMetrics: (writingMetrics) => set((state) => ({
metrics: {
...state.metrics,
writing: writingMetrics
}
})),
updateWeeklyReadingMetrics: (weeklyReadingMetrics) => set((state) => ({
weeklyMetrics: {
...state.weeklyMetrics,
reading: weeklyReadingMetrics
}
})),
updateWeeklyWritingMetrics: (weeklyWritingMetrics) => set((state) => ({
weeklyMetrics: {
...state.weeklyMetrics,
writing: weeklyWritingMetrics
}
})),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
resetMetrics: () => set({
metrics: initialMetrics,
weeklyMetrics: initialWeeklyMetrics,
loading: false,
error: null
})
}));