From 0b8c050bd74ed4fadc62a13c0ebc44e587dc94d6 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Sun, 22 Dec 2024 16:42:39 -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 fluxo de criação em etapas com cards - Implementa Edge Function para geração via GPT-4 - Cria interfaces e tipos para o gerador de histórias - Adiciona seleção de tema, disciplina, personagem e cenário - Integra com Supabase para armazenamento e processamento - Melhora UX com feedback visual e navegação intuitiva --- package-lock.json | 17 ++ package.json | 1 + src/components/story/StoryGenerator.tsx | 245 ++++++++++++++++++ src/hooks/useSession.ts | 28 ++ src/pages/story/StoryPage.tsx | 2 +- .../student-dashboard/CreateStoryPage.tsx | 194 +++----------- src/types/story-generator.ts | 29 +++ supabase/functions/generate-story/index.ts | 67 +++++ 8 files changed, 428 insertions(+), 155 deletions(-) create mode 100644 src/components/story/StoryGenerator.tsx create mode 100644 src/hooks/useSession.ts create mode 100644 src/types/story-generator.ts create mode 100644 supabase/functions/generate-story/index.ts diff --git a/package-lock.json b/package-lock.json index 4cb5eb0..bbd9ffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", "react-router-dom": "^6.28.0", "resend": "^3.2.0", "tailwind-merge": "^2.5.5" @@ -4243,6 +4244,22 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", diff --git a/package.json b/package.json index 0aeb344..3795189 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", "react-router-dom": "^6.28.0", "resend": "^3.2.0", "tailwind-merge": "^2.5.5" diff --git a/src/components/story/StoryGenerator.tsx b/src/components/story/StoryGenerator.tsx new file mode 100644 index 0000000..49e3f3c --- /dev/null +++ b/src/components/story/StoryGenerator.tsx @@ -0,0 +1,245 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { supabase } from '../../lib/supabase'; +import { useSession } from '../../hooks/useSession'; +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 +]; + +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 StoryChoices { + theme: string | null; + subject: string | null; + character: string | null; + setting: string | null; +} + +export function StoryGenerator() { + const navigate = useNavigate(); + const { session } = useSession(); + const [step, setStep] = React.useState(1); + const [choices, setChoices] = React.useState({ + theme: null, + subject: null, + character: null, + setting: null + }); + const [isGenerating, setIsGenerating] = React.useState(false); + const [error, setError] = React.useState(null); + + const steps = [ + { + title: 'Escolha o Tema', + items: THEMES, + key: 'theme' as keyof StoryChoices + }, + { + title: 'Escolha a Disciplina', + items: SUBJECTS, + key: 'subject' as keyof StoryChoices + }, + { + title: 'Escolha o Personagem', + items: CHARACTERS, + key: 'character' as keyof StoryChoices + }, + { + title: 'Escolha o Cenário', + items: SETTINGS, + key: 'setting' as keyof StoryChoices + } + ]; + + const currentStep = steps[step - 1]; + const isLastStep = step === steps.length; + + const handleSelect = (key: keyof StoryChoices, value: string) => { + setChoices(prev => ({ ...prev, [key]: value })); + }; + + const handleNext = () => { + if (step < steps.length) { + setStep(prev => prev + 1); + } + }; + + const handleBack = () => { + if (step > 1) { + setStep(prev => prev - 1); + } + }; + + const handleGenerate = async () => { + if (!session?.user?.id) return; + + try { + setIsGenerating(true); + setError(null); + + const { data: story, error: storyError } = await supabase + .from('stories') + .insert({ + student_id: session.user.id, + title: 'Gerando...', + theme: choices.theme, + status: 'generating', + content: { + prompt: choices, + pages: [] + } + }) + .select() + .single(); + + if (storyError) throw storyError; + 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); + } + }; + + return ( +
+ {/* Progress Bar */} +
+ {steps.map((s, i) => ( +
+ ))} +
+ +

+ {currentStep.title} +

+ + {/* Cards Grid */} +
+ {currentStep.items.map((item) => ( + + ))} +
+ + {error && ( +
+ {error} +
+ )} + + {/* Navigation Buttons */} +
+ + + {isLastStep ? ( + + ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/hooks/useSession.ts b/src/hooks/useSession.ts new file mode 100644 index 0000000..e44c8ac --- /dev/null +++ b/src/hooks/useSession.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; +import { Session } from '@supabase/supabase-js'; +import { supabase } from '../lib/supabase'; + +export function useSession() { + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Pega a sessão atual + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + setLoading(false); + }); + + // Escuta mudanças na autenticação + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setSession(session); + setLoading(false); + }); + + return () => subscription.unsubscribe(); + }, []); + + return { session, loading }; +} \ No newline at end of file diff --git a/src/pages/story/StoryPage.tsx b/src/pages/story/StoryPage.tsx index 59369ef..048bcf4 100644 --- a/src/pages/story/StoryPage.tsx +++ b/src/pages/story/StoryPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { supabase } from '../../lib/supabase'; -import { Story } from '../../types/database'; +import { Story, StoryRecording } from '../../types/database'; import { AudioRecorder } from '../../components/story/AudioRecorder'; import { Loader2 } from 'lucide-react'; import type { MetricsData } from '../../components/story/StoryMetrics'; diff --git a/src/pages/student-dashboard/CreateStoryPage.tsx b/src/pages/student-dashboard/CreateStoryPage.tsx index b536305..f81fd43 100644 --- a/src/pages/student-dashboard/CreateStoryPage.tsx +++ b/src/pages/student-dashboard/CreateStoryPage.tsx @@ -1,83 +1,27 @@ import React from 'react'; -import { ArrowLeft, Sparkles, Send } from 'lucide-react'; +import { ArrowLeft, Sparkles } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; -import { supabase } from '../../lib/supabase'; - -interface StoryForm { - title: string; - theme: string; - prompt: string; -} +import { StoryGenerator } from '../../components/story/StoryGenerator'; +import { useSession } from '../../hooks/useSession'; export function CreateStoryPage() { const navigate = useNavigate(); - const [formData, setFormData] = React.useState({ - title: '', - theme: '', - prompt: '' - }); - const [generating, setGenerating] = React.useState(false); + const { session } = useSession(); const [error, setError] = React.useState(null); - const themes = [ - { id: 'nature', name: 'Natureza e Meio Ambiente' }, - { id: 'culture', name: 'Cultura Brasileira' }, - { id: 'science', name: 'Ciência e Descobertas' }, - { id: 'adventure', name: 'Aventura e Exploração' }, - { id: 'friendship', name: 'Amizade e Cooperação' } - ]; - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setGenerating(true); - setError(null); - - try { - const { data: { session } } = await supabase.auth.getSession(); - if (!session?.user?.id) throw new Error('Usuário não autenticado'); - - // Em produção: Integrar com API de IA para gerar história - const generatedStory = { - title: formData.title, - content: { - pages: [ - { - text: "Era uma vez...", - image: "https://images.unsplash.com/photo-1472162072942-cd5147eb3902" - } - ] - }, - theme: formData.theme, - status: 'draft' - }; - - const { error: saveError } = await supabase - .from('stories') - .insert({ - student_id: session.user.id, - title: generatedStory.title, - theme: generatedStory.theme, - content: generatedStory.content, - status: generatedStory.status, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }); - - if (saveError) throw saveError; - navigate('/aluno/historias'); - - } catch (err) { - console.error('Erro ao criar história:', err); - setError('Não foi possível criar sua história. Tente novamente.'); - } finally { - setGenerating(false); - } - }; + if (!session) { + return ( +
+

Você precisa estar logado para criar histórias.

+ +
+ ); + } return (
@@ -90,7 +34,17 @@ export function CreateStoryPage() {
-

Criar Nova História

+
+
+ +
+
+

Criar Nova História

+

+ Vamos criar uma história personalizada baseada nos seus interesses +

+
+
{error && (
@@ -98,87 +52,19 @@ export function CreateStoryPage() {
)} -
-
- - -
+ -
- - -
- -
- -