// Configuração do Markdown parser personalizado com Highlight.js integrado const renderer = new marked.Renderer(); // Função auxiliar para escapar caracteres HTML function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } renderer.code = function(code, language) { // Ajuste para lidar com as diferenças na assinatura do marked.js const codeContent = typeof code === 'object' ? code.text : code; const lang = (typeof code === 'object' ? code.lang : language) || 'txt'; return `
${lang}
${escapeHtml(codeContent)}
`; }; marked.use({ renderer }); // Função global de copiar código para que funcione no onclick window.copyCode = function(button) { const container = button.closest('.code-container'); const codeElement = container.querySelector('code'); const textToCopy = codeElement.textContent; navigator.clipboard.writeText(textToCopy).then(() => { const textSpan = button.querySelector('span'); const originalText = textSpan.textContent; textSpan.textContent = 'Copiado!'; button.style.color = 'var(--success)'; setTimeout(() => { textSpan.textContent = originalText; button.style.color = ''; }, 2000); }).catch(err => { console.error('Erro ao copiar código:', err); }); }; document.addEventListener('DOMContentLoaded', () => { // Elementos do DOM const sidebar = document.getElementById('sidebar'); const btnMenu = document.getElementById('btnMenu'); const btnCloseSidebar = document.getElementById('btnCloseSidebar'); const sidebarOverlay = document.getElementById('sidebarOverlay'); const userInput = document.getElementById('userInput'); const btnSend = document.getElementById('btnSend'); const chatForm = document.getElementById('chatForm'); const messagesContainer = document.getElementById('messagesContainer'); const welcomeContainer = document.getElementById('welcomeContainer'); const conversationList = document.getElementById('conversationList'); const btnNewChat = document.getElementById('btnNewChat'); const btnNewChatMobile = document.getElementById('btnNewChatMobile'); const historyItems = document.getElementById('historyItems'); const btnLogout = document.getElementById('btnLogout'); const suggestionCards = document.querySelectorAll('.suggestion-card'); // Variáveis de Estado let chats = JSON.parse(localStorage.getItem('camila_chats')) || []; let currentChatId = localStorage.getItem('camila_current_chat_id') || null; let isGenerating = false; // ========================================================================== // CONTROLES DE INTERFACE & SIDEBAR // ========================================================================== const toggleSidebar = (open) => { if (open) { sidebar.classList.add('open'); } else { sidebar.classList.remove('open'); } }; btnMenu.addEventListener('click', () => toggleSidebar(true)); btnCloseSidebar.addEventListener('click', () => toggleSidebar(false)); sidebarOverlay.addEventListener('click', () => toggleSidebar(false)); // Redimensionamento automático do Input de Texto (textarea) userInput.addEventListener('input', () => { userInput.style.height = 'auto'; userInput.style.height = (userInput.scrollHeight - 4) + 'px'; // Habilitar ou desabilitar botão btnSend.disabled = userInput.value.trim() === '' || isGenerating; }); // Prevenir comportamento padrão do Enter no textarea (enviar em vez de quebrar linha, Shift+Enter para nova linha) userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (userInput.value.trim() !== '' && !isGenerating) { chatForm.requestSubmit(); } } }); // ========================================================================== // HISTÓRICO DE CHATS (LOCALSTORAGE) // ========================================================================== const saveChats = () => { localStorage.setItem('camila_chats', JSON.stringify(chats)); if (currentChatId) { localStorage.setItem('camila_current_chat_id', currentChatId); } else { localStorage.removeItem('camila_current_chat_id'); } }; const renderHistory = () => { historyItems.innerHTML = ''; if (chats.length === 0) { historyItems.innerHTML = '
Nenhum chat salvo
'; return; } // Ordenar por timestamp mais recente const sortedChats = [...chats].sort((a, b) => b.updatedAt - a.updatedAt); sortedChats.forEach(chat => { const item = document.createElement('div'); item.className = `history-item ${chat.id === currentChatId ? 'active' : ''}`; item.dataset.id = chat.id; item.innerHTML = `
${escapeHtml(chat.title)}
`; // Selecionar chat item.addEventListener('click', (e) => { if (e.target.closest('.btn-delete-chat')) { e.stopPropagation(); deleteChat(chat.id); return; } selectChat(chat.id); toggleSidebar(false); }); historyItems.appendChild(item); }); }; const selectChat = (id) => { currentChatId = id; const chat = chats.find(c => c.id === id); if (!chat) return; saveChats(); renderHistory(); // Ocultar welcome e renderizar as mensagens welcomeContainer.style.display = 'none'; conversationList.innerHTML = ''; chat.messages.forEach(msg => { appendMessageUI(msg.role, msg.content); }); scrollToBottom(); }; const createNewChat = () => { if (isGenerating) return; currentChatId = null; localStorage.removeItem('camila_current_chat_id'); welcomeContainer.style.display = 'flex'; conversationList.innerHTML = ''; userInput.value = ''; userInput.style.height = 'auto'; btnSend.disabled = true; renderHistory(); }; const deleteChat = (id) => { chats = chats.filter(c => c.id !== id); if (currentChatId === id) { currentChatId = null; createNewChat(); } else { saveChats(); renderHistory(); } }; btnNewChat.addEventListener('click', createNewChat); btnNewChatMobile.addEventListener('click', createNewChat); // ========================================================================== // FLUXO DO CHAT & RENDERIZAÇÃO // ========================================================================== const appendMessageUI = (role, content) => { const isUser = role === 'user'; const row = document.createElement('div'); row.className = `message-row ${role}`; const avatarHtml = isUser ? `
U
` : `
C
`; let contentHtml = ''; if (isUser) { contentHtml = `

${escapeHtml(content).replace(/\n/g, '
')}

`; } else { contentHtml = marked.parse(content); } row.innerHTML = `
${avatarHtml}
${contentHtml}
`; conversationList.appendChild(row); // Colorir blocos de código com Highlight.js row.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); return row; }; const scrollToBottom = () => { messagesContainer.scrollTop = messagesContainer.scrollHeight; }; // Função principal de envio de mensagens const handleSendMessage = async (text) => { if (!text || isGenerating) return; isGenerating = true; btnSend.disabled = true; userInput.disabled = true; userInput.value = ''; userInput.style.height = 'auto'; // Ocultar tela de boas-vindas se for a primeira mensagem if (welcomeContainer.style.display !== 'none') { welcomeContainer.style.display = 'none'; } // Criar o chat se for novo if (!currentChatId) { const newId = 'chat_' + Date.now(); const firstLine = text.split('\n')[0]; const title = firstLine.length > 26 ? firstLine.substring(0, 26) + '...' : firstLine; chats.push({ id: newId, title: title, messages: [], createdAt: Date.now(), updatedAt: Date.now() }); currentChatId = newId; } // Achar o chat atual no estado const chat = chats.find(c => c.id === currentChatId); if (!chat) return; // Adicionar mensagem do usuário no estado e na tela const userMsg = { role: 'user', content: text }; chat.messages.push(userMsg); chat.updatedAt = Date.now(); appendMessageUI('user', text); scrollToBottom(); // Salvar estado temporário do chat saveChats(); renderHistory(); // Criar container para a resposta do bot (Camila AI) na tela const botRow = document.createElement('div'); botRow.className = 'message-row assistant'; botRow.innerHTML = `
C
`; conversationList.appendChild(botRow); scrollToBottom(); const botContentDiv = botRow.querySelector('.message-content'); let assistantContent = ''; try { // Filtrar mensagens para enviar apenas o histórico necessário ao OpenRouter const messagesPayload = chat.messages.map(m => ({ role: m.role, content: m.content })); // Fazer a chamada HTTP usando stream const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: messagesPayload }) }); if (!response.ok) { throw new Error('Falha na resposta do servidor'); } // Processar stream de dados (SSE) 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'); // Deixar a última parte 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]') { continue; } try { const parsed = JSON.parse(dataContent); if (parsed.content) { assistantContent += parsed.content; // Renderizar markdown em progresso (com o cursor no final) botContentDiv.innerHTML = marked.parse(assistantContent) + ''; // Colorir códigos em tempo real botContentDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); scrollToBottom(); } } catch (err) { // Ignorar erros de JSON incompletos do buffer } } } } // Remover o cursor de digitação no final const cursor = botContentDiv.querySelector('.typing-cursor'); if (cursor) cursor.remove(); // Parse final limpo do markdown botContentDiv.innerHTML = marked.parse(assistantContent); botContentDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); scrollToBottom(); // Salvar resposta no estado e localStorage const assistantMsg = { role: 'assistant', content: assistantContent }; chat.messages.push(assistantMsg); chat.updatedAt = Date.now(); saveChats(); renderHistory(); } catch (error) { console.error('Erro na resposta do chat:', error); const cursor = botContentDiv.querySelector('.typing-cursor'); if (cursor) cursor.remove(); botContentDiv.innerHTML += `

Desculpe, ocorreu um erro ao obter resposta da Camila AI. Por favor, tente novamente.

`; scrollToBottom(); } finally { isGenerating = false; btnSend.disabled = userInput.value.trim() === ''; userInput.disabled = false; userInput.focus(); } }; // Envio pelo Formulário chatForm.addEventListener('submit', (e) => { e.preventDefault(); const text = userInput.value.trim(); handleSendMessage(text); }); // Sugestões de Prompt Rápidas suggestionCards.forEach(card => { card.addEventListener('click', () => { const prompt = card.dataset.prompt; if (prompt) { handleSendMessage(prompt); } }); }); // ========================================================================== // LOGOUT & INICIALIZAÇÃO // ========================================================================== btnLogout.addEventListener('click', async () => { try { const response = await fetch('/api/logout', { method: 'POST' }); if (response.ok) { window.location.href = '/login'; } } catch (error) { console.error('Erro ao fazer logout:', error); } }); // Inicializar interface if (currentChatId) { // Tenta carregar o chat ativo const activeChat = chats.find(c => c.id === currentChatId); if (activeChat) { selectChat(currentChatId); } else { createNewChat(); } } else { createNewChat(); } });