mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
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:
parent
190777dcd0
commit
2175458186
32
package-lock.json
generated
32
package-lock.json
generated
@ -56,7 +56,8 @@
|
|||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"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",
|
||||||
@ -11042,6 +11043,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,8 @@
|
|||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"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",
|
||||||
|
|||||||
@ -1,18 +1,7 @@
|
|||||||
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;
|
||||||
@ -44,11 +33,11 @@ const TIME_FILTERS: TimeFilterOption[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface MetricsChartProps {
|
interface MetricsChartProps {
|
||||||
data: WeeklyMetrics[];
|
data: WeeklyReadingMetrics[];
|
||||||
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))
|
||||||
);
|
);
|
||||||
@ -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;
|
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;
|
||||||
@ -74,21 +65,23 @@ 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 = filterDataByTime(data);
|
const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]);
|
||||||
|
|
||||||
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 das Métricas por Semana</h2>
|
<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 ao longo do tempo</p>
|
<p className="text-sm text-gray-500">Acompanhe seu progresso na leitura 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 */}
|
||||||
@ -191,9 +184,7 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
|||||||
accuracy: 'Precisão',
|
accuracy: 'Precisão',
|
||||||
comprehension: 'Compreensão',
|
comprehension: 'Compreensão',
|
||||||
wordsPerMinute: 'Palavras/Min',
|
wordsPerMinute: 'Palavras/Min',
|
||||||
pauses: 'Pausas',
|
minutesRead: 'Minutos Lendo'
|
||||||
errors: 'Erros',
|
|
||||||
minutesRead: 'Minutos Lidos'
|
|
||||||
};
|
};
|
||||||
return [value, metricNames[name] || name];
|
return [value, metricNames[name] || name];
|
||||||
}}
|
}}
|
||||||
@ -233,7 +224,7 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) {
|
|||||||
<Bar
|
<Bar
|
||||||
yAxisId="right"
|
yAxisId="right"
|
||||||
dataKey="minutesRead"
|
dataKey="minutesRead"
|
||||||
name="Minutos Lidos"
|
name="Minutos Lendo"
|
||||||
fill="url(#barGradient)"
|
fill="url(#barGradient)"
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ interface WritingMetricsChartProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WritingMetricsChart({ data, className = '' }: WritingMetricsChartProps) {
|
export function WritingMetricsChart({ data = [], className = '' }: WritingMetricsChartProps) {
|
||||||
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))
|
||||||
);
|
);
|
||||||
@ -57,6 +57,8 @@ export function WritingMetricsChart({ data, className = '' }: WritingMetricsChar
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filterDataByTime = (data: WeeklyWritingMetrics[]): WeeklyWritingMetrics[] => {
|
const filterDataByTime = (data: WeeklyWritingMetrics[]): WeeklyWritingMetrics[] => {
|
||||||
|
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;
|
||||||
@ -64,13 +66,15 @@ export function WritingMetricsChart({ data, className = '' }: WritingMetricsChar
|
|||||||
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 = filterDataByTime(data);
|
const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]);
|
||||||
|
|
||||||
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}`}>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ 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 { WritingMetricsSection } from '@/components/dashboard/WritingMetricsSection';
|
||||||
import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart';
|
import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart';
|
||||||
|
import { useMetricsStore } from '@/stores/metricsStore';
|
||||||
import type {
|
import type {
|
||||||
DashboardMetrics as DashboardMetricsType,
|
DashboardMetrics as DashboardMetricsType,
|
||||||
DashboardWeeklyMetrics,
|
DashboardWeeklyMetrics,
|
||||||
@ -49,41 +50,64 @@ 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<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 [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) => {
|
||||||
@ -132,9 +156,54 @@ 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;
|
||||||
|
|
||||||
@ -175,10 +244,8 @@ export function StudentDashboardPage() {
|
|||||||
|
|
||||||
// Processar métricas semanais
|
// Processar métricas semanais
|
||||||
const weeklyData = processWeeklyMetrics(recordings);
|
const weeklyData = processWeeklyMetrics(recordings);
|
||||||
setWeeklyMetrics({
|
// Atualizar métricas semanais de leitura
|
||||||
reading: weeklyData,
|
updateWeeklyReadingMetrics(weeklyData);
|
||||||
writing: []
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
||||||
@ -237,31 +304,108 @@ export function StudentDashboardPage() {
|
|||||||
errors: 0
|
errors: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calcular médias
|
// Atualizar métricas de leitura
|
||||||
setMetrics({
|
updateReadingMetrics({
|
||||||
reading: {
|
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,
|
currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)),
|
||||||
currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)),
|
averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings),
|
||||||
averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings),
|
averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings),
|
||||||
averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings),
|
averageComprehension: Math.round(metricsSum.comprehension / totalRecordings),
|
||||||
averageComprehension: Math.round(metricsSum.comprehension / totalRecordings),
|
averageWordsPerMinute: Math.round(metricsSum.wordsPerMinute / totalRecordings),
|
||||||
averageWordsPerMinute: Math.round(metricsSum.wordsPerMinute / totalRecordings),
|
averagePauses: Math.round(metricsSum.pauses / totalRecordings),
|
||||||
averagePauses: Math.round(metricsSum.pauses / totalRecordings),
|
averageErrors: Math.round(metricsSum.errors / 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: {
|
feedback: analysis.essay_analysis_feedback?.[0] || {
|
||||||
totalEssays: 0,
|
structure_feedback: '',
|
||||||
averageScore: 0,
|
content_feedback: '',
|
||||||
totalEssaysTime: 0,
|
language_feedback: ''
|
||||||
currentWritingLevel: 1,
|
|
||||||
averageAdequacy: 0,
|
|
||||||
averageCoherence: 0,
|
|
||||||
averageCohesion: 0,
|
|
||||||
averageVocabulary: 0,
|
|
||||||
averageGrammar: 0
|
|
||||||
}
|
}
|
||||||
|
}))
|
||||||
|
).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();
|
fetchDashboardData();
|
||||||
|
|
||||||
|
// Limpar métricas ao desmontar
|
||||||
|
return () => {
|
||||||
|
resetMetrics();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
106
src/stores/metricsStore.ts
Normal file
106
src/stores/metricsStore.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import type {
|
||||||
|
DashboardMetrics,
|
||||||
|
DashboardWeeklyMetrics,
|
||||||
|
ReadingMetrics,
|
||||||
|
WritingMetrics,
|
||||||
|
WeeklyReadingMetrics,
|
||||||
|
WeeklyWritingMetrics
|
||||||
|
} from '@/types/metrics';
|
||||||
|
|
||||||
|
interface MetricsState {
|
||||||
|
metrics: DashboardMetrics;
|
||||||
|
weeklyMetrics: DashboardWeeklyMetrics;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
setMetrics: (metrics: DashboardMetrics) => void;
|
||||||
|
setWeeklyMetrics: (weeklyMetrics: DashboardWeeklyMetrics) => void;
|
||||||
|
updateReadingMetrics: (readingMetrics: ReadingMetrics) => void;
|
||||||
|
updateWritingMetrics: (writingMetrics: WritingMetrics) => void;
|
||||||
|
updateWeeklyReadingMetrics: (weeklyReadingMetrics: WeeklyReadingMetrics[]) => void;
|
||||||
|
updateWeeklyWritingMetrics: (weeklyWritingMetrics: WeeklyWritingMetrics[]) => void;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
resetMetrics: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialMetrics: DashboardMetrics = {
|
||||||
|
reading: {
|
||||||
|
totalStories: 0,
|
||||||
|
averageReadingFluency: 0,
|
||||||
|
totalReadingTime: 0,
|
||||||
|
currentLevel: 1,
|
||||||
|
averagePronunciation: 0,
|
||||||
|
averageAccuracy: 0,
|
||||||
|
averageComprehension: 0,
|
||||||
|
averageWordsPerMinute: 0,
|
||||||
|
averagePauses: 0,
|
||||||
|
averageErrors: 0
|
||||||
|
},
|
||||||
|
writing: {
|
||||||
|
totalEssays: 0,
|
||||||
|
averageScore: 0,
|
||||||
|
totalEssaysTime: 0,
|
||||||
|
currentWritingLevel: 1,
|
||||||
|
averageAdequacy: 0,
|
||||||
|
averageCoherence: 0,
|
||||||
|
averageCohesion: 0,
|
||||||
|
averageVocabulary: 0,
|
||||||
|
averageGrammar: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialWeeklyMetrics: DashboardWeeklyMetrics = {
|
||||||
|
reading: [],
|
||||||
|
writing: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMetricsStore = create<MetricsState>((set) => ({
|
||||||
|
metrics: initialMetrics,
|
||||||
|
weeklyMetrics: initialWeeklyMetrics,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
setMetrics: (metrics) => set({ metrics }),
|
||||||
|
|
||||||
|
setWeeklyMetrics: (weeklyMetrics) => set({ weeklyMetrics }),
|
||||||
|
|
||||||
|
updateReadingMetrics: (readingMetrics) => set((state) => ({
|
||||||
|
metrics: {
|
||||||
|
...state.metrics,
|
||||||
|
reading: readingMetrics
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
|
updateWritingMetrics: (writingMetrics) => set((state) => ({
|
||||||
|
metrics: {
|
||||||
|
...state.metrics,
|
||||||
|
writing: writingMetrics
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
|
updateWeeklyReadingMetrics: (weeklyReadingMetrics) => set((state) => ({
|
||||||
|
weeklyMetrics: {
|
||||||
|
...state.weeklyMetrics,
|
||||||
|
reading: weeklyReadingMetrics
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
|
updateWeeklyWritingMetrics: (weeklyWritingMetrics) => set((state) => ({
|
||||||
|
weeklyMetrics: {
|
||||||
|
...state.weeklyMetrics,
|
||||||
|
writing: weeklyWritingMetrics
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
|
setLoading: (loading) => set({ loading }),
|
||||||
|
|
||||||
|
setError: (error) => set({ error }),
|
||||||
|
|
||||||
|
resetMetrics: () => set({
|
||||||
|
metrics: initialMetrics,
|
||||||
|
weeklyMetrics: initialWeeklyMetrics,
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
}));
|
||||||
Loading…
Reference in New Issue
Block a user