diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb7a1a..abf9791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ Todas as mudanças notáveis neste projeto serão documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/), e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). +## [0.5.1] - 2024-01-31 + +### Técnico +- Corrigido erro de constraint na tabela stories ao atualizar status +- Removida tentativa de atualizar coluna inexistente error_message +- Ajustados os status da história para valores válidos: 'pending', 'published', 'failed' +- Melhorada validação e logs durante o processo de geração da história + +### Modificado +- Alterado fluxo de status da história para usar estados válidos do banco de dados +- Melhorada mensagem de erro para usuário final em caso de falha na geração + ## [1.0.0] - 2024-03-20 ### Adicionado @@ -148,3 +160,24 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). - Adição de diagramas ER para visualização dos relacionamentos - Documentação de índices e políticas de segurança - Inclusão de considerações de performance e backup + +## [1.3.0] - 2024-01-31 + +### Adicionado +- Suporte a múltiplos idiomas na geração de histórias: + - Português (Brasil) + - Inglês (EUA) + - Espanhol (Espanha) +- Nova etapa de seleção de idioma no fluxo de criação de história +- Instruções específicas para cada idioma no prompt da IA + +### Modificado +- Fluxo de geração de história para incluir seleção de idioma +- Interface do gerador de histórias com novo passo de idioma +- Adaptação do prompt da IA para considerar o idioma selecionado + +### Técnico +- Adicionada constante `LANGUAGE_OPTIONS` com opções de idiomas suportados +- Implementada validação de idioma antes da geração +- Atualizado payload da Edge Function para incluir `language_type` +- Melhorada tipagem para suporte a múltiplos idiomas diff --git a/src/components/story/StoryGenerator.tsx b/src/components/story/StoryGenerator.tsx index c9f3db6..bd5ce1c 100644 --- a/src/components/story/StoryGenerator.tsx +++ b/src/components/story/StoryGenerator.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { supabase } from '../../lib/supabase'; import { useSession } from '../../hooks/useSession'; import { useStoryCategories } from '../../hooks/useStoryCategories'; -import { Wand2, ArrowLeft } from 'lucide-react'; +import { Wand2, ArrowLeft, Globe } from 'lucide-react'; import { useStudentTracking } from '../../hooks/useStudentTracking'; interface Category { @@ -19,6 +19,7 @@ interface StoryStep { key?: keyof StoryChoices; items?: Category[]; isContextStep?: boolean; + isLanguageStep?: boolean; } export interface StoryChoices { @@ -27,6 +28,7 @@ export interface StoryChoices { character_id: string | null; setting_id: string | null; context?: string; + language_type: string; } interface StoryGeneratorProps { @@ -42,6 +44,12 @@ interface StoryGeneratorProps { setChoices: React.Dispatch>; } +const LANGUAGE_OPTIONS = [ + { value: 'pt-BR', label: 'Português (Brasil)' }, + { value: 'en-US', label: 'Inglês (EUA)' }, + { value: 'es-ES', label: 'Espanhol (Espanha)' } +] as const; + export function StoryGenerator({ initialContext = '', onContextChange, @@ -54,10 +62,9 @@ export function StoryGenerator({ choices, setChoices }: StoryGeneratorProps) { - // 1. Obter dados da API const { themes, subjects, characters, settings, isLoading } = useStoryCategories(); - // 2. Definir steps com os dados obtidos + // Definir steps com os dados obtidos const steps: StoryStep[] = [ { title: 'Escolha o Tema', @@ -79,31 +86,51 @@ export function StoryGenerator({ items: settings || [], key: 'setting_id' }, + { + title: 'Escolha o Idioma da História', + isLanguageStep: true + }, { title: 'Contexto da História (Opcional)', isContextStep: true } ]; - // 3. useEffect que depende dos dados + // useEffect que depende dos dados React.useEffect(() => { - if (inputMode === 'voice' && voiceTranscript && themes) { - setStep(steps.length); + // Só aplicar escolhas aleatórias se estiver no modo voz + if (inputMode === 'voice' && voiceTranscript && themes && !choices.theme_id) { + setStep(steps.length); // Vai para o último passo (contexto) + // Selecionar IDs aleatórios válidos para cada categoria + const randomTheme = themes[Math.floor(Math.random() * themes.length)]; + const randomSubject = subjects?.[Math.floor(Math.random() * (subjects?.length || 1))] || null; + const randomCharacter = characters?.[Math.floor(Math.random() * (characters?.length || 1))] || null; + const randomSetting = settings?.[Math.floor(Math.random() * (settings?.length || 1))] || null; + setChoices(prev => ({ ...prev, - theme_id: 'auto', - subject_id: 'auto', - character_id: 'auto', - setting_id: 'auto' + theme_id: randomTheme?.id || null, + subject_id: randomSubject?.id || null, + character_id: randomCharacter?.id || null, + setting_id: randomSetting?.id || null, + language_type: prev.language_type // Mantém o idioma selecionado })); } - }, [inputMode, voiceTranscript, steps.length, themes, setStep, setChoices]); + }, [inputMode, voiceTranscript, themes, subjects, characters, settings, setStep, setChoices, choices.theme_id]); + // Atualizar apenas o contexto quando mudar o modo ou a transcrição React.useEffect(() => { - setChoices(prev => ({ - ...prev, - context: inputMode === 'voice' ? voiceTranscript : initialContext - })); + if (inputMode === 'voice' && voiceTranscript) { + setChoices(prev => ({ + ...prev, + context: voiceTranscript + })); + } else if (inputMode === 'form') { + setChoices(prev => ({ + ...prev, + context: initialContext + })); + } }, [voiceTranscript, initialContext, inputMode]); const handleContextChange = (e: React.ChangeEvent) => { @@ -120,10 +147,43 @@ export function StoryGenerator({ const startTime = React.useRef(Date.now()); const currentStep = steps[step - 1]; - const isLastStep = step === steps.length; const handleSelect = (key: keyof StoryChoices, value: string) => { + console.log(`Selecionando ${key}:`, value); // Log para debug + + if (!value) { + setError(`Valor inválido para ${key}`); + return; + } + setChoices(prev => ({ ...prev, [key]: value })); + + // Avançar apenas se houver um próximo passo + if (step < steps.length) { + setStep((prev: number) => prev + 1); + } + }; + + const handleNext = () => { + if (currentStep.isContextStep) { + setStep((prev: number) => prev + 1); + } + }; + + const handleLanguageSelect = (language: string) => { + console.log('Selecionando idioma:', language); + + if (!LANGUAGE_OPTIONS.some(opt => opt.value === language)) { + setError('Idioma inválido selecionado'); + return; + } + + setChoices(prev => ({ + ...prev, + language_type: language + })); + + // Avançar para o próximo passo if (step < steps.length) { setStep((prev: number) => prev + 1); } @@ -139,38 +199,115 @@ export function StoryGenerator({ // Contexto é opcional no formulário const finalContext = inputMode === 'voice' ? voiceTranscript : initialContext; - if (!session?.user?.id) return; - - if (!choices.theme_id || !choices.subject_id || !choices.character_id || !choices.setting_id) { - setError('Por favor, preencha todas as escolhas antes de continuar.'); + if (!session?.user?.id) { + setError('Usuário não autenticado'); return; } + + // Log inicial para debug + console.log('=== Iniciando geração de história ==='); + console.log('Modo:', inputMode); + console.log('Choices:', choices); + // Validações iniciais + if (!themes?.length || !subjects?.length || !characters?.length || !settings?.length) { + console.error('Dados das categorias não carregados:', { themes, subjects, characters, settings }); + setError('Erro ao carregar dados necessários. Tente novamente.'); + return; + } + + // Validar se todos os IDs são UUIDs válidos + const isValidUUID = (id: string | null) => { + if (!id) return false; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(id); + }; + + // Validar cada ID individualmente + const validations = [ + { field: 'theme_id', value: choices.theme_id, exists: themes.some(t => t.id === choices.theme_id) }, + { field: 'subject_id', value: choices.subject_id, exists: subjects.some(s => s.id === choices.subject_id) }, + { field: 'character_id', value: choices.character_id, exists: characters.some(c => c.id === choices.character_id) }, + { field: 'setting_id', value: choices.setting_id, exists: settings.some(s => s.id === choices.setting_id) } + ]; + + // Verificar cada validação + for (const validation of validations) { + console.log(`Validando ${validation.field}:`, validation); + + if (!validation.value) { + setError(`${validation.field} não selecionado`); + return; + } + + if (!isValidUUID(validation.value)) { + setError(`${validation.field} não é um UUID válido`); + return; + } + + if (!validation.exists) { + setError(`${validation.field} não encontrado na lista de opções`); + return; + } + } + + // Validar idioma + if (!choices.language_type || !LANGUAGE_OPTIONS.some(opt => opt.value === choices.language_type)) { + setError('Idioma não selecionado ou inválido'); + return; + } + try { setIsGenerating(true); setError(null); setGenerationStatus('creating'); + // Log detalhado antes de fazer a inserção + console.log('=== Dados validados para inserção ===', { + student_id: session.user.id, + theme_id: choices.theme_id, + subject_id: choices.subject_id, + character_id: choices.character_id, + setting_id: choices.setting_id, + context: finalContext, + language_type: choices.language_type + }); + + // Criar objeto da história antes da inserção para validação + const storyData = { + student_id: session.user.id, + title: 'Gerando...', + theme_id: choices.theme_id, + subject_id: choices.subject_id, + character_id: choices.character_id, + setting_id: choices.setting_id, + context: finalContext, + language_type: choices.language_type, + status: 'draft', + content: { + prompt: choices, + pages: [] + } + } as const; + + // Validar se todos os campos necessários estão presentes + const requiredFields = ['student_id', 'theme_id', 'subject_id', 'character_id', 'setting_id', 'language_type'] as const; + const missingFields = requiredFields.filter(field => !storyData[field]); + + if (missingFields.length > 0) { + throw new Error(`Campos obrigatórios faltando: ${missingFields.join(', ')}`); + } + const { data: story, error: storyError } = await supabase .from('stories') - .insert({ - student_id: session.user.id, - title: 'Gerando...', - theme_id: choices.theme_id, - subject_id: choices.subject_id, - character_id: choices.character_id, - setting_id: choices.setting_id, - context: finalContext, - status: 'draft', - content: { - prompt: choices, - pages: [] - } - }) + .insert(storyData) .select() .single(); - if (storyError) throw storyError; + if (storyError) { + console.error('Erro ao inserir história:', storyError); + throw storyError; + } // Tracking da criação da história const selectedTheme = themes?.find(t => t.id === choices.theme_id)?.title || ''; @@ -186,24 +323,103 @@ export function StoryGenerator({ setting: selectedSetting, context: finalContext, generation_time: Date.now() - startTime.current, - word_count: 0, // será atualizado após a geração + word_count: 0, student_id: session.user.id, school_id: session.user.user_metadata?.school_id, class_id: session.user.user_metadata?.class_id }); setGenerationStatus('generating-images'); - console.log('Chamando Edge Function com:', story); + console.log('=== Chamando Edge Function ==='); + console.log('Story ID:', story.id); + console.log('Story Data:', story); - const { data: functionData, error: functionError } = await supabase.functions - .invoke('generate-story', { - body: { record: story } - }); + try { + if (!story?.id) { + throw new Error('ID da história não encontrado'); + } - console.log('Resposta da Edge Function:', functionData); + const storyPayload = { + voice_context: finalContext || '', + student_id: session.user.id, + theme_id: choices.theme_id, + subject_id: choices.subject_id, + character_id: choices.character_id, + setting_id: choices.setting_id, + language_type: choices.language_type, + theme: selectedTheme, + subject: selectedSubject, + character: selectedCharacter, + setting: selectedSetting, + story_id: story.id // Garantindo que o ID existe + }; - if (functionError) { - throw new Error(`Erro na Edge Function: ${functionError.message}`); + console.log('=== Dados da História ==='); + console.log('ID:', story.id); + console.log('Payload completo:', storyPayload); + + const response = await supabase.functions + .invoke('generate-story', { + body: storyPayload + }); + + console.log('=== Resposta da Edge Function ==='); + console.log('Resposta completa:', response); + + // Se a resposta não for 200, lançar erro + if (response.error) { + console.error('Erro na Edge Function:', response.error); + throw new Error(`Erro na Edge Function: ${response.error.message}`); + } + + // Se não houver dados na resposta + if (!response.data) { + console.error('Edge Function não retornou dados'); + throw new Error('Edge Function não retornou dados'); + } + + // Atualizar o status da história para success + const { error: updateError } = await supabase + .from('stories') + .update({ + status: 'published', + title: response.data.title || 'História Gerada', + updated_at: new Date().toISOString() + }) + .eq('id', story.id) + .single(); + + if (updateError) { + console.error('Erro ao atualizar status da história:', updateError); + throw updateError; + } + + } catch (error) { + console.error('=== Erro na Edge Function ==='); + console.error('Erro completo:', error); + console.error('Story ID:', story?.id); + console.error('Estado atual:', { choices, inputMode, step }); + + if (!story?.id) { + throw new Error('ID da história não encontrado para atualizar status de erro'); + } + + // Atualizar status da história para erro + const { error: updateError } = await supabase + .from('stories') + .update({ + status: 'failed', + title: 'Erro na Geração', + updated_at: new Date().toISOString() + }) + .eq('id', story.id) + .single(); + + if (updateError) { + console.error('Erro ao atualizar status de erro:', updateError); + } + + throw new Error(`Erro na geração da história. Por favor, tente novamente.`); } setGenerationStatus('saving'); @@ -213,7 +429,10 @@ export function StoryGenerator({ .eq('id', story.id) .single(); - if (updateError) throw updateError; + if (updateError) { + console.error('Erro ao buscar história atualizada:', updateError); + throw updateError; + } // Atualizar a contagem de palavras após a geração const wordCount = updatedStory.content.pages.reduce((acc: number, page: { text: string }) => @@ -227,8 +446,15 @@ export function StoryGenerator({ navigate(`/aluno/historias/${story.id}`); } catch (err) { - console.error('Erro ao gerar história:', err); - setError('Não foi possível criar sua história. Tente novamente.'); + console.error('=== Erro detalhado ==='); + console.error('Erro:', err); + console.error('Estado atual:', { choices, inputMode, step }); + + if (err instanceof Error) { + setError(`Erro ao gerar história: ${err.message}`); + } else { + setError('Erro desconhecido ao gerar história. Tente novamente.'); + } } finally { setIsGenerating(false); setGenerationStatus('idle'); @@ -279,7 +505,31 @@ export function StoryGenerator({ {currentStep.title} - {currentStep.isContextStep ? ( + {currentStep.isLanguageStep ? ( +
+ {LANGUAGE_OPTIONS.map((option) => ( + + ))} +
+ ) : currentStep.isContextStep ? (