diff --git a/public/app.js b/public/app.js index 39599dd..c141528 100644 --- a/public/app.js +++ b/public/app.js @@ -74,11 +74,21 @@ document.addEventListener('DOMContentLoaded', () => { const historyItems = document.getElementById('historyItems'); const btnLogout = document.getElementById('btnLogout'); const suggestionCards = document.querySelectorAll('.suggestion-card'); + const searchInput = document.getElementById('searchInput'); + const searchClear = document.getElementById('searchClear'); + const searchResultsHeader = document.getElementById('searchResultsHeader'); + const searchResultsCount = document.getElementById('searchResultsCount'); + const searchClearAll = document.getElementById('searchClearAll'); + const sidebarTags = document.getElementById('sidebarTags'); // Variáveis de Estado let chats = JSON.parse(localStorage.getItem('camila_chats')) || []; + // Migração: adicionar campos tags e pinned a chats antigos + chats = chats.map(c => ({ ...c, tags: c.tags || [], pinned: c.pinned || false })); let currentChatId = localStorage.getItem('camila_current_chat_id') || null; let isGenerating = false; + let searchQuery = ''; + let activeTag = 'all'; // ========================================================================== // CONTROLES DE INTERFACE & SIDEBAR @@ -115,8 +125,39 @@ document.addEventListener('DOMContentLoaded', () => { } }); + // Busca no histórico + searchInput.addEventListener('input', () => { + searchQuery = searchInput.value; + searchClear.style.display = searchQuery ? 'flex' : 'none'; + renderHistory(); + }); + + searchClear.addEventListener('click', () => { + searchInput.value = ''; + searchQuery = ''; + searchClear.style.display = 'none'; + renderHistory(); + }); + + searchClearAll.addEventListener('click', () => { + searchInput.value = ''; + searchQuery = ''; + searchClear.style.display = 'none'; + renderHistory(); + }); + + // Filtro por tags + sidebarTags.addEventListener('click', (e) => { + const tagBtn = e.target.closest('.tag-filter'); + if (!tagBtn) return; + document.querySelectorAll('.tag-filter').forEach(b => b.classList.remove('active')); + tagBtn.classList.add('active'); + activeTag = tagBtn.dataset.tag; + renderHistory(); + }); + // ========================================================================== - // HISTÓRICO DE CHATS (LOCALSTORAGE) + // HISTÓRICO DE CHATS (LOCALSTORAGE) — VERSÃO COMPLETA COM PINNED, TAGS, BUSCA // ========================================================================== const saveChats = () => { @@ -128,46 +169,189 @@ document.addEventListener('DOMContentLoaded', () => { } }; + // Agrupamento por data + const getDateGroup = (timestamp) => { + const now = new Date(); + const date = new Date(timestamp); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + const weekAgo = new Date(today); + weekAgo.setDate(weekAgo.getDate() - 7); + const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); + + const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + + if (dateOnly.getTime() === today.getTime()) return 'Hoje'; + if (dateOnly.getTime() === yesterday.getTime()) return 'Ontem'; + if (dateOnly >= weekAgo) return 'Esta Semana'; + if (dateOnly >= monthStart) return 'Este Mês'; + return 'Mais Antigas'; + }; + + // Filtrar chats por tag + const filterByTag = (chatList) => { + if (activeTag === 'all') return chatList; + return chatList.filter(chat => chat.tags && chat.tags.includes(activeTag)); + }; + + // Filtrar chats por busca + const filterBySearch = (chatList) => { + if (!searchQuery.trim()) return chatList; + const q = searchQuery.toLowerCase(); + return chatList.filter(chat => { + if (chat.title.toLowerCase().includes(q)) return true; + return chat.messages.some(msg => msg.content.toLowerCase().includes(q)); + }); + }; + + // Renderizar um item de chat + const createHistoryItem = (chat) => { + const item = document.createElement('div'); + item.className = `history-item ${chat.id === currentChatId ? 'active' : ''} ${chat.pinned ? 'pinned' : ''}`; + item.dataset.id = chat.id; + + const pinIcon = chat.pinned + ? `` + : ''; + + const tagBadges = (chat.tags || []).map(tag => + `${getTagEmoji(tag)}` + ).join(''); + + item.innerHTML = ` +
+ ${pinIcon} +
${escapeHtml(chat.title)}
+ ${tagBadges ? `
${tagBadges}
` : ''} +
+
+ + + +
+ `; + + return item; + }; + + // Emoji para cada tag + const getTagEmoji = (tag) => { + const emojis = { relatorios: '📋', planejamento: '📝', mindlab: '🧠', estudos: '📚' }; + return emojis[tag] || '🏷️'; + }; + + // Renderizar histórico completo const renderHistory = () => { historyItems.innerHTML = ''; - + if (chats.length === 0) { historyItems.innerHTML = '
Nenhum chat salvo
'; return; } - // Ordenar por timestamp mais recente - const sortedChats = [...chats].sort((a, b) => b.updatedAt - a.updatedAt); + // Aplicar filtros + let filtered = [...chats]; + filtered = filterByTag(filtered); + filtered = filterBySearch(filtered); - sortedChats.forEach(chat => { - const item = document.createElement('div'); - item.className = `history-item ${chat.id === currentChatId ? 'active' : ''}`; - item.dataset.id = chat.id; - - item.innerHTML = ` -
${escapeHtml(chat.title)}
-
- -
- `; - - // Selecionar chat - item.addEventListener('click', (e) => { - if (e.target.closest('.btn-delete-chat')) { - e.stopPropagation(); - deleteChat(chat.id); - return; - } - selectChat(chat.id); - toggleSidebar(false); + // Se tem busca, mostrar resultados direto (sem grupo) + if (searchQuery.trim()) { + searchResultsHeader.style.display = 'block'; + searchResultsCount.textContent = `${filtered.length} resultado${filtered.length !== 1 ? 's' : ''}`; + filtered.sort((a, b) => b.updatedAt - a.updatedAt); + filtered.forEach(chat => { + const item = createHistoryItem(chat); + item.addEventListener('click', (e) => { + if (e.target.closest('.btn-delete-chat')) { e.stopPropagation(); deleteChat(chat.id); return; } + if (e.target.closest('.btn-pin-chat')) { e.stopPropagation(); togglePin(chat.id); return; } + if (e.target.closest('.btn-rename-chat')) { e.stopPropagation(); renameChat(chat.id); return; } + selectChat(chat.id); + toggleSidebar(false); + }); + historyItems.appendChild(item); }); + return; + } - historyItems.appendChild(item); + searchResultsHeader.style.display = 'none'; + + // Separar pinned e não-pinned + const pinned = filtered.filter(c => c.pinned).sort((a, b) => b.updatedAt - a.updatedAt); + const unpinned = filtered.filter(c => !c.pinned).sort((a, b) => b.updatedAt - a.updatedAt); + + // Renderizar grupo se tiver items + const renderGroup = (label, items) => { + if (items.length === 0) return; + const group = document.createElement('div'); + group.className = 'history-group'; + group.innerHTML = `

${label}

`; + const container = document.createElement('div'); + container.className = 'history-items'; + items.forEach(chat => { + const item = createHistoryItem(chat); + item.addEventListener('click', (e) => { + if (e.target.closest('.btn-delete-chat')) { e.stopPropagation(); deleteChat(chat.id); return; } + if (e.target.closest('.btn-pin-chat')) { e.stopPropagation(); togglePin(chat.id); return; } + if (e.target.closest('.btn-rename-chat')) { e.stopPropagation(); renameChat(chat.id); return; } + selectChat(chat.id); + toggleSidebar(false); + }); + container.appendChild(item); + }); + group.appendChild(container); + historyItems.appendChild(group); + }; + + // Pinned no topo + if (pinned.length > 0) { + renderGroup('📌 Fixadas', pinned); + } + + // Agrupar por data os não-pinned + const groups = {}; + unpinned.forEach(chat => { + const group = getDateGroup(chat.updatedAt); + if (!groups[group]) groups[group] = []; + groups[group].push(chat); }); + + ['Hoje', 'Ontem', 'Esta Semana', 'Este Mês', 'Mais Antigas'].forEach(g => { + if (groups[g]) renderGroup(g, groups[g]); + }); + }; + + // Fixar/desfixar chat + const togglePin = (id) => { + const chat = chats.find(c => c.id === id); + if (!chat) return; + chat.pinned = !chat.pinned; + saveChats(); + renderHistory(); + }; + + // Renomear chat + const renameChat = (id) => { + const chat = chats.find(c => c.id === id); + if (!chat) return; + const newTitle = prompt('Novo nome da conversa:', chat.title); + if (newTitle && newTitle.trim()) { + chat.title = newTitle.trim(); + saveChats(); + renderHistory(); + } }; const selectChat = (id) => { @@ -228,7 +412,7 @@ document.addEventListener('DOMContentLoaded', () => { const row = document.createElement('div'); row.className = `message-row ${role}`; - const avatarHtml = isUser + const avatarHtml = isUser ? `
U
` : `
Camila AI
`; @@ -239,18 +423,46 @@ document.addEventListener('DOMContentLoaded', () => { contentHtml = marked.parse(content); } + const actionsHtml = !isUser ? ` +
+ +
+ ` : ''; + row.innerHTML = `
${avatarHtml}
-
${contentHtml}
+
+
${contentHtml}
+ ${actionsHtml} +
`; conversationList.appendChild(row); - + // Colorir blocos de código com Highlight.js row.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); + // Event listener para copiar mensagem + const copyBtn = row.querySelector('.btn-copy-msg'); + if (copyBtn) { + copyBtn.addEventListener('click', () => { + const text = copyBtn.dataset.content; + navigator.clipboard.writeText(text).then(() => { + const span = copyBtn.querySelector('span'); + const original = span.textContent; + span.textContent = 'Copiado!'; + setTimeout(() => span.textContent = original, 2000); + }); + }); + } + return row; }; @@ -282,6 +494,8 @@ document.addEventListener('DOMContentLoaded', () => { chats.push({ id: newId, title: title, + tags: [], + pinned: false, messages: [], createdAt: Date.now(), updatedAt: Date.now() diff --git a/public/index.html b/public/index.html index 7bae7c3..c902b9c 100644 --- a/public/index.html +++ b/public/index.html @@ -31,15 +31,41 @@ + + + + + +
- -
-

Hoje

-
- + + + +
diff --git a/public/style.css b/public/style.css index 578fc8e..a1c7216 100644 --- a/public/style.css +++ b/public/style.css @@ -391,6 +391,138 @@ body { background-color: rgba(255, 255, 255, 0.15); } +/* Busca no Histórico */ +.sidebar-search { + padding: 12px 16px 0; +} + +.sidebar-search-wrapper { + display: flex; + align-items: center; + gap: 8px; + background-color: var(--bg-tertiary); + border: 1px solid var(--border-light); + border-radius: 10px; + padding: 8px 12px; + transition: border-color var(--transition-fast); +} + +.sidebar-search-wrapper:focus-within { + border-color: var(--border-focus); +} + +.search-icon { + width: 16px; + height: 16px; + color: var(--text-muted); + flex-shrink: 0; +} + +.search-input { + flex: 1; + background: none; + border: none; + outline: none; + color: var(--text-primary); + font-size: 13px; + font-family: var(--font-sans); + min-width: 0; +} + +.search-input::placeholder { + color: var(--text-muted); +} + +.search-clear { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 2px; + display: flex; + align-items: center; + border-radius: 4px; + flex-shrink: 0; + transition: color var(--transition-fast), background-color var(--transition-fast); +} + +.search-clear:hover { + color: var(--text-primary); + background-color: rgba(255,255,255,0.05); +} + +.search-clear svg { + width: 14px; + height: 14px; +} + +/* Tags de filtro */ +.sidebar-tags { + display: flex; + gap: 6px; + padding: 10px 16px 0; + overflow-x: auto; + scrollbar-width: none; +} + +.sidebar-tags::-webkit-scrollbar { + display: none; +} + +.tag-filter { + background-color: var(--bg-tertiary); + border: 1px solid var(--border-light); + border-radius: 20px; + color: var(--text-secondary); + font-size: 11.5px; + font-weight: 500; + padding: 5px 10px; + cursor: pointer; + white-space: nowrap; + transition: all var(--transition-fast); + font-family: var(--font-sans); +} + +.tag-filter:hover { + background-color: var(--bg-accent); + color: var(--text-primary); +} + +.tag-filter.active { + background-color: var(--brand-green); + border-color: var(--brand-green); + color: white; +} + +/* Busca results header */ +.search-results-header { + padding-bottom: 8px; +} + +.search-results-info { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: var(--text-muted); +} + +.search-clear-all { + background: none; + border: none; + color: var(--brand-green); + cursor: pointer; + font-size: 12px; + font-family: var(--font-sans); + padding: 2px 6px; + border-radius: 4px; + transition: background-color var(--transition-fast); +} + +.search-clear-all:hover { + background-color: var(--brand-glow); +} + .history-group h3 { font-size: 11px; font-weight: 600; @@ -416,7 +548,7 @@ body { cursor: pointer; color: var(--text-primary); font-size: 13.5px; - gap: 10px; + gap: 8px; text-decoration: none; position: relative; transition: background-color var(--transition-fast); @@ -432,11 +564,42 @@ body { font-weight: 500; } +.history-item.pinned { + border-left: 2px solid var(--brand-green); + padding-left: 8px; +} + +.history-item-content { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + flex: 1; +} + .history-item-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; + min-width: 0; +} + +.pin-icon { + width: 12px; + height: 12px; + color: var(--brand-green); + flex-shrink: 0; +} + +.history-item-tags { + display: flex; + gap: 2px; + flex-shrink: 0; +} + +.history-tag-badge { + font-size: 10px; } .history-item-actions { @@ -794,6 +957,53 @@ body { line-height: 1.6; } +.message-content-wrapper { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +/* Ações de mensagem (copiar) */ +.message-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity var(--transition-fast); + margin-top: 4px; +} + +.message-row:hover .message-actions { + opacity: 1; +} + +.msg-action-btn { + background: none; + border: 1px solid var(--border-light); + border-radius: 6px; + color: var(--text-muted); + cursor: pointer; + padding: 4px 8px; + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-family: var(--font-sans); + transition: all var(--transition-fast); +} + +.msg-action-btn:hover { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border-color: rgba(255,255,255,0.15); +} + +.msg-action-btn svg { + width: 14px; + height: 14px; +} + .message-content p { margin-bottom: 12px; }