mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
feat: adiciona suporte a múltiplos idiomas na geração de histórias
- Adiciona suporte para Português (Brasil), Inglês (EUA) e Espanhol (Espanha) - Implementa nova etapa de seleção de idioma no fluxo de criação - Adiciona instruções específicas por idioma no prompt da IA - Atualiza CHANGELOG.md para versão 1.3.0
This commit is contained in:
parent
45a4b1ba24
commit
fa8073dcee
33
CHANGELOG.md
33
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/),
|
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/).
|
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
|
## [1.0.0] - 2024-03-20
|
||||||
|
|
||||||
### Adicionado
|
### 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
|
- Adição de diagramas ER para visualização dos relacionamentos
|
||||||
- Documentação de índices e políticas de segurança
|
- Documentação de índices e políticas de segurança
|
||||||
- Inclusão de considerações de performance e backup
|
- 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
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { useSession } from '../../hooks/useSession';
|
import { useSession } from '../../hooks/useSession';
|
||||||
import { useStoryCategories } from '../../hooks/useStoryCategories';
|
import { useStoryCategories } from '../../hooks/useStoryCategories';
|
||||||
import { Wand2, ArrowLeft } from 'lucide-react';
|
import { Wand2, ArrowLeft, Globe } from 'lucide-react';
|
||||||
import { useStudentTracking } from '../../hooks/useStudentTracking';
|
import { useStudentTracking } from '../../hooks/useStudentTracking';
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
@ -19,6 +19,7 @@ interface StoryStep {
|
|||||||
key?: keyof StoryChoices;
|
key?: keyof StoryChoices;
|
||||||
items?: Category[];
|
items?: Category[];
|
||||||
isContextStep?: boolean;
|
isContextStep?: boolean;
|
||||||
|
isLanguageStep?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoryChoices {
|
export interface StoryChoices {
|
||||||
@ -27,6 +28,7 @@ export interface StoryChoices {
|
|||||||
character_id: string | null;
|
character_id: string | null;
|
||||||
setting_id: string | null;
|
setting_id: string | null;
|
||||||
context?: string;
|
context?: string;
|
||||||
|
language_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoryGeneratorProps {
|
interface StoryGeneratorProps {
|
||||||
@ -42,6 +44,12 @@ interface StoryGeneratorProps {
|
|||||||
setChoices: React.Dispatch<React.SetStateAction<StoryChoices>>;
|
setChoices: React.Dispatch<React.SetStateAction<StoryChoices>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
export function StoryGenerator({
|
||||||
initialContext = '',
|
initialContext = '',
|
||||||
onContextChange,
|
onContextChange,
|
||||||
@ -54,10 +62,9 @@ export function StoryGenerator({
|
|||||||
choices,
|
choices,
|
||||||
setChoices
|
setChoices
|
||||||
}: StoryGeneratorProps) {
|
}: StoryGeneratorProps) {
|
||||||
// 1. Obter dados da API
|
|
||||||
const { themes, subjects, characters, settings, isLoading } = useStoryCategories();
|
const { themes, subjects, characters, settings, isLoading } = useStoryCategories();
|
||||||
|
|
||||||
// 2. Definir steps com os dados obtidos
|
// Definir steps com os dados obtidos
|
||||||
const steps: StoryStep[] = [
|
const steps: StoryStep[] = [
|
||||||
{
|
{
|
||||||
title: 'Escolha o Tema',
|
title: 'Escolha o Tema',
|
||||||
@ -79,31 +86,51 @@ export function StoryGenerator({
|
|||||||
items: settings || [],
|
items: settings || [],
|
||||||
key: 'setting_id'
|
key: 'setting_id'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Escolha o Idioma da História',
|
||||||
|
isLanguageStep: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Contexto da História (Opcional)',
|
title: 'Contexto da História (Opcional)',
|
||||||
isContextStep: true
|
isContextStep: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 3. useEffect que depende dos dados
|
// useEffect que depende dos dados
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (inputMode === 'voice' && voiceTranscript && themes) {
|
// Só aplicar escolhas aleatórias se estiver no modo voz
|
||||||
setStep(steps.length);
|
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 => ({
|
setChoices(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
theme_id: 'auto',
|
theme_id: randomTheme?.id || null,
|
||||||
subject_id: 'auto',
|
subject_id: randomSubject?.id || null,
|
||||||
character_id: 'auto',
|
character_id: randomCharacter?.id || null,
|
||||||
setting_id: 'auto'
|
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(() => {
|
React.useEffect(() => {
|
||||||
setChoices(prev => ({
|
if (inputMode === 'voice' && voiceTranscript) {
|
||||||
...prev,
|
setChoices(prev => ({
|
||||||
context: inputMode === 'voice' ? voiceTranscript : initialContext
|
...prev,
|
||||||
}));
|
context: voiceTranscript
|
||||||
|
}));
|
||||||
|
} else if (inputMode === 'form') {
|
||||||
|
setChoices(prev => ({
|
||||||
|
...prev,
|
||||||
|
context: initialContext
|
||||||
|
}));
|
||||||
|
}
|
||||||
}, [voiceTranscript, initialContext, inputMode]);
|
}, [voiceTranscript, initialContext, inputMode]);
|
||||||
|
|
||||||
const handleContextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleContextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
@ -120,10 +147,43 @@ export function StoryGenerator({
|
|||||||
const startTime = React.useRef(Date.now());
|
const startTime = React.useRef(Date.now());
|
||||||
|
|
||||||
const currentStep = steps[step - 1];
|
const currentStep = steps[step - 1];
|
||||||
const isLastStep = step === steps.length;
|
|
||||||
|
|
||||||
const handleSelect = (key: keyof StoryChoices, value: string) => {
|
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 }));
|
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) {
|
if (step < steps.length) {
|
||||||
setStep((prev: number) => prev + 1);
|
setStep((prev: number) => prev + 1);
|
||||||
}
|
}
|
||||||
@ -139,38 +199,115 @@ export function StoryGenerator({
|
|||||||
// Contexto é opcional no formulário
|
// Contexto é opcional no formulário
|
||||||
const finalContext = inputMode === 'voice' ? voiceTranscript : initialContext;
|
const finalContext = inputMode === 'voice' ? voiceTranscript : initialContext;
|
||||||
|
|
||||||
if (!session?.user?.id) return;
|
if (!session?.user?.id) {
|
||||||
|
setError('Usuário não autenticado');
|
||||||
if (!choices.theme_id || !choices.subject_id || !choices.character_id || !choices.setting_id) {
|
|
||||||
setError('Por favor, preencha todas as escolhas antes de continuar.');
|
|
||||||
return;
|
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 {
|
try {
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setGenerationStatus('creating');
|
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
|
const { data: story, error: storyError } = await supabase
|
||||||
.from('stories')
|
.from('stories')
|
||||||
.insert({
|
.insert(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,
|
|
||||||
status: 'draft',
|
|
||||||
content: {
|
|
||||||
prompt: choices,
|
|
||||||
pages: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (storyError) throw storyError;
|
if (storyError) {
|
||||||
|
console.error('Erro ao inserir história:', storyError);
|
||||||
|
throw storyError;
|
||||||
|
}
|
||||||
|
|
||||||
// Tracking da criação da história
|
// Tracking da criação da história
|
||||||
const selectedTheme = themes?.find(t => t.id === choices.theme_id)?.title || '';
|
const selectedTheme = themes?.find(t => t.id === choices.theme_id)?.title || '';
|
||||||
@ -186,24 +323,103 @@ export function StoryGenerator({
|
|||||||
setting: selectedSetting,
|
setting: selectedSetting,
|
||||||
context: finalContext,
|
context: finalContext,
|
||||||
generation_time: Date.now() - startTime.current,
|
generation_time: Date.now() - startTime.current,
|
||||||
word_count: 0, // será atualizado após a geração
|
word_count: 0,
|
||||||
student_id: session.user.id,
|
student_id: session.user.id,
|
||||||
school_id: session.user.user_metadata?.school_id,
|
school_id: session.user.user_metadata?.school_id,
|
||||||
class_id: session.user.user_metadata?.class_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 ===');
|
||||||
|
console.log('Story ID:', story.id);
|
||||||
|
console.log('Story Data:', story);
|
||||||
|
|
||||||
const { data: functionData, error: functionError } = await supabase.functions
|
try {
|
||||||
.invoke('generate-story', {
|
if (!story?.id) {
|
||||||
body: { record: story }
|
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) {
|
console.log('=== Dados da História ===');
|
||||||
throw new Error(`Erro na Edge Function: ${functionError.message}`);
|
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');
|
setGenerationStatus('saving');
|
||||||
@ -213,7 +429,10 @@ export function StoryGenerator({
|
|||||||
.eq('id', story.id)
|
.eq('id', story.id)
|
||||||
.single();
|
.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
|
// Atualizar a contagem de palavras após a geração
|
||||||
const wordCount = updatedStory.content.pages.reduce((acc: number, page: { text: string }) =>
|
const wordCount = updatedStory.content.pages.reduce((acc: number, page: { text: string }) =>
|
||||||
@ -227,8 +446,15 @@ export function StoryGenerator({
|
|||||||
|
|
||||||
navigate(`/aluno/historias/${story.id}`);
|
navigate(`/aluno/historias/${story.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro ao gerar história:', err);
|
console.error('=== Erro detalhado ===');
|
||||||
setError('Não foi possível criar sua história. Tente novamente.');
|
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 {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
setGenerationStatus('idle');
|
setGenerationStatus('idle');
|
||||||
@ -279,7 +505,31 @@ export function StoryGenerator({
|
|||||||
{currentStep.title}
|
{currentStep.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{currentStep.isContextStep ? (
|
{currentStep.isLanguageStep ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{LANGUAGE_OPTIONS.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleLanguageSelect(option.value)}
|
||||||
|
className={`p-6 rounded-xl border-2 transition-all text-left ${
|
||||||
|
choices.language_type === option.value
|
||||||
|
? 'border-purple-500 bg-purple-50'
|
||||||
|
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Globe className="h-6 w-6 text-purple-600" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-900">{option.label}</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Escreva sua história em {option.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : currentStep.isContextStep ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<textarea
|
<textarea
|
||||||
value={initialContext}
|
value={initialContext}
|
||||||
@ -287,6 +537,14 @@ export function StoryGenerator({
|
|||||||
className="w-full p-3 border rounded-lg"
|
className="w-full p-3 border rounded-lg"
|
||||||
placeholder="Descreva sua história... (opcional)"
|
placeholder="Descreva sua história... (opcional)"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={isGenerating}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Wand2 className="h-5 w-5" />
|
||||||
|
{isGenerating ? getGenerationStatusText() : 'Criar História Mágica'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@ -335,14 +593,13 @@ export function StoryGenerator({
|
|||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isLastStep && (
|
{!currentStep.isLanguageStep && !currentStep.isContextStep && (
|
||||||
<button
|
<button
|
||||||
onClick={handleGenerate}
|
onClick={handleNext}
|
||||||
disabled={!choices.theme_id || !choices.subject_id || !choices.character_id || !choices.setting_id || isGenerating}
|
disabled={currentStep.key && !choices[currentStep.key] || isGenerating}
|
||||||
className="flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
className="flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Wand2 className="h-5 w-5" />
|
Próximo
|
||||||
{isGenerating ? getGenerationStatusText() : 'Criar História Mágica'}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ArrowLeft, Sparkles } from 'lucide-react';
|
import { ArrowLeft, Sparkles, Globe } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { StoryGenerator } from '../../components/story/StoryGenerator';
|
import { StoryGenerator } from '../../components/story/StoryGenerator';
|
||||||
import { useSession } from '../../hooks/useSession';
|
import { useSession } from '../../hooks/useSession';
|
||||||
@ -10,6 +10,12 @@ import { useSpeechRecognition } from '@/features/voice-commands/hooks/useSpeechR
|
|||||||
import { VoiceCommandButton } from '@/features/voice-commands/components/VoiceCommandButton';
|
import { VoiceCommandButton } from '@/features/voice-commands/components/VoiceCommandButton';
|
||||||
import type { StoryChoices } from '@/components/story/StoryGenerator';
|
import type { StoryChoices } from '@/components/story/StoryGenerator';
|
||||||
|
|
||||||
|
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 CreateStoryPage() {
|
export function CreateStoryPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
@ -38,7 +44,8 @@ export function CreateStoryPage() {
|
|||||||
subject_id: null,
|
subject_id: null,
|
||||||
character_id: null,
|
character_id: null,
|
||||||
setting_id: null,
|
setting_id: null,
|
||||||
context: ''
|
context: '',
|
||||||
|
language_type: 'pt-BR'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manipuladores para gravação de voz
|
// Manipuladores para gravação de voz
|
||||||
|
|||||||
@ -186,6 +186,7 @@
|
|||||||
| public | story_generations | | CHECK |
|
| public | story_generations | | CHECK |
|
||||||
| public | story_generations | | CHECK |
|
| public | story_generations | | CHECK |
|
||||||
| public | story_generations | | CHECK |
|
| public | story_generations | | CHECK |
|
||||||
|
| public | story_generations | | CHECK |
|
||||||
| public | schools | | CHECK |
|
| public | schools | | CHECK |
|
||||||
| public | schools | | CHECK |
|
| public | schools | | CHECK |
|
||||||
| public | schools | | CHECK |
|
| public | schools | | CHECK |
|
||||||
@ -250,6 +251,7 @@
|
|||||||
| public | stories | | CHECK |
|
| public | stories | | CHECK |
|
||||||
| public | stories | | CHECK |
|
| public | stories | | CHECK |
|
||||||
| public | stories | | CHECK |
|
| public | stories | | CHECK |
|
||||||
|
| public | stories | | CHECK |
|
||||||
| public | teacher_invites | | CHECK |
|
| public | teacher_invites | | CHECK |
|
||||||
| public | teacher_invites | | CHECK |
|
| public | teacher_invites | | CHECK |
|
||||||
| public | teacher_invites | | CHECK |
|
| public | teacher_invites | | CHECK |
|
||||||
|
|||||||
@ -338,6 +338,7 @@
|
|||||||
| public | story_exercise_words | created_at | timestamp with time zone | YES | now() |
|
| public | story_exercise_words | created_at | timestamp with time zone | YES | now() |
|
||||||
| public | student_phonics_progress | xp_earned | integer | YES | 0 |
|
| public | student_phonics_progress | xp_earned | integer | YES | 0 |
|
||||||
| public | student_phonics_progress | updated_at | timestamp with time zone | YES | CURRENT_TIMESTAMP |
|
| public | student_phonics_progress | updated_at | timestamp with time zone | YES | CURRENT_TIMESTAMP |
|
||||||
|
| public | story_generations | language_type | USER-DEFINED | NO | 'pt-BR'::language_enum |
|
||||||
| public | phonics_exercise_media | exercise_id | uuid | YES | |
|
| public | phonics_exercise_media | exercise_id | uuid | YES | |
|
||||||
| pgsodium | key | key_id | bigint | YES | nextval('pgsodium.key_key_id_seq'::regclass) |
|
| pgsodium | key | key_id | bigint | YES | nextval('pgsodium.key_key_id_seq'::regclass) |
|
||||||
| auth | mfa_amr_claims | session_id | uuid | NO | |
|
| auth | mfa_amr_claims | session_id | uuid | NO | |
|
||||||
@ -435,8 +436,8 @@
|
|||||||
| public | story_details | content | jsonb | YES | |
|
| public | story_details | content | jsonb | YES | |
|
||||||
| public | teachers | class_ids | ARRAY | YES | |
|
| public | teachers | class_ids | ARRAY | YES | |
|
||||||
| extensions | pg_stat_statements | shared_blks_hit | bigint | YES | |
|
| extensions | pg_stat_statements | shared_blks_hit | bigint | YES | |
|
||||||
| public | story_generations | id | uuid | NO | uuid_generate_v4() |
|
|
||||||
| public | teacher_classes | class_id | uuid | NO | |
|
| public | teacher_classes | class_id | uuid | NO | |
|
||||||
|
| public | story_generations | id | uuid | NO | uuid_generate_v4() |
|
||||||
| auth | flow_state | auth_code_issued_at | timestamp with time zone | YES | |
|
| auth | flow_state | auth_code_issued_at | timestamp with time zone | YES | |
|
||||||
| public | media_types | id | uuid | NO | uuid_generate_v4() |
|
| public | media_types | id | uuid | NO | uuid_generate_v4() |
|
||||||
| public | students | avatar_url | text | YES | |
|
| public | students | avatar_url | text | YES | |
|
||||||
@ -525,6 +526,7 @@
|
|||||||
| pgsodium | mask_columns | associated_columns | text | YES | |
|
| pgsodium | mask_columns | associated_columns | text | YES | |
|
||||||
| auth | users | phone_change_sent_at | timestamp with time zone | YES | |
|
| auth | users | phone_change_sent_at | timestamp with time zone | YES | |
|
||||||
| extensions | pg_stat_statements | local_blks_written | bigint | YES | |
|
| extensions | pg_stat_statements | local_blks_written | bigint | YES | |
|
||||||
|
| public | stories | language_type | USER-DEFINED | NO | 'pt-BR'::language_enum |
|
||||||
| auth | one_time_tokens | created_at | timestamp without time zone | NO | now() |
|
| auth | one_time_tokens | created_at | timestamp without time zone | NO | now() |
|
||||||
| auth | sessions | factor_id | uuid | YES | |
|
| auth | sessions | factor_id | uuid | YES | |
|
||||||
| extensions | pg_stat_statements | local_blks_dirtied | bigint | YES | |
|
| extensions | pg_stat_statements | local_blks_dirtied | bigint | YES | |
|
||||||
|
|||||||
@ -12,17 +12,24 @@ interface StoryPrompt {
|
|||||||
character_id: string;
|
character_id: string;
|
||||||
setting_id: string;
|
setting_id: string;
|
||||||
context?: string;
|
context?: string;
|
||||||
|
language_type: string;
|
||||||
difficulty: 'easy' | 'medium' | 'hard';
|
difficulty: 'easy' | 'medium' | 'hard';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EnhancedPayload {
|
interface StoryPayload {
|
||||||
// Campos existentes
|
story_id: string;
|
||||||
|
student_id: string;
|
||||||
|
theme_id: string;
|
||||||
|
subject_id: string;
|
||||||
|
character_id: string;
|
||||||
|
setting_id: string;
|
||||||
|
language_type: string;
|
||||||
|
theme?: string;
|
||||||
|
subject?: string;
|
||||||
|
character?: string;
|
||||||
|
setting?: string;
|
||||||
|
context?: string;
|
||||||
voice_context?: string;
|
voice_context?: string;
|
||||||
audio_metadata?: {
|
|
||||||
duration: number;
|
|
||||||
sample_rate: number;
|
|
||||||
language: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALLOWED_ORIGINS = [
|
const ALLOWED_ORIGINS = [
|
||||||
@ -70,53 +77,57 @@ serve(async (req) => {
|
|||||||
return new Response('ok', { headers: corsHeaders })
|
return new Response('ok', { headers: corsHeaders })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { voice_context, ...rest } = await req.json()
|
const payload = await req.json() as StoryPayload;
|
||||||
console.log('[Request]', rest)
|
console.log('[Request] Payload recebido:', payload);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const supabase = createClient(
|
const supabase = createClient(
|
||||||
Deno.env.get('SUPABASE_URL') ?? '',
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
)
|
);
|
||||||
console.log('[Supabase] Cliente inicializado')
|
console.log('[Supabase] Cliente inicializado');
|
||||||
|
|
||||||
console.log('[DB] Buscando categorias...')
|
if (!payload.story_id) {
|
||||||
|
throw new Error('ID da história não fornecido');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DB] Buscando categorias...');
|
||||||
const [themeResult, subjectResult, characterResult, settingResult] = await Promise.all([
|
const [themeResult, subjectResult, characterResult, settingResult] = await Promise.all([
|
||||||
supabase.from('story_themes').select('*').eq('id', rest.theme_id).single(),
|
supabase.from('story_themes').select('*').eq('id', payload.theme_id).single(),
|
||||||
supabase.from('story_subjects').select('*').eq('id', rest.subject_id).single(),
|
supabase.from('story_subjects').select('*').eq('id', payload.subject_id).single(),
|
||||||
supabase.from('story_characters').select('*').eq('id', rest.character_id).single(),
|
supabase.from('story_characters').select('*').eq('id', payload.character_id).single(),
|
||||||
supabase.from('story_settings').select('*').eq('id', rest.setting_id).single()
|
supabase.from('story_settings').select('*').eq('id', payload.setting_id).single()
|
||||||
])
|
]);
|
||||||
|
|
||||||
console.log('[DB] Resultados das consultas:', {
|
console.log('[DB] Resultados das consultas:', {
|
||||||
theme: themeResult,
|
theme: themeResult,
|
||||||
subject: subjectResult,
|
subject: subjectResult,
|
||||||
character: characterResult,
|
character: characterResult,
|
||||||
setting: settingResult
|
setting: settingResult
|
||||||
})
|
});
|
||||||
|
|
||||||
if (themeResult.error) throw new Error(`Erro ao buscar tema: ${themeResult.error.message}`);
|
if (themeResult.error) throw new Error(`Erro ao buscar tema: ${themeResult.error.message}`);
|
||||||
if (subjectResult.error) throw new Error(`Erro ao buscar disciplina: ${subjectResult.error.message}`);
|
if (subjectResult.error) throw new Error(`Erro ao buscar disciplina: ${subjectResult.error.message}`);
|
||||||
if (characterResult.error) throw new Error(`Erro ao buscar personagem: ${characterResult.error.message}`);
|
if (characterResult.error) throw new Error(`Erro ao buscar personagem: ${characterResult.error.message}`);
|
||||||
if (settingResult.error) throw new Error(`Erro ao buscar cenário: ${settingResult.error.message}`);
|
if (settingResult.error) throw new Error(`Erro ao buscar cenário: ${settingResult.error.message}`);
|
||||||
|
|
||||||
if (!themeResult.data) throw new Error(`Tema não encontrado: ${rest.theme_id}`);
|
if (!themeResult.data) throw new Error(`Tema não encontrado: ${payload.theme_id}`);
|
||||||
if (!subjectResult.data) throw new Error(`Disciplina não encontrada: ${rest.subject_id}`);
|
if (!subjectResult.data) throw new Error(`Disciplina não encontrada: ${payload.subject_id}`);
|
||||||
if (!characterResult.data) throw new Error(`Personagem não encontrado: ${rest.character_id}`);
|
if (!characterResult.data) throw new Error(`Personagem não encontrado: ${payload.character_id}`);
|
||||||
if (!settingResult.data) throw new Error(`Cenário não encontrado: ${rest.setting_id}`);
|
if (!settingResult.data) throw new Error(`Cenário não encontrado: ${payload.setting_id}`);
|
||||||
|
|
||||||
const theme = themeResult.data;
|
const theme = themeResult.data;
|
||||||
const subject = subjectResult.data;
|
const subject = subjectResult.data;
|
||||||
const character = characterResult.data;
|
const character = characterResult.data;
|
||||||
const setting = settingResult.data;
|
const setting = settingResult.data;
|
||||||
|
|
||||||
console.log('[Validation] Categorias validadas com sucesso')
|
console.log('[Validation] Categorias validadas com sucesso');
|
||||||
|
|
||||||
console.log('[GPT] Construindo prompt...')
|
console.log('[GPT] Construindo prompt...');
|
||||||
const prompt = buildPrompt(rest, voice_context);
|
const prompt = buildPrompt(payload, payload.voice_context);
|
||||||
console.log('[GPT] Prompt construído:', prompt)
|
console.log('[GPT] Prompt construído:', prompt);
|
||||||
|
|
||||||
console.log('[GPT] Iniciando geração da história...')
|
console.log('[GPT] Iniciando geração da história...');
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-4o-mini",
|
model: "gpt-4o-mini",
|
||||||
messages: [
|
messages: [
|
||||||
@ -125,15 +136,16 @@ serve(async (req) => {
|
|||||||
content: "Você é um contador de histórias infantis especializado em conteúdo educativo."
|
content: "Você é um contador de histórias infantis especializado em conteúdo educativo."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: prompt
|
content: prompt
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
max_tokens: 1000
|
max_tokens: 1000,
|
||||||
})
|
response_format: { type: "json_object" }
|
||||||
|
});
|
||||||
|
|
||||||
console.log('[GPT] História gerada:', completion.choices[0].message)
|
console.log('[GPT] História gerada:', completion.choices[0].message);
|
||||||
const storyContent = JSON.parse(completion.choices[0].message.content || '{}') as StoryResponse;
|
const storyContent = JSON.parse(completion.choices[0].message.content || '{}') as StoryResponse;
|
||||||
|
|
||||||
// Validar estrutura do retorno da IA
|
// Validar estrutura do retorno da IA
|
||||||
@ -142,10 +154,10 @@ serve(async (req) => {
|
|||||||
throw new Error('Formato inválido retornado pela IA');
|
throw new Error('Formato inválido retornado pela IA');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[DALL-E] Iniciando geração de imagens...')
|
console.log('[DALL-E] Iniciando geração de imagens...');
|
||||||
const pages = await Promise.all(
|
const pages = await Promise.all(
|
||||||
storyContent.content.pages.map(async (page, index) => {
|
storyContent.content.pages.map(async (page, index) => {
|
||||||
console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.content.pages.length}...`)
|
console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.content.pages.length}...`);
|
||||||
|
|
||||||
// Gerar imagem com DALL-E
|
// Gerar imagem com DALL-E
|
||||||
const imageResponse = await openai.images.generate({
|
const imageResponse = await openai.images.generate({
|
||||||
@ -153,52 +165,52 @@ serve(async (req) => {
|
|||||||
n: 1,
|
n: 1,
|
||||||
size: "1024x1024",
|
size: "1024x1024",
|
||||||
model: "dall-e-3"
|
model: "dall-e-3"
|
||||||
})
|
});
|
||||||
|
|
||||||
// Baixar a imagem do URL do DALL-E
|
// Baixar a imagem do URL do DALL-E
|
||||||
console.log(`[Storage] Baixando imagem ${index + 1}...`)
|
console.log(`[Storage] Baixando imagem ${index + 1}...`);
|
||||||
const imageUrl = imageResponse.data[0].url
|
const imageUrl = imageResponse.data[0].url;
|
||||||
const imageRes = await fetch(imageUrl)
|
const imageRes = await fetch(imageUrl);
|
||||||
const imageBuffer = await imageRes.arrayBuffer()
|
const imageBuffer = await imageRes.arrayBuffer();
|
||||||
|
|
||||||
// Gerar nome único para o arquivo
|
// Gerar nome único para o arquivo
|
||||||
const fileName = `${rest.id}/page-${index + 1}-${Date.now()}.png`
|
const fileName = `${payload.story_id}/page-${index + 1}-${Date.now()}.png`;
|
||||||
|
|
||||||
// Salvar no Storage do Supabase
|
// Salvar no Storage do Supabase
|
||||||
console.log(`[Storage] Salvando imagem ${index + 1} no bucket...`)
|
console.log(`[Storage] Salvando imagem ${index + 1} no bucket...`);
|
||||||
const { data: storageData, error: storageError } = await supabase
|
const { error: storageError } = await supabase
|
||||||
.storage
|
.storage
|
||||||
.from('story-images')
|
.from('story-images')
|
||||||
.upload(fileName, imageBuffer, {
|
.upload(fileName, imageBuffer, {
|
||||||
contentType: 'image/png',
|
contentType: 'image/png',
|
||||||
cacheControl: '3600',
|
cacheControl: '3600',
|
||||||
upsert: false
|
upsert: false
|
||||||
})
|
});
|
||||||
|
|
||||||
if (storageError) {
|
if (storageError) {
|
||||||
throw new Error(`Erro ao salvar imagem ${index + 1} no storage: ${storageError.message}`)
|
throw new Error(`Erro ao salvar imagem ${index + 1} no storage: ${storageError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gerar URL público da imagem sem transformações
|
// Gerar URL público da imagem sem transformações
|
||||||
const { data: publicUrl } = supabase
|
const { data: publicUrl } = supabase
|
||||||
.storage
|
.storage
|
||||||
.from('story-images')
|
.from('story-images')
|
||||||
.getPublicUrl(fileName)
|
.getPublicUrl(fileName);
|
||||||
|
|
||||||
console.log(`[Storage] Imagem ${index + 1} salva com sucesso`)
|
console.log(`[Storage] Imagem ${index + 1} salva com sucesso`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: page.text,
|
text: page.text,
|
||||||
image: publicUrl.publicUrl, // Salvar apenas o caminho do arquivo
|
image: publicUrl.publicUrl, // Salvar apenas o caminho do arquivo
|
||||||
image_path: fileName
|
image_path: fileName
|
||||||
}
|
};
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
console.log('[DALL-E] Todas as imagens geradas com sucesso')
|
console.log('[DALL-E] Todas as imagens geradas com sucesso');
|
||||||
|
|
||||||
// Preparar e salvar os dados
|
// Preparar e salvar os dados
|
||||||
const { data: story, error: storyError } = await supabase
|
const { error: storyError } = await supabase
|
||||||
.from('stories')
|
.from('stories')
|
||||||
.update({
|
.update({
|
||||||
title: storyContent.title,
|
title: storyContent.title,
|
||||||
@ -207,10 +219,10 @@ serve(async (req) => {
|
|||||||
subject_id: subject.id,
|
subject_id: subject.id,
|
||||||
character_id: character.id,
|
character_id: character.id,
|
||||||
setting_id: setting.id,
|
setting_id: setting.id,
|
||||||
context: rest.context,
|
context: payload.voice_context || payload.context || '',
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.eq('id', rest.id)
|
.eq('id', payload.story_id)
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
@ -221,7 +233,7 @@ serve(async (req) => {
|
|||||||
.from('story_pages')
|
.from('story_pages')
|
||||||
.insert(
|
.insert(
|
||||||
pages.map((page, index) => ({
|
pages.map((page, index) => ({
|
||||||
story_id: rest.id,
|
story_id: payload.story_id,
|
||||||
page_number: index + 1,
|
page_number: index + 1,
|
||||||
text: page.text,
|
text: page.text,
|
||||||
image_url: page.image,
|
image_url: page.image,
|
||||||
@ -235,7 +247,7 @@ serve(async (req) => {
|
|||||||
const { error: genError } = await supabase
|
const { error: genError } = await supabase
|
||||||
.from('story_generations')
|
.from('story_generations')
|
||||||
.insert({
|
.insert({
|
||||||
story_id: rest.id,
|
story_id: payload.story_id,
|
||||||
original_prompt: prompt,
|
original_prompt: prompt,
|
||||||
ai_response: completion.choices[0].message.content,
|
ai_response: completion.choices[0].message.content,
|
||||||
model_used: 'gpt-4o-mini'
|
model_used: 'gpt-4o-mini'
|
||||||
@ -250,7 +262,7 @@ serve(async (req) => {
|
|||||||
*,
|
*,
|
||||||
pages:story_pages(*)
|
pages:story_pages(*)
|
||||||
`)
|
`)
|
||||||
.eq('id', rest.id)
|
.eq('id', payload.story_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (fetchError) throw new Error(`Erro ao buscar história completa: ${fetchError.message}`);
|
if (fetchError) throw new Error(`Erro ao buscar história completa: ${fetchError.message}`);
|
||||||
@ -267,7 +279,7 @@ serve(async (req) => {
|
|||||||
if (!pageWithWord) return null;
|
if (!pageWithWord) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
story_id: rest.id,
|
story_id: payload.story_id,
|
||||||
word,
|
word,
|
||||||
exercise_type: type,
|
exercise_type: type,
|
||||||
phonemes: pageWithWord?.phonemes || null,
|
phonemes: pageWithWord?.phonemes || null,
|
||||||
@ -280,7 +292,10 @@ serve(async (req) => {
|
|||||||
if (exerciseWords.length > 0) {
|
if (exerciseWords.length > 0) {
|
||||||
const { error: wordsError } = await supabase
|
const { error: wordsError } = await supabase
|
||||||
.from('story_exercise_words')
|
.from('story_exercise_words')
|
||||||
.insert(exerciseWords);
|
.insert(exerciseWords.map(word => ({
|
||||||
|
...word,
|
||||||
|
story_id: payload.story_id
|
||||||
|
})));
|
||||||
|
|
||||||
if (wordsError) {
|
if (wordsError) {
|
||||||
console.error('Erro ao salvar palavras dos exercícios:', wordsError);
|
console.error('Erro ao salvar palavras dos exercícios:', wordsError);
|
||||||
@ -298,8 +313,8 @@ serve(async (req) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Error] Erro ao gerar história:', error)
|
console.error('[Error] Erro ao gerar história:', error);
|
||||||
console.error('[Error] Stack trace:', error.stack)
|
console.error('[Error] Stack trace:', error.stack);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -311,11 +326,28 @@ serve(async (req) => {
|
|||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
status: 500
|
status: 500
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
function buildPrompt(base: StoryPrompt, voice?: string) {
|
function buildPrompt(base: StoryPrompt, voice?: string) {
|
||||||
|
const languageInstructions = {
|
||||||
|
'pt-BR': {
|
||||||
|
language: 'português do Brasil',
|
||||||
|
instructions: 'Use linguagem apropriada para crianças brasileiras'
|
||||||
|
},
|
||||||
|
'en-US': {
|
||||||
|
language: 'English (US)',
|
||||||
|
instructions: 'Use language appropriate for American children'
|
||||||
|
},
|
||||||
|
'es-ES': {
|
||||||
|
language: 'español de España',
|
||||||
|
instructions: 'Usa un lenguaje apropiado para niños españoles'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedLanguage = languageInstructions[base.language_type as keyof typeof languageInstructions] || languageInstructions['pt-BR'];
|
||||||
|
|
||||||
return `
|
return `
|
||||||
Crie uma história educativa para crianças com as seguintes características:
|
Crie uma história educativa para crianças com as seguintes características:
|
||||||
|
|
||||||
@ -323,18 +355,21 @@ function buildPrompt(base: StoryPrompt, voice?: string) {
|
|||||||
Disciplina: ${base.subject_id}
|
Disciplina: ${base.subject_id}
|
||||||
Personagem Principal: ${base.character_id}
|
Personagem Principal: ${base.character_id}
|
||||||
Cenário: ${base.setting_id}
|
Cenário: ${base.setting_id}
|
||||||
|
Idioma: ${selectedLanguage.language}
|
||||||
${base.context ? `Contexto Adicional: ${base.context}` : ''}
|
${base.context ? `Contexto Adicional: ${base.context}` : ''}
|
||||||
|
|
||||||
Requisitos:
|
Requisitos:
|
||||||
- História adequada para crianças de 6-12 anos
|
- História adequada para crianças de 6-12 anos
|
||||||
- Conteúdo educativo focado em ${base.subject_id}
|
- Conteúdo educativo focado em ${base.subject_id}
|
||||||
|
- ${selectedLanguage.instructions}
|
||||||
- Linguagem clara e envolvente
|
- Linguagem clara e envolvente
|
||||||
- 3-5 páginas de conteúdo
|
- 3-8 páginas de conteúdo
|
||||||
- Cada página deve ter um texto curto e sugestão para uma imagem
|
- Cada página deve ter um texto curto e sugestão para uma imagem
|
||||||
- Evitar conteúdo sensível ou inadequado
|
- Evitar conteúdo sensível ou inadequado
|
||||||
- Incluir elementos de ${base.theme_id}
|
- Incluir elementos de ${base.theme_id}
|
||||||
- Ambientado em ${base.setting_id}
|
- Ambientado em ${base.setting_id}
|
||||||
- Personagem principal baseado em ${base.character_id}
|
- Personagem principal baseado em ${base.character_id}
|
||||||
|
- A resposta precisa ser em JSON
|
||||||
|
|
||||||
Requisitos específicos para exercícios:
|
Requisitos específicos para exercícios:
|
||||||
1. Para o exercício de completar frases:
|
1. Para o exercício de completar frases:
|
||||||
@ -353,7 +388,7 @@ function buildPrompt(base: StoryPrompt, voice?: string) {
|
|||||||
- Inclua palavras com encontros consonantais
|
- Inclua palavras com encontros consonantais
|
||||||
- Priorize palavras que sejam desafiadoras para a faixa etária
|
- Priorize palavras que sejam desafiadoras para a faixa etária
|
||||||
|
|
||||||
Formato da resposta:
|
Formato da resposta em JSON:
|
||||||
{
|
{
|
||||||
"title": "Título da História",
|
"title": "Título da História",
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user