mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
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:
parent
3701e692f1
commit
03732de610
27
package-lock.json
generated
27
package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@supabase/supabase-js": "^2.39.7",
|
||||
"@tanstack/react-query": "^5.62.8",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
@ -1673,6 +1674,32 @@
|
||||
"@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": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@supabase/supabase-js": "^2.39.7",
|
||||
"@tanstack/react-query": "^5.62.8",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
14
src/App.tsx
14
src/App.tsx
@ -10,6 +10,8 @@ import { AuthUser, SavedStory } from './types/auth';
|
||||
import { User, Theme } from './types';
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Router } from './Router'
|
||||
|
||||
type AppStep =
|
||||
| 'welcome'
|
||||
@ -20,6 +22,16 @@ type AppStep =
|
||||
| 'story'
|
||||
| 'library';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutos
|
||||
cacheTime: 1000 * 60 * 30, // 30 minutos
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function App() {
|
||||
const navigate = useNavigate();
|
||||
const [step, setStep] = useState<AppStep>('welcome');
|
||||
@ -74,6 +86,7 @@ export function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
||||
{step === 'welcome' && (
|
||||
@ -112,5 +125,6 @@ export function App() {
|
||||
)}
|
||||
</div>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@ -2,112 +2,74 @@ import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useSession } from '../../hooks/useSession';
|
||||
import { useStoryCategories } from '../../hooks/useStoryCategories';
|
||||
import { Wand2, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
|
||||
const THEMES = [
|
||||
{
|
||||
id: 'aventura',
|
||||
title: 'Aventura',
|
||||
description: 'Histórias emocionantes com muita ação',
|
||||
icon: '🗺️'
|
||||
},
|
||||
{
|
||||
id: 'fantasia',
|
||||
title: 'Fantasia',
|
||||
description: 'Mundos mágicos e encantados',
|
||||
icon: '🌟'
|
||||
interface Category {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
// ... outros temas
|
||||
];
|
||||
|
||||
const SUBJECTS = [
|
||||
{
|
||||
id: 'matematica',
|
||||
title: 'Matemática',
|
||||
description: 'Números e formas de um jeito divertido',
|
||||
icon: '🔢'
|
||||
},
|
||||
{
|
||||
id: 'ciencias',
|
||||
title: 'Ciências',
|
||||
description: 'Descobertas e experimentos incríveis',
|
||||
icon: '🔬'
|
||||
interface StoryStep {
|
||||
title: string;
|
||||
key?: keyof StoryChoices;
|
||||
items?: Category[];
|
||||
isContextStep?: boolean;
|
||||
}
|
||||
// ... 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 {
|
||||
theme: string | null;
|
||||
subject: string | null;
|
||||
character: string | null;
|
||||
setting: string | null;
|
||||
theme_id: string | null;
|
||||
subject_id: string | null;
|
||||
character_id: string | null;
|
||||
setting_id: string | null;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export function StoryGenerator() {
|
||||
const navigate = useNavigate();
|
||||
const { session } = useSession();
|
||||
const { themes, subjects, characters, settings, isLoading } = useStoryCategories();
|
||||
const [step, setStep] = React.useState(1);
|
||||
const [choices, setChoices] = React.useState<StoryChoices>({
|
||||
theme: null,
|
||||
subject: null,
|
||||
character: null,
|
||||
setting: null
|
||||
theme_id: null,
|
||||
subject_id: null,
|
||||
character_id: null,
|
||||
setting_id: null,
|
||||
context: ''
|
||||
});
|
||||
const [isGenerating, setIsGenerating] = React.useState(false);
|
||||
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',
|
||||
items: THEMES,
|
||||
key: 'theme' as keyof StoryChoices
|
||||
items: themes || [],
|
||||
key: 'theme_id'
|
||||
},
|
||||
{
|
||||
title: 'Escolha a Disciplina',
|
||||
items: SUBJECTS,
|
||||
key: 'subject' as keyof StoryChoices
|
||||
items: subjects || [],
|
||||
key: 'subject_id'
|
||||
},
|
||||
{
|
||||
title: 'Escolha o Personagem',
|
||||
items: CHARACTERS,
|
||||
key: 'character' as keyof StoryChoices
|
||||
items: characters || [],
|
||||
key: 'character_id'
|
||||
},
|
||||
{
|
||||
title: 'Escolha o Cenário',
|
||||
items: SETTINGS,
|
||||
key: 'setting' as keyof StoryChoices
|
||||
items: settings || [],
|
||||
key: 'setting_id'
|
||||
},
|
||||
{
|
||||
title: 'Adicione um Contexto (Opcional)',
|
||||
isContextStep: true
|
||||
}
|
||||
];
|
||||
|
||||
@ -116,14 +78,15 @@ export function StoryGenerator() {
|
||||
|
||||
const handleSelect = (key: keyof StoryChoices, value: string) => {
|
||||
setChoices(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (step < steps.length) {
|
||||
setStep(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setChoices(prev => ({ ...prev, context: event.target.value }));
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (step > 1) {
|
||||
setStep(prev => prev - 1);
|
||||
@ -133,16 +96,26 @@ export function StoryGenerator() {
|
||||
const handleGenerate = async () => {
|
||||
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 {
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
setGenerationStatus('creating');
|
||||
|
||||
const { data: story, error: storyError } = await supabase
|
||||
.from('stories')
|
||||
.insert({
|
||||
student_id: session.user.id,
|
||||
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',
|
||||
content: {
|
||||
prompt: choices,
|
||||
@ -153,15 +126,66 @@ export function StoryGenerator() {
|
||||
.single();
|
||||
|
||||
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}`);
|
||||
} catch (err) {
|
||||
console.error('Erro ao gerar história:', err);
|
||||
setError('Não foi possível criar sua história. Tente novamente.');
|
||||
} finally {
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
{/* Progress Bar */}
|
||||
@ -180,14 +204,23 @@ export function StoryGenerator() {
|
||||
{currentStep.title}
|
||||
</h2>
|
||||
|
||||
{/* Cards Grid */}
|
||||
{currentStep.isContextStep ? (
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={choices.context}
|
||||
onChange={handleContextChange}
|
||||
placeholder="Adicione detalhes ou ideias específicas para sua história..."
|
||||
className="w-full h-32 p-4 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{currentStep.items.map((item) => (
|
||||
{currentStep.items?.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleSelect(currentStep.key, item.id)}
|
||||
onClick={() => handleSelect(currentStep.key!, item.id)}
|
||||
className={`p-6 rounded-xl border-2 transition-all text-left ${
|
||||
choices[currentStep.key] === item.id
|
||||
choices[currentStep.key!] === item.id
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
|
||||
}`}
|
||||
@ -202,6 +235,7 @@ export function StoryGenerator() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-lg text-sm">
|
||||
@ -220,23 +254,14 @@ export function StoryGenerator() {
|
||||
Voltar
|
||||
</button>
|
||||
|
||||
{isLastStep ? (
|
||||
{isLastStep && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Wand2 className="h-5 w-5" />
|
||||
{isGenerating ? 'Criando história...' : '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" />
|
||||
{isGenerating ? getGenerationStatusText() : 'Criar História Mágica'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
72
src/hooks/useStoryCategories.ts
Normal file
72
src/hooks/useStoryCategories.ts
Normal 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
|
||||
};
|
||||
}
|
||||
13
src/main.tsx
13
src/main.tsx
@ -1,11 +1,24 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { router } from './routes';
|
||||
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(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
@ -1,67 +1,171 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
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')
|
||||
})
|
||||
const openai = new OpenAIApi(openaiConfig)
|
||||
});
|
||||
|
||||
interface StoryPrompt {
|
||||
theme_id: string;
|
||||
subject_id: string;
|
||||
character_id: string;
|
||||
setting_id: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
const { record } = await req.json()
|
||||
const prompt = record.content.prompt
|
||||
|
||||
try {
|
||||
const completion = await openai.createChatCompletion({
|
||||
model: "gpt-4",
|
||||
// Criar cliente Supabase
|
||||
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: [
|
||||
{
|
||||
role: "system",
|
||||
content: `Você é um contador de histórias infantis especializado em criar histórias educativas e envolventes para crianças.
|
||||
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.`
|
||||
content: "Você é um contador de histórias infantis especializado em conteúdo educativo."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Crie uma história com os seguintes elementos:
|
||||
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}`
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000
|
||||
});
|
||||
|
||||
const storyContent = JSON.parse(completion.choices[0].message.content || '{}');
|
||||
|
||||
// Gerar imagens com DALL-E
|
||||
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"
|
||||
});
|
||||
|
||||
return {
|
||||
text: page.text,
|
||||
image: imageResponse.data[0].url
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
const story = completion.data.choices[0].message?.content
|
||||
if (!story) throw new Error('Falha ao gerar história')
|
||||
|
||||
// Processar a história em páginas
|
||||
const pages = story.split('\n\n').map(text => ({ text }))
|
||||
|
||||
// Atualizar o registro no banco
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||
)
|
||||
|
||||
// Atualizar história no Supabase
|
||||
await supabase
|
||||
.from('stories')
|
||||
.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'
|
||||
})
|
||||
.eq('id', record.id)
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
console.error('Erro ao gerar história:', error)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 500
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user