require('dotenv').config(); const express = require('express'); const cookieParser = require('cookie-parser'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || 'puglindo26'; app.use(express.json()); app.use(cookieParser(process.env.SESSION_SECRET || 'camila-secret')); // Middleware para verificar autenticação const requireAuth = (req, res, next) => { const session = req.signedCookies.camila_session || req.cookies.camila_session; if (session === 'authenticated') { next(); } else { if (req.xhr || req.path.startsWith('/api/')) { res.status(401).json({ error: 'Não autorizado' }); } else { res.redirect('/login'); } } }; // Rota de login (página) app.get('/login', (req, res) => { const session = req.signedCookies.camila_session || req.cookies.camila_session; if (session === 'authenticated') { res.redirect('/'); } else { res.sendFile(path.join(__dirname, 'public', 'login.html')); } }); // API para Login app.post('/api/login', (req, res) => { const { password } = req.body; if (password === ACCESS_PASSWORD) { // Define cookie de sessão assinado que dura 30 dias res.cookie('camila_session', 'authenticated', { signed: true, httpOnly: true, maxAge: 30 * 24 * 60 * 60 * 1000, // 30 dias sameSite: 'lax', secure: process.env.NODE_ENV === 'production' }); res.json({ success: true }); } else { res.status(401).json({ error: 'Senha incorreta' }); } }); // API para Logout app.post('/api/logout', (req, res) => { res.clearCookie('camila_session'); res.json({ success: true }); }); // Proteger arquivos estáticos do diretório public exceto o login app.use((req, res, next) => { if (req.path === '/login' || req.path === '/login.html' || req.path === '/login.js' || req.path === '/style.css' || req.path.startsWith('/assets/')) { return next(); } requireAuth(req, res, next); }); // Servir arquivos estáticos do diretório public app.use(express.static(path.join(__dirname, 'public'))); // System prompt da Kemily (injetado em todas as conversas) const KEMILY_SYSTEM_PROMPT = { role: "system", content: `Você é a Kemily, uma assistente de educação infantil especializada. Você ajuda Camila (professora de educação infantil na rede municipal de São Bernardo do Campo, SP, crianças de 2 a 5 anos) no seu trabalho diário. Sua missão: facilitar a criação de relatórios, planejamentos, resumos de livros infantis, pesquisas pedagógicas e uso de metodologias como Mind Lab. Sua personalidade: carinhosa, paciente, prática, profissional. Você fala em português brasileiro e adapta ao nível da Camila. Você tem acesso a conhecimento detalhado sobre: - BNCC e campos de experiência para educação infantil (2-5 anos) - Metodologia Mind Lab (jogos pedagógicos certificados INMETRO para desenvolvimento socioemocional) - Estrutura da rede municipal de São Bernardo do Campo - Planejamento pedagógico e documentação - 31 módulos de atividades Mind Lab para educação infantil e ensino fundamental IMPORTANTE — ESTILO DE ESCRITA DA CAMILA: Quando você escrever textos para a Camila (relatórios, planejamentos, análises, etc), você DEVE escrever no estilo dela, como se ELA mesma tivesse escrito. Princípios: 1. FRASES LONGAS E DESENVOLVIDAS: Não fragmentar em frases curtas. Use frases longas com orações subordinadas encadeadas. Exemplo: "O professor media esse conhecimento prévio, estando atento e propondo experiências significativas." 2. VOCABULÁRIO TÉCNICO-PEDAGÓGICO: Use termos como protagonismo, intencionalidade educativa, desenvolvimento integral, escuta ativa, vivências, saberes prévios, mediação, autorregulação, potencialidades. 3. EXPRESSÕES CARACTERÍSTICAS: Use "colocando as crianças como protagonistas", "faz parte do mundo letrado", "nos mais diversos", "propiciando assim", "a meu entender", "nesse sentido", "para além de". 4. CONECTIVOS NATURAIS: Use "portanto", "assim", "dessa forma", "nesse sentido", "a partir de", "por meio de", "acerca de". EVITE "de acordo com" (use "conforme", "segundo", "a partir de"). 5. POSICIONAMENTO PESSOAL: Use "Considero que...", "Entendo que...", "Creio que...", "Ao meu ver..." para posicionar-se. 6. ESTRUTURA DE PARÁGRAFO: Afirmação inicial → Desenvolvimento com exemplos → Conexão com próximo ponto. 7. CENTRALIDADE NA CRIANÇA: Sempre volte ao estudante como protagonista do processo. 8. EVITAR: gírias, fragmentação em frases curtas, linguagem burocrática vazia, "no dia a dia" em excesso, "em relação a" repetido. EXEMPLO de texto no estilo da Camila: "Acredito que o planejamento pedagógico deve partir dos interesses das crianças, porém sem perder de vista as intencionalidades educativas previstas na BNCC e no currículo municipal. O professor, ao exercer escuta ativa e observação atenta, consegue identificar os saberes prévios dos estudantes e, a partir deles, propor experiências significativas que desafiem e promovam o desenvolvimento integral de cada um." Sempre que possível: - Forneça exemplos práticos - Conecte com atividades reais de sala de aula - Para relatórios, ofereça estrutura e modelo no estilo da Camila - Referencie BNCC e Mind Lab quando aplicável Responda SEMPRE em português do Brasil.` }; // Rota de Chat com OpenRouter (Streaming nativo) app.post('/api/chat', requireAuth, async (req, res) => { const { messages } = req.body; if (!messages || !Array.isArray(messages)) { return res.status(400).json({ error: 'Mensagens inválidas' }); } // Injetar system prompt da Kemily no início da conversa const enrichedMessages = [KEMILY_SYSTEM_PROMPT, ...messages]; try { const openrouterUrl = 'https://openrouter.ai/api/v1/chat/completions'; const response = await fetch(openrouterUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, 'HTTP-Referer': 'https://camila.reifonas.cloud', 'X-Title': 'Camila ChatGPT replica', 'Content-Type': 'application/json' }, body: JSON.stringify({ model: process.env.OPENROUTER_MODEL, messages: enrichedMessages, stream: true }) }); if (!response.ok) { const errorText = await response.text(); console.error('Erro na API do OpenRouter:', errorText); return res.status(response.status).json({ error: 'Erro ao processar requisição na API do OpenRouter' }); } // Configurar headers para Server-Sent Events (SSE) res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); // Manter a última linha inacabada no buffer buffer = lines.pop(); for (const line of lines) { const cleanLine = line.trim(); if (cleanLine.startsWith('data: ')) { const dataContent = cleanLine.slice(6); if (dataContent === '[DONE]') { res.write('data: [DONE]\n\n'); continue; } try { const parsed = JSON.parse(dataContent); const content = parsed.choices?.[0]?.delta?.content || ''; if (content) { res.write(`data: ${JSON.stringify({ content })}\n\n`); } } catch (e) { // Ignorar linhas incompletas ou JSON inválido } } } } // Enviar qualquer conteúdo restante no buffer if (buffer) { const cleanLine = buffer.trim(); if (cleanLine.startsWith('data: ')) { const dataContent = cleanLine.slice(6); if (dataContent !== '[DONE]') { try { const parsed = JSON.parse(dataContent); const content = parsed.choices?.[0]?.delta?.content || ''; if (content) { res.write(`data: ${JSON.stringify({ content })}\n\n`); } } catch (e) {} } } } res.write('data: [DONE]\n\n'); res.end(); } catch (error) { console.error('Erro na rota de chat:', error); res.status(500).json({ error: 'Erro interno no servidor ao chamar a API de IA' }); } }); // Rota coringa para redirecionar para index.html app.get('*', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.listen(PORT, '0.0.0.0', () => { console.log(`Servidor rodando na porta ${PORT}`); });