story-generator/supabase/functions/generate-story/index.ts
Lucas Santana 9840fe76b0 feat: aprimora interface do exercício de formação de palavras
- Adiciona barra de progresso e feedback visual
- Implementa lista de palavras encontradas
- Melhora interatividade e estados visuais
- Adiciona validação de palavras repetidas
- Otimiza transições e animações
- Mantém consistência com outros exercícios

type: feat
scope: exercises
breaking: false
2025-01-01 10:09:59 -03:00

376 lines
13 KiB
TypeScript

import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import OpenAI from 'https://esm.sh/openai@4.20.1'
const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY')
});
interface StoryPrompt {
theme_id: string;
subject_id: string;
character_id: string;
setting_id: string;
context?: string;
difficulty: 'easy' | 'medium' | 'hard';
}
const ALLOWED_ORIGINS = [
'http://localhost:5173', // Vite dev server
'http://localhost:3000', // Caso use outro port
'https://historiasmagicas.netlify.app' // Produção
];
// Função para otimizar URL da imagem
function getOptimizedImageUrl(originalUrl: string, width = 800): string {
// Se já for uma URL do Supabase Storage, adicionar transformações
if (originalUrl.includes('storage.googleapis.com')) {
return `${originalUrl}?width=${width}&quality=80&format=webp`;
}
return originalUrl;
}
interface StoryResponse {
title: string;
content: {
pages: Array<{
text: string;
imagePrompt: string;
keywords?: string[];
phonemes?: string[];
syllablePatterns?: string[];
}>;
};
metadata?: {
targetAge?: number;
difficulty?: string;
exerciseWords?: {
pronunciation?: string[];
formation?: string[];
completion?: string[];
};
};
}
serve(async (req) => {
const origin = req.headers.get('origin') || '';
const corsHeaders = {
'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0],
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Max-Age': '86400', // 24 horas
};
// Preflight request
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
const { record } = await req.json()
console.log('[Request]', record)
try {
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
console.log('[Supabase] Cliente inicializado')
console.log('[DB] Buscando categorias...')
const [themeResult, subjectResult, characterResult, settingResult] = await Promise.all([
supabase.from('story_themes').select('*').eq('id', record.theme_id).single(),
supabase.from('story_subjects').select('*').eq('id', record.subject_id).single(),
supabase.from('story_characters').select('*').eq('id', record.character_id).single(),
supabase.from('story_settings').select('*').eq('id', record.setting_id).single()
])
console.log('[DB] Resultados das consultas:', {
theme: themeResult,
subject: subjectResult,
character: characterResult,
setting: settingResult
})
if (themeResult.error) throw new Error(`Erro ao buscar tema: ${themeResult.error.message}`);
if (subjectResult.error) throw new Error(`Erro ao buscar disciplina: ${subjectResult.error.message}`);
if (characterResult.error) throw new Error(`Erro ao buscar personagem: ${characterResult.error.message}`);
if (settingResult.error) throw new Error(`Erro ao buscar cenário: ${settingResult.error.message}`);
if (!themeResult.data) throw new Error(`Tema não encontrado: ${record.theme_id}`);
if (!subjectResult.data) throw new Error(`Disciplina não encontrada: ${record.subject_id}`);
if (!characterResult.data) throw new Error(`Personagem não encontrado: ${record.character_id}`);
if (!settingResult.data) throw new Error(`Cenário não encontrado: ${record.setting_id}`);
const theme = themeResult.data;
const subject = subjectResult.data;
const character = characterResult.data;
const setting = settingResult.data;
console.log('[Validation] Categorias validadas com sucesso')
console.log('[GPT] Construindo prompt...')
const prompt = `
Crie uma história educativa para crianças com as seguintes características:
Tema: ${theme.title}
Disciplina: ${subject.title}
Personagem Principal: ${character.title}
Cenário: ${setting.title}
${record.context ? `Contexto Adicional: ${record.context}` : ''}
Requisitos:
- História adequada para crianças de 6-12 anos
- Conteúdo educativo focado em ${subject.title}
- Linguagem clara e envolvente
- 3-5 páginas de conteúdo
- Cada página deve ter um texto curto e sugestão para uma imagem
- Evitar conteúdo sensível ou inadequado
- Incluir elementos de ${theme.title}
- Ambientado em ${setting.title}
- Personagem principal baseado em ${character.title}
Requisitos específicos para exercícios:
1. Para o exercício de completar frases:
- Selecione 5-8 palavras importantes do texto
- Escolha palavras que sejam substantivos, verbos ou adjetivos
- Evite artigos, preposições ou palavras muito simples
- As palavras devem ser relevantes para o contexto da história
2. Para o exercício de formação de palavras:
- Selecione palavras com diferentes padrões silábicos
- Inclua palavras que possam ser divididas em sílabas
- Priorize palavras com 2-4 sílabas
3. Para o exercício de pronúncia:
- Selecione palavras que trabalhem diferentes fonemas
- Inclua palavras com encontros consonantais
- Priorize palavras que sejam desafiadoras para a faixa etária
Formato da resposta:
{
"title": "Título da História",
"content": {
"pages": [
{
"text": "Texto da página com frases completas...",
"imagePrompt": "Descrição para gerar imagem...",
"keywords": ["palavra1", "palavra2"],
"phonemes": ["fonema1", "fonema2"],
"syllablePatterns": ["CV", "CVC", "CCVC"]
}
]
},
"metadata": {
"targetAge": number,
"difficulty": string,
"exerciseWords": {
"pronunciation": ["palavra1", "palavra2"],
"formation": ["palavra3", "palavra4"],
"completion": ["palavra5", "palavra6"]
}
}
}
`
console.log('[GPT] Prompt construído:', prompt)
console.log('[GPT] Iniciando geração da história...')
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "Você é um contador de histórias infantis especializado em conteúdo educativo."
},
{
role: "user",
content: prompt
}
],
temperature: 0.7,
max_tokens: 1000
})
console.log('[GPT] História gerada:', completion.choices[0].message)
const storyContent = JSON.parse(completion.choices[0].message.content || '{}') as StoryResponse;
// Validar estrutura do retorno da IA
if (!storyContent.title || !storyContent.content?.pages?.length) {
console.error('Resposta da IA:', storyContent);
throw new Error('Formato inválido retornado pela IA');
}
console.log('[DALL-E] Iniciando geração de imagens...')
const pages = await Promise.all(
storyContent.content.pages.map(async (page, index) => {
console.log(`[DALL-E] Gerando imagem ${index + 1}/${storyContent.content.pages.length}...`)
// Gerar imagem com DALL-E
const imageResponse = await openai.images.generate({
prompt: `${page.imagePrompt}. For Kids. Educational Purpose. Style inpired by DreamWorks and Pixar. Provide a high quality image. Provide a ethnic diversity.`,
n: 1,
size: "1024x1024",
model: "dall-e-3"
})
// 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`)
// Otimizar URL antes de salvar
const optimizedUrl = getOptimizedImageUrl(imageResponse.data[0].url);
return {
text: page.text,
image: optimizedUrl,
image_path: fileName
}
})
)
console.log('[DALL-E] Todas as imagens geradas com sucesso')
// Preparar e salvar os dados
const { data: story, error: storyError } = await supabase
.from('stories')
.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)
.select()
.single();
if (storyError) throw new Error(`Erro ao atualizar história: ${storyError.message}`);
// 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-4o-mini'
});
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}`);
// Salvar palavras dos exercícios se existirem
if (storyContent.metadata?.exerciseWords) {
const exerciseWords = Object.entries(storyContent.metadata.exerciseWords)
.flatMap(([type, words]) =>
(words || []).map(word => {
const pageWithWord = storyContent.content.pages
.find(p => p.text.toLowerCase().includes(word.toLowerCase()));
// Só incluir palavras que realmente existem no texto
if (!pageWithWord) return null;
return {
story_id: record.id,
word,
exercise_type: type,
phonemes: pageWithWord?.phonemes || null,
syllable_pattern: pageWithWord?.syllablePatterns?.[0] || null
};
})
)
.filter(Boolean); // Remove null values
if (exerciseWords.length > 0) {
const { error: wordsError } = await supabase
.from('story_exercise_words')
.insert(exerciseWords);
if (wordsError) {
console.error('Erro ao salvar palavras dos exercícios:', wordsError);
}
}
}
return new Response(
JSON.stringify({
success: true,
message: 'História gerada e salva com sucesso',
story: fullStory
}),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('[Error] Erro ao gerar história:', error)
console.error('[Error] Stack trace:', error.stack)
return new Response(
JSON.stringify({
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500
}
)
}
})