🔴 Alta priority: tema claro/escuro, modal tags, favicon K, badge modelo dinâmico
This commit is contained in:
@@ -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
@@ -96,6 +96,120 @@ const initApp = () => {
|
||||
let recognition = null;
|
||||
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
|
||||
// ==========================================================================
|
||||
@@ -696,24 +810,13 @@ const initApp = () => {
|
||||
const editTags = (id) => {
|
||||
const chat = chats.find(c => c.id === id);
|
||||
if (!chat) return;
|
||||
|
||||
const availableTags = ['relatorios', 'planejamento', 'mindlab', 'estudos'];
|
||||
const currentTags = chat.tags || [];
|
||||
|
||||
// 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));
|
||||
|
||||
openTagModal(id, currentTags, (newTags) => {
|
||||
chat.tags = newTags;
|
||||
chat.updatedAt = Date.now();
|
||||
saveChats();
|
||||
renderHistory();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectChat = (id) => {
|
||||
|
||||
+35
-2
@@ -11,7 +11,8 @@
|
||||
<!-- 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">
|
||||
<!-- 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>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
@@ -101,12 +102,25 @@
|
||||
</button>
|
||||
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</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">
|
||||
<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" />
|
||||
@@ -159,6 +173,25 @@
|
||||
<p class="disclaimer">Camila AI pode cometer erros. Considere verificar informações importantes.</p>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
+316
-1
@@ -2,6 +2,7 @@
|
||||
CSS GLOBAL & VARIÁVEIS (DESIGN SYSTEM PREMIUM)
|
||||
========================================================================== */
|
||||
:root {
|
||||
/* Dark theme (default) */
|
||||
--bg-primary: #0d0d0d;
|
||||
--bg-secondary: #171717;
|
||||
--bg-tertiary: #212121;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
--text-primary: #ececec;
|
||||
--text-secondary: #b4b4b4;
|
||||
--text-muted: #8e8e8e;
|
||||
--text-muted: #9e9e9e;
|
||||
|
||||
--brand-green: #10a37f;
|
||||
--brand-green-hover: #1a7f64;
|
||||
@@ -39,6 +40,30 @@
|
||||
--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;
|
||||
margin: 0;
|
||||
@@ -1583,3 +1608,293 @@ code {
|
||||
outline: none !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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { password } = req.body;
|
||||
|
||||
Reference in New Issue
Block a user