🔴 Alta priority: tema claro/escuro, modal tags, favicon K, badge modelo dinâmico

This commit is contained in:
2026-05-26 15:39:37 +00:00
parent 36ea7cce7a
commit f42bb49360
5 changed files with 629 additions and 16 deletions
+153
View File
@@ -0,0 +1,153 @@
# Sugestões de Melhoria — Camila AI (Kemily)
*Análise feita em 2026-05-26 — App: /root/Apps/Camila*
---
## 1. UX / UI
### 1.1 Tela de Boas-Vindas (Welcome)
- **Prompt cards genéricos.** As sugestões rápidas ("Novo chat", etc.) não refletem a persona Kemily.
- Substituir por cards relevantes à educação infantil: "Relatório semanal", "Planejamento Mind Lab", "Resumo de livro infantil", "Atividade BNCC".
- Cards com ícones temáticos e descrição curta.
- **Cursor de digitação simples.** O cursor `|` durante streaming é funcional mas visualmente pobre.
- Substituir por uma bolinha pulsante roxa (matching com o user avatar gradient) ou uma barra de loading no estilo "thinking...".
### 1.2 Sidebar / Menu Lateral
- **Edição de tags via `prompt()` nativo.** O uso de `prompt()` do navegador é abrupto e quebrado visualmente.
- Substituir por um modal/dropdown popover com checkboxes para selecionar tags. Mesmo visual do app.
- **Ordem do histórico.** O chat ativo não fica visível no topo quando você volta para ele.
- Ao selecionar um chat existente, movê-lo para o topo do grupo "Hoje".
- **Favicon genérico.** O favicon aponta para `chatgpt.com/favicon.ico`.
- Criar um favicon próprio (iniciais "K" ou "CA" com o verde brand).
### 1.3 Input de Mensagem
- **Contador de caracteres.** Principalmente no mobile, um contador visual ajuda a perceber o tamanho da mensagem.
- **Preview de anexo no mobile.** O preview de arquivos anexados aparece, mas no mobile pode ficar mal posicionado.
- Garantir que o preview fique visível e bem adaptado antes do textarea.
### 1.4 Header
- **Badge do modelo hardcoded.** O header mostra "GPT-4.1 Nano" de forma estática.
- Tornar dinâmico via variável de ambiente ou `/api/status` endpoint.
### 1.5 Acessibilidade
- **Contraste em textos.** Alguns textos em `text-muted` (#8e8e8e) sobre fundo escuro podem ter contraste insuficiente para leitura prolongada.
- Aumentar ligeiramente o peso (weight 500+) ou escurecer a cor para #9e9e9e ou #a0a0a0.
- **Focus visible.** Botões e inputs precisam de `focus-visible` melhorado para navegação por teclado.
---
## 2. Visuais
### 2.1 Avatar do Bot ( Kemily )
- **Confusão persona/foto real.** O avatar de IA usa a foto real da Camila. Quando o usuário互换 for "Camila AI", pode gerar confusão.
- Considerar um avatar ilustrado (estilo cartoon/feminino) ou um avatar geométrico abstrato com o verde brand. A/Kemia combina com um visual mais lúdico que combine com educação infantil.
### 2.2 Paleta e Tema
- **Apenas tema escuro.** O app só funciona em dark mode.
- Oferecer toggle de tema claro/escuro (light mode com whites/cinzas claros, mantendo o verde brand). Persistir escolha no localStorage.
### 2.3 Ilustrações e Decorativos
- **Falta identidade visual além das fotos.** A tela de login tem um glow verde, mas não há ilustrações, ícones decorativos ou gradientes que deem personalidade.
- Adicionar ilustrações SVG minimalistas na tela de boas-vindas (lápis, livro, jogos).
- Usar o gradiente do avatar em mais lugares (bordas, destaques).
### 2.4 Animações
- **Transições de entrada das mensagens.** A animação `fadeInUp` é boa, mas não há animação na saída (quando uma mensagem é removida na regeneração).
- Adicionar fadeOut na remoção de mensagens.
- **Animação de loading na sidebar.** Ao carregar ou buscar histórico, um skeleton ou shimmer melhoraria a percepção.
### 2.5 Tipografia
- **Fira Code para código.** O CSS referencia 'Fira Code' mas não está importado no `index.html`.
- Adicionar `<link>` para Fira Code no Google Fonts, ou usar 'JetBrains Mono' como alternativa.
---
## 3. Técnicas
### 3.1 Segurança
- **Autenticação fraca.** Apenas senha em texto no `.env` (sem hash), sem proteção contra brute force.
- Adicionar rate limiting no endpoint `/api/chat` e `/api/login`.
- Considerar usar JWT ou session-based auth com cookie seguro.
- **Senha exposta no frontend.** O `login.js` envia a senha diretamente. Mesmo em HTTPS, credenciais ficam expostas em logs de rede.
- Implementar Challenge-Response ou hash no client antes de enviar (ex: bcrypt client-side).
### 3.2 Armazenamento
- **localStorage sem limite.** Não há controle de espaço. Chats extensos podem atingir o limite de ~5MB do localStorage.
- Implementar quota checking antes de salvar. Alertar usuário ou auto-arquivar chats antigos para API/Backend.
- **Sem backup/exportação.** Histórico de conversas não pode ser exportado.
- Adicionar botão "Exportar conversas" (JSON ou Markdown).
### 3.3 TTS (Text-to-Speech)
- **Web Speech API limitada.** A seleção de voz é boa (busca por nome), mas não há controle de velocidade/profundidade e vozes pt-BR são inconsistentes entre navegadores.
- Oferecer um mini popup de configurações de voz (velocidade: 0.75x, 1x, 1.25x; pitch).
- Considerar integração com Edge TTS (Microsoft) via backend para vozes neurais consistentes em pt-BR.
### 3.4 Streaming e Performance
- **Re-renderização total do markdown.** A cada chunk do stream, `marked.parse()` é chamado na string inteira.
- Para respostas longas, implementar parsing incremental ou renderizar apenas o delta.
- **Sem debounce no input de busca.** A busca filtra a cada `input` event, sem debounce.
- Adicionar debounce de 300ms para searches mais pesados.
### 3.5 Backend e Infraestrutura
- **Arquivos do Agente no filesystem.** Os MDs em `/Agente/` são lidos via código no backend.
- Se o app escalar, considerar migrar para uma estrutura de API documentada (ex: FastAPI com `/api/v1/`).
- **Sem métricas/monitoring.** Não há logs de uso, latência, ou erros de API.
- Adicionar logging básico (quem acessou, tokens usados, latência de resposta).
### 3.6 Testes
- **Zero testes automatizados.**
- Configurar um framework (Vitest para frontend, Jest para backend). Cover: parsing de markdown, escapeHtml, lógica de filtro, renderização de componentes.
### 3.7 Mobile / PWA
- **Não é um PWA.** O app não tem manifest nem service worker.
- Adicionar `manifest.json` e SW para funcionar offline básico (tela de login e histórico).
- Adicionar meta tags de iOS (apple-mobile-web-app-capable).
### 3.8 SEO e Meta
- **Meta tags limitadas.** Title e description genéricos.
- Adicionar Open Graph tags, description customizada, e meta tags para compartilhamento.
---
## 4. Melhorias Estratégicas (Maior Impacto)
| Prioridade | Sugestão | Motivo |
|------------|----------|--------|
| 🔴 Alta | Tema claro/escuro | Acessibilidade e preferência pessoal |
| 🔴 Alta | Modal de tags no lugar de `prompt()` | UX consistente e profissional |
| 🔴 Alta | Toggle de tema claro | Contraste e acessibilidade |
| 🟡 Média | Avatar ilustrado para Kemily | Identidade visual + educação infantil |
| 🟡 Média | Cards de prompt temáticos | Relevância imediata ao abrir |
| 🟡 Média | Config TTS com velocidade | Experiência de voz personalizável |
| 🟡 Média | Exportar conversas | Não perder dados |
| 🟡 Média | Fira/JetBrains Mono importado | Código com monospace melhor |
| 🟢 Baixa | PWA / Service Worker | Offline e instalação |
| 🟢 Baixa | Métricas de uso | Dados para iterar |
---
## 5. Observações Positivas (Manter)
- ✅ Design system com variáveis CSS bem organizadas
- ✅ Sidebar com busca + filtros por tags + pinned chats — muito funcional
- ✅ Streaming SSE implementado corretamente com parsing incremental
- ✅ Drag-and-drop para anexos funcionando
- ✅ Reconhecimento de voz (Web Speech API) com seleção inteligente de voz pt-BR
- ✅ Ctrl+N e Ctrl+K atalhos de teclado úteis
- ✅ Mobile-first com sidebar em overlay
- ✅ Código bem comentado e organizado em seções
---
*Documento gerado por Hermes — 2026-05-26*
+116 -13
View File
@@ -96,6 +96,120 @@ const initApp = () => {
let recognition = null; let recognition = null;
let isListening = false; let isListening = false;
// ==========================================================================
// THEME TOGGLE — Claro/Escuro
// ==========================================================================
const btnThemeToggle = document.getElementById('btnThemeToggle');
const iconThemeSun = document.getElementById('iconThemeSun');
const iconThemeMoon = document.getElementById('iconThemeMoon');
const applyTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('camila_theme', theme);
if (theme === 'light') {
iconThemeSun.style.display = 'none';
iconThemeMoon.style.display = 'block';
} else {
iconThemeSun.style.display = 'block';
iconThemeMoon.style.display = 'none';
}
};
// Init theme from localStorage or system preference
const savedTheme = localStorage.getItem('camila_theme');
if (savedTheme) {
applyTheme(savedTheme);
} else {
// Default to dark
applyTheme('dark');
}
btnThemeToggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// ==========================================================================
// BADGE DE MODELO DINÂMICO
// ==========================================================================
const modelBadgeText = document.getElementById('modelBadgeText');
const updateModelBadge = async () => {
try {
const res = await fetch('/api/status');
if (res.ok) {
const data = await res.json();
// Extrai nome abreviado do modelo (ex: "openai/gpt-4.1-nano" → "GPT-4.1 Nano")
const modelName = data.model.split('/').pop();
const formatted = modelName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
modelBadgeText.textContent = formatted;
}
} catch (e) {
// Mantém texto padrão se falhar
}
};
updateModelBadge();
// ==========================================================================
// MODAL DE TAGS
// ==========================================================================
const AVAILABLE_TAGS = ['HTPC', 'ATA', 'Planejamento', 'Formação', 'Projeto', 'Relatório', 'Sugestão', 'Dúvida'];
const modalTagOverlay = document.getElementById('modalTagOverlay');
const modalTagList = document.getElementById('modalTagList');
const modalTagClose = document.getElementById('modalTagClose');
const modalTagCancel = document.getElementById('modalTagCancel');
const modalTagConfirm = document.getElementById('modalTagConfirm');
let pendingTagAction = null; // { chatId, callback }
let selectedTagsState = [];
const openTagModal = (chatId, currentTags, callback) => {
pendingTagAction = { chatId, callback };
selectedTagsState = [...currentTags];
renderTagModalList();
modalTagOverlay.classList.add('active');
};
const closeTagModal = () => {
modalTagOverlay.classList.remove('active');
pendingTagAction = null;
};
const renderTagModalList = () => {
modalTagList.innerHTML = AVAILABLE_TAGS.map(tag => `
<div class="modal-tag-item ${selectedTagsState.includes(tag) ? 'selected' : ''}" data-tag="${tag}">
<input type="checkbox" ${selectedTagsState.includes(tag) ? 'checked' : ''}>
<span>${tag}</span>
</div>
`).join('');
modalTagList.querySelectorAll('.modal-tag-item').forEach(item => {
item.addEventListener('click', () => {
const tag = item.dataset.tag;
if (selectedTagsState.includes(tag)) {
selectedTagsState = selectedTagsState.filter(t => t !== tag);
} else {
selectedTagsState.push(tag);
}
renderTagModalList();
});
});
};
modalTagClose.addEventListener('click', closeTagModal);
modalTagCancel.addEventListener('click', closeTagModal);
modalTagOverlay.addEventListener('click', (e) => {
if (e.target === modalTagOverlay) closeTagModal();
});
modalTagConfirm.addEventListener('click', () => {
if (pendingTagAction) {
pendingTagAction.callback(selectedTagsState);
}
closeTagModal();
});
// Expose globally for inline onclick handlers
window.openTagModal = openTagModal;
// ========================================================================== // ==========================================================================
// CONTROLES DE INTERFACE & SIDEBAR // CONTROLES DE INTERFACE & SIDEBAR
// ========================================================================== // ==========================================================================
@@ -696,24 +810,13 @@ const initApp = () => {
const editTags = (id) => { const editTags = (id) => {
const chat = chats.find(c => c.id === id); const chat = chats.find(c => c.id === id);
if (!chat) return; if (!chat) return;
const availableTags = ['relatorios', 'planejamento', 'mindlab', 'estudos'];
const currentTags = chat.tags || []; const currentTags = chat.tags || [];
openTagModal(id, currentTags, (newTags) => {
// Prompt amigável mostrando opções
const promptText = `Atribua tags à conversa.\nOpções válidas: relatorios, planejamento, mindlab, estudos\n(Use vírgula para separar várias tags, ou deixe em branco para limpar):\n`;
const tagsStr = prompt(promptText, currentTags.join(', '));
if (tagsStr !== null) {
const newTags = tagsStr.split(',')
.map(t => t.trim().toLowerCase())
.filter(t => availableTags.includes(t));
chat.tags = newTags; chat.tags = newTags;
chat.updatedAt = Date.now(); chat.updatedAt = Date.now();
saveChats(); saveChats();
renderHistory(); renderHistory();
} });
}; };
const selectChat = (id) => { const selectChat = (id) => {
+35 -2
View File
@@ -11,7 +11,8 @@
<!-- Highlight.js para destaque de sintaxe em códigos --> <!-- 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="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="shortcut icon" href="https://chatgpt.com/favicon.ico" type="image/x-icon"> <!-- Favicon customizado K (Kemily/Camila) -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%2310a37f'/><text x='16' y='22' font-family='Arial' font-size='18' font-weight='bold' fill='white' text-anchor='middle'>K</text></svg>" type="image/svg+xml">
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
@@ -101,12 +102,25 @@
</button> </button>
<div class="model-badge"> <div class="model-badge">
<span>GPT-4.1 Nano</span> <span id="modelBadgeText">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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg> </svg>
</div> </div>
<div class="header-right">
<button class="btn-theme-toggle" id="btnThemeToggle" title="Alternar tema" aria-label="Alternar entre tema claro e escuro">
<!-- Sun icon (shown in dark mode, to switch to light) -->
<svg id="iconThemeSun" 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 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M12 6.75a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5Z" />
</svg>
<!-- Moon icon (shown in light mode, to switch to dark) -->
<svg id="iconThemeMoon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="display:none">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
</button>
</div>
<button class="btn-new-chat-mobile" id="btnNewChatMobile" aria-label="Novo chat"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@@ -159,6 +173,25 @@
<p class="disclaimer">Camila AI pode cometer erros. Considere verificar informações importantes.</p> <p class="disclaimer">Camila AI pode cometer erros. Considere verificar informações importantes.</p>
</div> </div>
</footer> </footer>
<!-- Modal de Tags -->
<div class="modal-overlay" id="modalTagOverlay">
<div class="modal-tag">
<div class="modal-tag-header">
<span class="modal-tag-title">Selecionar tags</span>
<button class="modal-tag-close" id="modalTagClose" aria-label="Fechar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="modal-tag-list" id="modalTagList"></div>
<div class="modal-tag-actions">
<button class="modal-tag-btn secondary" id="modalTagCancel">Cancelar</button>
<button class="modal-tag-btn primary" id="modalTagConfirm">Confirmar</button>
</div>
</div>
</div>
</main> </main>
</div> </div>
+316 -1
View File
@@ -2,6 +2,7 @@
CSS GLOBAL & VARIÁVEIS (DESIGN SYSTEM PREMIUM) CSS GLOBAL & VARIÁVEIS (DESIGN SYSTEM PREMIUM)
========================================================================== */ ========================================================================== */
:root { :root {
/* Dark theme (default) */
--bg-primary: #0d0d0d; --bg-primary: #0d0d0d;
--bg-secondary: #171717; --bg-secondary: #171717;
--bg-tertiary: #212121; --bg-tertiary: #212121;
@@ -9,7 +10,7 @@
--text-primary: #ececec; --text-primary: #ececec;
--text-secondary: #b4b4b4; --text-secondary: #b4b4b4;
--text-muted: #8e8e8e; --text-muted: #9e9e9e;
--brand-green: #10a37f; --brand-green: #10a37f;
--brand-green-hover: #1a7f64; --brand-green-hover: #1a7f64;
@@ -39,6 +40,30 @@
--transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1); --transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
} }
/* LIGHT THEME */
[data-theme="light"] {
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e8e8e8;
--bg-accent: #dcdcdc;
--text-primary: #1a1a1a;
--text-secondary: #4a4a4a;
--text-muted: #7a7a7a;
--brand-green: #10a37f;
--brand-green-hover: #0d8f6f;
--brand-glow: rgba(16, 163, 127, 0.1);
--border-light: rgba(0, 0, 0, 0.08);
--border-focus: rgba(16, 163, 127, 0.4);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.15), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@@ -1583,3 +1608,293 @@ code {
outline: none !important; outline: none !important;
box-shadow: 0 0 5px var(--brand-glow) !important; box-shadow: 0 0 5px var(--brand-glow) !important;
} }
/* ==========================================================================
THEME TOGGLE BUTTON
========================================================================== */
.btn-theme-toggle {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 8px;
color: var(--text-secondary);
transition: background var(--transition-fast), color var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
}
.btn-theme-toggle:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-theme-toggle svg {
width: 20px;
height: 20px;
}
/* Theme toggle in header - right side */
.header-right {
display: flex;
align-items: center;
gap: 4px;
}
/* ==========================================================================
MODAL DE TAGS (substitui prompt())
========================================================================== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-normal), visibility var(--transition-normal);
}
.modal-overlay.active {
opacity: 1;
visibility: visible;
}
.modal-tag {
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 16px;
padding: 24px;
min-width: 320px;
max-width: 90vw;
box-shadow: var(--shadow-xl);
transform: scale(0.9) translateY(20px);
transition: transform var(--transition-normal);
}
.modal-overlay.active .modal-tag {
transform: scale(1) translateY(0);
}
.modal-tag-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.modal-tag-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.modal-tag-close {
background: none;
border: none;
cursor: pointer;
padding: 6px;
border-radius: 6px;
color: var(--text-muted);
transition: background var(--transition-fast), color var(--transition-fast);
}
.modal-tag-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.modal-tag-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 300px;
overflow-y: auto;
}
.modal-tag-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 10px;
cursor: pointer;
transition: background var(--transition-fast);
border: 1px solid transparent;
}
.modal-tag-item:hover {
background: var(--bg-tertiary);
}
.modal-tag-item.selected {
background: var(--brand-glow);
border-color: var(--brand-green);
}
.modal-tag-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--brand-green);
cursor: pointer;
}
.modal-tag-item span {
font-size: 14px;
color: var(--text-primary);
}
.modal-tag-item.selected span {
color: var(--brand-green);
font-weight: 500;
}
.modal-tag-actions {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: flex-end;
}
.modal-tag-btn {
padding: 10px 20px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background var(--transition-fast), transform var(--transition-fast);
border: none;
}
.modal-tag-btn:active {
transform: scale(0.97);
}
.modal-tag-btn.primary {
background: var(--brand-green);
color: white;
}
.modal-tag-btn.primary:hover {
background: var(--brand-green-hover);
}
.modal-tag-btn.secondary {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.modal-tag-btn.secondary:hover {
background: var(--bg-accent);
color: var(--text-primary);
}
/* ==========================================================================
TAG INPUT DISPLAY
========================================================================== */
.tags-display {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding: 10px;
background: var(--bg-tertiary);
border-radius: 10px;
min-height: 44px;
align-items: center;
}
.tag-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
background: var(--brand-glow);
color: var(--brand-green);
border: 1px solid rgba(16, 163, 127, 0.3);
}
.tag-badge button {
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
color: var(--brand-green);
opacity: 0.7;
transition: opacity var(--transition-fast);
}
.tag-badge button:hover {
opacity: 1;
}
.no-tags {
color: var(--text-muted);
font-size: 12px;
font-style: italic;
}
/* ==========================================================================
WELCOME CARDS (tematicos)
========================================================================== */
.welcome-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
max-width: 600px;
margin-top: 20px;
}
@media (max-width: 500px) {
.welcome-grid {
grid-template-columns: 1fr;
}
}
.suggestion-card {
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 14px;
padding: 16px;
cursor: pointer;
text-align: left;
transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
display: flex;
flex-direction: column;
gap: 8px;
}
.suggestion-card:hover {
background: var(--bg-tertiary);
border-color: var(--brand-green);
transform: translateY(-2px);
}
.suggestion-card .card-icon {
font-size: 24px;
line-height: 1;
}
.suggestion-card .card-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.suggestion-card .card-desc {
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
}
+9
View File
@@ -34,6 +34,15 @@ app.get('/login', (req, res) => {
} }
}); });
// API de status do modelo
app.get('/api/status', requireAuth, (req, res) => {
res.json({
model: process.env.OPENAI_MODEL || 'openai/gpt-4.1-nano',
provider: 'openrouter',
timestamp: new Date().toISOString()
});
});
// API para Login // API para Login
app.post('/api/login', (req, res) => { app.post('/api/login', (req, res) => {
const { password } = req.body; const { password } = req.body;