diff --git a/supabase/functions/_shared/cors.ts b/supabase/functions/_shared/cors.ts index b31768a..a5077d2 100644 --- a/supabase/functions/_shared/cors.ts +++ b/supabase/functions/_shared/cors.ts @@ -1,4 +1,21 @@ -export const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +const ALLOWED_ORIGINS = [ + 'http://localhost:5173', // Vite dev server + 'http://localhost:9999', // Supabase Edge Functions local + 'http://localhost', // Vite dev server + 'http://localhost:3000', // Caso use outro port + 'https://leiturama.ai', // Produção + 'https://leiturama.netlify.app' // Staging +]; + +export function getCorsHeaders(req: Request) { + const origin = req.headers.get('origin') || ''; + + return { + 'Cross-Origin-Resource-Policy': 'cross-origin', + 'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0], + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', + 'Access-Control-Max-Age': '86400', // 24 horas + 'Cross-Origin-Embedder-Policy': 'credentialless' + }; } \ No newline at end of file diff --git a/supabase/functions/analyze-essay/index.ts b/supabase/functions/analyze-essay/index.ts index c064af1..132435d 100644 --- a/supabase/functions/analyze-essay/index.ts +++ b/supabase/functions/analyze-essay/index.ts @@ -1,6 +1,6 @@ -import { serve } from 'https://deno.fresh.run/std@0.168.0/http/server.ts' +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' -import { corsHeaders } from '../_shared/cors.ts' +import { getCorsHeaders } from '../_shared/cors.ts' import { OpenAI } from "https://deno.land/x/openai@v4.24.0/mod.ts"; interface EssayAnalysisRequest { @@ -50,58 +50,106 @@ interface EssayGenre { } serve(async (req) => { + console.log(`[${new Date().toISOString()}] Nova requisição recebida`) + // Handle CORS if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }) + console.log('Requisição OPTIONS - CORS preflight') + return new Response('ok', { headers: getCorsHeaders(req) }) } try { // Criar cliente Supabase - const supabaseClient = createClient( - Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_ANON_KEY') ?? '' - ) + console.log('Inicializando cliente Supabase...') + const supabaseUrl = Deno.env.get('SUPABASE_URL') + const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY') + + if (!supabaseUrl || !supabaseKey) { + throw new Error('Variáveis de ambiente do Supabase não configuradas') + } + + const supabaseClient = createClient(supabaseUrl, supabaseKey) // Criar cliente OpenAI + console.log('Inicializando cliente OpenAI...') + const openaiKey = Deno.env.get('OPENAI_API_KEY') + if (!openaiKey) { + throw new Error('OPENAI_API_KEY não configurada') + } + const openai = new OpenAI({ - apiKey: Deno.env.get('OPENAI_API_KEY'), + apiKey: openaiKey, }); // Obter dados da requisição - const { essay_id, content, type_id, genre_id }: EssayAnalysisRequest = await req.json() + console.log('Obtendo dados da requisição...') + const requestData = await req.json() + console.log('Dados recebidos:', JSON.stringify(requestData, null, 2)) + + const { essay_id, content, type_id, genre_id }: EssayAnalysisRequest = requestData // Validar dados obrigatórios if (!essay_id || !content || !type_id || !genre_id) { + console.error('Dados obrigatórios faltando:', { essay_id, content: !!content, type_id, genre_id }) return new Response( - JSON.stringify({ error: 'Dados obrigatórios não fornecidos' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ + error: 'Dados obrigatórios não fornecidos', + details: { + essay_id: !essay_id, + content: !content, + type_id: !type_id, + genre_id: !genre_id + } + }), + { status: 400, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } ) } // Buscar informações do tipo e gênero + console.log('Buscando informações do tipo e gênero...') const { data: typeData, error: typeError } = await supabaseClient .from('essay_types') .select('*') .eq('id', type_id) .single() + if (typeError) { + console.error('Erro ao buscar tipo:', typeError) + } + const { data: genreData, error: genreError } = await supabaseClient .from('essay_genres') .select('*') .eq('id', genre_id) .single() + if (genreError) { + console.error('Erro ao buscar gênero:', genreError) + } + if (typeError || genreError || !typeData || !genreData) { return new Response( - JSON.stringify({ error: 'Tipo ou gênero não encontrado' }), - { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ + error: 'Tipo ou gênero não encontrado', + details: { + typeError: typeError?.message, + genreError: genreError?.message + } + }), + { status: 404, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } ) } const essayType: EssayType = typeData const essayGenre: EssayGenre = genreData + console.log('Tipo e gênero encontrados:', { + type: essayType.title, + genre: essayGenre.title + }) + // Construir prompt para a análise + console.log('Construindo prompt para análise...') const prompt = `Você é um professor especialista em análise de textos. Analise a redação a seguir considerando que é do tipo "${essayType.title}" e gênero "${essayGenre.title}". Requisitos específicos do gênero: @@ -143,111 +191,148 @@ Responda em formato JSON seguindo exatamente esta estrutura: }` // Realizar análise com OpenAI - const completion = await openai.chat.completions.create({ - model: "gpt-4-turbo-preview", - messages: [ - { - role: "system", - content: "Você é um professor especialista em análise de textos, com vasta experiência em avaliação de redações." - }, - { - role: "user", - content: prompt - } - ], - response_format: { - type: "json_schema", - json_schema: { - type: "object", - name: "EssayAnalysis", + console.log('Enviando requisição para OpenAI...') + try { + const completion = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + role: "system", + content: "Você é um professor especialista em análise de textos, com vasta experiência em avaliação de redações." + }, + { + role: "user", + content: prompt + } + ], + response_format: { + type: "json_schema", schema: { type: "object", - schema: { - type: "object", - additionalProperties: false, - properties: { - overall_score: { type: "number" }, - feedback: { - type: "object", - additionalProperties: false, - properties: { - structure: { type: "string" }, - content: { type: "string" }, - language: { type: "string" } - } - }, - strengths: { - type: "array", - items: { type: "string" } - }, - improvements: { - type: "array", - items: { type: "string" } - }, - suggestions: { type: "string" }, - criteria_scores: { - type: "object", - additionalProperties: false, - properties: { - adequacy: { type: "number" }, - coherence: { type: "number" }, - cohesion: { type: "number" }, - vocabulary: { type: "number" }, - grammar: { type: "number" } + required: ["overall_score", "feedback", "strengths", "improvements", "suggestions", "criteria_scores"], + properties: { + overall_score: { + type: "number", + minimum: 0, + maximum: 100 + }, + feedback: { + type: "object", + required: ["structure", "content", "language"], + properties: { + structure: { type: "string" }, + content: { type: "string" }, + language: { type: "string" } + } + }, + strengths: { + type: "array", + items: { type: "string" } + }, + improvements: { + type: "array", + items: { type: "string" } + }, + suggestions: { type: "string" }, + criteria_scores: { + type: "object", + required: ["adequacy", "coherence", "cohesion", "vocabulary", "grammar"], + properties: { + adequacy: { + type: "number" + }, + coherence: { + type: "number" + }, + cohesion: { + type: "number" + }, + vocabulary: { + type: "number" + }, + grammar: { + type: "number" } } } } } } + }); + + console.log('Resposta recebida da OpenAI') + const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content) + console.log('Análise gerada:', JSON.stringify(analysis, null, 2)) + + // Salvar análise no banco + console.log('Salvando análise no banco...') + const { error: saveError } = await supabaseClient + .from('essay_analyses') + .insert({ + essay_id, + overall_score: analysis.overall_score, + feedback: analysis.feedback, + strengths: analysis.strengths, + improvements: analysis.improvements, + suggestions: analysis.suggestions, + criteria_scores: analysis.criteria_scores + }) + + if (saveError) { + console.error('Erro ao salvar análise:', saveError) + return new Response( + JSON.stringify({ + error: 'Erro ao salvar análise', + details: saveError.message + }), + { status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } + ) } - }); - const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content) + // Atualizar status da redação + console.log('Atualizando status da redação...') + const { error: updateError } = await supabaseClient + .from('student_essays') + .update({ status: 'analyzed' }) + .eq('id', essay_id) - // Salvar análise no banco - const { error: saveError } = await supabaseClient - .from('essay_analyses') - .insert({ - essay_id, - overall_score: analysis.overall_score, - feedback: analysis.feedback, - strengths: analysis.strengths, - improvements: analysis.improvements, - suggestions: analysis.suggestions, - criteria_scores: analysis.criteria_scores - }) + if (updateError) { + console.error('Erro ao atualizar status:', updateError) + return new Response( + JSON.stringify({ + error: 'Erro ao atualizar status da redação', + details: updateError.message + }), + { status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } + ) + } - if (saveError) { + console.log('Análise concluída com sucesso') + // Retornar análise return new Response( - JSON.stringify({ error: 'Erro ao salvar análise' }), - { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify(analysis), + { headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } + ) + + } catch (openaiError) { + console.error('Erro na chamada da OpenAI:', openaiError) + return new Response( + JSON.stringify({ + error: 'Erro ao gerar análise', + details: openaiError.message + }), + { status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } ) } - // Atualizar status da redação - const { error: updateError } = await supabaseClient - .from('student_essays') - .update({ status: 'analyzed' }) - .eq('id', essay_id) - - if (updateError) { - return new Response( - JSON.stringify({ error: 'Erro ao atualizar status da redação' }), - { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ) - } - - // Retornar análise - return new Response( - JSON.stringify(analysis), - { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ) - } catch (error) { + console.error('Erro geral na função:', error) return new Response( - JSON.stringify({ error: error.message }), - { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ + error: 'Erro interno do servidor', + details: error.message + }), + { status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } } ) } }) \ No newline at end of file