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 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
This commit is contained in:
parent
1a3a603ff6
commit
0b8c050bd7
17
package-lock.json
generated
17
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"resend": "^3.2.0",
|
"resend": "^3.2.0",
|
||||||
"tailwind-merge": "^2.5.5"
|
"tailwind-merge": "^2.5.5"
|
||||||
@ -4243,6 +4244,22 @@
|
|||||||
"react": "^18.3.1"
|
"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": {
|
"node_modules/react-promise-suspense": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"resend": "^3.2.0",
|
"resend": "^3.2.0",
|
||||||
"tailwind-merge": "^2.5.5"
|
"tailwind-merge": "^2.5.5"
|
||||||
|
|||||||
245
src/components/story/StoryGenerator.tsx
Normal file
245
src/components/story/StoryGenerator.tsx
Normal file
@ -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<StoryChoices>({
|
||||||
|
theme: null,
|
||||||
|
subject: null,
|
||||||
|
character: null,
|
||||||
|
setting: null
|
||||||
|
});
|
||||||
|
const [isGenerating, setIsGenerating] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState<string | null>(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 (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="flex gap-2 mb-8">
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`h-2 rounded-full flex-1 ${
|
||||||
|
i + 1 <= step ? 'bg-purple-600' : 'bg-gray-200'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-medium text-gray-900 mb-6">
|
||||||
|
{currentStep.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Cards Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{currentStep.items.map((item) => (
|
||||||
|
<button
|
||||||
|
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
|
||||||
|
? '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>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-600 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div className="flex justify-between pt-6">
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={step === 1}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 text-gray-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
Voltar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isLastStep ? (
|
||||||
|
<button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={!choices.setting || 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" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
src/hooks/useSession.ts
Normal file
28
src/hooks/useSession.ts
Normal file
@ -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<Session | null>(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 };
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { Story } from '../../types/database';
|
import { Story, StoryRecording } from '../../types/database';
|
||||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import type { MetricsData } from '../../components/story/StoryMetrics';
|
import type { MetricsData } from '../../components/story/StoryMetrics';
|
||||||
|
|||||||
@ -1,83 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ArrowLeft, Sparkles, Send } from 'lucide-react';
|
import { ArrowLeft, Sparkles } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { StoryGenerator } from '../../components/story/StoryGenerator';
|
||||||
|
import { useSession } from '../../hooks/useSession';
|
||||||
interface StoryForm {
|
|
||||||
title: string;
|
|
||||||
theme: string;
|
|
||||||
prompt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateStoryPage() {
|
export function CreateStoryPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [formData, setFormData] = React.useState<StoryForm>({
|
const { session } = useSession();
|
||||||
title: '',
|
|
||||||
theme: '',
|
|
||||||
prompt: ''
|
|
||||||
});
|
|
||||||
const [generating, setGenerating] = React.useState(false);
|
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const themes = [
|
if (!session) {
|
||||||
{ id: 'nature', name: 'Natureza e Meio Ambiente' },
|
return (
|
||||||
{ id: 'culture', name: 'Cultura Brasileira' },
|
<div className="text-center py-12">
|
||||||
{ id: 'science', name: 'Ciência e Descobertas' },
|
<p className="text-gray-600">Você precisa estar logado para criar histórias.</p>
|
||||||
{ id: 'adventure', name: 'Aventura e Exploração' },
|
<button
|
||||||
{ id: 'friendship', name: 'Amizade e Cooperação' }
|
onClick={() => navigate('/login')}
|
||||||
];
|
className="mt-4 text-purple-600 hover:text-purple-700"
|
||||||
|
>
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
Fazer login
|
||||||
const { name, value } = e.target;
|
</button>
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -90,7 +34,17 @@ export function CreateStoryPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Criar Nova História</h1>
|
<div className="flex items-center gap-3 mb-8">
|
||||||
|
<div className="p-2 bg-purple-100 rounded-lg">
|
||||||
|
<Sparkles className="h-6 w-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Criar Nova História</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Vamos criar uma história personalizada baseada nos seus interesses
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
||||||
@ -98,88 +52,20 @@ export function CreateStoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
|
<StoryGenerator />
|
||||||
<div>
|
|
||||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Título da História
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="title"
|
|
||||||
name="title"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full rounded-lg border-gray-300 focus:ring-purple-500 focus:border-purple-500"
|
|
||||||
required
|
|
||||||
placeholder="Ex: A Aventura na Floresta Encantada"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div className="mt-8 p-4 bg-purple-50 rounded-lg">
|
||||||
<label htmlFor="theme" className="block text-sm font-medium text-gray-700 mb-1">
|
<h3 className="text-sm font-medium text-purple-900 mb-2">
|
||||||
Tema
|
Como funciona?
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="theme"
|
|
||||||
name="theme"
|
|
||||||
value={formData.theme}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full rounded-lg border-gray-300 focus:ring-purple-500 focus:border-purple-500"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Selecione um tema</option>
|
|
||||||
{themes.map(theme => (
|
|
||||||
<option key={theme.id} value={theme.id}>
|
|
||||||
{theme.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Sobre o que você quer escrever?
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="prompt"
|
|
||||||
name="prompt"
|
|
||||||
value={formData.prompt}
|
|
||||||
onChange={handleChange}
|
|
||||||
rows={4}
|
|
||||||
className="w-full rounded-lg border-gray-300 focus:ring-purple-500 focus:border-purple-500"
|
|
||||||
required
|
|
||||||
placeholder="Descreva sua ideia para a história..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-purple-50 rounded-lg p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="p-2 bg-purple-100 rounded-lg">
|
|
||||||
<Sparkles className="h-5 w-5 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-purple-900">
|
|
||||||
Assistente de Criação
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-purple-700">
|
<ol className="text-sm text-purple-700 space-y-2">
|
||||||
Nossa IA vai ajudar você a criar uma história incrível baseada nas suas ideias!
|
<li>1. Conte-nos sobre seus interesses e preferências</li>
|
||||||
</p>
|
<li>2. Escolha personagens e cenários para sua história</li>
|
||||||
|
<li>3. Nossa IA criará uma história única e personalizada</li>
|
||||||
|
<li>4. Você poderá ler e praticar com sua nova história</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end pt-6">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={generating}
|
|
||||||
className="flex items-center gap-2 px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Send className="h-5 w-5" />
|
|
||||||
{generating ? 'Criando História...' : 'Criar História'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
29
src/types/story-generator.ts
Normal file
29
src/types/story-generator.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export interface StoryPrompt {
|
||||||
|
studentInterests: string[];
|
||||||
|
characters: {
|
||||||
|
main: string;
|
||||||
|
supporting?: string[];
|
||||||
|
};
|
||||||
|
setting: {
|
||||||
|
place: string;
|
||||||
|
time?: string;
|
||||||
|
};
|
||||||
|
practiceWords?: string[];
|
||||||
|
studentCharacteristics?: {
|
||||||
|
age?: number;
|
||||||
|
gender?: string;
|
||||||
|
personalityTraits?: string[];
|
||||||
|
};
|
||||||
|
theme?: string;
|
||||||
|
difficulty?: 'easy' | 'medium' | 'hard';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratedStory {
|
||||||
|
title: string;
|
||||||
|
content: {
|
||||||
|
pages: {
|
||||||
|
text: string;
|
||||||
|
image?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
67
supabase/functions/generate-story/index.ts
Normal file
67
supabase/functions/generate-story/index.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
const openaiConfig = new Configuration({
|
||||||
|
apiKey: Deno.env.get('OPENAI_API_KEY')
|
||||||
|
})
|
||||||
|
const openai = new OpenAIApi(openaiConfig)
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
const { record } = await req.json()
|
||||||
|
const prompt = record.content.prompt
|
||||||
|
|
||||||
|
try {
|
||||||
|
const completion = await openai.createChatCompletion({
|
||||||
|
model: "gpt-4",
|
||||||
|
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.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
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') ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.from('stories')
|
||||||
|
.update({
|
||||||
|
content: { pages },
|
||||||
|
status: 'published'
|
||||||
|
})
|
||||||
|
.eq('id', record.id)
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: error.message }), {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
status: 500
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user