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",
|
||||
"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",
|
||||
|
||||
@ -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"
|
||||
|
||||
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 { 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';
|
||||
|
||||
@ -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<StoryForm>({
|
||||
title: '',
|
||||
theme: '',
|
||||
prompt: ''
|
||||
});
|
||||
const [generating, setGenerating] = React.useState(false);
|
||||
const { session } = useSession();
|
||||
const [error, setError] = React.useState<string | null>(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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-600">Você precisa estar logado para criar histórias.</p>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="mt-4 text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Fazer login
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -90,7 +34,17 @@ export function CreateStoryPage() {
|
||||
</button>
|
||||
|
||||
<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 && (
|
||||
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
||||
@ -98,87 +52,19 @@ export function CreateStoryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
|
||||
<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>
|
||||
<StoryGenerator />
|
||||
|
||||
<div>
|
||||
<label htmlFor="theme" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tema
|
||||
</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>
|
||||
<p className="text-sm text-purple-700">
|
||||
Nossa IA vai ajudar você a criar uma história incrível baseada nas suas ideias!
|
||||
</p>
|
||||
</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 className="mt-8 p-4 bg-purple-50 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-purple-900 mb-2">
|
||||
Como funciona?
|
||||
</h3>
|
||||
<ol className="text-sm text-purple-700 space-y-2">
|
||||
<li>1. Conte-nos sobre seus interesses e preferências</li>
|
||||
<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>
|
||||
);
|
||||
|
||||
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