// Configuração do Markdown parser personalizado com Highlight.js integrado const renderer = new marked.Renderer(); // Função auxiliar para escapar caracteres HTML function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Helper global para extração segura de erros de resposta HTTP no frontend async function safeExtractError(resp, defaultMsg = 'Erro na operação') { if (!resp) return defaultMsg; try { const text = await resp.text(); try { const json = JSON.parse(text); return json.error || defaultMsg; } catch (e) { if (resp.status === 502 || text.includes('Bad Gateway')) { return 'O servidor está temporariamente ocupado processando a requisição. Por favor, tente novamente em instantes.'; } return text.slice(0, 160) || defaultMsg; } } catch (e) { return defaultMsg; } } // Função global para exibir notificações elegantes estilo Toast function showToast(message, type = 'info') { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.style.position = 'fixed'; container.style.bottom = '24px'; container.style.right = '24px'; container.style.zIndex = '99999'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.gap = '10px'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = `custom-toast toast-${type}`; toast.style.background = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'; toast.style.color = '#fff'; toast.style.padding = '12px 24px'; toast.style.borderRadius = '8px'; toast.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)'; toast.style.fontFamily = "'Outfit', sans-serif"; toast.style.fontSize = '0.9rem'; toast.style.fontWeight = '500'; toast.style.minWidth = '250px'; toast.style.transition = 'all 0.3s ease'; toast.style.opacity = '0'; toast.style.transform = 'translateY(20px)'; toast.style.display = 'flex'; toast.style.alignItems = 'center'; // Icon const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'; toast.innerHTML = `${icon}${message}`; container.appendChild(toast); // Trigger animation setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; }, 10); // Auto remove setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-20px)'; setTimeout(() => { toast.remove(); }, 300); }, 3500); } window.showToast = showToast; // Tornar acessível globalmente // Função auxiliar para exibir alerta customizado e elegante (substitui o alert nativo) function showCustomAlert(title, message) { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.className = 'custom-dialog-overlay'; overlay.innerHTML = `
🔔 ${escapeHtml(title)}

${escapeHtml(message)}

`; const btn = overlay.querySelector('.btn-dialog-confirm'); btn.addEventListener('click', () => { overlay.remove(); resolve(); }); document.body.appendChild(overlay); }); } // Função auxiliar para exibir diálogo de confirmação elegante (substitui o confirm nativo) function showCustomConfirm(title, message, isDanger = false) { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.className = 'custom-dialog-overlay'; overlay.innerHTML = `
${isDanger ? '⚠️' : '❓'} ${escapeHtml(title)}

${escapeHtml(message)}

`; const btnCancel = overlay.querySelector('.btn-dialog-cancel'); const btnConfirm = overlay.querySelector(isDanger ? '.btn-dialog-danger' : '.btn-dialog-confirm'); btnCancel.addEventListener('click', () => { overlay.remove(); resolve(false); }); btnConfirm.addEventListener('click', () => { overlay.remove(); resolve(true); }); document.body.appendChild(overlay); }); } // Função auxiliar para gerar nome de arquivo de download formatado como . function getDownloadFileName(type, ext) { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${type}_${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${ext}`; } renderer.code = function(code, language) { // Ajuste para lidar com as diferenças na assinatura do marked.js const codeContent = typeof code === 'object' ? code.text : code; const lang = (typeof code === 'object' ? code.lang : language) || 'txt'; return `
${lang}
${escapeHtml(codeContent)}
`; }; marked.use({ renderer }); // Função global de copiar código para que funcione no onclick window.copyCode = function(button) { const container = button.closest('.code-container'); const codeElement = container.querySelector('code'); const textToCopy = codeElement.textContent; navigator.clipboard.writeText(textToCopy).then(() => { const textSpan = button.querySelector('span'); const originalText = textSpan.textContent; textSpan.textContent = 'Copiado!'; button.style.color = 'var(--success)'; setTimeout(() => { textSpan.textContent = originalText; button.style.color = ''; }, 2000); }).catch(err => { console.error('Erro ao copiar código:', err); }); }; const initApp = () => { // Elementos do DOM const sidebar = document.getElementById('sidebar'); const btnMenu = document.getElementById('btnMenu'); const btnCloseSidebar = document.getElementById('btnCloseSidebar'); const sidebarOverlay = document.getElementById('sidebarOverlay'); const userInput = document.getElementById('userInput'); const btnSend = document.getElementById('btnSend'); const btnVoice = document.getElementById('btnVoice'); const chatForm = document.getElementById('chatForm'); const messagesContainer = document.getElementById('messagesContainer'); const welcomeContainer = document.getElementById('welcomeContainer'); const conversationList = document.getElementById('conversationList'); const btnNewChat = document.getElementById('btnNewChat'); const btnNewChatMobile = document.getElementById('btnNewChatMobile'); 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'); // Configuração de Identidade do Agente (PedagogIA/Pedagog) const btnSettings = document.getElementById('btnSettings'); const settingsModal = document.getElementById('settingsModal'); const closeSettingsModal = document.getElementById('closeSettingsModal'); const btnSaveSettings = document.getElementById('btnSaveSettings'); const settingAgentName = document.getElementById('settingAgentName'); const previewIaAvatar = document.getElementById('previewIaAvatar'); const uploadIaAvatar = document.getElementById('uploadIaAvatar'); const previewUserAvatar = document.getElementById('previewUserAvatar'); const uploadUserAvatar = document.getElementById('uploadUserAvatar'); const welcomeTitle = document.getElementById('welcomeTitle'); const welcomeAvatarImg = document.getElementById('welcomeAvatarImg'); const sidebarAvatarImg = document.getElementById('sidebarAvatarImg'); const temperatureOptions = document.getElementById('temperatureOptions'); const knowledgeList = document.getElementById('knowledgeList'); const manualKnowledgeInput = document.getElementById('manualKnowledgeInput'); const btnAddKnowledge = document.getElementById('btnAddKnowledge'); window.agentConfig = { agentName: "PedagogIA", iaAvatarUrl: "assets/pedagog_ia.png", userAvatarUrl: "assets/pedagog_user.png", temperaturePreset: 'equilibrado' }; window.temperaturePresets = { tecnico: { value: 0.2, label: 'Técnico e Direto', description: 'Respostas curtas, objetivas, sem rodeios. Ideal para quando precisa só do essencial.' }, equilibrado: { value: 0.6, label: 'Equilibrado', description: 'Bom para uso geral — nem muito curto, nem muito longo.' }, detalhista: { value: 1.0, label: 'Detalhista e Falante', description: 'Respostas extensas, explicações completas, contextualização máxima. Ideal para estudo e revisão.' } }; window.conhecimento = { facts: [], lastUpdated: null }; // Carrega as configurações do servidor const loadAgentConfig = async () => { try { const res = await fetch('/api/agent-config'); if (res.ok) { window.agentConfig = await res.json(); updateAgentIdentityUI(); } } catch (e) { console.error('Erro ao carregar configurações do agente:', e); } }; const updateAgentIdentityUI = () => { if (welcomeTitle) { welcomeTitle.innerText = `Olá! Sou sua assistente virtual e me chamo ${window.agentConfig.agentName || 'PedagogIA'}. Como posso ajudar?`; } if (welcomeAvatarImg) { welcomeAvatarImg.src = window.agentConfig.iaAvatarUrl || window.agentConfig.kemilyAvatarUrl || 'assets/pedagog_ia.png'; } if (sidebarAvatarImg) { sidebarAvatarImg.src = window.agentConfig.userAvatarUrl || window.agentConfig.camilaAvatarUrl || 'assets/pedagog_user.png'; } if (settingAgentName) { settingAgentName.value = window.agentConfig.agentName || 'PedagogIA'; } if (previewIaAvatar) { previewIaAvatar.src = window.agentConfig.iaAvatarUrl || window.agentConfig.kemilyAvatarUrl || 'assets/pedagog_ia.png'; } if (previewUserAvatar) { previewUserAvatar.src = window.agentConfig.userAvatarUrl || window.agentConfig.camilaAvatarUrl || 'assets/pedagog_user.png'; } // Atualiza seleção de temperatura renderTemperatureOptions(); }; // Carregar presets de temperatura do servidor const loadTemperaturePresets = async () => { try { const res = await fetch('/api/temperature-presets'); if (res.ok) { window.temperaturePresets = await res.json(); renderTemperatureOptions(); } } catch (e) { console.error('Erro ao carregar presets de temperatura:', e); } }; // Renderizar opções de temperatura const renderTemperatureOptions = () => { if (!temperatureOptions) return; const preset = window.agentConfig.temperaturePreset || 'equilibrado'; temperatureOptions.innerHTML = Object.entries(window.temperaturePresets).map(([key, t]) => ` `).join(''); // Event listeners para as opções temperatureOptions.querySelectorAll('.temperature-option').forEach(opt => { opt.addEventListener('click', () => { temperatureOptions.querySelectorAll('.temperature-option').forEach(o => o.classList.remove('selected')); opt.classList.add('selected'); opt.querySelector('input').checked = true; }); }); }; // Carregar conhecimento do servidor const loadConhecimento = async () => { try { const res = await fetch('/api/conhecimento'); if (res.ok) { window.conhecimento = await res.json(); renderKnowledgeList(); } } catch (e) { console.error('Erro ao carregar conhecimento:', e); } }; // Renderizar lista de conhecimento const renderKnowledgeList = () => { if (!knowledgeList) return; if (!window.conhecimento.facts || window.conhecimento.facts.length === 0) { knowledgeList.innerHTML = '

Nenhum conhecimento aprendido ainda. A assistente aprende automaticamente quando você ensinar algo novo!

'; return; } knowledgeList.innerHTML = window.conhecimento.facts.map(f => `
${f.source}

${f.content}

${new Date(f.addedAt).toLocaleDateString('pt-BR')}
`).join(''); }; // Iniciar edição de item de conhecimento window.startEditKnowledge = (id) => { const fact = window.conhecimento.facts.find(f => f.id === id); if (!fact) return; const contentDiv = document.getElementById(`knowledge-content-${id}`); const actionsDiv = document.getElementById(`knowledge-actions-${id}`); if (!contentDiv) return; if (actionsDiv) actionsDiv.style.display = 'none'; contentDiv.innerHTML = ` ${fact.source}
`; const textarea = document.getElementById(`edit-textarea-${id}`); if (textarea) { textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); } }; // Salvar edição de item de conhecimento window.saveEditKnowledge = async (id) => { const textarea = document.getElementById(`edit-textarea-${id}`); if (!textarea) return; const newContent = textarea.value.trim(); if (!newContent) { alert('O conteúdo do conhecimento não pode ser vazio.'); return; } try { const res = await fetch(`/api/conhecimento/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: newContent }) }); if (res.ok) { const fact = window.conhecimento.facts.find(f => f.id === id); if (fact) fact.content = newContent; renderKnowledgeList(); } else { const err = await res.json(); alert('Erro ao salvar conhecimento: ' + err.error); } } catch (e) { console.error('Erro ao atualizar conhecimento:', e); alert('Erro de conexão ao salvar conhecimento.'); } }; // Deletar item de conhecimento (função global para onclick) window.deleteKnowledge = async (id) => { if (!confirm('Deseja realmente remover este conhecimento aprendido?')) return; try { const res = await fetch(`/api/conhecimento/${id}`, { method: 'DELETE' }); if (res.ok) { window.conhecimento.facts = window.conhecimento.facts.filter(f => f.id !== id); renderKnowledgeList(); } } catch (e) { console.error('Erro ao deletar conhecimento:', e); } }; // Adicionar conhecimento manualmente if (btnAddKnowledge) { btnAddKnowledge.addEventListener('click', async () => { const input = manualKnowledgeInput?.value.trim(); if (!input) return; try { const res = await fetch('/api/conhecimento', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fact: input, source: 'manual' }) }); if (res.ok) { const data = await res.json(); if (data.added) { manualKnowledgeInput.value = ''; await loadConhecimento(); } else { alert('Esta informação já existe ou é similar a um conhecimento que a IA já possui.'); } } else { alert('Erro ao salvar no servidor. Verifique sua conexão.'); } } catch (e) { console.error('Erro ao adicionar conhecimento:', e); alert('Erro ao processar requisição.'); } }); } // Função para extrair knowledge tags da resposta da IA e enviar ao servidor window.extractAndSaveKnowledge = async (assistantMessage) => { if (!assistantMessage || !assistantMessage.includes('[KNOWLEDGE:')) return; try { await fetch('/api/conhecimento/extrair', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: assistantMessage }) }); } catch (e) { console.error('Erro ao extrair conhecimento:', e); } }; // Variáveis temporárias para avatares (em base64 ou URLs novas) let newIaAvatarData = null; let newUserAvatarData = null; // Eventos do Modal if (btnSettings) { btnSettings.addEventListener('click', () => { newIaAvatarData = null; newUserAvatarData = null; if (settingAgentName) settingAgentName.value = window.agentConfig.agentName || 'PedagogIA'; if (previewIaAvatar) previewIaAvatar.src = window.agentConfig.iaAvatarUrl || window.agentConfig.kemilyAvatarUrl || 'assets/pedagog_ia.png'; if (previewUserAvatar) previewUserAvatar.src = window.agentConfig.userAvatarUrl || window.agentConfig.camilaAvatarUrl || 'assets/pedagog_user.png'; renderTemperatureOptions(); loadConhecimento(); settingsModal.style.display = 'flex'; }); } if (closeSettingsModal) { closeSettingsModal.addEventListener('click', () => { settingsModal.style.display = 'none'; }); } // Tab switching logic document.querySelectorAll('.settings-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.settings-tab-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); const targetId = tab.dataset.tab; const targetContent = document.getElementById(targetId); if (targetContent) targetContent.classList.add('active'); }); }); // Fechar clicando fora do modal-content if (settingsModal) { settingsModal.addEventListener('click', (e) => { if (e.target === settingsModal) { settingsModal.style.display = 'none'; } }); } // Previews de Imagem const handleAvatarFileSelect = (input, previewImg, callback) => { const file = input.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { previewImg.src = e.target.result; callback(e.target.result); }; reader.readAsDataURL(file); } }; if (uploadIaAvatar) { uploadIaAvatar.addEventListener('change', () => { handleAvatarFileSelect(uploadIaAvatar, previewIaAvatar, (base64) => { newIaAvatarData = base64; }); }); } if (uploadUserAvatar) { uploadUserAvatar.addEventListener('change', () => { handleAvatarFileSelect(uploadUserAvatar, previewUserAvatar, (base64) => { newUserAvatarData = base64; }); }); } // Salvar Configurações if (btnSaveSettings) { btnSaveSettings.addEventListener('click', async () => { btnSaveSettings.disabled = true; btnSaveSettings.innerText = 'Salvando...'; try { let iaUrl = window.agentConfig.iaAvatarUrl || window.agentConfig.kemilyAvatarUrl || 'assets/pedagog_ia.png'; let userUrl = window.agentConfig.userAvatarUrl || window.agentConfig.camilaAvatarUrl || 'assets/pedagog_user.png'; // Se houver novas imagens, faz upload if (newIaAvatarData) { const res = await fetch('/api/upload-avatar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ avatarType: 'ia', base64Data: newIaAvatarData }) }); if (res.ok) { const data = await res.json(); iaUrl = data.url; } } if (newUserAvatarData) { const res = await fetch('/api/upload-avatar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ avatarType: 'user', base64Data: newUserAvatarData }) }); if (res.ok) { const data = await res.json(); userUrl = data.url; } } // Envia as configurações gerais const selectedTempPreset = temperatureOptions?.querySelector('input[name="temperature"]:checked')?.value || 'equilibrado'; const resConfig = await fetch('/api/agent-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentName: settingAgentName.value, iaAvatarUrl: iaUrl, userAvatarUrl: userUrl, temperaturePreset: selectedTempPreset }) }); if (resConfig.ok) { const result = await resConfig.json(); window.agentConfig = result.config; updateAgentIdentityUI(); settingsModal.style.display = 'none'; } else { alert('Erro ao salvar as configurações.'); } } catch (err) { console.error(err); alert('Ocorreu um erro ao salvar.'); } finally { btnSaveSettings.disabled = false; btnSaveSettings.innerText = 'Salvar Configurações'; } }); } // Executa o carregamento inicial das configurações loadAgentConfig(); // Variáveis de Estado let chats = JSON.parse(localStorage.getItem('pedagog_chats') || 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('pedagog_current_chat_id') || localStorage.getItem('camila_current_chat_id') || null; let isGenerating = false; let searchQuery = ''; let activeTag = 'all'; let currentUtterance = null; // para controlar TTS em andamento // Variáveis de Reconhecimento de Voz let recognition = null; let isListening = false; let lastInputWasVoice = false; // Rastrear se a última mensagem veio do microfone // ========================================================================== // 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('pedagog_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('pedagog_theme') || 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 & SIDEBAR TAGS // ========================================================================== const AVAILABLE_TAGS = [ { id: 'relatorios', label: '📋 Relatórios' }, { id: 'planejamento', label: '📝 Planejamento' }, { id: 'mindlab', label: '🧠 Mind Lab' }, { id: 'estudos', label: '📚 Estudos' }, { id: 'htpc', label: '💻 HTPC' }, { id: 'ata', label: '📝 ATA' }, { id: 'sugestao', label: '💡 Sugestão' }, { id: 'duvida', label: '❓ 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(); }); }); }; const renderSidebarTags = () => { if (!sidebarTags) return; let tagsHtml = ``; tagsHtml += AVAILABLE_TAGS.map(tag => ` `).join(''); sidebarTags.innerHTML = tagsHtml; }; 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 // ========================================================================== const toggleSidebar = (open) => { if (open) { sidebar.classList.add('open'); } else { sidebar.classList.remove('open'); } }; btnMenu.addEventListener('click', () => toggleSidebar(true)); btnCloseSidebar.addEventListener('click', () => toggleSidebar(false)); sidebarOverlay.addEventListener('click', () => toggleSidebar(false)); // Redimensionamento automático do Input de Texto (textarea) userInput.addEventListener('input', () => { userInput.style.height = 'auto'; userInput.style.height = (userInput.scrollHeight - 4) + 'px'; // Habilitar ou desabilitar botão btnSend.disabled = userInput.value.trim() === '' || isGenerating; }); // Prevenir comportamento padrão do Enter no textarea (enviar em vez de quebrar linha, Shift+Enter para nova linha) userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (userInput.value.trim() !== '' && !isGenerating) { chatForm.requestSubmit(); } } }); // 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(); }); // Alternar (expandir/recolher) as tags const btnToggleSidebarTags = document.getElementById('btnToggleSidebarTags'); if (btnToggleSidebarTags && sidebarTags) { btnToggleSidebarTags.addEventListener('click', () => { if (sidebarTags.style.display === 'none') { sidebarTags.style.display = 'flex'; } else { sidebarTags.style.display = 'none'; } }); } // Filtro por tags sidebarTags.addEventListener('click', (e) => { const tagBtn = e.target.closest('.tag-filter'); if (!tagBtn) return; activeTag = tagBtn.dataset.tag; renderHistory(); }); // ========================================================================== // VOZ — WEB SPEECH API (ditado + conversa por voz) // ========================================================================== const initVoiceRecognition = () => { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { btnVoice.style.display = 'none'; console.warn('SpeechRecognition não suportado neste navegador.'); return; } recognition = new SpeechRecognition(); recognition.lang = 'pt-BR'; recognition.continuous = false; recognition.interimResults = false; recognition.maxAlternatives = 1; recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; userInput.value = transcript; userInput.dispatchEvent(new Event('input')); setVoiceState(false); // Marca que o input veio do microfone para auto-responder por voz lastInputWasVoice = true; // Envia automaticamente a mensagem ditada if (transcript.trim()) { setTimeout(() => { chatForm.dispatchEvent(new Event('submit', { cancelable: true })); }, 300); } }; recognition.onerror = (event) => { console.warn('Voice error:', event.error); setVoiceState(false); }; recognition.onend = () => { setVoiceState(false); }; }; const setVoiceState = (listening) => { isListening = listening; if (listening) { btnVoice.classList.add('listening'); btnVoice.setAttribute('title', 'Clique para parar de ouvir'); } else { btnVoice.classList.remove('listening'); btnVoice.setAttribute('title', 'Conversar por voz'); } }; btnVoice.addEventListener('click', () => { if (!recognition) return; if (isListening) { recognition.stop(); } else { recognition.start(); setVoiceState(true); } }); // ========================================================================== // TTS — Speech Synthesis nativa (voz do navegador) // ========================================================================== // Selecionar melhor voz pt-BR disponível no dispositivo de forma dinâmica const getBestVoice = () => { const voices = window.speechSynthesis.getVoices(); const ptBR = voices.filter(v => v.lang === 'pt-BR' || v.lang === 'pt_BR'); if (ptBR.length === 0) { const pt = voices.filter(v => v.lang.startsWith('pt')); return pt[0] || null; } // 1. Microsoft Neural (Edge): Thalita, Francisca, Giovanna const msNeural = ptBR.find(v => /thalita|francisca|giovanna|natural|neural/i.test(v.name)); if (msNeural) return msNeural; // 2. Google (Chrome/Android): vozes naturais const google = ptBR.find(v => /google/i.test(v.name)); if (google) return google; // 3. Apple (Safari/iOS): Luciana é excelente const apple = ptBR.find(v => /luciana|fernanda/i.test(v.name)); if (apple) return apple; // 4. Qualquer feminina const female = ptBR.find(v => /female|femin|maria|raquel|camila/i.test(v.name)); if (female) return female; // 5. Primeira disponível return ptBR[0]; }; const resetSpeakBtnState = (btn) => { if (btn) { btn.classList.remove('speaking'); const span = btn.querySelector('span'); if (span) span.textContent = 'Ouvir'; } }; let currentSpeakingBtn = null; let currentAudio = null; let ttsFallbackTimeout = null; // Função auxiliar para parar qualquer áudio em andamento (tanto nativo quanto do backend) const stopAllSpeech = () => { // Parar áudio do backend if (currentAudio) { currentAudio.pause(); currentAudio = null; } // Parar SpeechSynthesis nativo window.speechSynthesis.cancel(); // Limpar timeouts pendentes if (ttsFallbackTimeout) { clearTimeout(ttsFallbackTimeout); ttsFallbackTimeout = null; } // Resetar estado do botão ativo anterior if (currentSpeakingBtn) { resetSpeakBtnState(currentSpeakingBtn); currentSpeakingBtn = null; } }; const speakTextLocalFallback = (cleanText, btn) => { console.log('[TTS] Acionando fallback local (voz nativa do navegador)...'); // Configura SpeechSynthesis nativo const utterance = new SpeechSynthesisUtterance(cleanText); utterance.lang = 'pt-BR'; utterance.rate = 1.15; utterance.pitch = 1.0; const voice = getBestVoice(); if (voice) utterance.voice = voice; utterance.onstart = () => { if (btn) { btn.classList.add('speaking'); const span = btn.querySelector('span'); if (span) span.textContent = 'Parar'; } }; const handleStop = (event) => { console.log('TTS nativo finalizado ou com erro:', event.type); if (currentSpeakingBtn === btn) { resetSpeakBtnState(btn); currentSpeakingBtn = null; } }; utterance.onend = handleStop; utterance.onerror = handleStop; window.speechSynthesis.speak(utterance); }; // Falar texto com reprodução híbrida (prioriza Opção A: MsEdgeTTS com fallback local) const speakText = async (text, btn) => { if (!text) return; // Se clicar no botão que já está ativo, para tudo e retorna if (currentSpeakingBtn === btn) { stopAllSpeech(); return; } // Para qualquer reprodução anterior stopAllSpeech(); currentSpeakingBtn = btn; // Limpar markdown e emojis antes de falar const clean = text .replace(/\p{Extended_Pictographic}/gu, '') // Remove emojis pictográficos (ex: ✍️, 📝, 🧒) .replace(/[\u{1F300}-\u{1F9FF}]|[\u{2700}-\u{27BF}]|[\u{2600}-\u{26FF}]/gu, '') // Fallback para símbolos/dingbats comuns .replace(/```[\s\S]*?```/g, '') .replace(/`[^`]+`/g, '') .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') .replace(/[*_#~\[\]>]/g, '') .replace(/\n+/g, '. ') .replace(/\s{2,}/g, ' ') .trim(); if (!clean) { currentSpeakingBtn = null; return; } // Coloca o botão em estado de "carregando" if (btn) { btn.classList.add('speaking'); const span = btn.querySelector('span'); if (span) span.textContent = 'Carregando...'; } let fallbackTriggered = false; // Configura o timeout de 15 segundos para o fallback nativo ttsFallbackTimeout = setTimeout(() => { if (currentSpeakingBtn === btn) { fallbackTriggered = true; if (currentAudio) { currentAudio.pause(); currentAudio = null; } speakTextLocalFallback(clean, btn); } }, 15000); try { console.log('[TTS] Solicitando voz neural de alta qualidade ao backend...'); const response = await fetch('/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: clean }) }); if (!response.ok) { throw new Error(`Erro no servidor: ${response.status}`); } const blob = await response.blob(); // Se o fallback já foi acionado durante a espera, ignora o retorno do áudio do backend if (fallbackTriggered) { return; } // Limpar o timeout de fallback já que o áudio respondeu a tempo if (ttsFallbackTimeout) { clearTimeout(ttsFallbackTimeout); ttsFallbackTimeout = null; } const audioUrl = URL.createObjectURL(blob); currentAudio = new Audio(audioUrl); currentAudio.onplay = () => { if (currentSpeakingBtn === btn) { const span = btn.querySelector('span'); if (span) span.textContent = 'Parar'; } }; const handleAudioStop = () => { if (currentSpeakingBtn === btn) { resetSpeakBtnState(btn); currentSpeakingBtn = null; currentAudio = null; } URL.revokeObjectURL(audioUrl); }; currentAudio.onended = handleAudioStop; currentAudio.onerror = (e) => { console.error('[TTS] Erro ao reproduzir áudio do backend:', e); if (currentSpeakingBtn === btn && !fallbackTriggered) { fallbackTriggered = true; speakTextLocalFallback(clean, btn); } }; await currentAudio.play(); } catch (err) { console.error('[TTS] Falha ao obter áudio do backend, acionando fallback local imediatamente:', err); // Limpa timeout e ativa fallback local imediatamente se der erro de rede if (ttsFallbackTimeout) { clearTimeout(ttsFallbackTimeout); ttsFallbackTimeout = null; } if (currentSpeakingBtn === btn && !fallbackTriggered) { fallbackTriggered = true; speakTextLocalFallback(clean, btn); } } }; // ========================================================================== // ANEXOS — Upload de imagens e documentos // ========================================================================== const fileInput = document.getElementById('fileInput'); const btnAttach = document.getElementById('btnAttach'); let attachedFiles = []; // Criar preview container se não existir let attachPreview = document.querySelector('.attachments-preview'); const ensureAttachPreview = () => { if (!attachPreview) { attachPreview = document.createElement('div'); attachPreview.className = 'attachments-preview'; document.getElementById('chatForm').appendChild(attachPreview); } }; // Botão de anexar → abre seletor de arquivos btnAttach.addEventListener('click', () => { fileInput.click(); }); // Botão alternador de ferramentas (recursos) - FAB Tools const fabTools = document.getElementById('fabTools'); const promptModesBar = document.querySelector('.prompt-modes-bar'); if (fabTools && promptModesBar) { fabTools.addEventListener('click', () => { promptModesBar.classList.toggle('show'); fabTools.classList.toggle('active'); }); } // Quando arquivos são selecionados fileInput.addEventListener('change', () => { const files = Array.from(fileInput.files); if (files.length === 0) return; files.forEach(file => { // Limite de 10MB por arquivo if (file.size > 10 * 1024 * 1024) { alert(`Arquivo "${file.name}" é muito grande. Máximo: 10MB.`); return; } attachedFiles.push(file); }); renderAttachPreview(); fileInput.value = ''; // reset para permitir selects again }); // Arrastar e soltar na área do chat const chatArea = document.querySelector('.chat-area'); if (chatArea) { chatArea.addEventListener('dragover', (e) => { e.preventDefault(); chatArea.classList.add('drag-over'); }); chatArea.addEventListener('dragleave', () => { chatArea.classList.remove('drag-over'); }); chatArea.addEventListener('drop', (e) => { e.preventDefault(); chatArea.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files); files.forEach(file => { if (file.size <= 10 * 1024 * 1024) { attachedFiles.push(file); } }); renderAttachPreview(); }); } // Renderizar preview dos anexos const renderAttachPreview = () => { ensureAttachPreview(); attachPreview.innerHTML = ''; if (attachedFiles.length === 0) { attachPreview.style.display = 'none'; btnAttach.classList.remove('has-files'); return; } btnAttach.classList.add('has-files'); attachPreview.style.display = 'flex'; attachedFiles.forEach((file, index) => { const item = document.createElement('div'); item.className = 'attachment-item'; const isImage = file.type.startsWith('image/'); let previewHtml = ''; if (isImage) { const url = URL.createObjectURL(file); previewHtml = `${file.name}`; } else { previewHtml = ` `; } item.innerHTML = ` ${previewHtml} ${file.name} `; attachPreview.appendChild(item); }); // Botões de remover attachPreview.querySelectorAll('.remove-attachment').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.index); attachedFiles.splice(idx, 1); renderAttachPreview(); }); }); }; // ========================================================================== // ATALHOS DE TECLADO // ========================================================================== document.addEventListener('keydown', (e) => { // Ctrl+N → novo chat if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); createNewChat(); } // Ctrl+F ou Ctrl+K → focar busca if ((e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'k')) { e.preventDefault(); toggleSidebar(true); setTimeout(() => searchInput.focus(), 50); } // Escape → fechar sidebar if (e.key === 'Escape') { toggleSidebar(false); } }); // ========================================================================== // HISTÓRICO DE CHATS (LOCALSTORAGE) — VERSÃO COMPLETA COM PINNED, TAGS, BUSCA // ========================================================================== const saveChats = () => { localStorage.setItem('pedagog_chats', JSON.stringify(chats)); if (currentChatId) { localStorage.setItem('pedagog_current_chat_id', currentChatId); } else { localStorage.removeItem('pedagog_current_chat_id'); } }; // 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: '📚', htpc: '💻', ata: '📝', sugestao: '💡', duvida: '❓' }; return emojis[tag] || '🏷️'; }; // Renderizar histórico completo const renderHistory = () => { renderSidebarTags(); // Renderizar tags da sidebar de forma dinâmica e atualizada historyItems.innerHTML = ''; if (chats.length === 0) { historyItems.innerHTML = '
Nenhum chat salvo
'; return; } // Aplicar filtros let filtered = [...chats]; filtered = filterByTag(filtered); filtered = filterBySearch(filtered); // 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(); renameChatInline(chat.id, item); return; } if (e.target.closest('.btn-tag-chat')) { e.stopPropagation(); editTags(chat.id); return; } selectChat(chat.id); toggleSidebar(false); }); historyItems.appendChild(item); }); return; } 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(); renameChatInline(chat.id, item); return; } if (e.target.closest('.btn-tag-chat')) { e.stopPropagation(); editTags(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 inline const renameChatInline = (id, itemElement) => { const chat = chats.find(c => c.id === id); if (!chat) return; const titleDiv = itemElement.querySelector('.history-item-title'); if (!titleDiv) return; const originalTitle = chat.title; const input = document.createElement('input'); input.type = 'text'; input.className = 'rename-input'; input.value = originalTitle; // Prevenir clique e propagação para o item input.addEventListener('click', (e) => e.stopPropagation()); const finishRename = () => { const newTitle = input.value.trim(); if (newTitle && newTitle !== originalTitle) { chat.title = newTitle; chat.updatedAt = Date.now(); saveChats(); } renderHistory(); }; input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); finishRename(); } else if (e.key === 'Escape') { e.preventDefault(); renderHistory(); } }); input.addEventListener('blur', finishRename); titleDiv.replaceWith(input); input.focus(); input.select(); }; // Gerenciar tags da conversa const editTags = (id) => { const chat = chats.find(c => c.id === id); if (!chat) return; const currentTags = chat.tags || []; openTagModal(id, currentTags, (newTags) => { chat.tags = newTags; chat.updatedAt = Date.now(); saveChats(); renderHistory(); }); }; const selectChat = (id) => { currentChatId = id; const chat = chats.find(c => c.id === id); if (!chat) return; saveChats(); renderHistory(); // Ocultar welcome e renderizar as mensagens welcomeContainer.style.display = 'none'; conversationList.innerHTML = ''; chat.messages.forEach(msg => { appendMessageUI(msg.role, msg.content, msg.media); }); scrollToBottom(); }; const createNewChat = () => { if (isGenerating) return; currentChatId = null; localStorage.removeItem('pedagog_current_chat_id'); welcomeContainer.style.display = 'flex'; conversationList.innerHTML = ''; userInput.value = ''; userInput.style.height = 'auto'; btnSend.disabled = true; renderHistory(); }; const deleteChat = (id) => { chats = chats.filter(c => c.id !== id); if (currentChatId === id) { currentChatId = null; createNewChat(); } else { saveChats(); renderHistory(); } }; btnNewChat.addEventListener('click', createNewChat); btnNewChatMobile.addEventListener('click', createNewChat); // ========================================================================== // FLUXO DO CHAT & RENDERIZAÇÃO // ========================================================================== const appendMessageUI = (role, content, media = null) => { const isUser = role === 'user'; const row = document.createElement('div'); row.className = `message-row ${role}`; const avatarHtml = isUser ? `
Usuário
` : `
${window.agentConfig?.agentName || 'PedagogIA'}
`; let contentHtml = ''; if (isUser) { contentHtml = `

${escapeHtml(content).replace(/\n/g, '
')}

`; } else { if (media) { if (media.type === 'image') { contentHtml = `
Ilustração gerada

✨ "${escapeHtml(media.originalPrompt)}"

`; } else if (media.type === 'audio') { const audioId = 'audio_' + Date.now() + '_' + Math.floor(Math.random() * 10000); contentHtml = `
0:00

🎵 "${escapeHtml(media.originalPrompt)}"

`; } else if (media.type === 'video') { contentHtml = `

🎥 "${escapeHtml(media.originalPrompt)}"

`; } } else { if (content.trim().startsWith('
')) { contentHtml = content; } else { contentHtml = marked.parse(content); } } } const actionsHtml = !isUser ? `
` : `
`; row.innerHTML = `
${avatarHtml}
${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); }); }); } // Event listener para editar/reenviar mensagem const editBtn = row.querySelector('.btn-edit'); if (editBtn) { editBtn.addEventListener('click', () => { const text = editBtn.dataset.content; const chat = chats.find(c => c.id === currentChatId); if (!chat) return; // Encontrar índice desta mensagem no chat const rows = conversationList.querySelectorAll('.message-row.user'); let msgRow = null; for (const r of rows) { const btn = r.querySelector('.btn-edit'); if (btn && btn.dataset.content === text) { msgRow = r; break; } } // Remover esta mensagem e todas as subsequentes (do user e do bot) if (msgRow) { const allRows = Array.from(conversationList.querySelectorAll('.message-row')); const idx = allRows.indexOf(msgRow); for (let i = allRows.length - 1; i >= idx; i--) { allRows[i].remove(); } // Remover do estado também const msgIndex = chat.messages.findIndex(m => m.role === 'user' && m.content === text); if (msgIndex >= 0) { chat.messages = chat.messages.slice(0, msgIndex); } saveChats(); } // Colocar texto no input e focar userInput.value = text; userInput.focus(); userInput.dispatchEvent(new Event('input')); btnSend.disabled = false; }); } // Event listener para ouvir resposta (TTS nativo) const speakBtn = row.querySelector('.btn-speak'); if (speakBtn) { speakBtn.addEventListener('click', () => { const text = speakBtn.dataset.content; speakText(text, speakBtn); }); } // Event listener para regenerar resposta const regenBtn = row.querySelector('.btn-regenerate'); if (regenBtn) { regenBtn.addEventListener('click', () => { if (isGenerating) return; regenerateLastResponse(); }); } return row; }; // Regenerar última resposta do bot const regenerateLastResponse = () => { const chat = chats.find(c => c.id === currentChatId); if (!chat || chat.messages.length === 0) return; // Encontrar última mensagem do bot let lastBotIdx = -1; for (let i = chat.messages.length - 1; i >= 0; i--) { if (chat.messages[i].role === 'assistant') { lastBotIdx = i; break; } } if (lastBotIdx === -1) return; // Remover última mensagem do bot e todas as mensagens depois da última mensagem do usuário let lastUserIdx = -1; for (let i = lastBotIdx - 1; i >= 0; i--) { if (chat.messages[i].role === 'user') { lastUserIdx = i; break; } } // Cortar mensagens até a última do usuário if (lastUserIdx >= 0) { chat.messages = chat.messages.slice(0, lastUserIdx + 1); } else { chat.messages = []; } // Remover da UI as mensagens depois do último user const rows = conversationList.querySelectorAll('.message-row'); for (let i = rows.length - 1; i >= 0; i--) { const row = rows[i]; const isBot = row.classList.contains('assistant'); const isUser = row.classList.contains('user'); if (isBot) { row.remove(); } else if (isUser) { break; } } saveChats(); scrollToBottom(); // Reenviar última mensagem do usuário para gerar nova resposta const lastUserMsg = chat.messages[chat.messages.length - 1]; if (lastUserMsg && lastUserMsg.role === 'user') { handleSendMessage(lastUserMsg.content, true); } }; const scrollToBottom = () => { messagesContainer.scrollTop = messagesContainer.scrollHeight; }; const getProgressSteps = (promptText) => { const promptLower = (promptText || '').toLowerCase(); if (promptLower.includes('historia') || promptLower.includes('conto') || promptLower.includes('história') || promptLower.includes('escrever')) { return [ { text: "Analisando a solicitação da professora...", progress: 15 }, { text: "Esboçando o enredo e personagens infantis...", progress: 40 }, { text: "Elaborando a história pedagógica...", progress: 70 }, { text: "Fazendo sugestões para uso pedagógico...", progress: 90 } ]; } if (promptLower.includes('relatorio') || promptLower.includes('relatório') || promptLower.includes('observa') || promptLower.includes('htpc')) { return [ { text: "Analisando as observações pedagógicas...", progress: 15 }, { text: "Sintetizando os avanços e interações...", progress: 40 }, { text: "Estruturando os blocos do relatório individual...", progress: 65 }, { text: "Refinando o texto e alinhando com a BNCC...", progress: 85 }, { text: "Ajustando o tom formal-pedagógico...", progress: 95 } ]; } if (promptLower.includes('planejamento') || promptLower.includes('aula') || promptLower.includes('atividade') || promptLower.includes('projeto') || promptLower.includes('brincar')) { return [ { text: "Revisando os campos de experiência da BNCC...", progress: 20 }, { text: "Estruturando os momentos da vivência lúdica...", progress: 45 }, { text: "Definindo a intencionalidade educativa e mediação...", progress: 70 }, { text: "Finalizando as comandas e recursos pedagógicos...", progress: 90 } ]; } // Caso geral return [ { text: "Analisando a solicitação...", progress: 20 }, { text: "Processando as diretrizes pedagógicas...", progress: 45 }, { text: "Elaborando a resposta...", progress: 75 }, { text: "Refinando a formatação e estilo...", progress: 90 } ]; }; // Função principal de envio de mensagens const handleSendMessage = async (text, skipUserAppend = false, files = null, forcedIntent = null) => { if (!text && (!files || files.length === 0)) return; // Se não recebe files por parâmetro, usa os anexados globalmente const filesToSend = files || attachedFiles; let fullText = text || ''; // Processar anexos: evitar base64 gigantesco em imagens e erro de leitura binária em PDF/DOCX if (filesToSend.length > 0) { for (const file of filesToSend) { if (file.type.startsWith('image/')) { // Apenas anexa uma referência textual limpa da imagem para a IA saber que ela existe fullText += `\n\n*[Imagem anexada: ${file.name} (${Math.round(file.size/1024)} KB)]*`; } else if (file.name.endsWith('.txt') || file.name.endsWith('.md') || file.name.endsWith('.json') || file.name.endsWith('.csv') || file.name.endsWith('.js') || file.name.endsWith('.css') || file.name.endsWith('.html')) { const reader = new FileReader(); const content = await new Promise((resolve) => { reader.onload = (e) => resolve(e.target.result); reader.readAsText(file); }); const truncated = content.length > 3000 ? content.substring(0, 3000) + '... [conteúdo truncado]' : content; fullText += `\n\n*[Conteúdo do arquivo "${file.name}":]*\n\`\`\`\n${truncated}\n\`\`\``; } else { // Para arquivos binários como PDF/DOCX que não podem ser lidos diretamente como texto cru no frontend fullText += `\n\n*[Arquivo anexado: ${file.name} (${Math.round(file.size/1024)} KB) - formato não textual]*`; } } // Limpar anexos após envio attachedFiles = []; if (attachPreview) { attachPreview.innerHTML = ''; attachPreview.style.display = 'none'; btnAttach.classList.remove('has-files'); } } isGenerating = true; btnSend.disabled = true; userInput.disabled = true; userInput.value = ''; userInput.style.height = 'auto'; // Ocultar tela de boas-vindas se for a primeira mensagem if (welcomeContainer.style.display !== 'none') { welcomeContainer.style.display = 'none'; } // Criar o chat se for novo if (!currentChatId) { const newId = 'chat_' + Date.now(); const firstLine = fullText.split('\n')[0]; const title = firstLine.length > 26 ? firstLine.substring(0, 26) + '...' : firstLine; chats.push({ id: newId, title: title, tags: [], pinned: false, messages: [], createdAt: Date.now(), updatedAt: Date.now() }); currentChatId = newId; } // Achar o chat atual no estado const chat = chats.find(c => c.id === currentChatId); if (!chat) return; // Adicionar mensagem do usuário no estado e na tela (pula se for regeneração) if (!skipUserAppend) { const userMsg = { role: 'user', content: fullText }; chat.messages.push(userMsg); chat.updatedAt = Date.now(); appendMessageUI('user', fullText); scrollToBottom(); saveChats(); renderHistory(); } // Criar container para a resposta do bot (PedaGog) na tela const botRow = document.createElement('div'); botRow.className = 'message-row assistant'; botRow.innerHTML = `
${window.agentConfig?.agentName || 'PedagogIA'}
`; conversationList.appendChild(botRow); scrollToBottom(); const botContentDiv = botRow.querySelector('.message-content'); let assistantContent = ''; // Obter etapas de progresso personalizadas const progressSteps = getProgressSteps(fullText); let currentStepIdx = 0; // Renderizar barra de progresso inicial botContentDiv.innerHTML = `
${progressSteps[0].text}
`; scrollToBottom(); // Fechar barra de prompt no mobile se estiver aberta para liberar espaço const mobilePromptBar = document.querySelector('.prompt-modes-bar'); const mobileToggleBtn = document.getElementById('fabTools'); if (mobilePromptBar && window.innerWidth <= 768) { mobilePromptBar.classList.remove('show'); if (mobileToggleBtn) mobileToggleBtn.classList.remove('active'); } // Iniciar timer para avançar as etapas let progressTimer = setInterval(() => { if (currentStepIdx < progressSteps.length - 1) { currentStepIdx++; const step = progressSteps[currentStepIdx]; const stepTextEl = botContentDiv.querySelector('#iaProgressStepText'); const barFillEl = botContentDiv.querySelector('#iaProgressBarFill'); if (stepTextEl && barFillEl) { stepTextEl.textContent = step.text; barFillEl.style.width = `${step.progress}%`; } } }, 1500); try { // Filtrar mensagens para enviar apenas o histórico necessário ao OpenRouter, removendo dados base64 pesados de mídias const messagesPayload = chat.messages.map(m => { let cleanContent = m.content || ''; if (cleanContent.includes('data:image/') || cleanContent.includes('data:audio/') || cleanContent.includes('data:video/')) { const typeLabel = cleanContent.includes('custom-audio-player') ? 'Áudio/Música' : (cleanContent.includes('media-video-container') ? 'Vídeo' : 'Imagem'); cleanContent = `[Mídia gerada anteriormente: ${typeLabel}]`; } return { role: m.role, content: cleanContent }; }); // Fazer a chamada HTTP usando stream const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: messagesPayload, forcedIntent: forcedIntent }) }); if (!response.ok) { throw new Error('Falha na resposta do servidor'); } // Processar stream de dados (SSE) const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let isMediaResponse = false; let mediaMsgData = null; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); // Deixar a última parte inacabada no buffer buffer = lines.pop(); for (const line of lines) { const cleanLine = line.trim(); if (cleanLine.startsWith('data: ')) { const dataContent = cleanLine.slice(6); if (dataContent === '[DONE]') { continue; } try { const parsed = JSON.parse(dataContent); if (parsed.content) { assistantContent += parsed.content; // Mantemos o progresso visível sem renderizar o texto parcial } else if (parsed.type === 'media_status') { isMediaResponse = true; if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } botContentDiv.innerHTML = `

${escapeHtml(parsed.message)}

`; scrollToBottom(); } else if (parsed.type === 'image') { isMediaResponse = true; if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } mediaMsgData = { type: 'image', url: parsed.url, originalPrompt: parsed.originalPrompt }; assistantContent = `
Ilustração gerada

✨ "${escapeHtml(parsed.originalPrompt)}"

`; botContentDiv.innerHTML = assistantContent; scrollToBottom(); } else if (parsed.type === 'error') { isMediaResponse = true; if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } botContentDiv.innerHTML = `

⚠️ Erro na geração: ${escapeHtml(parsed.message)}

`; scrollToBottom(); } else if (parsed.type === 'audio') { isMediaResponse = true; if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } mediaMsgData = { type: 'audio', url: parsed.url, originalPrompt: parsed.originalPrompt }; const audioId = 'audio_' + Date.now(); assistantContent = `
0:00

🎵 "${escapeHtml(parsed.originalPrompt)}"

`; botContentDiv.innerHTML = assistantContent; scrollToBottom(); } else if (parsed.type === 'video') { isMediaResponse = true; if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } mediaMsgData = { type: 'video', url: parsed.url, originalPrompt: parsed.originalPrompt }; assistantContent = `

🎥 "${escapeHtml(parsed.originalPrompt)}"

`; botContentDiv.innerHTML = assistantContent; scrollToBottom(); } } catch (err) { // Ignorar erros de JSON incompletos do buffer } } } } // Remover o cursor de digitação no final se não for mensagem de mídia const cursor = botContentDiv.querySelector('.typing-cursor'); if (cursor) cursor.remove(); if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } // Remover blocos nativos de modelos de reasoning antes de renderizar e extrair conhecimento assistantContent = assistantContent.replace(/[\s\S]*?(?:<\/think>|$)/g, '').trim(); // Parse final limpo do markdown (se for texto normal) if (!isMediaResponse) { const stepTextEl = botContentDiv.querySelector('#iaProgressStepText'); const barFillEl = botContentDiv.querySelector('#iaProgressBarFill'); if (stepTextEl && barFillEl) { stepTextEl.textContent = "Concluído com sucesso!"; barFillEl.style.width = "100%"; } // Aguardar um instante para o feedback visual de conclusão ser percebido await new Promise(resolve => setTimeout(resolve, 400)); botContentDiv.innerHTML = marked.parse(assistantContent); botContentDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); } // Extrair e salvar conhecimento aprendido (tags [KNOWLEDGE: ...]) window.extractAndSaveKnowledge(assistantContent); // Adicionar botões de ação (Copiar, Regenerar, Ouvir) após o streaming const escapedContent = escapeHtml(assistantContent).replace(/"/g, '"'); const actionsDiv = document.createElement('div'); actionsDiv.className = 'message-actions'; actionsDiv.innerHTML = ` `; const wrapper = botRow.querySelector('.message-content-wrapper'); wrapper.appendChild(actionsDiv); // Vincular listeners dos botões const copyBtn = actionsDiv.querySelector('.btn-copy-msg'); copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(assistantContent).then(() => { const span = copyBtn.querySelector('span'); const original = span.textContent; span.textContent = 'Copiado!'; setTimeout(() => span.textContent = original, 2000); }); }); const regenBtn = actionsDiv.querySelector('.btn-regenerate'); regenBtn.addEventListener('click', () => { if (!isGenerating) regenerateLastResponse(); }); const speakBtn = actionsDiv.querySelector('.btn-speak'); speakBtn.addEventListener('click', () => { speakText(assistantContent, speakBtn); }); scrollToBottom(); // AUTO-TTS: Se a mensagem veio do microfone, ler a resposta em voz alta automaticamente if (lastInputWasVoice && assistantContent && !isMediaResponse) { console.log('[VOZ] Mensagem veio do microfone — respondendo por voz automaticamente.'); setTimeout(() => { speakText(assistantContent, speakBtn); }, 400); } lastInputWasVoice = false; // Salvar resposta no estado e localStorage const assistantMsg = { role: 'assistant', content: assistantContent }; chat.messages.push(assistantMsg); chat.updatedAt = Date.now(); saveChats(); renderHistory(); } catch (error) { console.error('Erro na resposta do chat:', error); if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } const cursor = botContentDiv.querySelector('.typing-cursor'); if (cursor) cursor.remove(); botContentDiv.innerHTML = `

Desculpe, ocorreu um erro ao obter resposta da PedaGog. Por favor, tente novamente.

`; scrollToBottom(); } finally { isGenerating = false; btnSend.disabled = userInput.value.trim() === ''; userInput.disabled = false; userInput.focus(); } }; // Envio pelo Formulário chatForm.addEventListener('submit', (e) => { e.preventDefault(); const text = userInput.value.trim(); handleSendMessage(text); }); // Botões de Modos Rápidos de Prompt (Rodapé) document.querySelectorAll('.btn-prompt-mode').forEach(btn => { btn.addEventListener('click', () => { if (window.innerWidth <= 768) { if (promptModesBar) promptModesBar.classList.remove('show'); if (fabTools) fabTools.classList.remove('active'); } if (btn.dataset.prompt) { handleSendMessage(btn.dataset.prompt); return; } if (btn.dataset.fill) { userInput.value = btn.dataset.fill; userInput.focus(); btnSend.disabled = false; return; } if (btn.id === 'barBtnEstudio') return; // Manipulado separadamente if (btn.id === 'barBtnComics') return; // Manipulado separadamente if (btn.id === 'barBtnMusica') return; if (btn.id === 'barBtnHistoria') return; if (btn.id === 'barBtnMindLab') return; const mode = btn.dataset.mode; const text = userInput.value.trim(); let suggestion = ""; let baseText = text; // Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar const suggestionsList = [ "Crie uma música pedagógica alegre e animada sobre ", "Crie um roteiro detalhado para um vídeo educativo sobre ", "Crie uma ilustração pedagógica com cores vibrantes mostrando ", "Escreva uma poesia infantil rimada e educativa sobre ", "Escreva uma história educativa infantil curta sobre " ]; for (const sugg of suggestionsList) { if (baseText.startsWith(sugg)) { baseText = baseText.replace(sugg, "").trim(); break; } } if (mode === 'AUDIO') { suggestion = "Crie uma música pedagógica alegre e animada sobre "; } else if (mode === 'VIDEO') { suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre "; } else if (mode === 'IMAGE') { suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando "; } else if (mode === 'POETRY') { suggestion = "Escreva uma poesia infantil rimada e educativa sobre "; } else if (mode === 'TEXT') { suggestion = "Escreva uma história educativa infantil curta sobre "; } userInput.value = suggestion + baseText; userInput.focus(); // Mover cursor para o final do texto userInput.selectionStart = userInput.selectionEnd = userInput.value.length; // Ajustar altura do textarea para acomodar a sugestão userInput.style.height = 'auto'; userInput.style.height = userInput.scrollHeight + 'px'; if (btnSend) btnSend.disabled = false; }); }); // Sugestões de Prompt Rápidas suggestionCards.forEach(card => { card.addEventListener('click', () => { const prompt = card.dataset.prompt; if (prompt) { handleSendMessage(prompt); } }); }); // ========================================================================== // LOGOUT & INICIALIZAÇÃO // ========================================================================== btnLogout.addEventListener('click', async () => { try { const response = await fetch('/api/logout', { method: 'POST' }); if (response.ok) { window.location.href = '/login'; } } catch (error) { console.error('Erro ao fazer logout:', error); } }); // Inicializar interface if (currentChatId) { // Tenta carregar o chat ativo const activeChat = chats.find(c => c.id === currentChatId); if (activeChat) { selectChat(currentChatId); } else { createNewChat(); } } else { createNewChat(); } // Inicializar reconhecimento de voz initVoiceRecognition(); // ============================================================ // SISTEMA DE OBSERVAÇÃO POR VOZ // ============================================================ const recordModal = document.getElementById('recordModal'); const fabRecord = document.getElementById('fabRecord'); const btnExitRecord = document.getElementById('btnExitRecord'); const btnStopRecord = document.getElementById('btnStopRecord'); const recordStateRecording = document.getElementById('recordStateRecording'); const recordStateProcessing = document.getElementById('recordStateProcessing'); const recordStatePreview = document.getElementById('recordStatePreview'); const recordTimer = document.getElementById('recordTimer'); const reportContent = document.getElementById('reportContent'); const previewDateTime = document.getElementById('previewDateTime'); const btnDiscardReport = document.getElementById('btnDiscardReport'); const btnSaveReport = document.getElementById('btnSaveReport'); // Novos elementos de transcrição const recordStateTranscribed = document.getElementById('recordStateTranscribed'); const transcriptionTextarea = document.getElementById('transcriptionTextarea'); const btnDiscardTranscription = document.getElementById('btnDiscardTranscription'); const btnAnalyzeTranscription = document.getElementById('btnAnalyzeTranscription'); const recordProcessingText = document.getElementById('recordProcessingText'); const recordProcessingSubtext = document.getElementById('recordProcessingSubtext'); // Variáveis de gravação let mediaRecorder = null; let audioChunks = []; let recordingStartTime = null; let timerInterval = null; let audioContext = null; let analyser = null; let frequencyData = null; let silenceFrames = 0; let totalFrames = 0; let vadCheckInterval = null; let generatedReport = null; let generatedCriancas = null; let generatedTurma = 'não informada'; let generatedTags = []; // Fechar modal - colocada no topo para evitar problemas de inicialização function closeRecordModal() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { try { mediaRecorder.stop(); } catch (e) {} if (mediaRecorder.stream) { mediaRecorder.stream.getTracks().forEach(t => t.stop()); } } clearInterval(timerInterval); clearInterval(vadCheckInterval); if (audioContext) { audioContext.close(); audioContext = null; } if (analyser) { analyser = null; frequencyData = null; } audioChunks = []; generatedReport = null; recordModal.style.display = 'none'; } // Função para controlar a exibição dos estados function showRecordState(state) { recordStateRecording.style.display = 'none'; recordStateProcessing.style.display = 'none'; if (recordStateTranscribed) recordStateTranscribed.style.display = 'none'; recordStatePreview.style.display = 'none'; if (state === 'recording') { recordStateRecording.style.display = 'flex'; } else if (state === 'processing') { recordStateProcessing.style.display = 'flex'; } else if (state === 'transcribed') { if (recordStateTranscribed) recordStateTranscribed.style.display = 'flex'; } else if (state === 'preview') { recordStatePreview.style.display = 'flex'; } } // Abrir modal e iniciar gravação if (fabRecord) { fabRecord.addEventListener('click', async () => { audioChunks = []; generatedReport = null; // Mostra estado inicial (gravando) showRecordState('recording'); recordModal.style.display = 'flex'; // Inicia timer recordingStartTime = Date.now(); timerInterval = setInterval(updateTimer, 1000); updateTimer(); // Solicita permissão e inicia gravação try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); let options = {}; if (typeof MediaRecorder.isTypeSupported === 'function') { if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) { options = { mimeType: 'audio/webm;codecs=opus' }; } else if (MediaRecorder.isTypeSupported('audio/webm')) { options = { mimeType: 'audio/webm' }; } else if (MediaRecorder.isTypeSupported('audio/ogg')) { options = { mimeType: 'audio/ogg' }; } else if (MediaRecorder.isTypeSupported('audio/mp4')) { options = { mimeType: 'audio/mp4' }; } } // Configurar AudioContext para VAD audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); analyser = audioContext.createAnalyser(); analyser.fftSize = 256; source.connect(analyser); frequencyData = new Uint8Array(analyser.frequencyBinCount); // Iniciar verificação de silêncio (VAD) silenceFrames = 0; totalFrames = 0; vadCheckInterval = setInterval(() => { if (!analyser) return; analyser.getByteFrequencyData(frequencyData); const avg = frequencyData.reduce((a, b) => a + b, 0) / frequencyData.length; totalFrames++; if (avg < 10) { silenceFrames++; } else { silenceFrames = Math.max(0, silenceFrames - 2); // recupera mais rápido } // Atualizar indicator visual const recordWaveform = document.getElementById('recordWaveform'); if (recordWaveform && recordWaveform.children[0]) { const bars = recordWaveform.children; bars.forEach((bar) => { const h = Math.max(4, (avg / 255) * 50 + Math.random() * 10); bar.style.height = `${h}px`; }); } // Se mais de 60% dos últimos 30 frames for silêncio, avisar if (totalFrames > 30 && (silenceFrames / 30) > 0.6) { const btnStopRecord = document.getElementById('btnStopRecord'); if (btnStopRecord && !btnStopRecord.disabled) { btnStopRecord.style.borderColor = '#ff6b6b'; btnStopRecord.title = 'Silêncio detectado - Clique se quiser parar'; } } else { const btnStopRecord = document.getElementById('btnStopRecord'); if (btnStopRecord) { btnStopRecord.style.borderColor = ''; btnStopRecord.title = ''; } } }, 100); mediaRecorder = new MediaRecorder(stream, options); mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); }; mediaRecorder.start(100); // chunk a cada 100ms } catch (err) { console.error('Erro ao acessar microfone:', err); alert('Não foi possível acessar o microfone. Verifique as permissões.'); closeRecordModal(); } }); } // Atualizar timer function updateTimer() { if (!recordingStartTime) return; const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000); const mins = Math.floor(elapsed / 60).toString().padStart(2, '0'); const secs = (elapsed % 60).toString().padStart(2, '0'); recordTimer.textContent = `${mins}:${secs}`; } // Parar gravação e transcrever if (btnStopRecord) { btnStopRecord.addEventListener('click', async () => { if (mediaRecorder && mediaRecorder.state !== 'inactive') { try { mediaRecorder.stop(); } catch (e) {} if (mediaRecorder.stream) { mediaRecorder.stream.getTracks().forEach(t => t.stop()); } } clearInterval(timerInterval); clearInterval(vadCheckInterval); if (audioContext) { audioContext.close(); audioContext = null; } if (analyser) { analyser = null; frequencyData = null; } // Mostra processando com texto de transcrição if (recordProcessingText) recordProcessingText.textContent = 'Transcrevendo áudio...'; if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Aguarde a transcrição por Whisper'; showRecordState('processing'); // Envia áudio para servidor const blob = new Blob(audioChunks, { type: 'audio/webm' }); const formData = new FormData(); formData.append('audio', blob, 'recording.webm'); try { const res = await fetch('/api/observacao/transcrever', { method: 'POST', body: formData }); if (!res.ok) { const errData = await res.json(); throw new Error(errData.error || 'Erro na transcrição'); } const data = await res.json(); // Exibe o texto transcrito na área de texto para edição if (transcriptionTextarea) { transcriptionTextarea.value = data.transcribedText || ''; } showRecordState('transcribed'); } catch (err) { console.error('Erro ao processar observação:', err); alert('Erro ao transcrever áudio: ' + err.message); closeRecordModal(); } }); } // Ação de analisar transcrição if (btnAnalyzeTranscription) { btnAnalyzeTranscription.addEventListener('click', async () => { const text = transcriptionTextarea ? transcriptionTextarea.value.trim() : ''; if (!text || text.length < 5) { alert('Por favor, digite ou revise a transcrição (mínimo de 5 caracteres).'); return; } // Mostra processando com texto de análise if (recordProcessingText) recordProcessingText.textContent = 'Analisando observação...'; if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Gerando relatório pedagógico estruturado'; showRecordState('processing'); try { const turmaData = localStorage.getItem('pedagog_turma') || localStorage.getItem('camila_turma') || '[]'; const res = await fetch('/api/observacao/analisar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transcribedText: text, turmaContext: JSON.parse(turmaData) }) }); if (!res.ok) { const errData = await res.json(); throw new Error(errData.error || 'Erro na análise'); } const data = await res.json(); generatedReport = data.relatorio || data.report; generatedCriancas = data.criancas; generatedTurma = data.turma || 'não informada'; generatedTags = data.tags || []; showReportPreview(data); } catch (err) { console.error('Erro ao analisar observação:', err); alert('Erro ao analisar observação: ' + err.message); showRecordState('transcribed'); } }); } // Ação de descartar transcrição if (btnDiscardTranscription) { btnDiscardTranscription.addEventListener('click', () => { closeRecordModal(); }); } // Mostrar prévia do relatório function showReportPreview(data) { showRecordState('preview'); const now = new Date(); previewDateTime.textContent = now.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); // Monta HTML com metadados const criancasHtml = data.criancas && data.criancas.length ? `
👧 Crianças: ${data.criancas.join(', ')}
` : `
👧 Crianças: não identificadas
`; const tagsHtml = data.tags && data.tags.length ? `
🏷️ Tags: ${data.tags.map(t => `${t}`).join(' ')}
` : ''; const turmaHtml = data.turma && data.turma !== 'não informada' ? `
🏫 Turma: ${data.turma}
` : ''; reportContent.innerHTML = `
${criancasHtml}${turmaHtml}${tagsHtml}

${marked.parse(generatedReport || 'Relatório não disponível.')} `; reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b)); } // Descartar relatório if (btnDiscardReport) { btnDiscardReport.addEventListener('click', () => { generatedReport = null; closeRecordModal(); }); } // Salvar relatório if (btnSaveReport) { btnSaveReport.addEventListener('click', async () => { if (!generatedReport) return; btnSaveReport.disabled = true; btnSaveReport.textContent = 'Salvando...'; try { const res = await fetch('/api/observacao/salvar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ report: generatedReport, criancas: generatedCriancas, turma: generatedTurma, tags: generatedTags }) }); const data = await res.json(); if (data.success) { alert('Relatório salvo com sucesso!'); } else { alert('Erro ao salvar: ' + (data.error || 'desconhecido')); } } catch (err) { console.error('Erro ao salvar:', err); alert('Erro ao salvar o relatório.'); } finally { btnSaveReport.disabled = false; btnSaveReport.textContent = 'Salvar'; closeRecordModal(); } }); } // Sair / descartar if (btnExitRecord) { btnExitRecord.addEventListener('click', closeRecordModal); } // ============================================================ // MODAL: MINHAS OBSERVAÇÕES // ============================================================ const obsModal = document.getElementById('obsModal'); const btnObservacoes = document.getElementById('btnObservacoes'); const btnObsEspeciais = document.getElementById('btnObsEspeciais'); const btnCloseObsModal = document.getElementById('btnCloseObsModal'); const obsModalTitle = document.getElementById('obsModalTitle'); const obsList = document.getElementById('obsList'); const obsFilterMes = document.getElementById('obsFilterMes'); const obsFilterTag = document.getElementById('obsFilterTag'); const obsSearchCrianca = document.getElementById('obsSearchCrianca'); let allObservations = []; let isSpecialObsMode = false; // Abrir modal de observações if (btnObservacoes) { btnObservacoes.addEventListener('click', async () => { isSpecialObsMode = false; if (obsModalTitle) obsModalTitle.textContent = '📒 Minhas Observações'; obsModal.style.display = 'flex'; obsList.innerHTML = '
Carregando...
'; try { const res = await fetch('/api/observacoes'); const data = await res.json(); allObservations = data.observations || []; populateMonthFilter(); renderObsList(); } catch (err) { console.error('Erro ao carregar observações:', err); obsList.innerHTML = '
Erro ao carregar observações.
'; } }); } // Abrir modal de observações Especiais if (btnObsEspeciais) { btnObsEspeciais.addEventListener('click', async () => { isSpecialObsMode = true; if (obsModalTitle) obsModalTitle.textContent = '📒 Observações Educação Especial'; obsModal.style.display = 'flex'; obsList.innerHTML = '
Carregando...
'; try { const res = await fetch('/api/observacoes'); const data = await res.json(); allObservations = data.observations || []; populateMonthFilter(); renderObsList(); } catch (err) { console.error('Erro ao carregar observações:', err); obsList.innerHTML = '
Erro ao carregar observações.
'; } }); } // Fechar modal if (btnCloseObsModal) { btnCloseObsModal.addEventListener('click', () => { obsModal.style.display = 'none'; }); } // Filtros if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList); if (obsFilterTag) obsFilterTag.addEventListener('change', renderObsList); if (obsSearchCrianca) obsSearchCrianca.addEventListener('input', renderObsList); // Preenche select de meses function populateMonthFilter() { if (!obsFilterMes) return; const months = [...new Set(allObservations.map(o => o.date?.slice(0, 7)))] .filter(Boolean).sort().reverse(); obsFilterMes.innerHTML = '' + months.map(m => ``).join(''); } // Renderiza lista filtrada function renderObsList() { if (!obsList) return; const mes = obsFilterMes?.value || ''; const tag = obsFilterTag?.value || ''; const search = obsSearchCrianca?.value.toLowerCase() || ''; let especialKidsNames = []; if (isSpecialObsMode) { const turma = JSON.parse(localStorage.getItem('pedagog_turma') || localStorage.getItem('camila_turma') || '[]'); especialKidsNames = turma.filter(c => c.especial).map(c => (c.nome || '').toLowerCase().trim()).filter(Boolean); if (especialKidsNames.length === 0) { obsList.innerHTML = '
Nenhuma criança com "Educação Especial" cadastrada na Minha Turma.
'; return; } } const filtered = allObservations.filter(o => { if (mes && !o.date?.startsWith(mes)) return false; if (tag && !o.tags?.includes(tag)) return false; const nomes = (o.criancas || []).join(' ').toLowerCase(); if (isSpecialObsMode) { // Must contain at least one special kid's name const preview = (o.preview || '').toLowerCase(); const isEspecial = especialKidsNames.some(ekn => nomes.includes(ekn) || preview.includes(ekn)); if (!isEspecial) return false; } if (search) { if (!nomes.includes(search) && !(o.preview || '').toLowerCase().includes(search)) return false; } return true; }); if (filtered.length === 0) { obsList.innerHTML = '
Nenhuma observação encontrada.
'; return; } obsList.innerHTML = filtered.map(o => { const tagsHtml = (o.tags || []).map(t => `${t}`).join(''); const criancasHtml = (o.criancas || []).map(c => `${c}`).join(''); const dateFormatted = o.date ? o.date.split('-').reverse().join('/') : ''; return `
📋 ${dateFormatted}${o.time ? ' às ' + o.time : ''} ${o.turma && o.turma !== 'não informada' ? `🏫 ${o.turma}` : ''}
${o.preview || 'Sem prévia disponível.'}
`; }).join(''); // Click para abrir observação obsList.querySelectorAll('.obs-item').forEach(item => { item.addEventListener('click', async () => { const ym = item.dataset.yearmonth; const fn = item.dataset.filename; if (!ym || !fn) return; try { const res = await fetch(`/api/observacoes/${ym}/${fn}`); const text = await res.text(); showObsDetail(text); } catch (err) { console.error('Erro ao carregar observação:', err); } }); }); }; // ============================================================ // MODAL: MINHA TURMA (COM MÚLTIPLAS TURMAS E SUPABASE) // ============================================================ const minhaTurmaModal = document.getElementById('minhaTurmaModal'); const btnMinhaTurma = document.getElementById('btnMinhaTurma'); const btnCloseMinhaTurmaModal = document.getElementById('btnCloseMinhaTurmaModal'); // Turmas List & Form const turmasList = document.getElementById('turmasList'); const btnCreateTurma = document.getElementById('btnCreateTurma'); const btnCancelTurma = document.getElementById('btnCancelTurma'); const btnSaveTurmaConfig = document.getElementById('btnSaveTurmaConfig'); const configTurmaId = document.getElementById('configTurmaId'); const configTurmaNome = document.getElementById('configTurmaNome'); const configTurmaSala = document.getElementById('configTurmaSala'); const configTurmaPeriodo = document.getElementById('configTurmaPeriodo'); const configTurmaProfTitular = document.getElementById('configTurmaProfTitular'); const configTurmaProfAux1 = document.getElementById('configTurmaProfAux1'); const configTurmaProfAux2 = document.getElementById('configTurmaProfAux2'); const configTurmaCuidadora = document.getElementById('configTurmaCuidadora'); const turmaFormTitle = document.getElementById('turmaFormTitle'); // Alunos List & Form const filterAlunosTurma = document.getElementById('filterAlunosTurma'); const childrenList = document.getElementById('childrenList'); const btnCreateChild = document.getElementById('btnCreateChild'); const btnCancelChild = document.getElementById('btnCancelChild'); const btnSaveChild = document.getElementById('btnSaveChild'); const childFormTitle = document.getElementById('childFormTitle'); const cFormId = document.getElementById('childFormId'); const cFormTurma = document.getElementById('childFormTurma'); const cFormNome = document.getElementById('childFormNome'); const cFormApelido = document.getElementById('childFormApelido'); const cFormDataNasc = document.getElementById('childFormDataNasc'); const cFormEspecial = document.getElementById('childFormEspecial'); const divEspecialDetalhes = document.getElementById('divEspecialDetalhes'); const cFormEspecialDetalhes = document.getElementById('childFormEspecialDetalhes'); const cFormPais = document.getElementById('childFormPais'); const cFormAutorizados = document.getElementById('childFormAutorizados'); // Tabs const tabTurmaConfig = document.getElementById('tabTurmaConfig'); const tabTurmaAlunos = document.getElementById('tabTurmaAlunos'); const turmaConfigSection = document.getElementById('turmaConfigSection'); const turmaAlunosSection = document.getElementById('turmaAlunosSection'); let currentTurmas = []; let currentAlunos = []; // API Calls const fetchTurmas = async () => { try { const res = await fetch('/api/turmas'); currentTurmas = await res.json(); populateTurmaSelects(); return currentTurmas; } catch(e) { console.error(e); return []; } }; const fetchAlunos = async () => { try { const res = await fetch('/api/alunos'); currentAlunos = await res.json(); localStorage.setItem('pedagog_turma', JSON.stringify(currentAlunos)); return currentAlunos; } catch(e) { console.error(e); return []; } }; const populateTurmaSelects = () => { const opts = '' + currentTurmas.map(t => ``).join(''); if(filterAlunosTurma) filterAlunosTurma.innerHTML = opts; const formOpts = '' + currentTurmas.map(t => ``).join(''); if(cFormTurma) cFormTurma.innerHTML = formOpts; if(document.getElementById('emitirFilterTurma')) { document.getElementById('emitirFilterTurma').innerHTML = '' + currentTurmas.map(t => ``).join(''); } }; // Tabs Logic if (tabTurmaConfig) { tabTurmaConfig.addEventListener('click', () => { tabTurmaConfig.classList.add('active'); tabTurmaConfig.style.borderBottomColor = 'var(--brand-green)'; tabTurmaConfig.style.color = 'var(--brand-green)'; tabTurmaAlunos.classList.remove('active'); tabTurmaAlunos.style.borderBottomColor = 'transparent'; tabTurmaAlunos.style.color = 'var(--text-secondary)'; turmaConfigSection.style.display = 'flex'; turmaAlunosSection.style.display = 'none'; }); } if (tabTurmaAlunos) { tabTurmaAlunos.addEventListener('click', () => { tabTurmaAlunos.classList.add('active'); tabTurmaAlunos.style.borderBottomColor = 'var(--brand-green)'; tabTurmaAlunos.style.color = 'var(--brand-green)'; tabTurmaConfig.classList.remove('active'); tabTurmaConfig.style.borderBottomColor = 'transparent'; tabTurmaConfig.style.color = 'var(--text-secondary)'; turmaAlunosSection.style.display = 'flex'; turmaConfigSection.style.display = 'none'; }); } // Turmas Logic const clearTurmaForm = () => { configTurmaId.value = ''; configTurmaNome.value = ''; configTurmaSala.value = ''; configTurmaPeriodo.value = 'Integral'; configTurmaProfTitular.value = ''; configTurmaProfAux1.value = ''; configTurmaProfAux2.value = ''; configTurmaCuidadora.value = ''; turmaFormTitle.textContent = 'Cadastrar Turma'; }; const renderTurmasList = () => { if(!turmasList) return; if(currentTurmas.length === 0) { turmasList.innerHTML = '
Nenhuma turma cadastrada.
'; return; } turmasList.innerHTML = currentTurmas.map(t => `
${escapeHtml(t.nome)}
${t.periodo || ''} ${t.sala ? '- ' + t.sala : ''}
`).join(''); }; window.editTurma = (id) => { const t = currentTurmas.find(x => x.id === id); if(!t) return; configTurmaId.value = t.id; configTurmaNome.value = t.nome || ''; configTurmaSala.value = t.sala || ''; configTurmaPeriodo.value = t.periodo || 'Integral'; configTurmaProfTitular.value = t.prof_titular || ''; configTurmaProfAux1.value = t.prof_aux1 || ''; configTurmaProfAux2.value = t.prof_aux2 || ''; configTurmaCuidadora.value = t.cuidadora || ''; turmaFormTitle.textContent = 'Editar Turma'; }; window.deleteTurma = async (id) => { const isOk = await showCustomConfirm('Excluir Turma', 'Deseja excluir esta turma? Todos os alunos associados a ela também serão excluídos.', true); if(isOk) { await fetch('/api/turmas/' + id, { method: 'DELETE' }); await fetchTurmas(); await fetchAlunos(); renderTurmasList(); renderAlunosList(); clearTurmaForm(); } }; if(btnCreateTurma) btnCreateTurma.addEventListener('click', clearTurmaForm); if(btnCancelTurma) btnCancelTurma.addEventListener('click', clearTurmaForm); if (btnSaveTurmaConfig) { btnSaveTurmaConfig.addEventListener('click', async () => { const nome = configTurmaNome.value.trim(); if(!nome) { showCustomAlert('Aviso', 'O Nome da Turma é obrigatório.'); return; } const body = { nome, sala: configTurmaSala.value, periodo: configTurmaPeriodo.value, profTitular: configTurmaProfTitular.value, profAux1: configTurmaProfAux1.value, profAux2: configTurmaProfAux2.value, cuidadora: configTurmaCuidadora.value }; const id = configTurmaId.value; const url = id ? '/api/turmas/' + id : '/api/turmas'; const method = id ? 'PUT' : 'POST'; try { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if(res.ok) { showCustomAlert('Sucesso', 'Turma salva com sucesso!'); await fetchTurmas(); renderTurmasList(); clearTurmaForm(); } else { showCustomAlert('Erro', 'Falha ao salvar turma.'); } } catch(e) { console.error(e); } }); } // Alunos Logic const clearChildForm = () => { cFormId.value = ''; cFormTurma.value = filterAlunosTurma ? filterAlunosTurma.value : ''; cFormNome.value = ''; cFormApelido.value = ''; cFormDataNasc.value = ''; cFormEspecial.checked = false; cFormEspecialDetalhes.value = ''; divEspecialDetalhes.style.display = 'none'; cFormPais.value = ''; cFormAutorizados.value = ''; childFormTitle.textContent = 'Cadastrar Criança'; }; if (cFormEspecial) { cFormEspecial.addEventListener('change', () => { divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none'; }); } const renderAlunosList = () => { if (!childrenList) return; const filterTurmaId = filterAlunosTurma ? filterAlunosTurma.value : ''; let filtered = currentAlunos; if(filterTurmaId) { filtered = currentAlunos.filter(a => a.turma_id === filterTurmaId); } if (filtered.length === 0) { childrenList.innerHTML = '
Nenhuma criança encontrada.
'; return; } filtered.sort((a, b) => (a.nome || '').localeCompare(b.nome || '')); childrenList.innerHTML = filtered.map(c => { const turmaObj = currentTurmas.find(t => t.id === c.turma_id); const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma'; return `
${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}
${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}
`}).join(''); }; if(filterAlunosTurma) { filterAlunosTurma.addEventListener('change', () => { renderAlunosList(); if(cFormTurma) cFormTurma.value = filterAlunosTurma.value; }); } window.editChild = (id) => { const c = currentAlunos.find(x => x.id === id); if (!c) return; cFormId.value = c.id; cFormTurma.value = c.turma_id || ''; cFormNome.value = c.nome || ''; cFormApelido.value = c.apelido || ''; if(c.data_nasc) { cFormDataNasc.value = c.data_nasc.split('T')[0]; } else { cFormDataNasc.value = ''; } cFormEspecial.checked = !!c.especial; cFormEspecialDetalhes.value = c.especial_detalhes || ''; divEspecialDetalhes.style.display = c.especial ? 'flex' : 'none'; cFormPais.value = c.pais || ''; cFormAutorizados.value = c.autorizados || ''; childFormTitle.textContent = 'Editar Criança'; }; window.deleteChild = async (id) => { const isOk = await showCustomConfirm('Excluir', 'Deseja excluir esta criança? Isso não removerá as observações já feitas.', true); if (isOk) { await fetch('/api/alunos/' + id, { method: 'DELETE' }); await fetchAlunos(); renderAlunosList(); clearChildForm(); } }; if (btnMinhaTurma) { btnMinhaTurma.addEventListener('click', async () => { minhaTurmaModal.style.display = 'flex'; await fetchTurmas(); await fetchAlunos(); renderTurmasList(); renderAlunosList(); clearTurmaForm(); clearChildForm(); }); } if (btnCloseMinhaTurmaModal) btnCloseMinhaTurmaModal.addEventListener('click', () => minhaTurmaModal.style.display = 'none'); if (btnCreateChild) btnCreateChild.addEventListener('click', clearChildForm); if (btnCancelChild) btnCancelChild.addEventListener('click', clearChildForm); if (btnSaveChild) { btnSaveChild.addEventListener('click', async () => { if (!cFormNome.value.trim() || !cFormTurma.value) { showCustomAlert('Aviso', 'O Nome da Criança e a Turma são obrigatórios.'); return; } const id = cFormId.value; const body = { turma_id: cFormTurma.value, nome: cFormNome.value.trim(), apelido: cFormApelido.value.trim(), data_nasc: cFormDataNasc.value || null, especial: cFormEspecial.checked, especial_detalhes: cFormEspecialDetalhes.value.trim(), pais: cFormPais.value.trim(), autorizados: cFormAutorizados.value.trim() }; const url = id ? '/api/alunos/' + id : '/api/alunos'; const method = id ? 'PUT' : 'POST'; try { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if(res.ok) { await fetchAlunos(); renderAlunosList(); clearChildForm(); showCustomAlert('Sucesso', 'Criança salva com sucesso!'); } else { showCustomAlert('Erro', 'Falha ao salvar criança.'); } } catch(e) { console.error(e); } }); } // Preencher com Voz const btnVoiceRecordChild = document.getElementById('btnVoiceRecordChild'); if (btnVoiceRecordChild && (window.SpeechRecognition || window.webkitSpeechRecognition)) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const formRecognition = new SpeechRecognition(); formRecognition.lang = 'pt-BR'; formRecognition.continuous = false; formRecognition.interimResults = false; formRecognition.onstart = () => { btnVoiceRecordChild.innerHTML = '🛑 Gravando... (Clique para parar)'; btnVoiceRecordChild.style.background = 'rgba(239, 68, 68, 0.1)'; btnVoiceRecordChild.style.color = '#ef4444'; btnVoiceRecordChild.style.borderColor = '#ef4444'; }; formRecognition.onresult = async (event) => { const transcript = event.results[0][0].transcript; btnVoiceRecordChild.innerHTML = '⏳ Processando com IA...'; try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: `A professora ditou as seguintes informações para cadastrar uma criança na turma: "${transcript}". Por favor, extraia os dados e retorne APENAS um JSON válido com os seguintes campos: "nome", "apelido", "data_nasc" (formato YYYY-MM-DD), "especial" (boolean), "especial_detalhes", "pais", "autorizados". Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }], temperature: 0.1 }) }); if (response.ok) { const data = await response.json(); const assistantReply = data.message; const match = assistantReply.match(/```json\n([\s\S]*?)\n```/) || assistantReply.match(/\{[\s\S]*\}/); if (match) { const parsed = JSON.parse(match[1] || match[0]); if (parsed.nome) cFormNome.value = parsed.nome; if (parsed.apelido) cFormApelido.value = parsed.apelido; if (parsed.data_nasc) cFormDataNasc.value = parsed.data_nasc; if (parsed.especial) cFormEspecial.checked = parsed.especial; divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none'; if (parsed.especial_detalhes) cFormEspecialDetalhes.value = parsed.especial_detalhes; if (parsed.pais) cFormPais.value = parsed.pais; if (parsed.autorizados) cFormAutorizados.value = parsed.autorizados; } } } catch (err) { console.error('Erro ao processar voz:', err); showCustomAlert('Erro', 'Não foi possível extrair os dados. Tente novamente ou preencha manualmente.'); } }; formRecognition.onend = () => { btnVoiceRecordChild.innerHTML = '🎤 Preencher com Voz'; btnVoiceRecordChild.style.background = 'rgba(16, 163, 127, 0.1)'; btnVoiceRecordChild.style.color = 'var(--brand-green)'; btnVoiceRecordChild.style.borderColor = 'var(--brand-green)'; }; btnVoiceRecordChild.addEventListener('click', () => { if (btnVoiceRecordChild.innerHTML.includes('Gravando')) { formRecognition.stop(); } else { formRecognition.start(); } }); } // Mostra detalhe da observação em modal const showObsDetail = (mdText) => { const modal = document.createElement('div'); modal.className = 'settings-modal'; modal.style.display = 'flex'; modal.innerHTML = `

📋 Observação

${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
`; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); }; // // ============================================================ // CRUD DE MODELOS DE RELATÓRIO // ============================================================ const modelosRelatorioModal = document.getElementById('modelosRelatorioModal'); const btnModelosRelatorio = document.getElementById('btnModelosRelatorio'); const btnCloseModelosModal = document.getElementById('btnCloseModelosModal'); const templatesList = document.getElementById('templatesList'); const btnCreateTemplate = document.getElementById('btnCreateTemplate'); const btnCancelTemplate = document.getElementById('btnCancelTemplate'); const btnSaveTemplate = document.getElementById('btnSaveTemplate'); const templateFormId = document.getElementById('templateFormId'); const templateFormNome = document.getElementById('templateFormNome'); const templateFormFinalidade = document.getElementById('templateFormFinalidade'); const templateFormPeriodicidade = document.getElementById('templateFormPeriodicidade'); const templateFormEstrutura = document.getElementById('templateFormEstrutura'); const templateFormTitle = document.getElementById('templateFormTitle'); let allTemplates = []; // Abrir modal de modelos if (btnModelosRelatorio) { btnModelosRelatorio.addEventListener('click', async () => { modelosRelatorioModal.style.display = 'flex'; clearTemplateForm(); await fetchTemplates(); }); } // Fechar modal de modelos if (btnCloseModelosModal) { btnCloseModelosModal.addEventListener('click', () => { modelosRelatorioModal.style.display = 'none'; }); } // Buscar modelos do backend async function fetchTemplates() { templatesList.innerHTML = '
Carregando...
'; try { const res = await fetch('/api/modelos'); allTemplates = await res.json(); renderTemplatesList(); } catch (err) { console.error('Erro ao buscar modelos:', err); templatesList.innerHTML = '
Erro ao carregar modelos.
'; } } // Renderizar a lista de modelos na esquerda function renderTemplatesList() { if (allTemplates.length === 0) { templatesList.innerHTML = '
Nenhum modelo cadastrado.
'; return; } templatesList.innerHTML = allTemplates.map(t => `
${t.nome}
${t.periodicidade} • ${t.finalidade || 'Sem finalidade'}
`).join(''); // Eventos de clique na lista para editar/deletar/duplicar templatesList.querySelectorAll('.obs-item').forEach(item => { item.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON') return; const id = item.dataset.id; const template = allTemplates.find(t => t.id == id); if (template) fillTemplateForm(template); }); }); templatesList.querySelectorAll('.template-duplicate-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const id = btn.dataset.id; const template = allTemplates.find(t => t.id == id); if (template) { fillTemplateForm(template); templateFormId.value = ''; templateFormNome.value = template.nome + ' (Cópia)'; templateFormTitle.textContent = 'Duplicar Modelo'; if (templateFormNome) templateFormNome.focus(); } }); }); templatesList.querySelectorAll('.template-edit-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const id = btn.dataset.id; const template = allTemplates.find(t => t.id == id); if (template) fillTemplateForm(template); }); }); templatesList.querySelectorAll('.template-delete-btn').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const id = btn.dataset.id; const template = allTemplates.find(t => t.id == id); if (!template) return; if (confirm(`Tem certeza que deseja excluir o modelo "${template.nome}"?`)) { try { const res = await fetch(`/api/modelos/${id}`, { method: 'DELETE' }); if (res.ok) { alert('Modelo excluído com sucesso!'); clearTemplateForm(); await fetchTemplates(); } else { const err = await res.json(); alert('Erro ao excluir: ' + err.error); } } catch (err) { console.error('Erro ao excluir modelo:', err); } } }); }); } // Preencher formulário de edição function fillTemplateForm(template) { templateFormId.value = template.id; templateFormNome.value = template.nome; templateFormFinalidade.value = template.finalidade || ''; templateFormPeriodicidade.value = template.periodicidade || 'Semanal'; templateFormEstrutura.value = template.estrutura || ''; templateFormTitle.textContent = 'Editar Modelo'; } // Limpar formulário function clearTemplateForm() { templateFormId.value = ''; templateFormNome.value = ''; templateFormFinalidade.value = ''; templateFormPeriodicidade.value = 'Semanal'; templateFormEstrutura.value = ''; templateFormTitle.textContent = 'Novo Modelo'; } // Botão "Novo Modelo" if (btnCreateTemplate) { btnCreateTemplate.addEventListener('click', clearTemplateForm); } // Botão "Cancelar" if (btnCancelTemplate) { btnCancelTemplate.addEventListener('click', () => { clearTemplateForm(); if (modelosRelatorioModal) modelosRelatorioModal.style.display = 'none'; }); } // Botão "Salvar Modelo" if (btnSaveTemplate) { btnSaveTemplate.addEventListener('click', async () => { const id = templateFormId.value; const nome = templateFormNome.value.trim(); const finalidade = templateFormFinalidade.value.trim(); const periodicidade = templateFormPeriodicidade.value; const estrutura = templateFormEstrutura.value.trim(); if (!nome || !estrutura) { alert('Nome e estrutura do modelo são obrigatórios.'); return; } const body = { nome, finalidade, periodicidade, estrutura }; const url = id ? `/api/modelos/${id}` : '/api/modelos'; const method = id ? 'PUT' : 'POST'; try { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (res.ok) { alert('Modelo salvo com sucesso!'); clearTemplateForm(); await fetchTemplates(); } else { const err = await res.json(); alert('Erro ao salvar modelo: ' + err.error); } } catch (err) { console.error('Erro ao salvar modelo:', err); } }); } // ============================================================ // GERAÇÃO / EMISSÃO DE RELATÓRIO PEDAGÓGICO // ============================================================ const emitirRelatorioModal = document.getElementById('emitirRelatorioModal'); const btnEmitirRelatorio = document.getElementById('btnEmitirRelatorio'); const btnCloseEmitirModal = document.getElementById('btnCloseEmitirModal'); const emitirFilterAno = document.getElementById('emitirFilterAno'); const emitirFilterTurma = document.getElementById('emitirFilterTurma'); const emitirFilterCrianca = document.getElementById('emitirFilterCrianca'); const emitirFilterDataInicio = document.getElementById('emitirFilterDataInicio'); const emitirFilterDataFim = document.getElementById('emitirFilterDataFim'); const emitirTagsContainer = document.getElementById('emitirTagsContainer'); const emitirFilterTemplate = document.getElementById('emitirFilterTemplate'); let emitirSelectedTags = []; const emitirObsCountInfo = document.getElementById('emitirObsCountInfo'); const btnGenerateCompiledReport = document.getElementById('btnGenerateCompiledReport'); const emitirReportPreviewArea = document.getElementById('emitirReportPreviewArea'); const btnExportDocx = document.getElementById('btnExportDocx'); const btnExportPdf = document.getElementById('btnExportPdf'); let activeCompiledReport = null; // Guarda o texto do relatório compilado ativo // Abrir modal de emissão if (btnEmitirRelatorio) { btnEmitirRelatorio.addEventListener('click', async () => { emitirRelatorioModal.style.display = 'flex'; activeCompiledReport = null; emitirReportPreviewArea.innerHTML = `
Escolha uma criança e um modelo de relatório para começar.
`; btnExportDocx.disabled = true; btnExportDocx.style.cursor = 'not-allowed'; btnExportDocx.style.borderColor = 'var(--border-light)'; btnExportDocx.style.color = 'var(--text-secondary)'; btnExportPdf.disabled = true; btnExportPdf.style.cursor = 'not-allowed'; btnExportPdf.style.borderColor = 'var(--border-light)'; btnExportPdf.style.color = 'var(--text-secondary)'; emitirFilterDataInicio.value = ''; emitirFilterDataFim.value = ''; emitirSelectedTags = []; if (emitirTagsContainer) { emitirTagsContainer.querySelectorAll('.tag-pill').forEach(p => p.classList.remove('active')); } await loadFiltersData(); }); } // Fechar modal de emissão if (btnCloseEmitirModal) { btnCloseEmitirModal.addEventListener('click', () => { emitirRelatorioModal.style.display = 'none'; }); } // Buscar crianças com base nos filtros de Ano e Turma selecionados async function loadChildrenList() { try { const activeAno = emitirFilterAno ? emitirFilterAno.value : ''; const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : ''; const activeCrianca = emitirFilterCrianca ? emitirFilterCrianca.value : ''; let url = '/api/observacoes/criancas'; const params = []; if (activeAno) params.push(`ano=${activeAno}`); if (activeTurma) params.push(`turma=${encodeURIComponent(activeTurma)}`); if (params.length > 0) url += '?' + params.join('&'); const res = await fetch(url); const criancas = await res.json(); emitirFilterCrianca.innerHTML = '' + `` + (Array.isArray(criancas) ? criancas : []).map(c => ``).join(''); } catch (err) { console.error('Erro ao carregar lista de crianças:', err); } } // Carregar dados de filtros (anos, turmas, crianças e templates) async function loadFiltersData() { try { const activeAno = emitirFilterAno ? emitirFilterAno.value : ''; const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : ''; const res = await fetch('/api/observacoes/metadata'); const meta = await res.json(); if (emitirFilterAno) { emitirFilterAno.innerHTML = '' + (meta.anos || []).map(y => ``).join(''); } if (emitirFilterTurma) { emitirFilterTurma.innerHTML = '' + (meta.turmas || []).map(t => ``).join(''); } } catch (err) { console.error('Erro ao buscar metadados de filtros:', err); } await loadChildrenList(); try { const activeTemplate = emitirFilterTemplate ? emitirFilterTemplate.value : ''; const res = await fetch('/api/modelos'); const modelos = await res.json(); emitirFilterTemplate.innerHTML = '' + (Array.isArray(modelos) ? modelos : []).map(m => ``).join(''); } catch (err) { console.error('Erro ao buscar modelos para filtros:', err); } updateObservationsCount(); } // Ouvintes para atualizar contagem e filtros de criança if (emitirFilterAno) { emitirFilterAno.addEventListener('change', async () => { await loadChildrenList(); updateObservationsCount(); }); } if (emitirFilterTurma) { emitirFilterTurma.addEventListener('change', async () => { await loadChildrenList(); updateObservationsCount(); }); } [emitirFilterCrianca, emitirFilterDataInicio, emitirFilterDataFim, emitirFilterTemplate].forEach(el => { if (el) el.addEventListener('change', updateObservationsCount); }); if (emitirFilterCrianca) { emitirFilterCrianca.addEventListener('input', updateObservationsCount); } // Adicionar ouvintes para as pills de tags if (emitirTagsContainer) { emitirTagsContainer.querySelectorAll('.tag-pill').forEach(pill => { pill.addEventListener('click', () => { const tag = pill.getAttribute('data-tag'); if (pill.classList.contains('active')) { pill.classList.remove('active'); emitirSelectedTags = emitirSelectedTags.filter(t => t !== tag); } else { pill.classList.add('active'); emitirSelectedTags.push(tag); } updateObservationsCount(); }); }); } // Recalcular quantidade de observações elegíveis async function updateObservationsCount() { const crianca = emitirFilterCrianca ? emitirFilterCrianca.value : ''; const dataInicio = emitirFilterDataInicio ? emitirFilterDataInicio.value : ''; const dataFim = emitirFilterDataFim ? emitirFilterDataFim.value : ''; const tag = emitirSelectedTags.join(','); const modeloId = emitirFilterTemplate ? emitirFilterTemplate.value : ''; const ano = emitirFilterAno ? emitirFilterAno.value : ''; const turma = emitirFilterTurma ? emitirFilterTurma.value : ''; if (!crianca || !modeloId) { emitirObsCountInfo.textContent = 'Escolha uma criança (ou "Todas") e um modelo para prosseguir.'; btnGenerateCompiledReport.disabled = true; btnGenerateCompiledReport.style.opacity = '0.6'; btnGenerateCompiledReport.style.cursor = 'not-allowed'; return; } try { let url = `/api/observacoes/contar?crianca=${encodeURIComponent(crianca)}`; if (dataInicio) url += `&dataInicio=${dataInicio}`; if (dataFim) url += `&dataFim=${dataFim}`; if (tag) url += `&tags=${encodeURIComponent(tag)}`; if (ano) url += `&ano=${ano}`; if (turma) url += `&turma=${encodeURIComponent(turma)}`; const res = await fetch(url); const data = await res.json(); const count = data.count || 0; if (count === 0) { emitirObsCountInfo.innerHTML = `⚠️ Nenhuma observação encontrada para esses filtros.`; btnGenerateCompiledReport.disabled = true; btnGenerateCompiledReport.style.opacity = '0.6'; btnGenerateCompiledReport.style.cursor = 'not-allowed'; } else { emitirObsCountInfo.innerHTML = `✨ ${count} observações selecionadas para compilação.`; btnGenerateCompiledReport.disabled = false; btnGenerateCompiledReport.style.opacity = '1'; btnGenerateCompiledReport.style.cursor = 'pointer'; } } catch (err) { console.error('Erro ao contar observações:', err); } } // Gerar relatório consolidado com IA if (btnGenerateCompiledReport) { btnGenerateCompiledReport.addEventListener('click', async () => { const crianca = emitirFilterCrianca.value; const dataInicio = emitirFilterDataInicio.value; const dataFim = emitirFilterDataFim.value; const tags = emitirSelectedTags; const modeloId = emitirFilterTemplate.value; const ano = emitirFilterAno ? emitirFilterAno.value : ''; const turma = emitirFilterTurma ? emitirFilterTurma.value : ''; if (!crianca || !modeloId) return; emitirReportPreviewArea.innerHTML = `
Analisando observações e gerando relatório pedagógico...
`; btnGenerateCompiledReport.disabled = true; btnGenerateCompiledReport.style.opacity = '0.6'; try { const res = await fetch('/api/modelos/gerar-relatorio', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ crianca, dataInicio, dataFim, tags: tags.length > 0 ? tags : undefined, modeloId, ano, turma }) }); if (!res.ok) { const errData = await res.json(); throw new Error(errData.error || 'Erro desconhecido'); } const data = await res.json(); activeCompiledReport = data; // Renderizar prévia do relatório em markdown emitirReportPreviewArea.innerHTML = `
Estudante: ${data.crianca}
Período: ${data.periodo}
Modelo Aplicado: ${data.templateNome}
Quantidade de Observações Consolidadas: ${data.totalObservacoes}
${marked.parse(data.report)}
`; // Ativar botões de exportação btnExportDocx.disabled = false; btnExportDocx.style.cursor = 'pointer'; btnExportDocx.style.borderColor = 'var(--brand-green)'; btnExportDocx.style.color = 'var(--brand-green)'; btnExportPdf.disabled = false; btnExportPdf.style.cursor = 'pointer'; btnExportPdf.style.borderColor = 'var(--brand-green)'; btnExportPdf.style.color = 'var(--brand-green)'; // Rolar suavemente até o relatório no mobile if (window.innerWidth <= 768) { const modalBody = emitirRelatorioModal.querySelector('.settings-modal-body'); if (modalBody) { setTimeout(() => { modalBody.scrollTo({ top: modalBody.scrollHeight, behavior: 'smooth' }); }, 150); } } } catch (err) { console.error('Erro ao gerar relatório compilado:', err); emitirReportPreviewArea.innerHTML = `
Erro ao gerar relatório: ${err.message}
`; } finally { btnGenerateCompiledReport.disabled = false; btnGenerateCompiledReport.style.opacity = '1'; } }); } // Exportar para DOCX (Formato HTML-DOCX) if (btnExportDocx) { btnExportDocx.addEventListener('click', () => { if (!activeCompiledReport) return; const htmlContent = ` Relatório Pedagógico - ${activeCompiledReport.crianca}
Estudante: ${activeCompiledReport.crianca}
Período: ${activeCompiledReport.periodo}
Modelo: ${activeCompiledReport.templateNome}
Quantidade de observações consolidadas: ${activeCompiledReport.totalObservacoes}

${marked.parse(activeCompiledReport.report)} `; const blob = new Blob(['\ufeff' + htmlContent], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `relatorio_${activeCompiledReport.crianca.replace(/\s+/g, '_')}_${Date.now()}.doc`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); } // Exportar para PDF (Abre janela de impressão do navegador) if (btnExportPdf) { btnExportPdf.addEventListener('click', () => { if (!activeCompiledReport) return; const printWindow = window.open('', '_blank'); printWindow.document.write(` Relatório Pedagógico - ${activeCompiledReport.crianca}

Relatório Pedagógico de Desenvolvimento

Estudante: ${activeCompiledReport.crianca}
Período: ${activeCompiledReport.periodo}
Modelo Aplicado: ${activeCompiledReport.templateNome}
Quantidade de Observações: ${activeCompiledReport.totalObservacoes}
${marked.parse(activeCompiledReport.report)}
`); printWindow.document.close(); }); } // ============================================================ // ESTÚDIO MUSICAL (Criação de música cantada com MiniMax) // ============================================================ const estudioMusicalModal = document.getElementById('estudioMusicalModal'); const btnCloseEstudioModal = document.getElementById('btnCloseEstudioModal'); const sidebarBtnEstudio = document.getElementById('sidebarBtnEstudio'); const barBtnEstudio = document.getElementById('barBtnEstudio'); const welcomeCardEstudio = document.getElementById('welcomeCardEstudio'); const btnGenerateStudioMusic = document.getElementById('btnGenerateStudioMusic'); const musicTemaInput = document.getElementById('musicTemaInput'); const musicGenerateLoader = document.getElementById('musicGenerateLoader'); const musicLoaderText = document.getElementById('musicLoaderText'); const musicResultArea = document.getElementById('musicResultArea'); const musicStudioAudioPlayer = document.getElementById('musicStudioAudioPlayer'); const musicStudioDownloadBtn = document.getElementById('musicStudioDownloadBtn'); const musicStudioLyricsArea = document.getElementById('musicStudioLyricsArea'); const btnCopyMusicLyrics = document.getElementById('btnCopyMusicLyrics'); const btnDownloadMusicLyrics = document.getElementById('btnDownloadMusicLyrics'); let selectedVoice = 'mulher'; let selectedRhythm = 'roda'; let selectedDuration = '30s'; // Toggle classes active para botões de opções de voz do Estúdio Musical document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedVoice = btn.dataset.voice; }); }); document.querySelectorAll('.btn-music-rhythm').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-music-rhythm').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedRhythm = btn.dataset.rhythm; }); }); document.querySelectorAll('.btn-music-duration').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-music-duration').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedDuration = btn.dataset.duration; }); }); // Abrir e fechar modal const openEstudioModal = () => { estudioMusicalModal.style.display = 'flex'; musicTemaInput.value = ''; musicResultArea.style.display = 'none'; musicStudioAudioPlayer.src = ''; musicStudioLyricsArea.textContent = ''; if (musicGenerateLoader) musicGenerateLoader.style.display = 'none'; btnGenerateStudioMusic.disabled = false; }; if (sidebarBtnEstudio) { sidebarBtnEstudio.addEventListener('click', openEstudioModal); } if (barBtnEstudio) { barBtnEstudio.addEventListener('click', openEstudioModal); } if (welcomeCardEstudio) { welcomeCardEstudio.addEventListener('click', openEstudioModal); } if (btnCloseEstudioModal) { btnCloseEstudioModal.addEventListener('click', () => { estudioMusicalModal.style.display = 'none'; musicStudioAudioPlayer.pause(); }); } // Ação de gerar música no estúdio if (btnGenerateStudioMusic) { btnGenerateStudioMusic.addEventListener('click', async () => { const tema = musicTemaInput.value.trim(); if (!tema) { alert('Por favor, digite um tema para a música.'); return; } btnGenerateStudioMusic.disabled = true; musicGenerateLoader.style.display = 'flex'; musicResultArea.style.display = 'none'; const statusInterval = setInterval(() => { const statusMessages = [ 'Compondo a letra pedagógica...', 'Ajustando a métrica e as rimas...', 'Gerando o arranjo musical...', 'Adicionando os vocais cantados...', 'Quase pronto, finalizando o áudio...' ]; const randomMsg = statusMessages[Math.floor(Math.random() * statusMessages.length)]; musicLoaderText.textContent = randomMsg; }, 5000); try { const response = await fetch('/api/music/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tema, voz: selectedVoice, ritmo: selectedRhythm, duracao: selectedDuration }) }); clearInterval(statusInterval); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || 'Erro na geração de áudio'); } const data = await response.json(); // Exibir resultados musicStudioLyricsArea.textContent = data.lyrics; musicStudioAudioPlayer.src = data.audioUrl; musicStudioDownloadBtn.href = data.audioUrl; musicGenerateLoader.style.display = 'none'; musicResultArea.style.display = 'flex'; } catch (err) { clearInterval(statusInterval); console.error('Erro ao gerar música no estúdio:', err); alert('Erro ao gerar música: ' + err.message); musicGenerateLoader.style.display = 'none'; btnGenerateStudioMusic.disabled = false; } }); } // Copiar letra if (btnCopyMusicLyrics) { btnCopyMusicLyrics.addEventListener('click', () => { const lyrics = musicStudioLyricsArea.textContent; if (lyrics) { navigator.clipboard.writeText(lyrics) .then(() => { btnCopyMusicLyrics.textContent = '✅ Copiado!'; setTimeout(() => { btnCopyMusicLyrics.textContent = '📋 Copiar Letra'; }, 2000); }) .catch(err => { console.error('Falha ao copiar:', err); }); } }); } // Baixar letra em arquivo de texto (TXT) if (btnDownloadMusicLyrics) { btnDownloadMusicLyrics.addEventListener('click', () => { const lyrics = musicStudioLyricsArea.textContent; const tema = musicTemaInput.value.trim() || 'cantiga'; if (lyrics) { const blob = new Blob([lyrics], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `letra_${tema.replace(/\s+/g, '_').toLowerCase()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } }); } // ===================================== // HISTÓRICO DO ESTÚDIO MUSICAL // ===================================== const btnOpenEstudioHistory = document.getElementById('btnOpenEstudioHistory'); const estudioHistoryModal = document.getElementById('estudioHistoryModal'); const btnCloseEstudioHistory = document.getElementById('btnCloseEstudioHistory'); const estudioHistoryList = document.getElementById('estudioHistoryList'); if (btnOpenEstudioHistory) { btnOpenEstudioHistory.addEventListener('click', async () => { estudioHistoryModal.style.display = 'flex'; estudioHistoryList.innerHTML = '
Carregando histórico...
'; try { const res = await fetch('/api/music/list'); const data = await res.json(); estudioHistoryList.innerHTML = ''; if (data.length === 0) { estudioHistoryList.innerHTML = '
Nenhuma canção salva ainda no estúdio.
'; return; } data.forEach(item => { const div = document.createElement('div'); div.style.background = 'var(--bg-tertiary)'; div.style.padding = '12px 15px'; div.style.borderRadius = '8px'; div.style.display = 'flex'; div.style.justifyContent = 'space-between'; div.style.alignItems = 'center'; div.innerHTML = `
🎵 ${item.tema || 'Música'}
Voz: ${item.voz} | Ritmo: ${item.ritmo}
${new Date(item.created_at).toLocaleString()}
`; estudioHistoryList.appendChild(div); }); document.querySelectorAll('.btn-ver-estudio').forEach(btn => { btn.addEventListener('click', async (e) => { const id = e.currentTarget.dataset.id; try { const res = await fetch(`/api/music/${id}`); const itemData = await res.json(); musicStudioLyricsArea.textContent = itemData.letra; musicStudioAudioPlayer.src = itemData.audio_url; musicStudioDownloadBtn.href = itemData.audio_url; musicResultArea.style.display = 'flex'; estudioHistoryModal.style.display = 'none'; } catch (err) { alert('Erro ao carregar música do estúdio.'); } }); }); document.querySelectorAll('.btn-del-estudio').forEach(btn => { btn.addEventListener('click', async (e) => { if(!confirm('Tem certeza que deseja apagar esta canção?')) return; const id = e.currentTarget.dataset.id; try { await fetch(`/api/music/${id}`, { method: 'DELETE' }); e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove(); } catch (err) { alert('Erro ao deletar.'); } }); }); } catch (err) { estudioHistoryList.innerHTML = '
Erro ao carregar histórico.
'; } }); } if (btnCloseEstudioHistory) { btnCloseEstudioHistory.addEventListener('click', () => { estudioHistoryModal.style.display = 'none'; }); } // ============================================================ // FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas) // ============================================================ const fabricaQuadrinhosModal = document.getElementById('fabricaQuadrinhosModal'); const btnCloseComicsModal = document.getElementById('btnCloseComicsModal'); const barBtnComics = document.getElementById('barBtnComics'); const btnGenerateComics = document.getElementById('btnGenerateComics'); const comicsTemaInput = document.getElementById('comicsTemaInput'); const comicsGenerateLoader = document.getElementById('comicsGenerateLoader'); const comicsLoaderText = document.getElementById('comicsLoaderText'); const comicsLoaderProgress = document.getElementById('comicsLoaderProgress'); const comicsResultArea = document.getElementById('comicsResultArea'); const comicsResultTitle = document.getElementById('comicsResultTitle'); const comicsGridContainer = document.getElementById('comicsGridContainer'); const comicsCenarioSelect = document.getElementById('comicsCenarioSelect'); const comicsCenarioCustomInput = document.getElementById('comicsCenarioCustomInput'); // Export PDF / Vídeo const btnExportComicsPDFPortrait = document.getElementById('btnExportComicsPDFPortrait'); const btnExportComicsPDFLandscape = document.getElementById('btnExportComicsPDFLandscape'); const btnCompileComicsVideo = document.getElementById('btnCompileComicsVideo'); const comicsVideoMusic = document.getElementById('comicsVideoMusic'); const comicsVideoDuration = document.getElementById('comicsVideoDuration'); const comicsVideoLoader = document.getElementById('comicsVideoLoader'); const comicsVideoResult = document.getElementById('comicsVideoResult'); const comicsVideoPlayer = document.getElementById('comicsVideoPlayer'); const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn'); const comicsProjectSelect = document.getElementById('comicsProjectSelect'); const btnNewComicsProject = document.getElementById('btnNewComicsProject'); const btnDeleteComicsProject = document.getElementById('btnDeleteComicsProject'); const comicsProjectTitleInput = document.getElementById('comicsProjectTitleInput'); const btnSaveComicsProject = document.getElementById('btnSaveComicsProject'); const btnSaveNewComicsProject = document.getElementById('btnSaveNewComicsProject'); let currentComicsProjectId = null; let currentComicsCharDescEnglish = ''; let selectedComicsQty = 5; let selectedComicsBubbles = true; let selectedComicsRatio = '16:9'; let generatedPanelsData = []; // Toggle ativo quantidade de quadros document.querySelectorAll('.btn-comics-qty').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-comics-qty').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedComicsQty = parseInt(btn.dataset.qty); }); }); // Toggle ativo falas/diálogos document.querySelectorAll('.btn-comics-bubbles').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-comics-bubbles').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedComicsBubbles = btn.dataset.bubbles === 'true'; }); }); // Toggle ativo proporção visual document.querySelectorAll('.btn-comics-ratio').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-comics-ratio').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedComicsRatio = btn.dataset.ratio; }); }); // Gerenciador dinâmico de personagens const comicsCharListContainer = document.getElementById('comicsCharListContainer'); const btnAddHumanChar = document.getElementById('btnAddHumanChar'); const btnAddAnimalChar = document.getElementById('btnAddAnimalChar'); const addCharacterRow = (type = '', name = '', isAnimal = false) => { const row = document.createElement('div'); row.className = 'comic-char-row'; row.style.cssText = 'display: flex; gap: 8px; align-items: center; background: var(--bg-primary); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);'; const emoji = document.createElement('span'); emoji.style.fontSize = '1.1rem'; emoji.textContent = isAnimal ? '🐾' : '🧒'; row.appendChild(emoji); const typeInput = document.createElement('input'); typeInput.type = 'text'; typeInput.className = 'obs-input char-type-input'; typeInput.placeholder = isAnimal ? 'Tipo (ex: Gatinho, Leão)' : 'Tipo (ex: Menino, Menina)'; typeInput.value = type; typeInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;'; row.appendChild(typeInput); const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'obs-input char-name-input'; nameInput.placeholder = 'Nome'; nameInput.value = name; nameInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;'; row.appendChild(nameInput); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'btn-delete-char'; deleteBtn.innerHTML = '×'; deleteBtn.style.cssText = 'background: none; border: none; color: #ef4444; font-size: 1.3rem; cursor: pointer; padding: 0 4px; line-height: 1;'; deleteBtn.addEventListener('click', () => { row.remove(); }); row.appendChild(deleteBtn); comicsCharListContainer.appendChild(row); }; const btnImportTurmaChars = document.getElementById('btnImportTurmaChars'); if (btnAddHumanChar) { btnAddHumanChar.addEventListener('click', () => { addCharacterRow('', '', false); }); } if (btnAddAnimalChar) { btnAddAnimalChar.addEventListener('click', () => { addCharacterRow('', '', true); }); } if (btnImportTurmaChars) { btnImportTurmaChars.addEventListener('click', async () => { try { btnImportTurmaChars.disabled = true; btnImportTurmaChars.textContent = '⏳ Carregando...'; const res = await fetch('/api/alunos'); if (!res.ok) throw new Error('Falha ao obter lista de alunos'); const alunos = await res.json(); if (!alunos || alunos.length === 0) { await showCustomAlert('Nenhum Aluno', 'Cadastre alunos na seção "Minha Turma" para importá-los automaticamente.'); return; } // Adiciona os alunos como personagens (até 4 para manter a história focada) const selecionados = alunos.slice(0, 4); comicsCharListContainer.innerHTML = ''; selecionados.forEach(aluno => { const generoProvavel = aluno.nome.trim().endsWith('a') ? 'Menina' : 'Menino'; addCharacterRow(generoProvavel, aluno.nome.trim(), false); }); await showCustomAlert('Turma Importada!', `${selecionados.length} alunos foram adicionados como personagens da história.`); } catch (err) { console.error('Erro ao importar alunos:', err); await showCustomAlert('Erro', 'Não foi possível carregar a lista de alunos: ' + err.message); } finally { btnImportTurmaChars.disabled = false; btnImportTurmaChars.textContent = '+ 🏫 Importar da Turma'; } }); } // Exibir/ocultar cenário personalizado if (comicsCenarioSelect) { comicsCenarioSelect.addEventListener('change', () => { if (comicsCenarioSelect.value === 'custom') { comicsCenarioCustomInput.style.display = 'block'; } else { comicsCenarioCustomInput.style.display = 'none'; } }); } // --- GERENCIADOR DE PROJETOS DE HQ --- const startNewComicsProject = () => { currentComicsProjectId = null; currentComicsCharDescEnglish = ''; const modeContainer = document.getElementById('comicsGenerationModeContainer'); if (modeContainer) modeContainer.style.display = 'none'; const radioNew = document.querySelector('input[name="comicsGenMode"][value="new_story"]'); if (radioNew) radioNew.checked = true; if (comicsProjectSelect) comicsProjectSelect.value = ''; if (comicsProjectTitleInput) comicsProjectTitleInput.value = ''; if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none'; if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'none'; comicsTemaInput.value = ''; comicsResultArea.style.display = 'none'; comicsGenerateLoader.style.display = 'none'; comicsVideoResult.style.display = 'none'; comicsVideoLoader.style.display = 'none'; btnGenerateComics.disabled = false; if (comicsLoaderProgress) comicsLoaderProgress.style.width = '0%'; // Set active state for default quick options selectedComicsQty = 5; document.querySelectorAll('.btn-comics-qty').forEach(b => { if (b.dataset.qty === '5') b.classList.add('active'); else b.classList.remove('active'); }); selectedComicsBubbles = true; document.querySelectorAll('.btn-comics-bubbles').forEach(b => { if (b.dataset.bubbles === 'true') b.classList.add('active'); else b.classList.remove('active'); }); selectedComicsRatio = '16:9'; document.querySelectorAll('.btn-comics-ratio').forEach(b => { if (b.dataset.ratio === '16:9') b.classList.add('active'); else b.classList.remove('active'); }); if (comicsCenarioSelect) { comicsCenarioSelect.value = 'no parque de diversões colorido'; if (comicsCenarioCustomInput) { comicsCenarioCustomInput.style.display = 'none'; comicsCenarioCustomInput.value = ''; } } // Inicializar lista de personagens com padrão se estiver vazia comicsCharListContainer.innerHTML = ''; addCharacterRow("Menino", "Lucas", false); addCharacterRow("Menina", "Mariana", false); generatedPanelsData = []; comicsGridContainer.innerHTML = ''; }; const loadComicsProjectsList = async () => { if (!comicsProjectSelect) return; try { const resp = await fetch('/api/comics/projects'); if (!resp.ok) { console.warn('Projetos de quadrinhos temporariamente indisponíveis:', resp.status); return; } const projects = await resp.json(); if (!Array.isArray(projects)) return; comicsProjectSelect.innerHTML = ''; projects.forEach(proj => { const opt = document.createElement('option'); opt.value = proj.id; opt.textContent = `${proj.titulo} (${new Date(proj.updated_at).toLocaleDateString('pt-BR')})`; comicsProjectSelect.appendChild(opt); }); if (currentComicsProjectId) { comicsProjectSelect.value = currentComicsProjectId; } } catch (err) { console.warn('Aviso ao listar projetos:', err.message); } }; const loadComicsProject = async (id) => { if (!id) { startNewComicsProject(); return; } try { const resp = await fetch(`/api/comics/projects/${id}`); if (!resp.ok) { const errMsg = await safeExtractError(resp, 'Falha ao carregar projeto'); throw new Error(errMsg); } const { project, panels } = await resp.json(); currentComicsProjectId = project.id; currentComicsCharDescEnglish = project.character_description_english || ''; const modeContainer = document.getElementById('comicsGenerationModeContainer'); if (modeContainer) modeContainer.style.display = 'flex'; if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || ''; comicsTemaInput.value = project.tema || ''; if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block'; if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block'; // Carregar proporção selectedComicsRatio = project.proporcao || '16:9'; document.querySelectorAll('.btn-comics-ratio').forEach(b => { if (b.dataset.ratio === selectedComicsRatio) b.classList.add('active'); else b.classList.remove('active'); }); // Carregar cenário if (comicsCenarioSelect) { const standardOptions = ['no parque de diversões colorido', 'na sala de aula da escola infantil', 'em uma floresta encantada cheia de flores', 'no zoológico interagindo com animais', 'num jardim ensolarado de uma casa']; if (standardOptions.includes(project.cenario)) { comicsCenarioSelect.value = project.cenario; if (comicsCenarioCustomInput) comicsCenarioCustomInput.style.display = 'none'; } else { comicsCenarioSelect.value = 'custom'; if (comicsCenarioCustomInput) { comicsCenarioCustomInput.style.display = 'block'; comicsCenarioCustomInput.value = project.cenario || ''; } } } // Carregar quantidade e balões baseados nos painéis retornados selectedComicsQty = panels.length || 5; document.querySelectorAll('.btn-comics-qty').forEach(b => { if (parseInt(b.dataset.qty) === selectedComicsQty) b.classList.add('active'); else b.classList.remove('active'); }); const hasDialogue = panels.some(p => p.dialogue); selectedComicsBubbles = hasDialogue; document.querySelectorAll('.btn-comics-bubbles').forEach(b => { if ((b.dataset.bubbles === 'true') === selectedComicsBubbles) b.classList.add('active'); else b.classList.remove('active'); }); // Carregar personagens comicsCharListContainer.innerHTML = ''; if (project.character_description_global) { try { const chars = JSON.parse(project.character_description_global); if (Array.isArray(chars)) { chars.forEach(c => { addCharacterRow(c.type || '', c.name || '', c.isAnimal || false); }); } } catch (e) { addCharacterRow("Menino", "Lucas", false); addCharacterRow("Menina", "Mariana", false); } } else { addCharacterRow("Menino", "Lucas", false); addCharacterRow("Menina", "Mariana", false); } // Carregar os painéis generatedPanelsData = panels.map(p => ({ panel_number: p.panel_number, imageUrl: p.image_url, dialogue: p.dialogue || '', image_prompt: p.image_prompt || '' })); if (generatedPanelsData.length > 0) { if (comicsResultTitle) comicsResultTitle.textContent = project.titulo || 'História em Quadrinhos'; renderComicsStoryboard(); if (comicsResultArea) comicsResultArea.style.display = 'flex'; } comicsVideoResult.style.display = 'none'; comicsVideoLoader.style.display = 'none'; btnGenerateComics.disabled = false; } catch (err) { console.error(err); await showCustomAlert('Erro', 'Erro ao carregar projeto: ' + err.message); } }; const saveComicsProjectFlow = async (forceNew = false) => { let titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : ''; let tema = comicsTemaInput ? comicsTemaInput.value.trim() : ''; if (!titulo) { if (comicsResultTitle && comicsResultTitle.textContent.trim() && comicsResultTitle.textContent.trim() !== 'Resultado da História em Quadrinhos') { titulo = comicsResultTitle.textContent.trim(); } else if (tema) { titulo = tema; } else { titulo = 'Minha História em Quadrinhos'; } if (comicsProjectTitleInput) comicsProjectTitleInput.value = titulo; } if (!tema) { tema = titulo; } const characters = []; document.querySelectorAll('.comic-char-row').forEach(row => { const typeInput = row.querySelector('.char-type-input'); const nameInput = row.querySelector('.char-name-input'); const animalToggle = row.querySelector('.comic-char-animal-toggle'); const type = typeInput ? typeInput.value.trim() : ''; const name = nameInput ? nameInput.value.trim() : ''; const isAnimal = animalToggle ? animalToggle.classList.contains('active') : false; if (type && name) { characters.push({ type, name, isAnimal }); } }); let cenario = comicsCenarioSelect ? comicsCenarioSelect.value : 'no parque de diversões colorido'; if (cenario === 'custom' && comicsCenarioCustomInput) { cenario = comicsCenarioCustomInput.value.trim(); } const targetId = forceNew ? null : currentComicsProjectId; const activeBtn = forceNew ? btnSaveNewComicsProject : btnSaveComicsProject; const originalText = activeBtn ? activeBtn.textContent : ''; if (activeBtn) { activeBtn.disabled = true; activeBtn.textContent = '⏳ Salvando...'; } const body = { id: targetId, titulo, tema, cenario, proporcao: selectedComicsRatio, character_description_global: JSON.stringify(characters), character_description_english: currentComicsCharDescEnglish || '', panels: generatedPanelsData.map(p => ({ panel_number: p.panel_number, image_url: p.imageUrl || p.image_url || '', image_prompt: p.image_prompt || '', dialogue: p.dialogue || '' })) }; try { const resp = await fetch('/api/comics/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!resp.ok) { const errMsg = await safeExtractError(resp, 'Erro ao salvar projeto'); throw new Error(errMsg); } const data = await resp.json(); currentComicsProjectId = data.id; await showCustomAlert('Sucesso', 'Projeto salvo com sucesso!'); if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block'; if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block'; await loadComicsProjectsList(); } catch (err) { console.error(err); await showCustomAlert('Erro', 'Erro ao salvar projeto: ' + err.message); } finally { if (activeBtn) { activeBtn.disabled = false; activeBtn.textContent = originalText; } } }; const saveComicsProject = () => saveComicsProjectFlow(false); const saveNewComicsProject = () => saveComicsProjectFlow(true); const deleteComicsProject = async () => { if (!currentComicsProjectId) return; const confirmed = await showCustomConfirm( 'Confirmar Exclusão', 'Tem certeza de que deseja excluir este projeto permanentemente?', true ); if (!confirmed) return; try { const resp = await fetch(`/api/comics/projects/${currentComicsProjectId}`, { method: 'DELETE' }); if (!resp.ok) { const errMsg = await safeExtractError(resp, 'Falha ao excluir projeto'); throw new Error(errMsg); } await showCustomAlert('Excluído', 'Projeto excluído com sucesso!'); startNewComicsProject(); await loadComicsProjectsList(); } catch (err) { console.error(err); await showCustomAlert('Erro', 'Erro ao excluir projeto: ' + err.message); } }; // Abrir e fechar modal Fábrica de Quadrinhos const openComicsModal = () => { fabricaQuadrinhosModal.style.display = 'flex'; startNewComicsProject(); loadComicsProjectsList(); }; // Listeners do Gerenciador de Projetos if (comicsProjectSelect) { comicsProjectSelect.addEventListener('change', (e) => { loadComicsProject(e.target.value); }); } if (btnNewComicsProject) { btnNewComicsProject.addEventListener('click', async () => { const confirmed = await showCustomConfirm( 'Novo Projeto', 'Deseja iniciar um novo projeto? Alterações não salvas serão perdidas.' ); if (confirmed) { startNewComicsProject(); } }); } if (btnSaveComicsProject) { btnSaveComicsProject.addEventListener('click', saveComicsProject); } if (btnSaveNewComicsProject) { btnSaveNewComicsProject.addEventListener('click', saveNewComicsProject); } if (btnDeleteComicsProject) { btnDeleteComicsProject.addEventListener('click', deleteComicsProject); } if (barBtnComics) { barBtnComics.addEventListener('click', openComicsModal); } if (btnCloseComicsModal) { btnCloseComicsModal.addEventListener('click', async () => { if (comicsGenerateLoader && comicsGenerateLoader.style.display === 'flex') { const confirmed = await showCustomConfirm( 'Geração em Andamento', 'Sua história ainda está sendo gerada. Deseja realmente sair e cancelar a criação?' ); if (!confirmed) return; } fabricaQuadrinhosModal.style.display = 'none'; if (comicsVideoPlayer) comicsVideoPlayer.pause(); if (typeof stopComicsMusicPreview === 'function') stopComicsMusicPreview(); }); } // Prevenir fechamento acidental ao clicar fora da janela do modal window.addEventListener('click', (e) => { if (e.target === fabricaQuadrinhosModal) { if (comicsGenerateLoader && comicsGenerateLoader.style.display === 'flex') { showCustomAlert('Aviso', 'Sua história em quadrinhos está sendo gerada. Por favor, aguarde a conclusão.'); } // O modal permanece aberto para proteger o trabalho da professora contra cliques acidentais fora da janela } }); // Helper para converter URL de imagem local em base64 e obter dimensões reais sem deformar const getBase64ImageDetails = async (imgUrl) => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => { const width = img.naturalWidth || img.width || 1280; const height = img.naturalHeight || img.height || 720; const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); resolve({ dataUrl: canvas.toDataURL('image/jpeg'), width: width, height: height, aspectRatio: width / height }); }; img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl)); img.src = imgUrl; }); }; const getBase64Image = async (imgUrl) => { const details = await getBase64ImageDetails(imgUrl); return details.dataUrl; }; // Helper para execução concorrente controlada const mapConcurrent = async (items, limit, fn) => { const results = new Array(items.length); let currentIdx = 0; const worker = async () => { while (currentIdx < items.length) { const i = currentIdx++; results[i] = await fn(items[i], i); } }; const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker()); await Promise.all(workers); return results; }; // Ação de geração da história e imagens if (btnGenerateComics) { btnGenerateComics.addEventListener('click', async () => { const tema = comicsTemaInput.value.trim(); if (!tema) { await showCustomAlert('Campo Requerido', 'Por favor, digite um tema para a história.'); return; } // Reunir personagens dinamicamente const personagens = []; document.querySelectorAll('.comic-char-row').forEach(row => { const type = row.querySelector('.char-type-input').value.trim(); const name = row.querySelector('.char-name-input').value.trim(); if (type && name) { personagens.push(`${type} chamado(a) ${name}`); } }); if (personagens.length === 0) { await showCustomAlert('Personagens Faltando', 'Adicione e preencha pelo menos um personagem principal.'); return; } // Definir cenário let cenario = comicsCenarioSelect.value; if (cenario === 'custom') { cenario = comicsCenarioCustomInput.value.trim() || 'em um local bonito'; } btnGenerateComics.disabled = true; comicsGenerateLoader.style.display = 'flex'; comicsResultArea.style.display = 'none'; comicsVideoResult.style.display = 'none'; comicsLoaderProgress.style.width = '5%'; comicsLoaderText.textContent = 'Escrevendo o roteiro e diálogos dos quadrinhos...'; try { // 1. Gerar Roteiro const genMode = currentComicsProjectId ? (document.querySelector('input[name="comicsGenMode"]:checked')?.value || 'new_story') : 'new_story'; const requestBody = { tema, quantidade: selectedComicsQty, baloes: selectedComicsBubbles, proporcao: selectedComicsRatio, personagens, cenario }; if (currentComicsProjectId) { requestBody.continuationType = genMode; requestBody.character_description = currentComicsCharDescEnglish; requestBody.existingPanels = generatedPanelsData.map(p => ({ panel_number: p.panel_number, image_url: p.imageUrl || p.image_url || '', image_prompt: p.image_prompt || '', dialogue: p.dialogue || '' })); } const scriptResp = await fetch('/api/comics/generate-script', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!scriptResp.ok) { const errMsg = await safeExtractError(scriptResp, 'Erro ao planejar roteiro'); throw new Error(errMsg); } const scriptData = await scriptResp.json(); comicsLoaderProgress.style.width = '20%'; if (scriptData.character_description) { currentComicsCharDescEnglish = scriptData.character_description; } // 2. Gerar quadrinhos com concorrência balanceada (2 por vez) e tracking em tempo real if (genMode !== 'extend') { generatedPanelsData = []; } const totalPanels = scriptData.panels.length; let completedCount = 0; const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render'; const generateSinglePanel = async (panel) => { let fullPrompt = panel.image_prompt || ''; if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) { fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`; } fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; let attempts = 0; let frameData = null; while (attempts < 3 && !frameData) { attempts++; try { const frameResp = await fetch('/api/comics/generate-frame', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio }) }); if (frameResp.ok) { frameData = await frameResp.json(); } else { console.warn(`[Retry Comics] Tentativa ${attempts} para quadro ${panel.panel_number} respondeu ${frameResp.status}`); if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts)); } } catch (netErr) { console.warn(`[Retry Comics] Tentativa ${attempts} erro de rede no quadro ${panel.panel_number}:`, netErr.message); if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts)); } } completedCount++; const progressPercent = 20 + Math.floor((completedCount / totalPanels) * 75); if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`; if (comicsLoaderText) comicsLoaderText.textContent = `🎨 Ilustrando quadrinhos: ${completedCount}/${totalPanels} quadros concluídos...`; // Pequeno respiro entre gerações para respeitar a taxa da API await new Promise(r => setTimeout(r, 600)); if (frameData && frameData.imageUrl) { return { panel_number: panel.panel_number, imageUrl: frameData.imageUrl, dialogue: panel.dialogue || '', image_prompt: panel.image_prompt || '', needsRegeneration: !!frameData.needsRegeneration }; } else { console.warn(`[Comics Resiliencia] Quadro ${panel.panel_number} em espera. Preservando a história.`); return { panel_number: panel.panel_number, imageUrl: '/assets/img/placeholder_comic.png', dialogue: panel.dialogue || '', image_prompt: panel.image_prompt || '', needsRegeneration: true }; } }; // Executa com 1 quadro por vez sequencial para estabilidade absoluta e sem estourar rate limit const newPanels = await mapConcurrent(scriptData.panels, 1, generateSinglePanel); // Ordena por panel_number para garantir a sequência correta newPanels.sort((a, b) => a.panel_number - b.panel_number); if (genMode === 'extend') { generatedPanelsData = generatedPanelsData.concat(newPanels); } else { generatedPanelsData = newPanels; } // Atualizar cabeçalho e metadados comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Pedagog'; // Renderizar Storyboard Interativo renderComicsStoryboard(); // Se for uma nova história derivada de um projeto antigo, limpamos o ID para virar um novo projeto independente ao salvar if (genMode === 'new_story' && currentComicsProjectId) { currentComicsProjectId = null; if (comicsProjectSelect) comicsProjectSelect.value = ''; const modeContainer = document.getElementById('comicsGenerationModeContainer'); if (modeContainer) modeContainer.style.display = 'none'; if (comicsProjectTitleInput) { comicsProjectTitleInput.value = scriptData.title || (comicsProjectTitleInput.value + ' (Derivado)'); } if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none'; } comicsGenerateLoader.style.display = 'none'; comicsResultArea.style.display = 'flex'; btnGenerateComics.disabled = false; // Salvamento automático imediato pós-geração para garantir que a história NUNCA se perca try { await saveComicsProjectFlow(false); await showCustomAlert('🎉 História Gerada e Salva!', 'Sua história em quadrinhos foi concluída e salva automaticamente em "Meus Projetos"!'); } catch (autoSaveErr) { console.warn('Falha no auto-salvamento pós geração:', autoSaveErr.message); } } catch (err) { console.error('Erro ao fabricar quadrinhos:', err); await showCustomAlert('Erro', 'Erro ao fabricar quadrinhos: ' + err.message); comicsGenerateLoader.style.display = 'none'; btnGenerateComics.disabled = false; } }); } // --- RENDERIZADOR DO STORYBOARD PROFISSIONAL DOS QUADRINHOS --- const renderComicsStoryboard = () => { if (!comicsGridContainer) return; comicsGridContainer.innerHTML = ''; generatedPanelsData.forEach((panel, index) => { const panelCard = document.createElement('div'); panelCard.className = 'comic-panel-card'; panelCard.dataset.index = index; const imgContainer = document.createElement('div'); imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`; const img = document.createElement('img'); img.className = 'comic-panel-image'; img.src = panel.imageUrl || panel.image_url; img.alt = `Quadro ${panel.panel_number}`; img.loading = 'lazy'; imgContainer.appendChild(img); // Selo de número do quadrinho const badge = document.createElement('div'); badge.className = 'comic-panel-number-badge'; badge.textContent = panel.panel_number; imgContainer.appendChild(badge); // Barra de Ações Rápidas por Quadro (Mobile & Desktop) const actionsOverlay = document.createElement('div'); actionsOverlay.className = 'comic-panel-overlay-actions'; // Botão 🔍 Zoom const btnZoom = document.createElement('button'); btnZoom.type = 'button'; btnZoom.className = 'btn-comic-action'; btnZoom.title = 'Ampliar imagem'; btnZoom.innerHTML = '🔍'; btnZoom.addEventListener('click', (e) => { e.stopPropagation(); openComicsPresentation(index); }); actionsOverlay.appendChild(btnZoom); // Botão 🔄 Regenerar Imagem deste Quadro const btnRegen = document.createElement('button'); btnRegen.type = 'button'; btnRegen.className = 'btn-comic-action'; btnRegen.title = 'Regenerar apenas este quadrinho'; btnRegen.innerHTML = '🔄'; btnRegen.addEventListener('click', async (e) => { e.stopPropagation(); await regenerateSinglePanel(index, panelCard, img, btnRegen); }); actionsOverlay.appendChild(btnRegen); // Botão ✏️ Ver/Editar Prompt const btnPromptToggle = document.createElement('button'); btnPromptToggle.type = 'button'; btnPromptToggle.className = 'btn-comic-action'; btnPromptToggle.title = 'Editar prompt visual'; btnPromptToggle.innerHTML = '✏️'; actionsOverlay.appendChild(btnPromptToggle); // Botão 💾 Baixar Imagem Individual const btnDownloadImg = document.createElement('button'); btnDownloadImg.type = 'button'; btnDownloadImg.className = 'btn-comic-action'; btnDownloadImg.title = 'Baixar esta imagem PNG'; btnDownloadImg.innerHTML = '💾'; btnDownloadImg.addEventListener('click', (e) => { e.stopPropagation(); const a = document.createElement('a'); a.href = panel.imageUrl || panel.image_url; a.download = `quadrinho_${panel.panel_number}.png`; document.body.appendChild(a); a.click(); a.remove(); }); actionsOverlay.appendChild(btnDownloadImg); imgContainer.appendChild(actionsOverlay); panelCard.appendChild(imgContainer); // Área de Legenda / Diálogo const caption = document.createElement('div'); caption.className = 'comic-caption-text'; const textarea = document.createElement('textarea'); textarea.className = 'comic-caption-input'; textarea.value = panel.dialogue || ''; textarea.placeholder = 'Digite a fala ou legenda do quadrinho...'; textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 48px;'; textarea.addEventListener('input', (e) => { panel.dialogue = e.target.value; if (generatedPanelsData[index]) { generatedPanelsData[index].dialogue = e.target.value; } }); caption.appendChild(textarea); // Mini-Editor de Prompt Visual (Oculto por padrão, ativado no botão ✏️) const promptBox = document.createElement('div'); promptBox.className = 'comic-prompt-details'; const promptInput = document.createElement('textarea'); promptInput.style.cssText = 'width: 100%; font-size: 0.72rem; padding: 4px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-light); border-radius: 4px; resize: vertical; min-height: 40px; box-sizing: border-box;'; promptInput.value = panel.image_prompt || ''; promptInput.addEventListener('input', (e) => { panel.image_prompt = e.target.value; }); promptBox.appendChild(document.createTextNode('Prompt Visual: ')); promptBox.appendChild(promptInput); caption.appendChild(promptBox); btnPromptToggle.addEventListener('click', (e) => { e.stopPropagation(); promptBox.style.display = promptBox.style.display === 'block' ? 'none' : 'block'; }); panelCard.appendChild(caption); comicsGridContainer.appendChild(panelCard); }); }; // Regenerar um único painel inteligente (considera textos/legendas alterados e preserva âncora visual de personagens) const regenerateSinglePanel = async (index, panelCard, imgElement, btnRegen) => { const panel = generatedPanelsData[index]; if (!panel) return; try { btnRegen.disabled = true; btnRegen.textContent = '⏳'; imgElement.style.opacity = '0.4'; // Captura o diálogo/texto ATUAL que está escrito na textarea do quadrinho (caso o usuário tenha editado) const textarea = panelCard.querySelector('.comic-caption-input'); const currentDialogue = textarea ? textarea.value.trim() : (panel.dialogue || ''); panel.dialogue = currentDialogue; // Captura o prompt visual se o mini-editor tiver sido alterado const promptInput = panelCard.querySelector('.comic-prompt-details textarea'); let promptToUse = promptInput ? promptInput.value.trim() : (panel.image_prompt || ''); // Se o diálogo tiver sido preenchido/alterado, re-sincronizamos a cena visual com o novo texto if (currentDialogue) { try { const resyncResp = await fetch('/api/comics/resync-frame-prompt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentDialogue, oldPrompt: promptToUse, character_description: currentComicsCharDescEnglish, cenario: comicsCenarioSelect ? comicsCenarioSelect.value : '' }) }); if (resyncResp.ok) { const resyncData = await resyncResp.json(); if (resyncData.image_prompt) { promptToUse = resyncData.image_prompt; panel.image_prompt = promptToUse; if (promptInput) promptInput.value = promptToUse; } } } catch (resyncErr) { console.warn('Re-sincronização de prompt ao regenerar:', resyncErr.message); } } const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render'; let fullPrompt = promptToUse || ''; if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) { fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`; } fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; const resp = await fetch('/api/comics/generate-frame', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio }) }); if (!resp.ok) { const errMsg = await safeExtractError(resp, 'Erro na regeneração'); throw new Error(errMsg); } const data = await resp.json(); panel.imageUrl = data.imageUrl; imgElement.src = data.imageUrl + '?v=' + Date.now(); imgElement.style.opacity = '1'; // Auto-salva o projeto atualizado no banco saveComicsProjectFlow(false).catch(e => console.warn('Auto-save pós regeneração:', e.message)); } catch (err) { console.error('Erro ao regenerar painel:', err); await showCustomAlert('Erro', 'Não foi possível regenerar o quadrinho: ' + err.message); imgElement.style.opacity = '1'; } finally { btnRegen.disabled = false; btnRegen.innerHTML = '🔄'; } }; // --- EXPORTAR TODAS AS IMAGENS EM ZIP --- const btnExportComicsZIP = document.getElementById('btnExportComicsZIP'); if (btnExportComicsZIP) { btnExportComicsZIP.addEventListener('click', async () => { if (!generatedPanelsData || generatedPanelsData.length === 0) { await showCustomAlert('Aviso', 'Nenhum quadrinho gerado para exportar.'); return; } try { btnExportComicsZIP.disabled = true; btnExportComicsZIP.textContent = '⏳ Criando ZIP...'; if (typeof JSZip === 'undefined') { throw new Error('Biblioteca JSZip não carregada.'); } const zip = new JSZip(); const folder = zip.folder('quadrinhos'); const title = (comicsResultTitle?.textContent || 'Historia_Quadrinhos').replace(/[^a-zA-Z0-9_-]/g, '_'); let roteiroTxt = `HISTÓRIA EM QUADRINHOS: ${comicsResultTitle?.textContent || 'Fábrica de Quadrinhos'}\n`; roteiroTxt += `Criado com PedagogIA em ${new Date().toLocaleDateString('pt-BR')}\n\n`; for (let i = 0; i < generatedPanelsData.length; i++) { const panel = generatedPanelsData[i]; const imgUrl = panel.imageUrl || panel.image_url; roteiroTxt += `[QUADRO ${panel.panel_number}]\nFala/Legenda: ${panel.dialogue || '(Sem fala)'}\nPrompt Visual: ${panel.image_prompt || ''}\n\n`; try { const resp = await fetch(imgUrl); const blob = await resp.blob(); folder.file(`quadrinho_${panel.panel_number}.jpg`, blob); } catch (fetchErr) { console.warn(`Erro ao incluir imagem ${panel.panel_number} no ZIP:`, fetchErr); } } folder.file('roteiro.txt', roteiroTxt); const content = await zip.generateAsync({ type: 'blob' }); const downloadUrl = URL.createObjectURL(content); const a = document.createElement('a'); a.href = downloadUrl; a.download = `${title}_pedagog.zip`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(downloadUrl); } catch (err) { console.error('Erro ao gerar ZIP:', err); await showCustomAlert('Erro', 'Não foi possível gerar o ZIP: ' + err.message); } finally { btnExportComicsZIP.disabled = false; btnExportComicsZIP.innerHTML = '📦 Baixar Todas as Imagens (ZIP)'; } }); } // --- MODO APRESENTAÇÃO EM TELA CHEIA (LEITURA INFANTIL / MOBILE) --- const comicsPresModal = document.getElementById('comicsPresentationModal'); const comicsPresTitle = document.getElementById('comicsPresTitle'); const comicsPresImage = document.getElementById('comicsPresImage'); const comicsPresCaption = document.getElementById('comicsPresCaption'); const comicsPresCounter = document.getElementById('comicsPresCounter'); const btnPresPrev = document.getElementById('btnPresPrev'); const btnPresNext = document.getElementById('btnPresNext'); const btnCloseComicsPresModal = document.getElementById('btnCloseComicsPresModal'); const btnComicsPresentationMode = document.getElementById('btnComicsPresentationMode'); let presCurrentIndex = 0; const openComicsPresentation = (index = 0) => { if (!generatedPanelsData || generatedPanelsData.length === 0) return; presCurrentIndex = Math.max(0, Math.min(index, generatedPanelsData.length - 1)); if (comicsPresModal) comicsPresModal.style.display = 'flex'; updateComicsPresentationView(); }; const closeComicsPresentation = () => { if (comicsPresModal) comicsPresModal.style.display = 'none'; }; const updateComicsPresentationView = () => { if (!generatedPanelsData || generatedPanelsData.length === 0) return; const panel = generatedPanelsData[presCurrentIndex]; if (!panel) return; if (comicsPresTitle) comicsPresTitle.textContent = comicsResultTitle?.textContent || 'Fábrica de Quadrinhos'; if (comicsPresCounter) comicsPresCounter.textContent = `${presCurrentIndex + 1} / ${generatedPanelsData.length}`; if (comicsPresImage) comicsPresImage.src = panel.imageUrl || panel.image_url; if (comicsPresCaption) comicsPresCaption.textContent = panel.dialogue || '(História silenciosa)'; if (btnPresPrev) btnPresPrev.disabled = presCurrentIndex === 0; if (btnPresNext) btnPresNext.disabled = presCurrentIndex === generatedPanelsData.length - 1; }; if (btnComicsPresentationMode) { btnComicsPresentationMode.addEventListener('click', () => { openComicsPresentation(0); }); } if (btnPresPrev) { btnPresPrev.addEventListener('click', () => { if (presCurrentIndex > 0) { presCurrentIndex--; updateComicsPresentationView(); } }); } if (btnPresNext) { btnPresNext.addEventListener('click', () => { if (presCurrentIndex < generatedPanelsData.length - 1) { presCurrentIndex++; updateComicsPresentationView(); } }); } if (btnCloseComicsPresModal) { btnCloseComicsPresModal.addEventListener('click', closeComicsPresentation); } // Teclado (setas) para o modo apresentação window.addEventListener('keydown', (e) => { if (comicsPresModal && comicsPresModal.style.display === 'flex') { if (e.key === 'ArrowRight' || e.key === 'Space') { if (presCurrentIndex < generatedPanelsData.length - 1) { presCurrentIndex++; updateComicsPresentationView(); } } else if (e.key === 'ArrowLeft') { if (presCurrentIndex > 0) { presCurrentIndex--; updateComicsPresentationView(); } } else if (e.key === 'Escape') { closeComicsPresentation(); } } }); // Touch Swipe para smartphones no modo apresentação let touchStartX = 0; if (comicsPresModal) { comicsPresModal.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].screenX; }, { passive: true }); comicsPresModal.addEventListener('touchend', (e) => { const touchEndX = e.changedTouches[0].screenX; const diff = touchEndX - touchStartX; if (Math.abs(diff) > 50) { if (diff < 0 && presCurrentIndex < generatedPanelsData.length - 1) { // Swipe para a esquerda -> Próximo presCurrentIndex++; updateComicsPresentationView(); } else if (diff > 0 && presCurrentIndex > 0) { // Swipe para a direita -> Anterior presCurrentIndex--; updateComicsPresentationView(); } } }, { passive: true }); } // Exportar PDF Retrato (1 por página) if (btnExportComicsPDFPortrait) { btnExportComicsPDFPortrait.addEventListener('click', async () => { if (generatedPanelsData.length === 0) return; btnExportComicsPDFPortrait.disabled = true; btnExportComicsPDFPortrait.textContent = '⏳ Montando PDF...'; try { const { jsPDF } = window.jspdf; const doc = new jsPDF('p', 'pt', 'a4'); const title = comicsResultTitle.textContent || 'História em Quadrinhos'; for (let i = 0; i < generatedPanelsData.length; i++) { const panel = generatedPanelsData[i]; if (i > 0) doc.addPage(); // Título centralizado doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.setTextColor(30, 41, 59); doc.text(title, 297, 45, { align: 'center' }); const imgDetails = await getBase64ImageDetails(panel.imageUrl); const boxWidth = 515; const boxMaxHeight = 350; const textHeight = 150; // Cálculo exato mantendo a proporção real da imagem (sem esticar) let renderW = boxWidth; let renderH = renderW / imgDetails.aspectRatio; if (renderH > boxMaxHeight) { renderH = boxMaxHeight; renderW = renderH * imgDetails.aspectRatio; } const imgX = 40 + (boxWidth - renderW) / 2; const imgY = 70 + (boxMaxHeight - renderH) / 2; const cardY = 70; const textY = cardY + boxMaxHeight; // Desenha a imagem centralizada e proporcional doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH); // Fundo da caixa de texto doc.setFillColor(248, 250, 252); doc.rect(40, textY, boxWidth, textHeight, 'F'); // Moldura unificada ao redor de todo o quadro doc.setDrawColor(30, 41, 59); doc.setLineWidth(2); doc.rect(40, cardY, boxWidth, boxMaxHeight + textHeight, 'D'); // Linha divisória entre imagem e texto doc.line(40, textY, 555, textY); // Texto legível if (panel.dialogue) { doc.setFont('helvetica', 'normal'); doc.setFontSize(15); doc.setTextColor(15, 23, 42); const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 40); doc.text(splitText, 60, textY + 35); } // Numeração do quadrinho doc.setFillColor(254, 240, 138); doc.circle(65, 95, 12, 'F'); doc.setDrawColor(30, 41, 59); doc.setLineWidth(1.5); doc.circle(65, 95, 12, 'D'); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(30, 41, 59); doc.text(String(panel.panel_number), 65, 99, { align: 'center' }); // Rodapé doc.setFont('helvetica', 'italic'); doc.setFontSize(9); doc.setTextColor(148, 163, 184); doc.text(`Criado com PedaGog - Quadro ${panel.panel_number} de ${generatedPanelsData.length}`, 297, 815, { align: 'center' }); } doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}.pdf`); } catch (error) { console.error('Erro ao gerar PDF Retrato:', error); alert('Erro ao gerar PDF: ' + error.message); } finally { btnExportComicsPDFPortrait.disabled = false; btnExportComicsPDFPortrait.textContent = '📄 PDF (1 por página - Retrato)'; } }); } // Exportar PDF Paisagem (2 por página) if (btnExportComicsPDFLandscape) { btnExportComicsPDFLandscape.addEventListener('click', async () => { if (generatedPanelsData.length === 0) return; btnExportComicsPDFLandscape.disabled = true; btnExportComicsPDFLandscape.textContent = '⏳ Montando PDF...'; try { const { jsPDF } = window.jspdf; const doc = new jsPDF('l', 'pt', 'a4'); const title = comicsResultTitle.textContent || 'História em Quadrinhos'; const totalPanels = generatedPanelsData.length; const totalPages = Math.ceil(totalPanels / 2); for (let page = 0; page < totalPages; page++) { if (page > 0) doc.addPage(); // Título centralizado no topo doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.setTextColor(30, 41, 59); doc.text(`${title} - Página ${page + 1} de ${totalPages}`, 421, 45, { align: 'center' }); // Renderizar até dois quadrinhos lado a lado for (let col = 0; col < 2; col++) { const index = page * 2 + col; if (index >= totalPanels) break; const panel = generatedPanelsData[index]; const startX = col === 0 ? 40 : 426; const boxWidth = 376; const boxMaxHeight = 230; const textHeight = 140; const imgDetails = await getBase64ImageDetails(panel.imageUrl); // Cálculo rigoroso da dimensão sem deformar (preserva aspect ratio real) let renderW = boxWidth; let renderH = renderW / imgDetails.aspectRatio; if (renderH > boxMaxHeight) { renderH = boxMaxHeight; renderW = renderH * imgDetails.aspectRatio; } const imgX = startX + (boxWidth - renderW) / 2; const imgY = 70 + (boxMaxHeight - renderH) / 2; const cardY = 70; const textY = cardY + boxMaxHeight; // Imagem do quadrinho centralizada e mantendo proporções exatas doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH); // Fundo da caixa de texto doc.setFillColor(248, 250, 252); doc.rect(startX, textY, boxWidth, textHeight, 'F'); // Moldura unificada ao redor do quadro (imagem + texto) doc.setDrawColor(30, 41, 59); doc.setLineWidth(2); doc.rect(startX, cardY, boxWidth, boxMaxHeight + textHeight, 'D'); // Linha separadora entre imagem e caixa de texto doc.line(startX, textY, startX + boxWidth, textY); // Legenda formatada if (panel.dialogue) { doc.setFont('helvetica', 'normal'); doc.setFontSize(13); doc.setTextColor(15, 23, 42); const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 30); doc.text(splitText, startX + 15, textY + 30); } // Numeral do quadro doc.setFillColor(254, 240, 138); doc.circle(startX + 20, 90, 11, 'F'); doc.setDrawColor(30, 41, 59); doc.setLineWidth(1.5); doc.circle(startX + 20, 90, 11, 'D'); doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(30, 41, 59); doc.text(String(panel.panel_number), startX + 20, 93, { align: 'center' }); } } doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}_paisagem.pdf`); } catch (error) { console.error('Erro ao gerar PDF Paisagem:', error); alert('Erro ao gerar PDF: ' + error.message); } finally { btnExportComicsPDFLandscape.disabled = false; btnExportComicsPDFLandscape.textContent = '📄 PDF (2 por página - Paisagem)'; } }); } // Compilar Vídeo de Apresentação (FFmpeg com Narração IA) if (btnCompileComicsVideo) { btnCompileComicsVideo.addEventListener('click', async () => { if (generatedPanelsData.length === 0) return; stopComicsMusicPreview(); btnCompileComicsVideo.disabled = true; comicsVideoLoader.style.display = 'flex'; comicsVideoResult.style.display = 'none'; const voice = document.getElementById('comicsVideoVoice')?.value || 'mulher'; try { const response = await fetch('/api/comics/generate-video', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ frames: generatedPanelsData, musicSelection: comicsVideoMusic ? comicsVideoMusic.value : 'alegre', frameDuration: comicsVideoDuration ? comicsVideoDuration.value : 5, narrationVoice: voice, titulo: comicsResultTitle?.textContent || 'História em Quadrinhos' }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Erro na compilação do vídeo'); } const data = await response.json(); comicsVideoPlayer.src = data.videoUrl; comicsVideoDownloadBtn.href = data.videoUrl; comicsVideoResult.style.display = 'flex'; comicsVideoLoader.style.display = 'none'; btnCompileComicsVideo.disabled = false; } catch (err) { console.error('Erro ao gerar vídeo dos quadrinhos:', err); alert('Erro ao compilar o vídeo: ' + err.message); comicsVideoLoader.style.display = 'none'; btnCompileComicsVideo.disabled = false; } }); } // --- PREVIEW INTERATIVO DE TRILHAS SONORAS (PLAY/STOP) --- let comicsPreviewAudio = null; const btnPreviewComicsMusic = document.getElementById('btnPreviewComicsMusic'); const comicsMusicPreviewIcon = document.getElementById('comicsMusicPreviewIcon'); const comicsMusicPreviewText = document.getElementById('comicsMusicPreviewText'); const comicsMusicPlayingTag = document.getElementById('comicsMusicPlayingTag'); function stopComicsMusicPreview() { if (comicsPreviewAudio) { comicsPreviewAudio.pause(); comicsPreviewAudio.currentTime = 0; comicsPreviewAudio = null; } if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '▶️'; if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Ouvir'; if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'none'; if (btnPreviewComicsMusic) { btnPreviewComicsMusic.style.background = 'rgba(219, 39, 119, 0.1)'; btnPreviewComicsMusic.style.borderColor = 'var(--brand-pink)'; btnPreviewComicsMusic.style.color = 'var(--brand-pink)'; } } if (btnPreviewComicsMusic && comicsVideoMusic) { btnPreviewComicsMusic.addEventListener('click', () => { const selectedTrack = comicsVideoMusic.value; if (selectedTrack === 'sem_musica') { showCustomAlert('Sem Trilha', 'A opção "Sem Música de Fundo" está selecionada.'); return; } if (comicsPreviewAudio && !comicsPreviewAudio.paused) { stopComicsMusicPreview(); return; } stopComicsMusicPreview(); const audioUrl = `/assets/audio/${selectedTrack}.mp3`; comicsPreviewAudio = new Audio(audioUrl); comicsPreviewAudio.volume = 0.55; comicsPreviewAudio.play().then(() => { if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '⏹️'; if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Parar'; if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'inline'; if (btnPreviewComicsMusic) { btnPreviewComicsMusic.style.background = 'rgba(16, 185, 129, 0.15)'; btnPreviewComicsMusic.style.borderColor = '#10b981'; btnPreviewComicsMusic.style.color = '#10b981'; } }).catch(err => { console.warn('Erro ao reproduzir prévia de áudio:', err.message); stopComicsMusicPreview(); showCustomAlert('Áudio', 'Não foi possível carregar a prévia desta trilha.'); }); comicsPreviewAudio.onended = () => { stopComicsMusicPreview(); }; }); comicsVideoMusic.addEventListener('change', () => { stopComicsMusicPreview(); }); } // ============================================================ // VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO) // ============================================================ const videoMindModal = document.getElementById('videoMindModal'); const btnCloseVideoMindModal = document.getElementById('btnCloseVideoMindModal'); const barBtnVideoMind = document.getElementById('barBtnVideoMind'); const btnGenerateVideoMind = document.getElementById('btnGenerateVideoMind'); const videoMindUrlInput = document.getElementById('videoMindUrlInput'); const videoMindLoader = document.getElementById('videoMindLoader'); const videoMindLoaderText = document.getElementById('videoMindLoaderText'); const videoMindResultArea = document.getElementById('videoMindResultArea'); const videoMindThumbnail = document.getElementById('videoMindThumbnail'); const videoMindTitle = document.getElementById('videoMindTitle'); const videoMindAuthor = document.getElementById('videoMindAuthor'); const videoMindReportArea = document.getElementById('videoMindReportArea'); const btnCopyVideoMindReport = document.getElementById('btnCopyVideoMindReport'); const btnDownloadVideoMindReport = document.getElementById('btnDownloadVideoMindReport'); const btnPdfVideoMindReport = document.getElementById('btnPdfVideoMindReport'); let selectedVideoMindLines = 20; let activeVideoMindReport = null; // Listener para selecionar o tamanho do resumo document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedVideoMindLines = parseInt(btn.dataset.lines) || 20; }); }); // Abrir modal do VideoMind if (barBtnVideoMind) { barBtnVideoMind.addEventListener('click', () => { videoMindModal.style.display = 'flex'; videoMindUrlInput.value = ''; videoMindResultArea.style.display = 'none'; videoMindReportArea.innerHTML = ''; if (videoMindLoader) videoMindLoader.style.display = 'none'; btnGenerateVideoMind.disabled = false; activeVideoMindReport = null; }); } // Fechar modal if (btnCloseVideoMindModal) { btnCloseVideoMindModal.addEventListener('click', () => { videoMindModal.style.display = 'none'; }); } // Ação de analisar vídeo if (btnGenerateVideoMind) { btnGenerateVideoMind.addEventListener('click', async () => { const url = videoMindUrlInput.value.trim(); const pastedTranscriptInput = document.getElementById('videoMindPastedTranscriptInput'); const pastedTranscript = pastedTranscriptInput ? pastedTranscriptInput.value.trim() : ''; if (!url) { alert('Por favor, insira o link de um vídeo do YouTube.'); return; } btnGenerateVideoMind.disabled = true; videoMindLoader.style.display = 'flex'; videoMindResultArea.style.display = 'none'; videoMindLoaderText.textContent = 'Buscando metadados do vídeo...'; let statusMsgIdx = 0; const statusMessages = [ 'Buscando legenda automática do vídeo...', 'Carregando a transcrição completa...', 'Enviando transcrição para a IA...', 'Pedagoga IA analisando o conteúdo...', 'Alinhando temas do vídeo com a BNCC...', 'Gerando resumo de ' + selectedVideoMindLines + ' linhas...', 'Criando propostas de atividades pedagógicas...', 'Finalizando o parecer pedagógico...' ]; const statusInterval = setInterval(() => { if (statusMsgIdx < statusMessages.length) { videoMindLoaderText.textContent = statusMessages[statusMsgIdx]; statusMsgIdx++; } else { videoMindLoaderText.textContent = 'Ajustando formatação da análise...'; } }, 4000); try { const response = await fetch('/api/videomind/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, lines: selectedVideoMindLines, pastedTranscript }) }); clearInterval(statusInterval); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || 'Erro desconhecido'); } const data = await response.json(); activeVideoMindReport = data; // Preencher informações do vídeo videoMindTitle.textContent = data.metadata.title; videoMindAuthor.textContent = 'Canal: ' + data.metadata.author; videoMindThumbnail.src = data.metadata.thumbnail; // Renderizar Markdown if (window.marked) { videoMindReportArea.innerHTML = marked.parse(data.report); } else { videoMindReportArea.textContent = data.report; } videoMindLoader.style.display = 'none'; videoMindResultArea.style.display = 'flex'; } catch (err) { clearInterval(statusInterval); console.error('Erro no VideoMind:', err); alert('Erro ao analisar vídeo: ' + err.message); videoMindLoader.style.display = 'none'; btnGenerateVideoMind.disabled = false; } }); } // Copiar Parecer if (btnCopyVideoMindReport) { btnCopyVideoMindReport.addEventListener('click', () => { if (activeVideoMindReport && activeVideoMindReport.report) { navigator.clipboard.writeText(activeVideoMindReport.report) .then(() => { btnCopyVideoMindReport.textContent = '✅ Copiado!'; setTimeout(() => { btnCopyVideoMindReport.textContent = '📋 Copiar Parecer'; }, 2000); }) .catch(err => { console.error('Falha ao copiar:', err); }); } }); } // Baixar relatório TXT if (btnDownloadVideoMindReport) { btnDownloadVideoMindReport.addEventListener('click', () => { if (activeVideoMindReport && activeVideoMindReport.report) { const title = activeVideoMindReport.metadata.title || 'analise_video'; const blob = new Blob([activeVideoMindReport.report], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `VideoMind_${title.replace(/\s+/g, '_').toLowerCase()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } }); } // Baixar relatório PDF if (btnPdfVideoMindReport) { btnPdfVideoMindReport.addEventListener('click', () => { if (!activeVideoMindReport) return; const printWindow = window.open('', '_blank'); printWindow.document.write(` VideoMind - Parecer Pedagógico

🎬 VideoMind - Análise & Parecer Pedagógico

Vídeo: ${activeVideoMindReport.metadata.title}
Canal: ${activeVideoMindReport.metadata.author}
Resumo Escolhido: ${selectedVideoMindLines} Linhas
${window.marked ? marked.parse(activeVideoMindReport.report) : activeVideoMindReport.report}
`); }); } // ============================================================ // GARATUJAS (ANALISADOR DE TRAÇADO INFANTIL) // ============================================================ const garatujasModal = document.getElementById('garatujasModal'); const btnCloseGaratujasModal = document.getElementById('btnCloseGaratujasModal'); const barBtnGaratujas = document.getElementById('barBtnGaratujas'); const tabGaratujasNova = document.getElementById('tabGaratujasNova'); const tabGaratujasHistorico = document.getElementById('tabGaratujasHistorico'); const contentGaratujasNova = document.getElementById('contentGaratujasNova'); const contentGaratujasHistorico = document.getElementById('contentGaratujasHistorico'); const garatujasAlunoInput = document.getElementById('garatujasAlunoInput'); const garatujasIdadeInput = document.getElementById('garatujasIdadeInput'); const garatujaFileInput = document.getElementById('garatujaFileInput'); const btnUploadGaratuja = document.getElementById('btnUploadGaratuja'); const garatujaFileStatus = document.getElementById('garatujaFileStatus'); const garatujaPreviewContainer = document.getElementById('garatujaPreviewContainer'); const garatujaImgPreview = document.getElementById('garatujaImgPreview'); const btnGenerateGaratuja = document.getElementById('btnGenerateGaratuja'); const garatujaLoader = document.getElementById('garatujaLoader'); const garatujaLoaderText = document.getElementById('garatujaLoaderText'); const garatujaResultArea = document.getElementById('garatujaResultArea'); const garatujaReportArea = document.getElementById('garatujaReportArea'); const btnCopyGaratujaReport = document.getElementById('btnCopyGaratujaReport'); const btnPdfGaratujaReport = document.getElementById('btnPdfGaratujaReport'); const garatujasListContainer = document.getElementById('garatujasListContainer'); const garatujasNoData = document.getElementById('garatujasNoData'); let activeGaratujaReport = null; // Abas do Garatujas if (tabGaratujasNova) { tabGaratujasNova.addEventListener('click', () => { tabGaratujasNova.classList.add('active'); tabGaratujasHistorico.classList.remove('active'); contentGaratujasNova.style.display = 'block'; contentGaratujasHistorico.style.display = 'none'; }); } if (tabGaratujasHistorico) { tabGaratujasHistorico.addEventListener('click', () => { tabGaratujasHistorico.classList.add('active'); tabGaratujasNova.classList.remove('active'); contentGaratujasNova.style.display = 'none'; contentGaratujasHistorico.style.display = 'block'; loadGaratujasHistory(); }); } // Abrir Modal if (barBtnGaratujas) { barBtnGaratujas.addEventListener('click', () => { garatujasModal.style.display = 'flex'; garatujasAlunoInput.value = ''; garatujaFileInput.value = ''; garatujaFileStatus.textContent = 'Nenhum arquivo selecionado'; garatujaPreviewContainer.style.display = 'none'; garatujaResultArea.style.display = 'none'; garatujaLoader.style.display = 'none'; btnGenerateGaratuja.disabled = false; activeGaratujaReport = null; if (tabGaratujasNova) tabGaratujasNova.click(); }); } // Fechar Modal if (btnCloseGaratujasModal) { btnCloseGaratujasModal.addEventListener('click', () => { garatujasModal.style.display = 'none'; }); } // Upload trigger if (btnUploadGaratuja) { btnUploadGaratuja.addEventListener('click', () => { garatujaFileInput.click(); }); } if (garatujaFileInput) { garatujaFileInput.addEventListener('change', () => { const file = garatujaFileInput.files[0]; if (file) { garatujaFileStatus.textContent = file.name; const reader = new FileReader(); reader.onload = (e) => { garatujaImgPreview.src = e.target.result; garatujaPreviewContainer.style.display = 'block'; }; reader.readAsDataURL(file); } else { garatujaFileStatus.textContent = 'Nenhum arquivo selecionado'; garatujaPreviewContainer.style.display = 'none'; } }); } // Gerar Análise if (btnGenerateGaratuja) { btnGenerateGaratuja.addEventListener('click', async () => { const alunoName = garatujasAlunoInput.value.trim(); const alunoIdade = garatujasIdadeInput ? garatujasIdadeInput.value.trim() : ''; const file = garatujaFileInput.files[0]; if (!alunoName) { alert('Por favor, informe o nome do aluno.'); return; } if (!file) { alert('Por favor, selecione a foto do desenho.'); return; } btnGenerateGaratuja.disabled = true; garatujaLoader.style.display = 'flex'; garatujaResultArea.style.display = 'none'; garatujaLoaderText.textContent = 'Enviando imagem pedagógica...'; const formData = new FormData(); formData.append('alunoName', alunoName); if (alunoIdade) formData.append('alunoIdade', alunoIdade); formData.append('image', file); let statusMsgIdx = 0; const statusMessages = [ 'Carregando desenho infantil...', 'IA escaneando os traços e formas...', 'Analisando simetria e coordenação motora...', 'Definindo estágio evolutivo do desenho...', 'Criando sugestões de atividades formativas...', 'Finalizando laudo psicopedagógico...' ]; const statusInterval = setInterval(() => { if (statusMsgIdx < statusMessages.length) { garatujaLoaderText.textContent = statusMessages[statusMsgIdx]; statusMsgIdx++; } else { garatujaLoaderText.textContent = 'Formatando parecer...'; } }, 4500); try { const response = await fetch('/api/garatujas/analyze', { method: 'POST', body: formData }); clearInterval(statusInterval); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error || 'Erro desconhecido'); } const data = await response.json(); activeGaratujaReport = data; if (window.marked) { garatujaReportArea.innerHTML = marked.parse(data.analise); } else { garatujaReportArea.textContent = data.analise; } garatujaLoader.style.display = 'none'; garatujaResultArea.style.display = 'flex'; btnGenerateGaratuja.disabled = false; } catch (err) { clearInterval(statusInterval); console.error(err); alert('Erro na análise: ' + err.message); garatujaLoader.style.display = 'none'; btnGenerateGaratuja.disabled = false; } }); } // Copiar Parecer Garatujas if (btnCopyGaratujaReport) { btnCopyGaratujaReport.addEventListener('click', () => { if (activeGaratujaReport && activeGaratujaReport.analise) { navigator.clipboard.writeText(activeGaratujaReport.analise) .then(() => { btnCopyGaratujaReport.textContent = '✅ Copiado!'; setTimeout(() => { btnCopyGaratujaReport.textContent = '📋 Copiar'; }, 2000); }); } }); } // Baixar PDF Garatujas if (btnPdfGaratujaReport) { btnPdfGaratujaReport.addEventListener('click', () => { if (!activeGaratujaReport) return; const printWindow = window.open('', '_blank'); printWindow.document.write(` Garatujas - Laudo do Desenho

🎨 Garatujas - Parecer de Traçado Infantil

Aluno(a): ${activeGaratujaReport.alunoName}
Data de Análise: ${new Date(activeGaratujaReport.createdAt).toLocaleDateString('pt-BR')}
${window.marked ? marked.parse(activeGaratujaReport.analise) : activeGaratujaReport.analise}