Spaces:
Running
Running
| // State Management | |
| let isTyping = false; | |
| let messageHistory = []; | |
| let recognition = null; | |
| // Initialize Speech Recognition | |
| if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| recognition = new SpeechRecognition(); | |
| recognition.lang = 'pt-BR'; | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| } | |
| // DOM Elements | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const messageForm = document.getElementById('messageForm'); | |
| const messageInput = document.getElementById('messageInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const voiceModal = document.getElementById('voiceModal'); | |
| const voiceText = document.getElementById('voiceText'); | |
| // Knowledge Base | |
| const knowledgeBase = { | |
| greetings: { | |
| patterns: ['oi', 'olá', 'ola', 'hey', 'e aí', 'e ai', 'bom dia', 'boa tarde', 'boa noite', 'salve'], | |
| responses: [ | |
| 'Oi! Como posso ajudar você hoje? 😊', | |
| 'Olá! Que bom ver você por aqui! ✨', | |
| 'Hey! Pronto para ajudar! 🚀', | |
| 'Bem-vindo! O que posso fazer por você?' | |
| ] | |
| }, | |
| time: { | |
| patterns: ['hora', 'horas', 'que horas', 'horário', 'horario'], | |
| responses: () => { | |
| const now = new Date(); | |
| const timeString = now.toLocaleTimeString('pt-BR', { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| return `🕐 Agora são ${timeString}`; | |
| } | |
| }, | |
| date: { | |
| patterns: ['data', 'data de hoje', 'que dia é hoje', 'dia de hoje'], | |
| responses: () => { | |
| const now = new Date(); | |
| const dateString = now.toLocaleDateString('pt-BR', { | |
| weekday: 'long', | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| return `📅 Hoje é ${dateString}`; | |
| } | |
| }, | |
| weather: { | |
| patterns: ['clima', 'tempo', 'previsão', 'previsao', 'temperatura', 'está frio', 'está calor', 'ta frio', 'ta calor'], | |
| responses: [ | |
| '🌤️ Não tenho acesso a dados meteorológicos em tempo real, mas posso sugerir que você verifique um app de clima confiável!', | |
| '☀️ Infelizmente não consigo verificar o clima agora. Recomendo olhar o Weather.com ou o app do seu celular.' | |
| ] | |
| }, | |
| jokes: { | |
| patterns: ['piada', 'conta uma piada', 'me faça rir', 'me faz rir', 'algo engraçado'], | |
| responses: [ | |
| '😄 Por que o computador foi ao médico? Porque estava com vírus! 🦠', | |
| '😂 O que o pato disse para a pata? Vem quá! 🦆', | |
| '🤣 Qual é o animal mais antigo? A zebra, porque está em preto e branco! 🦓', | |
| '😅 Por que o livro de matemática se suicidou? Porque tinha muitos problemas! 📚', | |
| '🤪 O que é um pontinho amarelo no alto de um prédio? Uma ervilha com vertigem! 🟡' | |
| ] | |
| }, | |
| capital: { | |
| patterns: ['capital', 'capital da', 'qual a capital'], | |
| responses: { | |
| 'frança': '🇫🇷 A capital da França é Paris, também conhecida como Cidade Luz!', | |
| 'brasil': '🇧🇷 A capital do Brasil é Brasília, inaugurada em 1960!', | |
| 'japão': '🇯🇵 A capital do Japão é Tóquio, a maior metrópole do mundo!', | |
| 'estados unidos': '🇺🇸 A capital dos Estados Unidos é Washington, D.C.!', | |
| 'argentina': '🇦🇷 A capital da Argentina é Buenos Aires!', | |
| 'portugal': '🇵🇹 A capital de Portugal é Lisboa!', | |
| 'espanha': '🇪🇸 A capital da Espanha é Madri!', | |
| 'itália': '🇮🇹 A capital da Itália é Roma!', | |
| 'alemanha': '🇩🇪 A capital da Alemanha é Berlim!', | |
| 'canadá': '🇨🇦 A capital do Canadá é Ottawa!' | |
| } | |
| }, | |
| thanks: { | |
| patterns: ['obrigado', 'obrigada', 'valeu', 'agradeço', 'thanks', 'ty'], | |
| responses: [ | |
| 'Por nada! 😊 Estou aqui para ajudar!', | |
| 'De nada! Fico feliz em poder ajudar! ✨', | |
| 'Não há de quê! Precisar de mais alguma coisa? 🙌' | |
| ] | |
| }, | |
| calculator: { | |
| patterns: ['calcular', 'quanto é', 'resultado de', '+', '-', '*', '/', 'x'], | |
| responses: (message) => { | |
| try { | |
| // Remove non-math characters and evaluate | |
| const cleanExpression = message.replace(/[^0-9+\-*/().\s]/g, '').replace(/x/g, '*'); | |
| if (!cleanExpression) return null; | |
| const result = Function('"use strict"; return (' + cleanExpression + ')')(); | |
| return `🧮 O resultado de ${cleanExpression.replace('*', '×')} é ${result}`; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| }, | |
| name: { | |
| patterns: ['seu nome', 'como se chama', 'quem é você', 'quem e voce', 'qual seu nome'], | |
| responses: [ | |
| 'Eu sou a Nova, sua assistente virtual! 🤖✨', | |
| 'Me chamo Nova! Fui criada para ajudar você com o que precisar!', | |
| 'Olá! Eu sou Nova, prazer em conhecê-lo! 💜' | |
| ] | |
| }, | |
| help: { | |
| patterns: ['ajuda', 'help', 'o que você faz', 'o que voce faz', 'capacidades'], | |
| responses: [ | |
| '🛠️ **O que posso fazer:**\n\n• 🕐 Informar data e hora\n• 🌍 Responder sobre capitais\n• 😄 Contar piadas\n• 🧮 Fazer cálculos simples\n• 💬 Conversar sobre diversos assuntos\n\n**Tente perguntar:**\n• "Que horas são?"\n• "Qual a capital do Japão?"\n• "Quanto é 25 x 4?"' | |
| ] | |
| }, | |
| music: { | |
| patterns: ['música', 'musica', 'tocar', 'spotify', 'youtube music'], | |
| responses: [ | |
| '🎵 Não consigo tocar música diretamente, mas posso sugerir que você use Spotify, YouTube Music ou Apple Music!', | |
| '🎧 Para ouvir música, recomendo abrir seu app de streaming favorito!' | |
| ] | |
| }, | |
| news: { | |
| patterns: ['notícias', 'noticias', 'últimas notícias', 'novidades'], | |
| responses: [ | |
| '📰 Não tenho acesso a notícias em tempo real. Sugiro verificar sites como G1, Folha, ou Estadão para informações atualizadas!' | |
| ] | |
| }, | |
| motivation: { | |
| patterns: ['motivação', 'motivacao', 'ânimo', 'animo', 'triste', 'deprimido', 'incentivo'], | |
| responses: [ | |
| '💪 "O único limite para o nosso crescimento é a nossa própria determinação."\n\nVocê é mais forte do que imagina! ✨', | |
| '🌟 "Não importa quantas vezes você caia, o que importa é quantas vezes você se levanta."\n\nVai dar tudo certo! 💜', | |
| '⭐ "Acredite em você mesmo e tudo será possível!"\n\nVocê tem potencial para conquistar grandes coisas! 🚀' | |
| ] | |
| } | |
| }; | |
| // Auto-resize textarea | |
| function autoResize(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; | |
| } | |
| // Handle Enter key | |
| function handleKeyDown(e) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| messageForm.dispatchEvent(new Event('submit')); | |
| } | |
| } | |
| // Send suggestion | |
| function sendSuggestion(text) { | |
| messageInput.value = text; | |
| messageForm.dispatchEvent(new Event('submit')); | |
| } | |
| // Create message element | |
| function createMessageElement(text, isUser = false) { | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = `flex ${isUser ? 'justify-end' : 'justify-start'} message-bubble`; | |
| const content = document.createElement('div'); | |
| content.className = `max-w-[80%] md:max-w-[70%] px-4 py-3 rounded-2xl ${ | |
| isUser | |
| ? 'gradient-bg text-white rounded-br-md' | |
| : 'bg-white border border-gray-100 text-gray-800 rounded-bl-md shadow-sm' | |
| }`; | |
| // Parse markdown-like formatting | |
| const formattedText = text | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | |
| .replace(/\n/g, '<br>'); | |
| content.innerHTML = formattedText; | |
| wrapper.appendChild(content); | |
| return wrapper; | |
| } | |
| // Create typing indicator | |
| function createTypingIndicator() { | |
| const wrapper = document.createElement('div'); | |
| wrapper.id = 'typingIndicator'; | |
| wrapper.className = 'flex justify-start message-bubble'; | |
| const content = document.createElement('div'); | |
| content.className = 'bg-white border border-gray-100 px-4 py-3 rounded-2xl rounded-bl-md shadow-sm'; | |
| content.innerHTML = ` | |
| <div class="flex gap-1 items-center h-5"> | |
| <span class="typing-dot w-2 h-2 bg-purple-400 rounded-full"></span> | |
| <span class="typing-dot w-2 h-2 bg-purple-400 rounded-full"></span> | |
| <span class="typing-dot w-2 h-2 bg-purple-400 rounded-full"></span> | |
| </div> | |
| `; | |
| wrapper.appendChild(content); | |
| return wrapper; | |
| } | |
| // Process message and generate response | |
| function processMessage(message) { | |
| const lowerMessage = message.toLowerCase(); | |
| // Check knowledge base | |
| for (const category of Object.values(knowledgeBase)) { | |
| const matched = category.patterns.some(pattern => lowerMessage.includes(pattern)); | |
| if (matched) { | |
| // Special handlers | |
| if (category === knowledgeBase.calculator) { | |
| const result = category.responses(message); | |
| if (result) return result; | |
| } | |
| if (category === knowledgeBase.capital) { | |
| for (const [country, response] of Object.entries(category.responses)) { | |
| if (lowerMessage.includes(country)) { | |
| return response; | |
| } | |
| } | |
| return 'Posso ajudar com capitais! Tente perguntar sobre: França, Brasil, Japão, Estados Unidos, Argentina, Portugal, Espanha, Itália, Alemanha ou Canadá! 🌍'; | |
| } | |
| if (typeof category.responses === 'function') { | |
| return category.responses(); | |
| } | |
| // Random response from array | |
| if (Array.isArray(category.responses)) { | |
| return category.responses[Math.floor(Math.random() * category.responses.length)]; | |
| } | |
| } | |
| } | |
| // Default responses | |
| const defaults = [ | |
| '🤔 Interessante! Me conte mais sobre isso.', | |
| '💭 Não tenho certeza se entendi completamente. Pode reformular?', | |
| '🌟 Ótimo ponto! O que mais você gostaria de saber?', | |
| '💡 Estou aprendendo todos os dias! Pode me dar mais detalhes?', | |
| '🎯 Entendo! E como posso ajudar com isso?' | |
| ]; | |
| return defaults[Math.floor(Math.random() * defaults.length)]; | |
| } | |
| // Send message | |
| async function sendMessage(message) { | |
| if (!message.trim() || isTyping) return; | |
| // Add user message | |
| chatContainer.appendChild(createMessageElement(message, true)); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Save to history | |
| messageHistory.push({ role: 'user', content: message }); | |
| // Clear input | |
| messageInput.value = ''; | |
| messageInput.style.height = 'auto'; | |
| // Show typing indicator | |
| isTyping = true; | |
| sendBtn.disabled = true; | |
| const typingIndicator = createTypingIndicator(); | |
| chatContainer.appendChild(typingIndicator); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Simulate processing time | |
| await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000)); | |
| // Remove typing indicator | |
| typingIndicator.remove(); | |
| // Generate and show response | |
| const response = processMessage(message); | |
| chatContainer.appendChild(createMessageElement(response, false)); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| // Save to history | |
| messageHistory.push({ role: 'assistant', content: response }); | |
| // Reset state | |
| isTyping = false; | |
| sendBtn.disabled = false; | |
| } | |
| // Voice input | |
| function startVoiceInput() { | |
| if (!recognition) { | |
| alert('Seu navegador não suporta reconhecimento de voz. Tente usar Chrome ou Edge.'); | |
| return; | |
| } | |
| voiceModal.classList.remove('opacity-0', 'pointer-events-none'); | |
| voiceModal.querySelector('div').classList.remove('scale-90'); | |
| recognition.onresult = (event) => { | |
| const transcript = event.results[0][0].transcript; | |
| voiceText.textContent = `"${transcript}"`; | |
| setTimeout(() => { | |
| stopVoiceInput(); | |
| messageInput.value = transcript; | |
| messageForm.dispatchEvent(new Event('submit')); | |
| }, 500); | |
| }; | |
| recognition.onerror = () => { | |
| voiceText.textContent = 'Não entendi. Tente novamente.'; | |
| }; | |
| recognition.start(); | |
| } | |
| function stopVoiceInput() { | |
| if (recognition) recognition.stop(); | |
| voiceModal.classList.add('opacity-0', 'pointer-events-none'); | |
| voiceModal.querySelector('div').classList.add('scale-90'); | |
| voiceText.textContent = 'Fale algo'; | |
| } | |
| // Clear chat | |
| function clearChat() { | |
| if (confirm('Deseja limpar toda a conversa?')) { | |
| // Keep welcome message and suggestions | |
| const welcomeSection = chatContainer.querySelector('.text-center'); | |
| const suggestions = chatContainer.querySelector('.flex.flex-wrap, .flex.overflow-x-auto'); | |
| chatContainer.innerHTML = ''; | |
| if (welcomeSection) chatContainer.appendChild(welcomeSection); | |
| if (suggestions) chatContainer.appendChild(suggestions); | |
| messageHistory = []; | |
| } | |
| } | |
| // Theme management | |
| let isDarkMode = false; | |
| function initTheme() { | |
| // Check saved theme or system preference | |
| const savedTheme = localStorage.getItem('nova-theme'); | |
| if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| enableDarkMode(); | |
| } | |
| } | |
| function enableDarkMode() { | |
| isDarkMode = true; | |
| document.documentElement.classList.add('dark'); | |
| localStorage.setItem('nova-theme', 'dark'); | |
| updateThemeIcons(); | |
| } | |
| function disableDarkMode() { | |
| isDarkMode = false; | |
| document.documentElement.classList.remove('dark'); | |
| localStorage.setItem('nova-theme', 'light'); | |
| updateThemeIcons(); | |
| } | |
| function toggleTheme() { | |
| if (isDarkMode) { | |
| disableDarkMode(); | |
| } else { | |
| enableDarkMode(); | |
| } | |
| } | |
| function updateThemeIcons() { | |
| // Update desktop icon | |
| const desktopIcon = document.getElementById('themeIconDesktop'); | |
| if (desktopIcon) { | |
| desktopIcon.setAttribute('data-lucide', isDarkMode ? 'sun' : 'moon'); | |
| lucide.createIcons(); | |
| } | |
| // Update mobile icon and text | |
| const mobileIcon = document.getElementById('themeIconMobile'); | |
| const mobileText = document.getElementById('themeTextMobile'); | |
| if (mobileIcon) { | |
| mobileIcon.setAttribute('data-lucide', isDarkMode ? 'sun' : 'moon'); | |
| mobileIcon.className = isDarkMode ? 'w-5 h-5 text-yellow-500' : 'w-5 h-5 text-purple-500'; | |
| lucide.createIcons(); | |
| } | |
| if (mobileText) { | |
| mobileText.textContent = isDarkMode ? 'Modo claro' : 'Modo escuro'; | |
| } | |
| } | |
| // Initialize theme on load | |
| initTheme(); | |
| // Export chat (placeholder) | |
| function exportChat() { | |
| const chatText = messageHistory.map(m => `${m.role === 'user' ? 'Você' : 'Nova'}: ${m.content}`).join('\n\n'); | |
| if (!chatText) { | |
| alert('Não há mensagens para exportar!'); | |
| return; | |
| } | |
| // Try to use share API on mobile | |
| if (navigator.share) { | |
| navigator.share({ | |
| title: 'Conversa com Nova Assistant', | |
| text: chatText | |
| }).catch(() => {}); | |
| } else { | |
| // Fallback: copy to clipboard | |
| navigator.clipboard.writeText(chatText).then(() => { | |
| alert('Conversa copiada para a área de transferência!'); | |
| }); | |
| } | |
| } | |
| // Mobile menu functions | |
| function showMobileMenu() { | |
| const menu = document.getElementById('mobileMenu'); | |
| const sheet = menu.querySelector('.absolute.bottom-0'); | |
| menu.classList.remove('pointer-events-none', 'opacity-0'); | |
| menu.classList.add('pointer-events-auto', 'opacity-100'); | |
| setTimeout(() => { | |
| sheet.classList.remove('translate-y-full'); | |
| }, 10); | |
| } | |
| function hideMobileMenu() { | |
| const menu = document.getElementById('mobileMenu'); | |
| const sheet = menu.querySelector('.absolute.bottom-0'); | |
| sheet.classList.add('translate-y-full'); | |
| setTimeout(() => { | |
| menu.classList.add('pointer-events-none', 'opacity-0'); | |
| menu.classList.remove('pointer-events-auto', 'opacity-100'); | |
| }, 300); | |
| } | |
| // Toggle quick actions on mobile | |
| function toggleQuickActions() { | |
| const qa = document.getElementById('quickActions'); | |
| qa.classList.toggle('hidden'); | |
| } | |
| // Check input and enable/disable send button | |
| function checkInput() { | |
| const sendBtn = document.getElementById('sendBtn'); | |
| sendBtn.disabled = !messageInput.value.trim(); | |
| } | |
| // Event listeners | |
| messageForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| if (messageInput.value.trim()) { | |
| sendMessage(messageInput.value); | |
| checkInput(); | |
| } | |
| }); | |
| // Focus input on load (desktop only) | |
| if (window.innerWidth > 640) { | |
| messageInput.focus(); | |
| } | |
| // Prevent zoom on iOS when focusing input | |
| document.addEventListener('gesturestart', function(e) { | |
| e.preventDefault(); | |
| }); |