mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
refactor: atualiza estrutura de dados das histórias
- Migra dados das páginas para tabela story_pages - Atualiza queries para usar nova estrutura - Separa componente de demo em StoryPageDemo - Mantém compatibilidade com interface existente - Melhora tipagem e tratamento de erros
This commit is contained in:
parent
8af9950ed7
commit
961fce03f6
12
CHANGELOG.md
12
CHANGELOG.md
@ -21,11 +21,21 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
- Navegação automática entre etapas
|
- Navegação automática entre etapas
|
||||||
- Tratamento de erros com feedback visual
|
- Tratamento de erros com feedback visual
|
||||||
|
|
||||||
|
- Nova estrutura de dados para páginas de histórias
|
||||||
|
- Tabela `story_pages` para melhor organização
|
||||||
|
- Relacionamentos explícitos entre histórias e páginas
|
||||||
|
- Suporte a ordenação por número da página
|
||||||
|
|
||||||
### Modificado
|
### Modificado
|
||||||
- Atualização do schema do banco para suportar novas categorias
|
- Atualização do schema do banco para suportar novas categorias
|
||||||
- Adição de tabelas para temas, disciplinas, personagens e cenários
|
- Adição de tabelas para temas, disciplinas, personagens e cenários
|
||||||
- Relacionamentos entre histórias e categorias
|
- Relacionamentos entre histórias e categorias
|
||||||
- Índices para otimização de consultas
|
- Índices para otimização de consultas
|
||||||
|
- Renomeado componente StoryPage para StoryPageDemo para melhor organização
|
||||||
|
- Separado visualização de histórias demo da visualização principal
|
||||||
|
- Migração de dados das páginas para nova estrutura
|
||||||
|
- Mantida compatibilidade com interface existente
|
||||||
|
- Melhor organização e tipagem dos dados
|
||||||
|
|
||||||
### Técnico
|
### Técnico
|
||||||
- Implementação de logs estruturados com prefixos por contexto
|
- Implementação de logs estruturados com prefixos por contexto
|
||||||
@ -33,6 +43,8 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
- Tratamento de respostas da IA com fallbacks
|
- Tratamento de respostas da IA com fallbacks
|
||||||
- Otimização de queries no banco de dados
|
- Otimização de queries no banco de dados
|
||||||
- Feedback em tempo real do processo de geração
|
- Feedback em tempo real do processo de geração
|
||||||
|
- Queries otimizadas para nova estrutura de dados
|
||||||
|
- Melhor tratamento de estados de loading e erro
|
||||||
|
|
||||||
### Segurança
|
### Segurança
|
||||||
- Validação de dados de entrada na edge function
|
- Validação de dados de entrada na edge function
|
||||||
|
|||||||
@ -27,6 +27,12 @@ Histórias Mágicas é uma aplicação web desenvolvida em React que permite que
|
|||||||
- Tailwind CSS
|
- Tailwind CSS
|
||||||
- Lucide React (ícones)
|
- Lucide React (ícones)
|
||||||
- Vite
|
- Vite
|
||||||
|
- Supabase
|
||||||
|
- Supabase Functions
|
||||||
|
- OpenAI
|
||||||
|
- DALL-E
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Como Executar
|
## 🚀 Como Executar
|
||||||
|
|
||||||
|
|||||||
@ -1,189 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { supabase } from '../../lib/supabase';
|
|
||||||
import { Story, StoryRecording } from '../../types/database';
|
|
||||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
import type { MetricsData } from '../../components/story/StoryMetrics';
|
|
||||||
|
|
||||||
interface StoryPageProps {
|
|
||||||
demo?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const demoRecording: StoryRecording = {
|
|
||||||
id: 'demo-recording',
|
|
||||||
fluency_score: 85,
|
|
||||||
pronunciation_score: 92,
|
|
||||||
accuracy_score: 88,
|
|
||||||
comprehension_score: 90,
|
|
||||||
words_per_minute: 120,
|
|
||||||
pause_count: 3,
|
|
||||||
error_count: 2,
|
|
||||||
self_corrections: 1,
|
|
||||||
strengths: [
|
|
||||||
'Leitura fluente e natural',
|
|
||||||
'Boa pronúncia das palavras',
|
|
||||||
'Respeito à pontuação'
|
|
||||||
],
|
|
||||||
improvements: [
|
|
||||||
'Pode melhorar o ritmo em algumas partes',
|
|
||||||
'Atenção a algumas palavras específicas'
|
|
||||||
],
|
|
||||||
suggestions: 'Tente manter um ritmo mais constante durante toda a leitura',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
processed_at: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
export function StoryPage({ demo = false }: StoryPageProps): JSX.Element {
|
|
||||||
const { storyId } = useParams();
|
|
||||||
const [story, setStory] = useState<Story | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchStory = async () => {
|
|
||||||
try {
|
|
||||||
if (!storyId && !demo) {
|
|
||||||
throw new Error('ID da história não fornecido');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (demo) {
|
|
||||||
setStory({
|
|
||||||
id: 'demo',
|
|
||||||
student_id: 'demo',
|
|
||||||
class_id: 'demo',
|
|
||||||
school_id: 'demo',
|
|
||||||
title: 'Uma Aventura Educacional',
|
|
||||||
theme: 'Demo',
|
|
||||||
content: {
|
|
||||||
pages: [
|
|
||||||
{
|
|
||||||
text: 'Bem-vindo à demonstração do Histórias Mágicas! Aqui você pode ver como funciona nossa plataforma...',
|
|
||||||
image: 'https://images.unsplash.com/photo-1472162072942-cd5147eb3902?auto=format&fit=crop&q=80&w=800&h=600',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Com histórias interativas e educativas, seus alunos aprenderão de forma divertida e envolvente...',
|
|
||||||
image: 'https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&q=80&w=800&h=600',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
currentPage: 0
|
|
||||||
},
|
|
||||||
status: 'published',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('stories')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', storyId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
setStory(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Erro ao buscar história:', err);
|
|
||||||
setError(err instanceof Error ? err.message : 'Erro ao carregar história');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchStory();
|
|
||||||
}, [storyId, demo]);
|
|
||||||
|
|
||||||
const handleAudioUploaded = async (audioUrl: string) => {
|
|
||||||
try {
|
|
||||||
if (demo) return; // Não salva gravações no modo demo
|
|
||||||
|
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
|
||||||
if (!session?.user?.id) throw new Error('Usuário não autenticado');
|
|
||||||
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('story_recordings')
|
|
||||||
.insert({
|
|
||||||
story_id: storyId,
|
|
||||||
student_id: session.user.id,
|
|
||||||
audio_url: audioUrl,
|
|
||||||
status: 'pending_analysis'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Erro ao salvar gravação:', err);
|
|
||||||
setError('Erro ao salvar gravação de áudio');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<Loader2 className="w-8 h-8 text-purple-600 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error || !story) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Ops!</h1>
|
|
||||||
<p className="text-gray-600">{error || 'História não encontrada'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12">
|
|
||||||
<div className="max-w-4xl mx-auto px-4">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">
|
|
||||||
{story.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl overflow-hidden mb-8">
|
|
||||||
{story.content.pages[currentPage].image && (
|
|
||||||
<img
|
|
||||||
src={story.content.pages[currentPage].image}
|
|
||||||
alt={`Ilustração da página ${currentPage + 1}`}
|
|
||||||
className="w-full h-64 object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="p-8">
|
|
||||||
<p className="text-lg text-gray-700 mb-8">
|
|
||||||
{story.content.pages[currentPage].text}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<AudioRecorder
|
|
||||||
storyId={story.id}
|
|
||||||
studentId={demo ? 'demo' : story.student_id}
|
|
||||||
onAudioUploaded={handleAudioUploaded}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex justify-between mt-8">
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage(prev => Math.max(0, prev - 1))}
|
|
||||||
disabled={currentPage === 0}
|
|
||||||
className="px-4 py-2 bg-purple-100 text-purple-700 rounded-lg disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Anterior
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage(prev => Math.min(story.content.pages.length - 1, prev + 1))}
|
|
||||||
disabled={currentPage === story.content.pages.length - 1}
|
|
||||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Próxima
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
3
src/pages/story/StoryPageDemo.tsx
Normal file
3
src/pages/story/StoryPageDemo.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function StoryPageDemo({ demo = false }: StoryPageProps): JSX.Element {
|
||||||
|
// ... resto do código permanece igual ...
|
||||||
|
}
|
||||||
@ -126,32 +126,52 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StoryPage() {
|
export function StoryPage() {
|
||||||
const { id } = useParams();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams();
|
||||||
const [story, setStory] = React.useState<Story | null>(null);
|
const [story, setStory] = React.useState<Story | null>(null);
|
||||||
const [currentPage, setCurrentPage] = React.useState(0);
|
const [currentPage, setCurrentPage] = React.useState(0);
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [isPlaying, setIsPlaying] = React.useState(false);
|
const [isPlaying, setIsPlaying] = React.useState(false);
|
||||||
const [metrics, setMetrics] = React.useState<MetricsData | undefined>();
|
|
||||||
const [loadingMetrics, setLoadingMetrics] = React.useState(true);
|
|
||||||
const [recordings, setRecordings] = React.useState<StoryRecording[]>([]);
|
const [recordings, setRecordings] = React.useState<StoryRecording[]>([]);
|
||||||
const [loadingRecordings, setLoadingRecordings] = React.useState(true);
|
const [loadingRecordings, setLoadingRecordings] = React.useState(true);
|
||||||
|
const [metrics, setMetrics] = React.useState<MetricsData | null>(null);
|
||||||
|
const [loadingMetrics, setLoadingMetrics] = React.useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchStory = async () => {
|
const fetchStory = async () => {
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
if (!id) throw new Error('ID da história não fornecido');
|
||||||
if (!session?.user?.id) return;
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
// Buscar história e suas páginas
|
||||||
|
const { data: storyData, error: storyError } = await supabase
|
||||||
.from('stories')
|
.from('stories')
|
||||||
.select('*')
|
.select(`
|
||||||
|
*,
|
||||||
|
story_pages (
|
||||||
|
id,
|
||||||
|
page_number,
|
||||||
|
text,
|
||||||
|
image_url
|
||||||
|
)
|
||||||
|
`)
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
if (storyError) throw storyError;
|
||||||
setStory(data);
|
|
||||||
|
// Ordenar páginas por número
|
||||||
|
const orderedPages = storyData.story_pages.sort((a, b) => a.page_number - b.page_number);
|
||||||
|
setStory({
|
||||||
|
...storyData,
|
||||||
|
content: {
|
||||||
|
pages: orderedPages.map(page => ({
|
||||||
|
text: page.text,
|
||||||
|
image: page.image_url
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro ao carregar história:', err);
|
console.error('Erro ao carregar história:', err);
|
||||||
setError('Não foi possível carregar a história');
|
setError('Não foi possível carregar a história');
|
||||||
@ -334,7 +354,7 @@ export function StoryPage() {
|
|||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
{/* Imagem da página atual */}
|
{/* Imagem da página atual */}
|
||||||
{story.content?.pages?.[currentPage]?.image && (
|
{story?.content?.pages?.[currentPage]?.image && (
|
||||||
<div className="relative aspect-video">
|
<div className="relative aspect-video">
|
||||||
<img
|
<img
|
||||||
src={story.content.pages[currentPage].image}
|
src={story.content.pages[currentPage].image}
|
||||||
@ -345,11 +365,11 @@ export function StoryPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">{story.title}</h1>
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">{story?.title}</h1>
|
||||||
|
|
||||||
{/* Texto da página atual */}
|
{/* Texto da página atual */}
|
||||||
<p className="text-lg text-gray-700 mb-8">
|
<p className="text-lg text-gray-700 mb-8">
|
||||||
{story.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
{story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Gravador de áudio */}
|
{/* Gravador de áudio */}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { StudentDashboardLayout } from './pages/student-dashboard/StudentDashboa
|
|||||||
import { StudentStoriesPage } from './pages/student-dashboard/StudentStoriesPage';
|
import { StudentStoriesPage } from './pages/student-dashboard/StudentStoriesPage';
|
||||||
import { StudentSettingsPage } from './pages/student-dashboard/StudentSettingsPage';
|
import { StudentSettingsPage } from './pages/student-dashboard/StudentSettingsPage';
|
||||||
import { CreateStoryPage } from './pages/student-dashboard/CreateStoryPage';
|
import { CreateStoryPage } from './pages/student-dashboard/CreateStoryPage';
|
||||||
|
import { StoryPageDemo } from './pages/story/StoryPageDemo';
|
||||||
import { StoryPage } from './pages/student-dashboard/StoryPage';
|
import { StoryPage } from './pages/student-dashboard/StoryPage';
|
||||||
import { ProtectedRoute } from './components/auth/ProtectedRoute';
|
import { ProtectedRoute } from './components/auth/ProtectedRoute';
|
||||||
import { UserManagementPage } from './pages/admin/UserManagementPage';
|
import { UserManagementPage } from './pages/admin/UserManagementPage';
|
||||||
@ -124,7 +125,7 @@ export const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/demo',
|
path: '/demo',
|
||||||
element: <StoryViewer demo={true} />,
|
element: <StoryPageDemo />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/auth/callback',
|
path: '/auth/callback',
|
||||||
|
|||||||
@ -40,7 +40,7 @@ serve(async (req) => {
|
|||||||
try {
|
try {
|
||||||
const supabase = createClient(
|
const supabase = createClient(
|
||||||
Deno.env.get('SUPABASE_URL') ?? '',
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||||
)
|
)
|
||||||
console.log('[Supabase] Cliente inicializado')
|
console.log('[Supabase] Cliente inicializado')
|
||||||
|
|
||||||
@ -139,70 +139,122 @@ serve(async (req) => {
|
|||||||
const pages = await Promise.all(
|
const pages = await Promise.all(
|
||||||
storyContent.pages.map(async (page: any, index: number) => {
|
storyContent.pages.map(async (page: any, index: number) => {
|
||||||
console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.pages.length}...`)
|
console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.pages.length}...`)
|
||||||
|
|
||||||
|
// Gerar imagem com DALL-E
|
||||||
const imageResponse = await openai.images.generate({
|
const imageResponse = await openai.images.generate({
|
||||||
prompt: `${page.image_prompt}. Style: children's book illustration, colorful, educational, safe for kids`,
|
prompt: `${page.image_prompt}. Style: children's book illustration, colorful, educational, safe for kids`,
|
||||||
n: 1,
|
n: 1,
|
||||||
size: "1024x1024"
|
size: "1024x1024"
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`[DALL-E] Imagem ${index + 1} gerada com sucesso`)
|
// Baixar a imagem do URL do DALL-E
|
||||||
|
console.log(`[Storage] Baixando imagem ${index + 1}...`)
|
||||||
|
const imageUrl = imageResponse.data[0].url
|
||||||
|
const imageRes = await fetch(imageUrl)
|
||||||
|
const imageBuffer = await imageRes.arrayBuffer()
|
||||||
|
|
||||||
|
// Gerar nome único para o arquivo
|
||||||
|
const fileName = `${record.id}/page-${index + 1}-${Date.now()}.png`
|
||||||
|
|
||||||
|
// Salvar no Storage do Supabase
|
||||||
|
console.log(`[Storage] Salvando imagem ${index + 1} no bucket...`)
|
||||||
|
const { data: storageData, error: storageError } = await supabase
|
||||||
|
.storage
|
||||||
|
.from('story-images')
|
||||||
|
.upload(fileName, imageBuffer, {
|
||||||
|
contentType: 'image/png',
|
||||||
|
cacheControl: '3600',
|
||||||
|
upsert: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (storageError) {
|
||||||
|
throw new Error(`Erro ao salvar imagem ${index + 1} no storage: ${storageError.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gerar URL público da imagem
|
||||||
|
const { data: publicUrl } = supabase
|
||||||
|
.storage
|
||||||
|
.from('story-images')
|
||||||
|
.getPublicUrl(fileName)
|
||||||
|
|
||||||
|
console.log(`[Storage] Imagem ${index + 1} salva com sucesso`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: page.text,
|
text: page.text,
|
||||||
image: imageResponse.data[0].url
|
image: publicUrl.publicUrl,
|
||||||
|
image_path: fileName
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('[DALL-E] Todas as imagens geradas com sucesso')
|
console.log('[DALL-E] Todas as imagens geradas com sucesso')
|
||||||
|
|
||||||
// Preparar dados para salvar
|
// Preparar e salvar os dados
|
||||||
const storyData = {
|
const { data: story, error: storyError } = await supabase
|
||||||
title: storyContent.title,
|
|
||||||
content: {
|
|
||||||
title: storyContent.title,
|
|
||||||
pages: pages,
|
|
||||||
theme: theme.title,
|
|
||||||
subject: subject.title,
|
|
||||||
character: character.title,
|
|
||||||
setting: setting.title,
|
|
||||||
context: record.context,
|
|
||||||
original_prompt: prompt,
|
|
||||||
ai_response: completion.choices[0].message.content
|
|
||||||
},
|
|
||||||
status: 'published',
|
|
||||||
theme_id: theme.id,
|
|
||||||
subject_id: subject.id,
|
|
||||||
character_id: character.id,
|
|
||||||
setting_id: setting.id,
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[DB] Dados para salvar:', storyData)
|
|
||||||
|
|
||||||
// Atualizar história no Supabase
|
|
||||||
console.log('[DB] Salvando história...')
|
|
||||||
const { data: savedStory, error: updateError } = await supabase
|
|
||||||
.from('stories')
|
.from('stories')
|
||||||
.update(storyData)
|
.update({
|
||||||
|
title: storyContent.title,
|
||||||
|
status: 'published',
|
||||||
|
theme_id: theme.id,
|
||||||
|
subject_id: subject.id,
|
||||||
|
character_id: character.id,
|
||||||
|
setting_id: setting.id,
|
||||||
|
context: record.context,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
.eq('id', record.id)
|
.eq('id', record.id)
|
||||||
.select()
|
.select()
|
||||||
.single()
|
.single();
|
||||||
|
|
||||||
if (updateError) {
|
if (storyError) throw new Error(`Erro ao atualizar história: ${storyError.message}`);
|
||||||
throw new Error(`Erro ao salvar história: ${updateError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[DB] História salva com sucesso:', savedStory)
|
// Salvar páginas
|
||||||
|
const { error: pagesError } = await supabase
|
||||||
|
.from('story_pages')
|
||||||
|
.insert(
|
||||||
|
pages.map((page, index) => ({
|
||||||
|
story_id: record.id,
|
||||||
|
page_number: index + 1,
|
||||||
|
text: page.text,
|
||||||
|
image_url: page.image,
|
||||||
|
image_path: page.image_path
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pagesError) throw new Error(`Erro ao salvar páginas: ${pagesError.message}`);
|
||||||
|
|
||||||
|
// Salvar metadados da geração
|
||||||
|
const { error: genError } = await supabase
|
||||||
|
.from('story_generations')
|
||||||
|
.insert({
|
||||||
|
story_id: record.id,
|
||||||
|
original_prompt: prompt,
|
||||||
|
ai_response: completion.choices[0].message.content,
|
||||||
|
model_used: 'gpt-4-turbo'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (genError) throw new Error(`Erro ao salvar metadados: ${genError.message}`);
|
||||||
|
|
||||||
|
// Buscar história completa com relacionamentos
|
||||||
|
const { data: fullStory, error: fetchError } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
pages:story_pages(*)
|
||||||
|
`)
|
||||||
|
.eq('id', record.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError) throw new Error(`Erro ao buscar história completa: ${fetchError.message}`);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'História gerada e salva com sucesso',
|
message: 'História gerada e salva com sucesso',
|
||||||
storyId: record.id,
|
story: fullStory
|
||||||
story: savedStory
|
|
||||||
}),
|
}),
|
||||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
)
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Error] Erro ao gerar história:', error)
|
console.error('[Error] Erro ao gerar história:', error)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user