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-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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
14
src/App.tsx
14
src/App.tsx
@ -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,6 +86,7 @@ export function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
<div className="min-h-screen bg-gradient-to-b from-purple-100 to-blue-100">
|
||||||
{step === 'welcome' && (
|
{step === 'welcome' && (
|
||||||
@ -112,5 +125,6 @@ export function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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,14 +204,23 @@ export function StoryGenerator() {
|
|||||||
{currentStep.title}
|
{currentStep.title}
|
||||||
</h2>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{currentStep.items.map((item) => (
|
{currentStep.items?.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
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 ${
|
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-purple-500 bg-purple-50'
|
||||||
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
|
: 'border-gray-200 hover:border-purple-200 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
@ -202,6 +235,7 @@ export function StoryGenerator() {
|
|||||||
</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>
|
||||||
|
|||||||
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 { 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>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
@ -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}
|
temperature: 0.7,
|
||||||
Palavras para prática: ${prompt.practiceWords?.join(', ') || 'N/A'}
|
max_tokens: 1000
|
||||||
Características da criança: ${JSON.stringify(prompt.studentCharacteristics)}
|
});
|
||||||
Nível de dificuldade: ${prompt.difficulty}`
|
|
||||||
|
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
|
// Atualizar história no Supabase
|
||||||
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') ?? ''
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message }),
|
||||||
|
{
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
status: 500
|
status: 500
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Loading…
Reference in New Issue
Block a user