From 03732de6102e7955ad6800d069a73a85911b1831 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Mon, 23 Dec 2024 09:03:23 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementa=20gera=C3=A7=C3=A3o=20de=20h?= =?UTF-8?q?ist=C3=B3rias=20com=20IA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- package-lock.json | 27 +++ package.json | 1 + src/App.tsx | 86 ++++--- src/components/story/StoryGenerator.tsx | 257 +++++++++++---------- src/hooks/useStoryCategories.ts | 72 ++++++ src/main.tsx | 15 +- supabase/functions/generate-story/index.ts | 180 ++++++++++++--- 7 files changed, 447 insertions(+), 191 deletions(-) create mode 100644 src/hooks/useStoryCategories.ts diff --git a/package-lock.json b/package-lock.json index bbd9ffc..e04903a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3795189..3f2daed 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 9604ff1..47e3dae 100644 --- a/src/App.tsx +++ b/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('welcome'); @@ -74,43 +86,45 @@ export function App() { }; return ( - -
- {step === 'welcome' && ( - setStep('login')} - onRegisterClick={() => setStep('register')} - /> - )} - {step === 'login' && ( -
- + +
+ {step === 'welcome' && ( + setStep('login')} onRegisterClick={() => setStep('register')} /> -
- )} - {step === 'register' && ( - - )} - {step === 'avatar' && user && ( - - )} - {step === 'theme' && } - {step === 'story' && user && selectedTheme && ( - - )} - {step === 'library' && authUser && ( - - )} -
- + )} + {step === 'login' && ( +
+ setStep('register')} + /> +
+ )} + {step === 'register' && ( + + )} + {step === 'avatar' && user && ( + + )} + {step === 'theme' && } + {step === 'story' && user && selectedTheme && ( + + )} + {step === 'library' && authUser && ( + + )} +
+
+ ); } \ No newline at end of file diff --git a/src/components/story/StoryGenerator.tsx b/src/components/story/StoryGenerator.tsx index 4e00775..d849f2c 100644 --- a/src/components/story/StoryGenerator.tsx +++ b/src/components/story/StoryGenerator.tsx @@ -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: '🌟' - } - // ... outros temas -]; +interface Category { + id: string; + slug: string; + title: string; + description: string; + icon: string; +} -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: '🔬' - } - // ... 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 StoryStep { + title: string; + key?: keyof StoryChoices; + items?: Category[]; + isContextStep?: boolean; +} 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({ - 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(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) => { + 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 ( +
+
+
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+ ); + } + return (
{/* Progress Bar */} @@ -180,28 +204,38 @@ export function StoryGenerator() { {currentStep.title} - {/* Cards Grid */} -
- {currentStep.items.map((item) => ( -