diff --git a/SUGESTOES_MELHORIA.md b/SUGESTOES_MELHORIA.md new file mode 100644 index 0000000..6d12120 --- /dev/null +++ b/SUGESTOES_MELHORIA.md @@ -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 `` 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* \ No newline at end of file diff --git a/public/app.js b/public/app.js index 3f23270..2f0612e 100644 --- a/public/app.js +++ b/public/app.js @@ -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 => ` + + `).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) => { diff --git a/public/index.html b/public/index.html index 68b9334..1f46447 100644 --- a/public/index.html +++ b/public/index.html @@ -11,7 +11,8 @@ - + +
@@ -101,12 +102,25 @@
- GPT-4.1 Nano + GPT-4.1 Nano
+
+ +
+
+ + + diff --git a/public/style.css b/public/style.css index a03e5ca..2d73984 100644 --- a/public/style.css +++ b/public/style.css @@ -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; +} diff --git a/server.js b/server.js index e633c02..65630ee 100644 --- a/server.js +++ b/server.js @@ -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;