🔧 feat: Sistema de identidade dinâmica do agente - modal de configurações, avatares customizáveis, nome dinâmico sincronizado com Hermes/Telegram

This commit is contained in:
2026-05-26 19:40:28 +00:00
parent c78c4020cf
commit 6c35f7ed1b
5 changed files with 514 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"agentName": "Kemily",
"kemilyAvatarUrl": "assets/kemily.png",
"camilaAvatarUrl": "assets/camila_prof.png"
}
+184 -3
View File
@@ -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
? `<div class="avatar user-avatar">U</div>`
: `<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="assets/kemily.png" alt="Kemily AI" class="bot-avatar-img"></div>`;
? `<div class="avatar user-avatar user-avatar-img-wrapper"><img src="${window.agentConfig?.camilaAvatarUrl || 'assets/camila_prof.png'}" alt="Camila" class="user-avatar-img"></div>`
: `<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="${window.agentConfig?.kemilyAvatarUrl || 'assets/kemily.png'}" alt="${window.agentConfig?.agentName || 'Kemily'}" class="bot-avatar-img"></div>`;
let contentHtml = '';
if (isUser) {
@@ -1270,7 +1451,7 @@ const initApp = () => {
botRow.innerHTML = `
<div class="message-icon">
<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="assets/kemily.png" alt="Kemily AI" class="bot-avatar-img"></div>
<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="${window.agentConfig?.kemilyAvatarUrl || 'assets/kemily.png'}" alt="${window.agentConfig?.agentName || 'Kemily'}" class="bot-avatar-img"></div>
</div>
<div class="message-content-wrapper">
<div class="message-content">
+45 -3
View File
@@ -82,12 +82,17 @@
<div class="sidebar-footer">
<div class="user-profile">
<div class="avatar user-avatar sidebar-avatar-wrapper">
<img src="assets/camila_prof.png" alt="Camila Reifonas" class="sidebar-avatar-img">
<img src="assets/camila_prof.png" alt="Camila Reifonas" class="sidebar-avatar-img" id="sidebarAvatarImg">
</div>
<div class="user-info">
<span class="user-name">Camila Reifonas</span>
<span class="user-status">Online</span>
</div>
<button class="btn-settings" id="btnSettings" title="Configurações do Agente" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 8px; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: color var(--transition-fast), background-color var(--transition-fast); margin-right: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 20px; height: 20px;">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.43l-1.003.828c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.43l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 0 1 0-.255c.007-.378-.138-.75-.43-.991l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.645-.869L9.594 3.94ZM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
</svg>
</button>
<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" />
@@ -143,10 +148,10 @@
<div class="welcome-container" id="welcomeContainer">
<div class="welcome-logo">
<div class="logo-outer welcome-avatar-wrapper">
<img src="assets/kemily.png" alt="Kemily AI" class="welcome-avatar-img">
<img src="assets/kemily.png" alt="Kemily AI" class="welcome-avatar-img" id="welcomeAvatarImg">
</div>
</div>
<h2>Olá! Sou sua assistente virtual e me chamo Kemily. Como posso ajudar?</h2>
<h2 id="welcomeTitle">Olá! Sou sua assistente virtual e me chamo Kemily. Como posso ajudar?</h2>
<div class="welcome-grid">
<div class="suggestion-card" data-prompt="Escreva um relatório de observação semanal de um aluno de educação infantil focado em socialização e interação.">
@@ -242,6 +247,43 @@
</main>
</div>
<!-- Modal de Configurações do Agente -->
<div id="settingsModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content">
<div class="settings-modal-header">
<h3>Configurações da Assistente</h3>
<button id="closeSettingsModal" class="close-settings-btn">&times;</button>
</div>
<div class="settings-modal-body">
<div class="settings-group">
<label for="settingAgentName">Nome da Assistente</label>
<input type="text" id="settingAgentName" placeholder="Ex: Kemily">
</div>
<div class="settings-group">
<label>Avatar da Assistente</label>
<div class="avatar-preview-container">
<img id="previewKemilyAvatar" src="assets/kemily.png" alt="Kemily Preview" class="preview-avatar-img">
<input type="file" id="uploadKemilyAvatar" accept="image/*" style="display: none;">
<button type="button" class="btn-change-avatar" onclick="document.getElementById('uploadKemilyAvatar').click()">Escolher Imagem</button>
</div>
</div>
<div class="settings-group">
<label>Avatar da Camila (Usuária)</label>
<div class="avatar-preview-container">
<img id="previewCamilaAvatar" src="assets/camila_prof.png" alt="Camila Preview" class="preview-avatar-img">
<input type="file" id="uploadCamilaAvatar" accept="image/*" style="display: none;">
<button type="button" class="btn-change-avatar" onclick="document.getElementById('uploadCamilaAvatar').click()">Escolher Imagem</button>
</div>
</div>
</div>
<div class="settings-modal-footer">
<button id="btnSaveSettings" class="btn-save-settings">Salvar Configurações</button>
</div>
</div>
</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>
+185
View File
@@ -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; }
}
+95 -1
View File
@@ -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;