diff --git a/src/components/exercises/PronunciationPractice.tsx b/src/components/exercises/PronunciationPractice.tsx index c679549..2e91b61 100644 --- a/src/components/exercises/PronunciationPractice.tsx +++ b/src/components/exercises/PronunciationPractice.tsx @@ -1,12 +1,16 @@ import React, { useState, useRef } from 'react'; import { supabase } from '../../lib/supabase'; import { Mic, Square, Play, Loader2, ArrowRight } from 'lucide-react'; +import { useStudentTracking } from '../../hooks/useStudentTracking'; interface PronunciationPracticeProps { words: string[]; + storyId: string; + studentId: string; + onComplete?: (score: number) => void; } -export function PronunciationPractice({ words }: PronunciationPracticeProps) { +export function PronunciationPractice({ words, storyId, studentId, onComplete }: PronunciationPracticeProps) { const [currentWordIndex, setCurrentWordIndex] = useState(0); const [isRecording, setIsRecording] = useState(false); const [audioBlob, setAudioBlob] = useState(null); @@ -16,6 +20,10 @@ export function PronunciationPractice({ words }: PronunciationPracticeProps) { const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); + const { trackExerciseCompleted } = useStudentTracking(); + const startTime = useRef(Date.now()); + const wordsAttempted = useRef(0); + const wordsCorrect = useRef(0); // Verificar se há palavras para praticar if (!words.length) { @@ -69,15 +77,41 @@ export function PronunciationPractice({ words }: PronunciationPracticeProps) { const handleNextWord = async () => { setIsSubmitting(true); try { - // Aqui você pode implementar a lógica de análise da pronúncia - // Por enquanto, vamos apenas avançar e dar pontos - setScore(prev => prev + 10); + // Simular análise de pronúncia (você pode substituir por uma análise real) + const wordScore = Math.floor(Math.random() * 30) + 70; // Score entre 70-100 + const newScore = score + wordScore; + setScore(newScore); + wordsAttempted.current += 1; + if (wordScore >= 80) { + wordsCorrect.current += 1; + } + if (currentWordIndex < words.length - 1) { setCurrentWordIndex(prev => prev + 1); setAudioBlob(null); } else { setCompleted(true); + const timeSpent = Date.now() - startTime.current; + + // Track exercise completion + trackExerciseCompleted({ + exercise_id: `${storyId}_pronunciation`, + story_id: storyId, + student_id: studentId, + exercise_type: 'pronunciation', + score: Math.floor(newScore / words.length), + time_spent: timeSpent, + words_attempted: wordsAttempted.current, + words_correct: wordsCorrect.current, + pronunciation_score: Math.floor(newScore / words.length), + fluency_score: 85, // Este valor poderia vir de uma análise real de fluência + difficulty_level: words.length <= 5 ? 'easy' : words.length <= 10 ? 'medium' : 'hard' + }); + + if (onComplete) { + onComplete(Math.floor(newScore / words.length)); + } } } catch (error) { console.error('Erro ao processar áudio:', error); diff --git a/src/components/exercises/SentenceCompletion.tsx b/src/components/exercises/SentenceCompletion.tsx index cabe50a..414b9a3 100644 --- a/src/components/exercises/SentenceCompletion.tsx +++ b/src/components/exercises/SentenceCompletion.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { supabase } from '../../lib/supabase'; import { Loader2 } from 'lucide-react'; +import { useStudentTracking } from '../../hooks/useStudentTracking'; interface SentenceCompletionProps { story: { @@ -11,9 +12,11 @@ interface SentenceCompletionProps { }>; }; }; + studentId: string; + onComplete?: (score: number) => void; } -export function SentenceCompletion({ story }: SentenceCompletionProps) { +export function SentenceCompletion({ story, studentId, onComplete }: SentenceCompletionProps) { const [currentSentence, setCurrentSentence] = useState(0); const [userAnswer, setUserAnswer] = useState(''); const [score, setScore] = useState(0); @@ -23,6 +26,9 @@ export function SentenceCompletion({ story }: SentenceCompletionProps) { answer: string; }>>([]); const [isLoading, setIsLoading] = useState(true); + const { trackExerciseCompleted } = useStudentTracking(); + const startTime = useRef(Date.now()); + const correctAnswers = useRef(0); // Carregar palavras e preparar sentenças React.useEffect(() => { @@ -105,12 +111,31 @@ export function SentenceCompletion({ story }: SentenceCompletionProps) { if (isCorrect) { setScore(prev => prev + 10); + correctAnswers.current += 1; } - // Avançar para próxima sentença + // Avançar para próxima sentença ou finalizar if (currentSentence < exerciseSentences.length - 1) { setCurrentSentence(prev => prev + 1); setUserAnswer(''); + } else { + // Track exercise completion + const timeSpent = Date.now() - startTime.current; + trackExerciseCompleted({ + exercise_id: `${story.id}_completion`, + story_id: story.id, + student_id: studentId, + exercise_type: 'completion', + score: Math.floor((correctAnswers.current / exerciseSentences.length) * 100), + time_spent: timeSpent, + answers_correct: correctAnswers.current, + answers_total: exerciseSentences.length, + difficulty_level: exerciseSentences.length <= 5 ? 'easy' : exerciseSentences.length <= 10 ? 'medium' : 'hard' + }); + + if (onComplete) { + onComplete(Math.floor((correctAnswers.current / exerciseSentences.length) * 100)); + } } } catch (error) { diff --git a/src/components/exercises/WordFormation.tsx b/src/components/exercises/WordFormation.tsx index 30364e7..394e91a 100644 --- a/src/components/exercises/WordFormation.tsx +++ b/src/components/exercises/WordFormation.tsx @@ -1,9 +1,12 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { supabase } from '../../lib/supabase'; +import { useStudentTracking } from '../../hooks/useStudentTracking'; interface WordFormationProps { words: string[]; storyId: string; + studentId: string; + onComplete?: (score: number) => void; } interface SyllableWord { @@ -11,7 +14,7 @@ interface SyllableWord { syllables: string[]; } -export function WordFormation({ words, storyId }: WordFormationProps) { +export function WordFormation({ words, storyId, studentId, onComplete }: WordFormationProps) { const [availableSyllables, setAvailableSyllables] = useState([]); const [userWord, setUserWord] = useState(''); const [score, setScore] = useState(0); @@ -22,6 +25,9 @@ export function WordFormation({ words, storyId }: WordFormationProps) { type: 'success' | 'error'; message: string; } | null>(null); + const { trackExerciseCompleted } = useStudentTracking(); + const startTime = useRef(Date.now()); + const correctAnswers = useRef(0); useEffect(() => { const loadWords = async () => { @@ -105,11 +111,32 @@ export function WordFormation({ words, storyId }: WordFormationProps) { if (matchedWord && !completedWords.includes(matchedWord.word)) { setScore(prev => prev + 10); + correctAnswers.current += 1; setCompletedWords(prev => [...prev, matchedWord.word]); setShowFeedback({ type: 'success', message: 'Parabéns! Palavra correta!' }); + + // Se completou todas as palavras + if (completedWords.length + 1 === targetWords.length) { + const timeSpent = Date.now() - startTime.current; + trackExerciseCompleted({ + exercise_id: `${storyId}_word_formation`, + story_id: storyId, + student_id: studentId, + exercise_type: 'word_formation', + score: score + 10, + time_spent: timeSpent, + words_formed: correctAnswers.current, + words_correct: correctAnswers.current, + difficulty_level: targetWords.length <= 5 ? 'easy' : targetWords.length <= 10 ? 'medium' : 'hard' + }); + + if (onComplete) { + onComplete(score + 10); + } + } } else if (completedWords.includes(matchedWord?.word || '')) { setShowFeedback({ type: 'error', diff --git a/src/components/story/AudioRecorder.tsx b/src/components/story/AudioRecorder.tsx index 36facd6..4d229e8 100644 --- a/src/components/story/AudioRecorder.tsx +++ b/src/components/story/AudioRecorder.tsx @@ -2,6 +2,7 @@ import React, { useState, useRef } from 'react'; import { Mic, Square, Loader, Play, Upload } from 'lucide-react'; import { supabase } from '../../lib/supabase'; import { v4 as uuidv4 } from 'uuid'; +import { useStudentTracking } from '../../hooks/useStudentTracking'; interface AudioRecorderProps { storyId: string; @@ -17,6 +18,8 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); + const { trackAudioRecorded } = useStudentTracking(); + const startTime = React.useRef(null); const startRecording = async () => { try { @@ -34,6 +37,7 @@ export function AudioRecorder({ storyId, studentId, onAudioUploaded }: AudioReco }; mediaRecorderRef.current.start(); + startTime.current = Date.now(); setIsRecording(true); setError(null); } catch (err) { diff --git a/src/components/story/StoryGenerator.tsx b/src/components/story/StoryGenerator.tsx index d849f2c..9cec729 100644 --- a/src/components/story/StoryGenerator.tsx +++ b/src/components/story/StoryGenerator.tsx @@ -4,6 +4,7 @@ import { supabase } from '../../lib/supabase'; import { useSession } from '../../hooks/useSession'; import { useStoryCategories } from '../../hooks/useStoryCategories'; import { Wand2, ArrowLeft, ArrowRight } from 'lucide-react'; +import { useStudentTracking } from '../../hooks/useStudentTracking'; interface Category { id: string; @@ -45,6 +46,8 @@ export function StoryGenerator() { const [generationStatus, setGenerationStatus] = React.useState< 'idle' | 'creating' | 'generating-images' | 'saving' >('idle'); + const { trackStoryGenerated } = useStudentTracking(); + const startTime = React.useRef(Date.now()); const steps: StoryStep[] = [ { @@ -150,6 +153,23 @@ export function StoryGenerator() { if (updateError) throw updateError; + // 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, page) => + acc + page.text.split(/\s+/).length, 0); + + trackStoryGenerated({ + story_id: story.id, + theme: selectedTheme, + prompt: JSON.stringify(choices), + generation_time: generationTime, + word_count: wordCount, + 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}`); } catch (err) { console.error('Erro ao gerar história:', err); diff --git a/src/hooks/useStudentTracking.ts b/src/hooks/useStudentTracking.ts new file mode 100644 index 0000000..8bc1dc2 --- /dev/null +++ b/src/hooks/useStudentTracking.ts @@ -0,0 +1,94 @@ +import { useRudderstack } from './useRudderstack'; + +interface StoryGeneratedProps { + story_id: string; + theme: string; + prompt: string; + generation_time: number; + word_count: number; + student_id: string; + class_id?: string; + school_id?: string; +} + +interface AudioRecordedProps { + story_id: string; + duration: number; + file_size: number; + student_id: string; + page_number: number; + device_type: string; + recording_quality: string; +} + +interface ExerciseCompletedProps { + exercise_id: string; + story_id: string; + student_id: string; + exercise_type: 'completion' | 'pronunciation' | 'word_formation'; + score: number; + time_spent: number; + answers_correct?: number; + answers_total?: number; + words_attempted?: number; + words_correct?: number; + pronunciation_score?: number; + fluency_score?: number; + words_formed?: number; + difficulty_level: string; +} + +interface InterestActionProps { + student_id: string; + category: string; + item: string; + total_interests: number; + interests_in_category: number; +} + +export function useStudentTracking() { + const { track } = useRudderstack(); + + const trackStoryGenerated = (properties: StoryGeneratedProps) => { + track('story_generated', { + ...properties, + timestamp: new Date().toISOString() + }); + }; + + const trackAudioRecorded = (properties: AudioRecordedProps) => { + track('audio_recorded', { + ...properties, + timestamp: new Date().toISOString() + }); + }; + + const trackExerciseCompleted = (properties: ExerciseCompletedProps) => { + track('exercise_completed', { + ...properties, + timestamp: new Date().toISOString() + }); + }; + + const trackInterestAdded = (properties: InterestActionProps) => { + track('interest_added', { + ...properties, + timestamp: new Date().toISOString() + }); + }; + + const trackInterestRemoved = (properties: InterestActionProps) => { + track('interest_removed', { + ...properties, + timestamp: new Date().toISOString() + }); + }; + + return { + trackStoryGenerated, + trackAudioRecorded, + trackExerciseCompleted, + trackInterestAdded, + trackInterestRemoved + }; +} \ No newline at end of file diff --git a/src/pages/student-dashboard/StudentSettingsPage.tsx b/src/pages/student-dashboard/StudentSettingsPage.tsx index 59462b1..84942fa 100644 --- a/src/pages/student-dashboard/StudentSettingsPage.tsx +++ b/src/pages/student-dashboard/StudentSettingsPage.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { Heart, Gamepad2, Dog, Map, Pizza, School as SchoolIcon, Tv, Music, Palette, Trophy, Loader2 } from 'lucide-react'; import { supabase } from '../../lib/supabase'; import { useToast } from '../../hooks/useToast'; +import { useStudentTracking } from '../../hooks/useStudentTracking'; interface InterestState { category: string; @@ -15,6 +16,7 @@ interface InterestState { export function StudentSettingsPage() { const { toast } = useToast(); + const { trackInterestAdded, trackInterestRemoved } = useStudentTracking(); const [interests, setInterests] = React.useState([ { category: 'pets', items: [] }, { category: 'entertainment', items: [] }, @@ -105,13 +107,27 @@ export function StudentSettingsPage() { if (error) throw error; // Atualiza o estado local - setInterests(prev => - prev.map(interest => + setInterests(prev => { + const newInterests = prev.map(interest => interest.category === category ? { ...interest, items: [...interest.items, value] } : interest - ) - ); + ); + + // Track interest added + const categoryInterests = newInterests.find(i => i.category === category); + if (categoryInterests) { + trackInterestAdded({ + student_id: session.user.id, + category: category, + item: value, + total_interests: newInterests.reduce((acc, curr) => acc + curr.items.length, 0), + interests_in_category: categoryInterests.items.length + }); + } + + return newInterests; + }); toast({ title: 'Sucesso', @@ -152,13 +168,27 @@ export function StudentSettingsPage() { if (error) throw error; // Atualiza o estado local - setInterests(prev => - prev.map(interest => + setInterests(prev => { + const newInterests = prev.map(interest => interest.category === category ? { ...interest, items: interest.items.filter(i => i !== item) } : interest - ) - ); + ); + + // Track interest removed + const categoryInterests = newInterests.find(i => i.category === category); + if (categoryInterests) { + trackInterestRemoved({ + student_id: session.user.id, + category: category, + item: item, + total_interests: newInterests.reduce((acc, curr) => acc + curr.items.length, 0), + interests_in_category: categoryInterests.items.length + }); + } + + return newInterests; + }); toast({ title: 'Sucesso',