mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-19 14:57:51 +00:00
Compare commits
No commits in common. "f1f2906d09d2f4fe223fc5a645255337e578ede3" and "198cad0047936d5c321028afe21c61e0ce1f1c2c" have entirely different histories.
f1f2906d09
...
198cad0047
@ -50,7 +50,6 @@ export function AudioPlayer({ word, disabled }: AudioPlayerProps) {
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
onClick={playAudio}
|
onClick={playAudio}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
trackingId="audio-player-toggle"
|
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
|||||||
@ -2,20 +2,19 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Clock, Star, Timer } from "lucide-react";
|
import { Clock, Star } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import type { PhonicsExercise, PhonicsProgress } from "@/types/phonics";
|
||||||
import type { PhonicsExercise, StudentPhonicsProgress } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface ExerciseCardProps {
|
interface ExerciseCardProps {
|
||||||
exercise: PhonicsExercise;
|
exercise: PhonicsExercise;
|
||||||
progress?: StudentPhonicsProgress;
|
progress?: PhonicsProgress;
|
||||||
onStart: (exerciseId: string) => void;
|
onStart: (exerciseId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps) {
|
export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps) {
|
||||||
const isCompleted = progress?.completed;
|
const isCompleted = progress?.completed;
|
||||||
const stars = progress?.stars || 0;
|
const stars = progress?.stars || 0;
|
||||||
const progressValue = progress ? (progress.best_score * 100) : 0;
|
const progressValue = progress ? (progress.bestScore * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full hover:shadow-lg transition-shadow">
|
<Card className="w-full hover:shadow-lg transition-shadow">
|
||||||
@ -35,7 +34,7 @@ export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps)
|
|||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
<span>{Math.ceil((exercise.estimated_time_seconds ?? 0) / 60)} min</span>
|
<span>{Math.ceil(exercise.estimatedTimeSeconds / 60)} min</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
@ -58,10 +57,9 @@ export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps)
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mt-4"
|
className="w-full"
|
||||||
onClick={() => onStart(exercise.id)}
|
onClick={() => onStart(exercise.id)}
|
||||||
variant={isCompleted ? "secondary" : "default"}
|
variant={isCompleted ? "secondary" : "default"}
|
||||||
trackingId="exercise-card-start"
|
|
||||||
>
|
>
|
||||||
{isCompleted ? "Praticar Novamente" : "Começar"}
|
{isCompleted ? "Praticar Novamente" : "Começar"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export function ExerciseGrid({ categoryId, studentId, onSelectExercise }: Exerci
|
|||||||
<ExerciseCard
|
<ExerciseCard
|
||||||
key={exercise.id}
|
key={exercise.id}
|
||||||
exercise={exercise}
|
exercise={exercise}
|
||||||
progress={progress?.find((p) => p.exercise_id === exercise.id)}
|
progress={progress?.find((p) => p.exerciseId === exercise.id)}
|
||||||
onStart={onSelectExercise}
|
onStart={onSelectExercise}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -2,36 +2,35 @@ import { useState, useEffect } from "react";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { useExerciseWords } from "@/hooks/phonics/useExerciseAttempt";
|
||||||
import { useUpdatePhonicsProgress } from "@/hooks/phonics/usePhonicsProgress";
|
import { useUpdatePhonicsProgress } from "@/hooks/phonics/usePhonicsProgress";
|
||||||
import { ExerciseFactory } from "./exercises/ExerciseFactory";
|
import { ExerciseFactory } from "./exercises/ExerciseFactory";
|
||||||
import { Timer } from "lucide-react";
|
import { Timer } from "lucide-react";
|
||||||
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
|
import type { PhonicsExercise, PhonicsWord } from "@/types/phonics";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ExercisePlayerProps {
|
interface ExercisePlayerProps {
|
||||||
exercise: PhonicsExercise;
|
exercise: PhonicsExercise;
|
||||||
student_id: string;
|
studentId: string;
|
||||||
onComplete: (result: {
|
onComplete: () => void;
|
||||||
score: number;
|
|
||||||
stars: number;
|
|
||||||
xp_earned: number;
|
|
||||||
completed: boolean;
|
|
||||||
}) => void;
|
|
||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExercisePlayer({
|
export function ExercisePlayer({
|
||||||
exercise,
|
exercise,
|
||||||
student_id,
|
studentId,
|
||||||
onComplete,
|
onComplete,
|
||||||
onExit
|
onExit
|
||||||
}: ExercisePlayerProps) {
|
}: ExercisePlayerProps) {
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [timeSpent, setTimeSpent] = useState(0);
|
const [timeSpent, setTimeSpent] = useState(0);
|
||||||
|
const [answers, setAnswers] = useState<string[]>([]);
|
||||||
|
const [mistakes, setMistakes] = useState<string[]>([]);
|
||||||
const [showFeedback, setShowFeedback] = useState(false);
|
const [showFeedback, setShowFeedback] = useState(false);
|
||||||
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
|
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
const { data: exerciseWords, isLoading } = useExerciseWords(exercise.id);
|
||||||
const updateProgress = useUpdatePhonicsProgress();
|
const updateProgress = useUpdatePhonicsProgress();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -48,16 +47,16 @@ export function ExercisePlayer({
|
|||||||
|
|
||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
setScore((prev) => prev + 1);
|
setScore((prev) => prev + 1);
|
||||||
|
setAnswers((prev) => [...prev, word]);
|
||||||
|
} else {
|
||||||
|
setMistakes((prev) => [...prev, word]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aguardar feedback antes de prosseguir
|
// Aguardar feedback antes de prosseguir
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
|
|
||||||
// Filtra apenas as palavras corretas
|
if (currentStep < (exerciseWords?.length || 0) - 1) {
|
||||||
const correctWords = exercise.words?.filter(w => w.is_correct_answer) || [];
|
|
||||||
|
|
||||||
if (currentStep < correctWords.length - 1) {
|
|
||||||
setCurrentStep((prev) => prev + 1);
|
setCurrentStep((prev) => prev + 1);
|
||||||
} else {
|
} else {
|
||||||
handleComplete();
|
handleComplete();
|
||||||
@ -65,36 +64,24 @@ export function ExercisePlayer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
// Filtra apenas as palavras corretas
|
const finalScore = score / (exerciseWords?.length || 1);
|
||||||
const correctWords = exercise.words?.filter(w => w.is_correct_answer) || [];
|
|
||||||
const finalScore = score / correctWords.length;
|
|
||||||
const stars = Math.ceil(finalScore * 3);
|
const stars = Math.ceil(finalScore * 3);
|
||||||
const xp_earned = Math.round(finalScore * exercise.points);
|
|
||||||
const completed = finalScore >= exercise.required_score;
|
|
||||||
|
|
||||||
const updateParams: UpdateProgressParams = {
|
await updateProgress.mutateAsync({
|
||||||
student_id,
|
studentId,
|
||||||
exercise_id: exercise.id,
|
exerciseId: exercise.id,
|
||||||
best_score: finalScore,
|
attempts: 1,
|
||||||
last_score: finalScore,
|
bestScore: finalScore,
|
||||||
completed,
|
lastScore: finalScore,
|
||||||
|
completed: finalScore >= exercise.requiredScore,
|
||||||
stars,
|
stars,
|
||||||
xp_earned,
|
xpEarned: Math.round(finalScore * exercise.points)
|
||||||
time_spent_seconds: timeSpent,
|
|
||||||
correct_answers_count: score,
|
|
||||||
total_answers_count: correctWords.length
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateProgress.mutateAsync(updateParams);
|
|
||||||
onComplete({
|
|
||||||
score: finalScore,
|
|
||||||
stars,
|
|
||||||
xp_earned,
|
|
||||||
completed
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onComplete();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!exercise.words?.length) {
|
if (isLoading || !exerciseWords?.length) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl mx-auto">
|
<Card className="w-full max-w-2xl mx-auto">
|
||||||
<CardContent className="py-8">
|
<CardContent className="py-8">
|
||||||
@ -106,21 +93,8 @@ export function ExercisePlayer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtra apenas as palavras corretas e ordena por order_index
|
const progress = ((currentStep + 1) / exerciseWords.length) * 100;
|
||||||
const correctWords = exercise.words
|
const currentWord = exerciseWords[currentStep].word as unknown as PhonicsWord;
|
||||||
.filter(w => w.is_correct_answer)
|
|
||||||
.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
|
||||||
|
|
||||||
// Pega a palavra atual
|
|
||||||
const currentWord = correctWords[currentStep];
|
|
||||||
|
|
||||||
// Pega as opções (incluindo a palavra correta)
|
|
||||||
const options = exercise.words
|
|
||||||
.filter(w => w.order_index === currentWord.order_index)
|
|
||||||
.map(w => w.word)
|
|
||||||
.sort(() => Math.random() - 0.5);
|
|
||||||
|
|
||||||
const progress = ((currentStep + 1) / correctWords.length) * 100;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn(
|
<Card className={cn(
|
||||||
@ -136,8 +110,8 @@ export function ExercisePlayer({
|
|||||||
<Timer className="w-4 h-4" />
|
<Timer className="w-4 h-4" />
|
||||||
<span>{Math.floor(timeSpent / 60)}:{(timeSpent % 60).toString().padStart(2, '0')}</span>
|
<span>{Math.floor(timeSpent / 60)}:{(timeSpent % 60).toString().padStart(2, '0')}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={onExit} trackingId="exercise-player-exit">
|
<Button variant="outline" size="sm" onClick={onExit}>
|
||||||
Sair do Exercício
|
Sair
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -147,13 +121,13 @@ export function ExercisePlayer({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center text-muted-foreground mb-8">
|
<div className="text-center text-muted-foreground mb-8">
|
||||||
Exercício {currentStep + 1} de {correctWords.length}
|
Exercício {currentStep + 1} de {exerciseWords.length}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ExerciseFactory
|
<ExerciseFactory
|
||||||
type_id={exercise.type_id}
|
type={exercise.exerciseType}
|
||||||
currentWord={currentWord.word}
|
currentWord={currentWord}
|
||||||
options={options}
|
options={exerciseWords[currentStep].options}
|
||||||
onAnswer={handleAnswer}
|
onAnswer={handleAnswer}
|
||||||
disabled={showFeedback}
|
disabled={showFeedback}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,41 +1,47 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { BaseExercise, type BaseExerciseProps } from "./BaseExercise";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { PhonicsWord } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface AlliterationExerciseProps {
|
interface AlliterationExerciseProps extends BaseExerciseProps {
|
||||||
currentWord: PhonicsWord;
|
options: Array<{
|
||||||
options: PhonicsWord[];
|
word: string;
|
||||||
onAnswer: (word: string, isCorrect: boolean) => void;
|
hasSameInitialSound: boolean;
|
||||||
disabled?: boolean;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AlliterationExercise({ currentWord, options, onAnswer, disabled }: AlliterationExerciseProps) {
|
export function AlliterationExercise({
|
||||||
|
currentWord,
|
||||||
|
onAnswer,
|
||||||
|
options,
|
||||||
|
disabled
|
||||||
|
}: AlliterationExerciseProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div className="text-center">
|
<BaseExercise currentWord={currentWord} onAnswer={onAnswer} disabled={disabled} />
|
||||||
<h3 className="text-lg font-medium mb-2">Qual palavra começa com o mesmo som?</h3>
|
|
||||||
<div className="text-4xl font-bold">{currentWord.word}</div>
|
<div className="space-y-4">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
Qual palavra começa com o mesmo som que <span className="font-medium text-foreground">{currentWord.word}</span>?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<Button
|
<Button
|
||||||
key={option.id}
|
key={option.word}
|
||||||
size="lg"
|
onClick={() => onAnswer(option.word, option.hasSameInitialSound)}
|
||||||
|
disabled={disabled}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto py-6 text-xl font-medium",
|
"h-16 text-lg",
|
||||||
disabled && "opacity-50 cursor-not-allowed"
|
disabled && option.hasSameInitialSound && "border-green-500 bg-green-50",
|
||||||
|
disabled && !option.hasSameInitialSound && "border-red-500 bg-red-50"
|
||||||
)}
|
)}
|
||||||
onClick={() => onAnswer(option.word, option.id === currentWord.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
trackingId={`alliteration-option-${option.word}`}
|
|
||||||
>
|
>
|
||||||
{option.word}
|
{option.word}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,21 +1,20 @@
|
|||||||
|
import type { PhonicsExerciseType, PhonicsWord } from "@/types/phonics";
|
||||||
import { RhymeExercise } from "./RhymeExercise";
|
import { RhymeExercise } from "./RhymeExercise";
|
||||||
import { AlliterationExercise } from "./AlliterationExercise";
|
import { AlliterationExercise } from "./AlliterationExercise";
|
||||||
import { SyllablesExercise } from "./SyllablesExercise";
|
import { SyllablesExercise } from "./SyllablesExercise";
|
||||||
import { InitialSoundExercise } from "./InitialSoundExercise";
|
import { SoundMatchExercise } from "./SoundMatchExercise";
|
||||||
import { FinalSoundExercise } from "./FinalSoundExercise";
|
|
||||||
import type { PhonicsWord } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface ExerciseFactoryProps {
|
interface ExerciseFactoryProps {
|
||||||
type_id: string;
|
type: PhonicsExerciseType;
|
||||||
currentWord: PhonicsWord;
|
currentWord: PhonicsWord;
|
||||||
options: PhonicsWord[];
|
options: any; // Tipo específico para cada exercício
|
||||||
onAnswer: (word: string, isCorrect: boolean) => void;
|
onAnswer: (word: string, isCorrect: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExerciseFactory({ type_id, currentWord, options, onAnswer, disabled }: ExerciseFactoryProps) {
|
export function ExerciseFactory({ type, currentWord, options, onAnswer, disabled }: ExerciseFactoryProps) {
|
||||||
switch (type_id) {
|
switch (type) {
|
||||||
case '1': // Rima
|
case 'rhyme':
|
||||||
return (
|
return (
|
||||||
<RhymeExercise
|
<RhymeExercise
|
||||||
currentWord={currentWord}
|
currentWord={currentWord}
|
||||||
@ -24,7 +23,8 @@ export function ExerciseFactory({ type_id, currentWord, options, onAnswer, disab
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '2': // Aliteração
|
|
||||||
|
case 'alliteration':
|
||||||
return (
|
return (
|
||||||
<AlliterationExercise
|
<AlliterationExercise
|
||||||
currentWord={currentWord}
|
currentWord={currentWord}
|
||||||
@ -33,34 +33,45 @@ export function ExerciseFactory({ type_id, currentWord, options, onAnswer, disab
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '3': // Sílabas
|
|
||||||
|
case 'syllables':
|
||||||
return (
|
return (
|
||||||
<SyllablesExercise
|
<SyllablesExercise
|
||||||
currentWord={currentWord}
|
currentWord={currentWord}
|
||||||
options={options}
|
syllables={options.syllables}
|
||||||
|
correctOrder={options.correctOrder}
|
||||||
onAnswer={onAnswer}
|
onAnswer={onAnswer}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '4': // Som Inicial
|
|
||||||
|
case 'initial_sound':
|
||||||
return (
|
return (
|
||||||
<InitialSoundExercise
|
<SoundMatchExercise
|
||||||
currentWord={currentWord}
|
currentWord={currentWord}
|
||||||
|
type="initial"
|
||||||
options={options}
|
options={options}
|
||||||
onAnswer={onAnswer}
|
onAnswer={onAnswer}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '5': // Som Final
|
|
||||||
|
case 'final_sound':
|
||||||
return (
|
return (
|
||||||
<FinalSoundExercise
|
<SoundMatchExercise
|
||||||
currentWord={currentWord}
|
currentWord={currentWord}
|
||||||
|
type="final"
|
||||||
options={options}
|
options={options}
|
||||||
onAnswer={onAnswer}
|
onAnswer={onAnswer}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return (
|
||||||
|
<div className="text-center text-red-500">
|
||||||
|
Tipo de exercício não implementado: {type}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { PhonicsWord } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface FinalSoundExerciseProps {
|
|
||||||
currentWord: PhonicsWord;
|
|
||||||
options: PhonicsWord[];
|
|
||||||
onAnswer: (word: string, isCorrect: boolean) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FinalSoundExercise({ currentWord, options, onAnswer, disabled }: FinalSoundExerciseProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-lg font-medium mb-2">Qual palavra termina com o mesmo som?</h3>
|
|
||||||
<div className="text-4xl font-bold">{currentWord.word}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{options.map((option) => (
|
|
||||||
<Button
|
|
||||||
key={option.id}
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"h-auto py-6 text-xl font-medium",
|
|
||||||
disabled && "opacity-50 cursor-not-allowed"
|
|
||||||
)}
|
|
||||||
onClick={() => onAnswer(option.word, option.id === currentWord.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
trackingId={`final-sound-option-${option.word}`}
|
|
||||||
>
|
|
||||||
{option.word}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { PhonicsWord } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface InitialSoundExerciseProps {
|
|
||||||
currentWord: PhonicsWord;
|
|
||||||
options: PhonicsWord[];
|
|
||||||
onAnswer: (word: string, isCorrect: boolean) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InitialSoundExercise({ currentWord, options, onAnswer, disabled }: InitialSoundExerciseProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-lg font-medium mb-2">Qual palavra começa com o mesmo som?</h3>
|
|
||||||
<div className="text-4xl font-bold">{currentWord.word}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{options.map((option) => (
|
|
||||||
<Button
|
|
||||||
key={option.id}
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"h-auto py-6 text-xl font-medium",
|
|
||||||
disabled && "opacity-50 cursor-not-allowed"
|
|
||||||
)}
|
|
||||||
onClick={() => onAnswer(option.word, option.id === currentWord.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
trackingId={`initial-sound-option-${option.word}`}
|
|
||||||
>
|
|
||||||
{option.word}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,41 +1,42 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { BaseExercise, type BaseExerciseProps } from "./BaseExercise";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { PhonicsWord } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface RhymeExerciseProps {
|
interface RhymeExerciseProps extends BaseExerciseProps {
|
||||||
currentWord: PhonicsWord;
|
options: Array<{
|
||||||
options: PhonicsWord[];
|
word: string;
|
||||||
onAnswer: (word: string, isCorrect: boolean) => void;
|
isRhyme: boolean;
|
||||||
disabled?: boolean;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RhymeExercise({ currentWord, options, onAnswer, disabled }: RhymeExerciseProps) {
|
export function RhymeExercise({ currentWord, onAnswer, options, disabled }: RhymeExerciseProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div className="text-center">
|
<BaseExercise currentWord={currentWord} onAnswer={onAnswer} disabled={disabled} />
|
||||||
<h3 className="text-lg font-medium mb-2">Qual palavra rima com:</h3>
|
|
||||||
<div className="text-4xl font-bold">{currentWord.word}</div>
|
<div className="space-y-4">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
Qual palavra rima com <span className="font-medium text-foreground">{currentWord.word}</span>?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<Button
|
<Button
|
||||||
key={option.id}
|
key={option.word}
|
||||||
size="lg"
|
onClick={() => onAnswer(option.word, option.isRhyme)}
|
||||||
|
disabled={disabled}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto py-6 text-xl font-medium",
|
"h-16 text-lg",
|
||||||
disabled && "opacity-50 cursor-not-allowed"
|
disabled && option.isRhyme && "border-green-500 bg-green-50",
|
||||||
|
disabled && !option.isRhyme && "border-red-500 bg-red-50"
|
||||||
)}
|
)}
|
||||||
onClick={() => onAnswer(option.word, option.id === currentWord.id)}
|
|
||||||
disabled={disabled}
|
|
||||||
trackingId={`rhyme-option-${option.word}`}
|
|
||||||
>
|
>
|
||||||
{option.word}
|
{option.word}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -42,7 +42,6 @@ export function SoundMatchExercise({
|
|||||||
disabled && option.hasMatchingSound && "border-green-500 bg-green-50",
|
disabled && option.hasMatchingSound && "border-green-500 bg-green-50",
|
||||||
disabled && !option.hasMatchingSound && "border-red-500 bg-red-50"
|
disabled && !option.hasMatchingSound && "border-red-500 bg-red-50"
|
||||||
)}
|
)}
|
||||||
trackingId={`sound-match-option-${option.word}`}
|
|
||||||
>
|
>
|
||||||
{option.word}
|
{option.word}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,41 +1,81 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { BaseExercise, type BaseExerciseProps } from "./BaseExercise";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { PhonicsWord } from "@/types/phonics";
|
|
||||||
|
|
||||||
interface SyllablesExerciseProps {
|
interface SyllablesExerciseProps extends BaseExerciseProps {
|
||||||
currentWord: PhonicsWord;
|
syllables: string[];
|
||||||
options: PhonicsWord[];
|
correctOrder: number[];
|
||||||
onAnswer: (word: string, isCorrect: boolean) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SyllablesExercise({ currentWord, options, onAnswer, disabled }: SyllablesExerciseProps) {
|
export function SyllablesExercise({
|
||||||
|
currentWord,
|
||||||
|
onAnswer,
|
||||||
|
syllables,
|
||||||
|
correctOrder,
|
||||||
|
disabled
|
||||||
|
}: SyllablesExerciseProps) {
|
||||||
|
const [selectedSyllables, setSelectedSyllables] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const handleSyllableClick = (index: number) => {
|
||||||
|
if (selectedSyllables.includes(index)) {
|
||||||
|
setSelectedSyllables(prev => prev.filter(i => i !== index));
|
||||||
|
} else {
|
||||||
|
setSelectedSyllables(prev => [...prev, index]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheck = () => {
|
||||||
|
const isCorrect = selectedSyllables.every(
|
||||||
|
(syllableIndex, position) => syllableIndex === correctOrder[position]
|
||||||
|
);
|
||||||
|
onAnswer(currentWord.word, isCorrect);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div className="text-center">
|
<BaseExercise currentWord={currentWord} onAnswer={onAnswer} disabled={disabled} />
|
||||||
<h3 className="text-lg font-medium mb-2">Quantas sílabas tem a palavra?</h3>
|
|
||||||
<div className="text-4xl font-bold">{currentWord.word}</div>
|
<div className="space-y-4">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
Selecione as sílabas na ordem correta
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
{options.map((option) => (
|
{syllables.map((syllable, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={option.id}
|
key={index}
|
||||||
size="lg"
|
onClick={() => handleSyllableClick(index)}
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"h-auto py-6 text-xl font-medium",
|
|
||||||
disabled && "opacity-50 cursor-not-allowed"
|
|
||||||
)}
|
|
||||||
onClick={() => onAnswer(option.word, option.syllables_count === currentWord.syllables_count)}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
trackingId={`syllables-option-${option.syllables_count}`}
|
variant={selectedSyllables.includes(index) ? "default" : "outline"}
|
||||||
|
className={cn(
|
||||||
|
"text-lg px-4",
|
||||||
|
disabled && correctOrder.indexOf(index) === selectedSyllables.indexOf(index) && "border-green-500 bg-green-50",
|
||||||
|
disabled && correctOrder.indexOf(index) !== selectedSyllables.indexOf(index) && "border-red-500 bg-red-50"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{option.syllables_count} sílabas
|
{syllable}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedSyllables.length > 0 && !disabled && (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleCheck}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
Verificar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedSyllables.length > 0 && (
|
||||||
|
<div className="text-center text-xl font-medium">
|
||||||
|
{syllables.filter((_, i) => selectedSyllables.includes(i)).join("-")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -130,26 +130,6 @@ export function StoryGenerator() {
|
|||||||
|
|
||||||
if (storyError) throw storyError;
|
if (storyError) throw storyError;
|
||||||
|
|
||||||
// Tracking da criação da história
|
|
||||||
const selectedTheme = themes?.find(t => t.id === choices.theme_id)?.title || '';
|
|
||||||
const selectedSubject = subjects?.find(s => s.id === choices.subject_id)?.title || '';
|
|
||||||
const selectedCharacter = characters?.find(c => c.id === choices.character_id)?.title || '';
|
|
||||||
const selectedSetting = settings?.find(s => s.id === choices.setting_id)?.title || '';
|
|
||||||
|
|
||||||
trackStoryGenerated({
|
|
||||||
story_id: story.id,
|
|
||||||
theme: selectedTheme,
|
|
||||||
subject: selectedSubject,
|
|
||||||
character: selectedCharacter,
|
|
||||||
setting: selectedSetting,
|
|
||||||
context: choices.context,
|
|
||||||
generation_time: Date.now() - startTime.current,
|
|
||||||
word_count: 0, // será atualizado após a geração
|
|
||||||
student_id: session.user.id,
|
|
||||||
school_id: session.user.user_metadata?.school_id,
|
|
||||||
class_id: session.user.user_metadata?.class_id
|
|
||||||
});
|
|
||||||
|
|
||||||
setGenerationStatus('generating-images');
|
setGenerationStatus('generating-images');
|
||||||
console.log('Chamando Edge Function com:', story);
|
console.log('Chamando Edge Function com:', story);
|
||||||
|
|
||||||
@ -173,14 +153,21 @@ export function StoryGenerator() {
|
|||||||
|
|
||||||
if (updateError) throw updateError;
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
// Atualizar a contagem de palavras após a geração
|
// Track story generation
|
||||||
|
const selectedTheme = themes?.find(t => t.id === choices.theme_id)?.title || '';
|
||||||
|
const generationTime = Date.now() - startTime.current;
|
||||||
const wordCount = updatedStory.content.pages.reduce((acc: number, page: { text: string }) =>
|
const wordCount = updatedStory.content.pages.reduce((acc: number, page: { text: string }) =>
|
||||||
acc + page.text.split(/\s+/).length, 0);
|
acc + page.text.split(/\s+/).length, 0);
|
||||||
|
|
||||||
await supabase.from('story_metrics').insert({
|
trackStoryGenerated({
|
||||||
story_id: story.id,
|
story_id: story.id,
|
||||||
|
theme: selectedTheme,
|
||||||
|
prompt: JSON.stringify(choices),
|
||||||
|
generation_time: generationTime,
|
||||||
word_count: wordCount,
|
word_count: wordCount,
|
||||||
generation_time: Date.now() - startTime.current
|
student_id: session.user.id,
|
||||||
|
school_id: session.user.user_metadata?.school_id,
|
||||||
|
class_id: session.user.user_metadata?.class_id
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(`/aluno/historias/${story.id}`);
|
navigate(`/aluno/historias/${story.id}`);
|
||||||
|
|||||||
@ -1,23 +1,14 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import type { StudentPhonicsAttempt, PhonicsWord } from '@/types/phonics';
|
import type { PhonicsAttempt } from '@/types/phonics';
|
||||||
|
|
||||||
interface ExerciseWordResponse {
|
|
||||||
word: PhonicsWord;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExerciseWordWithOptions {
|
|
||||||
word: PhonicsWord;
|
|
||||||
options: PhonicsWord[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useExerciseAttempt() {
|
export function useExerciseAttempt() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (attempt: Omit<StudentPhonicsAttempt, 'id'>) => {
|
mutationFn: async (attempt: Omit<PhonicsAttempt, 'id'>) => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('student_phonics_attempts')
|
.from('phonics_student_attempts')
|
||||||
.insert(attempt)
|
.insert(attempt)
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
@ -27,7 +18,7 @@ export function useExerciseAttempt() {
|
|||||||
},
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['phonics-progress', variables.student_id]
|
queryKey: ['phonics-progress', variables.studentId]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -37,55 +28,17 @@ export function useExerciseWords(exerciseId: string) {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['exercise-words', exerciseId],
|
queryKey: ['exercise-words', exerciseId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// Primeiro, busca a palavra correta e suas opções
|
const { data, error } = await supabase
|
||||||
const { data: exerciseWords, error: exerciseWordsError } = await supabase
|
|
||||||
.from('phonics_exercise_words')
|
.from('phonics_exercise_words')
|
||||||
.select('word:phonics_words!inner(*)')
|
.select(`
|
||||||
|
*,
|
||||||
|
word:phonics_words(*)
|
||||||
|
`)
|
||||||
.eq('exercise_id', exerciseId)
|
.eq('exercise_id', exerciseId)
|
||||||
.eq('is_correct_answer', true)
|
|
||||||
.order('order_index', { ascending: true });
|
.order('order_index', { ascending: true });
|
||||||
|
|
||||||
if (exerciseWordsError) throw exerciseWordsError;
|
|
||||||
|
|
||||||
// Para cada palavra correta, busca suas opções
|
|
||||||
const wordsWithOptions = await Promise.all(
|
|
||||||
((exerciseWords as unknown) as ExerciseWordResponse[]).map(async (exerciseWord) => {
|
|
||||||
const { data: options, error: optionsError } = await supabase
|
|
||||||
.from('phonics_exercise_words')
|
|
||||||
.select('word:phonics_words!inner(*)')
|
|
||||||
.eq('exercise_id', exerciseId)
|
|
||||||
.neq('word.id', exerciseWord.word.id)
|
|
||||||
.limit(3);
|
|
||||||
|
|
||||||
if (optionsError) throw optionsError;
|
|
||||||
|
|
||||||
const optionWords = ((options as unknown) as ExerciseWordResponse[]).map(o => o.word);
|
|
||||||
|
|
||||||
return {
|
|
||||||
word: exerciseWord.word,
|
|
||||||
options: [exerciseWord.word, ...optionWords].sort(() => Math.random() - 0.5)
|
|
||||||
} satisfies ExerciseWordWithOptions;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return wordsWithOptions;
|
|
||||||
},
|
|
||||||
enabled: !!exerciseId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useExerciseAttempts(exerciseId: string) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['exercise-attempts', exerciseId],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('student_phonics_attempts')
|
|
||||||
.select('*')
|
|
||||||
.eq('exercise_id', exerciseId);
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data as StudentPhonicsAttempt[];
|
return data;
|
||||||
},
|
}
|
||||||
enabled: !!exerciseId
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -10,16 +10,9 @@ export function usePhonicsExercises(categoryId?: string) {
|
|||||||
.from('phonics_exercises')
|
.from('phonics_exercises')
|
||||||
.select(`
|
.select(`
|
||||||
*,
|
*,
|
||||||
category:phonics_categories(
|
category:phonics_exercise_categories(name),
|
||||||
id,
|
type:phonics_exercise_types(name),
|
||||||
name,
|
words:phonics_exercise_words(
|
||||||
description,
|
|
||||||
level
|
|
||||||
),
|
|
||||||
words:phonics_exercise_words!inner(
|
|
||||||
id,
|
|
||||||
is_correct_answer,
|
|
||||||
order_index,
|
|
||||||
word:phonics_words(*)
|
word:phonics_words(*)
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
@ -43,7 +36,7 @@ export function usePhonicsCategories() {
|
|||||||
queryKey: ['phonics-categories'],
|
queryKey: ['phonics-categories'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('phonics_categories')
|
.from('phonics_exercise_categories')
|
||||||
.select('*')
|
.select('*')
|
||||||
.order('order_index', { ascending: true });
|
.order('order_index', { ascending: true });
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import type { StudentPhonicsProgress, UpdateProgressParams } from '@/types/phonics';
|
import type { StudentPhonicsProgress } from '@/types/phonics';
|
||||||
|
|
||||||
export function usePhonicsProgress(studentId: string) {
|
export function usePhonicsProgress(studentId: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -22,49 +22,57 @@ export function useUpdatePhonicsProgress() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (params: UpdateProgressParams) => {
|
mutationFn: async ({
|
||||||
// Primeiro, busca o progresso atual para calcular os valores acumulados
|
studentId,
|
||||||
const { data: currentProgress } = await supabase
|
exerciseId,
|
||||||
.from('student_phonics_progress')
|
score,
|
||||||
.select('total_time_spent_seconds, correct_answers_count, total_answers_count')
|
timeSpent
|
||||||
.eq('student_id', params.student_id)
|
}: {
|
||||||
.eq('exercise_id', params.exercise_id)
|
studentId: string;
|
||||||
.single();
|
exerciseId: string;
|
||||||
|
score: number;
|
||||||
// Calcula os novos valores acumulados
|
timeSpent: number;
|
||||||
const total_time_spent_seconds = (currentProgress?.total_time_spent_seconds || 0) + params.time_spent_seconds;
|
}) => {
|
||||||
const correct_answers_count = (currentProgress?.correct_answers_count || 0) + params.correct_answers_count;
|
// Primeiro, registra a tentativa
|
||||||
const total_answers_count = (currentProgress?.total_answers_count || 0) + params.total_answers_count;
|
const { data: attemptData, error: attemptError } = await supabase
|
||||||
|
.from('student_phonics_attempts')
|
||||||
// Atualiza o progresso
|
.insert({
|
||||||
const { data, error } = await supabase
|
student_id: studentId,
|
||||||
.from('student_phonics_progress')
|
exercise_id: exerciseId,
|
||||||
.upsert({
|
score,
|
||||||
student_id: params.student_id,
|
time_spent_seconds: timeSpent
|
||||||
exercise_id: params.exercise_id,
|
|
||||||
best_score: params.best_score,
|
|
||||||
last_score: params.last_score,
|
|
||||||
completed: params.completed,
|
|
||||||
completed_at: params.completed ? new Date().toISOString() : null,
|
|
||||||
stars: params.stars,
|
|
||||||
xp_earned: params.xp_earned,
|
|
||||||
attempts: 'COALESCE(attempts, 0) + 1',
|
|
||||||
total_time_spent_seconds,
|
|
||||||
correct_answers_count,
|
|
||||||
total_answers_count,
|
|
||||||
last_attempt_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
}, {
|
|
||||||
onConflict: 'student_id,exercise_id'
|
|
||||||
})
|
})
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
if (attemptError) throw attemptError;
|
||||||
return data as StudentPhonicsProgress;
|
|
||||||
|
// Depois, atualiza ou cria o progresso
|
||||||
|
const { data: progressData, error: progressError } = await supabase
|
||||||
|
.from('student_phonics_progress')
|
||||||
|
.upsert({
|
||||||
|
student_id: studentId,
|
||||||
|
exercise_id: exerciseId,
|
||||||
|
attempts: 1,
|
||||||
|
best_score: score,
|
||||||
|
last_score: score,
|
||||||
|
completed: score >= 0.7,
|
||||||
|
completed_at: score >= 0.7 ? new Date().toISOString() : null,
|
||||||
|
stars: Math.ceil(score * 3),
|
||||||
|
xp_earned: Math.ceil(score * 100)
|
||||||
|
}, {
|
||||||
|
onConflict: 'student_id,exercise_id',
|
||||||
|
ignoreDuplicates: false
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (progressError) throw progressError;
|
||||||
|
|
||||||
|
return progressData;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { student_id }) => {
|
onSuccess: (_, { studentId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['phonics-progress', student_id] });
|
queryClient.invalidateQueries({ queryKey: ['phonics-progress', studentId] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -3,11 +3,7 @@ import { analytics } from '../lib/analytics';
|
|||||||
interface StoryGeneratedProps {
|
interface StoryGeneratedProps {
|
||||||
story_id: string;
|
story_id: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
subject: string;
|
prompt: string;
|
||||||
character: string;
|
|
||||||
setting: string;
|
|
||||||
context?: string;
|
|
||||||
prompt?: string;
|
|
||||||
generation_time: number;
|
generation_time: number;
|
||||||
word_count: number;
|
word_count: number;
|
||||||
student_id: string;
|
student_id: string;
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export function PhonicsPage() {
|
|||||||
return (
|
return (
|
||||||
<ExercisePlayer
|
<ExercisePlayer
|
||||||
exercise={exercise}
|
exercise={exercise}
|
||||||
student_id={user.id}
|
studentId={user.id}
|
||||||
onComplete={handleExerciseComplete}
|
onComplete={handleExerciseComplete}
|
||||||
onExit={() => setSelectedExercise(undefined)}
|
onExit={() => setSelectedExercise(undefined)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export function PhonicsProgressPage() {
|
|||||||
const totalExercises = exercises.length;
|
const totalExercises = exercises.length;
|
||||||
const completedExercises = progress.filter(p => p.completed).length;
|
const completedExercises = progress.filter(p => p.completed).length;
|
||||||
const totalStars = progress.reduce((acc, p) => acc + p.stars, 0);
|
const totalStars = progress.reduce((acc, p) => acc + p.stars, 0);
|
||||||
const totalXP = progress.reduce((acc, p) => acc + p.xp_earned, 0);
|
const totalXP = progress.reduce((acc, p) => acc + p.xpEarned, 0);
|
||||||
const completionRate = (completedExercises / totalExercises) * 100;
|
const completionRate = (completedExercises / totalExercises) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,8 +74,8 @@ export function PhonicsProgressPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{exercises.map((exercise) => {
|
{exercises.map((exercise) => {
|
||||||
const exerciseProgress = progress.find(p => p.exercise_id === exercise.id);
|
const exerciseProgress = progress.find(p => p.exerciseId === exercise.id);
|
||||||
const progressValue = exerciseProgress ? exerciseProgress.best_score * 100 : 0;
|
const progressValue = exerciseProgress ? exerciseProgress.bestScore * 100 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={exercise.id} className="flex items-center gap-4">
|
<div key={exercise.id} className="flex items-center gap-4">
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
// Tipos de Exercícios
|
export interface PhonicsExerciseType {
|
||||||
export type PhonicsExerciseType = 'rhyme' | 'alliteration' | 'syllables' | 'initial_sound' | 'final_sound';
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PhonicsExerciseCategory {
|
export interface PhonicsExerciseCategory {
|
||||||
id: string;
|
id: string;
|
||||||
@ -24,18 +28,6 @@ export interface PhonicsExercise {
|
|||||||
required_score: number;
|
required_score: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
category?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
level: number;
|
|
||||||
};
|
|
||||||
words?: Array<{
|
|
||||||
id: string;
|
|
||||||
is_correct_answer: boolean;
|
|
||||||
order_index: number | null;
|
|
||||||
word: PhonicsWord;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhonicsWord {
|
export interface PhonicsWord {
|
||||||
@ -55,58 +47,6 @@ export interface PhonicsExerciseWord {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudentPhonicsAttemptAnswer {
|
|
||||||
id: string;
|
|
||||||
attempt_id: string;
|
|
||||||
word_id: string;
|
|
||||||
is_correct: boolean;
|
|
||||||
answer_text: string | null;
|
|
||||||
time_taken_seconds?: number;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentPhonicsAttempt {
|
|
||||||
id: string;
|
|
||||||
student_id: string;
|
|
||||||
exercise_id: string;
|
|
||||||
score: number;
|
|
||||||
time_spent_seconds: number | null;
|
|
||||||
answers: StudentPhonicsAttemptAnswer[];
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StudentPhonicsProgress {
|
|
||||||
id: string;
|
|
||||||
student_id: string;
|
|
||||||
exercise_id: string;
|
|
||||||
best_score: number;
|
|
||||||
last_score: number;
|
|
||||||
completed: boolean;
|
|
||||||
completed_at: string | null;
|
|
||||||
stars: number;
|
|
||||||
xp_earned: number;
|
|
||||||
attempts: number;
|
|
||||||
total_time_spent_seconds: number;
|
|
||||||
correct_answers_count: number;
|
|
||||||
total_answers_count: number;
|
|
||||||
last_attempt_at: string | null;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateProgressParams {
|
|
||||||
student_id: string;
|
|
||||||
exercise_id: string;
|
|
||||||
best_score: number;
|
|
||||||
last_score: number;
|
|
||||||
completed: boolean;
|
|
||||||
stars: number;
|
|
||||||
xp_earned: number;
|
|
||||||
time_spent_seconds: number;
|
|
||||||
correct_answers_count: number;
|
|
||||||
total_answers_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MediaType {
|
export interface MediaType {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -124,6 +64,39 @@ export interface PhonicsExerciseMedia {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StudentPhonicsProgress {
|
||||||
|
id: string;
|
||||||
|
student_id: string;
|
||||||
|
exercise_id: string;
|
||||||
|
attempts: number;
|
||||||
|
best_score: number;
|
||||||
|
last_score: number;
|
||||||
|
completed: boolean;
|
||||||
|
completed_at: string | null;
|
||||||
|
stars: number;
|
||||||
|
xp_earned: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StudentPhonicsAttempt {
|
||||||
|
id: string;
|
||||||
|
student_id: string;
|
||||||
|
exercise_id: string;
|
||||||
|
score: number;
|
||||||
|
time_spent_seconds: number | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StudentPhonicsAttemptAnswer {
|
||||||
|
id: string;
|
||||||
|
attempt_id: string;
|
||||||
|
word_id: string;
|
||||||
|
is_correct: boolean;
|
||||||
|
answer_text: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AchievementType {
|
export interface AchievementType {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user