feat: implementa geração de histórias com IA

- Adiciona integração com OpenAI GPT e DALL-E
- Implementa fluxo de geração de histórias
- Adiciona feedback visual do processo
- Melhora tratamento de erros
- Adiciona logs para debug

Resolves: #FEAT-123
This commit is contained in:
Lucas Santana 2024-12-23 09:03:23 -03:00
parent 3701e692f1
commit 03732de610
7 changed files with 447 additions and 191 deletions

27
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@supabase/supabase-js": "^2.39.7", "@supabase/supabase-js": "^2.39.7",
"@tanstack/react-query": "^5.62.8",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.3.1", "react": "^18.3.1",
@ -1673,6 +1674,32 @@
"@supabase/storage-js": "2.7.1" "@supabase/storage-js": "2.7.1"
} }
}, },
"node_modules/@tanstack/query-core": {
"version": "5.62.8",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.8.tgz",
"integrity": "sha512-4fV31vDsUyvNGrKIOUNPrZztoyL187bThnoQOvAXEVlZbSiuPONpfx53634MKKdvsDir5NyOGm80ShFaoHS/mw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.62.8",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.8.tgz",
"integrity": "sha512-8TUstKxF/fysHonZsWg/hnlDVgasTdHx6Q+f1/s/oPKJBJbKUWPZEHwLTMOZgrZuroLMiqYKJ9w69Abm8mWP0Q==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.62.8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",

View File

@ -15,6 +15,7 @@
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@supabase/supabase-js": "^2.39.7", "@supabase/supabase-js": "^2.39.7",
"@tanstack/react-query": "^5.62.8",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -10,6 +10,8 @@ import { AuthUser, SavedStory } from './types/auth';
import { User, Theme } from './types'; import { User, Theme } from './types';
import { AuthProvider } from './contexts/AuthContext' import { AuthProvider } from './contexts/AuthContext'
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Router } from './Router'
type AppStep = type AppStep =
| 'welcome' | 'welcome'
@ -20,6 +22,16 @@ type AppStep =
| 'story' | 'story'
| 'library'; | 'library';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutos
cacheTime: 1000 * 60 * 30, // 30 minutos
refetchOnWindowFocus: false,
},
},
})
export function App() { export function App() {
const navigate = useNavigate(); const navigate = useNavigate();
const [step, setStep] = useState<AppStep>('welcome'); const [step, setStep] = useState<AppStep>('welcome');
@ -74,43 +86,45 @@ export function App() {
}; };
return ( return (
<AuthProvider> <QueryClientProvider client={queryClient}>
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100"> <AuthProvider>
{step === 'welcome' && ( <div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
<WelcomePage {step === 'welcome' && (
onLoginClick={() => setStep('login')} <WelcomePage
onRegisterClick={() => setStep('register')} onLoginClick={() => setStep('login')}
/>
)}
{step === 'login' && (
<div className="min-h-screen flex items-center justify-center p-6">
<LoginForm
userType="school"
onLogin={handleLogin}
onRegisterClick={() => setStep('register')} onRegisterClick={() => setStep('register')}
/> />
</div> )}
)} {step === 'login' && (
{step === 'register' && ( <div className="min-h-screen flex items-center justify-center p-6">
<RegistrationForm <LoginForm
userType="school" userType="school"
onComplete={handleRegistrationComplete} onLogin={handleLogin}
/> onRegisterClick={() => setStep('register')}
)} />
{step === 'avatar' && user && ( </div>
<AvatarSelector user={user} onComplete={handleAvatarComplete} /> )}
)} {step === 'register' && (
{step === 'theme' && <ThemeSelector onSelect={handleThemeSelect} />} <RegistrationForm
{step === 'story' && user && selectedTheme && ( userType="school"
<StoryViewer theme={selectedTheme} user={user} /> onComplete={handleRegistrationComplete}
)} />
{step === 'library' && authUser && ( )}
<StoryLibrary {step === 'avatar' && user && (
stories={savedStories} <AvatarSelector user={user} onComplete={handleAvatarComplete} />
onStorySelect={handleStorySelect} )}
/> {step === 'theme' && <ThemeSelector onSelect={handleThemeSelect} />}
)} {step === 'story' && user && selectedTheme && (
</div> <StoryViewer theme={selectedTheme} user={user} />
</AuthProvider> )}
{step === 'library' && authUser && (
<StoryLibrary
stories={savedStories}
onStorySelect={handleStorySelect}
/>
)}
</div>
</AuthProvider>
</QueryClientProvider>
); );
} }

View File

@ -2,112 +2,74 @@ import React from 'react';
import { useNavigate } from 'react-router-dom'; 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 { Wand2, ArrowLeft, ArrowRight } from 'lucide-react'; import { Wand2, ArrowLeft, ArrowRight } from 'lucide-react';
const THEMES = [ interface Category {
{ id: string;
id: 'aventura', slug: string;
title: 'Aventura', title: string;
description: 'Histórias emocionantes com muita ação', description: string;
icon: '🗺️' icon: string;
}, }
{
id: 'fantasia',
title: 'Fantasia',
description: 'Mundos mágicos e encantados',
icon: '🌟'
}
// ... outros temas
];
const SUBJECTS = [ interface StoryStep {
{ title: string;
id: 'matematica', key?: keyof StoryChoices;
title: 'Matemática', items?: Category[];
description: 'Números e formas de um jeito divertido', isContextStep?: boolean;
icon: '🔢' }
},
{
id: 'ciencias',
title: 'Ciências',
description: 'Descobertas e experimentos incríveis',
icon: '🔬'
}
// ... outras disciplinas
];
const CHARACTERS = [
{
id: 'explorer',
title: 'Explorador(a)',
description: 'Corajoso(a) e curioso(a)',
icon: '🧭'
},
{
id: 'scientist',
title: 'Cientista',
description: 'Inteligente e criativo(a)',
icon: '👩‍🔬'
}
// ... outros personagens
];
const SETTINGS = [
{
id: 'forest',
title: 'Floresta Mágica',
description: 'Um lugar cheio de mistérios',
icon: '🌳'
},
{
id: 'space',
title: 'Espaço Sideral',
description: 'Aventuras entre as estrelas',
icon: '🚀'
}
// ... outros cenários
];
interface StoryChoices { interface StoryChoices {
theme: string | null; theme_id: string | null;
subject: string | null; subject_id: string | null;
character: string | null; character_id: string | null;
setting: string | null; setting_id: string | null;
context?: string;
} }
export function StoryGenerator() { export function StoryGenerator() {
const navigate = useNavigate(); const navigate = useNavigate();
const { session } = useSession(); const { session } = useSession();
const { themes, subjects, characters, settings, isLoading } = useStoryCategories();
const [step, setStep] = React.useState(1); const [step, setStep] = React.useState(1);
const [choices, setChoices] = React.useState<StoryChoices>({ const [choices, setChoices] = React.useState<StoryChoices>({
theme: null, theme_id: null,
subject: null, subject_id: null,
character: null, character_id: null,
setting: null setting_id: null,
context: ''
}); });
const [isGenerating, setIsGenerating] = React.useState(false); const [isGenerating, setIsGenerating] = React.useState(false);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
const [generationStatus, setGenerationStatus] = React.useState<
'idle' | 'creating' | 'generating-images' | 'saving'
>('idle');
const steps = [ const steps: StoryStep[] = [
{ {
title: 'Escolha o Tema', title: 'Escolha o Tema',
items: THEMES, items: themes || [],
key: 'theme' as keyof StoryChoices key: 'theme_id'
}, },
{ {
title: 'Escolha a Disciplina', title: 'Escolha a Disciplina',
items: SUBJECTS, items: subjects || [],
key: 'subject' as keyof StoryChoices key: 'subject_id'
}, },
{ {
title: 'Escolha o Personagem', title: 'Escolha o Personagem',
items: CHARACTERS, items: characters || [],
key: 'character' as keyof StoryChoices key: 'character_id'
}, },
{ {
title: 'Escolha o Cenário', title: 'Escolha o Cenário',
items: SETTINGS, items: settings || [],
key: 'setting' as keyof StoryChoices key: 'setting_id'
},
{
title: 'Adicione um Contexto (Opcional)',
isContextStep: true
} }
]; ];
@ -116,14 +78,15 @@ export function StoryGenerator() {
const handleSelect = (key: keyof StoryChoices, value: string) => { const handleSelect = (key: keyof StoryChoices, value: string) => {
setChoices(prev => ({ ...prev, [key]: value })); setChoices(prev => ({ ...prev, [key]: value }));
};
const handleNext = () => {
if (step < steps.length) { if (step < steps.length) {
setStep(prev => prev + 1); setStep(prev => prev + 1);
} }
}; };
const handleContextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setChoices(prev => ({ ...prev, context: event.target.value }));
};
const handleBack = () => { const handleBack = () => {
if (step > 1) { if (step > 1) {
setStep(prev => prev - 1); setStep(prev => prev - 1);
@ -133,16 +96,26 @@ export function StoryGenerator() {
const handleGenerate = async () => { const handleGenerate = async () => {
if (!session?.user?.id) return; 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.');
return;
}
try { try {
setIsGenerating(true); setIsGenerating(true);
setError(null); setError(null);
setGenerationStatus('creating');
const { data: story, error: storyError } = await supabase const { data: story, error: storyError } = await supabase
.from('stories') .from('stories')
.insert({ .insert({
student_id: session.user.id, student_id: session.user.id,
title: 'Gerando...', title: 'Gerando...',
theme: choices.theme, theme_id: choices.theme_id,
subject_id: choices.subject_id,
character_id: choices.character_id,
setting_id: choices.setting_id,
context: choices.context || null,
status: 'draft', status: 'draft',
content: { content: {
prompt: choices, prompt: choices,
@ -153,15 +126,66 @@ export function StoryGenerator() {
.single(); .single();
if (storyError) throw storyError; if (storyError) throw storyError;
setGenerationStatus('generating-images');
console.log('Chamando Edge Function com:', story);
const { data: functionData, error: functionError } = await supabase.functions
.invoke('generate-story', {
body: { record: story }
});
console.log('Resposta da Edge Function:', functionData);
if (functionError) {
throw new Error(`Erro na Edge Function: ${functionError.message}`);
}
setGenerationStatus('saving');
const { data: updatedStory, error: updateError } = await supabase
.from('stories')
.select('*')
.eq('id', story.id)
.single();
if (updateError) throw updateError;
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 ao gerar história:', err);
setError('Não foi possível criar sua história. Tente novamente.'); setError('Não foi possível criar sua história. Tente novamente.');
} finally { } finally {
setIsGenerating(false); setIsGenerating(false);
setGenerationStatus('idle');
} }
}; };
const getGenerationStatusText = () => {
switch (generationStatus) {
case 'creating':
return 'Iniciando criação...';
case 'generating-images':
return 'Gerando história e imagens...';
case 'saving':
return 'Finalizando...';
default:
return 'Criar História Mágica';
}
};
if (isLoading) {
return (
<div className="animate-pulse space-y-8">
<div className="h-2 bg-gray-200 rounded-full" />
<div className="grid grid-cols-2 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-xl" />
))}
</div>
</div>
);
}
return ( return (
<div className="space-y-8"> <div className="space-y-8">
{/* Progress Bar */} {/* Progress Bar */}
@ -180,28 +204,38 @@ export function StoryGenerator() {
{currentStep.title} {currentStep.title}
</h2> </h2>
{/* Cards Grid */} {currentStep.isContextStep ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="space-y-4">
{currentStep.items.map((item) => ( <textarea
<button value={choices.context}
key={item.id} onChange={handleContextChange}
onClick={() => handleSelect(currentStep.key, item.id)} placeholder="Adicione detalhes ou ideias específicas para sua história..."
className={`p-6 rounded-xl border-2 transition-all text-left ${ className="w-full h-32 p-4 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
choices[currentStep.key] === item.id />
? 'border-purple-500 bg-purple-50' </div>
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50' ) : (
}`} <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
> {currentStep.items?.map((item) => (
<div className="flex items-center gap-3"> <button
<span className="text-2xl">{item.icon}</span> key={item.id}
<div> onClick={() => handleSelect(currentStep.key!, item.id)}
<h3 className="font-medium text-gray-900">{item.title}</h3> className={`p-6 rounded-xl border-2 transition-all text-left ${
<p className="text-sm text-gray-600">{item.description}</p> choices[currentStep.key!] === item.id
? 'border-purple-500 bg-purple-50'
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{item.icon}</span>
<div>
<h3 className="font-medium text-gray-900">{item.title}</h3>
<p className="text-sm text-gray-600">{item.description}</p>
</div>
</div> </div>
</div> </button>
</button> ))}
))} </div>
</div> )}
{error && ( {error && (
<div className="p-4 bg-red-50 text-red-600 rounded-lg text-sm"> <div className="p-4 bg-red-50 text-red-600 rounded-lg text-sm">
@ -220,23 +254,14 @@ export function StoryGenerator() {
Voltar Voltar
</button> </button>
{isLastStep ? ( {isLastStep && (
<button <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={!choices.setting || isGenerating} disabled={!choices.theme_id || !choices.subject_id || !choices.character_id || !choices.setting_id || 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" /> <Wand2 className="h-5 w-5" />
{isGenerating ? 'Criando história...' : 'Criar História Mágica'} {isGenerating ? getGenerationStatusText() : 'Criar História Mágica'}
</button>
) : (
<button
onClick={handleNext}
disabled={!choices[currentStep.key]}
className="flex items-center gap-2 px-4 py-2 text-purple-600 disabled:opacity-50"
>
Próximo
<ArrowRight className="h-5 w-5" />
</button> </button>
)} )}
</div> </div>

View File

@ -0,0 +1,72 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '../lib/supabase';
interface Category {
id: string;
slug: string;
title: string;
description: string;
icon: string;
}
export function useStoryCategories() {
const { data: themes, isLoading: loadingThemes } = useQuery({
queryKey: ['story-themes'],
queryFn: async () => {
const { data, error } = await supabase
.from('story_themes')
.select('*')
.order('title');
if (error) throw error;
return data as Category[];
}
});
const { data: subjects, isLoading: loadingSubjects } = useQuery({
queryKey: ['story-subjects'],
queryFn: async () => {
const { data, error } = await supabase
.from('story_subjects')
.select('*')
.order('title');
if (error) throw error;
return data as Category[];
}
});
const { data: characters, isLoading: loadingCharacters } = useQuery({
queryKey: ['story-characters'],
queryFn: async () => {
const { data, error } = await supabase
.from('story_characters')
.select('*')
.order('title');
if (error) throw error;
return data as Category[];
}
});
const { data: settings, isLoading: loadingSettings } = useQuery({
queryKey: ['story-settings'],
queryFn: async () => {
const { data, error } = await supabase
.from('story_settings')
.select('*')
.order('title');
if (error) throw error;
return data as Category[];
}
});
return {
themes,
subjects,
characters,
settings,
isLoading: loadingThemes || loadingSubjects || loadingCharacters || loadingSettings
};
}

View File

@ -1,11 +1,24 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { router } from './routes'; import { router } from './routes';
import './index.css'; import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutos
cacheTime: 1000 * 60 * 30, // 30 minutos
refetchOnWindowFocus: false,
},
},
});
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode> </StrictMode>
); );

View File

@ -1,67 +1,171 @@
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { Configuration, OpenAIApi } from 'https://esm.sh/openai@3.1.0' import OpenAI from 'https://esm.sh/openai@4.20.1'
const openaiConfig = new Configuration({ const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY') apiKey: Deno.env.get('OPENAI_API_KEY')
}) });
const openai = new OpenAIApi(openaiConfig)
interface StoryPrompt {
theme_id: string;
subject_id: string;
character_id: string;
setting_id: string;
context?: string;
}
serve(async (req) => { serve(async (req) => {
const { record } = await req.json() const { record } = await req.json()
const prompt = record.content.prompt
try { try {
const completion = await openai.createChatCompletion({ // Criar cliente Supabase
model: "gpt-4", const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
)
// Buscar detalhes das categorias selecionadas
const [themeResult, subjectResult, characterResult, settingResult] = await Promise.all([
supabase.from('story_themes').select('*').eq('id', record.theme_id).single(),
supabase.from('story_subjects').select('*').eq('id', record.subject_id).single(),
supabase.from('story_characters').select('*').eq('id', record.character_id).single(),
supabase.from('story_settings').select('*').eq('id', record.setting_id).single()
])
// Log para debug
console.log('Resultados das consultas:', {
theme: themeResult,
subject: subjectResult,
character: characterResult,
setting: settingResult
});
// Verificar erros nas consultas
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 (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}`);
// Verificar se os dados existem
if (!themeResult.data) throw new Error(`Tema não encontrado: ${record.theme_id}`);
if (!subjectResult.data) throw new Error(`Disciplina não encontrada: ${record.subject_id}`);
if (!characterResult.data) throw new Error(`Personagem não encontrado: ${record.character_id}`);
if (!settingResult.data) throw new Error(`Cenário não encontrado: ${record.setting_id}`);
const theme = themeResult.data;
const subject = subjectResult.data;
const character = characterResult.data;
const setting = settingResult.data;
// Log dos dados recebidos
console.log('Record recebido:', record);
console.log('Dados encontrados:', {
theme,
subject,
character,
setting
});
// Construir o prompt para o GPT
const prompt = `
Crie uma história educativa para crianças com as seguintes características:
Tema: ${theme.title}
Disciplina: ${subject.title}
Personagem Principal: ${character.title}
Cenário: ${setting.title}
${record.context ? `Contexto Adicional: ${record.context}` : ''}
Requisitos:
- História adequada para crianças de 6-12 anos
- Conteúdo educativo focado em ${subject.title}
- Linguagem clara e envolvente
- 3-5 páginas de conteúdo
- Cada página deve ter um texto curto e sugestão para uma imagem
- Evitar conteúdo sensível ou inadequado
- Incluir elementos de ${theme.title}
- Ambientado em ${setting.title}
- Personagem principal baseado em ${character.title}
Formato da resposta:
{
"title": "Título da História",
"pages": [
{
"text": "Texto da página",
"image_prompt": "Descrição para gerar a imagem"
}
]
}
`
// Gerar história com GPT-4 Turbo
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [ messages: [
{ {
role: "system", role: "system",
content: `Você é um contador de histórias infantis especializado em criar histórias educativas e envolventes para crianças. content: "Você é um contador de histórias infantis especializado em conteúdo educativo."
Crie uma história com 3-5 páginas, cada uma com 2-3 parágrafos curtos.
A história deve ser apropriada para a idade e incluir elementos dos interesses da criança.
Use linguagem simples e clara, mas inclua algumas das palavras para prática quando apropriado.`
}, },
{ {
role: "user", role: "user",
content: `Crie uma história com os seguintes elementos: content: prompt
Interesses: ${prompt.studentInterests.join(', ')}
Personagem Principal: ${prompt.characters.main}
Cenário: ${prompt.setting.place}
Palavras para prática: ${prompt.practiceWords?.join(', ') || 'N/A'}
Características da criança: ${JSON.stringify(prompt.studentCharacteristics)}
Nível de dificuldade: ${prompt.difficulty}`
} }
] ],
}) temperature: 0.7,
max_tokens: 1000
});
const story = completion.data.choices[0].message?.content const storyContent = JSON.parse(completion.choices[0].message.content || '{}');
if (!story) throw new Error('Falha ao gerar história')
// Processar a história em páginas // Gerar imagens com DALL-E
const pages = story.split('\n\n').map(text => ({ text })) const pages = await Promise.all(
storyContent.pages.map(async (page: any) => {
const imageResponse = await openai.images.generate({
prompt: `${page.image_prompt}. Style: children's book illustration, colorful, educational, safe for kids`,
n: 1,
size: "1024x1024"
});
// Atualizar o registro no banco return {
const supabase = createClient( text: page.text,
Deno.env.get('SUPABASE_URL') ?? '', image: imageResponse.data[0].url
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' }
) })
);
// Atualizar história no Supabase
await supabase await supabase
.from('stories') .from('stories')
.update({ .update({
content: { pages }, title: storyContent.title,
content: {
title: storyContent.title,
pages: pages,
theme: theme.title,
subject: subject.title,
character: character.title,
setting: setting.title,
context: record.context
},
status: 'published' status: 'published'
}) })
.eq('id', record.id) .eq('id', record.id)
return new Response(JSON.stringify({ success: true }), { return new Response(
headers: { 'Content-Type': 'application/json' } JSON.stringify({ success: true }),
}) { headers: { 'Content-Type': 'application/json' } }
)
} catch (error) { } catch (error) {
return new Response(JSON.stringify({ error: error.message }), { console.error('Erro ao gerar história:', error)
headers: { 'Content-Type': 'application/json' },
status: 500 return new Response(
}) JSON.stringify({ error: error.message }),
{
headers: { 'Content-Type': 'application/json' },
status: 500
}
)
} }
}) })