Files
camila/server.js
T

227 lines
8.7 KiB
JavaScript

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}`);
});