From 6c35f7ed1b59a1a1794a3d479c48a84331a98c10 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Tue, 26 May 2026 19:40:28 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20feat:=20Sistema=20de=20identidad?= =?UTF-8?q?e=20din=C3=A2mica=20do=20agente=20-=20modal=20de=20configura?= =?UTF-8?q?=C3=A7=C3=B5es,=20avatares=20customiz=C3=A1veis,=20nome=20din?= =?UTF-8?q?=C3=A2mico=20sincronizado=20com=20Hermes/Telegram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent_config.json | 5 ++ public/app.js | 187 +++++++++++++++++++++++++++++++++++++++++++++- public/index.html | 48 +++++++++++- public/style.css | 185 +++++++++++++++++++++++++++++++++++++++++++++ server.js | 96 +++++++++++++++++++++++- 5 files changed, 514 insertions(+), 7 deletions(-) create mode 100644 agent_config.json diff --git a/agent_config.json b/agent_config.json new file mode 100644 index 0000000..1e7ceaf --- /dev/null +++ b/agent_config.json @@ -0,0 +1,5 @@ +{ + "agentName": "Kemily", + "kemilyAvatarUrl": "assets/kemily.png", + "camilaAvatarUrl": "assets/camila_prof.png" +} diff --git a/public/app.js b/public/app.js index bbb976b..2691ce6 100644 --- a/public/app.js +++ b/public/app.js @@ -94,6 +94,187 @@ const initApp = () => { const searchClearAll = document.getElementById('searchClearAll'); const sidebarTags = document.getElementById('sidebarTags'); + // Configuração de Identidade do Agente (Kemily/Camila) + const btnSettings = document.getElementById('btnSettings'); + const settingsModal = document.getElementById('settingsModal'); + const closeSettingsModal = document.getElementById('closeSettingsModal'); + const btnSaveSettings = document.getElementById('btnSaveSettings'); + const settingAgentName = document.getElementById('settingAgentName'); + const previewKemilyAvatar = document.getElementById('previewKemilyAvatar'); + const uploadKemilyAvatar = document.getElementById('uploadKemilyAvatar'); + const previewCamilaAvatar = document.getElementById('previewCamilaAvatar'); + const uploadCamilaAvatar = document.getElementById('uploadCamilaAvatar'); + const welcomeTitle = document.getElementById('welcomeTitle'); + const welcomeAvatarImg = document.getElementById('welcomeAvatarImg'); + const sidebarAvatarImg = document.getElementById('sidebarAvatarImg'); + + window.agentConfig = { + agentName: "Kemily", + kemilyAvatarUrl: "assets/kemily.png", + camilaAvatarUrl: "assets/camila_prof.png" + }; + + // Carrega as configurações do servidor + const loadAgentConfig = async () => { + try { + const res = await fetch('/api/agent-config'); + if (res.ok) { + window.agentConfig = await res.json(); + updateAgentIdentityUI(); + } + } catch (e) { + console.error('Erro ao carregar configurações do agente:', e); + } + }; + + const updateAgentIdentityUI = () => { + if (welcomeTitle) { + welcomeTitle.innerText = `Olá! Sou sua assistente virtual e me chamo ${window.agentConfig.agentName}. Como posso ajudar?`; + } + if (welcomeAvatarImg) { + welcomeAvatarImg.src = window.agentConfig.kemilyAvatarUrl; + } + if (sidebarAvatarImg) { + sidebarAvatarImg.src = window.agentConfig.camilaAvatarUrl; + } + if (settingAgentName) { + settingAgentName.value = window.agentConfig.agentName; + } + if (previewKemilyAvatar) { + previewKemilyAvatar.src = window.agentConfig.kemilyAvatarUrl; + } + if (previewCamilaAvatar) { + previewCamilaAvatar.src = window.agentConfig.camilaAvatarUrl; + } + }; + + // Variáveis temporárias para avatares (em base64 ou URLs novas) + let newKemilyAvatarData = null; + let newCamilaAvatarData = null; + + // Eventos do Modal + if (btnSettings) { + btnSettings.addEventListener('click', () => { + newKemilyAvatarData = null; + newCamilaAvatarData = null; + if (settingAgentName) settingAgentName.value = window.agentConfig.agentName; + if (previewKemilyAvatar) previewKemilyAvatar.src = window.agentConfig.kemilyAvatarUrl; + if (previewCamilaAvatar) previewCamilaAvatar.src = window.agentConfig.camilaAvatarUrl; + settingsModal.style.display = 'flex'; + }); + } + + if (closeSettingsModal) { + closeSettingsModal.addEventListener('click', () => { + settingsModal.style.display = 'none'; + }); + } + + // Fechar clicando fora do modal-content + if (settingsModal) { + settingsModal.addEventListener('click', (e) => { + if (e.target === settingsModal) { + settingsModal.style.display = 'none'; + } + }); + } + + // Previews de Imagem + const handleAvatarFileSelect = (input, previewImg, callback) => { + const file = input.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + previewImg.src = e.target.result; + callback(e.target.result); + }; + reader.readAsDataURL(file); + } + }; + + if (uploadKemilyAvatar) { + uploadKemilyAvatar.addEventListener('change', () => { + handleAvatarFileSelect(uploadKemilyAvatar, previewKemilyAvatar, (base64) => { + newKemilyAvatarData = base64; + }); + }); + } + + if (uploadCamilaAvatar) { + uploadCamilaAvatar.addEventListener('change', () => { + handleAvatarFileSelect(uploadCamilaAvatar, previewCamilaAvatar, (base64) => { + newCamilaAvatarData = base64; + }); + }); + } + + // Salvar Configurações + if (btnSaveSettings) { + btnSaveSettings.addEventListener('click', async () => { + btnSaveSettings.disabled = true; + btnSaveSettings.innerText = 'Salvando...'; + + try { + let kemilyUrl = window.agentConfig.kemilyAvatarUrl; + let camilaUrl = window.agentConfig.camilaAvatarUrl; + + // Se houver novas imagens, faz upload + if (newKemilyAvatarData) { + const res = await fetch('/api/upload-avatar', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ avatarType: 'kemily', base64Data: newKemilyAvatarData }) + }); + if (res.ok) { + const data = await res.json(); + kemilyUrl = data.url; + } + } + + if (newCamilaAvatarData) { + const res = await fetch('/api/upload-avatar', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ avatarType: 'camila', base64Data: newCamilaAvatarData }) + }); + if (res.ok) { + const data = await res.json(); + camilaUrl = data.url; + } + } + + // Envia as configurações gerais + const resConfig = await fetch('/api/agent-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + agentName: settingAgentName.value, + kemilyAvatarUrl: kemilyUrl, + camilaAvatarUrl: camilaUrl + }) + }); + + if (resConfig.ok) { + const result = await resConfig.json(); + window.agentConfig = result.config; + updateAgentIdentityUI(); + settingsModal.style.display = 'none'; + } else { + alert('Erro ao salvar as configurações.'); + } + } catch (err) { + console.error(err); + alert('Ocorreu um erro ao salvar.'); + } finally { + btnSaveSettings.disabled = false; + btnSaveSettings.innerText = 'Salvar Configurações'; + } + }); + } + + // Executa o carregamento inicial das configurações + loadAgentConfig(); + // Variáveis de Estado let chats = JSON.parse(localStorage.getItem('camila_chats')) || []; // Migração: adicionar campos tags e pinned a chats antigos @@ -918,8 +1099,8 @@ const initApp = () => { row.className = `message-row ${role}`; const avatarHtml = isUser - ? `
U
` - : `
Kemily AI
`; + ? `
Camila
` + : `
${window.agentConfig?.agentName || 'Kemily'}
`; let contentHtml = ''; if (isUser) { @@ -1270,7 +1451,7 @@ const initApp = () => { botRow.innerHTML = `
-
Kemily AI
+
${window.agentConfig?.agentName || 'Kemily'}
diff --git a/public/index.html b/public/index.html index 6064090..fee311a 100644 --- a/public/index.html +++ b/public/index.html @@ -82,12 +82,17 @@ +
+ diff --git a/public/style.css b/public/style.css index d77d85d..878542e 100644 --- a/public/style.css +++ b/public/style.css @@ -2195,3 +2195,188 @@ code { background-color: rgba(16, 163, 127, 0.06); color: rgb(167, 243, 208); } + +/* Hover no botao de configuracoes da sidebar */ +.btn-settings:hover { + color: var(--text-primary) !important; + background-color: var(--bg-secondary) !important; +} + +/* Avatar do Usuario no Chat */ +.user-avatar-img-wrapper { + width: 32px; + height: 32px; + border-radius: 50%; + overflow: hidden; + border: 1.5px solid rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + background: none !important; +} + +.user-avatar-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Modal de Configurações */ +.settings-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + animation: fadeIn 0.2s ease-out; +} + +.settings-modal-content { + background-color: var(--bg-primary, #121212); + border: 1px solid var(--border-light, #262626); + border-radius: 16px; + width: 450px; + max-width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + overflow: hidden; + animation: scaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} + +.settings-modal-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-light, #262626); + display: flex; + justify-content: space-between; + align-items: center; +} + +.settings-modal-header h3 { + margin: 0; + font-size: 1.15rem; + color: var(--text-primary, #ffffff); + font-weight: 600; +} + +.close-settings-btn { + background: none; + border: none; + color: var(--text-secondary, #a3a3a3); + font-size: 1.5rem; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + line-height: 1; +} + +.close-settings-btn:hover { + color: var(--text-primary, #ffffff); + background-color: rgba(255, 255, 255, 0.05); +} + +.settings-modal-body { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.settings-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.settings-group label { + font-size: 0.85rem; + color: var(--text-secondary, #a3a3a3); + font-weight: 500; +} + +.settings-group input[type="text"] { + background-color: var(--bg-secondary, #1a1a1a); + border: 1px solid var(--border-light, #262626); + border-radius: 8px; + padding: 10px 12px; + color: var(--text-primary, #ffffff); + font-size: 0.95rem; + outline: none; + transition: border-color var(--transition-fast); +} + +.settings-group input[type="text"]:focus { + border-color: var(--brand-green, #0ea5e9); +} + +.avatar-preview-container { + display: flex; + align-items: center; + gap: 16px; + background-color: var(--bg-secondary, #1a1a1a); + border: 1px solid var(--border-light, #262626); + padding: 12px; + border-radius: 10px; +} + +.preview-avatar-img { + width: 50px; + height: 50px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--brand-green, #0ea5e9); +} + +.btn-change-avatar { + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-light, #262626); + color: var(--text-primary, #ffffff); + padding: 8px 14px; + border-radius: 6px; + font-size: 0.85rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.btn-change-avatar:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.settings-modal-footer { + padding: 16px 20px; + border-top: 1px solid var(--border-light, #262626); + display: flex; + justify-content: flex-end; +} + +.btn-save-settings { + background: linear-gradient(135deg, var(--brand-green) 0%, #0a5f49 100%); + border: none; + color: #ffffff; + padding: 10px 18px; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; + box-shadow: 0 0 12px var(--brand-glow); +} + +.btn-save-settings:hover { + opacity: 0.9; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scaleIn { + from { transform: scale(0.95); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} diff --git a/server.js b/server.js index e5da678..6e5621f 100644 --- a/server.js +++ b/server.js @@ -25,6 +25,92 @@ const requireAuth = (req, res, next) => { } }; +const fs = require('fs'); + +const CONFIG_FILE_PATH = path.join(__dirname, 'agent_config.json'); + +function readAgentConfig() { + try { + if (fs.existsSync(CONFIG_FILE_PATH)) { + return JSON.parse(fs.readFileSync(CONFIG_FILE_PATH, 'utf8')); + } + } catch (e) { + console.error('Erro ao ler agent_config.json:', e); + } + return { + agentName: "Kemily", + kemilyAvatarUrl: "assets/kemily.png", + camilaAvatarUrl: "assets/camila_prof.png" + }; +} + +function writeAgentConfig(config) { + try { + fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf8'); + // Copiar também para o BotVPS para sincronização com o Hermes + const botConfigDir = path.join(__dirname, '..', 'BotVPS', 'data'); + if (fs.existsSync(botConfigDir)) { + fs.writeFileSync(path.join(botConfigDir, 'agent_config.json'), JSON.stringify(config, null, 2), 'utf8'); + } + } catch (e) { + console.error('Erro ao salvar agent_config.json:', e); + } +} + +// Rota GET para obter configurações do agente +app.get('/api/agent-config', requireAuth, (req, res) => { + const config = readAgentConfig(); + res.json(config); +}); + +// Rota POST para atualizar configurações do agente +app.post('/api/agent-config', requireAuth, (req, res) => { + const { agentName, kemilyAvatarUrl, camilaAvatarUrl } = req.body; + if (!agentName) { + return res.status(400).json({ error: 'Nome do agente é obrigatório' }); + } + const config = { + agentName: agentName.trim(), + kemilyAvatarUrl: kemilyAvatarUrl || "assets/kemily.png", + camilaAvatarUrl: camilaAvatarUrl || "assets/camila_prof.png" + }; + writeAgentConfig(config); + res.json({ success: true, config }); +}); + +// Rota POST para upload de imagem em base64 +app.post('/api/upload-avatar', requireAuth, (req, res) => { + const { avatarType, base64Data } = req.body; + if (!avatarType || !base64Data) { + return res.status(400).json({ error: 'Dados inválidos' }); + } + + const matches = base64Data.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + if (!matches || matches.length !== 3) { + return res.status(400).json({ error: 'Formato de imagem inválido' }); + } + + const imageBuffer = Buffer.from(matches[2], 'base64'); + const fileExt = matches[1].split('/')[1] || 'png'; + const fileName = `custom_${avatarType}_avatar_${Date.now()}.${fileExt}`; + + // Garantir que a pasta public/assets existe + const assetsDir = path.join(__dirname, 'public', 'assets'); + if (!fs.existsSync(assetsDir)) { + fs.mkdirSync(assetsDir, { recursive: true }); + } + + const filePath = path.join(assetsDir, fileName); + + fs.writeFile(filePath, imageBuffer, (err) => { + if (err) { + console.error('Erro ao salvar avatar:', err); + return res.status(500).json({ error: 'Erro ao salvar arquivo' }); + } + return res.json({ success: true, url: `assets/${fileName}` }); + }); +}); + // Rota de login (página) app.get('/login', (req, res) => { const session = req.signedCookies.camila_session || req.cookies.camila_session; @@ -737,7 +823,15 @@ app.post('/api/chat', requireAuth, async (req, res) => { // ========================================================================== // FLUXO PADRÃO DE TEXTO (OpenRouter com SSE streaming original, com fallback Groq) // ========================================================================== - const enrichedMessages = [KEMILY_SYSTEM_PROMPT, ...messages]; + const agentConfig = readAgentConfig(); + const agentName = agentConfig.agentName || "Kemily"; + + const dynamicSystemPrompt = { + role: "system", + content: KEMILY_SYSTEM_PROMPT.content.replace(/Você é a Kemily/g, `Você é a ${agentName}`) + }; + + const enrichedMessages = [dynamicSystemPrompt, ...messages]; try { let chatResponse;