mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
467 lines
15 KiB
TypeScript
467 lines
15 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 { getCorsHeaders } 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;
|
|
suggestions: string;
|
|
feedback: {
|
|
structure: string;
|
|
content: string;
|
|
language: string;
|
|
};
|
|
strengths: string[];
|
|
improvements: string[];
|
|
criteria_scores: {
|
|
adequacy: number;
|
|
coherence: number;
|
|
cohesion: number;
|
|
vocabulary: number;
|
|
grammar: number;
|
|
};
|
|
}
|
|
|
|
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) => {
|
|
console.log(`[${new Date().toISOString()}] Nova requisição recebida`)
|
|
|
|
// Handle CORS
|
|
if (req.method === 'OPTIONS') {
|
|
console.log('Requisição OPTIONS - CORS preflight')
|
|
return new Response('ok', { headers: getCorsHeaders(req) })
|
|
}
|
|
|
|
try {
|
|
// Criar cliente Supabase
|
|
console.log('Inicializando cliente Supabase...')
|
|
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
|
|
|
|
if (!supabaseUrl || !supabaseServiceKey) {
|
|
throw new Error('Variáveis de ambiente do Supabase não configuradas')
|
|
}
|
|
|
|
const supabaseClient = createClient(supabaseUrl, supabaseServiceKey, {
|
|
auth: {
|
|
persistSession: false,
|
|
autoRefreshToken: false,
|
|
}
|
|
})
|
|
|
|
// 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: openaiKey,
|
|
});
|
|
|
|
// Obter dados da requisição
|
|
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',
|
|
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',
|
|
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:
|
|
- 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
|
|
"suggestions": string, // Sugestões específicas
|
|
"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
|
|
"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 e ortografia
|
|
}
|
|
}`
|
|
|
|
// Realizar análise com OpenAI
|
|
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",
|
|
json_schema: {
|
|
type: "object",
|
|
required: ["overall_score", "suggestions", "feedback", "strengths", "improvements", "criteria_scores"],
|
|
properties: {
|
|
overall_score: {
|
|
type: "number",
|
|
minimum: 0,
|
|
maximum: 100,
|
|
description: "Pontuação geral da redação (0-100)"
|
|
},
|
|
suggestions: {
|
|
type: "string",
|
|
description: "Sugestões específicas para aprimoramento"
|
|
},
|
|
feedback: {
|
|
type: "object",
|
|
required: ["structure", "content", "language"],
|
|
properties: {
|
|
structure: {
|
|
type: "string",
|
|
description: "Feedback sobre estrutura e organização"
|
|
},
|
|
content: {
|
|
type: "string",
|
|
description: "Feedback sobre conteúdo e ideias"
|
|
},
|
|
language: {
|
|
type: "string",
|
|
description: "Feedback sobre linguagem e gramática"
|
|
}
|
|
}
|
|
},
|
|
strengths: {
|
|
type: "array",
|
|
items: {
|
|
type: "string",
|
|
description: "Ponto forte da redação"
|
|
},
|
|
minItems: 1,
|
|
description: "Lista de pontos fortes da redação"
|
|
},
|
|
improvements: {
|
|
type: "array",
|
|
items: {
|
|
type: "string",
|
|
description: "Ponto a melhorar na redação"
|
|
},
|
|
minItems: 1,
|
|
description: "Lista de pontos a melhorar na redação"
|
|
},
|
|
criteria_scores: {
|
|
type: "object",
|
|
required: ["adequacy", "coherence", "cohesion", "vocabulary", "grammar"],
|
|
properties: {
|
|
adequacy: {
|
|
type: "number",
|
|
minimum: 0,
|
|
maximum: 100,
|
|
description: "Adequação ao tema/gênero (0-100)"
|
|
},
|
|
coherence: {
|
|
type: "number",
|
|
minimum: 0,
|
|
maximum: 100,
|
|
description: "Coerência textual (0-100)"
|
|
},
|
|
cohesion: {
|
|
type: "number",
|
|
minimum: 0,
|
|
maximum: 100,
|
|
description: "Coesão textual (0-100)"
|
|
},
|
|
vocabulary: {
|
|
type: "number",
|
|
minimum: 0,
|
|
maximum: 100,
|
|
description: "Vocabulário (0-100)"
|
|
},
|
|
grammar: {
|
|
type: "number",
|
|
minimum: 0,
|
|
maximum: 100,
|
|
description: "Gramática e ortografia (0-100)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
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...')
|
|
|
|
// Primeiro, criar a análise principal
|
|
const { data: analysisData, error: analysisError } = await supabaseClient
|
|
.from('essay_analyses')
|
|
.insert({
|
|
essay_id,
|
|
overall_score: analysis.overall_score,
|
|
suggestions: analysis.suggestions
|
|
})
|
|
.select()
|
|
.single()
|
|
|
|
if (analysisError) {
|
|
console.error('Erro ao salvar análise principal:', analysisError)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Erro ao salvar análise',
|
|
details: analysisError.message
|
|
}),
|
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
|
)
|
|
}
|
|
|
|
// Salvar feedback
|
|
const { error: feedbackError } = await supabaseClient
|
|
.from('essay_analysis_feedback')
|
|
.insert({
|
|
analysis_id: analysisData.id,
|
|
structure_feedback: analysis.feedback.structure,
|
|
content_feedback: analysis.feedback.content,
|
|
language_feedback: analysis.feedback.language
|
|
})
|
|
|
|
if (feedbackError) {
|
|
console.error('Erro ao salvar feedback:', feedbackError)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Erro ao salvar feedback',
|
|
details: feedbackError.message
|
|
}),
|
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
|
)
|
|
}
|
|
|
|
// Salvar pontos fortes
|
|
const strengths = analysis.strengths.map(strength => ({
|
|
analysis_id: analysisData.id,
|
|
strength
|
|
}))
|
|
|
|
const { error: strengthsError } = await supabaseClient
|
|
.from('essay_analysis_strengths')
|
|
.insert(strengths)
|
|
|
|
if (strengthsError) {
|
|
console.error('Erro ao salvar pontos fortes:', strengthsError)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Erro ao salvar pontos fortes',
|
|
details: strengthsError.message
|
|
}),
|
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
|
)
|
|
}
|
|
|
|
// Salvar pontos a melhorar
|
|
const improvements = analysis.improvements.map(improvement => ({
|
|
analysis_id: analysisData.id,
|
|
improvement
|
|
}))
|
|
|
|
const { error: improvementsError } = await supabaseClient
|
|
.from('essay_analysis_improvements')
|
|
.insert(improvements)
|
|
|
|
if (improvementsError) {
|
|
console.error('Erro ao salvar pontos a melhorar:', improvementsError)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Erro ao salvar pontos a melhorar',
|
|
details: improvementsError.message
|
|
}),
|
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
|
)
|
|
}
|
|
|
|
// Salvar notas por critério
|
|
const { error: scoresError } = await supabaseClient
|
|
.from('essay_analysis_scores')
|
|
.insert({
|
|
analysis_id: analysisData.id,
|
|
adequacy: analysis.criteria_scores.adequacy,
|
|
coherence: analysis.criteria_scores.coherence,
|
|
cohesion: analysis.criteria_scores.cohesion,
|
|
vocabulary: analysis.criteria_scores.vocabulary,
|
|
grammar: analysis.criteria_scores.grammar
|
|
})
|
|
|
|
if (scoresError) {
|
|
console.error('Erro ao salvar notas:', scoresError)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Erro ao salvar notas',
|
|
details: scoresError.message
|
|
}),
|
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
|
|
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' } }
|
|
)
|
|
}
|
|
|
|
console.log('Análise concluída com sucesso')
|
|
// Retornar análise
|
|
return new Response(
|
|
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' } }
|
|
)
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Erro geral na função:', error)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Erro interno do servidor',
|
|
details: error.message
|
|
}),
|
|
{ status: 500, headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' } }
|
|
)
|
|
}
|
|
})
|