diff --git a/supabase/functions/_shared/cors.ts b/supabase/functions/_shared/cors.ts new file mode 100644 index 0000000..b31768a --- /dev/null +++ b/supabase/functions/_shared/cors.ts @@ -0,0 +1,4 @@ +export const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} \ No newline at end of file diff --git a/supabase/functions/analyze-essay/index.ts b/supabase/functions/analyze-essay/index.ts new file mode 100644 index 0000000..eb5b1a5 --- /dev/null +++ b/supabase/functions/analyze-essay/index.ts @@ -0,0 +1,208 @@ +import { serve } from 'https://deno.fresh.run/std@0.168.0/http/server.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { corsHeaders } from '../_shared/cors.ts' +import { OpenAI } from "https://deno.land/x/openai@v4.24.0/mod.ts"; + +interface EssayAnalysisRequest { + essay_id: string; + content: string; + type_id: string; + genre_id: string; +} + +interface EssayAnalysisResponse { + overall_score: number; + feedback: { + structure: string; + content: string; + language: string; + }; + strengths: string[]; + improvements: string[]; + suggestions: string; + criteria_scores: { + adequacy: number; // Adequação ao tema/gênero + coherence: number; // Coerência textual + cohesion: number; // Coesão + vocabulary: number; // Vocabulário + grammar: number; // Gramática/ortografia + }; +} + +interface EssayType { + id: string; + slug: string; + title: string; + description: string; +} + +interface EssayGenre { + id: string; + type_id: string; + slug: string; + title: string; + description: string; + requirements: { + min_words: number; + max_words: number; + required_elements: string[]; + }; +} + +serve(async (req) => { + // Handle CORS + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + // Criar cliente Supabase + const supabaseClient = createClient( + Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_ANON_KEY') ?? '' + ) + + // Criar cliente OpenAI + const openai = new OpenAI({ + apiKey: Deno.env.get('OPENAI_API_KEY'), + }); + + // Obter dados da requisição + const { essay_id, content, type_id, genre_id }: EssayAnalysisRequest = await req.json() + + // Validar dados obrigatórios + if (!essay_id || !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' } } + ) + } + + // Buscar informações do tipo e gênero + const { data: typeData, error: typeError } = await supabaseClient + .from('essay_types') + .select('*') + .eq('id', type_id) + .single() + + const { data: genreData, error: genreError } = await supabaseClient + .from('essay_genres') + .select('*') + .eq('id', genre_id) + .single() + + 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' } } + ) + } + + const essayType: EssayType = typeData + const essayGenre: EssayGenre = genreData + + // Construir prompt para a 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: +- Mínimo de palavras: ${essayGenre.requirements.min_words} +- Máximo de palavras: ${essayGenre.requirements.max_words} +- Elementos obrigatórios: ${essayGenre.requirements.required_elements.join(', ')} + +Texto para análise: +${content} + +Forneça uma análise detalhada considerando: +1. Adequação ao tipo e gênero textual +2. Coerência e coesão textual +3. Vocabulário e linguagem +4. Gramática e ortografia +5. Elementos obrigatórios do gênero +6. Pontos fortes +7. Pontos a melhorar +8. Sugestões específicas para aprimoramento + +Responda em formato JSON seguindo exatamente esta estrutura: +{ + "overall_score": number, // 0 a 100 + "feedback": { + "structure": string, // Feedback sobre estrutura e organização + "content": string, // Feedback sobre conteúdo e ideias + "language": string // Feedback sobre linguagem e gramática + }, + "strengths": string[], // Lista de pontos fortes + "improvements": string[], // Lista de pontos a melhorar + "suggestions": string, // Sugestões específicas + "criteria_scores": { + "adequacy": number, // 0 a 100 - Adequação ao tema/gênero + "coherence": number, // 0 a 100 - Coerência textual + "cohesion": number, // 0 a 100 - Coesão + "vocabulary": number, // 0 a 100 - Vocabulário + "grammar": number // 0 a 100 - Gramática/ortografia + } +}` + + // 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_object" } + }); + + const analysis: EssayAnalysisResponse = JSON.parse(completion.choices[0].message.content) + + // 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 (saveError) { + return new Response( + JSON.stringify({ error: 'Erro ao salvar análise' }), + { status: 500, headers: { ...corsHeaders, '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) { + return new Response( + JSON.stringify({ error: error.message }), + { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ) + } +}) \ No newline at end of file