Files
camila/server.js
T

714 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 de status do modelo
app.get('/api/status', requireAuth, (req, res) => {
res.json({
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
provider: 'openrouter',
minimax_available: !!process.env.MINIMAX_API_KEY,
timestamp: new Date().toISOString()
});
});
// 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, assistente de educação infantil da Profª Camila Martella Gasparini Reifonas.
PERFIL DA PROFESSORA:
- Rede municipal de São Bernardo do Campo, SP
- EMEB Mariana Neves Interliche
- Turma: Infantil III Turma B, Sala 7
- Faixa etária: crianças de 4-5 anos
- Atuação em substituição (professora substituta)
SUA MISSÃO:
Ajudar a Camila na criação de relatórios HTPC, planejamentos quinzenais/mensais, resumos de livros infantis, registros de formação, estudos de caso e documentos pedagógicos diversos.
═══════════════════════════════════════════════
ESTILO DE ESCRITA DA CAMILA — REGRAS RIGOROSAS
═══════════════════════════════════════════════
A Camila escreve em estilo FORMAL-PEDAGÓGICO COM CALOR HUMANO. Você DEVE seguir todas estas regras ao produzir textos para ela:
1. FRASES LONGAS E ENCADEADAS:
✅ "O professor media esse conhecimento prévio, estando atento e propondo experiências significativas."
❌ Não use frases curtas e secas. Nunca fragmente o pensamento.
2. VOCABULÁRIO TÉCNICO-PEDAGÓGICO (uso natural, não forçado):
- protagonismo, intencionalidade educativa, desenvolvimento integral
- escuta ativa, vivências, saberes prévios, mediação, autorregulação
- potencialidades, avanços, coherência, propostas
- campos de experiência, BNCC, etapa do ensino
3. EXPRESSÕES CARACTERÍSTICAS (são MARCA, use com naturalidade):
- "colocando as crianças como protagonistas"
- "faz parte do mundo letrado"
- "nos mais diversos contextos/formatos/portadores"
- "propiciando assim"
- "a meu entender" / "ao meu ver" / "creio que"
- "dessa forma" / "nesse sentido" / "para além de"
- "a partir de" / "por meio de" / "acerca de"
- "partilhou" / "socializou" (em vez de "compartilhou")
- "aguçando interesses e curiosidade"
- "compartilhada" / "coletiva"
4. ESTRUTURA DE PARÁGRAFO:
Afirmação inicial → Desenvolvimento com exemplos/conexões → Transição para próximo ponto
5. CENTRALIDADE NA CRIANÇA:
Sempre volte ao estudante como protagonista do processo ensino-aprendizagem.
6. POSICIONAMENTO PESSOAL:
Use "Considero que...", "Entendo que...", "Creio que...", "Ao meu ver..." para se posicionar.
7. O QUE EVITAR:
- ❌ "de acordo com" → use "conforme", "segundo", "a partir de"
- ❌ "no dia a dia" → use "cotidianamente", "no cotidiano"
- ❌ "em relação a" → use "sobre", "acerca de", "a respeito de"
- ❌ fragmentação em frases curtas
- ❌ gírias ou linguagem informal
- ❌ linguagem burocrática vazia
═══════════════════════════════════════════════
EXEMPLOS REAIS DA ESCRITA DA CAMILA
═══════════════════════════════════════════════
EXEMPLO 1 — Relatório HTPC (avaliação anual):
"O professor como observador, faz escuta ativa da criança, para compreender o que mais chama atenção da turma, leva para a roda de conversa, aponta as curiosidades e assim consegue planejar e executar as práticas pedagógicas, colocando as crianças como protagonistas do processo ensino-aprendizagem. O planejamento se baseia principalmente nos interesses das crianças, trazendo situações que desafiam e aguçam seus interesses e curiosidade."
EXEMPLO 2 — Carta de Intenções:
"Inicio esta carta de intenções para o ano de 2026 com esta citação atribuída à escritora Lya Luft, pois reconheço claramente que a organização e o atendimento estabelecidos na Educação Infantil atuam como fundamentos desta etapa basilar da vida humana, merecendo especial atenção, dos aspectos simples aos mais complexos, e da qualidade deste trabalho dependem as fases posteriores de crescimento, aprendizagem, amadurecimento e desenvolvimento integral de cada criança."
EXEMPLO 3 — Planejamento quinzenal:
"Durante a visita ao mercadinho, pretendo propor que as crianças criem uma salad nutritiva, a partir dos vegetais que encontrarem nos setores de hortifruti, latarias e congelados, verificando o preço desses alimentos, e posteriormente faremos uma listagem verificando quais tipos de vitaminas e sais minerais encontraremos nessa salada e sua funcionalidade para o organismo."
EXEMPLO 4 — Registro de formação:
"Fomos divididas nos seguintes grupos: Grupo 1 — Viviane, Camila, Patrícia e Vanessa: jogos estruturados, conversamos sobre apresentar jogos de chão para ir trabalhando regras gerais com as crianças, para depois utilizar material ATTO e Mind Lab com regras mais estabelecidas, cuidando das peças, utilizar Lego, repertoriar as crianças, deixar explorar o material utilizando peças maiores, deixar as crianças criando por um momento livres e depois contextualizando as montagens com alguma história."
═══════════════════════════════════════════════
CONHECIMENTO ESPECÍFICO DA CAMILA
═══════════════════════════════════════════════
PROJETOS E VIVÊNCIAS DA ESCOLA:
- Entradas coletivas (com presentations de poesias)
- Atividades intersalas (jogos de tabuleiro, brincadeiras coletivas, tema)
- Piquenique literário (contração de histórias pelas crianças do Infantil IV e V)
- Saraus de poesia
- Painéis coletivos e murais
- Dia da família na escola
- Sábado letivo (apresentações de turmas para famílias)
- Estudo do meio (ex: Borboletário de Diadema)
- Mercadinho (sala 4 — trabalho com números, preços, lista)
- Projeto de leitura (empréstimo de livros da BEI)
- Show de acrobacias, baile à fantasia, cabelo maluco
- Teatro de bonecos, cineminha com pipoca
- Gincana, cataventos
MOMENTOS DO HTPC:
- "Nutrição" (momento inicial com livro/vídeo compartilhado)
- Frase do dia
- Momento da brincadeira
- Pauta da reunião
- Grupose para discussão
- Encerramento com registro
LIVROS INFANTIS CONHECIDOS:
- "Shhh! Nós temos um plano" (Chris Haughton)
- "A Galinha Ruiva" (Byron Barton)
- "Marcelo, Marmelo, Martelo" (Ruth Rocha)
- "O Ponto" (Peter H. Reynolds)
- "O Fabricante de Lápis"
- "A Primeira Infância" (vídeo)
- "Como os doces brasileiros ficaram tão doces" (vídeo)
FORMADORAS/MENTORAS:
- Formadora Márcia (formações sobre leitura e escrita)
- Sirlene (coordenadora)
- Fernanda (coordenadora)
- Maria (gestora)
METODOLOGIA:
- Mind Lab (jogos certificados INMETRO: Tik Tak Toy, Hora do Rush, Caixa Mágica, Resta-Um, Quatro-em-linha)
- Material ATTO (durável,替代 Mind Lab para jogos de chão)
- Brincadeiras musicais e cantigas de roda
- Contação de histórias
- Desenvolvimento fonológico (rimas, ritmo)
- Desenvolvimento sintático (estruturação de frases)
- Desenvolvimento semântico (significados e vocabulário)
- Desenvolvimento pragmático (uso social da linguagem)
- Professor como escriba
BNCC/CAMPOS DE EXPERIÊNCIA:
- O eu, o outro e o nós
- Corpo, gestos e movimentos
- Traços, sons, cores e formas
- Escuta, fala, pensamento e imaginação
- Espaços, tempos, quantidades, relações e transformações
ESCRITA DA CRIANÇA:
- Hipótese de escrita silábico-alfabética
- Hypothesis de escrita alfabética
═══════════════════════════════════════════════
REGRAS DE ATUAÇÃO
═══════════════════════════════════════════════
1. Quando a Camila pedir um documento (relatório, planejamento, etc):
- Seguir ESTRITAMENTE o estilo de escrita dela
- Usar frases longas e encadeadas
- Incluir vocabulário técnico-pedagógico natural
- Posicionar-se pessoalmente quando apropriado
- Manter centralidade na criança
2. Quando pedir resumo de livro infantil:
- Identificar temas, personagens, lições
- Sugerir atividades práticas de sala
- Conectar com BNCC e campos de experiência
3. Quando pedir pesquisa pedagógica:
- Fundamentar com referências reais
- Dar exemplos práticos conectando com a realidade da turma
4. Para relatórios HTPC, usar estrutura:
- Planejamento e execução das práticas pedagógicas
- Coerência entre propostas e desenvolvimento
- Diversidade de experiências e intencionalidade educativa
- Avaliação e acompanhamento das aprendizagens
5. Nunca inventar nomes de crianças, atividades ou datas. Use apenas o que conhece dos documentos.
6. Referências: usar "conforme a BNCC", "segundo a Proposta Curricular de SBC", "a partir do documento orientador".
Responda SEMPRE em português do Brasil.`
};
// Rota de Chat com OpenRouter com Smart Router Multimodal integrado (MiniMax)
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' });
}
const lastMessage = messages[messages.length - 1];
const promptText = lastMessage ? lastMessage.content : '';
// 1. Detectar se há imagens anexadas para desviar para OpenRouter multimodal (Vision)
const hasAttachedImages = promptText.includes('*[Imagem anexada:');
let intent = 'TEXT';
// Executar classificação apenas se houver prompt de texto e não houver imagens anexadas
if (!hasAttachedImages && promptText && (process.env.OPENROUTER_API_KEY || process.env.GROQ_API_KEY)) {
try {
const systemDetector = "Você é um classificador de intenções de prompt. Classifique a última mensagem do usuário em uma destas quatro categorias exatas: 'TEXT' (para conversas normais, perguntas, redação de relatórios, planejamentos, textos pedagógicos, respostas gerais), 'IMAGE' (se o usuário estiver pedindo explicitamente para gerar, desenhar, criar ou ilustrar uma imagem, figura, ilustração ou foto), 'AUDIO' (se o usuário estiver pedindo explicitamente para gerar uma música, melodia, trilha sonora, cantiga ou som), ou 'VIDEO' (se o usuário estiver pedindo explicitamente para gerar ou criar um vídeo ou animação). Responda APENAS com a palavra da categoria em letras maiúsculas: TEXT, IMAGE, AUDIO, VIDEO.";
let detectResponse;
let usingGroqForDetection = false;
try {
detectResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
messages: [
{ role: 'system', content: systemDetector },
{ role: 'user', content: promptText }
],
max_tokens: 20,
temperature: 0.0
})
});
} catch (err) {
console.error('[Smart Router] Falha de conexão com OpenRouter para detecção, tentando Groq...', err.message);
}
// Se OpenRouter falhou ou retornou erro (ex: status != 200), tenta o Groq como fallback
if ((!detectResponse || !detectResponse.ok) && process.env.GROQ_API_KEY) {
console.log('[Smart Router] Usando Groq (llama-3.1-8b-instant) para classificar intenção...');
try {
detectResponse = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'llama-3.1-8b-instant',
messages: [
{ role: 'system', content: systemDetector },
{ role: 'user', content: promptText }
],
max_tokens: 20,
temperature: 0.0
})
});
usingGroqForDetection = detectResponse.ok;
} catch (groqErr) {
console.error('[Smart Router] Falha de detecção na API da Groq:', groqErr.message);
}
}
if (detectResponse && detectResponse.ok) {
const detectData = await detectResponse.json();
const detectedText = detectData.choices?.[0]?.message?.content?.toUpperCase() || 'TEXT';
console.log(`[Smart Router] Resposta crua do detector: "${detectedText}"`);
if (detectedText.includes('IMAGE')) {
intent = 'IMAGE';
} else if (detectedText.includes('AUDIO') || detectedText.includes('MUSIC')) {
intent = 'AUDIO';
} else if (detectedText.includes('VIDEO')) {
intent = 'VIDEO';
} else {
intent = 'TEXT';
}
console.log(`[Smart Router] Intenção detectada para o prompt "${promptText.substring(0, 40)}...": ${intent}`);
} else {
const errText = await detectResponse.text();
console.error('[Smart Router] Chamada de detecção retornou erro:', errText);
}
} catch (err) {
console.error('Erro no classificador de intenções (usando fallback TEXT):', err);
}
}
// ==========================================================================
// FLUXO DE IMAGEM (MiniMax image-01)
// ==========================================================================
if (intent === 'IMAGE') {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Iniciando geração de imagem com o OpenRouter...' })}\n\n`);
try {
// Prompt rewriting: pedir à IA para enriquecer o prompt em inglês
let enrichedPrompt = promptText;
try {
const rewriteResp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
messages: [
{ role: 'system', content: "Reescreva o pedido de imagem do usuário em inglês como um prompt altamente detalhado para geradores de imagem (como Midjourney/Stable Diffusion). Adicione estilo de ilustração infantil fofa, vibrante, adequada para educação infantil, traço limpo, cores harmoniosas, sem textos/letras na imagem. Retorne apenas a tradução/prompt em inglês." },
{ role: 'user', content: promptText }
],
max_tokens: 150,
temperature: 0.7
})
});
if (rewriteResp.ok) {
const rewriteData = await rewriteResp.json();
enrichedPrompt = rewriteData.choices?.[0]?.message?.content?.trim() || promptText;
console.log('[Smart Router] Prompt reescrito para imagem:', enrichedPrompt);
}
} catch (err) {
console.error('Erro ao reescrever prompt de imagem:', err);
}
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Gerando ilustração pedagógica com o OpenRouter...' })}\n\n`);
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'x-ai/grok-imagine-image-quality',
messages: [
{ role: 'user', content: enrichedPrompt }
],
modalities: ['image']
})
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`OpenRouter respondeu com erro ao gerar imagem: ${errText}`);
}
const imgData = await response.json();
const imageObj = imgData.choices?.[0]?.message?.images?.[0];
const imageUrl = imageObj?.image_url?.url;
if (!imageUrl) {
throw new Error('Nenhuma imagem foi retornada pelo OpenRouter');
}
res.write(`data: ${JSON.stringify({ type: 'image', url: imageUrl, originalPrompt: promptText })}\n\n`);
res.write('data: [DONE]\n\n');
return res.end();
} catch (error) {
console.error('Erro na geração de imagem:', error);
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao gerar a imagem: ${error.message}` })}\n\n`);
res.write('data: [DONE]\n\n');
return res.end();
}
}
// ==========================================================================
// FLUXO DE ÁUDIO/MÚSICA (Mensagem Pedagógica Amigável - MiniMax Desativado)
// ==========================================================================
// ==========================================================================
// FLUXO DE ÁUDIO/MÚSICA (google/lyria-3-clip-preview)
// ==========================================================================
if (intent === 'AUDIO') {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'Compondo e gerando música pedagógica com Lyria AI...' })}\n\n`);
try {
if (!process.env.OPENROUTER_API_KEY) {
throw new Error('OPENROUTER_API_KEY não configurada no servidor.');
}
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
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: 'google/lyria-3-clip-preview',
messages: [
{ role: 'user', content: promptText }
],
modalities: ['audio'],
stream: true
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro na API do OpenRouter (Lyria): ${errorText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let audioBase64Chunks = [];
let textContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const cleanLine = line.trim();
if (cleanLine.startsWith('data: ')) {
const dataContent = cleanLine.slice(6);
if (dataContent === '[DONE]') continue;
try {
const parsed = JSON.parse(dataContent);
const delta = parsed.choices?.[0]?.delta;
if (delta) {
if (delta.content) {
textContent += delta.content;
res.write(`data: ${JSON.stringify({ content: delta.content })}\n\n`);
}
if (delta.audio?.data) {
audioBase64Chunks.push(delta.audio.data);
}
}
} catch (e) {
// Ignorar
}
}
}
}
// Processar restante do 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 delta = parsed.choices?.[0]?.delta;
if (delta) {
if (delta.content) {
textContent += delta.content;
res.write(`data: ${JSON.stringify({ content: delta.content })}\n\n`);
}
if (delta.audio?.data) {
audioBase64Chunks.push(delta.audio.data);
}
}
} catch (e) {
// Ignorar
}
}
}
}
if (audioBase64Chunks.length > 0) {
const fullAudioBase64 = audioBase64Chunks.join('');
const audioUrl = `data:audio/mp3;base64,${fullAudioBase64}`;
// Envia a mídia estruturada para o frontend
res.write(`data: ${JSON.stringify({ type: 'audio', url: audioUrl, originalPrompt: promptText })}\n\n`);
} else {
// Fallback se não retornou áudio (apenas texto)
if (textContent) {
res.write(`data: ${JSON.stringify({ content: '\n\n*(Nota: O áudio não pôde ser gerado por limitações do modelo, mas a letra acima foi composta com carinho!)*' })}\n\n`);
} else {
throw new Error('Nenhum dado de áudio ou texto foi retornado pelo modelo Lyria.');
}
}
res.write('data: [DONE]\n\n');
return res.end();
} catch (error) {
console.error('Erro na resposta de áudio com Lyria:', error);
res.write(`data: ${JSON.stringify({ content: `Desculpe, ocorreu um erro ao gerar a música: ${error.message}. No entanto, se você quiser, eu posso compor uma letra pedagógica sobre o assunto para você!` })}\n\n`);
res.write('data: [DONE]\n\n');
return res.end();
}
}
// ==========================================================================
// FLUXO DE VÍDEO (Mensagem Pedagógica Amigável - MiniMax Desativado)
// ==========================================================================
if (intent === 'VIDEO') {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write(`data: ${JSON.stringify({ type: 'media_status', message: 'A generation de vídeos no momento está indisponível...' })}\n\n`);
try {
const message = "A geração de vídeos no momento está indisponível devido às limitações do plano. No entanto, se você quiser, eu posso criar um roteiro pedagógico de vídeo ou de animação super detalhado para você! É só me dizer o tema que deseja abordar.";
res.write(`data: ${JSON.stringify({ content: message })}\n\n`);
res.write('data: [DONE]\n\n');
return res.end();
} catch (error) {
console.error('Erro na resposta de vídeo:', error);
res.write(`data: ${JSON.stringify({ content: `Ocorreu um erro ao gerar o vídeo: ${error.message}` })}\n\n`);
res.write('data: [DONE]\n\n');
return res.end();
}
}
// ==========================================================================
// FLUXO PADRÃO DE TEXTO (OpenRouter com SSE streaming original, com fallback Groq)
// ==========================================================================
const enrichedMessages = [KEMILY_SYSTEM_PROMPT, ...messages];
try {
let chatResponse;
let providerUsed = 'OpenRouter';
try {
chatResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
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
})
});
} catch (e) {
console.error('Erro na conexão com OpenRouter, tentando Groq...', e.message);
}
// Se o OpenRouter falhar ou retornar erro (ex: status != 200), caímos no fallback do Groq
if ((!chatResponse || !chatResponse.ok) && process.env.GROQ_API_KEY) {
console.log('[Smart Router] Usando Groq (llama-3.3-70b-versatile) como fallback de streaming de texto...');
providerUsed = 'Groq';
try {
chatResponse = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.GROQ_MODEL || 'llama-3.3-70b-versatile',
messages: enrichedMessages,
stream: true
})
});
} catch (groqErr) {
console.error('[Smart Router] Erro crítico: Falha no fallback do Groq também:', groqErr.message);
}
}
if (!chatResponse || !chatResponse.ok) {
const errorText = chatResponse ? await chatResponse.text() : 'Sem conexão com os servidores de IA';
console.error(`Erro na API de IA (${providerUsed}):`, errorText);
return res.status(500).json({ error: `Erro ao processar requisição na API de IA (${providerUsed})` });
}
// 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 = chatResponse.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}`);
});