mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
feat: Melhorando tracking
Some checks failed
Docker Build and Push / build (push) Has been cancelled
Some checks failed
Docker Build and Push / build (push) Has been cancelled
This commit is contained in:
parent
3cdd136a4e
commit
2852b889b2
@ -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<Blob | null>(null);
|
||||
@ -16,6 +20,10 @@ export function PronunciationPractice({ words }: PronunciationPracticeProps) {
|
||||
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const chunksRef = useRef<Blob[]>([]);
|
||||
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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<string[]>([]);
|
||||
const [userWord, setUserWord] = useState<string>('');
|
||||
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',
|
||||
|
||||
@ -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<MediaRecorder | null>(null);
|
||||
const chunksRef = useRef<Blob[]>([]);
|
||||
const { trackAudioRecorded } = useStudentTracking();
|
||||
const startTime = React.useRef<number | null>(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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
94
src/hooks/useStudentTracking.ts
Normal file
94
src/hooks/useStudentTracking.ts
Normal file
@ -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
|
||||
};
|
||||
}
|
||||
@ -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<InterestState[]>([
|
||||
{ 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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user