227 lines
8.7 KiB
JavaScript
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}`);
|
|
});
|