🚀 Auto-deploy: Camila atualizado em 25/05/2026 19:09:25
This commit is contained in:
+471
@@ -0,0 +1,471 @@
|
||||
// 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, """)
|
||||
.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 `
|
||||
<div class="code-container">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">${lang}</span>
|
||||
<button class="btn-copy-code" onclick="copyCode(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-3a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5M16.5 9h3.75c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125H16.5m-3-12h3m-3 3h3M13.5 14.25h3" />
|
||||
</svg>
|
||||
<span>Copiar</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre><code class="hljs language-${lang}">${escapeHtml(codeContent)}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
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 = '<div style="color: var(--text-muted); font-size: 12px; padding: 10px; text-align: center;">Nenhum chat salvo</div>';
|
||||
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 = `
|
||||
<div class="history-item-title">${escapeHtml(chat.title)}</div>
|
||||
<div class="history-item-actions">
|
||||
<button class="btn-history-action btn-delete-chat" aria-label="Excluir chat">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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
|
||||
? `<div class="avatar user-avatar">U</div>`
|
||||
: `<div class="avatar bot-avatar">C</div>`;
|
||||
|
||||
let contentHtml = '';
|
||||
if (isUser) {
|
||||
contentHtml = `<p>${escapeHtml(content).replace(/\n/g, '<br>')}</p>`;
|
||||
} else {
|
||||
contentHtml = marked.parse(content);
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="message-icon">${avatarHtml}</div>
|
||||
<div class="message-content">${contentHtml}</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="message-icon">
|
||||
<div class="avatar bot-avatar">C</div>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<span class="typing-cursor"></span>
|
||||
</div>
|
||||
`;
|
||||
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) + '<span class="typing-cursor"></span>';
|
||||
|
||||
// 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 += `
|
||||
<p style="color: var(--error); margin-top: 8px; font-size: 13.5px; display: flex; align-items: center; gap: 6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 16px; height: 16px;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
Desculpe, ocorreu um erro ao obter resposta da Camila AI. Por favor, tente novamente.
|
||||
</p>
|
||||
`;
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Camila AI</title>
|
||||
<!-- Google Fonts Outfit & Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Highlight.js para destaque de sintaxe em códigos -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="shortcut icon" href="https://chatgpt.com/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Sidebar / Menu lateral (Deslizante no mobile) -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<button class="btn-new-chat" id="btnNewChat">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>Novo chat</span>
|
||||
</button>
|
||||
<button class="btn-close-sidebar" id="btnCloseSidebar" aria-label="Fechar menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Histórico de Conversas -->
|
||||
<div class="chat-history" id="chatHistory">
|
||||
<!-- Grupos de datas dinâmicos por JS -->
|
||||
<div class="history-group">
|
||||
<h3>Hoje</h3>
|
||||
<div class="history-items" id="historyItems">
|
||||
<!-- Items gerados por JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rodapé do Menu Lateral -->
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-profile">
|
||||
<div class="avatar user-avatar">C</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">Camila Reifonas</span>
|
||||
<span class="user-status">Online</span>
|
||||
</div>
|
||||
<button class="btn-logout" id="btnLogout" title="Sair">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Overlay para fechar sidebar clicando fora no mobile -->
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
|
||||
<!-- Área principal do Chat -->
|
||||
<main class="chat-area">
|
||||
<!-- Topo fixo / Header -->
|
||||
<header class="chat-header">
|
||||
<button class="btn-menu" id="btnMenu" aria-label="Abrir menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="model-badge">
|
||||
<span>GPT-4.1 Nano</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="chevron">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<button class="btn-new-chat-mobile" id="btnNewChatMobile" aria-label="Novo chat">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Mensagens da conversa -->
|
||||
<div class="messages-container" id="messagesContainer">
|
||||
<!-- Estado inicial / Bem-vindo (se não houver mensagens) -->
|
||||
<div class="welcome-container" id="welcomeContainer">
|
||||
<div class="welcome-logo">
|
||||
<div class="logo-outer">
|
||||
<div class="logo-inner">C</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Como posso ajudar hoje?</h2>
|
||||
|
||||
<div class="suggestions-grid">
|
||||
<button class="suggestion-card" data-prompt="Me dê ideias criativas de nomes para uma marca de roupas.">
|
||||
<h4>Ideias criativas</h4>
|
||||
<p>Nomes para uma marca de roupas</p>
|
||||
</button>
|
||||
<button class="suggestion-card" data-prompt="Escreva um script básico em Python para ler um arquivo JSON.">
|
||||
<h4>Escrever código</h4>
|
||||
<p>Script simples em Python para JSON</p>
|
||||
</button>
|
||||
<button class="suggestion-card" data-prompt="Ajude-me a preparar um roteiro de viagem de 3 dias para o Rio de Janeiro.">
|
||||
<h4>Roteiro de viagem</h4>
|
||||
<p>3 dias no Rio de Janeiro</p>
|
||||
</button>
|
||||
<button class="suggestion-card" data-prompt="Explique como funciona o conceito de Server-Sent Events (SSE).">
|
||||
<h4>Explicar conceitos</h4>
|
||||
<p>Como funciona o Server-Sent Events</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de mensagens geradas dinamicamente -->
|
||||
<div class="conversation-list" id="conversationList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Barra inferior com input de texto -->
|
||||
<footer class="chat-footer">
|
||||
<div class="input-container">
|
||||
<form id="chatForm">
|
||||
<div class="input-box">
|
||||
<textarea id="userInput" placeholder="Mensagem Camila AI..." rows="1" autocomplete="off"></textarea>
|
||||
<button type="submit" id="btnSend" disabled aria-label="Enviar mensagem">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M11.47 2.47a.75.75 0 0 1 1.06 0l7.5 7.5a.75.75 0 1 1-1.06 1.06l-6.22-6.22V21a.75.75 0 0 1-1.5 0V4.81L5.03 11.03a.75.75 0 0 1-1.06-1.06l7.5-7.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="disclaimer">Camila AI pode cometer erros. Considere verificar informações importantes.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Bibliotecas externas para renderizar markdown e blocos de código -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Camila - Entrar</title>
|
||||
<!-- Google Fonts Outfit & Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="shortcut icon" href="https://chatgpt.com/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<div class="login-glow"></div>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="logo-orb">
|
||||
<div class="logo-inner">C</div>
|
||||
</div>
|
||||
<h1>Camila AI</h1>
|
||||
<p>Insira a senha de acesso para entrar no workspace</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="login-form">
|
||||
<div class="input-group">
|
||||
<label for="password">Senha do Portal</label>
|
||||
<div class="input-wrapper">
|
||||
<input type="password" id="password" required placeholder="Digite a senha..." autocomplete="current-password">
|
||||
<button type="button" id="togglePassword" class="toggle-password" aria-label="Mostrar senha">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
|
||||
<button type="submit" id="btnSubmit" class="btn-submit">
|
||||
<span>Entrar no Workspace</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="arrow-icon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,76 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const togglePasswordBtn = document.getElementById('togglePassword');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const btnSubmit = document.getElementById('btnSubmit');
|
||||
|
||||
// Alternar visualização da senha
|
||||
togglePasswordBtn.addEventListener('click', () => {
|
||||
const isPassword = passwordInput.getAttribute('type') === 'password';
|
||||
passwordInput.setAttribute('type', isPassword ? 'text' : 'password');
|
||||
|
||||
// Altera o ícone do olho
|
||||
togglePasswordBtn.innerHTML = isPassword
|
||||
? `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.815 7.815 3 3m-3-3-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>`
|
||||
: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>`;
|
||||
});
|
||||
|
||||
// Submissão do formulário
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Resetar estado de erro
|
||||
errorMessage.classList.remove('visible');
|
||||
errorMessage.textContent = '';
|
||||
|
||||
const password = passwordInput.value;
|
||||
if (!password) return;
|
||||
|
||||
// Desabilitar botão e mostrar loading
|
||||
btnSubmit.disabled = true;
|
||||
btnSubmit.classList.add('loading');
|
||||
btnSubmit.querySelector('span').textContent = 'Conectando...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// Sucesso - redireciona para a página principal
|
||||
btnSubmit.querySelector('span').textContent = 'Autenticado!';
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 600);
|
||||
} else {
|
||||
// Mostrar erro
|
||||
throw new Error(data.error || 'Acesso negado');
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.textContent = error.message;
|
||||
errorMessage.classList.add('visible');
|
||||
|
||||
// Habilitar botão novamente
|
||||
btnSubmit.disabled = false;
|
||||
btnSubmit.classList.remove('loading');
|
||||
btnSubmit.querySelector('span').textContent = 'Entrar no Workspace';
|
||||
|
||||
// Feedback vibratório (se houver API) e animação shake no card
|
||||
const card = document.querySelector('.login-card');
|
||||
card.classList.add('shake');
|
||||
setTimeout(() => card.classList.remove('shake'), 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
+1067
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user